Current File : /home/jvzmxxx/wiki/extensions/Flow/modules/engine/misc/flow-api.js
( function ( mw, $ ) {
	mw.flow = mw.flow || {}; // create mw.flow globally

	var apiTransformMap = {
		// Map of API submodule name, block name, and prefix name
		'moderate-post': [ 'topic_', 'mp' ],
		'new-topic': [ 'topiclist_', 'nt' ],
		'edit-header': [ 'header_', 'eh' ],
		'edit-post': [ 'topic_', 'ep' ],
		reply: [ 'topic_', 'rep' ],
		'moderate-topic': [ 'topic_', 'mt' ],
		'edit-title': [ 'topic_', 'et' ],
		'lock-topic': [ 'topic_', 'cot' ],
		'view-topiclist': [ 'topiclist_', 'vtl' ],
		'view-post': [ 'topic', 'vp' ],
		'view-topic': [ 'topic', 'vt' ],
		'view-header': [ 'header_', 'vh' ],
		'view-topic-summary': [ 'topicsummary_', 'vts' ],
		'edit-topic-summary': [ 'topicsummary_', 'ets' ]
	};

	/**
	 * Handles Flow API calls. Each FlowComponent has its own instance of FlowApi as component.Api,
	 * so that it can store a workflowId and pageName permanently for simplicity.
	 * @param {string} [workflowId]
	 * @param {string} [pageName]
	 * @return {FlowApi}
	 * @constructor
	 */
	function FlowApi( storageEngine, workflowId, pageName ) {
		this.StorageEngine = storageEngine;
		this.workflowId = workflowId;
		this.pageName = pageName;

		/**
		 * Makes the actual API call and returns
		 * @param {Object|string} [params] May be a JSON object string
		 * @param {string} [method]
		 * @return {jQuery.Promise}
		 */
		function flowApiCall( params, method ) {
			var mwApi, tokenType,
				$deferred = $.Deferred(),
				ajaxTimeoutSec = mw.config.get( 'wgFlowAjaxTimeout' ),
				apiConstructorParams = { ajax: {} };

			if ( ajaxTimeoutSec !== null && ajaxTimeoutSec > 0 ) {
				apiConstructorParams.ajax.timeout = ajaxTimeoutSec * 1000;
			}

			// IE8 caches POST under some conditions, prevent that here.
			// IE8 is most likely the only browser we support that doesn't
			// have addEventListener, and anything else that gets caught
			// up isn't that bad off.
			if ( !document.addEventListener ) {
				apiConstructorParams.ajax.cache = false;
			}

			mwApi = new mw.Api( apiConstructorParams );

			params = params || {};
			// Server is using page instead of title
			// @todo this should not be necessary
			params.page = params.page || this.pageName || mw.config.get( 'wgPageName' );
			method = method ? method.toUpperCase() : 'GET';

			if ( !params.action ) {
				mw.flow.debug( '[FlowApi] apiCall error: missing action string', arguments );
				return $deferred.reject( 'fail-apirequest', { error: { info: 'Invalid action' } } ).promise();
			}
			if ( !params.page ) {
				mw.flow.debug( '[FlowApi] apiCall error: missing page string', [ mw.config.get( 'wgPageName' ) ], arguments );
				return $deferred.reject( 'fail-apirequest', { error: { info: 'Invalid title' } } ).promise();
			}

			if ( method === 'POST' ) {
				if ( params._internal && params._internal.tokenType ) {
					tokenType = params._internal.tokenType;
				} else {
					tokenType = 'csrf';
				}

				delete params._internal;

				return mwApi.postWithToken( tokenType, params );
			} else if ( method !== 'GET' ) {
				return $deferred.reject( 'fail-apirequest', { error: { info: 'Unknown submission method: ' + method } } ).promise();
			} else {
				return mwApi.get( params );
			}
		}

		this.apiCall = flowApiCall;
	}

	/** @type {Storer} */
	FlowApi.prototype.StorageEngine = null;
	/** @type {string} */
	FlowApi.prototype.pageName = null;
	/** @type {string} */
	FlowApi.prototype.workflowId = null;
	/** @type {string} */
	FlowApi.prototype.defaultSubmodule = null;

	/**
	 * Sets the fixed pageName for this API instance.
	 * @param {string} pageName
	 */
	function flowApiSetPageName( pageName ) {
		this.pageName = pageName;
	}

	FlowApi.prototype.setPageName = flowApiSetPageName;

	/**
	 * Sets the fixed workflowId for this API instance.
	 * @param {string} workflowId
	 */
	function flowApiSetWorkflowId( workflowId ) {
		this.workflowId = workflowId;
	}

	FlowApi.prototype.setWorkflowId = flowApiSetWorkflowId;

	/**
	 * Transforms URL request parameters into API params
	 * @todo fix it server-side so we don't need this client-side
	 * @param {Object} queryMap
	 * @return {Object}
	 */
	function flowApiTransformMap( queryMap ) {
		var key,
			map = apiTransformMap[ queryMap.submodule ];
		if ( !map ) {
			return queryMap;
		}
		for ( key in queryMap ) {
			if ( queryMap.hasOwnProperty( key ) ) {
				if ( key.indexOf( map[ 0 ] ) === 0 ) {
					queryMap[ key.replace( map[ 0 ], map[ 1 ] ) ] = queryMap[ key ];
					delete queryMap[ key ];
				}
				if ( key.indexOf( 'flow_' ) === 0 ) {
					queryMap[ key.replace( 'flow_', map[ 1 ] ) ] = queryMap[ key ];
					delete queryMap[ key ];
				}
			}
		}

		return queryMap;
	}

	/**
	 * Sets the fixed defaultSubmodule for this API instance.
	 * @param {string} defaultSubmodule
	 */
	function flowApiSetDefaultSubmodule( defaultSubmodule ) {
		this.defaultSubmodule = defaultSubmodule;
	}

	FlowApi.prototype.setDefaultSubmodule = flowApiSetDefaultSubmodule;

	/**
	 * With a url (a://b.c/d?e=f&g#h) will return an object of key-value pairs ({e:'f', g:''}).
	 * @param {string|HTMLElement} url
	 * @param {Object} [queryMap]
	 * @return {Object}
	 */
	function flowApiGetQueryMap( url, queryMap ) {
		var uri,
			queryKey,
			queryValue,
			i = 0,
			$node, $form, formData;

		queryMap = queryMap || {};

		// If URL is an Element...
		if ( typeof url !== 'string' ) {
			$node = $( url );

			// Get the data-flow-api-action override from the node itself
			queryMap.submodule = $node.data( 'flow-api-action' );

			if ( $node.is( 'form, input, button, textarea, select, option' ) ) {
				// We are processing a form
				$form = $node.closest( 'form' );
				formData = $form.serializeArray();

				// Get the data-flow-api-action override from the form
				queryMap.submodule = queryMap.submodule || $form.data( 'flow-api-action' );

				// Build the queryMap manually from a serialized form
				for ( i = 0; i < formData.length; i++ ) {
					// skip wpEditToken, its handle independently
					if ( formData[ i ].name !== 'wpEditToken' ) {
						queryMap[ formData[ i ].name ] = formData[ i ].value;
					}
				}

				// Add the given button to the queryMap as well
				if ( $node.is( 'button, input' ) && $node.prop( 'name' ) ) {
					queryMap[ $node.prop( 'name' ) ] = $node.val();
				}

				// Now process the form action as the URL
				url = $form.attr( 'action' );
			} else if ( $node.is( 'a' ) ) {
				// It's an anchor, process the href as the URL
				url = $node.prop( 'href' );
			} else {
				// Somebody set up us the bomb
				url = '';
			}
		}

		// Parse the URL query params
		uri = new mw.Uri( url );

		for ( queryKey in uri.query ) {
			queryValue = uri.query[ queryKey ];
			if ( queryKey === 'action' ) {
				// Submodule is the action
				queryKey = 'submodule';
			}
			if ( queryKey === 'title' ) {
				// Server is using page
				queryKey = 'page';
			}

			// Only add this to the query map if it didn't already exist, eg. in a form input
			if ( !queryMap[ queryKey ] ) {
				queryMap[ queryKey ] = queryValue;
			}
		}

		// Use the default submodule if no action in URL
		queryMap.submodule = queryMap.submodule || this.defaultSubmodule;

		// Default action is flow
		queryMap.action = queryMap.action || 'flow';

		// Use the API map to transform this data if necessary, eg.
		queryMap = flowApiTransformMap( queryMap );

		return queryMap;
	}

	FlowApi.prototype.getQueryMap = flowApiGetQueryMap;

	/**
	 * Using a given form, parses its action, serializes the data, and sends it as GET or POST depending on form method.
	 * With button, its name=value is serialized in. If button is an Event, it will attempt to find the clicked button.
	 * Additional params can be set with data-flow-api-params on both the clicked button or the form.
	 * @param {Event|HTMLElement} button
	 * @param {Object} queryMap
	 * @return {jQuery.Promise}
	 */
	function flowApiRequestFromForm( button, queryMap ) {
		var $button = $( button ),
			method = $button.closest( 'form' ).attr( 'method' ) || 'GET';

		// Cancel any old form request, and also trigger a new one
		return this.abortOldRequestFromNode( $button, queryMap, method );
	}

	FlowApi.prototype.requestFromForm = flowApiRequestFromForm;

	/**
	 * Using a given anchor, parses its URL and sends it as a GET (default) or POST depending on data-flow-api-method.
	 * Additional params can be set with data-flow-api-params.
	 * @param {HTMLElement} anchor
	 * @param {Object} queryMap
	 * @return {jQuery.Promise}
	 */
	function flowApiRequestFromAnchor( anchor, queryMap ) {
		var $anchor = $( anchor ),
			method = $anchor.data( 'flow-api-method' ) || 'GET';

		// Abort any old requests, and have it issue a new one via GET or POST
		return this.abortOldRequestFromNode( $anchor, queryMap, method );
	}

	FlowApi.prototype.requestFromAnchor = flowApiRequestFromAnchor;

	/**
	 * Automatically calls requestFromAnchor or requestFromForm depending on the type of node given.
	 * @param {HTMLElement} node
	 * @param {Object} queryMap
	 * @return {jQuery.Promise}
	 */
	function flowApiRequestFromNode( node, queryMap ) {
		var $node = $( node );

		if ( $node.is( 'a' ) ) {
			return this.requestFromAnchor.apply( this, arguments );
		} else if ( $node.is( 'form, input, button, textarea, select, option' ) ) {
			return this.requestFromForm.apply( this, arguments );
		} else {
			return $.Deferred().reject( 'fail-apirequest', { error: { info: 'apiRequest element is not anchor or form element' } } );
		}
	}

	FlowApi.prototype.requestFromNode = flowApiRequestFromNode;

	/**
	 * Handles aborting an old in-flight API request.
	 * If startNewMethod is given, this method also STARTS a new API call and stores it for later abortion if needed.
	 * @param {jQuery|HTMLElement} $node
	 * @param {Object} queryMap
	 * @param {string} [startNewMethod] If given: starts, stores, and returns a new API call
	 * @return {undefined|jQuery.Promise}
	 */
	function flowApiAbortOldRequestFromNode( $node, queryMap, startNewMethod ) {
		$node = $( $node );

		// transform flow_* params into (nt_*, rep_*, ...)
		queryMap = flowApiTransformMap( queryMap );

		// If this anchor already has a request in flight, abort it
		var str = 'flow-api-query-temp-' + queryMap.action + '-' + queryMap.submodule,
			prevApiCall = $node.data( str ),
			newApiCall;

		// If a previous API call was found, let's abort it
		if ( prevApiCall ) {
			$node.removeData( str );

			if ( prevApiCall.abort ) {
				prevApiCall.abort();
			}

			mw.flow.debug( '[FlowApi] apiCall abort request in flight: ' + str, arguments );
		}

		// If a method was given, we want to also issue a new API request now
		if ( startNewMethod ) {
			// Make a new request with this info
			newApiCall = this.apiCall( queryMap, startNewMethod );

			// Store this request on the node if it needs to be aborted
			$node.data(
				'flow-api-query-temp-' + queryMap.action + '-' + queryMap.submodule,
				newApiCall
			);

			// Remove the request on success
			newApiCall.always( function () {
				$node.removeData( 'flow-api-query-temp-' + queryMap.action + '-' + queryMap.submodule );
			} );

			return newApiCall;
		}
	}

	FlowApi.prototype.abortOldRequestFromNode = flowApiAbortOldRequestFromNode;

	// Export
	mw.flow.FlowApi = FlowApi;
}( mw, jQuery ) );