MediaWiki  master
CommentStore.php
Go to the documentation of this file.
1 <?php
25 
31 class CommentStore {
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 
84  protected $stage;
85 
87  protected $joinCache = [];
88 
90  protected $lang;
91 
99  public function __construct( Language $lang, $stage ) {
100  if ( ( $stage & SCHEMA_COMPAT_WRITE_BOTH ) === 0 ) {
101  throw new InvalidArgumentException( '$stage must include a write mode' );
102  }
103  if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === 0 ) {
104  throw new InvalidArgumentException( '$stage must include a read mode' );
105  }
106 
107  $this->stage = $stage;
108  $this->lang = $lang;
109  }
110 
116  public static function getStore() {
117  return MediaWikiServices::getInstance()->getCommentStore();
118  }
119 
136  public function getFields( $key ) {
137  $fields = [];
138  if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
139  $fields["{$key}_text"] = $key;
140  $fields["{$key}_data"] = 'NULL';
141  $fields["{$key}_cid"] = 'NULL';
142  } else { // READ_BOTH or READ_NEW
143  if ( $this->stage & SCHEMA_COMPAT_READ_OLD ) {
144  $fields["{$key}_old"] = $key;
145  }
146 
147  $tempTableStage = isset( $this->tempTables[$key] )
148  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
149  if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
150  $fields["{$key}_pk"] = $this->tempTables[$key]['joinPK'];
151  }
152  if ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) {
153  $fields["{$key}_id"] = "{$key}_id";
154  }
155  }
156  return $fields;
157  }
158 
176  public function getJoin( $key ) {
177  if ( !array_key_exists( $key, $this->joinCache ) ) {
178  $tables = [];
179  $fields = [];
180  $joins = [];
181 
182  if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
183  $fields["{$key}_text"] = $key;
184  $fields["{$key}_data"] = 'NULL';
185  $fields["{$key}_cid"] = 'NULL';
186  } else { // READ_BOTH or READ_NEW
187  $join = ( $this->stage & SCHEMA_COMPAT_READ_OLD ) ? 'LEFT JOIN' : 'JOIN';
188 
189  $tempTableStage = isset( $this->tempTables[$key] )
190  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
191  if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
192  $t = $this->tempTables[$key];
193  $alias = "temp_$key";
194  $tables[$alias] = $t['table'];
195  $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
196  if ( ( $tempTableStage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
197  $joinField = "{$alias}.{$t['field']}";
198  } else {
199  // Nothing hits this code path for now, but will in the future when we set
200  // $this->tempTables['rev_comment']['stage'] to MIGRATION_WRITE_NEW while
201  // merging revision_comment_temp into revision.
202  // @codeCoverageIgnoreStart
203  $joins[$alias][0] = 'LEFT JOIN';
204  $joinField = "(CASE WHEN {$key}_id != 0 THEN {$key}_id ELSE {$alias}.{$t['field']} END)";
205  throw new LogicException( 'Nothing should reach this code path at this time' );
206  // @codeCoverageIgnoreEnd
207  }
208  } else {
209  $joinField = "{$key}_id";
210  }
211 
212  $alias = "comment_$key";
213  $tables[$alias] = 'comment';
214  $joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ];
215 
216  if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_NEW ) {
217  $fields["{$key}_text"] = "{$alias}.comment_text";
218  } else {
219  $fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )";
220  }
221  $fields["{$key}_data"] = "{$alias}.comment_data";
222  $fields["{$key}_cid"] = "{$alias}.comment_id";
223  }
224 
225  $this->joinCache[$key] = [
226  'tables' => $tables,
227  'fields' => $fields,
228  'joins' => $joins,
229  ];
230  }
231 
232  return $this->joinCache[$key];
233  }
234 
247  private function getCommentInternal( ?IDatabase $db, $key, $row, $fallback = false ) {
248  $row = (array)$row;
249  if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
250  $cid = $row["{$key}_cid"] ?? null;
251  $text = $row["{$key}_text"];
252  $data = $row["{$key}_data"];
253  } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
254  $cid = null;
255  if ( $fallback && isset( $row[$key] ) ) {
256  wfLogWarning( "Using deprecated fallback handling for comment $key" );
257  $text = $row[$key];
258  } else {
259  wfLogWarning(
260  "Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD / READ_OLD"
261  );
262  $text = '';
263  }
264  $data = null;
265  } else {
266  $tempTableStage = isset( $this->tempTables[$key] )
267  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
268  $row2 = null;
269  if ( ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) && array_key_exists( "{$key}_id", $row ) ) {
270  if ( !$db ) {
271  throw new InvalidArgumentException(
272  "\$row does not contain fields needed for comment $key and getComment(), but "
273  . "does have fields for getCommentLegacy()"
274  );
275  }
276  $id = $row["{$key}_id"];
277  $row2 = $db->selectRow(
278  'comment',
279  [ 'comment_id', 'comment_text', 'comment_data' ],
280  [ 'comment_id' => $id ],
281  __METHOD__
282  );
283  }
284  if ( !$row2 && ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) &&
285  array_key_exists( "{$key}_pk", $row )
286  ) {
287  if ( !$db ) {
288  throw new InvalidArgumentException(
289  "\$row does not contain fields needed for comment $key and getComment(), but "
290  . "does have fields for getCommentLegacy()"
291  );
292  }
293  $t = $this->tempTables[$key];
294  $id = $row["{$key}_pk"];
295  $row2 = $db->selectRow(
296  [ $t['table'], 'comment' ],
297  [ 'comment_id', 'comment_text', 'comment_data' ],
298  [ $t['pk'] => $id ],
299  __METHOD__,
300  [],
301  [ 'comment' => [ 'JOIN', [ "comment_id = {$t['field']}" ] ] ]
302  );
303  }
304  if ( $row2 === null && $fallback && isset( $row[$key] ) ) {
305  wfLogWarning( "Using deprecated fallback handling for comment $key" );
306  $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
307  }
308  if ( $row2 === null ) {
309  throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
310  }
311 
312  if ( $row2 ) {
313  $cid = $row2->comment_id;
314  $text = $row2->comment_text;
315  $data = $row2->comment_data;
316  } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_OLD ) &&
317  array_key_exists( "{$key}_old", $row )
318  ) {
319  $cid = null;
320  $text = $row["{$key}_old"];
321  $data = null;
322  } else {
323  // @codeCoverageIgnoreStart
324  wfLogWarning( "Missing comment row for $key, id=$id" );
325  $cid = null;
326  $text = '';
327  $data = null;
328  // @codeCoverageIgnoreEnd
329  }
330  }
331 
332  $msg = null;
333  if ( $data !== null ) {
334  $data = FormatJson::decode( $data, true );
335  if ( !is_array( $data ) ) {
336  // @codeCoverageIgnoreStart
337  wfLogWarning( "Invalid JSON object in comment: $data" );
338  $data = null;
339  // @codeCoverageIgnoreEnd
340  } else {
341  if ( isset( $data['_message'] ) ) {
342  $msg = self::decodeMessage( $data['_message'] )
343  ->setInterfaceMessageFlag( true );
344  }
345  if ( !empty( $data['_null'] ) ) {
346  $data = null;
347  } else {
348  foreach ( $data as $k => $v ) {
349  if ( substr( $k, 0, 1 ) === '_' ) {
350  unset( $data[$k] );
351  }
352  }
353  }
354  }
355  }
356 
357  return new CommentStoreComment( $cid, $text, $msg, $data );
358  }
359 
376  public function getComment( $key, $row = null, $fallback = false ) {
377  if ( $row === null ) {
378  // @codeCoverageIgnoreStart
379  throw new InvalidArgumentException( '$row must not be null' );
380  // @codeCoverageIgnoreEnd
381  }
382  return $this->getCommentInternal( null, $key, $row, $fallback );
383  }
384 
404  public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) {
405  if ( $row === null ) {
406  // @codeCoverageIgnoreStart
407  throw new InvalidArgumentException( '$row must not be null' );
408  // @codeCoverageIgnoreEnd
409  }
410  return $this->getCommentInternal( $db, $key, $row, $fallback );
411  }
412 
433  public function createComment( IDatabase $dbw, $comment, array $data = null ) {
434  $comment = CommentStoreComment::newUnsavedComment( $comment, $data );
435 
436  # Truncate comment in a Unicode-sensitive manner
437  $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT );
438 
439  if ( ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) && !$comment->id ) {
440  $dbData = $comment->data;
441  if ( !$comment->message instanceof RawMessage ) {
442  if ( $dbData === null ) {
443  $dbData = [ '_null' => true ];
444  }
445  $dbData['_message'] = self::encodeMessage( $comment->message );
446  }
447  if ( $dbData !== null ) {
448  $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
449  $len = strlen( $dbData );
450  if ( $len > self::MAX_DATA_LENGTH ) {
451  $max = self::MAX_DATA_LENGTH;
452  throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" );
453  }
454  }
455 
456  $hash = self::hash( $comment->text, $dbData );
457  $comment->id = $dbw->selectField(
458  'comment',
459  'comment_id',
460  [
461  'comment_hash' => $hash,
462  'comment_text' => $comment->text,
463  'comment_data' => $dbData,
464  ],
465  __METHOD__
466  );
467  if ( !$comment->id ) {
468  $dbw->insert(
469  'comment',
470  [
471  'comment_hash' => $hash,
472  'comment_text' => $comment->text,
473  'comment_data' => $dbData,
474  ],
475  __METHOD__
476  );
477  $comment->id = $dbw->insertId();
478  }
479  }
480 
481  return $comment;
482  }
483 
493  private function insertInternal( IDatabase $dbw, $key, $comment, $data ) {
494  $fields = [];
495  $callback = null;
496 
497  $comment = $this->createComment( $dbw, $comment, $data );
498 
499  if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
500  $fields[$key] = $this->lang->truncateForDatabase( $comment->text, 255 );
501  }
502 
503  if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
504  $tempTableStage = isset( $this->tempTables[$key] )
505  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
506  if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
507  $t = $this->tempTables[$key];
508  $func = __METHOD__;
509  $commentId = $comment->id;
510  $callback = function ( $id ) use ( $dbw, $commentId, $t, $func ) {
511  $dbw->insert(
512  $t['table'],
513  [
514  $t['pk'] => $id,
515  $t['field'] => $commentId,
516  ],
517  $func
518  );
519  };
520  }
521  if ( $tempTableStage & SCHEMA_COMPAT_WRITE_NEW ) {
522  $fields["{$key}_id"] = $comment->id;
523  }
524  }
525 
526  return [ $fields, $callback ];
527  }
528 
544  public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
545  if ( $comment === null ) {
546  // @codeCoverageIgnoreStart
547  throw new InvalidArgumentException( '$comment can not be null' );
548  // @codeCoverageIgnoreEnd
549  }
550 
551  $tempTableStage = isset( $this->tempTables[$key] )
552  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
553  if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
554  throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" );
555  }
556 
557  list( $fields ) = $this->insertInternal( $dbw, $key, $comment, $data );
558  return $fields;
559  }
560 
582  public function insertWithTempTable( IDatabase $dbw, $key, $comment = null, $data = null ) {
583  if ( $comment === null ) {
584  // @codeCoverageIgnoreStart
585  throw new InvalidArgumentException( '$comment can not be null' );
586  // @codeCoverageIgnoreEnd
587  }
588 
589  if ( !isset( $this->tempTables[$key] ) ) {
590  throw new InvalidArgumentException( "Must use insert() for $key" );
591  } elseif ( isset( $this->tempTables[$key]['deprecatedIn'] ) ) {
592  wfDeprecated( __METHOD__ . " for $key", $this->tempTables[$key]['deprecatedIn'] );
593  }
594 
595  list( $fields, $callback ) = $this->insertInternal( $dbw, $key, $comment, $data );
596  if ( !$callback ) {
597  $callback = function () {
598  // Do nothing.
599  };
600  }
601  return [ $fields, $callback ];
602  }
603 
609  protected static function encodeMessage( Message $msg ) {
610  $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
611  $params = $msg->getParams();
612  foreach ( $params as &$param ) {
613  if ( $param instanceof Message ) {
614  $param = [
615  'message' => self::encodeMessage( $param )
616  ];
617  }
618  }
619  array_unshift( $params, $key );
620  return $params;
621  }
622 
628  protected static function decodeMessage( $data ) {
629  $key = array_shift( $data );
630  foreach ( $data as &$param ) {
631  if ( is_object( $param ) ) {
632  $param = (array)$param;
633  }
634  if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
635  $param = self::decodeMessage( $param['message'] );
636  }
637  }
638  return new Message( $key, $data );
639  }
640 
647  public static function hash( $text, $data ) {
648  $hash = crc32( $text ) ^ crc32( (string)$data );
649 
650  // 64-bit PHP returns an unsigned CRC, change it to signed for
651  // insertion into the database.
652  if ( $hash >= 0x80000000 ) {
653  $hash |= -1 << 32;
654  }
655 
656  return $hash;
657  }
658 
659 }
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:66
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:176
CommentStore\insertInternal
insertInternal(IDatabase $dbw, $key, $comment, $data)
Implementation for self::insert() and self::insertWithTempTable()
Definition: CommentStore.php:493
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:137
true
return true
Definition: router.php:92
$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:376
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:544
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1067
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.
Message
CommentStore\$tempTables
array $tempTables
Define fields that use temporary tables for transitional purposes Keys are '$key',...
Definition: CommentStore.php:63
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
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:1033
CommentStore\$stage
int $stage
One of the MIGRATION_* constants, or an appropriate combination of SCHEMA_COMPAT_* constants.
Definition: CommentStore.php:84
CommentStore\getCommentInternal
getCommentInternal(?IDatabase $db, $key, $row, $fallback=false)
Extract the comment from a row.
Definition: CommentStore.php:247
CommentStore\decodeMessage
static decodeMessage( $data)
Decode a message that was encoded by self::encodeMessage()
Definition: CommentStore.php:628
CommentStore\encodeMessage
static encodeMessage(Message $msg)
Encode a Message as a PHP data structure.
Definition: CommentStore.php:609
MIGRATION_OLD
const MIGRATION_OLD
Definition: Defines.php:304
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:582
CommentStore\$joinCache
array[] $joinCache
Cache for self::getJoin()
Definition: CommentStore.php:87
CommentStore\__construct
__construct(Language $lang, $stage)
Definition: CommentStore.php:99
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:51
CommentStore\createComment
createComment(IDatabase $dbw, $comment, array $data=null)
Create a new CommentStoreComment, inserting it into the database if necessary.
Definition: CommentStore.php:433
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:647
CommentStore\COMMENT_CHARACTER_LIMIT
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
Definition: CommentStore.php:37
CommentStore\MAX_COMMENT_LENGTH
const MAX_COMMENT_LENGTH
Maximum length of a comment in bytes.
Definition: CommentStore.php:44
SCHEMA_COMPAT_WRITE_BOTH
const SCHEMA_COMPAT_WRITE_BOTH
Definition: Defines.php:277
CommentStore\getCommentLegacy
getCommentLegacy(IDatabase $db, $key, $row=null, $fallback=false)
Extract the comment from a row, with legacy lookups.
Definition: CommentStore.php:404
CommentStore\getFields
getFields( $key)
Get SELECT fields for the comment key.
Definition: CommentStore.php:136
$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:116
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:39
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:90