X-Git-Url: https://scripts.mit.edu/gitweb/autoinstallsdev/mediawiki.git/blobdiff_plain/19e297c21b10b1b8a3acad5e73fc71dcb35db44a..6932310fd58ebef145fa01eb76edf7150284d8ea:/resources/src/mediawiki.special/mediawiki.special.apisandbox.js diff --git a/resources/src/mediawiki.special/mediawiki.special.apisandbox.js b/resources/src/mediawiki.special/mediawiki.special.apisandbox.js new file mode 100644 index 00000000..7e9ad7f6 --- /dev/null +++ b/resources/src/mediawiki.special/mediawiki.special.apisandbox.js @@ -0,0 +1,1953 @@ +/* eslint-disable no-use-before-define */ +( function ( $, mw, OO ) { + 'use strict'; + var ApiSandbox, Util, WidgetMethods, Validators, + $content, panel, booklet, oldhash, windowManager, fullscreenButton, + formatDropdown, + api = new mw.Api(), + bookletPages = [], + availableFormats = {}, + resultPage = null, + suppressErrors = true, + updatingBooklet = false, + pages = {}, + moduleInfoCache = {}, + baseRequestParams; + + WidgetMethods = { + textInputWidget: { + getApiValue: function () { + return this.getValue(); + }, + setApiValue: function ( v ) { + if ( v === undefined ) { + v = this.paramInfo[ 'default' ]; + } + this.setValue( v ); + }, + apiCheckValid: function () { + var that = this; + return this.getValidity().then( function () { + return $.Deferred().resolve( true ).promise(); + }, function () { + return $.Deferred().resolve( false ).promise(); + } ).done( function ( ok ) { + ok = ok || suppressErrors; + that.setIcon( ok ? null : 'alert' ); + that.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() ); + } ); + } + }, + + dateTimeInputWidget: { + getValidity: function () { + if ( !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '' ) { + return $.Deferred().resolve().promise(); + } else { + return $.Deferred().reject().promise(); + } + } + }, + + tokenWidget: { + alertTokenError: function ( code, error ) { + windowManager.openWindow( 'errorAlert', { + title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ), + message: error, + actions: [ + { + action: 'accept', + label: OO.ui.msg( 'ooui-dialog-process-dismiss' ), + flags: 'primary' + } + ] + } ); + }, + fetchToken: function () { + this.pushPending(); + return api.getToken( this.paramInfo.tokentype ) + .done( this.setApiValue.bind( this ) ) + .fail( this.alertTokenError.bind( this ) ) + .always( this.popPending.bind( this ) ); + }, + setApiValue: function ( v ) { + WidgetMethods.textInputWidget.setApiValue.call( this, v ); + if ( v === '123ABC' ) { + this.fetchToken(); + } + } + }, + + passwordWidget: { + getApiValueForDisplay: function () { + return ''; + } + }, + + toggleSwitchWidget: { + getApiValue: function () { + return this.getValue() ? 1 : undefined; + }, + setApiValue: function ( v ) { + this.setValue( Util.apiBool( v ) ); + }, + apiCheckValid: function () { + return $.Deferred().resolve( true ).promise(); + } + }, + + dropdownWidget: { + getApiValue: function () { + var item = this.getMenu().getSelectedItem(); + return item === null ? undefined : item.getData(); + }, + setApiValue: function ( v ) { + var menu = this.getMenu(); + + if ( v === undefined ) { + v = this.paramInfo[ 'default' ]; + } + if ( v === undefined ) { + menu.selectItem(); + } else { + menu.selectItemByData( String( v ) ); + } + }, + apiCheckValid: function () { + var ok = this.getApiValue() !== undefined || suppressErrors; + this.setIcon( ok ? null : 'alert' ); + this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() ); + return $.Deferred().resolve( ok ).promise(); + } + }, + + capsuleWidget: { + getApiValue: function () { + var items = this.getItemsData(); + if ( items.join( '' ).indexOf( '|' ) === -1 ) { + return items.join( '|' ); + } else { + return '\x1f' + items.join( '\x1f' ); + } + }, + setApiValue: function ( v ) { + if ( v === undefined || v === '' || v === '\x1f' ) { + this.setItemsFromData( [] ); + } else { + v = String( v ); + if ( v.indexOf( '\x1f' ) !== 0 ) { + this.setItemsFromData( v.split( '|' ) ); + } else { + this.setItemsFromData( v.substr( 1 ).split( '\x1f' ) ); + } + } + }, + apiCheckValid: function () { + var ok = true, + pi = this.paramInfo; + + if ( !suppressErrors ) { + ok = this.getApiValue() !== undefined && !( + pi.allspecifier !== undefined && + this.getItemsData().length > 1 && + this.getItemsData().indexOf( pi.allspecifier ) !== -1 + ); + } + + this.setIcon( ok ? null : 'alert' ); + this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() ); + return $.Deferred().resolve( ok ).promise(); + }, + createItemWidget: function ( data, label ) { + var item = OO.ui.CapsuleMultiselectWidget.prototype.createItemWidget.call( this, data, label ); + if ( this.paramInfo.deprecatedvalues && + this.paramInfo.deprecatedvalues.indexOf( data ) >= 0 + ) { + item.$element.addClass( 'apihelp-deprecated-value' ); + } + return item; + } + }, + + optionalWidget: { + getApiValue: function () { + return this.isDisabled() ? undefined : this.widget.getApiValue(); + }, + setApiValue: function ( v ) { + this.setDisabled( v === undefined ); + this.widget.setApiValue( v ); + }, + apiCheckValid: function () { + if ( this.isDisabled() ) { + return $.Deferred().resolve( true ).promise(); + } else { + return this.widget.apiCheckValid(); + } + } + }, + + submoduleWidget: { + single: function () { + var v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue(); + return v === undefined ? [] : [ { value: v, path: this.paramInfo.submodules[ v ] } ]; + }, + multi: function () { + var map = this.paramInfo.submodules, + v = this.isDisabled() ? this.paramInfo[ 'default' ] : this.getApiValue(); + return v === undefined || v === '' ? [] : $.map( String( v ).split( '|' ), function ( v ) { + return { value: v, path: map[ v ] }; + } ); + } + }, + + uploadWidget: { + getApiValueForDisplay: function () { + return '...'; + }, + getApiValue: function () { + return this.getValue(); + }, + setApiValue: function () { + // Can't, sorry. + }, + apiCheckValid: function () { + var ok = this.getValue() !== null || suppressErrors; + this.setIcon( ok ? null : 'alert' ); + this.setIconTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() ); + return $.Deferred().resolve( ok ).promise(); + } + } + }; + + Validators = { + generic: function () { + return !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== ''; + } + }; + + /** + * @class mw.special.ApiSandbox.Util + * @private + */ + Util = { + /** + * Fetch API module info + * + * @param {string} module Module to fetch data for + * @return {jQuery.Promise} + */ + fetchModuleInfo: function ( module ) { + var apiPromise, + deferred = $.Deferred(); + + if ( moduleInfoCache.hasOwnProperty( module ) ) { + return deferred + .resolve( moduleInfoCache[ module ] ) + .promise( { abort: function () {} } ); + } else { + apiPromise = api.post( { + action: 'paraminfo', + modules: module, + helpformat: 'html', + uselang: mw.config.get( 'wgUserLanguage' ) + } ).done( function ( data ) { + var info; + + if ( data.warnings && data.warnings.paraminfo ) { + deferred.reject( '???', data.warnings.paraminfo[ '*' ] ); + return; + } + + info = data.paraminfo.modules; + if ( !info || info.length !== 1 || info[ 0 ].path !== module ) { + deferred.reject( '???', 'No module data returned' ); + return; + } + + moduleInfoCache[ module ] = info[ 0 ]; + deferred.resolve( info[ 0 ] ); + } ).fail( function ( code, details ) { + if ( code === 'http' ) { + details = 'HTTP error: ' + details.exception; + } else if ( details.error ) { + details = details.error.info; + } + deferred.reject( code, details ); + } ); + return deferred + .promise( { abort: apiPromise.abort } ); + } + }, + + /** + * Mark all currently-in-use tokens as bad + */ + markTokensBad: function () { + var page, subpages, i, + checkPages = [ pages.main ]; + + while ( checkPages.length ) { + page = checkPages.shift(); + + if ( page.tokenWidget ) { + api.badToken( page.tokenWidget.paramInfo.tokentype ); + } + + subpages = page.getSubpages(); + for ( i = 0; i < subpages.length; i++ ) { + if ( pages.hasOwnProperty( subpages[ i ].key ) ) { + checkPages.push( pages[ subpages[ i ].key ] ); + } + } + } + }, + + /** + * Test an API boolean + * + * @param {Mixed} value + * @return {boolean} + */ + apiBool: function ( value ) { + return value !== undefined && value !== false; + }, + + /** + * Create a widget for a parameter. + * + * @param {Object} pi Parameter info from API + * @param {Object} opts Additional options + * @return {OO.ui.Widget} + */ + createWidgetForParameter: function ( pi, opts ) { + var widget, innerWidget, finalWidget, items, $button, $content, func, + multiMode = 'none'; + + opts = opts || {}; + + switch ( pi.type ) { + case 'boolean': + widget = new OO.ui.ToggleSwitchWidget(); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.toggleSwitchWidget ); + pi.required = true; // Avoid wrapping in the non-required widget + break; + + case 'string': + case 'user': + if ( pi.tokentype ) { + widget = new TextInputWithIndicatorWidget( { + input: { + indicator: 'previous', + indicatorTitle: mw.message( 'apisandbox-fetch-token' ).text(), + required: Util.apiBool( pi.required ) + } + } ); + } else if ( Util.apiBool( pi.multi ) ) { + widget = new OO.ui.CapsuleMultiselectWidget( { + allowArbitrary: true, + allowDuplicates: Util.apiBool( pi.allowsduplicates ), + $overlay: $( '#mw-apisandbox-ui' ) + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.capsuleWidget ); + } else { + widget = new OO.ui.TextInputWidget( { + required: Util.apiBool( pi.required ) + } ); + } + if ( !Util.apiBool( pi.multi ) ) { + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + widget.setValidation( Validators.generic ); + } + if ( pi.tokentype ) { + $.extend( widget, WidgetMethods.tokenWidget ); + widget.input.paramInfo = pi; + $.extend( widget.input, WidgetMethods.textInputWidget ); + $.extend( widget.input, WidgetMethods.tokenWidget ); + widget.on( 'indicator', widget.fetchToken, [], widget ); + } + break; + + case 'text': + widget = new OO.ui.MultilineTextInputWidget( { + required: Util.apiBool( pi.required ) + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + widget.setValidation( Validators.generic ); + break; + + case 'password': + widget = new OO.ui.TextInputWidget( { + type: 'password', + required: Util.apiBool( pi.required ) + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + $.extend( widget, WidgetMethods.passwordWidget ); + widget.setValidation( Validators.generic ); + multiMode = 'enter'; + break; + + case 'integer': + widget = new OO.ui.NumberInputWidget( { + required: Util.apiBool( pi.required ), + isInteger: true + } ); + widget.setIcon = widget.input.setIcon.bind( widget.input ); + widget.setIconTitle = widget.input.setIconTitle.bind( widget.input ); + widget.getValidity = widget.input.getValidity.bind( widget.input ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + if ( Util.apiBool( pi.enforcerange ) ) { + widget.setRange( pi.min || -Infinity, pi.max || Infinity ); + } + multiMode = 'enter'; + break; + + case 'limit': + widget = new OO.ui.TextInputWidget( { + required: Util.apiBool( pi.required ) + } ); + widget.setValidation( function ( value ) { + var n, pi = this.paramInfo; + + if ( value === 'max' ) { + return true; + } else { + n = +value; + return !isNaN( n ) && isFinite( n ) && + Math.floor( n ) === n && + n >= pi.min && n <= pi.apiSandboxMax; + } + } ); + pi.min = pi.min || 0; + pi.apiSandboxMax = mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max; + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + multiMode = 'enter'; + break; + + case 'timestamp': + widget = new mw.widgets.datetime.DateTimeInputWidget( { + formatter: { + format: '${year|0}-${month|0}-${day|0}T${hour|0}:${minute|0}:${second|0}${zone|short}' + }, + required: Util.apiBool( pi.required ), + clearable: false + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.textInputWidget ); + $.extend( widget, WidgetMethods.dateTimeInputWidget ); + multiMode = 'indicator'; + break; + + case 'upload': + widget = new OO.ui.SelectFileWidget(); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.uploadWidget ); + break; + + case 'namespace': + items = $.map( mw.config.get( 'wgFormattedNamespaces' ), function ( name, ns ) { + if ( ns === '0' ) { + name = mw.message( 'blanknamespace' ).text(); + } + return new OO.ui.MenuOptionWidget( { data: ns, label: name } ); + } ).sort( function ( a, b ) { + return a.data - b.data; + } ); + if ( Util.apiBool( pi.multi ) ) { + if ( pi.allspecifier !== undefined ) { + items.unshift( new OO.ui.MenuOptionWidget( { + data: pi.allspecifier, + label: mw.message( 'apisandbox-multivalue-all-namespaces', pi.allspecifier ).text() + } ) ); + } + + widget = new OO.ui.CapsuleMultiselectWidget( { + menu: { items: items }, + $overlay: $( '#mw-apisandbox-ui' ) + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.capsuleWidget ); + } else { + widget = new OO.ui.DropdownWidget( { + menu: { items: items }, + $overlay: $( '#mw-apisandbox-ui' ) + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.dropdownWidget ); + } + break; + + default: + if ( !Array.isArray( pi.type ) ) { + throw new Error( 'Unknown parameter type ' + pi.type ); + } + + items = $.map( pi.type, function ( v ) { + var config = { + data: String( v ), + label: String( v ), + classes: [] + }; + if ( pi.deprecatedvalues && pi.deprecatedvalues.indexOf( v ) >= 0 ) { + config.classes.push( 'apihelp-deprecated-value' ); + } + return new OO.ui.MenuOptionWidget( config ); + } ); + if ( Util.apiBool( pi.multi ) ) { + if ( pi.allspecifier !== undefined ) { + items.unshift( new OO.ui.MenuOptionWidget( { + data: pi.allspecifier, + label: mw.message( 'apisandbox-multivalue-all-values', pi.allspecifier ).text() + } ) ); + } + + widget = new OO.ui.CapsuleMultiselectWidget( { + menu: { items: items }, + $overlay: $( '#mw-apisandbox-ui' ) + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.capsuleWidget ); + if ( Util.apiBool( pi.submodules ) ) { + widget.getSubmodules = WidgetMethods.submoduleWidget.multi; + widget.on( 'change', ApiSandbox.updateUI ); + } + } else { + widget = new OO.ui.DropdownWidget( { + menu: { items: items }, + $overlay: $( '#mw-apisandbox-ui' ) + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.dropdownWidget ); + if ( Util.apiBool( pi.submodules ) ) { + widget.getSubmodules = WidgetMethods.submoduleWidget.single; + widget.getMenu().on( 'select', ApiSandbox.updateUI ); + } + if ( pi.deprecatedvalues ) { + widget.getMenu().on( 'select', function ( item ) { + this.$element.toggleClass( + 'apihelp-deprecated-value', + pi.deprecatedvalues.indexOf( item.data ) >= 0 + ); + }, [], widget ); + } + } + + break; + } + + if ( Util.apiBool( pi.multi ) && multiMode !== 'none' ) { + innerWidget = widget; + switch ( multiMode ) { + case 'enter': + $content = innerWidget.$element; + break; + + case 'indicator': + $button = innerWidget.$indicator; + $button.css( 'cursor', 'pointer' ); + $button.attr( 'tabindex', 0 ); + $button.parent().append( $button ); + innerWidget.setIndicator( 'next' ); + $content = innerWidget.$element; + break; + + default: + throw new Error( 'Unknown multiMode "' + multiMode + '"' ); + } + + widget = new OO.ui.CapsuleMultiselectWidget( { + allowArbitrary: true, + allowDuplicates: Util.apiBool( pi.allowsduplicates ), + $overlay: $( '#mw-apisandbox-ui' ), + popup: { + classes: [ 'mw-apisandbox-popup' ], + $content: $content + } + } ); + widget.paramInfo = pi; + $.extend( widget, WidgetMethods.capsuleWidget ); + + func = function () { + if ( !innerWidget.isDisabled() ) { + innerWidget.apiCheckValid().done( function ( ok ) { + if ( ok ) { + widget.addItemsFromData( [ innerWidget.getApiValue() ] ); + innerWidget.setApiValue( undefined ); + } + } ); + return false; + } + }; + switch ( multiMode ) { + case 'enter': + innerWidget.connect( null, { enter: func } ); + break; + + case 'indicator': + $button.on( { + click: func, + keypress: function ( e ) { + if ( e.which === OO.ui.Keys.SPACE || e.which === OO.ui.Keys.ENTER ) { + func(); + } + } + } ); + break; + } + } + + if ( Util.apiBool( pi.required ) || opts.nooptional ) { + finalWidget = widget; + } else { + finalWidget = new OptionalWidget( widget ); + finalWidget.paramInfo = pi; + $.extend( finalWidget, WidgetMethods.optionalWidget ); + if ( widget.getSubmodules ) { + finalWidget.getSubmodules = widget.getSubmodules.bind( widget ); + finalWidget.on( 'disable', function () { setTimeout( ApiSandbox.updateUI ); } ); + } + finalWidget.setDisabled( true ); + } + + widget.setApiValue( pi[ 'default' ] ); + + return finalWidget; + }, + + /** + * Parse an HTML string and call Util.fixupHTML() + * + * @param {string} html HTML to parse + * @return {jQuery} + */ + parseHTML: function ( html ) { + var $ret = $( $.parseHTML( html ) ); + return Util.fixupHTML( $ret ); + }, + + /** + * Parse an i18n message and call Util.fixupHTML() + * + * @param {string} key Key of message to get + * @param {...Mixed} parameters Values for $N replacements + * @return {jQuery} + */ + parseMsg: function () { + var $ret = mw.message.apply( mw.message, arguments ).parseDom(); + return Util.fixupHTML( $ret ); + }, + + /** + * Fix HTML for ApiSandbox display + * + * Fixes are: + * - Add target="_blank" to any links + * + * @param {jQuery} $html DOM to process + * @return {jQuery} + */ + fixupHTML: function ( $html ) { + $html.filter( 'a' ).add( $html.find( 'a' ) ) + .filter( '[href]:not([target])' ) + .attr( 'target', '_blank' ); + return $html; + }, + + /** + * Format a request and return a bunch of menu option widgets + * + * @param {Object} displayParams Query parameters, sanitized for display. + * @param {Object} rawParams Query parameters. You should probably use displayParams instead. + * @return {OO.ui.MenuOptionWidget[]} Each item's data should be an OO.ui.FieldLayout + */ + formatRequest: function ( displayParams, rawParams ) { + var jsonInput, + items = [ + new OO.ui.MenuOptionWidget( { + label: Util.parseMsg( 'apisandbox-request-format-url-label' ), + data: new OO.ui.FieldLayout( + new OO.ui.TextInputWidget( { + readOnly: true, + value: mw.util.wikiScript( 'api' ) + '?' + $.param( displayParams ) + } ), { + label: Util.parseMsg( 'apisandbox-request-url-label' ) + } + ) + } ), + new OO.ui.MenuOptionWidget( { + label: Util.parseMsg( 'apisandbox-request-format-json-label' ), + data: new OO.ui.FieldLayout( + jsonInput = new OO.ui.MultilineTextInputWidget( { + classes: [ 'mw-apisandbox-textInputCode' ], + readOnly: true, + autosize: true, + maxRows: 6, + value: JSON.stringify( displayParams, null, '\t' ) + } ), { + label: Util.parseMsg( 'apisandbox-request-json-label' ) + } + ).on( 'toggle', function ( visible ) { + if ( visible ) { + // Call updatePosition instead of adjustSize + // because the latter has weird caching + // behavior and the former bypasses it. + jsonInput.updatePosition(); + } + } ) + } ) + ]; + + mw.hook( 'apisandbox.formatRequest' ).fire( items, displayParams, rawParams ); + + return items; + }, + + /** + * Event handler for when formatDropdown's selection changes + */ + onFormatDropdownChange: function () { + var i, + menu = formatDropdown.getMenu(), + items = menu.getItems(), + selectedField = menu.getSelectedItem() ? menu.getSelectedItem().getData() : null; + + for ( i = 0; i < items.length; i++ ) { + items[ i ].getData().toggle( items[ i ].getData() === selectedField ); + } + } + }; + + /** + * Interface to ApiSandbox UI + * + * @class mw.special.ApiSandbox + */ + ApiSandbox = { + /** + * Initialize the UI + * + * Automatically called on $.ready() + */ + init: function () { + var $toolbar; + + ApiSandbox.isFullscreen = false; + + $content = $( '#mw-apisandbox' ); + + windowManager = new OO.ui.WindowManager(); + $( 'body' ).append( windowManager.$element ); + windowManager.addWindows( { + errorAlert: new OO.ui.MessageDialog() + } ); + + fullscreenButton = new OO.ui.ButtonWidget( { + label: mw.message( 'apisandbox-fullscreen' ).text(), + title: mw.message( 'apisandbox-fullscreen-tooltip' ).text() + } ).on( 'click', ApiSandbox.toggleFullscreen ); + + $toolbar = $( '
' ).append( Util.parseMsg( 'apisandbox-intro' ) ) ) + .append( + $( '
][\s\S]*<\/pre>/ ) ) ) { + $result.append( Util.parseHTML( m[ 0 ] ) ); + if ( ( m = data.match( /"wgBackendResponseTime":\s*(\d+)/ ) ) ) { + loadTime = parseInt( m[ 1 ], 10 ); + } + } else { + $( '' ) + .addClass( 'api-pretty-content' ) + .text( data ) + .appendTo( $result ); + } + if ( paramsAreForced || data[ 'continue' ] ) { + $result.append( + $( '' ).append( + new OO.ui.ButtonWidget( { + label: mw.message( 'apisandbox-continue' ).text() + } ).on( 'click', function () { + ApiSandbox.sendRequest( $.extend( {}, baseRequestParams, data[ 'continue' ] ) ); + } ).setDisabled( !data[ 'continue' ] ).$element, + ( clear = new OO.ui.ButtonWidget( { + label: mw.message( 'apisandbox-continue-clear' ).text() + } ).on( 'click', function () { + ApiSandbox.updateUI( baseRequestParams ); + clear.setDisabled( true ); + booklet.setPage( '|results|' ); + } ).setDisabled( !paramsAreForced ) ).$element, + new OO.ui.PopupButtonWidget( { + $overlay: $( '#mw-apisandbox-ui' ), + framed: false, + icon: 'info', + popup: { + $content: $( '' ).append( Util.parseMsg( 'apisandbox-continue-help' ) ), + padded: true, + width: 'auto' + } + } ).$element + ) + ); + } + if ( typeof loadTime === 'number' ) { + $result.append( + $( '' ).append( + new OO.ui.LabelWidget( { + label: mw.message( 'apisandbox-request-time', loadTime ).text() + } ).$element + ) + ); + } + + if ( jqXHR.getResponseHeader( 'MediaWiki-API-Error' ) === 'badtoken' ) { + // Flush all saved tokens in case one of them is the bad one. + Util.markTokensBad(); + button = new OO.ui.ButtonWidget( { + label: mw.message( 'apisandbox-results-fixtoken' ).text() + } ); + button.on( 'click', ApiSandbox.fixTokenAndResend ) + .on( 'click', button.setDisabled, [ true ], button ) + .$element.appendTo( $result ); + } + }, function ( code, data ) { + var details = 'HTTP error: ' + data.exception; + $result.empty() + .append( + new OO.ui.LabelWidget( { + label: mw.message( 'apisandbox-results-error', details ).text(), + classes: [ 'error' ] + } ).$element + ); + } ); + } ); + }, + + /** + * Handler for the "Correct token and resubmit" button + * + * Used on a 'badtoken' error, it re-fetches token parameters for all + * pages and then re-submits the query. + */ + fixTokenAndResend: function () { + var page, subpages, i, k, + ok = true, + tokenWait = { dummy: true }, + checkPages = [ pages.main ], + success = function ( k ) { + delete tokenWait[ k ]; + if ( ok && $.isEmptyObject( tokenWait ) ) { + ApiSandbox.sendRequest(); + } + }, + failure = function ( k ) { + delete tokenWait[ k ]; + ok = false; + }; + + while ( checkPages.length ) { + page = checkPages.shift(); + + if ( page.tokenWidget ) { + k = page.apiModule + page.tokenWidget.paramInfo.name; + tokenWait[ k ] = page.tokenWidget.fetchToken(); + tokenWait[ k ] + .done( success.bind( page.tokenWidget, k ) ) + .fail( failure.bind( page.tokenWidget, k ) ); + } + + subpages = page.getSubpages(); + for ( i = 0; i < subpages.length; i++ ) { + if ( pages.hasOwnProperty( subpages[ i ].key ) ) { + checkPages.push( pages[ subpages[ i ].key ] ); + } + } + } + + success( 'dummy', '' ); + }, + + /** + * Reset validity indicators for all widgets + */ + updateValidityIndicators: function () { + var page, subpages, i, + checkPages = [ pages.main ]; + + while ( checkPages.length ) { + page = checkPages.shift(); + page.apiCheckValid(); + subpages = page.getSubpages(); + for ( i = 0; i < subpages.length; i++ ) { + if ( pages.hasOwnProperty( subpages[ i ].key ) ) { + checkPages.push( pages[ subpages[ i ].key ] ); + } + } + } + } + }; + + /** + * PageLayout for API modules + * + * @class + * @private + * @extends OO.ui.PageLayout + * @constructor + * @param {Object} [config] Configuration options + */ + ApiSandbox.PageLayout = function ( config ) { + config = $.extend( { prefix: '' }, config ); + this.displayText = config.key; + this.apiModule = config.path; + this.prefix = config.prefix; + this.paramInfo = null; + this.apiIsValid = true; + this.loadFromQueryParams = null; + this.widgets = {}; + this.tokenWidget = null; + this.indentLevel = config.indentLevel ? config.indentLevel : 0; + ApiSandbox.PageLayout[ 'super' ].call( this, config.key, config ); + this.loadParamInfo(); + }; + OO.inheritClass( ApiSandbox.PageLayout, OO.ui.PageLayout ); + ApiSandbox.PageLayout.prototype.setupOutlineItem = function () { + this.outlineItem.setLevel( this.indentLevel ); + this.outlineItem.setLabel( this.displayText ); + this.outlineItem.setIcon( this.apiIsValid || suppressErrors ? null : 'alert' ); + this.outlineItem.setIconTitle( + this.apiIsValid || suppressErrors ? '' : mw.message( 'apisandbox-alert-page' ).plain() + ); + }; + + /** + * Fetch module information for this page's module, then create UI + */ + ApiSandbox.PageLayout.prototype.loadParamInfo = function () { + var dynamicFieldset, dynamicParamNameWidget, + that = this, + removeDynamicParamWidget = function ( name, layout ) { + dynamicFieldset.removeItems( [ layout ] ); + delete that.widgets[ name ]; + }, + addDynamicParamWidget = function () { + var name, layout, widget, button; + + // Check name is filled in + name = dynamicParamNameWidget.getValue().trim(); + if ( name === '' ) { + dynamicParamNameWidget.focus(); + return; + } + + if ( that.widgets[ name ] !== undefined ) { + windowManager.openWindow( 'errorAlert', { + title: Util.parseMsg( 'apisandbox-dynamic-error-exists', name ), + actions: [ + { + action: 'accept', + label: OO.ui.msg( 'ooui-dialog-process-dismiss' ), + flags: 'primary' + } + ] + } ); + return; + } + + widget = Util.createWidgetForParameter( { + name: name, + type: 'string', + 'default': '' + }, { + nooptional: true + } ); + button = new OO.ui.ButtonWidget( { + icon: 'trash', + flags: 'destructive' + } ); + layout = new OO.ui.ActionFieldLayout( + widget, + button, + { + label: name, + align: 'left' + } + ); + button.on( 'click', removeDynamicParamWidget, [ name, layout ] ); + that.widgets[ name ] = widget; + dynamicFieldset.addItems( [ layout ], dynamicFieldset.getItems().length - 1 ); + widget.focus(); + + dynamicParamNameWidget.setValue( '' ); + }; + + this.$element.empty() + .append( new OO.ui.ProgressBarWidget( { + progress: false, + text: mw.message( 'apisandbox-loading', this.displayText ).text() + } ).$element ); + + Util.fetchModuleInfo( this.apiModule ) + .done( function ( pi ) { + var prefix, i, j, descriptionContainer, widget, widgetField, helpField, tmp, flag, count, + items = [], + deprecatedItems = [], + buttons = [], + filterFmModules = function ( v ) { + return v.substr( -2 ) !== 'fm' || + !availableFormats.hasOwnProperty( v.substr( 0, v.length - 2 ) ); + }, + widgetLabelOnClick = function () { + var f = this.getField(); + if ( $.isFunction( f.setDisabled ) ) { + f.setDisabled( false ); + } + if ( $.isFunction( f.focus ) ) { + f.focus(); + } + }; + + // This is something of a hack. We always want the 'format' and + // 'action' parameters from the main module to be specified, + // and for 'format' we also want to simplify the dropdown since + // we always send the 'fm' variant. + if ( that.apiModule === 'main' ) { + for ( i = 0; i < pi.parameters.length; i++ ) { + if ( pi.parameters[ i ].name === 'action' ) { + pi.parameters[ i ].required = true; + delete pi.parameters[ i ][ 'default' ]; + } + if ( pi.parameters[ i ].name === 'format' ) { + tmp = pi.parameters[ i ].type; + for ( j = 0; j < tmp.length; j++ ) { + availableFormats[ tmp[ j ] ] = true; + } + pi.parameters[ i ].type = $.grep( tmp, filterFmModules ); + pi.parameters[ i ][ 'default' ] = 'json'; + pi.parameters[ i ].required = true; + } + } + } + + // Hide the 'wrappedhtml' parameter on format modules + if ( pi.group === 'format' ) { + pi.parameters = $.grep( pi.parameters, function ( p ) { + return p.name !== 'wrappedhtml'; + } ); + } + + that.paramInfo = pi; + + items.push( new OO.ui.FieldLayout( + new OO.ui.Widget( {} ).toggle( false ), { + align: 'top', + label: Util.parseHTML( pi.description ) + } + ) ); + + if ( pi.helpurls.length ) { + buttons.push( new OO.ui.PopupButtonWidget( { + $overlay: $( '#mw-apisandbox-ui' ), + label: mw.message( 'apisandbox-helpurls' ).text(), + icon: 'help', + popup: { + width: 'auto', + padded: true, + $content: $( '' ).append( $.map( pi.helpurls, function ( link ) { + return $( '
- ' ).append( $( '', { + href: link, + target: '_blank', + text: link + } ) ); + } ) ) + } + } ) ); + } + + if ( pi.examples.length ) { + buttons.push( new OO.ui.PopupButtonWidget( { + $overlay: $( '#mw-apisandbox-ui' ), + label: mw.message( 'apisandbox-examples' ).text(), + icon: 'code', + popup: { + width: 'auto', + padded: true, + $content: $( '