MediaWiki master
CommentStore.php
Go to the documentation of this file.
1<?php
8
9use InvalidArgumentException;
14use OverflowException;
15use stdClass;
18
38
43 public const COMMENT_CHARACTER_LIMIT = 500;
44
50 public const MAX_DATA_LENGTH = 65535;
51
53 private $joinCache = [];
54
56 private $lang;
57
62 public function __construct( Language $lang ) {
63 $this->lang = $lang;
64 }
65
84 public function getJoin( $key ) {
85 if ( !array_key_exists( $key, $this->joinCache ) ) {
86 $tables = [];
87 $fields = [];
88 $joins = [];
89
90 $alias = "comment_$key";
91 $tables[$alias] = 'comment';
92 $joins[$alias] = [ 'JOIN', "{$alias}.comment_id = {$key}_id" ];
93
94 $fields["{$key}_text"] = "{$alias}.comment_text";
95 $fields["{$key}_data"] = "{$alias}.comment_data";
96 $fields["{$key}_cid"] = "{$alias}.comment_id";
97
98 $this->joinCache[$key] = [
99 'tables' => $tables,
100 'fields' => $fields,
101 'joins' => $joins,
102 ];
103 }
104
105 return $this->joinCache[$key];
106 }
107
120 private function getCommentInternal( ?IReadableDatabase $db, $key, $row, $fallback = false ) {
121 $row = (array)$row;
122 if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
123 $cid = $row["{$key}_cid"] ?? null;
124 $text = $row["{$key}_text"];
125 $data = $row["{$key}_data"];
126 } else {
127 $row2 = null;
128 if ( array_key_exists( "{$key}_id", $row ) ) {
129 if ( !$db ) {
130 throw new InvalidArgumentException(
131 "\$row does not contain fields needed for comment $key and getComment(), but "
132 . "does have fields for getCommentLegacy()"
133 );
134 }
135 $id = $row["{$key}_id"];
136 $row2 = $db->newSelectQueryBuilder()
137 ->select( [ 'comment_id', 'comment_text', 'comment_data' ] )
138 ->from( 'comment' )
139 ->where( [ 'comment_id' => $id ] )
140 ->caller( __METHOD__ )->fetchRow();
141 }
142 if ( $row2 === null && $fallback && isset( $row[$key] ) ) {
143 wfLogWarning( "Using deprecated fallback handling for comment $key" );
144 $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ];
145 }
146 if ( $row2 === null ) {
147 throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" );
148 }
149
150 if ( $row2 ) {
151 $cid = $row2->comment_id;
152 $text = $row2->comment_text;
153 $data = $row2->comment_data;
154 } else {
155 // @codeCoverageIgnoreStart
156 // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $id is set when $row2 is okay
157 wfLogWarning( "Missing comment row for $key, id=$id" );
158 $cid = null;
159 $text = '';
160 $data = null;
161 // @codeCoverageIgnoreEnd
162 }
163 }
164
165 $msg = null;
166 if ( $data !== null ) {
167 $data = FormatJson::decode( $data, true );
168 if ( !is_array( $data ) ) {
169 // @codeCoverageIgnoreStart
170 wfLogWarning( "Invalid JSON object in comment: $data" );
171 $data = null;
172 // @codeCoverageIgnoreEnd
173 } else {
174 if ( isset( $data['_message'] ) ) {
175 $msg = self::decodeMessage( $data['_message'] )
176 ->setInterfaceMessageFlag( true );
177 }
178 if ( !empty( $data['_null'] ) ) {
179 $data = null;
180 } else {
181 foreach ( $data as $k => $v ) {
182 if ( str_starts_with( $k, '_' ) ) {
183 unset( $data[$k] );
184 }
185 }
186 }
187 }
188 }
189
190 return new CommentStoreComment( $cid, $text, $msg, $data );
191 }
192
209 public function getComment( $key, $row = null, $fallback = false ) {
210 if ( $row === null ) {
211 // @codeCoverageIgnoreStart
212 throw new InvalidArgumentException( '$row must not be null' );
213 // @codeCoverageIgnoreEnd
214 }
215 return $this->getCommentInternal( null, $key, $row, $fallback );
216 }
217
237 public function getCommentLegacy( IReadableDatabase $db, $key, $row = null, $fallback = false ) {
238 if ( $row === null ) {
239 // @codeCoverageIgnoreStart
240 throw new InvalidArgumentException( '$row must not be null' );
241 // @codeCoverageIgnoreEnd
242 }
243 return $this->getCommentInternal( $db, $key, $row, $fallback );
244 }
245
266 public function createComment( IDatabase $dbw, $comment, ?array $data = null ) {
267 $comment = CommentStoreComment::newUnsavedComment( $comment, $data );
268
269 # Truncate comment in a Unicode-sensitive manner
270 $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT );
271
272 if ( !$comment->id ) {
273 $dbData = $comment->data;
274 if ( !$comment->message instanceof RawMessage ) {
275 $dbData ??= [ '_null' => true ];
276 $dbData['_message'] = self::encodeMessage( $comment->message );
277 }
278 if ( $dbData !== null ) {
279 $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
280 $len = strlen( $dbData );
281 if ( $len > self::MAX_DATA_LENGTH ) {
283 throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" );
284 }
285 }
286
287 $hash = self::hash( $comment->text, $dbData );
288 $commentId = $dbw->newSelectQueryBuilder()
289 ->select( 'comment_id' )
290 ->from( 'comment' )
291 ->where( [
292 'comment_hash' => $hash,
293 'comment_text' => $comment->text,
294 'comment_data' => $dbData,
295 ] )
296 ->caller( __METHOD__ )->fetchField();
297 if ( !$commentId ) {
299 ->insertInto( 'comment' )
300 ->row( [ 'comment_hash' => $hash, 'comment_text' => $comment->text, 'comment_data' => $dbData ] )
301 ->caller( __METHOD__ )->execute();
302 $commentId = $dbw->insertId();
303 }
304 $comment->id = (int)$commentId;
305 }
306
307 return $comment;
308 }
309
326 public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
327 if ( $comment === null ) {
328 // @codeCoverageIgnoreStart
329 throw new InvalidArgumentException( '$comment can not be null' );
330 // @codeCoverageIgnoreEnd
331 }
332
333 $comment = $this->createComment( $dbw, $comment, $data );
334 return [ "{$key}_id" => $comment->id ];
335 }
336
342 private static function encodeMessage( Message $msg ) {
343 $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey();
344 $params = $msg->getParams();
345 foreach ( $params as &$param ) {
346 if ( $param instanceof Message ) {
347 $param = [
348 'message' => self::encodeMessage( $param )
349 ];
350 }
351 }
352 array_unshift( $params, $key );
353 return $params;
354 }
355
361 private static function decodeMessage( $data ) {
362 $key = array_shift( $data );
363 foreach ( $data as &$param ) {
364 if ( is_object( $param ) ) {
365 $param = (array)$param;
366 }
367 if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) {
368 $param = self::decodeMessage( $param['message'] );
369 }
370 }
371 return new Message( $key, $data );
372 }
373
380 private static function hash( $text, $data ) {
381 $hash = crc32( $text ) ^ crc32( (string)$data );
382
383 // 64-bit PHP returns an unsigned CRC, change it to signed for
384 // insertion into the database.
385 if ( $hash >= 0x80000000 ) {
386 $hash |= -1 << 32;
387 }
388
389 return $hash;
390 }
391
392}
wfLogWarning( $msg, $callerOffset=1, $level=E_USER_WARNING)
Send a warning as a PHP error and the debug log.
$fallback
static newUnsavedComment( $comment, ?array $data=null)
Create a new, unsaved CommentStoreComment.
Handle database storage of comments such as edit summaries and log reasons.
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.
createComment(IDatabase $dbw, $comment, ?array $data=null)
Create a new CommentStoreComment, inserting it into the database if necessary.
const MAX_DATA_LENGTH
Maximum length of serialized data in bytes.
getComment( $key, $row=null, $fallback=false)
Extract the comment from a row.
JSON formatter wrapper class.
Base class for language-specific code.
Definition Language.php:69
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
setInterfaceMessageFlag( $interface)
Allows manipulating the interface message flag directly.
Definition Message.php:968
getParams()
Returns the message parameters.
Definition Message.php:402
getKey()
Returns the message key.
Definition Message.php:391
Interface to a relational database.
Definition IDatabase.php:31
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.