MediaWiki  REL1_24
SpecialActiveusers.php
Go to the documentation of this file.
00001 <?php
00033 class ActiveUsersPager extends UsersPager {
00037     protected $opts;
00038 
00042     protected $hideGroups = array();
00043 
00047     protected $hideRights = array();
00048 
00052     private $blockStatusByUid;
00053 
00059     function __construct( IContextSource $context = null, $group = null, $par = null ) {
00060         parent::__construct( $context );
00061 
00062         $this->RCMaxAge = $this->getConfig()->get( 'ActiveUserDays' );
00063         $un = $this->getRequest()->getText( 'username', $par );
00064         $this->requestedUser = '';
00065         if ( $un != '' ) {
00066             $username = Title::makeTitleSafe( NS_USER, $un );
00067             if ( !is_null( $username ) ) {
00068                 $this->requestedUser = $username->getText();
00069             }
00070         }
00071 
00072         $this->setupOptions();
00073     }
00074 
00075     public function setupOptions() {
00076         $this->opts = new FormOptions();
00077 
00078         $this->opts->add( 'hidebots', false, FormOptions::BOOL );
00079         $this->opts->add( 'hidesysops', false, FormOptions::BOOL );
00080 
00081         $this->opts->fetchValuesFromRequest( $this->getRequest() );
00082 
00083         if ( $this->opts->getValue( 'hidebots' ) == 1 ) {
00084             $this->hideRights[] = 'bot';
00085         }
00086         if ( $this->opts->getValue( 'hidesysops' ) == 1 ) {
00087             $this->hideGroups[] = 'sysop';
00088         }
00089     }
00090 
00091     function getIndexField() {
00092         return 'qcc_title';
00093     }
00094 
00095     function getQueryInfo() {
00096         $dbr = $this->getDatabase();
00097 
00098         $activeUserSeconds = $this->getConfig()->get( 'ActiveUserDays' ) * 86400;
00099         $timestamp = $dbr->timestamp( wfTimestamp( TS_UNIX ) - $activeUserSeconds );
00100         $conds = array(
00101             'qcc_type' => 'activeusers',
00102             'qcc_namespace' => NS_USER,
00103             'user_name = qcc_title',
00104             'rc_user_text = qcc_title',
00105             'rc_type != ' . $dbr->addQuotes( RC_EXTERNAL ), // Don't count wikidata.
00106             'rc_log_type IS NULL OR rc_log_type != ' . $dbr->addQuotes( 'newusers' ),
00107             'rc_timestamp >= ' . $dbr->addQuotes( $timestamp ),
00108         );
00109         if ( $this->requestedUser != '' ) {
00110             $conds[] = 'qcc_title >= ' . $dbr->addQuotes( $this->requestedUser );
00111         }
00112         if ( !$this->getUser()->isAllowed( 'hideuser' ) ) {
00113             $conds[] = 'NOT EXISTS (' . $dbr->selectSQLText(
00114                 'ipblocks', '1', array( 'ipb_user=user_id', 'ipb_deleted' => 1 )
00115             ) . ')';
00116         }
00117 
00118         return array(
00119             'tables' => array( 'querycachetwo', 'user', 'recentchanges' ),
00120             'fields' => array( 'user_name', 'user_id', 'recentedits' => 'COUNT(*)', 'qcc_title' ),
00121             'options' => array( 'GROUP BY' => array( 'qcc_title' ) ),
00122             'conds' => $conds
00123         );
00124     }
00125 
00126     function doBatchLookups() {
00127         $uids = array();
00128         foreach ( $this->mResult as $row ) {
00129             $uids[] = $row->user_id;
00130         }
00131         // Fetch the block status of the user for showing "(blocked)" text and for
00132         // striking out names of suppressed users when privileged user views the list.
00133         // Although the first query already hits the block table for un-privileged, this
00134         // is done in two queries to avoid huge quicksorts and to make COUNT(*) correct.
00135         $dbr = $this->getDatabase();
00136         $res = $dbr->select( 'ipblocks',
00137             array( 'ipb_user', 'MAX(ipb_deleted) AS block_status' ),
00138             array( 'ipb_user' => $uids ),
00139             __METHOD__,
00140             array( 'GROUP BY' => array( 'ipb_user' ) )
00141         );
00142         $this->blockStatusByUid = array();
00143         foreach ( $res as $row ) {
00144             $this->blockStatusByUid[$row->ipb_user] = $row->block_status; // 0 or 1
00145         }
00146         $this->mResult->seek( 0 );
00147     }
00148 
00149     function formatRow( $row ) {
00150         $userName = $row->user_name;
00151 
00152         $ulinks = Linker::userLink( $row->user_id, $userName );
00153         $ulinks .= Linker::userToolLinks( $row->user_id, $userName );
00154 
00155         $lang = $this->getLanguage();
00156 
00157         $list = array();
00158         $user = User::newFromId( $row->user_id );
00159 
00160         // User right filter
00161         foreach ( $this->hideRights as $right ) {
00162             // Calling User::getRights() within the loop so that
00163             // if the hideRights() filter is empty, we don't have to
00164             // trigger the lazy-init of the big userrights array in the
00165             // User object
00166             if ( in_array( $right, $user->getRights() ) ) {
00167                 return '';
00168             }
00169         }
00170 
00171         // User group filter
00172         // Note: This is a different loop than for user rights,
00173         // because we're reusing it to build the group links
00174         // at the same time
00175         foreach ( $user->getGroups() as $group ) {
00176             if ( in_array( $group, $this->hideGroups ) ) {
00177                 return '';
00178             }
00179             $list[] = self::buildGroupLink( $group, $userName );
00180         }
00181 
00182         $groups = $lang->commaList( $list );
00183 
00184         $item = $lang->specialList( $ulinks, $groups );
00185 
00186         $isBlocked = isset( $this->blockStatusByUid[$row->user_id] );
00187         if ( $isBlocked && $this->blockStatusByUid[$row->user_id] == 1 ) {
00188             $item = "<span class=\"deleted\">$item</span>";
00189         }
00190         $count = $this->msg( 'activeusers-count' )->numParams( $row->recentedits )
00191             ->params( $userName )->numParams( $this->RCMaxAge )->escaped();
00192         $blocked = $isBlocked ? ' ' . $this->msg( 'listusers-blocked', $userName )->escaped() : '';
00193 
00194         return Html::rawElement( 'li', array(), "{$item} [{$count}]{$blocked}" );
00195     }
00196 
00197     function getPageHeader() {
00198         $self = $this->getTitle();
00199         $limit = $this->mLimit ? Html::hidden( 'limit', $this->mLimit ) : '';
00200 
00201         # Form tag
00202         $out = Xml::openElement( 'form', array( 'method' => 'get', 'action' => wfScript() ) );
00203         $out .= Xml::fieldset( $this->msg( 'activeusers' )->text() ) . "\n";
00204         $out .= Html::hidden( 'title', $self->getPrefixedDBkey() ) . $limit . "\n";
00205 
00206         # Username field
00207         $out .= Xml::inputLabel( $this->msg( 'activeusers-from' )->text(),
00208             'username', 'offset', 20, $this->requestedUser, array( 'tabindex' => 1 ) ) . '<br />';
00209 
00210         $out .= Xml::checkLabel( $this->msg( 'activeusers-hidebots' )->text(),
00211             'hidebots', 'hidebots', $this->opts->getValue( 'hidebots' ), array( 'tabindex' => 2 ) );
00212 
00213         $out .= Xml::checkLabel(
00214             $this->msg( 'activeusers-hidesysops' )->text(),
00215             'hidesysops',
00216             'hidesysops',
00217             $this->opts->getValue( 'hidesysops' ),
00218             array( 'tabindex' => 3 )
00219         ) . '<br />';
00220 
00221         # Submit button and form bottom
00222         $out .= Xml::submitButton(
00223             $this->msg( 'allpagessubmit' )->text(),
00224             array( 'tabindex' => 4 )
00225         ) . "\n";
00226         $out .= Xml::closeElement( 'fieldset' );
00227         $out .= Xml::closeElement( 'form' );
00228 
00229         return $out;
00230     }
00231 }
00232 
00236 class SpecialActiveUsers extends SpecialPage {
00237 
00241     public function __construct() {
00242         parent::__construct( 'Activeusers' );
00243     }
00244 
00250     public function execute( $par ) {
00251         $days = $this->getConfig()->get( 'ActiveUserDays' );
00252 
00253         $this->setHeaders();
00254         $this->outputHeader();
00255 
00256         $out = $this->getOutput();
00257         $out->wrapWikiMsg( "<div class='mw-activeusers-intro'>\n$1\n</div>",
00258             array( 'activeusers-intro', $this->getLanguage()->formatNum( $days ) ) );
00259 
00260         // Occasionally merge in new updates
00261         $seconds = min( self::mergeActiveUsers( 600, $days ), $days * 86400 );
00262         // Mention the level of staleness
00263         $out->addWikiMsg( 'cachedspecial-viewing-cached-ttl',
00264             $this->getLanguage()->formatDuration( $seconds ) );
00265 
00266         $up = new ActiveUsersPager( $this->getContext(), null, $par );
00267 
00268         # getBody() first to check, if empty
00269         $usersbody = $up->getBody();
00270 
00271         $out->addHTML( $up->getPageHeader() );
00272         if ( $usersbody ) {
00273             $out->addHTML(
00274                 $up->getNavigationBar() .
00275                 Html::rawElement( 'ul', array(), $usersbody ) .
00276                 $up->getNavigationBar()
00277             );
00278         } else {
00279             $out->addWikiMsg( 'activeusers-noresult' );
00280         }
00281     }
00282 
00283     protected function getGroupName() {
00284         return 'users';
00285     }
00286 
00292     public static function mergeActiveUsers( $period, $days ) {
00293         $dbr = wfGetDB( DB_SLAVE );
00294         $cTime = $dbr->selectField( 'querycache_info',
00295             'qci_timestamp',
00296             array( 'qci_type' => 'activeusers' )
00297         );
00298 
00299         if ( !wfReadOnly() ) {
00300             if ( !$cTime || ( time() - wfTimestamp( TS_UNIX, $cTime ) ) > $period ) {
00301                 $dbw = wfGetDB( DB_MASTER );
00302                 if ( $dbw->estimateRowCount( 'recentchanges' ) <= 10000 ) {
00303                     $window = $days * 86400; // small wiki
00304                 } else {
00305                     $window = $period * 2;
00306                 }
00307                 $cTime = self::doQueryCacheUpdate( $dbw, $days, $window ) ?: $cTime;
00308             }
00309         }
00310 
00311         return ( time() -
00312             ( $cTime ? wfTimestamp( TS_UNIX, $cTime ) : $days * 86400 ) );
00313     }
00314 
00319     public static function cacheUpdate( DatabaseBase $dbw ) {
00320         global $wgActiveUserDays;
00321 
00322         self::doQueryCacheUpdate( $dbw, $wgActiveUserDays, $wgActiveUserDays * 86400 );
00323     }
00324 
00333     protected static function doQueryCacheUpdate( DatabaseBase $dbw, $days, $window ) {
00334         $lockKey = wfWikiID() . '-activeusers';
00335         if ( !$dbw->lock( $lockKey, __METHOD__, 1 ) ) {
00336             return false; // exclusive update (avoids duplicate entries)
00337         }
00338 
00339         $now = time();
00340         $cTime = $dbw->selectField( 'querycache_info',
00341             'qci_timestamp',
00342             array( 'qci_type' => 'activeusers' )
00343         );
00344         $cTimeUnix = $cTime ? wfTimestamp( TS_UNIX, $cTime ) : 1;
00345 
00346         // Pick the date range to fetch from. This is normally from the last
00347         // update to till the present time, but has a limited window for sanity.
00348         // If the window is limited, multiple runs are need to fully populate it.
00349         $sTimestamp = max( $cTimeUnix, $now - $days * 86400 );
00350         $eTimestamp = min( $sTimestamp + $window, $now );
00351 
00352         // Get all the users active since the last update
00353         $res = $dbw->select(
00354             array( 'recentchanges' ),
00355             array( 'rc_user_text', 'lastedittime' => 'MAX(rc_timestamp)' ),
00356             array(
00357                 'rc_user > 0', // actual accounts
00358                 'rc_type != ' . $dbw->addQuotes( RC_EXTERNAL ), // no wikidata
00359                 'rc_log_type IS NULL OR rc_log_type != ' . $dbw->addQuotes( 'newusers' ),
00360                 'rc_timestamp >= ' . $dbw->addQuotes( $dbw->timestamp( $sTimestamp ) ),
00361                 'rc_timestamp <= ' . $dbw->addQuotes( $dbw->timestamp( $eTimestamp ) )
00362             ),
00363             __METHOD__,
00364             array(
00365                 'GROUP BY' => array( 'rc_user_text' ),
00366                 'ORDER BY' => 'NULL' // avoid filesort
00367             )
00368         );
00369         $names = array();
00370         foreach ( $res as $row ) {
00371             $names[$row->rc_user_text] = $row->lastedittime;
00372         }
00373 
00374         // Rotate out users that have not edited in too long (according to old data set)
00375         $dbw->delete( 'querycachetwo',
00376             array(
00377                 'qcc_type' => 'activeusers',
00378                 'qcc_value < ' . $dbw->addQuotes( $now - $days * 86400 ) // TS_UNIX
00379             ),
00380             __METHOD__
00381         );
00382 
00383         // Find which of the recently active users are already accounted for
00384         if ( count( $names ) ) {
00385             $res = $dbw->select( 'querycachetwo',
00386                 array( 'user_name' => 'qcc_title' ),
00387                 array(
00388                     'qcc_type' => 'activeusers',
00389                     'qcc_namespace' => NS_USER,
00390                     'qcc_title' => array_keys( $names ) ),
00391                 __METHOD__
00392             );
00393             foreach ( $res as $row ) {
00394                 unset( $names[$row->user_name] );
00395             }
00396         }
00397 
00398         // Insert the users that need to be added to the list (which their last edit time
00399         if ( count( $names ) ) {
00400             $newRows = array();
00401             foreach ( $names as $name => $lastEditTime ) {
00402                 $newRows[] = array(
00403                     'qcc_type' => 'activeusers',
00404                     'qcc_namespace' => NS_USER,
00405                     'qcc_title' => $name,
00406                     'qcc_value' => wfTimestamp( TS_UNIX, $lastEditTime ),
00407                     'qcc_namespacetwo' => 0, // unused
00408                     'qcc_titletwo' => '' // unused
00409                 );
00410             }
00411             foreach ( array_chunk( $newRows, 500 ) as $rowBatch ) {
00412                 $dbw->insert( 'querycachetwo', $rowBatch, __METHOD__ );
00413                 if ( !$dbw->trxLevel() ) {
00414                     wfWaitForSlaves();
00415                 }
00416             }
00417         }
00418 
00419         // Touch the data freshness timestamp
00420         $dbw->replace( 'querycache_info',
00421             array( 'qci_type' ),
00422             array( 'qci_type' => 'activeusers',
00423                 'qci_timestamp' => $dbw->timestamp( $eTimestamp ) ), // not always $now
00424             __METHOD__
00425         );
00426 
00427         $dbw->unlock( $lockKey, __METHOD__ );
00428 
00429         return $eTimestamp;
00430     }
00431 }