MOBILE-3101 core: Install and use cordova-plugin-advanced-http

main
Dani Palou 2020-03-26 15:09:42 +01:00
parent 47decde520
commit 8b9635e675
6 changed files with 340 additions and 62 deletions

View File

@ -182,6 +182,7 @@
<variable name="FCM_VERSION" value="17.5.+" />
</plugin>
<plugin name="cordova-plugin-ionic-webview" spec="4.1.3" />
<plugin name="cordova-plugin-advanced-http" spec="2.4.1" />
<edit-config file="AndroidManifest.xml" mode="merge" target="/manifest/application/activity[@android:name='MainActivity']">
<activity android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|screenLayout|smallestScreenSize" android:debuggable="true" />
</edit-config>

10
package-lock.json generated
View File

@ -147,6 +147,11 @@
"resolved": "https://registry.npmjs.org/@ionic-native/globalization/-/globalization-4.20.0.tgz",
"integrity": "sha512-zyxaW+vZb1OHeDgGbrZHQe3hy30K4YeKjGr8KNGcwq+k2ZHkfqo/H6XIwf2m/UlFTgacvdR9XZtfP+6N0suybg=="
},
"@ionic-native/http": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@ionic-native/http/-/http-4.20.0.tgz",
"integrity": "sha512-DF+Y1oYoHTv9Y22a2jLgniOmj9Twba+9j8rzHA4xboVT2HpB6bsBSWOktdAXDVjoajXiLsA/u7fh6YD8//NVGg=="
},
"@ionic-native/in-app-browser": {
"version": "4.20.0",
"resolved": "https://registry.npmjs.org/@ionic-native/in-app-browser/-/in-app-browser-4.20.0.tgz",
@ -2621,6 +2626,11 @@
}
}
},
"cordova-plugin-advanced-http": {
"version": "2.4.1",
"resolved": "https://registry.npmjs.org/cordova-plugin-advanced-http/-/cordova-plugin-advanced-http-2.4.1.tgz",
"integrity": "sha512-6G8MTy/d02jE6n3Y9CVyCtD5hZGiBb+/dR2AIzhKN1RGGz38g1D2C8yE4MqHRvnmry6k/KHQWT1MsHNXrjouXQ=="
},
"cordova-plugin-badge": {
"version": "0.8.8",
"resolved": "https://registry.npmjs.org/cordova-plugin-badge/-/cordova-plugin-badge-0.8.8.tgz",

View File

@ -59,6 +59,7 @@
"@ionic-native/file-transfer": "4.20.0",
"@ionic-native/geolocation": "4.20.0",
"@ionic-native/globalization": "4.20.0",
"@ionic-native/http": "^4.20.0",
"@ionic-native/in-app-browser": "4.20.0",
"@ionic-native/keyboard": "4.20.0",
"@ionic-native/local-notifications": "4.20.0",
@ -81,6 +82,7 @@
"cordova-android-support-gradle-release": "3.0.1",
"cordova-clipboard": "1.3.0",
"cordova-ios": "5.1.1",
"cordova-plugin-advanced-http": "2.4.1",
"cordova-plugin-badge": "0.8.8",
"cordova-plugin-camera": "4.1.0",
"cordova-plugin-customurlscheme": "5.0.0",
@ -187,7 +189,10 @@
"cordova-plugin-geolocation": {
"GEOLOCATION_USAGE_DESCRIPTION": "To locate you"
},
"cordova-plugin-ionic-webview": {}
"cordova-plugin-ionic-webview": {},
"cordova-plugin-advanced-http": {
"OKHTTP_VERSION": "3.10.0"
}
}
},
"main": "desktop/electron.js",

View File

