Merge "autoloader entry for UserNotLoggedIn class"
authorNikerabbit <niklas.laxstrom@gmail.com>
Wed, 1 Aug 2012 11:15:08 +0000 (11:15 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 1 Aug 2012 11:15:08 +0000 (11:15 +0000)
15 files changed:
RELEASE-NOTES-1.20
includes/DefaultSettings.php
includes/Linker.php
includes/WikiMap.php
includes/api/ApiUpload.php
includes/specials/SpecialChangeEmail.php
languages/messages/MessagesEn.php
languages/messages/MessagesLt.php
languages/messages/MessagesQqq.php
languages/messages/MessagesSgs.php
maintenance/language/messages.inc
resources/Resources.php
resources/mediawiki/mediawiki.user.js
tests/phpunit/includes/libs/CSSJanusTest.php [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js

index 9383bf2..daed79c 100644 (file)
@@ -105,6 +105,11 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki.
   and 'subcats'
 * (bug 38362) Make Special:Listuser includeable on wiki pages.
 * Added support in jquery.localize for placeholder attributes.
+* (bug 38151) Implemented mw.user.getRights for getting and caching the current
+  user's user rights.
+* Implemented mw.user.getGroups for getting and caching user groups.
+* (bug 37830) Added $wgRequirePasswordforEmailChange to control whether password
+  confirmation is required for changing an email address or not.
 
 === Bug fixes in 1.20 ===
 * (bug 30245) Use the correct way to construct a log page title.
@@ -176,6 +181,8 @@ upgrade PHP if you have not done so prior to upgrading MediaWiki.
 * (bug 38093) Gender of changed user groups missing in Special:Log/rights
 * (bug 35893) Special:Block needs to load mediawiki.special.block.js.
 * (bug 37331) ResourceLoader modules sometimes execute twice in Firefox
+* (bug 31644) GlobalUsage, CentralAuth and AbuseLog extensions should not use
+  insecure links to foreign wikis in the WikiMap.
 
 === API changes in 1.20 ===
 * (bug 34316) Add ability to retrieve maximum upload size from MediaWiki API.
@@ -236,6 +243,8 @@ changes to languages because of Bugzilla reports.
   and only applies to MyISAM or similar DBs. Those should only be used
   for archived sites anyway. We can't get edit conflicts on such sites,
   so the WikiPage code wasn't useful there either.
+* Deprecated mw.user.name in favour of mw.user.getName.
+* Deprecated mw.user.anonymous in favour of mw.user.isAnon.
 
 == Compatibility ==
 
index abf25dc..69b632c 100644 (file)
@@ -2685,6 +2685,14 @@ $wgBetterDirectionality = true;
  */
 $wgSend404Code = true;
 
+
+/**
+ * The $wgShowRollbackEditCount variable is used to show how many edits will be
+ * rollback. The numeric value of the varible are the limit up to are counted.
+ * If the value is false or 0, the edits are not counted.
+ */
+$wgShowRollbackEditCount = 10;
+
 /** @} */ # End of output format settings }
 
 /*************************************************************************//**
@@ -6137,6 +6145,11 @@ $wgSeleniumConfigFile = null;
 $wgDBtestuser = ''; //db user that has permission to create and drop the test databases only
 $wgDBtestpassword = '';
 
+/**
+ * Whether the user must enter their password to change their e-mail address
+ */
+$wgRequirePasswordforEmailChange = true;
+
 /**
  * For really cool vim folding this needs to be at the end:
  * vim: foldmarker=@{,@} foldmethod=marker
index 083845d..ae334c6 100644 (file)
@@ -1673,6 +1673,8 @@ class Linker {
         * @return String: HTML fragment
         */
        public static function buildRollbackLink( $rev, IContextSource $context = null ) {
+               global $wgShowRollbackEditCount;
+
                if ( $context === null ) {
                        $context = RequestContext::getMain();
                }
@@ -1687,13 +1689,50 @@ class Linker {
                        $query['bot'] = '1';
                        $query['hidediff'] = '1'; // bug 15999
                }
-               return self::link(
-                       $title,
-                       $context->msg( 'rollbacklink' )->escaped(),
-                       array( 'title' => $context->msg( 'tooltip-rollback' )->text() ),
-                       $query,
-                       array( 'known', 'noclasses' )
-               );
+
+               if( is_int( $wgShowRollbackEditCount ) && $wgShowRollbackEditCount > 0 ) {
+                       $dbr = wfGetDB( DB_SLAVE );
+
+                       // Up to the value of $wgShowRollbackEditCount revisions are counted
+                       $res = $dbr->select( 'revision',
+                               array( 'rev_id', 'rev_user_text' ),
+                               array( 'rev_page' => $rev->getPage() ),
+                               __METHOD__,
+                               array(  'USE INDEX' => 'page_timestamp',
+                                       'ORDER BY' => 'rev_timestamp DESC',
+                                       'LIMIT' => $wgShowRollbackEditCount + 1 )
+                       );
+
+                       $editCount = 0;
+                       while( $row = $dbr->fetchObject( $res ) ) {
+                               if( $rev->getUserText() != $row->rev_user_text ) {
+                                       break;
+                               }
+                               $editCount++;
+                       }
+
+                       if( $editCount > $wgShowRollbackEditCount ) {
+                               $editCount_output = $context->msg( 'rollbacklinkcount-morethan' )->numParams( $wgShowRollbackEditCount )->parse();
+                       } else {
+                               $editCount_output = $context->msg( 'rollbacklinkcount' )->numParams( $editCount )->parse();
+                       }
+
+                       return self::link(
+                               $title,
+                               $editCount_output,
+                               array( 'title' => $context->msg( 'tooltip-rollback' )->text() ),
+                               $query,
+                               array( 'known', 'noclasses' )
+                       );
+               } else {
+                       return self::link(
+                               $title,
+                               $context->msg( 'rollbacklink' )->escaped(),
+                               array( 'title' => $context->msg( 'tooltip-rollback' )->text() ),
+                               $query,
+                               array( 'known', 'noclasses' )
+                       );
+               }
        }
 
        /**
index 7dd85b6..4a5e2bc 100644 (file)
@@ -109,7 +109,7 @@ class WikiMap {
                $wiki = WikiMap::getWiki( $wikiID );
 
                if ( $wiki ) {
-                       return $wiki->getUrl( $page );
+                       return $wiki->getFullUrl( $page );
                }
 
                return false;
index 31b4351..b3ec54b 100644 (file)
@@ -117,25 +117,26 @@ class ApiUpload extends ApiBase {
         */
        private function getContextResult(){
                $warnings = $this->getApiWarnings();
-               if ( $warnings ) {
+               if ( $warnings && !$this->mParams['ignorewarnings'] ) {
                        // Get warnings formated in result array format
                        return $this->getWarningsResult( $warnings );
                } elseif ( $this->mParams['chunk'] ) {
                        // Add chunk, and get result
-                       return $this->getChunkResult();
+                       return $this->getChunkResult( $warnings );
                } elseif ( $this->mParams['stash'] ) {
                        // Stash the file and get stash result
-                       return $this->getStashResult();
+                       return $this->getStashResult( $warnings );
                }
                // This is the most common case -- a normal upload with no warnings
                // performUpload will return a formatted properly for the API with status
-               return $this->performUpload();
+               return $this->performUpload( $warnings );
        }
        /**
         * Get Stash Result, throws an expetion if the file could not be stashed.
+        * @param $warnings array Array of Api upload warnings
         * @return array
         */
-       private function getStashResult(){
+       private function getStashResult( $warnings ){
                $result = array ();
                // Some uploads can request they be stashed, so as not to publish them immediately.
                // In this case, a failure to stash ought to be fatal
@@ -143,6 +144,9 @@ class ApiUpload extends ApiBase {
                        $result['result'] = 'Success';
                        $result['filekey'] = $this->performStash();
                        $result['sessionkey'] = $result['filekey']; // backwards compatibility
+                       if ( $warnings && count( $warnings ) > 0 ) {
+                               $result['warnings'] = $warnings;
+                       }
                } catch ( MWException $e ) {
                        $this->dieUsage( $e->getMessage(), 'stashfailed' );
                }
@@ -150,7 +154,7 @@ class ApiUpload extends ApiBase {
        }
        /**
         * Get Warnings Result
-        * @param $warnings Array of Api upload warnings
+        * @param $warnings array Array of Api upload warnings
         * @return array
         */
        private function getWarningsResult( $warnings ){
@@ -169,12 +173,16 @@ class ApiUpload extends ApiBase {
        }
        /**
         * Get the result of a chunk upload.
+        * @param $warnings array Array of Api upload warnings
         * @return array
         */
-       private function getChunkResult(){
+       private function getChunkResult( $warnings ){
                $result = array();
 
                $result['result'] = 'Continue';
+               if ( $warnings && count( $warnings ) > 0 ) {
+                       $result['warnings'] = $warnings;
+               }
                $request = $this->getMain()->getRequest();
                $chunkPath = $request->getFileTempname( 'chunk' );
                $chunkSize = $request->getUpload( 'chunk' )->getSize();
@@ -444,7 +452,7 @@ class ApiUpload extends ApiBase {
 
 
        /**
-        * Check warnings if ignorewarnings is not set.
+        * Check warnings.
         * Returns a suitable array for inclusion into API results if there were warnings
         * Returns the empty array if there were no warnings
         *
@@ -453,9 +461,8 @@ class ApiUpload extends ApiBase {
        protected function getApiWarnings() {
                $warnings = array();
 
-               if ( !$this->mParams['ignorewarnings'] ) {
-                       $warnings = $this->mUpload->checkWarnings();
-               }
+               $warnings = $this->mUpload->checkWarnings();
+
                return $this->transformWarnings( $warnings );
        }
 
@@ -488,9 +495,10 @@ class ApiUpload extends ApiBase {
         * Perform the actual upload. Returns a suitable result array on success;
         * dies on failure.
         *
+        * @param $warnings array Array of Api upload warnings
         * @return array
         */
-       protected function performUpload() {
+       protected function performUpload( $warnings ) {
                // Use comment as initial page text by default
                if ( is_null( $this->mParams['text'] ) ) {
                        $this->mParams['text'] = $this->mParams['comment'];
@@ -529,6 +537,9 @@ class ApiUpload extends ApiBase {
 
                $result['result'] = 'Success';
                $result['filename'] = $file->getName();
+               if ( $warnings && count( $warnings ) > 0 ) {
+                       $result['warnings'] = $warnings;
+               }
 
                return $result;
        }
index 167d4e2..fc72610 100644 (file)
  * @ingroup SpecialPage
  */
 class SpecialChangeEmail extends UnlistedSpecialPage {
+
+       /**
+        * Users password
+        * @var string
+        */
+       protected $mPassword;
+
+       /**
+        * Users new email address
+        * @var string
+        */
+       protected $mNewEmail;
+
        public function __construct() {
                parent::__construct( 'ChangeEmail' );
        }
 
+       /**
+        * @return Bool
+        */
        function isListed() {
                global $wgAuth;
                return $wgAuth->allowPropChange( 'emailaddress' );
@@ -90,6 +106,9 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
                $this->showForm();
        }
 
+       /**
+        * @param $type string
+        */
        protected function doReturnTo( $type = 'hard' ) {
                $titleObj = Title::newFromText( $this->getRequest()->getVal( 'returnto' ) );
                if ( !$titleObj instanceof Title ) {
@@ -102,11 +121,15 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
                }
        }
 
+       /**
+        * @param $msg string
+        */
        protected function error( $msg ) {
                $this->getOutput()->wrapWikiMsg( "<p class='error'>\n$1\n</p>", $msg );
        }
 
        protected function showForm() {
+               global $wgRequirePasswordforEmailChange;
                $user = $this->getUser();
 
                $oldEmailText = $user->getEmail()
@@ -123,13 +146,20 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
                        Html::hidden( 'token', $user->getEditToken() ) . "\n" .
                        Html::hidden( 'returnto', $this->getRequest()->getVal( 'returnto' ) ) . "\n" .
                        $this->msg( 'changeemail-text' )->parseAsBlock() . "\n" .
-                       Xml::openElement( 'table', array( 'id' => 'mw-changeemail-table' ) ) . "\n" .
-                       $this->pretty( array(
-                               array( 'wpName', 'username', 'text', $user->getName() ),
-                               array( 'wpOldEmail', 'changeemail-oldemail', 'text', $oldEmailText ),
-                               array( 'wpNewEmail', 'changeemail-newemail', 'input', $this->mNewEmail ),
-                               array( 'wpPassword', 'yourpassword', 'password', $this->mPassword ),
-                       ) ) . "\n" .
+                       Xml::openElement( 'table', array( 'id' => 'mw-changeemail-table' ) ) . "\n"
+               );
+               $items = array(
+                       array( 'wpName', 'username', 'text', $user->getName() ),
+                       array( 'wpOldEmail', 'changeemail-oldemail', 'text', $oldEmailText ),
+                       array( 'wpNewEmail', 'changeemail-newemail', 'input', $this->mNewEmail ),
+               );
+               if ( $wgRequirePasswordforEmailChange ) {
+                       $items[] = array( 'wpPassword', 'yourpassword', 'password', $this->mPassword );
+               }
+
+               $this->getOutput()->addHTML(
+                       $this->pretty( $items ) .
+                       "\n" .
                        "<tr>\n" .
                                "<td></td>\n" .
                                '<td class="mw-input">' .
@@ -143,6 +173,10 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
                );
        }
 
+       /**
+        * @param $fields array
+        * @return string
+        */
        protected function pretty( $fields ) {
                $out = '';
                foreach ( $fields as $list ) {
@@ -173,6 +207,9 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
        }
 
        /**
+        * @param $user User
+        * @param $pass string
+        * @param $newaddr string
         * @return bool|string true or string on success, false on failure
         */
        protected function attemptChange( User $user, $pass, $newaddr ) {
@@ -187,7 +224,8 @@ class SpecialChangeEmail extends UnlistedSpecialPage {
                        return false;
                }
 
-               if ( !$user->checkTemporaryPassword( $pass ) && !$user->checkPassword( $pass ) ) {
+               global $wgRequirePasswordforEmailChange;
+               if ( $wgRequirePasswordforEmailChange && !$user->checkTemporaryPassword( $pass ) && !$user->checkPassword( $pass ) ) {
                        $this->error( 'wrongpassword' );
                        return false;
                }
index ec67281..590f6ac 100644 (file)
@@ -2927,6 +2927,8 @@ proceed with caution.',
 'rollback'          => 'Roll back edits',
 'rollback_short'    => 'Rollback',
 'rollbacklink'      => 'rollback',
+'rollbacklinkcount' => 'rollback $1 {{PLURAL:$1|edit|edits}}',
+'rollbacklinkcount-morethan' => 'rollback more than $1 {{PLURAL:$1|edit|edits}}',
 'rollbackfailed'    => 'Rollback failed',
 'cantrollback'      => 'Cannot revert edit;
 last contributor is only author of this page.',
index 33aca58..3ce031b 100644 (file)
@@ -48,6 +48,11 @@ $namespaceNames = array(
        NS_CATEGORY_TALK    => 'Kategorijos_aptarimas',
 );
 
+$namespaceGenderAliases = array(
+       NS_USER      => array( 'male' => 'Naudotojas', 'female' => 'Naudotoja' ),
+       NS_USER_TALK => array( 'male' => 'Naudotojo_aptarimas', 'female' => 'Naudotojos_aptarimas' ),
+);
+
 $specialPageAliases = array(
        'Allmessages'               => array( 'Visi_praneÅ¡imai' ),
        'Allpages'                  => array( 'Visi_puslapiai' ),
index c5a511a..f9f7939 100644 (file)
@@ -2698,6 +2698,9 @@ $1 is the <b>approximate</b> number of revisions that the page has, the message
 'rollback_short' => '{{Identical|Rollback}}',
 'rollbacklink' => '{{Identical|Rollback}}
 This message has a tooltip {{msg-mw|tooltip-rollback}}',
+'rollbacklinkcount' => '* $1: the number of edit that will be rollbacked
+If $1 is over the value of $wgShowRollbackEditCount (default: 10) [[MediaWiki:Rollbacklinkcount-morethan/en|rollbacklinkcount-morethan]] is used',
+'rollbacklinkcount-morethan' => 'Similar to [[MediaWiki:Rollbacklinkcount/en|rollbacklinkcount]] but with prefix more than',
 'rollbackfailed' => '{{Identical|Rollback}}',
 'cantrollback' => '{{Identical|Revert}}
 {{Identical|Rollback}}',
index 1cba767..4ecc4df 100644 (file)
@@ -37,9 +37,8 @@ $namespaceNames = array(
 );
 
 /**
-  * Aliases from the fallback language 'lt' to avoid breakage of links
-  */
-
+ * Aliases from the fallback language 'lt' to avoid breakage of links
+ */
 $namespaceAliases = array(
        'Specialus'             => NS_SPECIAL,
        'Aptarimas'             => NS_TALK,
@@ -57,6 +56,8 @@ $namespaceAliases = array(
        'Kategorijos_aptarimas' => NS_CATEGORY_TALK,
 );
 
+$namespaceGenderAliases = array();
+
 $messages = array(
 # User preference toggles
 'tog-underline' => 'PabrauktÄ— nÅ«ruodas:',
index 126faaa..000517f 100644 (file)
@@ -1962,6 +1962,8 @@ $wgMessageStructure = array(
                'rollback',
                'rollback_short',
                'rollbacklink',
+               'rollbacklinkcount',
+               'rollbacklinkcount-morethan',
                'rollbackfailed',
                'cantrollback',
                'alreadyrolled',
index 337367f..380a099 100644 (file)
@@ -613,6 +613,7 @@ return array(
                'scripts' => 'resources/mediawiki/mediawiki.user.js',
                'dependencies' => array(
                        'jquery.cookie',
+                       'mediawiki.api',
                ),
        ),
        'mediawiki.util' => array(
index 7a15e29..8c6e90c 100644 (file)
@@ -12,6 +12,9 @@
                /* Private Members */
 
                var that = this;
+               var api = new mw.Api();
+               var groupsDeferred;
+               var rightsDeferred;
 
                /* Public Members */
 
                 *
                 * @return Mixed: User name string or null if users is anonymous
                 */
-               this.name = function() {
+               this.getName = function () {
                        return mw.config.get( 'wgUserName' );
                };
 
+               /**
+                * @deprecated since 1.20 use mw.user.getName() instead
+                */
+               this.name = function () {
+                       return this.getName();
+               };
+
                /**
                 * Checks if the current user is anonymous.
                 *
                 * @return Boolean
                 */
-               this.anonymous = function() {
-                       return that.name() ? false : true;
+               this.isAnon = function () {
+                       return that.getName() === null;
+               };
+
+               /**
+                * @deprecated since 1.20 use mw.user.isAnon() instead
+                */
+               this.anonymous = function () {
+                       return that.isAnon();
                };
 
                /**
                 * @return String: User name or random session ID
                 */
                this.id = function() {
-                       var name = that.name();
+                       var name = that.getName();
                        if ( name ) {
                                return name;
                        }
                        }
                        return bucket;
                };
+
+               /**
+                * Gets the current user's groups.
+                */
+               this.getGroups = function ( callback ) {
+                       if ( groupsDeferred ) {
+                               groupsDeferred.always( callback );
+                               return;
+                       }
+
+                       groupsDeferred = $.Deferred();
+                       groupsDeferred.always( callback );
+                       api.get( {
+                               action: 'query',
+                               meta: 'userinfo',
+                               uiprop: 'groups'
+                       } ).done( function ( data ) {
+                               if ( data.query && data.query.userinfo && data.query.userinfo.groups ) {
+                                       groupsDeferred.resolve( data.query.userinfo.groups );
+                               } else {
+                                       groupsDeferred.reject( [] );
+                               }
+                       } ).fail( function ( data ) {
+                                       groupsDeferred.reject( [] );
+                       } );
+               };
+
+               /**
+                * Gets the current user's rights.
+                */
+               this.getRights = function ( callback ) {
+                       if ( rightsDeferred ) {
+                               rightsDeferred.always( callback );
+                               return;
+                       }
+
+                       rightsDeferred = $.Deferred();
+                       rightsDeferred.always( callback );
+                       api.get( {
+                               action: 'query',
+                               meta: 'userinfo',
+                               uiprop: 'rights'
+                       } ).done( function ( data ) {
+                               if ( data.query && data.query.userinfo && data.query.userinfo.rights ) {
+                                       rightsDeferred.resolve( data.query.userinfo.rights );
+                               } else {
+                                       rightsDeferred.reject( [] );
+                               }
+                       } ).fail( function ( data ) {
+                               rightsDeferred.reject( [] );
+                       } );
+               };
        }
 
        // Extend the skeleton mw.user from mediawiki.js
diff --git a/tests/phpunit/includes/libs/CSSJanusTest.php b/tests/phpunit/includes/libs/CSSJanusTest.php
new file mode 100644 (file)
index 0000000..54f6607
--- /dev/null
@@ -0,0 +1,560 @@
+<?php
+/**
+ * Based on the test suite of the original Python
+ * CSSJanus libary:
+ * http://code.google.com/p/cssjanus/source/browse/trunk/cssjanus_test.py
+ * Ported to PHP for ResourceLoader and has been extended since.
+ */
+class CSSJanusTest extends MediaWikiTestCase {
+       /**
+        * @dataProvider provideTransformCases
+        */
+       function testTransform( $cssA, $cssB = null ) {
+
+               if ( $cssB ) {
+                       $transformedA = CSSJanus::transform( $cssA );
+                       $this->assertEquals( $transformedA, $cssB, 'Test A-B transformation' );
+
+                       $transformedB = CSSJanus::transform( $cssB );
+                       $this->assertEquals( $transformedB, $cssA, 'Test B-A transformation' );
+
+               // If no B version is provided, it means
+               // the output should equal the input.
+               } else {
+                       $transformedA = CSSJanus::transform( $cssA );
+                       $this->assertEquals( $transformedA, $cssA, 'Nothing was flipped' );
+               }
+       }
+
+       /**
+        * @dataProvider provideTransformAdvancedCases
+        */
+       function testTransformAdvanced( $code, $expectedOutput, $options = array() ) {
+               $swapLtrRtlInURL = isset( $options['swapLtrRtlInURL'] ) ? $options['swapLtrRtlInURL'] : false;
+               $swapLeftRightInURL = isset( $options['swapLeftRightInURL'] ) ? $options['swapLeftRightInURL'] : false;
+
+               $flipped = CSSJanus::transform( $code, $swapLtrRtlInURL, $swapLeftRightInURL );
+
+               $this->assertEquals( $expectedOutput, $flipped,
+                       'Test flipping, options: url-ltr-rtl=' . ($swapLtrRtlInURL ? 'true' : 'false')
+                               . ' url-left-right=' . ($swapLeftRightInURL ? 'true' : 'false')
+               );
+       }
+       /**
+        * @dataProvider provideTransformBrokenCases
+        * @group Broken
+        */
+       function testTransformBroken( $code, $expectedOutput ) {
+               $flipped = CSSJanus::transform( $code );
+
+               $this->assertEquals( $expectedOutput, $flipped, 'Test flipping' );
+       }
+
+       /**
+        * These transform cases are tested *in both directions*
+        * No need to declare a principle twice in both directions here.
+        */
+       function provideTransformCases() {
+               return array(
+                       // Property keys
+                       array(
+                               '.foo { left: 0; }',
+                               '.foo { right: 0; }'
+                       ),
+                       // Guard against partial keys
+                       // (CSS currently doesn't have flippable properties
+                       // that contain the direction as part of the key without
+                       // dash separation)
+                       array(
+                               '.foo { alright: 0; }'
+                       ),
+                       array(
+                               '.foo { balleft: 0; }'
+                       ),
+
+                       // Dashed property keys
+                       array(
+                               '.foo { padding-left: 0; }',
+                               '.foo { padding-right: 0; }'
+                       ),
+                       array(
+                               '.foo { margin-left: 0; }',
+                               '.foo { margin-right: 0; }'
+                       ),
+                       array(
+                               '.foo { border-left: 0; }',
+                               '.foo { border-right: 0; }'
+                       ),
+
+                       // Double-dashed property keys
+                       array(
+                               '.foo { border-left-color: red; }',
+                               '.foo { border-right-color: red; }'
+                       ),
+                       array(
+                               // Includes unknown properties?
+                               '.foo { x-left-y: 0; }',
+                               '.foo { x-right-y: 0; }'
+                       ),
+
+                       // Multi-value properties
+                       array(
+                               '.foo { padding: 0; }'
+                       ),
+                       array(
+                               '.foo { padding: 0 1px; }'
+                       ),
+                       array(
+                               '.foo { padding: 0 1px 2px; }'
+                       ),
+                       array(
+                               '.foo { padding: 0 1px 2px 3px; }',
+                               '.foo { padding: 0 3px 2px 1px; }'
+                       ),
+
+                       // Shorthand / Four notation
+                       array(
+                               '.foo { padding: .25em 15px 0pt 0ex; }',
+                               '.foo { padding: .25em 0ex 0pt 15px; }'
+                       ),
+                       array(
+                               '.foo { margin: 1px -4px 3px 2px; }',
+                               '.foo { margin: 1px 2px 3px -4px; }'
+                       ),
+                       array(
+                               '.foo { padding: 0 15px .25em 0; }',
+                               '.foo { padding: 0 0 .25em 15px; }'
+                       ),
+                       array(
+                               '.foo { padding: 1px 4.1grad 3px 2%; }',
+                               '.foo { padding: 1px 2% 3px 4.1grad; }'
+                       ),
+                       array(
+                               '.foo { padding: 1px 2px 3px auto; }',
+                               '.foo { padding: 1px auto 3px 2px; }'
+                       ),
+                       array(
+                               '.foo { padding: 1px inherit 3px auto; }',
+                               '.foo { padding: 1px auto 3px inherit; }'
+                       ),
+                       array(
+                               '.foo { border-radius: .25em 15px 0pt 0ex; }',
+                               '.foo { border-radius: .25em 0ex 0pt 15px; }'
+                       ),
+                       array(
+                               '.foo { x-unknown: a b c d; }'
+                       ),
+                       array(
+                               '.foo barpx 0 2% { opacity: 0; }'
+                       ),
+                       array(
+                               '#settings td p strong'
+                       ),
+                       array(
+                               # Not sure how 4+ values should behave,
+                               # testing to make sure changes are detected
+                               '.foo { x-unknown: 1 2 3 4 5; }',
+                               '.foo { x-unknown: 1 4 3 2 5; }',
+                       ),
+                       array(
+                               '.foo { x-unknown: 1 2 3 4 5 6; }',
+                               '.foo { x-unknown: 1 4 3 2 5 6; }',
+                       ),
+
+                       // Shorthand / Three notation
+                       array(
+                               '.foo { margin: 1em 0 .25em; }'
+                       ),
+                       array(
+                               '.foo { margin:-1.5em 0 -.75em; }'
+                       ),
+
+                       // Shorthand / Two notation
+                       array(
+                               '.foo { padding: 1px 2px; }'
+                       ),
+
+                       // Shorthand / One notation
+                       array(
+                               '.foo { padding: 1px; }'
+                       ),
+
+                       // Direction
+                       // Note: This differs from the Python implementation,
+                       // see also CSSJanus::fixDirection for more info.
+                       array(
+                               '.foo { direction: ltr; }',
+                               '.foo { direction: rtl; }'
+                       ),
+                       array(
+                               '.foo { direction: rtl; }',
+                               '.foo { direction: ltr; }'
+                       ),
+                       array(
+                               'input { direction: ltr; }',
+                               'input { direction: rtl; }'
+                       ),
+                       array(
+                               'input { direction: rtl; }',
+                               'input { direction: ltr; }'
+                       ),
+                       array(
+                               'body { direction: ltr; }',
+                               'body { direction: rtl; }'
+                       ),
+                       array(
+                               '.foo, body, input { direction: ltr; }',
+                               '.foo, body, input { direction: rtl; }'
+                       ),
+                       array(
+                               'body { padding: 10px; direction: ltr; }',
+                               'body { padding: 10px; direction: rtl; }'
+                       ),
+                       array(
+                               'body { direction: ltr } .myClass { direction: ltr }',
+                               'body { direction: rtl } .myClass { direction: rtl }'
+                       ),
+
+                       // Left/right values
+                       array(
+                               '.foo { float: left; }',
+                               '.foo { float: right; }'
+                       ),
+                       array(
+                               '.foo { text-align: left; }',
+                               '.foo { text-align: right; }'
+                       ),
+                       array(
+                               '.foo { -x-unknown: left; }',
+                               '.foo { -x-unknown: right; }'
+                       ),
+                       // Guard against selectors that look flippable
+                       array(
+                               '.column-left { width: 0; }'
+                       ),
+                       array(
+                               'a.left { width: 0; }'
+                       ),
+                       array(
+                               'a.leftification { width: 0; }'
+                       ),
+                       array(
+                               'a.ltr { width: 0; }'
+                       ),
+                       array(
+                               # <div class="a-ltr png">
+                               '.a-ltr.png { width: 0; }'
+                       ),
+                       array(
+                               # <foo-ltr attr="x">
+                               'foo-ltr[attr="x"] { width: 0; }'
+                       ),
+                       array(
+                               'div.left > span.right+span.left { width: 0; }'
+                       ),
+                       array(
+                               '.thisclass .left .myclass { width: 0; }'
+                       ),
+                       array(
+                               '.thisclass .left .myclass #myid { width: 0; }'
+                       ),
+
+                       // Cursor values (east/west)
+                       array(
+                               '.foo { cursor: e-resize; }',
+                               '.foo { cursor: w-resize; }'
+                       ),
+                       array(
+                               '.foo { cursor: se-resize; }',
+                               '.foo { cursor: sw-resize; }'
+                       ),
+                       array(
+                               '.foo { cursor: ne-resize; }',
+                               '.foo { cursor: nw-resize; }'
+                       ),
+
+                       // Background
+                       array(
+                               '.foo { background-position: top left; }',
+                               '.foo { background-position: top right; }'
+                       ),
+                       array(
+                               '.foo { background: url(/foo/bar.png) top left; }',
+                               '.foo { background: url(/foo/bar.png) top right; }'
+                       ),
+                       array(
+                               '.foo { background: url(/foo/bar.png) top left no-repeat; }',
+                               '.foo { background: url(/foo/bar.png) top right no-repeat; }'
+                       ),
+                       array(
+                               '.foo { background: url(/foo/bar.png) no-repeat top left; }',
+                               '.foo { background: url(/foo/bar.png) no-repeat top right; }'
+                       ),
+                       array(
+                               '.foo { background: #fff url(/foo/bar.png) no-repeat top left; }',
+                               '.foo { background: #fff url(/foo/bar.png) no-repeat top right; }'
+                       ),
+                       array(
+                               '.foo { background-position: 100% 40%; }',
+                               '.foo { background-position: 0% 40%; }'
+                       ),
+                       array(
+                               '.foo { background-position: 23% 0; }',
+                               '.foo { background-position: 77% 0; }'
+                       ),
+                       array(
+                               '.foo { background-position: 23% auto; }',
+                               '.foo { background-position: 77% auto; }'
+                       ),
+                       array(
+                               '.foo { background-position-x: 23%; }',
+                               '.foo { background-position-x: 77%; }'
+                       ),
+                       array(
+                               '.foo { background-position-y: 23%; }',
+                               '.foo { background-position-y: 23%; }'
+                       ),
+                       array(
+                               '.foo { background:url(../foo.png) no-repeat 75% 50%; }',
+                               '.foo { background:url(../foo.png) no-repeat 25% 50%; }'
+                       ),
+                       array(
+                               '.foo { background: 10% 20% } .bar { background: 40% 30% }',
+                               '.foo { background: 90% 20% } .bar { background: 60% 30% }'
+                       ),
+
+                       // Multiple rules
+                       array(
+                               'body { direction: rtl; float: right; } .foo { direction: ltr; float: right; }',
+                               'body { direction: ltr; float: left; } .foo { direction: rtl; float: left; }',
+                       ),
+
+                       // Duplicate properties
+                       array(
+                               '.foo { float: left; float: right; float: left; }',
+                               '.foo { float: right; float: left; float: right; }',
+                       ),
+
+                       // Preserve comments
+                       array(
+                               '/* left /* right */left: 10px',
+                               '/* left /* right */right: 10px'
+                       ),
+                       array(
+                               '/*left*//*left*/left: 10px',
+                               '/*left*//*left*/right: 10px'
+                       ),
+                       array(
+                               '/* Going right is cool */ .foo { width: 0 }',
+                       ),
+                       array(
+                               "/* padding-right 1 2 3 4 */\n#test { width: 0}\n/*right*/"
+                       ),
+                       array(
+                               "/** Two line comment\n * left\n \*/\n#test {width: 0}"
+                       ),
+
+                       // @noflip annotation
+                       array(
+                               // before selector (single)
+                               '/* @noflip */ div { float: left; }'
+                       ),
+                       array(
+                               // before selector (multiple)
+                               '/* @noflip */ div, .notme { float: left; }'
+                       ),
+                       array(
+                               // inside selector
+                               'div, /* @noflip */ .foo { float: left; }'
+                       ),
+                       array(
+                               // after selector
+                               'div, .notme /* @noflip */ { float: left; }'
+                       ),
+                       array(
+                               // before multiple rules
+                               '/* @noflip */ div { float: left; } .foo { float: left; }',
+                               '/* @noflip */ div { float: left; } .foo { float: right; }'
+                       ),
+                       array(
+                               // after multiple rules
+                               '.foo { float: left; } /* @noflip */ div { float: left; }',
+                               '.foo { float: right; } /* @noflip */ div { float: left; }'
+                       ),
+                       array(
+                               // before multiple properties
+                               'div { /* @noflip */ float: left; text-align: left; }',
+                               'div { /* @noflip */ float: left; text-align: right; }'
+                       ),
+                       array(
+                               // after multiple properties
+                               'div { float: left; /* @noflip */ text-align: left; }',
+                               'div { float: right; /* @noflip */ text-align: left; }'
+                       ),
+
+                       // Guard against css3 stuff
+                       array(
+                               'background-image: -moz-linear-gradient(#326cc1, #234e8c);'
+                       ),
+                       array(
+                               'background-image: -webkit-gradient(linear, 100% 0%, 0% 0%, from(#666666), to(#ffffff));'
+                       ),
+
+                       // CSS syntax / white-space variations
+                       // spaces, no spaces, tabs, new lines, omitting semi-colons
+                       array(
+                               ".foo { left: 0; }",
+                               ".foo { right: 0; }"
+                       ),
+                       array(
+                               ".foo{ left: 0; }",
+                               ".foo{ right: 0; }"
+                       ),
+                       array(
+                               ".foo{ left: 0 }",
+                               ".foo{ right: 0 }"
+                       ),
+                       array(
+                               ".foo{left:0 }",
+                               ".foo{right:0 }"
+                       ),
+                       array(
+                               ".foo{left:0}",
+                               ".foo{right:0}"
+                       ),
+                       array(
+                               ".foo  {  left : 0 ; }",
+                               ".foo  {  right : 0 ; }"
+                       ),
+                       array(
+                               ".foo\n  {  left : 0 ; }",
+                               ".foo\n  {  right : 0 ; }"
+                       ),
+                       array(
+                               ".foo\n  {  \nleft : 0 ; }",
+                               ".foo\n  {  \nright : 0 ; }"
+                       ),
+                       array(
+                               ".foo\n  { \n left : 0 ; }",
+                               ".foo\n  { \n right : 0 ; }"
+                       ),
+                       array(
+                               ".foo\n  { \n left\n  : 0; }",
+                               ".foo\n  { \n right\n  : 0; }"
+                       ),
+                       array(
+                               ".foo \n  { \n left\n  : 0; }",
+                               ".foo \n  { \n right\n  : 0; }"
+                       ),
+                       array(
+                               ".foo\n{\nleft\n:\n0;}",
+                               ".foo\n{\nright\n:\n0;}"
+                       ),
+                       array(
+                               ".foo\n.bar {\n\tleft: 0;\n}",
+                               ".foo\n.bar {\n\tright: 0;\n}"
+                       ),
+                       array(
+                               ".foo\t{\tleft\t:\t0;}",
+                               ".foo\t{\tright\t:\t0;}"
+                       ),
+               );
+       }
+
+       /**
+        * These cases are tested in one way only (format: actual, expected, msg).
+        * If both ways can be tested, either put both versions in here or move
+        * it to provideTransformCases().
+        */
+       function provideTransformAdvancedCases() {
+               $bgPairs = array(
+                       # [ - _ . ] <-> [ left right ltr rtl ]
+                       'foo.jpg' => 'foo.jpg',
+                       'left.jpg' => 'right.jpg',
+                       'ltr.jpg' => 'rtl.jpg',
+
+                       'foo-left.png' => 'foo-right.png',
+                       'foo_left.png' => 'foo_right.png',
+                       'foo.left.png' => 'foo.right.png',
+
+                       'foo-ltr.png' => 'foo-rtl.png',
+                       'foo_ltr.png' => 'foo_rtl.png',
+                       'foo.ltr.png' => 'foo.rtl.png',
+
+                       'left-foo.png' => 'right-foo.png',
+                       'left_foo.png' => 'right_foo.png',
+                       'left.foo.png' => 'right.foo.png',
+
+                       'ltr-foo.png' => 'rtl-foo.png',
+                       'ltr_foo.png' => 'rtl_foo.png',
+                       'ltr.foo.png' => 'rtl.foo.png',
+
+                       'foo-ltr-left.gif' => 'foo-rtl-right.gif',
+                       'foo_ltr_left.gif' => 'foo_rtl_right.gif',
+                       'foo.ltr.left.gif' => 'foo.rtl.right.gif',
+                       'foo-ltr_left.gif' => 'foo-rtl_right.gif',
+                       'foo_ltr.left.gif' => 'foo_rtl.right.gif',
+               );
+               $provider = array();
+               foreach ( $bgPairs as $left => $right ) {
+                       # By default '-rtl' and '-left' etc. are not touched,
+                       # Only when the appropiate parameter is set.
+                       $provider[] = array(
+                               ".foo { background: url(images/$left); }",
+                               ".foo { background: url(images/$left); }"
+                       );
+                       $provider[] = array(
+                               ".foo { background: url(images/$right); }",
+                               ".foo { background: url(images/$right); }"
+                       );
+                       $provider[] = array(
+                               ".foo { background: url(images/$left); }",
+                               ".foo { background: url(images/$right); }",
+                               array(
+                                       'swapLtrRtlInURL' => true,
+                                       'swapLeftRightInURL' => true,
+                               )
+                       );
+                       $provider[] = array(
+                               ".foo { background: url(images/$right); }",
+                               ".foo { background: url(images/$left); }",
+                               array(
+                                       'swapLtrRtlInURL' => true,
+                                       'swapLeftRightInURL' => true,
+                               )
+                       );
+               }
+
+               return $provider;
+       }
+
+       /**
+        * Cases that are currently failing, but
+        * should be looked at in the future as enhancements and/or bug fix
+        */
+       function provideTransformBrokenCases() {
+               return array(
+                       // Guard against partial keys
+                       array(
+                               '.foo { leftxx: 0; }',
+                               '.foo { leftxx: 0; }'
+                       ),
+                       array(
+                               '.foo { rightxx: 0; }',
+                               '.foo { rightxx: 0; }'
+                       ),
+
+                       // Guard against selectors that look flippable
+                       array(
+                               # <foo-left-x attr="x">
+                               'foo-left-x[attr="x"] { width: 0; }',
+                               'foo-left-x[attr="x"] { width: 0; }'
+                       ),
+                       array(
+                               # <div class="foo" data-left="x">
+                               '.foo[data-left="x"] { width: 0; }',
+                               '.foo[data-left="x"] { width: 0; }'
+                       ),
+               );
+       }
+}
index 79768da..c823baf 100644 (file)
@@ -6,7 +6,7 @@ QUnit.test( 'options', 1, function ( assert ) {
        assert.ok( mw.user.options instanceof mw.Map, 'options instance of mw.Map' );
 });
 
-QUnit.test( 'User login status', 5, function ( assert ) {
+QUnit.test( 'user status', 9, function ( assert ) {
        /**
         * Tests can be run under three different conditions:
         *   1) From tests/qunit/index.html, user will be anonymous.
@@ -15,18 +15,42 @@ QUnit.test( 'User login status', 5, function ( assert ) {
         */
 
        // Forge an anonymous user:
-       mw.config.set( 'wgUserName', null);
+       mw.config.set( 'wgUserName', null );
 
-       assert.strictEqual( mw.user.name(), null, 'user.name should return null when anonymous' );
-       assert.ok( mw.user.anonymous(), 'user.anonymous should reutrn true when anonymous' );
+       assert.strictEqual( mw.user.getName(), null, 'user.getName() returns null when anonymous' );
+       assert.strictEqual( mw.user.name(), null, 'user.name() compatibility' );
+       assert.assertTrue( mw.user.isAnon(), 'user.isAnon() returns true when anonymous' );
+       assert.assertTrue( mw.user.anonymous(), 'user.anonymous() compatibility' );
 
        // Not part of startUp module
        mw.config.set( 'wgUserName', 'John' );
 
-       assert.equal( mw.user.name(), 'John', 'user.name returns username when logged-in' );
-       assert.ok( !mw.user.anonymous(), 'user.anonymous returns false when logged-in' );
+       assert.equal( mw.user.getName(), 'John', 'user.getName() returns username when logged-in' );
+       assert.equal( mw.user.name(), 'John', 'user.name() compatibility' );
+       assert.assertFalse( mw.user.isAnon(), 'user.isAnon() returns false when logged-in' );
+       assert.assertFalse( mw.user.anonymous(), 'user.anonymous() compatibility' );
 
        assert.equal( mw.user.id(), 'John', 'user.id Returns username when logged-in' );
 });
 
+QUnit.asyncTest( 'getGroups', 3, function ( assert ) {
+       mw.user.getGroups( function ( groups ) {
+               // First group should always be '*'
+               assert.equal( $.type( groups ), 'array', 'Callback gets an array' );
+               assert.equal( groups[0], '*', '"*"" is the first group' );
+               // Sort needed because of different methods if creating the arrays,
+               // only the content matters.
+               assert.deepEqual( groups.sort(), mw.config.get( 'wgUserGroups' ).sort(), 'Array contains all groups, just like wgUserGroups' );
+               QUnit.start();
+       });
+});
+
+QUnit.asyncTest( 'getRights', 1, function ( assert ) {
+       mw.user.getRights( function ( rights ) {
+               // First group should always be '*'
+               assert.equal( $.type( rights ), 'array', 'Callback gets an array' );
+               QUnit.start();
+       });
+});
+
 }( mediaWiki ) );