MediaWiki master
CommentStore.php
Go to the documentation of this file.
1<?php
22
23use FormatJson;
24use InvalidArgumentException;
25use Language;
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
98 public function getJoin( $key ) {
99 if ( !array_key_exists( $key, $this->joinCache ) ) {
100 $tables = [];
101 $fields = [];
102 $joins = [];
103
104 $alias = "comment_$key";
105 $tables[$alias] = 'comment';
106 $joins[$alias] = [ 'JOIN', "{$alias}.comment_id = {$key}_id" ];
107
108 $fields["{$key}_text"] = "{$alias}.comment_text";
109 $fields["{$key}_data"] = "{$alias}.comment_data";
110 $fields["{$key}_cid"] = "{$alias}.comment_id";
111
112 $this->joinCache[$key] = [
113 'tables' => $tables,
114 'fields' => $fields,
115 'joins' => $joins,
116 ];
117 }
118
119 return $this->joinCache[$key];
120 }
121
134 private function getCommentInternal( ?IReadableDatabase $db, $key, $row, $fallback = false ) {
135 $row = (array)$row;
136 if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
137 $cid = $row["{$key}_cid"] ?? null;
138 $text = $row["{$key}_text"];
139 $data = $row["{$key}_data"];
140 } else {
141 $row2 = null;
142 if ( array_key_exists( "{$key}_id", $row ) ) {
143 if ( !$db ) {
144 throw new InvalidArgumentException(
145 "\$row does not contain fields needed for comment $key and getComment(), but "
146 . "does have fields for getCommentLegacy()"
147 );
148 }
149 $id = $row["{$key}_id"];
150 $row2 = $db->newSelectQueryBuilder()
151 ->select( [ 'comment_id', 'comment_text', 'comment_data' ] )
152 ->from( 'comment' )
153 ->where( [ 'comment_id' => $id ] )
154 ->caller( __METHOD__ )->fetchRow();
155 }
156 if ( $row2 === null && $fallback && isset( $row[$key] ) ) {
157 wfLogWarning( "Using deprecated fallback handling for comment $key" );
158 $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
159 }
160 if ( $row2 === null ) {
161 throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
162 }
163
164 if ( $row2 ) {
165 $cid = $row2->comment_id;
166 $text = $row2->comment_text;
167 $data = $row2->comment_data;
168 } else {
169 // @codeCoverageIgnoreStart
170 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $id is set when $row2 is okay
171 wfLogWarning( "Missing comment row for $key, id=$id" );
172 $cid = null;
173 $text = '';
174 $data = null;
175 // @codeCoverageIgnoreEnd
176 }
177 }
178
179 $msg = null;
180 if ( $data !== null ) {
181 $data = FormatJson::decode( $data, true );
182 if ( !is_array( $data ) ) {
183 // @codeCoverageIgnoreStart
184 wfLogWarning( "Invalid JSON object in comment: $data" );
185 $data = null;
186 // @codeCoverageIgnoreEnd
187 } else {
188 if ( isset( $data['_message'] ) ) {
189 $msg = self::decodeMessage( $data['_message'] )
190 ->setInterfaceMessageFlag( true );
191 }
192 if ( !empty( $data['_null'] ) ) {
193 $data = null;
194 } else {
195 foreach ( $data as $k => $v ) {
196 if ( substr( $k, 0, 1 ) === '_' ) {
197 unset( $data[$k] );
198 }
199 }
200 }
201 }
202 }
203
204 return new CommentStoreComment( $cid, $text, $msg, $data );
205 }
206
223 public function getComment( $key, $row = null, $fallback = false ) {
224 if ( $row === null ) {
225 // @codeCoverageIgnoreStart
226 throw new InvalidArgumentException( '$row must not be null' );
227 // @codeCoverageIgnoreEnd
228 }
229 return $this->getCommentInternal( null, $key, $row, $fallback );
230 }
231
251 public function getCommentLegacy( IReadableDatabase $db, $key, $row = null, $fallback = false ) {
252 if ( $row === null ) {
253 // @codeCoverageIgnoreStart
254 throw new InvalidArgumentException( '$row must not be null' );
255 // @codeCoverageIgnoreEnd
256 }
257 return $this->getCommentInternal( $db, $key, $row, $fallback );
258 }
259
280 public function createComment( IDatabase $dbw, $comment, array $data = null ) {
281 $comment = CommentStoreComment::newUnsavedComment( $comment, $data );
282
283 # Truncate comment in a Unicode-sensitive manner
284 $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT );
285
286 if ( !$comment->id ) {
287 $dbData = $comment->data;
288 if ( !$comment->message instanceof RawMessage ) {
289 $dbData ??= [ '_null' => true ];
290 $dbData['_message'] = self::encodeMessage( $comment->message );
291 }
292 if ( $dbData !== null ) {
293 $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
294 $len = strlen( $dbData );
295 if ( $len > self::MAX_DATA_LENGTH ) {
297 throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" );
298 }
299 }
300
301 $hash = self::hash( $comment->text, $dbData );
302 $commentId = $dbw->newSelectQueryBuilder()
303 ->select( 'comment_id' )
304 ->from( 'comment' )
305 ->where( [
306 'comment_hash' => $hash,
307 'comment_text' => $comment->text,
308 'comment_data' => $dbData,
309 ] )
310 ->caller( __METHOD__ )->fetchField();
311 if ( !$commentId ) {
313 ->insertInto( 'comment' )
314 ->row( [ 'comment_hash' => $hash, 'comment_text' => $comment->text, 'comment_data' => $dbData ] )
315 ->caller( __METHOD__ )->execute();
316 $commentId = $dbw->insertId();
317 }
318 $comment->id = (int)$commentId;
319 }
320
321 return $comment;
322 }
323
340 public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
341 if ( $comment === null ) {
342 // @codeCoverageIgnoreStart
343 throw new InvalidArgumentException( '$comment can not be null' );
344 // @codeCoverageIgnoreEnd
345 }
346
347 $comment = $this->createComment( $dbw, $comment, $data );
348 return [ "{$key}_id" => $comment->id ];
349 }
350
356 private static function encodeMessage( Message $msg ) {
357 $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
358 $params = $msg->getParams();
359 foreach ( $params as &$param ) {
360 if ( $param instanceof Message ) {
361 $param = [
362 'message' => self::encodeMessage( $param )
363 ];
364 }
365 }
366 array_unshift( $params, $key );
367 return $params;
368 }
369
375 private static function decodeMessage( $data ) {
376 $key = array_shift( $data );
377 foreach ( $data as &$param ) {
378 if ( is_object( $param ) ) {
379 $param = (array)$param;
380 }
381 if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
382 $param = self::decodeMessage( $param['message'] );
383 }
384 }
385 return new Message( $key, $data );
386 }
387
394 public static function hash( $text, $data ) {
395 $hash = crc32( $text ) ^ crc32( (string)$data );
396
397 // 64-bit PHP returns an unsigned CRC, change it to signed for
398 // insertion into the database.
399 if ( $hash >= 0x80000000 ) {
400 $hash |= -1 << 32;
401 }
402
403 return $hash;
404 }
405
406}
407
409class_alias( CommentStore::class, 'CommentStore' );
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
$fallback
array $params
The job parameters.
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.
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:158
setInterfaceMessageFlag( $interface)
Allows manipulating the interface message flag directly.
Definition Message.php:944
getParams()
Returns the message parameters.
Definition Message.php:397
getKey()
Returns the message key.
Definition Message.php:386
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.