MediaWiki  REL1_24
RecentChange.php
Go to the documentation of this file.
00001 <?php
00063 class RecentChange {
00064     // Constants for the rc_source field.  Extensions may also have
00065     // their own source constants.
00066     const SRC_EDIT = 'mw.edit';
00067     const SRC_NEW = 'mw.new';
00068     const SRC_LOG = 'mw.log';
00069     const SRC_EXTERNAL = 'mw.external'; // obsolete
00070 
00071     public $mAttribs = array();
00072     public $mExtra = array();
00073 
00077     public $mTitle = false;
00078 
00082     private $mPerformer = false;
00083 
00084     public $numberofWatchingusers = 0; # Dummy to prevent error message in SpecialRecentChangesLinked
00085     public $notificationtimestamp;
00086 
00090     public $counter = -1;
00091 
00092     # Factory methods
00093 
00098     public static function newFromRow( $row ) {
00099         $rc = new RecentChange;
00100         $rc->loadFromRow( $row );
00101 
00102         return $rc;
00103     }
00104 
00112     public static function parseToRCType( $type ) {
00113         if ( is_array( $type ) ) {
00114             $retval = array();
00115             foreach ( $type as $t ) {
00116                 $retval[] = RecentChange::parseToRCType( $t );
00117             }
00118 
00119             return $retval;
00120         }
00121 
00122         switch ( $type ) {
00123             case 'edit':
00124                 return RC_EDIT;
00125             case 'new':
00126                 return RC_NEW;
00127             case 'log':
00128                 return RC_LOG;
00129             case 'external':
00130                 return RC_EXTERNAL;
00131             default:
00132                 throw new MWException( "Unknown type '$type'" );
00133         }
00134     }
00135 
00142     public static function parseFromRCType( $rcType ) {
00143         switch ( $rcType ) {
00144             case RC_EDIT:
00145                 $type = 'edit';
00146                 break;
00147             case RC_NEW:
00148                 $type = 'new';
00149                 break;
00150             case RC_LOG:
00151                 $type = 'log';
00152                 break;
00153             case RC_EXTERNAL:
00154                 $type = 'external';
00155                 break;
00156             default:
00157                 $type = "$rcType";
00158         }
00159 
00160         return $type;
00161     }
00162 
00169     public static function newFromId( $rcid ) {
00170         return self::newFromConds( array( 'rc_id' => $rcid ), __METHOD__ );
00171     }
00172 
00181     public static function newFromConds( $conds, $fname = __METHOD__, $options = array() ) {
00182         $dbr = wfGetDB( DB_SLAVE );
00183         $row = $dbr->selectRow( 'recentchanges', self::selectFields(), $conds, $fname, $options );
00184         if ( $row !== false ) {
00185             return self::newFromRow( $row );
00186         } else {
00187             return null;
00188         }
00189     }
00190 
00196     public static function selectFields() {
00197         return array(
00198             'rc_id',
00199             'rc_timestamp',
00200             'rc_user',
00201             'rc_user_text',
00202             'rc_namespace',
00203             'rc_title',
00204             'rc_comment',
00205             'rc_minor',
00206             'rc_bot',
00207             'rc_new',
00208             'rc_cur_id',
00209             'rc_this_oldid',
00210             'rc_last_oldid',
00211             'rc_type',
00212             'rc_source',
00213             'rc_patrolled',
00214             'rc_ip',
00215             'rc_old_len',
00216             'rc_new_len',
00217             'rc_deleted',
00218             'rc_logid',
00219             'rc_log_type',
00220             'rc_log_action',
00221             'rc_params',
00222         );
00223     }
00224 
00225     # Accessors
00226 
00230     public function setAttribs( $attribs ) {
00231         $this->mAttribs = $attribs;
00232     }
00233 
00237     public function setExtra( $extra ) {
00238         $this->mExtra = $extra;
00239     }
00240 
00244     public function &getTitle() {
00245         if ( $this->mTitle === false ) {
00246             $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
00247         }
00248 
00249         return $this->mTitle;
00250     }
00251 
00257     public function getPerformer() {
00258         if ( $this->mPerformer === false ) {
00259             if ( $this->mAttribs['rc_user'] ) {
00260                 $this->mPerformer = User::newFromID( $this->mAttribs['rc_user'] );
00261             } else {
00262                 $this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
00263             }
00264         }
00265 
00266         return $this->mPerformer;
00267     }
00268 
00273     public function save( $noudp = false ) {
00274         global $wgPutIPinRC, $wgUseEnotif, $wgShowUpdatedMarker, $wgContLang;
00275 
00276         $dbw = wfGetDB( DB_MASTER );
00277         if ( !is_array( $this->mExtra ) ) {
00278             $this->mExtra = array();
00279         }
00280 
00281         if ( !$wgPutIPinRC ) {
00282             $this->mAttribs['rc_ip'] = '';
00283         }
00284 
00285         # If our database is strict about IP addresses, use NULL instead of an empty string
00286         if ( $dbw->strictIPs() and $this->mAttribs['rc_ip'] == '' ) {
00287             unset( $this->mAttribs['rc_ip'] );
00288         }
00289 
00290         # Trim spaces on user supplied text
00291         $this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] );
00292 
00293         # Make sure summary is truncated (whole multibyte characters)
00294         $this->mAttribs['rc_comment'] = $wgContLang->truncate( $this->mAttribs['rc_comment'], 255 );
00295 
00296         # Fixup database timestamps
00297         $this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] );
00298         $this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'recentchanges_rc_id_seq' );
00299 
00300         ## If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
00301         if ( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id'] == 0 ) {
00302             unset( $this->mAttribs['rc_cur_id'] );
00303         }
00304 
00305         # Insert new row
00306         $dbw->insert( 'recentchanges', $this->mAttribs, __METHOD__ );
00307 
00308         # Set the ID
00309         $this->mAttribs['rc_id'] = $dbw->insertId();
00310 
00311         # Notify extensions
00312         wfRunHooks( 'RecentChange_save', array( &$this ) );
00313 
00314         # Notify external application via UDP
00315         if ( !$noudp ) {
00316             $this->notifyRCFeeds();
00317         }
00318 
00319         # E-mail notifications
00320         if ( $wgUseEnotif || $wgShowUpdatedMarker ) {
00321             $editor = $this->getPerformer();
00322             $title = $this->getTitle();
00323 
00324             if ( wfRunHooks( 'AbortEmailNotification', array( $editor, $title, $this ) ) ) {
00325                 # @todo FIXME: This would be better as an extension hook
00326                 $enotif = new EmailNotification();
00327                 $enotif->notifyOnPageChange( $editor, $title,
00328                     $this->mAttribs['rc_timestamp'],
00329                     $this->mAttribs['rc_comment'],
00330                     $this->mAttribs['rc_minor'],
00331                     $this->mAttribs['rc_last_oldid'],
00332                     $this->mExtra['pageStatus'] );
00333             }
00334         }
00335     }
00336 
00341     public function notifyRCFeeds( array $feeds = null ) {
00342         global $wgRCFeeds;
00343         if ( $feeds === null ) {
00344             $feeds = $wgRCFeeds;
00345         }
00346 
00347         $performer = $this->getPerformer();
00348 
00349         foreach ( $feeds as $feed ) {
00350             $feed += array(
00351                 'omit_bots' => false,
00352                 'omit_anon' => false,
00353                 'omit_user' => false,
00354                 'omit_minor' => false,
00355                 'omit_patrolled' => false,
00356             );
00357 
00358             if (
00359                 ( $feed['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
00360                 ( $feed['omit_anon'] && $performer->isAnon() ) ||
00361                 ( $feed['omit_user'] && !$performer->isAnon() ) ||
00362                 ( $feed['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
00363                 ( $feed['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
00364                 $this->mAttribs['rc_type'] == RC_EXTERNAL
00365             ) {
00366                 continue;
00367             }
00368 
00369             $engine = self::getEngine( $feed['uri'] );
00370 
00371             if ( isset( $this->mExtra['actionCommentIRC'] ) ) {
00372                 $actionComment = $this->mExtra['actionCommentIRC'];
00373             } else {
00374                 $actionComment = null;
00375             }
00376 
00378             $formatter = is_object( $feed['formatter'] ) ? $feed['formatter'] : new $feed['formatter']();
00379             $line = $formatter->getLine( $feed, $this, $actionComment );
00380 
00381             $engine->send( $feed, $line );
00382         }
00383     }
00384 
00392     public static function getEngine( $uri ) {
00393         global $wgRCEngines;
00394 
00395         $scheme = parse_url( $uri, PHP_URL_SCHEME );
00396         if ( !$scheme ) {
00397             throw new MWException( __FUNCTION__ . ": Invalid stream logger URI: '$uri'" );
00398         }
00399 
00400         if ( !isset( $wgRCEngines[$scheme] ) ) {
00401             throw new MWException( __FUNCTION__ . ": Unknown stream logger URI scheme: $scheme" );
00402         }
00403 
00404         return new $wgRCEngines[$scheme];
00405     }
00406 
00414     public static function markPatrolled( $change, $auto = false ) {
00415         global $wgUser;
00416 
00417         $change = $change instanceof RecentChange
00418             ? $change
00419             : RecentChange::newFromId( $change );
00420 
00421         if ( !$change instanceof RecentChange ) {
00422             return null;
00423         }
00424 
00425         return $change->doMarkPatrolled( $wgUser, $auto );
00426     }
00427 
00437     public function doMarkPatrolled( User $user, $auto = false ) {
00438         global $wgUseRCPatrol, $wgUseNPPatrol;
00439         $errors = array();
00440         // If recentchanges patrol is disabled, only new pages
00441         // can be patrolled
00442         if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) ) {
00443             $errors[] = array( 'rcpatroldisabled' );
00444         }
00445         // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
00446         $right = $auto ? 'autopatrol' : 'patrol';
00447         $errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $user ) );
00448         if ( !wfRunHooks( 'MarkPatrolled', array( $this->getAttribute( 'rc_id' ), &$user, false ) ) ) {
00449             $errors[] = array( 'hookaborted' );
00450         }
00451         // Users without the 'autopatrol' right can't patrol their
00452         // own revisions
00453         if ( $user->getName() == $this->getAttribute( 'rc_user_text' )
00454             && !$user->isAllowed( 'autopatrol' )
00455         ) {
00456             $errors[] = array( 'markedaspatrollederror-noautopatrol' );
00457         }
00458         if ( $errors ) {
00459             return $errors;
00460         }
00461         // If the change was patrolled already, do nothing
00462         if ( $this->getAttribute( 'rc_patrolled' ) ) {
00463             return array();
00464         }
00465         // Actually set the 'patrolled' flag in RC
00466         $this->reallyMarkPatrolled();
00467         // Log this patrol event
00468         PatrolLog::record( $this, $auto, $user );
00469         wfRunHooks( 'MarkPatrolledComplete', array( $this->getAttribute( 'rc_id' ), &$user, false ) );
00470 
00471         return array();
00472     }
00473 
00478     public function reallyMarkPatrolled() {
00479         $dbw = wfGetDB( DB_MASTER );
00480         $dbw->update(
00481             'recentchanges',
00482             array(
00483                 'rc_patrolled' => 1
00484             ),
00485             array(
00486                 'rc_id' => $this->getAttribute( 'rc_id' )
00487             ),
00488             __METHOD__
00489         );
00490         // Invalidate the page cache after the page has been patrolled
00491         // to make sure that the Patrol link isn't visible any longer!
00492         $this->getTitle()->invalidateCache();
00493 
00494         return $dbw->affectedRows();
00495     }
00496 
00515     public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId,
00516         $lastTimestamp, $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0 ) {
00517         $rc = new RecentChange;
00518         $rc->mTitle = $title;
00519         $rc->mPerformer = $user;
00520         $rc->mAttribs = array(
00521             'rc_timestamp' => $timestamp,
00522             'rc_namespace' => $title->getNamespace(),
00523             'rc_title' => $title->getDBkey(),
00524             'rc_type' => RC_EDIT,
00525             'rc_source' => self::SRC_EDIT,
00526             'rc_minor' => $minor ? 1 : 0,
00527             'rc_cur_id' => $title->getArticleID(),
00528             'rc_user' => $user->getId(),
00529             'rc_user_text' => $user->getName(),
00530             'rc_comment' => $comment,
00531             'rc_this_oldid' => $newId,
00532             'rc_last_oldid' => $oldId,
00533             'rc_bot' => $bot ? 1 : 0,
00534             'rc_ip' => self::checkIPAddress( $ip ),
00535             'rc_patrolled' => intval( $patrol ),
00536             'rc_new' => 0, # obsolete
00537             'rc_old_len' => $oldSize,
00538             'rc_new_len' => $newSize,
00539             'rc_deleted' => 0,
00540             'rc_logid' => 0,
00541             'rc_log_type' => null,
00542             'rc_log_action' => '',
00543             'rc_params' => ''
00544         );
00545 
00546         $rc->mExtra = array(
00547             'prefixedDBkey' => $title->getPrefixedDBkey(),
00548             'lastTimestamp' => $lastTimestamp,
00549             'oldSize' => $oldSize,
00550             'newSize' => $newSize,
00551             'pageStatus' => 'changed'
00552         );
00553         $rc->save();
00554 
00555         return $rc;
00556     }
00557 
00574     public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot,
00575         $ip = '', $size = 0, $newId = 0, $patrol = 0 ) {
00576         $rc = new RecentChange;
00577         $rc->mTitle = $title;
00578         $rc->mPerformer = $user;
00579         $rc->mAttribs = array(
00580             'rc_timestamp' => $timestamp,
00581             'rc_namespace' => $title->getNamespace(),
00582             'rc_title' => $title->getDBkey(),
00583             'rc_type' => RC_NEW,
00584             'rc_source' => self::SRC_NEW,
00585             'rc_minor' => $minor ? 1 : 0,
00586             'rc_cur_id' => $title->getArticleID(),
00587             'rc_user' => $user->getId(),
00588             'rc_user_text' => $user->getName(),
00589             'rc_comment' => $comment,
00590             'rc_this_oldid' => $newId,
00591             'rc_last_oldid' => 0,
00592             'rc_bot' => $bot ? 1 : 0,
00593             'rc_ip' => self::checkIPAddress( $ip ),
00594             'rc_patrolled' => intval( $patrol ),
00595             'rc_new' => 1, # obsolete
00596             'rc_old_len' => 0,
00597             'rc_new_len' => $size,
00598             'rc_deleted' => 0,
00599             'rc_logid' => 0,
00600             'rc_log_type' => null,
00601             'rc_log_action' => '',
00602             'rc_params' => ''
00603         );
00604 
00605         $rc->mExtra = array(
00606             'prefixedDBkey' => $title->getPrefixedDBkey(),
00607             'lastTimestamp' => 0,
00608             'oldSize' => 0,
00609             'newSize' => $size,
00610             'pageStatus' => 'created'
00611         );
00612         $rc->save();
00613 
00614         return $rc;
00615     }
00616 
00632     public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip, $type,
00633         $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
00634     ) {
00635         global $wgLogRestrictions;
00636 
00637         # Don't add private logs to RC!
00638         if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
00639             return false;
00640         }
00641         $rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
00642             $target, $logComment, $params, $newId, $actionCommentIRC );
00643         $rc->save();
00644 
00645         return true;
00646     }
00647 
00663     public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip,
00664         $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '' ) {
00665         global $wgRequest;
00666 
00667         ## Get pageStatus for email notification
00668         switch ( $type . '-' . $action ) {
00669             case 'delete-delete':
00670                 $pageStatus = 'deleted';
00671                 break;
00672             case 'move-move':
00673             case 'move-move_redir':
00674                 $pageStatus = 'moved';
00675                 break;
00676             case 'delete-restore':
00677                 $pageStatus = 'restored';
00678                 break;
00679             case 'upload-upload':
00680                 $pageStatus = 'created';
00681                 break;
00682             case 'upload-overwrite':
00683             default:
00684                 $pageStatus = 'changed';
00685                 break;
00686         }
00687 
00688         $rc = new RecentChange;
00689         $rc->mTitle = $target;
00690         $rc->mPerformer = $user;
00691         $rc->mAttribs = array(
00692             'rc_timestamp' => $timestamp,
00693             'rc_namespace' => $target->getNamespace(),
00694             'rc_title' => $target->getDBkey(),
00695             'rc_type' => RC_LOG,
00696             'rc_source' => self::SRC_LOG,
00697             'rc_minor' => 0,
00698             'rc_cur_id' => $target->getArticleID(),
00699             'rc_user' => $user->getId(),
00700             'rc_user_text' => $user->getName(),
00701             'rc_comment' => $logComment,
00702             'rc_this_oldid' => 0,
00703             'rc_last_oldid' => 0,
00704             'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0,
00705             'rc_ip' => self::checkIPAddress( $ip ),
00706             'rc_patrolled' => 1,
00707             'rc_new' => 0, # obsolete
00708             'rc_old_len' => null,
00709             'rc_new_len' => null,
00710             'rc_deleted' => 0,
00711             'rc_logid' => $newId,
00712             'rc_log_type' => $type,
00713             'rc_log_action' => $action,
00714             'rc_params' => $params
00715         );
00716 
00717         $rc->mExtra = array(
00718             'prefixedDBkey' => $title->getPrefixedDBkey(),
00719             'lastTimestamp' => 0,
00720             'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
00721             'pageStatus' => $pageStatus,
00722             'actionCommentIRC' => $actionCommentIRC
00723         );
00724 
00725         return $rc;
00726     }
00727 
00733     public function loadFromRow( $row ) {
00734         $this->mAttribs = get_object_vars( $row );
00735         $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
00736         $this->mAttribs['rc_deleted'] = $row->rc_deleted; // MUST be set
00737     }
00738 
00745     public function getAttribute( $name ) {
00746         return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null;
00747     }
00748 
00752     public function getAttributes() {
00753         return $this->mAttribs;
00754     }
00755 
00762     public function diffLinkTrail( $forceCur ) {
00763         if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
00764             $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
00765                 "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
00766             if ( $forceCur ) {
00767                 $trail .= '&diff=0';
00768             } else {
00769                 $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
00770             }
00771         } else {
00772             $trail = '';
00773         }
00774 
00775         return $trail;
00776     }
00777 
00785     public function getCharacterDifference( $old = 0, $new = 0 ) {
00786         if ( $old === 0 ) {
00787             $old = $this->mAttribs['rc_old_len'];
00788         }
00789         if ( $new === 0 ) {
00790             $new = $this->mAttribs['rc_new_len'];
00791         }
00792         if ( $old === null || $new === null ) {
00793             return '';
00794         }
00795 
00796         return ChangesList::showCharacterDifference( $old, $new );
00797     }
00798 
00803     public static function purgeExpiredChanges() {
00804         if ( wfReadOnly() ) {
00805             return;
00806         }
00807 
00808         $method = __METHOD__;
00809         $dbw = wfGetDB( DB_MASTER );
00810         $dbw->onTransactionIdle( function () use ( $dbw, $method ) {
00811             global $wgRCMaxAge;
00812 
00813             $cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
00814             $dbw->delete(
00815                 'recentchanges',
00816                 array( 'rc_timestamp < ' . $dbw->addQuotes( $cutoff ) ),
00817                 $method
00818             );
00819         } );
00820     }
00821 
00822     private static function checkIPAddress( $ip ) {
00823         global $wgRequest;
00824         if ( $ip ) {
00825             if ( !IP::isIPAddress( $ip ) ) {
00826                 throw new MWException( "Attempt to write \"" . $ip .
00827                     "\" as an IP address into recent changes" );
00828             }
00829         } else {
00830             $ip = $wgRequest->getIP();
00831             if ( !$ip ) {
00832                 $ip = '';
00833             }
00834         }
00835 
00836         return $ip;
00837     }
00838 
00848     public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
00849         global $wgRCMaxAge;
00850 
00851         return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
00852     }
00853 }