config = $configFactory->makeConfig( 'discussiontools' ); $this->dbProvider = $dbProvider; $this->readOnlyMode = $readOnlyMode; $this->userFactory = $userFactory; $this->userIdentityUtils = $userIdentityUtils; } /** * @param IReadableDatabase $db * @param UserIdentity|null $user * @param array|null $itemNames * @param int|int[]|null $state One of (or an array of) SubscriptionStore::STATE_* constants * @return IResultWrapper|false */ private function fetchSubscriptions( IReadableDatabase $db, ?UserIdentity $user = null, ?array $itemNames = null, $state = null ) { $conditions = []; if ( $user ) { $conditions[ 'sub_user' ] = $user->getId(); } if ( $itemNames !== null ) { 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( [] ); } $conditions[ 'sub_item' ] = $itemNames; } if ( $state !== null ) { $conditions[ 'sub_state' ] = $state; } return $db->newSelectQueryBuilder() ->from( 'discussiontools_subscription' ) ->fields( [ 'sub_user', 'sub_item', 'sub_namespace', 'sub_title', 'sub_section', 'sub_state', 'sub_created', 'sub_notified' ] ) ->where( $conditions ) ->caller( __METHOD__ ) ->fetchResultSet(); } /** * @param UserIdentity $user * @param array|null $itemNames * @param int[]|null $state Array of SubscriptionStore::STATE_* constants * @param array $options * @return SubscriptionItem[] */ public function getSubscriptionItemsForUser( UserIdentity $user, ?array $itemNames = null, ?array $state = null, array $options = [] ): array { // Only a registered user can be subscribed if ( !$user->isRegistered() || $this->userIdentityUtils->isTemp( $user ) ) { return []; } $options += [ 'forWrite' => false ]; if ( $options['forWrite'] ) { $db = $this->dbProvider->getPrimaryDatabase(); } else { $db = $this->dbProvider->getReplicaDatabase(); } $rows = $this->fetchSubscriptions( $db, $user, $itemNames, $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 * @param int[]|null $state An array of SubscriptionStore::STATE_* constants * @param array $options * @return array */ public function getSubscriptionItemsForTopic( string $itemName, ?array $state = null, array $options = [] ): array { $options += [ 'forWrite' => false ]; if ( $options['forWrite'] ) { $db = $this->dbProvider->getPrimaryDatabase(); } else { $db = $this->dbProvider->getReplicaDatabase(); } $rows = $this->fetchSubscriptions( $db, null, [ $itemName ], $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; } /** * @param UserIdentity $user * @param LinkTarget $target * @param stdClass $row * @return SubscriptionItem */ private function getSubscriptionItemFromRow( UserIdentity $user, LinkTarget $target, stdClass $row ): SubscriptionItem { return new SubscriptionItem( $user, $row->sub_item, $target, $row->sub_state, $row->sub_created, $row->sub_notified ); } /** * @param UserIdentity $user * @param LinkTarget $target * @param string $itemName * @param int $state * @return bool */ public function addSubscriptionForUser( UserIdentity $user, LinkTarget $target, string $itemName, // Can not use static:: in compile-time constants int $state = self::STATE_SUBSCRIBED ): bool { if ( $this->readOnlyMode->isReadOnly() ) { return false; } // Only a registered user can subscribe if ( !$user->isRegistered() || $this->userIdentityUtils->isTemp( $user ) ) { return false; } $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; $dbw = $this->dbProvider->getPrimaryDatabase(); $dbw->newInsertQueryBuilder() ->table( 'discussiontools_subscription' ) ->row( [ 'sub_user' => $user->getId(), 'sub_namespace' => $target->getNamespace(), 'sub_title' => $target->getDBkey(), 'sub_section' => $truncSection, 'sub_item' => $itemName, 'sub_state' => $state, 'sub_created' => $dbw->timestamp(), ] ) ->onDuplicateKeyUpdate() ->uniqueIndexFields( [ 'sub_user', 'sub_item' ] ) ->set( [ 'sub_state' => $state, ] ) ->caller( __METHOD__ )->execute(); return (bool)$dbw->affectedRows(); } /** * @param UserIdentity $user * @param string $itemName * @return bool */ public function removeSubscriptionForUser( UserIdentity $user, string $itemName ): bool { if ( $this->readOnlyMode->isReadOnly() ) { return false; } // Only a registered user can subscribe if ( !$user->isRegistered() || $this->userIdentityUtils->isTemp( $user ) ) { return false; } $dbw = $this->dbProvider->getPrimaryDatabase(); $dbw->newUpdateQueryBuilder() ->table( 'discussiontools_subscription' ) ->set( [ 'sub_state' => static::STATE_UNSUBSCRIBED ] ) ->where( [ 'sub_user' => $user->getId(), 'sub_item' => $itemName, ] ) ->caller( __METHOD__ ) ->execute(); return (bool)$dbw->affectedRows(); } /** * @param UserIdentity $user * @param LinkTarget $target * @param string $itemName * @return bool */ public function addAutoSubscriptionForUser( UserIdentity $user, LinkTarget $target, string $itemName ): bool { // Check for existing subscriptions. $subscriptionItems = $this->getSubscriptionItemsForUser( $user, [ $itemName ], [ static::STATE_SUBSCRIBED, static::STATE_AUTOSUBSCRIBED ], [ 'forWrite' => true ] ); if ( $subscriptionItems ) { return false; } return $this->addSubscriptionForUser( $user, $target, $itemName, static::STATE_AUTOSUBSCRIBED ); } /** * @param string $field Timestamp field name * @param UserIdentity|null $user * @param string $itemName * @return bool */ private function updateSubscriptionTimestamp( string $field, ?UserIdentity $user, string $itemName ): bool { if ( $this->readOnlyMode->isReadOnly() ) { return false; } $dbw = $this->dbProvider->getPrimaryDatabase(); $conditions = [ 'sub_item' => $itemName, ]; if ( $user ) { $conditions[ 'sub_user' ] = $user->getId(); } $dbw->newUpdateQueryBuilder() ->table( 'discussiontools_subscription' ) ->set( [ $field => $dbw->timestamp() ] ) ->where( $conditions ) ->caller( __METHOD__ ) ->execute(); 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. * * @param UserIdentity|null $user * @param string $itemName * @return bool */ public function updateSubscriptionNotifiedTimestamp( ?UserIdentity $user, string $itemName ): bool { return $this->updateSubscriptionTimestamp( 'sub_notified', $user, $itemName ); } }