Merge "Support deleting all rows"
authorDemon <chadh@wikimedia.org>
Thu, 2 Aug 2012 12:24:11 +0000 (12:24 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Thu, 2 Aug 2012 12:24:11 +0000 (12:24 +0000)
32 files changed:
RELEASE-NOTES-1.20
includes/Article.php
includes/AutoLoader.php
includes/DefaultSettings.php
includes/Exception.php
includes/ImagePage.php
includes/Linker.php
includes/Pager.php
includes/WikiMap.php
includes/api/ApiQueryDuplicateFiles.php
includes/api/ApiQueryImageInfo.php
includes/api/ApiUpload.php
includes/filerepo/backend/SwiftFileBackend.php
includes/filerepo/file/File.php
includes/objectcache/MemcachedClient.php
includes/specials/SpecialCategories.php
includes/specials/SpecialChangeEmail.php
languages/messages/MessagesEn.php
languages/messages/MessagesLt.php
languages/messages/MessagesQqq.php
languages/messages/MessagesSgs.php
maintenance/Doxyfile
maintenance/copyFileBackend.php
maintenance/language/messages.inc
maintenance/mwdoc-filter.php [new file with mode: 0644]
resources/Resources.php
resources/jquery.ui/themes/vector/jquery.ui.button.css
resources/jquery/jquery.badge.css
resources/mediawiki/mediawiki.user.js
skins/common/commonElements.css
tests/phpunit/includes/libs/CSSJanusTest.php [new file with mode: 0644]
tests/qunit/suites/resources/mediawiki/mediawiki.user.test.js

index 9383bf2..1b1cd97 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,11 @@ 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.
+* (bug 36073) Avoid duplicate element IDs on File pages
+* (bug 25095) Special:Categories should also include the first relevant item
+  when "from" is filled.
 
 === API changes in 1.20 ===
 * (bug 34316) Add ability to retrieve maximum upload size from MediaWiki API.
@@ -236,6 +246,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 e1e08c9..6fc0acd 100644 (file)
@@ -1384,7 +1384,7 @@ class Article extends Page {
                        // @todo FIXME: i18n issue/patchwork message
                        $this->getContext()->getOutput()->addHTML( '<strong class="mw-delete-warning-revisions">' .
                                wfMsgExt( 'historywarning', array( 'parseinline' ), $this->getContext()->getLanguage()->formatNum( $revisions ) ) .
-                               wfMsgHtml( 'word-separator' ) . Linker::link( $title,
+                               wfMsgHtml( 'word-separator' ) . Linker::linkKnown( $title,
                                        wfMsgHtml( 'history' ),
                                        array( 'rel' => 'archives' ),
                                        array( 'action' => 'history' ) ) .
index 2987b31..7e11f3e 100644 (file)
@@ -256,6 +256,7 @@ $wgAutoloadLocalClasses = array(
        'UserArray' => 'includes/UserArray.php',
        'UserArrayFromResult' => 'includes/UserArray.php',
        'UserBlockedError' => 'includes/Exception.php',
+       'UserNotLoggedIn' => 'includes/Exception.php',
        'UserMailer' => 'includes/UserMailer.php',
        'UserRightsProxy' => 'includes/UserRightsProxy.php',
        'ViewCountUpdate' => 'includes/ViewCountUpdate.php',
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 0fc5cd7..7d4c551 100644 (file)
@@ -289,6 +289,7 @@ class MWException extends Exception {
  * Exception class which takes an HTML error message, and does not
  * produce a backtrace. Replacement for OutputPage::fatalError().
  *
+ * @since 1.7
  * @ingroup Exception
  */
 class FatalError extends MWException {
@@ -311,6 +312,7 @@ class FatalError extends MWException {
 /**
  * An error page which can definitely be safely rendered using the OutputPage.
  *
+ * @since 1.7
  * @ingroup Exception
  */
 class ErrorPageError extends MWException {
@@ -350,6 +352,7 @@ class ErrorPageError extends MWException {
  * Similar to ErrorPage, but emit a 400 HTTP error code to let mobile
  * browser it is not really a valid content.
  *
+ * @since 1.19
  * @ingroup Exception
  */
 class BadTitleError extends ErrorPageError {
@@ -381,6 +384,7 @@ class BadTitleError extends ErrorPageError {
  * Show an error when a user tries to do something they do not have the necessary
  * permissions for.
  *
+ * @since 1.18
  * @ingroup Exception
  */
 class PermissionsError extends ErrorPageError {
@@ -419,6 +423,7 @@ class PermissionsError extends ErrorPageError {
  * Show an error when the wiki is locked/read-only and the user tries to do
  * something that requires write access.
  *
+ * @since 1.18
  * @ingroup Exception
  */
 class ReadOnlyError extends ErrorPageError {
@@ -434,6 +439,7 @@ class ReadOnlyError extends ErrorPageError {
 /**
  * Show an error when the user hits a rate limit.
  *
+ * @since 1.18
  * @ingroup Exception
  */
 class ThrottledError extends ErrorPageError {
@@ -454,6 +460,7 @@ class ThrottledError extends ErrorPageError {
 /**
  * Show an error when the user tries to do something whilst blocked.
  *
+ * @since 1.18
  * @ingroup Exception
  */
 class UserBlockedError extends ErrorPageError {
@@ -500,6 +507,7 @@ class UserBlockedError extends ErrorPageError {
  * This is essentially an ErrorPageError exception which by default use the
  * 'exception-nologin' as a title and 'exception-nologin-text' for the message.
  * @see bug 37627
+ * @since 1.20
  *
  * @par Example:
  * @code
@@ -544,6 +552,7 @@ class UserNotLoggedIn extends ErrorPageError {
  * Show an error that looks like an HTTP server error.
  * Replacement for wfHttpError().
  *
+ * @since 1.19
  * @ingroup Exception
  */
 class HttpError extends MWException {
index cac407d..ab3e2e3 100644 (file)
@@ -778,7 +778,7 @@ EOT
                                        $link2 = Linker::linkKnown( Title::makeTitle( $row->page_namespace, $row->page_title ) );
                                        $ul .= Html::rawElement(
                                                'li',
-                                               array( 'id' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ),
+                                               array( 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ),
                                                $link2
                                                ) . "\n";
                                }
@@ -788,7 +788,7 @@ EOT
                        }
                        $out->addHTML( Html::rawElement(
                                        'li',
-                                       array( 'id' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ),
+                                       array( 'class' => 'mw-imagepage-linkstoimage-ns' . $element->page_namespace ),
                                        $liContents
                                ) . "\n"
                        );
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 d82f957..be43eda 100644 (file)
@@ -118,6 +118,11 @@ abstract class IndexPager extends ContextSource implements Pager {
 
        protected $mLastShown, $mFirstShown, $mPastTheEndIndex, $mDefaultQuery, $mNavigationBar;
 
+       /**
+        * Whether to include the offset in the query
+        */
+       protected $mIncludeOffset = false;
+
        /**
         * Result object for the query. Warning: seek before use.
         *
@@ -237,6 +242,17 @@ abstract class IndexPager extends ContextSource implements Pager {
                $this->mLimit = $limit;
        }
 
+       /**
+        * Set whether a row matching exactly the offset should be also included
+        * in the result or not. By default this is not the case, but when the
+        * offset is user-supplied this might be wanted.
+        *
+        * @param $include bool
+        */
+       public function setIncludeOffset( $include ) {
+               $this->mIncludeOffset = $include;
+       }
+
        /**
         * Extract some useful data from the result object for use by
         * the navigation bar, put it into $this
@@ -337,14 +353,14 @@ abstract class IndexPager extends ContextSource implements Pager {
                $sortColumns = array_merge( array( $this->mIndexField ), $this->mExtraSortFields );
                if ( $descending ) {
                        $options['ORDER BY'] = $sortColumns;
-                       $operator = '>';
+                       $operator = $this->mIncludeOffset ? '>=' : '>';
                } else {
                        $orderBy = array();
                        foreach ( $sortColumns as $col ) {
                                $orderBy[] = $col . ' DESC';
                        }
                        $options['ORDER BY'] = $orderBy;
-                       $operator = '<';
+                       $operator = $this->mIncludeOffset ? '<=' : '<';
                }
                if ( $offset != '' ) {
                        $conds[] = $this->mIndexField . $operator . $this->mDb->addQuotes( $offset );
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 719c84a..a4efc4c 100644 (file)
@@ -82,7 +82,12 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
                        }
                }
 
-               $files = RepoGroup::singleton()->findFiles( array_keys( $images ) );
+               $filesToFind = array_keys( $images );
+               if( $params['localonly'] ) {
+                       $files = RepoGroup::singleton()->getLocalRepo()->findFiles( $filesToFind );
+               } else {
+                       $files = RepoGroup::singleton()->findFiles( $filesToFind );
+               }
 
                $fit = true;
                $count = 0;
@@ -94,7 +99,12 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
                }
 
                // find all files with the hashes, result format is: array( hash => array( dup1, dup2 ), hash1 => ... )
-               $filesBySha1s = RepoGroup::singleton()->findBySha1s( array_unique( array_values( $sha1s ) ) );
+               $filesToFindBySha1s = array_unique( array_values( $sha1s ) );
+               if( $params['localonly'] ) {
+                       $filesBySha1s = RepoGroup::singleton()->getLocalRepo()->findBySha1s( $filesToFindBySha1s );
+               } else {
+                       $filesBySha1s = RepoGroup::singleton()->findBySha1s( $filesToFindBySha1s );
+               }
 
                // iterate over $images to handle continue param correct
                foreach( $images as $image => $pageId ) {
@@ -163,6 +173,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
                                        'descending'
                                )
                        ),
+                       'localonly' => false,
                );
        }
 
@@ -171,6 +182,7 @@ class ApiQueryDuplicateFiles extends ApiQueryGeneratorBase {
                        'limit' => 'How many duplicate files to return',
                        'continue' => 'When more results are available, use this to continue',
                        'dir' => 'The direction in which to list',
+                       'localonly' => 'Look only for files in the local repository',
                );
        }
 
index 7184c88..d822eed 100644 (file)
@@ -73,7 +73,12 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        }
 
                        $result = $this->getResult();
-                       $images = RepoGroup::singleton()->findFiles( $titles );
+                       //search only inside the local repo
+                       if( $params['localonly'] ) {
+                               $images = RepoGroup::singleton()->getLocalRepo()->findFiles( $titles );
+                       } else {
+                               $images = RepoGroup::singleton()->findFiles( $titles );
+                       }
                        foreach ( $images as $img ) {
                                // Skip redirects
                                if ( $img->getOriginalTitle()->isRedirect() ) {
@@ -471,6 +476,7 @@ class ApiQueryImageInfo extends ApiQueryBase {
                                ApiBase::PARAM_TYPE => 'string',
                        ),
                        'continue' => null,
+                       'localonly' => false,
                );
        }
 
@@ -543,7 +549,8 @@ class ApiQueryImageInfo extends ApiQueryBase {
                        'end' => 'Timestamp to stop listing at',
                        'metadataversion' => array( "Version of metadata to use. if 'latest' is specified, use latest version.",
                                                "Defaults to '1' for backwards compatibility" ),
-                       'continue' => 'If the query response includes a continue value, use it here to get another page of results'
+                       'continue' => 'If the query response includes a continue value, use it here to get another page of results',
+                       'localonly' => 'Look only for files in the local repository',
                );
        }
 
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 5f82a90..3063440 100644 (file)
@@ -188,6 +188,9 @@ class SwiftFileBackend extends FileBackendStore {
                        $obj->set_etag( md5( $params['content'] ) );
                        // Use the same content type as StreamFile for security
                        $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] );
+                       if ( !strlen( $obj->content_type ) ) { // special case
+                               $obj->content_type = 'unknown/unknown';
+                       }
                        if ( !empty( $params['async'] ) ) { // deferred
                                $handle = $obj->write_async( $params['content'] );
                                $status->value = new SwiftFileOpHandle( $this, $params, 'Create', $handle );
@@ -267,6 +270,9 @@ class SwiftFileBackend extends FileBackendStore {
                        $obj->set_etag( md5_file( $params['src'] ) );
                        // Use the same content type as StreamFile for security
                        $obj->content_type = StreamFile::contentTypeFromPath( $params['dst'] );
+                       if ( !strlen( $obj->content_type ) ) { // special case
+                               $obj->content_type = 'unknown/unknown';
+                       }
                        if ( !empty( $params['async'] ) ) { // deferred
                                wfSuppressWarnings();
                                $fp = fopen( $params['src'], 'rb' );
index 3fa8166..7489862 100644 (file)
@@ -246,15 +246,15 @@ abstract class File {
        }
 
        /**
-        * Callback for usort() to do file sorts by title
+        * Callback for usort() to do file sorts by name
         *
         * @param $a File
         * @param $b File
         *
-        * @return Integer: result of title comparison
+        * @return Integer: result of name comparison
         */
        public static function compare( File $a, File $b ) {
-               return Title::compare( $a->getTitle(), $b->getTitle() );
+               return strcmp( $a->getName(), $b->getName() );
        }
 
        /**
index ec67a39..63778b7 100644 (file)
@@ -895,7 +895,10 @@ class MWMemcached {
        function _load_items( $sock, &$ret ) {
                while ( 1 ) {
                        $decl = fgets( $sock );
-                       if ( $decl == "END\r\n" ) {
+                       if( $decl === false ) {
+                               $this->_debugprint( "Error reading socket for a memcached response\n" );
+                               return 0;
+                       } elseif ( $decl == "END\r\n" ) {
                                return true;
                        } elseif ( preg_match( '/^VALUE (\S+) (\d+) (\d+)\r\n$/', $decl, $match ) ) {
                                list( $rkey, $flags, $len ) = array( $match[1], $match[2], $match[3] );
@@ -939,7 +942,8 @@ class MWMemcached {
                                }
 
                        } else {
-                               $this->_debugprint( "Error parsing memcached response\n" );
+                               $peer = stream_socket_get_name( $sock, true /** remote **/ );
+                               $this->_debugprint( "Error parsing memcached response from [{$peer}]\n" );
                                return 0;
                        }
                }
index 6d2831c..1232e3f 100644 (file)
@@ -64,7 +64,8 @@ class CategoryPager extends AlphabeticPager {
                $from = str_replace( ' ', '_', $from );
                if( $from !== '' ) {
                        $from = Title::capitalize( $from, NS_CATEGORY );
-                       $this->mOffset = $from;
+                       $this->setOffset( $from );
+                       $this->setIncludeOffset( true );
                }
        }
 
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 aeb2e9d..e0868bf 100644 (file)
@@ -182,7 +182,7 @@ EXAMPLE_PATH           =
 EXAMPLE_PATTERNS       = *
 EXAMPLE_RECURSIVE      = NO
 IMAGE_PATH             =
-INPUT_FILTER           =
+INPUT_FILTER           = "php mwdoc-filter.php"
 FILTER_PATTERNS        =
 FILTER_SOURCE_FILES    = NO
 FILTER_SOURCE_PATTERNS =
index 498bc6b..70a9232 100644 (file)
@@ -140,6 +140,12 @@ class CopyFileBackend extends Maintenance {
                        $fsFile = $src->getLocalReference( array( 'src' => $srcPath, 'latest' => 1 ) );
                        if ( !$fsFile ) {
                                $this->error( "Could not get local copy of $srcPath.", 1 ); // die
+                       } elseif ( !$fsFile->exists() ) {
+                               // FSFileBackends just return the path for getLocalReference() and paths with
+                               // illegal slashes may get normalized to a different path. This can cause the
+                               // local reference to not exist...skip these broken files.
+                               $this->error( "Detected possible illegal path for $srcPath." );
+                               continue;
                        }
                        $fsFiles[] = $fsFile; // keep TempFSFile objects alive as needed
                        // Note: prepare() is usually fast for key/value backends
index 126faaa..000517f 100644 (file)
@@ -1962,6 +1962,8 @@ $wgMessageStructure = array(
                'rollback',
                'rollback_short',
                'rollbacklink',
+               'rollbacklinkcount',
+               'rollbacklinkcount-morethan',
                'rollbackfailed',
                'cantrollback',
                'alreadyrolled',
diff --git a/maintenance/mwdoc-filter.php b/maintenance/mwdoc-filter.php
new file mode 100644 (file)
index 0000000..75290f4
--- /dev/null
@@ -0,0 +1,13 @@
+<?php
+# Original source code by Goran Rakic
+# http://blog.goranrakic.com/
+# http://stackoverflow.com/questions/4325224
+
+# Should be filled in doxygen INPUT_FILTER as "php mwdoc-filter.php"
+
+$source = file_get_contents( $argv[1] );
+$regexp = '#\@var\s+([^\s]+)([^/]+)/\s+(var|public|protected|private)\s+(\$[^\s;=]+)#';
+$replac = '${2} */ ${3} ${1} ${4}';
+$source = preg_replace($regexp, $replac, $source);
+
+echo $source;
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 365a795..006bbea 100644 (file)
@@ -4,18 +4,16 @@
 .ui-button { display: inline-block; position: relative; padding: 0; margin-right: .1em; text-decoration: none !important; cursor: pointer; text-align: center; zoom: 1; overflow: visible; } /* the overflow property removes extra width in IE */
 .ui-button-icon-only { width: 2.2em; } /* to make room for the icon, a width needs to be set here */
 button.ui-button-icon-only { width: 2.4em; } /* button elements seem to need a little more width */
-.ui-button-icons-only { width: 3.4em; } 
-button.ui-button-icons-only { width: 3.7em; } 
+.ui-button-icons-only { width: 3.4em; }
+button.ui-button-icons-only { width: 3.7em; }
 
 /*button text element */
-.ui-button .ui-button-text { display: block; line-height: 1.4em;  }
+.ui-button .ui-button-text { display: block; line-height: 1.4;  }
 .ui-button-text-only .ui-button-text { padding: 0.3em 1em 0.25em 1em; }
 .ui-button-icon-only .ui-button-text, .ui-button-icons-only .ui-button-text { padding: 0.3em; text-indent: -9999999px; }
 .ui-button-text-icon-primary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: 0.3em 1em 0.25em 2.1em; }
 .ui-button-text-icon-secondary .ui-button-text, .ui-button-text-icons .ui-button-text { padding: 0.3em 2.1em 0.25em 1em; }
 .ui-button-text-icons .ui-button-text { padding-left: 2.1em; padding-right: 2.1em; }
-/* for older versions of jQuery UI */
-.ui-button-text-icon .ui-button-text { padding: 0.3em 1em 0.3em 2.1em; }
 
 /* no icon support for input elements, provide padding by default */
 input.ui-button { padding: 0.3em 1em; }
@@ -34,10 +32,7 @@ input.ui-button { padding: 0.3em 1em; }
 button.ui-button::-moz-focus-inner { border: 0; padding: 0; } /* reset extra padding in Firefox */
 
 body .ui-button {
-       -moz-border-radius: 4px;
-       -webkit-border-radius: 4px;
-       border-radius: 4px;
-       margin: 0.5em 0 0.5em 0.4em !important;
+       margin: 0.5em 0 0.5em 0.4em;
        border: 1px solid #a6a6a6 !important;
        /* @embed */
        background: #f2f2f2 url(images/button-off.png) repeat-x scroll 50% 100% !important;
@@ -47,6 +42,15 @@ body .ui-button {
        width: auto;
        overflow: visible;
 }
+
+/* Corner radius */
+/* This is normally handled in jquery.ui.theme.css, but in our case, the corner
+   styling of our buttons doesn't match our default widget corner styling */
+.ui-button.ui-corner-all, .ui-button.ui-corner-top, .ui-button.ui-corner-left, .ui-button.ui-corner-tl { -moz-border-radius-topleft: 4px; -webkit-border-top-left-radius: 4px; border-top-left-radius: 4px; }
+.ui-button.ui-corner-all, .ui-button.ui-corner-top, .ui-button.ui-corner-right, .ui-button.ui-corner-tr { -moz-border-radius-topright: 4px; -webkit-border-top-right-radius: 4px; border-top-right-radius: 4px; }
+.ui-button.ui-corner-all, .ui-button.ui-corner-bottom, .ui-button.ui-corner-left, .ui-button.ui-corner-bl { -moz-border-radius-bottomleft: 4px; -webkit-border-bottom-left-radius: 4px; border-bottom-left-radius: 4px; }
+.ui-button.ui-corner-all, .ui-button.ui-corner-bottom, .ui-button.ui-corner-right, .ui-button.ui-corner-br { -moz-border-radius-bottomright: 4px; -webkit-border-bottom-right-radius: 4px; border-bottom-right-radius: 4px; }
+
 body .ui-button:hover {
        border-color: #6e7273;
        /* @embed */
index f7eb692..49063ba 100644 (file)
@@ -27,7 +27,8 @@
 }
 
 .mw-badge-inline {
-       display: inline;
+       display: inline-block;
+       margin-left: 3px;
 }
 
 .mw-badge-overlay {
@@ -35,4 +36,4 @@
        bottom: -1px;
        right: -3px;
        z-index: 50;
-}
\ No newline at end of file
+}
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
index 57f81a0..02fd29f 100644 (file)
@@ -198,22 +198,6 @@ pre, .mw-code {
        border: 1px dashed #2f6fab;
        color: black;
        background-color: #f9f9f9;
-
-       /*
-        * Wrap properly.
-        * - pre-wrap: causes the browser to naturally wrap by displaying
-        *   words on the next line if they don't fit on the same line
-        *   within the box (does not cut off words).
-        * - break-word: forces the browser to wrap anywhere (even within
-        *   a word) if it is (still) too long for the line.
-        * When only using break-word in a <pre>, the browser only uses
-        * the force behavior and as a result almost always cuts half-way
-        * a word. When only using pre-wrap, too-long words will still
-        * cause the page layout to break. The combination is magic :).
-        * See also https://bugzilla.wikimedia.org/show_bug.cgi?id=260#c20
-        */
-       white-space: pre-wrap;
-       word-wrap: break-word;
 }
 
 /* Tables */
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 ) );