All Downloads are FREE. Search and download functionalities are using the official Maven repository.

META-INF.resources.screen.RequestScreen.js Maven / Gradle / Ivy

There is a newer version: 5.0.55
Show newest version
/**
 * SPDX-FileCopyrightText: (c) 2000 Liferay, Inc. https://liferay.com
 * SPDX-License-Identifier: LGPL-2.1-or-later OR LicenseRef-Liferay-DXP-EULA-2.0.0-2023-06
 */

import {fetch} from 'frontend-js-web';

import {getUrlPath} from '../util/utils';
import Screen from './Screen';

const INVALID_STATUS = 'Invalid status code';

const FAILED_TO_FETCH_MSG = 'Failed to fetch';
const LOAD_FAILED_MSG = 'Load failed';
const NETWORK_ERROR_MSG = 'NetworkError when attempting to fetch resource.';
const PREFLIGHT_ERROR_MSG = 'Preflight response is not successful';
const REQUEST_ERROR_MSG = 'Request error';
const REQUEST_TIMEOUT_MSG = 'Request timeout';
const REQUEST_PREMATURE_TERMINATION_MSG = 'Request terminated prematurely';

class RequestScreen extends Screen {

	/**
	 * Request screen abstract class to perform io operations on descendant
	 * screens.
	 */
	constructor() {
		super();

		/**
		 * @inheritDoc
		 * @default true
		 */
		this.cacheable = true;

		/**
		 * Holds default http headers to set on request.
		 * @type {?Object=}
		 * @default {
		 *   'X-PJAX': 'true',
		 *   'X-Requested-With': 'XMLHttpRequest'
		 * }
		 * @protected
		 */
		this.httpHeaders = {
			'X-PJAX': 'true',
			'X-Requested-With': 'XMLHttpRequest',
		};

		/**
		 * Holds default http method to perform the request.
		 * @type {!string}
		 * @default RequestScreen.GET
		 * @protected
		 */
		this.httpMethod = RequestScreen.GET;

		/**
		 * Holds the XHR object responsible for the request.
		 * @type {XMLHttpRequest}
		 * @default null
		 * @protected
		 */
		this.request = null;

		/**
		 * Holds the request timeout in milliseconds.
		 * @type {!number}
		 * @default 30000
		 * @protected
		 */
		this.timeout = 30000;
	}

	/**
	 * Asserts that response status code is valid.
	 * @param {number} status
	 * @protected
	 */
	assertValidResponseStatusCode(status) {
		if (!this.isValidResponseStatusCode(status)) {
			const error = new Error(INVALID_STATUS);
			error.invalidStatus = true;
			error.statusCode = status;
			throw error;
		}
	}

	/**
	 * @inheritDoc
	 */
	beforeUpdateHistoryPath(path) {
		const redirectPath = this.getRequestPath();
		if (redirectPath && redirectPath !== path) {
			return redirectPath;
		}

		return path;
	}

	/**
	 * @inheritDoc
	 */
	beforeUpdateHistoryState(state) {

		// If state is ours and navigate to post-without-redirect-get set
		// history state to null, that way Senna will reload the page on
		// popstate since it cannot predict post data.

		if (state.senna && state.form && state.redirectPath === state.path) {
			return null;
		}

		return state;
	}

	/**
	 * Formats load path before invoking ajax call.
	 * @param {string} path
	 * @return {string} Formatted path;
	 * @protected
	 */
	formatLoadPath(path) {
		const uri = new URL(path, window.location.origin);

		uri.hostname = window.location.hostname;
		uri.protocol = window.location.protocol;

		if (window.location.port) {
			uri.port = window.location.port;
		}

		return uri.toString();
	}

	/**
	 * Gets the http headers.
	 * @return {?Object=}
	 */
	getHttpHeaders() {
		return this.httpHeaders;
	}

	/**
	 * Gets the http method.
	 * @return {!string}
	 */
	getHttpMethod() {
		return this.httpMethod;
	}

	/**
	 * Gets request path.
	 * @return {string=}
	 */
	getRequestPath() {
		const request = this.getRequest();

		if (request) {
			let requestPath = request.url;

			const response = this.getResponse();

			if (response) {
				const responseUrl = response.url;

				if (responseUrl) {
					requestPath = responseUrl;
				}
			}

			return getUrlPath(requestPath);
		}

		return null;
	}

	/**
	 * Gets the request object.
	 * @return {?Object}
	 */
	getResponse() {
		return this.response;
	}

	/**
	 * Gets the request object.
	 * @return {?Object}
	 */
	getRequest() {
		return this.request;
	}

