3 const api = new mw.Api(),
5 ApiSandbox = require( './ApiSandbox.js' ),
6 OptionalParamWidget = require( './OptionalParamWidget.js' ),
7 BooleanToggleSwitchParamWidget = require( './BooleanToggleSwitchParamWidget.js' ),
8 DateTimeParamWidget = require( './DateTimeParamWidget.js' ),
9 LimitParamWidget = require( './LimitParamWidget.js' ),
10 PasswordParamWidget = require( './PasswordParamWidget.js' ),
11 UploadSelectFileParamWidget = require( './UploadSelectFileParamWidget.js' );
13 const WidgetMethods = {
15 getApiValue: function () {
16 return this.getValue();
18 setApiValue: function ( v ) {
19 if ( v === undefined ) {
20 v = this.paramInfo.default;
24 apiCheckValid: function ( shouldSuppressErrors ) {
25 return this.getValidity().then(
26 () => $.Deferred().resolve( true ).promise(),
27 () => $.Deferred().resolve( false ).promise()
29 ok = ok || shouldSuppressErrors;
30 this.setIcon( ok ? null : 'alert' );
31 this.setTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
37 alertTokenError: function ( code, error ) {
38 ApiSandbox.windowManager.openWindow( 'errorAlert', {
39 title: Util.parseMsg( 'apisandbox-results-fixtoken-fail', this.paramInfo.tokentype ),
44 label: OO.ui.msg( 'ooui-dialog-process-dismiss' ),
50 fetchToken: function () {
52 return api.getToken( this.paramInfo.tokentype )
53 .done( this.setApiValue.bind( this ) )
54 .fail( this.alertTokenError.bind( this ) )
55 .always( this.popPending.bind( this ) );
57 setApiValue: function ( v ) {
58 if ( v === undefined ) {
59 v = this.paramInfo.default;
62 if ( v === '123ABC' ) {
69 getApiValue: function () {
70 const selected = this.getMenu().findFirstSelectedItem();
71 return selected ? selected.getData() : undefined;
73 setApiValue: function ( v ) {
74 if ( v === undefined ) {
75 v = this.paramInfo.default;
77 const menu = this.getMenu();
78 if ( v === undefined ) {
81 menu.selectItemByData( String( v ) );
84 apiCheckValid: function ( shouldSuppressErrors ) {
85 const ok = this.getApiValue() !== undefined || shouldSuppressErrors;
86 this.setIcon( ok ? null : 'alert' );
87 this.setTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
88 return $.Deferred().resolve( ok ).promise();
93 parseApiValue: function ( v ) {
94 if ( v === undefined || v === '' || v === '\x1f' ) {
98 if ( v[ 0 ] !== '\x1f' ) {
99 return v.split( '|' );
101 return v.slice( 1 ).split( '\x1f' );
105 getApiValueForTemplates: function () {
106 return this.isDisabled() ? this.parseApiValue( this.paramInfo.default ) : this.getValue();
108 getApiValue: function () {
109 const items = this.getValue();
110 if ( items.join( '' ).indexOf( '|' ) === -1 ) {
111 return items.join( '|' );
113 return '\x1f' + items.join( '\x1f' );
116 setApiValue: function ( v ) {
117 if ( v === undefined ) {
118 v = this.paramInfo.default;
120 this.setValue( this.parseApiValue( v ) );
122 apiCheckValid: function ( shouldSuppressErrors ) {
124 if ( !shouldSuppressErrors ) {
125 const pi = this.paramInfo;
126 ok = this.getApiValue() !== undefined && !(
127 pi.allspecifier !== undefined &&
128 this.getValue().length > 1 &&
129 this.getValue().indexOf( pi.allspecifier ) !== -1
133 this.setIcon( ok ? null : 'alert' );
134 this.setTitle( ok ? '' : mw.message( 'apisandbox-alert-field' ).plain() );
135 return $.Deferred().resolve( ok ).promise();
137 createTagItemWidget: function ( data, label ) {
138 const item = OO.ui.TagMultiselectWidget.prototype.createTagItemWidget.call( this, data, label );
139 if ( this.paramInfo.deprecatedvalues &&
140 this.paramInfo.deprecatedvalues.indexOf( data ) >= 0
142 item.$element.addClass( 'mw-apisandbox-deprecated-value' );
144 if ( this.paramInfo.internalvalues &&
145 this.paramInfo.internalvalues.indexOf( data ) >= 0
147 item.$element.addClass( 'mw-apisandbox-internal-value' );
154 single: function () {
155 const v = this.isDisabled() ? this.paramInfo.default : this.getApiValue();
156 return v === undefined ? [] : [ { value: v, path: this.paramInfo.submodules[ v ] } ];
159 const map = this.paramInfo.submodules,
160 v = this.isDisabled() ? this.paramInfo.default : this.getApiValue();
161 return v === undefined || v === '' ?
163 String( v ).split( '|' ).map( ( val ) => ( { value: val, path: map[ val ] } ) );
169 generic: function () {
170 return !Util.apiBool( this.paramInfo.required ) || this.getApiValue() !== '';
175 * @class mw.special.ApiSandbox.Util
180 * Fetch API module info
182 * @param {string} module Module to fetch data for
183 * @return {jQuery.Promise}
185 fetchModuleInfo: function ( module ) {
186 const deferred = $.Deferred();
188 if ( Object.prototype.hasOwnProperty.call( moduleInfoCache, module ) ) {
190 .resolve( moduleInfoCache[ module ] )
191 .promise( { abort: () => {} } );
193 const apiPromise = api.post( {
197 uselang: mw.config.get( 'wgUserLanguage' )
198 } ).done( ( data ) => {
199 if ( data.warnings && data.warnings.paraminfo ) {
200 deferred.reject( '???', data.warnings.paraminfo[ '*' ] );
204 const info = data.paraminfo.modules;
205 if ( !info || info.length !== 1 || info[ 0 ].path !== module ) {
206 deferred.reject( '???', 'No module data returned' );
210 moduleInfoCache[ module ] = info[ 0 ];
211 deferred.resolve( info[ 0 ] );
212 } ).fail( ( code, details ) => {
213 if ( code === 'http' ) {
214 details = 'HTTP error: ' + details.exception;
215 } else if ( details.error ) {
216 details = details.error.info;
218 deferred.reject( code, details );
221 .promise( { abort: apiPromise.abort } );
226 * Mark all currently-in-use tokens as bad
228 markTokensBad: function () {
229 const checkPages = [ ApiSandbox.pages.main ];
231 while ( checkPages.length ) {
232 const page = checkPages.shift();
234 if ( page.tokenWidget ) {
235 api.badToken( page.tokenWidget.paramInfo.tokentype );
238 const subpages = page.getSubpages();
239 subpages.forEach( ( subpage ) => {
240 if ( Object.prototype.hasOwnProperty.call( ApiSandbox.pages, subpage.key ) ) {
241 checkPages.push( ApiSandbox.pages[ subpage.key ] );
248 * Test an API boolean
253 apiBool: function ( value ) {
254 return value !== undefined && value !== false;
258 * Create a widget for a parameter.
260 * @param {Object} pi Parameter info from API
261 * @param {Object} opts Additional options
262 * @return {OO.ui.Widget}
264 createWidgetForParameter: function ( pi, opts ) {
265 let multiModeButton = null,
266 multiModeInput = null,
267 multiModeAllowed = false;
274 widget = new BooleanToggleSwitchParamWidget();
275 widget.paramInfo = pi;
276 pi.required = true; // Avoid wrapping in the non-required widget
280 // ApiParamInfo only sets `tokentype` when the parameter
281 // name is `token` AND the module ::needsToken() returns
282 // a truthy value; ApiBase, when the module ::needsToken()
283 // returns a truthy value, sets the `token` param to PARAM_TYPE
284 // string always, so we only need to have handling for
285 // token widgets for `string`. The token never accepts multiple
286 // values, though that doesn't appear to be enforced anywhere...
287 // and the token widget methods all assume it only a single value
288 if ( pi.tokentype ) {
289 // We probably don't need to check if its required,
290 // it always is, but whats the harm
291 widget = new OO.ui.TextInputWidget( {
292 required: Util.apiBool( pi.required )
294 widget.paramInfo = pi;
295 Object.assign( widget, WidgetMethods.textInputWidget );
296 widget.setValidation( Validators.generic );
297 Object.assign( widget, WidgetMethods.tokenWidget );
300 // intentional fall through
303 if ( Util.apiBool( pi.multi ) ) {
304 widget = new OO.ui.TagMultiselectWidget( {
305 allowArbitrary: true,
306 allowDuplicates: Util.apiBool( pi.allowsduplicates ),
309 widget.paramInfo = pi;
310 Object.assign( widget, WidgetMethods.tagWidget );
312 widget = new OO.ui.TextInputWidget( {
313 required: Util.apiBool( pi.required )
315 widget.paramInfo = pi;
316 Object.assign( widget, WidgetMethods.textInputWidget );
317 widget.setValidation( Validators.generic );
323 widget = new OO.ui.MultilineTextInputWidget( {
324 required: Util.apiBool( pi.required )
326 widget.paramInfo = pi;
327 Object.assign( widget, WidgetMethods.textInputWidget );
328 widget.setValidation( Validators.generic );
332 widget = new PasswordParamWidget( {
333 required: Util.apiBool( pi.required )
335 widget.paramInfo = pi;
336 widget.setValidation( Validators.generic );
337 multiModeAllowed = true;
338 multiModeInput = widget;
342 widget = new OO.ui.NumberInputWidget( {
344 min: pi.min || -Infinity,
345 max: pi.max || Infinity,
346 required: Util.apiBool( pi.required )
348 widget.paramInfo = pi;
349 Object.assign( widget, WidgetMethods.textInputWidget );
350 multiModeAllowed = true;
351 multiModeInput = widget;
355 widget = new LimitParamWidget( {
356 required: Util.apiBool( pi.required )
358 pi.min = pi.min || 0;
359 pi.apiSandboxMax = ( mw.config.get( 'apihighlimits' ) ? pi.highmax : pi.max ) || pi.max;
360 widget.paramInfo = pi;
361 multiModeAllowed = true;
362 multiModeInput = widget;
366 widget = new DateTimeParamWidget( {
367 required: Util.apiBool( pi.required )
369 widget.paramInfo = pi;
370 multiModeAllowed = true;
374 widget = new UploadSelectFileParamWidget();
375 widget.paramInfo = pi;
379 // eslint-disable-next-line no-jquery/no-map-util
380 items = $.map( mw.config.get( 'wgFormattedNamespaces' ), ( name, ns ) => {
382 name = mw.msg( 'blanknamespace' );
384 return new OO.ui.MenuOptionWidget( { data: ns, label: name } );
385 } ).sort( ( a, b ) => a.data - b.data );
386 if ( Util.apiBool( pi.multi ) ) {
387 if ( pi.allspecifier !== undefined ) {
388 items.unshift( new OO.ui.MenuOptionWidget( {
389 data: pi.allspecifier,
390 label: mw.msg( 'apisandbox-multivalue-all-namespaces', pi.allspecifier )
394 widget = new OO.ui.MenuTagMultiselectWidget( {
395 menu: { items: items },
398 widget.paramInfo = pi;
399 Object.assign( widget, WidgetMethods.tagWidget );
401 widget = new OO.ui.DropdownWidget( {
402 menu: { items: items },
405 widget.paramInfo = pi;
406 Object.assign( widget, WidgetMethods.dropdownWidget );
411 if ( Util.apiBool( pi.multi ) ) {
412 widget = new mw.widgets.TitlesMultiselectWidget( {
413 required: Util.apiBool( pi.required ),
416 showMissing: !Util.apiBool( pi.mustExist ),
417 addQueryInput: !Util.apiBool( pi.mustExist ),
418 tagLimit: pi.limit || undefined
420 widget.paramInfo = pi;
421 Object.assign( widget, WidgetMethods.tagWidget );
423 widget = new mw.widgets.TitleInputWidget( {
424 required: Util.apiBool( pi.required ),
428 showMissing: !Util.apiBool( pi.mustExist ),
429 addQueryInput: !Util.apiBool( pi.mustExist )
431 widget.paramInfo = pi;
432 Object.assign( widget, WidgetMethods.textInputWidget );
437 if ( !Array.isArray( pi.type ) ) {
438 throw new Error( 'Unknown parameter type ' + pi.type );
441 items = pi.type.map( ( v ) => {
442 const optionWidget = new OO.ui.MenuOptionWidget( {
446 if ( pi.deprecatedvalues && pi.deprecatedvalues.indexOf( v ) >= 0 ) {
447 optionWidget.$element.addClass( 'mw-apisandbox-deprecated-value' );
448 optionWidget.$label.before(
449 $( '<span>' ).addClass( 'mw-apisandbox-flag' ).text( mw.msg( 'api-help-param-deprecated-label' ) )
452 if ( pi.internalvalues && pi.internalvalues.indexOf( v ) >= 0 ) {
453 optionWidget.$element.addClass( 'mw-apisandbox-internal-value' );
454 optionWidget.$label.before(
455 $( '<span>' ).addClass( 'mw-apisandbox-flag' ).text( mw.msg( 'api-help-param-internal-label' ) )
459 } ).sort( ( a, b ) => a.label < b.label ? -1 : ( a.label > b.label ? 1 : 0 ) );
460 if ( Util.apiBool( pi.multi ) ) {
461 if ( pi.allspecifier !== undefined ) {
462 items.unshift( new OO.ui.MenuOptionWidget( {
463 data: pi.allspecifier,
464 label: mw.msg( 'apisandbox-multivalue-all-values', pi.allspecifier )
468 widget = new OO.ui.MenuTagMultiselectWidget( {
469 menu: { items: items },
472 widget.paramInfo = pi;
473 Object.assign( widget, WidgetMethods.tagWidget );
474 if ( Util.apiBool( pi.submodules ) ) {
475 widget.getSubmodules = WidgetMethods.submoduleWidget.multi;
476 widget.on( 'change', ApiSandbox.updateUI );
479 widget = new OO.ui.DropdownWidget( {
480 menu: { items: items },
483 widget.paramInfo = pi;
484 Object.assign( widget, WidgetMethods.dropdownWidget );
485 if ( Util.apiBool( pi.submodules ) ) {
486 widget.getSubmodules = WidgetMethods.submoduleWidget.single;
487 widget.getMenu().on( 'select', ApiSandbox.updateUI );
489 if ( pi.deprecatedvalues ) {
490 widget.getMenu().on( 'select', ( item ) => {
491 widget.$element.toggleClass(
492 'mw-apisandbox-deprecated-value',
493 pi.deprecatedvalues.indexOf( item.data ) >= 0
497 if ( pi.internalvalues ) {
498 widget.getMenu().on( 'select', ( item ) => {
499 widget.$element.toggleClass(
500 'mw-apisandbox-internal-value',
501 pi.internalvalues.indexOf( item.data ) >= 0
510 if ( Util.apiBool( pi.multi ) && multiModeAllowed ) {
511 const innerWidget = widget;
513 multiModeButton = new OO.ui.ButtonWidget( {
514 label: mw.msg( 'apisandbox-add-multi' )
516 const $content = innerWidget.$element.add( multiModeButton.$element );
518 widget = new OO.ui.PopupTagMultiselectWidget( {
519 allowArbitrary: true,
520 allowDuplicates: Util.apiBool( pi.allowsduplicates ),
523 classes: [ 'mw-apisandbox-popup' ],
528 widget.paramInfo = pi;
529 Object.assign( widget, WidgetMethods.tagWidget );
532 if ( !innerWidget.isDisabled() ) {
533 innerWidget.apiCheckValid( ApiSandbox.suppressErrors ).done( ( ok ) => {
535 widget.addTag( innerWidget.getApiValue() );
536 innerWidget.setApiValue( undefined );
543 if ( multiModeInput ) {
544 multiModeInput.on( 'enter', func );
546 multiModeButton.on( 'click', func );
550 if ( Util.apiBool( pi.required ) || opts.nooptional ) {
551 finalWidget = widget;
553 finalWidget = new OptionalParamWidget( widget );
554 finalWidget.paramInfo = pi;
555 if ( widget.getSubmodules ) {
556 finalWidget.getSubmodules = widget.getSubmodules.bind( widget );
557 finalWidget.on( 'disable', () => {
558 setTimeout( ApiSandbox.updateUI );
561 if ( widget.getApiValueForTemplates ) {
562 finalWidget.getApiValueForTemplates = widget.getApiValueForTemplates.bind( widget );
564 finalWidget.setDisabled( true );
567 widget.setApiValue( pi.default );
573 * Parse an HTML string and call Util.fixupHTML()
575 * @param {string} html HTML to parse
578 parseHTML: function ( html ) {
579 const $ret = $( $.parseHTML( html ) );
580 return Util.fixupHTML( $ret );
584 * Parse an i18n message and call Util.fixupHTML()
586 * @param {string} key Key of message to get
587 * @param {...Mixed} parameters Values for $N replacements
590 parseMsg: function ( key, ...parameters ) {
591 // eslint-disable-next-line mediawiki/msg-doc
592 const $ret = mw.message( key, ...parameters ).parseDom();
593 return Util.fixupHTML( $ret );
597 * Fix HTML for ApiSandbox display
600 * - Add target="_blank" to any links
602 * @param {jQuery} $html DOM to process
605 fixupHTML: function ( $html ) {
606 $html.filter( 'a' ).add( $html.find( 'a' ) )
607 .filter( '[href]:not([target])' )
608 .attr( 'target', '_blank' );
613 * Format a request and return a bunch of menu option widgets
615 * @param {Object} displayParams Query parameters, sanitized for display.
616 * @param {Object} rawParams Query parameters. You should probably use displayParams instead.
617 * @param {string} method HTTP method that must be used for this request: 'get' or 'post'
618 * @param {Object} ajaxOptions Extra options that must be used for this request, in the format
619 * expected by jQuery#ajax.
620 * @return {OO.ui.MenuOptionWidget[]} Each item's data should be an OO.ui.FieldLayout
622 formatRequest: function ( displayParams, rawParams, method, ajaxOptions ) {
623 let jsonLayout, phpLayout;
624 const apiUrl = new URL( mw.util.wikiScript( 'api' ), location.origin ).toString();
626 new OO.ui.MenuOptionWidget( {
627 label: Util.parseMsg( 'apisandbox-request-format-url-label' ),
628 data: new mw.widgets.CopyTextLayout( {
629 label: Util.parseMsg( 'apisandbox-request-url-label' ),
630 copyText: apiUrl + '?' + $.param( displayParams )
633 new OO.ui.MenuOptionWidget( {
634 label: Util.parseMsg( 'apisandbox-request-format-json-label' ),
635 data: jsonLayout = new mw.widgets.CopyTextLayout( {
636 label: Util.parseMsg( 'apisandbox-request-json-label' ),
637 copyText: JSON.stringify( displayParams, null, '\t' ),
640 classes: [ 'mw-apisandbox-textInputCode' ],
644 } ).on( 'toggle', ( visible ) => {
646 // Call updatePosition instead of adjustSize
647 // because the latter has weird caching
648 // behavior and the former bypasses it.
649 jsonLayout.textInput.updatePosition();
653 new OO.ui.MenuOptionWidget( {
654 label: Util.parseMsg( 'apisandbox-request-format-php-label' ),
655 data: phpLayout = new mw.widgets.CopyTextLayout( {
656 label: Util.parseMsg( 'apisandbox-request-php-label' ),
658 Object.keys( displayParams ).map(
659 // displayParams is a dictionary of strings or numbers
661 JSON.stringify( param ) +
663 JSON.stringify( displayParams[ param ] ).replace( /\$/g, '\\$' )
668 classes: [ 'mw-apisandbox-textInputCode' ],
672 } ).on( 'toggle', ( visible ) => {
674 // Call updatePosition instead of adjustSize
675 // because the latter has weird caching
676 // behavior and the former bypasses it.
677 phpLayout.textInput.updatePosition();
683 mw.hook( 'apisandbox.formatRequest' ).fire( items, displayParams, rawParams, method, ajaxOptions );
689 * Event handler for when formatDropdown's selection changes
691 onFormatDropdownChange: function () {
692 const menu = ApiSandbox.formatDropdown.getMenu(),
693 selected = menu.findFirstSelectedItem(),
694 selectedField = selected ? selected.getData() : null;
696 menu.getItems().forEach( ( item ) => {
697 item.getData().toggle( item.getData() === selectedField );
702 module.exports = Util;