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