	/**
	 * Gets the request timeout.
	 * @return {!number}
	 */
	getTimeout() {
		return this.timeout;
	}

	/**
	 * Checks if response succeeded. Any status code 2xx or 3xx is considered
	 * valid.
	 * @param {number} statusCode
	 */
	isValidResponseStatusCode(statusCode) {
		return statusCode >= 200 && statusCode <= 399;
	}

	/**
	 * Returns the form data
	 * This method can be extended in order to have a custom implementation of the form params
	 * @param {!Element} formElement
	 * @param {!Element} submittedButtonElement
	 * @return {!FormData}
	 */
	getFormData(formElement, submittedButtonElement) {
		const formData = new FormData(formElement);
		this.maybeAppendSubmitButtonValue_(formData, submittedButtonElement);

		return formData;
	}

	/**
	 * @inheritDoc
	 */
	load(path) {
		const cache = this.getCache();
		if (cache) {
			return Promise.resolve(cache);
		}
		let body = null;
		let httpMethod = this.httpMethod;
		const requestHeaders = {'X-PJAX': 'true', ...this.httpHeaders};
		if (Liferay.SPA.__capturedFormElement__) {
			body = this.getFormData(
				Liferay.SPA.__capturedFormElement__,
				Liferay.SPA.__capturedFormButtonElement__
			);
			httpMethod = RequestScreen.POST;
		}

		const url = this.formatLoadPath(path);

		this.setRequest({
			method: httpMethod,
			requestBody: body,
			requestHeaders,
			url,
		});

		return Promise.race([
			fetch(url, {
				body,
				headers: requestHeaders,
				method: httpMethod,
				mode: 'cors',
				redirect: 'follow',
				referrer: 'about:client',
			})
				.then((resp) => {
					this.assertValidResponseStatusCode(resp.status);

					this.setResponse(resp);

					return resp.clone().text();
				})
				.then((text) => {
					if (
						httpMethod === RequestScreen.GET &&
						this.isCacheable()
					) {
						this.addCache(text);
					}

					return text;
				}),
			new Promise((_, reject) => {
				setTimeout(
					() => reject(new Error(REQUEST_TIMEOUT_MSG)),
					this.timeout
				);
			}),
		]).catch((reason) => {
			switch (reason.message) {
				case LOAD_FAILED_MSG:
					window.location.href = url;
					break;
				case REQUEST_TIMEOUT_MSG:
					reason.timeout = true;
					break;
				case REQUEST_PREMATURE_TERMINATION_MSG:
				case FAILED_TO_FETCH_MSG:
				case NETWORK_ERROR_MSG:
				case PREFLIGHT_ERROR_MSG:
					reason.requestError = true;
					reason.requestPrematureTermination = true;
					break;
				case REQUEST_ERROR_MSG:
				default:
					reason.requestError = true;
					break;
			}
			throw reason;
		});
	}

	/**
	 * Adds aditional data to the body of the request in case a submit button
	 * is captured during form submission.
	 * @param {!FormData} body The FormData containing the request body.
	 * @param {!Element} submittedButtonElement
	 * @protected
	 */
	maybeAppendSubmitButtonValue_(formData, submittedButtonElement) {
		if (submittedButtonElement && submittedButtonElement.name) {
			formData.append(
				submittedButtonElement.name,
				submittedButtonElement.value
			);
		}
	}

	/**
	 * Sets the http headers.
	 * @param {?Object=} httpHeaders
	 */
	setHttpHeaders(httpHeaders) {
		this.httpHeaders = httpHeaders;
	}

	/**
	 * Sets the http method.
	 * @param {!string} httpMethod
	 */
	setHttpMethod(httpMethod) {
		this.httpMethod = httpMethod.toLowerCase();
	}

	/**
	 * Sets the request object.
	 * @param {?Object} request
	 */
	setRequest(request) {
		this.request = request;
	}

	/**
	 * Sets the request object.
	 * @param {?Object} request
	 */
	setResponse(response) {
		this.response = response;
	}

	/**
	 * Sets the request timeout in milliseconds.
	 * @param {!number} timeout
	 */
	setTimeout(timeout) {
		this.timeout = timeout;
	}
}

/**
 * Holds value for method get.
 * @type {string}
 * @default 'get'
 * @static
 */
RequestScreen.GET = 'get';

/**
 * Holds value for method post.
 * @type {string}
 * @default 'post'
 * @static
 */
RequestScreen.POST = 'post';

export default RequestScreen;




© 2015 - 2024 Weber Informatics LLC | Privacy Policy