Merge "mediawiki.special.changeslist is a skinStyle module"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Tue, 26 Mar 2019 23:45:35 +0000 (23:45 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Tue, 26 Mar 2019 23:45:35 +0000 (23:45 +0000)
22 files changed:
includes/Linker.php
includes/actions/pagers/HistoryPager.php
includes/api/ApiStashEdit.php
includes/libs/objectcache/BagOStuff.php
includes/libs/objectcache/HashBagOStuff.php
includes/libs/objectcache/IExpiringStore.php
includes/libs/rdbms/database/DBConnRef.php
includes/libs/rdbms/database/Database.php
includes/libs/rdbms/database/DatabaseMysqlBase.php
includes/libs/rdbms/database/DatabasePostgres.php
includes/libs/rdbms/database/DatabaseSqlite.php
includes/libs/rdbms/database/IDatabase.php
includes/objectcache/SqlBagOStuff.php
resources/Resources.php
resources/src/jquery.spinner/images/spinner-large.gif [deleted file]
resources/src/jquery.spinner/images/spinner.gif [deleted file]
resources/src/jquery.spinner/spinner.css [deleted file]
resources/src/jquery.spinner/spinner.js
resources/src/jquery.spinner/spinner.less [new file with mode: 0644]
tests/phpunit/includes/db/DatabaseSqliteTest.php
tests/phpunit/includes/db/DatabaseTestHelper.php
tests/phpunit/includes/libs/rdbms/database/DatabaseSQLTest.php

index ec3b245..df99556 100644 (file)
@@ -1093,7 +1093,7 @@ class Linker {
                        $link = wfMessage( 'rev-deleted-user' )->escaped();
                }
                if ( $rev->isDeleted( Revision::DELETED_USER ) ) {
-                       return ' <span class="history-deleted">' . $link . '</span>';
+                       return ' <span class="history-deleted mw-userlink">' . $link . '</span>';
                }
                return $link;
        }
@@ -1519,7 +1519,7 @@ class Linker {
                        $block = " <span class=\"comment\">" . wfMessage( 'rev-deleted-comment' )->escaped() . "</span>";
                }
                if ( $rev->isDeleted( Revision::DELETED_COMMENT ) ) {
-                       return " <span class=\"history-deleted\">$block</span>";
+                       return " <span class=\"history-deleted comment\">$block</span>";
                }
                return $block;
        }
index 9e4080d..b333372 100644 (file)
@@ -485,7 +485,7 @@ class HistoryPager extends ReverseChronologicalPager {
                        $link = htmlspecialchars( $date );
                }
                if ( $rev->isDeleted( Revision::DELETED_TEXT ) ) {
-                       $link = "<span class=\"history-deleted\">$link</span>";
+                       $link = "<span class=\"history-deleted mw-changeslist-date\">$link</span>";
                }
 
                return $link;
index 455ff45..fe5f6c4 100644 (file)
@@ -46,6 +46,8 @@ class ApiStashEdit extends ApiBase {
        const MAX_CACHE_TTL = 300; // 5 minutes
        const MAX_SIGNATURE_TTL = 60;
 
+       const MAX_CACHE_RECENT = 2;
+
        public function execute() {
                $user = $this->getUser();
                $params = $this->extractRequestParams();
@@ -461,9 +463,32 @@ class ApiStashEdit extends ApiBase {
                        );
                }
 
+               if ( $ok ) {
+                       // These blobs can waste slots in low cardinality memcached slabs
+                       self::pruneExcessStashedEntries( $cache, $user, $key );
+               }
+
                return $ok ? true : 'store_error';
        }
 
