Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
0.00% |
0 / 371 |
|
0.00% |
0 / 18 |
CRAP | |
0.00% |
0 / 1 |
LogEventsList | |
0.00% |
0 / 371 |
|
0.00% |
0 / 18 |
8190 | |
0.00% |
0 / 1 |
__construct | |
0.00% |
0 / 7 |
|
0.00% |
0 / 1 |
6 | |||
getLinkRenderer | |
0.00% |
0 / 3 |
|
0.00% |
0 / 1 |
6 | |||
showOptions | |
0.00% |
0 / 64 |
|
0.00% |
0 / 1 |
110 | |||
getFiltersDesc | |
0.00% |
0 / 11 |
|
0.00% |
0 / 1 |
6 | |||
getTypeMenuDesc | |
0.00% |
0 / 26 |
|
0.00% |
0 / 1 |
20 | |||
getExtraInputsDesc | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
getActionSelectorDesc | |
0.00% |
0 / 10 |
|
0.00% |
0 / 1 |
6 | |||
beginLogEventsList | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
endLogEventsList | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
logLine | |
0.00% |
0 / 51 |
|
0.00% |
0 / 1 |
12 | |||
getShowHideLinks | |
0.00% |
0 / 40 |
|
0.00% |
0 / 1 |
272 | |||
typeAction | |
0.00% |
0 / 6 |
|
0.00% |
0 / 1 |
20 | |||
userCan | |
0.00% |
0 / 2 |
|
0.00% |
0 / 1 |
6 | |||
userCanBitfield | |
0.00% |
0 / 5 |
|
0.00% |
0 / 1 |
12 | |||
userCanViewLogType | |
0.00% |
0 / 4 |
|
0.00% |
0 / 1 |
12 | |||
isDeleted | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
showLogExtract | |
0.00% |
0 / 115 |
|
0.00% |
0 / 1 |
600 | |||
getExcludeClause | |
0.00% |
0 / 14 |
|
0.00% |
0 / 1 |
72 |
1 | <?php |
2 | /** |
3 | * Contain classes to list log entries |
4 | * |
5 | * Copyright © 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\ContextSource; |
27 | use MediaWiki\Context\IContextSource; |
28 | use MediaWiki\Context\RequestContext; |
29 | use MediaWiki\HookContainer\HookRunner; |
30 | use MediaWiki\Html\Html; |
31 | use MediaWiki\Linker\Linker; |
32 | use MediaWiki\Linker\LinkRenderer; |
33 | use MediaWiki\Logger\LoggerFactory; |
34 | use MediaWiki\MainConfigNames; |
35 | use MediaWiki\MediaWikiServices; |
36 | use MediaWiki\Output\OutputPage; |
37 | use MediaWiki\Page\PageReference; |
38 | use MediaWiki\Pager\LogPager; |
39 | use MediaWiki\Parser\Sanitizer; |
40 | use MediaWiki\Permissions\Authority; |
41 | use MediaWiki\SpecialPage\SpecialPage; |
42 | use MediaWiki\Status\Status; |
43 | |
44 | class LogEventsList extends ContextSource { |
45 | public const NO_ACTION_LINK = 1; |
46 | public const NO_EXTRA_USER_LINKS = 2; |
47 | public const USE_CHECKBOXES = 4; |
48 | |
49 | public $flags; |
50 | |
51 | /** |
52 | * @var bool |
53 | */ |
54 | protected $showTagEditUI; |
55 | |
56 | /** |
57 | * @var LinkRenderer|null |
58 | */ |
59 | private $linkRenderer; |
60 | |
61 | /** @var HookRunner */ |
62 | private $hookRunner; |
63 | |
64 | /** @var MapCacheLRU */ |
65 | private $tagsCache; |
66 | |
67 | /** |
68 | * @param IContextSource $context |
69 | * @param LinkRenderer|null $linkRenderer |
70 | * @param int $flags Can be a combination of self::NO_ACTION_LINK, |
71 | * self::NO_EXTRA_USER_LINKS or self::USE_CHECKBOXES. |
72 | */ |
73 | public function __construct( $context, $linkRenderer = null, $flags = 0 ) { |
74 | $this->setContext( $context ); |
75 | $this->flags = $flags; |
76 | $this->showTagEditUI = ChangeTags::showTagEditingUI( $this->getAuthority() ); |
77 | if ( $linkRenderer instanceof LinkRenderer ) { |
78 | $this->linkRenderer = $linkRenderer; |
79 | } |
80 | $this->hookRunner = new HookRunner( MediaWikiServices::getInstance()->getHookContainer() ); |
81 | $this->tagsCache = new MapCacheLRU( 50 ); |
82 | } |
83 | |
84 | /** |
85 | * @since 1.30 |
86 | * @return LinkRenderer |
87 | */ |
88 | protected function getLinkRenderer() { |
89 | if ( $this->linkRenderer !== null ) { |
90 | return $this->linkRenderer; |
91 | } else { |
92 | return MediaWikiServices::getInstance()->getLinkRenderer(); |
93 | } |
94 | } |
95 | |
96 | /** |
97 | * Show options for the log list |
98 | * |
99 | * @param string $type Log type |
100 | * @param int|string $year Use 0 to start with no year preselected. |
101 | * @param int|string $month A month in the 1..12 range. Use 0 to start with no month |
102 | * preselected. |
103 | * @param int|string $day A day in the 1..31 range. Use 0 to start with no month |
104 | * preselected. |
105 | * @return bool Whether the options are valid |
106 | */ |
107 | public function showOptions( $type = '', $year = 0, $month = 0, $day = 0 ) { |
108 | $formDescriptor = []; |
109 | |
110 | // Basic selectors |
111 | $formDescriptor['type'] = $this->getTypeMenuDesc(); |
112 | $formDescriptor['user'] = [ |
113 | 'class' => HTMLUserTextField::class, |
114 | 'label-message' => 'specialloguserlabel', |
115 | 'name' => 'user', |
116 | 'ipallowed' => true, |
117 | 'iprange' => true, |
118 | 'external' => true, |
119 | ]; |
120 | $formDescriptor['page'] = [ |
121 | 'class' => HTMLTitleTextField::class, |
122 | 'label-message' => 'speciallogtitlelabel', |
123 | 'name' => 'page', |
124 | 'required' => false, |
125 | ]; |
126 | |
127 | // Title pattern, if allowed |
128 | if ( !$this->getConfig()->get( MainConfigNames::MiserMode ) ) { |
129 | $formDescriptor['pattern'] = [ |
130 | 'type' => 'check', |
131 | 'label-message' => 'log-title-wildcard', |
132 | 'name' => 'pattern', |
133 | ]; |
134 | } |
135 | |
136 | // Add extra inputs if any |
137 | $extraInputsDescriptor = $this->getExtraInputsDesc( $type ); |
138 | if ( $extraInputsDescriptor ) { |
139 | $formDescriptor[ 'extra' ] = $extraInputsDescriptor; |
140 | } |
141 | |
142 | // Date menu |
143 | $formDescriptor['date'] = [ |
144 | 'type' => 'date', |
145 | 'label-message' => 'date', |
146 | 'default' => $year && $month && $day ? sprintf( "%04d-%02d-%02d", $year, $month, $day ) : '', |
147 | ]; |
148 | |
149 | // Tag filter |
150 | $formDescriptor['tagfilter'] = [ |
151 | 'type' => 'tagfilter', |
152 | 'name' => 'tagfilter', |
153 | 'label-message' => 'tag-filter', |
154 | ]; |
155 | $formDescriptor['tagInvert'] = [ |
156 | 'type' => 'check', |
157 | 'name' => 'tagInvert', |
158 | 'label-message' => 'invert', |
159 | 'hide-if' => [ '===', 'tagfilter', '' ], |
160 | ]; |
161 | |
162 | // Filter checkboxes, when work on all logs |
163 | if ( $type === '' ) { |
164 | $formDescriptor['filters'] = $this->getFiltersDesc(); |
165 | } |
166 | |
167 | // Action filter |
168 | $allowedActions = $this->getConfig()->get( MainConfigNames::ActionFilteredLogs ); |
169 | if ( isset( $allowedActions[$type] ) ) { |
170 | $formDescriptor['subtype'] = $this->getActionSelectorDesc( $type, $allowedActions[$type] ); |
171 | } |
172 | |
173 | $htmlForm = HTMLForm::factory( 'ooui', $formDescriptor, $this->getContext() ); |
174 | $htmlForm |
175 | ->setTitle( SpecialPage::getTitleFor( 'Log' ) ) // Remove subpage |
176 | ->setSubmitTextMsg( 'logeventslist-submit' ) |
177 | ->setMethod( 'GET' ) |
178 | ->setWrapperLegendMsg( 'log' ) |
179 | ->setFormIdentifier( 'logeventslist', true ) // T321154 |
180 | // Set callback for data validation and log type description. |
181 | ->setSubmitCallback( static function ( $formData, $form ) { |
182 | $form->addPreHtml( |
183 | ( new LogPage( $formData['type'] ) )->getDescription() |
184 | ->setContext( $form->getContext() )->parseAsBlock() |
185 | ); |
186 | return true; |
187 | } ); |
188 | |
189 | $result = $htmlForm->prepareForm()->trySubmit(); |
190 | $htmlForm->displayForm( $result ); |
191 | return $result === true || ( $result instanceof Status && $result->isGood() ); |
192 | } |
193 | |
194 | /** |
195 | * @return array Form descriptor |
196 | */ |
197 | private function getFiltersDesc() { |
198 | $optionsMsg = []; |
199 | $filters = $this->getConfig()->get( MainConfigNames::FilterLogTypes ); |
200 | foreach ( $filters as $type => $val ) { |
201 | $optionsMsg["logeventslist-{$type}-log"] = $type; |
202 | } |
203 | return [ |
204 | 'class' => HTMLMultiSelectField::class, |
205 | 'label-message' => 'logeventslist-more-filters', |
206 | 'flatlist' => true, |
207 | 'options-messages' => $optionsMsg, |
208 | 'default' => array_keys( array_intersect( $filters, [ false ] ) ), |
209 | ]; |
210 | } |
211 | |
212 | /** |
213 | * @return array Form descriptor |
214 | */ |
215 | private function getTypeMenuDesc() { |
216 | $typesByName = []; |
217 | // Load the log names |
218 | foreach ( LogPage::validTypes() as $type ) { |
219 | $page = new LogPage( $type ); |
220 | $pageText = $page->getName()->text(); |
221 | if ( in_array( $pageText, $typesByName ) ) { |
222 | LoggerFactory::getInstance( 'error' )->error( |
223 | 'The log type {log_type_one} has the same translation as {log_type_two} for {lang}. ' . |
224 | '{log_type_one} will not be displayed in the drop down menu on Special:Log.', |
225 | [ |
226 | 'log_type_one' => $type, |
227 | 'log_type_two' => array_search( $pageText, $typesByName ), |
228 | 'lang' => $this->getLanguage()->getCode(), |
229 | ] |
230 | ); |
231 | continue; |
232 | } |
233 | if ( $this->getAuthority()->isAllowed( $page->getRestriction() ) ) { |
234 | $typesByName[$type] = $pageText; |
235 | } |
236 | } |
237 | |
238 | asort( $typesByName ); |
239 | |
240 | // Always put "All public logs" on top |
241 | $public = $typesByName['']; |
242 | unset( $typesByName[''] ); |
243 | $typesByName = [ '' => $public ] + $typesByName; |
244 | |
245 | return [ |
246 | 'class' => HTMLSelectField::class, |
247 | 'name' => 'type', |
248 | 'options' => array_flip( $typesByName ), |
249 | ]; |
250 | } |
251 | |
252 | /** |
253 | * @param string $type |
254 | * @return array Form descriptor |
255 | */ |
256 | private function getExtraInputsDesc( $type ) { |
257 | if ( $type === 'suppress' ) { |
258 | return [ |
259 | 'type' => 'text', |
260 | 'label-message' => 'revdelete-offender', |
261 | 'name' => 'offender', |
262 | ]; |
263 | } else { |
264 | // Allow extensions to add an extra input into the descriptor array. |
265 | $unused = ''; // Deprecated since 1.32, removed in 1.41 |
266 | $formDescriptor = []; |
267 | $this->hookRunner->onLogEventsListGetExtraInputs( $type, $this, $unused, $formDescriptor ); |
268 | |
269 | return $formDescriptor; |
270 | } |
271 | } |
272 | |
273 | /** |
274 | * Drop down menu for selection of actions that can be used to filter the log |
275 | * @param string $type |
276 | * @param array $actions |
277 | * @return array Form descriptor |
278 | */ |
279 | private function getActionSelectorDesc( $type, $actions ) { |
280 | $actionOptions = [ 'log-action-filter-all' => '' ]; |
281 | |
282 | foreach ( $actions as $value => $_ ) { |
283 | $msgKey = "log-action-filter-$type-$value"; |
284 | $actionOptions[ $msgKey ] = $value; |
285 | } |
286 | |
287 | return [ |
288 | 'class' => HTMLSelectField::class, |
289 | 'name' => 'subtype', |
290 | 'options-messages' => $actionOptions, |
291 | 'label-message' => 'log-action-filter-' . $type, |
292 | ]; |
293 | } |
294 | |
295 | /** |
296 | * @return string |
297 | */ |
298 | public function beginLogEventsList() { |
299 | return "<ul class='mw-logevent-loglines'>\n"; |
300 | } |
301 | |
302 | /** |
303 | * @return string |
304 | */ |
305 | public function endLogEventsList() { |
306 | return "</ul>\n"; |
307 | } |
308 | |
309 | /** |
310 | * @param stdClass $row A single row from the result set |
311 | * @return string Formatted HTML list item |
312 | */ |
313 | public function logLine( $row ) { |
314 | $entry = DatabaseLogEntry::newFromRow( $row ); |
315 | $formatter = LogFormatter::newFromEntry( $entry ); |
316 | $formatter->setContext( $this->getContext() ); |
317 | $formatter->setLinkRenderer( $this->getLinkRenderer() ); |
318 | $formatter->setShowUserToolLinks( !( $this->flags & self::NO_EXTRA_USER_LINKS ) ); |
319 | |
320 | $time = $this->getLanguage()->userTimeAndDate( |
321 | $entry->getTimestamp(), |
322 | $this->getUser() |
323 | ); |
324 | // Link the time text to the specific log entry, see T207562 |
325 | $timeLink = $this->getLinkRenderer()->makeKnownLink( |
326 | SpecialPage::getTitleValueFor( 'Log' ), |
327 | $time, |
328 | [], |
329 | [ 'logid' => $entry->getId() ] |
330 | ); |
331 | |
332 | $action = $formatter->getActionText(); |
333 | |
334 | if ( $this->flags & self::NO_ACTION_LINK ) { |
335 | $revert = ''; |
336 | } else { |
337 | $revert = $formatter->getActionLinks(); |
338 | if ( $revert != '' ) { |
339 | $revert = '<span class="mw-logevent-actionlink">' . $revert . '</span>'; |
340 | } |
341 | } |
342 | |
343 | $comment = $formatter->getComment(); |
344 | |
345 | // Some user can hide log items and have review links |
346 | $del = $this->getShowHideLinks( $row ); |
347 | |
348 | // Any tags... |
349 | [ $tagDisplay, $newClasses ] = $this->tagsCache->getWithSetCallback( |
350 | $this->tagsCache->makeKey( |
351 | $row->ts_tags ?? '', |
352 | $this->getUser()->getName(), |
353 | $this->getLanguage()->getCode() |
354 | ), |
355 | fn () => ChangeTags::formatSummaryRow( |
356 | $row->ts_tags, |
357 | 'logevent', |
358 | $this->getContext() |
359 | ) |
360 | ); |
361 | $classes = array_merge( |
362 | [ 'mw-logline-' . $entry->getType() ], |
363 | $newClasses |
364 | ); |
365 | $attribs = [ |
366 | 'data-mw-logid' => $entry->getId(), |
367 | 'data-mw-logaction' => $entry->getFullType(), |
368 | ]; |
369 | $ret = "$del $timeLink $action $comment $revert $tagDisplay"; |
370 | |
371 | // Let extensions add data |
372 | $this->hookRunner->onLogEventsListLineEnding( $this, $ret, $entry, $classes, $attribs ); |
373 | $attribs = array_filter( $attribs, |
374 | [ Sanitizer::class, 'isReservedDataAttribute' ], |
375 | ARRAY_FILTER_USE_KEY |
376 | ); |
377 | $attribs['class'] = $classes; |
378 | |
379 | return Html::rawElement( 'li', $attribs, $ret ) . "\n"; |
380 | } |
381 | |
382 | /** |
383 | * @param stdClass $row |
384 | * @return string |
385 | */ |
386 | private function getShowHideLinks( $row ) { |
387 | // We don't want to see the links and |
388 | if ( $this->flags == self::NO_ACTION_LINK ) { |
389 | return ''; |
390 | } |
391 | |
392 | // If change tag editing is available to this user, return the checkbox |
393 | if ( $this->flags & self::USE_CHECKBOXES && $this->showTagEditUI ) { |
394 | return Xml::check( |
395 | 'showhiderevisions', |
396 | false, |
397 | [ 'name' => 'ids[' . $row->log_id . ']' ] |
398 | ); |
399 | } |
400 | |
401 | // no one can hide items from the suppress log. |
402 | if ( $row->log_type == 'suppress' ) { |
403 | return ''; |
404 | } |
405 | |
406 | $del = ''; |
407 | $authority = $this->getAuthority(); |
408 | // Don't show useless checkbox to people who cannot hide log entries |
409 | if ( $authority->isAllowed( 'deletedhistory' ) ) { |
410 | $canHide = $authority->isAllowed( 'deletelogentry' ); |
411 | $canViewSuppressedOnly = $authority->isAllowed( 'viewsuppressed' ) && |
412 | !$authority->isAllowed( 'suppressrevision' ); |
413 | $entryIsSuppressed = self::isDeleted( $row, LogPage::DELETED_RESTRICTED ); |
414 | $canViewThisSuppressedEntry = $canViewSuppressedOnly && $entryIsSuppressed; |
415 | if ( $row->log_deleted || $canHide ) { |
416 | // Show checkboxes instead of links. |
417 | if ( $canHide && $this->flags & self::USE_CHECKBOXES && !$canViewThisSuppressedEntry ) { |
418 | // If event was hidden from sysops |
419 | if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $authority ) ) { |
420 | $del = Xml::check( 'deleterevisions', false, [ 'disabled' => 'disabled' ] ); |
421 | } else { |
422 | $del = Xml::check( |
423 | 'showhiderevisions', |
424 | false, |
425 | [ 'name' => 'ids[' . $row->log_id . ']' ] |
426 | ); |
427 | } |
428 | } else { |
429 | // If event was hidden from sysops |
430 | if ( !self::userCan( $row, LogPage::DELETED_RESTRICTED, $authority ) ) { |
431 | $del = Linker::revDeleteLinkDisabled( $canHide ); |
432 | } else { |
433 | $query = [ |
434 | 'target' => SpecialPage::getTitleFor( 'Log', $row->log_type )->getPrefixedDBkey(), |
435 | 'type' => 'logging', |
436 | 'ids' => $row->log_id, |
437 | ]; |
438 | $del = Linker::revDeleteLink( |
439 | $query, |
440 | $entryIsSuppressed, |
441 | $canHide && !$canViewThisSuppressedEntry |
442 | ); |
443 | } |
444 | } |
445 | } |
446 | } |
447 | |
448 | return $del; |
449 | } |
450 | |
451 | /** |
452 | * @param stdClass $row |
453 | * @param string|array $type |
454 | * @param string|array $action |
455 | * @return bool |
456 | */ |
457 | public static function typeAction( $row, $type, $action ) { |
458 | $match = is_array( $type ) ? |
459 | in_array( $row->log_type, $type ) : $row->log_type == $type; |
460 | if ( $match ) { |
461 | $match = is_array( $action ) ? |
462 | in_array( $row->log_action, $action ) : $row->log_action == $action; |
463 | } |
464 | |
465 | return $match; |
466 | } |
467 | |
468 | /** |
469 | * Determine if the current user is allowed to view a particular |
470 | * field of this log row, if it's marked as deleted and/or restricted log type. |
471 | * |
472 | * @param stdClass $row |
473 | * @param int $field One of LogPage::DELETED_ACTION, ::DELETED_COMMENT, ::DELETED_USER, ::DELETED_RESTRICTED |
474 | * @param Authority $performer User to check |
475 | * @return bool |
476 | */ |
477 | public static function userCan( $row, $field, Authority $performer ) { |
478 | return self::userCanBitfield( $row->log_deleted, $field, $performer ) && |
479 | self::userCanViewLogType( $row->log_type, $performer ); |
480 | } |
481 | |
482 | /** |
483 | * Determine if the current user is allowed to view a particular |
484 | * field of this log row, if it's marked as deleted. |
485 | * |
486 | * @param int $bitfield Current field |
487 | * @param int $field One of LogPage::DELETED_ACTION, ::DELETED_COMMENT, ::DELETED_USER, ::DELETED_RESTRICTED |
488 | * @param Authority $performer User to check |
489 | * @return bool |
490 | */ |
491 | public static function userCanBitfield( $bitfield, $field, Authority $performer ) { |
492 | if ( $bitfield & $field ) { |
493 | if ( $bitfield & LogPage::DELETED_RESTRICTED ) { |
494 | return $performer->isAllowedAny( 'suppressrevision', 'viewsuppressed' ); |
495 | } else { |
496 | return $performer->isAllowed( 'deletedhistory' ); |
497 | } |
498 | } |
499 | return true; |
500 | } |
501 | |
502 | /** |
503 | * Determine if the current user is allowed to view a particular |
504 | * field of this log row, if it's marked as restricted log type. |
505 | * |
506 | * @param string $type |
507 | * @param Authority $performer User to check |
508 | * @return bool |
509 | */ |
510 | public static function userCanViewLogType( $type, Authority $performer ) { |
511 | $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions ); |
512 | if ( isset( $logRestrictions[$type] ) && !$performer->isAllowed( $logRestrictions[$type] ) ) { |
513 | return false; |
514 | } |
515 | return true; |
516 | } |
517 | |
518 | /** |
519 | * @param stdClass $row |
520 | * @param int $field One of LogPage::DELETED_ACTION, ::DELETED_COMMENT, ::DELETED_USER, ::DELETED_RESTRICTED |
521 | * @return bool |
522 | */ |
523 | public static function isDeleted( $row, $field ) { |
524 | return ( $row->log_deleted & $field ) == $field; |
525 | } |
526 | |
527 | /** |
528 | * Show log extract. Either with text and a box (set $msgKey) or without (don't set $msgKey) |
529 | * |
530 | * @param OutputPage|string &$out |
531 | * @param string|array $types Log types to show |
532 | * @param string|PageReference $page The page title to show log entries for |
533 | * @param string $user The user who made the log entries |
534 | * @param array $param Associative Array with the following additional options: |
535 | * - lim Integer Limit of items to show, default is 50 |
536 | * - conds Array Extra conditions for the query |
537 | * (e.g. $dbr->expr( 'log_action', '!=', 'revision' )) |
538 | * - showIfEmpty boolean Set to false if you don't want any output in case the loglist is empty |
539 | * if set to true (default), "No matching items in log" is displayed if loglist is empty |
540 | * - msgKey Array If you want a nice box with a message, set this to the key of the message. |
541 | * First element is the message key, additional optional elements are parameters for the key |
542 | * that are processed with wfMessage |
543 | * - offset Set to overwrite offset parameter in WebRequest |
544 | * set to '' to unset offset |
545 | * - wrap String Wrap the message in html (usually something like "<div ...>$1</div>"). |
546 | * - flags Integer display flags (NO_ACTION_LINK,NO_EXTRA_USER_LINKS) |
547 | * - useRequestParams boolean Set true to use Pager-related parameters in the WebRequest |
548 | * - useMaster boolean Use primary DB |
549 | * - extraUrlParams array|bool Additional url parameters for "full log" link (if it is shown) |
550 | * @return int Number of total log items (not limited by $lim) |
551 | */ |
552 | public static function showLogExtract( |
553 | &$out, $types = [], $page = '', $user = '', $param = [] |
554 | ) { |
555 | $defaultParameters = [ |
556 | 'lim' => 25, |
557 | 'conds' => [], |
558 | 'showIfEmpty' => true, |
559 | 'msgKey' => [ '' ], |
560 | 'wrap' => "$1", |
561 | 'flags' => 0, |
562 | 'useRequestParams' => false, |
563 | 'useMaster' => false, |
564 | 'extraUrlParams' => false, |
565 | ]; |
566 | # The + operator appends elements of remaining keys from the right |
567 | # handed array to the left handed, whereas duplicated keys are NOT overwritten. |
568 | $param += $defaultParameters; |
569 | # Convert $param array to individual variables |
570 | $lim = $param['lim']; |
571 | $conds = $param['conds']; |
572 | $showIfEmpty = $param['showIfEmpty']; |
573 | $msgKey = $param['msgKey']; |
574 | $wrap = $param['wrap']; |
575 | $flags = $param['flags']; |
576 | $extraUrlParams = $param['extraUrlParams']; |
577 | |
578 | $useRequestParams = $param['useRequestParams']; |
579 | // @phan-suppress-next-line PhanRedundantCondition |
580 | if ( !is_array( $msgKey ) ) { |
581 | $msgKey = [ $msgKey ]; |
582 | } |
583 | |
584 | if ( $out instanceof OutputPage ) { |
585 | $context = $out->getContext(); |
586 | } else { |
587 | $context = RequestContext::getMain(); |
588 | } |
589 | |
590 | $services = MediaWikiServices::getInstance(); |
591 | // FIXME: Figure out how to inject this |
592 | $linkRenderer = $services->getLinkRenderer(); |
593 | |
594 | # Insert list of top 50 (or top $lim) items |
595 | $loglist = new LogEventsList( $context, $linkRenderer, $flags ); |
596 | $pager = new LogPager( |
597 | $loglist, |
598 | $types, |
599 | $user, |
600 | $page, |
601 | false, |
602 | $conds, |
603 | false, |
604 | false, |
605 | false, |
606 | '', |
607 | '', |
608 | 0, |
609 | $services->getLinkBatchFactory(), |
610 | $services->getActorNormalization() |
611 | ); |
612 | // @phan-suppress-next-line PhanImpossibleCondition |
613 | if ( !$useRequestParams ) { |
614 | # Reset vars that may have been taken from the request |
615 | $pager->mLimit = 50; |
616 | $pager->mDefaultLimit = 50; |
617 | $pager->mOffset = ""; |
618 | $pager->mIsBackwards = false; |
619 | } |
620 | |
621 | // @phan-suppress-next-line PhanImpossibleCondition |
622 | if ( $param['useMaster'] ) { |
623 | $pager->mDb = $services->getConnectionProvider()->getPrimaryDatabase(); |
624 | } |
625 | // @phan-suppress-next-line PhanImpossibleCondition |
626 | if ( isset( $param['offset'] ) ) { # Tell pager to ignore WebRequest offset |
627 | $pager->setOffset( $param['offset'] ); |
628 | } |
629 | |
630 | // @phan-suppress-next-line PhanSuspiciousValueComparison |
631 | if ( $lim > 0 ) { |
632 | $pager->mLimit = $lim; |
633 | } |
634 | // Fetch the log rows and build the HTML if needed |
635 | $logBody = $pager->getBody(); |
636 | $numRows = $pager->getNumRows(); |
637 | |
638 | $s = ''; |
639 | |
640 | if ( $logBody ) { |
641 | if ( $msgKey[0] ) { |
642 | // @phan-suppress-next-line PhanParamTooFewUnpack Non-emptiness checked above |
643 | $msg = $context->msg( ...$msgKey ); |
644 | if ( $page instanceof PageReference ) { |
645 | $msg->page( $page ); |
646 | } |
647 | $s .= $msg->parseAsBlock(); |
648 | } |
649 | $s .= $loglist->beginLogEventsList() . |
650 | $logBody . |
651 | $loglist->endLogEventsList(); |
652 | // add styles for change tags |
653 | $context->getOutput()->addModuleStyles( 'mediawiki.interface.helpers.styles' ); |
654 | // @phan-suppress-next-line PhanRedundantCondition |
655 | } elseif ( $showIfEmpty ) { |
656 | $s = Html::rawElement( 'div', [ 'class' => 'mw-warning-logempty' ], |
657 | $context->msg( 'logempty' )->parse() ); |
658 | } |
659 | |
660 | if ( $page instanceof PageReference ) { |
661 | $titleFormatter = MediaWikiServices::getInstance()->getTitleFormatter(); |
662 | $pageName = $titleFormatter->getPrefixedDBkey( $page ); |
663 | } elseif ( $page != '' ) { |
664 | $pageName = $page; |
665 | } else { |
666 | $pageName = null; |
667 | } |
668 | |
669 | if ( $numRows > $pager->mLimit ) { # Show "Full log" link |
670 | $urlParam = []; |
671 | if ( $pageName ) { |
672 | $urlParam['page'] = $pageName; |
673 | } |
674 | |
675 | if ( $user != '' ) { |
676 | $urlParam['user'] = $user; |
677 | } |
678 | |
679 | if ( !is_array( $types ) ) { # Make it an array, if it isn't |
680 | $types = [ $types ]; |
681 | } |
682 | |
683 | # If there is exactly one log type, we can link to Special:Log?type=foo |
684 | if ( count( $types ) == 1 ) { |
685 | $urlParam['type'] = $types[0]; |
686 | } |
687 | |
688 | // @phan-suppress-next-line PhanSuspiciousValueComparison |
689 | if ( $extraUrlParams !== false ) { |
690 | $urlParam = array_merge( $urlParam, $extraUrlParams ); |
691 | } |
692 | |
693 | $s .= $linkRenderer->makeKnownLink( |
694 | SpecialPage::getTitleFor( 'Log' ), |
695 | $context->msg( 'log-fulllog' )->text(), |
696 | [], |
697 | $urlParam |
698 | ); |
699 | } |
700 | |
701 | if ( $logBody && $msgKey[0] ) { |
702 | // TODO: The condition above is weird. Should this be done in any other cases? |
703 | // Or is it always true in practice? |
704 | |
705 | // Mark as interface language (T60685) |
706 | $dir = $context->getLanguage()->getDir(); |
707 | $lang = $context->getLanguage()->getHtmlCode(); |
708 | $s = Html::rawElement( 'div', [ |
709 | 'class' => "mw-content-$dir", |
710 | 'dir' => $dir, |
711 | 'lang' => $lang, |
712 | ], $s ); |
713 | |
714 | // Wrap in warning box |
715 | $s = Html::warningBox( |
716 | $s, |
717 | 'mw-warning-with-logexcerpt' |
718 | ); |
719 | } |
720 | |
721 | // @phan-suppress-next-line PhanSuspiciousValueComparison, PhanRedundantCondition |
722 | if ( $wrap != '' ) { // Wrap message in html |
723 | $s = str_replace( '$1', $s, $wrap ); |
724 | } |
725 | |
726 | /* hook can return false, if we don't want the message to be emitted (Wikia BugId:7093) */ |
727 | $hookRunner = new HookRunner( $services->getHookContainer() ); |
728 | if ( $hookRunner->onLogEventsListShowLogExtract( $s, $types, $pageName, $user, $param ) ) { |
729 | // $out can be either an OutputPage object or a String-by-reference |
730 | if ( $out instanceof OutputPage ) { |
731 | $out->addHTML( $s ); |
732 | } else { |
733 | $out = $s; |
734 | } |
735 | } |
736 | |
737 | return $numRows; |
738 | } |
739 | |
740 | /** |
741 | * SQL clause to skip forbidden log types for this user |
742 | * |
743 | * @param \Wikimedia\Rdbms\IReadableDatabase $db |
744 | * @param string $audience Public/user |
745 | * @param Authority|null $performer User to check, required when audience isn't public |
746 | * @return string|false String on success, false on failure. |
747 | * @throws InvalidArgumentException |
748 | */ |
749 | public static function getExcludeClause( $db, $audience = 'public', Authority $performer = null ) { |
750 | $logRestrictions = MediaWikiServices::getInstance()->getMainConfig()->get( MainConfigNames::LogRestrictions ); |
751 | |
752 | if ( $audience != 'public' && $performer === null ) { |
753 | throw new InvalidArgumentException( |
754 | 'A User object must be given when checking for a user audience.' |
755 | ); |
756 | } |
757 | |
758 | // Reset the array, clears extra "where" clauses when $par is used |
759 | $hiddenLogs = []; |
760 | |
761 | // Don't show private logs to unprivileged users |
762 | foreach ( $logRestrictions as $logType => $right ) { |
763 | if ( $audience == 'public' || !$performer->isAllowed( $right ) ) { |
764 | $hiddenLogs[] = $logType; |
765 | } |
766 | } |
767 | if ( count( $hiddenLogs ) == 1 ) { |
768 | return 'log_type != ' . $db->addQuotes( $hiddenLogs[0] ); |
769 | } elseif ( $hiddenLogs ) { |
770 | return 'log_type NOT IN (' . $db->makeList( $hiddenLogs ) . ')'; |
771 | } |
772 | |
773 | return false; |
774 | } |
775 | } |