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"
}
},
"@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": {
"version": "5.28.0",
"resolved": "https://registry.npmjs.org/@ionic-native/keyboard/-/keyboard-5.28.0.tgz",
@ -3053,6 +3068,11 @@
"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": {
"version": "7.1.3",
"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/http": "^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/local-notifications": "^5.28.0",
"@ionic-native/network": "^5.28.0",
@ -60,6 +61,7 @@
"@ngx-translate/http-loader": "^6.0.0",
"@types/cordova": "0.0.34",
"@types/cordova-plugin-file-transfer": "^1.6.2",
"@types/dom-mediacapture-record": "^1.0.7",
"com-darryncampbell-cordova-plugin-intent": "^2.0.0",
"cordova": "^10.0.0",
"cordova-android": "^8.1.0",

View File

@ -15,11 +15,13 @@
import { NgModule, Injector } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { RouteReuseStrategy } from '@angular/router';
import { HttpClient, HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';
import { IonicModule, IonicRouteStrategy, Platform } from '@ionic/angular';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { CoreInterceptor } from '@classes/interceptor';
// Import core services.
import { CoreAppProvider } from '@services/app';
@ -49,23 +51,41 @@ import { CoreTimeUtilsProvider } from '@services/utils/time';
import { CoreUrlUtilsProvider } from '@services/utils/url';
import { CoreUtilsProvider } from '@services/utils/utils';
// Import core modules.
import { CoreEmulatorModule } from '@core/emulator/emulator.module';
import { CoreLoginModule } from '@core/login/login.module';
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({
declarations: [AppComponent],
entryComponents: [],
imports: [
BrowserModule,
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,
CoreEmulatorModule,
CoreLoginModule,
],
providers: [
{ provide: RouteReuseStrategy, useClass: IonicRouteStrategy },
{ provide: HTTP_INTERCEPTORS, useClass: CoreInterceptor, multi: true },
CoreAppProvider,
CoreConfigProvider,
CoreCronDelegate,
@ -96,8 +116,8 @@ import { setSingletonsInjector } from '@singletons/core.singletons';
bootstrap: [AppComponent],
})
export class AppModule {
constructor(injector: Injector, platform: Platform) {
constructor(injector: Injector, platform: Platform) {
// Set the injector.
setSingletonsInjector(injector);
@ -133,4 +153,5 @@ export class AppModule {
// Execute the init processes.
CoreInit.instance.executeInitProcesses();
}
}

View File

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

View File

@ -11,9 +11,10 @@
// 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 Faker from 'faker';
import { CoreError } from './error';
import { CoreError } from '@classes/errors/error';
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.
* @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 {
let query = '';
let fullSubName: string;
let subValue;
let innerObj;
for (const name in obj) {
const value = obj[name];
if (value instanceof Array) {
for (let i = 0; i < value.length; ++i) {
subValue = value[i];
fullSubName = name + '[' + i + ']';
innerObj = {};
const subValue = value[i];
const fullSubName = name + '[' + i + ']';
const innerObj = {};
innerObj[fullSubName] = subValue;
query += this.serialize(innerObj) + '&';
}
} else if (value instanceof Object) {
for (const subName in value) {
subValue = value[subName];
fullSubName = name + '[' + subName + ']';
innerObj = {};
const subValue = value[subName];
const fullSubName = name + '[' + subName + ']';
const innerObj = {};
innerObj[fullSubName] = subValue;
query += this.serialize(innerObj) + '&';
}
@ -63,6 +61,7 @@ export class CoreInterceptor implements HttpInterceptor {
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> {
// Add the header and serialize the body if needed.
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.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CoreQueueRunnerFunction<T> = (...args: any[]) => T | Promise<T>;
/**
* Queue item.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type CoreQueueRunnerItem<T = any> = {
/**
* 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 { SQLite, Platform } from '@singletons/core.singletons';
import { CoreError } from '@classes/errors/error';
/**
* Schema of a table.
@ -411,6 +412,7 @@ export class SQLiteDB {
* @param params Query parameters.
* @return Promise resolved with the result.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async execute(sql: string, params?: SQLiteDBRecordValue[]): Promise<any> {
await this.ready();
@ -425,7 +427,8 @@ export class SQLiteDB {
* @param sqlStatements SQL statements to execute.
* @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.db.sqlBatch(sqlStatements);
@ -453,9 +456,10 @@ export class SQLiteDB {
* Format the data to where params.
*
* @param data Object data.
* @return List of params.
*/
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.
* @return Promise resolved with the records.
*/
async getAllRecords(table: string): Promise<SQLiteDBRecordValues[]> {
async getAllRecords<T = unknown>(table: string): Promise<T[]> {
return this.getRecords(table);
}
@ -510,7 +514,7 @@ export class SQLiteDB {
async getFieldSql(sql: string, params?: SQLiteDBRecordValue[]): Promise<SQLiteDBRecordValue> {
const record = await this.getRecordSql(sql, params);
if (!record) {
throw null;
throw new CoreError('No record found.');
}
return record[Object.keys(record)[0]];
@ -574,10 +578,10 @@ export class SQLiteDB {
* @param fields A comma separated list of fields to return.
* @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);
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.
* @return Promise resolved with the record, rejected if not found.
*/
getRecordSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], fields: string = '*'):
Promise<SQLiteDBRecordValues> {
getRecordSelect<T = unknown>(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], fields: string = '*'):
Promise<T> {
if (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
* @return Promise resolved with the records.
*/
async getRecordSql(sql: string, params?: SQLiteDBRecordValue[]): Promise<SQLiteDBRecordValues> {
const result = await this.getRecordsSql(sql, params, 0, 1);
async getRecordSql<T = unknown>(sql: string, params?: SQLiteDBRecordValue[]): Promise<T> {
const result = await this.getRecordsSql<T>(sql, params, 0, 1);
if (!result || !result.length) {
// Not found, reject.
throw null;
throw new CoreError('No records found.');
}
return result[0];
@ -629,11 +633,11 @@ export class SQLiteDB {
* @param limitNum Return a subset comprising this many records in total.
* @return Promise resolved with the records.
*/
getRecords(table: string, conditions?: SQLiteDBRecordValues, sort: string = '', fields: string = '*', limitFrom: number = 0,
limitNum: number = 0): Promise<SQLiteDBRecordValues[]> {
getRecords<T = unknown>(table: string, conditions?: SQLiteDBRecordValues, sort: string = '', fields: string = '*',
limitFrom: number = 0, limitNum: number = 0): Promise<T[]> {
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.
* @return Promise resolved with the records.
*/
getRecordsList(table: string, field: string, values: SQLiteDBRecordValue[], sort: string = '', fields: string = '*',
limitFrom: number = 0, limitNum: number = 0): Promise<SQLiteDBRecordValues[]> {
getRecordsList<T = unknown>(table: string, field: string, values: SQLiteDBRecordValue[], sort: string = '',
fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise<T[]> {
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.
* @return Promise resolved with the records.
*/
getRecordsSelect(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], sort: string = '',
fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise<SQLiteDBRecordValues[]> {
getRecordsSelect<T = unknown>(table: string, select: string = '', params: SQLiteDBRecordValue[] = [], sort: string = '',
fields: string = '*', limitFrom: number = 0, limitNum: number = 0): Promise<T[]> {
if (select) {
select = ' WHERE ' + select;
}
@ -678,7 +682,7 @@ export class SQLiteDB {
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.
* @return Promise resolved with the records.
*/
async getRecordsSql(sql: string, params?: SQLiteDBRecordValue[], limitFrom?: number, limitNum?: number):
Promise<SQLiteDBRecordValues[]> {
async getRecordsSql<T = unknown>(sql: string, params?: SQLiteDBRecordValue[], limitFrom?: number, limitNum?: number):
Promise<T[]> {
const limits = this.normaliseLimitFromNum(limitFrom, limitNum);
if (limits[0] || limits[1]) {
@ -768,7 +772,7 @@ export class SQLiteDB {
*/
async insertRecords(table: string, dataObjects: SQLiteDBRecordValues[]): Promise<void> {
if (!Array.isArray(dataObjects)) {
throw null;
throw new CoreError('Invalid parameter supplied to insertRecords, it should be an array.');
}
const statements = dataObjects.map((dataObject) => {
@ -854,7 +858,7 @@ export class SQLiteDB {
async recordExists(table: string, conditions?: SQLiteDBRecordValues): Promise<void> {
const record = await this.getRecord(table, conditions);
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> {
const record = await this.getRecordSelect(table, select, params);
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> {
const record = await this.getRecordSql(sql, params);
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 { Geolocation } from '@ionic-native/geolocation/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 { WebView } from '@ionic-native/ionic-webview/ngx';
import { Keyboard } from '@ionic-native/keyboard/ngx';
import { LocalNotifications } from '@ionic-native/local-notifications/ngx';
import { Network } from '@ionic-native/network/ngx';
@ -58,6 +60,7 @@ import { Zip } from '@ionic-native/zip/ngx';
FileTransfer,
Geolocation,
Globalization,
HTTP,
InAppBrowser,
Keyboard,
LocalNotifications,
@ -68,6 +71,7 @@ import { Zip } from '@ionic-native/zip/ngx';
SQLite,
StatusBar,
WebIntent,
WebView,
Zip,
],
})

View File

@ -21,6 +21,7 @@ import { Pipe, PipeTransform } from '@angular/core';
name: 'coreCreateLinks',
})
export class CoreCreateLinksPipe implements PipeTransform {
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 {
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 {
return text.replace(/(<([^>]+)>)/ig, '');
}
}

View File

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

View File

@ -24,6 +24,7 @@ import moment from 'moment';
name: 'coreTimeAgo',
})
export class CoreTimeAgoPipe implements PipeTransform {
private logger: CoreLogger;
constructor() {
@ -48,6 +49,7 @@ export class CoreTimeAgoPipe implements PipeTransform {
timestamp = numberTimestamp;
}
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 { CoreEvents, CoreEventsProvider } from '@services/events';
import { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb';
import CoreConfigConstants from '@app/config.json';
import { makeSingleton, Keyboard, Network, StatusBar, Platform } from '@singletons/core.singletons';
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.
*
@ -38,23 +42,22 @@ import { CoreLogger } from '@singletons/logger';
*/
@Injectable()
export class CoreAppProvider {
protected DBNAME = 'MoodleMobile';
protected db: SQLiteDB;
protected logger: CoreLogger;
protected ssoAuthenticationPromise: Promise<any>;
protected ssoAuthenticationDeferred: PromiseDefer<void>;
protected isKeyboardShown = false;
protected _isKeyboardOpening = false;
protected _isKeyboardClosing = false;
protected backActions = [];
protected keyboardOpening = false;
protected keyboardClosing = false;
protected backActions: {callback: () => boolean; priority: number}[] = [];
protected mainMenuId = 0;
protected mainMenuOpen: number;
protected forceOffline = false;
// Variables for DB.
protected createVersionsTableReady: Promise<any>;
protected SCHEMA_VERSIONS_TABLE = 'schema_versions';
protected createVersionsTableReady: Promise<void>;
protected versionsTableSchema: SQLiteDBTableSchema = {
name: this.SCHEMA_VERSIONS_TABLE,
name: SCHEMA_VERSIONS_TABLE,
columns: [
{
name: 'name',
@ -68,11 +71,9 @@ export class CoreAppProvider {
],
};
constructor(appRef: ApplicationRef,
zone: NgZone) {
constructor(appRef: ApplicationRef, zone: NgZone) {
this.logger = CoreLogger.getInstance('CoreAppProvider');
this.db = CoreDB.instance.getDB(this.DBNAME);
this.db = CoreDB.instance.getDB(DBNAME);
// Create the schema versions table.
this.createVersionsTableReady = this.db.createTableFromSchema(this.versionsTableSchema);
@ -87,7 +88,7 @@ export class CoreAppProvider {
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.
zone.run(() => {
document.body.classList.remove('keyboard-is-open');
@ -95,18 +96,18 @@ export class CoreAppProvider {
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.
zone.run(() => {
this._isKeyboardOpening = true;
this._isKeyboardClosing = false;
this.keyboardOpening = true;
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.
zone.run(() => {
this._isKeyboardOpening = false;
this._isKeyboardClosing = true;
this.keyboardOpening = false;
this.keyboardClosing = true;
});
});
@ -116,8 +117,8 @@ export class CoreAppProvider {
// Export the app provider and appRef to control the application in Behat tests.
if (CoreAppProvider.isAutomated()) {
(<any> window).appProvider = this;
(<any> window).appRef = appRef;
(<WindowForAutomatedTests> window).appProvider = this;
(<WindowForAutomatedTests> window).appRef = appRef;
}
}
@ -145,7 +146,7 @@ export class CoreAppProvider {
* @return Whether the function is supported.
*/
canRecordMedia(): boolean {
return !!(<any> window).MediaRecorder;
return !!window.MediaRecorder;
}
/**
@ -163,7 +164,7 @@ export class CoreAppProvider {
* @param schema The schema to create.
* @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}`);
let oldVersion;
@ -173,7 +174,8 @@ export class CoreAppProvider {
await this.createVersionsTableReady;
// 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;
} catch (error) {
// No installed version yet.
@ -195,7 +197,7 @@ export class CoreAppProvider {
}
// 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 });
}
/**
@ -222,7 +224,7 @@ export class CoreAppProvider {
* @param storesConfig Config params to send the user to the right place.
* @return Store URL.
*/
getAppStoreUrl(storesConfig: CoreStoreConfig): string {
getAppStoreUrl(storesConfig: CoreStoreConfig): string {
if (this.isMac() && storesConfig.mac) {
return 'itms-apps://itunes.apple.com/app/' + storesConfig.mac;
}
@ -260,9 +262,7 @@ export class CoreAppProvider {
* @return Whether the app is running in a 64 bits desktop environment (not browser).
*/
is64Bits(): boolean {
const process = (<any> window).process;
return this.isDesktop() && process.arch == 'x64';
return this.isDesktop() && window.process.arch == 'x64';
}
/**
@ -280,9 +280,8 @@ export class CoreAppProvider {
* @return Whether the app is running in a desktop environment (not browser).
*/
isDesktop(): boolean {
const process = (<any> window).process;
return !!(process && process.versions && typeof process.versions.electron != 'undefined');
// @todo
return false;
}
/**
@ -300,7 +299,7 @@ export class CoreAppProvider {
* @return Whether keyboard is closing (animating).
*/
isKeyboardClosing(): boolean {
return this._isKeyboardClosing;
return this.keyboardClosing;
}
/**
@ -309,7 +308,7 @@ export class CoreAppProvider {
* @return Whether keyboard is opening (animating).
*/
isKeyboardOpening(): boolean {
return this._isKeyboardOpening;
return this.keyboardOpening;
}
/**
@ -462,8 +461,8 @@ export class CoreAppProvider {
*/
protected setKeyboardShown(shown: boolean): void {
this.isKeyboardShown = shown;
this._isKeyboardOpening = false;
this._isKeyboardClosing = false;
this.keyboardOpening = false;
this.keyboardClosing = false;
}
/**
@ -487,23 +486,15 @@ export class CoreAppProvider {
* NOT when the browser is opened.
*/
startSSOAuthentication(): void {
let cancelTimeout;
let resolvePromise;
this.ssoAuthenticationDeferred = CoreUtils.instance.promiseDefer<void>();
this.ssoAuthenticationPromise = new Promise((resolve, reject): void => {
resolvePromise = resolve;
// Resolve it automatically after 10 seconds (it should never take that long).
cancelTimeout = setTimeout(() => {
this.finishSSOAuthentication();
}, 10000);
});
// Store the resolve function in the promise itself.
(<any> this.ssoAuthenticationPromise).resolve = resolvePromise;
// Resolve it automatically after 10 seconds (it should never take that long).
const cancelTimeout = setTimeout(() => {
this.finishSSOAuthentication();
}, 10000);
// If the promise is resolved because finishSSOAuthentication is called, stop the cancel promise.
this.ssoAuthenticationPromise.then(() => {
this.ssoAuthenticationDeferred.promise.then(() => {
clearTimeout(cancelTimeout);
});
}
@ -512,9 +503,9 @@ export class CoreAppProvider {
* Finish an SSO authentication process.
*/
finishSSOAuthentication(): void {
if (this.ssoAuthenticationPromise) {
(<any> this.ssoAuthenticationPromise).resolve && (<any> this.ssoAuthenticationPromise).resolve();
this.ssoAuthenticationPromise = undefined;
if (this.ssoAuthenticationDeferred) {
this.ssoAuthenticationDeferred.resolve();
this.ssoAuthenticationDeferred = undefined;
}
}
@ -524,7 +515,7 @@ export class CoreAppProvider {
* @return Whether there's a SSO authentication ongoing.
*/
isSSOAuthenticationOngoing(): boolean {
return !!this.ssoAuthenticationPromise;
return !!this.ssoAuthenticationDeferred;
}
/**
@ -532,8 +523,8 @@ export class CoreAppProvider {
*
* @return Promise resolved once SSO authentication finishes.
*/
waitForSSOAuthentication(): Promise<any> {
return this.ssoAuthenticationPromise || Promise.resolve();
async waitForSSOAuthentication(): Promise<void> {
await this.ssoAuthenticationDeferred && this.ssoAuthenticationDeferred.promise;
}
/**
@ -542,27 +533,24 @@ export class CoreAppProvider {
* @param timeout Maximum time to wait, use null to wait forever.
*/
async waitForResume(timeout: number | null = null): Promise<void> {
let resolve: (value?: any) => void;
let resumeSubscription: any;
let timeoutId: NodeJS.Timer | false;
let deferred = CoreUtils.instance.promiseDefer<void>();
const promise = new Promise((r): any => resolve = r);
const stopWaiting = (): any => {
if (!resolve) {
const stopWaiting = () => {
if (!deferred) {
return;
}
resolve();
deferred.resolve();
resumeSubscription.unsubscribe();
timeoutId && clearTimeout(timeoutId);
resolve = null;
deferred = null;
};
resumeSubscription = Platform.instance.resume.subscribe(stopWaiting);
timeoutId = timeout ? setTimeout(stopWaiting, timeout) : false;
const resumeSubscription = Platform.instance.resume.subscribe(stopWaiting);
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
* actions has the highest priority and should be called.
*
* @param fn Called when the back button is pressed,
* if this registered action has the highest priority.
* @param callback Called when the back button is pressed, 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
* them returns true.
* * 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).
* @return A function that, when called, will unregister
* the back button action.
* - 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).
* @return A function that, when called, will unregister the back button action.
*/
registerBackButtonAction(fn: any, priority: number = 0): any {
const action = { fn, priority };
registerBackButtonAction(callback: () => boolean, priority: number = 0): () => boolean {
const action = { callback, priority };
this.backActions.push(action);
this.backActions.sort((a, b) => {
return b.priority - a.priority;
});
this.backActions.sort((a, b) => b.priority - a.priority);
return (): boolean => {
const index = this.backActions.indexOf(action);
@ -700,6 +684,7 @@ export class CoreAppProvider {
setForceOffline(value: boolean): void {
this.forceOffline = !!value;
}
}
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.
* @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 { makeSingleton } from '@singletons/core.singletons';
const TABLE_NAME = 'core_config';
/**
* Factory to provide access to dynamic and permanent config and settings.
* It should not be abused into a temporary storage.
*/
@Injectable()
export class CoreConfigProvider {
protected appDB: SQLiteDB;
protected TABLE_NAME = 'core_config';
protected tableSchema: CoreAppSchema = {
name: 'CoreConfigProvider',
version: 1,
tables: [
{
name: this.TABLE_NAME,
name: TABLE_NAME,
columns: [
{
name: 'name',
type: 'TEXT',
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() {
this.appDB = CoreApp.instance.getDB();
@ -62,10 +64,10 @@ export class CoreConfigProvider {
* @param name The config name.
* @return Promise resolved when done.
*/
async delete(name: string): Promise<any> {
async delete(name: string): Promise<void> {
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.
* @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;
try {
const entry = await this.appDB.getRecord(this.TABLE_NAME, { name });
const entry = await this.appDB.getRecord<ConfigDBEntry>(TABLE_NAME, { name });
return entry.value;
} catch (error) {
@ -98,11 +100,18 @@ export class CoreConfigProvider {
* @param value The config value. Can only store number or strings.
* @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;
return this.appDB.insertRecord(this.TABLE_NAME, { name, value });
await this.appDB.insertRecord(TABLE_NAME, { name, value });
}
}
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 { CoreConstants } from '@core/constants';
import { SQLiteDB } from '@classes/sqlitedb';
import { CoreError } from '@classes/errors/error';
import { makeSingleton, Network } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger';
/**
* 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>;
}
const CRON_TABLE = 'cron';
/*
* Service to handle cron processes. The registered processes will be executed every certain time.
*/
@Injectable()
export class CoreCronDelegate {
// Constants.
static DEFAULT_INTERVAL = 3600000; // Default interval is 1 hour.
static MIN_INTERVAL = 300000; // Minimum interval is 5 minutes.
static 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 DEFAULT_INTERVAL = 3600000; // Default interval is 1 hour.
static readonly MIN_INTERVAL = 300000; // Minimum interval is 5 minutes.
static readonly DESKTOP_MIN_INTERVAL = 60000; // Minimum interval in desktop is 1 minute.
static readonly MAX_TIME_PROCESS = 120000; // Max time a process can block the queue. Defaults to 2 minutes.
// Variables for database.
protected CRON_TABLE = 'cron';
protected tableSchema: CoreAppSchema = {
name: 'CoreCronDelegate',
version: 1,
tables: [
{
name: this.CRON_TABLE,
name: CRON_TABLE,
columns: [
{
name: 'id',
type: 'TEXT',
primaryKey: true
primaryKey: true,
},
{
name: 'value',
type: 'INTEGER'
type: 'INTEGER',
},
],
},
],
};
protected logger;
protected logger: CoreLogger;
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 queuePromise = Promise.resolve();
protected queuePromise: Promise<void> = Promise.resolve();
constructor(zone: NgZone) {
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.
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.
* @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) {
// 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);
@ -166,17 +112,17 @@ export class CoreCronDelegate {
if (usesNetwork && !CoreApp.instance.isOnline()) {
// 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);
return Promise.reject(null);
return Promise.reject(new CoreError(message));
}
if (isSync) {
// Check network connection.
promise = CoreConfig.instance.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false).then((syncOnlyOnWifi) => {
return !syncOnlyOnWifi || CoreApp.instance.isWifi();
});
promise = CoreConfig.instance.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false)
.then((syncOnlyOnWifi) => !syncOnlyOnWifi || CoreApp.instance.isWifi());
} else {
promise = Promise.resolve(true);
}
@ -184,30 +130,30 @@ export class CoreCronDelegate {
return promise.then((execute: boolean) => {
if (!execute) {
// 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);
return Promise.reject(null);
return Promise.reject(new CoreError(message));
}
// Add the execution to the queue.
this.queuePromise = this.queuePromise.catch(() => {
// Ignore errors in previous handlers.
}).then(() => {
return this.executeHandler(name, force, siteId).then(() => {
this.logger.debug(`Execution of handler '${name}' was a success.`);
}).then(() => this.executeHandler(name, force, siteId).then(() => {
this.logger.debug(`Execution of handler '${name}' was a success.`);
return this.setHandlerLastExecutionTime(name, Date.now()).then(() => {
this.scheduleNextExecution(name);
});
}, (error) => {
// Handler call failed. Retry soon.
this.logger.error(`Execution of handler '${name}' failed.`, error);
this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL);
return Promise.reject(null);
return this.setHandlerLastExecutionTime(name, Date.now()).then(() => {
this.scheduleNextExecution(name);
});
});
}, (error) => {
// Handler call failed. Retry soon.
const message = `Execution of handler '${name}' failed.`;
this.logger.error(message, error);
this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL);
return Promise.reject(new CoreError(message));
}));
return this.queuePromise;
});
@ -221,10 +167,8 @@ export class CoreCronDelegate {
* @param siteId Site ID. If not defined, all sites.
* @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 => {
let cancelTimeout;
this.logger.debug('Executing handler: ' + name);
// Wrap the call in Promise.resolve to make sure it's a promise.
@ -232,7 +176,7 @@ export class CoreCronDelegate {
clearTimeout(cancelTimeout);
});
cancelTimeout = setTimeout(() => {
const cancelTimeout = setTimeout(() => {
// 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.`);
resolve();
@ -247,7 +191,7 @@ export class CoreCronDelegate {
* @param siteId Site ID. If not defined, all sites.
* @return Promise resolved if all handlers are executed successfully, rejected otherwise.
*/
forceSyncExecution(siteId?: string): Promise<any> {
async forceSyncExecution(siteId?: string): Promise<void> {
const promises = [];
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.
* @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];
// Mark the handler as running (it might be running already).
@ -327,8 +271,9 @@ export class CoreCronDelegate {
const id = this.getHandlerLastExecutionId(name);
try {
const entry = await this.appDB.getRecord(this.CRON_TABLE, { id });
const time = parseInt(entry.value, 10);
const entry = await this.appDB.getRecord<CronDBEntry>(CRON_TABLE, { id });
const time = Number(entry.value);
return isNaN(time) ? 0 : time;
} catch (err) {
@ -489,16 +434,16 @@ export class CoreCronDelegate {
* @param time Time to set.
* @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;
const id = this.getHandlerLastExecutionId(name);
const entry = {
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);
delete this.handlers[name].timeout;
}
}
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()
export class CoreDbProvider {
protected dbInstances = {};
constructor() { }
protected dbInstances: {[name: string]: SQLiteDB} = {};
/**
* Get or create a database object.
@ -55,31 +53,31 @@ export class CoreDbProvider {
* @param name DB name.
* @return Promise resolved when the DB is deleted.
*/
deleteDB(name: string): Promise<any> {
let promise;
async deleteDB(name: string): Promise<void> {
if (typeof this.dbInstances[name] != 'undefined') {
// Close the database first.
promise = this.dbInstances[name].close();
} else {
promise = Promise.resolve();
}
await this.dbInstances[name].close();
return promise.then(() => {
const db = this.dbInstances[name];
delete this.dbInstances[name];
if (Platform.instance.is('cordova')) {
return SQLite.instance.deleteDatabase({
name,
location: 'default'
});
} else {
if (db instanceof SQLiteDBMock) {
// In WebSQL we cannot delete the database, just empty it.
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) {}

View File

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

View File

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

View File

@ -13,8 +13,10 @@
// limitations under the License.
import { Injectable } from '@angular/core';
import { FileEntry } from '@ionic-native/file';
import { CoreSites } from '@services/sites';
import { CoreWSExternalFile } from '@services/ws';
import { makeSingleton } from '@singletons/core.singletons';
/**
@ -26,9 +28,8 @@ import { makeSingleton } from '@singletons/core.singletons';
*/
@Injectable()
export class CoreFileSessionProvider {
protected files = {};
constructor() { }
protected files: {[siteId: string]: {[component: string]: {[id: string]: (CoreWSExternalFile | FileEntry)[]}}} = {};
/**
* Add a file to the session.
@ -38,7 +39,7 @@ export class CoreFileSessionProvider {
* @param file File to add.
* @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();
this.initFileArea(component, id, siteId);
@ -68,7 +69,7 @@ export class CoreFileSessionProvider {
* @param siteId Site ID. If not defined, current site.
* @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();
if (this.files[siteId] && this.files[siteId][component] && 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 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();
if (this.files[siteId] && this.files[siteId][component] && this.files[siteId][component][id]) {
const position = this.files[siteId][component][id].indexOf(file);
@ -140,13 +141,14 @@ export class CoreFileSessionProvider {
* @param newFiles Files to set.
* @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();
this.initFileArea(component, id, siteId);
this.files[siteId][component][id] = newFiles;
}
}
export class CoreFileSession extends makeSingleton(CoreFileSessionProvider) {}

View File

@ -14,15 +14,18 @@
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 { CoreWSExternalFile } from '@services/ws';
import { CoreMimetypeUtils } from '@services/utils/mimetype';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUtils } from '@services/utils/utils';
import CoreConfigConstants from '@app/config.json';
import { CoreError } from '@classes/errors/error';
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.
@ -49,23 +52,35 @@ export type CoreFileProgressEvent = {
*/
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.
*/
@Injectable()
export class CoreFileProvider {
// Formats to read a file.
static FORMATTEXT = 0;
static FORMATDATAURL = 1;
static FORMATBINARYSTRING = 2;
static FORMATARRAYBUFFER = 3;
static FORMATJSON = 4;
static readonly FORMATTEXT = CoreFileFormat.FORMATTEXT;
static readonly FORMATDATAURL = CoreFileFormat.FORMATDATAURL;
static readonly FORMATBINARYSTRING = CoreFileFormat.FORMATBINARYSTRING;
static readonly FORMATARRAYBUFFER = CoreFileFormat.FORMATARRAYBUFFER;
static readonly FORMATJSON = CoreFileFormat.FORMATJSON;
// Folders.
static SITESFOLDER = 'sites';
static TMPFOLDER = 'tmp';
static readonly SITESFOLDER = 'sites';
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 initialized = false;
@ -73,73 +88,9 @@ export class CoreFileProvider {
protected isHTMLAPI = false;
constructor() {
this.logger = CoreLogger.getInstance('CoreFileProvider');
if (CoreApp.instance.isAndroid() && !Object.getOwnPropertyDescriptor(FileReader.prototype, 'onloadend')) {
// 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);
}
}
// @todo: Check if redefining FileReader getters and setters is still needed in Android.
}
/**
@ -172,7 +123,6 @@ export class CoreFileProvider {
}
return Platform.instance.ready().then(() => {
if (CoreApp.instance.isAndroid()) {
this.basePath = File.instance.externalApplicationStorageDirectory || this.basePath;
} else if (CoreApp.instance.isIOS()) {
@ -180,7 +130,7 @@ export class CoreFileProvider {
} else if (!this.isAvailable() || this.basePath === '') {
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;
@ -208,9 +158,7 @@ export class CoreFileProvider {
this.logger.debug('Get file: ' + path);
return File.instance.resolveLocalFilesystemUrl(this.addBasePathIfNeeded(path));
}).then((entry) => {
return <FileEntry> entry;
});
}).then((entry) => <FileEntry> entry);
}
/**
@ -246,39 +194,36 @@ export class CoreFileProvider {
* @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.
*/
protected create(isDirectory: boolean, path: string, failIfExists?: boolean, base?: string): Promise<any> {
return this.init().then(() => {
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
base = base || this.basePath;
protected async create(isDirectory: boolean, path: string, failIfExists?: boolean, base?: string):
Promise<FileEntry | DirectoryEntry> {
await this.init();
if (path.indexOf('/') == -1) {
if (isDirectory) {
this.logger.debug('Create dir ' + path + ' in ' + base);
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
base = base || this.basePath;
return File.instance.createDir(base, path, !failIfExists);
} else {
this.logger.debug('Create file ' + path + ' in ' + base);
if (path.indexOf('/') == -1) {
if (isDirectory) {
this.logger.debug('Create dir ' + path + ' in ' + base);
return File.instance.createFile(base, path, !failIfExists);
}
return File.instance.createDir(base, path, !failIfExists);
} else {
// The file plugin doesn't allow creating more than 1 level at a time (e.g. tmp/folder).
// We need to create them 1 by 1.
const firstDir = path.substr(0, path.indexOf('/'));
const restOfPath = path.substr(path.indexOf('/') + 1);
this.logger.debug('Create file ' + path + ' in ' + base);
this.logger.debug('Create dir ' + firstDir + ' in ' + base);
return File.instance.createDir(base, firstDir, true).then((newDirEntry) => {
return this.create(isDirectory, restOfPath, failIfExists, newDirEntry.toURL());
}).catch((error) => {
this.logger.error('Error creating directory ' + firstDir + ' in ' + base);
return Promise.reject(error);
});
return File.instance.createFile(base, path, !failIfExists);
}
});
} else {
// The file plugin doesn't allow creating more than 1 level at a time (e.g. tmp/folder).
// We need to create them 1 by 1.
const firstDir = path.substr(0, path.indexOf('/'));
const restOfPath = path.substr(path.indexOf('/') + 1);
this.logger.debug('Create dir ' + firstDir + ' in ' + base);
const newDirEntry = await File.instance.createDir(base, firstDir, true);
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.
* @return Promise to be resolved when the directory is created.
*/
createDir(path: string, failIfExists?: boolean): Promise<DirectoryEntry> {
return this.create(true, path, failIfExists);
async createDir(path: string, failIfExists?: boolean): Promise<DirectoryEntry> {
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..
* @return Promise to be resolved when the file is created.
*/
createFile(path: string, failIfExists?: boolean): Promise<FileEntry> {
return this.create(false, path, failIfExists);
async createFile(path: string, failIfExists?: boolean): Promise<FileEntry> {
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.
* @return Promise to be resolved when the directory is deleted.
*/
removeDir(path: string): Promise<any> {
return this.init().then(() => {
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Remove directory: ' + path);
async removeDir(path: string): Promise<void> {
await this.init();
return File.instance.removeRecursively(this.basePath, path);
});
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Remove directory: ' + path);
await File.instance.removeRecursively(this.basePath, path);
}
/**
@ -325,23 +274,25 @@ export class CoreFileProvider {
* @param path Relative path to the file.
* @return Promise to be resolved when the file is deleted.
*/
removeFile(path: string): Promise<any> {
return this.init().then(() => {
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Remove file: ' + path);
async removeFile(path: string): Promise<void> {
await this.init();
return 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.
const decodedPath = decodeURI(path);
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Remove file: ' + path);
if (decodedPath != path) {
return File.instance.removeFile(this.basePath, decodedPath);
} else {
return Promise.reject(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.
const decodedPath = decodeURI(path);
if (decodedPath != path) {
await File.instance.removeFile(this.basePath, decodedPath);
} else {
throw error;
}
}
}
/**
@ -350,10 +301,8 @@ export class CoreFileProvider {
* @param fileEntry File Entry.
* @return Promise resolved when the file is deleted.
*/
removeFileByFileEntry(fileEntry: any): Promise<any> {
return new Promise((resolve, reject): void => {
fileEntry.remove(resolve, reject);
});
removeFileByFileEntry(entry: Entry): Promise<void> {
return new Promise((resolve, reject) => entry.remove(resolve, reject));
}
/**
@ -362,14 +311,26 @@ export class CoreFileProvider {
* @param path Relative path to the directory.
* @return Promise to be resolved when the contents are retrieved.
*/
getDirectoryContents(path: string): Promise<any> {
return this.init().then(() => {
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Get contents of dir: ' + path);
async getDirectoryContents(path: string): Promise<(FileEntry | DirectoryEntry)[]> {
await this.init();
return File.instance.listDir(this.basePath, path);
});
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Get contents of dir: ' + 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.
* @return Promise to be resolved when the size is calculated.
*/
protected getSize(entry: any): Promise<number> {
return new Promise((resolve, reject): void => {
if (entry.isDirectory) {
protected getSize(entry: DirectoryEntry | FileEntry): Promise<number> {
return new Promise((resolve, reject) => {
if (this.isDirectoryEntry(entry)) {
const directoryReader = entry.createReader();
directoryReader.readEntries((entries) => {
directoryReader.readEntries((entries: (DirectoryEntry | FileEntry)[]) => {
const promises = [];
for (let i = 0; i < entries.length; i++) {
promises.push(this.getSize(entries[i]));
}
Promise.all(promises).then((sizes) => {
let directorySize = 0;
for (let i = 0; i < sizes.length; i++) {
const fileSize = Number(sizes[i]);
@ -402,12 +362,9 @@ export class CoreFileProvider {
directorySize += fileSize;
}
resolve(directorySize);
}, reject);
}, reject);
} else if (entry.isFile) {
} else {
entry.file((file) => {
resolve(file.size);
}, reject);
@ -427,9 +384,7 @@ export class CoreFileProvider {
this.logger.debug('Get size of dir: ' + path);
return this.getDir(path).then((dirEntry) => {
return this.getSize(dirEntry);
});
return this.getDir(path).then((dirEntry) => this.getSize(dirEntry));
}
/**
@ -444,9 +399,7 @@ export class CoreFileProvider {
this.logger.debug('Get size of file: ' + path);
return this.getFile(path).then((fileEntry) => {
return this.getSize(fileEntry);
});
return this.getFile(path).then((fileEntry) => this.getSize(fileEntry));
}
/**
@ -455,7 +408,7 @@ export class CoreFileProvider {
* @param path Relative path to the file.
* @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 => {
this.logger.debug('Get file object of: ' + entry.fullPath);
entry.file(resolve, reject);
@ -496,15 +449,10 @@ export class CoreFileProvider {
* Read a file from local file system.
*
* @param path Relative path to the file.
* @param format Format to read the file. Must be one of:
* FORMATTEXT
* FORMATDATAURL
* FORMATBINARYSTRING
* FORMATARRAYBUFFER
* FORMATJSON
* @param format Format to read the file.
* @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.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Read file ' + path + ' with format ' + format);
@ -521,7 +469,7 @@ export class CoreFileProvider {
const parsed = CoreTextUtils.instance.parseJSON(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;
@ -535,27 +483,21 @@ export class CoreFileProvider {
* Read file contents from a file data object.
*
* @param fileData File's data.
* @param format Format to read the file. Must be one of:
* FORMATTEXT
* FORMATDATAURL
* FORMATBINARYSTRING
* FORMATARRAYBUFFER
* FORMATJSON
* @param format Format to read the file.
* @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;
this.logger.debug('Read file from file data with format ' + format);
return new Promise((resolve, reject): void => {
const reader = new FileReader();
reader.onloadend = (evt): void => {
const target = <any> evt.target; // Convert to <any> to be able to use non-standard properties.
if (target.result !== undefined && target.result !== null) {
reader.onloadend = (event): void => {
if (event.target.result !== undefined && event.target.result !== null) {
if (format == CoreFileProvider.FORMATJSON) {
// Convert to object.
const parsed = CoreTextUtils.instance.parseJSON(target.result, null);
const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null);
if (parsed == null) {
reject('Error parsing JSON file.');
@ -563,10 +505,10 @@ export class CoreFileProvider {
resolve(parsed);
} else {
resolve(target.result);
resolve(event.target.result);
}
} else if (target.error !== undefined && target.error !== null) {
reject(target.error);
} else if (event.target.error !== undefined && event.target.error !== null) {
reject(event.target.error);
} else {
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.
// Sometimes in Android the read doesn't start for some reason, so the promise never finishes.
let hasStarted = false;
reader.onloadstart = (evt): void => {
reader.onloadstart = () => {
hasStarted = true;
};
setTimeout(() => {
@ -597,7 +539,6 @@ export class CoreFileProvider {
default:
reader.readAsText(fileData);
}
});
}
@ -609,7 +550,7 @@ export class CoreFileProvider {
* @param append Whether to append the data to the end of the file.
* @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(() => {
// Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, ''));
@ -624,9 +565,8 @@ export class CoreFileProvider {
data = new Blob([data], { type: type || 'text/plain' });
}
return File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append }).then(() => {
return fileEntry;
});
return File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append })
.then(() => fileEntry);
});
});
}
@ -644,8 +584,7 @@ export class CoreFileProvider {
* @return Promise resolved when done.
*/
async writeFileDataInFile(file: Blob, path: string, onProgress?: CoreFileProgressFunction, offset: number = 0,
append?: boolean): Promise<FileEntry> {
append?: boolean): Promise<FileEntry> {
offset = offset || 0;
try {
@ -659,7 +598,7 @@ export class CoreFileProvider {
onProgress && onProgress({
lengthComputable: true,
loaded: offset,
total: file.size
total: file.size,
});
if (offset >= file.size) {
@ -671,8 +610,8 @@ export class CoreFileProvider {
return this.writeFileDataInFile(file, path, onProgress, offset, true);
} catch (error) {
if (error && error.target && error.target.error) {
// Error returned by the writer, get the "real" error.
error = error.target.error;
// Error returned by the writer, throw the "real" error.
throw error.target.error;
}
throw error;
@ -686,9 +625,7 @@ export class CoreFileProvider {
* @return Promise to be resolved when the file is retrieved.
*/
getExternalFile(fullPath: string): Promise<FileEntry> {
return File.instance.resolveLocalFilesystemUrl(fullPath).then((entry) => {
return <FileEntry> entry;
});
return File.instance.resolveLocalFilesystemUrl(fullPath).then((entry) => <FileEntry> entry);
}
/**
@ -709,11 +646,11 @@ export class CoreFileProvider {
* @param fullPath Absolute path to the file.
* @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 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(() => {
if (CoreApp.instance.isIOS()) {
// In iOS we want the internal URL (cdvfile://localhost/persistent/...).
return File.instance.resolveDirectoryUrl(this.basePath).then((dirEntry) => {
return dirEntry.toInternalURL();
});
return File.instance.resolveDirectoryUrl(this.basePath).then((dirEntry) => dirEntry.toInternalURL());
} else {
// In the other platforms we use the basePath as it is (file://...).
return this.basePath;
@ -776,8 +711,10 @@ export class CoreFileProvider {
* try to create it (slower).
* @return Promise resolved when the entry is moved.
*/
moveDir(originalPath: string, newPath: string, destDirExists?: boolean): Promise<any> {
return this.copyOrMoveFileOrDir(originalPath, newPath, true, false, destDirExists);
async moveDir(originalPath: string, newPath: string, destDirExists?: boolean): Promise<DirectoryEntry> {
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).
* @return Promise resolved when the entry is moved.
*/
moveFile(originalPath: string, newPath: string, destDirExists?: boolean): Promise<any> {
return this.copyOrMoveFileOrDir(originalPath, newPath, false, false, destDirExists);
async moveFile(originalPath: string, newPath: string, destDirExists?: boolean): Promise<FileEntry> {
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).
* @return Promise resolved when the entry is copied.
*/
copyDir(from: string, to: string, destDirExists?: boolean): Promise<any> {
return this.copyOrMoveFileOrDir(from, to, true, true, destDirExists);
async copyDir(from: string, to: string, destDirExists?: boolean): Promise<DirectoryEntry> {
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).
* @return Promise resolved when the entry is copied.
*/
copyFile(from: string, to: string, destDirExists?: boolean): Promise<any> {
return this.copyOrMoveFileOrDir(from, to, false, true, destDirExists);
async copyFile(from: string, to: string, destDirExists?: boolean): Promise<FileEntry> {
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).
* @return Promise resolved when the entry is copied.
*/
protected async copyOrMoveFileOrDir(from: string, to: string, isDir?: boolean, copy?: boolean, destDirExists?: boolean)
: Promise<Entry> {
protected async copyOrMoveFileOrDir(from: string, to: string, isDir?: boolean, copy?: boolean, destDirExists?: boolean):
Promise<FileEntry | DirectoryEntry> {
const fileIsInAppFolder = this.isPathInAppFolder(from);
if (!fileIsInAppFolder) {
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.moveDir.bind(File.instance) : File.instance.moveFile.bind(File.instance));
@ -885,10 +828,10 @@ export class CoreFileProvider {
* path/ -> directory: 'path', name: ''
* path -> directory: '', name: 'path'
*/
getFileAndDirectoryFromPath(path: string): {directory: string, name: string} {
getFileAndDirectoryFromPath(path: string): {directory: string; name: string} {
const file = {
directory: '',
name: ''
name: '',
};
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.
* @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.
let fileEntry: FileEntry;
@ -960,10 +904,10 @@ export class CoreFileProvider {
// Make sure the dest dir doesn't exist already.
return this.removeDir(destFolder).catch(() => {
// Ignore errors.
}).then(() => {
}).then(() =>
// 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(() => {
// 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);
}).then((result) => {
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.
* @return Promise resolved in success.
*/
replaceInFile(path: string, search: string | RegExp, newValue: string): Promise<any> {
return this.readFile(path).then((content) => {
if (typeof content == 'undefined' || content === null || !content.replace) {
return Promise.reject(null);
}
async replaceInFile(path: string, search: string | RegExp, newValue: string): Promise<void> {
let content = <string> await this.readFile(path);
if (content.match(search)) {
content = content.replace(search, newValue);
if (typeof content == 'undefined' || content === null || !content.replace) {
throw new CoreError(`Error reading file ${path}`);
}
return this.writeFile(path, content);
}
});
if (content.match(search)) {
content = content.replace(search, newValue);
await this.writeFile(path, content);
}
}
/**
@ -1007,7 +951,7 @@ export class CoreFileProvider {
*/
getMetadata(fileEntry: Entry): Promise<Metadata> {
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 => {
@ -1022,7 +966,7 @@ export class CoreFileProvider {
* @param isDir True if directory, false if file.
* @return Promise resolved with metadata.
*/
getMetadataFromPath(path: string, isDir?: boolean): Promise<any> {
getMetadataFromPath(path: string, isDir?: boolean): Promise<Metadata> {
let promise;
if (isDir) {
promise = this.getDir(path);
@ -1030,9 +974,7 @@ export class CoreFileProvider {
promise = this.getFile(path);
}
return promise.then((entry) => {
return this.getMetadata(entry);
});
return promise.then((entry) => this.getMetadata(entry));
}
/**
@ -1057,22 +999,22 @@ export class CoreFileProvider {
* @param copy True to copy, false to move.
* @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.
return this.getExternalFile(from).then((fileEntry) => {
// Create the destination dir if it doesn't exist.
const dirAndFile = this.getFileAndDirectoryFromPath(to);
return this.createDir(dirAndFile.directory).then((dirEntry) => {
return this.createDir(dirAndFile.directory).then((dirEntry) =>
// Now copy/move the file.
return new Promise((resolve, reject): void => {
new Promise((resolve, reject): void => {
if (copy) {
fileEntry.copyTo(dirEntry, dirAndFile.name, resolve, reject);
fileEntry.copyTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject);
} 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).
* @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);
}
@ -1094,7 +1036,7 @@ export class CoreFileProvider {
* @param to Relative new path of the file (inside the app folder).
* @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);
}
@ -1144,10 +1086,10 @@ export class CoreFileProvider {
// Ask the user what he wants to do.
return newName;
}
}).catch(() => {
}).catch(() =>
// 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.
*/
clearTmpFolder(): Promise<any> {
return this.removeDir(CoreFileProvider.TMPFOLDER).catch(() => {
// Ignore errors because the folder might not exist.
});
async clearTmpFolder(): Promise<void> {
// 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.
* @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.
return this.getDirectoryContents(dirPath).then((contents) => {
try {
const contents = await this.getDirectoryContents(dirPath);
if (!contents.length) {
return;
}
const filesMap = {};
const filesMap: {[fullPath: string]: FileEntry} = {};
const promises = [];
// Index the received files by fullPath and ignore the invalid ones.
files.forEach((file) => {
if (file.fullPath) {
if ('fullPath' in file) {
filesMap[file.fullPath] = file;
}
});
@ -1193,10 +1136,10 @@ export class CoreFileProvider {
}
});
return Promise.all(promises);
}).catch(() => {
await Promise.all(promises);
} catch (error) {
// Ignore errors, maybe it doesn't exist.
});
}
}
/**
@ -1246,7 +1189,7 @@ export class CoreFileProvider {
* @return Converted src.
*/
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 {
return !path || !path.match(/^[a-z0-9]+:\/\//i) || path.indexOf(this.basePath) != -1;
}
}
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 { CoreUtils, PromiseDefer } from '@services/utils/utils';
import { SQLiteDB } from '@classes/sqlitedb';
import { CoreError } from '@classes/errors/error';
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';
/*
@ -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_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))';
// Variables for database.
@ -239,7 +240,7 @@ export class CoreFilepoolProvider {
protected appDB: SQLiteDB;
protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized.
protected queueState: string;
protected urlAttributes = [
protected urlAttributes: RegExp[] = [
new RegExp('(\\?|&)token=([A-Za-z0-9]*)'),
new RegExp('(\\?|&)forcedownload=[0-1]'),
new RegExp('(\\?|&)preview=[A-Za-z0-9]+'),
@ -248,7 +249,7 @@ export class CoreFilepoolProvider {
// To handle file downloads using the queue.
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.
protected packagesPromises: { [s: string]: { [s: string]: Promise<void> } } = {};
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> {
if (!component) {
throw null;
throw new CoreError('Cannot add link because component is invalid.');
}
componentId = this.fixComponentId(componentId);
@ -358,8 +359,10 @@ export class CoreFilepoolProvider {
* @return Promise resolved on success.
*/
protected async addFileToPool(siteId: string, fileId: string, data: CoreFilepoolFileEntry): Promise<void> {
const record = Object.assign({}, data);
record.fileId = fileId;
const record = {
fileId,
...data,
};
const db = await CoreSites.instance.getSiteDb(siteId);
@ -457,12 +460,12 @@ export class CoreFilepoolProvider {
await this.dbReady;
if (!CoreFile.instance.isAvailable()) {
throw null;
throw new CoreError('File system cannot be used.');
}
const site = await CoreSites.instance.getSite(siteId);
if (!site.canDownloadFiles()) {
throw null;
throw new CoreError('Site doesn\'t allow downloading files.');
}
let file: CoreWSExternalFile;
@ -488,7 +491,7 @@ export class CoreFilepoolProvider {
const queueDeferred = this.getQueueDeferred(siteId, fileId, false, onProgress);
return this.hasFileInQueue(siteId, fileId).then((entry: CoreFilepoolQueueEntry) => {
const newData: CoreFilepoolQueueEntry = {};
const newData: CoreFilepoolQueueDBEntry = {};
let foundLink = false;
if (entry) {
@ -562,14 +565,14 @@ export class CoreFilepoolProvider {
* @param componentId An ID to use in conjunction with the component.
* @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 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.
* @param options Extra options (isexternalfile, repositorytype).
* @param revision File revision. If not defined, it will be calculated using the URL.
* @return Promise resolved when the file is downloaded.
*/
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> {
if (!checkSize) {
// No need to check size, just add it to the queue.
@ -584,7 +587,7 @@ export class CoreFilepoolProvider {
} else {
if (!CoreApp.instance.isOnline()) {
// Cannot check size in offline, stop.
throw null;
throw new CoreError(Translate.instance.instant('core.cannotconnect'));
}
size = await CoreWS.instance.getRemoteFileSize(fileUrl);
@ -592,16 +595,16 @@ export class CoreFilepoolProvider {
// Calculate the size of the file.
const isWifi = CoreApp.instance.isWifi();
const sizeAny = size <= 0;
const sizeUnknown = size <= 0;
if (!sizeAny) {
if (!sizeUnknown) {
// Store the size in the cache.
this.sizeCache[fileUrl] = size;
}
// Check if the file should be downloaded.
if (sizeAny) {
if (downloadAny && isWifi) {
if (sizeUnknown) {
if (downloadUnknown && isWifi) {
await this.addToQueueByUrl(siteId, fileUrl, component, componentId, timemodified, undefined, undefined,
0, options, revision, true);
}
@ -685,7 +688,7 @@ export class CoreFilepoolProvider {
const count = await db.countRecords(CoreFilepoolProvider.LINKS_TABLE, conditions);
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.
* @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) {
return { component, componentId: this.fixComponentId(componentId) };
}
@ -779,7 +782,7 @@ export class CoreFilepoolProvider {
if (poolFileObject && poolFileObject.fileId !== fileId) {
this.logger.error('Invalid object to update passed');
throw null;
throw new CoreError('Invalid object to update passed.');
}
const downloadId = this.getFileDownloadId(fileUrl, filePath);
@ -793,7 +796,7 @@ export class CoreFilepoolProvider {
this.filePromises[siteId][downloadId] = CoreSites.instance.getSite(siteId).then(async (site) => {
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);
@ -952,7 +955,7 @@ export class CoreFilepoolProvider {
try {
await Promise.all(promises);
// 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) {
// Error downloading, go back to previous status and reject the promise.
await this.setPackagePreviousStatus(siteId, component, componentId);
@ -1014,7 +1017,7 @@ export class CoreFilepoolProvider {
let alreadyDownloaded = true;
if (!CoreFile.instance.isAvailable()) {
throw null;
throw new CoreError('File system cannot be used.');
}
const file = await this.fixPluginfileURL(siteId, fileUrl);
@ -1093,14 +1096,12 @@ export class CoreFilepoolProvider {
let urls = [];
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++) {
const element = elements[i];
let url = element.tagName === 'A'
? (element as HTMLAnchorElement).href
: (element as HTMLImageElement | HTMLVideoElement | HTMLAudioElement |
HTMLAudioElement | HTMLTrackElement | HTMLSourceElement).src;
let url = 'href' in element ? element.href : element.src;
if (url && CoreUrlUtils.instance.isDownloadableUrl(url) && urls.indexOf(url) == -1) {
urls.push(url);
@ -1236,7 +1237,7 @@ export class CoreFilepoolProvider {
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) => {
item.componentId = this.fixComponentId(item.componentId);
});
@ -1252,16 +1253,16 @@ export class CoreFilepoolProvider {
* @return Resolved with the URL. Rejected otherwise.
*/
async getDirectoryUrlByUrl(siteId: string, fileUrl: string): Promise<string> {
if (CoreFile.instance.isAvailable()) {
const file = await this.fixPluginfileURL(siteId, fileUrl);
const fileId = this.getFileIdByUrl(file.fileurl);
const filePath = await this.getFilePath(siteId, fileId, '');
const dirEntry = await CoreFile.instance.getDir(filePath);
return dirEntry.toURL();
if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
throw null;
const file = await this.fixPluginfileURL(siteId, fileUrl);
const fileId = this.getFileIdByUrl(file.fileurl);
const filePath = await this.getFilePath(siteId, fileId, '');
const dirEntry = await CoreFile.instance.getDir(filePath);
return dirEntry.toURL();
}
/**
@ -1346,7 +1347,8 @@ export class CoreFilepoolProvider {
*/
protected async getFileLinks(siteId: string, fileId: string): Promise<CoreFilepoolLinksRecord[]> {
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) => {
item.componentId = this.fixComponentId(item.componentId);
});
@ -1421,7 +1423,7 @@ export class CoreFilepoolProvider {
const files = [];
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) {
return;
}
@ -1532,7 +1534,7 @@ export class CoreFilepoolProvider {
* @param componentId An ID to use in conjunction with the component.
* @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 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.
* @param options Extra options (isexternalfile, repositorytype).
* @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.
*/
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> {
const addToQueue = (fileUrl: string): void => {
// Add the file to queue if needed and ignore errors.
this.addToQueueIfNeeded(siteId, fileUrl, component, componentId, timemodified, checkSize,
downloadAny, options, revision).catch(() => {
downloadUnknown, options, revision).catch(() => {
// Ignore errors.
});
};
@ -1594,7 +1596,7 @@ export class CoreFilepoolProvider {
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.
@ -1614,14 +1616,14 @@ export class CoreFilepoolProvider {
* @return Resolved with the internal URL. Rejected otherwise.
*/
protected async getInternalSrcById(siteId: string, fileId: string): Promise<string> {
if (CoreFile.instance.isAvailable()) {
const path = await this.getFilePath(siteId, fileId);
const fileEntry = await CoreFile.instance.getFile(path);
return CoreFile.instance.convertFileSrc(fileEntry.toURL());
if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
throw null;
const path = await this.getFilePath(siteId, fileId);
const fileEntry = await CoreFile.instance.getFile(path);
return CoreFile.instance.convertFileSrc(fileEntry.toURL());
}
/**
@ -1632,19 +1634,19 @@ export class CoreFilepoolProvider {
* @return Resolved with the URL. Rejected otherwise.
*/
protected async getInternalUrlById(siteId: string, fileId: string): Promise<string> {
if (CoreFile.instance.isAvailable()) {
const path = await this.getFilePath(siteId, fileId);
const fileEntry = await CoreFile.instance.getFile(path);
// This URL is usually used to launch files or put them in HTML. In desktop we need the internal URL.
if (CoreApp.instance.isDesktop()) {
return fileEntry.toInternalURL();
} else {
return fileEntry.toURL();
}
if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
throw null;
const path = await this.getFilePath(siteId, fileId);
const fileEntry = await CoreFile.instance.getFile(path);
// This URL is usually used to launch files or put them in HTML. In desktop we need the internal URL.
if (CoreApp.instance.isDesktop()) {
return fileEntry.toInternalURL();
} else {
return fileEntry.toURL();
}
}
/**
@ -1654,13 +1656,13 @@ export class CoreFilepoolProvider {
* @return Resolved with the URL.
*/
protected async getInternalUrlByPath(filePath: string): Promise<string> {
if (CoreFile.instance.isAvailable()) {
const fileEntry = await CoreFile.instance.getFile(filePath);
return fileEntry.toURL();
if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
throw null;
const fileEntry = await CoreFile.instance.getFile(filePath);
return fileEntry.toURL();
}
/**
@ -1671,14 +1673,14 @@ export class CoreFilepoolProvider {
* @return Resolved with the URL. Rejected otherwise.
*/
async getInternalUrlByUrl(siteId: string, fileUrl: string): Promise<string> {
if (CoreFile.instance.isAvailable()) {
const file = await this.fixPluginfileURL(siteId, fileUrl);
const fileId = this.getFileIdByUrl(file.fileurl);
return this.getInternalUrlById(siteId, fileId);
if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
throw null;
const file = await this.fixPluginfileURL(siteId, fileUrl);
const fileId = this.getFileIdByUrl(file.fileurl);
return this.getInternalUrlById(siteId, fileId);
}
/**
@ -1748,16 +1750,16 @@ export class CoreFilepoolProvider {
* @return Resolved with the URL.
*/
async getPackageDirUrlByUrl(siteId: string, url: string): Promise<string> {
if (CoreFile.instance.isAvailable()) {
const file = await this.fixPluginfileURL(siteId, url);
const dirName = this.getPackageDirNameByUrl(file.fileurl);
const dirPath = await this.getFilePath(siteId, dirName, '');
const dirEntry = await CoreFile.instance.getDir(dirPath);
return dirEntry.toURL();
if (!CoreFile.instance.isAvailable()) {
throw new CoreError('File system cannot be used.');
}
throw null;
const file = await this.fixPluginfileURL(siteId, url);
const dirName = this.getPackageDirNameByUrl(file.fileurl);
const dirPath = await this.getFilePath(siteId, dirName, '');
const dirEntry = await CoreFile.instance.getDir(dirPath);
return dirEntry.toURL();
}
/**
@ -1973,7 +1975,7 @@ export class CoreFilepoolProvider {
* @param componentId An ID to use in conjunction with the component.
* @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 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.
* @param options Extra options (isexternalfile, repositorytype).
* @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.
*/
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> {
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 timemodified The time this file was modified.
* @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.
* @param options Extra options (isexternalfile, repositorytype).
* @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.
*/
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> {
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> {
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') {
throw null;
throw new CoreError('File not found in filepool.');
}
return entry;
@ -2118,12 +2121,13 @@ export class CoreFilepoolProvider {
protected async hasFileInQueue(siteId: string, fileId: string): Promise<CoreFilepoolQueueEntry> {
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') {
throw null;
throw new CoreError('File not found in queue.');
}
// Convert the links to an object.
entry.linksUnserialized = CoreTextUtils.instance.parseJSON(entry.links, []);
entry.linksUnserialized = <CoreFilepoolComponentLink[]> CoreTextUtils.instance.parseJSON(entry.links, []);
return entry;
}
@ -2132,14 +2136,14 @@ export class CoreFilepoolProvider {
* Invalidate all the files in a site.
*
* @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.
* @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 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);
}
@ -2171,11 +2175,11 @@ export class CoreFilepoolProvider {
* @param siteId The site ID.
* @param component The component to invalidate.
* @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.
* It is advised to set it to true to reduce the performance and data usage of the app.
* @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.
* @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> {
const db = await CoreSites.instance.getSiteDb(siteId);
@ -2191,8 +2195,8 @@ export class CoreFilepoolProvider {
whereAndParams[0] = 'fileId ' + whereAndParams[0];
if (onlyAny) {
whereAndParams[0] += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_ANY_WHERE_CLAUSE + ')';
if (onlyUnknown) {
whereAndParams[0] += ' AND (' + CoreFilepoolProvider.FILE_UPDATE_UNKNOWN_WHERE_CLAUSE + ')';
}
await db.updateRecordsWhere(CoreFilepoolProvider.FILES_TABLE, { stale: 1 }, whereAndParams[0], whereAndParams[1]);
@ -2258,7 +2262,7 @@ export class CoreFilepoolProvider {
* @param entry Filepool entry.
* @return Whether it cannot determine updates.
*/
protected isFileUpdateAny(entry: CoreFilepoolFileEntry): boolean {
protected isFileUpdateUnknown(entry: CoreFilepoolFileEntry): boolean {
return !!entry.isexternalfile || (!entry.revision && !entry.timemodified);
}
@ -2433,7 +2437,7 @@ export class CoreFilepoolProvider {
let items: CoreFilepoolQueueEntry[];
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);
} catch (err) {
throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY;
@ -2444,7 +2448,7 @@ export class CoreFilepoolProvider {
throw CoreFilepoolProvider.ERR_QUEUE_IS_EMPTY;
}
// 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);
}
@ -2760,7 +2764,7 @@ export class CoreFilepoolProvider {
const mimetype = await CoreUtils.instance.getMimeTypeFromUrl(url);
// If the file is streaming (audio or video) we reject.
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.
*/
@ -3025,7 +3029,12 @@ export type CoreFilepoolQueueEntry = CoreFilepoolFileOptions & {
* File links (to link the file to components and componentIds). Serialized to store on DB.
*/
links?: string;
};
/**
* Entry from the file's queue.
*/
export type CoreFilepoolQueueEntry = CoreFilepoolQueueDBEntry & {
/**
* 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 { CoreApp } from '@services/app';
import { CoreError } from '@classes/error';
import { CoreError } from '@classes/errors/error';
import { Geolocation, Diagnostic, makeSingleton } from '@singletons/core.singletons';
@Injectable()
@ -116,6 +116,7 @@ export class CoreGeolocationProvider {
*
* @param error Error.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
protected isCordovaPermissionDeniedError(error?: any): boolean {
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 { CoreSite, CoreSiteWSPreSets } from '@classes/site';
import { CoreError } from '@classes/errors/error';
import { makeSingleton, Translate } from '@singletons/core.singletons';
import { CoreWSExternalWarning } from '@services/ws';
import { CoreCourseBase } from '@/types/global';
@ -79,9 +80,11 @@ export class CoreGroupsProvider {
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) {
throw null;
throw new CoreError('Activity allowed groups not found.');
}
return response;
@ -195,9 +198,11 @@ export class CoreGroupsProvider {
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') {
throw null;
throw new CoreError('Activity group mode not found.');
}
return response.groupmode;
@ -267,9 +272,11 @@ export class CoreGroupsProvider {
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) {
throw null;
throw new CoreError('User groups in course not found.');
}
return response.groups;
@ -461,3 +468,26 @@ export type CoreGroupGetActivityAllowedGroupsResponse = {
canaccessallgroups?: boolean; // Whether the user will be able to access all the activity groups.
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 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 customStrings = {}; // Strings defined using the admin tool.
protected customStrings: CoreLanguageObject = {}; // Strings defined using the admin tool.
protected customStringsRaw: string;
protected sitePluginsStrings = {}; // Strings defined by site plugins.
protected sitePluginsStrings: CoreLanguageObject = {}; // Strings defined by site plugins.
constructor() {
// 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.
* @return Promise resolved when the change is finished.
*/
changeCurrentLanguage(language: string): Promise<unknown> {
async changeCurrentLanguage(language: string): Promise<void> {
const promises = [];
// 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) => {
// It's a language override, load the original one first.
const fallbackLang = Translate.instance.instant('core.parentlanguage');
@ -165,13 +165,15 @@ export class CoreLangProvider {
this.currentLanguage = language;
return Promise.all(promises).finally(() => {
try {
await Promise.all(promises);
} finally {
// Load the custom and site plugins strings for the language.
if (this.loadLangStrings(this.customStrings, language) || this.loadLangStrings(this.sitePluginsStrings, language)) {
// Some lang strings have changed, emit an event to update the pipes.
Translate.instance.onLangChange.emit({ lang: language, translations: Translate.instance.translations[language] });
}
});
}
}
/**
@ -196,7 +198,7 @@ export class CoreLangProvider {
*
* @return Custom strings.
*/
getAllCustomStrings(): unknown {
getAllCustomStrings(): CoreLanguageObject {
return this.customStrings;
}
@ -205,7 +207,7 @@ export class CoreLangProvider {
*
* @return Site plugins strings.
*/
getAllSitePluginsStrings(): unknown {
getAllSitePluginsStrings(): CoreLanguageObject {
return this.sitePluginsStrings;
}
@ -220,7 +222,7 @@ export class CoreLangProvider {
}
// 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.
if (CoreConfigConstants.default_lang && CoreConfigConstants.forcedefaultlanguage) {
return CoreConfigConstants.default_lang;
@ -283,7 +285,7 @@ export class CoreLangProvider {
* @param lang The language to check.
* @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.
return new Promise((resolve, reject): void => {
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 { CoreEventObserver, CoreEvents, CoreEventsProvider } from '@services/events';
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 { CoreSite } from '@classes/site';
import { CoreQueueRunner } from '@classes/queue-runner';
import { CoreError } from '@classes/errors/error';
import { CoreConstants } from '@core/constants';
import CoreConfigConstants from '@app/config.json';
import { makeSingleton, NgZone, Platform, Translate, LocalNotifications, Push, Device } from '@singletons/core.singletons';
@ -94,14 +96,9 @@ export class CoreLocalNotificationsProvider {
protected appDB: SQLiteDB;
protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized.
protected codes: { [s: string]: number } = {};
protected codeRequestsQueue = {};
protected observables = {};
protected currentNotification = {
title: '',
texts: [],
ids: [],
timeouts: [],
};
protected codeRequestsQueue: {[key: string]: CodeRequestsQueueItem} = {};
// eslint-disable-next-line @typescript-eslint/no-explicit-any
protected observables: {[eventName: string]: {[component: string]: Subject<any>}} = {};
protected triggerSubscription: 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) {
this.cancelSiteNotifications(site.id);
}
@ -270,13 +267,15 @@ export class CoreLocalNotificationsProvider {
try {
// 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;
return entry.code;
} catch (err) {
// 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;
if (entries.length > 0) {
newCode = entries[0].code + 1;
@ -326,7 +325,7 @@ export class CoreLocalNotificationsProvider {
*/
protected getUniqueNotificationId(notificationId: number, component: string, siteId: string): Promise<number> {
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) =>
@ -372,7 +371,9 @@ export class CoreLocalNotificationsProvider {
await this.dbReady;
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;
if (typeof triggered != 'number') {
@ -398,6 +399,7 @@ export class CoreLocalNotificationsProvider {
*
* @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 {
this.notifyEvent('click', data);
}
@ -408,6 +410,7 @@ export class CoreLocalNotificationsProvider {
* @param eventName Name of the event to notify.
* @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 {
// Execute the code in the Angular zone, so change detection doesn't stop working.
NgZone.instance.run(() => {
@ -426,6 +429,7 @@ export class CoreLocalNotificationsProvider {
* @param data Notification data.
* @return Parsed data.
*/
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any
protected parseNotificationData(data: any): any {
if (!data) {
return {};
@ -454,11 +458,11 @@ export class CoreLocalNotificationsProvider {
if (typeof request == 'object' && typeof request.table != 'undefined' && typeof request.id != 'undefined') {
// Get the code and resolve/reject all the promises of this request.
promise = this.getCode(request.table, request.id).then((code) => {
request.promises.forEach((p) => {
request.deferreds.forEach((p) => {
p.resolve(code);
});
}).catch((error) => {
request.promises.forEach((p) => {
request.deferreds.forEach((p) => {
p.reject(error);
});
});
@ -508,7 +512,7 @@ export class CoreLocalNotificationsProvider {
return {
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') {
// 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 {
// Add a pending request to the queue.
this.codeRequestsQueue[key] = {
table: table,
id: id,
promises: [deferred],
deferreds: [deferred],
};
}
@ -682,7 +686,7 @@ export class CoreLocalNotificationsProvider {
const entry = {
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);
@ -709,3 +713,9 @@ export class CoreLocalNotificationsProvider {
export class CoreLocalNotifications extends makeSingleton(CoreLocalNotificationsProvider) {}
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 { makeSingleton } from '@singletons/core.singletons';
const SYNC_TABLE = 'sync';
/*
* Service that provides some features regarding synchronization.
*/
@ -24,36 +26,35 @@ import { makeSingleton } from '@singletons/core.singletons';
export class CoreSyncProvider {
// Variables for the database.
protected SYNC_TABLE = 'sync';
protected siteSchema: CoreSiteSchema = {
name: 'CoreSyncProvider',
version: 1,
tables: [
{
name: this.SYNC_TABLE,
name: SYNC_TABLE,
columns: [
{
name: 'component',
type: 'TEXT',
notNull: true
notNull: true,
},
{
name: 'id',
type: 'TEXT',
notNull: true
notNull: true,
},
{
name: 'time',
type: 'INTEGER'
type: 'INTEGER',
},
{
name: 'warnings',
type: 'TEXT'
}
type: 'TEXT',
},
],
primaryKeys: ['component', 'id']
}
]
primaryKeys: ['component', 'id'],
},
],
};
// Store blocked sync objects.
@ -63,7 +64,7 @@ export class CoreSyncProvider {
CoreSites.instance.registerSiteSchema(this.siteSchema);
// Unblock all blocks on logout.
CoreEvents.instance.on(CoreEventsProvider.LOGOUT, (data) => {
CoreEvents.instance.on(CoreEventsProvider.LOGOUT, (data: {siteId: string}) => {
this.clearAllBlocks(data.siteId);
});
}
@ -125,32 +126,32 @@ export class CoreSyncProvider {
/**
* Returns a sync record.
*
* @param component Component name.
* @param id Unique ID per component.
* @param siteId Site ID. If not defined, current site.
* @return Record if found or reject.
*/
getSyncRecord(component: string, id: string | number, siteId?: string): Promise<any> {
return CoreSites.instance.getSiteDb(siteId).then((db) => {
return db.getRecord(this.SYNC_TABLE, { component: component, id: id });
});
getSyncRecord(component: string, id: string | number, siteId?: string): Promise<CoreSyncRecord> {
return CoreSites.instance.getSiteDb(siteId).then((db) => db.getRecord(SYNC_TABLE, { component: component, id: id }));
}
/**
* Inserts or Updates info of a sync record.
*
* @param component Component name.
* @param id Unique ID per component.
* @param data Data that updates the record.
* @param siteId Site ID. If not defined, current site.
* @return Promise resolved with done.
*/
insertOrUpdateSyncRecord(component: string, id: string | number, data: any, siteId?: string): Promise<any> {
return CoreSites.instance.getSiteDb(siteId).then((db) => {
data.component = component;
data.id = id;
async insertOrUpdateSyncRecord(component: string, id: string, data: CoreSyncRecord, siteId?: string): Promise<void> {
const db = await CoreSites.instance.getSiteDb(siteId);
return db.insertRecord(this.SYNC_TABLE, data);
});
data.component = component;
data.id = id;
await db.insertRecord(SYNC_TABLE, data);
}
/**
@ -206,6 +207,14 @@ export class CoreSyncProvider {
delete this.blockedItems[siteId][uniqueId][operation];
}
}
}
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 { CoreLogger } from '@singletons/logger';
const VERSION_APPLIED = 'version_applied';
/**
* Factory to handle app updates. This factory shouldn't be used outside of core.
*
@ -27,12 +29,12 @@ import { CoreLogger } from '@singletons/logger';
*/
@Injectable()
export class CoreUpdateManagerProvider implements CoreInitHandler {
// Data for init delegate.
name = 'CoreUpdateManager';
priority = CoreInitDelegate.MAX_RECOMMENDED_PRIORITY + 300;
blocking = true;
protected VERSION_APPLIED = 'version_applied';
protected logger: CoreLogger;
constructor() {
@ -45,11 +47,11 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
*
* @return Promise resolved when the update process finishes.
*/
async load(): Promise<any> {
async load(): Promise<void> {
const promises = [];
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) {
// @todo: H5P update.
@ -58,11 +60,12 @@ export class CoreUpdateManagerProvider implements CoreInitHandler {
try {
await Promise.all(promises);
await CoreConfig.instance.set(this.VERSION_APPLIED, versionCode);
await CoreConfig.instance.set(VERSION_APPLIED, versionCode);
} catch (error) {
this.logger.error(`Error applying update from ${versionApplied} to ${versionCode}`, error);
}
}
}
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;
}
if (!CoreUrlUtils.instance.isLocalFileUrlScheme(urlParts.protocol, urlParts.domain)) {
if (!CoreUrlUtils.instance.isLocalFileUrlScheme(urlParts.protocol)) {
// Scheme suggests it's an external resource.
event && event.preventDefault();

View File

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

View File

@ -424,18 +424,16 @@ export class CoreUrlUtilsProvider {
isLocalFileUrl(url: string): boolean {
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.
*
* @param scheme Scheme to check.
* @param notUsed Unused parameter.
* @return Whether the scheme belongs to a local file.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
isLocalFileUrlScheme(scheme: string, notUsed?: string): boolean {
isLocalFileUrlScheme(scheme: string): boolean {
if (scheme) {
scheme = scheme.toLowerCase();
}

View File

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

View File

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

View File

@ -15,7 +15,13 @@
import { Injector, NgZone as NgZoneService } from '@angular/core';
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 { 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 { Geolocation as GeolocationService } from '@ionic-native/geolocation/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 { WebView as WebViewService } from '@ionic-native/ionic-webview/ngx';
import { Keyboard as KeyboardService } from '@ionic-native/keyboard/ngx';
import { LocalNotifications as LocalNotificationsService } from '@ionic-native/local-notifications/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 Keyboard extends makeSingleton(KeyboardService) {}
export class LocalNotifications extends makeSingleton(LocalNotificationsService) {}
export class NativeHttp extends makeSingleton(HTTP) {}
export class Network extends makeSingleton(NetworkService) {}
export class Push extends makeSingleton(PushService) {}
export class QRScanner extends makeSingleton(QRScannerService) {}
@ -81,12 +90,17 @@ export class StatusBar extends makeSingleton(StatusBarService) {}
export class SplashScreen extends makeSingleton(SplashScreenService) {}
export class SQLite extends makeSingleton(SQLiteService) {}
export class WebIntent extends makeSingleton(WebIntentService) {}
export class WebView extends makeSingleton(WebViewService) {}
export class Zip extends makeSingleton(ZipService) {}
// Convert some Angular and Ionic injectables to singletons.
export class NgZone extends makeSingleton(NgZoneService) {}
export class Http extends makeSingleton(HttpClient) {}
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.
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/
// original by: Brett Zamir (https://brett-zamir.me)
// 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.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
navCtrl?: any; // @todo NavController;
};
@ -36,7 +37,7 @@ export class CoreWindow {
private constructor() {
// Nothing to do.
}
/**
* "Safe" implementation of window.open. It will open the URL without overriding the app.
*
@ -60,11 +61,13 @@ export class CoreWindow {
await CoreUtils.instance.openFile(url);
} else {
let treated: boolean;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
options = options || {};
if (name != '_system') {
// 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) {

View File

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

View File

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