diff --git a/config.xml b/config.xml
index a9d1ff306..fc0883b59 100644
--- a/config.xml
+++ b/config.xml
@@ -182,6 +182,7 @@
+
diff --git a/package-lock.json b/package-lock.json
index 36872c57b..07c1ea7d7 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -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",
diff --git a/package.json b/package.json
index 2d3f35c34..ecbd217aa 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/classes/native-to-angular-http.ts b/src/classes/native-to-angular-http.ts
new file mode 100644
index 000000000..e21e7eb7e
--- /dev/null
+++ b/src/classes/native-to-angular-http.ts
@@ -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 extends AngularHttpResponse {
+
+ 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 || ''
+ });
+ }
+}
diff --git a/src/classes/site.ts b/src/classes/site.ts
index 54b2dd294..befacd29e 100644
--- a/src/classes/site.ts
+++ b/src/classes/site.ts
@@ -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 {
+ async checkLocalMobilePlugin(retrying?: boolean): Promise {
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;
diff --git a/src/providers/ws.ts b/src/providers/ws.ts
index 9f8743a86..57c91985a 100644
--- a/src/providers/ws.ts
+++ b/src/providers/ws.ts
@@ -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 {
- 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) => {
+ 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 {
// 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> {
+
+ // 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>((resolve, reject): void => {
+ // We cannot use Ionic Native plugin because it doesn't have the sendRequest method.
+ ( cordova).plugin.http.sendRequest(url, options, (response) => {
+ resolve(new CoreNativeToAngularHttpResponse(response));
+ }, reject);
+ });
+ } else {
+ let observable: Observable;
+
+ // Use Angular's library.
+ switch (options.method) {
+ case 'get':
+ observable = this.http.get(url, {
+ headers: options.headers,
+ params: options.params,
+ observe: 'response',
+ responseType: 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: options.responseType,
+ });
+ break;
+
+ case 'head':
+ observable = this.http.head(url, {
+ headers: options.headers,
+ observe: 'response',
+ responseType: 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) {}