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