MediaWiki REL1_39
CommentStore.php
Go to the documentation of this file.
1<?php
23
43
48 public const COMMENT_CHARACTER_LIMIT = 500;
49
55 public const MAX_DATA_LENGTH = 65535;
56
67 protected const TEMP_TABLES = [
68 'rev_comment' => [
69 'table' => 'revision_comment_temp',
70 'pk' => 'revcomment_rev',
71 'field' => 'revcomment_comment_id',
72 'joinPK' => 'rev_id',
73 'stage' => MIGRATION_OLD,
74 'deprecatedIn' => null,
75 ],
76 'img_description' => [
77 'stage' => MIGRATION_NEW,
78 'deprecatedIn' => '1.32',
79 ],
80 ];
81
88 private $stage;
89
91 private $joinCache = [];
92
94 private $lang;
95
103 public function __construct( Language $lang, $stage ) {
104 if ( ( $stage & SCHEMA_COMPAT_WRITE_BOTH ) === 0 ) {
105 throw new InvalidArgumentException( '$stage must include a write mode' );
106 }
107 if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === 0 ) {
108 throw new InvalidArgumentException( '$stage must include a read mode' );
109 }
110
111 $this->stage = $stage;
112 $this->lang = $lang;
113 }
114
120 public static function getStore() {
121 return MediaWikiServices::getInstance()->getCommentStore();
122 }
123
140 public function getFields( $key ) {
141 $fields = [];
142 if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
143 $fields["{$key}_text"] = $key;
144 $fields["{$key}_data"] = 'NULL';
145 $fields["{$key}_cid"] = 'NULL';
146 } else { // READ_BOTH or READ_NEW
147 if ( $this->stage & SCHEMA_COMPAT_READ_OLD ) {
148 $fields["{$key}_old"] = $key;
149 }
150
151 $tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
152 if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
153 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Only set for stage old
154 $fields["{$key}_pk"] = static::TEMP_TABLES[$key]['joinPK'];
155 }
156 if ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) {
157 $fields["{$key}_id"] = "{$key}_id";
158 }
159 }
160 return $fields;
161 }
162
180 public function getJoin( $key ) {
181 if ( !array_key_exists( $key, $this->joinCache ) ) {
182 $tables = [];
183 $fields = [];
184 $joins = [];
185
186 if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
187 $fields["{$key}_text"] = $key;
188 $fields["{$key}_data"] = 'NULL';
189 $fields["{$key}_cid"] = 'NULL';
190 } else { // READ_BOTH or READ_NEW
191 $join = ( $this->stage & SCHEMA_COMPAT_READ_OLD ) ? 'LEFT JOIN' : 'JOIN';
192
193 $tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
194 if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
195 $t = static::TEMP_TABLES[$key];
196 $alias = "temp_$key";
197 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Only set for stage old
198 $tables[$alias] = $t['table'];
199 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Only set for stage old
200 $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
201 if ( ( $tempTableStage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
202 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Only set for stage old
203 $joinField = "{$alias}.{$t['field']}";
204 } else {
205 // Nothing hits this code path for now, but will in the future when we set
206 // static::TEMP_TABLES['rev_comment']['stage'] to MIGRATION_WRITE_NEW while
207 // merging revision_comment_temp into revision.
208 // @codeCoverageIgnoreStart
209 $joins[$alias][0] = 'LEFT JOIN';
210 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Only set for stage old
211 $joinField = "(CASE WHEN {$key}_id != 0 THEN {$key}_id ELSE {$alias}.{$t['field']} END)";
212 throw new LogicException( 'Nothing should reach this code path at this time' );
213 // @codeCoverageIgnoreEnd
214 }
215 } else {
216 $joinField = "{$key}_id";
217 }
218
219 $alias = "comment_$key";
220 $tables[$alias] = 'comment';
221 $joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ];
222
223 if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_NEW ) {
224 $fields["{$key}_text"] = "{$alias}.comment_text";
225 } else {
226 $fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )";
227 }
228 $fields["{$key}_data"] = "{$alias}.comment_data";
229 $fields["{$key}_cid"] = "{$alias}.comment_id";
230 }
231
232 $this->joinCache[$key] = [
233 'tables' => $tables,
234 'fields' => $fields,
235 'joins' => $joins,
236 ];
237 }
238
239 return $this->joinCache[$key];
240 }
241
254 private function getCommentInternal( ?IDatabase $db, $key, $row, $fallback = false ) {
255 $row = (array)$row;
256 if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
257 $cid = $row["{$key}_cid"] ?? null;
258 $text = $row["{$key}_text"];
259 $data = $row["{$key}_data"];
260 } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
261 $cid = null;
262 if ( $fallback && isset( $row[$key] ) ) {
263 wfLogWarning( "Using deprecated fallback handling for comment $key" );
264 $text = $row[$key];
265 } else {
267 "Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD / READ_OLD"
268 );
269 $text = '';
270 }
271 $data = null;
272 } else {
273 $tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
274 $row2 = null;
275 if ( ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) && array_key_exists( "{$key}_id", $row ) ) {
276 if ( !$db ) {
277 throw new InvalidArgumentException(
278 "\$row does not contain fields needed for comment $key and getComment(), but "
279 . "does have fields for getCommentLegacy()"
280 );
281 }
282 $id = $row["{$key}_id"];
283 $row2 = $db->selectRow(
284 'comment',
285 [ 'comment_id', 'comment_text', 'comment_data' ],
286 [ 'comment_id' => $id ],
287 __METHOD__
288 );
289 }
290 if ( !$row2 && ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) &&
291 array_key_exists( "{$key}_pk", $row )
292 ) {
293 if ( !$db ) {
294 throw new InvalidArgumentException(
295 "\$row does not contain fields needed for comment $key and getComment(), but "
296 . "does have fields for getCommentLegacy()"
297 );
298 }
299 $t = static::TEMP_TABLES[$key];
300 $id = $row["{$key}_pk"];
301 $row2 = $db->selectRow(
302 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Only set for stage old
303 [ $t['table'], 'comment' ],
304 [ 'comment_id', 'comment_text', 'comment_data' ],
305 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Only set for stage old
306 [ $t['pk'] => $id ],
307 __METHOD__,
308 [],
309 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Only set for stage old
310 [ 'comment' => [ 'JOIN', [ "comment_id = {$t['field']}" ] ] ]
311 );
312 }
313 if ( $row2 === null && $fallback && isset( $row[$key] ) ) {
314 wfLogWarning( "Using deprecated fallback handling for comment $key" );
315 $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
316 }
317 if ( $row2 === null ) {
318 throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
319 }
320
321 if ( $row2 ) {
322 $cid = $row2->comment_id;
323 $text = $row2->comment_text;
324 $data = $row2->comment_data;
325 } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_OLD ) &&
326 array_key_exists( "{$key}_old", $row )
327 ) {
328 $cid = null;
329 $text = $row["{$key}_old"];
330 $data = null;
331 } else {
332 // @codeCoverageIgnoreStart
333 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $id is set when $row2 is okay
334 wfLogWarning( "Missing comment row for $key, id=$id" );
335 $cid = null;
336 $text = '';
337 $data = null;
338 // @codeCoverageIgnoreEnd
339 }
340 }
341
342 $msg = null;
343 if ( $data !== null ) {
344 $data = FormatJson::decode( $data, true );
345 if ( !is_array( $data ) ) {
346 // @codeCoverageIgnoreStart
347 wfLogWarning( "Invalid JSON object in comment: $data" );
348 $data = null;
349 // @codeCoverageIgnoreEnd
350 } else {
351 if ( isset( $data['_message'] ) ) {
352 $msg = self::decodeMessage( $data['_message'] )
353 ->setInterfaceMessageFlag( true );
354 }
355 if ( !empty( $data['_null'] ) ) {
356 $data = null;
357 } else {
358 foreach ( $data as $k => $v ) {
359 if ( substr( $k, 0, 1 ) === '_' ) {
360 unset( $data[$k] );
361 }
362 }
363 }
364 }
365 }
366
367 return new CommentStoreComment( $cid, $text, $msg, $data );
368 }
369
386 public function getComment( $key, $row = null, $fallback = false ) {
387 if ( $row === null ) {
388 // @codeCoverageIgnoreStart
389 throw new InvalidArgumentException( '$row must not be null' );
390 // @codeCoverageIgnoreEnd
391 }
392 return $this->getCommentInternal( null, $key, $row, $fallback );
393 }
394
414 public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) {
415 if ( $row === null ) {
416 // @codeCoverageIgnoreStart
417 throw new InvalidArgumentException( '$row must not be null' );
418 // @codeCoverageIgnoreEnd
419 }
420 return $this->getCommentInternal( $db, $key, $row, $fallback );
421 }
422
443 public function createComment( IDatabase $dbw, $comment, array $data = null ) {
444 $comment = CommentStoreComment::newUnsavedComment( $comment, $data );
445
446 # Truncate comment in a Unicode-sensitive manner
447 $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT );
448
449 if ( ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) && !$comment->id ) {
450 $dbData = $comment->data;
451 if ( !$comment->message instanceof RawMessage ) {
452 if ( $dbData === null ) {
453 $dbData = [ '_null' => true ];
454 }
455 $dbData['_message'] = self::encodeMessage( $comment->message );
456 }
457 if ( $dbData !== null ) {
458 $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
459 $len = strlen( $dbData );
460 if ( $len > self::MAX_DATA_LENGTH ) {
461 $max = self::MAX_DATA_LENGTH;
462 throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" );
463 }
464 }
465
466 $hash = self::hash( $comment->text, $dbData );
467 $commentId = $dbw->selectField(
468 'comment',
469 'comment_id',
470 [
471 'comment_hash' => $hash,
472 'comment_text' => $comment->text,
473 'comment_data' => $dbData,
474 ],
475 __METHOD__
476 );
477 if ( !$commentId ) {
478 $dbw->insert(
479 'comment',
480 [
481 'comment_hash' => $hash,
482 'comment_text' => $comment->text,
483 'comment_data' => $dbData,
484 ],
485 __METHOD__
486 );
487 $commentId = $dbw->insertId();
488 }
489 $comment->id = (int)$commentId;
490 }
491
492 return $comment;
493 }
494
504 private function insertInternal( IDatabase $dbw, $key, $comment, $data ) {
505 $fields = [];
506 $callback = null;
507
508 $comment = $this->createComment( $dbw, $comment, $data );
509
510 if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
511 $fields[$key] = $this->lang->truncateForDatabase( $comment->text, 255 );
512 }
513
514 if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
515 $tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
516 if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
517 $t = static::TEMP_TABLES[$key];
518 $func = __METHOD__;
519 $commentId = $comment->id;
520 $callback = static function ( $id ) use ( $dbw, $commentId, $t, $func ) {
521 $dbw->insert(
522 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Only set for stage old
523 $t['table'],
524 [
525 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Only set for stage old
526 $t['pk'] => $id,
527 // @phan-suppress-next-line PhanTypePossiblyInvalidDimOffset Only set for stage old
528 $t['field'] => $commentId,
529 ],
530 $func
531 );
532 };
533 }
534 if ( $tempTableStage & SCHEMA_COMPAT_WRITE_NEW ) {
535 $fields["{$key}_id"] = $comment->id;
536 }
537 }
538
539 return [ $fields, $callback ];
540 }
541
557 public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
558 if ( $comment === null ) {
559 // @codeCoverageIgnoreStart
560 throw new InvalidArgumentException( '$comment can not be null' );
561 // @codeCoverageIgnoreEnd
562 }
563
564 $tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
565 if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
566 throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" );
567 }
568
569 list( $fields ) = $this->insertInternal( $dbw, $key, $comment, $data );
570 return $fields;
571 }
572
594 public function insertWithTempTable( IDatabase $dbw, $key, $comment = null, $data = null ) {
595 if ( $comment === null ) {
596 // @codeCoverageIgnoreStart
597 throw new InvalidArgumentException( '$comment can not be null' );
598 // @codeCoverageIgnoreEnd
599 }
600
601 if ( !isset( static::TEMP_TABLES[$key] ) ) {
602 throw new InvalidArgumentException( "Must use insert() for $key" );
603 } elseif ( isset( static::TEMP_TABLES[$key]['deprecatedIn'] ) ) {
604 wfDeprecated( __METHOD__ . " for $key", static::TEMP_TABLES[$key]['deprecatedIn'] );
605 }
606
607 list( $fields, $callback ) = $this->insertInternal( $dbw, $key, $comment, $data );
608 if ( !$callback ) {
609 $callback = static function () {
610 // Do nothing.
611 };
612 }
613 return [ $fields, $callback ];
614 }
615
621 private static function encodeMessage( Message $msg ) {
622 $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
623 $params = $msg->getParams();
624 foreach ( $params as &$param ) {
625 if ( $param instanceof Message ) {
626 $param = [
627 'message' => self::encodeMessage( $param )
628 ];
629 }
630 }
631 array_unshift( $params, $key );
632 return $params;
633 }
634
640 private static function decodeMessage( $data ) {
641 $key = array_shift( $data );
642 foreach ( $data as &$param ) {
643 if ( is_object( $param ) ) {
644 $param = (array)$param;
645 }
646 if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
647 $param = self::decodeMessage( $param['message'] );
648 }
649 }
650 return new Message( $key, $data );
651 }
652
659 public static function hash( $text, $data ) {
660 $hash = crc32( $text ) ^ crc32( (string)$data );
661
662 // 64-bit PHP returns an unsigned CRC, change it to signed for
663 // insertion into the database.
664 if ( $hash >= 0x80000000 ) {
665 $hash |= -1 << 32;
666 }
667
668 return $hash;
669 }
670
671}
const SCHEMA_COMPAT_WRITE_BOTH
Definition Defines.php:273
const SCHEMA_COMPAT_READ_NEW
Definition Defines.php:270
const SCHEMA_COMPAT_READ_BOTH
Definition Defines.php:276
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:265
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:266
const MIGRATION_NEW
Definition Defines.php:306
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:269
const MIGRATION_OLD
Definition Defines.php:303
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)
Logs a warning that a deprecated feature was used.
$fallback
Value object for a comment stored by CommentStore.
Handle database storage of comments such as edit summaries and log reasons.
getFields( $key)
Get SELECT fields for the comment key.
getComment( $key, $row=null, $fallback=false)
Extract the comment from a row.
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.
const TEMP_TABLES
Define fields that use temporary tables for transitional purposes Array keys are field names,...
insert(IDatabase $dbw, $key, $comment=null, $data=null)
Insert a comment in preparation for a row that references it.
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.
getJoin( $key)
Get SELECT fields and joins for the comment key.
static decode( $value, $assoc=false)
Decodes a JSON string.
Base class for language-specific code.
Definition Language.php:53
Service locator for MediaWiki core services.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:140
getParams()
Returns the message parameters.
Definition Message.php:371
setInterfaceMessageFlag( $interface)
Allows manipulating the interface message flag directly.
Definition Message.php:889
getKeysToTry()
Definition Message.php:345
getKey()
Returns the message key.
Definition Message.php:360
Variant of the Message class.
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:39
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
insert( $table, $rows, $fname=__METHOD__, $options=[])
Insert row(s) into a table, in the provided order.
insertId()
Get the inserted value of an auto-increment row.
return true
Definition router.php:92
if(!isset( $args[0])) $lang