Merge pull request #2558 from dpalou/MOBILE-3565

Mobile 3565
main
Dani Palou 2020-10-14 18:15:34 +02:00 committed by GitHub
commit df7c68b07c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
50 changed files with 2557 additions and 1838 deletions

20
package-lock.json generated
View File

@ -2224,6 +2224,21 @@
"@types/cordova": "^0.0.34" "@types/cordova": "^0.0.34"
} }
}, },
"@ionic-native/ionic-webview": {
"version": "5.28.0",
"resolved": "https://registry.npmjs.org/@ionic-native/ionic-webview/-/ionic-webview-5.28.0.tgz",
"integrity": "sha512-Ex/IH/LIa+4X4yGFgo4/W00IWaVsF6KkZuwIG2s3zZQEgXU3tvcgxAOEzkNCbcDC5dXcFH0z/41twZ+YC6gu+A==",
"requires": {
"@types/cordova": "^0.0.34"
},
"dependencies": {
"@types/cordova": {
"version": "0.0.34",
"resolved": "https://registry.npmjs.org/@types/cordova/-/cordova-0.0.34.tgz",
"integrity": "sha1-6nrd907Ow9dimCegw54smt3HPQQ="
}
}
},
"@ionic-native/keyboard": { "@ionic-native/keyboard": {
"version": "5.28.0", "version": "5.28.0",
"resolved": "https://registry.npmjs.org/@ionic-native/keyboard/-/keyboard-5.28.0.tgz", "resolved": "https://registry.npmjs.org/@ionic-native/keyboard/-/keyboard-5.28.0.tgz",
@ -3053,6 +3068,11 @@
"cordova-plugin-file-transfer": "*" "cordova-plugin-file-transfer": "*"
} }
}, },
"@types/dom-mediacapture-record": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/dom-mediacapture-record/-/dom-mediacapture-record-1.0.7.tgz",
"integrity": "sha512-ddDIRTO1ajtbxaNo2o7fPJggpN54PZf1ZUJKOjto2ENMJE/9GKUvaw3ZRuQzlS/p0E+PnIcssxfoqYJ4yiXSBw=="
},
"@types/glob": { "@types/glob": {
"version": "7.1.3", "version": "7.1.3",
"resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz",

View File

@ -45,6 +45,7 @@
"@ionic-native/globalization": "^5.28.0", "@ionic-native/globalization": "^5.28.0",
"@ionic-native/http": "^5.28.0", "@ionic-native/http": "^5.28.0",
"@ionic-native/in-app-browser": "^5.28.0", "@ionic-native/in-app-browser": "^5.28.0",
"@ionic-native/ionic-webview": "^5.28.0",
"@ionic-native/keyboard": "^5.28.0", "@ionic-native/keyboard": "^5.28.0",
"@ionic-native/local-notifications": "^5.28.0", "@ionic-native/local-notifications": "^5.28.0",
"@ionic-native/network": "^5.28.0", "@ionic-native/network": "^5.28.0",
@ -60,6 +61,7 @@
"@ngx-translate/http-loader": "^6.0.0", "@ngx-translate/http-loader": "^6.0.0",
"@types/cordova": "0.0.34", "@types/cordova": "0.0.34",
"@types/cordova-plugin-file-transfer": "^1.6.2", "@types/cordova-plugin-file-transfer": "^1.6.2",
"@types/dom-mediacapture-record": "^1.0.7",
"com-darryncampbell-cordova-plugin-intent": "^2.0.0", "com-darryncampbell-cordova-plugin-intent": "^2.0.0",
"cordova": "^10.0.0", "cordova": "^10.0.0",
"cordova-android": "^8.1.0", "cordova-android": "^8.1.0",

View File

@ -15,11 +15,13 @@
import { NgModule, Injector } from '@angular/core'; import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router'; import { RouteReuseStrategy } from '@angular/router';
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { IonicModule, IonicRouteStrategy, Platform } from '@ionic/angular'; import { IonicModule, IonicRouteStrategy, Platform } from '@ionic/angular';
import { AppComponent } from './app.component'; import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module'; import { AppRoutingModule } from './app-routing.module';
import { CoreInterceptor } from '@classes/interceptor';
// Import core services. // Import core services.
import { CoreAppProvider } from '@services/app'; import { CoreAppProvider } from '@services/app';
@ -49,23 +51,41 @@ import { CoreTimeUtilsProvider } from '@services/utils/time';
import { CoreUrlUtilsProvider } from '@services/utils/url'; import { CoreUrlUtilsProvider } from '@services/utils/url';
import { CoreUtilsProvider } from '@services/utils/utils'; import { CoreUtilsProvider } from '@services/utils/utils';
// Import core modules.
import { CoreEmulatorModule } from '@core/emulator/emulator.module'; import { CoreEmulatorModule } from '@core/emulator/emulator.module';
import { CoreLoginModule } from '@core/login/login.module'; import { CoreLoginModule } from '@core/login/login.module';
import { setSingletonsInjector } from '@singletons/core.singletons'; import { setSingletonsInjector } from '@singletons/core.singletons';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
// For translate loader. AoT requires an exported function for factories.
export function createTranslateLoader(http: HttpClient): TranslateHttpLoader {
return new TranslateHttpLoader(http, './assets/lang/', '.json');
}
@NgModule({ @NgModule({
declarations: [AppComponent], declarations: [AppComponent],
entryComponents: [], entryComponents: [],
imports: [ imports: [
BrowserModule, BrowserModule,
IonicModule.forRoot(), IonicModule.forRoot(),
HttpClientModule, // HttpClient is used to make JSON requests. It fails for HEAD requests because there is no content.
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useFactory: (createTranslateLoader),
deps: [HttpClient],
},
}),
AppRoutingModule, AppRoutingModule,
CoreEmulatorModule, CoreEmulatorModule,
CoreLoginModule, CoreLoginModule,
], ],
providers: [ providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, { provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{ provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true },
CoreAppProvider, CoreAppProvider,
CoreConfigProvider, CoreConfigProvider,
CoreCronDelegate, CoreCronDelegate,
@ -96,8 +116,8 @@ import { setSingletonsInjector } from '@singletons/core.singletons';
bootstrap: [AppComponent], bootstrap: [AppComponent],
}) })
export class AppModule { export class AppModule {
constructor(injector: Injector, platform: Platform) {
constructor(injector: Injector, platform: Platform) {
// Set the injector. // Set the injector.
setSingletonsInjector(injector); setSingletonsInjector(injector);
@ -133,4 +153,5 @@ export class AppModule {
// Execute the init processes. // Execute the init processes.
CoreInit.instance.executeInitProcesses(); CoreInit.instance.executeInitProcesses();
} }
} }

View File

@ -78,7 +78,7 @@ export class CoreDelegate {
/** /**
* Function to resolve the handlers init promise. * Function to resolve the handlers init promise.
*/ */
protected handlersInitResolve: (value?: any) => void; protected handlersInitResolve: () => void;
/** /**
* Constructor of the Delegate. * Constructor of the Delegate.
@ -110,8 +110,8 @@ export class CoreDelegate {
* @param params Parameters to pass to the function. * @param params Parameters to pass to the function.
* @return Function returned value or default value. * @return Function returned value or default value.
*/ */
protected executeFunctionOnEnabled(handlerName: string, fnName: string, params?: any[]): any { protected executeFunctionOnEnabled<T = unknown>(handlerName: string, fnName: string, params?: unknown[]): T {
return this.execute(this.enabledHandlers[handlerName], fnName, params); return this.execute<T>(this.enabledHandlers[handlerName], fnName, params);
} }
/** /**
@ -123,7 +123,7 @@ export class CoreDelegate {
* @param params Parameters to pass to the function. * @param params Parameters to pass to the function.
* @return Function returned value or default value. * @return Function returned value or default value.
*/ */
protected executeFunction(handlerName: string, fnName: string, params?: any[]): any { protected executeFunction<T = unknown>(handlerName: string, fnName: string, params?: unknown[]): T {
return this.execute(this.handlers[handlerName], fnName, params); return this.execute(this.handlers[handlerName], fnName, params);
} }
@ -136,7 +136,7 @@ export class CoreDelegate {
* @param params Parameters to pass to the function. * @param params Parameters to pass to the function.
* @return Function returned value or default value. * @return Function returned value or default value.
*/ */
private execute(handler: CoreDelegateHandler, fnName: string, params?: any[]): any { private execute<T = unknown>(handler: CoreDelegateHandler, fnName: string, params?: unknown[]): T {
if (handler && handler[fnName]) { if (handler && handler[fnName]) {
return handler[fnName].apply(handler, params); return handler[fnName].apply(handler, params);
} else if (this.defaultHandler && this.defaultHandler[fnName]) { } else if (this.defaultHandler && this.defaultHandler[fnName]) {
@ -180,10 +180,10 @@ export class CoreDelegate {
* @param onlyEnabled If check only enabled handlers or all. * @param onlyEnabled If check only enabled handlers or all.
* @return Function returned value or default value. * @return Function returned value or default value.
*/ */
protected hasFunction(handlerName: string, fnName: string, onlyEnabled: boolean = true): any { protected hasFunction(handlerName: string, fnName: string, onlyEnabled: boolean = true): boolean {
const handler = onlyEnabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName]; const handler = onlyEnabled ? this.enabledHandlers[handlerName] : this.handlers[handlerName];
return handler && handler[fnName]; return handler && typeof handler[fnName] == 'function';
} }
/** /**
@ -240,7 +240,7 @@ export class CoreDelegate {
* @param time Time this update process started. * @param time Time this update process started.
* @return Resolved when done. * @return Resolved when done.
*/ */
protected updateHandler(handler: CoreDelegateHandler, time: number): Promise<void> { protected updateHandler(handler: CoreDelegateHandler): Promise<void> {
const siteId = CoreSites.instance.getCurrentSiteId(); const siteId = CoreSites.instance.getCurrentSiteId();
const currentSite = CoreSites.instance.getCurrentSite(); const currentSite = CoreSites.instance.getCurrentSite();
let promise: Promise<boolean>; let promise: Promise<boolean>;
@ -255,7 +255,7 @@ export class CoreDelegate {
if (!CoreSites.instance.isLoggedIn() || this.isFeatureDisabled(handler, currentSite)) { if (!CoreSites.instance.isLoggedIn() || this.isFeatureDisabled(handler, currentSite)) {
promise = Promise.resolve(false); promise = Promise.resolve(false);
} else { } else {
promise = handler.isEnabled().catch(() => false); promise = Promise.resolve(handler.isEnabled()).catch(() => false);
} }
// Checks if the handler is enabled. // Checks if the handler is enabled.
@ -304,7 +304,7 @@ export class CoreDelegate {
// Loop over all the handlers. // Loop over all the handlers.
for (const name in this.handlers) { for (const name in this.handlers) {
promises.push(this.updateHandler(this.handlers[name], now)); promises.push(this.updateHandler(this.handlers[name]));
} }
try { try {
@ -326,7 +326,7 @@ export class CoreDelegate {
* Update handlers Data. * Update handlers Data.
* Override this function to update handlers data. * Override this function to update handlers data.
*/ */
updateData(): any { updateData(): void {
// To be overridden. // To be overridden.
} }

View File

@ -11,9 +11,10 @@
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and // See the License for the specific language governing permissions and
// limitations under the License. // limitations under the License.
import Faker from 'faker'; import Faker from 'faker';
import { CoreError } from './error'; import { CoreError } from '@classes/errors/error';
describe('CoreError', () => { describe('CoreError', () => {

View File

@ -0,0 +1,30 @@
// (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 { CoreError } from '@classes/errors/error';
/**
* Generic error returned by an Ajax call.
*/
export class CoreAjaxError extends CoreError {
available?: number; // Whether the AJAX call is available. 0 if unknown, 1 if available, -1 if not available.
constructor(message: string, available?: number) {
super(message);
this.available = typeof available == 'undefined' ? 0 : available;
}
}

View File

@ -0,0 +1,53 @@
// (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 { CoreError } from '@classes/errors/error';
/**
* Error returned by WS.
*/
export class CoreAjaxWSError extends CoreError {
exception?: string; // Name of the Moodle exception.
errorcode?: string;
warningcode?: string;
link?: string; // Link to the site.
moreinfourl?: string; // Link to a page with more info.
debuginfo?: string; // Debug info. Only if debug mode is enabled.
backtrace?: string; // Backtrace. Only if debug mode is enabled.
available?: number; // Whether the AJAX call is available. 0 if unknown, 1 if available, -1 if not available.
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
constructor(error: any, available?: number) {
super(error.message);
this.exception = error.exception;
this.errorcode = error.errorcode;
this.warningcode = error.warningcode;
this.link = error.link;
this.moreinfourl = error.moreinfourl;
this.debuginfo = error.debuginfo;
this.backtrace = error.backtrace;
this.available = available;
if (typeof this.available == 'undefined') {
if (this.errorcode) {
this.available = this.errorcode == 'invalidrecord' ? -1 : 1;
} else {
this.available = 0;
}
}
}
}

View File

@ -0,0 +1,20 @@
// (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 { CoreSilentError } from '@classes/errors/silenterror';
/**
* User canceled an action.
*/
export class CoreCanceledError extends CoreSilentError { }

View File

@ -0,0 +1,20 @@
// (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 { CoreError } from '@classes/errors/error';
/**
* Error that won't be displayed to the user.
*/
export class CoreSilentError extends CoreError { }

View File

@ -0,0 +1,41 @@
// (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 { CoreError } from '@classes/errors/error';
/**
* Error returned when performing operations regarding a site (check if it exists, authenticate user, etc.).
*/
export class CoreSiteError extends CoreError {
errorcode?: string;
critical?: boolean;
loggedOut?: boolean;
constructor(protected error: SiteError) {
super(error.message);
this.errorcode = error.errorcode;
this.critical = error.critical;
this.loggedOut = error.loggedOut;
}
}
export type SiteError = {
message: string;
errorcode?: string;
critical?: boolean; // Whether the error is important enough to abort the operation.
loggedOut?: boolean; // Whether site has been marked as logged out.
};

View File

@ -0,0 +1,42 @@
// (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 { CoreError } from '@classes/errors/error';
/**
* Error returned by WS.
*/
export class CoreWSError extends CoreError {
exception?: string; // Name of the Moodle exception.
errorcode?: string;
warningcode?: string;
link?: string; // Link to the site.
moreinfourl?: string; // Link to a page with more info.
debuginfo?: string; // Debug info. Only if debug mode is enabled.
backtrace?: string; // Backtrace. Only if debug mode is enabled.
constructor(error: any) {
super(error.message);
this.exception = error.exception;
this.errorcode = error.errorcode;
this.warningcode = error.warningcode;
this.link = error.link;
this.moreinfourl = error.moreinfourl;
this.debuginfo = error.debuginfo;
this.backtrace = error.backtrace;
}
}

View File

@ -30,28 +30,26 @@ export class CoreInterceptor implements HttpInterceptor {
* @param addNull Add null values to the serialized as empty parameters. * @param addNull Add null values to the serialized as empty parameters.
* @return Serialization of the object. * @return Serialization of the object.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
static serialize(obj: any, addNull?: boolean): string { static serialize(obj: any, addNull?: boolean): string {
let query = ''; let query = '';
let fullSubName: string;
let subValue;
let innerObj;
for (const name in obj) { for (const name in obj) {
const value = obj[name]; const value = obj[name];
if (value instanceof Array) { if (value instanceof Array) {
for (let i = 0; i < value.length; ++i) { for (let i = 0; i < value.length; ++i) {
subValue = value[i]; const subValue = value[i];
fullSubName = name + '[' + i + ']'; const fullSubName = name + '[' + i + ']';
innerObj = {}; const innerObj = {};
innerObj[fullSubName] = subValue; innerObj[fullSubName] = subValue;
query += this.serialize(innerObj) + '&'; query += this.serialize(innerObj) + '&';
} }
} else if (value instanceof Object) { } else if (value instanceof Object) {
for (const subName in value) { for (const subName in value) {
subValue = value[subName]; const subValue = value[subName];
fullSubName = name + '[' + subName + ']'; const fullSubName = name + '[' + subName + ']';
innerObj = {}; const innerObj = {};
innerObj[fullSubName] = subValue; innerObj[fullSubName] = subValue;
query += this.serialize(innerObj) + '&'; query += this.serialize(innerObj) + '&';
} }
@ -63,6 +61,7 @@ export class CoreInterceptor implements HttpInterceptor {
return query.length ? query.substr(0, query.length - 1) : query; return query.length ? query.substr(0, query.length - 1) : query;
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
// Add the header and serialize the body if needed. // Add the header and serialize the body if needed.
const newReq = req.clone({ const newReq = req.clone({

View File

@ -0,0 +1,55 @@
// (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 { CoreUtils } from '@services/utils/utils';
/**
* Class to improve the behaviour of HTMLIonLoadingElement.
* It's not a subclass of HTMLIonLoadingElement because we cannot override the dismiss function.
*/
export class CoreIonLoadingElement {
protected isPresented = false;
protected isDismissed = false;
constructor(public loading: HTMLIonLoadingElement) { }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
async dismiss(data?: any, role?: string): Promise<boolean> {
if (!this.isPresented || this.isDismissed) {
this.isDismissed = true;
return true;
}
this.isDismissed = true;
return this.loading.dismiss(data, role);
}
/**
* Present the loading.
*/
async present(): Promise<void> {
// Wait a bit before presenting the modal, to prevent it being displayed if dissmiss is called fast.
await CoreUtils.instance.wait(40);
if (!this.isDismissed) {
this.isPresented = true;
await this.loading.present();
}
}
}

View File

@ -17,11 +17,13 @@ import { CoreUtils, PromiseDefer } from '@services/utils/utils';
/** /**
* Function to add to the queue. * Function to add to the queue.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CoreQueueRunnerFunction<T> = (...args: any[]) => T | Promise<T>; export type CoreQueueRunnerFunction<T> = (...args: any[]) => T | Promise<T>;
/** /**
* Queue item. * Queue item.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CoreQueueRunnerItem<T = any> = { export type CoreQueueRunnerItem<T = any> = {
/** /**
* Item ID. * Item ID.

File diff suppressed because it is too large Load Diff

View File

@ -15,6 +15,7 @@
import { SQLiteObject } from '@ionic-native/sqlite/ngx'; import { SQLiteObject } from '@ionic-native/sqlite/ngx';
import { SQLite, Platform } from '@singletons/core.singletons'; import { SQLite, Platform } from '@singletons/core.singletons';
import { CoreError } from '@classes/errors/error';
/** /**
* Schema of a table. * Schema of a table.
@ -411,6 +412,7 @@ export class SQLiteDB {
* @param params Query parameters. * @param params Query parameters.
* @return Promise resolved with the result. * @return Promise resolved with the result.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise<any> { async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise<any> {
await this.ready(); await this.ready();
@ -425,7 +427,8 @@ export class SQLiteDB {
* @param sqlStatements SQL statements to execute. * @param sqlStatements SQL statements to execute.
* @return Promise resolved with the result. * @return Promise resolved with the result.
*/ */
async executeBatch(sqlStatements: (string | SQLiteDBRecordValue[])[][]): Promise<void> { // eslint-disable-next-line @typescript-eslint/no-explicit-any
async executeBatch(sqlStatements: (string | string[] | any)[]): Promise<void> {
await this.ready(); await this.ready();
await this.db.sqlBatch(sqlStatements); await this.db.sqlBatch(sqlStatements);
@ -453,6 +456,7 @@ export class SQLiteDB {
* Format the data to where params. * Format the data to where params.
* *
* @param data Object data. * @param data Object data.
* @return List of params.
*/ */
protected formatDataToSQLParams(data: SQLiteDBRecordValues): SQLiteDBRecordValue[] { protected formatDataToSQLParams(data: SQLiteDBRecordValues): SQLiteDBRecordValue[] {
return Object.keys(data).map((key) => data[key]); return Object.keys(data).map((key) => data[key]);
@ -464,7 +468,7 @@ export class SQLiteDB {
* @param table The table to query. * @param table The table to query.
* @return Promise resolved with the records. * @return Promise resolved with the records.
*/ */
async getAllRecords(table: string): Promise<SQLiteDBRecordValues[]> { async getAllRecords<T = unknown>(table: string): Promise<T[]> {
return this.getRecords(table); return this.getRecords(table);
} }
@ -510,7 +514,7 @@ export class SQLiteDB {
async getFieldSql(sql: string, params?: SQLiteDBRecordValue[]): Promise<SQLiteDBRecordValue> { async getFieldSql(sql: string, params?: SQLiteDBRecordValue[]): Promise<SQLiteDBRecordValue> {
const record = await this.getRecordSql(sql, params); const record = await this.getRecordSql(sql, params);
if (!record) { if (!record) {
throw null; throw new CoreError('No record found.');
} }
return record[Object.keys(record)[0]]; return record[Object.keys(record)[0]];
@ -574,10 +578,10 @@ export class SQLiteDB {
* @param fields A comma separated list of fields to return. * @param fields A comma separated list of fields to return.
* @return Promise resolved with the record, rejected if not found. * @return Promise resolved with the record, rejected if not found.
*/ */
getRecord(table: string, conditions?: SQLiteDBRecordValues, fields: string = '*'): Promise<SQLiteDBRecordValues> { getRecord<T = unknown>(table: string, conditions?: SQLiteDBRecordValues, fields: string = '*'): Promise<T> {
const selectAndParams = this.whereClause(conditions); const selectAndParams = this.whereClause(conditions);
return this.getRecordSelect(table, selectAndParams[0], selectAndParams[1], fields); return this.getRecordSelect<T>(table, selectAndParams[0], selectAndParams[1], fields);
} }
/** /**
@ -589,13 +593,13 @@ export class SQLiteDB {
* @param fields A comma separated list of fields to return. * @param fields A comma separated list of fields to return.
* @return Promise resolved with the record, rejected if not found. * @return Promise resolved with the record, rejected if not found.
*/ */
getRecordSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], fields: string = '*'): getRecordSelect<T = unknown>(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], fields: string = '*'):
Promise<SQLiteDBRecordValues> { Promise<T> {
if (select) { if (select) {
select = ' WHERE ' + select; select = ' WHERE ' + select;
} }
return this.getRecordSql(`SELECT ${fields} FROM ${table} ${select}`, params); return this.getRecordSql<T>(`SELECT ${fields} FROM ${table} ${select}`, params);
} }
/** /**
@ -608,11 +612,11 @@ export class SQLiteDB {
* @param params List of sql parameters * @param params List of sql parameters
* @return Promise resolved with the records. * @return Promise resolved with the records.
*/ */
async getRecordSql(sql: string, params?: SQLiteDBRecordValue[]): Promise<SQLiteDBRecordValues> { async getRecordSql<T = unknown>(sql: string, params?: SQLiteDBRecordValue[]): Promise<T> {
const result = await this.getRecordsSql(sql, params, 0, 1); const result = await this.getRecordsSql<T>(sql, params, 0, 1);
if (!result || !result.length) { if (!result || !result.length) {
// Not found, reject. // Not found, reject.
throw null; throw new CoreError('No records found.');
} }
return result[0]; return result[0];
@ -629,11 +633,11 @@ export class SQLiteDB {
* @param limitNum Return a subset comprising this many records in total. * @param limitNum Return a subset comprising this many records in total.
* @return Promise resolved with the records. * @return Promise resolved with the records.
*/ */
getRecords(table: string, conditions?: SQLiteDBRecordValues, sort: string = '', fields: string = '*', limitFrom: number = 0, getRecords<T = unknown>(table: string, conditions?: SQLiteDBRecordValues, sort: string = '', fields: string = '*',
limitNum: number = 0): Promise<SQLiteDBRecordValues[]> { limitFrom: number = 0, limitNum: number = 0): Promise<T[]> {
const selectAndParams = this.whereClause(conditions); const selectAndParams = this.whereClause(conditions);
return this.getRecordsSelect(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum); return this.getRecordsSelect<T>(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum);
} }
/** /**
@ -648,11 +652,11 @@ export class SQLiteDB {
* @param limitNum Return a subset comprising this many records in total. * @param limitNum Return a subset comprising this many records in total.
* @return Promise resolved with the records. * @return Promise resolved with the records.
*/ */
getRecordsList(table: string, field: string, values: SQLiteDBRecordValue[], sort: string = '', fields: string = '*', getRecordsList<T = unknown>(table: string, field: string, values: SQLiteDBRecordValue[], sort: string = '',
limitFrom: number = 0, limitNum: number = 0): Promise<SQLiteDBRecordValues[]> { fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise<T[]> {
const selectAndParams = this.whereClauseList(field, values); const selectAndParams = this.whereClauseList(field, values);
return this.getRecordsSelect(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum); return this.getRecordsSelect<T>(table, selectAndParams[0], selectAndParams[1], sort, fields, limitFrom, limitNum);
} }
/** /**
@ -667,8 +671,8 @@ export class SQLiteDB {
* @param limitNum Return a subset comprising this many records in total. * @param limitNum Return a subset comprising this many records in total.
* @return Promise resolved with the records. * @return Promise resolved with the records.
*/ */
getRecordsSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], sort: string = '', getRecordsSelect<T = unknown>(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], sort: string = '',
fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise<SQLiteDBRecordValues[]> { fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise<T[]> {
if (select) { if (select) {
select = ' WHERE ' + select; select = ' WHERE ' + select;
} }
@ -678,7 +682,7 @@ export class SQLiteDB {
const sql = `SELECT ${fields} FROM ${table} ${select} ${sort}`; const sql = `SELECT ${fields} FROM ${table} ${select} ${sort}`;
return this.getRecordsSql(sql, params, limitFrom, limitNum); return this.getRecordsSql<T>(sql, params, limitFrom, limitNum);
} }
/** /**
@ -690,8 +694,8 @@ export class SQLiteDB {
* @param limitNum Return a subset comprising this many records. * @param limitNum Return a subset comprising this many records.
* @return Promise resolved with the records. * @return Promise resolved with the records.
*/ */
async getRecordsSql(sql: string, params?: SQLiteDBRecordValue[], limitFrom?: number, limitNum?: number): async getRecordsSql<T = unknown>(sql: string, params?: SQLiteDBRecordValue[], limitFrom?: number, limitNum?: number):
Promise<SQLiteDBRecordValues[]> { Promise<T[]> {
const limits = this.normaliseLimitFromNum(limitFrom, limitNum); const limits = this.normaliseLimitFromNum(limitFrom, limitNum);
if (limits[0] || limits[1]) { if (limits[0] || limits[1]) {
@ -768,7 +772,7 @@ export class SQLiteDB {
*/ */
async insertRecords(table: string, dataObjects: SQLiteDBRecordValues[]): Promise<void> { async insertRecords(table: string, dataObjects: SQLiteDBRecordValues[]): Promise<void> {
if (!Array.isArray(dataObjects)) { if (!Array.isArray(dataObjects)) {
throw null; throw new CoreError('Invalid parameter supplied to insertRecords, it should be an array.');
} }
const statements = dataObjects.map((dataObject) => { const statements = dataObjects.map((dataObject) => {
@ -854,7 +858,7 @@ export class SQLiteDB {
async recordExists(table: string, conditions?: SQLiteDBRecordValues): Promise<void> { async recordExists(table: string, conditions?: SQLiteDBRecordValues): Promise<void> {
const record = await this.getRecord(table, conditions); const record = await this.getRecord(table, conditions);
if (!record) { if (!record) {
throw null; throw new CoreError('Record does not exist.');
} }
} }
@ -869,7 +873,7 @@ export class SQLiteDB {
async recordExistsSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = []): Promise<void> { async recordExistsSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = []): Promise<void> {
const record = await this.getRecordSelect(table, select, params); const record = await this.getRecordSelect(table, select, params);
if (!record) { if (!record) {
throw null; throw new CoreError('Record does not exist.');
} }
} }
@ -883,7 +887,7 @@ export class SQLiteDB {
async recordExistsSql(sql: string, params?: SQLiteDBRecordValue[]): Promise<void> { async recordExistsSql(sql: string, params?: SQLiteDBRecordValue[]): Promise<void> {
const record = await this.getRecordSql(sql, params); const record = await this.getRecordSql(sql, params);
if (!record) { if (!record) {
throw null; throw new CoreError('Record does not exist.');
} }
} }

View File

@ -23,7 +23,9 @@ import { FileOpener } from '@ionic-native/file-opener/ngx';
import { FileTransfer } from '@ionic-native/file-transfer/ngx'; import { FileTransfer } from '@ionic-native/file-transfer/ngx';
import { Geolocation } from '@ionic-native/geolocation/ngx'; import { Geolocation } from '@ionic-native/geolocation/ngx';
import { Globalization } from '@ionic-native/globalization/ngx'; import { Globalization } from '@ionic-native/globalization/ngx';
import { HTTP } from '@ionic-native/http/ngx';
import { InAppBrowser } from '@ionic-native/in-app-browser/ngx'; import { InAppBrowser } from '@ionic-native/in-app-browser/ngx';
import { WebView } from '@ionic-native/ionic-webview/ngx';
import { Keyboard } from '@ionic-native/keyboard/ngx'; import { Keyboard } from '@ionic-native/keyboard/ngx';
import { LocalNotifications } from '@ionic-native/local-notifications/ngx'; import { LocalNotifications } from '@ionic-native/local-notifications/ngx';
import { Network } from '@ionic-native/network/ngx'; import { Network } from '@ionic-native/network/ngx';
@ -58,6 +60,7 @@ import { Zip } from '@ionic-native/zip/ngx';
FileTransfer, FileTransfer,
Geolocation, Geolocation,
Globalization, Globalization,
HTTP,
InAppBrowser, InAppBrowser,
Keyboard, Keyboard,
LocalNotifications, LocalNotifications,
@ -68,6 +71,7 @@ import { Zip } from '@ionic-native/zip/ngx';
SQLite, SQLite,
StatusBar, StatusBar,
WebIntent, WebIntent,
WebView,
Zip, Zip,
], ],
}) })

View File

@ -21,6 +21,7 @@ import { Pipe, PipeTransform } from '@angular/core';
name: 'coreCreateLinks', name: 'coreCreateLinks',
}) })
export class CoreCreateLinksPipe implements PipeTransform { export class CoreCreateLinksPipe implements PipeTransform {
protected static replacePattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])(?![^<]*>|[^<>]*<\/)/gim; protected static replacePattern = /(\b(https?|ftp):\/\/[-A-Z0-9+&@#\/%?=~_|!:,.;]*[-A-Z0-9+&@#\/%=~_|])(?![^<]*>|[^<>]*<\/)/gim;
/** /**
@ -32,4 +33,5 @@ export class CoreCreateLinksPipe implements PipeTransform {
transform(text: string): string { transform(text: string): string {
return text.replace(CoreCreateLinksPipe.replacePattern, '<a href="$1">$1</a>'); return text.replace(CoreCreateLinksPipe.replacePattern, '<a href="$1">$1</a>');
} }
} }

View File

@ -31,4 +31,5 @@ export class CoreNoTagsPipe implements PipeTransform {
transform(text: string): string { transform(text: string): string {
return text.replace(/(<([^>]+)>)/ig, ''); return text.replace(/(<([^>]+)>)/ig, '');
} }
} }

View File

@ -28,6 +28,6 @@ import { CoreTimeAgoPipe } from './time-ago.pipe';
CoreCreateLinksPipe, CoreCreateLinksPipe,
CoreNoTagsPipe, CoreNoTagsPipe,
CoreTimeAgoPipe, CoreTimeAgoPipe,
] ],
}) })
export class CorePipesModule {} export class CorePipesModule {}

View File

@ -24,6 +24,7 @@ import moment from 'moment';
name: 'coreTimeAgo', name: 'coreTimeAgo',
}) })
export class CoreTimeAgoPipe implements PipeTransform { export class CoreTimeAgoPipe implements PipeTransform {
private logger: CoreLogger; private logger: CoreLogger;
constructor() { constructor() {
@ -50,4 +51,5 @@ export class CoreTimeAgoPipe implements PipeTransform {
return Translate.instance.instant('core.ago', { $a: moment(timestamp * 1000).fromNow(true) }); return Translate.instance.instant('core.ago', { $a: moment(timestamp * 1000).fromNow(true) });
} }
} }

View File

@ -17,12 +17,16 @@ import { Connection } from '@ionic-native/network/ngx';
import { CoreDB } from '@services/db'; import { CoreDB } from '@services/db';
import { CoreEvents, CoreEventsProvider } from '@services/events'; import { CoreEvents, CoreEventsProvider } from '@services/events';
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb'; import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb';
import CoreConfigConstants from '@app/config.json'; import CoreConfigConstants from '@app/config.json';
import { makeSingleton, Keyboard, Network, StatusBar, Platform } from '@singletons/core.singletons'; import { makeSingleton, Keyboard, Network, StatusBar, Platform } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
const DBNAME = 'MoodleMobile';
const SCHEMA_VERSIONS_TABLE = 'schema_versions';
/** /**
* Factory to provide some global functionalities, like access to the global app database. * Factory to provide some global functionalities, like access to the global app database.
* *
@ -38,23 +42,22 @@ import { CoreLogger } from '@singletons/logger';
*/ */
@Injectable() @Injectable()
export class CoreAppProvider { export class CoreAppProvider {
protected DBNAME = 'MoodleMobile';
protected db: SQLiteDB; protected db: SQLiteDB;
protected logger: CoreLogger; protected logger: CoreLogger;
protected ssoAuthenticationPromise: Promise<any>; protected ssoAuthenticationDeferred: PromiseDefer<void>;
protected isKeyboardShown = false; protected isKeyboardShown = false;
protected _isKeyboardOpening = false; protected keyboardOpening = false;
protected _isKeyboardClosing = false; protected keyboardClosing = false;
protected backActions = []; protected backActions: {callback: () => boolean; priority: number}[] = [];
protected mainMenuId = 0; protected mainMenuId = 0;
protected mainMenuOpen: number; protected mainMenuOpen: number;
protected forceOffline = false; protected forceOffline = false;
// Variables for DB. // Variables for DB.
protected createVersionsTableReady: Promise<any>; protected createVersionsTableReady: Promise<void>;
protected SCHEMA_VERSIONS_TABLE = 'schema_versions';
protected versionsTableSchema: SQLiteDBTableSchema = { protected versionsTableSchema: SQLiteDBTableSchema = {
name: this.SCHEMA_VERSIONS_TABLE, name: SCHEMA_VERSIONS_TABLE,
columns: [ columns: [
{ {
name: 'name', name: 'name',
@ -68,11 +71,9 @@ export class CoreAppProvider {
], ],
}; };
constructor(appRef: ApplicationRef, constructor(appRef: ApplicationRef, zone: NgZone) {
zone: NgZone) {
this.logger = CoreLogger.getInstance('CoreAppProvider'); this.logger = CoreLogger.getInstance('CoreAppProvider');
this.db = CoreDB.instance.getDB(this.DBNAME); this.db = CoreDB.instance.getDB(DBNAME);
// Create the schema versions table. // Create the schema versions table.
this.createVersionsTableReady = this.db.createTableFromSchema(this.versionsTableSchema); this.createVersionsTableReady = this.db.createTableFromSchema(this.versionsTableSchema);
@ -87,7 +88,7 @@ export class CoreAppProvider {
CoreEvents.instance.trigger(CoreEventsProvider.KEYBOARD_CHANGE, data.keyboardHeight); CoreEvents.instance.trigger(CoreEventsProvider.KEYBOARD_CHANGE, data.keyboardHeight);
}); });
}); });
Keyboard.instance.onKeyboardHide().subscribe((data) => { Keyboard.instance.onKeyboardHide().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working. // Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => { zone.run(() => {
document.body.classList.remove('keyboard-is-open'); document.body.classList.remove('keyboard-is-open');
@ -95,18 +96,18 @@ export class CoreAppProvider {
CoreEvents.instance.trigger(CoreEventsProvider.KEYBOARD_CHANGE, 0); CoreEvents.instance.trigger(CoreEventsProvider.KEYBOARD_CHANGE, 0);
}); });
}); });
Keyboard.instance.onKeyboardWillShow().subscribe((data) => { Keyboard.instance.onKeyboardWillShow().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working. // Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => { zone.run(() => {
this._isKeyboardOpening = true; this.keyboardOpening = true;
this._isKeyboardClosing = false; this.keyboardClosing = false;
}); });
}); });
Keyboard.instance.onKeyboardWillHide().subscribe((data) => { Keyboard.instance.onKeyboardWillHide().subscribe(() => {
// Execute the callback in the Angular zone, so change detection doesn't stop working. // Execute the callback in the Angular zone, so change detection doesn't stop working.
zone.run(() => { zone.run(() => {
this._isKeyboardOpening = false; this.keyboardOpening = false;
this._isKeyboardClosing = true; this.keyboardClosing = true;
}); });
}); });
@ -116,8 +117,8 @@ export class CoreAppProvider {
// Export the app provider and appRef to control the application in Behat tests. // Export the app provider and appRef to control the application in Behat tests.
if (CoreAppProvider.isAutomated()) { if (CoreAppProvider.isAutomated()) {
(<any> window).appProvider = this; (<WindowForAutomatedTests> window).appProvider = this;
(<any> window).appRef = appRef; (<WindowForAutomatedTests> window).appRef = appRef;
} }
} }
@ -145,7 +146,7 @@ export class CoreAppProvider {
* @return Whether the function is supported. * @return Whether the function is supported.
*/ */
canRecordMedia(): boolean { canRecordMedia(): boolean {
return !!(<any> window).MediaRecorder; return !!window.MediaRecorder;
} }
/** /**
@ -163,7 +164,7 @@ export class CoreAppProvider {
* @param schema The schema to create. * @param schema The schema to create.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async createTablesFromSchema(schema: CoreAppSchema): Promise<any> { async createTablesFromSchema(schema: CoreAppSchema): Promise<void> {
this.logger.debug(`Apply schema to app DB: ${schema.name}`); this.logger.debug(`Apply schema to app DB: ${schema.name}`);
let oldVersion; let oldVersion;
@ -173,7 +174,8 @@ export class CoreAppProvider {
await this.createVersionsTableReady; await this.createVersionsTableReady;
// Fetch installed version of the schema. // Fetch installed version of the schema.
const entry = await this.db.getRecord(this.SCHEMA_VERSIONS_TABLE, {name: schema.name}); const entry = await this.db.getRecord<SchemaVersionsDBEntry>(SCHEMA_VERSIONS_TABLE, { name: schema.name });
oldVersion = entry.version; oldVersion = entry.version;
} catch (error) { } catch (error) {
// No installed version yet. // No installed version yet.
@ -195,7 +197,7 @@ export class CoreAppProvider {
} }
// Set installed version. // Set installed version.
await this.db.insertRecord(this.SCHEMA_VERSIONS_TABLE, {name: schema.name, version: schema.version}); await this.db.insertRecord(SCHEMA_VERSIONS_TABLE, { name: schema.name, version: schema.version });
} }
/** /**
@ -260,9 +262,7 @@ export class CoreAppProvider {
* @return Whether the app is running in a 64 bits desktop environment (not browser). * @return Whether the app is running in a 64 bits desktop environment (not browser).
*/ */
is64Bits(): boolean { is64Bits(): boolean {
const process = (<any> window).process; return this.isDesktop() && window.process.arch == 'x64';
return this.isDesktop() && process.arch == 'x64';
} }
/** /**
@ -280,9 +280,8 @@ export class CoreAppProvider {
* @return Whether the app is running in a desktop environment (not browser). * @return Whether the app is running in a desktop environment (not browser).
*/ */
isDesktop(): boolean { isDesktop(): boolean {
const process = (<any> window).process; // @todo
return false;
return !!(process && process.versions && typeof process.versions.electron != 'undefined');
} }
/** /**
@ -300,7 +299,7 @@ export class CoreAppProvider {
* @return Whether keyboard is closing (animating). * @return Whether keyboard is closing (animating).
*/ */
isKeyboardClosing(): boolean { isKeyboardClosing(): boolean {
return this._isKeyboardClosing; return this.keyboardClosing;
} }
/** /**
@ -309,7 +308,7 @@ export class CoreAppProvider {
* @return Whether keyboard is opening (animating). * @return Whether keyboard is opening (animating).
*/ */
isKeyboardOpening(): boolean { isKeyboardOpening(): boolean {
return this._isKeyboardOpening; return this.keyboardOpening;
} }
/** /**
@ -462,8 +461,8 @@ export class CoreAppProvider {
*/ */
protected setKeyboardShown(shown: boolean): void { protected setKeyboardShown(shown: boolean): void {
this.isKeyboardShown = shown; this.isKeyboardShown = shown;
this._isKeyboardOpening = false; this.keyboardOpening = false;
this._isKeyboardClosing = false; this.keyboardClosing = false;
} }
/** /**
@ -487,23 +486,15 @@ export class CoreAppProvider {
* NOT when the browser is opened. * NOT when the browser is opened.
*/ */
startSSOAuthentication(): void { startSSOAuthentication(): void {
let cancelTimeout; this.ssoAuthenticationDeferred = CoreUtils.instance.promiseDefer<void>();
let resolvePromise;
this.ssoAuthenticationPromise = new Promise((resolve, reject): void => {
resolvePromise = resolve;
// Resolve it automatically after 10 seconds (it should never take that long). // Resolve it automatically after 10 seconds (it should never take that long).
cancelTimeout = setTimeout(() => { const cancelTimeout = setTimeout(() => {
this.finishSSOAuthentication(); this.finishSSOAuthentication();
}, 10000); }, 10000);
});
// Store the resolve function in the promise itself.
(<any> this.ssoAuthenticationPromise).resolve = resolvePromise;
// If the promise is resolved because finishSSOAuthentication is called, stop the cancel promise. // If the promise is resolved because finishSSOAuthentication is called, stop the cancel promise.
this.ssoAuthenticationPromise.then(() => { this.ssoAuthenticationDeferred.promise.then(() => {
clearTimeout(cancelTimeout); clearTimeout(cancelTimeout);
}); });
} }
@ -512,9 +503,9 @@ export class CoreAppProvider {
* Finish an SSO authentication process. * Finish an SSO authentication process.
*/ */
finishSSOAuthentication(): void { finishSSOAuthentication(): void {
if (this.ssoAuthenticationPromise) { if (this.ssoAuthenticationDeferred) {
(<any> this.ssoAuthenticationPromise).resolve && (<any> this.ssoAuthenticationPromise).resolve(); this.ssoAuthenticationDeferred.resolve();
this.ssoAuthenticationPromise = undefined; this.ssoAuthenticationDeferred = undefined;
} }
} }
@ -524,7 +515,7 @@ export class CoreAppProvider {
* @return Whether there's a SSO authentication ongoing. * @return Whether there's a SSO authentication ongoing.
*/ */
isSSOAuthenticationOngoing(): boolean { isSSOAuthenticationOngoing(): boolean {
return !!this.ssoAuthenticationPromise; return !!this.ssoAuthenticationDeferred;
} }
/** /**
@ -532,8 +523,8 @@ export class CoreAppProvider {
* *
* @return Promise resolved once SSO authentication finishes. * @return Promise resolved once SSO authentication finishes.
*/ */
waitForSSOAuthentication(): Promise<any> { async waitForSSOAuthentication(): Promise<void> {
return this.ssoAuthenticationPromise || Promise.resolve(); await this.ssoAuthenticationDeferred && this.ssoAuthenticationDeferred.promise;
} }
/** /**
@ -542,27 +533,24 @@ export class CoreAppProvider {
* @param timeout Maximum time to wait, use null to wait forever. * @param timeout Maximum time to wait, use null to wait forever.
*/ */
async waitForResume(timeout: number | null = null): Promise<void> { async waitForResume(timeout: number | null = null): Promise<void> {
let resolve: (value?: any) => void; let deferred = CoreUtils.instance.promiseDefer<void>();
let resumeSubscription: any;
let timeoutId: NodeJS.Timer | false;
const promise = new Promise((r): any => resolve = r); const stopWaiting = () => {
const stopWaiting = (): any => { if (!deferred) {
if (!resolve) {
return; return;
} }
resolve(); deferred.resolve();
resumeSubscription.unsubscribe(); resumeSubscription.unsubscribe();
timeoutId && clearTimeout(timeoutId); timeoutId && clearTimeout(timeoutId);
resolve = null; deferred = null;
}; };
resumeSubscription = Platform.instance.resume.subscribe(stopWaiting); const resumeSubscription = Platform.instance.resume.subscribe(stopWaiting);
timeoutId = timeout ? setTimeout(stopWaiting, timeout) : false; const timeoutId = timeout ? setTimeout(stopWaiting, timeout) : false;
await promise; await deferred.promise;
} }
/** /**
@ -626,23 +614,19 @@ export class CoreAppProvider {
* button is pressed. This method decides which of the registered back button * button is pressed. This method decides which of the registered back button
* actions has the highest priority and should be called. * actions has the highest priority and should be called.
* *
* @param fn Called when the back button is pressed, * @param callback Called when the back button is pressed, if this registered action has the highest priority.
* if this registered action has the highest priority.
* @param priority Set the priority for this action. All actions sorted by priority will be executed since one of * @param priority Set the priority for this action. All actions sorted by priority will be executed since one of
* them returns true. * them returns true.
* * Priorities higher or equal than 1000 will go before closing modals * - Priorities higher or equal than 1000 will go before closing modals
* * Priorities lower than 500 will only be executed if you are in the first state of the app (before exit). * - Priorities lower than 500 will only be executed if you are in the first state of the app (before exit).
* @return A function that, when called, will unregister * @return A function that, when called, will unregister the back button action.
* the back button action.
*/ */
registerBackButtonAction(fn: any, priority: number = 0): any { registerBackButtonAction(callback: () => boolean, priority: number = 0): () => boolean {
const action = { fn, priority }; const action = { callback, priority };
this.backActions.push(action); this.backActions.push(action);
this.backActions.sort((a, b) => { this.backActions.sort((a, b) => b.priority - a.priority);
return b.priority - a.priority;
});
return (): boolean => { return (): boolean => {
const index = this.backActions.indexOf(action); const index = this.backActions.indexOf(action);
@ -700,6 +684,7 @@ export class CoreAppProvider {
setForceOffline(value: boolean): void { setForceOffline(value: boolean): void {
this.forceOffline = !!value; this.forceOffline = !!value;
} }
} }
export class CoreApp extends makeSingleton(CoreAppProvider) {} export class CoreApp extends makeSingleton(CoreAppProvider) {}
@ -802,5 +787,18 @@ export type CoreAppSchema = {
* @param oldVersion Old version of the schema or 0 if not installed. * @param oldVersion Old version of the schema or 0 if not installed.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
migrate?(db: SQLiteDB, oldVersion: number): Promise<any>; migrate?(db: SQLiteDB, oldVersion: number): Promise<void>;
};
/**
* Extended window type for automated tests.
*/
export type WindowForAutomatedTests = Window & {
appProvider?: CoreAppProvider;
appRef?: ApplicationRef;
};
type SchemaVersionsDBEntry = {
name: string;
version: number;
}; };

View File

@ -18,36 +18,38 @@ import { CoreApp, CoreAppSchema } from '@services/app';
import { SQLiteDB } from '@classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
import { makeSingleton } from '@singletons/core.singletons'; import { makeSingleton } from '@singletons/core.singletons';
const TABLE_NAME = 'core_config';
/** /**
* Factory to provide access to dynamic and permanent config and settings. * Factory to provide access to dynamic and permanent config and settings.
* It should not be abused into a temporary storage. * It should not be abused into a temporary storage.
*/ */
@Injectable() @Injectable()
export class CoreConfigProvider { export class CoreConfigProvider {
protected appDB: SQLiteDB; protected appDB: SQLiteDB;
protected TABLE_NAME = 'core_config';
protected tableSchema: CoreAppSchema = { protected tableSchema: CoreAppSchema = {
name: 'CoreConfigProvider', name: 'CoreConfigProvider',
version: 1, version: 1,
tables: [ tables: [
{ {
name: this.TABLE_NAME, name: TABLE_NAME,
columns: [ columns: [
{ {
name: 'name', name: 'name',
type: 'TEXT', type: 'TEXT',
unique: true, unique: true,
notNull: true notNull: true,
}, },
{ {
name: 'value' name: 'value',
}, },
], ],
}, },
], ],
}; };
protected dbReady: Promise<any>; // Promise resolved when the app DB is initialized. protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized.
constructor() { constructor() {
this.appDB = CoreApp.instance.getDB(); this.appDB = CoreApp.instance.getDB();
@ -62,10 +64,10 @@ export class CoreConfigProvider {
* @param name The config name. * @param name The config name.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async delete(name: string): Promise<any> { async delete(name: string): Promise<void> {
await this.dbReady; await this.dbReady;
return this.appDB.deleteRecords(this.TABLE_NAME, { name }); await this.appDB.deleteRecords(TABLE_NAME, { name });
} }
/** /**
@ -75,11 +77,11 @@ export class CoreConfigProvider {
* @param defaultValue Default value to use if the entry is not found. * @param defaultValue Default value to use if the entry is not found.
* @return Resolves upon success along with the config data. Reject on failure. * @return Resolves upon success along with the config data. Reject on failure.
*/ */
async get(name: string, defaultValue?: any): Promise<any> { async get<T>(name: string, defaultValue?: T): Promise<T> {
await this.dbReady; await this.dbReady;
try { try {
const entry = await this.appDB.getRecord(this.TABLE_NAME, { name }); const entry = await this.appDB.getRecord<ConfigDBEntry>(TABLE_NAME, { name });
return entry.value; return entry.value;
} catch (error) { } catch (error) {
@ -98,11 +100,18 @@ export class CoreConfigProvider {
* @param value The config value. Can only store number or strings. * @param value The config value. Can only store number or strings.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async set(name: string, value: number | string): Promise<any> { async set(name: string, value: number | string): Promise<void> {
await this.dbReady; await this.dbReady;
return this.appDB.insertRecord(this.TABLE_NAME, { name, value }); await this.appDB.insertRecord(TABLE_NAME, { name, value });
} }
} }
export class CoreConfig extends makeSingleton(CoreConfigProvider) {} export class CoreConfig extends makeSingleton(CoreConfigProvider) {}
type ConfigDBEntry = {
name: string;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
value: any;
};

View File

@ -19,107 +19,52 @@ import { CoreConfig } from '@services/config';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { SQLiteDB } from '@classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
import { CoreError } from '@classes/errors/error';
import { makeSingleton, Network } from '@singletons/core.singletons'; import { makeSingleton, Network } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
/** const CRON_TABLE = 'cron';
* Interface that all cron handlers must implement.
*/
export interface CoreCronHandler {
/**
* A name to identify the handler.
*/
name: string;
/**
* Whether the handler is running. Used internally by the provider, there's no need to set it.
*/
running?: boolean;
/**
* Timeout ID for the handler scheduling. Used internally by the provider, there's no need to set it.
*/
timeout?: number;
/**
* Returns handler's interval in milliseconds. Defaults to CoreCronDelegate.DEFAULT_INTERVAL.
*
* @return Interval time (in milliseconds).
*/
getInterval?(): number;
/**
* Check whether the process uses network or not. True if not defined.
*
* @return Whether the process uses network or not
*/
usesNetwork?(): boolean;
/**
* Check whether it's a synchronization process or not. True if not defined.
*
* @return Whether it's a synchronization process or not.
*/
isSync?(): boolean;
/**
* Check whether the sync can be executed manually. Call isSync if not defined.
*
* @return Whether the sync can be executed manually.
*/
canManualSync?(): boolean;
/**
* Execute the process.
*
* @param siteId ID of the site affected. If not defined, all sites.
* @param force Determines if it's a forced execution.
* @return Promise resolved when done. If the promise is rejected, this function will be called again often,
* it shouldn't be abused.
*/
execute?(siteId?: string, force?: boolean): Promise<any>;
}
/* /*
* Service to handle cron processes. The registered processes will be executed every certain time. * Service to handle cron processes. The registered processes will be executed every certain time.
*/ */
@Injectable() @Injectable()
export class CoreCronDelegate { export class CoreCronDelegate {
// Constants. // Constants.
static DEFAULT_INTERVAL = 3600000; // Default interval is 1 hour. static readonly DEFAULT_INTERVAL = 3600000; // Default interval is 1 hour.
static MIN_INTERVAL = 300000; // Minimum interval is 5 minutes. static readonly MIN_INTERVAL = 300000; // Minimum interval is 5 minutes.
static DESKTOP_MIN_INTERVAL = 60000; // Minimum interval in desktop is 1 minute. static readonly DESKTOP_MIN_INTERVAL = 60000; // Minimum interval in desktop is 1 minute.
static MAX_TIME_PROCESS = 120000; // Max time a process can block the queue. Defaults to 2 minutes. static readonly MAX_TIME_PROCESS = 120000; // Max time a process can block the queue. Defaults to 2 minutes.
// Variables for database. // Variables for database.
protected CRON_TABLE = 'cron';
protected tableSchema: CoreAppSchema = { protected tableSchema: CoreAppSchema = {
name: 'CoreCronDelegate', name: 'CoreCronDelegate',
version: 1, version: 1,
tables: [ tables: [
{ {
name: this.CRON_TABLE, name: CRON_TABLE,
columns: [ columns: [
{ {
name: 'id', name: 'id',
type: 'TEXT', type: 'TEXT',
primaryKey: true primaryKey: true,
}, },
{ {
name: 'value', name: 'value',
type: 'INTEGER' type: 'INTEGER',
}, },
], ],
}, },
], ],
}; };
protected logger; protected logger: CoreLogger;
protected appDB: SQLiteDB; protected appDB: SQLiteDB;
protected dbReady: Promise<any>; // Promise resolved when the app DB is initialized. protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized.
protected handlers: { [s: string]: CoreCronHandler } = {}; protected handlers: { [s: string]: CoreCronHandler } = {};
protected queuePromise = Promise.resolve(); protected queuePromise: Promise<void> = Promise.resolve();
constructor(zone: NgZone) { constructor(zone: NgZone) {
this.logger = CoreLogger.getInstance('CoreCronDelegate'); this.logger = CoreLogger.getInstance('CoreCronDelegate');
@ -139,7 +84,7 @@ export class CoreCronDelegate {
// Export the sync provider so Behat tests can trigger cron tasks without waiting. // Export the sync provider so Behat tests can trigger cron tasks without waiting.
if (CoreAppProvider.isAutomated()) { if (CoreAppProvider.isAutomated()) {
(<any> window).cronProvider = this; (<WindowForAutomatedTests> window).cronProvider = this;
} }
} }
@ -152,12 +97,13 @@ export class CoreCronDelegate {
* @param siteId Site ID. If not defined, all sites. * @param siteId Site ID. If not defined, all sites.
* @return Promise resolved if handler is executed successfully, rejected otherwise. * @return Promise resolved if handler is executed successfully, rejected otherwise.
*/ */
protected checkAndExecuteHandler(name: string, force?: boolean, siteId?: string): Promise<any> { protected checkAndExecuteHandler(name: string, force?: boolean, siteId?: string): Promise<void> {
if (!this.handlers[name] || !this.handlers[name].execute) { if (!this.handlers[name] || !this.handlers[name].execute) {
// Invalid handler. // Invalid handler.
this.logger.debug('Cannot execute handler because is invalid: ' + name); const message = `Cannot execute handler because is invalid: ${name}`;
this.logger.debug(message);
return Promise.reject(null); return Promise.reject(new CoreError(message));
} }
const usesNetwork = this.handlerUsesNetwork(name); const usesNetwork = this.handlerUsesNetwork(name);
@ -166,17 +112,17 @@ export class CoreCronDelegate {
if (usesNetwork && !CoreApp.instance.isOnline()) { if (usesNetwork && !CoreApp.instance.isOnline()) {
// Offline, stop executing. // Offline, stop executing.
this.logger.debug('Cannot execute handler because device is offline: ' + name); const message = `Cannot execute handler because device is offline: ${name}`;
this.logger.debug(message);
this.stopHandler(name); this.stopHandler(name);
return Promise.reject(null); return Promise.reject(new CoreError(message));
} }
if (isSync) { if (isSync) {
// Check network connection. // Check network connection.
promise = CoreConfig.instance.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false).then((syncOnlyOnWifi) => { promise = CoreConfig.instance.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false)
return !syncOnlyOnWifi || CoreApp.instance.isWifi(); .then((syncOnlyOnWifi) => !syncOnlyOnWifi || CoreApp.instance.isWifi());
});
} else { } else {
promise = Promise.resolve(true); promise = Promise.resolve(true);
} }
@ -184,17 +130,17 @@ export class CoreCronDelegate {
return promise.then((execute: boolean) => { return promise.then((execute: boolean) => {
if (!execute) { if (!execute) {
// Cannot execute in this network connection, retry soon. // Cannot execute in this network connection, retry soon.
this.logger.debug('Cannot execute handler because device is using limited connection: ' + name); const message = `Cannot execute handler because device is using limited connection: ${name}`;
this.logger.debug(message);
this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL); this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL);
return Promise.reject(null); return Promise.reject(new CoreError(message));
} }
// Add the execution to the queue. // Add the execution to the queue.
this.queuePromise = this.queuePromise.catch(() => { this.queuePromise = this.queuePromise.catch(() => {
// Ignore errors in previous handlers. // Ignore errors in previous handlers.
}).then(() => { }).then(() => this.executeHandler(name, force, siteId).then(() => {
return this.executeHandler(name, force, siteId).then(() => {
this.logger.debug(`Execution of handler '${name}' was a success.`); this.logger.debug(`Execution of handler '${name}' was a success.`);
return this.setHandlerLastExecutionTime(name, Date.now()).then(() => { return this.setHandlerLastExecutionTime(name, Date.now()).then(() => {
@ -202,12 +148,12 @@ export class CoreCronDelegate {
}); });
}, (error) => { }, (error) => {
// Handler call failed. Retry soon. // Handler call failed. Retry soon.
this.logger.error(`Execution of handler '${name}' failed.`, error); const message = `Execution of handler '${name}' failed.`;
this.logger.error(message, error);
this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL); this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL);
return Promise.reject(null); return Promise.reject(new CoreError(message));
}); }));
});
return this.queuePromise; return this.queuePromise;
}); });
@ -221,10 +167,8 @@ export class CoreCronDelegate {
* @param siteId Site ID. If not defined, all sites. * @param siteId Site ID. If not defined, all sites.
* @return Promise resolved when the handler finishes or reaches max time, rejected if it fails. * @return Promise resolved when the handler finishes or reaches max time, rejected if it fails.
*/ */
protected executeHandler(name: string, force?: boolean, siteId?: string): Promise<any> { protected executeHandler(name: string, force?: boolean, siteId?: string): Promise<void> {
return new Promise((resolve, reject): void => { return new Promise((resolve, reject): void => {
let cancelTimeout;
this.logger.debug('Executing handler: ' + name); this.logger.debug('Executing handler: ' + name);
// Wrap the call in Promise.resolve to make sure it's a promise. // Wrap the call in Promise.resolve to make sure it's a promise.
@ -232,7 +176,7 @@ export class CoreCronDelegate {
clearTimeout(cancelTimeout); clearTimeout(cancelTimeout);
}); });
cancelTimeout = setTimeout(() => { const cancelTimeout = setTimeout(() => {
// The handler took too long. Resolve because we don't want to retry soon. // The handler took too long. Resolve because we don't want to retry soon.
this.logger.debug(`Resolving execution of handler '${name}' because it took too long.`); this.logger.debug(`Resolving execution of handler '${name}' because it took too long.`);
resolve(); resolve();
@ -247,7 +191,7 @@ export class CoreCronDelegate {
* @param siteId Site ID. If not defined, all sites. * @param siteId Site ID. If not defined, all sites.
* @return Promise resolved if all handlers are executed successfully, rejected otherwise. * @return Promise resolved if all handlers are executed successfully, rejected otherwise.
*/ */
forceSyncExecution(siteId?: string): Promise<any> { async forceSyncExecution(siteId?: string): Promise<void> {
const promises = []; const promises = [];
for (const name in this.handlers) { for (const name in this.handlers) {
@ -257,7 +201,7 @@ export class CoreCronDelegate {
} }
} }
return CoreUtils.instance.allPromises(promises); await CoreUtils.instance.allPromises(promises);
} }
/** /**
@ -268,7 +212,7 @@ export class CoreCronDelegate {
* @param siteId Site ID. If not defined, all sites. * @param siteId Site ID. If not defined, all sites.
* @return Promise resolved if handler has been executed successfully, rejected otherwise. * @return Promise resolved if handler has been executed successfully, rejected otherwise.
*/ */
forceCronHandlerExecution(name?: string, siteId?: string): Promise<any> { forceCronHandlerExecution(name?: string, siteId?: string): Promise<void> {
const handler = this.handlers[name]; const handler = this.handlers[name];
// Mark the handler as running (it might be running already). // Mark the handler as running (it might be running already).
@ -327,8 +271,9 @@ export class CoreCronDelegate {
const id = this.getHandlerLastExecutionId(name); const id = this.getHandlerLastExecutionId(name);
try { try {
const entry = await this.appDB.getRecord(this.CRON_TABLE, { id }); const entry = await this.appDB.getRecord<CronDBEntry>(CRON_TABLE, { id });
const time = parseInt(entry.value, 10);
const time = Number(entry.value);
return isNaN(time) ? 0 : time; return isNaN(time) ? 0 : time;
} catch (err) { } catch (err) {
@ -489,16 +434,16 @@ export class CoreCronDelegate {
* @param time Time to set. * @param time Time to set.
* @return Promise resolved when the execution time is saved. * @return Promise resolved when the execution time is saved.
*/ */
protected async setHandlerLastExecutionTime(name: string, time: number): Promise<any> { protected async setHandlerLastExecutionTime(name: string, time: number): Promise<void> {
await this.dbReady; await this.dbReady;
const id = this.getHandlerLastExecutionId(name); const id = this.getHandlerLastExecutionId(name);
const entry = { const entry = {
id, id,
value: time value: time,
}; };
return this.appDB.insertRecord(this.CRON_TABLE, entry); await this.appDB.insertRecord(CRON_TABLE, entry);
} }
/** /**
@ -559,6 +504,78 @@ export class CoreCronDelegate {
clearTimeout(this.handlers[name].timeout); clearTimeout(this.handlers[name].timeout);
delete this.handlers[name].timeout; delete this.handlers[name].timeout;
} }
} }
export class CoreCron extends makeSingleton(CoreCronDelegate) {} export class CoreCron extends makeSingleton(CoreCronDelegate) {}
/**
* Interface that all cron handlers must implement.
*/
export interface CoreCronHandler {
/**
* A name to identify the handler.
*/
name: string;
/**
* Whether the handler is running. Used internally by the provider, there's no need to set it.
*/
running?: boolean;
/**
* Timeout ID for the handler scheduling. Used internally by the provider, there's no need to set it.
*/
timeout?: number;
/**
* Returns handler's interval in milliseconds. Defaults to CoreCronDelegate.DEFAULT_INTERVAL.
*
* @return Interval time (in milliseconds).
*/
getInterval?(): number;
/**
* Check whether the process uses network or not. True if not defined.
*
* @return Whether the process uses network or not
*/
usesNetwork?(): boolean;
/**
* Check whether it's a synchronization process or not. True if not defined.
*
* @return Whether it's a synchronization process or not.
*/
isSync?(): boolean;
/**
* Check whether the sync can be executed manually. Call isSync if not defined.
*
* @return Whether the sync can be executed manually.
*/
canManualSync?(): boolean;
/**
* Execute the process.
*
* @param siteId ID of the site affected. If not defined, all sites.
* @param force Determines if it's a forced execution.
* @return Promise resolved when done. If the promise is rejected, this function will be called again often,
* it shouldn't be abused.
*/
execute?(siteId?: string, force?: boolean): Promise<void>;
}
/**
* Extended window type for automated tests.
*/
export type WindowForAutomatedTests = Window & {
cronProvider?: CoreCronDelegate;
};
type CronDBEntry = {
id: string;
value: number;
};

View File

@ -24,9 +24,7 @@ import { makeSingleton, SQLite, Platform } from '@singletons/core.singletons';
@Injectable() @Injectable()
export class CoreDbProvider { export class CoreDbProvider {
protected dbInstances = {}; protected dbInstances: {[name: string]: SQLiteDB} = {};
constructor() { }
/** /**
* Get or create a database object. * Get or create a database object.
@ -55,31 +53,31 @@ export class CoreDbProvider {
* @param name DB name. * @param name DB name.
* @return Promise resolved when the DB is deleted. * @return Promise resolved when the DB is deleted.
*/ */
deleteDB(name: string): Promise<any> { async deleteDB(name: string): Promise<void> {
let promise;
if (typeof this.dbInstances[name] != 'undefined') { if (typeof this.dbInstances[name] != 'undefined') {
// Close the database first. // Close the database first.
promise = this.dbInstances[name].close(); await this.dbInstances[name].close();
} else {
promise = Promise.resolve();
}
return promise.then(() => {
const db = this.dbInstances[name]; const db = this.dbInstances[name];
delete this.dbInstances[name]; delete this.dbInstances[name];
if (Platform.instance.is('cordova')) { if (db instanceof SQLiteDBMock) {
return SQLite.instance.deleteDatabase({
name,
location: 'default'
});
} else {
// In WebSQL we cannot delete the database, just empty it. // In WebSQL we cannot delete the database, just empty it.
return db.emptyDatabase(); return db.emptyDatabase();
} else {
return SQLite.instance.deleteDatabase({
name,
location: 'default',
});
} }
} else if (Platform.instance.is('cordova')) {
return SQLite.instance.deleteDatabase({
name,
location: 'default',
}); });
} }
} }
}
export class CoreDB extends makeSingleton(CoreDbProvider) {} export class CoreDB extends makeSingleton(CoreDbProvider) {}

View File

@ -33,46 +33,47 @@ export interface CoreEventObserver {
*/ */
@Injectable() @Injectable()
export class CoreEventsProvider { export class CoreEventsProvider {
static SESSION_EXPIRED = 'session_expired';
static PASSWORD_CHANGE_FORCED = 'password_change_forced'; static readonly SESSION_EXPIRED = 'session_expired';
static USER_NOT_FULLY_SETUP = 'user_not_fully_setup'; static readonly PASSWORD_CHANGE_FORCED = 'password_change_forced';
static SITE_POLICY_NOT_AGREED = 'site_policy_not_agreed'; static readonly USER_NOT_FULLY_SETUP = 'user_not_fully_setup';
static LOGIN = 'login'; static readonly SITE_POLICY_NOT_AGREED = 'site_policy_not_agreed';
static LOGOUT = 'logout'; static readonly LOGIN = 'login';
static LANGUAGE_CHANGED = 'language_changed'; static readonly LOGOUT = 'logout';
static NOTIFICATION_SOUND_CHANGED = 'notification_sound_changed'; static readonly LANGUAGE_CHANGED = 'language_changed';
static SITE_ADDED = 'site_added'; static readonly NOTIFICATION_SOUND_CHANGED = 'notification_sound_changed';
static SITE_UPDATED = 'site_updated'; static readonly SITE_ADDED = 'site_added';
static SITE_DELETED = 'site_deleted'; static readonly SITE_UPDATED = 'site_updated';
static COMPLETION_MODULE_VIEWED = 'completion_module_viewed'; static readonly SITE_DELETED = 'site_deleted';
static USER_DELETED = 'user_deleted'; static readonly COMPLETION_MODULE_VIEWED = 'completion_module_viewed';
static PACKAGE_STATUS_CHANGED = 'package_status_changed'; static readonly USER_DELETED = 'user_deleted';
static COURSE_STATUS_CHANGED = 'course_status_changed'; static readonly PACKAGE_STATUS_CHANGED = 'package_status_changed';
static SECTION_STATUS_CHANGED = 'section_status_changed'; static readonly COURSE_STATUS_CHANGED = 'course_status_changed';
static COMPONENT_FILE_ACTION = 'component_file_action'; static readonly SECTION_STATUS_CHANGED = 'section_status_changed';
static SITE_PLUGINS_LOADED = 'site_plugins_loaded'; static readonly COMPONENT_FILE_ACTION = 'component_file_action';
static SITE_PLUGINS_COURSE_RESTRICT_UPDATED = 'site_plugins_course_restrict_updated'; static readonly SITE_PLUGINS_LOADED = 'site_plugins_loaded';
static LOGIN_SITE_CHECKED = 'login_site_checked'; static readonly SITE_PLUGINS_COURSE_RESTRICT_UPDATED = 'site_plugins_course_restrict_updated';
static LOGIN_SITE_UNCHECKED = 'login_site_unchecked'; static readonly LOGIN_SITE_CHECKED = 'login_site_checked';
static IAB_LOAD_START = 'inappbrowser_load_start'; static readonly LOGIN_SITE_UNCHECKED = 'login_site_unchecked';
static IAB_EXIT = 'inappbrowser_exit'; static readonly IAB_LOAD_START = 'inappbrowser_load_start';
static APP_LAUNCHED_URL = 'app_launched_url'; // App opened with a certain URL (custom URL scheme). static readonly IAB_EXIT = 'inappbrowser_exit';
static FILE_SHARED = 'file_shared'; static readonly APP_LAUNCHED_URL = 'app_launched_url'; // App opened with a certain URL (custom URL scheme).
static KEYBOARD_CHANGE = 'keyboard_change'; static readonly FILE_SHARED = 'file_shared';
static CORE_LOADING_CHANGED = 'core_loading_changed'; static readonly KEYBOARD_CHANGE = 'keyboard_change';
static ORIENTATION_CHANGE = 'orientation_change'; static readonly CORE_LOADING_CHANGED = 'core_loading_changed';
static LOAD_PAGE_MAIN_MENU = 'load_page_main_menu'; static readonly ORIENTATION_CHANGE = 'orientation_change';
static SEND_ON_ENTER_CHANGED = 'send_on_enter_changed'; static readonly LOAD_PAGE_MAIN_MENU = 'load_page_main_menu';
static MAIN_MENU_OPEN = 'main_menu_open'; static readonly SEND_ON_ENTER_CHANGED = 'send_on_enter_changed';
static SELECT_COURSE_TAB = 'select_course_tab'; static readonly MAIN_MENU_OPEN = 'main_menu_open';
static WS_CACHE_INVALIDATED = 'ws_cache_invalidated'; static readonly SELECT_COURSE_TAB = 'select_course_tab';
static SITE_STORAGE_DELETED = 'site_storage_deleted'; static readonly WS_CACHE_INVALIDATED = 'ws_cache_invalidated';
static FORM_ACTION = 'form_action'; static readonly SITE_STORAGE_DELETED = 'site_storage_deleted';
static ACTIVITY_DATA_SENT = 'activity_data_sent'; static readonly FORM_ACTION = 'form_action';
static readonly ACTIVITY_DATA_SENT = 'activity_data_sent';
protected logger: CoreLogger; protected logger: CoreLogger;
protected observables: { [s: string]: Subject<any> } = {}; protected observables: { [eventName: string]: Subject<unknown> } = {};
protected uniqueEvents = {}; protected uniqueEvents: { [eventName: string]: {data: unknown} } = {};
constructor() { constructor() {
this.logger = CoreLogger.getInstance('CoreEventsProvider'); this.logger = CoreLogger.getInstance('CoreEventsProvider');
@ -89,7 +90,7 @@ export class CoreEventsProvider {
* @param siteId Site where to trigger the event. Undefined won't check the site. * @param siteId Site where to trigger the event. Undefined won't check the site.
* @return Observer to stop listening. * @return Observer to stop listening.
*/ */
on(eventName: string, callBack: (value: any) => void, siteId?: string): CoreEventObserver { on(eventName: string, callBack: (value: unknown) => void, siteId?: string): CoreEventObserver {
// If it's a unique event and has been triggered already, call the callBack. // If it's a unique event and has been triggered already, call the callBack.
// We don't need to create an observer because the event won't be triggered again. // We don't need to create an observer because the event won't be triggered again.
if (this.uniqueEvents[eventName]) { if (this.uniqueEvents[eventName]) {
@ -99,7 +100,7 @@ export class CoreEventsProvider {
return { return {
off: (): void => { off: (): void => {
// Nothing to do. // Nothing to do.
} },
}; };
} }
@ -107,10 +108,10 @@ export class CoreEventsProvider {
if (typeof this.observables[eventName] == 'undefined') { if (typeof this.observables[eventName] == 'undefined') {
// No observable for this event, create a new one. // No observable for this event, create a new one.
this.observables[eventName] = new Subject<any>(); this.observables[eventName] = new Subject<unknown>();
} }
const subscription = this.observables[eventName].subscribe((value: any) => { const subscription = this.observables[eventName].subscribe((value: {siteId?: string; [key: string]: unknown}) => {
if (!siteId || value.siteId == siteId) { if (!siteId || value.siteId == siteId) {
callBack(value); callBack(value);
} }
@ -121,7 +122,7 @@ export class CoreEventsProvider {
off: (): void => { off: (): void => {
this.logger.debug(`Stop listening to event '${eventName}'`); this.logger.debug(`Stop listening to event '${eventName}'`);
subscription.unsubscribe(); subscription.unsubscribe();
} },
}; };
} }
@ -136,11 +137,8 @@ export class CoreEventsProvider {
* @param siteId Site where to trigger the event. Undefined won't check the site. * @param siteId Site where to trigger the event. Undefined won't check the site.
* @return Observer to stop listening. * @return Observer to stop listening.
*/ */
onMultiple(eventNames: string[], callBack: (value: any) => void, siteId?: string): CoreEventObserver { onMultiple(eventNames: string[], callBack: (value: unknown) => void, siteId?: string): CoreEventObserver {
const observers = eventNames.map((name) => this.on(name, callBack, siteId));
const observers = eventNames.map((name) => {
return this.on(name, callBack, siteId);
});
// Create and return a CoreEventObserver. // Create and return a CoreEventObserver.
return { return {
@ -148,7 +146,7 @@ export class CoreEventsProvider {
observers.forEach((observer) => { observers.forEach((observer) => {
observer.off(); observer.off();
}); });
} },
}; };
} }
@ -159,14 +157,11 @@ export class CoreEventsProvider {
* @param data Data to pass to the observers. * @param data Data to pass to the observers.
* @param siteId Site where to trigger the event. Undefined means no Site. * @param siteId Site where to trigger the event. Undefined means no Site.
*/ */
trigger(eventName: string, data?: any, siteId?: string): void { trigger(eventName: string, data?: unknown, siteId?: string): void {
this.logger.debug(`Event '${eventName}' triggered.`); this.logger.debug(`Event '${eventName}' triggered.`);
if (this.observables[eventName]) { if (this.observables[eventName]) {
if (siteId) { if (siteId) {
if (!data) { data = Object.assign(data || {}, { siteId });
data = {};
}
data.siteId = siteId;
} }
this.observables[eventName].next(data); this.observables[eventName].next(data);
} }
@ -179,17 +174,14 @@ export class CoreEventsProvider {
* @param data Data to pass to the observers. * @param data Data to pass to the observers.
* @param siteId Site where to trigger the event. Undefined means no Site. * @param siteId Site where to trigger the event. Undefined means no Site.
*/ */
triggerUnique(eventName: string, data: any, siteId?: string): void { triggerUnique(eventName: string, data: unknown, siteId?: string): void {
if (this.uniqueEvents[eventName]) { if (this.uniqueEvents[eventName]) {
this.logger.debug(`Unique event '${eventName}' ignored because it was already triggered.`); this.logger.debug(`Unique event '${eventName}' ignored because it was already triggered.`);
} else { } else {
this.logger.debug(`Unique event '${eventName}' triggered.`); this.logger.debug(`Unique event '${eventName}' triggered.`);
if (siteId) { if (siteId) {
if (!data) { data = Object.assign(data || {}, { siteId });
data = {};
}
data.siteId = siteId;
} }
// Store the data so it can be passed to observers that register from now on. // Store the data so it can be passed to observers that register from now on.
@ -203,6 +195,7 @@ export class CoreEventsProvider {
} }
} }
} }
} }
export class CoreEvents extends makeSingleton(CoreEventsProvider) {} export class CoreEvents extends makeSingleton(CoreEventsProvider) {}

View File

@ -13,16 +13,18 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { FileEntry } from '@ionic-native/file';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreFile } from '@services/file'; import { CoreFile } from '@services/file';
import { CoreFilepool } from '@services/filepool'; import { CoreFilepool } from '@services/filepool';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreWS } from '@services/ws'; import { CoreWS, CoreWSExternalFile } from '@services/ws';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreUrlUtils } from '@services/utils/url'; import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils } from '@services/utils/utils';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { CoreError } from '@classes/errors/error';
import { makeSingleton, Translate } from '@singletons/core.singletons'; import { makeSingleton, Translate } from '@singletons/core.singletons';
/** /**
@ -42,8 +44,8 @@ export class CoreFileHelperProvider {
* @param siteId The site ID. If not defined, current site. * @param siteId The site ID. If not defined, current site.
* @return Resolved on success. * @return Resolved on success.
*/ */
async downloadAndOpenFile(file: any, component: string, componentId: string | number, state?: string, async downloadAndOpenFile(file: CoreWSExternalFile, component: string, componentId: string | number, state?: string,
onProgress?: (event: any) => any, siteId?: string): Promise<void> { onProgress?: CoreFileHelperOnProgress, siteId?: string): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId(); siteId = siteId || CoreSites.instance.getCurrentSiteId();
const fileUrl = this.getFileUrl(file); const fileUrl = this.getFileUrl(file);
@ -80,7 +82,7 @@ export class CoreFileHelperProvider {
} }
if (state == CoreConstants.DOWNLOADING) { if (state == CoreConstants.DOWNLOADING) {
throw new Error(Translate.instance.instant('core.erroropenfiledownloading')); throw new CoreError(Translate.instance.instant('core.erroropenfiledownloading'));
} }
if (state === CoreConstants.NOT_DOWNLOADED) { if (state === CoreConstants.NOT_DOWNLOADED) {
@ -109,14 +111,11 @@ export class CoreFileHelperProvider {
* @param siteId The site ID. If not defined, current site. * @param siteId The site ID. If not defined, current site.
* @return Resolved with the URL to use on success. * @return Resolved with the URL to use on success.
*/ */
protected downloadFileIfNeeded(file: any, fileUrl: string, component?: string, componentId?: string | number, protected downloadFileIfNeeded(file: CoreWSExternalFile, fileUrl: string, component?: string, componentId?: string | number,
timemodified?: number, state?: string, onProgress?: (event: any) => any, siteId?: string): Promise<string> { timemodified?: number, state?: string, onProgress?: CoreFileHelperOnProgress, siteId?: string): Promise<string> {
siteId = siteId || CoreSites.instance.getCurrentSiteId(); siteId = siteId || CoreSites.instance.getCurrentSiteId();
return CoreSites.instance.getSite(siteId).then((site) => { return CoreSites.instance.getSite(siteId).then((site) => site.checkAndFixPluginfileURL(fileUrl)).then((fixedUrl) => {
return site.checkAndFixPluginfileURL(fileUrl);
}).then((fixedUrl) => {
if (CoreFile.instance.isAvailable()) { if (CoreFile.instance.isAvailable()) {
let promise; let promise;
if (state) { if (state) {
@ -138,7 +137,7 @@ export class CoreFileHelperProvider {
} else { } else {
if (!isOnline && !this.isStateDownloaded(state)) { if (!isOnline && !this.isStateDownloaded(state)) {
// Not downloaded and user is offline, reject. // Not downloaded and user is offline, reject.
return Promise.reject(Translate.instance.instant('core.networkerrormsg')); return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg')));
} }
if (onProgress) { if (onProgress) {
@ -191,27 +190,27 @@ export class CoreFileHelperProvider {
* @return Resolved with internal URL on success, rejected otherwise. * @return Resolved with internal URL on success, rejected otherwise.
*/ */
downloadFile(fileUrl: string, component?: string, componentId?: string | number, timemodified?: number, downloadFile(fileUrl: string, component?: string, componentId?: string | number, timemodified?: number,
onProgress?: (event: any) => any, file?: any, siteId?: string): Promise<string> { onProgress?: (event: ProgressEvent) => void, file?: CoreWSExternalFile, siteId?: string): Promise<string> {
siteId = siteId || CoreSites.instance.getCurrentSiteId(); siteId = siteId || CoreSites.instance.getCurrentSiteId();
// Get the site and check if it can download files. // Get the site and check if it can download files.
return CoreSites.instance.getSite(siteId).then((site) => { return CoreSites.instance.getSite(siteId).then((site) => {
if (!site.canDownloadFiles()) { if (!site.canDownloadFiles()) {
return Promise.reject(Translate.instance.instant('core.cannotdownloadfiles')); return Promise.reject(new CoreError(Translate.instance.instant('core.cannotdownloadfiles')));
} }
return CoreFilepool.instance.downloadUrl(siteId, fileUrl, false, component, componentId, return CoreFilepool.instance.downloadUrl(siteId, fileUrl, false, component, componentId,
timemodified, onProgress, undefined, file).catch((error) => { timemodified, onProgress, undefined, file).catch((error) =>
// Download failed, check the state again to see if the file was downloaded before. // Download failed, check the state again to see if the file was downloaded before.
return CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified).then((state) => { CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified).then((state) => {
if (this.isStateDownloaded(state)) { if (this.isStateDownloaded(state)) {
return CoreFilepool.instance.getInternalUrlByUrl(siteId, fileUrl); return CoreFilepool.instance.getInternalUrlByUrl(siteId, fileUrl);
} else { } else {
return Promise.reject(error); return Promise.reject(error);
} }
}); }),
}); );
}); });
} }
@ -219,9 +218,10 @@ export class CoreFileHelperProvider {
* Get the file's URL. * Get the file's URL.
* *
* @param file The file. * @param file The file.
* @deprecated since 3.9.5. Get directly the fileurl instead.
*/ */
getFileUrl(file: any): string { getFileUrl(file: CoreWSExternalFile): string {
return file.fileurl || file.url; return file.fileurl;
} }
/** /**
@ -229,7 +229,7 @@ export class CoreFileHelperProvider {
* *
* @param file The file. * @param file The file.
*/ */
getFileTimemodified(file: any): number { getFileTimemodified(file: CoreWSExternalFile): number {
return file.timemodified || 0; return file.timemodified || 0;
} }
@ -249,7 +249,7 @@ export class CoreFileHelperProvider {
* @param file The file to check. * @param file The file to check.
* @return Whether the file should be opened in browser. * @return Whether the file should be opened in browser.
*/ */
shouldOpenInBrowser(file: any): boolean { shouldOpenInBrowser(file: CoreWSExternalFile): boolean {
if (!file || !file.isexternalfile || !file.mimetype) { if (!file || !file.isexternalfile || !file.mimetype) {
return false; return false;
} }
@ -275,7 +275,7 @@ export class CoreFileHelperProvider {
* @param files The files to check. * @param files The files to check.
* @return Total files size. * @return Total files size.
*/ */
async getTotalFilesSize(files: any[]): Promise<number> { async getTotalFilesSize(files: (CoreWSExternalFile | FileEntry)[]): Promise<number> {
let totalSize = 0; let totalSize = 0;
for (const file of files) { for (const file of files) {
@ -291,27 +291,29 @@ export class CoreFileHelperProvider {
* @param file The file to check. * @param file The file to check.
* @return File size. * @return File size.
*/ */
async getFileSize(file: any): Promise<number> { async getFileSize(file: CoreWSExternalFile | FileEntry): Promise<number> {
if (file.filesize) { if ('filesize' in file && (file.filesize || file.filesize === 0)) {
return file.filesize; return file.filesize;
} }
// If it's a remote file. First check if we have the file downloaded since it's more reliable. // If it's a remote file. First check if we have the file downloaded since it's more reliable.
if (file.filename && !file.name) { if ('filename' in file) {
const fileUrl = file.fileurl;
try { try {
const siteId = CoreSites.instance.getCurrentSiteId(); const siteId = CoreSites.instance.getCurrentSiteId();
const path = await CoreFilepool.instance.getFilePathByUrl(siteId, file.fileurl); const path = await CoreFilepool.instance.getFilePathByUrl(siteId, fileUrl);
const fileEntry = await CoreFile.instance.getFile(path); const fileEntry = await CoreFile.instance.getFile(path);
const fileObject = await CoreFile.instance.getFileObjectFromFileEntry(fileEntry); const fileObject = await CoreFile.instance.getFileObjectFromFileEntry(fileEntry);
return fileObject.size; return fileObject.size;
} catch (error) { } catch (error) {
// Error getting the file, maybe it's not downloaded. Get remote size. // Error getting the file, maybe it's not downloaded. Get remote size.
const size = await CoreWS.instance.getRemoteFileSize(file.fileurl); const size = await CoreWS.instance.getRemoteFileSize(fileUrl);
if (size === -1) { if (size === -1) {
throw new Error('Couldn\'t determine file size: ' + file.fileurl); throw new CoreError(`Couldn't determine file size: ${fileUrl}`);
} }
return size; return size;
@ -319,13 +321,13 @@ export class CoreFileHelperProvider {
} }
// If it's a local file, get its size. // If it's a local file, get its size.
if (file.name) { if ('name' in file) {
const fileObject = await CoreFile.instance.getFileObjectFromFileEntry(file); const fileObject = await CoreFile.instance.getFileObjectFromFileEntry(file);
return fileObject.size; return fileObject.size;
} }
throw new Error('Couldn\'t determine file size: ' + file.fileurl); throw new CoreError('Couldn\'t determine file size');
} }
/** /**
@ -334,7 +336,7 @@ export class CoreFileHelperProvider {
* @param file The file to check. * @param file The file to check.
* @return bool. * @return bool.
*/ */
isOpenableInApp(file: {filename?: string, name?: string}): boolean { isOpenableInApp(file: {filename?: string; name?: string}): boolean {
const re = /(?:\.([^.]+))?$/; const re = /(?:\.([^.]+))?$/;
const ext = re.exec(file.filename || file.name)[1]; const ext = re.exec(file.filename || file.name)[1];
@ -363,7 +365,7 @@ export class CoreFileHelperProvider {
*/ */
isFileTypeExcludedInApp(fileType: string): boolean { isFileTypeExcludedInApp(fileType: string): boolean {
const currentSite = CoreSites.instance.getCurrentSite(); const currentSite = CoreSites.instance.getCurrentSite();
const fileTypeExcludeList = currentSite && currentSite.getStoredConfig('tool_mobile_filetypeexclusionlist'); const fileTypeExcludeList = currentSite && <string> currentSite.getStoredConfig('tool_mobile_filetypeexclusionlist');
if (!fileTypeExcludeList) { if (!fileTypeExcludeList) {
return false; return false;
@ -373,6 +375,10 @@ export class CoreFileHelperProvider {
return !!fileTypeExcludeList.match(regEx); return !!fileTypeExcludeList.match(regEx);
} }
} }
export class CoreFileHelper extends makeSingleton(CoreFileHelperProvider) {} export class CoreFileHelper extends makeSingleton(CoreFileHelperProvider) {}
export type CoreFileHelperOnProgress = (event?: ProgressEvent | { calculating: true }) => void;

View File

@ -13,8 +13,10 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { FileEntry } from '@ionic-native/file';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton } from '@singletons/core.singletons'; import { makeSingleton } from '@singletons/core.singletons';
/** /**
@ -26,9 +28,8 @@ import { makeSingleton } from '@singletons/core.singletons';
*/ */
@Injectable() @Injectable()
export class CoreFileSessionProvider { export class CoreFileSessionProvider {
protected files = {};
constructor() { } protected files: {[siteId: string]: {[component: string]: {[id: string]: (CoreWSExternalFile | FileEntry)[]}}} = {};
/** /**
* Add a file to the session. * Add a file to the session.
@ -38,7 +39,7 @@ export class CoreFileSessionProvider {
* @param file File to add. * @param file File to add.
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
*/ */
addFile(component: string, id: string | number, file: any, siteId?: string): void { addFile(component: string, id: string | number, file: CoreWSExternalFile | FileEntry, siteId?: string): void {
siteId = siteId || CoreSites.instance.getCurrentSiteId(); siteId = siteId || CoreSites.instance.getCurrentSiteId();
this.initFileArea(component, id, siteId); this.initFileArea(component, id, siteId);
@ -68,7 +69,7 @@ export class CoreFileSessionProvider {
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Array of files in session. * @return Array of files in session.
*/ */
getFiles(component: string, id: string | number, siteId?: string): any[] { getFiles(component: string, id: string | number, siteId?: string): (CoreWSExternalFile | FileEntry)[] {
siteId = siteId || CoreSites.instance.getCurrentSiteId(); siteId = siteId || CoreSites.instance.getCurrentSiteId();
if (this.files[siteId] && this.files[siteId][component] && this.files[siteId][component][id]) { if (this.files[siteId] && this.files[siteId][component] && this.files[siteId][component][id]) {
return this.files[siteId][component][id]; return this.files[siteId][component][id];
@ -106,7 +107,7 @@ export class CoreFileSessionProvider {
* @param file File to remove. The instance should be exactly the same as the one stored in session. * @param file File to remove. The instance should be exactly the same as the one stored in session.
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
*/ */
removeFile(component: string, id: string | number, file: any, siteId?: string): void { removeFile(component: string, id: string | number, file: CoreWSExternalFile | FileEntry, siteId?: string): void {
siteId = siteId || CoreSites.instance.getCurrentSiteId(); siteId = siteId || CoreSites.instance.getCurrentSiteId();
if (this.files[siteId] && this.files[siteId][component] && this.files[siteId][component][id]) { if (this.files[siteId] && this.files[siteId][component] && this.files[siteId][component][id]) {
const position = this.files[siteId][component][id].indexOf(file); const position = this.files[siteId][component][id].indexOf(file);
@ -140,13 +141,14 @@ export class CoreFileSessionProvider {
* @param newFiles Files to set. * @param newFiles Files to set.
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
*/ */
setFiles(component: string, id: string | number, newFiles: any[], siteId?: string): void { setFiles(component: string, id: string | number, newFiles: (CoreWSExternalFile | FileEntry)[], siteId?: string): void {
siteId = siteId || CoreSites.instance.getCurrentSiteId(); siteId = siteId || CoreSites.instance.getCurrentSiteId();
this.initFileArea(component, id, siteId); this.initFileArea(component, id, siteId);
this.files[siteId][component][id] = newFiles; this.files[siteId][component][id] = newFiles;
} }
} }
export class CoreFileSession extends makeSingleton(CoreFileSessionProvider) {} export class CoreFileSession extends makeSingleton(CoreFileSessionProvider) {}

View File

@ -14,15 +14,18 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { FileEntry, DirectoryEntry, Entry, Metadata } from '@ionic-native/file'; import { FileEntry, DirectoryEntry, Entry, Metadata, IFile } from '@ionic-native/file';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreWSExternalFile } from '@services/ws';
import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import CoreConfigConstants from '@app/config.json'; import CoreConfigConstants from '@app/config.json';
import { CoreError } from '@classes/errors/error';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { makeSingleton, File, Zip, Platform } from '@singletons/core.singletons'; import { makeSingleton, File, Zip, Platform, WebView } from '@singletons/core.singletons';
/** /**
* Progress event used when writing a file data into a file. * Progress event used when writing a file data into a file.
@ -49,23 +52,35 @@ export type CoreFileProgressEvent = {
*/ */
export type CoreFileProgressFunction = (event: CoreFileProgressEvent) => void; export type CoreFileProgressFunction = (event: CoreFileProgressEvent) => void;
/**
* Constants to define the format to read a file.
*/
export const enum CoreFileFormat {
FORMATTEXT = 0,
FORMATDATAURL = 1,
FORMATBINARYSTRING = 2,
FORMATARRAYBUFFER = 3,
FORMATJSON = 4,
}
/** /**
* Factory to interact with the file system. * Factory to interact with the file system.
*/ */
@Injectable() @Injectable()
export class CoreFileProvider { export class CoreFileProvider {
// Formats to read a file. // Formats to read a file.
static FORMATTEXT = 0; static readonly FORMATTEXT = CoreFileFormat.FORMATTEXT;
static FORMATDATAURL = 1; static readonly FORMATDATAURL = CoreFileFormat.FORMATDATAURL;
static FORMATBINARYSTRING = 2; static readonly FORMATBINARYSTRING = CoreFileFormat.FORMATBINARYSTRING;
static FORMATARRAYBUFFER = 3; static readonly FORMATARRAYBUFFER = CoreFileFormat.FORMATARRAYBUFFER;
static FORMATJSON = 4; static readonly FORMATJSON = CoreFileFormat.FORMATJSON;
// Folders. // Folders.
static SITESFOLDER = 'sites'; static readonly SITESFOLDER = 'sites';
static TMPFOLDER = 'tmp'; static readonly TMPFOLDER = 'tmp';
static CHUNK_SIZE = 1048576; // 1 MB. Same chunk size as Ionic Native. static readonly CHUNK_SIZE = 1048576; // 1 MB. Same chunk size as Ionic Native.
protected logger: CoreLogger; protected logger: CoreLogger;
protected initialized = false; protected initialized = false;
@ -73,73 +88,9 @@ export class CoreFileProvider {
protected isHTMLAPI = false; protected isHTMLAPI = false;
constructor() { constructor() {
this.logger = CoreLogger.getInstance('CoreFileProvider'); this.logger = CoreLogger.getInstance('CoreFileProvider');
if (CoreApp.instance.isAndroid() && !Object.getOwnPropertyDescriptor(FileReader.prototype, 'onloadend')) { // @todo: Check if redefining FileReader getters and setters is still needed in Android.
// Cordova File plugin creates some getters and setter for FileReader, but Ionic's polyfills override them in Android.
// Create the getters and setters again. This code comes from FileReader.js in cordova-plugin-file.
// @todo: Check if this is still needed.
this.defineGetterSetter(FileReader.prototype, 'readyState', function(): any {
return this._localURL ? this._readyState : this._realReader.readyState;
});
this.defineGetterSetter(FileReader.prototype, 'error', function(): any {
return this._localURL ? this._error : this._realReader.error;
});
this.defineGetterSetter(FileReader.prototype, 'result', function(): any {
return this._localURL ? this._result : this._realReader.result;
});
this.defineEvent('onloadstart');
this.defineEvent('onprogress');
this.defineEvent('onload');
this.defineEvent('onerror');
this.defineEvent('onloadend');
this.defineEvent('onabort');
}
}
/**
* Define an event for FileReader.
*
* @param eventName Name of the event.
*/
protected defineEvent(eventName: string): void {
this.defineGetterSetter(FileReader.prototype, eventName, function(): any {
return this._realReader[eventName] || null;
}, function(value: any): void {
this._realReader[eventName] = value;
});
}
/**
* Define a getter and, optionally, a setter for a certain property in an object.
*
* @param obj Object to set the getter/setter for.
* @param key Name of the property where to set them.
* @param getFunc The getter function.
* @param setFunc The setter function.
*/
protected defineGetterSetter(obj: any, key: string, getFunc: () => any, setFunc?: (value?: any) => any): void {
if (Object.defineProperty) {
const desc: any = {
get: getFunc,
configurable: true
};
if (setFunc) {
desc.set = setFunc;
}
Object.defineProperty(obj, key, desc);
} else {
obj.__defineGetter__(key, getFunc);
if (setFunc) {
obj.__defineSetter__(key, setFunc);
}
}
} }
/** /**
@ -172,7 +123,6 @@ export class CoreFileProvider {
} }
return Platform.instance.ready().then(() => { return Platform.instance.ready().then(() => {
if (CoreApp.instance.isAndroid()) { if (CoreApp.instance.isAndroid()) {
this.basePath = File.instance.externalApplicationStorageDirectory || this.basePath; this.basePath = File.instance.externalApplicationStorageDirectory || this.basePath;
} else if (CoreApp.instance.isIOS()) { } else if (CoreApp.instance.isIOS()) {
@ -180,7 +130,7 @@ export class CoreFileProvider {
} else if (!this.isAvailable() || this.basePath === '') { } else if (!this.isAvailable() || this.basePath === '') {
this.logger.error('Error getting device OS.'); this.logger.error('Error getting device OS.');
return Promise.reject(null); return Promise.reject(new CoreError('Error getting device OS to initialize file system.'));
} }
this.initialized = true; this.initialized = true;
@ -208,9 +158,7 @@ export class CoreFileProvider {
this.logger.debug('Get file: ' + path); this.logger.debug('Get file: ' + path);
return File.instance.resolveLocalFilesystemUrl(this.addBasePathIfNeeded(path)); return File.instance.resolveLocalFilesystemUrl(this.addBasePathIfNeeded(path));
}).then((entry) => { }).then((entry) => <FileEntry> entry);
return <FileEntry> entry;
});
} }
/** /**
@ -246,8 +194,10 @@ export class CoreFileProvider {
* @param base Base path to create the dir/file in. If not set, use basePath. * @param base Base path to create the dir/file in. If not set, use basePath.
* @return Promise to be resolved when the dir/file is created. * @return Promise to be resolved when the dir/file is created.
*/ */
protected create(isDirectory: boolean, path: string, failIfExists?: boolean, base?: string): Promise<any> { protected async create(isDirectory: boolean, path: string, failIfExists?: boolean, base?: string):
return this.init().then(() => { Promise<FileEntry | DirectoryEntry> {
await this.init();
// Remove basePath if it's in the path. // Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, '')); path = this.removeStartingSlash(path.replace(this.basePath, ''));
base = base || this.basePath; base = base || this.basePath;
@ -270,15 +220,10 @@ export class CoreFileProvider {
this.logger.debug('Create dir ' + firstDir + ' in ' + base); this.logger.debug('Create dir ' + firstDir + ' in ' + base);
return File.instance.createDir(base, firstDir, true).then((newDirEntry) => { const newDirEntry = await File.instance.createDir(base, firstDir, true);
return this.create(isDirectory, restOfPath, failIfExists, newDirEntry.toURL());
}).catch((error) => {
this.logger.error('Error creating directory ' + firstDir + ' in ' + base);
return Promise.reject(error); return this.create(isDirectory, restOfPath, failIfExists, newDirEntry.toURL());
});
} }
});
} }
/** /**
@ -288,8 +233,10 @@ export class CoreFileProvider {
* @param failIfExists True if it should fail if the directory exists, false otherwise. * @param failIfExists True if it should fail if the directory exists, false otherwise.
* @return Promise to be resolved when the directory is created. * @return Promise to be resolved when the directory is created.
*/ */
createDir(path: string, failIfExists?: boolean): Promise<DirectoryEntry> { async createDir(path: string, failIfExists?: boolean): Promise<DirectoryEntry> {
return this.create(true, path, failIfExists); const entry = <DirectoryEntry> await this.create(true, path, failIfExists);
return entry;
} }
/** /**
@ -299,8 +246,10 @@ export class CoreFileProvider {
* @param failIfExists True if it should fail if the file exists, false otherwise.. * @param failIfExists True if it should fail if the file exists, false otherwise..
* @return Promise to be resolved when the file is created. * @return Promise to be resolved when the file is created.
*/ */
createFile(path: string, failIfExists?: boolean): Promise<FileEntry> { async createFile(path: string, failIfExists?: boolean): Promise<FileEntry> {
return this.create(false, path, failIfExists); const entry = <FileEntry> await this.create(true, path, failIfExists);
return entry;
} }
/** /**
@ -309,14 +258,14 @@ export class CoreFileProvider {
* @param path Relative path to the directory. * @param path Relative path to the directory.
* @return Promise to be resolved when the directory is deleted. * @return Promise to be resolved when the directory is deleted.
*/ */
removeDir(path: string): Promise<any> { async removeDir(path: string): Promise<void> {
return this.init().then(() => { await this.init();
// Remove basePath if it's in the path. // Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, '')); path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Remove directory: ' + path); this.logger.debug('Remove directory: ' + path);
return File.instance.removeRecursively(this.basePath, path); await File.instance.removeRecursively(this.basePath, path);
});
} }
/** /**
@ -325,23 +274,25 @@ export class CoreFileProvider {
* @param path Relative path to the file. * @param path Relative path to the file.
* @return Promise to be resolved when the file is deleted. * @return Promise to be resolved when the file is deleted.
*/ */
removeFile(path: string): Promise<any> { async removeFile(path: string): Promise<void> {
return this.init().then(() => { await this.init();
// Remove basePath if it's in the path. // Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, '')); path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Remove file: ' + path); this.logger.debug('Remove file: ' + path);
return File.instance.removeFile(this.basePath, path).catch((error) => { try {
await File.instance.removeFile(this.basePath, path);
} catch (error) {
// The delete can fail if the path has encoded characters. Try again if that's the case. // The delete can fail if the path has encoded characters. Try again if that's the case.
const decodedPath = decodeURI(path); const decodedPath = decodeURI(path);
if (decodedPath != path) { if (decodedPath != path) {
return File.instance.removeFile(this.basePath, decodedPath); await File.instance.removeFile(this.basePath, decodedPath);
} else { } else {
return Promise.reject(error); throw error;
}
} }
});
});
} }
/** /**
@ -350,10 +301,8 @@ export class CoreFileProvider {
* @param fileEntry File Entry. * @param fileEntry File Entry.
* @return Promise resolved when the file is deleted. * @return Promise resolved when the file is deleted.
*/ */
removeFileByFileEntry(fileEntry: any): Promise<any> { removeFileByFileEntry(entry: Entry): Promise<void> {
return new Promise((resolve, reject): void => { return new Promise((resolve, reject) => entry.remove(resolve, reject));
fileEntry.remove(resolve, reject);
});
} }
/** /**
@ -362,14 +311,26 @@ export class CoreFileProvider {
* @param path Relative path to the directory. * @param path Relative path to the directory.
* @return Promise to be resolved when the contents are retrieved. * @return Promise to be resolved when the contents are retrieved.
*/ */
getDirectoryContents(path: string): Promise<any> { async getDirectoryContents(path: string): Promise<(FileEntry | DirectoryEntry)[]> {
return this.init().then(() => { await this.init();
// Remove basePath if it's in the path. // Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, '')); path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Get contents of dir: ' + path); this.logger.debug('Get contents of dir: ' + path);
return File.instance.listDir(this.basePath, path); const result = await File.instance.listDir(this.basePath, path);
});
return <(FileEntry | DirectoryEntry)[]> result;
}
/**
* Type guard to check if the param is a DirectoryEntry.
*
* @param entry Param to check.
* @return Whether the param is a DirectoryEntry.
*/
protected isDirectoryEntry(entry: FileEntry | DirectoryEntry): entry is DirectoryEntry {
return entry.isDirectory === true;
} }
/** /**
@ -378,19 +339,18 @@ export class CoreFileProvider {
* @param entry Directory or file. * @param entry Directory or file.
* @return Promise to be resolved when the size is calculated. * @return Promise to be resolved when the size is calculated.
*/ */
protected getSize(entry: any): Promise<number> { protected getSize(entry: DirectoryEntry | FileEntry): Promise<number> {
return new Promise((resolve, reject): void => { return new Promise((resolve, reject) => {
if (entry.isDirectory) { if (this.isDirectoryEntry(entry)) {
const directoryReader = entry.createReader(); const directoryReader = entry.createReader();
directoryReader.readEntries((entries) => {
directoryReader.readEntries((entries: (DirectoryEntry | FileEntry)[]) => {
const promises = []; const promises = [];
for (let i = 0; i < entries.length; i++) { for (let i = 0; i < entries.length; i++) {
promises.push(this.getSize(entries[i])); promises.push(this.getSize(entries[i]));
} }
Promise.all(promises).then((sizes) => { Promise.all(promises).then((sizes) => {
let directorySize = 0; let directorySize = 0;
for (let i = 0; i < sizes.length; i++) { for (let i = 0; i < sizes.length; i++) {
const fileSize = Number(sizes[i]); const fileSize = Number(sizes[i]);
@ -402,12 +362,9 @@ export class CoreFileProvider {
directorySize += fileSize; directorySize += fileSize;
} }
resolve(directorySize); resolve(directorySize);
}, reject); }, reject);
}, reject); }, reject);
} else {
} else if (entry.isFile) {
entry.file((file) => { entry.file((file) => {
resolve(file.size); resolve(file.size);
}, reject); }, reject);
@ -427,9 +384,7 @@ export class CoreFileProvider {
this.logger.debug('Get size of dir: ' + path); this.logger.debug('Get size of dir: ' + path);
return this.getDir(path).then((dirEntry) => { return this.getDir(path).then((dirEntry) => this.getSize(dirEntry));
return this.getSize(dirEntry);
});
} }
/** /**
@ -444,9 +399,7 @@ export class CoreFileProvider {
this.logger.debug('Get size of file: ' + path); this.logger.debug('Get size of file: ' + path);
return this.getFile(path).then((fileEntry) => { return this.getFile(path).then((fileEntry) => this.getSize(fileEntry));
return this.getSize(fileEntry);
});
} }
/** /**
@ -455,7 +408,7 @@ export class CoreFileProvider {
* @param path Relative path to the file. * @param path Relative path to the file.
* @return Promise to be resolved when the file is retrieved. * @return Promise to be resolved when the file is retrieved.
*/ */
getFileObjectFromFileEntry(entry: FileEntry): Promise<any> { getFileObjectFromFileEntry(entry: FileEntry): Promise<IFile> {
return new Promise((resolve, reject): void => { return new Promise((resolve, reject): void => {
this.logger.debug('Get file object of: ' + entry.fullPath); this.logger.debug('Get file object of: ' + entry.fullPath);
entry.file(resolve, reject); entry.file(resolve, reject);
@ -496,15 +449,10 @@ export class CoreFileProvider {
* Read a file from local file system. * Read a file from local file system.
* *
* @param path Relative path to the file. * @param path Relative path to the file.
* @param format Format to read the file. Must be one of: * @param format Format to read the file.
* FORMATTEXT
* FORMATDATAURL
* FORMATBINARYSTRING
* FORMATARRAYBUFFER
* FORMATJSON
* @return Promise to be resolved when the file is read. * @return Promise to be resolved when the file is read.
*/ */
readFile(path: string, format: number = CoreFileProvider.FORMATTEXT): Promise<any> { readFile(path: string, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> {
// Remove basePath if it's in the path. // Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, '')); path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Read file ' + path + ' with format ' + format); this.logger.debug('Read file ' + path + ' with format ' + format);
@ -521,7 +469,7 @@ export class CoreFileProvider {
const parsed = CoreTextUtils.instance.parseJSON(text, null); const parsed = CoreTextUtils.instance.parseJSON(text, null);
if (parsed == null && text != null) { if (parsed == null && text != null) {
return Promise.reject('Error parsing JSON file: ' + path); return Promise.reject(new CoreError('Error parsing JSON file: ' + path));
} }
return parsed; return parsed;
@ -535,27 +483,21 @@ export class CoreFileProvider {
* Read file contents from a file data object. * Read file contents from a file data object.
* *
* @param fileData File's data. * @param fileData File's data.
* @param format Format to read the file. Must be one of: * @param format Format to read the file.
* FORMATTEXT
* FORMATDATAURL
* FORMATBINARYSTRING
* FORMATARRAYBUFFER
* FORMATJSON
* @return Promise to be resolved when the file is read. * @return Promise to be resolved when the file is read.
*/ */
readFileData(fileData: any, format: number = CoreFileProvider.FORMATTEXT): Promise<any> { readFileData(fileData: IFile, format: CoreFileFormat = CoreFileProvider.FORMATTEXT): Promise<string | ArrayBuffer | unknown> {
format = format || CoreFileProvider.FORMATTEXT; format = format || CoreFileProvider.FORMATTEXT;
this.logger.debug('Read file from file data with format ' + format); this.logger.debug('Read file from file data with format ' + format);
return new Promise((resolve, reject): void => { return new Promise((resolve, reject): void => {
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = (evt): void => { reader.onloadend = (event): void => {
const target = <any> evt.target; // Convert to <any> to be able to use non-standard properties. if (event.target.result !== undefined && event.target.result !== null) {
if (target.result !== undefined && target.result !== null) {
if (format == CoreFileProvider.FORMATJSON) { if (format == CoreFileProvider.FORMATJSON) {
// Convert to object. // Convert to object.
const parsed = CoreTextUtils.instance.parseJSON(target.result, null); const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null);
if (parsed == null) { if (parsed == null) {
reject('Error parsing JSON file.'); reject('Error parsing JSON file.');
@ -563,10 +505,10 @@ export class CoreFileProvider {
resolve(parsed); resolve(parsed);
} else { } else {
resolve(target.result); resolve(event.target.result);
} }
} else if (target.error !== undefined && target.error !== null) { } else if (event.target.error !== undefined && event.target.error !== null) {
reject(target.error); reject(event.target.error);
} else { } else {
reject({ code: null, message: 'READER_ONLOADEND_ERR' }); reject({ code: null, message: 'READER_ONLOADEND_ERR' });
} }
@ -575,7 +517,7 @@ export class CoreFileProvider {
// Check if the load starts. If it doesn't start in 3 seconds, reject. // Check if the load starts. If it doesn't start in 3 seconds, reject.
// Sometimes in Android the read doesn't start for some reason, so the promise never finishes. // Sometimes in Android the read doesn't start for some reason, so the promise never finishes.
let hasStarted = false; let hasStarted = false;
reader.onloadstart = (evt): void => { reader.onloadstart = () => {
hasStarted = true; hasStarted = true;
}; };
setTimeout(() => { setTimeout(() => {
@ -597,7 +539,6 @@ export class CoreFileProvider {
default: default:
reader.readAsText(fileData); reader.readAsText(fileData);
} }
}); });
} }
@ -609,7 +550,7 @@ export class CoreFileProvider {
* @param append Whether to append the data to the end of the file. * @param append Whether to append the data to the end of the file.
* @return Promise to be resolved when the file is written. * @return Promise to be resolved when the file is written.
*/ */
writeFile(path: string, data: any, append?: boolean): Promise<FileEntry> { writeFile(path: string, data: string | Blob, append?: boolean): Promise<FileEntry> {
return this.init().then(() => { return this.init().then(() => {
// Remove basePath if it's in the path. // Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, '')); path = this.removeStartingSlash(path.replace(this.basePath, ''));
@ -624,9 +565,8 @@ export class CoreFileProvider {
data = new Blob([data], { type: type || 'text/plain' }); data = new Blob([data], { type: type || 'text/plain' });
} }
return File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append }).then(() => { return File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append })
return fileEntry; .then(() => fileEntry);
});
}); });
}); });
} }
@ -645,7 +585,6 @@ export class CoreFileProvider {
*/ */
async writeFileDataInFile(file: Blob, path: string, onProgress?: CoreFileProgressFunction, offset: number = 0, async writeFileDataInFile(file: Blob, path: string, onProgress?: CoreFileProgressFunction, offset: number = 0,
append?: boolean): Promise<FileEntry> { append?: boolean): Promise<FileEntry> {
offset = offset || 0; offset = offset || 0;
try { try {
@ -659,7 +598,7 @@ export class CoreFileProvider {
onProgress && onProgress({ onProgress && onProgress({
lengthComputable: true, lengthComputable: true,
loaded: offset, loaded: offset,
total: file.size total: file.size,
}); });
if (offset >= file.size) { if (offset >= file.size) {
@ -671,8 +610,8 @@ export class CoreFileProvider {
return this.writeFileDataInFile(file, path, onProgress, offset, true); return this.writeFileDataInFile(file, path, onProgress, offset, true);
} catch (error) { } catch (error) {
if (error && error.target && error.target.error) { if (error && error.target && error.target.error) {
// Error returned by the writer, get the "real" error. // Error returned by the writer, throw the "real" error.
error = error.target.error; throw error.target.error;
} }
throw error; throw error;
@ -686,9 +625,7 @@ export class CoreFileProvider {
* @return Promise to be resolved when the file is retrieved. * @return Promise to be resolved when the file is retrieved.
*/ */
getExternalFile(fullPath: string): Promise<FileEntry> { getExternalFile(fullPath: string): Promise<FileEntry> {
return File.instance.resolveLocalFilesystemUrl(fullPath).then((entry) => { return File.instance.resolveLocalFilesystemUrl(fullPath).then((entry) => <FileEntry> entry);
return <FileEntry> entry;
});
} }
/** /**
@ -709,11 +646,11 @@ export class CoreFileProvider {
* @param fullPath Absolute path to the file. * @param fullPath Absolute path to the file.
* @return Promise to be resolved when the file is removed. * @return Promise to be resolved when the file is removed.
*/ */
removeExternalFile(fullPath: string): Promise<any> { async removeExternalFile(fullPath: string): Promise<void> {
const directory = fullPath.substring(0, fullPath.lastIndexOf('/')); const directory = fullPath.substring(0, fullPath.lastIndexOf('/'));
const filename = fullPath.substr(fullPath.lastIndexOf('/') + 1); const filename = fullPath.substr(fullPath.lastIndexOf('/') + 1);
return File.instance.removeFile(directory, filename); await File.instance.removeFile(directory, filename);
} }
/** /**
@ -742,9 +679,7 @@ export class CoreFileProvider {
return this.init().then(() => { return this.init().then(() => {
if (CoreApp.instance.isIOS()) { if (CoreApp.instance.isIOS()) {
// In iOS we want the internal URL (cdvfile://localhost/persistent/...). // In iOS we want the internal URL (cdvfile://localhost/persistent/...).
return File.instance.resolveDirectoryUrl(this.basePath).then((dirEntry) => { return File.instance.resolveDirectoryUrl(this.basePath).then((dirEntry) => dirEntry.toInternalURL());
return dirEntry.toInternalURL();
});
} else { } else {
// In the other platforms we use the basePath as it is (file://...). // In the other platforms we use the basePath as it is (file://...).
return this.basePath; return this.basePath;
@ -776,8 +711,10 @@ export class CoreFileProvider {
* try to create it (slower). * try to create it (slower).
* @return Promise resolved when the entry is moved. * @return Promise resolved when the entry is moved.
*/ */
moveDir(originalPath: string, newPath: string, destDirExists?: boolean): Promise<any> { async moveDir(originalPath: string, newPath: string, destDirExists?: boolean): Promise<DirectoryEntry> {
return this.copyOrMoveFileOrDir(originalPath, newPath, true, false, destDirExists); const entry = await this.copyOrMoveFileOrDir(originalPath, newPath, true, false, destDirExists);
return <DirectoryEntry> entry;
} }
/** /**
@ -789,8 +726,10 @@ export class CoreFileProvider {
* try to create it (slower). * try to create it (slower).
* @return Promise resolved when the entry is moved. * @return Promise resolved when the entry is moved.
*/ */
moveFile(originalPath: string, newPath: string, destDirExists?: boolean): Promise<any> { async moveFile(originalPath: string, newPath: string, destDirExists?: boolean): Promise<FileEntry> {
return this.copyOrMoveFileOrDir(originalPath, newPath, false, false, destDirExists); const entry = await this.copyOrMoveFileOrDir(originalPath, newPath, false, false, destDirExists);
return <FileEntry> entry;
} }
/** /**
@ -802,8 +741,10 @@ export class CoreFileProvider {
* try to create it (slower). * try to create it (slower).
* @return Promise resolved when the entry is copied. * @return Promise resolved when the entry is copied.
*/ */
copyDir(from: string, to: string, destDirExists?: boolean): Promise<any> { async copyDir(from: string, to: string, destDirExists?: boolean): Promise<DirectoryEntry> {
return this.copyOrMoveFileOrDir(from, to, true, true, destDirExists); const entry = await this.copyOrMoveFileOrDir(from, to, true, true, destDirExists);
return <DirectoryEntry> entry;
} }
/** /**
@ -815,8 +756,10 @@ export class CoreFileProvider {
* try to create it (slower). * try to create it (slower).
* @return Promise resolved when the entry is copied. * @return Promise resolved when the entry is copied.
*/ */
copyFile(from: string, to: string, destDirExists?: boolean): Promise<any> { async copyFile(from: string, to: string, destDirExists?: boolean): Promise<FileEntry> {
return this.copyOrMoveFileOrDir(from, to, false, true, destDirExists); const entry = await this.copyOrMoveFileOrDir(from, to, false, true, destDirExists);
return <FileEntry> entry;
} }
/** /**
@ -830,16 +773,16 @@ export class CoreFileProvider {
* try to create it (slower). * try to create it (slower).
* @return Promise resolved when the entry is copied. * @return Promise resolved when the entry is copied.
*/ */
protected async copyOrMoveFileOrDir(from: string, to: string, isDir?: boolean, copy?: boolean, destDirExists?: boolean) protected async copyOrMoveFileOrDir(from: string, to: string, isDir?: boolean, copy?: boolean, destDirExists?: boolean):
: Promise<Entry> { Promise<FileEntry | DirectoryEntry> {
const fileIsInAppFolder = this.isPathInAppFolder(from); const fileIsInAppFolder = this.isPathInAppFolder(from);
if (!fileIsInAppFolder) { if (!fileIsInAppFolder) {
return this.copyOrMoveExternalFile(from, to, copy); return this.copyOrMoveExternalFile(from, to, copy);
} }
const moveCopyFn = copy ? const moveCopyFn: (path: string, dirName: string, newPath: string, newDirName: string) =>
Promise<FileEntry | DirectoryEntry> = copy ?
(isDir ? File.instance.copyDir.bind(File.instance) : File.instance.copyFile.bind(File.instance)) : (isDir ? File.instance.copyDir.bind(File.instance) : File.instance.copyFile.bind(File.instance)) :
(isDir ? File.instance.moveDir.bind(File.instance) : File.instance.moveFile.bind(File.instance)); (isDir ? File.instance.moveDir.bind(File.instance) : File.instance.moveFile.bind(File.instance));
@ -885,10 +828,10 @@ export class CoreFileProvider {
* path/ -> directory: 'path', name: '' * path/ -> directory: 'path', name: ''
* path -> directory: '', name: 'path' * path -> directory: '', name: 'path'
*/ */
getFileAndDirectoryFromPath(path: string): {directory: string, name: string} { getFileAndDirectoryFromPath(path: string): {directory: string; name: string} {
const file = { const file = {
directory: '', directory: '',
name: '' name: '',
}; };
file.directory = path.substring(0, path.lastIndexOf('/')); file.directory = path.substring(0, path.lastIndexOf('/'));
@ -949,7 +892,8 @@ export class CoreFileProvider {
* @param recreateDir Delete the dest directory before unzipping. Defaults to true. * @param recreateDir Delete the dest directory before unzipping. Defaults to true.
* @return Promise resolved when the file is unzipped. * @return Promise resolved when the file is unzipped.
*/ */
unzipFile(path: string, destFolder?: string, onProgress?: (progress: any) => void, recreateDir: boolean = true): Promise<any> { unzipFile(path: string, destFolder?: string, onProgress?: (progress: ProgressEvent) => void, recreateDir: boolean = true):
Promise<void> {
// Get the source file. // Get the source file.
let fileEntry: FileEntry; let fileEntry: FileEntry;
@ -960,10 +904,10 @@ export class CoreFileProvider {
// Make sure the dest dir doesn't exist already. // Make sure the dest dir doesn't exist already.
return this.removeDir(destFolder).catch(() => { return this.removeDir(destFolder).catch(() => {
// Ignore errors. // Ignore errors.
}).then(() => { }).then(() =>
// Now create the dir, otherwise if any of the ancestor dirs doesn't exist the unzip would fail. // Now create the dir, otherwise if any of the ancestor dirs doesn't exist the unzip would fail.
return this.createDir(destFolder); this.createDir(destFolder),
}); );
} }
}).then(() => { }).then(() => {
// If destFolder is not set, use same location as ZIP file. We need to use absolute paths (including basePath). // If destFolder is not set, use same location as ZIP file. We need to use absolute paths (including basePath).
@ -972,7 +916,7 @@ export class CoreFileProvider {
return Zip.instance.unzip(fileEntry.toURL(), destFolder, onProgress); return Zip.instance.unzip(fileEntry.toURL(), destFolder, onProgress);
}).then((result) => { }).then((result) => {
if (result == -1) { if (result == -1) {
return Promise.reject('Unzip failed.'); return Promise.reject(new CoreError('Unzip failed.'));
} }
}); });
} }
@ -985,18 +929,18 @@ export class CoreFileProvider {
* @param newValue New value. * @param newValue New value.
* @return Promise resolved in success. * @return Promise resolved in success.
*/ */
replaceInFile(path: string, search: string | RegExp, newValue: string): Promise<any> { async replaceInFile(path: string, search: string | RegExp, newValue: string): Promise<void> {
return this.readFile(path).then((content) => { let content = <string> await this.readFile(path);
if (typeof content == 'undefined' || content === null || !content.replace) { if (typeof content == 'undefined' || content === null || !content.replace) {
return Promise.reject(null); throw new CoreError(`Error reading file ${path}`);
} }
if (content.match(search)) { if (content.match(search)) {
content = content.replace(search, newValue); content = content.replace(search, newValue);
return this.writeFile(path, content); await this.writeFile(path, content);
} }
});
} }
/** /**
@ -1007,7 +951,7 @@ export class CoreFileProvider {
*/ */
getMetadata(fileEntry: Entry): Promise<Metadata> { getMetadata(fileEntry: Entry): Promise<Metadata> {
if (!fileEntry || !fileEntry.getMetadata) { if (!fileEntry || !fileEntry.getMetadata) {
return Promise.reject(null); return Promise.reject(new CoreError('Cannot get metadata from file entry.'));
} }
return new Promise((resolve, reject): void => { return new Promise((resolve, reject): void => {
@ -1022,7 +966,7 @@ export class CoreFileProvider {
* @param isDir True if directory, false if file. * @param isDir True if directory, false if file.
* @return Promise resolved with metadata. * @return Promise resolved with metadata.
*/ */
getMetadataFromPath(path: string, isDir?: boolean): Promise<any> { getMetadataFromPath(path: string, isDir?: boolean): Promise<Metadata> {
let promise; let promise;
if (isDir) { if (isDir) {
promise = this.getDir(path); promise = this.getDir(path);
@ -1030,9 +974,7 @@ export class CoreFileProvider {
promise = this.getFile(path); promise = this.getFile(path);
} }
return promise.then((entry) => { return promise.then((entry) => this.getMetadata(entry));
return this.getMetadata(entry);
});
} }
/** /**
@ -1057,22 +999,22 @@ export class CoreFileProvider {
* @param copy True to copy, false to move. * @param copy True to copy, false to move.
* @return Promise resolved when the entry is copied/moved. * @return Promise resolved when the entry is copied/moved.
*/ */
protected copyOrMoveExternalFile(from: string, to: string, copy?: boolean): Promise<any> { protected copyOrMoveExternalFile(from: string, to: string, copy?: boolean): Promise<FileEntry> {
// Get the file to copy/move. // Get the file to copy/move.
return this.getExternalFile(from).then((fileEntry) => { return this.getExternalFile(from).then((fileEntry) => {
// Create the destination dir if it doesn't exist. // Create the destination dir if it doesn't exist.
const dirAndFile = this.getFileAndDirectoryFromPath(to); const dirAndFile = this.getFileAndDirectoryFromPath(to);
return this.createDir(dirAndFile.directory).then((dirEntry) => { return this.createDir(dirAndFile.directory).then((dirEntry) =>
// Now copy/move the file. // Now copy/move the file.
return new Promise((resolve, reject): void => { new Promise((resolve, reject): void => {
if (copy) { if (copy) {
fileEntry.copyTo(dirEntry, dirAndFile.name, resolve, reject); fileEntry.copyTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject);
} else { } else {
fileEntry.moveTo(dirEntry, dirAndFile.name, resolve, reject); fileEntry.moveTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject);
} }
}); }),
}); );
}); });
} }
@ -1083,7 +1025,7 @@ export class CoreFileProvider {
* @param to Relative new path of the file (inside the app folder). * @param to Relative new path of the file (inside the app folder).
* @return Promise resolved when the entry is copied. * @return Promise resolved when the entry is copied.
*/ */
copyExternalFile(from: string, to: string): Promise<any> { copyExternalFile(from: string, to: string): Promise<FileEntry> {
return this.copyOrMoveExternalFile(from, to, true); return this.copyOrMoveExternalFile(from, to, true);
} }
@ -1094,7 +1036,7 @@ export class CoreFileProvider {
* @param to Relative new path of the file (inside the app folder). * @param to Relative new path of the file (inside the app folder).
* @return Promise resolved when the entry is moved. * @return Promise resolved when the entry is moved.
*/ */
moveExternalFile(from: string, to: string): Promise<any> { moveExternalFile(from: string, to: string): Promise<FileEntry> {
return this.copyOrMoveExternalFile(from, to, false); return this.copyOrMoveExternalFile(from, to, false);
} }
@ -1144,10 +1086,10 @@ export class CoreFileProvider {
// Ask the user what he wants to do. // Ask the user what he wants to do.
return newName; return newName;
} }
}).catch(() => { }).catch(() =>
// Folder doesn't exist, name is unique. Clean it and return it. // Folder doesn't exist, name is unique. Clean it and return it.
return CoreTextUtils.instance.removeSpecialCharactersForFiles(CoreTextUtils.instance.decodeURIComponent(fileName)); CoreTextUtils.instance.removeSpecialCharactersForFiles(CoreTextUtils.instance.decodeURIComponent(fileName)),
}); );
} }
/** /**
@ -1155,10 +1097,9 @@ export class CoreFileProvider {
* *
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
clearTmpFolder(): Promise<any> { async clearTmpFolder(): Promise<void> {
return this.removeDir(CoreFileProvider.TMPFOLDER).catch(() => {
// Ignore errors because the folder might not exist. // Ignore errors because the folder might not exist.
}); await CoreUtils.instance.ignoreErrors(this.removeDir(CoreFileProvider.TMPFOLDER));
} }
/** /**
@ -1168,19 +1109,21 @@ export class CoreFileProvider {
* @param files List of used files. * @param files List of used files.
* @return Promise resolved when done, rejected if failure. * @return Promise resolved when done, rejected if failure.
*/ */
removeUnusedFiles(dirPath: string, files: any[]): Promise<any> { async removeUnusedFiles(dirPath: string, files: (CoreWSExternalFile | FileEntry)[]): Promise<void> {
// Get the directory contents. // Get the directory contents.
return this.getDirectoryContents(dirPath).then((contents) => { try {
const contents = await this.getDirectoryContents(dirPath);
if (!contents.length) { if (!contents.length) {
return; return;
} }
const filesMap = {}; const filesMap: {[fullPath: string]: FileEntry} = {};
const promises = []; const promises = [];
// Index the received files by fullPath and ignore the invalid ones. // Index the received files by fullPath and ignore the invalid ones.
files.forEach((file) => { files.forEach((file) => {
if (file.fullPath) { if ('fullPath' in file) {
filesMap[file.fullPath] = file; filesMap[file.fullPath] = file;
} }
}); });
@ -1193,10 +1136,10 @@ export class CoreFileProvider {
} }
}); });
return Promise.all(promises); await Promise.all(promises);
}).catch(() => { } catch (error) {
// Ignore errors, maybe it doesn't exist. // Ignore errors, maybe it doesn't exist.
}); }
} }
/** /**
@ -1246,7 +1189,7 @@ export class CoreFileProvider {
* @return Converted src. * @return Converted src.
*/ */
convertFileSrc(src: string): string { convertFileSrc(src: string): string {
return CoreApp.instance.isIOS() ? (<any> window).Ionic.WebView.convertFileSrc(src) : src; return CoreApp.instance.isIOS() ? WebView.instance.convertFileSrc(src) : src;
} }
/** /**
@ -1272,6 +1215,7 @@ export class CoreFileProvider {
protected isPathInAppFolder(path: string): boolean { protected isPathInAppFolder(path: string): boolean {
return !path || !path.match(/^[a-z0-9]+:\/\//i) || path.indexOf(this.basePath) != -1; return !path || !path.match(/^[a-z0-9]+:\/\//i) || path.indexOf(this.basePath) != -1;
} }
} }
export class CoreFile extends makeSingleton(CoreFileProvider) {} export class CoreFile extends makeSingleton(CoreFileProvider) {}

View File

@ -29,8 +29,9 @@ import { CoreTimeUtils } from '@services/utils/time';
import { CoreUrlUtils } from '@services/utils/url'; import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils, PromiseDefer } from '@services/utils/utils'; import { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { SQLiteDB } from '@classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
import { CoreError } from '@classes/errors/error';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { makeSingleton, Network, NgZone } from '@singletons/core.singletons'; import { makeSingleton, Network, NgZone, Translate } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
/* /*
@ -56,7 +57,7 @@ export class CoreFilepoolProvider {
protected static readonly ERR_FS_OR_NETWORK_UNAVAILABLE = 'CoreFilepoolError:ERR_FS_OR_NETWORK_UNAVAILABLE'; protected static readonly ERR_FS_OR_NETWORK_UNAVAILABLE = 'CoreFilepoolError:ERR_FS_OR_NETWORK_UNAVAILABLE';
protected static readonly ERR_QUEUE_ON_PAUSE = 'CoreFilepoolError:ERR_QUEUE_ON_PAUSE'; protected static readonly ERR_QUEUE_ON_PAUSE = 'CoreFilepoolError:ERR_QUEUE_ON_PAUSE';
protected static readonly FILE_UPDATE_ANY_WHERE_CLAUSE = protected static readonly FILE_UPDATE_UNKNOWN_WHERE_CLAUSE =
'isexternalfile = 1 OR ((revision IS NULL OR revision = 0) AND (timemodified IS NULL OR timemodified = 0))'; 'isexternalfile = 1 OR ((revision IS NULL OR revision = 0) AND (timemodified IS NULL OR timemodified = 0))';
// Variables for database. // Variables for database.
@ -239,7 +240,7 @@ export class CoreFilepoolProvider {
protected appDB: SQLiteDB; protected appDB: SQLiteDB;
protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized. protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized.
protected queueState: string; protected queueState: string;
protected urlAttributes = [ protected urlAttributes: RegExp[] = [
new RegExp('(\\?|&)token=([A-Za-z0-9]*)'), new RegExp('(\\?|&)token=([A-Za-z0-9]*)'),
new RegExp('(\\?|&)forcedownload=[0-1]'), new RegExp('(\\?|&)forcedownload=[0-1]'),
new RegExp('(\\?|&)preview=[A-Za-z0-9]+'), new RegExp('(\\?|&)preview=[A-Za-z0-9]+'),
@ -248,7 +249,7 @@ export class CoreFilepoolProvider {
// To handle file downloads using the queue. // To handle file downloads using the queue.
protected queueDeferreds: { [s: string]: { [s: string]: CoreFilepoolPromiseDefer } } = {}; protected queueDeferreds: { [s: string]: { [s: string]: CoreFilepoolPromiseDefer } } = {};
protected sizeCache = {}; // A "cache" to store file sizes to prevent performing too many HEAD requests. protected sizeCache: {[fileUrl: string]: number} = {}; // A "cache" to store file sizes.
// Variables to prevent downloading packages/files twice at the same time. // Variables to prevent downloading packages/files twice at the same time.
protected packagesPromises: { [s: string]: { [s: string]: Promise<void> } } = {}; protected packagesPromises: { [s: string]: { [s: string]: Promise<void> } } = {};
protected filePromises: { [s: string]: { [s: string]: Promise<string> } } = {}; protected filePromises: { [s: string]: { [s: string]: Promise<string> } } = {};
@ -288,7 +289,7 @@ export class CoreFilepoolProvider {
*/ */
protected async addFileLink(siteId: string, fileId: string, component: string, componentId?: string | number): Promise<void> { protected async addFileLink(siteId: string, fileId: string, component: string, componentId?: string | number): Promise<void> {
if (!component) { if (!component) {
throw null; throw new CoreError('Cannot add link because component is invalid.');
} }
componentId = this.fixComponentId(componentId); componentId = this.fixComponentId(componentId);
@ -358,8 +359,10 @@ export class CoreFilepoolProvider {
* @return Promise resolved on success. * @return Promise resolved on success.
*/ */
protected async addFileToPool(siteId: string, fileId: string, data: CoreFilepoolFileEntry): Promise<void> { protected async addFileToPool(siteId: string, fileId: string, data: CoreFilepoolFileEntry): Promise<void> {
const record = Object.assign({}, data); const record = {
record.fileId = fileId; fileId,
...data,
};
const db = await CoreSites.instance.getSiteDb(siteId); const db = await CoreSites.instance.getSiteDb(siteId);
@ -457,12 +460,12 @@ export class CoreFilepoolProvider {
await this.dbReady; await this.dbReady;
if (!CoreFile.instance.isAvailable()) { if (!CoreFile.instance.isAvailable()) {
throw null; throw new CoreError('File system cannot be used.');
} }
const site = await CoreSites.instance.getSite(siteId); const site = await CoreSites.instance.getSite(siteId);
if (!site.canDownloadFiles()) { if (!site.canDownloadFiles()) {
throw null; throw new CoreError('Site doesn\'t allow downloading files.');
} }
let file: CoreWSExternalFile; let file: CoreWSExternalFile;
@ -488,7 +491,7 @@ export class CoreFilepoolProvider {
const queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress); const queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress);
return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => { return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => {
const newData: CoreFilepoolQueueEntry = {}; const newData: CoreFilepoolQueueDBEntry = {};
let foundLink = false; let foundLink = false;
if (entry) { if (entry) {
@ -562,14 +565,14 @@ export class CoreFilepoolProvider {
* @param componentId An ID to use in conjunction with the component. * @param componentId An ID to use in conjunction with the component.
* @param timemodified The time this file was modified. * @param timemodified The time this file was modified.
* @param checkSize True if we shouldn't download files if their size is big, false otherwise. * @param checkSize True if we shouldn't download files if their size is big, false otherwise.
* @param downloadAny True to download file in WiFi if their size is any, false otherwise. * @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise.
* Ignored if checkSize=false. * Ignored if checkSize=false.
* @param options Extra options (isexternalfile, repositorytype). * @param options Extra options (isexternalfile, repositorytype).
* @param revision File revision. If not defined, it will be calculated using the URL. * @param revision File revision. If not defined, it will be calculated using the URL.
* @return Promise resolved when the file is downloaded. * @return Promise resolved when the file is downloaded.
*/ */
protected async addToQueueIfNeeded(siteId: string, fileUrl: string, component: string, componentId?: string | number, protected async addToQueueIfNeeded(siteId: string, fileUrl: string, component: string, componentId?: string | number,
timemodified: number = 0, checkSize: boolean = true, downloadAny?: boolean, options: CoreFilepoolFileOptions = {}, timemodified: number = 0, checkSize: boolean = true, downloadUnknown?: boolean, options: CoreFilepoolFileOptions = {},
revision?: number): Promise<void> { revision?: number): Promise<void> {
if (!checkSize) { if (!checkSize) {
// No need to check size, just add it to the queue. // No need to check size, just add it to the queue.
@ -584,7 +587,7 @@ export class CoreFilepoolProvider {
} else { } else {
if (!CoreApp.instance.isOnline()) { if (!CoreApp.instance.isOnline()) {
// Cannot check size in offline, stop. // Cannot check size in offline, stop.
throw null; throw new CoreError(Translate.instance.instant('core.cannotconnect'));
} }
size = await CoreWS.instance.getRemoteFileSize(fileUrl); size = await CoreWS.instance.getRemoteFileSize(fileUrl);
@ -592,16 +595,16 @@ export class CoreFilepoolProvider {
// Calculate the size of the file. // Calculate the size of the file.
const isWifi = CoreApp.instance.isWifi(); const isWifi = CoreApp.instance.isWifi();
const sizeAny = size <= 0; const sizeUnknown = size <= 0;
if (!sizeAny) { if (!sizeUnknown) {
// Store the size in the cache. // Store the size in the cache.
this.sizeCache[fileUrl] = size; this.sizeCache[fileUrl] = size;
} }
// Check if the file should be downloaded. // Check if the file should be downloaded.
if (sizeAny) { if (sizeUnknown) {
if (downloadAny && isWifi) { if (downloadUnknown && isWifi) {
await this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined, await this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined,
0, options, revision, true); 0, options, revision, true);
} }
@ -685,7 +688,7 @@ export class CoreFilepoolProvider {
const count = await db.countRecords(CoreFilepoolProvider.LINKS_TABLE, conditions); const count = await db.countRecords(CoreFilepoolProvider.LINKS_TABLE, conditions);
if (count <= 0) { if (count <= 0) {
return null; throw new CoreError('Component doesn\'t have files');
} }
} }
@ -696,7 +699,7 @@ export class CoreFilepoolProvider {
* @param componentId An ID to use in conjunction with the component. * @param componentId An ID to use in conjunction with the component.
* @return Link, null if nothing to link. * @return Link, null if nothing to link.
*/ */
protected createComponentLink(component: string, componentId?: string | number): CoreFilepoolComponentLink { protected createComponentLink(component: string, componentId?: string | number): CoreFilepoolComponentLink | null {
if (typeof component != 'undefined' && component != null) { if (typeof component != 'undefined' && component != null) {
return { component, componentId: this.fixComponentId(componentId) }; return { component, componentId: this.fixComponentId(componentId) };
} }
@ -779,7 +782,7 @@ export class CoreFilepoolProvider {
if (poolFileObject && poolFileObject.fileId !== fileId) { if (poolFileObject && poolFileObject.fileId !== fileId) {
this.logger.error('Invalid object to update passed'); this.logger.error('Invalid object to update passed');
throw null; throw new CoreError('Invalid object to update passed.');
} }
const downloadId = this.getFileDownloadId(fileUrl, filePath); const downloadId = this.getFileDownloadId(fileUrl, filePath);
@ -793,7 +796,7 @@ export class CoreFilepoolProvider {
this.filePromises[siteId][downloadId] = CoreSites.instance.getSite(siteId).then(async (site) => { this.filePromises[siteId][downloadId] = CoreSites.instance.getSite(siteId).then(async (site) => {
if (!site.canDownloadFiles()) { if (!site.canDownloadFiles()) {
return Promise.reject(null); throw new CoreError('Site doesn\'t allow downloading files.');
} }
const entry = await CoreWS.instance.downloadFile(fileUrl, filePath, addExtension, onProgress); const entry = await CoreWS.instance.downloadFile(fileUrl, filePath, addExtension, onProgress);
@ -952,7 +955,7 @@ export class CoreFilepoolProvider {
try { try {
await Promise.all(promises); await Promise.all(promises);
// Success prefetching, store package as downloaded. // Success prefetching, store package as downloaded.
this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra); await this.storePackageStatus(siteId, CoreConstants.DOWNLOADED, component, componentId, extra);
} catch (error) { } catch (error) {
// Error downloading, go back to previous status and reject the promise. // Error downloading, go back to previous status and reject the promise.
await this.setPackagePreviousStatus(siteId, component, componentId); await this.setPackagePreviousStatus(siteId, component, componentId);
@ -1014,7 +1017,7 @@ export class CoreFilepoolProvider {
let alreadyDownloaded = true; let alreadyDownloaded = true;
if (!CoreFile.instance.isAvailable()) { if (!CoreFile.instance.isAvailable()) {
throw null; throw new CoreError('File system cannot be used.');
} }
const file = await this.fixPluginfileURL(siteId, fileUrl); const file = await this.fixPluginfileURL(siteId, fileUrl);
@ -1093,14 +1096,12 @@ export class CoreFilepoolProvider {
let urls = []; let urls = [];
const element = CoreDomUtils.instance.convertToElement(html); const element = CoreDomUtils.instance.convertToElement(html);
const elements = element.querySelectorAll('a, img, audio, video, source, track'); const elements: (HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement |
HTMLTrackElement)[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track'));
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
const element = elements[i]; const element = elements[i];
let url = element.tagName === 'A' let url = 'href' in element ? element.href : element.src;
? (element as HTMLAnchorElement).href
: (element as HTMLImageElement | HTMLVideoElement | HTMLAudioElement |
HTMLAudioElement | HTMLTrackElement | HTMLSourceElement).src;
if (url && CoreUrlUtils.instance.isDownloadableUrl(url) && urls.indexOf(url) == -1) { if (url && CoreUrlUtils.instance.isDownloadableUrl(url) && urls.indexOf(url) == -1) {
urls.push(url); urls.push(url);
@ -1236,7 +1237,7 @@ export class CoreFilepoolProvider {
componentId: this.fixComponentId(componentId), componentId: this.fixComponentId(componentId),
}; };
const items = await db.getRecords(CoreFilepoolProvider.LINKS_TABLE, conditions); const items = await db.getRecords<CoreFilepoolLinksRecord>(CoreFilepoolProvider.LINKS_TABLE, conditions);
items.forEach((item) => { items.forEach((item) => {
item.componentId = this.fixComponentId(item.componentId); item.componentId = this.fixComponentId(item.componentId);
}); });
@ -1252,7 +1253,10 @@ export class CoreFilepoolProvider {
* @return Resolved with the URL. Rejected otherwise. * @return Resolved with the URL. Rejected otherwise.
*/ */
async getDirectoryUrlByUrl(siteId: string, fileUrl: string): Promise<string> { async getDirectoryUrlByUrl(siteId: string, fileUrl: string): Promise<string> {
if (CoreFile.instance.isAvailable()) { if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
const file = await this.fixPluginfileURL(siteId, fileUrl); const file = await this.fixPluginfileURL(siteId, fileUrl);
const fileId = this.getFileIdByUrl(file.fileurl); const fileId = this.getFileIdByUrl(file.fileurl);
const filePath = await this.getFilePath(siteId, fileId, ''); const filePath = await this.getFilePath(siteId, fileId, '');
@ -1261,9 +1265,6 @@ export class CoreFilepoolProvider {
return dirEntry.toURL(); return dirEntry.toURL();
} }
throw null;
}
/** /**
* Get the ID of a file download. Used to keep track of filePromises. * Get the ID of a file download. Used to keep track of filePromises.
* *
@ -1346,7 +1347,8 @@ export class CoreFilepoolProvider {
*/ */
protected async getFileLinks(siteId: string, fileId: string): Promise<CoreFilepoolLinksRecord[]> { protected async getFileLinks(siteId: string, fileId: string): Promise<CoreFilepoolLinksRecord[]> {
const db = await CoreSites.instance.getSiteDb(siteId); const db = await CoreSites.instance.getSiteDb(siteId);
const items = await db.getRecords(CoreFilepoolProvider.LINKS_TABLE, { fileId }); const items = await db.getRecords<CoreFilepoolLinksRecord>(CoreFilepoolProvider.LINKS_TABLE, { fileId });
items.forEach((item) => { items.forEach((item) => {
item.componentId = this.fixComponentId(item.componentId); item.componentId = this.fixComponentId(item.componentId);
}); });
@ -1421,7 +1423,7 @@ export class CoreFilepoolProvider {
const files = []; const files = [];
const promises = items.map((item) => const promises = items.map((item) =>
db.getRecord(CoreFilepoolProvider.FILES_TABLE, { fileId: item.fileId }).then((fileEntry) => { db.getRecord<CoreFilepoolFileEntry>(CoreFilepoolProvider.FILES_TABLE, { fileId: item.fileId }).then((fileEntry) => {
if (!fileEntry) { if (!fileEntry) {
return; return;
} }
@ -1532,7 +1534,7 @@ export class CoreFilepoolProvider {
* @param componentId An ID to use in conjunction with the component. * @param componentId An ID to use in conjunction with the component.
* @param timemodified The time this file was modified. * @param timemodified The time this file was modified.
* @param checkSize True if we shouldn't download files if their size is big, false otherwise. * @param checkSize True if we shouldn't download files if their size is big, false otherwise.
* @param downloadAny True to download file in WiFi if their size is any, false otherwise. * @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise.
* Ignored if checkSize=false. * Ignored if checkSize=false.
* @param options Extra options (isexternalfile, repositorytype). * @param options Extra options (isexternalfile, repositorytype).
* @param revision File revision. If not defined, it will be calculated using the URL. * @param revision File revision. If not defined, it will be calculated using the URL.
@ -1544,12 +1546,12 @@ export class CoreFilepoolProvider {
* If the file isn't downloaded or it's outdated, return the online URL and add it to the queue to be downloaded later. * If the file isn't downloaded or it's outdated, return the online URL and add it to the queue to be downloaded later.
*/ */
protected async getFileUrlByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number, protected async getFileUrlByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number,
mode: string = 'url', timemodified: number = 0, checkSize: boolean = true, downloadAny?: boolean, mode: string = 'url', timemodified: number = 0, checkSize: boolean = true, downloadUnknown?: boolean,
options: CoreFilepoolFileOptions = {}, revision?: number): Promise<string> { options: CoreFilepoolFileOptions = {}, revision?: number): Promise<string> {
const addToQueue = (fileUrl: string): void => { const addToQueue = (fileUrl: string): void => {
// Add the file to queue if needed and ignore errors. // Add the file to queue if needed and ignore errors.
this.addToQueueIfNeeded(siteId, fileUrl, component, componentId, timemodified, checkSize, this.addToQueueIfNeeded(siteId, fileUrl, component, componentId, timemodified, checkSize,
downloadAny, options, revision).catch(() => { downloadUnknown, options, revision).catch(() => {
// Ignore errors. // Ignore errors.
}); });
}; };
@ -1594,7 +1596,7 @@ export class CoreFilepoolProvider {
return fileUrl; return fileUrl;
} }
throw null; throw new CoreError('File not found.');
} }
}, () => { }, () => {
// We do not have the file in store yet. Add to queue and return the fixed URL. // We do not have the file in store yet. Add to queue and return the fixed URL.
@ -1614,16 +1616,16 @@ export class CoreFilepoolProvider {
* @return Resolved with the internal URL. Rejected otherwise. * @return Resolved with the internal URL. Rejected otherwise.
*/ */
protected async getInternalSrcById(siteId: string, fileId: string): Promise<string> { protected async getInternalSrcById(siteId: string, fileId: string): Promise<string> {
if (CoreFile.instance.isAvailable()) { if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
const path = await this.getFilePath(siteId, fileId); const path = await this.getFilePath(siteId, fileId);
const fileEntry = await CoreFile.instance.getFile(path); const fileEntry = await CoreFile.instance.getFile(path);
return CoreFile.instance.convertFileSrc(fileEntry.toURL()); return CoreFile.instance.convertFileSrc(fileEntry.toURL());
} }
throw null;
}
/** /**
* Returns the local URL of a file. * Returns the local URL of a file.
* *
@ -1632,7 +1634,10 @@ export class CoreFilepoolProvider {
* @return Resolved with the URL. Rejected otherwise. * @return Resolved with the URL. Rejected otherwise.
*/ */
protected async getInternalUrlById(siteId: string, fileId: string): Promise<string> { protected async getInternalUrlById(siteId: string, fileId: string): Promise<string> {
if (CoreFile.instance.isAvailable()) { if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
const path = await this.getFilePath(siteId, fileId); const path = await this.getFilePath(siteId, fileId);
const fileEntry = await CoreFile.instance.getFile(path); const fileEntry = await CoreFile.instance.getFile(path);
@ -1644,9 +1649,6 @@ export class CoreFilepoolProvider {
} }
} }
throw null;
}
/** /**
* Returns the local URL of a file. * Returns the local URL of a file.
* *
@ -1654,15 +1656,15 @@ export class CoreFilepoolProvider {
* @return Resolved with the URL. * @return Resolved with the URL.
*/ */
protected async getInternalUrlByPath(filePath: string): Promise<string> { protected async getInternalUrlByPath(filePath: string): Promise<string> {
if (CoreFile.instance.isAvailable()) { if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
const fileEntry = await CoreFile.instance.getFile(filePath); const fileEntry = await CoreFile.instance.getFile(filePath);
return fileEntry.toURL(); return fileEntry.toURL();
} }
throw null;
}
/** /**
* Returns the local URL of a file. * Returns the local URL of a file.
* *
@ -1671,16 +1673,16 @@ export class CoreFilepoolProvider {
* @return Resolved with the URL. Rejected otherwise. * @return Resolved with the URL. Rejected otherwise.
*/ */
async getInternalUrlByUrl(siteId: string, fileUrl: string): Promise<string> { async getInternalUrlByUrl(siteId: string, fileUrl: string): Promise<string> {
if (CoreFile.instance.isAvailable()) { if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
const file = await this.fixPluginfileURL(siteId, fileUrl); const file = await this.fixPluginfileURL(siteId, fileUrl);
const fileId = this.getFileIdByUrl(file.fileurl); const fileId = this.getFileIdByUrl(file.fileurl);
return this.getInternalUrlById(siteId, fileId); return this.getInternalUrlById(siteId, fileId);
} }
throw null;
}
/** /**
* Get the data stored for a package. * Get the data stored for a package.
* *
@ -1748,7 +1750,10 @@ export class CoreFilepoolProvider {
* @return Resolved with the URL. * @return Resolved with the URL.
*/ */
async getPackageDirUrlByUrl(siteId: string, url: string): Promise<string> { async getPackageDirUrlByUrl(siteId: string, url: string): Promise<string> {
if (CoreFile.instance.isAvailable()) { if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
const file = await this.fixPluginfileURL(siteId, url); const file = await this.fixPluginfileURL(siteId, url);
const dirName = this.getPackageDirNameByUrl(file.fileurl); const dirName = this.getPackageDirNameByUrl(file.fileurl);
const dirPath = await this.getFilePath(siteId, dirName, ''); const dirPath = await this.getFilePath(siteId, dirName, '');
@ -1757,9 +1762,6 @@ export class CoreFilepoolProvider {
return dirEntry.toURL(); return dirEntry.toURL();
} }
throw null;
}
/** /**
* Get a download promise. If the promise is not set, return undefined. * Get a download promise. If the promise is not set, return undefined.
* *
@ -1973,7 +1975,7 @@ export class CoreFilepoolProvider {
* @param componentId An ID to use in conjunction with the component. * @param componentId An ID to use in conjunction with the component.
* @param timemodified The time this file was modified. * @param timemodified The time this file was modified.
* @param checkSize True if we shouldn't download files if their size is big, false otherwise. * @param checkSize True if we shouldn't download files if their size is big, false otherwise.
* @param downloadAny True to download file in WiFi if their size is any, false otherwise. * @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise.
* Ignored if checkSize=false. * Ignored if checkSize=false.
* @param options Extra options (isexternalfile, repositorytype). * @param options Extra options (isexternalfile, repositorytype).
* @param revision File revision. If not defined, it will be calculated using the URL. * @param revision File revision. If not defined, it will be calculated using the URL.
@ -1983,10 +1985,10 @@ export class CoreFilepoolProvider {
* The URL returned is compatible to use with IMG tags. * The URL returned is compatible to use with IMG tags.
*/ */
getSrcByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number, timemodified: number = 0, getSrcByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number, timemodified: number = 0,
checkSize: boolean = true, downloadAny?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number): checkSize: boolean = true, downloadUnknown?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number):
Promise<string> { Promise<string> {
return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'src', return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'src',
timemodified, checkSize, downloadAny, options, revision); timemodified, checkSize, downloadUnknown, options, revision);
} }
/** /**
@ -2017,7 +2019,7 @@ export class CoreFilepoolProvider {
* @param componentId An ID to use in conjunction with the component. * @param componentId An ID to use in conjunction with the component.
* @param timemodified The time this file was modified. * @param timemodified The time this file was modified.
* @param checkSize True if we shouldn't download files if their size is big, false otherwise. * @param checkSize True if we shouldn't download files if their size is big, false otherwise.
* @param downloadAny True to download file in WiFi if their size is any, false otherwise. * @param downloadUnknown True to download file in WiFi if their size is unknown, false otherwise.
* Ignored if checkSize=false. * Ignored if checkSize=false.
* @param options Extra options (isexternalfile, repositorytype). * @param options Extra options (isexternalfile, repositorytype).
* @param revision File revision. If not defined, it will be calculated using the URL. * @param revision File revision. If not defined, it will be calculated using the URL.
@ -2027,10 +2029,10 @@ export class CoreFilepoolProvider {
* The URL returned is compatible to use with a local browser. * The URL returned is compatible to use with a local browser.
*/ */
getUrlByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number, timemodified: number = 0, getUrlByUrl(siteId: string, fileUrl: string, component: string, componentId?: string | number, timemodified: number = 0,
checkSize: boolean = true, downloadAny?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number): checkSize: boolean = true, downloadUnknown?: boolean, options: CoreFilepoolFileOptions = {}, revision?: number):
Promise<string> { Promise<string> {
return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'url', return this.getFileUrlByUrl(siteId, fileUrl, component, componentId, 'url',
timemodified, checkSize, downloadAny, options, revision); timemodified, checkSize, downloadUnknown, options, revision);
} }
/** /**
@ -2100,9 +2102,10 @@ export class CoreFilepoolProvider {
*/ */
protected async hasFileInPool(siteId: string, fileId: string): Promise<CoreFilepoolFileEntry> { protected async hasFileInPool(siteId: string, fileId: string): Promise<CoreFilepoolFileEntry> {
const db = await CoreSites.instance.getSiteDb(siteId); const db = await CoreSites.instance.getSiteDb(siteId);
const entry = await db.getRecord(CoreFilepoolProvider.FILES_TABLE, { fileId }); const entry = await db.getRecord<CoreFilepoolFileEntry>(CoreFilepoolProvider.FILES_TABLE, { fileId });
if (typeof entry === 'undefined') { if (typeof entry === 'undefined') {
throw null; throw new CoreError('File not found in filepool.');
} }
return entry; return entry;
@ -2118,12 +2121,13 @@ export class CoreFilepoolProvider {
protected async hasFileInQueue(siteId: string, fileId: string): Promise<CoreFilepoolQueueEntry> { protected async hasFileInQueue(siteId: string, fileId: string): Promise<CoreFilepoolQueueEntry> {
await this.dbReady; await this.dbReady;
const entry = await this.appDB.getRecord(CoreFilepoolProvider.QUEUE_TABLE, { siteId, fileId }); const entry = await this.appDB.getRecord<CoreFilepoolQueueEntry>(CoreFilepoolProvider.QUEUE_TABLE, { siteId, fileId });
if (typeof entry === 'undefined') { if (typeof entry === 'undefined') {
throw null; throw new CoreError('File not found in queue.');
} }
// Convert the links to an object. // Convert the links to an object.
entry.linksUnserialized = CoreTextUtils.instance.parseJSON(entry.links, []); entry.linksUnserialized = <CoreFilepoolComponentLink[]> CoreTextUtils.instance.parseJSON(entry.links, []);
return entry; return entry;
} }
@ -2132,14 +2136,14 @@ export class CoreFilepoolProvider {
* Invalidate all the files in a site. * Invalidate all the files in a site.
* *
* @param siteId The site ID. * @param siteId The site ID.
* @param onlyAny True to only invalidate files from external repos or without revision/timemodified. * @param onlyUnknown True to only invalidate files from external repos or without revision/timemodified.
* It is advised to set it to true to reduce the performance and data usage of the app. * It is advised to set it to true to reduce the performance and data usage of the app.
* @return Resolved on success. * @return Resolved on success.
*/ */
async invalidateAllFiles(siteId: string, onlyAny: boolean = true): Promise<void> { async invalidateAllFiles(siteId: string, onlyUnknown: boolean = true): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId); const db = await CoreSites.instance.getSiteDb(siteId);
const where = onlyAny ? CoreFilepoolProvider.FILE_UPDATE_ANY_WHERE_CLAUSE : null; const where = onlyUnknown ? CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE : null;
await db.updateRecordsWhere(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, where); await db.updateRecordsWhere(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, where);
} }
@ -2171,11 +2175,11 @@ export class CoreFilepoolProvider {
* @param siteId The site ID. * @param siteId The site ID.
* @param component The component to invalidate. * @param component The component to invalidate.
* @param componentId An ID to use in conjunction with the component. * @param componentId An ID to use in conjunction with the component.
* @param onlyAny True to only invalidate files from external repos or without revision/timemodified. * @param onlyUnknown True to only invalidate files from external repos or without revision/timemodified.
* It is advised to set it to true to reduce the performance and data usage of the app. * It is advised to set it to true to reduce the performance and data usage of the app.
* @return Resolved when done. * @return Resolved when done.
*/ */
async invalidateFilesByComponent(siteId: string, component: string, componentId?: string | number, onlyAny: boolean = true): async invalidateFilesByComponent(siteId: string, component: string, componentId?: string | number, onlyUnknown: boolean = true):
Promise<void> { Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId); const db = await CoreSites.instance.getSiteDb(siteId);
@ -2191,8 +2195,8 @@ export class CoreFilepoolProvider {
whereAndParams[0] = 'fileId ' + whereAndParams[0]; whereAndParams[0] = 'fileId ' + whereAndParams[0];
if (onlyAny) { if (onlyUnknown) {
whereAndParams[0] += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_ANY_WHERE_CLAUSE + ')'; whereAndParams[0] += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE + ')';
} }
await db.updateRecordsWhere(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, whereAndParams[0], whereAndParams[1]); await db.updateRecordsWhere(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, whereAndParams[0], whereAndParams[1]);
@ -2258,7 +2262,7 @@ export class CoreFilepoolProvider {
* @param entry Filepool entry. * @param entry Filepool entry.
* @return Whether it cannot determine updates. * @return Whether it cannot determine updates.
*/ */
protected isFileUpdateAny(entry: CoreFilepoolFileEntry): boolean { protected isFileUpdateUnknown(entry: CoreFilepoolFileEntry): boolean {
return !!entry.isexternalfile || (!entry.revision && !entry.timemodified); return !!entry.isexternalfile || (!entry.revision && !entry.timemodified);
} }
@ -2433,7 +2437,7 @@ export class CoreFilepoolProvider {
let items: CoreFilepoolQueueEntry[]; let items: CoreFilepoolQueueEntry[];
try { try {
items = await this.appDB.getRecords(CoreFilepoolProvider.QUEUE_TABLE, undefined, items = await this.appDB.getRecords<CoreFilepoolQueueEntry>(CoreFilepoolProvider.QUEUE_TABLE, undefined,
'priority DESC, added ASC', undefined, 0, 1); 'priority DESC, added ASC', undefined, 0, 1);
} catch (err) { } catch (err) {
throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY; throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY;
@ -2444,7 +2448,7 @@ export class CoreFilepoolProvider {
throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY; throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY;
} }
// Convert the links to an object. // Convert the links to an object.
item.linksUnserialized = CoreTextUtils.instance.parseJSON(item.links, []); item.linksUnserialized = <CoreFilepoolComponentLink[]> CoreTextUtils.instance.parseJSON(item.links, []);
return this.processQueueItem(item); return this.processQueueItem(item);
} }
@ -2760,7 +2764,7 @@ export class CoreFilepoolProvider {
const mimetype = await CoreUtils.instance.getMimeTypeFromUrl(url); const mimetype = await CoreUtils.instance.getMimeTypeFromUrl(url);
// If the file is streaming (audio or video) we reject. // If the file is streaming (audio or video) we reject.
if (mimetype.indexOf('video') != -1 || mimetype.indexOf('audio') != -1) { if (mimetype.indexOf('video') != -1 || mimetype.indexOf('audio') != -1) {
throw null; throw new CoreError('File is audio or video.');
} }
} }
@ -2988,9 +2992,9 @@ export type CoreFilepoolFileEntry = CoreFilepoolFileOptions & {
}; };
/** /**
* Entry from the file's queue. * DB data for entry from file's queue.
*/ */
export type CoreFilepoolQueueEntry = CoreFilepoolFileOptions & { export type CoreFilepoolQueueDBEntry = CoreFilepoolFileOptions & {
/** /**
* The site the file belongs to. * The site the file belongs to.
*/ */
@ -3025,7 +3029,12 @@ export type CoreFilepoolQueueEntry = CoreFilepoolFileOptions & {
* File links (to link the file to components and componentIds). Serialized to store on DB. * File links (to link the file to components and componentIds). Serialized to store on DB.
*/ */
links?: string; links?: string;
};
/**
* Entry from the file's queue.
*/
export type CoreFilepoolQueueEntry = CoreFilepoolQueueDBEntry & {
/** /**
* File links (to link the file to components and componentIds). * File links (to link the file to components and componentIds).
*/ */

View File

@ -16,7 +16,7 @@ import { Injectable } from '@angular/core';
import { Coordinates } from '@ionic-native/geolocation'; import { Coordinates } from '@ionic-native/geolocation';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreError } from '@classes/error'; import { CoreError } from '@classes/errors/error';
import { Geolocation, Diagnostic, makeSingleton } from '@singletons/core.singletons'; import { Geolocation, Diagnostic, makeSingleton } from '@singletons/core.singletons';
@Injectable() @Injectable()
@ -116,6 +116,7 @@ export class CoreGeolocationProvider {
* *
* @param error Error. * @param error Error.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
protected isCordovaPermissionDeniedError(error?: any): boolean { protected isCordovaPermissionDeniedError(error?: any): boolean {
return error && 'code' in error && 'PERMISSION_DENIED' in error && error.code === error.PERMISSION_DENIED; return error && 'code' in error && 'PERMISSION_DENIED' in error && error.code === error.PERMISSION_DENIED;
} }

View File

@ -16,6 +16,7 @@ import { Injectable } from '@angular/core';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreSite, CoreSiteWSPreSets } from '@classes/site'; import { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreError } from '@classes/errors/error';
import { makeSingleton, Translate } from '@singletons/core.singletons'; import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreWSExternalWarning } from '@services/ws'; import { CoreWSExternalWarning } from '@services/ws';
import { CoreCourseBase } from '@/types/global'; import { CoreCourseBase } from '@/types/global';
@ -79,9 +80,11 @@ export class CoreGroupsProvider {
preSets.emergencyCache = false; preSets.emergencyCache = false;
} }
const response = await site.read('core_group_get_activity_allowed_groups', params, preSets); const response: CoreGroupGetActivityAllowedGroupsResponse =
await site.read('core_group_get_activity_allowed_groups', params, preSets);
if (!response || !response.groups) { if (!response || !response.groups) {
throw null; throw new CoreError('Activity allowed groups not found.');
} }
return response; return response;
@ -195,9 +198,11 @@ export class CoreGroupsProvider {
preSets.emergencyCache = false; preSets.emergencyCache = false;
} }
const response = await site.read('core_group_get_activity_groupmode', params, preSets); const response: CoreGroupGetActivityGroupModeResponse =
await site.read('core_group_get_activity_groupmode', params, preSets);
if (!response || typeof response.groupmode == 'undefined') { if (!response || typeof response.groupmode == 'undefined') {
throw null; throw new CoreError('Activity group mode not found.');
} }
return response.groupmode; return response.groupmode;
@ -267,9 +272,11 @@ export class CoreGroupsProvider {
updateFrequency: CoreSite.FREQUENCY_RARELY, updateFrequency: CoreSite.FREQUENCY_RARELY,
}; };
const response = await site.read('core_group_get_course_user_groups', data, preSets); const response: CoreGroupGetCourseUserGroupsResponse =
await site.read('core_group_get_course_user_groups', data, preSets);
if (!response || !response.groups) { if (!response || !response.groups) {
throw null; throw new CoreError('User groups in course not found.');
} }
return response.groups; return response.groups;
@ -461,3 +468,26 @@ export type CoreGroupGetActivityAllowedGroupsResponse = {
canaccessallgroups?: boolean; // Whether the user will be able to access all the activity groups. canaccessallgroups?: boolean; // Whether the user will be able to access all the activity groups.
warnings?: CoreWSExternalWarning[]; warnings?: CoreWSExternalWarning[];
}; };
/**
* Result of WS core_group_get_activity_groupmode.
*/
export type CoreGroupGetActivityGroupModeResponse = {
groupmode: number; // Group mode: 0 for no groups, 1 for separate groups, 2 for visible groups.
warnings?: CoreWSExternalWarning[];
};
/**
* Result of WS core_group_get_course_user_groups.
*/
export type CoreGroupGetCourseUserGroupsResponse = {
groups: {
id: number; // Group record id.
name: string; // Multilang compatible name, course unique.
description: string; // Group description text.
descriptionformat: number; // Description format (1 = HTML, 0 = MOODLE, 2 = PLAIN or 4 = MARKDOWN).
idnumber: string; // Id number.
courseid?: number; // Course id.
}[];
warnings?: CoreWSExternalWarning[];
};

View File

@ -30,9 +30,9 @@ export class CoreLangProvider {
protected fallbackLanguage = 'en'; // Always use English as fallback language since it contains all strings. protected fallbackLanguage = 'en'; // Always use English as fallback language since it contains all strings.
protected defaultLanguage = CoreConfigConstants.default_lang || 'en'; // Lang to use if device lang not valid or is forced. protected defaultLanguage = CoreConfigConstants.default_lang || 'en'; // Lang to use if device lang not valid or is forced.
protected currentLanguage: string; // Save current language in a variable to speed up the get function. protected currentLanguage: string; // Save current language in a variable to speed up the get function.
protected customStrings = {}; // Strings defined using the admin tool. protected customStrings: CoreLanguageObject = {}; // Strings defined using the admin tool.
protected customStringsRaw: string; protected customStringsRaw: string;
protected sitePluginsStrings = {}; // Strings defined by site plugins. protected sitePluginsStrings: CoreLanguageObject = {}; // Strings defined by site plugins.
constructor() { constructor() {
// Set fallback language and language to use until the app determines the right language to use. // Set fallback language and language to use until the app determines the right language to use.
@ -110,11 +110,11 @@ export class CoreLangProvider {
* @param language New language to use. * @param language New language to use.
* @return Promise resolved when the change is finished. * @return Promise resolved when the change is finished.
*/ */
changeCurrentLanguage(language: string): Promise<unknown> { async changeCurrentLanguage(language: string): Promise<void> {
const promises = []; const promises = [];
// Change the language, resolving the promise when we receive the first value. // Change the language, resolving the promise when we receive the first value.
promises.push(new Promise((resolve, reject): void => { promises.push(new Promise((resolve, reject) => {
const subscription = Translate.instance.use(language).subscribe((data) => { const subscription = Translate.instance.use(language).subscribe((data) => {
// It's a language override, load the original one first. // It's a language override, load the original one first.
const fallbackLang = Translate.instance.instant('core.parentlanguage'); const fallbackLang = Translate.instance.instant('core.parentlanguage');
@ -165,13 +165,15 @@ export class CoreLangProvider {
this.currentLanguage = language; this.currentLanguage = language;
return Promise.all(promises).finally(() => { try {
await Promise.all(promises);
} finally {
// Load the custom and site plugins strings for the language. // Load the custom and site plugins strings for the language.
if (this.loadLangStrings(this.customStrings, language) || this.loadLangStrings(this.sitePluginsStrings, language)) { if (this.loadLangStrings(this.customStrings, language) || this.loadLangStrings(this.sitePluginsStrings, language)) {
// Some lang strings have changed, emit an event to update the pipes. // Some lang strings have changed, emit an event to update the pipes.
Translate.instance.onLangChange.emit({ lang: language, translations: Translate.instance.translations[language] }); Translate.instance.onLangChange.emit({ lang: language, translations: Translate.instance.translations[language] });
} }
}); }
} }
/** /**
@ -196,7 +198,7 @@ export class CoreLangProvider {
* *
* @return Custom strings. * @return Custom strings.
*/ */
getAllCustomStrings(): unknown { getAllCustomStrings(): CoreLanguageObject {
return this.customStrings; return this.customStrings;
} }
@ -205,7 +207,7 @@ export class CoreLangProvider {
* *
* @return Site plugins strings. * @return Site plugins strings.
*/ */
getAllSitePluginsStrings(): unknown { getAllSitePluginsStrings(): CoreLanguageObject {
return this.sitePluginsStrings; return this.sitePluginsStrings;
} }
@ -220,7 +222,7 @@ export class CoreLangProvider {
} }
// Get current language from config (user might have changed it). // Get current language from config (user might have changed it).
return CoreConfig.instance.get('current_language').then((language) => language).catch(() => { return CoreConfig.instance.get<string>('current_language').then((language) => language).catch(() => {
// User hasn't defined a language. If default language is forced, use it. // User hasn't defined a language. If default language is forced, use it.
if (CoreConfigConstants.default_lang && CoreConfigConstants.forcedefaultlanguage) { if (CoreConfigConstants.default_lang && CoreConfigConstants.forcedefaultlanguage) {
return CoreConfigConstants.default_lang; return CoreConfigConstants.default_lang;
@ -283,7 +285,7 @@ export class CoreLangProvider {
* @param lang The language to check. * @param lang The language to check.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
getTranslationTable(lang: string): Promise<unknown> { getTranslationTable(lang: string): Promise<Record<string, unknown>> {
// Create a promise to convert the observable into a promise. // Create a promise to convert the observable into a promise.
return new Promise((resolve, reject): void => { return new Promise((resolve, reject): void => {
const observer = Translate.instance.getTranslation(lang).subscribe((table) => { const observer = Translate.instance.getTranslation(lang).subscribe((table) => {

View File

@ -20,9 +20,11 @@ import { CoreApp, CoreAppSchema } from '@services/app';
import { CoreConfig } from '@services/config'; import { CoreConfig } from '@services/config';
import { CoreEventObserver, CoreEvents, CoreEventsProvider } from '@services/events'; import { CoreEventObserver, CoreEvents, CoreEventsProvider } from '@services/events';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils'; import { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { SQLiteDB } from '@classes/sqlitedb'; import { SQLiteDB } from '@classes/sqlitedb';
import { CoreSite } from '@classes/site';
import { CoreQueueRunner } from '@classes/queue-runner'; import { CoreQueueRunner } from '@classes/queue-runner';
import { CoreError } from '@classes/errors/error';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import CoreConfigConstants from '@app/config.json'; import CoreConfigConstants from '@app/config.json';
import { makeSingleton, NgZone, Platform, Translate, LocalNotifications, Push, Device } from '@singletons/core.singletons'; import { makeSingleton, NgZone, Platform, Translate, LocalNotifications, Push, Device } from '@singletons/core.singletons';
@ -94,14 +96,9 @@ export class CoreLocalNotificationsProvider {
protected appDB: SQLiteDB; protected appDB: SQLiteDB;
protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized. protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized.
protected codes: { [s: string]: number } = {}; protected codes: { [s: string]: number } = {};
protected codeRequestsQueue = {}; protected codeRequestsQueue: {[key: string]: CodeRequestsQueueItem} = {};
protected observables = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any
protected currentNotification = { protected observables: {[eventName: string]: {[component: string]: Subject<any>}} = {};
title: '',
texts: [],
ids: [],
timeouts: [],
};
protected triggerSubscription: Subscription; protected triggerSubscription: Subscription;
protected clickSubscription: Subscription; protected clickSubscription: Subscription;
@ -156,7 +153,7 @@ export class CoreLocalNotificationsProvider {
}); });
}); });
CoreEvents.instance.on(CoreEventsProvider.SITE_DELETED, (site) => { CoreEvents.instance.on(CoreEventsProvider.SITE_DELETED, (site: CoreSite) => {
if (site) { if (site) {
this.cancelSiteNotifications(site.id); this.cancelSiteNotifications(site.id);
} }
@ -270,13 +267,15 @@ export class CoreLocalNotificationsProvider {
try { try {
// Check if we already have a code stored for that ID. // Check if we already have a code stored for that ID.
const entry = await this.appDB.getRecord(table, { id: id }); const entry = await this.appDB.getRecord<{id: string; code: number}>(table, { id: id });
this.codes[key] = entry.code; this.codes[key] = entry.code;
return entry.code; return entry.code;
} catch (err) { } catch (err) {
// No code stored for that ID. Create a new code for it. // No code stored for that ID. Create a new code for it.
const entries = await this.appDB.getRecords(table, undefined, 'code DESC'); const entries = await this.appDB.getRecords<{id: string; code: number}>(table, undefined, 'code DESC');
let newCode = 0; let newCode = 0;
if (entries.length > 0) { if (entries.length > 0) {
newCode = entries[0].code + 1; newCode = entries[0].code + 1;
@ -326,7 +325,7 @@ export class CoreLocalNotificationsProvider {
*/ */
protected getUniqueNotificationId(notificationId: number, component: string, siteId: string): Promise<number> { protected getUniqueNotificationId(notificationId: number, component: string, siteId: string): Promise<number> {
if (!siteId || !component) { if (!siteId || !component) {
return Promise.reject(null); return Promise.reject(new CoreError('Site ID or component not supplied.'));
} }
return this.getSiteCode(siteId).then((siteCode) => this.getComponentCode(component).then((componentCode) => return this.getSiteCode(siteId).then((siteCode) => this.getComponentCode(component).then((componentCode) =>
@ -372,7 +371,9 @@ export class CoreLocalNotificationsProvider {
await this.dbReady; await this.dbReady;
try { try {
const stored = await this.appDB.getRecord(CoreLocalNotificationsProvider.TRIGGERED_TABLE, { id: notification.id }); const stored = await this.appDB.getRecord<{id: number; at: number}>(CoreLocalNotificationsProvider.TRIGGERED_TABLE,
{ id: notification.id });
let triggered = (notification.trigger && notification.trigger.at) || 0; let triggered = (notification.trigger && notification.trigger.at) || 0;
if (typeof triggered != 'number') { if (typeof triggered != 'number') {
@ -398,6 +399,7 @@ export class CoreLocalNotificationsProvider {
* *
* @param data Data received by the notification. * @param data Data received by the notification.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
notifyClick(data: any): void { notifyClick(data: any): void {
this.notifyEvent('click', data); this.notifyEvent('click', data);
} }
@ -408,6 +410,7 @@ export class CoreLocalNotificationsProvider {
* @param eventName Name of the event to notify. * @param eventName Name of the event to notify.
* @param data Data received by the notification. * @param data Data received by the notification.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
notifyEvent(eventName: string, data: any): void { notifyEvent(eventName: string, data: any): void {
// Execute the code in the Angular zone, so change detection doesn't stop working. // Execute the code in the Angular zone, so change detection doesn't stop working.
NgZone.instance.run(() => { NgZone.instance.run(() => {
@ -426,6 +429,7 @@ export class CoreLocalNotificationsProvider {
* @param data Notification data. * @param data Notification data.
* @return Parsed data. * @return Parsed data.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
protected parseNotificationData(data: any): any { protected parseNotificationData(data: any): any {
if (!data) { if (!data) {
return {}; return {};
@ -454,11 +458,11 @@ export class CoreLocalNotificationsProvider {
if (typeof request == 'object' && typeof request.table != 'undefined' && typeof request.id != 'undefined') { if (typeof request == 'object' && typeof request.table != 'undefined' && typeof request.id != 'undefined') {
// Get the code and resolve/reject all the promises of this request. // Get the code and resolve/reject all the promises of this request.
promise = this.getCode(request.table, request.id).then((code) => { promise = this.getCode(request.table, request.id).then((code) => {
request.promises.forEach((p) => { request.deferreds.forEach((p) => {
p.resolve(code); p.resolve(code);
}); });
}).catch((error) => { }).catch((error) => {
request.promises.forEach((p) => { request.deferreds.forEach((p) => {
p.reject(error); p.reject(error);
}); });
}); });
@ -508,7 +512,7 @@ export class CoreLocalNotificationsProvider {
return { return {
off: (): void => { off: (): void => {
this.observables[eventName][component].unsubscribe(callback); this.observables[eventName][component].unsubscribe();
}, },
}; };
} }
@ -539,13 +543,13 @@ export class CoreLocalNotificationsProvider {
if (typeof this.codeRequestsQueue[key] != 'undefined') { if (typeof this.codeRequestsQueue[key] != 'undefined') {
// There's already a pending request for this store and ID, add the promise to it. // There's already a pending request for this store and ID, add the promise to it.
this.codeRequestsQueue[key].promises.push(deferred); this.codeRequestsQueue[key].deferreds.push(deferred);
} else { } else {
// Add a pending request to the queue. // Add a pending request to the queue.
this.codeRequestsQueue[key] = { this.codeRequestsQueue[key] = {
table: table, table: table,
id: id, id: id,
promises: [deferred], deferreds: [deferred],
}; };
} }
@ -682,7 +686,7 @@ export class CoreLocalNotificationsProvider {
const entry = { const entry = {
id: notification.id, id: notification.id,
at: notification.trigger && notification.trigger.at ? notification.trigger.at : Date.now(), at: notification.trigger && notification.trigger.at ? notification.trigger.at.getTime() : Date.now(),
}; };
return this.appDB.insertRecord(CoreLocalNotificationsProvider.TRIGGERED_TABLE, entry); return this.appDB.insertRecord(CoreLocalNotificationsProvider.TRIGGERED_TABLE, entry);
@ -709,3 +713,9 @@ export class CoreLocalNotificationsProvider {
export class CoreLocalNotifications extends makeSingleton(CoreLocalNotificationsProvider) {} export class CoreLocalNotifications extends makeSingleton(CoreLocalNotificationsProvider) {}
export type CoreLocalNotificationsClickCallback<T = unknown> = (value: T) => void; export type CoreLocalNotificationsClickCallback<T = unknown> = (value: T) => void;
type CodeRequestsQueueItem = {
table: string;
id: string;
deferreds: PromiseDefer<number>[];
};

File diff suppressed because it is too large Load Diff

View File

@ -17,6 +17,8 @@ import { CoreEvents, CoreEventsProvider } from '@services/events';
import { CoreSites, CoreSiteSchema } from '@services/sites'; import { CoreSites, CoreSiteSchema } from '@services/sites';
import { makeSingleton } from '@singletons/core.singletons'; import { makeSingleton } from '@singletons/core.singletons';
const SYNC_TABLE = 'sync';
/* /*
* Service that provides some features regarding synchronization. * Service that provides some features regarding synchronization.
*/ */
@ -24,36 +26,35 @@ import { makeSingleton } from '@singletons/core.singletons';
export class CoreSyncProvider { export class CoreSyncProvider {
// Variables for the database. // Variables for the database.
protected SYNC_TABLE = 'sync';
protected siteSchema: CoreSiteSchema = { protected siteSchema: CoreSiteSchema = {
name: 'CoreSyncProvider', name: 'CoreSyncProvider',
version: 1, version: 1,
tables: [ tables: [
{ {
name: this.SYNC_TABLE, name: SYNC_TABLE,
columns: [ columns: [
{ {
name: 'component', name: 'component',
type: 'TEXT', type: 'TEXT',
notNull: true notNull: true,
}, },
{ {
name: 'id', name: 'id',
type: 'TEXT', type: 'TEXT',
notNull: true notNull: true,
}, },
{ {
name: 'time', name: 'time',
type: 'INTEGER' type: 'INTEGER',
}, },
{ {
name: 'warnings', name: 'warnings',
type: 'TEXT' type: 'TEXT',
} },
],
primaryKeys: ['component', 'id'],
},
], ],
primaryKeys: ['component', 'id']
}
]
}; };
// Store blocked sync objects. // Store blocked sync objects.
@ -63,7 +64,7 @@ export class CoreSyncProvider {
CoreSites.instance.registerSiteSchema(this.siteSchema); CoreSites.instance.registerSiteSchema(this.siteSchema);
// Unblock all blocks on logout. // Unblock all blocks on logout.
CoreEvents.instance.on(CoreEventsProvider.LOGOUT, (data) => { CoreEvents.instance.on(CoreEventsProvider.LOGOUT, (data: {siteId: string}) => {
this.clearAllBlocks(data.siteId); this.clearAllBlocks(data.siteId);
}); });
} }
@ -125,32 +126,32 @@ export class CoreSyncProvider {
/** /**
* Returns a sync record. * Returns a sync record.
*
* @param component Component name. * @param component Component name.
* @param id Unique ID per component. * @param id Unique ID per component.
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Record if found or reject. * @return Record if found or reject.
*/ */
getSyncRecord(component: string, id: string | number, siteId?: string): Promise<any> { getSyncRecord(component: string, id: string | number, siteId?: string): Promise<CoreSyncRecord> {
return CoreSites.instance.getSiteDb(siteId).then((db) => { return CoreSites.instance.getSiteDb(siteId).then((db) => db.getRecord(SYNC_TABLE, { component: component, id: id }));
return db.getRecord(this.SYNC_TABLE, { component: component, id: id });
});
} }
/** /**
* Inserts or Updates info of a sync record. * Inserts or Updates info of a sync record.
*
* @param component Component name. * @param component Component name.
* @param id Unique ID per component. * @param id Unique ID per component.
* @param data Data that updates the record. * @param data Data that updates the record.
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
* @return Promise resolved with done. * @return Promise resolved with done.
*/ */
insertOrUpdateSyncRecord(component: string, id: string | number, data: any, siteId?: string): Promise<any> { async insertOrUpdateSyncRecord(component: string, id: string, data: CoreSyncRecord, siteId?: string): Promise<void> {
return CoreSites.instance.getSiteDb(siteId).then((db) => { const db = await CoreSites.instance.getSiteDb(siteId);
data.component = component; data.component = component;
data.id = id; data.id = id;
return db.insertRecord(this.SYNC_TABLE, data); await db.insertRecord(SYNC_TABLE, data);
});
} }
/** /**
@ -206,6 +207,14 @@ export class CoreSyncProvider {
delete this.blockedItems[siteId][uniqueId][operation]; delete this.blockedItems[siteId][uniqueId][operation];
} }
} }
} }
export class CoreSync extends makeSingleton(CoreSyncProvider) {} export class CoreSync extends makeSingleton(CoreSyncProvider) {}
export type CoreSyncRecord = {
component: string;
id: string;
time: number;
warnings: string;
};

View File

@ -20,6 +20,8 @@ import CoreConfigConstants from '@app/config.json';
import { makeSingleton } from '@singletons/core.singletons'; import { makeSingleton } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
const VERSION_APPLIED = 'version_applied';
/** /**
* Factory to handle app updates. This factory shouldn't be used outside of core. * Factory to handle app updates. This factory shouldn't be used outside of core.
* *
@ -27,12 +29,12 @@ import { CoreLogger } from '@singletons/logger';
*/ */
@Injectable() @Injectable()
export class CoreUpdateManagerProvider implements CoreInitHandler { export class CoreUpdateManagerProvider implements CoreInitHandler {
// Data for init delegate. // Data for init delegate.
name = 'CoreUpdateManager'; name = 'CoreUpdateManager';
priority = CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 300; priority = CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 300;
blocking = true; blocking = true;
protected VERSION_APPLIED = 'version_applied';
protected logger: CoreLogger; protected logger: CoreLogger;
constructor() { constructor() {
@ -45,11 +47,11 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
* *
* @return Promise resolved when the update process finishes. * @return Promise resolved when the update process finishes.
*/ */
async load(): Promise<any> { async load(): Promise<void> {
const promises = []; const promises = [];
const versionCode = CoreConfigConstants.versioncode; const versionCode = CoreConfigConstants.versioncode;
const versionApplied: number = await CoreConfig.instance.get(this.VERSION_APPLIED, 0); const versionApplied = await CoreConfig.instance.get<number>(VERSION_APPLIED, 0);
if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) { if (versionCode >= 3900 && versionApplied < 3900 && versionApplied > 0) {
// @todo: H5P update. // @todo: H5P update.
@ -58,11 +60,12 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
try { try {
await Promise.all(promises); await Promise.all(promises);
await CoreConfig.instance.set(this.VERSION_APPLIED, versionCode); await CoreConfig.instance.set(VERSION_APPLIED, versionCode);
} catch (error) { } catch (error) {
this.logger.error(`Error applying update from ${versionApplied} to ${versionCode}`, error); this.logger.error(`Error applying update from ${versionApplied} to ${versionCode}`, error);
} }
} }
} }
export class CoreUpdateManager extends makeSingleton(CoreUpdateManagerProvider) {} export class CoreUpdateManager extends makeSingleton(CoreUpdateManagerProvider) {}

File diff suppressed because it is too large Load Diff

View File

@ -391,7 +391,7 @@ export class CoreIframeUtilsProvider {
return; return;
} }
if (!CoreUrlUtils.instance.isLocalFileUrlScheme(urlParts.protocol, urlParts.domain)) { if (!CoreUrlUtils.instance.isLocalFileUrlScheme(urlParts.protocol)) {
// Scheme suggests it's an external resource. // Scheme suggests it's an external resource.
event && event.preventDefault(); event && event.preventDefault();

View File

@ -17,6 +17,7 @@ import { DomSanitizer, SafeUrl } from '@angular/platform-browser';
import { CoreApp } from '@services/app'; import { CoreApp } from '@services/app';
import { CoreLang } from '@services/lang'; import { CoreLang } from '@services/lang';
import { CoreError } from '@classes/errors/error';
import { makeSingleton, Translate } from '@singletons/core.singletons'; import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreWSExternalFile } from '@services/ws'; import { CoreWSExternalFile } from '@services/ws';
import { Locutus } from '@singletons/locutus'; import { Locutus } from '@singletons/locutus';
@ -29,6 +30,8 @@ export type CoreTextErrorObject = {
error?: string; error?: string;
content?: string; content?: string;
body?: string; body?: string;
debuginfo?: string;
backtrace?: string;
}; };
/* /*
@ -526,10 +529,13 @@ export class CoreTextUtilsProvider {
* @param error Error object. * @param error Error object.
* @return Error message, undefined if not found. * @return Error message, undefined if not found.
*/ */
getErrorMessageFromError(error: string | CoreTextErrorObject): string { getErrorMessageFromError(error: string | CoreError | CoreTextErrorObject): string {
if (typeof error == 'string') { if (typeof error == 'string') {
return error; return error;
} }
if (error instanceof CoreError) {
return error.message;
}
return error && (error.message || error.error || error.content || error.body); return error && (error.message || error.error || error.content || error.body);
} }

View File

@ -424,18 +424,16 @@ export class CoreUrlUtilsProvider {
isLocalFileUrl(url: string): boolean { isLocalFileUrl(url: string): boolean {
const urlParts = CoreUrl.parse(url); const urlParts = CoreUrl.parse(url);
return this.isLocalFileUrlScheme(urlParts.protocol, urlParts.domain); return this.isLocalFileUrlScheme(urlParts.protocol);
} }
/** /**
* Check whether a URL scheme belongs to a local file. * Check whether a URL scheme belongs to a local file.
* *
* @param scheme Scheme to check. * @param scheme Scheme to check.
* @param notUsed Unused parameter.
* @return Whether the scheme belongs to a local file. * @return Whether the scheme belongs to a local file.
*/ */
// eslint-disable-next-line @typescript-eslint/no-unused-vars isLocalFileUrlScheme(scheme: string): boolean {
isLocalFileUrlScheme(scheme: string, notUsed?: string): boolean {
if (scheme) { if (scheme) {
scheme = scheme.toLowerCase(); scheme = scheme.toLowerCase();
} }

View File

@ -21,10 +21,11 @@ import { CoreApp } from '@services/app';
import { CoreEvents, CoreEventsProvider } from '@services/events'; import { CoreEvents, CoreEventsProvider } from '@services/events';
import { CoreFile } from '@services/file'; import { CoreFile } from '@services/file';
import { CoreLang } from '@services/lang'; import { CoreLang } from '@services/lang';
import { CoreWS, CoreWSError, CoreWSExternalFile } from '@services/ws'; import { CoreWS, CoreWSExternalFile } from '@services/ws';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreWSError } from '@classes/errors/wserror';
import { import {
makeSingleton, Clipboard, InAppBrowser, Platform, FileOpener, WebIntent, QRScanner, Translate, makeSingleton, Clipboard, InAppBrowser, Platform, FileOpener, WebIntent, QRScanner, Translate,
} from '@singletons/core.singletons'; } from '@singletons/core.singletons';
@ -325,6 +326,7 @@ export class CoreUtilsProvider {
* @param message The message to include in the error. * @param message The message to include in the error.
* @param needsTranslate If the message needs to be translated. * @param needsTranslate If the message needs to be translated.
* @return Fake WS error. * @return Fake WS error.
* @deprecated since 3.9.5. Just create the error directly.
*/ */
createFakeWSError(message: string, needsTranslate?: boolean): CoreWSError { createFakeWSError(message: string, needsTranslate?: boolean): CoreWSError {
return CoreWS.instance.createFakeWSError(message, needsTranslate); return CoreWS.instance.createFakeWSError(message, needsTranslate);

View File

@ -13,8 +13,9 @@
// limitations under the License. // limitations under the License.
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { HttpResponse } from '@angular/common/http'; import { HttpResponse, HttpParams } from '@angular/common/http';
import { FileEntry } from '@ionic-native/file';
import { FileUploadOptions } from '@ionic-native/file-transfer/ngx'; import { FileUploadOptions } from '@ionic-native/file-transfer/ngx';
import { Md5 } from 'ts-md5/dist/md5'; import { Md5 } from 'ts-md5/dist/md5';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
@ -25,20 +26,28 @@ import { CoreApp } from '@services/app';
import { CoreFile, CoreFileProvider } from '@services/file'; import { CoreFile, CoreFileProvider } from '@services/file';
import { CoreMimetypeUtils } from '@services/utils/mimetype'; import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text'; import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { CoreConstants } from '@core/constants'; import { CoreConstants } from '@core/constants';
import { CoreError } from '@classes/errors/error';
import { CoreInterceptor } from '@classes/interceptor'; import { CoreInterceptor } from '@classes/interceptor';
import { makeSingleton, Translate, FileTransfer, Http, Platform } from '@singletons/core.singletons'; import { makeSingleton, Translate, FileTransfer, Http, Platform, NativeHttp } from '@singletons/core.singletons';
import { CoreArray } from '@singletons/array';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
import { CoreWSError } from '@classes/errors/wserror';
import { CoreAjaxError } from '@classes/errors/ajaxerror';
import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
/** /**
* This service allows performing WS calls and download/upload files. * This service allows performing WS calls and download/upload files.
*/ */
@Injectable() @Injectable()
export class CoreWSProvider { export class CoreWSProvider {
protected logger: CoreLogger; protected logger: CoreLogger;
protected mimeTypeCache = {}; // A "cache" to store file mimetypes to prevent performing too many HEAD requests. protected mimeTypeCache: {[url: string]: string} = {}; // A "cache" to store file mimetypes to decrease HEAD requests.
protected ongoingCalls = {}; // eslint-disable-next-line @typescript-eslint/no-explicit-any
protected retryCalls = []; protected ongoingCalls: {[queueItemId: string]: Promise<any>} = {};
protected retryCalls: RetryCall[] = [];
protected retryTimeout = 0; protected retryTimeout = 0;
constructor() { constructor() {
@ -46,7 +55,7 @@ export class CoreWSProvider {
Platform.instance.ready().then(() => { Platform.instance.ready().then(() => {
if (CoreApp.instance.isIOS()) { if (CoreApp.instance.isIOS()) {
(<any> cordova).plugin.http.setHeader('User-Agent', navigator.userAgent); NativeHttp.instance.setHeader('*', 'User-Agent', navigator.userAgent);
} }
}); });
} }
@ -61,20 +70,15 @@ export class CoreWSProvider {
* @return Deferred promise resolved with the response data in success and rejected with the error message * @return Deferred promise resolved with the response data in success and rejected with the error message
* if it fails. * if it fails.
*/ */
protected addToRetryQueue(method: string, siteUrl: string, ajaxData: any, preSets: CoreWSPreSets): Promise<any> { protected addToRetryQueue<T = unknown>(method: string, siteUrl: string, data: unknown, preSets: CoreWSPreSets): Promise<T> {
const call: any = { const call = {
method, method,
siteUrl, siteUrl,
ajaxData, data,
preSets, preSets,
deferred: {} deferred: CoreUtils.instance.promiseDefer<T>(),
}; };
call.deferred.promise = new Promise((resolve, reject): void => {
call.deferred.resolve = resolve;
call.deferred.reject = reject;
});
this.retryCalls.push(call); this.retryCalls.push(call);
return call.deferred.promise; return call.deferred.promise;
@ -88,14 +92,11 @@ export class CoreWSProvider {
* @param preSets Extra settings and information. * @param preSets Extra settings and information.
* @return Promise resolved with the response data in success and rejected if it fails. * @return Promise resolved with the response data in success and rejected if it fails.
*/ */
call(method: string, data: any, preSets: CoreWSPreSets): Promise<any> { call<T = unknown>(method: string, data: unknown, preSets: CoreWSPreSets): Promise<T> {
let siteUrl;
if (!preSets) { if (!preSets) {
return Promise.reject(this.createFakeWSError('core.unexpectederror', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.unexpectederror')));
} else if (!CoreApp.instance.isOnline()) { } else if (!CoreApp.instance.isOnline()) {
return Promise.reject(this.createFakeWSError('core.networkerrormsg', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg')));
} }
preSets.typeExpected = preSets.typeExpected || 'object'; preSets.typeExpected = preSets.typeExpected || 'object';
@ -103,18 +104,18 @@ export class CoreWSProvider {
preSets.responseExpected = true; preSets.responseExpected = true;
} }
data = Object.assign({}, data); // Create a new object so the changes don't affect the original data. const dataToSend = Object.assign({}, data); // Create a new object so the changes don't affect the original data.
data.wsfunction = method; dataToSend['wsfunction'] = method;
data.wstoken = preSets.wsToken; dataToSend['wstoken'] = preSets.wsToken;
siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json'; const siteUrl = preSets.siteUrl + '/webservice/rest/server.php?moodlewsrestformat=json';
// There are some ongoing retry calls, wait for timeout. // There are some ongoing retry calls, wait for timeout.
if (this.retryCalls.length > 0) { if (this.retryCalls.length > 0) {
this.logger.warn('Calls locked, trying later...'); this.logger.warn('Calls locked, trying later...');
return this.addToRetryQueue(method, siteUrl, data, preSets); return this.addToRetryQueue<T>(method, siteUrl, data, preSets);
} else { } else {
return this.performPost(method, siteUrl, data, preSets); return this.performPost<T>(method, siteUrl, data, preSets);
} }
} }
@ -130,17 +131,17 @@ export class CoreWSProvider {
* - errorcode: Error code returned by the site (if any). * - errorcode: Error code returned by the site (if any).
* - available: 0 if unknown, 1 if available, -1 if not available. * - available: 0 if unknown, 1 if available, -1 if not available.
*/ */
callAjax(method: string, data: any, preSets: CoreWSAjaxPreSets): Promise<any> { callAjax<T = unknown>(method: string, data: Record<string, unknown>, preSets: CoreWSAjaxPreSets): Promise<T> {
const cacheParams = { const cacheParams = {
methodname: method, methodname: method,
args: data, args: data,
}; };
let promise = this.getPromiseHttp('ajax', preSets.siteUrl, cacheParams); let promise = this.getPromiseHttp<T>('ajax', preSets.siteUrl, cacheParams);
if (!promise) { if (!promise) {
promise = this.performAjax(method, data, preSets); promise = this.performAjax<T>(method, data, preSets);
promise = this.setPromiseHttp(promise, 'ajax', preSets.siteUrl, cacheParams); promise = this.setPromiseHttp<T>(promise, 'ajax', preSets.siteUrl, cacheParams);
} }
return promise; return promise;
@ -154,7 +155,9 @@ export class CoreWSProvider {
* @param stripUnicode If Unicode long chars need to be stripped. * @param stripUnicode If Unicode long chars need to be stripped.
* @return The cleaned object or null if some strings becomes empty after stripping Unicode. * @return The cleaned object or null if some strings becomes empty after stripping Unicode.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
convertValuesToString(data: any, stripUnicode?: boolean): any { convertValuesToString(data: any, stripUnicode?: boolean): any {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = Array.isArray(data) ? [] : {}; const result: any = Array.isArray(data) ? [] : {};
for (const key in data) { for (const key in data) {
@ -210,15 +213,14 @@ export class CoreWSProvider {
* @param needsTranslate If the message needs to be translated. * @param needsTranslate If the message needs to be translated.
* @param translateParams Translation params, if needed. * @param translateParams Translation params, if needed.
* @return Fake WS error. * @return Fake WS error.
* @deprecated since 3.9.5. Just create the error directly.
*/ */
createFakeWSError(message: string, needsTranslate?: boolean, translateParams?: {}): CoreWSError { createFakeWSError(message: string, needsTranslate?: boolean, translateParams?: {[name: string]: string}): CoreError {
if (needsTranslate) { if (needsTranslate) {
message = Translate.instance.instant(message, translateParams); message = Translate.instance.instant(message, translateParams);
} }
return { return new CoreError(message);
message,
};
} }
/** /**
@ -230,71 +232,68 @@ export class CoreWSProvider {
* @param onProgress Function to call on progress. * @param onProgress Function to call on progress.
* @return Promise resolved with the downloaded file. * @return Promise resolved with the downloaded file.
*/ */
downloadFile(url: string, path: string, addExtension?: boolean, onProgress?: (event: ProgressEvent) => any): Promise<any> { async downloadFile(url: string, path: string, addExtension?: boolean, onProgress?: (event: ProgressEvent) => void):
Promise<CoreWSDownloadedFileEntry> {
this.logger.debug('Downloading file', url, path, addExtension); this.logger.debug('Downloading file', url, path, addExtension);
if (!CoreApp.instance.isOnline()) { if (!CoreApp.instance.isOnline()) {
return Promise.reject(Translate.instance.instant('core.networkerrormsg')); throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
} }
// Use a tmp path to download the file and then move it to final location. // 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. // This is because if the download fails, the local file is deleted.
const tmpPath = path + '.tmp'; const tmpPath = path + '.tmp';
try {
// Create the tmp file as an empty file. // Create the tmp file as an empty file.
return CoreFile.instance.createFile(tmpPath).then((fileEntry) => { const fileEntry = await CoreFile.instance.createFile(tmpPath);
const transfer = FileTransfer.instance.create(); const transfer = FileTransfer.instance.create();
transfer.onProgress(onProgress); transfer.onProgress(onProgress);
return transfer.download(url, fileEntry.toURL(), true).then(() => { // Download the file in the tmp file.
let promise; await transfer.download(url, fileEntry.toURL(), true);
let extension = '';
if (addExtension) { if (addExtension) {
const ext = CoreMimetypeUtils.instance.getFileExtension(path); extension = CoreMimetypeUtils.instance.getFileExtension(path);
// Google Drive extensions will be considered invalid since Moodle usually converts them. // Google Drive extensions will be considered invalid since Moodle usually converts them.
if (!ext || ext == 'gdoc' || ext == 'gsheet' || ext == 'gslides' || ext == 'gdraw' || ext == 'php') { if (!extension || CoreArray.contains(['gdoc', 'gsheet', 'gslides', 'gdraw', 'php'], extension)) {
// Not valid, get the file's mimetype. // Not valid, get the file's mimetype.
promise = this.getRemoteFileMimeType(url).then((mime) => { const mimetype = await this.getRemoteFileMimeType(url);
if (mime) {
const remoteExt = CoreMimetypeUtils.instance.getExtension(mime, url); if (mimetype) {
const remoteExtension = CoreMimetypeUtils.instance.getExtension(mimetype, url);
// If the file is from Google Drive, ignore mimetype application/json. // If the file is from Google Drive, ignore mimetype application/json.
if (remoteExt && (!ext || mime != 'application/json')) { if (remoteExtension && (!extension || mimetype != 'application/json')) {
if (ext) { if (extension) {
// Remove existing extension since we will use another one. // Remove existing extension since we will use another one.
path = CoreMimetypeUtils.instance.removeExtension(path); path = CoreMimetypeUtils.instance.removeExtension(path);
} }
path += '.' + remoteExt; path += '.' + remoteExtension;
return remoteExt; extension = remoteExtension;
}
}
} }
} }
return ext; // Move the file to the final location.
}); const movedEntry: CoreWSDownloadedFileEntry = await CoreFile.instance.moveFile(tmpPath, path);
} else {
promise = Promise.resolve(ext);
}
} else {
promise = Promise.resolve('');
}
return promise.then((extension) => {
return CoreFile.instance.moveFile(tmpPath, path).then((movedEntry) => {
// Save the extension. // Save the extension.
movedEntry.extension = extension; movedEntry.extension = extension;
movedEntry.path = path; movedEntry.path = path;
this.logger.debug(`Success downloading file ${url} to ${path} with extension ${extension}`); this.logger.debug(`Success downloading file ${url} to ${path} with extension ${extension}`);
return movedEntry; return movedEntry;
}); } catch (error) {
}); this.logger.error(`Error downloading ${url} to ${path}`, error);
});
}).catch((err) => {
this.logger.error(`Error downloading ${url} to ${path}`, err);
return Promise.reject(err); throw error;
}); }
} }
/** /**
@ -304,13 +303,11 @@ export class CoreWSProvider {
* @param url Base URL of the HTTP request. * @param url Base URL of the HTTP request.
* @param params Params of the HTTP request. * @param params Params of the HTTP request.
*/ */
protected getPromiseHttp(method: string, url: string, params?: any): any { protected getPromiseHttp<T = unknown>(method: string, url: string, params?: Record<string, unknown>): Promise<T> {
const queueItemId = this.getQueueItemId(method, url, params); const queueItemId = this.getQueueItemId(method, url, params);
if (typeof this.ongoingCalls[queueItemId] != 'undefined') { if (typeof this.ongoingCalls[queueItemId] != 'undefined') {
return this.ongoingCalls[queueItemId]; return this.ongoingCalls[queueItemId];
} }
return false;
} }
/** /**
@ -334,10 +331,10 @@ export class CoreWSProvider {
this.mimeTypeCache[url] = mimeType; this.mimeTypeCache[url] = mimeType;
return mimeType || ''; return mimeType || '';
}).catch(() => { }).catch(() =>
// Error, resolve with empty mimetype. // Error, resolve with empty mimetype.
return ''; '',
}); );
} }
/** /**
@ -355,10 +352,10 @@ export class CoreWSProvider {
} }
return -1; return -1;
}).catch(() => { }).catch(() =>
// Error, return -1. // Error, return -1.
return -1; -1,
}); );
} }
/** /**
@ -378,7 +375,7 @@ export class CoreWSProvider {
* @param params Params of the HTTP request. * @param params Params of the HTTP request.
* @return Queue item ID. * @return Queue item ID.
*/ */
protected getQueueItemId(method: string, url: string, params?: any): string { protected getQueueItemId(method: string, url: string, params?: Record<string, unknown>): string {
if (params) { if (params) {
url += '###' + CoreInterceptor.serialize(params); url += '###' + CoreInterceptor.serialize(params);
} }
@ -397,14 +394,14 @@ export class CoreWSProvider {
* - errorcode: Error code returned by the site (if any). * - errorcode: Error code returned by the site (if any).
* - available: 0 if unknown, 1 if available, -1 if not available. * - available: 0 if unknown, 1 if available, -1 if not available.
*/ */
protected performAjax(method: string, data: any, preSets: CoreWSAjaxPreSets): Promise<any> { 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; let promise: Promise<HttpResponse<any>>;
if (typeof preSets.siteUrl == 'undefined') { if (typeof preSets.siteUrl == 'undefined') {
return rejectWithError(this.createFakeWSError('core.unexpectederror', true)); return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.unexpectederror')));
} else if (!CoreApp.instance.isOnline()) { } else if (!CoreApp.instance.isOnline()) {
return rejectWithError(this.createFakeWSError('core.networkerrormsg', true)); return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.networkerrormsg')));
} }
if (typeof preSets.responseExpected == 'undefined') { if (typeof preSets.responseExpected == 'undefined') {
@ -415,7 +412,7 @@ export class CoreWSProvider {
const ajaxData = [{ const ajaxData = [{
index: 0, index: 0,
methodname: method, methodname: method,
args: this.convertValuesToString(data) args: this.convertValuesToString(data),
}]; }];
// The info= parameter has no function. It is just to help with debugging. // The info= parameter has no function. It is just to help with debugging.
@ -426,18 +423,19 @@ export class CoreWSProvider {
// Send params using GET. // Send params using GET.
siteUrl += '&args=' + encodeURIComponent(JSON.stringify(ajaxData)); siteUrl += '&args=' + encodeURIComponent(JSON.stringify(ajaxData));
promise = this.sendHTTPRequest(siteUrl, { promise = this.sendHTTPRequest<T>(siteUrl, {
method: 'get', method: 'get',
}); });
} else { } else {
promise = this.sendHTTPRequest(siteUrl, { promise = this.sendHTTPRequest<T>(siteUrl, {
method: 'post', method: 'post',
data: ajaxData, // eslint-disable-next-line @typescript-eslint/no-explicit-any
data: <any> ajaxData,
serializer: 'json', serializer: 'json',
}); });
} }
return promise.then((response: HttpResponse<any>) => { return promise.then((response) => {
let data = response.body; let data = response.body;
// Some moodle web services return null. // Some moodle web services return null.
@ -448,39 +446,24 @@ export class CoreWSProvider {
// Check if error. Ajax layer should always return an object (if error) or an array (if success). // Check if error. Ajax layer should always return an object (if error) or an array (if success).
if (!data || typeof data != 'object') { if (!data || typeof data != 'object') {
return rejectWithError(this.createFakeWSError('core.serverconnection', true)); return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.serverconnection')));
} else if (data.error) { } else if (data.error) {
return rejectWithError(data); return Promise.reject(new CoreAjaxWSError(data));
} }
// Get the first response since only one request was done. // Get the first response since only one request was done.
data = data[0]; data = data[0];
if (data.error) { if (data.error) {
return rejectWithError(data.exception); return Promise.reject(new CoreAjaxWSError(data.exception));
} }
return data.data; return data.data;
}, (data) => { }, (data) => {
const available = data.status == 404 ? -1 : 0; const available = data.status == 404 ? -1 : 0;
return rejectWithError(this.createFakeWSError('core.serverconnection', true), available); return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.serverconnection'), available));
}); });
// Convenience function to return an error.
function rejectWithError(exception: any, available?: number): Promise<never> {
if (typeof available == 'undefined') {
if (exception.errorcode) {
available = exception.errorcode == 'invalidrecord' ? -1 : 1;
} else {
available = 0;
}
}
exception.available = available;
return Promise.reject(exception);
}
} }
/** /**
@ -489,16 +472,16 @@ export class CoreWSProvider {
* @param url URL to perform the request. * @param url URL to perform the request.
* @return Promise resolved with the response. * @return Promise resolved with the response.
*/ */
performHead(url: string): Promise<HttpResponse<any>> { performHead<T = unknown>(url: string): Promise<HttpResponse<T>> {
let promise = this.getPromiseHttp('head', url); let promise = this.getPromiseHttp<HttpResponse<T>>('head', url);
if (!promise) { if (!promise) {
promise = this.sendHTTPRequest(url, { promise = this.sendHTTPRequest<T>(url, {
method: 'head', method: 'head',
responseType: 'text', responseType: 'text',
}); });
promise = this.setPromiseHttp(promise, 'head', url); promise = this.setPromiseHttp<HttpResponse<T>>(promise, 'head', url);
} }
return promise; return promise;
@ -513,12 +496,12 @@ export class CoreWSProvider {
* @param preSets Extra settings and information. * @param preSets Extra settings and information.
* @return Promise resolved with the response data in success and rejected with CoreWSError if it fails. * @return Promise resolved with the response data in success and rejected with CoreWSError if it fails.
*/ */
performPost(method: string, siteUrl: string, ajaxData: any, preSets: CoreWSPreSets): Promise<any> { performPost<T = unknown>(method: string, siteUrl: string, ajaxData: unknown, preSets: CoreWSPreSets): Promise<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const options: any = {}; const options: any = {};
// This is done because some returned values like 0 are treated as null if responseType is json. // 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') { if (preSets.typeExpected == 'number' || preSets.typeExpected == 'boolean' || preSets.typeExpected == 'string') {
// Avalaible values are: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/responseType
options.responseType = 'text'; options.responseType = 'text';
} }
@ -530,8 +513,8 @@ export class CoreWSProvider {
// Perform the post request. // Perform the post request.
const promise = Http.instance.post(requestUrl, ajaxData, options).pipe(timeout(this.getRequestTimeout())).toPromise(); const promise = Http.instance.post(requestUrl, ajaxData, options).pipe(timeout(this.getRequestTimeout())).toPromise();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
return promise.then((data: any) => { return promise.then((data: any) => {
// Some moodle web services return null. // 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 the responseExpected value is set to false, we create a blank object if the response is null.
if (!data && !preSets.responseExpected) { if (!data && !preSets.responseExpected) {
@ -539,7 +522,7 @@ export class CoreWSProvider {
} }
if (!data) { if (!data) {
return Promise.reject(this.createFakeWSError('core.serverconnection', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection')));
} else if (typeof data != preSets.typeExpected) { } else if (typeof data != preSets.typeExpected) {
// If responseType is text an string will be returned, parse before returning. // If responseType is text an string will be returned, parse before returning.
if (typeof data == 'string') { if (typeof data == 'string') {
@ -548,7 +531,7 @@ export class CoreWSProvider {
if (isNaN(data)) { if (isNaN(data)) {
this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`); this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`);
return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
} else if (preSets.typeExpected == 'boolean') { } else if (preSets.typeExpected == 'boolean') {
if (data === 'true') { if (data === 'true') {
@ -558,17 +541,17 @@ export class CoreWSProvider {
} else { } else {
this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`); this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`);
return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
} else { } else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
} else { } else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
return Promise.reject(this.createFakeWSError('core.errorinvalidresponse', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
} }
@ -578,18 +561,18 @@ export class CoreWSProvider {
this.logger.error('Error calling WS', method, data); this.logger.error('Error calling WS', method, data);
} }
return Promise.reject(data); return Promise.reject(new CoreWSError(data));
} }
if (typeof data.debuginfo != 'undefined') { if (typeof data.debuginfo != 'undefined') {
return Promise.reject(this.createFakeWSError('Error. ' + data.message)); return Promise.reject(new CoreError('Error. ' + data.message));
} }
return data; return data;
}, (error) => { }, (error) => {
// If server has heavy load, retry after some seconds. // If server has heavy load, retry after some seconds.
if (error.status == 429) { if (error.status == 429) {
const retryPromise = this.addToRetryQueue(method, siteUrl, ajaxData, preSets); const retryPromise = this.addToRetryQueue<T>(method, siteUrl, ajaxData, preSets);
// Only process the queue one time. // Only process the queue one time.
if (this.retryTimeout == 0) { if (this.retryTimeout == 0) {
@ -610,7 +593,7 @@ export class CoreWSProvider {
return retryPromise; return retryPromise;
} }
return Promise.reject(this.createFakeWSError('core.serverconnection', true)); return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection')));
}); });
} }
@ -623,7 +606,7 @@ export class CoreWSProvider {
const call = this.retryCalls.shift(); const call = this.retryCalls.shift();
// Add a delay between calls. // Add a delay between calls.
setTimeout(() => { setTimeout(() => {
call.deferred.resolve(this.performPost(call.method, call.siteUrl, call.ajaxData, call.preSets)); call.deferred.resolve(this.performPost(call.method, call.siteUrl, call.data, call.preSets));
this.processRetryQueue(); this.processRetryQueue();
}, 200); }, 200);
} else { } else {
@ -640,14 +623,14 @@ export class CoreWSProvider {
* @param params Params of the HTTP request. * @param params Params of the HTTP request.
* @return The promise saved. * @return The promise saved.
*/ */
protected setPromiseHttp(promise: Promise<any>, method: string, url: string, params?: any): Promise<any> { protected setPromiseHttp<T = unknown>(promise: Promise<T>, method: string, url: string, params?: Record<string, unknown>):
Promise<T> {
const queueItemId = this.getQueueItemId(method, url, params); const queueItemId = this.getQueueItemId(method, url, params);
let timeout;
this.ongoingCalls[queueItemId] = promise; this.ongoingCalls[queueItemId] = promise;
// HTTP not finished, but we should delete the promise after timeout. // HTTP not finished, but we should delete the promise after timeout.
timeout = setTimeout(() => { const timeout = setTimeout(() => {
delete this.ongoingCalls[queueItemId]; delete this.ongoingCalls[queueItemId];
}, this.getRequestTimeout()); }, this.getRequestTimeout());
@ -667,22 +650,14 @@ export class CoreWSProvider {
* @param data Arguments to pass to the method. * @param data Arguments to pass to the method.
* @param preSets Extra settings and information. * @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 Promise resolved with the response data in success and rejected with the error message if it fails.
* @return Request response. If the request fails, returns an object with 'error'=true and 'message' properties. * @return Request response.
*/ */
syncCall(method: string, data: any, preSets: CoreWSPreSets): any { // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
const errorResponse = { syncCall<T = unknown>(method: string, data: any, preSets: CoreWSPreSets): T {
error: true,
message: '',
};
if (!preSets) { if (!preSets) {
errorResponse.message = Translate.instance.instant('core.unexpectederror'); throw new CoreError(Translate.instance.instant('core.unexpectederror'));
return errorResponse;
} else if (!CoreApp.instance.isOnline()) { } else if (!CoreApp.instance.isOnline()) {
errorResponse.message = Translate.instance.instant('core.networkerrormsg'); throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
return errorResponse;
} }
preSets.typeExpected = preSets.typeExpected || 'object'; preSets.typeExpected = preSets.typeExpected || 'object';
@ -693,9 +668,7 @@ export class CoreWSProvider {
data = this.convertValuesToString(data || {}, preSets.cleanUnicode); data = this.convertValuesToString(data || {}, preSets.cleanUnicode);
if (data == null) { if (data == null) {
// Empty cleaned text found. // Empty cleaned text found.
errorResponse.message = Translate.instance.instant('core.unicodenotsupportedcleanerror'); throw new CoreError(Translate.instance.instant('core.unicodenotsupportedcleanerror'));
return errorResponse;
} }
data.wsfunction = method; data.wsfunction = method;
@ -706,22 +679,21 @@ export class CoreWSProvider {
data = CoreInterceptor.serialize(data); data = CoreInterceptor.serialize(data);
// Perform sync request using XMLHttpRequest. // Perform sync request using XMLHttpRequest.
const xhr = new (<any> window).XMLHttpRequest(); const xhr = new XMLHttpRequest();
xhr.open('post', siteUrl, false); xhr.open('post', siteUrl, false);
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8'); xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded;charset=utf-8');
xhr.send(data); xhr.send(data);
// Get response. // Get response.
data = ('response' in xhr) ? xhr.response : xhr.responseText; // eslint-disable-next-line @typescript-eslint/no-explicit-any
data = ('response' in xhr) ? xhr.response : (<any> xhr).responseText;
// Check status. // Check status.
const status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0); const status = Math.max(xhr.status === 1223 ? 204 : xhr.status, 0);
if (status < 200 || status >= 300) { if (status < 200 || status >= 300) {
// Request failed. // Request failed.
errorResponse.message = data; throw new CoreError(data);
return errorResponse;
} }
// Treat response. // Treat response.
@ -734,18 +706,14 @@ export class CoreWSProvider {
} }
if (!data) { if (!data) {
errorResponse.message = Translate.instance.instant('core.serverconnection'); throw new CoreError(Translate.instance.instant('core.serverconnection'));
} else if (typeof data != preSets.typeExpected) { } else if (typeof data != preSets.typeExpected) {
this.logger.warn('Response of type "' + typeof data + '" received, expecting "' + preSets.typeExpected + '"'); this.logger.warn('Response of type "' + typeof data + '" received, expecting "' + preSets.typeExpected + '"');
errorResponse.message = Translate.instance.instant('core.errorinvalidresponse'); throw new CoreError(Translate.instance.instant('core.errorinvalidresponse'));
} }
if (typeof data.exception != 'undefined' || typeof data.debuginfo != 'undefined') { if (typeof data.exception != 'undefined' || typeof data.debuginfo != 'undefined') {
errorResponse.message = data.message; throw new CoreWSError(data);
}
if (errorResponse.message !== '') {
return errorResponse;
} }
return data; return data;
@ -760,16 +728,16 @@ export class CoreWSProvider {
* @param onProgress Function to call on progress. * @param onProgress Function to call on progress.
* @return Promise resolved when uploaded. * @return Promise resolved when uploaded.
*/ */
uploadFile(filePath: string, options: CoreWSFileUploadOptions, preSets: CoreWSPreSets, uploadFile<T = unknown>(filePath: string, options: CoreWSFileUploadOptions, preSets: CoreWSPreSets,
onProgress?: (event: ProgressEvent) => any): Promise<any> { onProgress?: (event: ProgressEvent) => void): Promise<T> {
this.logger.debug(`Trying to upload file: ${filePath}`); this.logger.debug(`Trying to upload file: ${filePath}`);
if (!filePath || !options || !preSets) { if (!filePath || !options || !preSets) {
return Promise.reject(null); return Promise.reject(new CoreError('Invalid options passed to upload file.'));
} }
if (!CoreApp.instance.isOnline()) { if (!CoreApp.instance.isOnline()) {
return Promise.reject(Translate.instance.instant('core.networkerrormsg')); return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg')));
} }
const uploadUrl = preSets.siteUrl + '/webservice/upload.php'; const uploadUrl = preSets.siteUrl + '/webservice/upload.php';
@ -781,34 +749,40 @@ export class CoreWSProvider {
options.params = { options.params = {
token: preSets.wsToken, token: preSets.wsToken,
filearea: options.fileArea || 'draft', filearea: options.fileArea || 'draft',
itemid: options.itemId || 0 itemid: options.itemId || 0,
}; };
options.chunkedMode = false; options.chunkedMode = false;
options.headers = { options.headers = {};
Connection: 'close' options['Connection'] = 'close';
};
return transfer.upload(filePath, uploadUrl, options, true).then((success) => { return transfer.upload(filePath, uploadUrl, options, true).then((success) => {
const data = CoreTextUtils.instance.parseJSON(success.response, null, const data = CoreTextUtils.instance.parseJSON(success.response, null,
this.logger.error.bind(this.logger, 'Error parsing response from upload', success.response)); this.logger.error.bind(this.logger, 'Error parsing response from upload', success.response));
if (data === null) { if (data === null) {
return Promise.reject(Translate.instance.instant('core.errorinvalidresponse')); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
if (!data) { if (!data) {
return Promise.reject(Translate.instance.instant('core.serverconnection')); return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection')));
} else if (typeof data != 'object') { } else if (typeof data != 'object') {
this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"'); this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"');
return Promise.reject(Translate.instance.instant('core.errorinvalidresponse')); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
} }
if (typeof data.exception !== 'undefined') { if (typeof data.exception !== 'undefined') {
return Promise.reject(data.message); return Promise.reject(new CoreWSError(data));
} else if (data && typeof data.error !== 'undefined') { } else if (typeof data.error !== 'undefined') {
return Promise.reject(data.error); return Promise.reject(new CoreWSError({
errorcode: data.errortype,
message: data.error,
}));
} else if (data[0] && typeof data[0].error !== 'undefined') { } else if (data[0] && typeof data[0].error !== 'undefined') {
return Promise.reject(data[0].error); return Promise.reject(new CoreWSError({
errorcode: data[0].errortype,
message: data[0].error,
}));
} }
// We uploaded only 1 file, so we only return the first file returned. // We uploaded only 1 file, so we only return the first file returned.
@ -818,7 +792,7 @@ export class CoreWSProvider {
}).catch((error) => { }).catch((error) => {
this.logger.error('Error while uploading file', filePath, error); this.logger.error('Error while uploading file', filePath, error);
return Promise.reject(Translate.instance.instant('core.errorinvalidresponse')); return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse')));
}); });
} }
@ -835,7 +809,7 @@ export class CoreWSProvider {
responseType: 'text', responseType: 'text',
}; };
const response = await this.sendHTTPRequest(url, options); const response = await this.sendHTTPRequest<string>(url, options);
const content = response.body; const content = response.body;
@ -853,8 +827,7 @@ export class CoreWSProvider {
* @param options Options for the request. * @param options Options for the request.
* @return Promise resolved with the response. * @return Promise resolved with the response.
*/ */
async sendHTTPRequest(url: string, options: HttpRequestOptions): Promise<HttpResponse<any>> { async sendHTTPRequest<T = unknown>(url: string, options: HttpRequestOptions): Promise<HttpResponse<T>> {
// Set default values. // Set default values.
options.responseType = options.responseType || 'json'; options.responseType = options.responseType || 'json';
options.timeout = typeof options.timeout == 'undefined' ? this.getRequestTimeout() : options.timeout; options.timeout = typeof options.timeout == 'undefined' ? this.getRequestTimeout() : options.timeout;
@ -867,8 +840,8 @@ export class CoreWSProvider {
const content = await CoreFile.instance.readFile(url, format); const content = await CoreFile.instance.readFile(url, format);
return new HttpResponse({ return new HttpResponse<T>({
body: content, body: <T> content,
headers: null, headers: null,
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
@ -876,81 +849,78 @@ export class CoreWSProvider {
}); });
} }
return new Promise<HttpResponse<any>>((resolve, reject): void => { return NativeHttp.instance.sendRequest(url, options).then((response) => new CoreNativeToAngularHttpResponse(response));
// 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 { } else {
let observable: Observable<any>; // eslint-disable-next-line @typescript-eslint/no-explicit-any
let observable: Observable<HttpResponse<any>>;
const angularOptions = <AngularHttpRequestOptions> options;
// Use Angular's library. // Use Angular's library.
switch (options.method) { switch (angularOptions.method) {
case 'get': case 'get':
observable = Http.instance.get(url, { observable = Http.instance.get(url, {
headers: options.headers, headers: angularOptions.headers,
params: options.params, params: angularOptions.params,
observe: 'response', observe: 'response',
responseType: <any> options.responseType, // eslint-disable-next-line @typescript-eslint/no-explicit-any
responseType: <any> angularOptions.responseType,
}); });
break; break;
case 'post': case 'post':
if (options.serializer == 'json') { if (angularOptions.serializer == 'json') {
options.data = JSON.stringify(options.data); angularOptions.data = JSON.stringify(angularOptions.data);
} }
observable = Http.instance.post(url, options.data, { observable = Http.instance.post(url, angularOptions.data, {
headers: options.headers, headers: angularOptions.headers,
observe: 'response', observe: 'response',
responseType: <any> options.responseType, // eslint-disable-next-line @typescript-eslint/no-explicit-any
responseType: <any> angularOptions.responseType,
}); });
break; break;
case 'head': case 'head':
observable = Http.instance.head(url, { observable = Http.instance.head(url, {
headers: options.headers, headers: angularOptions.headers,
observe: 'response', observe: 'response',
responseType: <any> options.responseType // eslint-disable-next-line @typescript-eslint/no-explicit-any
responseType: <any> angularOptions.responseType,
}); });
break; break;
default: default:
return Promise.reject('Method not implemented yet.'); return Promise.reject(new CoreError('Method not implemented yet.'));
} }
if (options.timeout) { if (angularOptions.timeout) {
observable = observable.pipe(timeout(options.timeout)); observable = observable.pipe(timeout(angularOptions.timeout));
} }
return observable.toPromise(); 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 class CoreWS extends makeSingleton(CoreWSProvider) {} export class CoreWS extends makeSingleton(CoreWSProvider) {}
/**
* Error returned by a WS call.
*/
export interface CoreWSError {
/**
* The error message.
*/
message: string;
/**
* Name of the exception. Undefined for local errors (fake WS errors).
*/
exception?: string;
/**
* The error code. Undefined for local errors (fake WS errors).
*/
errorcode?: string;
}
/** /**
* File upload options. * File upload options.
*/ */
@ -1084,7 +1054,7 @@ export type CoreWSPreSets = {
* Defaults to false. Clean multibyte Unicode chars from data. * Defaults to false. Clean multibyte Unicode chars from data.
*/ */
cleanUnicode?: boolean; cleanUnicode?: boolean;
} };
/** /**
* PreSets accepted by AJAX WS calls. * PreSets accepted by AJAX WS calls.
@ -1109,7 +1079,7 @@ export type CoreWSAjaxPreSets = {
* Whether to send the parameters via GET. Only if noLogin is true. * Whether to send the parameters via GET. Only if noLogin is true.
*/ */
useGet?: boolean; useGet?: boolean;
} };
/** /**
* Options for HTTP requests. * Options for HTTP requests.
@ -1118,17 +1088,17 @@ export type HttpRequestOptions = {
/** /**
* The HTTP method. * The HTTP method.
*/ */
method: string; method: 'get' | 'post' | 'put' | 'patch' | 'head' | 'delete' | 'options' | 'upload' | 'download';
/** /**
* Payload to send to the server. Only applicable on post, put or patch methods. * Payload to send to the server. Only applicable on post, put or patch methods.
*/ */
data?: any; data?: Record<string, unknown>;
/** /**
* Query params to be appended to the URL (only applicable on get, head, delete, upload or download methods). * Query params to be appended to the URL (only applicable on get, head, delete, upload or download methods).
*/ */
params?: any; params?: Record<string, string | number>;
/** /**
* Response type. Defaults to json. * Response type. Defaults to json.
@ -1143,7 +1113,7 @@ export type HttpRequestOptions = {
/** /**
* Serializer to use. Defaults to 'urlencoded'. Only for mobile environments. * Serializer to use. Defaults to 'urlencoded'. Only for mobile environments.
*/ */
serializer?: string; serializer?: 'json' | 'urlencoded' | 'utf8' | 'multipart';
/** /**
* Whether to follow redirects. Defaults to true. Only for mobile environments. * Whether to follow redirects. Defaults to true. Only for mobile environments.
@ -1153,16 +1123,45 @@ export type HttpRequestOptions = {
/** /**
* Headers. Only for mobile environments. * Headers. Only for mobile environments.
*/ */
headers?: {[name: string]: string}; headers?: Record<string, string>;
/** /**
* File paths to use for upload or download. Only for mobile environments. * File paths to use for upload or download. Only for mobile environments.
*/ */
filePath?: string; filePath?: string | string[];
/** /**
* Name to use during upload. Only for mobile environments. * Name to use during upload. Only for mobile environments.
*/ */
name?: string; 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: 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.
};

View File

@ -38,7 +38,7 @@ export class CoreArray {
*/ */
static flatten<T>(arr: T[][]): T[] { static flatten<T>(arr: T[][]): T[] {
if ('flat' in arr) { if ('flat' in arr) {
return (arr as any).flat(); return (arr as any).flat(); // eslint-disable-line @typescript-eslint/no-explicit-any
} }
return [].concat(...arr); return [].concat(...arr);

View File

@ -15,7 +15,13 @@
import { Injector, NgZone as NgZoneService } from '@angular/core'; import { Injector, NgZone as NgZoneService } from '@angular/core';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Platform as PlatformService } from '@ionic/angular'; import {
Platform as PlatformService,
AlertController as AlertControllerService,
LoadingController as LoadingControllerService,
ModalController as ModalControllerService,
ToastController as ToastControllerService,
} from '@ionic/angular';
import { Clipboard as ClipboardService } from '@ionic-native/clipboard/ngx'; import { Clipboard as ClipboardService } from '@ionic-native/clipboard/ngx';
import { Diagnostic as DiagnosticService } from '@ionic-native/diagnostic/ngx'; import { Diagnostic as DiagnosticService } from '@ionic-native/diagnostic/ngx';
@ -25,7 +31,9 @@ import { FileOpener as FileOpenerService } from '@ionic-native/file-opener/ngx';
import { FileTransfer as FileTransferService } from '@ionic-native/file-transfer/ngx'; import { FileTransfer as FileTransferService } from '@ionic-native/file-transfer/ngx';
import { Geolocation as GeolocationService } from '@ionic-native/geolocation/ngx'; import { Geolocation as GeolocationService } from '@ionic-native/geolocation/ngx';
import { Globalization as GlobalizationService } from '@ionic-native/globalization/ngx'; import { Globalization as GlobalizationService } from '@ionic-native/globalization/ngx';
import { HTTP } from '@ionic-native/http/ngx';
import { InAppBrowser as InAppBrowserService } from '@ionic-native/in-app-browser/ngx'; import { InAppBrowser as InAppBrowserService } from '@ionic-native/in-app-browser/ngx';
import { WebView as WebViewService } from '@ionic-native/ionic-webview/ngx';
import { Keyboard as KeyboardService } from '@ionic-native/keyboard/ngx'; import { Keyboard as KeyboardService } from '@ionic-native/keyboard/ngx';
import { LocalNotifications as LocalNotificationsService } from '@ionic-native/local-notifications/ngx'; import { LocalNotifications as LocalNotificationsService } from '@ionic-native/local-notifications/ngx';
import { Network as NetworkService } from '@ionic-native/network/ngx'; import { Network as NetworkService } from '@ionic-native/network/ngx';
@ -74,6 +82,7 @@ export class Globalization extends makeSingleton(GlobalizationService) {}
export class InAppBrowser extends makeSingleton(InAppBrowserService) {} export class InAppBrowser extends makeSingleton(InAppBrowserService) {}
export class Keyboard extends makeSingleton(KeyboardService) {} export class Keyboard extends makeSingleton(KeyboardService) {}
export class LocalNotifications extends makeSingleton(LocalNotificationsService) {} export class LocalNotifications extends makeSingleton(LocalNotificationsService) {}
export class NativeHttp extends makeSingleton(HTTP) {}
export class Network extends makeSingleton(NetworkService) {} export class Network extends makeSingleton(NetworkService) {}
export class Push extends makeSingleton(PushService) {} export class Push extends makeSingleton(PushService) {}
export class QRScanner extends makeSingleton(QRScannerService) {} export class QRScanner extends makeSingleton(QRScannerService) {}
@ -81,12 +90,17 @@ export class StatusBar extends makeSingleton(StatusBarService) {}
export class SplashScreen extends makeSingleton(SplashScreenService) {} export class SplashScreen extends makeSingleton(SplashScreenService) {}
export class SQLite extends makeSingleton(SQLiteService) {} export class SQLite extends makeSingleton(SQLiteService) {}
export class WebIntent extends makeSingleton(WebIntentService) {} export class WebIntent extends makeSingleton(WebIntentService) {}
export class WebView extends makeSingleton(WebViewService) {}
export class Zip extends makeSingleton(ZipService) {} export class Zip extends makeSingleton(ZipService) {}
// Convert some Angular and Ionic injectables to singletons. // Convert some Angular and Ionic injectables to singletons.
export class NgZone extends makeSingleton(NgZoneService) {} export class NgZone extends makeSingleton(NgZoneService) {}
export class Http extends makeSingleton(HttpClient) {} export class Http extends makeSingleton(HttpClient) {}
export class Platform extends makeSingleton(PlatformService) {} export class Platform extends makeSingleton(PlatformService) {}
export class AlertController extends makeSingleton(AlertControllerService) {}
export class LoadingController extends makeSingleton(LoadingControllerService) {}
export class ModalController extends makeSingleton(ModalControllerService) {}
export class ToastController extends makeSingleton(ToastControllerService) {}
// Convert external libraries injectables. // Convert external libraries injectables.
export class Translate extends makeSingleton(TranslateService) {} export class Translate extends makeSingleton(TranslateService) {}

View File

@ -400,7 +400,7 @@ function unserialize (str) {
} }
} }
function substr_replace (str, replace, start, length) { // eslint-disable-line camelcase function substr_replace (str, replace, start, length) {
// discuss at: https://locutus.io/php/substr_replace/ // discuss at: https://locutus.io/php/substr_replace/
// original by: Brett Zamir (https://brett-zamir.me) // original by: Brett Zamir (https://brett-zamir.me)
// example 1: substr_replace('ABCDEFGH:/MNRPQR/', 'bob', 0) // example 1: substr_replace('ABCDEFGH:/MNRPQR/', 'bob', 0)

View File

@ -24,6 +24,7 @@ export type CoreWindowOpenOptions = {
/** /**
* NavController to use when opening the link in the app. * NavController to use when opening the link in the app.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
navCtrl?: any; // @todo NavController; navCtrl?: any; // @todo NavController;
}; };
@ -60,11 +61,13 @@ export class CoreWindow {
await CoreUtils.instance.openFile(url); await CoreUtils.instance.openFile(url);
} else { } else {
let treated: boolean; let treated: boolean;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options = options || {}; options = options || {};
if (name != '_system') { if (name != '_system') {
// Check if it can be opened in the app. // Check if it can be opened in the app.
treated = false; // @todo await CoreContentLinksHelper.instance.handleLink(url, undefined, options.navCtrl, true, true); treated = false;
// @todo await CoreContentLinksHelper.instance.handleLink(url, undefined, options.navCtrl, true, true);
} }
if (!treated) { if (!treated) {

View File

@ -6,7 +6,8 @@
"cordova-plugin-file-transfer", "cordova-plugin-file-transfer",
"cordova-plugin-inappbrowser", "cordova-plugin-inappbrowser",
"cordova", "cordova",
"node" "node",
"dom-mediacapture-record"
], ],
"paths": { "paths": {
"@/*": ["*"], "@/*": ["*"],

View File

@ -22,7 +22,8 @@
"cordova-plugin-inappbrowser", "cordova-plugin-inappbrowser",
"cordova", "cordova",
"jest", "jest",
"node" "node",
"dom-mediacapture-record"
], ],
"paths": { "paths": {
"@/*": ["*"], "@/*": ["*"],