Merge "Allow searching for IPs' logs"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 24 Dec 2013 15:27:15 +0000 (15:27 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 24 Dec 2013 15:27:15 +0000 (15:27 +0000)
23 files changed:
RELEASE-NOTES-1.23
includes/DefaultSettings.php
includes/WikiPage.php
includes/deferred/LinksUpdate.php
includes/installer/MysqlUpdater.php
includes/installer/PostgresUpdater.php
includes/installer/SqliteUpdater.php
includes/installer/WebInstallerOutput.php
includes/job/Job.php
includes/job/JobQueueGroup.php
includes/job/jobs/HTMLCacheUpdateJob.php
includes/job/jobs/RefreshLinksJob.php
includes/specialpage/SpecialPage.php
maintenance/archives/patch-page_links_updated.sql [new file with mode: 0644]
maintenance/postgres/tables.sql
maintenance/runJobs.php
maintenance/tables.sql
resources/Resources.php
resources/mediawiki/mediawiki.Uri.js
resources/mediawiki/mediawiki.js
skins/MonoBook.php
skins/Vector.php
tests/qunit/suites/resources/mediawiki/mediawiki.Uri.test.js

index 416a7fc..db80ad3 100644 (file)
@@ -116,6 +116,16 @@ changes to languages because of Bugzilla reports.
 * The ExpandTemplates extension has been moved into MediaWiki core.
 * (bug 52812) Removed "Disable search suggestions" from Preference.
 * (bug 52809) Removed "Disable browser page caching" from Preference.
+* Three new modules intended for use by custom skins were added:
+  'skins.common.elements', 'skins.common.content', and 'skins.common.interface',
+  representing three levels of standard MediaWiki styling. Previously skin
+  creators wishing to use them had to refer to the file names of appropriate
+  files directly, which is now discouraged.
+* The modules 'skins.vector' and 'skins.monobook' have been renamed to
+  'skins.vector.styles' and 'skins.monobook.styles', respectively,
+  and their definition was changed not to include the common*.css files;
+  the two skins now load the 'skins.common.interface' module instead.
+* A page_links_updated field has been added to the page table.
 
 == Compatibility ==
 
index 690bec2..0f7707c 100644 (file)
@@ -6130,9 +6130,19 @@ $wgJobClasses = array(
  * - Jobs that you would never want to run as part of a page rendering request.
  * - Jobs that you want to run on specialized machines ( like transcoding, or a particular
  *   machine on your cluster has 'outside' web access you could restrict uploadFromUrl )
+ * These settings should be global to all wikis.
  */
 $wgJobTypesExcludedFromDefaultQueue = array( 'AssembleUploadChunks', 'PublishStashedFile' );
 
+/**
+ * Map of job types to how many job "work items" should be run per second
+ * on each job runner process. The meaning of "work items" varies per job,
+ * but typically would be something like "pages to update". A single job
+ * may have a variable number of work items, as is the case with batch jobs.
+ * These settings should be global to all wikis.
+ */
+$wgJobBackoffThrottling = array();
+
 /**
  * Map of job types to configuration arrays.
  * This determines which queue class and storage system is used for each job type.
index 0d987aa..cc65ee8 100644 (file)
@@ -83,6 +83,11 @@ class WikiPage implements Page, IDBAccessObject {
         */
        protected $mTouched = '19700101000000';
 
+       /**
+        * @var string
+        */
+       protected $mLinksUpdated = '19700101000000';
+
        /**
         * @var int|null
         */
@@ -241,6 +246,7 @@ class WikiPage implements Page, IDBAccessObject {
                $this->mRedirectTarget = null; // Title object if set
                $this->mLastRevision = null; // Latest revision
                $this->mTouched = '19700101000000';
+               $this->mLinksUpdated = '19700101000000';
                $this->mTimestamp = '';
                $this->mIsRedirect = false;
                $this->mLatest = false;
@@ -278,6 +284,7 @@ class WikiPage implements Page, IDBAccessObject {
                        'page_is_new',
                        'page_random',
                        'page_touched',
+                       'page_links_updated',
                        'page_latest',
                        'page_len',
                );
@@ -405,6 +412,7 @@ class WikiPage implements Page, IDBAccessObject {
                        $this->mId = intval( $data->page_id );
                        $this->mCounter = intval( $data->page_counter );
                        $this->mTouched = wfTimestamp( TS_MW, $data->page_touched );
+                       $this->mLinksUpdated = wfTimestampOrNull( TS_MW, $data->page_links_updated );
                        $this->mIsRedirect = intval( $data->page_is_redirect );
                        $this->mLatest = intval( $data->page_latest );
                        // Bug 37225: $latest may no longer match the cached latest Revision object.
@@ -533,6 +541,17 @@ class WikiPage implements Page, IDBAccessObject {
                return $this->mTouched;
        }
 
+       /**
+        * Get the page_links_updated field
+        * @return string|null containing GMT timestamp
+        */
+       public function getLinksTimestamp() {
+               if ( !$this->mDataLoaded ) {
+                       $this->loadPageData();
+               }
+               return $this->mLinksUpdated;
+       }
+
        /**
         * Get the page_latest field
         * @return integer rev_id of current revision
index 9cd7708..d5ed250 100644 (file)
@@ -218,6 +218,9 @@ class LinksUpdate extends SqlDataUpdate {
                $changed = $propertiesDeletes + array_diff_assoc( $this->mProperties, $existing );
                $this->invalidateProperties( $changed );
 
+               # Update the links table freshness for this title
+               $this->updateLinksTimestamp();
+
                # Refresh links of all pages including this page
                # This will be in a separate transaction
                if ( $this->mRecursive ) {
@@ -855,6 +858,19 @@ class LinksUpdate extends SqlDataUpdate {
 
                return $result;
        }
+
+       /**
+        * Update links table freshness
+        */
+       protected function updateLinksTimestamp() {
+               if ( $this->mId ) {
+                       $this->mDb->update( 'page',
+                               array( 'page_links_updated' => $this->mDb->timestamp() ),
+                               array( 'page_id' => $this->mId ),
+                               __METHOD__
+                       );
+               }
+       }
 }
 
 /**
index 773afca..b3ea964 100644 (file)
@@ -248,6 +248,7 @@ class MysqlUpdater extends DatabaseUpdater {
                        array( 'addField', 'recentchanges', 'rc_source', 'patch-rc_source.sql' ),
                        array( 'addIndex', 'logging', 'log_user_text_type_time',  'patch-logging_user_text_type_time_index.sql' ),
                        array( 'addIndex', 'logging', 'log_user_text_time',  'patch-logging_user_text_time_index.sql' ),
+                       array( 'addField', 'page', 'page_links_updated', 'patch-page_links_updated.sql' ),
                );
        }
 
index dac1400..fa58a62 100644 (file)
@@ -402,6 +402,7 @@ class PostgresUpdater extends DatabaseUpdater {
 
                        // 1.23
                        array( 'addPgField', 'recentchanges', 'rc_source', "TEXT NOT NULL DEFAULT ''" ),
+                       array( 'addPgField', 'page', 'page_links_updated', "TIMESTAMPTZ NULL" ),
                );
        }
 
index 8f117df..3db3758 100644 (file)
@@ -125,6 +125,7 @@ class SqliteUpdater extends DatabaseUpdater {
                        array( 'addField', 'recentchanges', 'rc_source', 'patch-rc_source.sql' ),
                        array( 'addIndex', 'logging', 'log_user_text_type_time',  'patch-logging_user_text_type_time_index.sql' ),
                        array( 'addIndex', 'logging', 'log_user_text_time',  'patch-logging_user_text_time_index.sql' ),
+                       array( 'addField', 'page', 'page_links_updated', 'patch-page_links_updated.sql' ),
                );
        }
 
index 2a3a584..25634ba 100644 (file)
@@ -117,7 +117,7 @@ class WebInstallerOutput {
                // and loaded as one file.
                $moduleNames = array(
                        'mediawiki.legacy.shared',
-                       'skins.common',
+                       'skins.common.interface',
                        'skins.vector.styles',
                        'mediawiki.legacy.config',
                );
index 77652a4..067ede1 100644 (file)
@@ -197,6 +197,15 @@ abstract class Job {
                return true;
        }
 
+       /**
+        * @return integer Number of actually "work items" handled in this job
+        * @see $wgJobBackoffThrottling
+        * @since 1.23
+        */
+       public function workItemCount() {
+               return 1;
+       }
+
        /**
         * Subclasses may need to override this to make duplication detection work.
         * The resulting map conveys everything that makes the job unique. This is
index 9d206a3..d71df15 100644 (file)
@@ -44,7 +44,6 @@ class JobQueueGroup {
        const TYPE_ANY = 2; // integer; any job
 
        const USE_CACHE = 1; // integer; use process or persistent cache
-       const USE_PRIORITY = 2; // integer; respect deprioritization
 
        const PROC_CACHE_TTL = 15; // integer; seconds
 
@@ -149,18 +148,21 @@ class JobQueueGroup {
         * This pops a job off a queue as specified by $wgJobTypeConf and
         * updates the aggregate job queue information cache as needed.
         *
-        * @param int|string $qtype JobQueueGroup::TYPE_DEFAULT or type string
+        * @param int|string $qtype JobQueueGroup::TYPE_* constant or job type string
         * @param int $flags Bitfield of JobQueueGroup::USE_* constants
+        * @param array $blacklist List of job types to ignore
         * @return Job|bool Returns false on failure
         */
-       public function pop( $qtype = self::TYPE_DEFAULT, $flags = 0 ) {
+       public function pop( $qtype = self::TYPE_DEFAULT, $flags = 0, array $blacklist = array() ) {
+               $job = false;
+
                if ( is_string( $qtype ) ) { // specific job type
-                       $job = $this->get( $qtype )->pop();
-                       if ( !$job ) {
-                               JobQueueAggregator::singleton()->notifyQueueEmpty( $this->wiki, $qtype );
+                       if ( !in_array( $qtype, $blacklist ) ) {
+                               $job = $this->get( $qtype )->pop();
+                               if ( !$job ) {
+                                       JobQueueAggregator::singleton()->notifyQueueEmpty( $this->wiki, $qtype );
+                               }
                        }
-
-                       return $job;
                } else { // any job in the "default" jobs types
                        if ( $flags & self::USE_CACHE ) {
                                if ( !$this->cache->has( 'queues-ready', 'list', self::PROC_CACHE_TTL ) ) {
@@ -174,20 +176,22 @@ class JobQueueGroup {
                        if ( $qtype == self::TYPE_DEFAULT ) {
                                $types = array_intersect( $types, $this->getDefaultQueueTypes() );
                        }
+
+                       $types = array_diff( $types, $blacklist ); // avoid selected types
                        shuffle( $types ); // avoid starvation
 
                        foreach ( $types as $type ) { // for each queue...
                                $job = $this->get( $type )->pop();
                                if ( $job ) { // found
-                                       return $job;
+                                       break;
                                } else { // not found
                                        JobQueueAggregator::singleton()->notifyQueueEmpty( $this->wiki, $type );
                                        $this->cache->clear( 'queues-ready' );
                                }
                        }
-
-                       return false; // no jobs found
                }
+
+               return $job;
        }
 
        /**
index 833616d..61d100c 100644 (file)
@@ -154,4 +154,8 @@ class HTMLCacheUpdateJob extends Job {
                        }
                }
        }
+
+       public function workItemCount() {
+               return isset( $this->params['pages'] ) ? count( $this->params['pages'] ) : 1;
+       }
 }
index d8526ee..78ac84d 100644 (file)
@@ -133,6 +133,10 @@ class RefreshLinksJob extends Job {
                if ( isset( $this->params['rootJobTimestamp'] ) ) {
                        $page = WikiPage::factory( $title );
                        $skewedTimestamp = wfTimestamp( TS_UNIX, $this->params['rootJobTimestamp'] ) + 5;
+                       if ( $page->getLinksTimestamp() > wfTimestamp( TS_MW, $skewedTimestamp ) ) {
+                               // Something already updated the backlinks since this job was made
+                               return true;
+                       }
                        if ( $page->getTouched() > wfTimestamp( TS_MW, $skewedTimestamp ) ) {
                                $parserOptions = $page->makeParserOptions( 'canonical' );
                                $parserOutput = ParserCache::singleton()->getDirty( $page, $parserOptions );
@@ -170,4 +174,8 @@ class RefreshLinksJob extends Job {
 
                return $info;
        }
+
+       public function workItemCount() {
+               return isset( $this->params['pages'] ) ? count( $this->params['pages'] ) : 1;
+       }
 }
index e874b3e..392d04b 100644 (file)
@@ -755,10 +755,9 @@ class SpecialPage {
         * also the name that will be listed in Special:Specialpages
         *
         * Derived classes can override this, but usually it is easier to keep the
-        * default behavior. Messages can be added at run-time, see
-        * MessageCache.php.
+        * default behavior.
         *
-        * @return String
+        * @return string
         */
        function getDescription() {
                return $this->msg( strtolower( $this->mName ) )->text();
diff --git a/maintenance/archives/patch-page_links_updated.sql b/maintenance/archives/patch-page_links_updated.sql
new file mode 100644 (file)
index 0000000..18d9e2d
--- /dev/null
@@ -0,0 +1,2 @@
+ALTER TABLE /*$wgDBprefix*/page
+  ADD page_links_updated varbinary(14) NULL default NULL;
index b8a7185..3940ba2 100644 (file)
@@ -80,6 +80,7 @@ CREATE TABLE page (
   page_is_new        SMALLINT       NOT NULL  DEFAULT 0,
   page_random        NUMERIC(15,14) NOT NULL  DEFAULT RANDOM(),
   page_touched       TIMESTAMPTZ,
+  page_links_updated TIMESTAMPTZ    NULL,
   page_latest        INTEGER        NOT NULL, -- FK?
   page_len           INTEGER        NOT NULL,
   page_content_model TEXT
index 8a4a5ab..deea5ed 100644 (file)
@@ -64,12 +64,12 @@ class RunJobs extends Maintenance {
                                }
                        }
                }
+
+               $type = $this->getOption( 'type', false );
                $maxJobs = $this->getOption( 'maxjobs', false );
                $maxTime = $this->getOption( 'maxtime', false );
                $startTime = time();
-               $type = $this->getOption( 'type', false );
                $wgTitle = Title::newFromText( 'RunJobs.php' );
-               $jobsRun = 0; // counter
 
                $group = JobQueueGroup::singleton();
                // Handle any required periodic queue maintenance
@@ -78,12 +78,21 @@ class RunJobs extends Maintenance {
                        $this->runJobsLog( "Executed $count periodic queue task(s)." );
                }
 
-               $flags = JobQueueGroup::USE_CACHE | JobQueueGroup::USE_PRIORITY;
+               $backoffs = $this->loadBackoffs(); // map of (type => UNIX expiry)
+               $startingBackoffs = $backoffs; // avoid unnecessary writes
+               $backoffExpireFunc = function( $t ) { return $t > time(); };
+
+               $jobsRun = 0; // counter
+               $flags = JobQueueGroup::USE_CACHE;
                $lastTime = time(); // time since last slave check
                do {
-                       $job = ( $type === false )
-                               ? $group->pop( JobQueueGroup::TYPE_DEFAULT, $flags )
-                               : $group->pop( $type ); // job from a single queue
+                       if ( $type === false ) {
+                               $backoffs = array_filter( $backoffs, $backoffExpireFunc );
+                               $blacklist = array_keys( $backoffs );
+                               $job = $group->pop( JobQueueGroup::TYPE_DEFAULT, $flags, $blacklist );
+                       } else {
+                               $group->pop( $type ); // job from a single queue
+                       }
                        if ( $job ) { // found a job
                                ++$jobsRun;
                                $this->runJobsLog( $job->toString() . " STARTING" );
@@ -117,6 +126,14 @@ class RunJobs extends Maintenance {
                                        $this->runJobsLog( $job->toString() . " t=$timeMs good" );
                                }
 
+                               // Back off of certain jobs for a while
+                               $ttw = $this->getBackoffTimeToWait( $job );
+                               if ( $ttw > 0 ) {
+                                       $jType = $job->getType();
+                                       $backoffs[$jType] = isset( $backoffs[$jType] ) ? $backoffs[$jType] : 0;
+                                       $backoffs[$jType] = max( $backoffs[$jType], time() + $ttw );
+                               }
+
                                // Break out if we hit the job count or wall time limits...
                                if ( $maxJobs && $jobsRun >= $maxJobs ) {
                                        break;
@@ -139,10 +156,87 @@ class RunJobs extends Maintenance {
                                $this->assertMemoryOK();
                        }
                } while ( $job ); // stop when there are no jobs
+               // Sync the persistent backoffs for the next runJobs.php pass
+               $backoffs = array_filter( $backoffs, $backoffExpireFunc );
+               if ( $backoffs !== $startingBackoffs ) {
+                       $this->syncBackoffs( $backoffs );
+               }
+       }
+
+       /**
+        * @param Job $job
+        * @return integer Seconds for this runner to avoid doing more jobs of this type
+        * @see $wgJobBackoffThrottling
+        */
+       private function getBackoffTimeToWait( Job $job ) {
+               global $wgJobBackoffThrottling;
+
+               if ( !isset( $wgJobBackoffThrottling[$job->getType()] ) ) {
+                       return 0; // not throttled
+               }
+               $itemsPerSecond = $wgJobBackoffThrottling[$job->getType()];
+               if ( $itemsPerSecond <= 0 ) {
+                       return 0; // not throttled
+               }
+
+               $seconds = 0;
+               if ( $job->workItemCount() > 0 ) {
+                       $seconds = floor( $job->workItemCount() / $itemsPerSecond );
+                       $remainder = $job->workItemCount() % $itemsPerSecond;
+                       $seconds += ( mt_rand( 1, $itemsPerSecond ) <= $remainder ) ? 1 : 0;
+               }
+
+               return (int)$seconds;
+       }
+
+       /**
+        * Get the previous backoff expiries from persistent storage
+        *
+        * @return array Map of (job type => backoff expiry timestamp)
+        */
+       private function loadBackoffs() {
+               $section = new ProfileSection( __METHOD__ );
+
+               $backoffs = array();
+               $file = wfTempDir() . '/mw-runJobs-backoffs.json';
+               if ( is_file( $file ) ) {
+                       $handle = fopen( $file, 'rb' );
+                       flock( $handle, LOCK_SH );
+                       $content = stream_get_contents( $handle );
+                       flock( $handle, LOCK_UN );
+                       fclose( $handle );
+                       $backoffs = json_decode( $content, true ) ?: array();
+               }
+
+               return $backoffs;
+       }
+
+       /**
+        * Merge the current backoff expiries from persistent storage
+        *
+        * @param array $backoffs Map of (job type => backoff expiry timestamp)
+        */
+       private function syncBackoffs( array $backoffs ) {
+               $section = new ProfileSection( __METHOD__ );
+
+               $file = wfTempDir() . '/mw-runJobs-backoffs.json';
+               $handle = fopen( $file, 'wb+' );
+               flock( $handle, LOCK_EX );
+               $content = stream_get_contents( $handle );
+               $cBackoffs = json_decode( $content, true ) ?: array();
+               foreach ( $backoffs as $type => $timestamp ) {
+                       $cBackoffs[$type] = isset( $cBackoffs[$type] ) ? $cBackoffs[$type] : 0;
+                       $cBackoffs[$type] = max( $cBackoffs[$type], $backoffs[$type] );
+               }
+               ftruncate( $handle, 0 );
+               fwrite( $handle, json_encode( $backoffs ) );
+               flock( $handle, LOCK_UN );
+               fclose( $handle );
        }
 
        /**
-        * Make sure that this script is not too close to the memory usage limit
+        * Make sure that this script is not too close to the memory usage limit.
+        * It is better to die in between jobs than OOM right in the middle of one.
         * @throws MWException
         */
        private function assertMemoryOK() {
@@ -151,7 +245,7 @@ class RunJobs extends Maintenance {
                        $m = array();
                        if ( preg_match( '!^(\d+)(k|m|g|)$!i', ini_get( 'memory_limit' ), $m ) ) {
                                list( , $num, $unit ) = $m;
-                               $conv = array( 'g' => 1024 * 1024 * 1024, 'm' => 1024 * 1024, 'k' => 1024, '' => 1 );
+                               $conv = array( 'g' => 1073741824, 'm' => 1048576, 'k' => 1024, '' => 1 );
                                $maxBytes = $num * $conv[strtolower( $unit )];
                        } else {
                                $maxBytes = 0;
index 9a93c44..fb2f0e1 100644 (file)
@@ -256,6 +256,11 @@ CREATE TABLE /*_*/page (
   -- of contained templates.
   page_touched binary(14) NOT NULL default '',
 
+  -- This timestamp is updated whenever a page is re-parsed and
+  -- it has all the link tracking tables updated for it. This is
+  -- useful for de-duplicating expensive backlink update jobs.
+  page_links_updated varbinary(14) NULL default NULL,
+
   -- Handy key to revision.rev_id of the current revision.
   -- This may be 0 during page creation, but that shouldn't
   -- happen outside of a transaction... hopefully.
index d280c75..68dc984 100644 (file)
@@ -50,6 +50,59 @@ return array(
        // Scripts for the dynamic language specific data, like grammar forms.
        'mediawiki.language.data' => array( 'class' => 'ResourceLoaderLanguageDataModule' ),
 
+       /**
+        * Common skin styles, grouped into three graded levels.
+        *
+        * Level 1 "elements":
+        *     The base level that only contains the most basic of common skin styles.
+        *     Only styles for single elements are included, no styling for complex structures like the TOC
+        *     is present. This level is for skins that want to implement the entire style of even content area
+        *     structures like the TOC themselves.
+        *
+        * Level 2 "content":
+        *     The most commonly used level for skins implemented from scratch. This level includes all the single
+        *     element styles from "elements" as well as styles for complex structures such as the TOC that are output
+        *     in the content area by MediaWiki rather than the skin. Essentially this is the common level that lets
+        *     skins leave the style of the content area as it is normally styled, while leaving the rest of the skin
+        *     up to the skin implementation.
+        *
+        * Level 3 "interface":
+        *     The highest level, this stylesheet contains extra common styles for classes like .firstHeading, #contentSub,
+        *     et cetera which are not outputted by MediaWiki but are common to skins like MonoBook, Vector, etc...
+        *     Essentially this level is for styles that are common to MonoBook clones. And since practically every skin
+        *     that currently exists within core is a MonoBook clone, all our core skins currently use this level.
+        *
+        * These modules are typically loaded by addModuleStyles which has absolutely no concept of dependency
+        * management. As a result the skins.common.* modules contain duplicate stylesheet references instead of
+        * setting 'dependencies' to the lower level the module is based on. For this reason avoid including multiple
+        * skins.common.* modules into your skin as this will result in duplicate css.
+        */
+       'skins.common.elements' => array(
+               'styles' => array(
+                       'common/commonElements.css' => array( 'media' => 'screen' ),
+               ),
+               'remoteBasePath' => $GLOBALS['wgStylePath'],
+               'localBasePath' => $GLOBALS['wgStyleDirectory'],
+       ),
+       'skins.common.content' => array(
+               'styles' => array(
+                       'common/commonElements.css' => array( 'media' => 'screen' ),
+                       'common/commonContent.css' => array( 'media' => 'screen' ),
+               ),
+               'remoteBasePath' => $GLOBALS['wgStylePath'],
+               'localBasePath' => $GLOBALS['wgStyleDirectory'],
+       ),
+       'skins.common.interface' => array(
+               // Used in the web installer. Test it after modifying this definition!
+               'styles' => array(
+                       'common/commonElements.css' => array( 'media' => 'screen' ),
+                       'common/commonContent.css' => array( 'media' => 'screen' ),
+                       'common/commonInterface.css' => array( 'media' => 'screen' ),
+               ),
+               'remoteBasePath' => $GLOBALS['wgStylePath'],
+               'localBasePath' => $GLOBALS['wgStyleDirectory'],
+       ),
+
        /**
         * Skins
         * Be careful not to add 'scripts' to these modules,
@@ -76,16 +129,6 @@ return array(
                'remoteBasePath' => $GLOBALS['wgStylePath'],
                'localBasePath' => $GLOBALS['wgStyleDirectory'],
        ),
-       'skins.common' => array(
-               // Used in the web installer. Test it after modifying this definition!
-               'styles' => array(
-                       'common/commonElements.css' => array( 'media' => 'screen' ),
-                       'common/commonContent.css' => array( 'media' => 'screen' ),
-                       'common/commonInterface.css' => array( 'media' => 'screen' ),
-               ),
-               'remoteBasePath' => $GLOBALS['wgStylePath'],
-               'localBasePath' => $GLOBALS['wgStyleDirectory'],
-       ),
        // FIXME: Remove in favour of skins.monobook.styles when cache expires
        'skins.monobook' => array(
                'styles' => array(
index a2d4d6c..98d7e82 100644 (file)
  *
  * Example:
  *
- *     var uri = new mw.Uri( 'http://foo.com/mysite/mypage.php?quux=2' );
+ *     var uri = new mw.Uri( 'http://example.com/mysite/mypage.php?quux=2' );
  *
- *     if ( uri.host == 'foo.com' ) {
- *         uri.host = 'www.foo.com';
+ *     if ( uri.host == 'example.com' ) {
+ *         uri.host = 'foo.example.com';
  *         uri.extend( { bar: 1 } );
  *
  *         $( 'a#id1' ).attr( 'href', uri );
- *         // anchor with id 'id1' now links to http://foo.com/mysite/mypage.php?bar=1&quux=2
+ *         // anchor with id 'id1' now links to http://foo.example.com/mysite/mypage.php?bar=1&quux=2
  *
  *         $( 'a#id2' ).attr( 'href', uri.clone().extend( { bar: 3, pif: 'paf' } ) );
- *         // anchor with id 'id2' now links to http://foo.com/mysite/mypage.php?bar=3&quux=2&pif=paf
+ *         // anchor with id 'id2' now links to http://foo.example.com/mysite/mypage.php?bar=3&quux=2&pif=paf
  *     }
  *
  * Parsing here is regex based, so may not work on all URIs, but is good enough for most.
  *
  * Given a URI like
- * 'http://usr:pwd@www.test.com:81/dir/dir.2/index.htm?q1=0&&test1&test2=&test3=value+%28escaped%29&r=1&r=2#top':
+ * 'http://usr:pwd@www.example.com:81/dir/dir.2/index.htm?q1=0&&test1&test2=&test3=value+%28escaped%29&r=1&r=2#top':
  * The returned object will have the following properties:
  *
  *    protocol  'http'
  *    user      'usr'
  *    password  'pwd'
- *    host      'www.test.com'
+ *    host      'www.example.com'
  *    port      '81'
  *    path      '/dir/dir.2/index.htm'
  *    query     {
@@ -85,7 +85,7 @@
                'protocol',  // http
                'user',      // usr
                'password',  // pwd
-               'host',      // www.test.com
+               'host',      // www.example.com
                'port',      // 81
                'path',      // /dir/dir.2/index.htm
                'query',     // q1=0&&test1&test2=value (will become { q1: '0', test1: '', test2: 'value' } )
index 7764bea..89da4a2 100644 (file)
@@ -12,7 +12,9 @@ var mw = ( function ( $, undefined ) {
        /* Private Members */
 
        var hasOwn = Object.prototype.hasOwnProperty,
-               slice = Array.prototype.slice;
+               slice = Array.prototype.slice,
+               trackCallbacks = $.Callbacks( 'memory' ),
+               trackQueue = [];
 
        /**
         * Log a message to window.console, if possible. Useful to force logging of some
@@ -325,6 +327,70 @@ var mw = ( function ( $, undefined ) {
        return {
                /* Public Members */
 
+               /**
+                * Get the current time, measured in milliseconds since January 1, 1970 (UTC).
+                *
+                * On browsers that implement the Navigation Timing API, this function will produce floating-point
+                * values with microsecond precision that are guaranteed to be monotonic. On all other browsers,
+                * it will fall back to using `Date`.
+                *
+                * @returns {number} Current time
+                */
+               now: ( function () {
+                       var perf = window.performance,
+                               navStart = perf && perf.timing && perf.timing.navigationStart;
+                       return navStart && typeof perf.now === 'function' ?
+                               function () { return navStart + perf.now(); } :
+                               function () { return +new Date(); };
+               }() ),
+
+               /**
+                * Track an analytic event.
+                *
+                * This method provides a generic means for MediaWiki JavaScript code to capture state
+                * information for analysis. Each logged event specifies a string topic name that describes
+                * the kind of event that it is. Topic names consist of dot-separated path components,
+                * arranged from most general to most specific. Each path component should have a clear and
+                * well-defined purpose.
+                *
+                * Data handlers are registered via `mw.trackSubscribe`, and receive the full set of
+                * events that match their subcription, including those that fired before the handler was
+                * bound.
+                *
+                * @param {string} topic Topic name
+                * @param {Object} [data] Data describing the event, encoded as an object
+                */
+               track: function ( topic, data ) {
+                       trackQueue.push( { topic: topic, timeStamp: mw.now(), data: data } );
+                       trackCallbacks.fire( trackQueue );
+               },
+
+               /**
+                * Register a handler for subset of analytic events, specified by topic
+                *
+                * Handlers will be called once for each tracked event, including any events that fired before the
+                * handler was registered; 'this' is set to a plain object with a 'timeStamp' property indicating
+                * the exact time at which the event fired, a string 'topic' property naming the event, and a
+                * 'data' property which is an object of event-specific data. The event topic and event data are
+                * also passed to the callback as the first and second arguments, respectively.
+                *
+                * @param {string} topic Handle events whose name starts with this string prefix
+                * @param {Function} callback Handler to call for each matching tracked event
+                */
+               trackSubscribe: function ( topic, callback ) {
+                       var seen = 0;
+
+                       trackCallbacks.add( function ( trackQueue ) {
+                               var event;
+                               for ( ; seen < trackQueue.length; seen++ ) {
+                                       event = trackQueue[ seen ];
+                                       if ( event.topic.indexOf( topic ) === 0 ) {
+                                               callback.call( event, event.topic, event.data );
+                                       }
+                               }
+                       } );
+               },
+
                /**
                 * Dummy placeholder for {@link mw.log}
                 * @method
index fdcb0c1..bd914ce 100644 (file)
@@ -45,7 +45,7 @@ class SkinMonoBook extends SkinTemplate {
        function setupSkinUserCss( OutputPage $out ) {
                parent::setupSkinUserCss( $out );
 
-               $out->addModuleStyles( array( 'skins.common', 'skins.monobook.styles' ) );
+               $out->addModuleStyles( array( 'skins.common.interface', 'skins.monobook.styles' ) );
 
                // TODO: Migrate all of these
                $out->addStyle( 'monobook/IE60Fixes.css', 'screen', 'IE 6' );
index a9ab8fb..00b920a 100644 (file)
@@ -67,7 +67,7 @@ class SkinVector extends SkinTemplate {
        function setupSkinUserCss( OutputPage $out ) {
                parent::setupSkinUserCss( $out );
 
-               $styles = array( 'skins.common', 'skins.vector.styles' );
+               $styles = array( 'skins.common.interface', 'skins.vector.styles' );
                wfRunHooks( 'SkinVectorStyleModules', array( &$this, &$styles ) );
                $out->addModuleStyles( $styles );
        }
index 9913f5e..b6a8a46 100644 (file)
 
                assert.throws(
                        function () {
-                               return new mw.Uri( 'foo.com/bar/baz', {
+                               return new mw.Uri( 'example.com/bar/baz', {
                                        strictMode: true
                                } );
                        },
                        'throw error on URI without protocol or // or leading / in strict mode'
                );
 
-               uri = new mw.Uri( 'foo.com/bar/baz', {
+               uri = new mw.Uri( 'example.com/bar/baz', {
                        strictMode: false
                } );
-               assert.equal( uri.toString(), 'http://foo.com/bar/baz', 'normalize URI without protocol or // in loose mode' );
+               assert.equal( uri.toString(), 'http://example.com/bar/baz', 'normalize URI without protocol or // in loose mode' );
        } );
 
        QUnit.test( 'Constructor( Object )', 3, function ( assert ) {
        } );
 
        QUnit.test( '.getQueryString()', 2, function ( assert ) {
-               var uri = new mw.Uri( 'http://www.google.com/?q=uri' );
+               var uri = new mw.Uri( 'http://search.example.com/?q=uri' );
 
                assert.deepEqual(
                        {
                        },
                        {
                                protocol: 'http',
-                               host: 'www.google.com',
+                               host: 'search.example.com',
                                port: undefined,
                                path: '/',
                                query: { q: 'uri' },
                        'basic object properties'
                );
 
-               uri = new mw.Uri( 'https://example.org/mw/index.php?title=Sandbox/7&other=Sandbox/7&foo' );
+               uri = new mw.Uri( 'https://example.com/mw/index.php?title=Sandbox/7&other=Sandbox/7&foo' );
                assert.equal(
                        uri.getQueryString(),
                        'title=Sandbox/7&other=Sandbox%2F7&foo',