+       /**
+        * @param BagOStuff $cache
+        * @param User $user
+        * @param string $newKey
+        */
+       private static function pruneExcessStashedEntries( BagOStuff $cache, User $user, $newKey ) {
+               $key = $cache->makeKey( 'stash-edit-recent', $user->getId() );
+
+               $keyList = $cache->get( $key ) ?: [];
+               if ( count( $keyList ) >= self::MAX_CACHE_RECENT ) {
+                       $oldestKey = array_shift( $keyList );
+                       $cache->delete( $oldestKey );
+               }
+
+               $keyList[] = $newKey;
+               $cache->set( $key, $keyList, 2 * self::MAX_CACHE_TTL );
+       }
+
        public function getAllowedParams() {
                return [
                        'title' => [
index 4fe64f2..bdfed82 100644 (file)
@@ -260,6 +260,17 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
         */
        abstract public function delete( $key, $flags = 0 );
 
+       /**
+        * Insert an item if it does not already exist
+        *
+        * @param string $key
+        * @param mixed $value
+        * @param int $exptime
+        * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
+        * @return bool Success
+        */
+       abstract public function add( $key, $value, $exptime = 0, $flags = 0 );
+
        /**
         * Merge changes into the existing cache value (possibly creating a new one)
         *
@@ -587,16 +598,6 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                return $res;
        }
 
-       /**
-        * Insertion
-        * @param string $key
-        * @param mixed $value
-        * @param int $exptime
-        * @param int $flags Bitfield of BagOStuff::WRITE_* constants (since 1.33)
-        * @return bool Success
-        */
-       abstract public function add( $key, $value, $exptime = 0, $flags = 0 );
-
        /**
         * Increase stored value of $key by $value while preserving its TTL
         * @param string $key Key to increase
@@ -703,13 +704,21 @@ abstract class BagOStuff implements IExpiringStore, LoggerAwareInterface {
                }
        }
 
+       /**
+        * @param int $exptime
+        * @return bool
+        */
+       protected function expiryIsRelative( $exptime ) {
+               return ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) );
+       }
+
        /**
         * Convert an optionally relative time to an absolute time
         * @param int $exptime
         * @return int
         */
-       protected function convertExpiry( $exptime ) {
-               if ( $exptime != 0 && $exptime < ( 10 * self::TTL_YEAR ) ) {
+       protected function convertToExpiry( $exptime ) {
+               if ( $this->expiryIsRelative( $exptime ) ) {
                        return (int)$this->getCurrentTime() + $exptime;
                } else {
                        return $exptime;
index 3c6520e..eaea2d1 100644 (file)
@@ -91,7 +91,7 @@ class HashBagOStuff extends BagOStuff {
                unset( $this->bag[$key] );
                $this->bag[$key] = [
                        self::KEY_VAL => $value,
-                       self::KEY_EXP => $this->convertExpiry( $exptime ),
+                       self::KEY_EXP => $this->convertToExpiry( $exptime ),
                        self::KEY_CAS => $this->token . ':' . ++self::$casCounter
                ];
 
index c1edff7..61a4c61 100644 (file)
@@ -21,7 +21,7 @@
  */
 
 /**
- * Generic base class for storage interfaces.
+ * Generic interface for lightweight expiring object stores.
  *
  * Provides convenient TTL constants.
  *
@@ -44,16 +44,19 @@ interface IExpiringStore {
 
        const TTL_INDEFINITE = 0;
 
-       // Attribute and QoS constants; higher QOS values with the same prefix rank higher...
-       // Medium attributes constants related to emulation or media type
+       // Emulation/fallback medium attribute (e.g. SQLBagOStuff)
        const ATTR_EMULATION = 1;
+       // Quality of service constants for ATTR_EMULATION (higher means faster)
        const QOS_EMULATION_SQL = 1;
-       // Medium attributes constants related to replica consistency
-       const ATTR_SYNCWRITES = 2; // SYNC_WRITES flag support
+
+       // Replica synchronization/consistency attribute of medium when SYNC_WRITES is used
+       const ATTR_SYNCWRITES = 2;
+       // Quality of service constants for ATTR_SYNCWRITES (higher means more consistent)
        const QOS_SYNCWRITES_NONE = 1; // replication only supports eventual consistency or less
        const QOS_SYNCWRITES_BE = 2; // best effort synchronous with limited retries
        const QOS_SYNCWRITES_QC = 3; // write quorum applied directly to state machines where R+W > N
        const QOS_SYNCWRITES_SS = 4; // strict-serializable, nodes refuse reads if possible stale
+
        // Generic "unknown" value that is useful for comparisons (e.g. always good enough)
        const QOS_UNKNOWN = INF;
 
index 70b0583..cf582b7 100644 (file)
@@ -254,7 +254,7 @@ class DBConnRef implements IDatabase {
                throw new DBUnexpectedError( $this->conn, 'Cannot close shared connection.' );
        }
 
-       public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
+       public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
                return $this->__call( __FUNCTION__, func_get_args() );
        }
 
index 9ce3086..dea7aab 100644 (file)
@@ -280,6 +280,11 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
        /** @var int No transaction is active */
        const STATUS_TRX_NONE = 3;
 
+       /** @var int Writes to this temporary table do not affect lastDoneWrites() */
+       const TEMP_NORMAL = 1;
+       /** @var int Writes to this temporary table effect lastDoneWrites() */
+       const TEMP_PSEUDO_PERMANENT = 2;
+
        /**
         * @note exceptions for missing libraries/drivers should be thrown in initConnection()
         * @param array $params Parameters passed from Database::factory()
@@ -1135,47 +1140,55 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        /**
         * @param string $sql A SQL query
-        * @return bool Whether $sql is SQL for TEMPORARY table operation
+        * @param bool $pseudoPermanent Treat any table from CREATE TEMPORARY as pseudo-permanent
+        * @return int|null A self::TEMP_* constant for temp table operations or null otherwise
         */
-       protected function registerTempTableOperation( $sql ) {
+       protected function registerTempTableWrite( $sql, $pseudoPermanent ) {
+               static $qt = '[`"\']?(\w+)[`"\']?'; // quoted table
+
                if ( preg_match(
-                       '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+                       '/^CREATE\s+TEMPORARY\s+TABLE\s+(?:IF\s+NOT\s+EXISTS\s+)?' . $qt . '/i',
                        $sql,
                        $matches
                ) ) {
-                       $this->sessionTempTables[$matches[1]] = 1;
+                       $type = $pseudoPermanent ? self::TEMP_PSEUDO_PERMANENT : self::TEMP_NORMAL;
+                       $this->sessionTempTables[$matches[1]] = $type;
 
-                       return true;
+                       return $type;
                } elseif ( preg_match(
-                       '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+                       '/^DROP\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
                        $sql,
                        $matches
                ) ) {
-                       $isTemp = isset( $this->sessionTempTables[$matches[1]] );
+                       $type = $this->sessionTempTables[$matches[1]] ?? null;
                        unset( $this->sessionTempTables[$matches[1]] );
 
-                       return $isTemp;
+                       return $type;
                } elseif ( preg_match(
-                       '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?[`"\']?(\w+)[`"\']?/i',
+                       '/^TRUNCATE\s+(?:TEMPORARY\s+)?TABLE\s+(?:IF\s+EXISTS\s+)?' . $qt . '/i',
                        $sql,
                        $matches
                ) ) {
-                       return isset( $this->sessionTempTables[$matches[1]] );
+                       return $this->sessionTempTables[$matches[1]] ?? null;
                } elseif ( preg_match(
-                       '/^(?:INSERT\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+[`"\']?(\w+)[`"\']?/i',
+                       '/^(?:(?:INSERT|REPLACE)\s+(?:\w+\s+)?INTO|UPDATE|DELETE\s+FROM)\s+' . $qt . '/i',
                        $sql,
                        $matches
                ) ) {
-                       return isset( $this->sessionTempTables[$matches[1]] );
+                       return $this->sessionTempTables[$matches[1]] ?? null;
                }
 
-               return false;
+               return null;
        }
 
-       public function query( $sql, $fname = __METHOD__, $tempIgnore = false ) {
+       public function query( $sql, $fname = __METHOD__, $flags = 0 ) {
                $this->assertTransactionStatus( $sql, $fname );
                $this->assertHasConnectionHandle();
 
+               $flags = (int)$flags; // b/c; this field used to be a bool
+               $ignoreErrors = $this->hasFlags( $flags, self::QUERY_SILENCE_ERRORS );
+               $pseudoPermanent = $this->hasFlags( $flags, self::QUERY_PSEUDO_PERMANENT );
+
                $priorTransaction = $this->trxLevel;
                $priorWritesPending = $this->writesOrCallbacksPending();
                $this->lastQuery = $sql;
@@ -1184,8 +1197,10 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                        # In theory, non-persistent writes are allowed in read-only mode, but due to things
                        # like https://bugs.mysql.com/bug.php?id=33669 that might not work anyway...
                        $this->assertIsWritableMaster();
-                       # Avoid treating temporary table operations as meaningful "writes"
-                       $isEffectiveWrite = !$this->registerTempTableOperation( $sql );
+                       # Do not treat temporary table writes as "meaningful writes" that need committing.
+                       # Profile them as reads. Integration tests can override this behavior via $flags.
+                       $tableType = $this->registerTempTableWrite( $sql, $pseudoPermanent );
+                       $isEffectiveWrite = ( $tableType !== self::TEMP_NORMAL );
                } else {
                        $isEffectiveWrite = false;
                }
@@ -1240,12 +1255,12 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                                        $this->trxStatus = self::STATUS_TRX_ERROR;
                                        $this->trxStatusCause =
                                                $this->getQueryExceptionAndLog( $lastError, $lastErrno, $sql, $fname );
-                                       $tempIgnore = false; // cannot recover
+                                       $ignoreErrors = false; // cannot recover
                                        $this->trxStatusIgnoredCause = null;
                                }
                        }
 
-                       $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $tempIgnore );
+                       $this->reportQueryError( $lastError, $lastErrno, $sql, $fname, $ignoreErrors );
                }
 
                return $this->resultObject( $ret );
@@ -1514,17 +1529,17 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
 
        /**
         * Report a query error. Log the error, and if neither the object ignore
-        * flag nor the $tempIgnore flag is set, throw a DBQueryError.
+        * flag nor the $ignoreErrors flag is set, throw a DBQueryError.
         *
         * @param string $error
         * @param int $errno
         * @param string $sql
         * @param string $fname
-        * @param bool $tempIgnore
+        * @param bool $ignoreErrors
         * @throws DBQueryError
         */
-       public function reportQueryError( $error, $errno, $sql, $fname, $tempIgnore = false ) {
-               if ( $tempIgnore ) {
+       public function reportQueryError( $error, $errno, $sql, $fname, $ignoreErrors = false ) {
+               if ( $ignoreErrors ) {
                        $this->queryLogger->debug( "SQL ERROR (ignored): $error\n" );
                } else {
                        $exception = $this->getQueryExceptionAndLog( $error, $errno, $sql, $fname );
@@ -4684,6 +4699,15 @@ abstract class Database implements IDatabase, IMaintainableDatabase, LoggerAware
                $this->indexAliases = $aliases;
        }
 
+       /**
+        * @param int $field
+        * @param int $flags
+        * @return bool
+        */
+       protected function hasFlags( $field, $flags ) {
+               return ( ( $field & $flags ) === $flags );
+       }
+
        /**
         * Get the underlying binding connection handle
         *
index 77bb677..7fccd57 100644 (file)
@@ -1460,7 +1460,7 @@ abstract class DatabaseMysqlBase extends Database {
                $oldName = $this->addIdentifierQuotes( $oldName );
                $query = "CREATE $tmp TABLE $newName (LIKE $oldName)";
 
-               return $this->query( $query, $fname );
+               return $this->query( $query, $fname, $this::QUERY_PSEUDO_PERMANENT );
        }
 
        /**
index 4ae4104..a5dc171 100644 (file)
@@ -819,8 +819,12 @@ __INDEXATTR__;
 
                $temporary = $temporary ? 'TEMPORARY' : '';
 
-               $ret = $this->query( "CREATE $temporary TABLE $newNameE " .
-                       "(LIKE $oldNameE INCLUDING DEFAULTS INCLUDING INDEXES)", $fname );
+               $ret = $this->query(
+                       "CREATE $temporary TABLE $newNameE " .
+                               "(LIKE $oldNameE INCLUDING DEFAULTS INCLUDING INDEXES)",
+                       $fname,
+                       $this::QUERY_PSEUDO_PERMANENT
+               );
                if ( !$ret ) {
                        return $ret;
                }
@@ -842,7 +846,10 @@ __INDEXATTR__;
                        $fieldE = $this->addIdentifierQuotes( $field );
                        $newSeqE = $this->addIdentifierQuotes( $newSeq );
                        $newSeqQ = $this->addQuotes( $newSeq );
-                       $this->query( "CREATE $temporary SEQUENCE $newSeqE OWNED BY $newNameE.$fieldE", $fname );
+                       $this->query(
+                               "CREATE $temporary SEQUENCE $newSeqE OWNED BY $newNameE.$fieldE",
+                               $fname
+                       );
                        $this->query(
                                "ALTER TABLE $newNameE ALTER COLUMN $fieldE SET DEFAULT nextval({$newSeqQ}::regclass)",
                                $fname
index f2bc01d..82a7e35 100644 (file)
@@ -1018,7 +1018,7 @@ class DatabaseSqlite extends Database {
                        }
                }
 
-               $res = $this->query( $sql, $fname );
+               $res = $this->query( $sql, $fname, self::QUERY_PSEUDO_PERMANENT );
 
                // Take over indexes
                $indexList = $this->query( 'PRAGMA INDEX_LIST(' . $this->addQuotes( $oldName ) . ')' );
index 6f58cc6..eac9bae 100644 (file)
@@ -106,6 +106,14 @@ interface IDatabase {
        /** @var int Enable compression in connection protocol */
        const DBO_COMPRESS = 512;
 
+       /** @var int Ignore query errors and return false when they happen */
+       const QUERY_SILENCE_ERRORS = 1; // b/c for 1.32 query() argument; note that (int)true = 1
+       /**
+        * @var int Treat the TEMPORARY table from the given CREATE query as if it is
+        *   permanent as far as write tracking is concerned. This is useful for testing.
+        */
+       const QUERY_PSEUDO_PERMANENT = 2;
+
        /**
         * A string describing the current software version, and possibly
         * other details in a user-friendly way. Will be listed on Special:Version, etc.
@@ -527,13 +535,13 @@ interface IDatabase {
         * @param string $sql SQL query
         * @param string $fname Name of the calling function, for profiling/SHOW PROCESSLIST
         *     comment (you can use __METHOD__ or add some extra info)
-        * @param bool $tempIgnore Whether to avoid throwing an exception on errors...
-        *     maybe best to catch the exception instead?
+        * @param int $flags Bitfield of IDatabase::QUERY_* constants. Note that suppression
+        *     of errors is best handled by try/catch rather than using one of these flags.
         * @return bool|IResultWrapper True for a successful write query, IResultWrapper object
-        *     for a successful read query, or false on failure if $tempIgnore set
+        *     for a successful read query, or false on failure if QUERY_SILENCE_ERRORS is set.
         * @throws DBError
         */
-       public function query( $sql, $fname = __METHOD__, $tempIgnore = false );
+       public function query( $sql, $fname = __METHOD__, $flags = 0 );
 
        /**
         * Free a result object returned by query() or select(). It's usually not
index b2d61a8..e450212 100644 (file)
@@ -344,7 +344,7 @@ class SqlBagOStuff extends BagOStuff {
                        if ( $exptime == 0 ) {
                                $encExpiry = $this->getMaxDateTime( $db );
                        } else {
-                               $exptime = $this->convertExpiry( $exptime );
+                               $exptime = $this->convertToExpiry( $exptime );
                                $encExpiry = $db->timestamp( $exptime );
                        }
                        foreach ( $serverKeys as $tableName => $tableKeys ) {
@@ -406,7 +406,7 @@ class SqlBagOStuff extends BagOStuff {
                        if ( $exptime == 0 ) {
                                $encExpiry = $this->getMaxDateTime( $db );
                        } else {
-                               $exptime = $this->convertExpiry( $exptime );
+                               $exptime = $this->convertToExpiry( $exptime );
                                $encExpiry = $db->timestamp( $exptime );
                        }
                        // (T26425) use a replace if the db supports it instead of
@@ -542,7 +542,7 @@ class SqlBagOStuff extends BagOStuff {
                        $db = $this->getDB( $serverIndex );
                        $db->update(
                                $tableName,
-                               [ 'exptime' => $db->timestamp( $this->convertExpiry( $expiry ) ) ],
+                               [ 'exptime' => $db->timestamp( $this->convertToExpiry( $expiry ) ) ],
                                [ 'keyname' => $key, 'exptime > ' . $db->addQuotes( $db->timestamp( time() ) ) ],
                                __METHOD__
                        );
index cb32340..23b0baf 100644 (file)
@@ -294,7 +294,7 @@ return [
        ],
        'jquery.spinner' => [
                'scripts' => 'resources/src/jquery.spinner/spinner.js',
-               'styles' => 'resources/src/jquery.spinner/spinner.css',
+               'styles' => 'resources/src/jquery.spinner/spinner.less',
                'targets' => [ 'desktop', 'mobile' ],
        ],
        'jquery.jStorage' => [
@@ -1454,7 +1454,9 @@ return [
                'styles' => 'resources/src/mediawiki.action/mediawiki.action.history.css',
        ],
        'mediawiki.action.history.styles' => [
-               'styles' => 'resources/src/mediawiki.action/mediawiki.action.history.styles.css',
+               'skinStyles' => [
+                       'default' => 'resources/src/mediawiki.action/mediawiki.action.history.styles.css',
+               ],
        ],
        'mediawiki.action.view.dblClickEdit' => [
                'scripts' => 'resources/src/mediawiki.action/mediawiki.action.view.dblClickEdit.js',
@@ -2024,8 +2026,8 @@ return [
                        'parentheses-end',
                        'pipe-separator'
                ],
-               'styles' => [
-                       'resources/src/mediawiki.interface.helpers.styles.less',
+               'skinStyles' => [
+                       'default' => 'resources/src/mediawiki.interface.helpers.styles.less',
                ],
                'targets' => [
                        'desktop', 'mobile'
diff --git a/resources/src/jquery.spinner/images/spinner-large.gif b/resources/src/jquery.spinner/images/spinner-large.gif
deleted file mode 100644 (file)
index 72203fd..0000000
Binary files a/resources/src/jquery.spinner/images/spinner-large.gif and /dev/null differ
diff --git a/resources/src/jquery.spinner/images/spinner.gif b/resources/src/jquery.spinner/images/spinner.gif
deleted file mode 100644 (file)
index 6146be4..0000000
Binary files a/resources/src/jquery.spinner/images/spinner.gif and /dev/null differ
diff --git a/resources/src/jquery.spinner/spinner.css b/resources/src/jquery.spinner/spinner.css
deleted file mode 100644 (file)
index 9c819a6..0000000
+++ /dev/null
@@ -1,36 +0,0 @@
-.mw-spinner {
-       background-color: transparent;
-       background-position: center center;
-       background-repeat: no-repeat;
-}
-
-.mw-spinner-small {
-       /* @embed */
-       background-image: url( images/spinner.gif );
-       height: 20px;
-       width: 20px;
-       /* Avoid issues with .mw-spinner-block when floated without width. */
-       min-width: 20px;
-}
-
-.mw-spinner-large {
-       /* @embed */
-       background-image: url( images/spinner-large.gif );
-       height: 32px;
-       width: 32px;
-       /* Avoid issues with .mw-spinner-block when floated without width. */
-       min-width: 32px;
-}
-
-.mw-spinner-block {
-       display: block;
-       /* This overrides width from .mw-spinner-large / .mw-spinner-small,
-        * This is where the min-width kicks in.
-        */
-       width: 100%;
-}
-
-.mw-spinner-inline {
-       display: inline-block;
-       vertical-align: middle;
-}
index 5dfbab7..8cccedb 100644 (file)
@@ -59,7 +59,7 @@
                 * @return {jQuery}
                 */
                createSpinner: function ( opts ) {
-                       var $spinner;
+                       var i, $spinner, $container;
 
                        if ( typeof opts === 'string' ) {
                                opts = {
                                $spinner.attr( 'id', 'mw-spinner-' + opts.id );
                        }
 
-                       $spinner.addClass( opts.size === 'large' ? 'mw-spinner-large' : 'mw-spinner-small' );
-                       $spinner.addClass( opts.type === 'block' ? 'mw-spinner-block' : 'mw-spinner-inline' );
+                       $spinner
+                               .addClass( opts.size === 'large' ? 'mw-spinner-large' : 'mw-spinner-small' )
+                               .addClass( opts.type === 'block' ? 'mw-spinner-block' : 'mw-spinner-inline' );
+
+                       $container = $( '<div>' ).addClass( 'mw-spinner-container' ).appendTo( $spinner );
+                       for ( i = 0; i < 12; i++ ) {
+                               $container.append( $( '<div>' ) );
+                       }
 
                        return $spinner;
                },
diff --git a/resources/src/jquery.spinner/spinner.less b/resources/src/jquery.spinner/spinner.less
new file mode 100644 (file)
index 0000000..596858e
--- /dev/null
@@ -0,0 +1,143 @@
+.mw-spinner {
+       position: relative;
+
+       > .mw-spinner-container {
+               transform-origin: 0 0;
+       }
+}
+
+@mw-spinner-small-size: 20px;
+@mw-spinner-large-size: 32px;
+
+.mw-spinner-small {
+       width: @mw-spinner-small-size;
+       height: @mw-spinner-small-size;
+
+       > .mw-spinner-container {
+               transform: scale( unit( @mw-spinner-small-size / 64 ) );
+       }
+}
+
+.mw-spinner-large {
+       width: @mw-spinner-large-size;
+       height: @mw-spinner-large-size;
+
+       > .mw-spinner-container {
+               transform: scale( unit( @mw-spinner-large-size / 64 ) );
+       }
+}
+
+.mw-spinner-block {
+       display: block;
+       width: 100%;
+       text-align: center;
+
+       > .mw-spinner-container {
+               display: inline-block;
+               vertical-align: top;
+       }
+
+       &.mw-spinner-small > .mw-spinner-container {
+               min-width: @mw-spinner-small-size;
+       }
+
+       &.mw-spinner-large > .mw-spinner-container {
+               min-width: @mw-spinner-large-size;
+       }
+}
+
+.mw-spinner-inline {
+       display: inline-block;
+       vertical-align: middle;
+}
+
+/**
+ * CSS loading spinner adapted from loadingio, CC0
+ * https://github.com/loadingio/css-spinner/
+ */
+.mw-spinner-container > div {
+       transform-origin: 32px 32px;
+       animation: mw-spinner 1.2s linear infinite;
+}
+
+.mw-spinner-container > div:after {
+       content: ' ';
+       display: block;
+       position: absolute;
+       top: 3px;
+       left: 29px;
+       width: 5px;
+       height: 14px;
+       border-radius: 20%;
+       background: #000;
+}
+
+.mw-spinner-container > div:nth-child( 1 ) {
+       transform: rotate( 0deg );
+       animation-delay: -1.1s;
+}
+
+.mw-spinner-container > div:nth-child( 2 ) {
+       transform: rotate( 30deg );
+       animation-delay: -1s;
+}
+
+.mw-spinner-container > div:nth-child( 3 ) {
+       transform: rotate( 60deg );
+       animation-delay: -0.9s;
+}
+
+.mw-spinner-container > div:nth-child( 4 ) {
+       transform: rotate( 90deg );
+       animation-delay: -0.8s;
+}
+
+.mw-spinner-container > div:nth-child( 5 ) {
+       transform: rotate( 120deg );
+       animation-delay: -0.7s;
+}
+
+.mw-spinner-container > div:nth-child( 6 ) {
+       transform: rotate( 150deg );
+       animation-delay: -0.6s;
+}
+
+.mw-spinner-container > div:nth-child( 7 ) {
+       transform: rotate( 180deg );
+       animation-delay: -0.5s;
+}
+
+.mw-spinner-container > div:nth-child( 8 ) {
+       transform: rotate( 210deg );
+       animation-delay: -0.4s;
+}
+
+.mw-spinner-container > div:nth-child( 9 ) {
+       transform: rotate( 240deg );
+       animation-delay: -0.3s;
+}
+
+.mw-spinner-container > div:nth-child( 10 ) {
+       transform: rotate( 270deg );
+       animation-delay: -0.2s;
+}
+
+.mw-spinner-container > div:nth-child( 11 ) {
+       transform: rotate( 300deg );
+       animation-delay: -0.1s;
+}
+
+.mw-spinner-container > div:nth-child( 12 ) {
+       transform: rotate( 330deg );
+       animation-delay: 0s;
+}
+
+@keyframes mw-spinner {
+       0% {
+               opacity: 1;
+       }
+
+       100% {
+               opacity: 0;
+       }
+}
index 2ea737f..63b24dc 100644 (file)
@@ -531,7 +531,7 @@ class DatabaseSqliteMock extends DatabaseSqlite {
                return Database::factory( 'SqliteMock', $p );
        }
 
-       function query( $sql, $fname = '', $tempIgnore = false ) {
+       function query( $sql, $fname = '', $flags = 0 ) {
                return true;
        }
 
index 9679c6c..fb4041d 100644 (file)
@@ -133,10 +133,10 @@ class DatabaseTestHelper extends Database {
                return $s;
        }
 
-       public function query( $sql, $fname = '', $tempIgnore = false ) {
+       public function query( $sql, $fname = '', $flags = 0 ) {
                $this->checkFunctionName( $fname );
 
-               return parent::query( $sql, $fname, $tempIgnore );
+               return parent::query( $sql, $fname, $flags );
        }
 
        public function tableExists( $table, $fname = __METHOD__ ) {
index 40c260c..c0d2555 100644 (file)
@@ -1343,7 +1343,7 @@ class DatabaseSQLTest extends PHPUnit\Framework\TestCase {
        }
 
        /**
-        * @covers Wikimedia\Rdbms\Database::registerTempTableOperation
+        * @covers Wikimedia\Rdbms\Database::registerTempTableWrite
         */
        public function testSessionTempTables() {
                $temp1 = $this->database->tableName( 'tmp_table_1' );