Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
94.44% |
51 / 54 |
|
75.00% |
6 / 8 |
CRAP | |
0.00% |
0 / 1 |
Logger | |
94.44% |
51 / 54 |
|
75.00% |
6 / 8 |
13.03 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
logViewInfobox | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
logViewPopup | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
2 | |||
logAccessEnabled | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
logAccessDisabled | |
100.00% |
4 / 4 |
|
100.00% |
1 / 1 |
1 | |||
debouncedLog | |
92.31% |
24 / 26 |
|
0.00% |
0 / 1 |
3.00 | |||
log | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
2 | |||
createManualLogEntry | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 |
1 | <?php |
2 | |
3 | namespace MediaWiki\IPInfo\Logging; |
4 | |
5 | use ManualLogEntry; |
6 | use MediaWiki\Title\Title; |
7 | use MediaWiki\User\ActorStore; |
8 | use MediaWiki\User\UserIdentity; |
9 | use Wikimedia\Assert\Assert; |
10 | use Wikimedia\Assert\ParameterAssertionException; |
11 | use Wikimedia\IPUtils; |
12 | use Wikimedia\Rdbms\IDatabase; |
13 | use Wikimedia\Rdbms\IExpression; |
14 | use Wikimedia\Rdbms\LikeValue; |
15 | |
16 | /** |
17 | * Defines the API for the component responsible for logging the following interactions: |
18 | * |
19 | * 1. A user views information about an IP via the infobox |
20 | * 2. A user views information about an IP via the popup |
21 | * 3. A user enables IP Info (via Special:Preferences) |
22 | * 4. A user disables IP Info |
23 | * |
24 | * 1 and 2 are debounced. By default, if the same user views information about the same IP via the |
25 | * same treatment within 24 hours, then only one such action should be logged. |
26 | * |
27 | * All the above interactions will be logged to the `logging` table with a log type `ipinfo`. |
28 | */ |
29 | class Logger { |
30 | /** |
31 | * Represents a user (the performer) viewing information about an IP via the infobox. |
32 | * |
33 | * @var string |
34 | */ |
35 | public const ACTION_VIEW_INFOBOX = 'view_infobox'; |
36 | |
37 | /** |
38 | * Represents a user (the performer) viewing information about an IP via the popup. |
39 | * |
40 | * @var string |
41 | */ |
42 | public const ACTION_VIEW_POPUP = 'view_popup'; |
43 | |
44 | /** |
45 | * Represents a user enabling or disabling their own access to IPInfo |
46 | * |
47 | * @var string |
48 | */ |
49 | public const ACTION_CHANGE_ACCESS = 'change_access'; |
50 | |
51 | /** |
52 | * @var string |
53 | */ |
54 | public const ACTION_ACCESS_ENABLED = 'enable'; |
55 | |
56 | /** |
57 | * @var string |
58 | */ |
59 | public const ACTION_ACCESS_DISABLED = 'disable'; |
60 | |
61 | /** |
62 | * @var string |
63 | */ |
64 | public const LOG_TYPE = 'ipinfo'; |
65 | |
66 | private ActorStore $actorStore; |
67 | |
68 | private IDatabase $dbw; |
69 | |
70 | private int $delay; |
71 | |
72 | /** |
73 | * @param ActorStore $actorStore |
74 | * @param IDatabase $dbw |
75 | * @param int $delay The number of seconds after which a duplicate log entry can be |
76 | * created by `Logger::logViewInfobox` or `Logger::logViewPopup` |
77 | * @throws ParameterAssertionException if `$delay` is less than 1 |
78 | */ |
79 | public function __construct( |
80 | ActorStore $actorStore, |
81 | IDatabase $dbw, |
82 | int $delay |
83 | ) { |
84 | Assert::parameter( $delay > 0, 'delay', 'delay must be positive' ); |
85 | |
86 | $this->actorStore = $actorStore; |
87 | $this->dbw = $dbw; |
88 | $this->delay = $delay; |
89 | } |
90 | |
91 | /** |
92 | * Logs the user (the performer) viewing information about an IP via the infobox. |
93 | * |
94 | * @param UserIdentity $performer |
95 | * @param string $ip |
96 | * @param int $timestamp |
97 | * @param string|null $level |
98 | */ |
99 | public function logViewInfobox( UserIdentity $performer, string $ip, int $timestamp, ?string $level ): void { |
100 | if ( !$level ) { |
101 | return; |
102 | } |
103 | $params = [ '4::level' => $level ]; |
104 | $this->debouncedLog( $performer, $ip, self::ACTION_VIEW_INFOBOX, $timestamp, $params ); |
105 | } |
106 | |
107 | /** |
108 | * Logs the user (the performer) viewing information about an IP via the popup. |
109 | * |
110 | * @param UserIdentity $performer |
111 | * @param string $ip |
112 | * @param int $timestamp |
113 | * @param string|null $level |
114 | */ |
115 | public function logViewPopup( UserIdentity $performer, string $ip, int $timestamp, ?string $level ): void { |
116 | if ( !$level ) { |
117 | return; |
118 | } |
119 | $params = [ '4::level' => $level ]; |
120 | $this->debouncedLog( $performer, $ip, self::ACTION_VIEW_POPUP, $timestamp, $params ); |
121 | } |
122 | |
123 | /** |
124 | * Log when the user enables their own access |
125 | * |
126 | * @param UserIdentity $performer |
127 | */ |
128 | public function logAccessEnabled( UserIdentity $performer ): void { |
129 | $params = [ |
130 | '4::changeType' => self::ACTION_ACCESS_ENABLED, |
131 | ]; |
132 | $this->log( $performer, $performer->getName(), self::ACTION_CHANGE_ACCESS, $params ); |
133 | } |
134 | |
135 | /** |
136 | * Log when the user disables their own access |
137 | * |
138 | * @param UserIdentity $performer |
139 | */ |
140 | public function logAccessDisabled( UserIdentity $performer ): void { |
141 | $params = [ |
142 | '4::changeType' => self::ACTION_ACCESS_DISABLED, |
143 | ]; |
144 | $this->log( $performer, $performer->getName(), self::ACTION_CHANGE_ACCESS, $params ); |
145 | } |
146 | |
147 | /** |
148 | * @param UserIdentity $performer |
149 | * @param string $ip |
150 | * @param string $action Either `Logger::ACTION_VIEW_INFOBOX` or |
151 | * `Logger::ACTION_VIEW_POPUP` |
152 | * @param int $timestamp |
153 | * @param array $params |
154 | */ |
155 | private function debouncedLog( |
156 | UserIdentity $performer, |
157 | string $ip, |
158 | string $action, |
159 | int $timestamp, |
160 | array $params |
161 | ): void { |
162 | $timestampMinusDelay = $timestamp - $this->delay; |
163 | $ip = IPUtils::sanitizeIP( $ip ); |
164 | $actorId = $this->actorStore->findActorId( $performer, $this->dbw ); |
165 | if ( !$actorId ) { |
166 | $this->log( $performer, $ip, $action, $params ); |
167 | return; |
168 | } |
169 | |
170 | $logLine = $this->dbw->newSelectQueryBuilder() |
171 | ->select( '*' ) |
172 | ->from( 'logging' ) |
173 | ->where( [ |
174 | 'log_type' => self::LOG_TYPE, |
175 | 'log_action' => $action, |
176 | 'log_actor' => $actorId, |
177 | 'log_namespace' => NS_USER, |
178 | 'log_title' => $ip, |
179 | $this->dbw->expr( 'log_timestamp', '>', $this->dbw->timestamp( $timestampMinusDelay ) ), |
180 | $this->dbw->expr( 'log_params', IExpression::LIKE, new LikeValue( |
181 | $this->dbw->anyString(), |
182 | $params['4::level'], |
183 | $this->dbw->anyString() |
184 | ) ), |
185 | ] ) |
186 | ->caller( __METHOD__ ) |
187 | ->fetchRow(); |
188 | |
189 | if ( !$logLine ) { |
190 | $this->log( $performer, $ip, $action, $params, $timestamp ); |
191 | } |
192 | } |
193 | |
194 | /** |
195 | * @param UserIdentity $performer |
196 | * @param string $ip |
197 | * @param string $action |
198 | * @param array $params |
199 | * @param int|null $timestamp |
200 | */ |
201 | private function log( |
202 | UserIdentity $performer, |
203 | string $ip, |
204 | string $action, |
205 | array $params, |
206 | ?int $timestamp = null |
207 | ): void { |
208 | $logEntry = $this->createManualLogEntry( $action ); |
209 | $logEntry->setPerformer( $performer ); |
210 | $logEntry->setTarget( Title::makeTitle( NS_USER, $ip ) ); |
211 | $logEntry->setParameters( $params ); |
212 | |
213 | if ( $timestamp ) { |
214 | $logEntry->setTimestamp( wfTimestamp( TS_MW, $timestamp ) ); |
215 | } |
216 | |
217 | $logEntry->insert( $this->dbw ); |
218 | } |
219 | |
220 | /** |
221 | * There is no `LogEntryFactory` (or `Logger::insert()` method) in MediaWiki Core to inject |
222 | * via the constructor, so use this method to isolate the creation of `LogEntry` objects during |
223 | * testing. |
224 | * |
225 | * @private |
226 | * |
227 | * @param string $subtype |
228 | * @return ManualLogEntry |
229 | */ |
230 | protected function createManualLogEntry( string $subtype ): ManualLogEntry { |
231 | return new ManualLogEntry( self::LOG_TYPE, $subtype ); |
232 | } |
233 | } |