Merge "mediawiki.Upload.Dialog: Factor out booklet layout"
authorjenkins-bot <jenkins-bot@gerrit.wikimedia.org>
Wed, 23 Sep 2015 16:37:19 +0000 (16:37 +0000)
committerGerrit Code Review <gerrit@wikimedia.org>
Wed, 23 Sep 2015 16:37:19 +0000 (16:37 +0000)
docs/hooks.txt
includes/DefaultSettings.php
includes/api/ApiImport.php
includes/specials/SpecialImport.php
includes/upload/UploadFromUrl.php
resources/src/mediawiki.widgets/mw.widgets.CategorySelector.js
resources/src/mediawiki/mediawiki.ForeignStructuredUpload.js

index 866e891..9aa2964 100644 (file)
@@ -1633,6 +1633,11 @@ Return false to stop further processing of the tag
 $reader: XMLReader object
 $revisionInfo: Array of information
 
+'ImportSources': Called when reading from the $wgImportSources configuration
+variable. Can be used to lazy-load the import sources list.
+&$importSources: The value of $wgImportSources. Modify as necessary. See the
+comment in DefaultSettings.php for the detail of how to structure this array.
+
 'InfoAction': When building information to display on the action=info page.
 $context: IContextSource object
 &$pageInfo: Array of information
index 37429b9..c5fdbac 100644 (file)
@@ -6363,8 +6363,8 @@ $wgShowCreditsIfMax = true;
 
 /**
  * List of interwiki prefixes for wikis we'll accept as sources for
- * Special:Import (for sysops). Since complete page history can be imported,
- * these should be 'trusted'.
+ * Special:Import and API action=import. Since complete page history can be
+ * imported, these should be 'trusted'.
  *
  * This can either be a regular array, or an associative map specifying
  * subprojects on the interwiki map of the target wiki, or a mix of the two,
@@ -6377,6 +6377,9 @@ $wgShowCreditsIfMax = true;
  *     );
  * @endcode
  *
+ * If you have a very complex import sources setup, you can lazy-load it using
+ * the ImportSources hook.
+ *
  * If a user has the 'import' permission but not the 'importupload' permission,
  * they will only be able to run imports through this transwiki interface.
  */
index 735cc7f..a6aae4b 100644 (file)
@@ -92,6 +92,30 @@ class ApiImport extends ApiBase {
                $result->addValue( null, $this->getModuleName(), $resultData );
        }
 
+       /**
+        * Returns a list of interwiki prefixes corresponding to each defined import
+        * source.
+        *
+        * @return array
+        * @since 1.26
+        */
+       public function getAllowedImportSources() {
+               $importSources = $this->getConfig()->get( 'ImportSources' );
+               Hooks::run( 'ImportSources', array( &$importSources ) );
+
+               $result = array();
+               foreach ( $importSources as $key => $value ) {
+                       if ( is_int( $key ) ) {
+                               $result[] = $value;
+                       } else {
+                               foreach ( $value as $subproject ) {
+                                       $result[] = "$key:$subproject";
+                               }
+                       }
+               }
+               return $result;
+       }
+
        public function mustBePosted() {
                return true;
        }
@@ -107,7 +131,7 @@ class ApiImport extends ApiBase {
                                ApiBase::PARAM_TYPE => 'upload',
                        ),
                        'interwikisource' => array(
-                               ApiBase::PARAM_TYPE => $this->getConfig()->get( 'ImportSources' ),
+                               ApiBase::PARAM_TYPE => $this->getAllowedImportSources(),
                        ),
                        'interwikipage' => null,
                        'fullhistory' => false,
