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