Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
42.28% |
63 / 149 |
|
35.29% |
6 / 17 |
CRAP | |
0.00% |
0 / 1 |
LogPage | |
42.28% |
63 / 149 |
|
35.29% |
6 / 17 |
347.65 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
saveContent | |
0.00% |
0 / 43 |
|
0.00% |
0 / 1 |
30 | |||
getRcComment | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
getRcCommentIRC | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
3.03 | |||
getComment | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
validTypes | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
2 | |||
isLogType | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
actionText | |
66.67% |
18 / 27 |
|
0.00% |
0 / 1 |
7.33 | |||
getTitleLink | |
0.00% |
0 / 13 |
|
0.00% |
0 / 1 |
20 | |||
addEntry | |
90.48% |
19 / 21 |
|
0.00% |
0 / 1 |
3.01 | |||
addRelations | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
20 | |||
makeParamBlob | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
extractParams | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
getName | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getDescription | |
100.00% |
3 / 3 |
|
100.00% |
1 / 1 |
1 | |||
getRestriction | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
1 | |||
isRestricted | |
100.00% |
2 / 2 |
|
100.00% |
1 / 1 |
2 |
1 | <?php |
2 | /** |
3 | * Contain log classes |
4 | * |
5 | * Copyright © 2002, 2004 Brooke Vibber <bvibber@wikimedia.org> |
6 | * https://www.mediawiki.org/ |
7 | * |
8 | * This program is free software; you can redistribute it and/or modify |
9 | * it under the terms of the GNU General Public License as published by |
10 | * the Free Software Foundation; either version 2 of the License, or |
11 | * (at your option) any later version. |
12 | * |
13 | * This program is distributed in the hope that it will be useful, |
14 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
15 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
16 | * GNU General Public License for more details. |
17 | * |
18 | * You should have received a copy of the GNU General Public License along |
19 | * with this program; if not, write to the Free Software Foundation, Inc., |
20 | * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. |
21 | * http://www.gnu.org/copyleft/gpl.html |
22 | * |
23 | * @file |
24 | */ |
25 | |
26 | use MediaWiki\Context\RequestContext; |
27 | use MediaWiki\Language\Language; |
28 | use MediaWiki\MainConfigNames; |
29 | use MediaWiki\MediaWikiServices; |
30 | use MediaWiki\Message\Message; |
31 | use MediaWiki\SpecialPage\SpecialPage; |
32 | use MediaWiki\StubObject\StubUserLang; |
33 | use MediaWiki\Title\Title; |
34 | use MediaWiki\User\User; |
35 | use MediaWiki\User\UserIdentity; |
36 | |
37 | /** |
38 | * Class to simplify the use of log pages. |
39 | * The logs are now kept in a table which is easier to manage and trim |
40 | * than ever-growing wiki pages. |
41 | * |
42 | * @newable |
43 | * @note marked as newable in 1.35 for lack of a better alternative, |
44 | * but should become a stateless service, use the command pattern. |
45 | */ |
46 | class LogPage { |
47 | public const DELETED_ACTION = 1; |
48 | public const DELETED_COMMENT = 2; |
49 | public const DELETED_USER = 4; |
50 | public const DELETED_RESTRICTED = 8; |
51 | |
52 | // Convenience fields |
53 | public const SUPPRESSED_USER = self::DELETED_USER | self::DELETED_RESTRICTED; |
54 | public const SUPPRESSED_ACTION = self::DELETED_ACTION | self::DELETED_RESTRICTED; |
55 | |
56 | /** @var bool */ |
57 | public $updateRecentChanges; |
58 | |
59 | /** @var bool */ |
60 | public $sendToUDP; |
61 | |
62 | /** @var string Plaintext version of the message for IRC */ |
63 | private $ircActionText; |
64 | |
65 | /** @var string Plaintext version of the message */ |
66 | private $actionText; |
67 | |
68 | /** @var string One of '', 'block', 'protect', 'rights', 'delete', |
69 | * 'upload', 'move' |
70 | */ |
71 | private $type; |
72 | |
73 | /** @var string One of '', 'block', 'protect', 'rights', 'delete', |
74 | * 'upload', 'move', 'move_redir' |
75 | */ |
76 | private $action; |
77 | |
78 | /** @var string Comment associated with action */ |
79 | private $comment; |
80 | |
81 | /** @var string Blob made of a parameters array */ |
82 | private $params; |
83 | |
84 | /** @var UserIdentity The user doing the action */ |
85 | private $performer; |
86 | |
87 | /** @var Title */ |
88 | private $target; |
89 | |
90 | /** |
91 | * @stable to call |
92 | * @param string $type One of '', 'block', 'protect', 'rights', 'delete', |
93 | * 'upload', 'move' |
94 | * @param bool $rc Whether to update recent changes as well as the logging table |
95 | * @param string $udp Pass 'UDP' to send to the UDP feed if NOT sent to RC |
96 | */ |
97 | public function __construct( $type, $rc = true, $udp = 'skipUDP' ) { |
98 | $this->type = $type; |
99 | $this->updateRecentChanges = $rc; |
100 | $this->sendToUDP = ( $udp == 'UDP' ); |
101 | } |
102 | |
103 | /** |
104 | * @return int The log_id of the inserted log entry |
105 | */ |
106 | protected function saveContent() { |
107 | $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions ); |
108 | |
109 | $dbw = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase(); |
110 | |
111 | $now = wfTimestampNow(); |
112 | $actorId = MediaWikiServices::getInstance()->getActorNormalization() |
113 | ->acquireActorId( $this->performer, $dbw ); |
114 | $data = [ |
115 | 'log_type' => $this->type, |
116 | 'log_action' => $this->action, |
117 | 'log_timestamp' => $dbw->timestamp( $now ), |
118 | 'log_actor' => $actorId, |
119 | 'log_namespace' => $this->target->getNamespace(), |
120 | 'log_title' => $this->target->getDBkey(), |
121 | 'log_page' => $this->target->getArticleID(), |
122 | 'log_params' => $this->params |
123 | ]; |
124 | $data += MediaWikiServices::getInstance()->getCommentStore()->insert( |
125 | $dbw, |
126 | 'log_comment', |
127 | $this->comment |
128 | ); |
129 | $dbw->newInsertQueryBuilder() |
130 | ->insertInto( 'logging' ) |
131 | ->row( $data ) |
132 | ->caller( __METHOD__ )->execute(); |
133 | $newId = $dbw->insertId(); |
134 | |
135 | # And update recentchanges |
136 | if ( $this->updateRecentChanges ) { |
137 | $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); |
138 | |
139 | RecentChange::notifyLog( |
140 | $now, $titleObj, $this->performer, $this->getRcComment(), '', |
141 | $this->type, $this->action, $this->target, $this->comment, |
142 | $this->params, $newId, $this->getRcCommentIRC() |
143 | ); |
144 | } elseif ( $this->sendToUDP ) { |
145 | # Don't send private logs to UDP |
146 | if ( isset( $logRestrictions[$this->type] ) && $logRestrictions[$this->type] != '*' ) { |
147 | return $newId; |
148 | } |
149 | |
150 | // Notify external application via UDP. |
151 | // We send this to IRC but do not want to add it the RC table. |
152 | $titleObj = SpecialPage::getTitleFor( 'Log', $this->type ); |
153 | $rc = RecentChange::newLogEntry( |
154 | $now, $titleObj, $this->performer, $this->getRcComment(), '', |
155 | $this->type, $this->action, $this->target, $this->comment, |
156 | $this->params, $newId, $this->getRcCommentIRC() |
157 | ); |
158 | $rc->notifyRCFeeds(); |
159 | } |
160 | |
161 | return $newId; |
162 | } |
163 | |
164 | /** |
165 | * Get the RC comment from the last addEntry() call |
166 | * |
167 | * @return string |
168 | */ |
169 | public function getRcComment() { |
170 | $rcComment = $this->actionText; |
171 | |
172 | if ( $this->comment != '' ) { |
173 | if ( $rcComment == '' ) { |
174 | $rcComment = $this->comment; |
175 | } else { |
176 | $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . |
177 | $this->comment; |
178 | } |
179 | } |
180 | |
181 | return $rcComment; |
182 | } |
183 | |
184 | /** |
185 | * Get the RC comment from the last addEntry() call for IRC |
186 | * |
187 | * @return string |
188 | */ |
189 | public function getRcCommentIRC() { |
190 | $rcComment = $this->ircActionText; |
191 | |
192 | if ( $this->comment != '' ) { |
193 | if ( $rcComment == '' ) { |
194 | $rcComment = $this->comment; |
195 | } else { |
196 | $rcComment .= wfMessage( 'colon-separator' )->inContentLanguage()->text() . |
197 | $this->comment; |
198 | } |
199 | } |
200 | |
201 | return $rcComment; |
202 | } |
203 | |
204 | /** |
205 | * Get the comment from the last addEntry() call |
206 | * @return string |
207 | */ |
208 | public function getComment() { |
209 | return $this->comment; |
210 | } |
211 | |
212 | /** |
213 | * Get the list of valid log types |
214 | * |
215 | * @return string[] |
216 | */ |
217 | public static function validTypes() { |
218 | $logTypes = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogTypes ); |
219 | |
220 | return $logTypes; |
221 | } |
222 | |
223 | /** |
224 | * Is $type a valid log type |
225 | * |
226 | * @param string $type Log type to check |
227 | * @return bool |
228 | */ |
229 | public static function isLogType( $type ) { |
230 | return in_array( $type, self::validTypes() ); |
231 | } |
232 | |
233 | /** |
234 | * Generate text for a log entry. |
235 | * Only LogFormatter should call this function. |
236 | * |
237 | * @param string $type Log type |
238 | * @param string $action Log action |
239 | * @param Title|null $title |
240 | * @param Skin|null $skin Skin object or null. If null, we want to use the wiki |
241 | * content language, since that will go to the IRC feed. |
242 | * @param array $params |
243 | * @param bool $filterWikilinks Whether to filter wiki links |
244 | * @return string HTML |
245 | */ |
246 | public static function actionText( $type, $action, $title = null, $skin = null, |
247 | $params = [], $filterWikilinks = false |
248 | ) { |
249 | global $wgLang; |
250 | $config = MediaWikiServices::getInstance()->getMainConfig(); |
251 | $key = "$type/$action"; |
252 | |
253 | $logActions = $config->get( MainConfigNames::LogActions ); |
254 | |
255 | if ( isset( $logActions[$key] ) ) { |
256 | $message = $logActions[$key]; |
257 | } else { |
258 | wfDebug( "LogPage::actionText - unknown action $key" ); |
259 | $message = "log-unknown-action"; |
260 | $params = [ $key ]; |
261 | } |
262 | |
263 | if ( $skin === null ) { |
264 | $langObj = MediaWikiServices::getInstance()->getContentLanguage(); |
265 | $langObjOrNull = null; |
266 | } else { |
267 | // TODO Is $skin->getLanguage() safe here? |
268 | StubUserLang::unstub( $wgLang ); |
269 | $langObj = $wgLang; |
270 | $langObjOrNull = $wgLang; |
271 | } |
272 | if ( $title === null ) { |
273 | $rv = wfMessage( $message )->inLanguage( $langObj )->escaped(); |
274 | } else { |
275 | $titleLink = self::getTitleLink( $title, $langObjOrNull ); |
276 | |
277 | if ( count( $params ) == 0 ) { |
278 | $rv = wfMessage( $message )->rawParams( $titleLink ) |
279 | ->inLanguage( $langObj )->escaped(); |
280 | } else { |
281 | array_unshift( $params, $titleLink ); |
282 | |
283 | $rv = wfMessage( $message )->rawParams( $params ) |
284 | ->inLanguage( $langObj )->escaped(); |
285 | } |
286 | } |
287 | |
288 | // For the perplexed, this feature was added in r7855 by Erik. |
289 | // The feature was added because we liked adding [[$1]] in our log entries |
290 | // but the log entries are parsed as Wikitext on RecentChanges but as HTML |
291 | // on Special:Log. The hack is essentially that [[$1]] represented a link |
292 | // to the title in question. The first parameter to the HTML version (Special:Log) |
293 | // is that link in HTML form, and so this just gets rid of the ugly [[]]. |
294 | // However, this is a horrible hack and it doesn't work like you expect if, say, |
295 | // you want to link to something OTHER than the title of the log entry. |
296 | // The real problem, which Erik was trying to fix (and it sort-of works now) is |
297 | // that the same messages are being treated as both wikitext *and* HTML. |
298 | if ( $filterWikilinks ) { |
299 | $rv = str_replace( '[[', '', $rv ); |
300 | $rv = str_replace( ']]', '', $rv ); |
301 | } |
302 | |
303 | return $rv; |
304 | } |
305 | |
306 | /** |
307 | * @param Title $title |
308 | * @param ?Language $lang |
309 | * @return string HTML |
310 | */ |
311 | private static function getTitleLink( Title $title, ?Language $lang ): string { |
312 | if ( !$lang ) { |
313 | return $title->getPrefixedText(); |
314 | } |
315 | |
316 | $services = MediaWikiServices::getInstance(); |
317 | $linkRenderer = $services->getLinkRenderer(); |
318 | |
319 | if ( $title->isSpecialPage() ) { |
320 | [ $name, $par ] = $services->getSpecialPageFactory()->resolveAlias( $title->getDBkey() ); |
321 | |
322 | if ( $name === 'Log' ) { |
323 | $logPage = new LogPage( $par ); |
324 | return wfMessage( 'parentheses' ) |
325 | ->rawParams( $linkRenderer->makeLink( $title, $logPage->getName()->text() ) ) |
326 | ->inLanguage( $lang ) |
327 | ->escaped(); |
328 | } |
329 | } |
330 | |
331 | return $linkRenderer->makeLink( $title ); |
332 | } |
333 | |
334 | /** |
335 | * Add a log entry |
336 | * |
337 | * @param string $action One of '', 'block', 'protect', 'rights', 'delete', |
338 | * 'upload', 'move', 'move_redir' |
339 | * @param Title $target |
340 | * @param string|null $comment Description associated |
341 | * @param array $params Parameters passed later to wfMessage function |
342 | * @param int|UserIdentity $performer The user doing the action, or their user id. |
343 | * Calling with user ID is deprecated since 1.36. |
344 | * |
345 | * @return int The log_id of the inserted log entry |
346 | */ |
347 | public function addEntry( $action, $target, $comment, $params, $performer ) { |
348 | // FIXME $params is only documented to accept an array |
349 | if ( !is_array( $params ) ) { |
350 | $params = [ $params ]; |
351 | } |
352 | |
353 | # Trim spaces on user supplied text |
354 | $comment = trim( $comment ?? '' ); |
355 | |
356 | $this->action = $action; |
357 | $this->target = $target; |
358 | $this->comment = $comment; |
359 | $this->params = self::makeParamBlob( $params ); |
360 | |
361 | if ( !is_object( $performer ) ) { |
362 | $performer = User::newFromId( $performer ); |
363 | } |
364 | |
365 | $this->performer = $performer; |
366 | |
367 | $logEntry = new ManualLogEntry( $this->type, $action ); |
368 | $logEntry->setTarget( $target ); |
369 | $logEntry->setPerformer( $performer ); |
370 | $logEntry->setParameters( $params ); |
371 | // All log entries using the LogPage to insert into the logging table |
372 | // are using the old logging system and therefore the legacy flag is |
373 | // needed to say the LogFormatter the parameters have numeric keys |
374 | $logEntry->setLegacy( true ); |
375 | |
376 | $formatter = MediaWikiServices::getInstance()->getLogFormatterFactory()->newFromEntry( $logEntry ); |
377 | $context = RequestContext::newExtraneousContext( $target ); |
378 | $formatter->setContext( $context ); |
379 | |
380 | $this->actionText = $formatter->getPlainActionText(); |
381 | $this->ircActionText = $formatter->getIRCActionText(); |
382 | |
383 | return $this->saveContent(); |
384 | } |
385 | |
386 | /** |
387 | * Add relations to log_search table |
388 | * |
389 | * @param string $field |
390 | * @param array $values |
391 | * @param int $logid |
392 | * @return bool |
393 | */ |
394 | public function addRelations( $field, $values, $logid ) { |
395 | if ( !strlen( $field ) || !$values ) { |
396 | return false; |
397 | } |
398 | $insert = MediaWikiServices::getInstance()->getConnectionProvider()->getPrimaryDatabase() |
399 | ->newInsertQueryBuilder() |
400 | ->insertInto( 'log_search' ) |
401 | ->ignore(); |
402 | foreach ( $values as $value ) { |
403 | $insert->row( [ 'ls_field' => $field, 'ls_value' => $value, 'ls_log_id' => $logid ] ); |
404 | } |
405 | $insert->caller( __METHOD__ )->execute(); |
406 | |
407 | return true; |
408 | } |
409 | |
410 | /** |
411 | * Create a blob from a parameter array |
412 | * |
413 | * @param array $params |
414 | * @return string |
415 | */ |
416 | public static function makeParamBlob( $params ) { |
417 | return implode( "\n", $params ); |
418 | } |
419 | |
420 | /** |
421 | * Extract a parameter array from a blob |
422 | * |
423 | * @param string $blob |
424 | * @return array |
425 | */ |
426 | public static function extractParams( $blob ) { |
427 | if ( $blob === '' ) { |
428 | return []; |
429 | } else { |
430 | return explode( "\n", $blob ); |
431 | } |
432 | } |
433 | |
434 | /** |
435 | * Name of the log. |
436 | * @return Message |
437 | * @since 1.19 |
438 | */ |
439 | public function getName() { |
440 | $logNames = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogNames ); |
441 | |
442 | // BC |
443 | $key = $logNames[$this->type] ?? 'log-name-' . $this->type; |
444 | |
445 | return wfMessage( $key ); |
446 | } |
447 | |
448 | /** |
449 | * Description of this log type. |
450 | * @return Message |
451 | * @since 1.19 |
452 | */ |
453 | public function getDescription() { |
454 | $logHeaders = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogHeaders ); |
455 | // BC |
456 | $key = $logHeaders[$this->type] ?? 'log-description-' . $this->type; |
457 | |
458 | return wfMessage( $key ); |
459 | } |
460 | |
461 | /** |
462 | * Returns the right needed to read this log type. |
463 | * @return string |
464 | * @since 1.19 |
465 | */ |
466 | public function getRestriction() { |
467 | $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions ); |
468 | // The empty string fallback will |
469 | // always return true in permission check |
470 | return $logRestrictions[$this->type] ?? ''; |
471 | } |
472 | |
473 | /** |
474 | * Tells if this log is not viewable by all. |
475 | * @return bool |
476 | * @since 1.19 |
477 | */ |
478 | public function isRestricted() { |
479 | $restriction = $this->getRestriction(); |
480 | |
481 | return $restriction !== '' && $restriction !== '*'; |
482 | } |
483 | } |