MediaWiki master
SpecialLog.php
Go to the documentation of this file.
1<?php
7namespace MediaWiki\Specials;
8
9use MediaWiki\Cache\LinkBatchFactory;
21use MediaWiki\Pager\LogPager;
29use Wikimedia\IPUtils;
32use Wikimedia\Timestamp\TimestampException;
33
39class SpecialLog extends SpecialPage {
40
41 private LinkBatchFactory $linkBatchFactory;
42
43 private IConnectionProvider $dbProvider;
44
45 private ActorNormalization $actorNormalization;
46
47 private UserIdentityLookup $userIdentityLookup;
48
49 private UserNameUtils $userNameUtils;
50
51 private LogFormatterFactory $logFormatterFactory;
52
53 private TempUserConfig $tempUserConfig;
54
55 public function __construct(
56 LinkBatchFactory $linkBatchFactory,
57 IConnectionProvider $dbProvider,
58 ActorNormalization $actorNormalization,
59 UserIdentityLookup $userIdentityLookup,
60 UserNameUtils $userNameUtils,
61 LogFormatterFactory $logFormatterFactory,
62 ?TempUserConfig $tempUserConfig = null
63 ) {
64 parent::__construct( 'Log' );
65 $this->linkBatchFactory = $linkBatchFactory;
66 $this->dbProvider = $dbProvider;
67 $this->actorNormalization = $actorNormalization;
68 $this->userIdentityLookup = $userIdentityLookup;
69 $this->userNameUtils = $userNameUtils;
70 $this->logFormatterFactory = $logFormatterFactory;
71 if ( $tempUserConfig instanceof TempUserConfig ) {
72 $this->tempUserConfig = $tempUserConfig;
73 } else {
74 $this->tempUserConfig = MediaWikiServices::getInstance()->getTempUserConfig();
75 }
76 }
77
79 public function execute( $par ) {
80 $this->setHeaders();
81 $this->outputHeader();
82 $out = $this->getOutput();
83 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' );
84 $this->addHelpLink( 'Help:Log' );
85
86 $opts = new FormOptions;
87 $opts->add( 'type', '' );
88 $opts->add( 'user', '' );
89 $opts->add( 'page', [] );
90 $opts->add( 'pattern', false );
91 $opts->add( 'year', null, FormOptions::INTNULL );
92 $opts->add( 'month', null, FormOptions::INTNULL );
93 $opts->add( 'day', null, FormOptions::INTNULL );
94 $opts->add( 'tagfilter', '' );
95 $opts->add( 'tagInvert', false );
96 $opts->add( 'offset', '' );
97 $opts->add( 'dir', '' );
98 $opts->add( 'offender', '' );
99 $opts->add( 'subtype', '' );
100 $opts->add( 'logid', '' );
101
102 // Set values
103 if ( $par !== null ) {
104 $this->parseParams( (string)$par );
105 }
106 $opts->fetchValuesFromRequest( $this->getRequest() );
107
108 // Set date values
109 $dateString = $this->getRequest()->getVal( 'wpdate' );
110 if ( $dateString ) {
111 try {
112 $dateStamp = MWTimestamp::getInstance( $dateString . ' 00:00:00' );
113 } catch ( TimestampException ) {
114 // If users provide an invalid date, silently ignore it
115 // instead of letting an exception bubble up (T201411)
116 $dateStamp = false;
117 }
118 if ( $dateStamp ) {
119 $opts->setValue( 'year', (int)$dateStamp->format( 'Y' ) );
120 $opts->setValue( 'month', (int)$dateStamp->format( 'm' ) );
121 $opts->setValue( 'day', (int)$dateStamp->format( 'd' ) );
122 }
123 }
124
125 // If the user doesn't have the right permission to view the specific
126 // log type, throw a PermissionsError
127 $logRestrictions = $this->getConfig()->get( MainConfigNames::LogRestrictions );
128 $type = $opts->getValue( 'type' );
129 if ( isset( $logRestrictions[$type] )
130 && !$this->getAuthority()->isAllowed( $logRestrictions[$type] )
131 ) {
132 throw new PermissionsError( $logRestrictions[$type] );
133 }
134
135 # TODO: Move this into LogPager like other query conditions.
136 # Handle type-specific inputs
137 $qc = [];
138 $offenderName = $opts->getValue( 'offender' );
139 if ( $opts->getValue( 'type' ) == 'suppress' && $offenderName !== '' ) {
140 $dbr = $this->dbProvider->getReplicaDatabase();
141 $offenderId = $this->actorNormalization->findActorIdByName( $offenderName, $dbr );
142 if ( $offenderId ) {
143 $qc = [ 'ls_field' => 'target_author_actor', 'ls_value' => strval( $offenderId ) ];
144 } else {
145 // Unknown offender, thus results have to be empty
146 $qc = [ '1=0' ];
147 }
148 } else {
149 if ( $this->tempUserConfig->isKnown() ) {
150 // See T398423
151 // Three cases possible:
152 // 1. Special:Log/newusers is loaded as-is with 'user' field empty or set to non-temp user.
153 // The checkbox will be shown checked by default but have no value and is expected to exclude
154 // temporary accounts.
155 // 2. Special:Log/newusers is loaded as-is with 'user' field set to a temp username.
156 // The checkbox will be shown unchecked and have no value. We expect not to exclude temporary accounts.
157 // 3. form submitted, exclude temp accounts
158 // 4. form submitted, include temp accounts
159 // Check for cases 1 and 3 and omit temporary accounts in the pager query.
160 $formWasSubmitted = $this->getRequest()->getVal( 'wpFormIdentifier' ) === 'logeventslist';
161 if (
162 (
163 !$formWasSubmitted &&
164 !$this->getRequest()->getVal( 'excludetempacct' ) &&
165 !$this->tempUserConfig->isTempName( $opts->getValue( 'user' ) )
166 ) ||
167 (
168 $formWasSubmitted &&
169 $this->getRequest()->getVal( 'excludetempacct' )
170 )
171 ) {
172 $dbr = $this->dbProvider->getReplicaDatabase();
173 if ( $opts->getValue( 'type' ) === '' ) {
174 // Support excluding temporary account creations on Special:Log
175 $qc = [
176 $dbr->expr( 'log_type', '!=', 'newusers' )->orExpr(
177 $dbr->expr( 'log_type', '=', 'newusers' )
178 ->andExpr( $this->tempUserConfig
179 ->getMatchCondition( $dbr, 'logging_actor.actor_name', IExpression::NOT_LIKE ) )
180 )
181 ];
182 } elseif ( $opts->getValue( 'type' ) === 'newusers' ) {
183 $qc = [
184 $this->tempUserConfig
185 ->getMatchCondition( $dbr, 'logging_actor.actor_name', IExpression::NOT_LIKE )
186 ];
187 }
188 }
189 }
190
191 // Allow extensions to add relations to their search types
192 $this->getHookRunner()->onSpecialLogAddLogSearchRelations(
193 $opts->getValue( 'type' ), $this->getRequest(), $qc );
194 }
195
196 # TODO: Move this into LogEventList and use it as filter-callback in the field descriptor.
197 # Some log types are only for a 'User:' title but we might have been given
198 # only the username instead of the full title 'User:username'. This part try
199 # to lookup for a user by that name and eventually fix user input. See T3697.
200 if ( in_array( $opts->getValue( 'type' ), self::getLogTypesOnUser( $this->getHookRunner() ) ) ) {
201 $pages = [];
202 foreach ( $opts->getValue( 'page' ) as $page ) {
203 $page = $this->normalizeUserPage( $page );
204 if ( $page !== null ) {
205 $pages[] = $page->getPrefixedText();
206 }
207 }
208 $opts->setValue( 'page', $pages );
209 }
210
211 $this->show( $opts, $qc );
212 }
213
220 private function normalizeUserPage( $page ) {
221 $target = Title::newFromText( $page );
222 if ( $target && $target->getNamespace() === NS_MAIN ) {
223 if ( IPUtils::isValidRange( $target->getText() ) ) {
224 $page = IPUtils::sanitizeRange( $target->getText() );
225 }
226 # User forgot to add 'User:', we are adding it for them
227 $target = Title::makeTitleSafe( NS_USER, $page );
228 } elseif ( $target && $target->getNamespace() === NS_USER
229 && IPUtils::isValidRange( $target->getText() )
230 ) {
231 $ipOrRange = IPUtils::sanitizeRange( $target->getText() );
232 if ( $ipOrRange !== $target->getText() ) {
233 $target = Title::makeTitleSafe( NS_USER, $ipOrRange );
234 }
235 }
236 return $target;
237 }
238
250 public static function getLogTypesOnUser( ?HookRunner $runner = null ) {
251 static $types = null;
252 if ( $types !== null ) {
253 return $types;
254 }
255 $types = [
256 'block',
257 'newusers',
258 'rights',
259 'renameuser',
260 ];
261
263 ->onGetLogTypesOnUser( $types );
264 return $types;
265 }
266
272 public function getSubpagesForPrefixSearch() {
273 $subpages = LogPage::validTypes();
274 $subpages[] = 'all';
275 sort( $subpages );
276 return $subpages;
277 }
278
287 private function parseParams( string $par ) {
288 $params = explode( '/', $par, 2 );
289 $logType = $this->resolveLogType( $params );
290
291 if ( $logType ) {
292 $this->getRequest()->setVal( 'type', $logType );
293 if ( count( $params ) === 2 ) {
294 $this->getRequest()->setVal( 'user', $params[1] );
295 }
296 } elseif ( $par !== '' ) {
297 $this->getRequest()->setVal( 'user', $par );
298 }
299 }
300
317 private function resolveLogType( array $params ): string {
318 // Mechanism for changing the parameters of Special:Log
319 // from extensions (T381875)
320 $logType = $params[0] ?? null;
321
322 $this->getHookRunner()->onSpecialLogResolveLogType(
323 $params,
324 $logType
325 );
326
327 if ( $logType !== '' ) {
328 $symsForAll = [ '*', 'all' ];
329 $allowedTypes = array_merge( LogPage::validTypes(), $symsForAll );
330
331 if ( in_array( $logType, $allowedTypes ) ) {
332 return $logType;
333 }
334 }
335
336 return '';
337 }
338
339 private function show( FormOptions $opts, array $extraConds ) {
340 # Create a LogPager item to get the results and a LogEventsList item to format them...
341 $loglist = new LogEventsList(
342 $this->getContext(),
343 $this->getLinkRenderer(),
344 LogEventsList::USE_CHECKBOXES
345 );
346 $pager = new LogPager(
347 $loglist,
348 $opts->getValue( 'type' ),
349 $opts->getValue( 'user' ),
350 $opts->getValue( 'page' ),
351 $opts->getValue( 'pattern' ),
352 $extraConds,
353 $opts->getValue( 'year' ),
354 $opts->getValue( 'month' ),
355 $opts->getValue( 'day' ),
356 $opts->getValue( 'tagfilter' ),
357 $opts->getValue( 'subtype' ),
358 $opts->getValue( 'logid' ),
359 $this->linkBatchFactory,
360 $this->actorNormalization,
361 $this->logFormatterFactory,
362 $opts->getValue( 'tagInvert' )
363 );
364
365 # Set relevant user
366 $performer = $pager->getPerformer();
367 if ( $performer ) {
368 $performerUser = $this->userIdentityLookup->getUserIdentityByName( $performer );
369 // Only set valid local user as the relevant user (T344886)
370 // Uses the same condition as the SpecialContributions class did
371 if ( $performerUser && !IPUtils::isValidRange( $performer ) &&
372 ( $this->userNameUtils->isIP( $performer ) || $performerUser->isRegistered() )
373 ) {
374 $this->getSkin()->setRelevantUser( $performerUser );
375 }
376 }
377
378 # Show form options
379 $succeed = $loglist->showOptions(
380 $opts->getValue( 'type' ),
381 $opts->getValue( 'year' ),
382 $opts->getValue( 'month' ),
383 $opts->getValue( 'day' ),
384 $opts->getValue( 'user' ),
385 );
386 if ( !$succeed ) {
387 return;
388 }
389
390 $this->getOutput()->setPageTitleMsg(
391 ( new LogPage( $opts->getValue( 'type' ) ) )->getName()
392 );
393
394 # Insert list
395 $logBody = $pager->getBody();
396 if ( $logBody ) {
397 $this->getOutput()->addHTML(
398 $pager->getNavigationBar() .
399 $this->getActionButtons(
400 $loglist->beginLogEventsList() .
401 $logBody .
402 $loglist->endLogEventsList()
403 ) .
404 $pager->getNavigationBar()
405 );
406 } else {
407 $this->getOutput()->addWikiMsg( 'logempty' );
408 }
409 }
410
411 private function getActionButtons( string $formcontents ): string {
412 $canRevDelete = $this->getAuthority()
413 ->isAllowedAll( 'deletedhistory', 'deletelogentry' );
414 $showTagEditUI = ChangeTags::showTagEditingUI( $this->getAuthority() );
415 # If the user doesn't have the ability to delete log entries nor edit tags,
416 # don't bother showing them the button(s).
417 if ( !$canRevDelete && !$showTagEditUI ) {
418 return $formcontents;
419 }
420
421 # Show button to hide log entries and/or edit change tags
422 $s = Html::openElement(
423 'form',
424 [ 'action' => wfScript(), 'id' => 'mw-log-deleterevision-submit' ]
425 ) . "\n";
426 $s .= Html::hidden( 'type', 'logging' ) . "\n";
427
428 $buttons = '';
429 if ( $canRevDelete ) {
430 $buttons .= Html::element(
431 'button',
432 [
433 'type' => 'submit',
434 'name' => 'title',
435 'value' => SpecialPage::getTitleFor( 'Revisiondelete' )->getPrefixedDBkey(),
436 'class' => "deleterevision-log-submit mw-log-deleterevision-button mw-ui-button"
437 ],
438 $this->msg( 'showhideselectedlogentries' )->text()
439 ) . "\n";
440 }
441 if ( $showTagEditUI ) {
442 $buttons .= Html::element(
443 'button',
444 [
445 'type' => 'submit',
446 'name' => 'title',
447 'value' => SpecialPage::getTitleFor( 'EditTags' )->getPrefixedDBkey(),
448 'class' => "editchangetags-log-submit mw-log-editchangetags-button mw-ui-button"
449 ],
450 $this->msg( 'log-edit-tags' )->text()
451 ) . "\n";
452 }
453
454 $buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML();
455
456 $s .= $buttons . $formcontents . $buttons;
457 $s .= Html::closeElement( 'form' );
458
459 return $s;
460 }
461
463 protected function getGroupName() {
464 return 'changes';
465 }
466}
467
469class_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.
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.
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(LinkBatchFactory $linkBatchFactory, IConnectionProvider $dbProvider, ActorNormalization $actorNormalization, UserIdentityLookup $userIdentityLookup, UserNameUtils $userNameUtils, 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:70
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)