Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
93.62% covered (success)
93.62%
88 / 94
80.00% covered (warning)
80.00%
4 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
CheckUserLogService
93.62% covered (success)
93.62%
88 / 94
80.00% covered (warning)
80.00%
4 / 5
17.08
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 addLogEntry
87.23% covered (warning)
87.23%
41 / 47
0.00% covered (danger)
0.00%
0 / 1
4.03
 getPlaintextReason
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 getTargetSearchConds
100.00% covered (success)
100.00%
26 / 26
100.00% covered (success)
100.00%
1 / 1
6
 verifyTarget
100.00% covered (success)
100.00%
9 / 9
100.00% covered (success)
100.00%
1 / 1
5
1<?php
2
3namespace MediaWiki\CheckUser\Services;
4
5use LogicException;
6use MediaWiki\CommentFormatter\CommentFormatter;
7use MediaWiki\CommentStore\CommentStore;
8use MediaWiki\Deferred\DeferredUpdates;
9use MediaWiki\Parser\Sanitizer;
10use MediaWiki\Title\Title;
11use MediaWiki\User\ActorStore;
12use MediaWiki\User\UserIdentity;
13use MediaWiki\User\UserIdentityLookup;
14use Psr\Log\LoggerInterface;
15use Wikimedia\IPUtils;
16use Wikimedia\Rdbms\DBError;
17use Wikimedia\Rdbms\IConnectionProvider;
18use Wikimedia\Timestamp\ConvertibleTimestamp;
19
20/**
21 * A service for methods that interact with the cu_log table, either for insertion or
22 * reading log entries.
23 */
24class CheckUserLogService {
25
26    private IConnectionProvider $dbProvider;
27    private CommentStore $commentStore;
28    private CommentFormatter $commentFormatter;
29    private LoggerInterface $logger;
30    private ActorStore $actorStore;
31    private UserIdentityLookup $userIdentityLookup;
32
33    /**
34     * @param IConnectionProvider $dbProvider
35     * @param CommentStore $commentStore
36     * @param CommentFormatter $commentFormatter
37     * @param LoggerInterface $logger
38     * @param ActorStore $actorStore
39     * @param UserIdentityLookup $userIdentityLookup
40     */
41    public function __construct(
42        IConnectionProvider $dbProvider,
43        CommentStore $commentStore,
44        CommentFormatter $commentFormatter,
45        LoggerInterface $logger,
46        ActorStore $actorStore,
47        UserIdentityLookup $userIdentityLookup
48    ) {
49        $this->dbProvider = $dbProvider;
50        $this->commentStore = $commentStore;
51        $this->commentFormatter = $commentFormatter;
52        $this->logger = $logger;
53        $this->actorStore = $actorStore;
54        $this->userIdentityLookup = $userIdentityLookup;
55    }
56
57    /**
58     * Adds a log entry to the CheckUserLog.
59     *
60     * @param UserIdentity $user
61     * @param string $logType
62     * @param string $targetType
63     * @param string $target
64     * @param string $reason
65     * @param int $targetID
66     * @return void
67     */
68    public function addLogEntry(
69        UserIdentity $user, string $logType, string $targetType, string $target, string $reason, int $targetID = 0
70    ) {
71        if ( $targetType == 'ip' ) {
72            [ $rangeStart, $rangeEnd ] = IPUtils::parseRange( $target );
73            $targetHex = $rangeStart;
74            if ( $rangeStart == $rangeEnd ) {
75                $rangeStart = '';
76                $rangeEnd = '';
77            }
78        } else {
79            $targetHex = '';
80            $rangeStart = '';
81            $rangeEnd = '';
82        }
83
84        $timestamp = ConvertibleTimestamp::now();
85        $dbw = $this->dbProvider->getPrimaryDatabase();
86
87        $data = [
88            'cul_actor' => $this->actorStore->acquireActorId( $user, $dbw ),
89            'cul_type' => $logType,
90            'cul_target_id' => $targetID,
91            'cul_target_text' => trim( $target ),
92            'cul_target_hex' => $targetHex,
93            'cul_range_start' => $rangeStart,
94            'cul_range_end' => $rangeEnd
95        ];
96
97        $plaintextReason = $this->getPlaintextReason( $reason );
98
99        $fname = __METHOD__;
100        $commentStore = $this->commentStore;
101        $logger = $this->logger;
102
103        DeferredUpdates::addCallableUpdate(
104            static function () use (
105                $data, $timestamp, $reason, $plaintextReason, $fname, $dbw, $commentStore, $logger
106            ) {
107                try {
108                    $data += $commentStore->insert( $dbw, 'cul_reason', $reason );
109                    $data += $commentStore->insert( $dbw, 'cul_reason_plaintext', $plaintextReason );
110                    $dbw->newInsertQueryBuilder()
111                        ->insertInto( 'cu_log' )
112                        ->row(
113                            [
114                                'cul_timestamp' => $dbw->timestamp( $timestamp )
115                            ] + $data
116                        )
117                        ->caller( $fname )
118                        ->execute();
119                } catch ( DBError $e ) {
120                    $logger->critical(
121                        'CheckUserLog entry was not recorded. This means checks can occur without being auditable. ' .
122                        'Immediate fix required.'
123                    );
124                    throw $e;
125                }
126            }
127        );
128    }
129
130    /**
131     * Get the plaintext reason
132     *
133     * @param string $reason
134     * @return string
135     */
136    public function getPlaintextReason( $reason ) {
137        return Sanitizer::stripAllTags(
138            $this->commentFormatter->formatBlock(
139                $reason, Title::newFromText( 'Special:CheckUserLog' ),
140                false, false, false
141            )
142        );
143    }
144
145    /**
146     * Get DB search conditions for the cu_log table according to the target given.
147     *
148     * @param string $target the username, IP address or range of the target.
149     * @return array|null array if valid target, null if invalid target given
150     */
151    public function getTargetSearchConds( string $target ): ?array {
152        $result = $this->verifyTarget( $target );
153        if ( is_array( $result ) ) {
154            $dbr = $this->dbProvider->getReplicaDatabase();
155            switch ( count( $result ) ) {
156                case 1:
157                    return [
158                        'cul_target_hex = ' . $dbr->addQuotes( $result[0] ) . ' OR ' .
159                        '(cul_range_end >= ' . $dbr->addQuotes( $result[0] ) . ' AND ' .
160                        'cul_range_start <= ' . $dbr->addQuotes( $result[0] ) . ')'
161                    ];
162                case 2:
163                    return [
164                        '(cul_target_hex >= ' . $dbr->addQuotes( $result[0] ) . ' AND ' .
165                        'cul_target_hex <= ' . $dbr->addQuotes( $result[1] ) . ') OR ' .
166                        '(cul_range_end >= ' . $dbr->addQuotes( $result[0] ) . ' AND ' .
167                        'cul_range_start <= ' . $dbr->addQuotes( $result[1] ) . ')'
168                    ];
169                default:
170                    throw new LogicException(
171                        "Array returned from ::verifyTarget had the wrong number of items."
172                    );
173            }
174        } elseif ( is_int( $result ) ) {
175            return [
176                'cul_type' => [ 'userips', 'useredits', 'investigate' ],
177                'cul_target_id' => $result,
178            ];
179        }
180        return null;
181    }
182
183    /**
184     * Verify if the target is a valid IP, IP range or user.
185     *
186     * @param string $target
187     * @return false|int|array If the target is a user, then the user's ID is returned.
188     *   If the target is valid IP address, then the IP address
189     *   in hexadecimal is returned as a one item array.
190     *   If the target is a valid IP address range, then the
191     *   start and end of the range in hexadecimal is returned
192     *   as an array.
193     *   Returns false for an invalid target.
194     */
195    public function verifyTarget( string $target ) {
196        [ $start, $end ] = IPUtils::parseRange( $target );
197
198        if ( $start !== false ) {
199            if ( $start === $end ) {
200                return [ $start ];
201            }
202
203            return [ $start, $end ];
204        }
205
206        $user = $this->userIdentityLookup->getUserIdentityByName( $target );
207        if ( $user && $user->getId() ) {
208            return $user->getId();
209        }
210
211        return false;
212    }
213}