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