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 
83  protected $key = null;
84 
91  protected $stage;
92 
94  protected $joinCache = [];
95 
97  protected $lang;
98 
106  public function __construct( Language $lang, $stage ) {
107  if ( ( $stage & SCHEMA_COMPAT_WRITE_BOTH ) === 0 ) {
108  throw new InvalidArgumentException( '$stage must include a write mode' );
109  }
110  if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === 0 ) {
111  throw new InvalidArgumentException( '$stage must include a read mode' );
112  }
113 
114  $this->stage = $stage;
115  $this->lang = $lang;
116  }
117 
125  public static function newKey( $key ) {
126  wfDeprecated( __METHOD__, '1.31' );
127  $store = new CommentStore(
128  MediaWikiServices::getInstance()->getContentLanguage(), MIGRATION_NEW
129  );
130  $store->key = $key;
131  return $store;
132  }
133 
139  public static function getStore() {
140  return MediaWikiServices::getInstance()->getCommentStore();
141  }
142 
149  private function getKey( $methodKey = null ) {
150  $key = $this->key ?? $methodKey;
151  if ( $key === null ) {
152  // @codeCoverageIgnoreStart
153  throw new InvalidArgumentException( '$key should not be null' );
154  // @codeCoverageIgnoreEnd
155  }
156  return $key;
157  }
158 
175  public function getFields( $key = null ) {
176  $key = $this->getKey( $key );
177  $fields = [];
178  if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
179  $fields["{$key}_text"] = $key;
180  $fields["{$key}_data"] = 'NULL';
181  $fields["{$key}_cid"] = 'NULL';
182  } else { // READ_BOTH or READ_NEW
183  if ( $this->stage & SCHEMA_COMPAT_READ_OLD ) {
184  $fields["{$key}_old"] = $key;
185  }
186 
187  $tempTableStage = isset( $this->tempTables[$key] )
188  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
189  if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
190  $fields["{$key}_pk"] = $this->tempTables[$key]['joinPK'];
191  }
192  if ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) {
193  $fields["{$key}_id"] = "{$key}_id";
194  }
195  }
196  return $fields;
197  }
198 
216  public function getJoin( $key = null ) {
217  $key = $this->getKey( $key );
218  if ( !array_key_exists( $key, $this->joinCache ) ) {
219  $tables = [];
220  $fields = [];
221  $joins = [];
222 
223  if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
224  $fields["{$key}_text"] = $key;
225  $fields["{$key}_data"] = 'NULL';
226  $fields["{$key}_cid"] = 'NULL';
227  } else { // READ_BOTH or READ_NEW
228  $join = ( $this->stage & SCHEMA_COMPAT_READ_OLD ) ? 'LEFT JOIN' : 'JOIN';
229 
230  $tempTableStage = isset( $this->tempTables[$key] )
231  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
232  if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
233  $t = $this->tempTables[$key];
234  $alias = "temp_$key";
235  $tables[$alias] = $t['table'];
236  $joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
237  if ( ( $tempTableStage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
238  $joinField = "{$alias}.{$t['field']}";
239  } else {
240  // Nothing hits this code path for now, but will in the future when we set
241  // $this->tempTables['rev_comment']['stage'] to MIGRATION_WRITE_NEW while
242  // merging revision_comment_temp into revision.
243  // @codeCoverageIgnoreStart
244  $joins[$alias][0] = 'LEFT JOIN';
245  $joinField = "(CASE WHEN {$key}_id != 0 THEN {$key}_id ELSE {$alias}.{$t['field']} END)";
246  throw new LogicException( 'Nothing should reach this code path at this time' );
247  // @codeCoverageIgnoreEnd
248  }
249  } else {
250  $joinField = "{$key}_id";
251  }
252 
253  $alias = "comment_$key";
254  $tables[$alias] = 'comment';
255  $joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ];
256 
257  if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_NEW ) {
258  $fields["{$key}_text"] = "{$alias}.comment_text";
259  } else {
260  $fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )";
261  }
262  $fields["{$key}_data"] = "{$alias}.comment_data";
263  $fields["{$key}_cid"] = "{$alias}.comment_id";
264  }
265 
266  $this->joinCache[$key] = [
267  'tables' => $tables,
268  'fields' => $fields,
269  'joins' => $joins,
270  ];
271  }
272 
273  return $this->joinCache[$key];
274  }
275 
288  private function getCommentInternal( ?IDatabase $db, $key, $row, $fallback = false ) {
289  $row = (array)$row;
290  if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
291  $cid = $row["{$key}_cid"] ?? null;
292  $text = $row["{$key}_text"];
293  $data = $row["{$key}_data"];
294  } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
295  $cid = null;
296  if ( $fallback && isset( $row[$key] ) ) {
297  wfLogWarning( "Using deprecated fallback handling for comment $key" );
298  $text = $row[$key];
299  } else {
300  wfLogWarning(
301  "Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD / READ_OLD"
302  );
303  $text = '';
304  }
305  $data = null;
306  } else {
307  $tempTableStage = isset( $this->tempTables[$key] )
308  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
309  $row2 = null;
310  if ( ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) && array_key_exists( "{$key}_id", $row ) ) {
311  if ( !$db ) {
312  throw new InvalidArgumentException(
313  "\$row does not contain fields needed for comment $key and getComment(), but "
314  . "does have fields for getCommentLegacy()"
315  );
316  }
317  $id = $row["{$key}_id"];
318  $row2 = $db->selectRow(
319  'comment',
320  [ 'comment_id', 'comment_text', 'comment_data' ],
321  [ 'comment_id' => $id ],
322  __METHOD__
323  );
324  }
325  if ( !$row2 && ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) &&
326  array_key_exists( "{$key}_pk", $row )
327  ) {
328  if ( !$db ) {
329  throw new InvalidArgumentException(
330  "\$row does not contain fields needed for comment $key and getComment(), but "
331  . "does have fields for getCommentLegacy()"
332  );
333  }
334  $t = $this->tempTables[$key];
335  $id = $row["{$key}_pk"];
336  $row2 = $db->selectRow(
337  [ $t['table'], 'comment' ],
338  [ 'comment_id', 'comment_text', 'comment_data' ],
339  [ $t['pk'] => $id ],
340  __METHOD__,
341  [],
342  [ 'comment' => [ 'JOIN', [ "comment_id = {$t['field']}" ] ] ]
343  );
344  }
345  if ( $row2 === null && $fallback && isset( $row[$key] ) ) {
346  wfLogWarning( "Using deprecated fallback handling for comment $key" );
347  $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
348  }
349  if ( $row2 === null ) {
350  throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
351  }
352 
353  if ( $row2 ) {
354  $cid = $row2->comment_id;
355  $text = $row2->comment_text;
356  $data = $row2->comment_data;
357  } elseif ( ( $this->stage & SCHEMA_COMPAT_READ_OLD ) &&
358  array_key_exists( "{$key}_old", $row )
359  ) {
360  $cid = null;
361  $text = $row["{$key}_old"];
362  $data = null;
363  } else {
364  // @codeCoverageIgnoreStart
365  wfLogWarning( "Missing comment row for $key, id=$id" );
366  $cid = null;
367  $text = '';
368  $data = null;
369  // @codeCoverageIgnoreEnd
370  }
371  }
372 
373  $msg = null;
374  if ( $data !== null ) {
375  $data = FormatJson::decode( $data, true );
376  if ( !is_array( $data ) ) {
377  // @codeCoverageIgnoreStart
378  wfLogWarning( "Invalid JSON object in comment: $data" );
379  $data = null;
380  // @codeCoverageIgnoreEnd
381  } else {
382  if ( isset( $data['_message'] ) ) {
383  $msg = self::decodeMessage( $data['_message'] )
384  ->setInterfaceMessageFlag( true );
385  }
386  if ( !empty( $data['_null'] ) ) {
387  $data = null;
388  } else {
389  foreach ( $data as $k => $v ) {
390  if ( substr( $k, 0, 1 ) === '_' ) {
391  unset( $data[$k] );
392  }
393  }
394  }
395  }
396  }
397 
398  return new CommentStoreComment( $cid, $text, $msg, $data );
399  }
400 
417  public function getComment( $key, $row = null, $fallback = false ) {
418  // Compat for method sig change in 1.31 (introduction of $key)
419  if ( $this->key !== null ) {
420  $fallback = $row;
421  $row = $key;
422  $key = $this->getKey();
423  }
424  if ( $row === null ) {
425  // @codeCoverageIgnoreStart
426  throw new InvalidArgumentException( '$row must not be null' );
427  // @codeCoverageIgnoreEnd
428  }
429  return $this->getCommentInternal( null, $key, $row, $fallback );
430  }
431 
451  public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) {
452  // Compat for method sig change in 1.31 (introduction of $key)
453  if ( $this->key !== null ) {
454  $fallback = $row;
455  $row = $key;
456  $key = $this->getKey();
457  }
458  if ( $row === null ) {
459  // @codeCoverageIgnoreStart
460  throw new InvalidArgumentException( '$row must not be null' );
461  // @codeCoverageIgnoreEnd
462  }
463  return $this->getCommentInternal( $db, $key, $row, $fallback );
464  }
465 
486  public function createComment( IDatabase $dbw, $comment, array $data = null ) {
487  $comment = CommentStoreComment::newUnsavedComment( $comment, $data );
488 
489  # Truncate comment in a Unicode-sensitive manner
490  $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT );
491 
492  if ( ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) && !$comment->id ) {
493  $dbData = $comment->data;
494  if ( !$comment->message instanceof RawMessage ) {
495  if ( $dbData === null ) {
496  $dbData = [ '_null' => true ];
497  }
498  $dbData['_message'] = self::encodeMessage( $comment->message );
499  }
500  if ( $dbData !== null ) {
501  $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
502  $len = strlen( $dbData );
503  if ( $len > self::MAX_DATA_LENGTH ) {
504  $max = self::MAX_DATA_LENGTH;
505  throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" );
506  }
507  }
508 
509  $hash = self::hash( $comment->text, $dbData );
510  $comment->id = $dbw->selectField(
511  'comment',
512  'comment_id',
513  [
514  'comment_hash' => $hash,
515  'comment_text' => $comment->text,
516  'comment_data' => $dbData,
517  ],
518  __METHOD__
519  );
520  if ( !$comment->id ) {
521  $dbw->insert(
522  'comment',
523  [
524  'comment_hash' => $hash,
525  'comment_text' => $comment->text,
526  'comment_data' => $dbData,
527  ],
528  __METHOD__
529  );
530  $comment->id = $dbw->insertId();
531  }
532  }
533 
534  return $comment;
535  }
536 
546  private function insertInternal( IDatabase $dbw, $key, $comment, $data ) {
547  $fields = [];
548  $callback = null;
549 
550  $comment = $this->createComment( $dbw, $comment, $data );
551 
552  if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
553  $fields[$key] = $this->lang->truncateForDatabase( $comment->text, 255 );
554  }
555 
556  if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
557  $tempTableStage = isset( $this->tempTables[$key] )
558  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
559  if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
560  $t = $this->tempTables[$key];
561  $func = __METHOD__;
562  $commentId = $comment->id;
563  $callback = function ( $id ) use ( $dbw, $commentId, $t, $func ) {
564  $dbw->insert(
565  $t['table'],
566  [
567  $t['pk'] => $id,
568  $t['field'] => $commentId,
569  ],
570  $func
571  );
572  };
573  }
574  if ( $tempTableStage & SCHEMA_COMPAT_WRITE_NEW ) {
575  $fields["{$key}_id"] = $comment->id;
576  }
577  }
578 
579  return [ $fields, $callback ];
580  }
581 
597  public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
598  // Compat for method sig change in 1.31 (introduction of $key)
599  if ( $this->key !== null ) {
600  $data = $comment;
601  $comment = $key;
602  $key = $this->key;
603  }
604  if ( $comment === null ) {
605  // @codeCoverageIgnoreStart
606  throw new InvalidArgumentException( '$comment can not be null' );
607  // @codeCoverageIgnoreEnd
608  }
609 
610  $tempTableStage = isset( $this->tempTables[$key] )
611  ? $this->tempTables[$key]['stage'] : MIGRATION_NEW;
612  if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
613  throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" );
614  }
615 
616  list( $fields ) = $this->insertInternal( $dbw, $key, $comment, $data );
617  return $fields;
618  }
619 
641  public function insertWithTempTable( IDatabase $dbw, $key, $comment = null, $data = null ) {
642  // Compat for method sig change in 1.31 (introduction of $key)
643  if ( $this->key !== null ) {
644  $data = $comment;
645  $comment = $key;
646  $key = $this->getKey();
647  }
648  if ( $comment === null ) {
649  // @codeCoverageIgnoreStart
650  throw new InvalidArgumentException( '$comment can not be null' );
651  // @codeCoverageIgnoreEnd
652  }
653 
654  if ( !isset( $this->tempTables[$key] ) ) {
655  throw new InvalidArgumentException( "Must use insert() for $key" );
656  } elseif ( isset( $this->tempTables[$key]['deprecatedIn'] ) ) {
657  wfDeprecated( __METHOD__ . " for $key", $this->tempTables[$key]['deprecatedIn'] );
658  }
659 
660  list( $fields, $callback ) = $this->insertInternal( $dbw, $key, $comment, $data );
661  if ( !$callback ) {
662  $callback = function () {
663  // Do nothing.
664  };
665  }
666  return [ $fields, $callback ];
667  }
668 
674  protected static function encodeMessage( Message $msg ) {
675  $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
676  $params = $msg->getParams();
677  foreach ( $params as &$param ) {
678  if ( $param instanceof Message ) {
679  $param = [
680  'message' => self::encodeMessage( $param )
681  ];
682  }
683  }
684  array_unshift( $params, $key );
685  return $params;
686  }
687 
693  protected static function decodeMessage( $data ) {
694  $key = array_shift( $data );
695  foreach ( $data as &$param ) {
696  if ( is_object( $param ) ) {
697  $param = (array)$param;
698  }
699  if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
700  $param = self::decodeMessage( $param['message'] );
701  }
702  }
703  return new Message( $key, $data );
704  }
705 
712  public static function hash( $text, $data ) {
713  $hash = crc32( $text ) ^ crc32( (string)$data );
714 
715  // 64-bit PHP returns an unsigned CRC, change it to signed for
716  // insertion into the database.
717  if ( $hash >= 0x80000000 ) {
718  $hash |= -1 << 32;
719  }
720 
721  return $hash;
722  }
723 
724 }
const SCHEMA_COMPAT_WRITE_OLD
Definition: Defines.php:264
getKeysToTry()
Definition: Message.php:343
getKey()
Returns the message key.
Definition: Message.php:358
const SCHEMA_COMPAT_READ_BOTH
Definition: Defines.php:269
static encodeMessage(Message $msg)
Encode a Message as a PHP data structure.
static newKey( $key)
Static constructor for easier chaining.
insertWithTempTable(IDatabase $dbw, $key, $comment=null, $data=null)
Insert a comment in a temporary table in preparation for a row that references it.
array string null $key
selectField( $table, $var, $cond='', $fname=__METHOD__, $options=[], $join_conds=[])
A SELECT wrapper which returns a single field from a single result row.
The Message class provides methods which fulfil two basic services:
Definition: Message.php:162
__construct(Language $lang, $stage)
const ALL_OK
Skip escaping as many characters as reasonably possible.
Definition: FormatJson.php:55
static hash( $text, $data)
Hashing function for comment storage.
const SCHEMA_COMPAT_READ_NEW
Definition: Defines.php:267
const MAX_DATA_LENGTH
Maximum length of serialized data in bytes.
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
Definition: FormatJson.php:115
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
insertId()
Get the inserted value of an auto-increment row.
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
createComment(IDatabase $dbw, $comment, array $data=null)
Create a new CommentStoreComment, inserting it into the database if necessary.
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
selectRow( $table, $vars, $conds, $fname=__METHOD__, $options=[], $join_conds=[])
Wrapper to IDatabase::select() that only fetches one row (via LIMIT)
getParams()
Returns the message parameters.
Definition: Message.php:369
getFields( $key=null)
Get SELECT fields for the comment key.
getJoin( $key=null)
Get SELECT fields and joins for the comment key.
const MIGRATION_OLD
Definition: Defines.php:295
const SCHEMA_COMPAT_WRITE_NEW
Definition: Defines.php:266
static decode( $value, $assoc=false)
Decodes a JSON string.
Definition: FormatJson.php:174
Language $lang
Language to use for comment truncation.
const MAX_COMMENT_LENGTH
Maximum length of a comment in bytes.
const SCHEMA_COMPAT_WRITE_BOTH
Definition: Defines.php:268
insert(IDatabase $dbw, $key, $comment=null, $data=null)
Insert a comment in preparation for a row that references it.
getCommentLegacy(IDatabase $db, $key, $row=null, $fallback=false)
Extract the comment from a row, with legacy lookups.
$fallback
Definition: MessagesAb.php:11
static getStore()
Basic database interface for live and lazy-loaded relation database handles.
Definition: IDatabase.php:38
Variant of the Message class.
Definition: RawMessage.php:34
insert( $table, $rows, $fname=__METHOD__, $options=[])
INSERT wrapper, inserts an array into a table.
wfDeprecated( $function, $version=false, $component=false, $callerOffset=2)
Throws a warning that $function is deprecated.
getComment( $key, $row=null, $fallback=false)
Extract the comment from a row.
array [] $joinCache
Cache for self::getJoin()
int $stage
One of the MIGRATION_* constants, or an appropriate combination of SCHEMA_COMPAT_* constants...
getCommentInternal(?IDatabase $db, $key, $row, $fallback=false)
Extract the comment from a row.
array $tempTables
Define fields that use temporary tables for transitional purposes Keys are &#39;$key&#39;, values are arrays with these possible fields:
const SCHEMA_COMPAT_READ_OLD
Definition: Defines.php:265
getKey( $methodKey=null)
Compat method allowing use of self::newKey until removed.
const MIGRATION_NEW
Definition: Defines.php:298
static decodeMessage( $data)
Decode a message that was encoded by self::encodeMessage()
insertInternal(IDatabase $dbw, $key, $comment, $data)
Implementation for self::insert() and self::insertWithTempTable()
return true
Definition: router.php:92