index 4cdf6dd..e2bc629 100644 (file)
@@ -42,6 +42,7 @@ class SpecialImport extends SpecialPage {
        private $history = true;
        private $includeTemplates = false;
        private $pageLinkDepth;
+       private $importSources;
 
        /**
         * Constructor
@@ -66,6 +67,9 @@ class SpecialImport extends SpecialPage {
 
                $this->getOutput()->addModules( 'mediawiki.special.import' );
 
+               $this->importSources = $this->getConfig()->get( 'ImportSources' );
+               Hooks::run( 'ImportSources', array( &$this->importSources ) );
+
                $user = $this->getUser();
                if ( !$user->isAllowedAny( 'import', 'importupload' ) ) {
                        throw new PermissionsError( 'import' );
@@ -136,16 +140,17 @@ class SpecialImport extends SpecialPage {
                        }
                        $this->interwiki = $this->fullInterwikiPrefix = $request->getVal( 'interwiki' );
                        // does this interwiki have subprojects?
-                       $importSources = $this->getConfig()->get( 'ImportSources' );
-                       $hasSubprojects = array_key_exists( $this->interwiki, $importSources );
-                       if ( !$hasSubprojects && !in_array( $this->interwiki, $importSources ) ) {
+                       $hasSubprojects = array_key_exists( $this->interwiki, $this->importSources );
+                       if ( !$hasSubprojects && !in_array( $this->interwiki, $this->importSources ) ) {
                                $source = Status::newFatal( "import-invalid-interwiki" );
                        } else {
                                if ( $hasSubprojects ) {
                                        $this->subproject = $request->getVal( 'subproject' );
                                        $this->fullInterwikiPrefix .= ':' . $request->getVal( 'subproject' );
                                }
-                               if ( $hasSubprojects && !in_array( $this->subproject, $importSources[$this->interwiki] ) ) {
+                               if ( $hasSubprojects &&
+                                       !in_array( $this->subproject, $this->importSources[$this->interwiki] )
+                               ) {
                                        $source = Status::newFatal( "import-invalid-interwiki" );
                                } else {
                                        $this->history = $request->getCheck( 'interwikiHistory' );
@@ -306,7 +311,6 @@ class SpecialImport extends SpecialPage {
                $user = $this->getUser();
                $out = $this->getOutput();
                $this->addHelpLink( '//meta.wikimedia.org/wiki/Special:MyLanguage/Help:Import', true );
-               $importSources = $this->getConfig()->get( 'ImportSources' );
 
                if ( $user->isAllowed( 'importupload' ) ) {
                        $mappingSelection = $this->getMappingFormPart( 'upload' );
@@ -356,12 +360,12 @@ class SpecialImport extends SpecialPage {
                                        Xml::closeElement( 'fieldset' )
                        );
                } else {
-                       if ( empty( $importSources ) ) {
+                       if ( empty( $this->importSources ) ) {
                                $out->addWikiMsg( 'importnosources' );
                        }
                }
 
-               if ( $user->isAllowed( 'import' ) && !empty( $importSources ) ) {
+               if ( $user->isAllowed( 'import' ) && !empty( $this->importSources ) ) {
                        # Show input field for import depth only if $wgExportMaxLinkDepth > 0
                        $importDepth = '';
                        if ( $this->getConfig()->get( 'ExportMaxLinkDepth' ) > 0 ) {
@@ -403,7 +407,7 @@ class SpecialImport extends SpecialPage {
                        );
 
                        $needSubprojectField = false;
-                       foreach ( $importSources as $key => $value ) {
+                       foreach ( $this->importSources as $key => $value ) {
                                if ( is_int( $key ) ) {
                                        $key = $value;
                                } elseif ( $value !== $key ) {
@@ -435,7 +439,7 @@ class SpecialImport extends SpecialPage {
                                );
 
                                $subprojectsToAdd = array();
-                               foreach ( $importSources as $key => $value ) {
+                               foreach ( $this->importSources as $key => $value ) {
                                        if ( is_array( $value ) ) {
                                                $subprojectsToAdd = array_merge( $subprojectsToAdd, $value );
                                        }
index f578745..f897a79 100644 (file)
@@ -175,7 +175,6 @@ class UploadFromUrl extends UploadBase {
                $url = $request->getVal( 'wpUploadFileURL' );
 
                return !empty( $url )
-                       && Http::isValidURI( $url )
                        && $wgUser->isAllowed( 'upload_by_url' );
        }
 
index 8d6452c..995561e 100644 (file)
@@ -5,34 +5,67 @@
  * @license The MIT License (MIT); see LICENSE.txt
  */
 ( function ( $, mw ) {
+       var CSP;
 
        /**
         * Category selector widget. Displays an OO.ui.CapsuleMultiSelectWidget
         * and autocompletes with available categories.
         *
-        * @class
+        *     var selector = new mw.widgets.CategorySelector( {
+        *       searchTypes: [
+        *         mw.widgets.CategorySelector.SearchType.OpenSearch,
+        *         mw.widgets.CategorySelector.SearchType.InternalSearch
+        *       ]
+        *     } );
+        *
+        *     $( '#content' ).append( selector.$element );
+        *
+        *     selector.setSearchType( [ mw.widgets.CategorySelector.SearchType.SubCategories ] );
+        *
+        *
+        * @class mw.widgets.CategorySelector
         * @uses mw.Api
         * @extends OO.ui.CapsuleMultiSelectWidget
         *
         * @constructor
         * @param {Object} [config] Configuration options
         * @cfg {number} [limit=10] Maximum number of results to load
+        * @cfg {mw.widgets.CategorySelector.SearchType[]} [searchTypes=[mw.widgets.CategorySelector.SearchType.OpenSearch]]
+        *   Default search API to use when searching.
         */
-       mw.widgets.CategorySelector = function ( config ) {
+       function CategorySelector( config ) {
                // Config initialization
-               config = $.extend( { limit: 10 }, config );
+               config = $.extend( {
+                       limit: 10,
+                       searchTypes: [ CategorySelector.SearchType.OpenSearch ]
+               }, config );
                this.limit = config.limit;
+               this.searchTypes = config.searchTypes;
+               this.validateSearchTypes();
 
                // Parent constructor
-               mw.widgets.CategorySelector.parent.call( this, config );
+               mw.widgets.CategorySelector.parent.call( this, $.extend( true, {}, config, {
+                       menu: {
+                               filterFromInput: false
+                       },
+                       // This allows the user to both select non-existent categories, and prevents the selector from
+                       // being wiped from #onMenuItemsChange when we change the available options in the dropdown
+                       allowArbitrary: true
+               } ) );
 
                // Event handler to call the autocomplete methods
                this.$input.on( 'change input cut paste', OO.ui.debounce( this.updateMenuItems.bind( this ), 100 ) );
-       };
+
+               // Initialize
+               this.catNsId = mw.config.get( 'wgNamespaceIds' ).category;
+               this.api = new mw.Api();
+
+       }
 
        /* Setup */
 
-       OO.inheritClass( mw.widgets.CategorySelector, OO.ui.CapsuleMultiSelectWidget );
+       OO.inheritClass( CategorySelector, OO.ui.CapsuleMultiSelectWidget );
+       CSP = CategorySelector.prototype;
 
        /* Methods */
 
@@ -44,7 +77,8 @@
         * @private
         * @method
         */
-       mw.widgets.CategorySelector.prototype.updateMenuItems = function () {
+       CSP.updateMenuItems = function () {
+               this.getMenu().clearItems();
                this.getNewMenuItems( this.$input.val() ).then( function ( items ) {
                        var existingItems, filteredItems,
                                menu = this.getMenu();
                                } );
                        } );
 
-                       menu.addItems( filteredItems ).updateItemVisibility();
+                       menu.addItems( filteredItems ).toggle( true );
                }.bind( this ) );
        };
 
         * @param {string} input The input used to prefix search categories
         * @return {jQuery.Promise} Resolves with an array of categories
         */
