Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
80.85% |
38 / 47 |
|
57.14% |
4 / 7 |
CRAP | |
0.00% |
0 / 1 |
TemporaryAccountLogger | |
80.85% |
38 / 47 |
|
57.14% |
4 / 7 |
11.85 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
5 / 5 |
|
100.00% |
1 / 1 |
1 | |||
logViewIPs | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
logAccessEnabled | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
logAccessDisabled | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
debouncedLog | |
89.47% |
17 / 19 |
|
0.00% |
0 / 1 |
3.01 | |||
log | |
53.85% |
7 / 13 |
|
0.00% |
0 / 1 |
3.88 | |||
createManualLogEntry | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\CheckUser\Logging; |
4 | |
5 | use ManualLogEntry; |
6 | use MediaWiki\Title\Title; |
7 | use MediaWiki\User\ActorStore; |
8 | use MediaWiki\User\UserIdentity; |
9 | use Psr\Log\LoggerInterface; |
10 | use Wikimedia\Assert\Assert; |
11 | use Wikimedia\Assert\ParameterAssertionException; |
12 | use Wikimedia\Rdbms\DBError; |
13 | use Wikimedia\Rdbms\IDatabase; |
14 | |
15 | /** |
16 | * Defines the API for the component responsible for logging the following interactions: |
17 | * |
18 | * - A user views IP addresses for a temporary account |
19 | * - A user enables temporary account IP viewing |
20 | * - A user disables temporary account IP viewing |
21 | * |
22 | * All the above interactions will be logged to the `logging` table with a log type |
23 | * `checkuser-temporary-account`. |
24 | */ |
25 | class TemporaryAccountLogger { |
26 | /** |
27 | * Represents a user (the performer) viewing IP addresses for a temporary account. |
28 | * |
29 | * @var string |
30 | */ |
31 | public const ACTION_VIEW_IPS = 'view-ips'; |
32 | |
33 | /** |
34 | * Represents a user enabling or disabling their own access to view IPs |
35 | * |
36 | * @var string |
37 | */ |
38 | public const ACTION_CHANGE_ACCESS = 'change-access'; |
39 | |
40 | /** |
41 | * @var string |
42 | */ |
43 | public const ACTION_ACCESS_ENABLED = 'enable'; |
44 | |
45 | /** |
46 | * @var string |
47 | */ |
48 | public const ACTION_ACCESS_DISABLED = 'disable'; |
49 | |
50 | /** |
51 | * @var string |
52 | */ |
53 | public const LOG_TYPE = 'checkuser-temporary-account'; |
54 | |
55 | private ActorStore $actorStore; |
56 | private LoggerInterface $logger; |
57 | private IDatabase $dbw; |
58 | |
59 | private int $delay; |
60 | |
61 | /** |
62 | * @param ActorStore $actorStore |
63 | * @param LoggerInterface $logger |
64 | * @param IDatabase $dbw |
65 | * @param int $delay The number of seconds after which a duplicate log entry can be |
66 | * created for a debounced log |
67 | * @throws ParameterAssertionException |
68 | */ |
69 | public function __construct( |
70 | ActorStore $actorStore, |
71 | LoggerInterface $logger, |
72 | IDatabase $dbw, |
73 | int $delay |
74 | ) { |
75 | Assert::parameter( $delay > 0, 'delay', 'delay must be positive' ); |
76 | |
77 | $this->actorStore = $actorStore; |
78 | $this->logger = $logger; |
79 | $this->dbw = $dbw; |
80 | $this->delay = $delay; |
81 | } |
82 | |
83 | /** |
84 | * Logs the user (the performer) viewing IP addresses for a temporary account. |
85 | * |
86 | * @param UserIdentity $performer |
87 | * @param string $tempUser |
88 | * @param int $timestamp |
89 | */ |
90 | public function logViewIPs( UserIdentity $performer, string $tempUser, int $timestamp ): void { |
91 | $this->debouncedLog( $performer, $tempUser, self::ACTION_VIEW_IPS, $timestamp ); |
92 | } |
93 | |
94 | /** |
95 | * Log when the user enables their own access |
96 | * |
97 | * @param UserIdentity $performer |
98 | */ |
99 | public function logAccessEnabled( UserIdentity $performer ): void { |
100 | $params = [ |
101 | '4::changeType' => self::ACTION_ACCESS_ENABLED, |
102 | ]; |
103 | $this->log( $performer, $performer->getName(), self::ACTION_CHANGE_ACCESS, $params ); |
104 | } |
105 | |
106 | /** |
107 | * Log when the user disables their own access |
108 | * |
109 | * @param UserIdentity $performer |
110 | */ |
111 | public function logAccessDisabled( UserIdentity $performer ): void { |
112 | $params = [ |
113 | '4::changeType' => self::ACTION_ACCESS_DISABLED, |
114 | ]; |
115 | $this->log( $performer, $performer->getName(), self::ACTION_CHANGE_ACCESS, $params ); |
116 | } |
117 | |
118 | /** |
119 | * @param UserIdentity $performer |
120 | * @param string $tempUser |
121 | * @param string $action |
122 | * @param int $timestamp |
123 | * @param array|null $params |
124 | */ |
125 | private function debouncedLog( |
126 | UserIdentity $performer, |
127 | string $tempUser, |
128 | string $action, |
129 | int $timestamp, |
130 | ?array $params = [] |
131 | ): void { |
132 | $timestampMinusDelay = $timestamp - $this->delay; |
133 | $actorId = $this->actorStore->findActorId( $performer, $this->dbw ); |
134 | if ( !$actorId ) { |
135 | $this->log( $performer, $tempUser, $action, $params ); |
136 | return; |
137 | } |
138 | |
139 | $logline = $this->dbw->newSelectQueryBuilder() |
140 | ->select( '*' ) |
141 | ->from( 'logging' ) |
142 | ->where( [ |
143 | 'log_type' => self::LOG_TYPE, |
144 | 'log_action' => $action, |
145 | 'log_actor' => $actorId, |
146 | 'log_namespace' => NS_USER, |
147 | 'log_title' => $tempUser, |
148 | $this->dbw->expr( 'log_timestamp', '>', $this->dbw->timestamp( $timestampMinusDelay ) ), |
149 | ] ) |
150 | ->fetchRow(); |
151 | |
152 | if ( !$logline ) { |
153 | $this->log( $performer, $tempUser, $action, $params, $timestamp ); |
154 | } |
155 | } |
156 | |
157 | /** |
158 | * @param UserIdentity $performer |
159 | * @param string $tempUser |
160 | * @param string $action |
161 | * @param array $params |
162 | * @param int|null $timestamp |
163 | */ |
164 | private function log( |
165 | UserIdentity $performer, |
166 | string $tempUser, |
167 | string $action, |
168 | array $params, |
169 | ?int $timestamp = null |
170 | ): void { |
171 | $logEntry = $this->createManualLogEntry( $action ); |
172 | $logEntry->setPerformer( $performer ); |
173 | $logEntry->setTarget( Title::makeTitle( NS_USER, $tempUser ) ); |
174 | $logEntry->setParameters( $params ); |
175 | |
176 | if ( $timestamp ) { |
177 | $logEntry->setTimestamp( wfTimestamp( TS_MW, $timestamp ) ); |
178 | } |
179 | |
180 | try { |
181 | $logEntry->insert( $this->dbw ); |
182 | } catch ( DBError $e ) { |
183 | $this->logger->critical( |
184 | 'CheckUser temporary account log entry was not recorded. ' . |
185 | 'This means checks can occur without being auditable. ' . |
186 | 'Immediate fix required.' |
187 | ); |
188 | } |
189 | } |
190 | |
191 | /** |
192 | * There is no `LogEntryFactory` (or `Logger::insert()` method) in MediaWiki Core to inject |
193 | * via the constructor so use this method to isolate the creation of `LogEntry` objects during |
194 | * testing. |
195 | * |
196 | * @private |
197 | * |
198 | * @param string $subtype |
199 | * @return ManualLogEntry |
200 | */ |
201 | protected function createManualLogEntry( string $subtype ): ManualLogEntry { |
202 | return new ManualLogEntry( self::LOG_TYPE, $subtype ); |
203 | } |
204 | } |