@ -0,0 +1,98 @@
// (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 { HttpResponse as AngularHttpResponse, HttpHeaders } from '@angular/common/http';
import { HTTPResponse as NativeHttpResponse } from '@ionic-native/http';
const HTTP_STATUS_MESSAGES = {
100: 'Continue',
101: 'Switching Protocol',
102: 'Processing',
103: 'Early Hints',
200: 'OK',
201: 'Created',
202: 'Accepted',
203: 'Non-Authoritative Information',
204: 'No Content',
205: 'Reset Content',
206: 'Partial Content',
207: 'Multi-Status',
208: 'Already Reported',
226: 'IM Used',
300: 'Multiple Choice',
301: 'Moved Permanently',
302: 'Found',
303: 'See Other',
304: 'Not Modified',
305: 'Use Proxy',
306: 'unused',
307: 'Temporary Redirect',
308: 'Permanent Redirect',
400: 'Bad Request',
401: 'Unauthorized',
402: 'Payment Required',
403: 'Forbidden',
404: 'Not Found',
405: 'Method Not Allowed',
406: 'Not Acceptable',
407: 'Proxy Authentication Required',
408: 'Request Timeout',
409: 'Conflict',
410: 'Gone',
411: 'Length Required',
412: 'Precondition Failed',
413: 'Payload Too Large',
414: 'URI Too Long',
415: 'Unsupported Media Type',
416: 'Range Not Satisfiable',
417: 'Expectation Failed',
418: 'I\'m a teapot',
421: 'Misdirected Request',
422: 'Unprocessable Entity',
423: 'Locked',
424: 'Failed Dependency',
425: 'Too Early',
426: 'Upgrade Required',
428: 'Precondition Required',
429: 'Too Many Requests',
431: 'Request Header Fields Too Large',
451: 'Unavailable For Legal Reasons',
500: 'Internal Server Error',
501: 'Not Implemented',
502: 'Bad Gateway',
503: 'Service Unavailable',
504: 'Gateway Timeout',
505: 'HTTP Version Not Supported',
506: 'Variant Also Negotiates',
507: 'Insufficient Storage',
508: 'Loop Detected',
510: 'Not Extended',
511: 'Network Authentication Required',
};
/**
* Class that adapts a Cordova plugin http response to an Angular http response.
*/
export class CoreNativeToAngularHttpResponse<T> extends AngularHttpResponse<T> {
constructor(protected nativeResponse: NativeHttpResponse) {
super({
body: nativeResponse.data,
headers: new HttpHeaders(nativeResponse.headers),
status: nativeResponse.status,
statusText: HTTP_STATUS_MESSAGES[nativeResponse.status] || '',
url: nativeResponse.url || ''
});
}
}

View File

