MediaWiki  master
CommentStore.php
Go to the documentation of this file.
1 <?php
25 
31 class CommentStore {
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 {
252  wfLogWarning(
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 }
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:66
Message\getParams
getParams()
Returns the message parameters.
Definition: Message.php:376
SCHEMA_COMPAT_READ_NEW
const SCHEMA_COMPAT_READ_NEW
Definition: Defines.php:276
CommentStore\getJoin
getJoin( $key)
Get SELECT fields and joins for the comment key.
Definition: CommentStore.php:169
CommentStore\insertInternal
insertInternal(IDatabase $dbw, $key, $comment, $data)
Implementation for self::insert() and self::insertWithTempTable()
Definition: CommentStore.php:486
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:152
true
return true
Definition: router.php:90
$fallback
$fallback
Definition: MessagesAb.php:11
MIGRATION_NEW
const MIGRATION_NEW
Definition: Defines.php:307
CommentStore\getComment
getComment( $key, $row=null, $fallback=false)
Extract the comment from a row.
Definition: CommentStore.php:369
CommentStore
CommentStore handles storage of comments (edit summaries, log reasons, etc) in the database.
Definition: CommentStore.php:31
CommentStore\insert
insert(IDatabase $dbw, $key, $comment=null, $data=null)
Insert a comment in preparation for a row that references it.
Definition: CommentStore.php:537
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1089
Wikimedia\Rdbms\IDatabase\selectField
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
FormatJson\ALL_OK
const ALL_OK
Skip escaping as many characters as reasonably possible.
Definition: FormatJson.php:55
Wikimedia\Rdbms\IDatabase
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
Message\getKey
getKey()
Returns the message key.
Definition: Message.php:365
FormatJson\decode
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
FormatJson\encode
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:115
wfDeprecated
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Logs a warning that $function is deprecated.
Definition: GlobalFunctions.php:1029
CommentStore\$stage
int $stage
One of the MIGRATION_* constants, or an appropriate combination of SCHEMA_COMPAT_* constants.
Definition: CommentStore.php:77
CommentStore\getCommentInternal
getCommentInternal(?IDatabase $db, $key, $row, $fallback=false)
Extract the comment from a row.
Definition: CommentStore.php:240
CommentStore\decodeMessage
static decodeMessage( $data)
Decode a message that was encoded by self::encodeMessage()
Definition: CommentStore.php:621
CommentStore\encodeMessage
static encodeMessage(Message $msg)
Encode a Message as a PHP data structure.
Definition: CommentStore.php:602
MIGRATION_OLD
const MIGRATION_OLD
Definition: Defines.php:304
Message\getKeysToTry
getKeysToTry()
Definition: Message.php:350
CommentStore\$tempTables
array[] $tempTables
Define fields that use temporary tables for transitional purposes Keys are '$key',...
Definition: CommentStore.php:56
CommentStore\insertWithTempTable
insertWithTempTable(IDatabase $dbw, $key, $comment=null, $data=null)
Insert a comment in a temporary table in preparation for a row that references it.
Definition: CommentStore.php:575
CommentStore\$joinCache
array[] $joinCache
Cache for self::getJoin()
Definition: CommentStore.php:80
CommentStore\__construct
__construct(Language $lang, $stage)
Definition: CommentStore.php:92
Wikimedia\Rdbms\IDatabase\selectRow
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
SCHEMA_COMPAT_WRITE_OLD
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:273
SCHEMA_COMPAT_WRITE_NEW
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:275
CommentStore\MAX_DATA_LENGTH
const MAX_DATA_LENGTH
Maximum length of serialized data in bytes.
Definition: CommentStore.php:44
CommentStore\createComment
createComment(IDatabase $dbw, $comment, array $data=null)
Create a new CommentStoreComment, inserting it into the database if necessary.
Definition: CommentStore.php:426
Wikimedia\Rdbms\IDatabase\insert
insert( $table, $rows, $fname=__METHOD__, $options=[])
Insert the given row(s) into a table.
CommentStore\hash
static hash( $text, $data)
Hashing function for comment storage.
Definition: CommentStore.php:640
CommentStore\COMMENT_CHARACTER_LIMIT
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
Definition: CommentStore.php:37
SCHEMA_COMPAT_WRITE_BOTH
const SCHEMA_COMPAT_WRITE_BOTH
Definition: Defines.php:277
Message
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:160
CommentStore\getCommentLegacy
getCommentLegacy(IDatabase $db, $key, $row=null, $fallback=false)
Extract the comment from a row, with legacy lookups.
Definition: CommentStore.php:397
CommentStore\getFields
getFields( $key)
Get SELECT fields for the comment key.
Definition: CommentStore.php:129
$t
$t
Definition: testCompression.php:74
Wikimedia\Rdbms\IDatabase\insertId
insertId()
Get the inserted value of an auto-increment row.
RawMessage
Variant of the Message class.
Definition: RawMessage.php:34
CommentStore\getStore
static getStore()
Definition: CommentStore.php:109
SCHEMA_COMPAT_READ_BOTH
const SCHEMA_COMPAT_READ_BOTH
Definition: Defines.php:278
CommentStoreComment
CommentStoreComment represents a comment stored by CommentStore.
Definition: CommentStoreComment.php:29
Language
Internationalisation code See https://www.mediawiki.org/wiki/Special:MyLanguage/Localisation for more...
Definition: Language.php:41
SCHEMA_COMPAT_READ_OLD
const SCHEMA_COMPAT_READ_OLD
Definition: Defines.php:274
CommentStore\$lang
Language $lang
Language to use for comment truncation.
Definition: CommentStore.php:83