Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
35.71% covered (danger)
35.71%
15 / 42
25.00% covered (danger)
25.00%
1 / 4
CRAP
0.00% covered (danger)
0.00%
0 / 1
LogStore
35.71% covered (danger)
35.71%
15 / 42
25.00% covered (danger)
25.00%
1 / 4
43.15
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 thank
87.50% covered (warning)
87.50%
14 / 16
0.00% covered (danger)
0.00%
0 / 1
4.03
 haveThanked
0.00% covered (danger)
0.00%
0 / 15
0.00% covered (danger)
0.00%
0 / 1
2
 getLogEntryFromId
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
30
1<?php
2
3namespace MediaWiki\Extension\Thanks\Storage;
4
5use InvalidArgumentException;
6use MediaWiki\CheckUser\Services\CheckUserInsert;
7use MediaWiki\Config\ServiceOptions;
8use MediaWiki\Extension\Thanks\Storage\Exceptions\InvalidLogType;
9use MediaWiki\Extension\Thanks\Storage\Exceptions\LogDeleted;
10use MediaWiki\Logging\DatabaseLogEntry;
11use MediaWiki\Logging\ManualLogEntry;
12use MediaWiki\MediaWikiServices;
13use MediaWiki\Registration\ExtensionRegistry;
14use MediaWiki\User\ActorNormalization;
15use MediaWiki\User\User;
16use Wikimedia\Rdbms\IConnectionProvider;
17
18/**
19 * Manages the storage for Thank events.
20 * Thanks are stored as logs with relations between users.
21 * Each thank action should have an unique ID generated by callers.
22 */
23class LogStore {
24
25    public const CONSTRUCTOR_OPTIONS = [ 'ThanksLogging', 'ThanksAllowedLogTypes' ];
26
27    public function __construct(
28        protected readonly IConnectionProvider $conn,
29        protected readonly ActorNormalization $actorNormalization,
30        protected readonly ExtensionRegistry $extensionRegistry,
31        protected readonly ServiceOptions $serviceOptions,
32    ) {
33        $serviceOptions->assertRequiredOptions( self::CONSTRUCTOR_OPTIONS );
34    }
35
36    /**
37     * @param User $user The user performing the thanks (and the log entry).
38     * @param User $recipient The target of the thanks (and the log entry).
39     * @param string $uniqueId A unique Id to identify the event being thanked for, to use
40     *                         when checking for duplicate thanks
41     */
42    public function thank( User $user, User $recipient, string $uniqueId ): void {
43        if ( $user->isTemp() ) {
44            throw new InvalidArgumentException(
45                'Temporary accounts may not thank other users.'
46            );
47        }
48        if ( !$this->serviceOptions->get( 'ThanksLogging' ) ) {
49            return;
50        }
51        $logEntry = new ManualLogEntry( 'thanks', 'thank' );
52        $logEntry->setPerformer( $user );
53        $logEntry->setRelations( [ 'thankid' => $uniqueId ] );
54        $target = $recipient->getUserPage();
55        $logEntry->setTarget( $target );
56        $logId = $logEntry->insert();
57        $logEntry->publish( $logId, 'udp' );
58
59        if ( $this->extensionRegistry->isLoaded( 'CheckUser' ) ) {
60            // TODO: This should be done in a separate hook handler
61            /** @var CheckUserInsert $checkUserInsert */
62            $checkUserInsert = MediaWikiServices::getInstance()->get( 'CheckUserInsert' );
63            $checkUserInsert->updateCheckUserData( $logEntry->getRecentChange( $logId ) );
64        }
65    }
66
67    /**
68     * This checks the log_search data.
69     *
70     * @param User $thanker The user sending the thanks.
71     * @param string $uniqueId The identifier for the thanks.
72     * @return bool Whether thanks has already been sent
73     */
74    public function haveThanked( User $thanker, string $uniqueId ): bool {
75        // TODO: Figure out why it's not getting the data from a replica
76        $dbw = $this->conn->getPrimaryDatabase();
77        $thankerActor = $this->actorNormalization->acquireActorId( $thanker, $dbw );
78        return (bool)$dbw->newSelectQueryBuilder()
79            ->select( 'ls_value' )
80            ->from( 'log_search' )
81            ->join( 'logging', null, [ 'ls_log_id=log_id' ] )
82            ->where(
83                [
84                    'log_actor' => $thankerActor,
85                    'ls_field' => 'thankid',
86                    'ls_value' => $uniqueId,
87                ]
88            )
89            ->caller( __METHOD__ )
90            ->fetchRow();
91    }
92
93    /**
94     * @throws InvalidLogType
95     * @throws LogDeleted
96     */
97    public function getLogEntryFromId( int $logId ): ?DatabaseLogEntry {
98        $logEntry = DatabaseLogEntry::newFromId( $logId, $this->conn->getPrimaryDatabase() );
99
100        if ( !$logEntry ) {
101            return null;
102        }
103
104        // Make sure this log type is allowed.
105        $allowedLogTypes = $this->serviceOptions->get( 'ThanksAllowedLogTypes' );
106        if ( !in_array( $logEntry->getType(), $allowedLogTypes )
107             && !in_array( $logEntry->getType() . '/' . $logEntry->getSubtype(), $allowedLogTypes ) ) {
108            throw new InvalidLogType( $logEntry->getType() );
109        }
110
111        // Don't permit thanks if any part of the log entry is deleted.
112        if ( $logEntry->getDeleted() ) {
113            throw new LogDeleted();
114        }
115        return $logEntry;
116    }
117}