diff options
author | Alexander Vorwerk <zabe@avorwerk.net> | 2023-09-01 12:17:14 +0200 |
---|---|---|
committer | Alexander Vorwerk <zabe@avorwerk.net> | 2023-09-01 12:17:14 +0200 |
commit | 4a4d9e2621a408e4cd52df61c196d1e48a8115dd (patch) | |
tree | 5436669baa4ba76e45e573aae65356cb13fa3f0d /includes/CommentStore | |
parent | b4af67e6ce0686b024a795ee1f5a8e77735faccb (diff) | |
download | mediawikicore-4a4d9e2621a408e4cd52df61c196d1e48a8115dd.tar.gz mediawikicore-4a4d9e2621a408e4cd52df61c196d1e48a8115dd.zip |
Merge CommentStoreBase into CommentStore
Bug: T343558
Change-Id: I74cd68b7689f750d9300141575506910878f1802
Diffstat (limited to 'includes/CommentStore')
-rw-r--r-- | includes/CommentStore/CommentStore.php | 383 | ||||
-rw-r--r-- | includes/CommentStore/CommentStoreBase.php | 411 |
2 files changed, 382 insertions, 412 deletions
diff --git a/includes/CommentStore/CommentStore.php b/includes/CommentStore/CommentStore.php index 261202e4c310..f9591b7eeb98 100644 --- a/includes/CommentStore/CommentStore.php +++ b/includes/CommentStore/CommentStore.php @@ -20,6 +20,15 @@ namespace MediaWiki\CommentStore; +use FormatJson; +use InvalidArgumentException; +use Language; +use MediaWiki\Language\RawMessage; +use Message; +use OverflowException; +use stdClass; +use Wikimedia\Rdbms\IDatabase; + /** * @defgroup CommentStore CommentStore * @@ -38,7 +47,379 @@ namespace MediaWiki\CommentStore; * @ingroup CommentStore * @since 1.30 */ -class CommentStore extends CommentStoreBase { +class CommentStore { + + /** + * Maximum length of a comment in UTF-8 characters. Longer comments will be truncated. + * @note This must be at least 255 and not greater than floor( MAX_DATA_LENGTH / 4 ). + */ + public const COMMENT_CHARACTER_LIMIT = 500; + + /** + * Maximum length of serialized data in bytes. Longer data will result in an exception. + * @note This value is determined by the size of the underlying database field, + * currently BLOB in MySQL/MariaDB. + */ + public const MAX_DATA_LENGTH = 65535; + + /** @var array[] Cache for `self::getJoin()` */ + private $joinCache = []; + + /** @var Language Language to use for comment truncation */ + private $lang; + + /** + * @param Language $lang Language to use for comment truncation. Defaults + * to content language. + */ + public function __construct( Language $lang ) { + $this->lang = $lang; + } + + /** + * Get SELECT fields for the comment key + * + * Each resulting row should be passed to `self::getCommentLegacy()` to get the + * actual comment. + * + * @note Use of this method may require a subsequent database query to + * actually fetch the comment. If possible, use `self::getJoin()` instead. + * + * @since 1.30 + * @since 1.31 Method signature changed, $key parameter added (required since 1.35) + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @return string[] to include in the `$vars` to `IDatabase->select()`. All + * fields are aliased, so `+` is safe to use. + */ + public function getFields( $key ) { + return [ "{$key}_id" => "{$key}_id" ]; + } + + /** + * Get SELECT fields and joins for the comment key + * + * Each resulting row should be passed to `self::getComment()` to get the + * actual comment. + * + * @since 1.30 + * @since 1.31 Method signature changed, $key parameter added (required since 1.35) + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @return array[] With three keys: + * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables` + * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields` + * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds` + * All tables, fields, and joins are aliased, so `+` is safe to use. + * @phan-return array{tables:string[],fields:string[],joins:array} + */ + public function getJoin( $key ) { + if ( !array_key_exists( $key, $this->joinCache ) ) { + $tables = []; + $fields = []; + $joins = []; + + $alias = "comment_$key"; + $tables[$alias] = 'comment'; + $joins[$alias] = [ 'JOIN', "{$alias}.comment_id = {$key}_id" ]; + + $fields["{$key}_text"] = "{$alias}.comment_text"; + $fields["{$key}_data"] = "{$alias}.comment_data"; + $fields["{$key}_cid"] = "{$alias}.comment_id"; + + $this->joinCache[$key] = [ + 'tables' => $tables, + 'fields' => $fields, + 'joins' => $joins, + ]; + } + + return $this->joinCache[$key]; + } + + /** + * Extract the comment from a row + * + * Shared implementation for getComment() and getCommentLegacy() + * + * @param IDatabase|null $db Database handle for getCommentLegacy(), or null for getComment() + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @param stdClass|array $row + * @param bool $fallback + * @return CommentStoreComment + */ + private function getCommentInternal( ?IDatabase $db, $key, $row, $fallback = false ) { + $row = (array)$row; + if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) { + $cid = $row["{$key}_cid"] ?? null; + $text = $row["{$key}_text"]; + $data = $row["{$key}_data"]; + } else { + $row2 = null; + if ( array_key_exists( "{$key}_id", $row ) ) { + if ( !$db ) { + throw new InvalidArgumentException( + "\$row does not contain fields needed for comment $key and getComment(), but " + . "does have fields for getCommentLegacy()" + ); + } + $id = $row["{$key}_id"]; + $row2 = $db->newSelectQueryBuilder() + ->select( [ 'comment_id', 'comment_text', 'comment_data' ] ) + ->from( 'comment' ) + ->where( [ 'comment_id' => $id ] ) + ->caller( __METHOD__ )->fetchRow(); + } + if ( $row2 === null && $fallback && isset( $row[$key] ) ) { + wfLogWarning( "Using deprecated fallback handling for comment $key" ); + $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ]; + } + if ( $row2 === null ) { + throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" ); + } + + if ( $row2 ) { + $cid = $row2->comment_id; + $text = $row2->comment_text; + $data = $row2->comment_data; + } else { + // @codeCoverageIgnoreStart + // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $id is set when $row2 is okay + wfLogWarning( "Missing comment row for $key, id=$id" ); + $cid = null; + $text = ''; + $data = null; + // @codeCoverageIgnoreEnd + } + } + + $msg = null; + if ( $data !== null ) { + $data = FormatJson::decode( $data, true ); + if ( !is_array( $data ) ) { + // @codeCoverageIgnoreStart + wfLogWarning( "Invalid JSON object in comment: $data" ); + $data = null; + // @codeCoverageIgnoreEnd + } else { + if ( isset( $data['_message'] ) ) { + $msg = self::decodeMessage( $data['_message'] ) + ->setInterfaceMessageFlag( true ); + } + if ( !empty( $data['_null'] ) ) { + $data = null; + } else { + foreach ( $data as $k => $v ) { + if ( substr( $k, 0, 1 ) === '_' ) { + unset( $data[$k] ); + } + } + } + } + } + + return new CommentStoreComment( $cid, $text, $msg, $data ); + } + + /** + * Extract the comment from a row + * + * Use `self::getJoin()` to ensure the row contains the needed data. + * + * If you need to fake a comment in a row for some reason, set fields + * `{$key}_text` (string) and `{$key}_data` (JSON string or null). + * + * @since 1.30 + * @since 1.31 Method signature changed, $key parameter added (required since 1.35) + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @param stdClass|array|null $row Result row. + * @param bool $fallback If true, fall back as well as possible instead of throwing an exception. + * @return CommentStoreComment + */ + public function getComment( $key, $row = null, $fallback = false ) { + if ( $row === null ) { + // @codeCoverageIgnoreStart + throw new InvalidArgumentException( '$row must not be null' ); + // @codeCoverageIgnoreEnd + } + return $this->getCommentInternal( null, $key, $row, $fallback ); + } + + /** + * Extract the comment from a row, with legacy lookups. + * + * If `$row` might have been generated using `self::getFields()` rather + * than `self::getJoin()`, use this. Prefer `self::getComment()` if you + * know callers used `self::getJoin()` for the row fetch. + * + * If you need to fake a comment in a row for some reason, set fields + * `{$key}_text` (string) and `{$key}_data` (JSON string or null). + * + * @since 1.30 + * @since 1.31 Method signature changed, $key parameter added (required since 1.35) + * @param IDatabase $db Database handle to use for lookup + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @param stdClass|array|null $row Result row. + * @param bool $fallback If true, fall back as well as possible instead of throwing an exception. + * @return CommentStoreComment + */ + public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) { + if ( $row === null ) { + // @codeCoverageIgnoreStart + throw new InvalidArgumentException( '$row must not be null' ); + // @codeCoverageIgnoreEnd + } + return $this->getCommentInternal( $db, $key, $row, $fallback ); + } + + /** + * Create a new CommentStoreComment, inserting it into the database if necessary + * + * If a comment is going to be passed to `self::insert()` or the like + * multiple times, it will be more efficient to pass a CommentStoreComment + * once rather than making `self::insert()` do it every time through. + * + * @note When passing a CommentStoreComment, this may set `$comment->id` if + * it's not already set. If `$comment->id` is already set, it will not be + * verified that the specified comment actually exists or that it + * corresponds to the comment text, message, and/or data in the + * CommentStoreComment. + * @param IDatabase $dbw Database handle to insert on. Unused if `$comment` + * is a CommentStoreComment and `$comment->id` is set. + * @param string|Message|CommentStoreComment $comment Comment text or Message object, or + * a CommentStoreComment. + * @param array|null $data Structured data to store. Keys beginning with '_' are reserved. + * Ignored if $comment is a CommentStoreComment. + * @return CommentStoreComment + */ + public function createComment( IDatabase $dbw, $comment, array $data = null ) { + $comment = CommentStoreComment::newUnsavedComment( $comment, $data ); + + # Truncate comment in a Unicode-sensitive manner + $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT ); + + if ( !$comment->id ) { + $dbData = $comment->data; + if ( !$comment->message instanceof RawMessage ) { + $dbData ??= [ '_null' => true ]; + $dbData['_message'] = self::encodeMessage( $comment->message ); + } + if ( $dbData !== null ) { + $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK ); + $len = strlen( $dbData ); + if ( $len > self::MAX_DATA_LENGTH ) { + $max = self::MAX_DATA_LENGTH; + throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" ); + } + } + + $hash = self::hash( $comment->text, $dbData ); + $commentId = $dbw->newSelectQueryBuilder() + ->select( 'comment_id' ) + ->from( 'comment' ) + ->where( [ + 'comment_hash' => $hash, + 'comment_text' => $comment->text, + 'comment_data' => $dbData, + ] ) + ->caller( __METHOD__ )->fetchField(); + if ( !$commentId ) { + $dbw->newInsertQueryBuilder() + ->insert( 'comment' ) + ->row( [ 'comment_hash' => $hash, 'comment_text' => $comment->text, 'comment_data' => $dbData ] ) + ->caller( __METHOD__ )->execute(); + $commentId = $dbw->insertId(); + } + $comment->id = (int)$commentId; + } + + return $comment; + } + + /** + * Insert a comment in preparation for a row that references it + * + * @note It's recommended to include both the call to this method and the + * row insert in the same transaction. + * + * @since 1.30 + * @since 1.31 Method signature changed, $key parameter added (required since 1.35) + * @param IDatabase $dbw Database handle to insert on + * @param string $key A key such as "rev_comment" identifying the comment + * field being fetched. + * @param string|Message|CommentStoreComment|null $comment As for `self::createComment()` + * @param array|null $data As for `self::createComment()` + * @return array Fields for the insert or update + */ + public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) { + if ( $comment === null ) { + // @codeCoverageIgnoreStart + throw new InvalidArgumentException( '$comment can not be null' ); + // @codeCoverageIgnoreEnd + } + + $comment = $this->createComment( $dbw, $comment, $data ); + return [ "{$key}_id" => $comment->id ]; + } + + /** + * Encode a Message as a PHP data structure + * @param Message $msg + * @return array + */ + private static function encodeMessage( Message $msg ) { + $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey(); + $params = $msg->getParams(); + foreach ( $params as &$param ) { + if ( $param instanceof Message ) { + $param = [ + 'message' => self::encodeMessage( $param ) + ]; + } + } + array_unshift( $params, $key ); + return $params; + } + + /** + * Decode a message that was encoded by self::encodeMessage() + * @param array $data + * @return Message + */ + private static function decodeMessage( $data ) { + $key = array_shift( $data ); + foreach ( $data as &$param ) { + if ( is_object( $param ) ) { + $param = (array)$param; + } + if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) { + $param = self::decodeMessage( $param['message'] ); + } + } + return new Message( $key, $data ); + } + + /** + * Hashing function for comment storage + * @param string $text Comment text + * @param string|null $data Comment data + * @return int 32-bit signed integer + */ + public static function hash( $text, $data ) { + $hash = crc32( $text ) ^ crc32( (string)$data ); + + // 64-bit PHP returns an unsigned CRC, change it to signed for + // insertion into the database. + if ( $hash >= 0x80000000 ) { + $hash |= -1 << 32; + } + + return $hash; + } + } /** diff --git a/includes/CommentStore/CommentStoreBase.php b/includes/CommentStore/CommentStoreBase.php deleted file mode 100644 index 2e8e4a1f1922..000000000000 --- a/includes/CommentStore/CommentStoreBase.php +++ /dev/null @@ -1,411 +0,0 @@ -<?php -/** - * This program is free software; you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation; either version 2 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License along - * with this program; if not, write to the Free Software Foundation, Inc., - * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. - * http://www.gnu.org/copyleft/gpl.html - * - * @file - */ - -namespace MediaWiki\CommentStore; - -use FormatJson; -use InvalidArgumentException; -use Language; -use MediaWiki\Language\RawMessage; -use Message; -use OverflowException; -use stdClass; -use Wikimedia\Rdbms\IDatabase; - -/* - * Handle database storage of comments such as edit summaries and log reasons. - * - * @ingroup CommentStore - * @since 1.40 - */ -class CommentStoreBase { - - /** - * Maximum length of a comment in UTF-8 characters. Longer comments will be truncated. - * @note This must be at least 255 and not greater than floor( MAX_DATA_LENGTH / 4 ). - */ - public const COMMENT_CHARACTER_LIMIT = 500; - - /** - * Maximum length of serialized data in bytes. Longer data will result in an exception. - * @note This value is determined by the size of the underlying database field, - * currently BLOB in MySQL/MariaDB. - */ - public const MAX_DATA_LENGTH = 65535; - - /** @var array[] Cache for `self::getJoin()` */ - private $joinCache = []; - - /** @var Language Language to use for comment truncation */ - private $lang; - - /** - * @param Language $lang Language to use for comment truncation. Defaults - * to content language. - */ - public function __construct( Language $lang ) { - $this->lang = $lang; - } - - /** - * Get SELECT fields for the comment key - * - * Each resulting row should be passed to `self::getCommentLegacy()` to get the - * actual comment. - * - * @note Use of this method may require a subsequent database query to - * actually fetch the comment. If possible, use `self::getJoin()` instead. - * - * @since 1.30 - * @since 1.31 Method signature changed, $key parameter added (required since 1.35) - * @param string $key A key such as "rev_comment" identifying the comment - * field being fetched. - * @return string[] to include in the `$vars` to `IDatabase->select()`. All - * fields are aliased, so `+` is safe to use. - */ - public function getFields( $key ) { - return [ "{$key}_id" => "{$key}_id" ]; - } - - /** - * Get SELECT fields and joins for the comment key - * - * Each resulting row should be passed to `self::getComment()` to get the - * actual comment. - * - * @since 1.30 - * @since 1.31 Method signature changed, $key parameter added (required since 1.35) - * @param string $key A key such as "rev_comment" identifying the comment - * field being fetched. - * @return array[] With three keys: - * - tables: (string[]) to include in the `$table` to `IDatabase->select()` or `SelectQueryBuilder::tables` - * - fields: (string[]) to include in the `$vars` to `IDatabase->select()` or `SelectQueryBuilder::fields` - * - joins: (array) to include in the `$join_conds` to `IDatabase->select()` or `SelectQueryBuilder::joinConds` - * All tables, fields, and joins are aliased, so `+` is safe to use. - * @phan-return array{tables:string[],fields:string[],joins:array} - */ - public function getJoin( $key ) { - if ( !array_key_exists( $key, $this->joinCache ) ) { - $tables = []; - $fields = []; - $joins = []; - - $alias = "comment_$key"; - $tables[$alias] = 'comment'; - $joins[$alias] = [ 'JOIN', "{$alias}.comment_id = {$key}_id" ]; - - $fields["{$key}_text"] = "{$alias}.comment_text"; - $fields["{$key}_data"] = "{$alias}.comment_data"; - $fields["{$key}_cid"] = "{$alias}.comment_id"; - - $this->joinCache[$key] = [ - 'tables' => $tables, - 'fields' => $fields, - 'joins' => $joins, - ]; - } - - return $this->joinCache[$key]; - } - - /** - * Extract the comment from a row - * - * Shared implementation for getComment() and getCommentLegacy() - * - * @param IDatabase|null $db Database handle for getCommentLegacy(), or null for getComment() - * @param string $key A key such as "rev_comment" identifying the comment - * field being fetched. - * @param stdClass|array $row - * @param bool $fallback - * @return CommentStoreComment - */ - private function getCommentInternal( ?IDatabase $db, $key, $row, $fallback = false ) { - $row = (array)$row; - if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) { - $cid = $row["{$key}_cid"] ?? null; - $text = $row["{$key}_text"]; - $data = $row["{$key}_data"]; - } else { - $row2 = null; - if ( array_key_exists( "{$key}_id", $row ) ) { - if ( !$db ) { - throw new InvalidArgumentException( - "\$row does not contain fields needed for comment $key and getComment(), but " - . "does have fields for getCommentLegacy()" - ); - } - $id = $row["{$key}_id"]; - $row2 = $db->newSelectQueryBuilder() - ->select( [ 'comment_id', 'comment_text', 'comment_data' ] ) - ->from( 'comment' ) - ->where( [ 'comment_id' => $id ] ) - ->caller( __METHOD__ )->fetchRow(); - } - if ( $row2 === null && $fallback && isset( $row[$key] ) ) { - wfLogWarning( "Using deprecated fallback handling for comment $key" ); - $row2 = (object)[ 'comment_text' => $row[$key], 'comment_data' => null ]; - } - if ( $row2 === null ) { - throw new InvalidArgumentException( "\$row does not contain fields needed for comment $key" ); - } - - if ( $row2 ) { - $cid = $row2->comment_id; - $text = $row2->comment_text; - $data = $row2->comment_data; - } else { - // @codeCoverageIgnoreStart - // @phan-suppress-next-line PhanPossiblyUndeclaredVariable $id is set when $row2 is okay - wfLogWarning( "Missing comment row for $key, id=$id" ); - $cid = null; - $text = ''; - $data = null; - // @codeCoverageIgnoreEnd - } - } - - $msg = null; - if ( $data !== null ) { - $data = FormatJson::decode( $data, true ); - if ( !is_array( $data ) ) { - // @codeCoverageIgnoreStart - wfLogWarning( "Invalid JSON object in comment: $data" ); - $data = null; - // @codeCoverageIgnoreEnd - } else { - if ( isset( $data['_message'] ) ) { - $msg = self::decodeMessage( $data['_message'] ) - ->setInterfaceMessageFlag( true ); - } - if ( !empty( $data['_null'] ) ) { - $data = null; - } else { - foreach ( $data as $k => $v ) { - if ( substr( $k, 0, 1 ) === '_' ) { - unset( $data[$k] ); - } - } - } - } - } - - return new CommentStoreComment( $cid, $text, $msg, $data ); - } - - /** - * Extract the comment from a row - * - * Use `self::getJoin()` to ensure the row contains the needed data. - * - * If you need to fake a comment in a row for some reason, set fields - * `{$key}_text` (string) and `{$key}_data` (JSON string or null). - * - * @since 1.30 - * @since 1.31 Method signature changed, $key parameter added (required since 1.35) - * @param string $key A key such as "rev_comment" identifying the comment - * field being fetched. - * @param stdClass|array|null $row Result row. - * @param bool $fallback If true, fall back as well as possible instead of throwing an exception. - * @return CommentStoreComment - */ - public function getComment( $key, $row = null, $fallback = false ) { - if ( $row === null ) { - // @codeCoverageIgnoreStart - throw new InvalidArgumentException( '$row must not be null' ); - // @codeCoverageIgnoreEnd - } - return $this->getCommentInternal( null, $key, $row, $fallback ); - } - - /** - * Extract the comment from a row, with legacy lookups. - * - * If `$row` might have been generated using `self::getFields()` rather - * than `self::getJoin()`, use this. Prefer `self::getComment()` if you - * know callers used `self::getJoin()` for the row fetch. - * - * If you need to fake a comment in a row for some reason, set fields - * `{$key}_text` (string) and `{$key}_data` (JSON string or null). - * - * @since 1.30 - * @since 1.31 Method signature changed, $key parameter added (required since 1.35) - * @param IDatabase $db Database handle to use for lookup - * @param string $key A key such as "rev_comment" identifying the comment - * field being fetched. - * @param stdClass|array|null $row Result row. - * @param bool $fallback If true, fall back as well as possible instead of throwing an exception. - * @return CommentStoreComment - */ - public function getCommentLegacy( IDatabase $db, $key, $row = null, $fallback = false ) { - if ( $row === null ) { - // @codeCoverageIgnoreStart - throw new InvalidArgumentException( '$row must not be null' ); - // @codeCoverageIgnoreEnd - } - return $this->getCommentInternal( $db, $key, $row, $fallback ); - } - - /** - * Create a new CommentStoreComment, inserting it into the database if necessary - * - * If a comment is going to be passed to `self::insert()` or the like - * multiple times, it will be more efficient to pass a CommentStoreComment - * once rather than making `self::insert()` do it every time through. - * - * @note When passing a CommentStoreComment, this may set `$comment->id` if - * it's not already set. If `$comment->id` is already set, it will not be - * verified that the specified comment actually exists or that it - * corresponds to the comment text, message, and/or data in the - * CommentStoreComment. - * @param IDatabase $dbw Database handle to insert on. Unused if `$comment` - * is a CommentStoreComment and `$comment->id` is set. - * @param string|Message|CommentStoreComment $comment Comment text or Message object, or - * a CommentStoreComment. - * @param array|null $data Structured data to store. Keys beginning with '_' are reserved. - * Ignored if $comment is a CommentStoreComment. - * @return CommentStoreComment - */ - public function createComment( IDatabase $dbw, $comment, array $data = null ) { - $comment = CommentStoreComment::newUnsavedComment( $comment, $data ); - - # Truncate comment in a Unicode-sensitive manner - $comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT ); - - if ( !$comment->id ) { - $dbData = $comment->data; - if ( !$comment->message instanceof RawMessage ) { - $dbData ??= [ '_null' => true ]; - $dbData['_message'] = self::encodeMessage( $comment->message ); - } - if ( $dbData !== null ) { - $dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK ); - $len = strlen( $dbData ); - if ( $len > self::MAX_DATA_LENGTH ) { - $max = self::MAX_DATA_LENGTH; - throw new OverflowException( "Comment data is too long ($len bytes, maximum is $max)" ); - } - } - - $hash = self::hash( $comment->text, $dbData ); - $commentId = $dbw->newSelectQueryBuilder() - ->select( 'comment_id' ) - ->from( 'comment' ) - ->where( [ - 'comment_hash' => $hash, - 'comment_text' => $comment->text, - 'comment_data' => $dbData, - ] ) - ->caller( __METHOD__ )->fetchField(); - if ( !$commentId ) { - $dbw->newInsertQueryBuilder() - ->insert( 'comment' ) - ->row( [ 'comment_hash' => $hash, 'comment_text' => $comment->text, 'comment_data' => $dbData ] ) - ->caller( __METHOD__ )->execute(); - $commentId = $dbw->insertId(); - } - $comment->id = (int)$commentId; - } - - return $comment; - } - - /** - * Insert a comment in preparation for a row that references it - * - * @note It's recommended to include both the call to this method and the - * row insert in the same transaction. - * - * @since 1.30 - * @since 1.31 Method signature changed, $key parameter added (required since 1.35) - * @param IDatabase $dbw Database handle to insert on - * @param string $key A key such as "rev_comment" identifying the comment - * field being fetched. - * @param string|Message|CommentStoreComment|null $comment As for `self::createComment()` - * @param array|null $data As for `self::createComment()` - * @return array Fields for the insert or update - */ - public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) { - if ( $comment === null ) { - // @codeCoverageIgnoreStart - throw new InvalidArgumentException( '$comment can not be null' ); - // @codeCoverageIgnoreEnd - } - - $comment = $this->createComment( $dbw, $comment, $data ); - return [ "{$key}_id" => $comment->id ]; - } - - /** - * Encode a Message as a PHP data structure - * @param Message $msg - * @return array - */ - private static function encodeMessage( Message $msg ) { - $key = count( $msg->getKeysToTry() ) > 1 ? $msg->getKeysToTry() : $msg->getKey(); - $params = $msg->getParams(); - foreach ( $params as &$param ) { - if ( $param instanceof Message ) { - $param = [ - 'message' => self::encodeMessage( $param ) - ]; - } - } - array_unshift( $params, $key ); - return $params; - } - - /** - * Decode a message that was encoded by self::encodeMessage() - * @param array $data - * @return Message - */ - private static function decodeMessage( $data ) { - $key = array_shift( $data ); - foreach ( $data as &$param ) { - if ( is_object( $param ) ) { - $param = (array)$param; - } - if ( is_array( $param ) && count( $param ) === 1 && isset( $param['message'] ) ) { - $param = self::decodeMessage( $param['message'] ); - } - } - return new Message( $key, $data ); - } - - /** - * Hashing function for comment storage - * @param string $text Comment text - * @param string|null $data Comment data - * @return int 32-bit signed integer - */ - public static function hash( $text, $data ) { - $hash = crc32( $text ) ^ crc32( (string)$data ); - - // 64-bit PHP returns an unsigned CRC, change it to signed for - // insertion into the database. - if ( $hash >= 0x80000000 ) { - $hash |= -1 << 32; - } - - return $hash; - } - -} |