CommentStore.php 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700
  1. <?php
  2. /**
  3. * Manage storage of comments in the database
  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. * @file
  21. */
  22. use MediaWiki\MediaWikiServices;
  23. use Wikimedia\Rdbms\IDatabase;
  24. /**
  25. * CommentStore handles storage of comments (edit summaries, log reasons, etc)
  26. * in the database.
  27. * @since 1.30
  28. */
  29. class CommentStore {
  30. /**
  31. * Maximum length of a comment in UTF-8 characters. Longer comments will be truncated.
  32. * @note This must be at least 255 and not greater than floor( MAX_COMMENT_LENGTH / 4 ).
  33. */
  34. const COMMENT_CHARACTER_LIMIT = 500;
  35. /**
  36. * Maximum length of a comment in bytes. Longer comments will be truncated.
  37. * @note This value is determined by the size of the underlying database field,
  38. * currently BLOB in MySQL/MariaDB.
  39. */
  40. const MAX_COMMENT_LENGTH = 65535;
  41. /**
  42. * Maximum length of serialized data in bytes. Longer data will result in an exception.
  43. * @note This value is determined by the size of the underlying database field,
  44. * currently BLOB in MySQL/MariaDB.
  45. */
  46. const MAX_DATA_LENGTH = 65535;
  47. /**
  48. * Define fields that use temporary tables for transitional purposes
  49. * @var array Keys are '$key', values are arrays with four fields:
  50. * - table: Temporary table name
  51. * - pk: Temporary table column referring to the main table's primary key
  52. * - field: Temporary table column referring comment.comment_id
  53. * - joinPK: Main table's primary key
  54. */
  55. protected static $tempTables = [
  56. 'rev_comment' => [
  57. 'table' => 'revision_comment_temp',
  58. 'pk' => 'revcomment_rev',
  59. 'field' => 'revcomment_comment_id',
  60. 'joinPK' => 'rev_id',
  61. ],
  62. 'img_description' => [
  63. 'table' => 'image_comment_temp',
  64. 'pk' => 'imgcomment_name',
  65. 'field' => 'imgcomment_description_id',
  66. 'joinPK' => 'img_name',
  67. ],
  68. ];
  69. /**
  70. * Fields that formerly used $tempTables
  71. * @var array Key is '$key', value is the MediaWiki version in which it was
  72. * removed from $tempTables.
  73. */
  74. protected static $formerTempTables = [];
  75. /**
  76. * @since 1.30
  77. * @deprecated in 1.31
  78. * @var string|null
  79. */
  80. protected $key = null;
  81. /** @var int One of the MIGRATION_* constants */
  82. protected $stage;
  83. /** @var array[] Cache for `self::getJoin()` */
  84. protected $joinCache = [];
  85. /** @var Language Language to use for comment truncation */
  86. protected $lang;
  87. /**
  88. * @param Language $lang Language to use for comment truncation. Defaults
  89. * to $wgContLang.
  90. * @param int $migrationStage One of the MIGRATION_* constants
  91. */
  92. public function __construct( Language $lang, $migrationStage ) {
  93. $this->stage = $migrationStage;
  94. $this->lang = $lang;
  95. }
  96. /**
  97. * Static constructor for easier chaining
  98. * @deprecated in 1.31 Should not be constructed with a $key, use CommentStore::getStore
  99. * @param string $key A key such as "rev_comment" identifying the comment
  100. * field being fetched.
  101. * @return CommentStore
  102. */
  103. public static function newKey( $key ) {
  104. global $wgCommentTableSchemaMigrationStage, $wgContLang;
  105. // TODO uncomment once not used in extensions
  106. // wfDeprecated( __METHOD__, '1.31' );
  107. $store = new CommentStore( $wgContLang, $wgCommentTableSchemaMigrationStage );
  108. $store->key = $key;
  109. return $store;
  110. }
  111. /**
  112. * @since 1.31
  113. * @deprecated in 1.31 Use DI to inject a CommentStore instance into your class.
  114. * @return CommentStore
  115. */
  116. public static function getStore() {
  117. return MediaWikiServices::getInstance()->getCommentStore();
  118. }
  119. /**
  120. * Compat method allowing use of self::newKey until removed.
  121. * @param string|null $methodKey
  122. * @throw InvalidArgumentException
  123. * @return string
  124. */
  125. private function getKey( $methodKey = null ) {
  126. $key = $this->key !== null ? $this->key : $methodKey;
  127. if ( $key === null ) {
  128. // @codeCoverageIgnoreStart
  129. throw new InvalidArgumentException( '$key should not be null' );
  130. // @codeCoverageIgnoreEnd
  131. }
  132. return $key;
  133. }
  134. /**
  135. * Get SELECT fields for the comment key
  136. *
  137. * Each resulting row should be passed to `self::getCommentLegacy()` to get the
  138. * actual comment.
  139. *
  140. * @note Use of this method may require a subsequent database query to
  141. * actually fetch the comment. If possible, use `self::getJoin()` instead.
  142. *
  143. * @since 1.30
  144. * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
  145. * @param string $key A key such as "rev_comment" identifying the comment
  146. * field being fetched.
  147. * @return string[] to include in the `$vars` to `IDatabase->select()`. All
  148. * fields are aliased, so `+` is safe to use.
  149. */
  150. public function getFields( $key = null ) {
  151. $key = $this->getKey( $key );
  152. $fields = [];
  153. if ( $this->stage === MIGRATION_OLD ) {
  154. $fields["{$key}_text"] = $key;
  155. $fields["{$key}_data"] = 'NULL';
  156. $fields["{$key}_cid"] = 'NULL';
  157. } else {
  158. if ( $this->stage < MIGRATION_NEW ) {
  159. $fields["{$key}_old"] = $key;
  160. }
  161. if ( isset( self::$tempTables[$key] ) ) {
  162. $fields["{$key}_pk"] = self::$tempTables[$key]['joinPK'];
  163. } else {
  164. $fields["{$key}_id"] = "{$key}_id";
  165. }
  166. }
  167. return $fields;
  168. }
  169. /**
  170. * Get SELECT fields and joins for the comment key
  171. *
  172. * Each resulting row should be passed to `self::getComment()` to get the
  173. * actual comment.
  174. *
  175. * @since 1.30
  176. * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
  177. * @param string $key A key such as "rev_comment" identifying the comment
  178. * field being fetched.
  179. * @return array With three keys:
  180. * - tables: (string[]) to include in the `$table` to `IDatabase->select()`
  181. * - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
  182. * - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
  183. * All tables, fields, and joins are aliased, so `+` is safe to use.
  184. */
  185. public function getJoin( $key = null ) {
  186. $key = $this->getKey( $key );
  187. if ( !array_key_exists( $key, $this->joinCache ) ) {
  188. $tables = [];
  189. $fields = [];
  190. $joins = [];
  191. if ( $this->stage === MIGRATION_OLD ) {
  192. $fields["{$key}_text"] = $key;
  193. $fields["{$key}_data"] = 'NULL';
  194. $fields["{$key}_cid"] = 'NULL';
  195. } else {
  196. $join = $this->stage === MIGRATION_NEW ? 'JOIN' : 'LEFT JOIN';
  197. if ( isset( self::$tempTables[$key] ) ) {
  198. $t = self::$tempTables[$key];
  199. $alias = "temp_$key";
  200. $tables[$alias] = $t['table'];
  201. $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
  202. $joinField = "{$alias}.{$t['field']}";
  203. } else {
  204. $joinField = "{$key}_id";
  205. }
  206. $alias = "comment_$key";
  207. $tables[$alias] = 'comment';
  208. $joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ];
  209. if ( $this->stage === MIGRATION_NEW ) {
  210. $fields["{$key}_text"] = "{$alias}.comment_text";
  211. } else {
  212. $fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )";
  213. }
  214. $fields["{$key}_data"] = "{$alias}.comment_data";
  215. $fields["{$key}_cid"] = "{$alias}.comment_id";
  216. }
  217. $this->joinCache[$key] = [
  218. 'tables' => $tables,
  219. 'fields' => $fields,
  220. 'joins' => $joins,
  221. ];
  222. }
  223. return $this->joinCache[$key];
  224. }
  225. /**
  226. * Extract the comment from a row
  227. *
  228. * Shared implementation for getComment() and getCommentLegacy()
  229. *
  230. * @param IDatabase|null $db Database handle for getCommentLegacy(), or null for getComment()
  231. * @param string $key A key such as "rev_comment" identifying the comment
  232. * field being fetched.
  233. * @param object|array $row
  234. * @param bool $fallback
  235. * @return CommentStoreComment
  236. */
  237. private function getCommentInternal( IDatabase $db = null, $key, $row, $fallback = false ) {
  238. $row = (array)$row;
  239. if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
  240. $cid = isset( $row["{$key}_cid"] ) ? $row["{$key}_cid"] : null;
  241. $text = $row["{$key}_text"];
  242. $data = $row["{$key}_data"];
  243. } elseif ( $this->stage === MIGRATION_OLD ) {
  244. $cid = null;
  245. if ( $fallback && isset( $row[$key] ) ) {
  246. wfLogWarning( "Using deprecated fallback handling for comment $key" );
  247. $text = $row[$key];
  248. } else {
  249. wfLogWarning( "Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD" );
  250. $text = '';
  251. }
  252. $data = null;
  253. } else {
  254. if ( isset( self::$tempTables[$key] ) ) {
  255. if ( array_key_exists( "{$key}_pk", $row ) ) {
  256. if ( !$db ) {
  257. throw new InvalidArgumentException(
  258. "\$row does not contain fields needed for comment $key and getComment(), but "
  259. . "does have fields for getCommentLegacy()"
  260. );
  261. }
  262. $t = self::$tempTables[$key];
  263. $id = $row["{$key}_pk"];
  264. $row2 = $db->selectRow(
  265. [ $t['table'], 'comment' ],
  266. [ 'comment_id', 'comment_text', 'comment_data' ],
  267. [ $t['pk'] => $id ],
  268. __METHOD__,
  269. [],
  270. [ 'comment' => [ 'JOIN', [ "comment_id = {$t['field']}" ] ] ]
  271. );
  272. } elseif ( $fallback && isset( $row[$key] ) ) {
  273. wfLogWarning( "Using deprecated fallback handling for comment $key" );
  274. $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
  275. } else {
  276. throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
  277. }
  278. } else {
  279. if ( array_key_exists( "{$key}_id", $row ) ) {
  280. if ( !$db ) {
  281. throw new InvalidArgumentException(
  282. "\$row does not contain fields needed for comment $key and getComment(), but "
  283. . "does have fields for getCommentLegacy()"
  284. );
  285. }
  286. $id = $row["{$key}_id"];
  287. $row2 = $db->selectRow(
  288. 'comment',
  289. [ 'comment_id', 'comment_text', 'comment_data' ],
  290. [ 'comment_id' => $id ],
  291. __METHOD__
  292. );
  293. } elseif ( $fallback && isset( $row[$key] ) ) {
  294. wfLogWarning( "Using deprecated fallback handling for comment $key" );
  295. $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
  296. } else {
  297. throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
  298. }
  299. }
  300. if ( $row2 ) {
  301. $cid = $row2->comment_id;
  302. $text = $row2->comment_text;
  303. $data = $row2->comment_data;
  304. } elseif ( $this->stage < MIGRATION_NEW && array_key_exists( "{$key}_old", $row ) ) {
  305. $cid = null;
  306. $text = $row["{$key}_old"];
  307. $data = null;
  308. } else {
  309. // @codeCoverageIgnoreStart
  310. wfLogWarning( "Missing comment row for $key, id=$id" );
  311. $cid = null;
  312. $text = '';
  313. $data = null;
  314. // @codeCoverageIgnoreEnd
  315. }
  316. }
  317. $msg = null;
  318. if ( $data !== null ) {
  319. $data = FormatJson::decode( $data );
  320. if ( !is_object( $data ) ) {
  321. // @codeCoverageIgnoreStart
  322. wfLogWarning( "Invalid JSON object in comment: $data" );
  323. $data = null;
  324. // @codeCoverageIgnoreEnd
  325. } else {
  326. $data = (array)$data;
  327. if ( isset( $data['_message'] ) ) {
  328. $msg = self::decodeMessage( $data['_message'] )
  329. ->setInterfaceMessageFlag( true );
  330. }
  331. if ( !empty( $data['_null'] ) ) {
  332. $data = null;
  333. } else {
  334. foreach ( $data as $k => $v ) {
  335. if ( substr( $k, 0, 1 ) === '_' ) {
  336. unset( $data[$k] );
  337. }
  338. }
  339. }
  340. }
  341. }
  342. return new CommentStoreComment( $cid, $text, $msg, $data );
  343. }
  344. /**
  345. * Extract the comment from a row
  346. *
  347. * Use `self::getJoin()` to ensure the row contains the needed data.
  348. *
  349. * If you need to fake a comment in a row for some reason, set fields
  350. * `{$key}_text` (string) and `{$key}_data` (JSON string or null).
  351. *
  352. * @since 1.30
  353. * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
  354. * @param string $key A key such as "rev_comment" identifying the comment
  355. * field being fetched.
  356. * @param object|array $row Result row.
  357. * @param bool $fallback If true, fall back as well as possible instead of throwing an exception.
  358. * @return CommentStoreComment
  359. */
  360. public function getComment( $key, $row = null, $fallback = false ) {
  361. // Compat for method sig change in 1.31 (introduction of $key)
  362. if ( $this->key !== null ) {
  363. $fallback = $row;
  364. $row = $key;
  365. $key = $this->getKey();
  366. }
  367. if ( $row === null ) {
  368. // @codeCoverageIgnoreStart
  369. throw new InvalidArgumentException( '$row must not be null' );
  370. // @codeCoverageIgnoreEnd
  371. }
  372. return $this->getCommentInternal( null, $key, $row, $fallback );
  373. }
  374. /**
  375. * Extract the comment from a row, with legacy lookups.
  376. *
  377. * If `$row` might have been generated using `self::getFields()` rather
  378. * than `self::getJoin()`, use this. Prefer `self::getComment()` if you
  379. * know callers used `self::getJoin()` for the row fetch.
  380. *
  381. * If you need to fake a comment in a row for some reason, set fields
  382. * `{$key}_text` (string) and `{$key}_data` (JSON string or null).
  383. *
  384. * @since 1.30
  385. * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
  386. * @param IDatabase $db Database handle to use for lookup
  387. * @param string $key A key such as "rev_comment" identifying the comment
  388. * field being fetched.
  389. * @param object|array $row Result row.
  390. * @param bool $fallback If true, fall back as well as possible instead of throwing an exception.
  391. * @return CommentStoreComment
  392. */
  393. public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) {
  394. // Compat for method sig change in 1.31 (introduction of $key)
  395. if ( $this->key !== null ) {
  396. $fallback = $row;
  397. $row = $key;
  398. $key = $this->getKey();
  399. }
  400. if ( $row === null ) {
  401. // @codeCoverageIgnoreStart
  402. throw new InvalidArgumentException( '$row must not be null' );
  403. // @codeCoverageIgnoreEnd
  404. }
  405. return $this->getCommentInternal( $db, $key, $row, $fallback );
  406. }
  407. /**
  408. * Create a new CommentStoreComment, inserting it into the database if necessary
  409. *
  410. * If a comment is going to be passed to `self::insert()` or the like
  411. * multiple times, it will be more efficient to pass a CommentStoreComment
  412. * once rather than making `self::insert()` do it every time through.
  413. *
  414. * @note When passing a CommentStoreComment, this may set `$comment->id` if
  415. * it's not already set. If `$comment->id` is already set, it will not be
  416. * verified that the specified comment actually exists or that it
  417. * corresponds to the comment text, message, and/or data in the
  418. * CommentStoreComment.
  419. * @param IDatabase $dbw Database handle to insert on. Unused if `$comment`
  420. * is a CommentStoreComment and `$comment->id` is set.
  421. * @param string|Message|CommentStoreComment $comment Comment text or Message object, or
  422. * a CommentStoreComment.
  423. * @param array|null $data Structured data to store. Keys beginning with '_' are reserved.
  424. * Ignored if $comment is a CommentStoreComment.
  425. * @return CommentStoreComment
  426. */
  427. public function createComment( IDatabase $dbw, $comment, array $data = null ) {
  428. $comment = CommentStoreComment::newUnsavedComment( $comment, $data );
  429. # Truncate comment in a Unicode-sensitive manner
  430. $comment->text = $this->lang->truncate( $comment->text, self::MAX_COMMENT_LENGTH );
  431. if ( mb_strlen( $comment->text, 'UTF-8' ) > self::COMMENT_CHARACTER_LIMIT ) {
  432. $ellipsis = wfMessage( 'ellipsis' )->inLanguage( $this->lang )->escaped();
  433. if ( mb_strlen( $ellipsis ) >= self::COMMENT_CHARACTER_LIMIT ) {
  434. // WTF?
  435. $ellipsis = '...';
  436. }
  437. $maxLength = self::COMMENT_CHARACTER_LIMIT - mb_strlen( $ellipsis, 'UTF-8' );
  438. $comment->text = mb_substr( $comment->text, 0, $maxLength, 'UTF-8' ) . $ellipsis;
  439. }
  440. if ( $this->stage > MIGRATION_OLD && !$comment->id ) {
  441. $dbData = $comment->data;
  442. if ( !$comment->message instanceof RawMessage ) {
  443. if ( $dbData === null ) {
  444. $dbData = [ '_null' => true ];
  445. }
  446. $dbData['_message'] = self::encodeMessage( $comment->message );
  447. }
  448. if ( $dbData !== null ) {
  449. $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
  450. $len = strlen( $dbData );
  451. if ( $len > self::MAX_DATA_LENGTH ) {
  452. $max = self::MAX_DATA_LENGTH;
  453. throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" );
  454. }
  455. }
  456. $hash = self::hash( $comment->text, $dbData );
  457. $comment->id = $dbw->selectField(
  458. 'comment',
  459. 'comment_id',
  460. [
  461. 'comment_hash' => $hash,
  462. 'comment_text' => $comment->text,
  463. 'comment_data' => $dbData,
  464. ],
  465. __METHOD__
  466. );
  467. if ( !$comment->id ) {
  468. $dbw->insert(
  469. 'comment',
  470. [
  471. 'comment_hash' => $hash,
  472. 'comment_text' => $comment->text,
  473. 'comment_data' => $dbData,
  474. ],
  475. __METHOD__
  476. );
  477. $comment->id = $dbw->insertId();
  478. }
  479. }
  480. return $comment;
  481. }
  482. /**
  483. * Implementation for `self::insert()` and `self::insertWithTempTable()`
  484. * @param IDatabase $dbw
  485. * @param string $key A key such as "rev_comment" identifying the comment
  486. * field being fetched.
  487. * @param string|Message|CommentStoreComment $comment
  488. * @param array|null $data
  489. * @return array [ array $fields, callable $callback ]
  490. */
  491. private function insertInternal( IDatabase $dbw, $key, $comment, $data ) {
  492. $fields = [];
  493. $callback = null;
  494. $comment = $this->createComment( $dbw, $comment, $data );
  495. if ( $this->stage <= MIGRATION_WRITE_BOTH ) {
  496. $fields[$key] = $this->lang->truncate( $comment->text, 255 );
  497. }
  498. if ( $this->stage >= MIGRATION_WRITE_BOTH ) {
  499. if ( isset( self::$tempTables[$key] ) ) {
  500. $t = self::$tempTables[$key];
  501. $func = __METHOD__;
  502. $commentId = $comment->id;
  503. $callback = function ( $id ) use ( $dbw, $commentId, $t, $func ) {
  504. $dbw->insert(
  505. $t['table'],
  506. [
  507. $t['pk'] => $id,
  508. $t['field'] => $commentId,
  509. ],
  510. $func
  511. );
  512. };
  513. } else {
  514. $fields["{$key}_id"] = $comment->id;
  515. }
  516. }
  517. return [ $fields, $callback ];
  518. }
  519. /**
  520. * Insert a comment in preparation for a row that references it
  521. *
  522. * @note It's recommended to include both the call to this method and the
  523. * row insert in the same transaction.
  524. *
  525. * @since 1.30
  526. * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
  527. * @param IDatabase $dbw Database handle to insert on
  528. * @param string $key A key such as "rev_comment" identifying the comment
  529. * field being fetched.
  530. * @param string|Message|CommentStoreComment $comment As for `self::createComment()`
  531. * @param array|null $data As for `self::createComment()`
  532. * @return array Fields for the insert or update
  533. */
  534. public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
  535. // Compat for method sig change in 1.31 (introduction of $key)
  536. if ( $this->key !== null ) {
  537. $data = $comment;
  538. $comment = $key;
  539. $key = $this->key;
  540. }
  541. if ( $comment === null ) {
  542. // @codeCoverageIgnoreStart
  543. throw new InvalidArgumentException( '$comment can not be null' );
  544. // @codeCoverageIgnoreEnd
  545. }
  546. if ( isset( self::$tempTables[$key] ) ) {
  547. throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" );
  548. }
  549. list( $fields ) = $this->insertInternal( $dbw, $key, $comment, $data );
  550. return $fields;
  551. }
  552. /**
  553. * Insert a comment in a temporary table in preparation for a row that references it
  554. *
  555. * This is currently needed for "rev_comment" and "img_description". In the
  556. * future that requirement will be removed.
  557. *
  558. * @note It's recommended to include both the call to this method and the
  559. * row insert in the same transaction.
  560. *
  561. * @since 1.30
  562. * @since 1.31 Method signature changed, $key parameter added (with deprecated back compat)
  563. * @param IDatabase $dbw Database handle to insert on
  564. * @param string $key A key such as "rev_comment" identifying the comment
  565. * field being fetched.
  566. * @param string|Message|CommentStoreComment $comment As for `self::createComment()`
  567. * @param array|null $data As for `self::createComment()`
  568. * @return array Two values:
  569. * - array Fields for the insert or update
  570. * - callable Function to call when the primary key of the row being
  571. * inserted/updated is known. Pass it that primary key.
  572. */
  573. public function insertWithTempTable( IDatabase $dbw, $key, $comment = null, $data = null ) {
  574. // Compat for method sig change in 1.31 (introduction of $key)
  575. if ( $this->key !== null ) {
  576. $data = $comment;
  577. $comment = $key;
  578. $key = $this->getKey();
  579. }
  580. if ( $comment === null ) {
  581. // @codeCoverageIgnoreStart
  582. throw new InvalidArgumentException( '$comment can not be null' );
  583. // @codeCoverageIgnoreEnd
  584. }
  585. if ( isset( self::$formerTempTables[$key] ) ) {
  586. wfDeprecated( __METHOD__ . " for $key", self::$formerTempTables[$key] );
  587. } elseif ( !isset( self::$tempTables[$key] ) ) {
  588. throw new InvalidArgumentException( "Must use insert() for $key" );
  589. }
  590. list( $fields, $callback ) = $this->insertInternal( $dbw, $key, $comment, $data );
  591. if ( !$callback ) {
  592. $callback = function () {
  593. // Do nothing.
  594. };
  595. }
  596. return [ $fields, $callback ];
  597. }
  598. /**
  599. * Encode a Message as a PHP data structure
  600. * @param Message $msg
  601. * @return array
  602. */
  603. protected static function encodeMessage( Message $msg ) {
  604. $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
  605. $params = $msg->getParams();
  606. foreach ( $params as &$param ) {
  607. if ( $param instanceof Message ) {
  608. $param = [
  609. 'message' => self::encodeMessage( $param )
  610. ];
  611. }
  612. }
  613. array_unshift( $params, $key );
  614. return $params;
  615. }
  616. /**
  617. * Decode a message that was encoded by self::encodeMessage()
  618. * @param array $data
  619. * @return Message
  620. */
  621. protected static function decodeMessage( $data ) {
  622. $key = array_shift( $data );
  623. foreach ( $data as &$param ) {
  624. if ( is_object( $param ) ) {
  625. $param = (array)$param;
  626. }
  627. if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
  628. $param = self::decodeMessage( $param['message'] );
  629. }
  630. }
  631. return new Message( $key, $data );
  632. }
  633. /**
  634. * Hashing function for comment storage
  635. * @param string $text Comment text
  636. * @param string|null $data Comment data
  637. * @return int 32-bit signed integer
  638. */
  639. public static function hash( $text, $data ) {
  640. $hash = crc32( $text ) ^ crc32( (string)$data );
  641. // 64-bit PHP returns an unsigned CRC, change it to signed for
  642. // insertion into the database.
  643. if ( $hash >= 0x80000000 ) {
  644. $hash |= -1 << 32;
  645. }
  646. return $hash;
  647. }
  648. }