MediaWiki  REL1_23
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 
00111     public static function newFromCurRow( $row ) {
00112         wfDeprecated( __METHOD__, '1.22' );
00113         $rc = new RecentChange;
00114         $rc->loadFromCurRow( $row );
00115         $rc->notificationtimestamp = false;
00116         $rc->numberofWatchingusers = false;
00117 
00118         return $rc;
00119     }
00120 
00127     public static function newFromId( $rcid ) {
00128         return self::newFromConds( array( 'rc_id' => $rcid ), __METHOD__ );
00129     }
00130 
00139     public static function newFromConds( $conds, $fname = __METHOD__, $options = array() ) {
00140         $dbr = wfGetDB( DB_SLAVE );
00141         $row = $dbr->selectRow( 'recentchanges', self::selectFields(), $conds, $fname, $options );
00142         if ( $row !== false ) {
00143             return self::newFromRow( $row );
00144         } else {
00145             return null;
00146         }
00147     }
00148 
00154     public static function selectFields() {
00155         return array(
00156             'rc_id',
00157             'rc_timestamp',
00158             'rc_user',
00159             'rc_user_text',
00160             'rc_namespace',
00161             'rc_title',
00162             'rc_comment',
00163             'rc_minor',
00164             'rc_bot',
00165             'rc_new',
00166             'rc_cur_id',
00167             'rc_this_oldid',
00168             'rc_last_oldid',
00169             'rc_type',
00170             'rc_source',
00171             'rc_patrolled',
00172             'rc_ip',
00173             'rc_old_len',
00174             'rc_new_len',
00175             'rc_deleted',
00176             'rc_logid',
00177             'rc_log_type',
00178             'rc_log_action',
00179             'rc_params',
00180         );
00181     }
00182 
00183     # Accessors
00184 
00188     public function setAttribs( $attribs ) {
00189         $this->mAttribs = $attribs;
00190     }
00191 
00195     public function setExtra( $extra ) {
00196         $this->mExtra = $extra;
00197     }
00198 
00203     public function &getTitle() {
00204         if ( $this->mTitle === false ) {
00205             $this->mTitle = Title::makeTitle( $this->mAttribs['rc_namespace'], $this->mAttribs['rc_title'] );
00206         }
00207 
00208         return $this->mTitle;
00209     }
00210 
00216     public function getPerformer() {
00217         if ( $this->mPerformer === false ) {
00218             if ( $this->mAttribs['rc_user'] ) {
00219                 $this->mPerformer = User::newFromID( $this->mAttribs['rc_user'] );
00220             } else {
00221                 $this->mPerformer = User::newFromName( $this->mAttribs['rc_user_text'], false );
00222             }
00223         }
00224 
00225         return $this->mPerformer;
00226     }
00227 
00232     public function save( $noudp = false ) {
00233         global $wgPutIPinRC, $wgUseEnotif, $wgShowUpdatedMarker, $wgContLang;
00234 
00235         $dbw = wfGetDB( DB_MASTER );
00236         if ( !is_array( $this->mExtra ) ) {
00237             $this->mExtra = array();
00238         }
00239 
00240         if ( !$wgPutIPinRC ) {
00241             $this->mAttribs['rc_ip'] = '';
00242         }
00243 
00244         # If our database is strict about IP addresses, use NULL instead of an empty string
00245         if ( $dbw->strictIPs() and $this->mAttribs['rc_ip'] == '' ) {
00246             unset( $this->mAttribs['rc_ip'] );
00247         }
00248 
00249         # Trim spaces on user supplied text
00250         $this->mAttribs['rc_comment'] = trim( $this->mAttribs['rc_comment'] );
00251 
00252         # Make sure summary is truncated (whole multibyte characters)
00253         $this->mAttribs['rc_comment'] = $wgContLang->truncate( $this->mAttribs['rc_comment'], 255 );
00254 
00255         # Fixup database timestamps
00256         $this->mAttribs['rc_timestamp'] = $dbw->timestamp( $this->mAttribs['rc_timestamp'] );
00257         $this->mAttribs['rc_id'] = $dbw->nextSequenceValue( 'recentchanges_rc_id_seq' );
00258 
00259         ## If we are using foreign keys, an entry of 0 for the page_id will fail, so use NULL
00260         if ( $dbw->cascadingDeletes() and $this->mAttribs['rc_cur_id'] == 0 ) {
00261             unset( $this->mAttribs['rc_cur_id'] );
00262         }
00263 
00264         # Insert new row
00265         $dbw->insert( 'recentchanges', $this->mAttribs, __METHOD__ );
00266 
00267         # Set the ID
00268         $this->mAttribs['rc_id'] = $dbw->insertId();
00269 
00270         # Notify extensions
00271         wfRunHooks( 'RecentChange_save', array( &$this ) );
00272 
00273         # Notify external application via UDP
00274         if ( !$noudp ) {
00275             $this->notifyRCFeeds();
00276         }
00277 
00278         # E-mail notifications
00279         if ( $wgUseEnotif || $wgShowUpdatedMarker ) {
00280             $editor = $this->getPerformer();
00281             $title = $this->getTitle();
00282 
00283             if ( wfRunHooks( 'AbortEmailNotification', array( $editor, $title ) ) ) {
00284                 # @todo FIXME: This would be better as an extension hook
00285                 $enotif = new EmailNotification();
00286                 $enotif->notifyOnPageChange( $editor, $title,
00287                     $this->mAttribs['rc_timestamp'],
00288                     $this->mAttribs['rc_comment'],
00289                     $this->mAttribs['rc_minor'],
00290                     $this->mAttribs['rc_last_oldid'],
00291                     $this->mExtra['pageStatus'] );
00292             }
00293         }
00294     }
00295 
00299     public function notifyRC2UDP() {
00300         wfDeprecated( __METHOD__, '1.22' );
00301         $this->notifyRCFeeds();
00302     }
00303 
00308     public static function sendToUDP( $line, $address = '', $prefix = '', $port = '' ) {
00309         global $wgRC2UDPAddress, $wgRC2UDPInterwikiPrefix, $wgRC2UDPPort, $wgRC2UDPPrefix;
00310 
00311         wfDeprecated( __METHOD__, '1.22' );
00312 
00313         # Assume default for standard RC case
00314         $address = $address ? $address : $wgRC2UDPAddress;
00315         $prefix = $prefix ? $prefix : $wgRC2UDPPrefix;
00316         $port = $port ? $port : $wgRC2UDPPort;
00317 
00318         $engine = new UDPRCFeedEngine();
00319         $feed = array(
00320             'uri' => "udp://$address:$port/$prefix",
00321             'formatter' => 'IRCColourfulRCFeedFormatter',
00322             'add_interwiki_prefix' => $wgRC2UDPInterwikiPrefix,
00323         );
00324 
00325         $engine->send( $feed, $line );
00326     }
00327 
00331     public function notifyRCFeeds() {
00332         global $wgRCFeeds;
00333 
00334         $performer = $this->getPerformer();
00335 
00336         foreach ( $wgRCFeeds as $feed ) {
00337             $feed += array(
00338                 'omit_bots' => false,
00339                 'omit_anon' => false,
00340                 'omit_user' => false,
00341                 'omit_minor' => false,
00342                 'omit_patrolled' => false,
00343             );
00344 
00345             if (
00346                 ( $feed['omit_bots'] && $this->mAttribs['rc_bot'] ) ||
00347                 ( $feed['omit_anon'] && $performer->isAnon() ) ||
00348                 ( $feed['omit_user'] && !$performer->isAnon() ) ||
00349                 ( $feed['omit_minor'] && $this->mAttribs['rc_minor'] ) ||
00350                 ( $feed['omit_patrolled'] && $this->mAttribs['rc_patrolled'] ) ||
00351                 $this->mAttribs['rc_type'] == RC_EXTERNAL
00352             ) {
00353                 continue;
00354             }
00355 
00356             $engine = self::getEngine( $feed['uri'] );
00357 
00358             if ( isset( $this->mExtra['actionCommentIRC'] ) ) {
00359                 $actionComment = $this->mExtra['actionCommentIRC'];
00360             } else {
00361                 $actionComment = null;
00362             }
00363 
00365             $formatter = new $feed['formatter']();
00366             $line = $formatter->getLine( $feed, $this, $actionComment );
00367 
00368             $engine->send( $feed, $line );
00369         }
00370     }
00371 
00379     public static function getEngine( $uri ) {
00380         global $wgRCEngines;
00381 
00382         $scheme = parse_url( $uri, PHP_URL_SCHEME );
00383         if ( !$scheme ) {
00384             throw new MWException( __FUNCTION__ . ": Invalid stream logger URI: '$uri'" );
00385         }
00386 
00387         if ( !isset( $wgRCEngines[$scheme] ) ) {
00388             throw new MWException( __FUNCTION__ . ": Unknown stream logger URI scheme: $scheme" );
00389         }
00390 
00391         return new $wgRCEngines[$scheme];
00392     }
00393 
00397     public static function cleanupForIRC( $text ) {
00398         wfDeprecated( __METHOD__, '1.22' );
00399 
00400         return IRCColourfulRCFeedFormatter::cleanupForIRC( $text );
00401     }
00402 
00410     public static function markPatrolled( $change, $auto = false ) {
00411         global $wgUser;
00412 
00413         $change = $change instanceof RecentChange
00414             ? $change
00415             : RecentChange::newFromId( $change );
00416 
00417         if ( !$change instanceof RecentChange ) {
00418             return null;
00419         }
00420 
00421         return $change->doMarkPatrolled( $wgUser, $auto );
00422     }
00423 
00433     public function doMarkPatrolled( User $user, $auto = false ) {
00434         global $wgUseRCPatrol, $wgUseNPPatrol;
00435         $errors = array();
00436         // If recentchanges patrol is disabled, only new pages
00437         // can be patrolled
00438         if ( !$wgUseRCPatrol && ( !$wgUseNPPatrol || $this->getAttribute( 'rc_type' ) != RC_NEW ) ) {
00439             $errors[] = array( 'rcpatroldisabled' );
00440         }
00441         // Automatic patrol needs "autopatrol", ordinary patrol needs "patrol"
00442         $right = $auto ? 'autopatrol' : 'patrol';
00443         $errors = array_merge( $errors, $this->getTitle()->getUserPermissionsErrors( $right, $user ) );
00444         if ( !wfRunHooks( 'MarkPatrolled', array( $this->getAttribute( 'rc_id' ), &$user, false ) ) ) {
00445             $errors[] = array( 'hookaborted' );
00446         }
00447         // Users without the 'autopatrol' right can't patrol their
00448         // own revisions
00449         if ( $user->getName() == $this->getAttribute( 'rc_user_text' )
00450             && !$user->isAllowed( 'autopatrol' )
00451         ) {
00452             $errors[] = array( 'markedaspatrollederror-noautopatrol' );
00453         }
00454         if ( $errors ) {
00455             return $errors;
00456         }
00457         // If the change was patrolled already, do nothing
00458         if ( $this->getAttribute( 'rc_patrolled' ) ) {
00459             return array();
00460         }
00461         // Actually set the 'patrolled' flag in RC
00462         $this->reallyMarkPatrolled();
00463         // Log this patrol event
00464         PatrolLog::record( $this, $auto, $user );
00465         wfRunHooks( 'MarkPatrolledComplete', array( $this->getAttribute( 'rc_id' ), &$user, false ) );
00466 
00467         return array();
00468     }
00469 
00474     public function reallyMarkPatrolled() {
00475         $dbw = wfGetDB( DB_MASTER );
00476         $dbw->update(
00477             'recentchanges',
00478             array(
00479                 'rc_patrolled' => 1
00480             ),
00481             array(
00482                 'rc_id' => $this->getAttribute( 'rc_id' )
00483             ),
00484             __METHOD__
00485         );
00486         // Invalidate the page cache after the page has been patrolled
00487         // to make sure that the Patrol link isn't visible any longer!
00488         $this->getTitle()->invalidateCache();
00489 
00490         return $dbw->affectedRows();
00491     }
00492 
00511     public static function notifyEdit( $timestamp, &$title, $minor, &$user, $comment, $oldId,
00512         $lastTimestamp, $bot, $ip = '', $oldSize = 0, $newSize = 0, $newId = 0, $patrol = 0 ) {
00513         $rc = new RecentChange;
00514         $rc->mTitle = $title;
00515         $rc->mPerformer = $user;
00516         $rc->mAttribs = array(
00517             'rc_timestamp' => $timestamp,
00518             'rc_namespace' => $title->getNamespace(),
00519             'rc_title' => $title->getDBkey(),
00520             'rc_type' => RC_EDIT,
00521             'rc_source' => self::SRC_EDIT,
00522             'rc_minor' => $minor ? 1 : 0,
00523             'rc_cur_id' => $title->getArticleID(),
00524             'rc_user' => $user->getId(),
00525             'rc_user_text' => $user->getName(),
00526             'rc_comment' => $comment,
00527             'rc_this_oldid' => $newId,
00528             'rc_last_oldid' => $oldId,
00529             'rc_bot' => $bot ? 1 : 0,
00530             'rc_ip' => self::checkIPAddress( $ip ),
00531             'rc_patrolled' => intval( $patrol ),
00532             'rc_new' => 0, # obsolete
00533             'rc_old_len' => $oldSize,
00534             'rc_new_len' => $newSize,
00535             'rc_deleted' => 0,
00536             'rc_logid' => 0,
00537             'rc_log_type' => null,
00538             'rc_log_action' => '',
00539             'rc_params' => ''
00540         );
00541 
00542         $rc->mExtra = array(
00543             'prefixedDBkey' => $title->getPrefixedDBkey(),
00544             'lastTimestamp' => $lastTimestamp,
00545             'oldSize' => $oldSize,
00546             'newSize' => $newSize,
00547             'pageStatus' => 'changed'
00548         );
00549         $rc->save();
00550 
00551         return $rc;
00552     }
00553 
00570     public static function notifyNew( $timestamp, &$title, $minor, &$user, $comment, $bot,
00571         $ip = '', $size = 0, $newId = 0, $patrol = 0 ) {
00572         $rc = new RecentChange;
00573         $rc->mTitle = $title;
00574         $rc->mPerformer = $user;
00575         $rc->mAttribs = array(
00576             'rc_timestamp' => $timestamp,
00577             'rc_namespace' => $title->getNamespace(),
00578             'rc_title' => $title->getDBkey(),
00579             'rc_type' => RC_NEW,
00580             'rc_source' => self::SRC_NEW,
00581             'rc_minor' => $minor ? 1 : 0,
00582             'rc_cur_id' => $title->getArticleID(),
00583             'rc_user' => $user->getId(),
00584             'rc_user_text' => $user->getName(),
00585             'rc_comment' => $comment,
00586             'rc_this_oldid' => $newId,
00587             'rc_last_oldid' => 0,
00588             'rc_bot' => $bot ? 1 : 0,
00589             'rc_ip' => self::checkIPAddress( $ip ),
00590             'rc_patrolled' => intval( $patrol ),
00591             'rc_new' => 1, # obsolete
00592             'rc_old_len' => 0,
00593             'rc_new_len' => $size,
00594             'rc_deleted' => 0,
00595             'rc_logid' => 0,
00596             'rc_log_type' => null,
00597             'rc_log_action' => '',
00598             'rc_params' => ''
00599         );
00600 
00601         $rc->mExtra = array(
00602             'prefixedDBkey' => $title->getPrefixedDBkey(),
00603             'lastTimestamp' => 0,
00604             'oldSize' => 0,
00605             'newSize' => $size,
00606             'pageStatus' => 'created'
00607         );
00608         $rc->save();
00609 
00610         return $rc;
00611     }
00612 
00628     public static function notifyLog( $timestamp, &$title, &$user, $actionComment, $ip, $type,
00629         $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = ''
00630     ) {
00631         global $wgLogRestrictions;
00632 
00633         # Don't add private logs to RC!
00634         if ( isset( $wgLogRestrictions[$type] ) && $wgLogRestrictions[$type] != '*' ) {
00635             return false;
00636         }
00637         $rc = self::newLogEntry( $timestamp, $title, $user, $actionComment, $ip, $type, $action,
00638             $target, $logComment, $params, $newId, $actionCommentIRC );
00639         $rc->save();
00640 
00641         return true;
00642     }
00643 
00659     public static function newLogEntry( $timestamp, &$title, &$user, $actionComment, $ip,
00660         $type, $action, $target, $logComment, $params, $newId = 0, $actionCommentIRC = '' ) {
00661         global $wgRequest;
00662 
00663         ## Get pageStatus for email notification
00664         switch ( $type . '-' . $action ) {
00665             case 'delete-delete':
00666                 $pageStatus = 'deleted';
00667                 break;
00668             case 'move-move':
00669             case 'move-move_redir':
00670                 $pageStatus = 'moved';
00671                 break;
00672             case 'delete-restore':
00673                 $pageStatus = 'restored';
00674                 break;
00675             case 'upload-upload':
00676                 $pageStatus = 'created';
00677                 break;
00678             case 'upload-overwrite':
00679             default:
00680                 $pageStatus = 'changed';
00681                 break;
00682         }
00683 
00684         $rc = new RecentChange;
00685         $rc->mTitle = $target;
00686         $rc->mPerformer = $user;
00687         $rc->mAttribs = array(
00688             'rc_timestamp' => $timestamp,
00689             'rc_namespace' => $target->getNamespace(),
00690             'rc_title' => $target->getDBkey(),
00691             'rc_type' => RC_LOG,
00692             'rc_source' => self::SRC_LOG,
00693             'rc_minor' => 0,
00694             'rc_cur_id' => $target->getArticleID(),
00695             'rc_user' => $user->getId(),
00696             'rc_user_text' => $user->getName(),
00697             'rc_comment' => $logComment,
00698             'rc_this_oldid' => 0,
00699             'rc_last_oldid' => 0,
00700             'rc_bot' => $user->isAllowed( 'bot' ) ? $wgRequest->getBool( 'bot', true ) : 0,
00701             'rc_ip' => self::checkIPAddress( $ip ),
00702             'rc_patrolled' => 1,
00703             'rc_new' => 0, # obsolete
00704             'rc_old_len' => null,
00705             'rc_new_len' => null,
00706             'rc_deleted' => 0,
00707             'rc_logid' => $newId,
00708             'rc_log_type' => $type,
00709             'rc_log_action' => $action,
00710             'rc_params' => $params
00711         );
00712 
00713         $rc->mExtra = array(
00714             'prefixedDBkey' => $title->getPrefixedDBkey(),
00715             'lastTimestamp' => 0,
00716             'actionComment' => $actionComment, // the comment appended to the action, passed from LogPage
00717             'pageStatus' => $pageStatus,
00718             'actionCommentIRC' => $actionCommentIRC
00719         );
00720 
00721         return $rc;
00722     }
00723 
00729     public function loadFromRow( $row ) {
00730         $this->mAttribs = get_object_vars( $row );
00731         $this->mAttribs['rc_timestamp'] = wfTimestamp( TS_MW, $this->mAttribs['rc_timestamp'] );
00732         $this->mAttribs['rc_deleted'] = $row->rc_deleted; // MUST be set
00733     }
00734 
00741     public function loadFromCurRow( $row ) {
00742         wfDeprecated( __METHOD__, '1.22' );
00743         $this->mAttribs = array(
00744             'rc_timestamp' => wfTimestamp( TS_MW, $row->rev_timestamp ),
00745             'rc_user' => $row->rev_user,
00746             'rc_user_text' => $row->rev_user_text,
00747             'rc_namespace' => $row->page_namespace,
00748             'rc_title' => $row->page_title,
00749             'rc_comment' => $row->rev_comment,
00750             'rc_minor' => $row->rev_minor_edit ? 1 : 0,
00751             'rc_type' => $row->page_is_new ? RC_NEW : RC_EDIT,
00752             'rc_source' => $row->page_is_new ? self::SRC_NEW : self::SRC_EDIT,
00753             'rc_cur_id' => $row->page_id,
00754             'rc_this_oldid' => $row->rev_id,
00755             'rc_last_oldid' => isset( $row->rc_last_oldid ) ? $row->rc_last_oldid : 0,
00756             'rc_bot' => 0,
00757             'rc_ip' => '',
00758             'rc_id' => $row->rc_id,
00759             'rc_patrolled' => $row->rc_patrolled,
00760             'rc_new' => $row->page_is_new, # obsolete
00761             'rc_old_len' => $row->rc_old_len,
00762             'rc_new_len' => $row->rc_new_len,
00763             'rc_params' => isset( $row->rc_params ) ? $row->rc_params : '',
00764             'rc_log_type' => isset( $row->rc_log_type ) ? $row->rc_log_type : null,
00765             'rc_log_action' => isset( $row->rc_log_action ) ? $row->rc_log_action : null,
00766             'rc_logid' => isset( $row->rc_logid ) ? $row->rc_logid : 0,
00767             'rc_deleted' => $row->rc_deleted // MUST be set
00768         );
00769     }
00770 
00777     public function getAttribute( $name ) {
00778         return isset( $this->mAttribs[$name] ) ? $this->mAttribs[$name] : null;
00779     }
00780 
00784     public function getAttributes() {
00785         return $this->mAttribs;
00786     }
00787 
00794     public function diffLinkTrail( $forceCur ) {
00795         if ( $this->mAttribs['rc_type'] == RC_EDIT ) {
00796             $trail = "curid=" . (int)( $this->mAttribs['rc_cur_id'] ) .
00797                 "&oldid=" . (int)( $this->mAttribs['rc_last_oldid'] );
00798             if ( $forceCur ) {
00799                 $trail .= '&diff=0';
00800             } else {
00801                 $trail .= '&diff=' . (int)( $this->mAttribs['rc_this_oldid'] );
00802             }
00803         } else {
00804             $trail = '';
00805         }
00806 
00807         return $trail;
00808     }
00809 
00817     public function getCharacterDifference( $old = 0, $new = 0 ) {
00818         if ( $old === 0 ) {
00819             $old = $this->mAttribs['rc_old_len'];
00820         }
00821         if ( $new === 0 ) {
00822             $new = $this->mAttribs['rc_new_len'];
00823         }
00824         if ( $old === null || $new === null ) {
00825             return '';
00826         }
00827 
00828         return ChangesList::showCharacterDifference( $old, $new );
00829     }
00830 
00835     public static function purgeExpiredChanges() {
00836         if ( wfReadOnly() ) {
00837             return;
00838         }
00839 
00840         $method = __METHOD__;
00841         $dbw = wfGetDB( DB_MASTER );
00842         $dbw->onTransactionIdle( function () use ( $dbw, $method ) {
00843             global $wgRCMaxAge;
00844 
00845             $cutoff = $dbw->timestamp( time() - $wgRCMaxAge );
00846             $dbw->delete(
00847                 'recentchanges',
00848                 array( 'rc_timestamp < ' . $dbw->addQuotes( $cutoff ) ),
00849                 $method
00850             );
00851         } );
00852     }
00853 
00854     private static function checkIPAddress( $ip ) {
00855         global $wgRequest;
00856         if ( $ip ) {
00857             if ( !IP::isIPAddress( $ip ) ) {
00858                 throw new MWException( "Attempt to write \"" . $ip .
00859                     "\" as an IP address into recent changes" );
00860             }
00861         } else {
00862             $ip = $wgRequest->getIP();
00863             if ( !$ip ) {
00864                 $ip = '';
00865             }
00866         }
00867 
00868         return $ip;
00869     }
00870 
00880     public static function isInRCLifespan( $timestamp, $tolerance = 0 ) {
00881         global $wgRCMaxAge;
00882 
00883         return wfTimestamp( TS_UNIX, $timestamp ) > time() - $tolerance - $wgRCMaxAge;
00884     }
00885 }