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