2021-02-16 07:51:49 +00:00
|
|
|
<?php
|
|
|
|
|
|
|
|
namespace MediaWiki\Extension\DiscussionTools;
|
|
|
|
|
2023-12-11 15:38:02 +00:00
|
|
|
use MediaWiki\Config\Config;
|
|
|
|
use MediaWiki\Config\ConfigFactory;
|
2021-02-16 07:51:49 +00:00
|
|
|
use MediaWiki\Linker\LinkTarget;
|
2023-12-11 15:38:02 +00:00
|
|
|
use MediaWiki\Title\TitleValue;
|
2021-02-16 07:51:49 +00:00
|
|
|
use MediaWiki\User\UserFactory;
|
|
|
|
use MediaWiki\User\UserIdentity;
|
2023-07-26 10:32:27 +00:00
|
|
|
use MediaWiki\User\UserIdentityUtils;
|
2021-02-16 07:51:49 +00:00
|
|
|
use stdClass;
|
2021-05-05 18:16:54 +00:00
|
|
|
use Wikimedia\Rdbms\FakeResultWrapper;
|
2023-04-29 22:58:13 +00:00
|
|
|
use Wikimedia\Rdbms\IConnectionProvider;
|
2023-02-20 15:19:52 +00:00
|
|
|
use Wikimedia\Rdbms\IReadableDatabase;
|
2021-02-16 07:51:49 +00:00
|
|
|
use Wikimedia\Rdbms\IResultWrapper;
|
2023-12-11 15:38:02 +00:00
|
|
|
use Wikimedia\Rdbms\ReadOnlyMode;
|
2021-02-16 07:51:49 +00:00
|
|
|
|
|
|
|
class SubscriptionStore {
|
2022-10-21 19:34:18 +00:00
|
|
|
|
2021-08-16 16:13:12 +00:00
|
|
|
/**
|
|
|
|
* Constants for the values of the sub_state field.
|
|
|
|
*/
|
|
|
|
public const STATE_UNSUBSCRIBED = 0;
|
|
|
|
public const STATE_SUBSCRIBED = 1;
|
2021-08-17 20:23:27 +00:00
|
|
|
public const STATE_AUTOSUBSCRIBED = 2;
|
2021-08-16 16:13:12 +00:00
|
|
|
|
2022-10-21 19:34:18 +00:00
|
|
|
private Config $config;
|
2023-04-29 22:58:13 +00:00
|
|
|
private IConnectionProvider $dbProvider;
|
2022-10-21 19:34:18 +00:00
|
|
|
private ReadOnlyMode $readOnlyMode;
|
|
|
|
private UserFactory $userFactory;
|
2023-07-26 10:32:27 +00:00
|
|
|
private UserIdentityUtils $userIdentityUtils;
|
2021-02-16 07:51:49 +00:00
|
|
|
|
|
|
|
public function __construct(
|
2021-12-15 16:15:01 +00:00
|
|
|
ConfigFactory $configFactory,
|
2023-04-29 22:58:13 +00:00
|
|
|
IConnectionProvider $dbProvider,
|
2021-02-16 07:51:49 +00:00
|
|
|
ReadOnlyMode $readOnlyMode,
|
2023-06-02 19:11:36 +00:00
|
|
|
UserFactory $userFactory,
|
2023-07-26 10:32:27 +00:00
|
|
|
UserIdentityUtils $userIdentityUtils
|
2021-02-16 07:51:49 +00:00
|
|
|
) {
|
2022-09-02 22:24:20 +00:00
|
|
|
$this->config = $configFactory->makeConfig( 'discussiontools' );
|
2023-04-29 22:58:13 +00:00
|
|
|
$this->dbProvider = $dbProvider;
|
2021-02-16 07:51:49 +00:00
|
|
|
$this->readOnlyMode = $readOnlyMode;
|
2022-09-02 22:24:20 +00:00
|
|
|
$this->userFactory = $userFactory;
|
2023-07-26 10:32:27 +00:00
|
|
|
$this->userIdentityUtils = $userIdentityUtils;
|
2021-02-16 07:51:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2023-02-20 15:19:52 +00:00
|
|
|
* @param IReadableDatabase $db
|
2021-02-16 07:51:49 +00:00
|
|
|
* @param UserIdentity|null $user
|
2021-04-23 18:54:49 +00:00
|
|
|
* @param array|null $itemNames
|
2021-08-17 20:23:27 +00:00
|
|
|
* @param int|int[]|null $state One of (or an array of) SubscriptionStore::STATE_* constants
|
2021-02-16 07:51:49 +00:00
|
|
|
* @return IResultWrapper|false
|
|
|
|
*/
|
|
|
|
private function fetchSubscriptions(
|
2023-02-20 15:19:52 +00:00
|
|
|
IReadableDatabase $db,
|
2021-02-16 07:51:49 +00:00
|
|
|
?UserIdentity $user = null,
|
2021-04-23 18:54:49 +00:00
|
|
|
?array $itemNames = null,
|
2021-08-17 20:23:27 +00:00
|
|
|
$state = null
|
2021-02-16 07:51:49 +00:00
|
|
|
) {
|
|
|
|
$conditions = [];
|
|
|
|
|
|
|
|
if ( $user ) {
|
|
|
|
$conditions[ 'sub_user' ] = $user->getId();
|
|
|
|
}
|
|
|
|
|
2021-04-23 18:54:49 +00:00
|
|
|
if ( $itemNames !== null ) {
|
2021-05-05 18:16:54 +00:00
|
|
|
if ( !count( $itemNames ) ) {
|
|
|
|
// We are not allowed to construct a filter with an empty array.
|
|
|
|
// Any empty array should result in no items being returned.
|
|
|
|
return new FakeResultWrapper( [] );
|
|
|
|
}
|
2021-04-23 18:54:49 +00:00
|
|
|
$conditions[ 'sub_item' ] = $itemNames;
|
2021-02-16 07:51:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
if ( $state !== null ) {
|
|
|
|
$conditions[ 'sub_state' ] = $state;
|
|
|
|
}
|
|
|
|
|
2022-08-12 22:15:24 +00:00
|
|
|
return $db->newSelectQueryBuilder()
|
|
|
|
->from( 'discussiontools_subscription' )
|
|
|
|
->fields( [
|
2021-02-16 07:51:49 +00:00
|
|
|
'sub_user', 'sub_item', 'sub_namespace', 'sub_title', 'sub_section', 'sub_state',
|
|
|
|
'sub_created', 'sub_notified'
|
2022-08-12 22:15:24 +00:00
|
|
|
] )
|
|
|
|
->where( $conditions )
|
|
|
|
->caller( __METHOD__ )
|
|
|
|
->fetchResultSet();
|
2021-02-16 07:51:49 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param UserIdentity $user
|
2021-04-23 18:54:49 +00:00
|
|
|
* @param array|null $itemNames
|
2021-08-17 20:23:27 +00:00
|
|
|
* @param int[]|null $state Array of SubscriptionStore::STATE_* constants
|
2021-02-16 07:51:49 +00:00
|
|
|
* @param array $options
|
|
|
|
* @return SubscriptionItem[]
|
|
|
|
*/
|
|
|
|
public function getSubscriptionItemsForUser(
|
|
|
|
UserIdentity $user,
|
2021-04-23 18:54:49 +00:00
|
|
|
?array $itemNames = null,
|
2021-08-17 20:23:27 +00:00
|
|
|
?array $state = null,
|
2021-02-16 07:51:49 +00:00
|
|
|
array $options = []
|
2021-07-22 07:25:13 +00:00
|
|
|
): array {
|
2021-02-16 07:51:49 +00:00
|
|
|
// Only a registered user can be subscribed
|
2023-07-26 10:32:27 +00:00
|
|
|
if ( !$user->isRegistered() || $this->userIdentityUtils->isTemp( $user ) ) {
|
2021-02-16 07:51:49 +00:00
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
$options += [ 'forWrite' => false ];
|
2023-02-20 15:19:52 +00:00
|
|
|
if ( $options['forWrite'] ) {
|
2023-04-29 22:58:13 +00:00
|
|
|
$db = $this->dbProvider->getPrimaryDatabase();
|
2023-02-20 15:19:52 +00:00
|
|
|
} else {
|
2023-04-29 22:58:13 +00:00
|
|
|
$db = $this->dbProvider->getReplicaDatabase();
|
2023-02-20 15:19:52 +00:00
|
|
|
}
|
2021-02-16 07:51:49 +00:00
|
|
|
|
|
|
|
$rows = $this->fetchSubscriptions(
|
|
|
|
$db,
|
|
|
|
$user,
|
2021-04-23 18:54:49 +00:00
|
|
|
$itemNames,
|
2021-02-16 07:51:49 +00:00
|
|
|
$state
|
|
|
|
);
|
|
|
|
|
|
|
|
if ( !$rows ) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
$items = [];
|
|
|
|
foreach ( $rows as $row ) {
|
|
|
|
$target = new TitleValue( (int)$row->sub_namespace, $row->sub_title, $row->sub_section );
|
|
|
|
$items[] = $this->getSubscriptionItemFromRow( $user, $target, $row );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $items;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string $itemName
|
2021-08-17 20:23:27 +00:00
|
|
|
* @param int[]|null $state An array of SubscriptionStore::STATE_* constants
|
2021-02-16 07:51:49 +00:00
|
|
|
* @param array $options
|
|
|
|
* @return array
|
|
|
|
*/
|
|
|
|
public function getSubscriptionItemsForTopic(
|
|
|
|
string $itemName,
|
2021-08-17 20:23:27 +00:00
|
|
|
?array $state = null,
|
2021-02-16 07:51:49 +00:00
|
|
|
array $options = []
|
2021-07-22 07:25:13 +00:00
|
|
|
): array {
|
2021-02-16 07:51:49 +00:00
|
|
|
$options += [ 'forWrite' => false ];
|
2023-02-20 15:19:52 +00:00
|
|
|
if ( $options['forWrite'] ) {
|
2023-04-29 22:58:13 +00:00
|
|
|
$db = $this->dbProvider->getPrimaryDatabase();
|
2023-02-20 15:19:52 +00:00
|
|
|
} else {
|
2023-04-29 22:58:13 +00:00
|
|
|
$db = $this->dbProvider->getReplicaDatabase();
|
2023-02-20 15:19:52 +00:00
|
|
|
}
|
2021-02-16 07:51:49 +00:00
|
|
|
|
|
|
|
$rows = $this->fetchSubscriptions(
|
|
|
|
$db,
|
|
|
|
null,
|
2021-04-23 18:54:49 +00:00
|
|
|
[ $itemName ],
|
2021-02-16 07:51:49 +00:00
|
|
|
$state
|
|
|
|
);
|
|
|
|
|
|
|
|
if ( !$rows ) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
$items = [];
|
|
|
|
foreach ( $rows as $row ) {
|
|
|
|
$target = new TitleValue( (int)$row->sub_namespace, $row->sub_title, $row->sub_section );
|
|
|
|
$user = $this->userFactory->newFromId( $row->sub_user );
|
|
|
|
$items[] = $this->getSubscriptionItemFromRow( $user, $target, $row );
|
|
|
|
}
|
|
|
|
|
|
|
|
return $items;
|
|
|
|
}
|
|
|
|
|
|
|
|
private function getSubscriptionItemFromRow(
|
|
|
|
UserIdentity $user,
|
|
|
|
LinkTarget $target,
|
|
|
|
stdClass $row
|
2021-07-22 07:25:13 +00:00
|
|
|
): SubscriptionItem {
|
2021-02-16 07:51:49 +00:00
|
|
|
return new SubscriptionItem(
|
|
|
|
$user,
|
|
|
|
$row->sub_item,
|
|
|
|
$target,
|
|
|
|
$row->sub_state,
|
|
|
|
$row->sub_created,
|
|
|
|
$row->sub_notified
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
public function addSubscriptionForUser(
|
|
|
|
UserIdentity $user,
|
|
|
|
LinkTarget $target,
|
2021-08-17 20:23:27 +00:00
|
|
|
string $itemName,
|
2023-09-14 15:58:16 +00:00
|
|
|
// Can not use static:: in compile-time constants
|
2021-08-17 20:23:27 +00:00
|
|
|
int $state = self::STATE_SUBSCRIBED
|
2021-07-22 07:25:13 +00:00
|
|
|
): bool {
|
2021-02-16 07:51:49 +00:00
|
|
|
if ( $this->readOnlyMode->isReadOnly() ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// Only a registered user can subscribe
|
2023-07-26 10:32:27 +00:00
|
|
|
if ( !$user->isRegistered() || $this->userIdentityUtils->isTemp( $user ) ) {
|
2021-02-16 07:51:49 +00:00
|
|
|
return false;
|
|
|
|
}
|
2023-09-07 18:44:43 +00:00
|
|
|
|
|
|
|
$section = $target->getFragment();
|
|
|
|
// Truncate to the database field length, taking care not to mess up multibyte characters,
|
|
|
|
// appending a marker so that we can recognize this happened and display an ellipsis later.
|
|
|
|
// Using U+001F "Unit Separator" seems appropriate, and it can't occur in wikitext.
|
|
|
|
$truncSection = strlen( $section ) > 254 ? mb_strcut( $section, 0, 254 ) . "\x1f" : $section;
|
|
|
|
|
2023-04-29 22:58:13 +00:00
|
|
|
$dbw = $this->dbProvider->getPrimaryDatabase();
|
2023-10-21 21:12:17 +00:00
|
|
|
$dbw->newInsertQueryBuilder()
|
|
|
|
->table( 'discussiontools_subscription' )
|
|
|
|
->row( [
|
2021-02-16 07:51:49 +00:00
|
|
|
'sub_user' => $user->getId(),
|
|
|
|
'sub_namespace' => $target->getNamespace(),
|
|
|
|
'sub_title' => $target->getDBkey(),
|
2023-09-07 18:44:43 +00:00
|
|
|
'sub_section' => $truncSection,
|
2021-02-16 07:51:49 +00:00
|
|
|
'sub_item' => $itemName,
|
2021-08-17 20:23:27 +00:00
|
|
|
'sub_state' => $state,
|
2021-02-16 07:51:49 +00:00
|
|
|
'sub_created' => $dbw->timestamp(),
|
2023-10-21 21:12:17 +00:00
|
|
|
] )
|
|
|
|
->onDuplicateKeyUpdate()
|
|
|
|
->uniqueIndexFields( [ 'sub_user', 'sub_item' ] )
|
|
|
|
->set( [
|
2021-08-17 20:23:27 +00:00
|
|
|
'sub_state' => $state,
|
2023-10-21 21:12:17 +00:00
|
|
|
] )
|
|
|
|
->caller( __METHOD__ )->execute();
|
2021-02-16 07:51:49 +00:00
|
|
|
return (bool)$dbw->affectedRows();
|
|
|
|
}
|
|
|
|
|
|
|
|
public function removeSubscriptionForUser(
|
|
|
|
UserIdentity $user,
|
|
|
|
string $itemName
|
2021-07-22 07:25:13 +00:00
|
|
|
): bool {
|
2021-02-16 07:51:49 +00:00
|
|
|
if ( $this->readOnlyMode->isReadOnly() ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
// Only a registered user can subscribe
|
2023-07-26 10:32:27 +00:00
|
|
|
if ( !$user->isRegistered() || $this->userIdentityUtils->isTemp( $user ) ) {
|
2021-02-16 07:51:49 +00:00
|
|
|
return false;
|
|
|
|
}
|
2023-04-29 22:58:13 +00:00
|
|
|
$dbw = $this->dbProvider->getPrimaryDatabase();
|
2023-08-11 19:53:25 +00:00
|
|
|
$dbw->newUpdateQueryBuilder()
|
|
|
|
->table( 'discussiontools_subscription' )
|
|
|
|
->set( [ 'sub_state' => static::STATE_UNSUBSCRIBED ] )
|
|
|
|
->where( [
|
2021-02-16 07:51:49 +00:00
|
|
|
'sub_user' => $user->getId(),
|
|
|
|
'sub_item' => $itemName,
|
2023-08-11 19:53:25 +00:00
|
|
|
] )
|
|
|
|
->caller( __METHOD__ )
|
|
|
|
->execute();
|
2021-02-16 07:51:49 +00:00
|
|
|
return (bool)$dbw->affectedRows();
|
|
|
|
}
|
|
|
|
|
2021-08-17 20:23:27 +00:00
|
|
|
public function addAutoSubscriptionForUser(
|
|
|
|
UserIdentity $user,
|
|
|
|
LinkTarget $target,
|
|
|
|
string $itemName
|
|
|
|
): bool {
|
|
|
|
// Check for existing subscriptions.
|
|
|
|
$subscriptionItems = $this->getSubscriptionItemsForUser(
|
|
|
|
$user,
|
|
|
|
[ $itemName ],
|
2022-06-09 13:51:33 +00:00
|
|
|
[ static::STATE_SUBSCRIBED, static::STATE_AUTOSUBSCRIBED ],
|
2021-08-17 20:23:27 +00:00
|
|
|
[ 'forWrite' => true ]
|
|
|
|
);
|
|
|
|
if ( $subscriptionItems ) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return $this->addSubscriptionForUser(
|
|
|
|
$user,
|
|
|
|
$target,
|
|
|
|
$itemName,
|
2022-06-09 13:51:33 +00:00
|
|
|
static::STATE_AUTOSUBSCRIBED
|
2021-08-17 20:23:27 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2021-02-16 07:51:49 +00:00
|
|
|
/**
|
|
|
|
* @param string $field Timestamp field name
|
|
|
|
* @param UserIdentity|null $user
|
|
|
|
* @param string $itemName
|
|
|
|
* @return bool
|
|
|
|
*/
|
|
|
|
private function updateSubscriptionTimestamp(
|
|
|
|
string $field,
|
|
|
|
?UserIdentity $user,
|
|
|
|
string $itemName
|
2021-07-22 07:25:13 +00:00
|
|
|
): bool {
|
2021-02-16 07:51:49 +00:00
|
|
|
if ( $this->readOnlyMode->isReadOnly() ) {
|
|
|
|
return false;
|
|
|
|
}
|
2023-04-29 22:58:13 +00:00
|
|
|
$dbw = $this->dbProvider->getPrimaryDatabase();
|
2021-02-16 07:51:49 +00:00
|
|
|
|
|
|
|
$conditions = [
|
|
|
|
'sub_item' => $itemName,
|
|
|
|
];
|
|
|
|
|
|
|
|
if ( $user ) {
|
|
|
|
$conditions[ 'sub_user' ] = $user->getId();
|
|
|
|
}
|
|
|
|
|
2023-08-11 19:53:25 +00:00
|
|
|
$dbw->newUpdateQueryBuilder()
|
|
|
|
->table( 'discussiontools_subscription' )
|
|
|
|
->set( [ $field => $dbw->timestamp() ] )
|
|
|
|
->where( $conditions )
|
|
|
|
->caller( __METHOD__ )
|
|
|
|
->execute();
|
2021-02-16 07:51:49 +00:00
|
|
|
return (bool)$dbw->affectedRows();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update the notified timestamp on a subscription
|
|
|
|
*
|
|
|
|
* This field could be used in future to cleanup notifications
|
|
|
|
* that are no longer needed (e.g. because the conversation has
|
|
|
|
* been archived), so should be set for muted notifications too.
|
|
|
|
*/
|
|
|
|
public function updateSubscriptionNotifiedTimestamp(
|
|
|
|
?UserIdentity $user,
|
|
|
|
string $itemName
|
2021-07-22 07:25:13 +00:00
|
|
|
): bool {
|
2021-02-16 07:51:49 +00:00
|
|
|
return $this->updateSubscriptionTimestamp(
|
|
|
|
'sub_notified',
|
|
|
|
$user,
|
|
|
|
$itemName
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|