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