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