MediaWiki REL1_34
CommentStore.php
Go to the documentation of this file.
1<?php
25
32
38
44 const MAX_COMMENT_LENGTH = 65535;
45
51 const MAX_DATA_LENGTH = 65535;
52
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
83 protected $key = null;
84
91 protected $stage;
92
94 protected $joinCache = [];
95
97 protected $lang;
98
106 public function __construct( Language $lang, $stage ) {
107 if ( ( $stage & SCHEMA_COMPAT_WRITE_BOTH ) === 0 ) {
108 throw new InvalidArgumentException( '$stage must include a write mode' );
109 }
110 if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === 0 ) {
111 throw new InvalidArgumentException( '$stage must include a read mode' );
112 }
113
114 $this->stage = $stage;
115 $this->lang = $lang;
116 }
117
125 public static function newKey( $key ) {
126 wfDeprecated( __METHOD__, '1.31' );
127 $store = new CommentStore(
128 MediaWikiServices::getInstance()->getContentLanguage(), MIGRATION_NEW
129 );
130 $store->key = $key;
131 return $store;
132 }
133
139 public static function getStore() {
140 return MediaWikiServices::getInstance()->getCommentStore();
141 }
142
149 private function getKey( $methodKey = null ) {
150 $key = $this->key ?? $methodKey;
151 if ( $key === null ) {
152 // @codeCoverageIgnoreStart
153 throw new InvalidArgumentException( '$key should not be null' );
154 // @codeCoverageIgnoreEnd
155 }
156 return $key;
157 }
158
175 public function getFields( $key = null ) {
176 $key = $this->getKey( $key );
177 $fields = [];
178 if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
179 $fields["{$key}_text"] = $key;
180 $fields["{$key}_data"] = 'NULL';
181 $fields["{$key}_cid"] = 'NULL';
182 } else { // READ_BOTH or READ_NEW
183 if ( $this->stage & SCHEMA_COMPAT_READ_OLD ) {
184 $fields["{$key}_old"] = $key;
185 }
186
187 $tempTableStage = isset( $this->tempTables[$key] )
188 ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
189 if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
190 $fields["{$key}_pk"] = $this->tempTables[$key]['joinPK'];
191 }
192 if ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) {
193 $fields["{$key}_id"] = "{$key}_id";
194 }
195 }
196 return $fields;
197 }
198
216 public function getJoin( $key = null ) {
217 $key = $this->getKey( $key );
218 if ( !array_key_exists( $key, $this->joinCache ) ) {
219 $tables = [];
220 $fields = [];
221 $joins = [];
222
223 if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
224 $fields["{$key}_text"] = $key;
225 $fields["{$key}_data"] = 'NULL';
226 $fields["{$key}_cid"] = 'NULL';
227 } else { // READ_BOTH or READ_NEW
228 $join = ( $this->stage & SCHEMA_COMPAT_READ_OLD ) ? 'LEFT JOIN' : 'JOIN';
229
230 $tempTableStage = isset( $this->tempTables[$key] )
231 ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
232 if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
233 $t = $this->tempTables[$key];
234 $alias = "temp_$key";
235 $tables[$alias] = $t['table'];
236 $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
237 if ( ( $tempTableStage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
238 $joinField = "{$alias}.{$t['field']}";
239 } else {
240 // Nothing hits this code path for now, but will in the future when we set
241 // $this->tempTables['rev_comment']['stage'] to MIGRATION_WRITE_NEW while
242 // merging revision_comment_temp into revision.
243 // @codeCoverageIgnoreStart
244 $joins[$alias][0] = 'LEFT JOIN';
245 $joinField = "(CASE WHEN {$key}_id != 0 THEN {$key}_id ELSE {$alias}.{$t['field']} END)";
246 throw new LogicException( 'Nothing should reach this code path at this time' );
247 // @codeCoverageIgnoreEnd
248 }
249 } else {
250 $joinField = "{$key}_id";
251 }
252
253 $alias = "comment_$key";
254 $tables[$alias] = 'comment';
255 $joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ];
256
257 if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_NEW ) {
258 $fields["{$key}_text"] = "{$alias}.comment_text";
259 } else {
260 $fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )";
261 }
262 $fields["{$key}_data"] = "{$alias}.comment_data";
263 $fields["{$key}_cid"] = "{$alias}.comment_id";
264 }
265
266 $this->joinCache[$key] = [
267 'tables' => $tables,
268 'fields' => $fields,
269 'joins' => $joins,
270 ];
271 }
272
273 return $this->joinCache[$key];
274 }
275
288 private function getCommentInternal( IDatabase $db = null, $key, $row, $fallback = false ) {
289 $row = (array)$row;
290 if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
291 $cid = $row["{$key}_cid"] ?? null;
292 $text = $row["{$key}_text"];
293 $data = $row["{$key}_data"];
294 } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
295 $cid = null;
296 if ( $fallback && isset( $row[$key] ) ) {
297 wfLogWarning( "Using deprecated fallback handling for comment $key" );
298 $text = $row[$key];
299 } else {
301 "Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD / READ_OLD"
302 );
303 $text = '';
304 }
305 $data = null;
306 } else {
307 $tempTableStage = isset( $this->tempTables[$key] )
308 ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
309 $row2 = null;
310 if ( ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) && array_key_exists( "{$key}_id", $row ) ) {
311 if ( !$db ) {
312 throw new InvalidArgumentException(
313 "\$row does not contain fields needed for comment $key and getComment(), but "
314 . "does have fields for getCommentLegacy()"
315 );
316 }
317 $id = $row["{$key}_id"];
318 $row2 = $db->selectRow(
319 'comment',
320 [ 'comment_id', 'comment_text', 'comment_data' ],
321 [ 'comment_id' => $id ],
322 __METHOD__
323 );
324 }
325 if ( !$row2 && ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) &&
326 array_key_exists( "{$key}_pk", $row )
327 ) {
328 if ( !$db ) {
329 throw new InvalidArgumentException(
330 "\$row does not contain fields needed for comment $key and getComment(), but "
331 . "does have fields for getCommentLegacy()"
332 );
333 }
334 $t = $this->tempTables[$key];
335 $id = $row["{$key}_pk"];
336 $row2 = $db->selectRow(
337 [ $t['table'], 'comment' ],
338 [ 'comment_id', 'comment_text', 'comment_data' ],
339 [ $t['pk'] => $id ],
340 __METHOD__,
341 [],
342 [ 'comment' => [ 'JOIN', [ "comment_id = {$t['field']}" ] ] ]
343 );
344 }
345 if ( $row2 === null && $fallback && isset( $row[$key] ) ) {
346 wfLogWarning( "Using deprecated fallback handling for comment $key" );
347 $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
348 }
349 if ( $row2 === null ) {
350 throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
351 }
352
353 if ( $row2 ) {
354 $cid = $row2->comment_id;
355 $text = $row2->comment_text;
356 $data = $row2->comment_data;
357 } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_OLD ) &&
358 array_key_exists( "{$key}_old", $row )
359 ) {
360 $cid = null;
361 $text = $row["{$key}_old"];
362 $data = null;
363 } else {
364 // @codeCoverageIgnoreStart
365 wfLogWarning( "Missing comment row for $key, id=$id" );
366 $cid = null;
367 $text = '';
368 $data = null;
369 // @codeCoverageIgnoreEnd
370 }
371 }
372
373 $msg = null;
374 if ( $data !== null ) {
375 $data = FormatJson::decode( $data, true );
376 if ( !is_array( $data ) ) {
377 // @codeCoverageIgnoreStart
378 wfLogWarning( "Invalid JSON object in comment: $data" );
379 $data = null;
380 // @codeCoverageIgnoreEnd
381 } else {
382 if ( isset( $data['_message'] ) ) {
383 $msg = self::decodeMessage( $data['_message'] )
384 ->setInterfaceMessageFlag( true );
385 }
386 if ( !empty( $data['_null'] ) ) {
387 $data = null;
388 } else {
389 foreach ( $data as $k => $v ) {
390 if ( substr( $k, 0, 1 ) === '_' ) {
391 unset( $data[$k] );
392 }
393 }
394 }
395 }
396 }
397
398 return new CommentStoreComment( $cid, $text, $msg, $data );
399 }
400
417 public function getComment( $key, $row = null, $fallback = false ) {
418 // Compat for method sig change in 1.31 (introduction of $key)
419 if ( $this->key !== null ) {
420 $fallback = $row;
421 $row = $key;
422 $key = $this->getKey();
423 }
424 if ( $row === null ) {
425 // @codeCoverageIgnoreStart
426 throw new InvalidArgumentException( '$row must not be null' );
427 // @codeCoverageIgnoreEnd
428 }
429 return $this->getCommentInternal( null, $key, $row, $fallback );
430 }
431
451 public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) {
452 // Compat for method sig change in 1.31 (introduction of $key)
453 if ( $this->key !== null ) {
454 $fallback = $row;
455 $row = $key;
456 $key = $this->getKey();
457 }
458 if ( $row === null ) {
459 // @codeCoverageIgnoreStart
460 throw new InvalidArgumentException( '$row must not be null' );
461 // @codeCoverageIgnoreEnd
462 }
463 return $this->getCommentInternal( $db, $key, $row, $fallback );
464 }
465
486 public function createComment( IDatabase $dbw, $comment, array $data = null ) {
487 $comment = CommentStoreComment::newUnsavedComment( $comment, $data );
488
489 # Truncate comment in a Unicode-sensitive manner
490 $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT );
491
492 if ( ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) && !$comment->id ) {
493 $dbData = $comment->data;
494 if ( !$comment->message instanceof RawMessage ) {
495 if ( $dbData === null ) {
496 $dbData = [ '_null' => true ];
497 }
498 $dbData['_message'] = self::encodeMessage( $comment->message );
499 }
500 if ( $dbData !== null ) {
501 $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
502 $len = strlen( $dbData );
503 if ( $len > self::MAX_DATA_LENGTH ) {
504 $max = self::MAX_DATA_LENGTH;
505 throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" );
506 }
507 }
508
509 $hash = self::hash( $comment->text, $dbData );
510 $comment->id = $dbw->selectField(
511 'comment',
512 'comment_id',
513 [
514 'comment_hash' => $hash,
515 'comment_text' => $comment->text,
516 'comment_data' => $dbData,
517 ],
518 __METHOD__
519 );
520 if ( !$comment->id ) {
521 $dbw->insert(
522 'comment',
523 [
524 'comment_hash' => $hash,
525 'comment_text' => $comment->text,
526 'comment_data' => $dbData,
527 ],
528 __METHOD__
529 );
530 $comment->id = $dbw->insertId();
531 }
532 }
533
534 return $comment;
535 }
536
546 private function insertInternal( IDatabase $dbw, $key, $comment, $data ) {
547 $fields = [];
548 $callback = null;
549
550 $comment = $this->createComment( $dbw, $comment, $data );
551
552 if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
553 $fields[$key] = $this->lang->truncateForDatabase( $comment->text, 255 );
554 }
555
556 if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
557 $tempTableStage = isset( $this->tempTables[$key] )
558 ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
559 if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
560 $t = $this->tempTables[$key];
561 $func = __METHOD__;
562 $commentId = $comment->id;
563 $callback = function ( $id ) use ( $dbw, $commentId, $t, $func ) {
564 $dbw->insert(
565 $t['table'],
566 [
567 $t['pk'] => $id,
568 $t['field'] => $commentId,
569 ],
570 $func
571 );
572 };
573 }
574 if ( $tempTableStage & SCHEMA_COMPAT_WRITE_NEW ) {
575 $fields["{$key}_id"] = $comment->id;
576 }
577 }
578
579 return [ $fields, $callback ];
580 }
581
597 public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
598 // Compat for method sig change in 1.31 (introduction of $key)
599 if ( $this->key !== null ) {
600 $data = $comment;
601 $comment = $key;
602 $key = $this->key;
603 }
604 if ( $comment === null ) {
605 // @codeCoverageIgnoreStart
606 throw new InvalidArgumentException( '$comment can not be null' );
607 // @codeCoverageIgnoreEnd
608 }
609
610 $tempTableStage = isset( $this->tempTables[$key] )
611 ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
612 if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
613 throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" );
614 }
615
616 list( $fields ) = $this->insertInternal( $dbw, $key, $comment, $data );
617 return $fields;
618 }
619
641 public function insertWithTempTable( IDatabase $dbw, $key, $comment = null, $data = null ) {
642 // Compat for method sig change in 1.31 (introduction of $key)
643 if ( $this->key !== null ) {
644 $data = $comment;
645 $comment = $key;
646 $key = $this->getKey();
647 }
648 if ( $comment === null ) {
649 // @codeCoverageIgnoreStart
650 throw new InvalidArgumentException( '$comment can not be null' );
651 // @codeCoverageIgnoreEnd
652 }
653
654 if ( !isset( $this->tempTables[$key] ) ) {
655 throw new InvalidArgumentException( "Must use insert() for $key" );
656 } elseif ( isset( $this->tempTables[$key]['deprecatedIn'] ) ) {
657 wfDeprecated( __METHOD__ . " for $key", $this->tempTables[$key]['deprecatedIn'] );
658 }
659
660 list( $fields, $callback ) = $this->insertInternal( $dbw, $key, $comment, $data );
661 if ( !$callback ) {
662 $callback = function () {
663 // Do nothing.
664 };
665 }
666 return [ $fields, $callback ];
667 }
668
674 protected static function encodeMessage( Message $msg ) {
675 $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
676 $params = $msg->getParams();
677 foreach ( $params as &$param ) {
678 if ( $param instanceof Message ) {
679 $param = [
680 'message' => self::encodeMessage( $param )
681 ];
682 }
683 }
684 array_unshift( $params, $key );
685 return $params;
686 }
687
693 protected static function decodeMessage( $data ) {
694 $key = array_shift( $data );
695 foreach ( $data as &$param ) {
696 if ( is_object( $param ) ) {
697 $param = (array)$param;
698 }
699 if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
700 $param = self::decodeMessage( $param['message'] );
701 }
702 }
703 return new Message( $key, $data );
704 }
705
712 public static function hash( $text, $data ) {
713 $hash = crc32( $text ) ^ crc32( (string)$data );
714
715 // 64-bit PHP returns an unsigned CRC, change it to signed for
716 // insertion into the database.
717 if ( $hash >= 0x80000000 ) {
718 $hash |= -1 << 32;
719 }
720
721 return $hash;
722 }
723
724}
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
$fallback
CommentStoreComment represents a comment stored by CommentStore.
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
int $stage
One of the MIGRATION_* constants, or an appropriate combination of SCHEMA_COMPAT_* constants.
static newKey( $key)
Static constructor for easier chaining.
getKey( $methodKey=null)
Compat method allowing use of self::newKey until removed.
getComment( $key, $row=null, $fallback=false)
Extract the comment from a row.
getJoin( $key=null)
Get SELECT fields and joins for the comment key.
createComment(IDatabase $dbw, $comment, array $data=null)
Create a new CommentStoreComment, inserting it into the database if necessary.
insertWithTempTable(IDatabase $dbw, $key, $comment=null, $data=null)
Insert a comment in a temporary table in preparation for a row that references it.
__construct(Language $lang, $stage)
getCommentLegacy(IDatabase $db, $key, $row=null, $fallback=false)
Extract the comment from a row, with legacy lookups.
static hash( $text, $data)
Hashing function for comment storage.
getFields( $key=null)
Get SELECT fields for the comment key.
insertInternal(IDatabase $dbw, $key, $comment, $data)
Implementation for self::insert() and self::insertWithTempTable()
Language $lang
Language to use for comment truncation.
const MAX_COMMENT_LENGTH
Maximum length of a comment in bytes.
static decodeMessage( $data)
Decode a message that was encoded by self::encodeMessage()
string null $key
array[] $joinCache
Cache for self::getJoin()
static encodeMessage(Message $msg)
Encode a Message as a PHP data structure.
insert(IDatabase $dbw, $key, $comment=null, $data=null)
Insert a comment in preparation for a row that references it.
array $tempTables
Define fields that use temporary tables for transitional purposes Keys are '$key',...
const MAX_DATA_LENGTH
Maximum length of serialized data in bytes.
static getStore()
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
getCommentInternal(IDatabase $db=null, $key, $row, $fallback=false)
Extract the comment from a row.
Internationalisation code.
Definition Language.php:37
MediaWikiServices is the service locator for the application scope of MediaWiki.
The Message class provides methods which fulfil two basic services:
Definition Message.php:162
getParams()
Returns the message parameters.
Definition Message.php:369
getKeysToTry()
Definition Message.php:343
getKey()
Returns the message key.
Definition Message.php:358
Variant of the Message class.
const SCHEMA_COMPAT_WRITE_BOTH
Definition Defines.php:277
const SCHEMA_COMPAT_READ_NEW
Definition Defines.php:276
const SCHEMA_COMPAT_READ_BOTH
Definition Defines.php:278
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:273
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:274
const MIGRATION_NEW
Definition Defines.php:307
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:275
const MIGRATION_OLD
Definition Defines.php:304
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
insert( $table, $a, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
insertId()
Get the inserted value of an auto-increment row.
return true
Definition router.php:94
if(!isset( $args[0])) $lang