1338 lines
46 KiB
TypeScript
1338 lines
46 KiB
TypeScript
// (C) Copyright 2015 Moodle Pty Ltd.
|
|
//
|
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
// you may not use this file except in compliance with the License.
|
|
// You may obtain a copy of the License at
|
|
//
|
|
// http://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
// Unless required by applicable law or agreed to in writing, software
|
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
// See the License for the specific language governing permissions and
|
|
// limitations under the License.
|
|
|
|
import { Injectable } from '@angular/core';
|
|
import { HttpResponse, HttpParams } from '@angular/common/http';
|
|
|
|
import { FileEntry } from '@ionic-native/file/ngx';
|
|
import { FileUploadOptions } from '@ionic-native/file-transfer/ngx';
|
|
import { Md5 } from 'ts-md5/dist/md5';
|
|
import { Observable } from 'rxjs';
|
|
import { timeout } from 'rxjs/operators';
|
|
|
|
import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http';
|
|
import { CoreApp } from '@services/app';
|
|
import { CoreFile, CoreFileFormat } from '@services/file';
|
|
import { CoreMimetypeUtils } from '@services/utils/mimetype';
|
|
import { CoreTextUtils } from '@services/utils/text';
|
|
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
|
|
import { CoreConstants } from '@/core/constants';
|
|
import { CoreError } from '@classes/errors/error';
|
|
import { CoreInterceptor } from '@classes/interceptor';
|
|
import { makeSingleton, Translate, FileTransfer, Http, Platform, NativeHttp } from '@singletons';
|
|
import { CoreArray } from '@singletons/array';
|
|
import { CoreLogger } from '@singletons/logger';
|
|
import { CoreWSError } from '@classes/errors/wserror';
|
|
import { CoreAjaxError } from '@classes/errors/ajaxerror';
|
|
import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
|
|
import { CoreNetworkError } from '@classes/errors/network-error';
|
|
|
|
/**
|
|
* This service allows performing WS calls and download/upload files.
|
|
*/
|
|
@Injectable({ providedIn: 'root' })
|
|
export class CoreWSProvider {
|
|
|
|
protected logger: CoreLogger;
|
|
protected mimeTypeCache: {[url: string]: string | null} = {}; // A "cache" to store file mimetypes to decrease HEAD requests.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
protected ongoingCalls: {[queueItemId: string]: Promise<any>} = {};
|
|
protected retryCalls: RetryCall[] = [];
|
|
protected retryTimeout = 0;
|
|
|
|
constructor() {
|
|
this.logger = CoreLogger.getInstance('CoreWSProvider');
|
|
|
|
this.init();
|
|
}
|
|
|
|
/**
|
|
* Initialize some data.
|
|
*/
|
|
protected async init(): Promise<void> {
|
|
await Platform.ready();
|
|
|
|
if (CoreApp.isMobile()) {
|
|
NativeHttp.setHeader('*', 'User-Agent', navigator.userAgent);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Adds the call data to an special queue to be processed when retrying.
|
|
*
|
|
* @param method The WebService method to be called.
|
|
* @param siteUrl Complete site url to perform the call.
|
|
* @param data Arguments to pass to the method.
|
|
* @param preSets Extra settings and information.
|
|
* @return Deferred promise resolved with the response data in success and rejected with the error if it fails.
|
|
*/
|
|
protected addToRetryQueue<T = unknown>(
|
|
method: string,
|
|
siteUrl: string,
|
|
data: Record<string, unknown>,
|
|
preSets: CoreWSPreSets,
|
|
): Promise<T> {
|
|
const call = {
|
|
method,
|
|
siteUrl,
|
|
data,
|
|
preSets,
|
|
deferred: CoreUtils.promiseDefer<T>(),
|
|
};
|
|
|
|
this.retryCalls.push(call);
|
|
|
|
return call.deferred.promise;
|
|
}
|
|
|
|
/**
|
|
* A wrapper function for a moodle WebService call.
|
|
*
|
|
* @param method The WebService method to be called.
|
|
* @param data Arguments to pass to the method. It's recommended to call convertValuesToString before passing the data.
|
|
* @param preSets Extra settings and information.
|
|
* @return Promise resolved with the response data in success and rejected if it fails.
|
|
*/
|
|
call<T = unknown>(method: string, data: Record<string, unknown>, preSets: CoreWSPreSets): Promise<T> {
|
|
if (!preSets) {
|
|
throw new CoreError(Translate.instant('core.unexpectederror'));
|
|
} else if (!CoreApp.isOnline()) {
|
|
throw new CoreNetworkError();
|
|
}
|
|
|
|
preSets.typeExpected = preSets.typeExpected || 'object';
|
|
if (typeof preSets.responseExpected == 'undefined') {
|
|
preSets.responseExpected = true;
|
|
}
|
|
|
|
const dataToSend = Object.assign({}, data); // Create a new object so the changes don't affect the original data.
|
|
dataToSend['wsfunction'] = method;
|
|
dataToSend['wstoken'] = preSets.wsToken;
|
|
const siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json';
|
|
|
|
// There are some ongoing retry calls, wait for timeout.
|
|
if (this.retryCalls.length > 0) {
|
|
this.logger.warn('Calls locked, trying later...');
|
|
|
|
return this.addToRetryQueue<T>(method, siteUrl, dataToSend, preSets);
|
|
} else {
|
|
return this.performPost<T>(method, siteUrl, dataToSend, preSets);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Call a Moodle WS using the AJAX API. Please use it if the WS layer is not an option.
|
|
* It uses a cache to prevent duplicate requests.
|
|
*
|
|
* @param method The WebService method to be called.
|
|
* @param data Arguments to pass to the method.
|
|
* @param preSets Extra settings and information. Only some
|
|
* @return Promise resolved with the response data in success and rejected with CoreAjaxError.
|
|
*/
|
|
callAjax<T = unknown>(method: string, data: Record<string, unknown>, preSets: CoreWSAjaxPreSets): Promise<T> {
|
|
const cacheParams = {
|
|
methodname: method,
|
|
args: data,
|
|
};
|
|
|
|
let promise = this.getPromiseHttp<T>('ajax', preSets.siteUrl, cacheParams);
|
|
|
|
if (!promise) {
|
|
promise = this.performAjax<T>(method, data, preSets);
|
|
promise = this.setPromiseHttp<T>(promise, 'ajax', preSets.siteUrl, cacheParams);
|
|
}
|
|
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Converts an objects values to strings where appropriate.
|
|
* Arrays (associative or otherwise) will be maintained, null values will be removed.
|
|
*
|
|
* @param data The data that needs all the non-object values set to strings.
|
|
* @param stripUnicode If Unicode long chars need to be stripped.
|
|
* @return The cleaned object or null if some strings becomes empty after stripping Unicode.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
convertValuesToString(data: any, stripUnicode?: boolean): any {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const result: any = Array.isArray(data) ? [] : {};
|
|
|
|
for (const key in data) {
|
|
let value = data[key];
|
|
|
|
if (value == null) {
|
|
// Skip null or undefined value.
|
|
continue;
|
|
} else if (typeof value == 'object') {
|
|
// Object or array.
|
|
value = this.convertValuesToString(value, stripUnicode);
|
|
if (value == null) {
|
|
return null;
|
|
}
|
|
} else if (typeof value == 'string') {
|
|
if (stripUnicode) {
|
|
const stripped = CoreTextUtils.stripUnicode(value);
|
|
if (stripped != value && stripped.trim().length == 0) {
|
|
return null;
|
|
}
|
|
value = stripped;
|
|
}
|
|
} else if (typeof value == 'boolean') {
|
|
/* Moodle does not allow "true" or "false" in WS parameters, only in POST parameters.
|
|
We've been using "true" and "false" for WS settings "filter" and "fileurl",
|
|
we keep it this way to avoid changing cache keys. */
|
|
if (key == 'moodlewssettingfilter' || key == 'moodlewssettingfileurl') {
|
|
value = value ? 'true' : 'false';
|
|
} else {
|
|
value = value ? '1' : '0';
|
|
}
|
|
} else if (typeof value == 'number') {
|
|
value = String(value);
|
|
} else {
|
|
// Unknown type.
|
|
continue;
|
|
}
|
|
|
|
if (Array.isArray(result)) {
|
|
result.push(value);
|
|
} else {
|
|
result[key] = value;
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
/**
|
|
* Create a "fake" WS error for local errors.
|
|
*
|
|
* @param message The message to include in the error.
|
|
* @param needsTranslate If the message needs to be translated.
|
|
* @param translateParams Translation params, if needed.
|
|
* @return Fake WS error.
|
|
* @deprecated since 3.9.5. Just create the error directly.
|
|
*/
|
|
createFakeWSError(message: string, needsTranslate?: boolean, translateParams?: {[name: string]: string}): CoreError {
|
|
if (needsTranslate) {
|
|
message = Translate.instant(message, translateParams);
|
|
}
|
|
|
|
return new CoreError(message);
|
|
}
|
|
|
|
/**
|
|
* It will check if response has failed and throw the propper error.
|
|
*
|
|
* @param response WS response.
|
|
* @param defaultMessage Message to be used in case warnings is empty.
|
|
*/
|
|
throwOnFailedStatus(response: CoreStatusWithWarningsWSResponse, defaultMessage: string): void {
|
|
if (!response.status) {
|
|
if (response.warnings && response.warnings.length) {
|
|
throw new CoreWSError(response.warnings[0]);
|
|
}
|
|
|
|
throw new CoreError(defaultMessage);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Downloads a file from Moodle using Cordova File API.
|
|
*
|
|
* @param url Download url.
|
|
* @param path Local path to store the file.
|
|
* @param addExtension True if extension need to be added to the final path.
|
|
* @param onProgress Function to call on progress.
|
|
* @return Promise resolved with the downloaded file.
|
|
*/
|
|
async downloadFile(
|
|
url: string,
|
|
path: string,
|
|
addExtension?: boolean,
|
|
onProgress?: (event: ProgressEvent) => void,
|
|
): Promise<CoreWSDownloadedFileEntry> {
|
|
this.logger.debug('Downloading file', url, path, addExtension);
|
|
|
|
if (!CoreApp.isOnline()) {
|
|
throw new CoreNetworkError();
|
|
}
|
|
|
|
// Use a tmp path to download the file and then move it to final location.
|
|
// This is because if the download fails, the local file is deleted.
|
|
const tmpPath = path + '.tmp';
|
|
|
|
try {
|
|
// Create the tmp file as an empty file.
|
|
const fileEntry = await CoreFile.createFile(tmpPath);
|
|
|
|
const transfer = FileTransfer.create();
|
|
onProgress && transfer.onProgress(onProgress);
|
|
|
|
// Download the file in the tmp file.
|
|
await transfer.download(url, fileEntry.toURL(), true);
|
|
|
|
let extension = '';
|
|
|
|
if (addExtension) {
|
|
extension = CoreMimetypeUtils.getFileExtension(path) || '';
|
|
|
|
// Google Drive extensions will be considered invalid since Moodle usually converts them.
|
|
if (!extension || CoreArray.contains(['gdoc', 'gsheet', 'gslides', 'gdraw', 'php'], extension)) {
|
|
// Not valid, get the file's mimetype.
|
|
const mimetype = await this.getRemoteFileMimeType(url);
|
|
|
|
if (mimetype) {
|
|
const remoteExtension = CoreMimetypeUtils.getExtension(mimetype, url);
|
|
// If the file is from Google Drive, ignore mimetype application/json.
|
|
if (remoteExtension && (!extension || mimetype != 'application/json')) {
|
|
if (extension) {
|
|
// Remove existing extension since we will use another one.
|
|
path = CoreMimetypeUtils.removeExtension(path);
|
|
}
|
|
path += '.' + remoteExtension;
|
|
|
|
extension = remoteExtension;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Move the file to the final location.
|
|
const movedEntry = await CoreFile.moveFile(tmpPath, path);
|
|
|
|
this.logger.debug(`Success downloading file ${url} to ${path} with extension ${extension}`);
|
|
|
|
// Also return the extension and path.
|
|
return <CoreWSDownloadedFileEntry> Object.assign(movedEntry, {
|
|
extension: extension,
|
|
path: path,
|
|
});
|
|
} catch (error) {
|
|
this.logger.error(`Error downloading ${url} to ${path}`, error);
|
|
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a promise from the cache.
|
|
*
|
|
* @param method Method of the HTTP request.
|
|
* @param url Base URL of the HTTP request.
|
|
* @param params Params of the HTTP request.
|
|
*/
|
|
protected getPromiseHttp<T = unknown>(method: string, url: string, params?: Record<string, unknown>): Promise<T> | undefined {
|
|
const queueItemId = this.getQueueItemId(method, url, params);
|
|
if (typeof this.ongoingCalls[queueItemId] != 'undefined') {
|
|
return this.ongoingCalls[queueItemId];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Perform a HEAD request to get the mimetype of a remote file.
|
|
*
|
|
* @param url File URL.
|
|
* @param ignoreCache True to ignore cache, false otherwise.
|
|
* @return Promise resolved with the mimetype or '' if failure.
|
|
*/
|
|
async getRemoteFileMimeType(url: string, ignoreCache?: boolean): Promise<string> {
|
|
if (this.mimeTypeCache[url] && !ignoreCache) {
|
|
return this.mimeTypeCache[url]!;
|
|
}
|
|
|
|
try {
|
|
const response = await this.performHead(url);
|
|
|
|
let mimeType = response.headers.get('Content-Type');
|
|
if (mimeType) {
|
|
// Remove "parameters" like charset.
|
|
mimeType = mimeType.split(';')[0];
|
|
}
|
|
this.mimeTypeCache[url] = mimeType;
|
|
|
|
return mimeType || '';
|
|
} catch (error) {
|
|
// Error, resolve with empty mimetype.
|
|
return '';
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Perform a HEAD request to get the size of a remote file.
|
|
*
|
|
* @param url File URL.
|
|
* @return Promise resolved with the size or -1 if failure.
|
|
*/
|
|
getRemoteFileSize(url: string): Promise<number> {
|
|
return this.performHead(url).then((response) => {
|
|
const contentLength = response.headers.get('Content-Length');
|
|
const size = contentLength ? parseInt(contentLength, 10) : 0;
|
|
|
|
if (size) {
|
|
return size;
|
|
}
|
|
|
|
return -1;
|
|
}).catch(() => -1);
|
|
}
|
|
|
|
/**
|
|
* Get a request timeout based on the network connection.
|
|
*
|
|
* @return Timeout in ms.
|
|
*/
|
|
getRequestTimeout(): number {
|
|
return CoreApp.isNetworkAccessLimited() ? CoreConstants.WS_TIMEOUT : CoreConstants.WS_TIMEOUT_WIFI;
|
|
}
|
|
|
|
/**
|
|
* Get the unique queue item id of the cache for a HTTP request.
|
|
*
|
|
* @param method Method of the HTTP request.
|
|
* @param url Base URL of the HTTP request.
|
|
* @param params Params of the HTTP request.
|
|
* @return Queue item ID.
|
|
*/
|
|
protected getQueueItemId(method: string, url: string, params?: Record<string, unknown>): string {
|
|
if (params) {
|
|
url += '###' + CoreInterceptor.serialize(params);
|
|
}
|
|
|
|
return method + '#' + Md5.hashAsciiStr(url);
|
|
}
|
|
|
|
/**
|
|
* Call a Moodle WS using the AJAX API.
|
|
*
|
|
* @param method The WebService method to be called.
|
|
* @param data Arguments to pass to the method.
|
|
* @param preSets Extra settings and information. Only some
|
|
* @return Promise resolved with the response data in success and rejected with CoreAjaxError.
|
|
*/
|
|
protected performAjax<T = unknown>(method: string, data: Record<string, unknown>, preSets: CoreWSAjaxPreSets): Promise<T> {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let promise: Promise<HttpResponse<any>>;
|
|
|
|
if (typeof preSets.siteUrl == 'undefined') {
|
|
throw new CoreAjaxError(Translate.instant('core.unexpectederror'));
|
|
} else if (!CoreApp.isOnline()) {
|
|
throw new CoreAjaxError(Translate.instant('core.networkerrormsg'));
|
|
}
|
|
|
|
if (typeof preSets.responseExpected == 'undefined') {
|
|
preSets.responseExpected = true;
|
|
}
|
|
|
|
const script = preSets.noLogin ? 'service-nologin.php' : 'service.php';
|
|
const ajaxData = [{
|
|
index: 0,
|
|
methodname: method,
|
|
args: this.convertValuesToString(data),
|
|
}];
|
|
|
|
// The info= parameter has no function. It is just to help with debugging.
|
|
// We call it info to match the parameter name use by Moodle's AMD ajax module.
|
|
let siteUrl = preSets.siteUrl + '/lib/ajax/' + script + '?info=' + method;
|
|
|
|
if (preSets.noLogin && preSets.useGet) {
|
|
// Send params using GET.
|
|
siteUrl += '&args=' + encodeURIComponent(JSON.stringify(ajaxData));
|
|
|
|
promise = this.sendHTTPRequest<T>(siteUrl, {
|
|
method: 'get',
|
|
});
|
|
} else {
|
|
promise = this.sendHTTPRequest<T>(siteUrl, {
|
|
method: 'post',
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
data: <any> ajaxData,
|
|
serializer: 'json',
|
|
});
|
|
}
|
|
|
|
return promise.then((response) => {
|
|
let data = response.body;
|
|
|
|
// Some moodle web services return null.
|
|
// If the responseExpected value is set then so long as no data is returned, we create a blank object.
|
|
if (!data && !preSets.responseExpected) {
|
|
data = [{}];
|
|
}
|
|
|
|
// Check if error. Ajax layer should always return an object (if error) or an array (if success).
|
|
if (!data || typeof data != 'object') {
|
|
throw new CoreAjaxError(Translate.instant('core.serverconnection'));
|
|
} else if (data.error) {
|
|
throw new CoreAjaxWSError(data);
|
|
}
|
|
|
|
// Get the first response since only one request was done.
|
|
data = data[0];
|
|
|
|
if (data.error) {
|
|
throw new CoreAjaxWSError(data.exception);
|
|
}
|
|
|
|
return data.data;
|
|
}, (data) => {
|
|
const available = data.status == 404 ? -1 : 0;
|
|
|
|
throw new CoreAjaxError(Translate.instant('core.serverconnection'), available);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Perform a HEAD request and save the promise while waiting to be resolved.
|
|
*
|
|
* @param url URL to perform the request.
|
|
* @return Promise resolved with the response.
|
|
*/
|
|
performHead<T = unknown>(url: string): Promise<HttpResponse<T>> {
|
|
let promise = this.getPromiseHttp<HttpResponse<T>>('head', url);
|
|
|
|
if (!promise) {
|
|
promise = this.sendHTTPRequest<T>(url, {
|
|
method: 'head',
|
|
responseType: 'text',
|
|
});
|
|
|
|
promise = this.setPromiseHttp<HttpResponse<T>>(promise, 'head', url);
|
|
}
|
|
|
|
return promise;
|
|
}
|
|
|
|
/**
|
|
* Perform the post call. It can be split into several requests.
|
|
*
|
|
* @param method The WebService method to be called.
|
|
* @param siteUrl Complete site url to perform the call.
|
|
* @param ajaxData Arguments to pass to the method.
|
|
* @param preSets Extra settings and information.
|
|
* @return Promise resolved with the response data in success and rejected with CoreWSError if it fails.
|
|
*/
|
|
async performPost<T = unknown>(
|
|
method: string,
|
|
siteUrl: string,
|
|
ajaxData: Record<string, unknown>,
|
|
preSets: CoreWSPreSets,
|
|
): Promise<T> {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const options: any = {};
|
|
|
|
// This is done because some returned values like 0 are treated as null if responseType is json.
|
|
if (preSets.typeExpected == 'number' || preSets.typeExpected == 'boolean' || preSets.typeExpected == 'string') {
|
|
options.responseType = 'text';
|
|
}
|
|
|
|
if (!preSets.splitRequest || !ajaxData[preSets.splitRequest.param]) {
|
|
return this.performSinglePost(method, siteUrl, ajaxData, preSets, options);
|
|
}
|
|
|
|
// Split the request into several requests if needed.
|
|
const promises: Promise<T>[] = [];
|
|
const splitParam = <unknown[]> ajaxData[preSets.splitRequest.param];
|
|
|
|
for (let i = 0; i < splitParam.length; i += preSets.splitRequest.maxLength) {
|
|
// Limit the array sent.
|
|
const limitedData = Object.assign({}, ajaxData);
|
|
limitedData[preSets.splitRequest.param] = splitParam.slice(i, i + preSets.splitRequest.maxLength);
|
|
|
|
promises.push(this.performSinglePost(method, siteUrl, limitedData, preSets, options));
|
|
}
|
|
|
|
const results = await Promise.all(promises);
|
|
|
|
// Combine the results.
|
|
const firstResult = results.shift();
|
|
|
|
if (preSets.splitRequest.combineCallback) {
|
|
return <T> results.reduce(preSets.splitRequest.combineCallback, firstResult);
|
|
}
|
|
|
|
return <T> results.reduce((previous: T, current: T) => this.combineObjectsArrays<T>(previous, current), firstResult);
|
|
}
|
|
|
|
/**
|
|
* Combine the arrays of two objects, adding them to the first object.
|
|
*
|
|
* @param object1 First object.
|
|
* @param object2 Second object.
|
|
* @return First object with items added.
|
|
*/
|
|
protected combineObjectsArrays<T>(object1: T, object2: T): T {
|
|
for (const name in object2) {
|
|
const value = object2[name];
|
|
|
|
if (Array.isArray(value)) {
|
|
(object1 as Record<string, unknown>)[name] = (object1[name] as typeof value).concat(value);
|
|
}
|
|
}
|
|
|
|
return object1;
|
|
}
|
|
|
|
/**
|
|
* Perform a single post request.
|
|
*
|
|
* @param method The WebService method to be called.
|
|
* @param siteUrl Complete site url to perform the call.
|
|
* @param ajaxData Arguments to pass to the method.
|
|
* @param preSets Extra settings and information.
|
|
* @param options Request options.
|
|
* @return Promise resolved with the response data in success and rejected with CoreWSError if it fails.
|
|
*/
|
|
protected performSinglePost<T>(
|
|
method: string,
|
|
siteUrl: string,
|
|
ajaxData: Record<string, unknown>,
|
|
preSets: CoreWSPreSets,
|
|
options: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
): Promise<T> {
|
|
|
|
// We add the method name to the URL purely to help with debugging.
|
|
// This duplicates what is in the ajaxData, but that does no harm.
|
|
// POST variables take precedence over GET.
|
|
const requestUrl = siteUrl + '&wsfunction=' + method;
|
|
|
|
// Perform the post request.
|
|
const promise = Http.post(requestUrl, ajaxData, options).pipe(timeout(this.getRequestTimeout())).toPromise();
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
return promise.then((data: any) => {
|
|
// Some moodle web services return null.
|
|
// If the responseExpected value is set to false, we create a blank object if the response is null.
|
|
if (!data && !preSets.responseExpected) {
|
|
data = {};
|
|
}
|
|
|
|
if (!data) {
|
|
throw new CoreError(Translate.instant('core.serverconnection'));
|
|
} else if (typeof data != preSets.typeExpected) {
|
|
// If responseType is text an string will be returned, parse before returning.
|
|
if (typeof data == 'string') {
|
|
if (preSets.typeExpected == 'number') {
|
|
data = Number(data);
|
|
if (isNaN(data)) {
|
|
this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`);
|
|
|
|
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
|
|
}
|
|
} else if (preSets.typeExpected == 'boolean') {
|
|
if (data === 'true') {
|
|
data = true;
|
|
} else if (data === 'false') {
|
|
data = false;
|
|
} else {
|
|
this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`);
|
|
|
|
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
|
|
}
|
|
} else {
|
|
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
|
|
|
|
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
|
|
}
|
|
} else {
|
|
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
|
|
|
|
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
|
|
}
|
|
}
|
|
|
|
if (typeof data.exception !== 'undefined') {
|
|
// Special debugging for site plugins, otherwise it's hard to debug errors if the data is cached.
|
|
if (method == 'tool_mobile_get_content') {
|
|
this.logger.error('Error calling WS', method, data);
|
|
}
|
|
|
|
throw new CoreWSError(data);
|
|
}
|
|
|
|
if (typeof data.debuginfo != 'undefined') {
|
|
throw new CoreError('Error. ' + data.message);
|
|
}
|
|
|
|
return data;
|
|
}, (error) => {
|
|
// If server has heavy load, retry after some seconds.
|
|
if (error.status == 429) {
|
|
const retryPromise = this.addToRetryQueue<T>(method, siteUrl, ajaxData, preSets);
|
|
|
|
// Only process the queue one time.
|
|
if (this.retryTimeout == 0) {
|
|
this.retryTimeout = parseInt(error.headers.get('Retry-After'), 10) || 5;
|
|
this.logger.warn(`${error.statusText}. Retrying in ${this.retryTimeout} seconds. ` +
|
|
`${this.retryCalls.length} calls left.`);
|
|
|
|
setTimeout(() => {
|
|
this.logger.warn(`Retrying now with ${this.retryCalls.length} calls to process.`);
|
|
// Finish timeout.
|
|
this.retryTimeout = 0;
|
|
this.processRetryQueue();
|
|
}, this.retryTimeout * 1000);
|
|
} else {
|
|
this.logger.warn('Calls locked, trying later...');
|
|
}
|
|
|
|
return retryPromise;
|
|
}
|
|
|
|
throw new CoreError(Translate.instant('core.serverconnection'));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Retry all requests in the queue.
|
|
* This function uses recursion in order to add a delay between requests to reduce stress.
|
|
*/
|
|
protected processRetryQueue(): void {
|
|
if (this.retryCalls.length > 0 && this.retryTimeout == 0) {
|
|
const call = this.retryCalls.shift();
|
|
// Add a delay between calls.
|
|
setTimeout(() => {
|
|
call!.deferred.resolve(this.performPost(call!.method, call!.siteUrl, call!.data, call!.preSets));
|
|
this.processRetryQueue();
|
|
}, 200);
|
|
} else {
|
|
this.logger.warn(`Retry queue has stopped with ${this.retryCalls.length} calls and ${this.retryTimeout} timeout secs.`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Save promise on the cache.
|
|
*
|
|
* @param promise Promise to be saved.
|
|
* @param method Method of the HTTP request.
|
|
* @param url Base URL of the HTTP request.
|
|
* @param params Params of the HTTP request.
|
|
* @return The promise saved.
|
|
*/
|
|
protected setPromiseHttp<T = unknown>(
|
|
promise: Promise<T>,
|
|
method: string,
|
|
url: string,
|
|
params?: Record<string, unknown>,
|
|
): Promise<T> {
|
|
const queueItemId = this.getQueueItemId(method, url, params);
|
|
|
|
this.ongoingCalls[queueItemId] = promise;
|
|
|
|
// HTTP not finished, but we should delete the promise after timeout.
|
|
const timeout = setTimeout(() => {
|
|
delete this.ongoingCalls[queueItemId];
|
|
}, this.getRequestTimeout());
|
|
|
|
// HTTP finished, delete from ongoing.
|
|
return promise.finally(() => {
|
|
delete this.ongoingCalls[queueItemId];
|
|
|
|
clearTimeout(timeout);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* A wrapper function for a synchronous Moodle WebService call.
|
|
* Warning: This function should only be used if synchronous is a must. It's recommended to use call.
|
|
*
|
|
* @param method The WebService method to be called.
|
|
* @param data Arguments to pass to the method.
|
|
* @param preSets Extra settings and information.
|
|
* @return Promise resolved with the response data in success and rejected with the error message if it fails.
|
|
* @return Request response.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
syncCall<T = unknown>(method: string, data: any, preSets: CoreWSPreSets): T {
|
|
if (!preSets) {
|
|
throw new CoreError(Translate.instant('core.unexpectederror'));
|
|
} else if (!CoreApp.isOnline()) {
|
|
throw new CoreNetworkError();
|
|
}
|
|
|
|
preSets.typeExpected = preSets.typeExpected || 'object';
|
|
if (typeof preSets.responseExpected == 'undefined') {
|
|
preSets.responseExpected = true;
|
|
}
|
|
|
|
data = this.convertValuesToString(data || {}, preSets.cleanUnicode);
|
|
if (data == null) {
|
|
// Empty cleaned text found.
|
|
throw new CoreError(Translate.instant('core.unicodenotsupportedcleanerror'));
|
|
}
|
|
|
|
data.wsfunction = method;
|
|
data.wstoken = preSets.wsToken;
|
|
const siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json';
|
|
|
|
// Serialize data.
|
|
data = CoreInterceptor.serialize(data);
|
|
|
|
// Perform sync request using XMLHttpRequest.
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open('post', siteUrl, false);
|
|
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
|
|
|
|
xhr.send(data);
|
|
|
|
// Get response.
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
data = ('response' in xhr) ? xhr.response : (<any> xhr).responseText;
|
|
|
|
// Check status.
|
|
const status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0);
|
|
if (status < 200 || status >= 300) {
|
|
// Request failed.
|
|
throw new CoreError(data);
|
|
}
|
|
|
|
// Treat response.
|
|
data = CoreTextUtils.parseJSON(data);
|
|
|
|
// Some moodle web services return null.
|
|
// If the responseExpected value is set then so long as no data is returned, we create a blank object.
|
|
if ((!data || !data.data) && !preSets.responseExpected) {
|
|
data = {};
|
|
}
|
|
|
|
if (!data) {
|
|
throw new CoreError(Translate.instant('core.serverconnection'));
|
|
} else if (typeof data != preSets.typeExpected) {
|
|
this.logger.warn('Response of type "' + typeof data + '" received, expecting "' + preSets.typeExpected + '"');
|
|
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
|
|
}
|
|
|
|
if (typeof data.exception != 'undefined' || typeof data.debuginfo != 'undefined') {
|
|
throw new CoreWSError(data);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
/*
|
|
* Uploads a file.
|
|
*
|
|
* @param filePath File path.
|
|
* @param options File upload options.
|
|
* @param preSets Must contain siteUrl and wsToken.
|
|
* @param onProgress Function to call on progress.
|
|
* @return Promise resolved when uploaded.
|
|
*/
|
|
async uploadFile(
|
|
filePath: string,
|
|
options: CoreWSFileUploadOptions,
|
|
preSets: CoreWSPreSets,
|
|
onProgress?: (event: ProgressEvent) => void,
|
|
): Promise<CoreWSUploadFileResult> {
|
|
this.logger.debug(`Trying to upload file: ${filePath}`);
|
|
|
|
if (!filePath || !options || !preSets) {
|
|
throw new CoreError('Invalid options passed to upload file.');
|
|
}
|
|
|
|
if (!CoreApp.isOnline()) {
|
|
throw new CoreNetworkError();
|
|
}
|
|
|
|
const uploadUrl = preSets.siteUrl + '/webservice/upload.php';
|
|
const transfer = FileTransfer.create();
|
|
|
|
onProgress && transfer.onProgress(onProgress);
|
|
|
|
options.httpMethod = 'POST';
|
|
options.params = {
|
|
token: preSets.wsToken,
|
|
filearea: options.fileArea || 'draft',
|
|
itemid: options.itemId || 0,
|
|
};
|
|
options.chunkedMode = false;
|
|
options.headers = {};
|
|
options['Connection'] = 'close';
|
|
|
|
try {
|
|
const success = await transfer.upload(filePath, uploadUrl, options, true);
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
const data = CoreTextUtils.parseJSON<any>(
|
|
success.response,
|
|
null,
|
|
this.logger.error.bind(this.logger, 'Error parsing response from upload', success.response),
|
|
);
|
|
|
|
if (data === null) {
|
|
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
|
|
}
|
|
|
|
if (!data) {
|
|
throw new CoreError(Translate.instant('core.serverconnection'));
|
|
} else if (typeof data != 'object') {
|
|
this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"');
|
|
|
|
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
|
|
}
|
|
|
|
if (typeof data.exception !== 'undefined') {
|
|
throw new CoreWSError(data);
|
|
} else if (typeof data.error !== 'undefined') {
|
|
throw new CoreWSError({
|
|
errorcode: data.errortype,
|
|
message: data.error,
|
|
});
|
|
} else if (data[0] && typeof data[0].error !== 'undefined') {
|
|
throw new CoreWSError({
|
|
errorcode: data[0].errortype,
|
|
message: data[0].error,
|
|
});
|
|
}
|
|
|
|
// We uploaded only 1 file, so we only return the first file returned.
|
|
this.logger.debug('Successfully uploaded file', filePath);
|
|
|
|
return data[0];
|
|
} catch (error) {
|
|
this.logger.error('Error while uploading file', filePath, error);
|
|
|
|
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Perform an HTTP request requesting for a text response.
|
|
*
|
|
* @param url Url to get.
|
|
* @return Resolved with the text when done.
|
|
*/
|
|
async getText(url: string): Promise<string> {
|
|
// Fetch the URL content.
|
|
const options: HttpRequestOptions = {
|
|
method: 'get',
|
|
responseType: 'text',
|
|
};
|
|
|
|
const response = await this.sendHTTPRequest<string>(url, options);
|
|
|
|
const content = response.body;
|
|
|
|
if (typeof content !== 'string') {
|
|
throw new Error('Error reading content');
|
|
}
|
|
|
|
return content;
|
|
}
|
|
|
|
/**
|
|
* Send an HTTP request. In mobile devices it will use the cordova plugin.
|
|
*
|
|
* @param url URL of the request.
|
|
* @param options Options for the request.
|
|
* @return Promise resolved with the response.
|
|
*/
|
|
async sendHTTPRequest<T = unknown>(url: string, options: HttpRequestOptions): Promise<HttpResponse<T>> {
|
|
// Set default values.
|
|
options.responseType = options.responseType || 'json';
|
|
options.timeout = typeof options.timeout == 'undefined' ? this.getRequestTimeout() : options.timeout;
|
|
|
|
if (CoreApp.isMobile()) {
|
|
// Use the cordova plugin.
|
|
if (url.indexOf('file://') === 0) {
|
|
// We cannot load local files using the http native plugin. Use file provider instead.
|
|
const content = options.responseType == 'json' ?
|
|
await CoreFile.readFile<T>(url, CoreFileFormat.FORMATJSON) :
|
|
await CoreFile.readFile(url, CoreFileFormat.FORMATTEXT);
|
|
|
|
return new HttpResponse<T>({
|
|
body: <T> content,
|
|
headers: undefined,
|
|
status: 200,
|
|
statusText: 'OK',
|
|
url,
|
|
});
|
|
}
|
|
|
|
return NativeHttp.sendRequest(url, options).then((response) => new CoreNativeToAngularHttpResponse(response));
|
|
} else {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
let observable: Observable<HttpResponse<any>>;
|
|
const angularOptions = <AngularHttpRequestOptions> options;
|
|
|
|
// Use Angular's library.
|
|
switch (angularOptions.method) {
|
|
case 'get':
|
|
observable = Http.get(url, {
|
|
headers: angularOptions.headers,
|
|
params: angularOptions.params,
|
|
observe: 'response',
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
responseType: <any> angularOptions.responseType,
|
|
});
|
|
break;
|
|
|
|
case 'post':
|
|
if (angularOptions.serializer == 'json') {
|
|
angularOptions.data = JSON.stringify(angularOptions.data);
|
|
}
|
|
|
|
observable = Http.post(url, angularOptions.data, {
|
|
headers: angularOptions.headers,
|
|
observe: 'response',
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
responseType: <any> angularOptions.responseType,
|
|
});
|
|
break;
|
|
|
|
case 'head':
|
|
observable = Http.head(url, {
|
|
headers: angularOptions.headers,
|
|
observe: 'response',
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
responseType: <any> angularOptions.responseType,
|
|
});
|
|
break;
|
|
|
|
default:
|
|
throw new CoreError('Method not implemented yet.');
|
|
}
|
|
|
|
if (angularOptions.timeout) {
|
|
observable = observable.pipe(timeout(angularOptions.timeout));
|
|
}
|
|
|
|
return observable.toPromise();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if a URL works (it returns a 2XX status).
|
|
*
|
|
* @param url URL to check.
|
|
* @return Promise resolved with boolean: whether it works.
|
|
*/
|
|
async urlWorks(url: string): Promise<boolean> {
|
|
try {
|
|
const result = await this.performHead(url);
|
|
|
|
return result.status >= 200 && result.status < 300;
|
|
} catch (error) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
export const CoreWS = makeSingleton(CoreWSProvider);
|
|
|
|
/**
|
|
* File upload options.
|
|
*/
|
|
export interface CoreWSFileUploadOptions extends FileUploadOptions {
|
|
/**
|
|
* The file area where to put the file. By default, 'draft'.
|
|
*/
|
|
fileArea?: string;
|
|
|
|
/**
|
|
* Item ID of the area where to put the file. By default, 0.
|
|
*/
|
|
itemId?: number;
|
|
}
|
|
|
|
/**
|
|
* Structure of warnings returned by WS.
|
|
*/
|
|
export type CoreWSExternalWarning = {
|
|
/**
|
|
* Item.
|
|
*/
|
|
item?: string;
|
|
|
|
/**
|
|
* Item id.
|
|
*/
|
|
itemid?: number;
|
|
|
|
/**
|
|
* The warning code can be used by the client app to implement specific behaviour.
|
|
*/
|
|
warningcode: string;
|
|
|
|
/**
|
|
* Untranslated english message to explain the warning.
|
|
*/
|
|
message: string;
|
|
|
|
};
|
|
|
|
/**
|
|
* Special response structure of many webservices that contains success status and warnings.
|
|
*/
|
|
export type CoreStatusWithWarningsWSResponse = {
|
|
status: boolean; // Status: true if success.
|
|
offline?: boolean; // True if information has been stored in offline for future use.
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Special response structure of many webservices that contains only warnings.
|
|
*/
|
|
export type CoreWarningsWSResponse = {
|
|
warnings?: CoreWSExternalWarning[];
|
|
};
|
|
|
|
/**
|
|
* Structure of files returned by WS.
|
|
*/
|
|
export type CoreWSExternalFile = {
|
|
fileurl: string; // Downloadable file url.
|
|
filename?: string; // File name.
|
|
filepath?: string; // File path.
|
|
filesize?: number; // File size.
|
|
timemodified?: number; // Time modified.
|
|
mimetype?: string; // File mime type.
|
|
isexternalfile?: number; // Whether is an external file.
|
|
repositorytype?: string; // The repository type for external files.
|
|
};
|
|
|
|
/**
|
|
* Structure of files returned by stored_file_exporter.
|
|
*/
|
|
export type CoreWSStoredFile = {
|
|
contextid: number; // Contextid.
|
|
component: string; // Component.
|
|
filearea: string; // Filearea.
|
|
itemid: number; // Itemid.
|
|
filepath: string; // Filepath.
|
|
filename: string; // Filename.
|
|
isdir: boolean; // Isdir.
|
|
isimage: boolean; // Isimage.
|
|
timemodified: number; // Timemodified.
|
|
timecreated: number; // Timecreated.
|
|
filesize: number; // Filesize.
|
|
author: string; // Author.
|
|
license: string; // License.
|
|
filenameshort: string; // Filenameshort.
|
|
filesizeformatted: string; // Filesizeformatted.
|
|
icon: string; // Icon.
|
|
timecreatedformatted: string; // Timecreatedformatted.
|
|
timemodifiedformatted: string; // Timemodifiedformatted.
|
|
url: string; // Url.
|
|
urls: {
|
|
export?: string; // The URL used to export the attachment.
|
|
};
|
|
html: {
|
|
plagiarism?: string; // The HTML source for the Plagiarism Response.
|
|
};
|
|
mimetype: undefined; // File mimetype. @todo Not implemented yet in Moodle, see MDL-71354.
|
|
};
|
|
|
|
/**
|
|
* Common file structures returned by WS.
|
|
*/
|
|
export type CoreWSFile = CoreWSExternalFile | CoreWSStoredFile;
|
|
|
|
/**
|
|
* Data returned by date_exporter.
|
|
*/
|
|
export type CoreWSDate = {
|
|
seconds: number; // Seconds.
|
|
minutes: number; // Minutes.
|
|
hours: number; // Hours.
|
|
mday: number; // Mday.
|
|
wday: number; // Wday.
|
|
mon: number; // Mon.
|
|
year: number; // Year.
|
|
yday: number; // Yday.
|
|
weekday: string; // Weekday.
|
|
month: string; // Month.
|
|
timestamp: number; // Timestamp.
|
|
};
|
|
|
|
/**
|
|
* PreSets accepted by the WS call.
|
|
*/
|
|
export type CoreWSPreSets = {
|
|
/**
|
|
* The site URL.
|
|
*/
|
|
siteUrl: string;
|
|
|
|
/**
|
|
* The Webservice token.
|
|
*/
|
|
wsToken: string;
|
|
|
|
/**
|
|
* Defaults to true. Set to false when the expected response is null.
|
|
*/
|
|
responseExpected?: boolean;
|
|
|
|
/**
|
|
* Defaults to 'object'. Use it when you expect a type that's not an object|array.
|
|
*/
|
|
typeExpected?: string;
|
|
|
|
/**
|
|
* Defaults to false. Clean multibyte Unicode chars from data.
|
|
*/
|
|
cleanUnicode?: boolean;
|
|
|
|
/**
|
|
* Whether to split a request if it has too many parameters. Sending too many parameters to the site
|
|
* can cause the request to fail (see PHP's max_input_vars).
|
|
*/
|
|
splitRequest?: CoreWSPreSetsSplitRequest;
|
|
};
|
|
|
|
/**
|
|
* Options to split a request.
|
|
*/
|
|
export type CoreWSPreSetsSplitRequest = {
|
|
/**
|
|
* Name of the parameter used to split the request if too big. Must be an array parameter.
|
|
*/
|
|
param: string;
|
|
|
|
/**
|
|
* Max number of entries sent per request.
|
|
*/
|
|
maxLength: number;
|
|
|
|
/**
|
|
* Callback to combine the results. If not supplied, arrays in the result will be concatenated.
|
|
*/
|
|
combineCallback?: (previousValue: unknown, currentValue: unknown, currentIndex: number, array: unknown[]) => unknown;
|
|
};
|
|
|
|
/**
|
|
* PreSets accepted by AJAX WS calls.
|
|
*/
|
|
export type CoreWSAjaxPreSets = {
|
|
/**
|
|
* The site URL.
|
|
*/
|
|
siteUrl: string;
|
|
|
|
/**
|
|
* Defaults to true. Set to false when the expected response is null.
|
|
*/
|
|
responseExpected?: boolean;
|
|
|
|
/**
|
|
* Whether to use the no-login endpoint instead of the normal one. Use it for requests that don't require authentication.
|
|
*/
|
|
noLogin?: boolean;
|
|
|
|
/**
|
|
* Whether to send the parameters via GET. Only if noLogin is true.
|
|
*/
|
|
useGet?: boolean;
|
|
};
|
|
|
|
/**
|
|
* Options for HTTP requests.
|
|
*/
|
|
export type HttpRequestOptions = {
|
|
/**
|
|
* The HTTP method.
|
|
*/
|
|
method: 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' | 'options' | 'upload' | 'download';
|
|
|
|
/**
|
|
* Payload to send to the server. Only applicable on post, put or patch methods.
|
|
*/
|
|
data?: Record<string, unknown>;
|
|
|
|
/**
|
|
* Query params to be appended to the URL (only applicable on get, head, delete, upload or download methods).
|
|
*/
|
|
params?: Record<string, string | number>;
|
|
|
|
/**
|
|
* Response type. Defaults to json.
|
|
*/
|
|
responseType?: 'json' | 'text' | 'arraybuffer' | 'blob';
|
|
|
|
/**
|
|
* Timeout for the request in seconds. If undefined, the default value will be used. If null, no timeout.
|
|
*/
|
|
timeout?: number;
|
|
|
|
/**
|
|
* Serializer to use. Defaults to 'urlencoded'. Only for mobile environments.
|
|
*/
|
|
serializer?: 'json' | 'urlencoded' | 'utf8' | 'multipart';
|
|
|
|
/**
|
|
* Whether to follow redirects. Defaults to true. Only for mobile environments.
|
|
*/
|
|
followRedirect?: boolean;
|
|
|
|
/**
|
|
* Headers. Only for mobile environments.
|
|
*/
|
|
headers?: Record<string, string>;
|
|
|
|
/**
|
|
* File paths to use for upload or download. Only for mobile environments.
|
|
*/
|
|
filePath?: string | string[];
|
|
|
|
/**
|
|
* Name to use during upload. Only for mobile environments.
|
|
*/
|
|
name?: string | string[];
|
|
};
|
|
|
|
/**
|
|
* Options for JSON HTTP requests using Angular Http.
|
|
*/
|
|
type AngularHttpRequestOptions = Omit<HttpRequestOptions, 'data'|'params'> & {
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
data?: Record<string, any> | string;
|
|
params?: HttpParams | {
|
|
[param: string]: string | string[];
|
|
};
|
|
};
|
|
|
|
/**
|
|
* Data needed to retry a WS call.
|
|
*/
|
|
type RetryCall = {
|
|
method: string;
|
|
siteUrl: string;
|
|
data: Record<string, unknown>;
|
|
preSets: CoreWSPreSets;
|
|
deferred: PromiseDefer<unknown>;
|
|
};
|
|
|
|
/**
|
|
* Downloaded file entry. It includes some calculated data.
|
|
*/
|
|
export type CoreWSDownloadedFileEntry = FileEntry & {
|
|
extension: string; // File extension.
|
|
path: string; // File path.
|
|
};
|
|
|
|
export type CoreWSUploadFileResult = {
|
|
component: string; // Component the file was uploaded to.
|
|
context: string; // Context the file was uploaded to.
|
|
userid: number; // User that uploaded the file.
|
|
filearea: string; // File area the file was uploaded to.
|
|
filename: string; // File name.
|
|
filepath: string; // File path.
|
|
itemid: number; // Item ID the file was uploaded to.
|
|
license: string; // File license.
|
|
author: string; // Author name.
|
|
source: string; // File source.
|
|
};
|