CommentStore.php 24 KB

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