MediaWiki  master
CommentStore.php
Go to the documentation of this file.
1 <?php
23 
42 class CommentStore {
43 
48  public const COMMENT_CHARACTER_LIMIT = 500;
49 
55  public const MAX_DATA_LENGTH = 65535;
56 
67  private $tempTables = [
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 = isset( $this->tempTables[$key] )
152  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
153  if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
154  $fields["{$key}_pk"] = $this->tempTables[$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 = isset( $this->tempTables[$key] )
194  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
195  if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
196  $t = $this->tempTables[$key];
197  $alias = "temp_$key";
198  $tables[$alias] = $t['table'];
199  $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
200  if ( ( $tempTableStage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
201  $joinField = "{$alias}.{$t['field']}";
202  } else {
203  // Nothing hits this code path for now, but will in the future when we set
204  // $this->tempTables['rev_comment']['stage'] to MIGRATION_WRITE_NEW while
205  // merging revision_comment_temp into revision.
206  // @codeCoverageIgnoreStart
207  $joins[$alias][0] = 'LEFT JOIN';
208  $joinField = "(CASE WHEN {$key}_id != 0 THEN {$key}_id ELSE {$alias}.{$t['field']} END)";
209  throw new LogicException( 'Nothing should reach this code path at this time' );
210  // @codeCoverageIgnoreEnd
211  }
212  } else {
213  $joinField = "{$key}_id";
214  }
215 
216  $alias = "comment_$key";
217  $tables[$alias] = 'comment';
218  $joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ];
219 
220  if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_NEW ) {
221  $fields["{$key}_text"] = "{$alias}.comment_text";
222  } else {
223  $fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )";
224  }
225  $fields["{$key}_data"] = "{$alias}.comment_data";
226  $fields["{$key}_cid"] = "{$alias}.comment_id";
227  }
228 
229  $this->joinCache[$key] = [
230  'tables' => $tables,
231  'fields' => $fields,
232  'joins' => $joins,
233  ];
234  }
235 
236  return $this->joinCache[$key];
237  }
238 
251  private function getCommentInternal( ?IDatabase $db, $key, $row, $fallback = false ) {
252  $row = (array)$row;
253  if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
254  $cid = $row["{$key}_cid"] ?? null;
255  $text = $row["{$key}_text"];
256  $data = $row["{$key}_data"];
257  } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
258  $cid = null;
259  if ( $fallback && isset( $row[$key] ) ) {
260  wfLogWarning( "Using deprecated fallback handling for comment $key" );
261  $text = $row[$key];
262  } else {
263  wfLogWarning(
264  "Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD / READ_OLD"
265  );
266  $text = '';
267  }
268  $data = null;
269  } else {
270  $tempTableStage = isset( $this->tempTables[$key] )
271  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
272  $row2 = null;
273  if ( ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) && array_key_exists( "{$key}_id", $row ) ) {
274  if ( !$db ) {
275  throw new InvalidArgumentException(
276  "\$row does not contain fields needed for comment $key and getComment(), but "
277  . "does have fields for getCommentLegacy()"
278  );
279  }
280  $id = $row["{$key}_id"];
281  $row2 = $db->selectRow(
282  'comment',
283  [ 'comment_id', 'comment_text', 'comment_data' ],
284  [ 'comment_id' => $id ],
285  __METHOD__
286  );
287  }
288  if ( !$row2 && ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) &&
289  array_key_exists( "{$key}_pk", $row )
290  ) {
291  if ( !$db ) {
292  throw new InvalidArgumentException(
293  "\$row does not contain fields needed for comment $key and getComment(), but "
294  . "does have fields for getCommentLegacy()"
295  );
296  }
297  $t = $this->tempTables[$key];
298  $id = $row["{$key}_pk"];
299  $row2 = $db->selectRow(
300  [ $t['table'], 'comment' ],
301  [ 'comment_id', 'comment_text', 'comment_data' ],
302  [ $t['pk'] => $id ],
303  __METHOD__,
304  [],
305  [ 'comment' => [ 'JOIN', [ "comment_id = {$t['field']}" ] ] ]
306  );
307  }
308  if ( $row2 === null && $fallback && isset( $row[$key] ) ) {
309  wfLogWarning( "Using deprecated fallback handling for comment $key" );
310  $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
311  }
312  if ( $row2 === null ) {
313  throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
314  }
315 
316  if ( $row2 ) {
317  $cid = $row2->comment_id;
318  $text = $row2->comment_text;
319  $data = $row2->comment_data;
320  } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_OLD ) &&
321  array_key_exists( "{$key}_old", $row )
322  ) {
323  $cid = null;
324  $text = $row["{$key}_old"];
325  $data = null;
326  } else {
327  // @codeCoverageIgnoreStart
328  wfLogWarning( "Missing comment row for $key, id=$id" );
329  $cid = null;
330  $text = '';
331  $data = null;
332  // @codeCoverageIgnoreEnd
333  }
334  }
335 
336  $msg = null;
337  if ( $data !== null ) {
338  $data = FormatJson::decode( $data, true );
339  if ( !is_array( $data ) ) {
340  // @codeCoverageIgnoreStart
341  wfLogWarning( "Invalid JSON object in comment: $data" );
342  $data = null;
343  // @codeCoverageIgnoreEnd
344  } else {
345  if ( isset( $data['_message'] ) ) {
346  $msg = self::decodeMessage( $data['_message'] )
347  ->setInterfaceMessageFlag( true );
348  }
349  if ( !empty( $data['_null'] ) ) {
350  $data = null;
351  } else {
352  foreach ( $data as $k => $v ) {
353  if ( substr( $k, 0, 1 ) === '_' ) {
354  unset( $data[$k] );
355  }
356  }
357  }
358  }
359  }
360 
361  return new CommentStoreComment( $cid, $text, $msg, $data );
362  }
363 
380  public function getComment( $key, $row = null, $fallback = false ) {
381  if ( $row === null ) {
382  // @codeCoverageIgnoreStart
383  throw new InvalidArgumentException( '$row must not be null' );
384  // @codeCoverageIgnoreEnd
385  }
386  return $this->getCommentInternal( null, $key, $row, $fallback );
387  }
388 
408  public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) {
409  if ( $row === null ) {
410  // @codeCoverageIgnoreStart
411  throw new InvalidArgumentException( '$row must not be null' );
412  // @codeCoverageIgnoreEnd
413  }
414  return $this->getCommentInternal( $db, $key, $row, $fallback );
415  }
416 
437  public function createComment( IDatabase $dbw, $comment, array $data = null ) {
438  $comment = CommentStoreComment::newUnsavedComment( $comment, $data );
439 
440  # Truncate comment in a Unicode-sensitive manner
441  $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT );
442 
443  if ( ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) && !$comment->id ) {
444  $dbData = $comment->data;
445  if ( !$comment->message instanceof RawMessage ) {
446  if ( $dbData === null ) {
447  $dbData = [ '_null' => true ];
448  }
449  $dbData['_message'] = self::encodeMessage( $comment->message );
450  }
451  if ( $dbData !== null ) {
452  $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
453  $len = strlen( $dbData );
454  if ( $len > self::MAX_DATA_LENGTH ) {
455  $max = self::MAX_DATA_LENGTH;
456  throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" );
457  }
458  }
459 
460  $hash = self::hash( $comment->text, $dbData );
461  $comment->id = $dbw->selectField(
462  'comment',
463  'comment_id',
464  [
465  'comment_hash' => $hash,
466  'comment_text' => $comment->text,
467  'comment_data' => $dbData,
468  ],
469  __METHOD__
470  );
471  if ( !$comment->id ) {
472  $dbw->insert(
473  'comment',
474  [
475  'comment_hash' => $hash,
476  'comment_text' => $comment->text,
477  'comment_data' => $dbData,
478  ],
479  __METHOD__
480  );
481  $comment->id = $dbw->insertId();
482  }
483  }
484 
485  return $comment;
486  }
487 
497  private function insertInternal( IDatabase $dbw, $key, $comment, $data ) {
498  $fields = [];
499  $callback = null;
500 
501  $comment = $this->createComment( $dbw, $comment, $data );
502 
503  if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
504  $fields[$key] = $this->lang->truncateForDatabase( $comment->text, 255 );
505  }
506 
507  if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
508  $tempTableStage = isset( $this->tempTables[$key] )
509  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
510  if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
511  $t = $this->tempTables[$key];
512  $func = __METHOD__;
513  $commentId = $comment->id;
514  $callback = function ( $id ) use ( $dbw, $commentId, $t, $func ) {
515  $dbw->insert(
516  $t['table'],
517  [
518  $t['pk'] => $id,
519  $t['field'] => $commentId,
520  ],
521  $func
522  );
523  };
524  }
525  if ( $tempTableStage & SCHEMA_COMPAT_WRITE_NEW ) {
526  $fields["{$key}_id"] = $comment->id;
527  }
528  }
529 
530  return [ $fields, $callback ];
531  }
532 
548  public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
549  if ( $comment === null ) {
550  // @codeCoverageIgnoreStart
551  throw new InvalidArgumentException( '$comment can not be null' );
552  // @codeCoverageIgnoreEnd
553  }
554 
555  $tempTableStage = isset( $this->tempTables[$key] )
556  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
557  if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
558  throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" );
559  }
560 
561  list( $fields ) = $this->insertInternal( $dbw, $key, $comment, $data );
562  return $fields;
563  }
564 
586  public function insertWithTempTable( IDatabase $dbw, $key, $comment = null, $data = null ) {
587  if ( $comment === null ) {
588  // @codeCoverageIgnoreStart
589  throw new InvalidArgumentException( '$comment can not be null' );
590  // @codeCoverageIgnoreEnd
591  }
592 
593  if ( !isset( $this->tempTables[$key] ) ) {
594  throw new InvalidArgumentException( "Must use insert() for $key" );
595  } elseif ( isset( $this->tempTables[$key]['deprecatedIn'] ) ) {
596  wfDeprecated( __METHOD__ . " for $key", $this->tempTables[$key]['deprecatedIn'] );
597  }
598 
599  list( $fields, $callback ) = $this->insertInternal( $dbw, $key, $comment, $data );
600  if ( !$callback ) {
601  $callback = function () {
602  // Do nothing.
603  };
604  }
605  return [ $fields, $callback ];
606  }
607 
613  private static function encodeMessage( Message $msg ) {
614  $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
615  $params = $msg->getParams();
616  foreach ( $params as &$param ) {
617  if ( $param instanceof Message ) {
618  $param = [
619  'message' => self::encodeMessage( $param )
620  ];
621  }
622  }
623  array_unshift( $params, $key );
624  return $params;
625  }
626 
632  private static function decodeMessage( $data ) {
633  $key = array_shift( $data );
634  foreach ( $data as &$param ) {
635  if ( is_object( $param ) ) {
636  $param = (array)$param;
637  }
638  if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
639  $param = self::decodeMessage( $param['message'] );
640  }
641  }
642  return new Message( $key, $data );
643  }
644 
651  public static function hash( $text, $data ) {
652  $hash = crc32( $text ) ^ crc32( (string)$data );
653 
654  // 64-bit PHP returns an unsigned CRC, change it to signed for
655  // insertion into the database.
656  if ( $hash >= 0x80000000 ) {
657  $hash |= -1 << 32;
658  }
659 
660  return $hash;
661  }
662 
663 }
CommentStoreComment\newUnsavedComment
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Definition: CommentStoreComment.php:67
Message\getParams
getParams()
Returns the message parameters.
Definition: Message.php:378
SCHEMA_COMPAT_READ_NEW
const SCHEMA_COMPAT_READ_NEW
Definition: Defines.php:277
CommentStore\getJoin
getJoin( $key)
Get SELECT fields and joins for the comment key.
Definition: CommentStore.php:180
CommentStore\insertInternal
insertInternal(IDatabase $dbw, $key, $comment, $data)
Implementation for self::insert() and self::insertWithTempTable()
Definition: CommentStore.php:497
MediaWiki\MediaWikiServices
MediaWikiServices is the service locator for the application scope of MediaWiki.
Definition: MediaWikiServices.php:160
true
return true
Definition: router.php:90
$fallback
$fallback
Definition: MessagesAb.php:11
MIGRATION_NEW
const MIGRATION_NEW
Definition: Defines.php:308
CommentStore\getComment
getComment( $key, $row=null, $fallback=false)
Extract the comment from a row.
Definition: CommentStore.php:380
CommentStore
Handle database storage of comments such as edit summaries and log reasons.
Definition: CommentStore.php:42
CommentStore\insert
insert(IDatabase $dbw, $key, $comment=null, $data=null)
Insert a comment in preparation for a row that references it.
Definition: CommentStore.php:548
wfLogWarning
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
Definition: GlobalFunctions.php:1087
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:367
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:1027
CommentStore\$stage
int $stage
One of the MIGRATION_* constants, or an appropriate combination of SCHEMA_COMPAT_* constants.
Definition: CommentStore.php:88
CommentStore\getCommentInternal
getCommentInternal(?IDatabase $db, $key, $row, $fallback=false)
Extract the comment from a row.
Definition: CommentStore.php:251
CommentStore\decodeMessage
static decodeMessage( $data)
Decode a message that was encoded by self::encodeMessage()
Definition: CommentStore.php:632
CommentStore\encodeMessage
static encodeMessage(Message $msg)
Encode a Message as a PHP data structure.
Definition: CommentStore.php:613
MIGRATION_OLD
const MIGRATION_OLD
Definition: Defines.php:305
Message\getKeysToTry
getKeysToTry()
Definition: Message.php:352
CommentStore\$tempTables
array[] $tempTables
Define fields that use temporary tables for transitional purposes Keys are '$key',...
Definition: CommentStore.php:67
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:586
CommentStore\$joinCache
array[] $joinCache
Cache for self::getJoin()
Definition: CommentStore.php:91
CommentStore\__construct
__construct(Language $lang, $stage)
Definition: CommentStore.php:103
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:274
SCHEMA_COMPAT_WRITE_NEW
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:276
CommentStore\MAX_DATA_LENGTH
const MAX_DATA_LENGTH
Maximum length of serialized data in bytes.
Definition: CommentStore.php:55
CommentStore\createComment
createComment(IDatabase $dbw, $comment, array $data=null)
Create a new CommentStoreComment, inserting it into the database if necessary.
Definition: CommentStore.php:437
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:651
CommentStore\COMMENT_CHARACTER_LIMIT
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
Definition: CommentStore.php:48
SCHEMA_COMPAT_WRITE_BOTH
const SCHEMA_COMPAT_WRITE_BOTH
Definition: Defines.php:278
Message
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition: Message.php:161
CommentStore\getCommentLegacy
getCommentLegacy(IDatabase $db, $key, $row=null, $fallback=false)
Extract the comment from a row, with legacy lookups.
Definition: CommentStore.php:408
CommentStore\getFields
getFields( $key)
Get SELECT fields for the comment key.
Definition: CommentStore.php:140
$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:35
CommentStore\getStore
static getStore()
Definition: CommentStore.php:120
SCHEMA_COMPAT_READ_BOTH
const SCHEMA_COMPAT_READ_BOTH
Definition: Defines.php:279
CommentStoreComment
Value object for a comment stored by CommentStore.
Definition: CommentStoreComment.php:30
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:275
CommentStore\$lang
Language $lang
Language to use for comment truncation.
Definition: CommentStore.php:94