MediaWiki REL1_35
CommentStore.php
Go to the documentation of this file.
1<?php
25
32
37 public const COMMENT_CHARACTER_LIMIT = 500;
38
44 public const MAX_DATA_LENGTH = 65535;
45
56 private $tempTables = [
57 'rev_comment' => [
58 'table' => 'revision_comment_temp',
59 'pk' => 'revcomment_rev',
60 'field' => 'revcomment_comment_id',
61 'joinPK' => 'rev_id',
62 'stage' => MIGRATION_OLD,
63 'deprecatedIn' => null,
64 ],
65 'img_description' => [
66 'stage' => MIGRATION_NEW,
67 'deprecatedIn' => '1.32',
68 ],
69 ];
70
77 private $stage;
78
80 private $joinCache = [];
81
83 private $lang;
84
92 public function __construct( Language $lang, $stage ) {
93 if ( ( $stage & SCHEMA_COMPAT_WRITE_BOTH ) === 0 ) {
94 throw new InvalidArgumentException( '$stage must include a write mode' );
95 }
96 if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === 0 ) {
97 throw new InvalidArgumentException( '$stage must include a read mode' );
98 }
99
100 $this->stage = $stage;
101 $this->lang = $lang;
102 }
103
109 public static function getStore() {
110 return MediaWikiServices::getInstance()->getCommentStore();
111 }
112
129 public function getFields( $key ) {
130 $fields = [];
131 if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
132 $fields["{$key}_text"] = $key;
133 $fields["{$key}_data"] = 'NULL';
134 $fields["{$key}_cid"] = 'NULL';
135 } else { // READ_BOTH or READ_NEW
136 if ( $this->stage & SCHEMA_COMPAT_READ_OLD ) {
137 $fields["{$key}_old"] = $key;
138 }
139
140 $tempTableStage = isset( $this->tempTables[$key] )
141 ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
142 if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
143 $fields["{$key}_pk"] = $this->tempTables[$key]['joinPK'];
144 }
145 if ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) {
146 $fields["{$key}_id"] = "{$key}_id";
147 }
148 }
149 return $fields;
150 }
151
169 public function getJoin( $key ) {
170 if ( !array_key_exists( $key, $this->joinCache ) ) {
171 $tables = [];
172 $fields = [];
173 $joins = [];
174
175 if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
176 $fields["{$key}_text"] = $key;
177 $fields["{$key}_data"] = 'NULL';
178 $fields["{$key}_cid"] = 'NULL';
179 } else { // READ_BOTH or READ_NEW
180 $join = ( $this->stage & SCHEMA_COMPAT_READ_OLD ) ? 'LEFT JOIN' : 'JOIN';
181
182 $tempTableStage = isset( $this->tempTables[$key] )
183 ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
184 if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
185 $t = $this->tempTables[$key];
186 $alias = "temp_$key";
187 $tables[$alias] = $t['table'];
188 $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
189 if ( ( $tempTableStage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
190 $joinField = "{$alias}.{$t['field']}";
191 } else {
192 // Nothing hits this code path for now, but will in the future when we set
193 // $this->tempTables['rev_comment']['stage'] to MIGRATION_WRITE_NEW while
194 // merging revision_comment_temp into revision.
195 // @codeCoverageIgnoreStart
196 $joins[$alias][0] = 'LEFT JOIN';
197 $joinField = "(CASE WHEN {$key}_id != 0 THEN {$key}_id ELSE {$alias}.{$t['field']} END)";
198 throw new LogicException( 'Nothing should reach this code path at this time' );
199 // @codeCoverageIgnoreEnd
200 }
201 } else {
202 $joinField = "{$key}_id";
203 }
204
205 $alias = "comment_$key";
206 $tables[$alias] = 'comment';
207 $joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ];
208
209 if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_NEW ) {
210 $fields["{$key}_text"] = "{$alias}.comment_text";
211 } else {
212 $fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )";
213 }
214 $fields["{$key}_data"] = "{$alias}.comment_data";
215 $fields["{$key}_cid"] = "{$alias}.comment_id";
216 }
217
218 $this->joinCache[$key] = [
219 'tables' => $tables,
220 'fields' => $fields,
221 'joins' => $joins,
222 ];
223 }
224
225 return $this->joinCache[$key];
226 }
227
240 private function getCommentInternal( ?IDatabase $db, $key, $row, $fallback = false ) {
241 $row = (array)$row;
242 if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
243 $cid = $row["{$key}_cid"] ?? null;
244 $text = $row["{$key}_text"];
245 $data = $row["{$key}_data"];
246 } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
247 $cid = null;
248 if ( $fallback && isset( $row[$key] ) ) {
249 wfLogWarning( "Using deprecated fallback handling for comment $key" );
250 $text = $row[$key];
251 } else {
253 "Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD / READ_OLD"
254 );
255 $text = '';
256 }
257 $data = null;
258 } else {
259 $tempTableStage = isset( $this->tempTables[$key] )
260 ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
261 $row2 = null;
262 if ( ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) && array_key_exists( "{$key}_id", $row ) ) {
263 if ( !$db ) {
264 throw new InvalidArgumentException(
265 "\$row does not contain fields needed for comment $key and getComment(), but "
266 . "does have fields for getCommentLegacy()"
267 );
268 }
269 $id = $row["{$key}_id"];
270 $row2 = $db->selectRow(
271 'comment',
272 [ 'comment_id', 'comment_text', 'comment_data' ],
273 [ 'comment_id' => $id ],
274 __METHOD__
275 );
276 }
277 if ( !$row2 && ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) &&
278 array_key_exists( "{$key}_pk", $row )
279 ) {
280 if ( !$db ) {
281 throw new InvalidArgumentException(
282 "\$row does not contain fields needed for comment $key and getComment(), but "
283 . "does have fields for getCommentLegacy()"
284 );
285 }
286 $t = $this->tempTables[$key];
287 $id = $row["{$key}_pk"];
288 $row2 = $db->selectRow(
289 [ $t['table'], 'comment' ],
290 [ 'comment_id', 'comment_text', 'comment_data' ],
291 [ $t['pk'] => $id ],
292 __METHOD__,
293 [],
294 [ 'comment' => [ 'JOIN', [ "comment_id = {$t['field']}" ] ] ]
295 );
296 }
297 if ( $row2 === null && $fallback && isset( $row[$key] ) ) {
298 wfLogWarning( "Using deprecated fallback handling for comment $key" );
299 $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
300 }
301 if ( $row2 === null ) {
302 throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
303 }
304
305 if ( $row2 ) {
306 $cid = $row2->comment_id;
307 $text = $row2->comment_text;
308 $data = $row2->comment_data;
309 } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_OLD ) &&
310 array_key_exists( "{$key}_old", $row )
311 ) {
312 $cid = null;
313 $text = $row["{$key}_old"];
314 $data = null;
315 } else {
316 // @codeCoverageIgnoreStart
317 wfLogWarning( "Missing comment row for $key, id=$id" );
318 $cid = null;
319 $text = '';
320 $data = null;
321 // @codeCoverageIgnoreEnd
322 }
323 }
324
325 $msg = null;
326 if ( $data !== null ) {
327 $data = FormatJson::decode( $data, true );
328 if ( !is_array( $data ) ) {
329 // @codeCoverageIgnoreStart
330 wfLogWarning( "Invalid JSON object in comment: $data" );
331 $data = null;
332 // @codeCoverageIgnoreEnd
333 } else {
334 if ( isset( $data['_message'] ) ) {
335 $msg = self::decodeMessage( $data['_message'] )
336 ->setInterfaceMessageFlag( true );
337 }
338 if ( !empty( $data['_null'] ) ) {
339 $data = null;
340 } else {
341 foreach ( $data as $k => $v ) {
342 if ( substr( $k, 0, 1 ) === '_' ) {
343 unset( $data[$k] );
344 }
345 }
346 }
347 }
348 }
349
350 return new CommentStoreComment( $cid, $text, $msg, $data );
351 }
352
369 public function getComment( $key, $row = null, $fallback = false ) {
370 if ( $row === null ) {
371 // @codeCoverageIgnoreStart
372 throw new InvalidArgumentException( '$row must not be null' );
373 // @codeCoverageIgnoreEnd
374 }
375 return $this->getCommentInternal( null, $key, $row, $fallback );
376 }
377
397 public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) {
398 if ( $row === null ) {
399 // @codeCoverageIgnoreStart
400 throw new InvalidArgumentException( '$row must not be null' );
401 // @codeCoverageIgnoreEnd
402 }
403 return $this->getCommentInternal( $db, $key, $row, $fallback );
404 }
405
426 public function createComment( IDatabase $dbw, $comment, array $data = null ) {
427 $comment = CommentStoreComment::newUnsavedComment( $comment, $data );
428
429 # Truncate comment in a Unicode-sensitive manner
430 $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT );
431
432 if ( ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) && !$comment->id ) {
433 $dbData = $comment->data;
434 if ( !$comment->message instanceof RawMessage ) {
435 if ( $dbData === null ) {
436 $dbData = [ '_null' => true ];
437 }
438 $dbData['_message'] = self::encodeMessage( $comment->message );
439 }
440 if ( $dbData !== null ) {
441 $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
442 $len = strlen( $dbData );
443 if ( $len > self::MAX_DATA_LENGTH ) {
444 $max = self::MAX_DATA_LENGTH;
445 throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" );
446 }
447 }
448
449 $hash = self::hash( $comment->text, $dbData );
450 $comment->id = $dbw->selectField(
451 'comment',
452 'comment_id',
453 [
454 'comment_hash' => $hash,
455 'comment_text' => $comment->text,
456 'comment_data' => $dbData,
457 ],
458 __METHOD__
459 );
460 if ( !$comment->id ) {
461 $dbw->insert(
462 'comment',
463 [
464 'comment_hash' => $hash,
465 'comment_text' => $comment->text,
466 'comment_data' => $dbData,
467 ],
468 __METHOD__
469 );
470 $comment->id = $dbw->insertId();
471 }
472 }
473
474 return $comment;
475 }
476
486 private function insertInternal( IDatabase $dbw, $key, $comment, $data ) {
487 $fields = [];
488 $callback = null;
489
490 $comment = $this->createComment( $dbw, $comment, $data );
491
492 if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
493 $fields[$key] = $this->lang->truncateForDatabase( $comment->text, 255 );
494 }
495
496 if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
497 $tempTableStage = isset( $this->tempTables[$key] )
498 ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
499 if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
500 $t = $this->tempTables[$key];
501 $func = __METHOD__;
502 $commentId = $comment->id;
503 $callback = function ( $id ) use ( $dbw, $commentId, $t, $func ) {
504 $dbw->insert(
505 $t['table'],
506 [
507 $t['pk'] => $id,
508 $t['field'] => $commentId,
509 ],
510 $func
511 );
512 };
513 }
514 if ( $tempTableStage & SCHEMA_COMPAT_WRITE_NEW ) {
515 $fields["{$key}_id"] = $comment->id;
516 }
517 }
518
519 return [ $fields, $callback ];
520 }
521
537 public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
538 if ( $comment === null ) {
539 // @codeCoverageIgnoreStart
540 throw new InvalidArgumentException( '$comment can not be null' );
541 // @codeCoverageIgnoreEnd
542 }
543
544 $tempTableStage = isset( $this->tempTables[$key] )
545 ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
546 if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
547 throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" );
548 }
549
550 list( $fields ) = $this->insertInternal( $dbw, $key, $comment, $data );
551 return $fields;
552 }
553
575 public function insertWithTempTable( IDatabase $dbw, $key, $comment = null, $data = null ) {
576 if ( $comment === null ) {
577 // @codeCoverageIgnoreStart
578 throw new InvalidArgumentException( '$comment can not be null' );
579 // @codeCoverageIgnoreEnd
580 }
581
582 if ( !isset( $this->tempTables[$key] ) ) {
583 throw new InvalidArgumentException( "Must use insert() for $key" );
584 } elseif ( isset( $this->tempTables[$key]['deprecatedIn'] ) ) {
585 wfDeprecated( __METHOD__ . " for $key", $this->tempTables[$key]['deprecatedIn'] );
586 }
587
588 list( $fields, $callback ) = $this->insertInternal( $dbw, $key, $comment, $data );
589 if ( !$callback ) {
590 $callback = function () {
591 // Do nothing.
592 };
593 }
594 return [ $fields, $callback ];
595 }
596
602 private static function encodeMessage( Message $msg ) {
603 $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
604 $params = $msg->getParams();
605 foreach ( $params as &$param ) {
606 if ( $param instanceof Message ) {
607 $param = [
608 'message' => self::encodeMessage( $param )
609 ];
610 }
611 }
612 array_unshift( $params, $key );
613 return $params;
614 }
615
621 private static function decodeMessage( $data ) {
622 $key = array_shift( $data );
623 foreach ( $data as &$param ) {
624 if ( is_object( $param ) ) {
625 $param = (array)$param;
626 }
627 if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
628 $param = self::decodeMessage( $param['message'] );
629 }
630 }
631 return new Message( $key, $data );
632 }
633
640 public static function hash( $text, $data ) {
641 $hash = crc32( $text ) ^ crc32( (string)$data );
642
643 // 64-bit PHP returns an unsigned CRC, change it to signed for
644 // insertion into the database.
645 if ( $hash >= 0x80000000 ) {
646 $hash |= -1 << 32;
647 }
648
649 return $hash;
650 }
651
652}
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 $function is deprecated.
$fallback
CommentStoreComment represents a comment stored by CommentStore.
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
getFields( $key)
Get SELECT fields for the comment key.
int $stage
One of the MIGRATION_* constants, or an appropriate combination of SCHEMA_COMPAT_* constants.
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)
array[] $tempTables
Define fields that use temporary tables for transitional purposes Keys are '$key',...
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.
getCommentInternal(?IDatabase $db, $key, $row, $fallback=false)
Extract the comment from a row.
insertInternal(IDatabase $dbw, $key, $comment, $data)
Implementation for self::insert() and self::insertWithTempTable()
Language $lang
Language to use for comment truncation.
static decodeMessage( $data)
Decode a message that was encoded by self::encodeMessage()
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.
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.
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition Language.php:41
MediaWikiServices is the service locator for the application scope of MediaWiki.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:161
getParams()
Returns the message parameters.
Definition Message.php:394
getKeysToTry()
Definition Message.php:368
getKey()
Returns the message key.
Definition Message.php:383
Variant of the Message class.
const SCHEMA_COMPAT_WRITE_BOTH
Definition Defines.php:278
const SCHEMA_COMPAT_READ_NEW
Definition Defines.php:277
const SCHEMA_COMPAT_READ_BOTH
Definition Defines.php:279
const SCHEMA_COMPAT_WRITE_OLD
Definition Defines.php:274
const SCHEMA_COMPAT_READ_OLD
Definition Defines.php:275
const MIGRATION_NEW
Definition Defines.php:308
const SCHEMA_COMPAT_WRITE_NEW
Definition Defines.php:276
const MIGRATION_OLD
Definition Defines.php:305
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:38
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 the given row(s) into a table.
insertId()
Get the inserted value of an auto-increment row.
return true
Definition router.php:92
if(!isset( $args[0])) $lang