MediaWiki 1.41.2
CommentStore.php
Go to the documentation of this file.
1<?php
22
23use FormatJson;
24use InvalidArgumentException;
25use Language;
27use Message;
28use OverflowException;
29use stdClass;
32
52
57 public const COMMENT_CHARACTER_LIMIT = 500;
58
64 public const MAX_DATA_LENGTH = 65535;
65
67 private $joinCache = [];
68
70 private $lang;
71
76 public function __construct( Language $lang ) {
77 $this->lang = $lang;
78 }
79
96 public function getFields( $key ) {
97 return [ "{$key}_id" => "{$key}_id" ];
98 }
99
118 public function getJoin( $key ) {
119 if ( !array_key_exists( $key, $this->joinCache ) ) {
120 $tables = [];
121 $fields = [];
122 $joins = [];
123
124 $alias = "comment_$key";
125 $tables[$alias] = 'comment';
126 $joins[$alias] = [ 'JOIN', "{$alias}.comment_id = {$key}_id" ];
127
128 $fields["{$key}_text"] = "{$alias}.comment_text";
129 $fields["{$key}_data"] = "{$alias}.comment_data";
130 $fields["{$key}_cid"] = "{$alias}.comment_id";
131
132 $this->joinCache[$key] = [
133 'tables' => $tables,
134 'fields' => $fields,
135 'joins' => $joins,
136 ];
137 }
138
139 return $this->joinCache[$key];
140 }
141
154 private function getCommentInternal( ?IReadableDatabase $db, $key, $row, $fallback = false ) {
155 $row = (array)$row;
156 if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
157 $cid = $row["{$key}_cid"] ?? null;
158 $text = $row["{$key}_text"];
159 $data = $row["{$key}_data"];
160 } else {
161 $row2 = null;
162 if ( array_key_exists( "{$key}_id", $row ) ) {
163 if ( !$db ) {
164 throw new InvalidArgumentException(
165 "\$row does not contain fields needed for comment $key and getComment(), but "
166 . "does have fields for getCommentLegacy()"
167 );
168 }
169 $id = $row["{$key}_id"];
170 $row2 = $db->newSelectQueryBuilder()
171 ->select( [ 'comment_id', 'comment_text', 'comment_data' ] )
172 ->from( 'comment' )
173 ->where( [ 'comment_id' => $id ] )
174 ->caller( __METHOD__ )->fetchRow();
175 }
176 if ( $row2 === null && $fallback && isset( $row[$key] ) ) {
177 wfLogWarning( "Using deprecated fallback handling for comment $key" );
178 $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
179 }
180 if ( $row2 === null ) {
181 throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
182 }
183
184 if ( $row2 ) {
185 $cid = $row2->comment_id;
186 $text = $row2->comment_text;
187 $data = $row2->comment_data;
188 } else {
189 // @codeCoverageIgnoreStart
190 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $id is set when $row2 is okay
191 wfLogWarning( "Missing comment row for $key, id=$id" );
192 $cid = null;
193 $text = '';
194 $data = null;
195 // @codeCoverageIgnoreEnd
196 }
197 }
198
199 $msg = null;
200 if ( $data !== null ) {
201 $data = FormatJson::decode( $data, true );
202 if ( !is_array( $data ) ) {
203 // @codeCoverageIgnoreStart
204 wfLogWarning( "Invalid JSON object in comment: $data" );
205 $data = null;
206 // @codeCoverageIgnoreEnd
207 } else {
208 if ( isset( $data['_message'] ) ) {
209 $msg = self::decodeMessage( $data['_message'] )
210 ->setInterfaceMessageFlag( true );
211 }
212 if ( !empty( $data['_null'] ) ) {
213 $data = null;
214 } else {
215 foreach ( $data as $k => $v ) {
216 if ( substr( $k, 0, 1 ) === '_' ) {
217 unset( $data[$k] );
218 }
219 }
220 }
221 }
222 }
223
224 return new CommentStoreComment( $cid, $text, $msg, $data );
225 }
226
243 public function getComment( $key, $row = null, $fallback = false ) {
244 if ( $row === null ) {
245 // @codeCoverageIgnoreStart
246 throw new InvalidArgumentException( '$row must not be null' );
247 // @codeCoverageIgnoreEnd
248 }
249 return $this->getCommentInternal( null, $key, $row, $fallback );
250 }
251
271 public function getCommentLegacy( IReadableDatabase $db, $key, $row = null, $fallback = false ) {
272 if ( $row === null ) {
273 // @codeCoverageIgnoreStart
274 throw new InvalidArgumentException( '$row must not be null' );
275 // @codeCoverageIgnoreEnd
276 }
277 return $this->getCommentInternal( $db, $key, $row, $fallback );
278 }
279
300 public function createComment( IDatabase $dbw, $comment, array $data = null ) {
301 $comment = CommentStoreComment::newUnsavedComment( $comment, $data );
302
303 # Truncate comment in a Unicode-sensitive manner
304 $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT );
305
306 if ( !$comment->id ) {
307 $dbData = $comment->data;
308 if ( !$comment->message instanceof RawMessage ) {
309 $dbData ??= [ '_null' => true ];
310 $dbData['_message'] = self::encodeMessage( $comment->message );
311 }
312 if ( $dbData !== null ) {
313 $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
314 $len = strlen( $dbData );
315 if ( $len > self::MAX_DATA_LENGTH ) {
317 throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" );
318 }
319 }
320
321 $hash = self::hash( $comment->text, $dbData );
322 $commentId = $dbw->newSelectQueryBuilder()
323 ->select( 'comment_id' )
324 ->from( 'comment' )
325 ->where( [
326 'comment_hash' => $hash,
327 'comment_text' => $comment->text,
328 'comment_data' => $dbData,
329 ] )
330 ->caller( __METHOD__ )->fetchField();
331 if ( !$commentId ) {
333 ->insertInto( 'comment' )
334 ->row( [ 'comment_hash' => $hash, 'comment_text' => $comment->text, 'comment_data' => $dbData ] )
335 ->caller( __METHOD__ )->execute();
336 $commentId = $dbw->insertId();
337 }
338 $comment->id = (int)$commentId;
339 }
340
341 return $comment;
342 }
343
360 public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
361 if ( $comment === null ) {
362 // @codeCoverageIgnoreStart
363 throw new InvalidArgumentException( '$comment can not be null' );
364 // @codeCoverageIgnoreEnd
365 }
366
367 $comment = $this->createComment( $dbw, $comment, $data );
368 return [ "{$key}_id" => $comment->id ];
369 }
370
376 private static function encodeMessage( Message $msg ) {
377 $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
378 $params = $msg->getParams();
379 foreach ( $params as &$param ) {
380 if ( $param instanceof Message ) {
381 $param = [
382 'message' => self::encodeMessage( $param )
383 ];
384 }
385 }
386 array_unshift( $params, $key );
387 return $params;
388 }
389
395 private static function decodeMessage( $data ) {
396 $key = array_shift( $data );
397 foreach ( $data as &$param ) {
398 if ( is_object( $param ) ) {
399 $param = (array)$param;
400 }
401 if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
402 $param = self::decodeMessage( $param['message'] );
403 }
404 }
405 return new Message( $key, $data );
406 }
407
414 public static function hash( $text, $data ) {
415 $hash = crc32( $text ) ^ crc32( (string)$data );
416
417 // 64-bit PHP returns an unsigned CRC, change it to signed for
418 // insertion into the database.
419 if ( $hash >= 0x80000000 ) {
420 $hash |= -1 << 32;
421 }
422
423 return $hash;
424 }
425
426}
427
431class_alias( CommentStore::class, 'CommentStore' );
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
$fallback
Definition MessagesAb.php:8
JSON formatter wrapper class.
static encode( $value, $pretty=false, $escaping=0)
Returns the JSON representation of a value.
static decode( $value, $assoc=false)
Decodes a JSON string.
const ALL_OK
Skip escaping as many characters as reasonably possible.
Base class for language-specific code.
Definition Language.php:63
static newUnsavedComment( $comment, array $data=null)
Create a new, unsaved CommentStoreComment.
Handle database storage of comments such as edit summaries and log reasons.
createComment(IDatabase $dbw, $comment, array $data=null)
Create a new CommentStoreComment, inserting it into the database if necessary.
getJoin( $key)
Get SELECT fields and joins for the comment key.
getFields( $key)
Get SELECT fields for the comment key.
const COMMENT_CHARACTER_LIMIT
Maximum length of a comment in UTF-8 characters.
insert(IDatabase $dbw, $key, $comment=null, $data=null)
Insert a comment in preparation for a row that references it.
getCommentLegacy(IReadableDatabase $db, $key, $row=null, $fallback=false)
Extract the comment from a row, with legacy lookups.
static hash( $text, $data)
Hashing function for comment storage.
const MAX_DATA_LENGTH
Maximum length of serialized data in bytes.
getComment( $key, $row=null, $fallback=false)
Extract the comment from a row.
Variant of the Message class.
The Message class deals with fetching and processing of interface message into a variety of formats.
Definition Message.php:144
getParams()
Returns the message parameters.
Definition Message.php:376
setInterfaceMessageFlag( $interface)
Allows manipulating the interface message flag directly.
Definition Message.php:892
getKeysToTry()
Definition Message.php:350
getKey()
Returns the message key.
Definition Message.php:365
Basic database interface for live and lazy-loaded relation database handles.
Definition IDatabase.php:36
insertId()
Get the sequence-based ID assigned by the last query method call.
newInsertQueryBuilder()
Get an InsertQueryBuilder bound to this connection.
A database connection without write operations.
newSelectQueryBuilder()
Create an empty SelectQueryBuilder which can be used to run queries against this connection.
return true
Definition router.php:92