@ -14,7 +14,6 @@
import { Injector } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
import { SQLiteDB } from './sqlitedb';
import { CoreAppProvider } from '@providers/app';
import { CoreDbProvider } from '@providers/db';
@ -190,7 +189,6 @@ export class CoreSite {
protected domUtils: CoreDomUtilsProvider;
protected eventsProvider: CoreEventsProvider;
protected fileProvider: CoreFileProvider;
protected http: HttpClient;
protected textUtils: CoreTextUtilsProvider;
protected timeUtils: CoreTimeUtilsProvider;
protected translate: TranslateService;
@ -256,7 +254,6 @@ export class CoreSite {
this.domUtils = injector.get(CoreDomUtilsProvider);
this.eventsProvider = injector.get(CoreEventsProvider);
this.fileProvider = injector.get(CoreFileProvider);
this.http = injector.get(HttpClient);
this.textUtils = injector.get(CoreTextUtilsProvider);
this.timeUtils = injector.get(CoreTimeUtilsProvider);
this.translate = injector.get(TranslateService);
@ -1357,55 +1354,62 @@ export class CoreSite {
* @param retrying True if we're retrying the check.
* @return Promise resolved when the check is done.
*/
checkLocalMobilePlugin(retrying?: boolean): Promise<LocalMobileResponse> {
async checkLocalMobilePlugin(retrying?: boolean): Promise<LocalMobileResponse> {
const checkUrl = this.siteUrl + '/local/mobile/check.php',
service = CoreConfigConstants.wsextservice;
if (!service) {
// External service not defined.
return Promise.resolve({ code: 0 });
return { code: 0 };
}
const promise = this.http.post(checkUrl, { service: service }).timeout(this.wsProvider.getRequestTimeout()).toPromise();
let data;
return promise.then((data: any) => {
if (typeof data != 'undefined' && data.errorcode === 'requirecorrectaccess') {
if (!retrying) {
this.siteUrl = this.urlUtils.addOrRemoveWWW(this.siteUrl);
try {
const response = await this.wsProvider.sendHTTPRequest(checkUrl, {
method: 'post',
data: { service: service },
});
return this.checkLocalMobilePlugin(true);
} else {
return Promise.reject(data.error);
}
} else if (typeof data == 'undefined' || typeof data.code == 'undefined') {
// The local_mobile returned something we didn't expect. Let's assume it's not installed.
return { code: 0, warning: 'core.login.localmobileunexpectedresponse' };
}
const code = parseInt(data.code, 10);
if (data.error) {
switch (code) {
case 1:
// Site in maintenance mode.
return Promise.reject(this.translate.instant('core.login.siteinmaintenance'));
case 2:
// Web services not enabled.
return Promise.reject(this.translate.instant('core.login.webservicesnotenabled'));
case 3:
// Extended service not enabled, but the official is enabled.
return { code: 0 };
case 4:
// Neither extended or official services enabled.
return Promise.reject(this.translate.instant('core.login.mobileservicesnotenabled'));
default:
return Promise.reject(this.translate.instant('core.unexpectederror'));
}
} else {
return { code: code, service: service, coreSupported: !!data.coresupported };
}
}, () => {
data = response.body;
} catch (ex) {
return { code: 0 };
});
}
if (typeof data != 'undefined' && data.errorcode === 'requirecorrectaccess') {
if (!retrying) {
this.siteUrl = this.urlUtils.addOrRemoveWWW(this.siteUrl);
return this.checkLocalMobilePlugin(true);
} else {
throw data.error;
}
} else if (typeof data == 'undefined' || typeof data.code == 'undefined') {
// The local_mobile returned something we didn't expect. Let's assume it's not installed.
return { code: 0, warning: 'core.login.localmobileunexpectedresponse' };
}
const code = parseInt(data.code, 10);
if (data.error) {
switch (code) {
case 1:
// Site in maintenance mode.
throw this.translate.instant('core.login.siteinmaintenance');
case 2:
// Web services not enabled.
throw this.translate.instant('core.login.webservicesnotenabled');
case 3:
// Extended service not enabled, but the official is enabled.
return { code: 0 };
case 4:
// Neither extended or official services enabled.
throw this.translate.instant('core.login.mobileservicesnotenabled');
default:
throw this.translate.instant('core.unexpectederror');
}
} else {
return { code: code, service: service, coreSupported: !!data.coresupported };
}
}
/**
@ -1970,7 +1974,7 @@ export class CoreSite {
url = this.fixPluginfileURL(url);
this.tokenPluginFileWorksPromise = this.wsProvider.performHead(url).then((result) => {
return result.ok;
return result.status >= 200 && result.status < 300;
}).catch((error) => {
// Error performing head request.
return false;

View File

@ -25,6 +25,8 @@ import { CoreConstants } from '@core/constants';
import { Md5 } from 'ts-md5/dist/md5';
import { CoreInterceptor } from '@classes/interceptor';
import { makeSingleton } from '@singletons/core.singletons';
import { Observable } from 'rxjs/Observable';
import { CoreNativeToAngularHttpResponse } from '@classes/native-to-angular-http';
/**
* PreSets accepted by the WS call.
@ -81,6 +83,61 @@ export interface CoreWSAjaxPreSets {
useGet?: boolean;
}
/**
* Options for HTTP requests.
*/
export type HttpRequestOptions = {
/**
* The HTTP method.
*/
method: string;
/**
* Payload to send to the server. Only applicable on post, put or patch methods.
*/
data?: any;
/**
* Query params to be appended to the URL (only applicable on get, head, delete, upload or download methods).
*/
params?: any;
/**
* 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 | null;
/**
* Serializer to use. Defaults to 'urlencoded'. Only for mobile environments.
*/
serializer?: string;
/**
* Whether to follow redirects. Defaults to true. Only for mobile environments.
*/
followRedirect?: boolean;
/**
* Headers. Only for mobile environments.
*/
headers?: {[name: string]: string};
/**
* File paths to use for upload or download. Only for mobile environments.
*/
filePath?: string;
/**
* Name to use during upload. Only for mobile environments.
*/
name?: string;
};
/**
* This service allows performing WS calls and download/upload files.
*/
@ -377,8 +434,8 @@ export class CoreWSProvider {
return Promise.resolve(this.mimeTypeCache[url]);
}
return this.performHead(url).then((data) => {
let mimeType = data.headers.get('Content-Type');
return this.performHead(url).then((response) => {
let mimeType = response.headers.get('Content-Type');
if (mimeType) {
// Remove "parameters" like charset.
mimeType = mimeType.split(';')[0];
@ -399,8 +456,8 @@ export class CoreWSProvider {
* @return Promise resolved with the size or -1 if failure.
*/
getRemoteFileSize(url: string): Promise<number> {
return this.performHead(url).then((data) => {
const size = parseInt(data.headers.get('Content-Length'), 10);
return this.performHead(url).then((response) => {
const size = parseInt(response.headers.get('Content-Length'), 10);
if (size) {
return size;
@ -463,12 +520,12 @@ export class CoreWSProvider {
preSets.responseExpected = true;
}
const script = preSets.noLogin ? 'service-nologin.php' : 'service.php',
ajaxData = JSON.stringify([{
index: 0,
methodname: method,
args: this.convertValuesToString(data)
}]);
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.
@ -476,13 +533,22 @@ export class CoreWSProvider {
if (preSets.noLogin && preSets.useGet) {
// Send params using GET.
siteUrl += '&args=' + encodeURIComponent(ajaxData);
promise = this.http.get(siteUrl).timeout(this.getRequestTimeout()).toPromise();
siteUrl += '&args=' + encodeURIComponent(JSON.stringify(ajaxData));
promise = this.sendHTTPRequest(siteUrl, {
method: 'get',
});
} else {
promise = this.http.post(siteUrl, ajaxData).timeout(this.getRequestTimeout()).toPromise();
promise = this.sendHTTPRequest(siteUrl, {
method: 'post',
data: ajaxData,
serializer: 'json',
});
}
return promise.then((data: any) => {
return promise.then((response: HttpResponse<any>) => {
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) {
@ -536,8 +602,11 @@ export class CoreWSProvider {
let promise = this.getPromiseHttp('head', url);
if (!promise) {
promise = this.http.head(url, {observe: 'response', responseType: 'blob'}).timeout(this.getRequestTimeout())
.toPromise();
promise = this.sendHTTPRequest(url, {
method: 'head',
responseType: 'text',
});
promise = this.setPromiseHttp(promise, 'head', url);
}
@ -571,6 +640,7 @@ export class CoreWSProvider {
const promise = this.http.post(requestUrl, ajaxData, options).timeout(this.getRequestTimeout()).toPromise();
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) {
@ -871,13 +941,103 @@ export class CoreWSProvider {
*/
async getText(url: string): Promise<string> {
// Fetch the URL content.
const content = await this.http.get(url, { responseType: 'text' }).toPromise();
const options: HttpRequestOptions = {
method: 'get',
responseType: 'text',
};
const response = await this.sendHTTPRequest(url, options);
const content = response.body;
if (typeof content !== 'string') {
return Promise.reject(null);
throw '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(url: string, options: HttpRequestOptions): Promise<HttpResponse<any>> {
// Set default values.
options.responseType = options.responseType || 'json';
options.timeout = typeof options.timeout == 'undefined' ? this.getRequestTimeout() : options.timeout;
if (this.appProvider.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 format = options.responseType == 'json' ? CoreFileProvider.FORMATJSON : CoreFileProvider.FORMATTEXT;
const content = await this.fileProvider.readFile(url, format);
return new HttpResponse({
body: content,
headers: null,
status: 200,
statusText: 'OK',
url: url
});
}
return new Promise<HttpResponse<any>>((resolve, reject): void => {
// We cannot use Ionic Native plugin because it doesn't have the sendRequest method.
(<any> cordova).plugin.http.sendRequest(url, options, (response) => {
resolve(new CoreNativeToAngularHttpResponse(response));
}, reject);
});
} else {
let observable: Observable<any>;
// Use Angular's library.
switch (options.method) {
case 'get':
observable = this.http.get(url, {
headers: options.headers,
params: options.params,
observe: 'response',
responseType: <any> options.responseType,
});
break;
case 'post':
if (options.serializer == 'json') {
options.data = JSON.stringify(options.data);
}
observable = this.http.post(url, options.data, {
headers: options.headers,
observe: 'response',
responseType: <any> options.responseType,
});
break;
case 'head':
observable = this.http.head(url, {
headers: options.headers,
observe: 'response',
responseType: <any> options.responseType
});
break;
default:
return Promise.reject('Method not implemented yet.');
}
if (options.timeout) {
observable = observable.timeout(options.timeout);
}
return observable.toPromise();
}
}
}
export class CoreWS extends makeSingleton(CoreWSProvider) {}