2017-06-06 17:39:14 +00:00
|
|
|
<?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
|
|
|
|
|
*/
|
|
|
|
|
|
2018-01-24 23:41:01 +00:00
|
|
|
use MediaWiki\MediaWikiServices;
|
2017-06-06 17:39:14 +00:00
|
|
|
use Wikimedia\Rdbms\IDatabase;
|
|
|
|
|
|
|
|
|
|
/**
|
2020-07-24 18:46:44 +00:00
|
|
|
* @defgroup CommentStore CommentStore
|
|
|
|
|
*
|
|
|
|
|
* The Comment store in MediaWiki is responsible for storing edit summaries,
|
|
|
|
|
* log action comments and other such short strings (referred to as "comments").
|
|
|
|
|
*
|
|
|
|
|
* The CommentStore class handles the database abstraction for reading
|
|
|
|
|
* and writing comments, which are represented by CommentStoreComment objects.
|
|
|
|
|
*
|
2021-08-23 08:56:24 +00:00
|
|
|
* Data is internally stored in the `comment` table.
|
2020-07-24 18:46:44 +00:00
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle database storage of comments such as edit summaries and log reasons.
|
|
|
|
|
*
|
|
|
|
|
* @ingroup CommentStore
|
2017-06-06 17:39:14 +00:00
|
|
|
* @since 1.30
|
|
|
|
|
*/
|
|
|
|
|
class CommentStore {
|
|
|
|
|
|
2017-10-10 15:52:13 +00:00
|
|
|
/**
|
|
|
|
|
* Maximum length of a comment in UTF-8 characters. Longer comments will be truncated.
|
2020-05-11 00:48:27 +00:00
|
|
|
* @note This must be at least 255 and not greater than floor( MAX_DATA_LENGTH / 4 ).
|
2017-10-10 15:52:13 +00:00
|
|
|
*/
|
2020-05-11 00:48:27 +00:00
|
|
|
public const COMMENT_CHARACTER_LIMIT = 500;
|
2017-09-01 18:30:53 +00:00
|
|
|
|
2017-10-10 15:52:13 +00:00
|
|
|
/**
|
|
|
|
|
* 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.
|
|
|
|
|
*/
|
2020-05-11 00:48:27 +00:00
|
|
|
public const MAX_DATA_LENGTH = 65535;
|
2017-09-01 18:30:53 +00:00
|
|
|
|
2017-06-06 17:39:14 +00:00
|
|
|
/**
|
|
|
|
|
* Define fields that use temporary tables for transitional purposes
|
2021-02-10 19:40:48 +00:00
|
|
|
* Array keys are field names, values are arrays with these possible fields:
|
2017-06-06 17:39:14 +00:00
|
|
|
* - table: Temporary table name
|
|
|
|
|
* - pk: Temporary table column referring to the main table's primary key
|
|
|
|
|
* - field: Temporary table column referring comment.comment_id
|
|
|
|
|
* - joinPK: Main table's primary key
|
2018-03-07 16:40:56 +00:00
|
|
|
* - stage: Migration stage
|
|
|
|
|
* - deprecatedIn: Version when using insertWithTempTable() was deprecated
|
2017-06-06 17:39:14 +00:00
|
|
|
*/
|
2021-02-10 19:40:48 +00:00
|
|
|
protected const TEMP_TABLES = [
|
2017-06-06 17:39:14 +00:00
|
|
|
'rev_comment' => [
|
|
|
|
|
'table' => 'revision_comment_temp',
|
|
|
|
|
'pk' => 'revcomment_rev',
|
|
|
|
|
'field' => 'revcomment_comment_id',
|
|
|
|
|
'joinPK' => 'rev_id',
|
2018-03-07 16:40:56 +00:00
|
|
|
'stage' => MIGRATION_OLD,
|
|
|
|
|
'deprecatedIn' => null,
|
2017-06-06 17:39:14 +00:00
|
|
|
],
|
|
|
|
|
'img_description' => [
|
2018-03-07 20:25:53 +00:00
|
|
|
'stage' => MIGRATION_NEW,
|
2018-03-07 16:40:56 +00:00
|
|
|
'deprecatedIn' => '1.32',
|
2017-06-06 17:39:14 +00:00
|
|
|
],
|
|
|
|
|
];
|
|
|
|
|
|
2019-01-04 18:55:11 +00:00
|
|
|
/**
|
2019-09-20 18:35:39 +00:00
|
|
|
* @var int One of the MIGRATION_* constants, or an appropriate combination
|
|
|
|
|
* of SCHEMA_COMPAT_* constants.
|
2019-01-04 18:55:11 +00:00
|
|
|
* @todo Deprecate and remove once extensions seem unlikely to need to use
|
|
|
|
|
* it for migration anymore.
|
|
|
|
|
*/
|
2020-03-24 08:52:35 +00:00
|
|
|
private $stage;
|
2017-06-06 17:39:14 +00:00
|
|
|
|
2018-01-24 23:41:01 +00:00
|
|
|
/** @var array[] Cache for `self::getJoin()` */
|
2020-03-24 08:52:35 +00:00
|
|
|
private $joinCache = [];
|
2017-06-06 17:39:14 +00:00
|
|
|
|
2017-09-01 18:30:53 +00:00
|
|
|
/** @var Language Language to use for comment truncation */
|
2020-03-24 08:52:35 +00:00
|
|
|
private $lang;
|
2017-09-01 18:30:53 +00:00
|
|
|
|
2017-06-06 17:39:14 +00:00
|
|
|
/**
|
2017-09-01 18:30:53 +00:00
|
|
|
* @param Language $lang Language to use for comment truncation. Defaults
|
2018-07-29 12:24:54 +00:00
|
|
|
* to content language.
|
2019-09-20 18:35:39 +00:00
|
|
|
* @param int $stage One of the MIGRATION_* constants, or an appropriate
|
|
|
|
|
* combination of SCHEMA_COMPAT_* constants. Always MIGRATION_NEW for
|
|
|
|
|
* MediaWiki core since 1.33.
|
2017-06-06 17:39:14 +00:00
|
|
|
*/
|
2019-09-20 18:35:39 +00:00
|
|
|
public function __construct( Language $lang, $stage ) {
|
|
|
|
|
if ( ( $stage & SCHEMA_COMPAT_WRITE_BOTH ) === 0 ) {
|
|
|
|
|
throw new InvalidArgumentException( '$stage must include a write mode' );
|
|
|
|
|
}
|
|
|
|
|
if ( ( $stage & SCHEMA_COMPAT_READ_BOTH ) === 0 ) {
|
|
|
|
|
throw new InvalidArgumentException( '$stage must include a read mode' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$this->stage = $stage;
|
2018-01-24 23:41:01 +00:00
|
|
|
$this->lang = $lang;
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
2018-01-24 23:41:01 +00:00
|
|
|
/**
|
|
|
|
|
* @since 1.31
|
|
|
|
|
* @deprecated in 1.31 Use DI to inject a CommentStore instance into your class.
|
|
|
|
|
* @return CommentStore
|
|
|
|
|
*/
|
|
|
|
|
public static function getStore() {
|
|
|
|
|
return MediaWikiServices::getInstance()->getCommentStore();
|
|
|
|
|
}
|
|
|
|
|
|
2017-06-06 17:39:14 +00:00
|
|
|
/**
|
|
|
|
|
* 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.
|
2018-01-24 23:41:01 +00:00
|
|
|
*
|
|
|
|
|
* @since 1.30
|
2020-02-13 05:29:57 +00:00
|
|
|
* @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
|
2018-01-24 23:41:01 +00:00
|
|
|
* field being fetched.
|
2017-06-06 17:39:14 +00:00
|
|
|
* @return string[] to include in the `$vars` to `IDatabase->select()`. All
|
|
|
|
|
* fields are aliased, so `+` is safe to use.
|
|
|
|
|
*/
|
2020-02-13 05:29:57 +00:00
|
|
|
public function getFields( $key ) {
|
2017-06-06 17:39:14 +00:00
|
|
|
$fields = [];
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
|
2018-01-24 23:41:01 +00:00
|
|
|
$fields["{$key}_text"] = $key;
|
|
|
|
|
$fields["{$key}_data"] = 'NULL';
|
|
|
|
|
$fields["{$key}_cid"] = 'NULL';
|
2019-09-20 18:35:39 +00:00
|
|
|
} else { // READ_BOTH or READ_NEW
|
|
|
|
|
if ( $this->stage & SCHEMA_COMPAT_READ_OLD ) {
|
2018-01-24 23:41:01 +00:00
|
|
|
$fields["{$key}_old"] = $key;
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
2018-03-07 16:40:56 +00:00
|
|
|
|
2021-02-10 19:40:48 +00:00
|
|
|
$tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
|
2021-02-10 19:40:48 +00:00
|
|
|
$fields["{$key}_pk"] = static::TEMP_TABLES[$key]['joinPK'];
|
2018-03-07 16:40:56 +00:00
|
|
|
}
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) {
|
2018-01-24 23:41:01 +00:00
|
|
|
$fields["{$key}_id"] = "{$key}_id";
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return $fields;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get SELECT fields and joins for the comment key
|
|
|
|
|
*
|
|
|
|
|
* Each resulting row should be passed to `self::getComment()` to get the
|
|
|
|
|
* actual comment.
|
|
|
|
|
*
|
2018-01-24 23:41:01 +00:00
|
|
|
* @since 1.30
|
2020-02-13 05:29:57 +00:00
|
|
|
* @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
|
2018-01-24 23:41:01 +00:00
|
|
|
* field being fetched.
|
2019-07-03 13:45:01 +00:00
|
|
|
* @return array[] With three keys:
|
2017-06-06 17:39:14 +00:00
|
|
|
* - tables: (string[]) to include in the `$table` to `IDatabase->select()`
|
|
|
|
|
* - fields: (string[]) to include in the `$vars` to `IDatabase->select()`
|
|
|
|
|
* - joins: (array) to include in the `$join_conds` to `IDatabase->select()`
|
|
|
|
|
* All tables, fields, and joins are aliased, so `+` is safe to use.
|
2019-04-06 06:31:16 +00:00
|
|
|
* @phan-return array{tables:string[],fields:string[],joins:array}
|
2017-06-06 17:39:14 +00:00
|
|
|
*/
|
2020-02-13 05:29:57 +00:00
|
|
|
public function getJoin( $key ) {
|
2018-01-24 23:41:01 +00:00
|
|
|
if ( !array_key_exists( $key, $this->joinCache ) ) {
|
2017-06-06 17:39:14 +00:00
|
|
|
$tables = [];
|
|
|
|
|
$fields = [];
|
|
|
|
|
$joins = [];
|
|
|
|
|
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
|
2018-01-24 23:41:01 +00:00
|
|
|
$fields["{$key}_text"] = $key;
|
|
|
|
|
$fields["{$key}_data"] = 'NULL';
|
|
|
|
|
$fields["{$key}_cid"] = 'NULL';
|
2019-09-20 18:35:39 +00:00
|
|
|
} else { // READ_BOTH or READ_NEW
|
|
|
|
|
$join = ( $this->stage & SCHEMA_COMPAT_READ_OLD ) ? 'LEFT JOIN' : 'JOIN';
|
2017-06-06 17:39:14 +00:00
|
|
|
|
2021-02-10 19:40:48 +00:00
|
|
|
$tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) {
|
2021-02-10 19:40:48 +00:00
|
|
|
$t = static::TEMP_TABLES[$key];
|
2018-01-24 23:41:01 +00:00
|
|
|
$alias = "temp_$key";
|
2017-06-06 17:39:14 +00:00
|
|
|
$tables[$alias] = $t['table'];
|
|
|
|
|
$joins[$alias] = [ $join, "{$alias}.{$t['pk']} = {$t['joinPK']}" ];
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( ( $tempTableStage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
|
2018-03-07 16:40:56 +00:00
|
|
|
$joinField = "{$alias}.{$t['field']}";
|
|
|
|
|
} else {
|
2018-03-07 20:25:53 +00:00
|
|
|
// Nothing hits this code path for now, but will in the future when we set
|
2021-02-10 19:40:48 +00:00
|
|
|
// static::TEMP_TABLES['rev_comment']['stage'] to MIGRATION_WRITE_NEW while
|
2018-03-07 20:25:53 +00:00
|
|
|
// merging revision_comment_temp into revision.
|
|
|
|
|
// @codeCoverageIgnoreStart
|
2018-03-07 16:40:56 +00:00
|
|
|
$joins[$alias][0] = 'LEFT JOIN';
|
|
|
|
|
$joinField = "(CASE WHEN {$key}_id != 0 THEN {$key}_id ELSE {$alias}.{$t['field']} END)";
|
2018-03-07 20:25:53 +00:00
|
|
|
throw new LogicException( 'Nothing should reach this code path at this time' );
|
|
|
|
|
// @codeCoverageIgnoreEnd
|
2018-03-07 16:40:56 +00:00
|
|
|
}
|
2017-06-06 17:39:14 +00:00
|
|
|
} else {
|
2018-01-24 23:41:01 +00:00
|
|
|
$joinField = "{$key}_id";
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
2018-01-24 23:41:01 +00:00
|
|
|
$alias = "comment_$key";
|
2017-06-06 17:39:14 +00:00
|
|
|
$tables[$alias] = 'comment';
|
|
|
|
|
$joins[$alias] = [ $join, "{$alias}.comment_id = {$joinField}" ];
|
|
|
|
|
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_NEW ) {
|
2018-01-24 23:41:01 +00:00
|
|
|
$fields["{$key}_text"] = "{$alias}.comment_text";
|
2017-06-06 17:39:14 +00:00
|
|
|
} else {
|
2018-01-24 23:41:01 +00:00
|
|
|
$fields["{$key}_text"] = "COALESCE( {$alias}.comment_text, $key )";
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
2018-01-24 23:41:01 +00:00
|
|
|
$fields["{$key}_data"] = "{$alias}.comment_data";
|
|
|
|
|
$fields["{$key}_cid"] = "{$alias}.comment_id";
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
2018-01-24 23:41:01 +00:00
|
|
|
$this->joinCache[$key] = [
|
2017-06-06 17:39:14 +00:00
|
|
|
'tables' => $tables,
|
|
|
|
|
'fields' => $fields,
|
|
|
|
|
'joins' => $joins,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-24 23:41:01 +00:00
|
|
|
return $this->joinCache[$key];
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Extract the comment from a row
|
|
|
|
|
*
|
|
|
|
|
* Shared implementation for getComment() and getCommentLegacy()
|
|
|
|
|
*
|
|
|
|
|
* @param IDatabase|null $db Database handle for getCommentLegacy(), or null for getComment()
|
2018-01-24 23:41:01 +00:00
|
|
|
* @param string $key A key such as "rev_comment" identifying the comment
|
|
|
|
|
* field being fetched.
|
2020-11-12 22:22:22 +00:00
|
|
|
* @param stdClass|array $row
|
2017-06-06 17:39:14 +00:00
|
|
|
* @param bool $fallback
|
|
|
|
|
* @return CommentStoreComment
|
|
|
|
|
*/
|
2019-10-06 17:01:52 +00:00
|
|
|
private function getCommentInternal( ?IDatabase $db, $key, $row, $fallback = false ) {
|
2017-06-06 17:39:14 +00:00
|
|
|
$row = (array)$row;
|
|
|
|
|
if ( array_key_exists( "{$key}_text", $row ) && array_key_exists( "{$key}_data", $row ) ) {
|
2017-10-06 22:17:58 +00:00
|
|
|
$cid = $row["{$key}_cid"] ?? null;
|
2017-06-06 17:39:14 +00:00
|
|
|
$text = $row["{$key}_text"];
|
|
|
|
|
$data = $row["{$key}_data"];
|
2019-09-20 18:35:39 +00:00
|
|
|
} elseif ( ( $this->stage & SCHEMA_COMPAT_READ_BOTH ) === SCHEMA_COMPAT_READ_OLD ) {
|
2017-06-06 17:39:14 +00:00
|
|
|
$cid = null;
|
|
|
|
|
if ( $fallback && isset( $row[$key] ) ) {
|
|
|
|
|
wfLogWarning( "Using deprecated fallback handling for comment $key" );
|
|
|
|
|
$text = $row[$key];
|
|
|
|
|
} else {
|
2019-09-20 18:35:39 +00:00
|
|
|
wfLogWarning(
|
|
|
|
|
"Missing {$key}_text and {$key}_data fields in row with MIGRATION_OLD / READ_OLD"
|
|
|
|
|
);
|
2017-06-06 17:39:14 +00:00
|
|
|
$text = '';
|
|
|
|
|
}
|
|
|
|
|
$data = null;
|
|
|
|
|
} else {
|
2021-02-10 19:40:48 +00:00
|
|
|
$tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
|
2018-03-07 16:40:56 +00:00
|
|
|
$row2 = null;
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( ( $tempTableStage & SCHEMA_COMPAT_READ_NEW ) && array_key_exists( "{$key}_id", $row ) ) {
|
2018-03-07 16:40:56 +00:00
|
|
|
if ( !$db ) {
|
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
"\$row does not contain fields needed for comment $key and getComment(), but "
|
|
|
|
|
. "does have fields for getCommentLegacy()"
|
2017-06-06 17:39:14 +00:00
|
|
|
);
|
|
|
|
|
}
|
2018-03-07 16:40:56 +00:00
|
|
|
$id = $row["{$key}_id"];
|
|
|
|
|
$row2 = $db->selectRow(
|
|
|
|
|
'comment',
|
|
|
|
|
[ 'comment_id', 'comment_text', 'comment_data' ],
|
|
|
|
|
[ 'comment_id' => $id ],
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
|
|
|
|
}
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( !$row2 && ( $tempTableStage & SCHEMA_COMPAT_READ_OLD ) &&
|
|
|
|
|
array_key_exists( "{$key}_pk", $row )
|
|
|
|
|
) {
|
2018-03-07 16:40:56 +00:00
|
|
|
if ( !$db ) {
|
|
|
|
|
throw new InvalidArgumentException(
|
|
|
|
|
"\$row does not contain fields needed for comment $key and getComment(), but "
|
|
|
|
|
. "does have fields for getCommentLegacy()"
|
2017-06-06 17:39:14 +00:00
|
|
|
);
|
|
|
|
|
}
|
2021-02-10 19:40:48 +00:00
|
|
|
$t = static::TEMP_TABLES[$key];
|
2018-03-07 16:40:56 +00:00
|
|
|
$id = $row["{$key}_pk"];
|
|
|
|
|
$row2 = $db->selectRow(
|
|
|
|
|
[ $t['table'], 'comment' ],
|
|
|
|
|
[ 'comment_id', 'comment_text', 'comment_data' ],
|
|
|
|
|
[ $t['pk'] => $id ],
|
|
|
|
|
__METHOD__,
|
|
|
|
|
[],
|
|
|
|
|
[ 'comment' => [ 'JOIN', [ "comment_id = {$t['field']}" ] ] ]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
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" );
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( $row2 ) {
|
|
|
|
|
$cid = $row2->comment_id;
|
|
|
|
|
$text = $row2->comment_text;
|
|
|
|
|
$data = $row2->comment_data;
|
2019-09-20 18:35:39 +00:00
|
|
|
} elseif ( ( $this->stage & SCHEMA_COMPAT_READ_OLD ) &&
|
|
|
|
|
array_key_exists( "{$key}_old", $row )
|
|
|
|
|
) {
|
2017-06-06 17:39:14 +00:00
|
|
|
$cid = null;
|
|
|
|
|
$text = $row["{$key}_old"];
|
|
|
|
|
$data = null;
|
|
|
|
|
} else {
|
|
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
|
wfLogWarning( "Missing comment row for $key, id=$id" );
|
|
|
|
|
$cid = null;
|
|
|
|
|
$text = '';
|
|
|
|
|
$data = null;
|
|
|
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$msg = null;
|
|
|
|
|
if ( $data !== null ) {
|
2018-11-07 19:04:21 +00:00
|
|
|
$data = FormatJson::decode( $data, true );
|
|
|
|
|
if ( !is_array( $data ) ) {
|
2017-06-06 17:39:14 +00:00
|
|
|
// @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).
|
|
|
|
|
*
|
2018-01-24 23:41:01 +00:00
|
|
|
* @since 1.30
|
2020-02-13 05:29:57 +00:00
|
|
|
* @since 1.31 Method signature changed, $key parameter added (required since 1.35)
|
2018-01-24 23:41:01 +00:00
|
|
|
* @param string $key A key such as "rev_comment" identifying the comment
|
|
|
|
|
* field being fetched.
|
2020-11-12 22:22:22 +00:00
|
|
|
* @param stdClass|array|null $row Result row.
|
2017-06-06 17:39:14 +00:00
|
|
|
* @param bool $fallback If true, fall back as well as possible instead of throwing an exception.
|
|
|
|
|
* @return CommentStoreComment
|
|
|
|
|
*/
|
2018-01-24 23:41:01 +00:00
|
|
|
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 );
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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).
|
|
|
|
|
*
|
2018-01-24 23:41:01 +00:00
|
|
|
* @since 1.30
|
2020-02-13 05:29:57 +00:00
|
|
|
* @since 1.31 Method signature changed, $key parameter added (required since 1.35)
|
2017-06-06 17:39:14 +00:00
|
|
|
* @param IDatabase $db Database handle to use for lookup
|
2018-01-24 23:41:01 +00:00
|
|
|
* @param string $key A key such as "rev_comment" identifying the comment
|
|
|
|
|
* field being fetched.
|
2020-11-12 22:22:22 +00:00
|
|
|
* @param stdClass|array|null $row Result row.
|
2017-06-06 17:39:14 +00:00
|
|
|
* @param bool $fallback If true, fall back as well as possible instead of throwing an exception.
|
|
|
|
|
* @return CommentStoreComment
|
|
|
|
|
*/
|
2018-01-24 23:41:01 +00:00
|
|
|
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 );
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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 ) {
|
2017-09-11 18:27:01 +00:00
|
|
|
$comment = CommentStoreComment::newUnsavedComment( $comment, $data );
|
2017-06-06 17:39:14 +00:00
|
|
|
|
2017-09-01 18:30:53 +00:00
|
|
|
# Truncate comment in a Unicode-sensitive manner
|
2018-06-13 17:49:29 +00:00
|
|
|
$comment->text = $this->lang->truncateForVisual( $comment->text, self::COMMENT_CHARACTER_LIMIT );
|
2017-09-01 18:30:53 +00:00
|
|
|
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) && !$comment->id ) {
|
2017-06-06 17:39:14 +00:00
|
|
|
$dbData = $comment->data;
|
|
|
|
|
if ( !$comment->message instanceof RawMessage ) {
|
|
|
|
|
if ( $dbData === null ) {
|
|
|
|
|
$dbData = [ '_null' => true ];
|
|
|
|
|
}
|
|
|
|
|
$dbData['_message'] = self::encodeMessage( $comment->message );
|
|
|
|
|
}
|
|
|
|
|
if ( $dbData !== null ) {
|
|
|
|
|
$dbData = FormatJson::encode( (object)$dbData, false, FormatJson::ALL_OK );
|
2017-09-01 18:30:53 +00:00
|
|
|
$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)" );
|
|
|
|
|
}
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$hash = self::hash( $comment->text, $dbData );
|
2021-08-05 23:15:03 +00:00
|
|
|
$commentId = $dbw->selectField(
|
2017-06-06 17:39:14 +00:00
|
|
|
'comment',
|
|
|
|
|
'comment_id',
|
|
|
|
|
[
|
|
|
|
|
'comment_hash' => $hash,
|
|
|
|
|
'comment_text' => $comment->text,
|
|
|
|
|
'comment_data' => $dbData,
|
|
|
|
|
],
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
2021-08-05 23:15:03 +00:00
|
|
|
if ( !$commentId ) {
|
2017-06-06 17:39:14 +00:00
|
|
|
$dbw->insert(
|
|
|
|
|
'comment',
|
|
|
|
|
[
|
|
|
|
|
'comment_hash' => $hash,
|
|
|
|
|
'comment_text' => $comment->text,
|
|
|
|
|
'comment_data' => $dbData,
|
|
|
|
|
],
|
|
|
|
|
__METHOD__
|
|
|
|
|
);
|
2021-08-05 23:15:03 +00:00
|
|
|
$commentId = $dbw->insertId();
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
2021-08-05 23:15:03 +00:00
|
|
|
$comment->id = (int)$commentId;
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $comment;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Implementation for `self::insert()` and `self::insertWithTempTable()`
|
|
|
|
|
* @param IDatabase $dbw
|
2018-01-24 23:41:01 +00:00
|
|
|
* @param string $key A key such as "rev_comment" identifying the comment
|
|
|
|
|
* field being fetched.
|
2017-06-06 17:39:14 +00:00
|
|
|
* @param string|Message|CommentStoreComment $comment
|
|
|
|
|
* @param array|null $data
|
|
|
|
|
* @return array [ array $fields, callable $callback ]
|
|
|
|
|
*/
|
2018-01-24 23:41:01 +00:00
|
|
|
private function insertInternal( IDatabase $dbw, $key, $comment, $data ) {
|
2017-06-06 17:39:14 +00:00
|
|
|
$fields = [];
|
|
|
|
|
$callback = null;
|
|
|
|
|
|
|
|
|
|
$comment = $this->createComment( $dbw, $comment, $data );
|
|
|
|
|
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( $this->stage & SCHEMA_COMPAT_WRITE_OLD ) {
|
2018-06-13 17:49:29 +00:00
|
|
|
$fields[$key] = $this->lang->truncateForDatabase( $comment->text, 255 );
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( $this->stage & SCHEMA_COMPAT_WRITE_NEW ) {
|
2021-02-10 19:40:48 +00:00
|
|
|
$tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
|
2021-02-10 19:40:48 +00:00
|
|
|
$t = static::TEMP_TABLES[$key];
|
2017-06-06 17:39:14 +00:00
|
|
|
$func = __METHOD__;
|
|
|
|
|
$commentId = $comment->id;
|
2021-02-10 22:31:02 +00:00
|
|
|
$callback = static function ( $id ) use ( $dbw, $commentId, $t, $func ) {
|
2017-06-06 17:39:14 +00:00
|
|
|
$dbw->insert(
|
|
|
|
|
$t['table'],
|
|
|
|
|
[
|
|
|
|
|
$t['pk'] => $id,
|
|
|
|
|
$t['field'] => $commentId,
|
|
|
|
|
],
|
|
|
|
|
$func
|
|
|
|
|
);
|
|
|
|
|
};
|
2018-03-07 16:40:56 +00:00
|
|
|
}
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( $tempTableStage & SCHEMA_COMPAT_WRITE_NEW ) {
|
2018-01-24 23:41:01 +00:00
|
|
|
$fields["{$key}_id"] = $comment->id;
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return [ $fields, $callback ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2017-09-05 19:02:33 +00:00
|
|
|
* Insert a comment in preparation for a row that references it
|
2017-06-06 17:39:14 +00:00
|
|
|
*
|
|
|
|
|
* @note It's recommended to include both the call to this method and the
|
|
|
|
|
* row insert in the same transaction.
|
2018-01-24 23:41:01 +00:00
|
|
|
*
|
|
|
|
|
* @since 1.30
|
2020-02-13 05:29:57 +00:00
|
|
|
* @since 1.31 Method signature changed, $key parameter added (required since 1.35)
|
2017-06-06 17:39:14 +00:00
|
|
|
* @param IDatabase $dbw Database handle to insert on
|
2018-01-24 23:41:01 +00:00
|
|
|
* @param string $key A key such as "rev_comment" identifying the comment
|
|
|
|
|
* field being fetched.
|
2018-06-26 21:14:43 +00:00
|
|
|
* @param string|Message|CommentStoreComment|null $comment As for `self::createComment()`
|
2017-06-06 17:39:14 +00:00
|
|
|
* @param array|null $data As for `self::createComment()`
|
|
|
|
|
* @return array Fields for the insert or update
|
|
|
|
|
*/
|
2018-01-24 23:41:01 +00:00
|
|
|
public function insert( IDatabase $dbw, $key, $comment = null, $data = null ) {
|
|
|
|
|
if ( $comment === null ) {
|
|
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
|
throw new InvalidArgumentException( '$comment can not be null' );
|
|
|
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-10 19:40:48 +00:00
|
|
|
$tempTableStage = static::TEMP_TABLES[$key]['stage'] ?? MIGRATION_NEW;
|
2019-09-20 18:35:39 +00:00
|
|
|
if ( $tempTableStage & SCHEMA_COMPAT_WRITE_OLD ) {
|
2018-01-24 23:41:01 +00:00
|
|
|
throw new InvalidArgumentException( "Must use insertWithTempTable() for $key" );
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
2018-01-24 23:41:01 +00:00
|
|
|
list( $fields ) = $this->insertInternal( $dbw, $key, $comment, $data );
|
2017-06-06 17:39:14 +00:00
|
|
|
return $fields;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
2017-09-05 19:02:33 +00:00
|
|
|
* Insert a comment in a temporary table in preparation for a row that references it
|
2017-06-06 17:39:14 +00:00
|
|
|
*
|
|
|
|
|
* This is currently needed for "rev_comment" and "img_description". In the
|
|
|
|
|
* future that requirement will be removed.
|
|
|
|
|
*
|
|
|
|
|
* @note It's recommended to include both the call to this method and the
|
|
|
|
|
* row insert in the same transaction.
|
2018-01-24 23:41:01 +00:00
|
|
|
*
|
|
|
|
|
* @since 1.30
|
2020-02-13 05:29:57 +00:00
|
|
|
* @since 1.31 Method signature changed, $key parameter added (required since 1.35)
|
2017-06-06 17:39:14 +00:00
|
|
|
* @param IDatabase $dbw Database handle to insert on
|
2018-01-24 23:41:01 +00:00
|
|
|
* @param string $key A key such as "rev_comment" identifying the comment
|
|
|
|
|
* field being fetched.
|
2018-06-26 21:14:43 +00:00
|
|
|
* @param string|Message|CommentStoreComment|null $comment As for `self::createComment()`
|
2017-06-06 17:39:14 +00:00
|
|
|
* @param array|null $data As for `self::createComment()`
|
|
|
|
|
* @return array Two values:
|
|
|
|
|
* - array Fields for the insert or update
|
|
|
|
|
* - callable Function to call when the primary key of the row being
|
|
|
|
|
* inserted/updated is known. Pass it that primary key.
|
|
|
|
|
*/
|
2018-01-24 23:41:01 +00:00
|
|
|
public function insertWithTempTable( IDatabase $dbw, $key, $comment = null, $data = null ) {
|
|
|
|
|
if ( $comment === null ) {
|
|
|
|
|
// @codeCoverageIgnoreStart
|
|
|
|
|
throw new InvalidArgumentException( '$comment can not be null' );
|
|
|
|
|
// @codeCoverageIgnoreEnd
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-10 19:40:48 +00:00
|
|
|
if ( !isset( static::TEMP_TABLES[$key] ) ) {
|
2018-01-24 23:41:01 +00:00
|
|
|
throw new InvalidArgumentException( "Must use insert() for $key" );
|
2021-02-10 19:40:48 +00:00
|
|
|
} elseif ( isset( static::TEMP_TABLES[$key]['deprecatedIn'] ) ) {
|
|
|
|
|
wfDeprecated( __METHOD__ . " for $key", static::TEMP_TABLES[$key]['deprecatedIn'] );
|
2017-06-06 17:39:14 +00:00
|
|
|
}
|
|
|
|
|
|
2018-01-24 23:41:01 +00:00
|
|
|
list( $fields, $callback ) = $this->insertInternal( $dbw, $key, $comment, $data );
|
2017-06-06 17:39:14 +00:00
|
|
|
if ( !$callback ) {
|
2021-02-10 22:31:02 +00:00
|
|
|
$callback = static function () {
|
2017-06-06 17:39:14 +00:00
|
|
|
// Do nothing.
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
return [ $fields, $callback ];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Encode a Message as a PHP data structure
|
|
|
|
|
* @param Message $msg
|
|
|
|
|
* @return array
|
|
|
|
|
*/
|
2020-03-24 08:52:35 +00:00
|
|
|
private static function encodeMessage( Message $msg ) {
|
2017-06-06 17:39:14 +00:00
|
|
|
$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
|
|
|
|
|
*/
|
2020-03-24 08:52:35 +00:00
|
|
|
private static function decodeMessage( $data ) {
|
2017-06-06 17:39:14 +00:00
|
|
|
$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;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|