MediaWiki master
SpecialLog.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
21use MediaWiki\Pager\LogPager;
29use Wikimedia\IPUtils;
32use Wikimedia\Timestamp\TimestampException;
33
39class SpecialLog extends SpecialPage {
40
41 private readonly TempUserConfig $tempUserConfig;
42
43 public function __construct(
44 private readonly LinkBatchFactory $linkBatchFactory,
45 private readonly IConnectionProvider $dbProvider,
46 private readonly ActorNormalization $actorNormalization,
47 private readonly UserIdentityLookup $userIdentityLookup,
48 private readonly UserNameUtils $userNameUtils,
49 private readonly LogFormatterFactory $logFormatterFactory,
50 ?TempUserConfig $tempUserConfig = null
51 ) {
52 parent::__construct( 'Log' );
53 if ( $tempUserConfig instanceof TempUserConfig ) {
54 $this->tempUserConfig = $tempUserConfig;
55 } else {
56 $this->tempUserConfig = MediaWikiServices::getInstance()->getTempUserConfig();
57 }
58 }
59
61 public function execute( $par ) {
62 $this->setHeaders();
63 $this->outputHeader();
64 $out = $this->getOutput();
65 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
66 $this->addHelpLink( 'Help:Log' );
67
68 $opts = new FormOptions;
69 $opts->add( 'type', '' );
70 $opts->add( 'user', '' );
71 $opts->add( 'page', [] );
72 $opts->add( 'pattern', false );
73 $opts->add( 'year', null, FormOptions::INTNULL );
74 $opts->add( 'month', null, FormOptions::INTNULL );
75 $opts->add( 'day', null, FormOptions::INTNULL );
76 $opts->add( 'tagfilter', '' );
77 $opts->add( 'tagInvert', false );
78 $opts->add( 'offset', '' );
79 $opts->add( 'dir', '' );
80 $opts->add( 'offender', '' );
81 $opts->add( 'subtype', '' );
82 $opts->add( 'logid', '' );
83
84 // Set values
85 if ( $par !== null ) {
86 $this->parseParams( (string)$par );
87 }
88 $opts->fetchValuesFromRequest( $this->getRequest() );
89
90 // Set date values
91 $dateString = $this->getRequest()->getVal( 'wpdate' );
92 if ( $dateString ) {
93 try {
94 $dateStamp = MWTimestamp::getInstance( $dateString . ' 00:00:00' );
95 } catch ( TimestampException ) {
96 // If users provide an invalid date, silently ignore it
97 // instead of letting an exception bubble up (T201411)
98 $dateStamp = false;
99 }
100 if ( $dateStamp ) {
101 $opts->setValue( 'year', (int)$dateStamp->format( 'Y' ) );
102 $opts->setValue( 'month', (int)$dateStamp->format( 'm' ) );
103 $opts->setValue( 'day', (int)$dateStamp->format( 'd' ) );
104 }
105 }
106
107 // If the user doesn't have the right permission to view the specific
108 // log type, throw a PermissionsError
109 $logRestrictions = $this->getConfig()->get( MainConfigNames::LogRestrictions );
110 $type = $opts->getValue( 'type' );
111 if ( isset( $logRestrictions[$type] )
112 && !$this->getAuthority()->isAllowed( $logRestrictions[$type] )
113 ) {
114 throw new PermissionsError( $logRestrictions[$type] );
115 }
116
117 # TODO: Move this into LogPager like other query conditions.
118 # Handle type-specific inputs
119 $qc = [];
120 $offenderName = $opts->getValue( 'offender' );
121 if ( $opts->getValue( 'type' ) == 'suppress' && $offenderName !== '' ) {
122 $dbr = $this->dbProvider->getReplicaDatabase();
123 $offenderId = $this->actorNormalization->findActorIdByName( $offenderName, $dbr );
124 if ( $offenderId ) {
125 $qc = [ 'ls_field' => 'target_author_actor', 'ls_value' => strval( $offenderId ) ];
126 } else {
127 // Unknown offender, thus results have to be empty
128 $qc = [ '1=0' ];
129 }
130 } else {
131 if ( $this->tempUserConfig->isKnown() ) {
132 // See T398423
133 // Three cases possible:
134 // 1. Special:Log/newusers is loaded as-is with 'user' field empty or set to non-temp user.
135 // The checkbox will be shown checked by default but have no value and is expected to exclude
136 // temporary accounts.
137 // 2. Special:Log/newusers is loaded as-is with 'user' field set to a temp username.
138 // The checkbox will be shown unchecked and have no value. We expect not to exclude temporary accounts.
139 // 3. form submitted, exclude temp accounts
140 // 4. form submitted, include temp accounts
141 // Check for cases 1 and 3 and omit temporary accounts in the pager query.
142 $formWasSubmitted = $this->getRequest()->getVal( 'wpFormIdentifier' ) === 'logeventslist';
143 if (
144 (
145 !$formWasSubmitted &&
146 !$this->getRequest()->getVal( 'excludetempacct' ) &&
147 !$this->tempUserConfig->isTempName( $opts->getValue( 'user' ) )
148 ) ||
149 (
150 $formWasSubmitted &&
151 $this->getRequest()->getVal( 'excludetempacct' )
152 )
153 ) {
154 $dbr = $this->dbProvider->getReplicaDatabase();
155 if ( $opts->getValue( 'type' ) === '' ) {
156 // Support excluding temporary account creations on Special:Log
157 $qc = [
158 $dbr->expr( 'log_type', '!=', 'newusers' )->orExpr(
159 $dbr->expr( 'log_type', '=', 'newusers' )
160 ->andExpr( $this->tempUserConfig
161 ->getMatchCondition( $dbr, 'logging_actor.actor_name', IExpression::NOT_LIKE ) )
162 )
163 ];
164 } elseif ( $opts->getValue( 'type' ) === 'newusers' ) {
165 $qc = [
166 $this->tempUserConfig
167 ->getMatchCondition( $dbr, 'logging_actor.actor_name', IExpression::NOT_LIKE )
168 ];
169 }
170 }
171 }
172
173 // Allow extensions to add relations to their search types
174 $this->getHookRunner()->onSpecialLogAddLogSearchRelations(
175 $opts->getValue( 'type' ), $this->getRequest(), $qc );
176 }
177
178 # TODO: Move this into LogEventList and use it as filter-callback in the field descriptor.
179 # Some log types are only for a 'User:' title but we might have been given
180 # only the username instead of the full title 'User:username'. This part try
181 # to lookup for a user by that name and eventually fix user input. See T3697.
182 if ( in_array( $opts->getValue( 'type' ), self::getLogTypesOnUser( $this->getHookRunner() ) ) ) {
183 $pages = [];
184 foreach ( $opts->getValue( 'page' ) as $page ) {
185 $page = $this->normalizeUserPage( $page );
186 if ( $page !== null ) {
187 $pages[] = $page->getPrefixedText();
188 }
189 }
190 $opts->setValue( 'page', $pages );
191 }
192
193 $this->show( $opts, $qc );
194 }
195
202 private function normalizeUserPage( $page ) {
203 $target = Title::newFromText( $page );
204 if ( $target && $target->getNamespace() === NS_MAIN ) {
205 if ( IPUtils::isValidRange( $target->getText() ) ) {
206 $page = IPUtils::sanitizeRange( $target->getText() );
207 }
208 # User forgot to add 'User:', we are adding it for them
209 $target = Title::makeTitleSafe( NS_USER, $page );
210 } elseif ( $target && $target->getNamespace() === NS_USER
211 && IPUtils::isValidRange( $target->getText() )
212 ) {
213 $ipOrRange = IPUtils::sanitizeRange( $target->getText() );
214 if ( $ipOrRange !== $target->getText() ) {
215 $target = Title::makeTitleSafe( NS_USER, $ipOrRange );
216 }
217 }
218 return $target;
219 }
220
232 public static function getLogTypesOnUser( ?HookRunner $runner = null ) {
233 static $types = null;
234 if ( $types !== null ) {
235 return $types;
236 }
237 $types = [
238 'block',
239 'newusers',
240 'rights',
241 'renameuser',
242 ];
243
245 ->onGetLogTypesOnUser( $types );
246 return $types;
247 }
248
254 public function getSubpagesForPrefixSearch() {
255 $subpages = LogPage::validTypes();
256
257 // Mechanism allowing extensions to change the log types listed as
258 // search hints (T398293).
259 $this->getHookRunner()->onSpecialLogGetSubpagesForPrefixSearch(
260 $this->getContext(),
261 $subpages
262 );
263
264 $subpages[] = 'all';
265
266 // Remove duplicates just in case a hook provides the same subpage twice
267 $subpages = array_unique( $subpages );
268 sort( $subpages );
269 return $subpages;
270 }
271
280 private function parseParams( string $par ) {
281 $params = explode( '/', $par, 2 );
282 $logType = $this->resolveLogType( $params );
283
284 if ( $logType ) {
285 $this->getRequest()->setVal( 'type', $logType );
286 if ( count( $params ) === 2 ) {
287 $this->getRequest()->setVal( 'user', $params[1] );
288 }
289 } elseif ( $par !== '' ) {
290 $this->getRequest()->setVal( 'user', $par );
291 }
292 }
293
310 private function resolveLogType( array $params ): string {
311 // Mechanism for changing the parameters of Special:Log
312 // from extensions (T381875)
313 $logType = $params[0] ?? null;
314
315 $this->getHookRunner()->onSpecialLogResolveLogType(
316 $params,
317 $logType
318 );
319
320 if ( $logType !== '' ) {
321 $symsForAll = [ '*', 'all' ];
322 $allowedTypes = array_merge( LogPage::validTypes(), $symsForAll );
323
324 if ( in_array( $logType, $allowedTypes ) ) {
325 return $logType;
326 }
327 }
328
329 return '';
330 }
331
332 private function show( FormOptions $opts, array $extraConds ) {
333 # Create a LogPager item to get the results and a LogEventsList item to format them...
334 $loglist = new LogEventsList(
335 $this->getContext(),
336 $this->getLinkRenderer(),
337 LogEventsList::USE_CHECKBOXES
338 );
339 $pager = new LogPager(
340 $loglist,
341 $opts->getValue( 'type' ),
342 $opts->getValue( 'user' ),
343 $opts->getValue( 'page' ),
344 $opts->getValue( 'pattern' ),
345 $extraConds,
346 $opts->getValue( 'year' ),
347 $opts->getValue( 'month' ),
348 $opts->getValue( 'day' ),
349 $opts->getValue( 'tagfilter' ),
350 $opts->getValue( 'subtype' ),
351 $opts->getValue( 'logid' ),
352 $this->linkBatchFactory,
353 $this->actorNormalization,
354 $this->logFormatterFactory,
355 $opts->getValue( 'tagInvert' )
356 );
357
358 # Set relevant user
359 $performer = $pager->getPerformer();
360 if ( $performer ) {
361 $performerUser = $this->userIdentityLookup->getUserIdentityByName( $performer );
362 // Only set valid local user as the relevant user (T344886)
363 // Uses the same condition as the SpecialContributions class did
364 if ( $performerUser && !IPUtils::isValidRange( $performer ) &&
365 ( $this->userNameUtils->isIP( $performer ) || $performerUser->isRegistered() )
366 ) {
367 $this->getSkin()->setRelevantUser( $performerUser );
368 }
369 }
370
371 # Show form options
372 $succeed = $loglist->showOptions(
373 $opts->getValue( 'type' ),
374 $opts->getValue( 'year' ),
375 $opts->getValue( 'month' ),
376 $opts->getValue( 'day' ),
377 $opts->getValue( 'user' ),
378 );
379 if ( !$succeed ) {
380 return;
381 }
382
383 $this->getOutput()->setPageTitleMsg(
384 ( new LogPage( $opts->getValue( 'type' ) ) )->getName()
385 );
386
387 # Insert list
388 $logBody = $pager->getBody();
389 if ( $logBody ) {
390 $this->getOutput()->addHTML(
391 $pager->getNavigationBar() .
392 $this->getActionButtons(
393 $loglist->beginLogEventsList() .
394 $logBody .
395 $loglist->endLogEventsList()
396 ) .
397 $pager->getNavigationBar()
398 );
399 } else {
400 $this->getOutput()->addWikiMsg( 'logempty' );
401 }
402 }
403
404 private function getActionButtons( string $formcontents ): string {
405 $canRevDelete = $this->getAuthority()
406 ->isAllowedAll( 'deletedhistory', 'deletelogentry' );
407 $showTagEditUI = ChangeTags::showTagEditingUI( $this->getAuthority() );
408 # If the user doesn't have the ability to delete log entries nor edit tags,
409 # don't bother showing them the button(s).
410 if ( !$canRevDelete && !$showTagEditUI ) {
411 return $formcontents;
412 }
413
414 # Show button to hide log entries and/or edit change tags
415 $s = Html::openElement(
416 'form',
417 [ 'action' => wfScript(), 'id' => 'mw-log-deleterevision-submit' ]
418 ) . "\n";
419 $s .= Html::hidden( 'type', 'logging' ) . "\n";
420
421 $buttons = '';
422 if ( $canRevDelete ) {
423 $buttons .= Html::element(
424 'button',
425 [
426 'type' => 'submit',
427 'name' => 'title',
428 'value' => SpecialPage::getTitleFor( 'Revisiondelete' )->getPrefixedDBkey(),
429 'class' => "deleterevision-log-submit mw-log-deleterevision-button mw-ui-button"
430 ],
431 $this->msg( 'showhideselectedlogentries' )->text()
432 ) . "\n";
433 }
434 if ( $showTagEditUI ) {
435 $buttons .= Html::element(
436 'button',
437 [
438 'type' => 'submit',
439 'name' => 'title',
440 'value' => SpecialPage::getTitleFor( 'EditTags' )->getPrefixedDBkey(),
441 'class' => "editchangetags-log-submit mw-log-editchangetags-button mw-ui-button"
442 ],
443 $this->msg( 'log-edit-tags' )->text()
444 ) . "\n";
445 }
446
447 $buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
448
449 $s .= $buttons . $formcontents . $buttons;
450 $s .= Html::closeElement( 'form' );
451
452 return $s;
453 }
454
456 protected function getGroupName() {
457 return 'changes';
458 }
459}
460
462class_alias( SpecialLog::class, 'SpecialLog' );
const NS_USER
Definition Defines.php:53
const NS_MAIN
Definition Defines.php:51
wfScript( $script='index')
Get the URL path to a MediaWiki entry point.
Recent changes tagging.
Show an error when a user tries to do something they do not have the necessary permissions for.
This class provides an implementation of the core hook interfaces, forwarding hook calls to HookConta...
Helper class to keep track of options when mixing links and form elements.
add( $name, $default, $type=self::AUTO)
Add an option to be handled by this FormOptions instance.
This class is a collection of static functions that serve two purposes:
Definition Html.php:43
Class for generating clickable toggle links for a list of checkboxes.
Class to simplify the use of log pages.
Definition LogPage.php:35
A class containing constants representing the names of configuration variables.
const LogRestrictions
Name constant for the LogRestrictions setting, for use with Config::get()
Service locator for MediaWiki core services.
static getInstance()
Returns the global default instance of the top level service locator.
Factory for LinkBatch objects to batch query page metadata.
Parent class for all special pages.
setHeaders()
Sets headers - this should be called from the execute() method of all derived classes!
static getTitleFor( $name, $subpage=false, $fragment='')
Get a localised Title object for a specified special page name If you don't need a full Title object,...
getConfig()
Shortcut to get main config object.
getContext()
Gets the context this SpecialPage is executed in.
getRequest()
Get the WebRequest being used for this instance.
getOutput()
Get the OutputPage being used for this instance.
getAuthority()
Shortcut to get the Authority executing this instance.
outputHeader( $summaryMessageKey='')
Outputs a summary message on top of special pages By default the message key is the canonical name of...
addHelpLink( $to, $overrideBaseUrl=false)
Adds help link with an icon via page indicators.
A special page that lists log entries.
static getLogTypesOnUser(?HookRunner $runner=null)
List log type for which the target is a user Thus if the given target is in NS_MAIN we can alter it t...
getGroupName()
Under which header this special page is listed in Special:SpecialPages See messages 'specialpages-gro...
__construct(private readonly LinkBatchFactory $linkBatchFactory, private readonly IConnectionProvider $dbProvider, private readonly ActorNormalization $actorNormalization, private readonly UserIdentityLookup $userIdentityLookup, private readonly UserNameUtils $userNameUtils, private readonly LogFormatterFactory $logFormatterFactory, ?TempUserConfig $tempUserConfig=null)
execute( $par)
Default execute method Checks user permissions.This must be overridden by subclasses; it will be made...
getSubpagesForPrefixSearch()
Return an array of subpages that this special page will accept.
Represents a title within MediaWiki.
Definition Title.php:69
UserNameUtils service.
Library for creating and parsing MW-style timestamps.
$runner
Service for dealing with the actor table.
Interface for temporary user creation config and name matching.
Service for looking up UserIdentity.
Provide primary and replica IDatabase connections.
element(SerializerNode $parent, SerializerNode $node, $contents)
array $params
The job parameters.
msg( $key,... $params)