1<?php 2/** 3 * Implements Special:Log 4 * 5 * This program is free software; you can redistribute it and/or modify 6 * it under the terms of the GNU General Public License as published by 7 * the Free Software Foundation; either version 2 of the License, or 8 * (at your option) any later version. 9 * 10 * This program is distributed in the hope that it will be useful, 11 * but WITHOUT ANY WARRANTY; without even the implied warranty of 12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 * GNU General Public License for more details. 14 * 15 * You should have received a copy of the GNU General Public License along 16 * with this program; if not, write to the Free Software Foundation, Inc., 17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 * http://www.gnu.org/copyleft/gpl.html 19 * 20 * @file 21 * @ingroup SpecialPage 22 */ 23 24use MediaWiki\MediaWikiServices; 25use Wikimedia\Timestamp\TimestampException; 26 27/** 28 * A special page that lists log entries 29 * 30 * @ingroup SpecialPage 31 */ 32class SpecialLog extends SpecialPage { 33 public function __construct() { 34 parent::__construct( 'Log' ); 35 } 36 37 public function execute( $par ) { 38 $this->setHeaders(); 39 $this->outputHeader(); 40 $out = $this->getOutput(); 41 $out->addModules( 'mediawiki.userSuggest' ); 42 $out->addModuleStyles( 'mediawiki.interface.helpers.styles' ); 43 $this->addHelpLink( 'Help:Log' ); 44 45 $opts = new FormOptions; 46 $opts->add( 'type', '' ); 47 $opts->add( 'user', '' ); 48 $opts->add( 'page', '' ); 49 $opts->add( 'pattern', false ); 50 $opts->add( 'year', null, FormOptions::INTNULL ); 51 $opts->add( 'month', null, FormOptions::INTNULL ); 52 $opts->add( 'day', null, FormOptions::INTNULL ); 53 $opts->add( 'tagfilter', '' ); 54 $opts->add( 'offset', '' ); 55 $opts->add( 'dir', '' ); 56 $opts->add( 'offender', '' ); 57 $opts->add( 'subtype', '' ); 58 $opts->add( 'logid', '' ); 59 60 // Set values 61 $opts->fetchValuesFromRequest( $this->getRequest() ); 62 if ( $par !== null ) { 63 $this->parseParams( $opts, (string)$par ); 64 } 65 66 // Set date values 67 $dateString = $this->getRequest()->getVal( 'wpdate' ); 68 if ( !empty( $dateString ) ) { 69 try { 70 $dateStamp = MWTimestamp::getInstance( $dateString . ' 00:00:00' ); 71 } catch ( TimestampException $e ) { 72 // If users provide an invalid date, silently ignore it 73 // instead of letting an exception bubble up (T201411) 74 $dateStamp = false; 75 } 76 if ( $dateStamp ) { 77 $opts->setValue( 'year', (int)$dateStamp->format( 'Y' ) ); 78 $opts->setValue( 'month', (int)$dateStamp->format( 'm' ) ); 79 $opts->setValue( 'day', (int)$dateStamp->format( 'd' ) ); 80 } 81 } 82 83 # Don't let the user get stuck with a certain date 84 if ( $opts->getValue( 'offset' ) || $opts->getValue( 'dir' ) == 'prev' ) { 85 $opts->setValue( 'year', '' ); 86 $opts->setValue( 'month', '' ); 87 } 88 89 // If the user doesn't have the right permission to view the specific 90 // log type, throw a PermissionsError 91 // If the log type is invalid, just show all public logs 92 $logRestrictions = $this->getConfig()->get( 'LogRestrictions' ); 93 $type = $opts->getValue( 'type' ); 94 if ( !LogPage::isLogType( $type ) ) { 95 $opts->setValue( 'type', '' ); 96 } elseif ( isset( $logRestrictions[$type] ) 97 && !MediaWikiServices::getInstance() 98 ->getPermissionManager() 99 ->userHasRight( $this->getUser(), $logRestrictions[$type] ) 100 ) { 101 throw new PermissionsError( $logRestrictions[$type] ); 102 } 103 104 # Handle type-specific inputs 105 $qc = []; 106 if ( $opts->getValue( 'type' ) == 'suppress' ) { 107 $offenderName = $opts->getValue( 'offender' ); 108 $offender = empty( $offenderName ) ? null : User::newFromName( $offenderName, false ); 109 if ( $offender ) { 110 $qc = [ 'ls_field' => 'target_author_actor', 'ls_value' => (string)$offender->getActorId() ]; 111 } 112 } else { 113 // Allow extensions to add relations to their search types 114 $this->getHookRunner()->onSpecialLogAddLogSearchRelations( 115 $opts->getValue( 'type' ), $this->getRequest(), $qc ); 116 } 117 118 # Some log types are only for a 'User:' title but we might have been given 119 # only the username instead of the full title 'User:username'. This part try 120 # to lookup for a user by that name and eventually fix user input. See T3697. 121 if ( in_array( $opts->getValue( 'type' ), self::getLogTypesOnUser() ) ) { 122 # ok we have a type of log which expect a user title. 123 $target = Title::newFromText( $opts->getValue( 'page' ) ); 124 if ( $target && $target->getNamespace() === NS_MAIN ) { 125 # User forgot to add 'User:', we are adding it for him 126 $opts->setValue( 'page', 127 Title::makeTitleSafe( NS_USER, $opts->getValue( 'page' ) ) 128 ); 129 } 130 } 131 132 $this->show( $opts, $qc ); 133 } 134 135 /** 136 * List log type for which the target is a user 137 * Thus if the given target is in NS_MAIN we can alter it to be an NS_USER 138 * Title user instead. 139 * 140 * @since 1.25 141 * @return array 142 */ 143 public static function getLogTypesOnUser() { 144 static $types = null; 145 if ( $types !== null ) { 146 return $types; 147 } 148 $types = [ 149 'block', 150 'newusers', 151 'rights', 152 ]; 153 154 Hooks::runner()->onGetLogTypesOnUser( $types ); 155 return $types; 156 } 157 158 /** 159 * Return an array of subpages that this special page will accept. 160 * 161 * @return string[] subpages 162 */ 163 public function getSubpagesForPrefixSearch() { 164 $subpages = LogPage::validTypes(); 165 $subpages[] = 'all'; 166 sort( $subpages ); 167 return $subpages; 168 } 169 170 /** 171 * Set options based on the subpage title parts: 172 * - One part that is a valid log type: Special:Log/logtype 173 * - Two parts: Special:Log/logtype/username 174 * - Otherwise, assume the whole subpage is a username. 175 * 176 * @param FormOptions $opts 177 * @param string $par 178 */ 179 private function parseParams( FormOptions $opts, $par ) { 180 # Get parameters 181 $par = $par ?? ''; 182 $parms = explode( '/', $par ); 183 $symsForAll = [ '*', 'all' ]; 184 if ( $parms[0] != '' && 185 ( in_array( $par, LogPage::validTypes() ) || in_array( $par, $symsForAll ) ) 186 ) { 187 $opts->setValue( 'type', $par ); 188 } elseif ( count( $parms ) == 2 ) { 189 $opts->setValue( 'type', $parms[0] ); 190 $opts->setValue( 'user', $parms[1] ); 191 } elseif ( $par != '' ) { 192 $opts->setValue( 'user', $par ); 193 } 194 } 195 196 private function show( FormOptions $opts, array $extraConds ) { 197 # Create a LogPager item to get the results and a LogEventsList item to format them... 198 $loglist = new LogEventsList( 199 $this->getContext(), 200 $this->getLinkRenderer(), 201 LogEventsList::USE_CHECKBOXES 202 ); 203 204 $pager = new LogPager( 205 $loglist, 206 $opts->getValue( 'type' ), 207 $opts->getValue( 'user' ), 208 $opts->getValue( 'page' ), 209 $opts->getValue( 'pattern' ), 210 $extraConds, 211 $opts->getValue( 'year' ), 212 $opts->getValue( 'month' ), 213 $opts->getValue( 'day' ), 214 $opts->getValue( 'tagfilter' ), 215 $opts->getValue( 'subtype' ), 216 $opts->getValue( 'logid' ) 217 ); 218 219 $this->addHeader( $opts->getValue( 'type' ) ); 220 221 # Set relevant user 222 if ( $pager->getPerformer() ) { 223 $performerUser = User::newFromName( $pager->getPerformer(), false ); 224 $this->getSkin()->setRelevantUser( $performerUser ); 225 } 226 227 # Show form options 228 $loglist->showOptions( 229 $pager->getType(), 230 $pager->getPerformer(), 231 $pager->getPage(), 232 $pager->getPattern(), 233 $pager->getYear(), 234 $pager->getMonth(), 235 $pager->getDay(), 236 $pager->getFilterParams(), 237 $pager->getTagFilter(), 238 $pager->getAction() 239 ); 240 241 # Insert list 242 $logBody = $pager->getBody(); 243 if ( $logBody ) { 244 $this->getOutput()->addHTML( 245 $pager->getNavigationBar() . 246 $this->getActionButtons( 247 $loglist->beginLogEventsList() . 248 $logBody . 249 $loglist->endLogEventsList() 250 ) . 251 $pager->getNavigationBar() 252 ); 253 } else { 254 $this->getOutput()->addWikiMsg( 'logempty' ); 255 } 256 } 257 258 private function getActionButtons( $formcontents ) { 259 $user = $this->getUser(); 260 $canRevDelete = MediaWikiServices::getInstance() 261 ->getPermissionManager() 262 ->userHasAllRights( $user, 'deletedhistory', 'deletelogentry' ); 263 $showTagEditUI = ChangeTags::showTagEditingUI( $user ); 264 # If the user doesn't have the ability to delete log entries nor edit tags, 265 # don't bother showing them the button(s). 266 if ( !$canRevDelete && !$showTagEditUI ) { 267 return $formcontents; 268 } 269 270 # Show button to hide log entries and/or edit change tags 271 $s = Html::openElement( 272 'form', 273 [ 'action' => wfScript(), 'id' => 'mw-log-deleterevision-submit' ] 274 ) . "\n"; 275 $s .= Html::hidden( 'action', 'historysubmit' ) . "\n"; 276 $s .= Html::hidden( 'type', 'logging' ) . "\n"; 277 278 // If no title is set, the fallback is to use the main page, as defined 279 // by MediaWiki:Mainpage 280 // On wikis where the main page can be translated, MediaWiki:Mainpage 281 // is sometimes set to use Special:MyLanguage to redirect to the 282 // appropriate version. This is interpreted as a special page, and 283 // Action::getActionName forces the action to be 'view' if the title 284 // cannot be used as a WikiPage, which includes all pages in NS_SPECIAL. 285 // Set a dummy title to avoid this. The title provided is unused 286 // by the SpecialPageAction class and does not matter. 287 // See T205908 288 $s .= Html::hidden( 'title', 'Unused' ) . "\n"; 289 290 $buttons = ''; 291 if ( $canRevDelete ) { 292 $buttons .= Html::element( 293 'button', 294 [ 295 'type' => 'submit', 296 'name' => 'revisiondelete', 297 'value' => '1', 298 'class' => "deleterevision-log-submit mw-log-deleterevision-button mw-ui-button" 299 ], 300 $this->msg( 'showhideselectedlogentries' )->text() 301 ) . "\n"; 302 } 303 if ( $showTagEditUI ) { 304 $buttons .= Html::element( 305 'button', 306 [ 307 'type' => 'submit', 308 'name' => 'editchangetags', 309 'value' => '1', 310 'class' => "editchangetags-log-submit mw-log-editchangetags-button mw-ui-button" 311 ], 312 $this->msg( 'log-edit-tags' )->text() 313 ) . "\n"; 314 } 315 316 $buttons .= ( new ListToggle( $this->getOutput() ) )->getHTML(); 317 318 $s .= $buttons . $formcontents . $buttons; 319 $s .= Html::closeElement( 'form' ); 320 321 return $s; 322 } 323 324 /** 325 * Set page title and show header for this log type 326 * @param string $type 327 * @since 1.19 328 */ 329 protected function addHeader( $type ) { 330 $page = new LogPage( $type ); 331 $this->getOutput()->setPageTitle( $page->getName() ); 332 $this->getOutput()->addHTML( $page->getDescription() 333 ->setContext( $this->getContext() )->parseAsBlock() ); 334 } 335 336 protected function getGroupName() { 337 return 'changes'; 338 } 339} 340