-       mw.widgets.CategorySelector.prototype.getNewMenuItems = function ( input ) {
-               var deferred = new $.Deferred(),
-                       catNsId = mw.config.get( 'wgNamespaceIds' ).category,
-                       api = new mw.Api();
-
-               api.get( {
-                       action: 'opensearch',
-                       namespace: catNsId,
-                       limit: this.limit,
-                       search: input
-               } ).done( function ( res ) {
-                       var categoryNames = res[ 1 ].map( function ( name ) {
-                               return mw.Title.newFromText( name, catNsId ).getMainText();
+       CSP.getNewMenuItems = function ( input ) {
+               var i,
+                       promises = [],
+                       deferred = new $.Deferred();
+
+               for ( i = 0; i < this.searchTypes.length; i++ ) {
+                       promises.push( this.searchCategories( input, this.searchTypes[ i ] ) );
+               }
+
+               $.when.apply( $, promises ).done( function () {
+                       var categories, categoryNames,
+                               allData = [],
+                               dataSets = Array.prototype.slice.apply( arguments );
+
+                       // Collect values from all results
+                       allData = allData.concat.apply( allData, dataSets );
+
+                       // Remove duplicates
+                       categories = allData.filter( function ( value, index, self ) {
+                               return self.indexOf( value ) === index;
+                       } );
+
+                       // Get titles
+                       categoryNames = categories.map( function ( name ) {
+                               return mw.Title.newFromText( name, this.catNsId ).getMainText();
                        } );
 
                        deferred.resolve( categoryNames );
+
                } );
 
                return deferred.promise();
        };
+
+       /**
+        * Validates the values in `this.searchType`.
+        *
+        * @private
+        * @return {boolean}
+        */
+       CSP.validateSearchTypes = function () {
+               var validSearchTypes = false,
+                       searchTypeEnumCount = Object.keys( CategorySelector.SearchType ).length;
+
+               // Check if all values are in the SearchType enum
+               validSearchTypes = this.searchTypes.every( function ( searchType ) {
+                       return searchType > -1 && searchType < searchTypeEnumCount;
+               } );
+
+               if ( validSearchTypes === false ) {
+                       throw new Error( 'Unknown searchType in searchTypes' );
+               }
+
+               // If the searchTypes has CategorySelector.SearchType.SubCategories
+               // it can be the only search type.
+               if ( this.searchTypes.indexOf( CategorySelector.SearchType.SubCategories ) > -1 &&
+                       this.searchTypes.length > 1
+               ) {
+                       throw new Error( 'Can\'t have additional search types with CategorySelector.SearchType.SubCategories' );
+               }
+
+               // If the searchTypes has CategorySelector.SearchType.ParentCategories
+               // it can be the only search type.
+               if ( this.searchTypes.indexOf( CategorySelector.SearchType.ParentCategories ) > -1 &&
+                       this.searchTypes.length > 1
+               ) {
+                       throw new Error( 'Can\'t have additional search types with CategorySelector.SearchType.ParentCategories' );
+               }
+
+               return true;
+       };
+
+       /**
+        * Sets and validates the value of `this.searchType`.
+        *
+        * @param {mw.widgets.CategorySelector.SearchType[]} searchTypes
+        */
+       CSP.setSearchTypes = function ( searchTypes ) {
+               this.searchTypes = searchTypes;
+               this.validateSearchTypes();
+       };
+
+       /**
+        * Searches categories based on input and searchType.
+        *
+        * @private
+        * @method
+        * @param {string} input The input used to prefix search categories
+        * @param {mw.widgets.CategorySelector.SearchType} searchType
+        * @return {jQuery.Promise} Resolves with an array of categories
+        */
+       CSP.searchCategories = function ( input, searchType ) {
+               var deferred = new $.Deferred();
+
+               switch ( searchType ) {
+                       case CategorySelector.SearchType.OpenSearch:
+                               this.api.get( {
+                                       action: 'opensearch',
+                                       namespace: this.catNsId,
+                                       limit: this.limit,
+                                       search: input
+                               } ).done( function ( res ) {
+                                       var categories = res[ 1 ];
+                                       deferred.resolve( categories );
+                               } );
+                               break;
+
+                       case CategorySelector.SearchType.InternalSearch:
+                               this.api.get( {
+                                       action: 'query',
+                                       list: 'allpages',
+                                       apnamespace: this.catNsId,
+                                       aplimit: this.limit,
+                                       apfrom: input,
+                                       apprefix: input
+                               } ).done( function ( res ) {
+                                       var categories = res.query.allpages.map( function ( page ) {
+                                               return page.title;
+                                       } );
+                                       deferred.resolve( categories );
+                               } );
+                               break;
+
+                       case CategorySelector.SearchType.Exists:
+                               if ( input.indexOf( '|' ) > -1 ) {
+                                       deferred.resolve( [] );
+                                       break;
+                               }
+
+                               this.api.get( {
+                                       action: 'query',
+                                       prop: 'info',
+                                       titles: 'Category:' + input
+                               } ).done( function ( res ) {
+                                       var page,
+                                               categories = [];
+
+                                       for ( page in res.query.pages ) {
+                                               if ( parseInt( page, 10 ) > -1 ) {
+                                                       categories.push( res.query.pages[ page ].title );
+                                               }
+                                       }
+
+                                       deferred.resolve( categories );
+                               } );
+                               break;
+
+                       case CategorySelector.SearchType.SubCategories:
+                               if ( input.indexOf( '|' ) > -1 ) {
+                                       deferred.resolve( [] );
+                                       break;
+                               }
+
+                               this.api.get( {
+                                       action: 'query',
+                                       list: 'categorymembers',
+                                       cmtype: 'subcat',
+                                       cmlimit: this.limit,
+                                       cmtitle: 'Category:' + input
+                               } ).done( function ( res ) {
+                                       var categories = res.query.categorymembers.map( function ( category ) {
+                                               return category.title;
+                                       } );
+                                       deferred.resolve( categories );
+                               } );
+                               break;
+
+                       case CategorySelector.SearchType.ParentCategories:
+                               if ( input.indexOf( '|' ) > -1 ) {
+                                       deferred.resolve( [] );
+                                       break;
+                               }
+
+                               this.api.get( {
+                                       action: 'query',
+                                       prop: 'categories',
+                                       cllimit: this.limit,
+                                       titles: 'Category:' + input
+                               } ).done( function ( res )  {
+                                       var page,
+                                               categories = [];
+
+                                       for ( page in res.query.pages ) {
+                                               if ( parseInt( page, 10 ) > -1 ) {
+                                                       if ( $.isArray( res.query.pages[ page ].categories ) ) {
+                                                               categories.push.apply( categories, res.query.pages[ page ].categories.map( function ( category ) {
+                                                                       return category.title;
+                                                               } ) );
+                                                       }
+                                               }
+                                       }
+
+                                       deferred.resolve( categories );
+                               } );
+                               break;
+
+                       default:
+                               throw new Error( 'Unknown searchType' );
+               }
+
+               return deferred.promise();
+       };
+
+       /**
+        * @enum mw.widgets.CategorySelector.SearchType
+        * Types of search available.
+        */
+       CategorySelector.SearchType = {
+               /** Search using action=opensearch */
+               OpenSearch: 0,
+
+               /** Search using action=query */
+               InternalSearch: 1,
+
+               /** Search for existing categories with the exact title */
+               Exists: 2,
+
+               /** Search only subcategories  */
+               SubCategories: 3,
+
+               /** Search only parent categories */
+               ParentCategories: 4
+       };
+
+       mw.widgets.CategorySelector = CategorySelector;
 }( jQuery, mediaWiki ) );
index fe8520b..3a70a66 100644 (file)
@@ -11,8 +11,8 @@
         * See <https://commons.wikimedia.org/wiki/Commons:Structured_data> for
         * a more detailed description of how that system works.
         *
-        * TODO this currently only supports uploads under CC-BY-SA 4.0,
-        *     and should really have support for more licenses.
+        * **TODO: This currently only supports uploads under CC-BY-SA 4.0,
+        * and should really have support for more licenses.**
         *
         * @inheritdoc
         */