MOBILE-3565 core: Fix errors caused by strict checks

main
Dani Palou 2020-10-21 16:32:27 +02:00
parent 317444049c
commit 7a517ba271
37 changed files with 2100 additions and 1615 deletions

View File

@ -40,18 +40,18 @@ export class CoreDelegate {
/** /**
* Default handler * Default handler
*/ */
protected defaultHandler: CoreDelegateHandler; protected defaultHandler?: CoreDelegateHandler;
/** /**
* Time when last updateHandler functions started. * Time when last updateHandler functions started.
*/ */
protected lastUpdateHandlersStart: number; protected lastUpdateHandlersStart = 0;
/** /**
* Feature prefix to check is feature is enabled or disabled in site. * Feature prefix to check is feature is enabled or disabled in site.
* This check is only made if not false. Override on the subclass or override isFeatureDisabled function. * This check is only made if not false. Override on the subclass or override isFeatureDisabled function.
*/ */
protected featurePrefix: string; protected featurePrefix?: string;
/** /**
* Name of the property to be used to index the handlers. By default, the handler's name will be used. * Name of the property to be used to index the handlers. By default, the handler's name will be used.
@ -78,7 +78,7 @@ export class CoreDelegate {
/** /**
* Function to resolve the handlers init promise. * Function to resolve the handlers init promise.
*/ */
protected handlersInitResolve: () => void; protected handlersInitResolve!: () => void;
/** /**
* Constructor of the Delegate. * Constructor of the Delegate.
@ -110,7 +110,7 @@ export class CoreDelegate {
* @param params Parameters to pass to the function. * @param params Parameters to pass to the function.
* @return Function returned value or default value. * @return Function returned value or default value.
*/ */
protected executeFunctionOnEnabled<T = unknown>(handlerName: string, fnName: string, params?: unknown[]): T { protected executeFunctionOnEnabled<T = unknown>(handlerName: string, fnName: string, params?: unknown[]): T | undefined {
return this.execute<T>(this.enabledHandlers[handlerName], fnName, params); return this.execute<T>(this.enabledHandlers[handlerName], fnName, params);
} }
@ -123,7 +123,7 @@ export class CoreDelegate {
* @param params Parameters to pass to the function. * @param params Parameters to pass to the function.
* @return Function returned value or default value. * @return Function returned value or default value.
*/ */
protected executeFunction<T = unknown>(handlerName: string, fnName: string, params?: unknown[]): T { protected executeFunction<T = unknown>(handlerName: string, fnName: string, params?: unknown[]): T | undefined {
return this.execute(this.handlers[handlerName], fnName, params); return this.execute(this.handlers[handlerName], fnName, params);
} }
@ -136,7 +136,7 @@ export class CoreDelegate {
* @param params Parameters to pass to the function. * @param params Parameters to pass to the function.
* @return Function returned value or default value. * @return Function returned value or default value.
*/ */
private execute<T = unknown>(handler: CoreDelegateHandler, fnName: string, params?: unknown[]): T { private execute<T = unknown>(handler: CoreDelegateHandler, fnName: string, params?: unknown[]): T | undefined {
if (handler && handler[fnName]) { if (handler && handler[fnName]) {
return handler[fnName].apply(handler, params); return handler[fnName].apply(handler, params);
} else if (this.defaultHandler && this.defaultHandler[fnName]) { } else if (this.defaultHandler && this.defaultHandler[fnName]) {
@ -252,7 +252,7 @@ export class CoreDelegate {
this.updatePromises[siteId] = {}; this.updatePromises[siteId] = {};
} }
if (!CoreSites.instance.isLoggedIn() || this.isFeatureDisabled(handler, currentSite)) { if (!CoreSites.instance.isLoggedIn() || this.isFeatureDisabled(handler, currentSite!)) {
promise = Promise.resolve(false); promise = Promise.resolve(false);
} else { } else {
promise = Promise.resolve(handler.isEnabled()).catch(() => false); promise = Promise.resolve(handler.isEnabled()).catch(() => false);
@ -270,6 +270,8 @@ export class CoreDelegate {
delete this.enabledHandlers[key]; delete this.enabledHandlers[key];
} }
} }
return;
}).finally(() => { }).finally(() => {
// Update finished, delete the promise. // Update finished, delete the promise.
delete this.updatePromises[siteId][handler.name]; delete this.updatePromises[siteId][handler.name];
@ -295,7 +297,7 @@ export class CoreDelegate {
* @return Resolved when done. * @return Resolved when done.
*/ */
protected async updateHandlers(): Promise<void> { protected async updateHandlers(): Promise<void> {
const promises = []; const promises: Promise<void>[] = [];
const now = Date.now(); const now = Date.now();
this.logger.debug('Updating handlers for current site.'); this.logger.debug('Updating handlers for current site.');

View File

@ -28,7 +28,7 @@ export class CoreAjaxWSError extends CoreError {
backtrace?: string; // Backtrace. 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. 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 // eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(error: any, available?: number) { constructor(error: any, available?: number) {
super(error.message); super(error.message);

View File

@ -27,6 +27,7 @@ export class CoreWSError extends CoreError {
debuginfo?: string; // Debug info. Only if debug mode is enabled. debuginfo?: string; // Debug info. Only if debug mode is enabled.
backtrace?: string; // Backtrace. Only if debug mode is enabled. backtrace?: string; // Backtrace. Only if debug mode is enabled.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
constructor(error: any) { constructor(error: any) {
super(error.message); super(error.message);

View File

@ -30,7 +30,7 @@ export class CoreInterceptor implements HttpInterceptor {
* @param addNull Add null values to the serialized as empty parameters. * @param addNull Add null values to the serialized as empty parameters.
* @return Serialization of the object. * @return Serialization of the object.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
static serialize(obj: any, addNull?: boolean): string { static serialize(obj: any, addNull?: boolean): string {
let query = ''; let query = '';
@ -61,7 +61,7 @@ export class CoreInterceptor implements HttpInterceptor {
return query.length ? query.substr(0, query.length - 1) : query; return query.length ? query.substr(0, query.length - 1) : query;
} }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> { intercept(req: HttpRequest<any>, next: HttpHandler): Observable<any> {
// Add the header and serialize the body if needed. // Add the header and serialize the body if needed.
const newReq = req.clone({ const newReq = req.clone({

View File

@ -25,7 +25,7 @@ export class CoreIonLoadingElement {
constructor(public loading: HTMLIonLoadingElement) { } constructor(public loading: HTMLIonLoadingElement) { }
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
async dismiss(data?: any, role?: string): Promise<boolean> { async dismiss(data?: any, role?: string): Promise<boolean> {
if (!this.isPresented || this.isDismissed) { if (!this.isPresented || this.isDismissed) {
this.isDismissed = true; this.isDismissed = true;

View File

@ -91,7 +91,7 @@ export class CoreQueueRunner {
return; return;
} }
const item = this.orderedQueue.shift(); const item = this.orderedQueue.shift()!;
this.numberRunning++; this.numberRunning++;
try { try {

File diff suppressed because it is too large Load Diff

View File

@ -472,7 +472,7 @@ export class SQLiteDB {
* @return List of params. * @return List of params.
*/ */
protected formatDataToSQLParams(data: SQLiteDBRecordValues): SQLiteDBRecordValue[] { protected formatDataToSQLParams(data: SQLiteDBRecordValues): SQLiteDBRecordValue[] {
return Object.keys(data).map((key) => data[key]); return Object.keys(data).map((key) => data[key]!);
} }
/** /**
@ -1087,7 +1087,7 @@ export class SQLiteDB {
} }
export type SQLiteDBRecordValues = { export type SQLiteDBRecordValues = {
[key in string ]: SQLiteDBRecordValue; [key in string ]: SQLiteDBRecordValue | undefined;
}; };
export type SQLiteDBQueryParams = { export type SQLiteDBQueryParams = {

View File

@ -38,7 +38,7 @@ export class CoreIconComponent implements OnChanges, OnDestroy {
@Input() ios?: string; @Input() ios?: string;
// FontAwesome params. // FontAwesome params.
@Input('fixed-width') fixedWidth: boolean; @Input('fixed-width') fixedWidth?: boolean; // eslint-disable-line @angular-eslint/no-input-rename
@Input() label?: string; @Input() label?: string;
@Input() flipRtl?: boolean; // Whether to flip the icon in RTL. Defaults to false. @Input() flipRtl?: boolean; // Whether to flip the icon in RTL. Defaults to false.
@ -48,7 +48,7 @@ export class CoreIconComponent implements OnChanges, OnDestroy {
constructor(el: ElementRef) { constructor(el: ElementRef) {
this.element = el.nativeElement; this.element = el.nativeElement;
this.newElement = this.element this.newElement = this.element;
} }
/** /**

View File

@ -48,9 +48,9 @@ export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit {
@Input() hideUntil: unknown; // Determine when should the contents be shown. @Input() hideUntil: unknown; // Determine when should the contents be shown.
@Input() message?: string; // Message to show while loading. @Input() message?: string; // Message to show while loading.
@ViewChild('content') content: ElementRef; @ViewChild('content') content?: ElementRef;
protected uniqueId: string; protected uniqueId!: string;
protected element: HTMLElement; // Current element. protected element: HTMLElement; // Current element.
constructor(element: ElementRef) { constructor(element: ElementRef) {

View File

@ -40,16 +40,16 @@ import { CoreUtils } from '@services/utils/utils';
}) })
export class CoreShowPasswordComponent implements OnInit, AfterViewInit { export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
@Input() name: string; // Name of the input affected. @Input() name?: string; // Name of the input affected.
@Input() initialShown?: boolean | string; // Whether the password should be shown at start. @Input() initialShown?: boolean | string; // Whether the password should be shown at start.
@ContentChild(IonInput) ionInput: IonInput; @ContentChild(IonInput) ionInput?: IonInput;
shown: boolean; // Whether the password is shown. shown!: boolean; // Whether the password is shown.
label: string; // Label for the button to show/hide. label?: string; // Label for the button to show/hide.
iconName: string; // Name of the icon of the button to show/hide. iconName?: string; // Name of the icon of the button to show/hide.
selector = ''; // Selector to identify the input. selector = ''; // Selector to identify the input.
protected input: HTMLInputElement; // Input affected. protected input?: HTMLInputElement | null; // Input affected.
protected element: HTMLElement; // Current element. protected element: HTMLElement; // Current element.
constructor(element: ElementRef) { constructor(element: ElementRef) {
@ -84,7 +84,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
} }
// Search the input. // Search the input.
this.input = this.element.querySelector(this.selector); this.input = <HTMLInputElement> this.element.querySelector(this.selector);
if (this.input) { if (this.input) {
// Input found. Set the right type. // Input found. Set the right type.
@ -128,7 +128,7 @@ export class CoreShowPasswordComponent implements OnInit, AfterViewInit {
if (isFocused && CoreApp.instance.isAndroid()) { if (isFocused && CoreApp.instance.isAndroid()) {
// In Android, the keyboard is closed when the input type changes. Focus it again. // In Android, the keyboard is closed when the input type changes. Focus it again.
setTimeout(() => { setTimeout(() => {
CoreDomUtils.instance.focusElement(this.input); CoreDomUtils.instance.focusElement(this.input!);
}, 400); }, 400);
} }
} }

View File

@ -39,7 +39,7 @@ export class CoreConstants {
static readonly DOWNLOAD_THRESHOLD = 10485760; // 10MB. static readonly DOWNLOAD_THRESHOLD = 10485760; // 10MB.
static readonly MINIMUM_FREE_SPACE = 10485760; // 10MB. static readonly MINIMUM_FREE_SPACE = 10485760; // 10MB.
static readonly IOS_FREE_SPACE_THRESHOLD = 524288000; // 500MB. static readonly IOS_FREE_SPACE_THRESHOLD = 524288000; // 500MB.
static readonly DONT_SHOW_ERROR = 'CoreDontShowError'; static readonly DONT_SHOW_ERROR = 'CoreDontShowError'; // @deprecated since 3.9.5. Use CoreSilentError instead.
static readonly NO_SITE_ID = 'NoSite'; static readonly NO_SITE_ID = 'NoSite';
// Settings constants. // Settings constants.

View File

@ -35,6 +35,7 @@ export class SQLiteDBMock extends SQLiteDB {
* *
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
close(): Promise<any> { close(): Promise<any> {
// WebSQL databases aren't closed. // WebSQL databases aren't closed.
return Promise.resolve(); return Promise.resolve();
@ -45,6 +46,7 @@ export class SQLiteDBMock extends SQLiteDB {
* *
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async emptyDatabase(): Promise<any> { async emptyDatabase(): Promise<any> {
await this.ready(); await this.ready();
@ -89,6 +91,7 @@ export class SQLiteDBMock extends SQLiteDB {
* @param params Query parameters. * @param params Query parameters.
* @return Promise resolved with the result. * @return Promise resolved with the result.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async execute(sql: string, params?: any[]): Promise<any> { async execute(sql: string, params?: any[]): Promise<any> {
await this.ready(); await this.ready();
@ -115,6 +118,7 @@ export class SQLiteDBMock extends SQLiteDB {
* @param sqlStatements SQL statements to execute. * @param sqlStatements SQL statements to execute.
* @return Promise resolved with the result. * @return Promise resolved with the result.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
async executeBatch(sqlStatements: any[]): Promise<any> { async executeBatch(sqlStatements: any[]): Promise<any> {
await this.ready(); await this.ready();
@ -148,6 +152,7 @@ export class SQLiteDBMock extends SQLiteDB {
})); }));
}); });
// eslint-disable-next-line promise/catch-or-return
Promise.all(promises).then(resolve, reject); Promise.all(promises).then(resolve, reject);
}); });
}); });
@ -158,6 +163,7 @@ export class SQLiteDBMock extends SQLiteDB {
*/ */
init(): void { init(): void {
// This DB is for desktop apps, so use a big size to be sure it isn't filled. // This DB is for desktop apps, so use a big size to be sure it isn't filled.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
this.db = (<any> window).openDatabase(this.name, '1.0', this.name, 500 * 1024 * 1024); this.db = (<any> window).openDatabase(this.name, '1.0', this.name, 500 * 1024 * 1024);
this.promise = Promise.resolve(); this.promise = Promise.resolve();
} }

View File

@ -36,27 +36,27 @@ import { CoreEvents, CoreEventsProvider } from '@/app/services/events';
}) })
export class CoreLoginCredentialsPage implements OnInit, OnDestroy { export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
@ViewChild('credentialsForm') formElement: ElementRef; @ViewChild('credentialsForm') formElement?: ElementRef;
credForm: FormGroup; credForm!: FormGroup;
siteUrl: string; siteUrl!: string;
siteChecked = false; siteChecked = false;
siteName: string; siteName?: string;
logoUrl: string; logoUrl?: string;
authInstructions: string; authInstructions?: string;
canSignup: boolean; canSignup?: boolean;
identityProviders: CoreSiteIdentityProvider[]; identityProviders?: CoreSiteIdentityProvider[];
pageLoaded = false; pageLoaded = false;
isBrowserSSO = false; isBrowserSSO = false;
isFixedUrlSet = false; isFixedUrlSet = false;
showForgottenPassword = true; showForgottenPassword = true;
showScanQR: boolean; showScanQR: boolean;
protected siteConfig: CoreSitePublicConfigResponse; protected siteConfig?: CoreSitePublicConfigResponse;
protected eventThrown = false; protected eventThrown = false;
protected viewLeft = false; protected viewLeft = false;
protected siteId: string; protected siteId?: string;
protected urlToOpen: string; protected urlToOpen?: string;
constructor( constructor(
protected fb: FormBuilder, protected fb: FormBuilder,
@ -82,8 +82,8 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.route.queryParams.subscribe(params => { this.route.queryParams.subscribe(params => {
this.siteUrl = params['siteUrl']; this.siteUrl = params['siteUrl'];
this.siteName = params['siteName'] || null; this.siteName = params['siteName'] || undefined;
this.logoUrl = !CoreConfigConstants.forceLoginLogo && params['logoUrl'] || null; this.logoUrl = !CoreConfigConstants.forceLoginLogo && params['logoUrl'] || undefined;
this.siteConfig = params['siteConfig']; this.siteConfig = params['siteConfig'];
this.urlToOpen = params['urlToOpen']; this.urlToOpen = params['urlToOpen'];
@ -138,7 +138,11 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
// Check that there's no SSO authentication ongoing and the view hasn't changed. // Check that there's no SSO authentication ongoing and the view hasn't changed.
if (!CoreApp.instance.isSSOAuthenticationOngoing() && !this.viewLeft) { if (!CoreApp.instance.isSSOAuthenticationOngoing() && !this.viewLeft) {
CoreLoginHelper.instance.confirmAndOpenBrowserForSSOLogin( CoreLoginHelper.instance.confirmAndOpenBrowserForSSOLogin(
result.siteUrl, result.code, result.service, result.config?.launchurl); result.siteUrl,
result.code,
result.service,
result.config?.launchurl,
);
} }
} else { } else {
this.isBrowserSSO = false; this.isBrowserSSO = false;
@ -171,7 +175,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
CoreEvents.instance.trigger(CoreEventsProvider.LOGIN_SITE_CHECKED, { config: this.siteConfig }); CoreEvents.instance.trigger(CoreEventsProvider.LOGIN_SITE_CHECKED, { config: this.siteConfig });
} }
} else { } else {
this.authInstructions = null; this.authInstructions = undefined;
this.canSignup = false; this.canSignup = false;
this.identityProviders = []; this.identityProviders = [];
} }
@ -261,7 +265,11 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
*/ */
forgottenPassword(): void { forgottenPassword(): void {
CoreLoginHelper.instance.forgottenPasswordClicked( CoreLoginHelper.instance.forgottenPasswordClicked(
this.navCtrl, this.siteUrl, this.credForm.value.username, this.siteConfig); this.navCtrl,
this.siteUrl,
this.credForm.value.username,
this.siteConfig,
);
} }
/** /**
@ -270,7 +278,7 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
* @param provider The provider that was clicked. * @param provider The provider that was clicked.
*/ */
oauthClicked(provider: CoreSiteIdentityProvider): void { oauthClicked(provider: CoreSiteIdentityProvider): void {
if (!CoreLoginHelper.instance.openBrowserForOAuthLogin(this.siteUrl, provider, this.siteConfig.launchurl)) { if (!CoreLoginHelper.instance.openBrowserForOAuthLogin(this.siteUrl, provider, this.siteConfig?.launchurl)) {
CoreDomUtils.instance.showErrorModal('Invalid data.'); CoreDomUtils.instance.showErrorModal('Invalid data.');
} }
} }
@ -289,8 +297,10 @@ export class CoreLoginCredentialsPage implements OnInit, OnDestroy {
// Show some instructions first. // Show some instructions first.
CoreDomUtils.instance.showAlertWithOptions({ CoreDomUtils.instance.showAlertWithOptions({
header: Translate.instance.instant('core.login.faqwhereisqrcode'), header: Translate.instance.instant('core.login.faqwhereisqrcode'),
message: Translate.instance.instant('core.login.faqwhereisqrcodeanswer', message: Translate.instance.instant(
{ $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML }), 'core.login.faqwhereisqrcodeanswer',
{ $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML },
),
buttons: [ buttons: [
{ {
text: Translate.instance.instant('core.cancel'), text: Translate.instance.instant('core.cancel'),

View File

@ -34,9 +34,10 @@ export class CoreLoginInitPage implements OnInit {
/** /**
* Initialize the component. * Initialize the component.
*/ */
ngOnInit(): void { async ngOnInit(): Promise<void> {
// Wait for the app to be ready. // Wait for the app to be ready.
CoreInit.instance.ready().then(() => { await CoreInit.instance.ready();
// Check if there was a pending redirect. // Check if there was a pending redirect.
const redirectData = CoreApp.instance.getRedirect(); const redirectData = CoreApp.instance.getRedirect();
if (redirectData.siteId) { if (redirectData.siteId) {
@ -44,7 +45,7 @@ export class CoreLoginInitPage implements OnInit {
CoreApp.instance.storeRedirect('', '', {}); CoreApp.instance.storeRedirect('', '', {});
// Only accept the redirect if it was stored less than 20 seconds ago. // Only accept the redirect if it was stored less than 20 seconds ago.
if (Date.now() - redirectData.timemodified < 20000) { if (redirectData.timemodified && Date.now() - redirectData.timemodified < 20000) {
// if (redirectData.siteId != CoreConstants.NO_SITE_ID) { // if (redirectData.siteId != CoreConstants.NO_SITE_ID) {
// // The redirect is pointing to a site, load it. // // The redirect is pointing to a site, load it.
// return this.sitesProvider.loadSite(redirectData.siteId, redirectData.page, redirectData.params) // return this.sitesProvider.loadSite(redirectData.siteId, redirectData.page, redirectData.params)
@ -65,13 +66,12 @@ export class CoreLoginInitPage implements OnInit {
} }
} }
return this.loadPage(); await this.loadPage();
}).then(() => {
// If we hide the splash screen now, the init view is still seen for an instant. Wait a bit to make sure it isn't seen. // If we hide the splash screen now, the init view is still seen for an instant. Wait a bit to make sure it isn't seen.
setTimeout(() => { setTimeout(() => {
SplashScreen.instance.hide(); SplashScreen.instance.hide();
}, 100); }, 100);
});
} }
/** /**

View File

@ -40,11 +40,11 @@ import { NavController } from '@ionic/angular';
}) })
export class CoreLoginSitePage implements OnInit { export class CoreLoginSitePage implements OnInit {
@ViewChild('siteFormEl') formElement: ElementRef; @ViewChild('siteFormEl') formElement?: ElementRef;
siteForm: FormGroup; siteForm: FormGroup;
fixedSites: CoreLoginSiteInfoExtended[]; fixedSites?: CoreLoginSiteInfoExtended[];
filteredSites: CoreLoginSiteInfoExtended[]; filteredSites?: CoreLoginSiteInfoExtended[];
siteSelector = 'sitefinder'; siteSelector = 'sitefinder';
showKeyboard = false; showKeyboard = false;
filter = ''; filter = '';
@ -53,7 +53,7 @@ export class CoreLoginSitePage implements OnInit {
loadingSites = false; loadingSites = false;
searchFunction: (search: string) => void; searchFunction: (search: string) => void;
showScanQR: boolean; showScanQR: boolean;
enteredSiteUrl: CoreLoginSiteInfoExtended; enteredSiteUrl?: CoreLoginSiteInfoExtended;
siteFinderSettings: SiteFinderSettings; siteFinderSettings: SiteFinderSettings;
constructor( constructor(
@ -95,10 +95,10 @@ export class CoreLoginSitePage implements OnInit {
if (search.length >= 3) { if (search.length >= 3) {
// Update the sites list. // Update the sites list.
this.sites = await CoreSites.instance.findSites(search); const sites = await CoreSites.instance.findSites(search);
// Add UI tweaks. // Add UI tweaks.
this.sites = this.extendCoreLoginSiteInfo(this.sites); this.sites = this.extendCoreLoginSiteInfo(<CoreLoginSiteInfoExtended[]> sites);
this.hasSites = !!this.sites.length; this.hasSites = !!this.sites.length;
} else { } else {
@ -201,7 +201,7 @@ export class CoreLoginSitePage implements OnInit {
let valid = value.length >= 3 && CoreUrl.isValidMoodleUrl(value); let valid = value.length >= 3 && CoreUrl.isValidMoodleUrl(value);
if (!valid) { if (!valid) {
const demo = !!this.getDemoSiteData(value); const demo = !!CoreSites.instance.getDemoSiteData(value);
if (demo) { if (demo) {
valid = true; valid = true;
@ -212,19 +212,6 @@ export class CoreLoginSitePage implements OnInit {
}; };
} }
/**
* Get the demo data for a certain "name" if it is a demo site.
*
* @param name Name of the site to check.
* @return Site data if it's a demo site, undefined otherwise.
*/
getDemoSiteData(name: string): CoreSitesDemoSiteData {
const demoSites = CoreConfigConstants.demo_sites;
if (typeof demoSites != 'undefined' && typeof demoSites[name] != 'undefined') {
return demoSites[name];
}
}
/** /**
* Show a help modal. * Show a help modal.
*/ */
@ -361,7 +348,11 @@ export class CoreLoginSitePage implements OnInit {
if (CoreLoginHelper.instance.isSSOLoginNeeded(response.code)) { if (CoreLoginHelper.instance.isSSOLoginNeeded(response.code)) {
// SSO. User needs to authenticate in a browser. // SSO. User needs to authenticate in a browser.
CoreLoginHelper.instance.confirmAndOpenBrowserForSSOLogin( CoreLoginHelper.instance.confirmAndOpenBrowserForSSOLogin(
response.siteUrl, response.code, response.service, response.config && response.config.launchurl); response.siteUrl,
response.code,
response.service,
response.config?.launchurl,
);
} else { } else {
const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config }; const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config };
if (foundSite) { if (foundSite) {
@ -382,7 +373,7 @@ export class CoreLoginSitePage implements OnInit {
* @param url The URL the user was trying to connect to. * @param url The URL the user was trying to connect to.
* @param error Error to display. * @param error Error to display.
*/ */
protected showLoginIssue(url: string, error: CoreError): void { protected showLoginIssue(url: string | null, error: CoreError): void {
let errorMessage = CoreDomUtils.instance.getErrorMessage(error); let errorMessage = CoreDomUtils.instance.getErrorMessage(error);
if (errorMessage == Translate.instance.instant('core.cannotconnecttrouble')) { if (errorMessage == Translate.instance.instant('core.cannotconnecttrouble')) {
@ -452,10 +443,12 @@ export class CoreLoginSitePage implements OnInit {
this.enteredSiteUrl = { this.enteredSiteUrl = {
url: search, url: search,
name: 'connect', name: 'connect',
title: '',
location: '',
noProtocolUrl: CoreUrl.removeProtocol(search), noProtocolUrl: CoreUrl.removeProtocol(search),
}; };
} else { } else {
this.enteredSiteUrl = null; this.enteredSiteUrl = undefined;
} }
this.searchFunction(search.trim()); this.searchFunction(search.trim());
@ -468,8 +461,10 @@ export class CoreLoginSitePage implements OnInit {
// Show some instructions first. // Show some instructions first.
CoreDomUtils.instance.showAlertWithOptions({ CoreDomUtils.instance.showAlertWithOptions({
header: Translate.instance.instant('core.login.faqwhereisqrcode'), header: Translate.instance.instant('core.login.faqwhereisqrcode'),
message: Translate.instance.instant('core.login.faqwhereisqrcodeanswer', message: Translate.instance.instant(
{ $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML }), 'core.login.faqwhereisqrcodeanswer',
{ $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML },
),
buttons: [ buttons: [
{ {
text: Translate.instance.instant('core.cancel'), text: Translate.instance.instant('core.cancel'),
@ -505,9 +500,9 @@ export class CoreLoginSitePage implements OnInit {
* Extended data for UI implementation. * Extended data for UI implementation.
*/ */
type CoreLoginSiteInfoExtended = CoreLoginSiteInfo & { type CoreLoginSiteInfoExtended = CoreLoginSiteInfo & {
noProtocolUrl?: string; // Url wihtout protocol. noProtocolUrl: string; // Url wihtout protocol.
location?: string; // City + country. location: string; // City + country.
title?: string; // Name + alias. title: string; // Name + alias.
}; };
type SiteFinderSettings = { type SiteFinderSettings = {

View File

@ -50,7 +50,7 @@ export class CoreLoginHelperProvider {
protected logger: CoreLogger; protected logger: CoreLogger;
protected isSSOConfirmShown = false; protected isSSOConfirmShown = false;
protected isOpenEditAlertShown = false; protected isOpenEditAlertShown = false;
protected pageToLoad: {page: string; params: Params; time: number}; // Page to load once main menu is opened. protected pageToLoad?: {page: string; params: Params; time: number}; // Page to load once main menu is opened.
protected isOpeningReconnect = false; protected isOpeningReconnect = false;
waitingForBrowser = false; waitingForBrowser = false;
@ -208,7 +208,7 @@ export class CoreLoginHelperProvider {
const categories: Record<number, AuthEmailSignupProfileFieldsCategory> = {}; const categories: Record<number, AuthEmailSignupProfileFieldsCategory> = {};
profileFields.forEach((field) => { profileFields.forEach((field) => {
if (!field.signup) { if (!field.signup || !field.categoryid) {
// Not a signup field, ignore it. // Not a signup field, ignore it.
return; return;
} }
@ -216,7 +216,7 @@ export class CoreLoginHelperProvider {
if (!categories[field.categoryid]) { if (!categories[field.categoryid]) {
categories[field.categoryid] = { categories[field.categoryid] = {
id: field.categoryid, id: field.categoryid,
name: field.categoryname, name: field.categoryname || '',
fields: [], fields: [],
}; };
} }
@ -233,8 +233,8 @@ export class CoreLoginHelperProvider {
* @param config Site public config. * @param config Site public config.
* @return Disabled features. * @return Disabled features.
*/ */
getDisabledFeatures(config: CoreSitePublicConfigResponse): string { getDisabledFeatures(config?: CoreSitePublicConfigResponse): string {
const disabledFeatures = config && config.tool_mobile_disabledfeatures; const disabledFeatures = config?.tool_mobile_disabledfeatures;
if (!disabledFeatures) { if (!disabledFeatures) {
return ''; return '';
} }
@ -302,8 +302,8 @@ export class CoreLoginHelperProvider {
* @param config Site public config. * @param config Site public config.
* @return Logo URL. * @return Logo URL.
*/ */
getLogoUrl(config: CoreSitePublicConfigResponse): string { getLogoUrl(config: CoreSitePublicConfigResponse): string | undefined {
return !CoreConfigConstants.forceLoginLogo && config ? (config.logourl || config.compactlogourl) : null; return !CoreConfigConstants.forceLoginLogo && config ? (config.logourl || config.compactlogourl) : undefined;
} }
/** /**
@ -314,7 +314,7 @@ export class CoreLoginHelperProvider {
*/ */
getLogoutLabel(site?: CoreSite): string { getLogoutLabel(site?: CoreSite): string {
site = site || CoreSites.instance.getCurrentSite(); site = site || CoreSites.instance.getCurrentSite();
const config = <CoreSiteConfig> site.getStoredConfig(); const config = <CoreSiteConfig> site?.getStoredConfig();
return 'core.mainmenu.' + (config && config.tool_mobile_forcelogout == '1' ? 'logout' : 'changesite'); return 'core.mainmenu.' + (config && config.tool_mobile_forcelogout == '1' ? 'logout' : 'changesite');
} }
@ -325,7 +325,7 @@ export class CoreLoginHelperProvider {
* @param params Params. * @param params Params.
* @return OAuth ID. * @return OAuth ID.
*/ */
getOAuthIdFromParams(params: CoreUrlParams): number { getOAuthIdFromParams(params: CoreUrlParams): number | undefined {
return params && typeof params.oauthsso != 'undefined' ? Number(params.oauthsso) : undefined; return params && typeof params.oauthsso != 'undefined' ? Number(params.oauthsso) : undefined;
} }
@ -338,15 +338,18 @@ export class CoreLoginHelperProvider {
async getSitePolicy(siteId?: string): Promise<string> { async getSitePolicy(siteId?: string): Promise<string> {
const site = await CoreSites.instance.getSite(siteId); const site = await CoreSites.instance.getSite(siteId);
let sitePolicy: string; let sitePolicy: string | undefined;
try { try {
// Try to get the latest config, maybe the site policy was just added or has changed. // Try to get the latest config, maybe the site policy was just added or has changed.
sitePolicy = <string> await site.getConfig('sitepolicy', true); sitePolicy = <string> await site.getConfig('sitepolicy', true);
} catch (error) { } catch (error) {
// Cannot get config, try to get the site policy using auth_email_get_signup_settings. // Cannot get config, try to get the site policy using auth_email_get_signup_settings.
const settings = <AuthEmailSignupSettings> await CoreWS.instance.callAjax('auth_email_get_signup_settings', {}, const settings = <AuthEmailSignupSettings> await CoreWS.instance.callAjax(
{ siteUrl: site.getURL() }); 'auth_email_get_signup_settings',
{},
{ siteUrl: site.getURL() },
);
sitePolicy = settings.sitepolicy; sitePolicy = settings.sitepolicy;
} }
@ -374,7 +377,10 @@ export class CoreLoginHelperProvider {
* @param disabledFeatures List of disabled features already treated. If not provided it will be calculated. * @param disabledFeatures List of disabled features already treated. If not provided it will be calculated.
* @return Valid identity providers. * @return Valid identity providers.
*/ */
getValidIdentityProviders(siteConfig: CoreSitePublicConfigResponse, disabledFeatures?: string): CoreSiteIdentityProvider[] { getValidIdentityProviders(siteConfig?: CoreSitePublicConfigResponse, disabledFeatures?: string): CoreSiteIdentityProvider[] {
if (!siteConfig) {
return [];
}
if (this.isFeatureDisabled('NoDelegate_IdentityProviders', siteConfig, disabledFeatures)) { if (this.isFeatureDisabled('NoDelegate_IdentityProviders', siteConfig, disabledFeatures)) {
// Identity providers are disabled, return an empty list. // Identity providers are disabled, return an empty list.
return []; return [];
@ -460,8 +466,8 @@ export class CoreLoginHelperProvider {
* @return Whether there are several fixed URLs. * @return Whether there are several fixed URLs.
*/ */
hasSeveralFixedSites(): boolean { hasSeveralFixedSites(): boolean {
return CoreConfigConstants.siteurl && Array.isArray(CoreConfigConstants.siteurl) && return !!(CoreConfigConstants.siteurl && Array.isArray(CoreConfigConstants.siteurl) &&
CoreConfigConstants.siteurl.length > 1; CoreConfigConstants.siteurl.length > 1);
} }
/** /**
@ -923,8 +929,14 @@ export class CoreLoginHelperProvider {
try { try {
this.waitingForBrowser = true; this.waitingForBrowser = true;
this.openBrowserForSSOLogin(result.siteUrl, result.code, result.service, result.config?.launchurl, this.openBrowserForSSOLogin(
data.pageName, data.params); result.siteUrl,
result.code,
result.service,
result.config?.launchurl,
data.pageName,
data.params,
);
} catch (error) { } catch (error) {
// User cancelled, logout him. // User cancelled, logout him.
CoreSites.instance.logout(); CoreSites.instance.logout();
@ -956,8 +968,13 @@ export class CoreLoginHelperProvider {
this.waitingForBrowser = true; this.waitingForBrowser = true;
CoreSites.instance.unsetCurrentSite(); // Unset current site to make authentication work fine. CoreSites.instance.unsetCurrentSite(); // Unset current site to make authentication work fine.
this.openBrowserForOAuthLogin(siteUrl, providerToUse, result.config.launchurl, data.pageName, this.openBrowserForOAuthLogin(
data.params); siteUrl,
providerToUse,
result.config?.launchurl,
data.pageName,
data.params,
);
} catch (error) { } catch (error) {
// User cancelled, logout him. // User cancelled, logout him.
CoreSites.instance.logout(); CoreSites.instance.logout();
@ -1067,10 +1084,13 @@ export class CoreLoginHelperProvider {
try { try {
const result = <ResendConfirmationEmailResult> await CoreWS.instance.callAjax( const result = <ResendConfirmationEmailResult> await CoreWS.instance.callAjax(
'core_auth_resend_confirmation_email', data, preSets); 'core_auth_resend_confirmation_email',
data,
preSets,
);
if (!result.status) { if (!result.status) {
throw new CoreWSError(result.warnings[0]); throw new CoreWSError(result.warnings?.[0]);
} }
const message = Translate.instance.instant('core.login.emailconfirmsentsuccess'); const message = Translate.instance.instant('core.login.emailconfirmsentsuccess');
@ -1096,6 +1116,8 @@ export class CoreLoginHelperProvider {
try { try {
// This call will always fail because we aren't sending parameters. // This call will always fail because we aren't sending parameters.
await CoreWS.instance.callAjax('core_auth_resend_confirmation_email', {}, { siteUrl }); await CoreWS.instance.callAjax('core_auth_resend_confirmation_email', {}, { siteUrl });
return true; // We should never reach here.
} catch (error) { } catch (error) {
// If the WS responds with an invalid parameter error it means the WS is avaiable. // If the WS responds with an invalid parameter error it means the WS is avaiable.
return error?.errorcode === 'invalidparameter'; return error?.errorcode === 'invalidparameter';
@ -1134,13 +1156,13 @@ export class CoreLoginHelperProvider {
*/ */
treatUserTokenError(siteUrl: string, error: CoreWSError, username?: string, password?: string): void { treatUserTokenError(siteUrl: string, error: CoreWSError, username?: string, password?: string): void {
if (error.errorcode == 'forcepasswordchangenotice') { if (error.errorcode == 'forcepasswordchangenotice') {
this.openChangePassword(siteUrl, CoreTextUtils.instance.getErrorMessageFromError(error)); this.openChangePassword(siteUrl, CoreTextUtils.instance.getErrorMessageFromError(error)!);
} else if (error.errorcode == 'usernotconfirmed') { } else if (error.errorcode == 'usernotconfirmed') {
this.showNotConfirmedModal(siteUrl, undefined, username, password); this.showNotConfirmedModal(siteUrl, undefined, username, password);
} else if (error.errorcode == 'connecttomoodleapp') { } else if (error.errorcode == 'connecttomoodleapp') {
this.showMoodleAppNoticeModal(CoreTextUtils.instance.getErrorMessageFromError(error)); this.showMoodleAppNoticeModal(CoreTextUtils.instance.getErrorMessageFromError(error)!);
} else if (error.errorcode == 'connecttoworkplaceapp') { } else if (error.errorcode == 'connecttoworkplaceapp') {
this.showWorkplaceNoticeModal(CoreTextUtils.instance.getErrorMessageFromError(error)); this.showWorkplaceNoticeModal(CoreTextUtils.instance.getErrorMessageFromError(error)!);
} else { } else {
CoreDomUtils.instance.showErrorModal(error); CoreDomUtils.instance.showErrorModal(error);
} }

View File

@ -15,7 +15,7 @@
import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core'; import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core';
import { NavController, IonContent } from '@ionic/angular'; import { NavController, IonContent } from '@ionic/angular';
import { CoreEventLoadingChangedData, CoreEvents, CoreEventsProvider } from '@services/events'; import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents, CoreEventsProvider } from '@services/events';
import { CoreSites } from '@services/sites'; import { CoreSites } from '@services/sites';
import { CoreDomUtils } from '@services/utils/dom'; import { CoreDomUtils } from '@services/utils/dom';
import { CoreIframeUtils, CoreIframeUtilsProvider } from '@services/utils/iframe'; import { CoreIframeUtils, CoreIframeUtilsProvider } from '@services/utils/iframe';
@ -38,7 +38,7 @@ import { Translate } from '@singletons/core.singletons';
}) })
export class CoreFormatTextDirective implements OnChanges { export class CoreFormatTextDirective implements OnChanges {
@Input() text: string; // The text to format. @Input() text?: string; // The text to format.
@Input() siteId?: string; // Site ID to use. @Input() siteId?: string; // Site ID to use.
@Input() component?: string; // Component for CoreExternalContentDirective. @Input() component?: string; // Component for CoreExternalContentDirective.
@Input() componentId?: string | number; // Component ID to use in conjunction with the component. @Input() componentId?: string | number; // Component ID to use in conjunction with the component.
@ -56,11 +56,11 @@ export class CoreFormatTextDirective implements OnChanges {
@Input() contextInstanceId?: number; // The instance ID related to the context. @Input() contextInstanceId?: number; // The instance ID related to the context.
@Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters. @Input() courseId?: number; // Course ID the text belongs to. It can be used to improve performance with filters.
@Input() wsNotFiltered?: boolean | string; // If true it means the WS didn't filter the text for some reason. @Input() wsNotFiltered?: boolean | string; // If true it means the WS didn't filter the text for some reason.
@Output() afterRender?: EventEmitter<void>; // Called when the data is rendered. @Output() afterRender: EventEmitter<void>; // Called when the data is rendered.
protected element: HTMLElement; protected element: HTMLElement;
protected showMoreDisplayed: boolean; protected showMoreDisplayed = false;
protected loadingChangedListener; protected loadingChangedListener?: CoreEventObserver;
constructor( constructor(
element: ElementRef, element: ElementRef,
@ -116,9 +116,9 @@ export class CoreFormatTextDirective implements OnChanges {
const container = document.createElement('span'); const container = document.createElement('span');
const originalWidth = img.attributes.getNamedItem('width'); const originalWidth = img.attributes.getNamedItem('width');
const forcedWidth = parseInt(originalWidth && originalWidth.value); const forcedWidth = Number(originalWidth?.value);
if (!isNaN(forcedWidth)) { if (!isNaN(forcedWidth)) {
if (originalWidth.value.indexOf('%') < 0) { if (originalWidth!.value.indexOf('%') < 0) {
img.style.width = forcedWidth + 'px'; img.style.width = forcedWidth + 'px';
} else { } else {
img.style.width = forcedWidth + '%'; img.style.width = forcedWidth + '%';
@ -160,7 +160,7 @@ export class CoreFormatTextDirective implements OnChanges {
return; return;
} }
let imgWidth = parseInt(img.getAttribute('width')); let imgWidth = Number(img.getAttribute('width'));
if (!imgWidth) { if (!imgWidth) {
// No width attribute, use real size. // No width attribute, use real size.
imgWidth = img.naturalWidth; imgWidth = img.naturalWidth;
@ -185,7 +185,7 @@ export class CoreFormatTextDirective implements OnChanges {
CoreDomUtils.instance.viewImage(imgSrc, img.getAttribute('alt'), this.component, this.componentId, true); CoreDomUtils.instance.viewImage(imgSrc, img.getAttribute('alt'), this.component, this.componentId, true);
}); });
img.parentNode.appendChild(anchor); img.parentNode?.appendChild(anchor);
}); });
} }
@ -194,10 +194,13 @@ export class CoreFormatTextDirective implements OnChanges {
*/ */
protected calculateHeight(): void { protected calculateHeight(): void {
// @todo: Work on calculate this height better. // @todo: Work on calculate this height better.
if (!this.maxHeight) {
return;
}
// Remove max-height (if any) to calculate the real height. // Remove max-height (if any) to calculate the real height.
const initialMaxHeight = this.element.style.maxHeight; const initialMaxHeight = this.element.style.maxHeight;
this.element.style.maxHeight = null; this.element.style.maxHeight = '';
const height = this.getElementHeight(this.element); const height = this.getElementHeight(this.element);
@ -245,6 +248,9 @@ export class CoreFormatTextDirective implements OnChanges {
// Ignore it if the event was prevented by some other listener. // Ignore it if the event was prevented by some other listener.
return; return;
} }
if (!this.text) {
return;
}
const expandInFullview = CoreUtils.instance.isTrueOrOne(this.fullOnClick) || false; const expandInFullview = CoreUtils.instance.isTrueOrOne(this.fullOnClick) || false;
@ -265,14 +271,18 @@ export class CoreFormatTextDirective implements OnChanges {
// Open a new state with the contents. // Open a new state with the contents.
const filter = typeof this.filter != 'undefined' ? CoreUtils.instance.isTrueOrOne(this.filter) : undefined; const filter = typeof this.filter != 'undefined' ? CoreUtils.instance.isTrueOrOne(this.filter) : undefined;
CoreTextUtils.instance.viewText(this.fullTitle || Translate.instance.instant('core.description'), this.text, { CoreTextUtils.instance.viewText(
this.fullTitle || Translate.instance.instant('core.description'),
this.text,
{
component: this.component, component: this.component,
componentId: this.componentId, componentId: this.componentId,
filter: filter, filter: filter,
contextLevel: this.contextLevel, contextLevel: this.contextLevel,
instanceId: this.contextInstanceId, instanceId: this.contextInstanceId,
courseId: this.courseId, courseId: this.courseId,
}); },
);
} }
} }
@ -357,29 +367,17 @@ export class CoreFormatTextDirective implements OnChanges {
* @return Promise resolved with a div element containing the code. * @return Promise resolved with a div element containing the code.
*/ */
protected async formatContents(): Promise<FormatContentsResult> { protected async formatContents(): Promise<FormatContentsResult> {
const result: FormatContentsResult = {
div: null,
filters: [],
options: {},
siteId: this.siteId,
};
// Retrieve the site since it might be needed later. // Retrieve the site since it might be needed later.
const site = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSite(this.siteId)); const site = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSite(this.siteId));
if (site) { if (site && this.contextLevel == 'course' && this.contextInstanceId !== undefined && this.contextInstanceId <= 0) {
result.siteId = site.getId();
}
if (this.contextLevel == 'course' && this.contextInstanceId <= 0) {
this.contextInstanceId = site.getSiteHomeId(); this.contextInstanceId = site.getSiteHomeId();
} }
const filter = typeof this.filter == 'undefined' ? const filter = typeof this.filter == 'undefined' ?
!!(this.contextLevel && typeof this.contextInstanceId != 'undefined') : CoreUtils.instance.isTrueOrOne(this.filter); !!(this.contextLevel && typeof this.contextInstanceId != 'undefined') : CoreUtils.instance.isTrueOrOne(this.filter);
result.options = { const options = {
clean: CoreUtils.instance.isTrueOrOne(this.clean), clean: CoreUtils.instance.isTrueOrOne(this.clean),
singleLine: CoreUtils.instance.isTrueOrOne(this.singleLine), singleLine: CoreUtils.instance.isTrueOrOne(this.singleLine),
highlight: this.highlight, highlight: this.highlight,
@ -391,10 +389,10 @@ export class CoreFormatTextDirective implements OnChanges {
if (filter) { if (filter) {
// @todo // @todo
formatted = this.text; formatted = this.text!;
} else { } else {
// @todo // @todo
formatted = this.text; formatted = this.text!;
} }
formatted = this.treatWindowOpen(formatted); formatted = this.treatWindowOpen(formatted);
@ -405,9 +403,12 @@ export class CoreFormatTextDirective implements OnChanges {
this.treatHTMLElements(div, site); this.treatHTMLElements(div, site);
result.div = div; return {
div,
return result; filters: [],
options,
siteId: site?.getId(),
};
} }
/** /**
@ -418,7 +419,7 @@ export class CoreFormatTextDirective implements OnChanges {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async treatHTMLElements(div: HTMLElement, site?: CoreSite): Promise<void> { protected async treatHTMLElements(div: HTMLElement, site?: CoreSite): Promise<void> {
const canTreatVimeo = site && site.isVersionGreaterEqualThan(['3.3.4', '3.4']); const canTreatVimeo = site?.isVersionGreaterEqualThan(['3.3.4', '3.4']) || false;
const navCtrl = this.navCtrl; // @todo this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl; const navCtrl = this.navCtrl; // @todo this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
const images = Array.from(div.querySelectorAll('img')); const images = Array.from(div.querySelectorAll('img'));
@ -537,7 +538,8 @@ export class CoreFormatTextDirective implements OnChanges {
if (!width) { if (!width) {
// All elements inside are floating or inline. Change display mode to allow calculate the width. // All elements inside are floating or inline. Change display mode to allow calculate the width.
const parentWidth = CoreDomUtils.instance.getElementWidth(element.parentElement, true, false, false, true); const parentWidth = element.parentElement ?
CoreDomUtils.instance.getElementWidth(element.parentElement, true, false, false, true) : 0;
const previousDisplay = getComputedStyle(element, null).display; const previousDisplay = getComputedStyle(element, null).display;
element.style.display = 'inline-block'; element.style.display = 'inline-block';
@ -578,7 +580,7 @@ export class CoreFormatTextDirective implements OnChanges {
this.element.classList.remove('core-expand-in-fullview'); this.element.classList.remove('core-expand-in-fullview');
this.element.classList.remove('core-text-formatted'); this.element.classList.remove('core-text-formatted');
this.element.classList.remove('core-shortened'); this.element.classList.remove('core-shortened');
this.element.style.maxHeight = null; this.element.style.maxHeight = '';
this.showMoreDisplayed = false; this.showMoreDisplayed = false;
} }
@ -595,7 +597,7 @@ export class CoreFormatTextDirective implements OnChanges {
const tracks = Array.from(element.querySelectorAll('track')); const tracks = Array.from(element.querySelectorAll('track'));
sources.forEach((source) => { sources.forEach((source) => {
source.setAttribute('target-src', source.getAttribute('src')); source.setAttribute('target-src', source.getAttribute('src') || '');
source.removeAttribute('src'); source.removeAttribute('src');
this.addExternalContent(source); this.addExternalContent(source);
}); });
@ -618,8 +620,12 @@ export class CoreFormatTextDirective implements OnChanges {
* @param canTreatVimeo Whether Vimeo videos can be treated in the site. * @param canTreatVimeo Whether Vimeo videos can be treated in the site.
* @param navCtrl NavController to use. * @param navCtrl NavController to use.
*/ */
protected async treatIframe(iframe: HTMLIFrameElement, site: CoreSite, canTreatVimeo: boolean, navCtrl: NavController): protected async treatIframe(
Promise<void> { iframe: HTMLIFrameElement,
site: CoreSite | undefined,
canTreatVimeo: boolean,
navCtrl: NavController,
): Promise<void> {
const src = iframe.src; const src = iframe.src;
const currentSite = CoreSites.instance.getCurrentSite(); const currentSite = CoreSites.instance.getCurrentSite();
@ -636,7 +642,7 @@ export class CoreFormatTextDirective implements OnChanges {
return; return;
} }
if (src && canTreatVimeo) { if (site && src && canTreatVimeo) {
// Check if it's a Vimeo video. If it is, use the wsplayer script instead to make restricted videos work. // Check if it's a Vimeo video. If it is, use the wsplayer script instead to make restricted videos work.
const matches = iframe.src.match(/https?:\/\/player\.vimeo\.com\/video\/([0-9]+)/); const matches = iframe.src.match(/https?:\/\/player\.vimeo\.com\/video\/([0-9]+)/);
if (matches && matches[1]) { if (matches && matches[1]) {
@ -679,7 +685,7 @@ export class CoreFormatTextDirective implements OnChanges {
} }
// Do the iframe responsive. // Do the iframe responsive.
if (iframe.parentElement.classList.contains('embed-responsive')) { if (iframe.parentElement?.classList.contains('embed-responsive')) {
iframe.addEventListener('load', () => { iframe.addEventListener('load', () => {
if (iframe.contentDocument) { if (iframe.contentDocument) {
const css = document.createElement('style'); const css = document.createElement('style');
@ -723,5 +729,5 @@ type FormatContentsResult = {
div: HTMLElement; div: HTMLElement;
filters: any[]; filters: any[];
options: any; options: any;
siteId: string; siteId?: string;
}; };

View File

@ -38,7 +38,7 @@ import { Directive, ElementRef, OnInit, Input, Output, EventEmitter } from '@ang
}) })
export class CoreSupressEventsDirective implements OnInit { export class CoreSupressEventsDirective implements OnInit {
@Input('core-suppress-events') suppressEvents: string | string[]; @Input('core-suppress-events') suppressEvents?: string | string[];
@Output() onClick = new EventEmitter(); // eslint-disable-line @angular-eslint/no-output-on-prefix @Output() onClick = new EventEmitter(); // eslint-disable-line @angular-eslint/no-output-on-prefix
protected element: HTMLElement; protected element: HTMLElement;

View File

@ -13,6 +13,7 @@
// limitations under the License. // limitations under the License.
import { Injectable, NgZone, ApplicationRef } from '@angular/core'; import { Injectable, NgZone, ApplicationRef } from '@angular/core';
import { Params } from '@angular/router';
import { Connection } from '@ionic-native/network/ngx'; import { Connection } from '@ionic-native/network/ngx';
import { CoreDB } from '@services/db'; import { CoreDB } from '@services/db';
@ -224,7 +225,7 @@ export class CoreAppProvider {
* @param storesConfig Config params to send the user to the right place. * @param storesConfig Config params to send the user to the right place.
* @return Store URL. * @return Store URL.
*/ */
getAppStoreUrl(storesConfig: CoreStoreConfig): string | null { getAppStoreUrl(storesConfig: CoreStoreConfig): string | undefined {
if (this.isMac() && storesConfig.mac) { if (this.isMac() && storesConfig.mac) {
return 'itms-apps://itunes.apple.com/app/' + storesConfig.mac; return 'itms-apps://itunes.apple.com/app/' + storesConfig.mac;
} }
@ -253,7 +254,7 @@ export class CoreAppProvider {
return storesConfig.mobile; return storesConfig.mobile;
} }
return storesConfig.default || null; return storesConfig.default;
} }
/** /**
@ -563,11 +564,11 @@ export class CoreAppProvider {
* *
* @return Object with siteid, state, params and timemodified. * @return Object with siteid, state, params and timemodified.
*/ */
getRedirect<Params extends Record<string, unknown> = Record<string, unknown>>(): CoreRedirectData<Params> { getRedirect(): CoreRedirectData {
if (localStorage?.getItem) { if (localStorage?.getItem) {
try { try {
const paramsJson = localStorage.getItem('CoreRedirectParams'); const paramsJson = localStorage.getItem('CoreRedirectParams');
const data: CoreRedirectData<Params> = { const data: CoreRedirectData = {
siteId: localStorage.getItem('CoreRedirectSiteId') || undefined, siteId: localStorage.getItem('CoreRedirectSiteId') || undefined,
page: localStorage.getItem('CoreRedirectState') || undefined, page: localStorage.getItem('CoreRedirectState') || undefined,
timemodified: parseInt(localStorage.getItem('CoreRedirectTime') || '0', 10), timemodified: parseInt(localStorage.getItem('CoreRedirectTime') || '0', 10),
@ -593,7 +594,7 @@ export class CoreAppProvider {
* @param page Page to go. * @param page Page to go.
* @param params Page params. * @param params Page params.
*/ */
storeRedirect(siteId: string, page: string, params: Record<string, unknown>): void { storeRedirect(siteId: string, page: string, params: Params): void {
if (localStorage && localStorage.setItem) { if (localStorage && localStorage.setItem) {
try { try {
localStorage.setItem('CoreRedirectSiteId', siteId); localStorage.setItem('CoreRedirectSiteId', siteId);
@ -697,7 +698,7 @@ export class CoreApp extends makeSingleton(CoreAppProvider) {}
/** /**
* Data stored for a redirect to another page/site. * Data stored for a redirect to another page/site.
*/ */
export type CoreRedirectData<Params extends Record<string, unknown>> = { export type CoreRedirectData = {
/** /**
* ID of the site to load. * ID of the site to load.
*/ */

View File

@ -97,18 +97,17 @@ export class CoreCronDelegate {
* @param siteId Site ID. If not defined, all sites. * @param siteId Site ID. If not defined, all sites.
* @return Promise resolved if handler is executed successfully, rejected otherwise. * @return Promise resolved if handler is executed successfully, rejected otherwise.
*/ */
protected checkAndExecuteHandler(name: string, force?: boolean, siteId?: string): Promise<void> { protected async checkAndExecuteHandler(name: string, force?: boolean, siteId?: string): Promise<void> {
if (!this.handlers[name] || !this.handlers[name].execute) { if (!this.handlers[name] || !this.handlers[name].execute) {
// Invalid handler. // Invalid handler.
const message = `Cannot execute handler because is invalid: ${name}`; const message = `Cannot execute handler because is invalid: ${name}`;
this.logger.debug(message); this.logger.debug(message);
return Promise.reject(new CoreError(message)); throw new CoreError(message);
} }
const usesNetwork = this.handlerUsesNetwork(name); const usesNetwork = this.handlerUsesNetwork(name);
const isSync = !force && this.isHandlerSync(name); const isSync = !force && this.isHandlerSync(name);
let promise;
if (usesNetwork && !CoreApp.instance.isOnline()) { if (usesNetwork && !CoreApp.instance.isOnline()) {
// Offline, stop executing. // Offline, stop executing.
@ -116,47 +115,46 @@ export class CoreCronDelegate {
this.logger.debug(message); this.logger.debug(message);
this.stopHandler(name); this.stopHandler(name);
return Promise.reject(new CoreError(message)); throw new CoreError(message);
} }
if (isSync) { if (isSync) {
// Check network connection. // Check network connection.
promise = CoreConfig.instance.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false) const syncOnlyOnWifi = await CoreConfig.instance.get(CoreConstants.SETTINGS_SYNC_ONLY_ON_WIFI, false);
.then((syncOnlyOnWifi) => !syncOnlyOnWifi || CoreApp.instance.isWifi());
} else {
promise = Promise.resolve(true);
}
return promise.then((execute: boolean) => { if (syncOnlyOnWifi && !CoreApp.instance.isWifi()) {
if (!execute) {
// Cannot execute in this network connection, retry soon. // Cannot execute in this network connection, retry soon.
const message = `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.logger.debug(message);
this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL); this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL);
return Promise.reject(new CoreError(message)); throw new CoreError(message);
}
} }
// Add the execution to the queue. // Add the execution to the queue.
this.queuePromise = this.queuePromise.catch(() => { this.queuePromise = CoreUtils.instance.ignoreErrors(this.queuePromise).then(async () => {
// Ignore errors in previous handlers. try {
}).then(() => this.executeHandler(name, force, siteId).then(() => { await this.executeHandler(name, force, siteId);
this.logger.debug(`Execution of handler '${name}' was a success.`); this.logger.debug(`Execution of handler '${name}' was a success.`);
return this.setHandlerLastExecutionTime(name, Date.now()).then(() => { await CoreUtils.instance.ignoreErrors(this.setHandlerLastExecutionTime(name, Date.now()));
this.scheduleNextExecution(name); this.scheduleNextExecution(name);
});
}, (error) => { return;
} catch (error) {
// Handler call failed. Retry soon. // Handler call failed. Retry soon.
const message = `Execution of handler '${name}' failed.`; const message = `Execution of handler '${name}' failed.`;
this.logger.error(message, error); this.logger.error(message, error);
this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL); this.scheduleNextExecution(name, CoreCronDelegate.MIN_INTERVAL);
return Promise.reject(new CoreError(message)); throw new CoreError(message);
})); }
});
return this.queuePromise; return this.queuePromise;
});
} }
/** /**
@ -172,7 +170,7 @@ export class CoreCronDelegate {
this.logger.debug('Executing handler: ' + name); this.logger.debug('Executing handler: ' + name);
// Wrap the call in Promise.resolve to make sure it's a promise. // Wrap the call in Promise.resolve to make sure it's a promise.
Promise.resolve(this.handlers[name].execute(siteId, force)).then(resolve).catch(reject).finally(() => { Promise.resolve(this.handlers[name].execute!(siteId, force)).then(resolve).catch(reject).finally(() => {
clearTimeout(cancelTimeout); clearTimeout(cancelTimeout);
}); });
@ -192,7 +190,7 @@ export class CoreCronDelegate {
* @return Promise resolved if all handlers are executed successfully, rejected otherwise. * @return Promise resolved if all handlers are executed successfully, rejected otherwise.
*/ */
async forceSyncExecution(siteId?: string): Promise<void> { async forceSyncExecution(siteId?: string): Promise<void> {
const promises = []; const promises: Promise<void>[] = [];
for (const name in this.handlers) { for (const name in this.handlers) {
if (this.isHandlerManualSync(name)) { if (this.isHandlerManualSync(name)) {
@ -208,11 +206,11 @@ export class CoreCronDelegate {
* Force execution of a cron tasks without waiting for the scheduled time. * Force execution of a cron tasks without waiting for the scheduled time.
* Please notice that some tasks may not be executed depending on the network connection and sync settings. * Please notice that some tasks may not be executed depending on the network connection and sync settings.
* *
* @param name If provided, the name of the handler. * @param name Name of the handler.
* @param siteId Site ID. If not defined, all sites. * @param siteId Site ID. If not defined, all sites.
* @return Promise resolved if handler has been executed successfully, rejected otherwise. * @return Promise resolved if handler has been executed successfully, rejected otherwise.
*/ */
forceCronHandlerExecution(name?: string, siteId?: string): Promise<void> { forceCronHandlerExecution(name: string, siteId?: string): Promise<void> {
const handler = this.handlers[name]; const handler = this.handlers[name];
// Mark the handler as running (it might be running already). // Mark the handler as running (it might be running already).
@ -240,7 +238,7 @@ export class CoreCronDelegate {
// Don't allow intervals lower than the minimum. // Don't allow intervals lower than the minimum.
const minInterval = CoreApp.instance.isDesktop() ? CoreCronDelegate.DESKTOP_MIN_INTERVAL : CoreCronDelegate.MIN_INTERVAL; const minInterval = CoreApp.instance.isDesktop() ? CoreCronDelegate.DESKTOP_MIN_INTERVAL : CoreCronDelegate.MIN_INTERVAL;
const handlerInterval = this.handlers[name].getInterval(); const handlerInterval = this.handlers[name].getInterval!();
if (!handlerInterval) { if (!handlerInterval) {
return CoreCronDelegate.DEFAULT_INTERVAL; return CoreCronDelegate.DEFAULT_INTERVAL;
@ -288,12 +286,12 @@ export class CoreCronDelegate {
* @return True if handler uses network or not defined, false otherwise. * @return True if handler uses network or not defined, false otherwise.
*/ */
protected handlerUsesNetwork(name: string): boolean { protected handlerUsesNetwork(name: string): boolean {
if (!this.handlers[name] || !this.handlers[name].usesNetwork) { if (!this.handlers[name] || this.handlers[name].usesNetwork) {
// Invalid, return default. // Invalid, return default.
return true; return true;
} }
return this.handlers[name].usesNetwork(); return this.handlers[name].usesNetwork!();
} }
/** /**
@ -338,7 +336,7 @@ export class CoreCronDelegate {
return this.isHandlerSync(name); return this.isHandlerSync(name);
} }
return this.handlers[name].canManualSync(); return this.handlers[name].canManualSync!();
} }
/** /**
@ -353,7 +351,7 @@ export class CoreCronDelegate {
return true; return true;
} }
return this.handlers[name].isSync(); return this.handlers[name].isSync!();
} }
/** /**
@ -385,10 +383,10 @@ export class CoreCronDelegate {
* Schedule a next execution for a handler. * Schedule a next execution for a handler.
* *
* @param name Name of the handler. * @param name Name of the handler.
* @param time Time to the next execution. If not supplied it will be calculated using the last execution and * @param timeToNextExecution Time (in milliseconds). If not supplied it will be calculated.
* the handler's interval. This param should be used only if it's really necessary. * @return Promise resolved when done.
*/ */
protected scheduleNextExecution(name: string, time?: number): void { protected async scheduleNextExecution(name: string, timeToNextExecution?: number): Promise<void> {
if (!this.handlers[name]) { if (!this.handlers[name]) {
// Invalid handler. // Invalid handler.
return; return;
@ -398,33 +396,24 @@ export class CoreCronDelegate {
return; return;
} }
let promise; if (!timeToNextExecution) {
if (time) {
promise = Promise.resolve(time);
} else {
// Get last execution time to check when do we need to execute it. // Get last execution time to check when do we need to execute it.
promise = this.getHandlerLastExecutionTime(name).then((lastExecution) => { const lastExecution = await this.getHandlerLastExecutionTime(name);
const interval = this.getHandlerInterval(name);
const nextExecution = lastExecution + interval;
return nextExecution - Date.now(); const interval = this.getHandlerInterval(name);
});
timeToNextExecution = lastExecution + interval - Date.now();
} }
promise.then((nextExecution) => { this.logger.debug(`Scheduling next execution of handler '${name}' in '${timeToNextExecution}' ms`);
this.logger.debug(`Scheduling next execution of handler '${name}' in '${nextExecution}' ms`); if (timeToNextExecution < 0) {
if (nextExecution < 0) { timeToNextExecution = 0; // Big negative numbers aren't executed immediately.
nextExecution = 0; // Big negative numbers aren't executed immediately.
} }
this.handlers[name].timeout = window.setTimeout(() => { this.handlers[name].timeout = window.setTimeout(() => {
delete this.handlers[name].timeout; delete this.handlers[name].timeout;
this.checkAndExecuteHandler(name).catch(() => { CoreUtils.instance.ignoreErrors(this.checkAndExecuteHandler(name));
// Ignore errors. }, timeToNextExecution);
});
}, nextExecution);
});
} }
/** /**

View File

@ -44,11 +44,17 @@ export class CoreFileHelperProvider {
* @param siteId The site ID. If not defined, current site. * @param siteId The site ID. If not defined, current site.
* @return Resolved on success. * @return Resolved on success.
*/ */
async downloadAndOpenFile(file: CoreWSExternalFile, component: string, componentId: string | number, state?: string, async downloadAndOpenFile(
onProgress?: CoreFileHelperOnProgress, siteId?: string): Promise<void> { file: CoreWSExternalFile,
component: string,
componentId: string | number,
state?: string,
onProgress?: CoreFileHelperOnProgress,
siteId?: string,
): Promise<void> {
siteId = siteId || CoreSites.instance.getCurrentSiteId(); siteId = siteId || CoreSites.instance.getCurrentSiteId();
const fileUrl = this.getFileUrl(file); const fileUrl = file.fileurl;
const timemodified = this.getFileTimemodified(file); const timemodified = this.getFileTimemodified(file);
if (!this.isOpenableInApp(file)) { if (!this.isOpenableInApp(file)) {
@ -111,33 +117,42 @@ export class CoreFileHelperProvider {
* @param siteId The site ID. If not defined, current site. * @param siteId The site ID. If not defined, current site.
* @return Resolved with the URL to use on success. * @return Resolved with the URL to use on success.
*/ */
protected downloadFileIfNeeded(file: CoreWSExternalFile, fileUrl: string, component?: string, componentId?: string | number, protected async downloadFileIfNeeded(
timemodified?: number, state?: string, onProgress?: CoreFileHelperOnProgress, siteId?: string): Promise<string> { file: CoreWSExternalFile,
fileUrl: string,
component?: string,
componentId?: string | number,
timemodified?: number,
state?: string,
onProgress?: CoreFileHelperOnProgress,
siteId?: string,
): Promise<string> {
siteId = siteId || CoreSites.instance.getCurrentSiteId(); siteId = siteId || CoreSites.instance.getCurrentSiteId();
return CoreSites.instance.getSite(siteId).then((site) => site.checkAndFixPluginfileURL(fileUrl)).then((fixedUrl) => { const site = await CoreSites.instance.getSite(siteId);
if (CoreFile.instance.isAvailable()) { const fixedUrl = await site.checkAndFixPluginfileURL(fileUrl);
let promise;
if (state) { if (!CoreFile.instance.isAvailable()) {
promise = Promise.resolve(state); // Use the online URL.
} else { return fixedUrl;
// Calculate the state. }
promise = CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified);
if (!state) {
// Calculate the state.
state = await CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified);
} }
return promise.then((state) => {
// The file system is available. // The file system is available.
const isWifi = CoreApp.instance.isWifi(); const isWifi = CoreApp.instance.isWifi();
const isOnline = CoreApp.instance.isOnline(); const isOnline = CoreApp.instance.isOnline();
if (state == CoreConstants.DOWNLOADED) { if (state == CoreConstants.DOWNLOADED) {
// File is downloaded, get the local file URL. // File is downloaded, get the local file URL.
return CoreFilepool.instance.getUrlByUrl( return CoreFilepool.instance.getUrlByUrl(siteId, fileUrl, component, componentId, timemodified, false, false, file);
siteId, fileUrl, component, componentId, timemodified, false, false, file);
} else { } else {
if (!isOnline && !this.isStateDownloaded(state)) { if (!isOnline && !this.isStateDownloaded(state)) {
// Not downloaded and user is offline, reject. // Not downloaded and user is offline, reject.
return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
} }
if (onProgress) { if (onProgress) {
@ -145,15 +160,9 @@ export class CoreFileHelperProvider {
onProgress({ calculating: true }); onProgress({ calculating: true });
} }
return CoreFilepool.instance.shouldDownloadBeforeOpen(fixedUrl, file.filesize).then(() => { try {
if (state == CoreConstants.DOWNLOADING) { await CoreFilepool.instance.shouldDownloadBeforeOpen(fixedUrl, file.filesize || 0);
// It's already downloading, stop. } catch (error) {
return;
}
// Download and then return the local URL.
return this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId);
}, () => {
// Start the download if in wifi, but return the URL right away so the file is opened. // Start the download if in wifi, but return the URL right away so the file is opened.
if (isWifi) { if (isWifi) {
this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId);
@ -167,14 +176,17 @@ export class CoreFileHelperProvider {
return CoreFilepool.instance.getUrlByUrl( return CoreFilepool.instance.getUrlByUrl(
siteId, fileUrl, component, componentId, timemodified, false, false, file); siteId, fileUrl, component, componentId, timemodified, false, false, file);
} }
});
} }
});
} else { // Download the file first.
// Use the online URL. if (state == CoreConstants.DOWNLOADING) {
// It's already downloading, stop.
return fixedUrl; return fixedUrl;
} }
});
// Download and then return the local URL.
return this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId);
}
} }
/** /**
@ -189,29 +201,37 @@ export class CoreFileHelperProvider {
* @param siteId The site ID. If not defined, current site. * @param siteId The site ID. If not defined, current site.
* @return Resolved with internal URL on success, rejected otherwise. * @return Resolved with internal URL on success, rejected otherwise.
*/ */
downloadFile(fileUrl: string, component?: string, componentId?: string | number, timemodified?: number, async downloadFile(
onProgress?: (event: ProgressEvent) => void, file?: CoreWSExternalFile, siteId?: string): Promise<string> { fileUrl: string,
component?: string,
componentId?: string | number,
timemodified?: number,
onProgress?: (event: ProgressEvent) => void,
file?: CoreWSExternalFile,
siteId?: string,
): Promise<string> {
siteId = siteId || CoreSites.instance.getCurrentSiteId(); siteId = siteId || CoreSites.instance.getCurrentSiteId();
// Get the site and check if it can download files. // Get the site and check if it can download files.
return CoreSites.instance.getSite(siteId).then((site) => { const site = await CoreSites.instance.getSite(siteId);
if (!site.canDownloadFiles()) { if (!site.canDownloadFiles()) {
return Promise.reject(new CoreError(Translate.instance.instant('core.cannotdownloadfiles'))); throw new CoreError(Translate.instance.instant('core.cannotdownloadfiles'));
} }
return CoreFilepool.instance.downloadUrl(siteId, fileUrl, false, component, componentId, try {
timemodified, onProgress, undefined, file).catch((error) => return await CoreFilepool.instance.downloadUrl(siteId, fileUrl, false, component, componentId, timemodified,
onProgress, undefined, file);
} catch (error) {
// Download failed, check the state again to see if the file was downloaded before. // Download failed, check the state again to see if the file was downloaded before.
CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified).then((state) => { const state = await CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified);
if (this.isStateDownloaded(state)) { if (this.isStateDownloaded(state)) {
return CoreFilepool.instance.getInternalUrlByUrl(siteId, fileUrl); return CoreFilepool.instance.getInternalUrlByUrl(siteId, fileUrl);
} else { } else {
return Promise.reject(error); throw error;
}
} }
}),
);
});
} }
/** /**
@ -220,7 +240,7 @@ export class CoreFileHelperProvider {
* @param file The file. * @param file The file.
* @deprecated since 3.9.5. Get directly the fileurl instead. * @deprecated since 3.9.5. Get directly the fileurl instead.
*/ */
getFileUrl(file: CoreWSExternalFile): string { getFileUrl(file: CoreWSExternalFile): string | undefined {
return file.fileurl; return file.fileurl;
} }
@ -337,11 +357,15 @@ export class CoreFileHelperProvider {
* @return bool. * @return bool.
*/ */
isOpenableInApp(file: {filename?: string; name?: string}): boolean { isOpenableInApp(file: {filename?: string; name?: string}): boolean {
const re = /(?:\.([^.]+))?$/; const regex = /(?:\.([^.]+))?$/;
const regexResult = regex.exec(file.filename || file.name || '');
const ext = re.exec(file.filename || file.name)[1]; if (!regexResult || !regexResult[1]) {
// Couldn't find the extension. Assume it's openable.
return true;
}
return !this.isFileTypeExcludedInApp(ext); return !this.isFileTypeExcludedInApp(regexResult[1]);
} }
/** /**
@ -365,7 +389,7 @@ export class CoreFileHelperProvider {
*/ */
isFileTypeExcludedInApp(fileType: string): boolean { isFileTypeExcludedInApp(fileType: string): boolean {
const currentSite = CoreSites.instance.getCurrentSite(); const currentSite = CoreSites.instance.getCurrentSite();
const fileTypeExcludeList = currentSite && <string> currentSite.getStoredConfig('tool_mobile_filetypeexclusionlist'); const fileTypeExcludeList = currentSite?.getStoredConfig('tool_mobile_filetypeexclusionlist');
if (!fileTypeExcludeList) { if (!fileTypeExcludeList) {
return false; return false;

View File

@ -85,7 +85,7 @@ export class CoreFileSessionProvider {
* @param id File area identifier. * @param id File area identifier.
* @param siteId Site ID. If not defined, current site. * @param siteId Site ID. If not defined, current site.
*/ */
protected initFileArea(component: string, id: string | number, siteId?: string): void { protected initFileArea(component: string, id: string | number, siteId: string): void {
if (!this.files[siteId]) { if (!this.files[siteId]) {
this.files[siteId] = {}; this.files[siteId] = {};
} }

View File

@ -117,12 +117,13 @@ export class CoreFileProvider {
* *
* @return Promise to be resolved when the initialization is finished. * @return Promise to be resolved when the initialization is finished.
*/ */
init(): Promise<void> { async init(): Promise<void> {
if (this.initialized) { if (this.initialized) {
return Promise.resolve(); return;
} }
return Platform.instance.ready().then(() => { await Platform.instance.ready();
if (CoreApp.instance.isAndroid()) { if (CoreApp.instance.isAndroid()) {
this.basePath = File.instance.externalApplicationStorageDirectory || this.basePath; this.basePath = File.instance.externalApplicationStorageDirectory || this.basePath;
} else if (CoreApp.instance.isIOS()) { } else if (CoreApp.instance.isIOS()) {
@ -135,7 +136,6 @@ export class CoreFileProvider {
this.initialized = true; this.initialized = true;
this.logger.debug('FS initialized: ' + this.basePath); this.logger.debug('FS initialized: ' + this.basePath);
});
} }
/** /**
@ -194,8 +194,12 @@ export class CoreFileProvider {
* @param base Base path to create the dir/file in. If not set, use basePath. * @param base Base path to create the dir/file in. If not set, use basePath.
* @return Promise to be resolved when the dir/file is created. * @return Promise to be resolved when the dir/file is created.
*/ */
protected async create(isDirectory: boolean, path: string, failIfExists?: boolean, base?: string): protected async create(
Promise<FileEntry | DirectoryEntry> { isDirectory: boolean,
path: string,
failIfExists?: boolean,
base?: string,
): Promise<FileEntry | DirectoryEntry> {
await this.init(); await this.init();
// Remove basePath if it's in the path. // Remove basePath if it's in the path.
@ -340,17 +344,19 @@ export class CoreFileProvider {
* @return Promise to be resolved when the size is calculated. * @return Promise to be resolved when the size is calculated.
*/ */
protected getSize(entry: DirectoryEntry | FileEntry): Promise<number> { protected getSize(entry: DirectoryEntry | FileEntry): Promise<number> {
return new Promise((resolve, reject) => { return new Promise<number>((resolve, reject) => {
if (this.isDirectoryEntry(entry)) { if (this.isDirectoryEntry(entry)) {
const directoryReader = entry.createReader(); const directoryReader = entry.createReader();
directoryReader.readEntries((entries: (DirectoryEntry | FileEntry)[]) => { directoryReader.readEntries(async (entries: (DirectoryEntry | FileEntry)[]) => {
const promises = []; const promises: Promise<number>[] = [];
for (let i = 0; i < entries.length; i++) { for (let i = 0; i < entries.length; i++) {
promises.push(this.getSize(entries[i])); promises.push(this.getSize(entries[i]));
} }
Promise.all(promises).then((sizes) => { try {
const sizes = await Promise.all(promises);
let directorySize = 0; let directorySize = 0;
for (let i = 0; i < sizes.length; i++) { for (let i = 0; i < sizes.length; i++) {
const fileSize = Number(sizes[i]); const fileSize = Number(sizes[i]);
@ -362,7 +368,9 @@ export class CoreFileProvider {
directorySize += fileSize; directorySize += fileSize;
} }
resolve(directorySize); resolve(directorySize);
}, reject); } catch (error) {
reject(error);
}
}, reject); }, reject);
} else { } else {
entry.file((file) => { entry.file((file) => {
@ -469,7 +477,7 @@ export class CoreFileProvider {
const parsed = CoreTextUtils.instance.parseJSON(text, null); const parsed = CoreTextUtils.instance.parseJSON(text, null);
if (parsed == null && text != null) { if (parsed == null && text != null) {
return Promise.reject(new CoreError('Error parsing JSON file: ' + path)); throw new CoreError('Error parsing JSON file: ' + path);
} }
return parsed; return parsed;
@ -494,7 +502,7 @@ export class CoreFileProvider {
const reader = new FileReader(); const reader = new FileReader();
reader.onloadend = (event): void => { reader.onloadend = (event): void => {
if (event.target.result !== undefined && event.target.result !== null) { if (event.target?.result !== undefined && event.target.result !== null) {
if (format == CoreFileProvider.FORMATJSON) { if (format == CoreFileProvider.FORMATJSON) {
// Convert to object. // Convert to object.
const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null); const parsed = CoreTextUtils.instance.parseJSON(<string> event.target.result, null);
@ -507,7 +515,7 @@ export class CoreFileProvider {
} else { } else {
resolve(event.target.result); resolve(event.target.result);
} }
} else if (event.target.error !== undefined && event.target.error !== null) { } else if (event.target?.error !== undefined && event.target.error !== null) {
reject(event.target.error); reject(event.target.error);
} else { } else {
reject({ code: null, message: 'READER_ONLOADEND_ERR' }); reject({ code: null, message: 'READER_ONLOADEND_ERR' });
@ -550,25 +558,27 @@ export class CoreFileProvider {
* @param append Whether to append the data to the end of the file. * @param append Whether to append the data to the end of the file.
* @return Promise to be resolved when the file is written. * @return Promise to be resolved when the file is written.
*/ */
writeFile(path: string, data: string | Blob, append?: boolean): Promise<FileEntry> { async writeFile(path: string, data: string | Blob, append?: boolean): Promise<FileEntry> {
return this.init().then(() => { await this.init();
// Remove basePath if it's in the path. // Remove basePath if it's in the path.
path = this.removeStartingSlash(path.replace(this.basePath, '')); path = this.removeStartingSlash(path.replace(this.basePath, ''));
this.logger.debug('Write file: ' + path); this.logger.debug('Write file: ' + path);
// Create file (and parent folders) to prevent errors. // Create file (and parent folders) to prevent errors.
return this.createFile(path).then((fileEntry) => { const fileEntry = await this.createFile(path);
if (this.isHTMLAPI && !CoreApp.instance.isDesktop() && if (this.isHTMLAPI && !CoreApp.instance.isDesktop() &&
(typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) { (typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) {
// We need to write Blobs. // We need to write Blobs.
const type = CoreMimetypeUtils.instance.getMimeType(CoreMimetypeUtils.instance.getFileExtension(path)); const extension = CoreMimetypeUtils.instance.getFileExtension(path);
const type = extension ? CoreMimetypeUtils.instance.getMimeType(extension) : '';
data = new Blob([data], { type: type || 'text/plain' }); data = new Blob([data], { type: type || 'text/plain' });
} }
return File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append }) await File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append });
.then(() => fileEntry);
}); return fileEntry;
});
} }
/** /**
@ -583,8 +593,13 @@ export class CoreFileProvider {
* @param append Whether to append the data to the end of the file. * @param append Whether to append the data to the end of the file.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async writeFileDataInFile(file: Blob, path: string, onProgress?: CoreFileProgressFunction, offset: number = 0, async writeFileDataInFile(
append?: boolean): Promise<FileEntry> { file: Blob,
path: string,
onProgress?: CoreFileProgressFunction,
offset: number = 0,
append?: boolean,
): Promise<FileEntry> {
offset = offset || 0; offset = offset || 0;
try { try {
@ -675,16 +690,18 @@ export class CoreFileProvider {
* *
* @return Promise to be resolved when the base path is retrieved. * @return Promise to be resolved when the base path is retrieved.
*/ */
getBasePathToDownload(): Promise<string> { async getBasePathToDownload(): Promise<string> {
return this.init().then(() => { await this.init();
if (CoreApp.instance.isIOS()) { if (CoreApp.instance.isIOS()) {
// In iOS we want the internal URL (cdvfile://localhost/persistent/...). // In iOS we want the internal URL (cdvfile://localhost/persistent/...).
return File.instance.resolveDirectoryUrl(this.basePath).then((dirEntry) => dirEntry.toInternalURL()); const dirEntry = await File.instance.resolveDirectoryUrl(this.basePath);
return dirEntry.toInternalURL();
} else { } else {
// In the other platforms we use the basePath as it is (file://...). // In the other platforms we use the basePath as it is (file://...).
return this.basePath; return this.basePath;
} }
});
} }
/** /**
@ -773,16 +790,20 @@ export class CoreFileProvider {
* try to create it (slower). * try to create it (slower).
* @return Promise resolved when the entry is copied. * @return Promise resolved when the entry is copied.
*/ */
protected async copyOrMoveFileOrDir(from: string, to: string, isDir?: boolean, copy?: boolean, destDirExists?: boolean): protected async copyOrMoveFileOrDir(
Promise<FileEntry | DirectoryEntry> { from: string,
to: string,
isDir?: boolean,
copy?: boolean,
destDirExists?: boolean,
): Promise<FileEntry | DirectoryEntry> {
const fileIsInAppFolder = this.isPathInAppFolder(from); const fileIsInAppFolder = this.isPathInAppFolder(from);
if (!fileIsInAppFolder) { if (!fileIsInAppFolder) {
return this.copyOrMoveExternalFile(from, to, copy); return this.copyOrMoveExternalFile(from, to, copy);
} }
const moveCopyFn: (path: string, dirName: string, newPath: string, newDirName: string) => const moveCopyFn: MoveCopyFunction = copy ?
Promise<FileEntry | DirectoryEntry> = copy ?
(isDir ? File.instance.copyDir.bind(File.instance) : File.instance.copyFile.bind(File.instance)) : (isDir ? File.instance.copyDir.bind(File.instance) : File.instance.copyFile.bind(File.instance)) :
(isDir ? File.instance.moveDir.bind(File.instance) : File.instance.moveFile.bind(File.instance)); (isDir ? File.instance.moveDir.bind(File.instance) : File.instance.moveFile.bind(File.instance));
@ -880,6 +901,8 @@ export class CoreFileProvider {
if (path.indexOf(this.basePath) > -1) { if (path.indexOf(this.basePath) > -1) {
return path.replace(this.basePath, ''); return path.replace(this.basePath, '');
} }
return path;
} }
/** /**
@ -892,33 +915,31 @@ export class CoreFileProvider {
* @param recreateDir Delete the dest directory before unzipping. Defaults to true. * @param recreateDir Delete the dest directory before unzipping. Defaults to true.
* @return Promise resolved when the file is unzipped. * @return Promise resolved when the file is unzipped.
*/ */
unzipFile(path: string, destFolder?: string, onProgress?: (progress: ProgressEvent) => void, recreateDir: boolean = true): async unzipFile(
Promise<void> { path: string,
destFolder?: string,
onProgress?: (progress: ProgressEvent) => void,
recreateDir: boolean = true,
): Promise<void> {
// Get the source file. // Get the source file.
let fileEntry: FileEntry; const fileEntry = await this.getFile(path);
return this.getFile(path).then((fe) => {
fileEntry = fe;
if (destFolder && recreateDir) { if (destFolder && recreateDir) {
// Make sure the dest dir doesn't exist already. // Make sure the dest dir doesn't exist already.
return this.removeDir(destFolder).catch(() => { await CoreUtils.instance.ignoreErrors(this.removeDir(destFolder));
// Ignore errors.
}).then(() =>
// Now create the dir, otherwise if any of the ancestor dirs doesn't exist the unzip would fail. // Now create the dir, otherwise if any of the ancestor dirs doesn't exist the unzip would fail.
this.createDir(destFolder), await this.createDir(destFolder);
);
} }
}).then(() => {
// If destFolder is not set, use same location as ZIP file. We need to use absolute paths (including basePath). // If destFolder is not set, use same location as ZIP file. We need to use absolute paths (including basePath).
destFolder = this.addBasePathIfNeeded(destFolder || CoreMimetypeUtils.instance.removeExtension(path)); destFolder = this.addBasePathIfNeeded(destFolder || CoreMimetypeUtils.instance.removeExtension(path));
return Zip.instance.unzip(fileEntry.toURL(), destFolder, onProgress); const result = await Zip.instance.unzip(fileEntry.toURL(), destFolder, onProgress);
}).then((result) => {
if (result == -1) { if (result == -1) {
return Promise.reject(new CoreError('Unzip failed.')); throw new CoreError('Unzip failed.');
} }
});
} }
/** /**
@ -999,22 +1020,22 @@ export class CoreFileProvider {
* @param copy True to copy, false to move. * @param copy True to copy, false to move.
* @return Promise resolved when the entry is copied/moved. * @return Promise resolved when the entry is copied/moved.
*/ */
protected copyOrMoveExternalFile(from: string, to: string, copy?: boolean): Promise<FileEntry> { protected async copyOrMoveExternalFile(from: string, to: string, copy?: boolean): Promise<FileEntry> {
// Get the file to copy/move. // Get the file to copy/move.
return this.getExternalFile(from).then((fileEntry) => { const fileEntry = await this.getExternalFile(from);
// Create the destination dir if it doesn't exist. // Create the destination dir if it doesn't exist.
const dirAndFile = this.getFileAndDirectoryFromPath(to); const dirAndFile = this.getFileAndDirectoryFromPath(to);
return this.createDir(dirAndFile.directory).then((dirEntry) => const dirEntry = await this.createDir(dirAndFile.directory);
// Now copy/move the file. // Now copy/move the file.
new Promise((resolve, reject): void => { return new Promise((resolve, reject): void => {
if (copy) { if (copy) {
fileEntry.copyTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject); fileEntry.copyTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject);
} else { } else {
fileEntry.moveTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject); fileEntry.moveTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject);
} }
}),
);
}); });
} }
@ -1048,9 +1069,11 @@ export class CoreFileProvider {
* @param defaultExt Default extension to use if no extension found in the file. * @param defaultExt Default extension to use if no extension found in the file.
* @return Promise resolved with the unique file name. * @return Promise resolved with the unique file name.
*/ */
getUniqueNameInFolder(dirPath: string, fileName: string, defaultExt?: string): Promise<string> { async getUniqueNameInFolder(dirPath: string, fileName: string, defaultExt?: string): Promise<string> {
// Get existing files in the folder. // Get existing files in the folder.
return this.getDirectoryContents(dirPath).then((entries) => { try {
const entries = await this.getDirectoryContents(dirPath);
const files = {}; const files = {};
let num = 1; let num = 1;
let fileNameWithoutExtension = CoreMimetypeUtils.instance.removeExtension(fileName); let fileNameWithoutExtension = CoreMimetypeUtils.instance.removeExtension(fileName);
@ -1058,7 +1081,8 @@ export class CoreFileProvider {
// Clean the file name. // Clean the file name.
fileNameWithoutExtension = CoreTextUtils.instance.removeSpecialCharactersForFiles( fileNameWithoutExtension = CoreTextUtils.instance.removeSpecialCharactersForFiles(
CoreTextUtils.instance.decodeURIComponent(fileNameWithoutExtension)); CoreTextUtils.instance.decodeURIComponent(fileNameWithoutExtension),
);
// Index the files by name. // Index the files by name.
entries.forEach((entry) => { entries.forEach((entry) => {
@ -1086,10 +1110,10 @@ export class CoreFileProvider {
// Ask the user what he wants to do. // Ask the user what he wants to do.
return newName; return newName;
} }
}).catch(() => } catch (error) {
// Folder doesn't exist, name is unique. Clean it and return it. // Folder doesn't exist, name is unique. Clean it and return it.
CoreTextUtils.instance.removeSpecialCharactersForFiles(CoreTextUtils.instance.decodeURIComponent(fileName)), return CoreTextUtils.instance.removeSpecialCharactersForFiles(CoreTextUtils.instance.decodeURIComponent(fileName));
); }
} }
/** /**
@ -1119,7 +1143,7 @@ export class CoreFileProvider {
} }
const filesMap: {[fullPath: string]: FileEntry} = {}; const filesMap: {[fullPath: string]: FileEntry} = {};
const promises = []; const promises: Promise<void>[] = [];
// Index the received files by fullPath and ignore the invalid ones. // Index the received files by fullPath and ignore the invalid ones.
files.forEach((file) => { files.forEach((file) => {
@ -1219,3 +1243,5 @@ export class CoreFileProvider {
} }
export class CoreFile extends makeSingleton(CoreFileProvider) {} export class CoreFile extends makeSingleton(CoreFileProvider) {}
type MoveCopyFunction = (path: string, dirName: string, newPath: string, newDirName: string) => Promise<FileEntry | DirectoryEntry>;

File diff suppressed because it is too large Load Diff

View File

@ -106,7 +106,7 @@ export class CoreLocalNotificationsProvider {
protected cancelSubscription?: Subscription; protected cancelSubscription?: Subscription;
protected addSubscription?: Subscription; protected addSubscription?: Subscription;
protected updateSubscription?: Subscription; protected updateSubscription?: Subscription;
protected queueRunner?: CoreQueueRunner; // Queue to decrease the number of concurrent calls to the plugin (see MOBILE-3477). protected queueRunner: CoreQueueRunner; // Queue to decrease the number of concurrent calls to the plugin (see MOBILE-3477).
constructor() { constructor() {
this.logger = CoreLogger.getInstance('CoreLocalNotificationsProvider'); this.logger = CoreLogger.getInstance('CoreLocalNotificationsProvider');
@ -116,7 +116,15 @@ export class CoreLocalNotificationsProvider {
// Ignore errors. // Ignore errors.
}); });
Platform.instance.ready().then(() => { this.init();
}
/**
* Init some properties.
*/
protected async init(): Promise<void> {
await Platform.instance.ready();
// Listen to events. // Listen to events.
this.triggerSubscription = LocalNotifications.instance.on('trigger').subscribe((notification: ILocalNotification) => { this.triggerSubscription = LocalNotifications.instance.on('trigger').subscribe((notification: ILocalNotification) => {
this.trigger(notification); this.trigger(notification);
@ -151,11 +159,10 @@ export class CoreLocalNotificationsProvider {
// Update the channel name. // Update the channel name.
this.createDefaultChannel(); this.createDefaultChannel();
}); });
});
CoreEvents.instance.on(CoreEventsProvider.SITE_DELETED, (site: CoreSite) => { CoreEvents.instance.on(CoreEventsProvider.SITE_DELETED, (site: CoreSite) => {
if (site) { if (site) {
this.cancelSiteNotifications(site.id); this.cancelSiteNotifications(site.id!);
} }
}); });
} }
@ -193,13 +200,13 @@ export class CoreLocalNotificationsProvider {
const scheduled = await this.getAllScheduled(); const scheduled = await this.getAllScheduled();
const ids = []; const ids: number[] = [];
const queueId = 'cancelSiteNotifications-' + siteId; const queueId = 'cancelSiteNotifications-' + siteId;
scheduled.forEach((notif) => { scheduled.forEach((notif) => {
notif.data = this.parseNotificationData(notif.data); notif.data = this.parseNotificationData(notif.data);
if (typeof notif.data == 'object' && notif.data.siteId === siteId) { if (notif.id && typeof notif.data == 'object' && notif.data.siteId === siteId) {
ids.push(notif.id); ids.push(notif.id);
} }
}); });
@ -355,10 +362,9 @@ export class CoreLocalNotificationsProvider {
* @return Whether local notifications plugin is installed. * @return Whether local notifications plugin is installed.
*/ */
isAvailable(): boolean { isAvailable(): boolean {
const win = <any> window; const win = <any> window; // eslint-disable-line @typescript-eslint/no-explicit-any
return CoreApp.instance.isDesktop() || !!(win.cordova && win.cordova.plugins && win.cordova.plugins.notification && return CoreApp.instance.isDesktop() || !!win.cordova?.plugins?.notification?.local;
win.cordova.plugins.notification.local);
} }
/** /**
@ -388,11 +394,11 @@ export class CoreLocalNotificationsProvider {
if (useQueue) { if (useQueue) {
const queueId = 'isTriggered-' + notification.id; const queueId = 'isTriggered-' + notification.id;
return this.queueRunner.run(queueId, () => LocalNotifications.instance.isTriggered(notification.id), { return this.queueRunner.run(queueId, () => LocalNotifications.instance.isTriggered(notification.id!), {
allowRepeated: true, allowRepeated: true,
}); });
} else { } else {
return LocalNotifications.instance.isTriggered(notification.id); return LocalNotifications.instance.isTriggered(notification.id || 0);
} }
} }
} }
@ -446,9 +452,8 @@ export class CoreLocalNotificationsProvider {
/** /**
* Process the next request in queue. * Process the next request in queue.
*/ */
protected processNextRequest(): void { protected async processNextRequest(): Promise<void> {
const nextKey = Object.keys(this.codeRequestsQueue)[0]; const nextKey = Object.keys(this.codeRequestsQueue)[0];
let promise: Promise<void>;
if (typeof nextKey == 'undefined') { if (typeof nextKey == 'undefined') {
// No more requests in queue, stop. // No more requests in queue, stop.
@ -457,27 +462,27 @@ export class CoreLocalNotificationsProvider {
const request = this.codeRequestsQueue[nextKey]; const request = this.codeRequestsQueue[nextKey];
try {
// Check if request is valid. // Check if request is valid.
if (typeof request == 'object' && typeof request.table != 'undefined' && typeof request.id != 'undefined') { if (typeof request != 'object' || request.table === undefined || request.id === undefined) {
return;
}
// Get the code and resolve/reject all the promises of this request. // Get the code and resolve/reject all the promises of this request.
promise = this.getCode(request.table, request.id).then((code) => { const code = await this.getCode(request.table, request.id);
request.deferreds.forEach((p) => { request.deferreds.forEach((p) => {
p.resolve(code); p.resolve(code);
}); });
}).catch((error) => { } catch (error) {
request.deferreds.forEach((p) => { request.deferreds.forEach((p) => {
p.reject(error); p.reject(error);
}); });
}); } finally {
} else {
promise = Promise.resolve();
}
// Once this item is treated, remove it and process next. // Once this item is treated, remove it and process next.
promise.finally(() => {
delete this.codeRequestsQueue[nextKey]; delete this.codeRequestsQueue[nextKey];
this.processNextRequest(); this.processNextRequest();
}); }
} }
/** /**
@ -596,7 +601,7 @@ export class CoreLocalNotificationsProvider {
*/ */
async schedule(notification: ILocalNotification, component: string, siteId: string, alreadyUnique?: boolean): Promise<void> { async schedule(notification: ILocalNotification, component: string, siteId: string, alreadyUnique?: boolean): Promise<void> {
if (!alreadyUnique) { if (!alreadyUnique) {
notification.id = await this.getUniqueNotificationId(notification.id, component, siteId); notification.id = await this.getUniqueNotificationId(notification.id || 0, component, siteId);
} }
notification.data = notification.data || {}; notification.data = notification.data || {};
@ -663,7 +668,7 @@ export class CoreLocalNotificationsProvider {
} }
if (!soundEnabled) { if (!soundEnabled) {
notification.sound = null; notification.sound = undefined;
} else { } else {
delete notification.sound; // Use default value. delete notification.sound; // Use default value.
} }
@ -671,7 +676,7 @@ export class CoreLocalNotificationsProvider {
notification.foreground = true; notification.foreground = true;
// Remove from triggered, since the notification could be in there with a different time. // Remove from triggered, since the notification could be in there with a different time.
this.removeTriggered(notification.id); this.removeTriggered(notification.id || 0);
LocalNotifications.instance.schedule(notification); LocalNotifications.instance.schedule(notification);
} }
}); });

View File

@ -206,7 +206,7 @@ export class CorePluginFileDelegate extends CoreDelegate {
} }
} }
return downloadableFile.filesize; return downloadableFile.filesize || 0;
} }
/** /**
@ -215,7 +215,7 @@ export class CorePluginFileDelegate extends CoreDelegate {
* @param file File data. * @param file File data.
* @return Handler. * @return Handler.
*/ */
protected getHandlerForFile(file: CoreWSExternalFile): CorePluginFileHandler { protected getHandlerForFile(file: CoreWSExternalFile): CorePluginFileHandler | undefined {
for (const component in this.enabledHandlers) { for (const component in this.enabledHandlers) {
const handler = <CorePluginFileHandler> this.enabledHandlers[component]; const handler = <CorePluginFileHandler> this.enabledHandlers[component];

View File

@ -130,7 +130,7 @@ export class CoreSitesProvider {
// Move the records from the old table. // Move the records from the old table.
const sites = await db.getAllRecords<SiteDBEntry>(oldTable); const sites = await db.getAllRecords<SiteDBEntry>(oldTable);
const promises = []; const promises: Promise<number>[] = [];
sites.forEach((site) => { sites.forEach((site) => {
promises.push(db.insertRecord(newTable, site)); promises.push(db.insertRecord(newTable, site));
@ -153,12 +153,12 @@ export class CoreSitesProvider {
protected readonly VALID_VERSION = 1; protected readonly VALID_VERSION = 1;
protected readonly INVALID_VERSION = -1; protected readonly INVALID_VERSION = -1;
protected isWPApp: boolean; protected isWPApp = false;
protected logger: CoreLogger; protected logger: CoreLogger;
protected services = {}; protected services = {};
protected sessionRestored = false; protected sessionRestored = false;
protected currentSite: CoreSite; protected currentSite?: CoreSite;
protected sites: { [s: string]: CoreSite } = {}; protected sites: { [s: string]: CoreSite } = {};
protected appDB: SQLiteDB; protected appDB: SQLiteDB;
protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized. protected dbReady: Promise<void>; // Promise resolved when the app DB is initialized.
@ -249,7 +249,8 @@ export class CoreSitesProvider {
await db.execute( await db.execute(
'INSERT INTO ' + newTable + ' ' + 'INSERT INTO ' + newTable + ' ' +
'SELECT id, data, key, expirationTime, NULL as component, NULL as componentId ' + 'SELECT id, data, key, expirationTime, NULL as component, NULL as componentId ' +
'FROM ' + oldTable); 'FROM ' + oldTable,
);
try { try {
await db.dropTable(oldTable); await db.dropTable(oldTable);
@ -276,7 +277,7 @@ export class CoreSitesProvider {
* @param name Name of the site to check. * @param name Name of the site to check.
* @return Site data if it's a demo site, undefined otherwise. * @return Site data if it's a demo site, undefined otherwise.
*/ */
getDemoSiteData(name: string): CoreSitesDemoSiteData { getDemoSiteData(name: string): CoreSitesDemoSiteData | undefined {
const demoSites = CoreConfigConstants.demo_sites; const demoSites = CoreConfigConstants.demo_sites;
name = name.toLowerCase(); name = name.toLowerCase();
@ -293,39 +294,43 @@ export class CoreSitesProvider {
* @param protocol Protocol to use first. * @param protocol Protocol to use first.
* @return A promise resolved when the site is checked. * @return A promise resolved when the site is checked.
*/ */
checkSite(siteUrl: string, protocol: string = 'https://'): Promise<CoreSiteCheckResponse> { async checkSite(siteUrl: string, protocol: string = 'https://'): Promise<CoreSiteCheckResponse> {
// The formatURL function adds the protocol if is missing. // The formatURL function adds the protocol if is missing.
siteUrl = CoreUrlUtils.instance.formatURL(siteUrl); siteUrl = CoreUrlUtils.instance.formatURL(siteUrl);
if (!CoreUrlUtils.instance.isHttpURL(siteUrl)) { if (!CoreUrlUtils.instance.isHttpURL(siteUrl)) {
return Promise.reject(new CoreError(Translate.instance.instant('core.login.invalidsite'))); throw new CoreError(Translate.instance.instant('core.login.invalidsite'));
} else if (!CoreApp.instance.isOnline()) { } else if (!CoreApp.instance.isOnline()) {
return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
} else { }
return this.checkSiteWithProtocol(siteUrl, protocol).catch((error: CoreSiteError) => {
try {
return await this.checkSiteWithProtocol(siteUrl, protocol);
} catch (error) {
// Do not continue checking if a critical error happened. // Do not continue checking if a critical error happened.
if (error.critical) { if (error.critical) {
return Promise.reject(error); throw error;
} }
// Retry with the other protocol. // Retry with the other protocol.
protocol = protocol == 'https://' ? 'http://' : 'https://'; protocol = protocol == 'https://' ? 'http://' : 'https://';
return this.checkSiteWithProtocol(siteUrl, protocol).catch((secondError: CoreSiteError) => { try {
return await this.checkSiteWithProtocol(siteUrl, protocol);
} catch (secondError) {
if (secondError.critical) { if (secondError.critical) {
return Promise.reject(secondError); throw secondError;
} }
// Site doesn't exist. Return the error message. // Site doesn't exist. Return the error message.
if (CoreTextUtils.instance.getErrorMessageFromError(error)) { if (CoreTextUtils.instance.getErrorMessageFromError(error)) {
return Promise.reject(error); throw error;
} else if (CoreTextUtils.instance.getErrorMessageFromError(secondError)) { } else if (CoreTextUtils.instance.getErrorMessageFromError(secondError)) {
return Promise.reject(secondError); throw secondError;
} else { } else {
return Translate.instance.instant('core.cannotconnecttrouble'); throw new CoreError(Translate.instance.instant('core.cannotconnecttrouble'));
}
} }
});
});
} }
} }
@ -336,82 +341,96 @@ export class CoreSitesProvider {
* @param protocol Protocol to use. * @param protocol Protocol to use.
* @return A promise resolved when the site is checked. * @return A promise resolved when the site is checked.
*/ */
checkSiteWithProtocol(siteUrl: string, protocol: string): Promise<CoreSiteCheckResponse> { async checkSiteWithProtocol(siteUrl: string, protocol: string): Promise<CoreSiteCheckResponse> {
let publicConfig: CoreSitePublicConfigResponse; let publicConfig: CoreSitePublicConfigResponse | undefined;
// Now, replace the siteUrl with the protocol. // Now, replace the siteUrl with the protocol.
siteUrl = siteUrl.replace(/^https?:\/\//i, protocol); siteUrl = siteUrl.replace(/^https?:\/\//i, protocol);
return this.siteExists(siteUrl).catch((error: CoreSiteError) => { try {
await this.siteExists(siteUrl);
} catch (error) {
// Do not continue checking if WS are not enabled. // Do not continue checking if WS are not enabled.
if (error.errorcode == 'enablewsdescription') { if (error.errorcode == 'enablewsdescription') {
error.critical = true; error.critical = true;
return Promise.reject(error); throw error;
} }
// Site doesn't exist. Try to add or remove 'www'. // Site doesn't exist. Try to add or remove 'www'.
const treatedUrl = CoreUrlUtils.instance.addOrRemoveWWW(siteUrl); const treatedUrl = CoreUrlUtils.instance.addOrRemoveWWW(siteUrl);
return this.siteExists(treatedUrl).then(() => { try {
await this.siteExists(treatedUrl);
// Success, use this new URL as site url. // Success, use this new URL as site url.
siteUrl = treatedUrl; siteUrl = treatedUrl;
}).catch((secondError: CoreSiteError) => { } catch (secondError) {
// Do not continue checking if WS are not enabled. // Do not continue checking if WS are not enabled.
if (secondError.errorcode == 'enablewsdescription') { if (secondError.errorcode == 'enablewsdescription') {
secondError.critical = true; secondError.critical = true;
return Promise.reject(secondError); throw secondError;
} }
// Return the error. // Return the error.
if (CoreTextUtils.instance.getErrorMessageFromError(error)) { if (CoreTextUtils.instance.getErrorMessageFromError(error)) {
return Promise.reject(error); throw error;
} else { } else {
return Promise.reject(secondError); throw secondError;
}
}
}
// Site exists. Create a temporary site to check if local_mobile is installed.
const temporarySite = new CoreSite(undefined, siteUrl);
let data: LocalMobileResponse;
try {
data = await temporarySite.checkLocalMobilePlugin();
} catch (error) {
// Local mobile check returned an error. This only happens if the plugin is installed and it returns an error.
throw new CoreSiteError({
message: error.message,
critical: true,
});
} }
});
}).then(() => {
// Create a temporary site to check if local_mobile is installed.
const temporarySite = new CoreSite(undefined, siteUrl);
return temporarySite.checkLocalMobilePlugin().then((data) => {
data.service = data.service || CoreConfigConstants.wsservice; data.service = data.service || CoreConfigConstants.wsservice;
this.services[siteUrl] = data.service; // No need to store it in DB. this.services[siteUrl] = data.service; // No need to store it in DB.
if (data.coreSupported || if (data.coreSupported || (data.code != CoreConstants.LOGIN_SSO_CODE && data.code != CoreConstants.LOGIN_SSO_INAPP_CODE)) {
(data.code != CoreConstants.LOGIN_SSO_CODE && data.code != CoreConstants.LOGIN_SSO_INAPP_CODE)) {
// SSO using local_mobile not needed, try to get the site public config. // SSO using local_mobile not needed, try to get the site public config.
return temporarySite.getPublicConfig().then((config) => { try {
const config = await temporarySite.getPublicConfig();
publicConfig = config; publicConfig = config;
// Check that the user can authenticate. // Check that the user can authenticate.
if (!config.enablewebservices) { if (!config.enablewebservices) {
return Promise.reject(new CoreSiteError({ throw new CoreSiteError({
message: Translate.instance.instant('core.login.webservicesnotenabled'), message: Translate.instance.instant('core.login.webservicesnotenabled'),
})); });
} else if (!config.enablemobilewebservice) { } else if (!config.enablemobilewebservice) {
return Promise.reject(new CoreSiteError({ throw new CoreSiteError({
message: Translate.instance.instant('core.login.mobileservicesnotenabled'), message: Translate.instance.instant('core.login.mobileservicesnotenabled'),
})); });
} else if (config.maintenanceenabled) { } else if (config.maintenanceenabled) {
let message = Translate.instance.instant('core.sitemaintenance'); let message = Translate.instance.instant('core.sitemaintenance');
if (config.maintenancemessage) { if (config.maintenancemessage) {
message += config.maintenancemessage; message += config.maintenancemessage;
} }
return Promise.reject(new CoreSiteError({ throw new CoreSiteError({
message, message,
})); });
} }
// Everything ok. // Everything ok.
if (data.code === 0) { if (data.code === 0) {
data.code = config.typeoflogin; data.code = config.typeoflogin;
} }
} catch (error) {
return data;
}, async (error) => {
// Error, check if not supported. // Error, check if not supported.
if (error.available === 1) { if (error.available === 1) {
// Service supported but an error happened. Return error. // Service supported but an error happened. Return error.
@ -427,30 +446,18 @@ export class CoreSitesProvider {
} }
} }
return Promise.reject(new CoreSiteError({ throw new CoreSiteError({
message: error.error, message: error.error,
errorcode: error.errorcode, errorcode: error.errorcode,
critical: true, critical: true,
}));
}
return data;
}); });
} }
}
}
return data;
}, (error: CoreError) =>
// Local mobile check returned an error. This only happens if the plugin is installed and it returns an error.
Promise.reject(new CoreSiteError({
message: error.message,
critical: true,
})),
).then((data: LocalMobileResponse) => {
siteUrl = temporarySite.getURL(); siteUrl = temporarySite.getURL();
return { siteUrl, code: data.code, warning: data.warning, service: data.service, config: publicConfig }; return { siteUrl, code: data.code, warning: data.warning, service: data.service, config: publicConfig };
});
});
} }
/** /**
@ -482,7 +489,7 @@ export class CoreSitesProvider {
if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) { if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) {
throw new CoreSiteError({ throw new CoreSiteError({
errorcode: data.errorcode, errorcode: data.errorcode,
message: data.error, message: data.error!,
}); });
} }
@ -506,10 +513,15 @@ export class CoreSitesProvider {
* @param retry Whether we are retrying with a prefixed URL. * @param retry Whether we are retrying with a prefixed URL.
* @return A promise resolved when the token is retrieved. * @return A promise resolved when the token is retrieved.
*/ */
getUserToken(siteUrl: string, username: string, password: string, service?: string, retry?: boolean): async getUserToken(
Promise<CoreSiteUserTokenResponse> { siteUrl: string,
username: string,
password: string,
service?: string,
retry?: boolean,
): Promise<CoreSiteUserTokenResponse> {
if (!CoreApp.instance.isOnline()) { if (!CoreApp.instance.isOnline()) {
return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
} }
if (!service) { if (!service) {
@ -522,11 +534,16 @@ export class CoreSitesProvider {
service, service,
}; };
const loginUrl = siteUrl + '/login/token.php'; const loginUrl = siteUrl + '/login/token.php';
const promise = Http.instance.post(loginUrl, params).pipe(timeout(CoreWS.instance.getRequestTimeout())).toPromise(); let data: CoreSitesLoginTokenResponse;
try {
data = await Http.instance.post(loginUrl, params).pipe(timeout(CoreWS.instance.getRequestTimeout())).toPromise();
} catch (error) {
throw new CoreError(Translate.instance.instant('core.cannotconnecttrouble'));
}
return promise.then((data: CoreSitesLoginTokenResponse) => {
if (typeof data == 'undefined') { if (typeof data == 'undefined') {
return Promise.reject(new CoreError(Translate.instance.instant('core.cannotconnecttrouble'))); throw new CoreError(Translate.instance.instant('core.cannotconnecttrouble'));
} else { } else {
if (typeof data.token != 'undefined') { if (typeof data.token != 'undefined') {
return { token: data.token, siteUrl, privateToken: data.privatetoken }; return { token: data.token, siteUrl, privateToken: data.privatetoken };
@ -539,31 +556,25 @@ export class CoreSitesProvider {
return this.getUserToken(siteUrl, username, password, service, true); return this.getUserToken(siteUrl, username, password, service, true);
} else if (data.errorcode == 'missingparam') { } else if (data.errorcode == 'missingparam') {
// It seems the server didn't receive all required params, it could be due to a redirect. // It seems the server didn't receive all required params, it could be due to a redirect.
return CoreUtils.instance.checkRedirect(loginUrl).then((redirect) => { const redirect = await CoreUtils.instance.checkRedirect(loginUrl);
if (redirect) { if (redirect) {
return Promise.reject(new CoreSiteError({ throw new CoreSiteError({
message: Translate.instance.instant('core.login.sitehasredirect'), message: Translate.instance.instant('core.login.sitehasredirect'),
}));
} else {
return Promise.reject(new CoreSiteError({
message: data.error,
errorcode: data.errorcode,
}));
}
}); });
} else { }
return Promise.reject(new CoreSiteError({ }
throw new CoreSiteError({
message: data.error, message: data.error,
errorcode: data.errorcode, errorcode: data.errorcode,
})); });
} }
} else {
return Promise.reject(new CoreError(Translate.instance.instant('core.login.invalidaccount'))); throw new CoreError(Translate.instance.instant('core.login.invalidaccount'));
} }
} }
} }
}, () => Promise.reject(new CoreError(Translate.instance.instant('core.cannotconnecttrouble'))));
}
/** /**
* Add a new site to the site list and authenticate the user in this site. * Add a new site to the site list and authenticate the user in this site.
@ -575,7 +586,13 @@ export class CoreSitesProvider {
* @param oauthId OAuth ID. Only if the authentication was using an OAuth method. * @param oauthId OAuth ID. Only if the authentication was using an OAuth method.
* @return A promise resolved with siteId when the site is added and the user is authenticated. * @return A promise resolved with siteId when the site is added and the user is authenticated.
*/ */
newSite(siteUrl: string, token: string, privateToken: string = '', login: boolean = true, oauthId?: number): Promise<string> { async newSite(
siteUrl: string,
token: string,
privateToken: string = '',
login: boolean = true,
oauthId?: number,
): Promise<string> {
if (typeof login != 'boolean') { if (typeof login != 'boolean') {
login = true; login = true;
} }
@ -584,15 +601,19 @@ export class CoreSitesProvider {
let candidateSite = new CoreSite(undefined, siteUrl, token, undefined, privateToken, undefined, undefined); let candidateSite = new CoreSite(undefined, siteUrl, token, undefined, privateToken, undefined, undefined);
let isNewSite = true; let isNewSite = true;
return candidateSite.fetchSiteInfo().then((info) => { try {
const info = await candidateSite.fetchSiteInfo();
const result = this.isValidMoodleVersion(info); const result = this.isValidMoodleVersion(info);
if (result == this.VALID_VERSION) { if (result != this.VALID_VERSION) {
return this.treatInvalidAppVersion(result, siteUrl);
}
const siteId = this.createSiteID(info.siteurl, info.username); const siteId = this.createSiteID(info.siteurl, info.username);
// Check if the site already exists. // Check if the site already exists.
return this.getSite(siteId).catch(() => { const site = await CoreUtils.instance.ignoreErrors<CoreSite>(this.getSite(siteId));
// Not exists.
}).then((site) => {
if (site) { if (site) {
// Site already exists, update its data and use it. // Site already exists, update its data and use it.
isNewSite = false; isNewSite = false;
@ -610,17 +631,21 @@ export class CoreSitesProvider {
candidateSite.setOAuthId(oauthId); candidateSite.setOAuthId(oauthId);
// Create database tables before login and before any WS call. // Create database tables before login and before any WS call.
return this.migrateSiteSchemas(candidateSite); await this.migrateSiteSchemas(candidateSite);
} }
}).then(() =>
// Try to get the site config. // Try to get the site config.
this.getSiteConfig(candidateSite).catch((error) => { let config: CoreSiteConfig | undefined;
try {
config = await this.getSiteConfig(candidateSite);
} catch (error) {
// Ignore errors if it's not a new site, we'll use the config already stored. // Ignore errors if it's not a new site, we'll use the config already stored.
if (isNewSite) { if (isNewSite) {
return Promise.reject(error); throw error;
} }
}).then((config) => { }
if (typeof config != 'undefined') { if (typeof config != 'undefined') {
candidateSite.setConfig(config); candidateSite.setConfig(config);
} }
@ -639,19 +664,14 @@ export class CoreSitesProvider {
CoreEvents.instance.trigger(CoreEventsProvider.SITE_ADDED, info, siteId); CoreEvents.instance.trigger(CoreEventsProvider.SITE_ADDED, info, siteId);
return siteId; return siteId;
}), } catch (error) {
);
}
return this.treatInvalidAppVersion(result, siteUrl);
}).catch((error) => {
// Error invaliddevice is returned by Workplace server meaning the same as connecttoworkplaceapp. // Error invaliddevice is returned by Workplace server meaning the same as connecttoworkplaceapp.
if (error && error.errorcode == 'invaliddevice') { if (error && error.errorcode == 'invaliddevice') {
return this.treatInvalidAppVersion(this.WORKPLACE_APP, siteUrl); return this.treatInvalidAppVersion(this.WORKPLACE_APP, siteUrl);
} }
return Promise.reject(error); throw error;
}); }
} }
/** /**
@ -663,8 +683,8 @@ export class CoreSitesProvider {
* @return A promise rejected with the error info. * @return A promise rejected with the error info.
*/ */
protected async treatInvalidAppVersion(result: number, siteUrl: string, siteId?: string): Promise<never> { protected async treatInvalidAppVersion(result: number, siteUrl: string, siteId?: string): Promise<never> {
let errorCode; let errorCode: string | undefined;
let errorKey; let errorKey: string | undefined;
let translateParams; let translateParams;
switch (result) { switch (result) {
@ -816,8 +836,15 @@ export class CoreSitesProvider {
* @param oauthId OAuth ID. Only if the authentication was using an OAuth method. * @param oauthId OAuth ID. Only if the authentication was using an OAuth method.
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
async addSite(id: string, siteUrl: string, token: string, info: CoreSiteInfoResponse, privateToken: string = '', async addSite(
config?: CoreSiteConfig, oauthId?: number): Promise<void> { id: string,
siteUrl: string,
token: string,
info: CoreSiteInfoResponse,
privateToken: string = '',
config?: CoreSiteConfig,
oauthId?: number,
): Promise<void> {
await this.dbReady; await this.dbReady;
const entry = { const entry = {
@ -850,34 +877,43 @@ export class CoreSitesProvider {
* @param siteId ID of the site to check. Current site id will be used otherwise. * @param siteId ID of the site to check. Current site id will be used otherwise.
* @return Resolved with if meets the requirements, rejected otherwise. * @return Resolved with if meets the requirements, rejected otherwise.
*/ */
async checkRequiredMinimumVersion(config: CoreSitePublicConfigResponse, siteId?: string): Promise<void> { async checkRequiredMinimumVersion(config?: CoreSitePublicConfigResponse, siteId?: string): Promise<void> {
if (config && config.tool_mobile_minimumversion) { if (!config || !config.tool_mobile_minimumversion) {
return;
}
const requiredVersion = this.convertVersionName(config.tool_mobile_minimumversion); const requiredVersion = this.convertVersionName(config.tool_mobile_minimumversion);
const appVersion = this.convertVersionName(CoreConfigConstants.versionname); const appVersion = this.convertVersionName(CoreConfigConstants.versionname);
if (requiredVersion > appVersion) { if (requiredVersion > appVersion) {
const storesConfig: CoreStoreConfig = { const storesConfig: CoreStoreConfig = {
android: config.tool_mobile_androidappid || null, android: config.tool_mobile_androidappid,
ios: config.tool_mobile_iosappid || null, ios: config.tool_mobile_iosappid,
desktop: config.tool_mobile_setuplink || 'https://download.moodle.org/desktop/', desktop: config.tool_mobile_setuplink || 'https://download.moodle.org/desktop/',
mobile: config.tool_mobile_setuplink || 'https://download.moodle.org/mobile/', mobile: config.tool_mobile_setuplink || 'https://download.moodle.org/mobile/',
default: config.tool_mobile_setuplink, default: config.tool_mobile_setuplink,
}; };
const downloadUrl = CoreApp.instance.getAppStoreUrl(storesConfig);
siteId = siteId || this.getCurrentSiteId(); siteId = siteId || this.getCurrentSiteId();
const downloadUrl = CoreApp.instance.getAppStoreUrl(storesConfig);
if (downloadUrl != null) {
// Do not block interface. // Do not block interface.
CoreDomUtils.instance.showConfirm( CoreDomUtils.instance.showConfirm(
Translate.instance.instant('core.updaterequireddesc', { $a: config.tool_mobile_minimumversion }), Translate.instance.instant('core.updaterequireddesc', { $a: config.tool_mobile_minimumversion }),
Translate.instance.instant('core.updaterequired'), Translate.instance.instant('core.updaterequired'),
Translate.instance.instant('core.download'), Translate.instance.instant('core.download'),
Translate.instance.instant(siteId ? 'core.mainmenu.logout' : 'core.cancel')).then(() => { Translate.instance.instant(siteId ? 'core.mainmenu.logout' : 'core.cancel'),
CoreUtils.instance.openInBrowser(downloadUrl); ).then(() => CoreUtils.instance.openInBrowser(downloadUrl)).catch(() => {
}).catch(() => {
// Do nothing. // Do nothing.
}); });
} else {
CoreDomUtils.instance.showAlert(
Translate.instance.instant('core.updaterequired'),
Translate.instance.instant('core.updaterequireddesc', { $a: config.tool_mobile_minimumversion }),
);
}
if (siteId) { if (siteId) {
// Logout if it's the currentSite. // Logout if it's the currentSite.
@ -892,7 +928,6 @@ export class CoreSitesProvider {
throw new CoreError('Current app version is lower than required version.'); throw new CoreError('Current app version is lower than required version.');
} }
} }
}
/** /**
* Convert version name to numbers. * Convert version name to numbers.
@ -952,7 +987,7 @@ export class CoreSitesProvider {
return false; return false;
} catch (error) { } catch (error) {
let config: CoreSitePublicConfigResponse; let config: CoreSitePublicConfigResponse | undefined;
try { try {
config = await site.getPublicConfig(); config = await site.getPublicConfig();
@ -979,7 +1014,7 @@ export class CoreSitesProvider {
* *
* @return Current site. * @return Current site.
*/ */
getCurrentSite(): CoreSite { getCurrentSite(): CoreSite | undefined {
return this.currentSite; return this.currentSite;
} }
@ -1015,11 +1050,7 @@ export class CoreSitesProvider {
* @return Current site User ID. * @return Current site User ID.
*/ */
getCurrentSiteUserId(): number { getCurrentSiteUserId(): number {
if (this.currentSite) { return this.currentSite?.getUserId() || 0;
return this.currentSite.getUserId();
} else {
return 0;
}
} }
/** /**
@ -1150,7 +1181,7 @@ export class CoreSitesProvider {
* @param siteId The site ID. If not defined, current site (if available). * @param siteId The site ID. If not defined, current site (if available).
* @return Promise resolved with the database. * @return Promise resolved with the database.
*/ */
getSiteDb(siteId: string): Promise<SQLiteDB> { getSiteDb(siteId?: string): Promise<SQLiteDB> {
return this.getSite(siteId).then((site) => site.getDb()); return this.getSite(siteId).then((site) => site.getDb());
} }
@ -1175,7 +1206,7 @@ export class CoreSitesProvider {
const sites = await this.appDB.getAllRecords<SiteDBEntry>(SITES_TABLE); const sites = await this.appDB.getAllRecords<SiteDBEntry>(SITES_TABLE);
const formattedSites = []; const formattedSites: CoreSiteBasicInfo[] = [];
sites.forEach((site) => { sites.forEach((site) => {
if (!ids || ids.indexOf(site.id) > -1) { if (!ids || ids.indexOf(site.id) > -1) {
// Parse info. // Parse info.
@ -1184,7 +1215,7 @@ export class CoreSitesProvider {
id: site.id, id: site.id,
siteUrl: site.siteUrl, siteUrl: site.siteUrl,
fullName: siteInfo?.fullname, fullName: siteInfo?.fullname,
siteName: CoreConfigConstants.sitename ? CoreConfigConstants.sitename : siteInfo?.sitename, siteName: CoreConfigConstants.sitename ?? siteInfo?.sitename,
avatar: siteInfo?.userpictureurl, avatar: siteInfo?.userpictureurl,
siteHomeId: siteInfo?.siteid || 1, siteHomeId: siteInfo?.siteid || 1,
}; };
@ -1206,19 +1237,23 @@ export class CoreSitesProvider {
// Sort sites by url and ful lname. // Sort sites by url and ful lname.
sites.sort((a, b) => { sites.sort((a, b) => {
// First compare by site url without the protocol. // First compare by site url without the protocol.
let compareA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); const urlA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
let compareB = b.siteUrl.replace(/^https?:\/\//, '').toLowerCase(); const urlB = b.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
const compare = compareA.localeCompare(compareB); const compare = urlA.localeCompare(urlB);
if (compare !== 0) { if (compare !== 0) {
return compare; return compare;
} }
// If site url is the same, use fullname instead. // If site url is the same, use fullname instead.
compareA = a.fullName.toLowerCase().trim(); const fullNameA = a.fullName?.toLowerCase().trim();
compareB = b.fullName.toLowerCase().trim(); const fullNameB = b.fullName?.toLowerCase().trim();
return compareA.localeCompare(compareB); if (!fullNameA || !fullNameB) {
return 0;
}
return fullNameA.localeCompare(fullNameB);
}); });
return sites; return sites;
@ -1279,10 +1314,10 @@ export class CoreSitesProvider {
await this.dbReady; await this.dbReady;
let siteId; let siteId;
const promises = []; const promises: Promise<unknown>[] = [];
if (this.currentSite) { if (this.currentSite) {
const siteConfig = <CoreSiteConfig> this.currentSite.getStoredConfig(); const siteConfig = this.currentSite.getStoredConfig();
siteId = this.currentSite.getId(); siteId = this.currentSite.getId();
this.currentSite = undefined; this.currentSite = undefined;
@ -1418,7 +1453,7 @@ export class CoreSitesProvider {
} }
// Try to get the site config. // Try to get the site config.
let config; let config: CoreSiteConfig | undefined;
try { try {
config = await this.getSiteConfig(site); config = await this.getSiteConfig(site);
@ -1426,10 +1461,9 @@ export class CoreSitesProvider {
// Error getting config, keep the current one. // Error getting config, keep the current one.
} }
const newValues = { const newValues: Record<string, string | number> = {
info: JSON.stringify(info), info: JSON.stringify(info),
loggedOut: site.isLoggedOut() ? 1 : 0, loggedOut: site.isLoggedOut() ? 1 : 0,
config: undefined,
}; };
if (typeof config != 'undefined') { if (typeof config != 'undefined') {
@ -1475,7 +1509,7 @@ export class CoreSitesProvider {
// If prioritize is true, check current site first. // If prioritize is true, check current site first.
if (prioritize && this.currentSite && this.currentSite.containsUrl(url)) { if (prioritize && this.currentSite && this.currentSite.containsUrl(url)) {
if (!username || this.currentSite.getInfo().username == username) { if (!username || this.currentSite?.getInfo()?.username == username) {
return [this.currentSite.getId()]; return [this.currentSite.getId()];
} }
} }
@ -1498,8 +1532,8 @@ export class CoreSitesProvider {
try { try {
const siteEntries = await this.appDB.getAllRecords<SiteDBEntry>(SITES_TABLE); const siteEntries = await this.appDB.getAllRecords<SiteDBEntry>(SITES_TABLE);
const ids = []; const ids: string[] = [];
const promises = []; const promises: Promise<unknown>[] = [];
siteEntries.forEach((site) => { siteEntries.forEach((site) => {
if (!this.sites[site.id]) { if (!this.sites[site.id]) {
@ -1507,7 +1541,7 @@ export class CoreSitesProvider {
} }
if (this.sites[site.id].containsUrl(url)) { if (this.sites[site.id].containsUrl(url)) {
if (!username || this.sites[site.id].getInfo().username == username) { if (!username || this.sites[site.id].getInfo()?.username == username) {
ids.push(site.id); ids.push(site.id);
} }
} }
@ -1553,15 +1587,13 @@ export class CoreSitesProvider {
* @param site The site to get the config. * @param site The site to get the config.
* @return Promise resolved with config if available. * @return Promise resolved with config if available.
*/ */
protected async getSiteConfig(site: CoreSite): Promise<CoreSiteConfig> { protected async getSiteConfig(site: CoreSite): Promise<CoreSiteConfig | undefined> {
if (!site.wsAvailable('tool_mobile_get_config')) { if (!site.wsAvailable('tool_mobile_get_config')) {
// WS not available, cannot get config. // WS not available, cannot get config.
return; return;
} }
const config = <CoreSiteConfig> await site.getConfig(undefined, true); return await site.getConfig(undefined, true);
return config;
} }
/** /**
@ -1611,7 +1643,7 @@ export class CoreSitesProvider {
wsAvailableInCurrentSite(method: string, checkPrefix: boolean = true): boolean { wsAvailableInCurrentSite(method: string, checkPrefix: boolean = true): boolean {
const site = this.getCurrentSite(); const site = this.getCurrentSite();
return site && site.wsAvailable(method, checkPrefix); return site ? site.wsAvailable(method, checkPrefix) : false;
} }
/** /**
@ -1659,6 +1691,10 @@ export class CoreSitesProvider {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
migrateSiteSchemas(site: CoreSite): Promise<void> { migrateSiteSchemas(site: CoreSite): Promise<void> {
if (!site.id) {
return Promise.resolve();
}
if (this.siteSchemasMigration[site.id]) { if (this.siteSchemasMigration[site.id]) {
return this.siteSchemasMigration[site.id]; return this.siteSchemasMigration[site.id];
} }
@ -1672,7 +1708,7 @@ export class CoreSitesProvider {
this.siteSchemasMigration[site.id] = promise; this.siteSchemasMigration[site.id] = promise;
return promise.finally(() => { return promise.finally(() => {
delete this.siteSchemasMigration[site.id]; delete this.siteSchemasMigration[site.id!];
}); });
} }
@ -1694,7 +1730,7 @@ export class CoreSitesProvider {
versions[record.name] = record.version; versions[record.name] = record.version;
}); });
const promises = []; const promises: Promise<void>[] = [];
for (const name in schemas) { for (const name in schemas) {
const schema = schemas[name]; const schema = schemas[name];
const oldVersion = versions[name] || 0; const oldVersion = versions[name] || 0;
@ -1720,6 +1756,10 @@ export class CoreSitesProvider {
* @return Promise resolved when done. * @return Promise resolved when done.
*/ */
protected async applySiteSchema(site: CoreSite, schema: CoreRegisteredSiteSchema, oldVersion: number): Promise<void> { protected async applySiteSchema(site: CoreSite, schema: CoreRegisteredSiteSchema, oldVersion: number): Promise<void> {
if (!site.id) {
return;
}
const db = site.getDb(); const db = site.getDb();
if (schema.tables) { if (schema.tables) {
@ -1741,19 +1781,24 @@ export class CoreSitesProvider {
* @return Promise resolved with site to use and the list of sites that have * @return Promise resolved with site to use and the list of sites that have
* the URL. Site will be undefined if it isn't the root URL of any stored site. * the URL. Site will be undefined if it isn't the root URL of any stored site.
*/ */
isStoredRootURL(url: string, username?: string): Promise<{site: CoreSite; siteIds: string[]}> { async isStoredRootURL(url: string, username?: string): Promise<{site?: CoreSite; siteIds: string[]}> {
// Check if the site is stored. // Check if the site is stored.
return this.getSiteIdsFromUrl(url, true, username).then((siteIds) => { const siteIds = await this.getSiteIdsFromUrl(url, true, username);
const result = {
const result: {site?: CoreSite; siteIds: string[]} = {
siteIds, siteIds,
site: undefined,
}; };
if (siteIds.length > 0) { if (!siteIds.length) {
return result;
}
// If more than one site is returned it usually means there are different users stored. Use any of them. // If more than one site is returned it usually means there are different users stored. Use any of them.
return this.getSite(siteIds[0]).then((site) => { const site = await this.getSite(siteIds[0]);
const siteUrl = CoreTextUtils.instance.removeEndingSlash( const siteUrl = CoreTextUtils.instance.removeEndingSlash(
CoreUrlUtils.instance.removeProtocolAndWWW(site.getURL())); CoreUrlUtils.instance.removeProtocolAndWWW(site.getURL()),
);
const treatedUrl = CoreTextUtils.instance.removeEndingSlash(CoreUrlUtils.instance.removeProtocolAndWWW(url)); const treatedUrl = CoreTextUtils.instance.removeEndingSlash(CoreUrlUtils.instance.removeProtocolAndWWW(url));
if (siteUrl == treatedUrl) { if (siteUrl == treatedUrl) {
@ -1761,11 +1806,6 @@ export class CoreSitesProvider {
} }
return result; return result;
});
}
return result;
});
} }
/** /**
@ -1775,12 +1815,12 @@ export class CoreSitesProvider {
* @return Name of the site schemas. * @return Name of the site schemas.
*/ */
getSiteTableSchemasToClear(site: CoreSite): string[] { getSiteTableSchemasToClear(site: CoreSite): string[] {
let reset = []; let reset: string[] = [];
for (const name in this.siteSchemas) { for (const name in this.siteSchemas) {
const schema = this.siteSchemas[name]; const schema = this.siteSchemas[name];
if (schema.canBeCleared && (!schema.siteId || site.getId() == schema.siteId)) { if (schema.canBeCleared && (!schema.siteId || site.getId() == schema.siteId)) {
reset = reset.concat(this.siteSchemas[name].canBeCleared); reset = reset.concat(schema.canBeCleared);
} }
} }
@ -1900,17 +1940,17 @@ export type CoreSiteBasicInfo = {
/** /**
* User's full name. * User's full name.
*/ */
fullName: string; fullName?: string;
/** /**
* Site's name. * Site's name.
*/ */
siteName: string; siteName?: string;
/** /**
* User's avatar. * User's avatar.
*/ */
avatar: string; avatar?: string;
/** /**
* Badge to display in the site. * Badge to display in the site.

View File

@ -30,6 +30,7 @@ import { CoreConstants } from '@core/constants';
import { CoreIonLoadingElement } from '@classes/ion-loading'; import { CoreIonLoadingElement } from '@classes/ion-loading';
import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreCanceledError } from '@classes/errors/cancelederror';
import { CoreError } from '@classes/errors/error'; import { CoreError } from '@classes/errors/error';
import { CoreSilentError } from '@classes/errors/silenterror';
import { makeSingleton, Translate, AlertController, LoadingController, ToastController } from '@singletons/core.singletons'; import { makeSingleton, Translate, AlertController, LoadingController, ToastController } from '@singletons/core.singletons';
import { CoreLogger } from '@singletons/logger'; import { CoreLogger } from '@singletons/logger';
@ -40,14 +41,15 @@ import { CoreLogger } from '@singletons/logger';
@Injectable() @Injectable()
export class CoreDomUtilsProvider { export class CoreDomUtilsProvider {
protected readonly INSTANCE_ID_ATTR_NAME = 'core-instance-id';
// List of input types that support keyboard. // List of input types that support keyboard.
protected readonly INPUT_SUPPORT_KEYBOARD: string[] = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number', protected readonly INPUT_SUPPORT_KEYBOARD: string[] = ['date', 'datetime', 'datetime-local', 'email', 'month', 'number',
'password', 'search', 'tel', 'text', 'time', 'url', 'week']; 'password', 'search', 'tel', 'text', 'time', 'url', 'week'];
protected readonly INSTANCE_ID_ATTR_NAME: string = 'core-instance-id';
protected template: HTMLTemplateElement = document.createElement('template'); // A template element to convert HTML to element. protected template: HTMLTemplateElement = document.createElement('template'); // A template element to convert HTML to element.
protected matchesFn: string; // Name of the "matches" function to use when simulating a closest call. protected matchesFunctionName?: string; // Name of the "matches" function to use when simulating a closest call.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
protected instances: {[id: string]: any} = {}; // Store component/directive instances by id. protected instances: {[id: string]: any} = {}; // Store component/directive instances by id.
protected lastInstanceId = 0; protected lastInstanceId = 0;
@ -58,10 +60,17 @@ export class CoreDomUtilsProvider {
constructor(protected domSanitizer: DomSanitizer) { constructor(protected domSanitizer: DomSanitizer) {
this.logger = CoreLogger.getInstance('CoreDomUtilsProvider'); this.logger = CoreLogger.getInstance('CoreDomUtilsProvider');
this.init();
}
/**
* Init some properties.
*/
protected async init(): Promise<void> {
// Check if debug messages should be displayed. // Check if debug messages should be displayed.
CoreConfig.instance.get(CoreConstants.SETTINGS_DEBUG_DISPLAY, false).then((debugDisplay) => { const debugDisplay = await CoreConfig.instance.get<number>(CoreConstants.SETTINGS_DEBUG_DISPLAY, 0);
this.debugDisplay = !!debugDisplay;
}); this.debugDisplay = debugDisplay != 0;
} }
/** /**
@ -73,17 +82,21 @@ export class CoreDomUtilsProvider {
* @param selector Selector to search. * @param selector Selector to search.
* @return Closest ancestor. * @return Closest ancestor.
*/ */
closest(element: Element, selector: string): Element { closest(element: Element | undefined | null, selector: string): Element | null {
if (!element) {
return null;
}
// Try to use closest if the browser supports it. // Try to use closest if the browser supports it.
if (typeof element.closest == 'function') { if (typeof element.closest == 'function') {
return element.closest(selector); return element.closest(selector);
} }
if (!this.matchesFn) { if (!this.matchesFunctionName) {
// Find the matches function supported by the browser. // Find the matches function supported by the browser.
['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector', 'oMatchesSelector'].some((fn) => { ['matches', 'webkitMatchesSelector', 'mozMatchesSelector', 'msMatchesSelector', 'oMatchesSelector'].some((fn) => {
if (typeof document.body[fn] == 'function') { if (typeof document.body[fn] == 'function') {
this.matchesFn = fn; this.matchesFunctionName = fn;
return true; return true;
} }
@ -91,18 +104,22 @@ export class CoreDomUtilsProvider {
return false; return false;
}); });
if (!this.matchesFn) { if (!this.matchesFunctionName) {
return; return null;
} }
} }
// Traverse parents. // Traverse parents.
while (element) { let elementToTreat: Element | null = element;
if (element[this.matchesFn](selector)) {
return element; while (elementToTreat) {
if (elementToTreat[this.matchesFunctionName](selector)) {
return elementToTreat;
} }
element = element.parentElement; elementToTreat = elementToTreat.parentElement;
} }
return null;
} }
/** /**
@ -116,7 +133,7 @@ export class CoreDomUtilsProvider {
* @param alwaysConfirm True to show a confirm even if the size isn't high, false otherwise. * @param alwaysConfirm True to show a confirm even if the size isn't high, false otherwise.
* @return Promise resolved when the user confirms or if no confirm needed. * @return Promise resolved when the user confirms or if no confirm needed.
*/ */
confirmDownloadSize( async confirmDownloadSize(
size: {size: number; total: boolean}, size: {size: number; total: boolean},
message?: string, message?: string,
unknownMessage?: string, unknownMessage?: string,
@ -126,12 +143,14 @@ export class CoreDomUtilsProvider {
): Promise<void> { ): Promise<void> {
const readableSize = CoreTextUtils.instance.bytesToSize(size.size, 2); const readableSize = CoreTextUtils.instance.bytesToSize(size.size, 2);
const getAvailableBytes = new Promise((resolve): void => { const getAvailableBytes = async (): Promise<number | null> => {
if (CoreApp.instance.isDesktop()) { if (CoreApp.instance.isDesktop()) {
// Free space calculation is not supported on desktop. // Free space calculation is not supported on desktop.
resolve(null); return null;
} else { }
CoreFile.instance.calculateFreeSpace().then((availableBytes) => {
const availableBytes = await CoreFile.instance.calculateFreeSpace();
if (CoreApp.instance.isAndroid()) { if (CoreApp.instance.isAndroid()) {
return availableBytes; return availableBytes;
} else { } else {
@ -143,27 +162,31 @@ export class CoreDomUtilsProvider {
return null; return null;
} }
} }
}).then((availableBytes) => { };
resolve(availableBytes);
});
}
});
const getAvailableSpace = getAvailableBytes.then((availableBytes: number) => { const getAvailableSpace = (availableBytes: number | null): string => {
if (availableBytes === null) { if (availableBytes === null) {
return ''; return '';
} else { } else {
const availableSize = CoreTextUtils.instance.bytesToSize(availableBytes, 2); const availableSize = CoreTextUtils.instance.bytesToSize(availableBytes, 2);
if (CoreApp.instance.isAndroid() && size.size > availableBytes - CoreConstants.MINIMUM_FREE_SPACE) { if (CoreApp.instance.isAndroid() && size.size > availableBytes - CoreConstants.MINIMUM_FREE_SPACE) {
return Promise.reject(new CoreError(Translate.instance.instant('core.course.insufficientavailablespace', throw new CoreError(
{ size: readableSize }))); Translate.instance.instant(
'core.course.insufficientavailablespace',
{ size: readableSize },
),
);
} }
return Translate.instance.instant('core.course.availablespace', { available: availableSize }); return Translate.instance.instant('core.course.availablespace', { available: availableSize });
} }
}); };
const availableBytes = await getAvailableBytes();
const availableSpace = getAvailableSpace(availableBytes);
return getAvailableSpace.then((availableSpace) => {
wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold; wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold;
limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.DOWNLOAD_THRESHOLD : limitedThreshold; limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.DOWNLOAD_THRESHOLD : limitedThreshold;
@ -176,23 +199,32 @@ export class CoreDomUtilsProvider {
// Seems size was unable to be calculated. Show a warning. // Seems size was unable to be calculated. Show a warning.
unknownMessage = unknownMessage || 'core.course.confirmdownloadunknownsize'; unknownMessage = unknownMessage || 'core.course.confirmdownloadunknownsize';
return this.showConfirm(wifiPrefix + Translate.instance.instant( return this.showConfirm(
unknownMessage, { availableSpace: availableSpace })); wifiPrefix + Translate.instance.instant(
unknownMessage,
{ availableSpace: availableSpace },
),
);
} else if (!size.total) { } else if (!size.total) {
// Filesize is only partial. // Filesize is only partial.
return this.showConfirm(wifiPrefix + Translate.instance.instant('core.course.confirmpartialdownloadsize', return this.showConfirm(
{ size: readableSize, availableSpace: availableSpace })); wifiPrefix + Translate.instance.instant(
'core.course.confirmpartialdownloadsize',
{ size: readableSize, availableSpace: availableSpace },
),
);
} else if (alwaysConfirm || size.size >= wifiThreshold || } else if (alwaysConfirm || size.size >= wifiThreshold ||
(CoreApp.instance.isNetworkAccessLimited() && size.size >= limitedThreshold)) { (CoreApp.instance.isNetworkAccessLimited() && size.size >= limitedThreshold)) {
message = message || (size.size === 0 ? 'core.course.confirmdownloadzerosize' : 'core.course.confirmdownload'); message = message || (size.size === 0 ? 'core.course.confirmdownloadzerosize' : 'core.course.confirmdownload');
return this.showConfirm(wifiPrefix + Translate.instance.instant(message, return this.showConfirm(
{ size: readableSize, availableSpace: availableSpace })); wifiPrefix + Translate.instance.instant(
message,
{ size: readableSize, availableSpace: availableSpace },
),
);
} }
return Promise.resolve();
});
} }
/** /**
@ -255,11 +287,10 @@ export class CoreDomUtilsProvider {
this.logger.error('The function extractDownloadableFilesFromHtml has been moved to CoreFilepoolProvider.' + this.logger.error('The function extractDownloadableFilesFromHtml has been moved to CoreFilepoolProvider.' +
' Please use that function instead of this one.'); ' Please use that function instead of this one.');
const urls = []; const urls: string[] = [];
const element = this.convertToElement(html); const element = this.convertToElement(html);
const elements: (HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | const elements: AnchorOrMediaElement[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track'));
HTMLTrackElement)[] = Array.from(element.querySelectorAll('a, img, audio, video, source, track'));
for (let i = 0; i < elements.length; i++) { for (let i = 0; i < elements.length; i++) {
const element = elements[i]; const element = elements[i];
@ -271,7 +302,7 @@ export class CoreDomUtilsProvider {
// Treat video poster. // Treat video poster.
if (element.tagName == 'VIDEO' && element.getAttribute('poster')) { if (element.tagName == 'VIDEO' && element.getAttribute('poster')) {
url = element.getAttribute('poster'); url = element.getAttribute('poster') || '';
if (url && CoreUrlUtils.instance.isDownloadableUrl(url) && urls.indexOf(url) == -1) { if (url && CoreUrlUtils.instance.isDownloadableUrl(url) && urls.indexOf(url) == -1) {
urls.push(url); urls.push(url);
} }
@ -305,7 +336,7 @@ export class CoreDomUtilsProvider {
*/ */
extractUrlsFromCSS(code: string): string[] { extractUrlsFromCSS(code: string): string[] {
// First of all, search all the url(...) occurrences that don't include "data:". // First of all, search all the url(...) occurrences that don't include "data:".
const urls = []; const urls: string[] = [];
const matches = code.match(/url\(\s*["']?(?!data:)([^)]+)\)/igm); const matches = code.match(/url\(\s*["']?(?!data:)([^)]+)\)/igm);
if (!matches) { if (!matches) {
@ -394,7 +425,7 @@ export class CoreDomUtilsProvider {
* @param selector Selector to search. * @param selector Selector to search.
* @return Selection contents. Undefined if not found. * @return Selection contents. Undefined if not found.
*/ */
getContentsOfElement(element: HTMLElement, selector: string): string { getContentsOfElement(element: HTMLElement, selector: string): string | undefined {
if (element) { if (element) {
const selected = element.querySelector(selector); const selected = element.querySelector(selector);
if (selected) { if (selected) {
@ -447,7 +478,7 @@ export class CoreDomUtilsProvider {
* @param attribute Attribute to get. * @param attribute Attribute to get.
* @return Attribute value. * @return Attribute value.
*/ */
getHTMLElementAttribute(html: string, attribute: string): string { getHTMLElementAttribute(html: string, attribute: string): string | null {
return this.convertToElement(html).children[0].getAttribute(attribute); return this.convertToElement(html).children[0].getAttribute(attribute);
} }
@ -584,8 +615,8 @@ export class CoreDomUtilsProvider {
* @param positionParentClass Parent Class where to stop calculating the position. Default inner-scroll. * @param positionParentClass Parent Class where to stop calculating the position. Default inner-scroll.
* @return positionLeft, positionTop of the element relative to. * @return positionLeft, positionTop of the element relative to.
*/ */
getElementXY(container: HTMLElement, selector?: string, positionParentClass?: string): number[] { getElementXY(container: HTMLElement, selector?: string, positionParentClass?: string): number[] | null {
let element: HTMLElement = <HTMLElement> (selector ? container.querySelector(selector) : container); let element: HTMLElement | null = <HTMLElement> (selector ? container.querySelector(selector) : container);
let positionTop = 0; let positionTop = 0;
let positionLeft = 0; let positionLeft = 0;
@ -645,9 +676,9 @@ export class CoreDomUtilsProvider {
* @param needsTranslate Whether the error needs to be translated. * @param needsTranslate Whether the error needs to be translated.
* @return Error message, null if no error should be displayed. * @return Error message, null if no error should be displayed.
*/ */
getErrorMessage(error: CoreError | CoreTextErrorObject | string, needsTranslate?: boolean): string { getErrorMessage(error: CoreError | CoreTextErrorObject | string, needsTranslate?: boolean): string | null {
let extraInfo = ''; let extraInfo = '';
let errorMessage: string; let errorMessage: string | undefined;
if (typeof error == 'object') { if (typeof error == 'object') {
if (this.debugDisplay) { if (this.debugDisplay) {
@ -657,19 +688,21 @@ export class CoreDomUtilsProvider {
} }
if ('backtrace' in error && error.backtrace) { if ('backtrace' in error && error.backtrace) {
extraInfo += '<br><br>' + CoreTextUtils.instance.replaceNewLines( extraInfo += '<br><br>' + CoreTextUtils.instance.replaceNewLines(
CoreTextUtils.instance.escapeHTML(error.backtrace, false), '<br>'); CoreTextUtils.instance.escapeHTML(error.backtrace, false),
'<br>',
);
} }
// eslint-disable-next-line no-console // eslint-disable-next-line no-console
console.error(error); console.error(error);
} }
// We received an object instead of a string. Search for common properties. if (this.isSilentError(error)) {
if (this.isCanceledError(error)) { // It's a silent error, don't display an error.
// It's a canceled error, don't display an error.
return null; return null;
} }
// We received an object instead of a string. Search for common properties.
errorMessage = CoreTextUtils.instance.getErrorMessageFromError(error); errorMessage = CoreTextUtils.instance.getErrorMessageFromError(error);
if (!errorMessage) { if (!errorMessage) {
// No common properties found, just stringify it. // No common properties found, just stringify it.
@ -712,7 +745,7 @@ export class CoreDomUtilsProvider {
getInstanceByElement(element: Element): any { getInstanceByElement(element: Element): any {
const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME); const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME);
return this.instances[id]; return id && this.instances[id];
} }
/** /**
@ -725,6 +758,16 @@ export class CoreDomUtilsProvider {
return error instanceof CoreCanceledError; return error instanceof CoreCanceledError;
} }
/**
* Check whether an error is an error caused because the user canceled a showConfirm.
*
* @param error Error to check.
* @return Whether it's a canceled error.
*/
isSilentError(error: CoreError | CoreTextErrorObject | string): boolean {
return error instanceof CoreSilentError;
}
/** /**
* Wait an element to exists using the findFunction. * Wait an element to exists using the findFunction.
* *
@ -898,7 +941,7 @@ export class CoreDomUtilsProvider {
*/ */
removeInstanceByElement(element: Element): void { removeInstanceByElement(element: Element): void {
const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME); const id = element.getAttribute(this.INSTANCE_ID_ATTR_NAME);
delete this.instances[id]; id && delete this.instances[id];
} }
/** /**
@ -946,7 +989,8 @@ export class CoreDomUtilsProvider {
// Treat elements with src (img, audio, video, ...). // Treat elements with src (img, audio, video, ...).
const media = Array.from(element.querySelectorAll('img, video, audio, source, track')); const media = Array.from(element.querySelectorAll('img, video, audio, source, track'));
media.forEach((media: HTMLElement) => { media.forEach((media: HTMLElement) => {
let newSrc = paths[CoreTextUtils.instance.decodeURIComponent(media.getAttribute('src'))]; const currentSrc = media.getAttribute('src');
const newSrc = currentSrc ? paths[CoreTextUtils.instance.decodeURIComponent(currentSrc)] : undefined;
if (typeof newSrc != 'undefined') { if (typeof newSrc != 'undefined') {
media.setAttribute('src', newSrc); media.setAttribute('src', newSrc);
@ -954,9 +998,10 @@ export class CoreDomUtilsProvider {
// Treat video posters. // Treat video posters.
if (media.tagName == 'VIDEO' && media.getAttribute('poster')) { if (media.tagName == 'VIDEO' && media.getAttribute('poster')) {
newSrc = paths[CoreTextUtils.instance.decodeURIComponent(media.getAttribute('poster'))]; const currentPoster = media.getAttribute('poster');
if (typeof newSrc !== 'undefined') { const newPoster = paths[CoreTextUtils.instance.decodeURIComponent(currentPoster!)];
media.setAttribute('poster', newSrc); if (typeof newPoster !== 'undefined') {
media.setAttribute('poster', newPoster);
} }
} }
}); });
@ -964,14 +1009,14 @@ export class CoreDomUtilsProvider {
// Now treat links. // Now treat links.
const anchors = Array.from(element.querySelectorAll('a')); const anchors = Array.from(element.querySelectorAll('a'));
anchors.forEach((anchor: HTMLElement) => { anchors.forEach((anchor: HTMLElement) => {
const href = CoreTextUtils.instance.decodeURIComponent(anchor.getAttribute('href')); const currentHref = anchor.getAttribute('href');
const newUrl = paths[href]; const newHref = currentHref ? paths[CoreTextUtils.instance.decodeURIComponent(currentHref)] : undefined;
if (typeof newUrl != 'undefined') { if (typeof newHref != 'undefined') {
anchor.setAttribute('href', newUrl); anchor.setAttribute('href', newHref);
if (typeof anchorFn == 'function') { if (typeof anchorFn == 'function') {
anchorFn(anchor, href); anchorFn(anchor, newHref);
} }
} }
}); });
@ -990,7 +1035,7 @@ export class CoreDomUtilsProvider {
* @deprecated since 3.9.5. Use directly the IonContent class. * @deprecated since 3.9.5. Use directly the IonContent class.
*/ */
scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise<void> { scrollTo(content: IonContent, x: number, y: number, duration?: number): Promise<void> {
return content?.scrollByPoint(x, y, duration); return content?.scrollByPoint(x, y, duration || 0);
} }
/** /**
@ -1080,7 +1125,7 @@ export class CoreDomUtilsProvider {
return false; return false;
} }
content?.scrollByPoint(position[0], position[1], duration); content?.scrollByPoint(position[0], position[1], duration || 0);
return true; return true;
} }
@ -1108,7 +1153,7 @@ export class CoreDomUtilsProvider {
return false; return false;
} }
content?.scrollByPoint(position[0], position[1], duration); content?.scrollByPoint(position[0], position[1], duration || 0);
return true; return true;
} catch (error) { } catch (error) {
@ -1186,31 +1231,36 @@ export class CoreDomUtilsProvider {
const alert = await AlertController.instance.create(options); const alert = await AlertController.instance.create(options);
// eslint-disable-next-line promise/catch-or-return
alert.present().then(() => { alert.present().then(() => {
if (hasHTMLTags) { if (hasHTMLTags) {
// Treat all anchors so they don't override the app. // Treat all anchors so they don't override the app.
const alertMessageEl: HTMLElement = alert.querySelector('.alert-message'); const alertMessageEl: HTMLElement | null = alert.querySelector('.alert-message');
this.treatAnchors(alertMessageEl); alertMessageEl && this.treatAnchors(alertMessageEl);
} }
return;
}); });
// Store the alert and remove it when dismissed. // Store the alert and remove it when dismissed.
this.displayedAlerts[alertId] = alert; this.displayedAlerts[alertId] = alert;
// // Set the callbacks to trigger an observable event. // // Set the callbacks to trigger an observable event.
// eslint-disable-next-line promise/catch-or-return, promise/always-return
alert.onDidDismiss().then(() => { alert.onDidDismiss().then(() => {
delete this.displayedAlerts[alertId]; delete this.displayedAlerts[alertId];
}); });
if (autocloseTime > 0) { if (autocloseTime && autocloseTime > 0) {
setTimeout(async () => { setTimeout(async () => {
await alert.dismiss(); await alert.dismiss();
if (options.buttons) { if (options.buttons) {
// Execute dismiss function if any. // Execute dismiss function if any.
const cancelButton = <AlertButton> options.buttons.find((button) => typeof button != 'string' && const cancelButton = <AlertButton> options.buttons.find(
typeof button.handler != 'undefined' && button.role == 'cancel'); (button) => typeof button != 'string' && typeof button.handler != 'undefined' && button.role == 'cancel',
cancelButton?.handler(null); );
cancelButton.handler?.(null);
} }
}, autocloseTime); }, autocloseTime);
} }
@ -1248,8 +1298,13 @@ export class CoreDomUtilsProvider {
translateArgs: Record<string, string> = {}, translateArgs: Record<string, string> = {},
options?: AlertOptions, options?: AlertOptions,
): Promise<void> { ): Promise<void> {
return this.showConfirm(Translate.instance.instant(translateMessage, translateArgs), undefined, return this.showConfirm(
Translate.instance.instant('core.delete'), undefined, options); Translate.instance.instant(translateMessage, translateArgs),
undefined,
Translate.instance.instant('core.delete'),
undefined,
options,
);
} }
/** /**
@ -1306,7 +1361,7 @@ export class CoreDomUtilsProvider {
): Promise<HTMLIonAlertElement | null> { ): Promise<HTMLIonAlertElement | null> {
if (this.isCanceledError(error)) { if (this.isCanceledError(error)) {
// It's a canceled error, don't display an error. // It's a canceled error, don't display an error.
return null; return Promise.resolve(null);
} }
const message = this.getErrorMessage(error, needsTranslate); const message = this.getErrorMessage(error, needsTranslate);
@ -1339,7 +1394,7 @@ export class CoreDomUtilsProvider {
return null; return null;
} }
let errorMessage = error; let errorMessage = error || undefined;
if (error && typeof error != 'string') { if (error && typeof error != 'string') {
errorMessage = CoreTextUtils.instance.getErrorMessageFromError(error); errorMessage = CoreTextUtils.instance.getErrorMessageFromError(error);
@ -1428,8 +1483,8 @@ export class CoreDomUtilsProvider {
const isDevice = CoreApp.instance.isAndroid() || CoreApp.instance.isIOS(); const isDevice = CoreApp.instance.isAndroid() || CoreApp.instance.isIOS();
if (!isDevice) { if (!isDevice) {
// Treat all anchors so they don't override the app. // Treat all anchors so they don't override the app.
const alertMessageEl: HTMLElement = alert.querySelector('.alert-message'); const alertMessageEl: HTMLElement | null = alert.querySelector('.alert-message');
this.treatAnchors(alertMessageEl); alertMessageEl && this.treatAnchors(alertMessageEl);
} }
} }
@ -1448,8 +1503,7 @@ export class CoreDomUtilsProvider {
header?: string, header?: string,
placeholder?: string, placeholder?: string,
type: TextFieldTypes | 'checkbox' | 'radio' | 'textarea' = 'password', type: TextFieldTypes | 'checkbox' | 'radio' | 'textarea' = 'password',
// eslint-disable-next-line @typescript-eslint/no-explicit-any ): Promise<any> { // eslint-disable-line @typescript-eslint/no-explicit-any
): Promise<any> {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
placeholder = placeholder ?? Translate.instance.instant('core.login.password'); placeholder = placeholder ?? Translate.instance.instant('core.login.password');
@ -1537,7 +1591,7 @@ export class CoreDomUtilsProvider {
* @param instance The instance to store. * @param instance The instance to store.
* @return ID to identify the instance. * @return ID to identify the instance.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
storeInstanceByElement(element: Element, instance: any): string { storeInstanceByElement(element: Element, instance: any): string {
const id = String(this.lastInstanceId++); const id = String(this.lastInstanceId++);
@ -1607,7 +1661,7 @@ export class CoreDomUtilsProvider {
* @param componentId An ID to use in conjunction with the component. * @param componentId An ID to use in conjunction with the component.
* @param fullScreen Whether the modal should be full screen. * @param fullScreen Whether the modal should be full screen.
*/ */
viewImage(image: string, title?: string, component?: string, componentId?: string | number, fullScreen?: boolean): void { viewImage(image: string, title?: string | null, component?: string, componentId?: string | number, fullScreen?: boolean): void {
// @todo // @todo
} }
@ -1619,7 +1673,7 @@ export class CoreDomUtilsProvider {
*/ */
waitForImages(element: HTMLElement): Promise<boolean> { waitForImages(element: HTMLElement): Promise<boolean> {
const imgs = Array.from(element.querySelectorAll('img')); const imgs = Array.from(element.querySelectorAll('img'));
const promises = []; const promises: Promise<void>[] = [];
let hasImgToLoad = false; let hasImgToLoad = false;
imgs.forEach((img) => { imgs.forEach((img) => {
@ -1651,7 +1705,7 @@ export class CoreDomUtilsProvider {
*/ */
wrapElement(el: HTMLElement, wrapper: HTMLElement): void { wrapElement(el: HTMLElement, wrapper: HTMLElement): void {
// Insert the wrapper before the element. // Insert the wrapper before the element.
el.parentNode.insertBefore(wrapper, el); el.parentNode?.insertBefore(wrapper, el);
// Now move the element into the wrapper. // Now move the element into the wrapper.
wrapper.appendChild(el); wrapper.appendChild(el);
} }
@ -1680,7 +1734,7 @@ export class CoreDomUtilsProvider {
* @param online Whether the action was done in offline or not. * @param online Whether the action was done in offline or not.
* @param siteId The site affected. If not provided, no site affected. * @param siteId The site affected. If not provided, no site affected.
*/ */
triggerFormSubmittedEvent(formRef: ElementRef, online?: boolean, siteId?: string): void { triggerFormSubmittedEvent(formRef: ElementRef | undefined, online?: boolean, siteId?: string): void {
if (!formRef) { if (!formRef) {
return; return;
} }
@ -1695,3 +1749,6 @@ export class CoreDomUtilsProvider {
} }
export class CoreDomUtils extends makeSingleton(CoreDomUtilsProvider) {} export class CoreDomUtils extends makeSingleton(CoreDomUtilsProvider) {}
type AnchorOrMediaElement =
HTMLAnchorElement | HTMLImageElement | HTMLAudioElement | HTMLVideoElement | HTMLSourceElement | HTMLTrackElement;

View File

@ -423,7 +423,7 @@ export class CoreIframeUtilsProvider {
if (!CoreSites.instance.isLoggedIn()) { if (!CoreSites.instance.isLoggedIn()) {
CoreUtils.instance.openInBrowser(link.href); CoreUtils.instance.openInBrowser(link.href);
} else { } else {
await CoreSites.instance.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(link.href); await CoreSites.instance.getCurrentSite()!.openInBrowserWithAutoLoginIfSameSite(link.href);
} }
} else if (link.target == '_parent' || link.target == '_top' || link.target == '_blank') { } else if (link.target == '_parent' || link.target == '_top' || link.target == '_blank') {
// Opening links with _parent, _top or _blank can break the app. We'll open it in InAppBrowser. // Opening links with _parent, _top or _blank can break the app. We'll open it in InAppBrowser.

View File

@ -401,7 +401,7 @@ export class CoreMimetypeUtilsProvider {
* @param capitalise If true, capitalises first character of result. * @param capitalise If true, capitalises first character of result.
* @return Type description. * @return Type description.
*/ */
getMimetypeDescription(obj: FileEntry | { filename: string; mimetype: string } | string, capitalise?: boolean): string { getMimetypeDescription(obj: FileEntry | CoreWSExternalFile | string, capitalise?: boolean): string {
const langPrefix = 'assets.mimetypes.'; const langPrefix = 'assets.mimetypes.';
let filename: string | undefined = ''; let filename: string | undefined = '';
let mimetype: string | undefined = ''; let mimetype: string | undefined = '';

View File

@ -417,7 +417,7 @@ export class CoreTextUtilsProvider {
* @param doubleEncode If false, it will not convert existing html entities. Defaults to true. * @param doubleEncode If false, it will not convert existing html entities. Defaults to true.
* @return Escaped text. * @return Escaped text.
*/ */
escapeHTML(text: string | number, doubleEncode: boolean = true): string { escapeHTML(text?: string | number | null, doubleEncode: boolean = true): string {
if (typeof text == 'undefined' || text === null || (typeof text == 'number' && isNaN(text))) { if (typeof text == 'undefined' || text === null || (typeof text == 'number' && isNaN(text))) {
return ''; return '';
} else if (typeof text != 'string') { } else if (typeof text != 'string') {
@ -670,7 +670,7 @@ export class CoreTextUtilsProvider {
* @param text Text to treat. * @param text Text to treat.
* @return Treated text. * @return Treated text.
*/ */
removeEndingSlash(text: string): string { removeEndingSlash(text?: string): string {
if (!text) { if (!text) {
return ''; return '';
} }

View File

@ -44,7 +44,7 @@ import { CoreAjaxWSError } from '@classes/errors/ajaxwserror';
export class CoreWSProvider { export class CoreWSProvider {
protected logger: CoreLogger; protected logger: CoreLogger;
protected mimeTypeCache: {[url: string]: string} = {}; // A "cache" to store file mimetypes to decrease HEAD requests. protected mimeTypeCache: {[url: string]: string | null} = {}; // A "cache" to store file mimetypes to decrease HEAD requests.
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
protected ongoingCalls: {[queueItemId: string]: Promise<any>} = {}; protected ongoingCalls: {[queueItemId: string]: Promise<any>} = {};
protected retryCalls: RetryCall[] = []; protected retryCalls: RetryCall[] = [];
@ -53,11 +53,18 @@ export class CoreWSProvider {
constructor() { constructor() {
this.logger = CoreLogger.getInstance('CoreWSProvider'); this.logger = CoreLogger.getInstance('CoreWSProvider');
Platform.instance.ready().then(() => { this.init();
}
/**
* Initialize some data.
*/
protected async init(): Promise<void> {
await Platform.instance.ready();
if (CoreApp.instance.isIOS()) { if (CoreApp.instance.isIOS()) {
NativeHttp.instance.setHeader('*', 'User-Agent', navigator.userAgent); NativeHttp.instance.setHeader('*', 'User-Agent', navigator.userAgent);
} }
});
} }
/** /**
@ -67,8 +74,7 @@ export class CoreWSProvider {
* @param siteUrl Complete site url to perform the call. * @param siteUrl Complete site url to perform the call.
* @param ajaxData Arguments to pass to the method. * @param ajaxData Arguments to pass to the method.
* @param preSets Extra settings and information. * @param preSets Extra settings and information.
* @return Deferred promise resolved with the response data in success and rejected with the error message * @return Deferred promise resolved with the response data in success and rejected with the error if it fails.
* if it fails.
*/ */
protected addToRetryQueue<T = unknown>(method: string, siteUrl: string, data: unknown, preSets: CoreWSPreSets): Promise<T> { protected addToRetryQueue<T = unknown>(method: string, siteUrl: string, data: unknown, preSets: CoreWSPreSets): Promise<T> {
const call = { const call = {
@ -94,9 +100,9 @@ export class CoreWSProvider {
*/ */
call<T = unknown>(method: string, data: unknown, preSets: CoreWSPreSets): Promise<T> { call<T = unknown>(method: string, data: unknown, preSets: CoreWSPreSets): Promise<T> {
if (!preSets) { if (!preSets) {
return Promise.reject(new CoreError(Translate.instance.instant('core.unexpectederror'))); throw new CoreError(Translate.instance.instant('core.unexpectederror'));
} else if (!CoreApp.instance.isOnline()) { } else if (!CoreApp.instance.isOnline()) {
return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
} }
preSets.typeExpected = preSets.typeExpected || 'object'; preSets.typeExpected = preSets.typeExpected || 'object';
@ -126,10 +132,7 @@ export class CoreWSProvider {
* @param method The WebService method to be called. * @param method The WebService method to be called.
* @param data Arguments to pass to the method. * @param data Arguments to pass to the method.
* @param preSets Extra settings and information. Only some * @param preSets Extra settings and information. Only some
* @return Promise resolved with the response data in success and rejected with an object containing: * @return Promise resolved with the response data in success and rejected with CoreAjaxError.
* - error: Error message.
* - errorcode: Error code returned by the site (if any).
* - available: 0 if unknown, 1 if available, -1 if not available.
*/ */
callAjax<T = unknown>(method: string, data: Record<string, unknown>, preSets: CoreWSAjaxPreSets): Promise<T> { callAjax<T = unknown>(method: string, data: Record<string, unknown>, preSets: CoreWSAjaxPreSets): Promise<T> {
const cacheParams = { const cacheParams = {
@ -155,7 +158,7 @@ export class CoreWSProvider {
* @param stripUnicode If Unicode long chars need to be stripped. * @param stripUnicode If Unicode long chars need to be stripped.
* @return The cleaned object or null if some strings becomes empty after stripping Unicode. * @return The cleaned object or null if some strings becomes empty after stripping Unicode.
*/ */
// eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types, @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
convertValuesToString(data: any, stripUnicode?: boolean): any { convertValuesToString(data: any, stripUnicode?: boolean): any {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
const result: any = Array.isArray(data) ? [] : {}; const result: any = Array.isArray(data) ? [] : {};
@ -232,8 +235,12 @@ export class CoreWSProvider {
* @param onProgress Function to call on progress. * @param onProgress Function to call on progress.
* @return Promise resolved with the downloaded file. * @return Promise resolved with the downloaded file.
*/ */
async downloadFile(url: string, path: string, addExtension?: boolean, onProgress?: (event: ProgressEvent) => void): async downloadFile(
Promise<CoreWSDownloadedFileEntry> { url: string,
path: string,
addExtension?: boolean,
onProgress?: (event: ProgressEvent) => void,
): Promise<CoreWSDownloadedFileEntry> {
this.logger.debug('Downloading file', url, path, addExtension); this.logger.debug('Downloading file', url, path, addExtension);
if (!CoreApp.instance.isOnline()) { if (!CoreApp.instance.isOnline()) {
@ -249,7 +256,7 @@ export class CoreWSProvider {
const fileEntry = await CoreFile.instance.createFile(tmpPath); const fileEntry = await CoreFile.instance.createFile(tmpPath);
const transfer = FileTransfer.instance.create(); const transfer = FileTransfer.instance.create();
transfer.onProgress(onProgress); onProgress && transfer.onProgress(onProgress);
// Download the file in the tmp file. // Download the file in the tmp file.
await transfer.download(url, fileEntry.toURL(), true); await transfer.download(url, fileEntry.toURL(), true);
@ -257,7 +264,7 @@ export class CoreWSProvider {
let extension = ''; let extension = '';
if (addExtension) { if (addExtension) {
extension = CoreMimetypeUtils.instance.getFileExtension(path); extension = CoreMimetypeUtils.instance.getFileExtension(path) || '';
// Google Drive extensions will be considered invalid since Moodle usually converts them. // Google Drive extensions will be considered invalid since Moodle usually converts them.
if (!extension || CoreArray.contains(['gdoc', 'gsheet', 'gslides', 'gdraw', 'php'], extension)) { if (!extension || CoreArray.contains(['gdoc', 'gsheet', 'gslides', 'gdraw', 'php'], extension)) {
@ -281,14 +288,15 @@ export class CoreWSProvider {
} }
// Move the file to the final location. // Move the file to the final location.
const movedEntry: CoreWSDownloadedFileEntry = await CoreFile.instance.moveFile(tmpPath, path); const movedEntry = await CoreFile.instance.moveFile(tmpPath, path);
// Save the extension.
movedEntry.extension = extension;
movedEntry.path = path;
this.logger.debug(`Success downloading file ${url} to ${path} with extension ${extension}`); this.logger.debug(`Success downloading file ${url} to ${path} with extension ${extension}`);
return movedEntry; // Also return the extension and path.
return <CoreWSDownloadedFileEntry> Object.assign(movedEntry, {
extension: extension,
path: path,
});
} catch (error) { } catch (error) {
this.logger.error(`Error downloading ${url} to ${path}`, error); this.logger.error(`Error downloading ${url} to ${path}`, error);
@ -303,7 +311,7 @@ export class CoreWSProvider {
* @param url Base URL of the HTTP request. * @param url Base URL of the HTTP request.
* @param params Params of the HTTP request. * @param params Params of the HTTP request.
*/ */
protected getPromiseHttp<T = unknown>(method: string, url: string, params?: Record<string, unknown>): Promise<T> { protected getPromiseHttp<T = unknown>(method: string, url: string, params?: Record<string, unknown>): Promise<T> | undefined {
const queueItemId = this.getQueueItemId(method, url, params); const queueItemId = this.getQueueItemId(method, url, params);
if (typeof this.ongoingCalls[queueItemId] != 'undefined') { if (typeof this.ongoingCalls[queueItemId] != 'undefined') {
return this.ongoingCalls[queueItemId]; return this.ongoingCalls[queueItemId];
@ -317,12 +325,14 @@ export class CoreWSProvider {
* @param ignoreCache True to ignore cache, false otherwise. * @param ignoreCache True to ignore cache, false otherwise.
* @return Promise resolved with the mimetype or '' if failure. * @return Promise resolved with the mimetype or '' if failure.
*/ */
getRemoteFileMimeType(url: string, ignoreCache?: boolean): Promise<string> { async getRemoteFileMimeType(url: string, ignoreCache?: boolean): Promise<string> {
if (this.mimeTypeCache[url] && !ignoreCache) { if (this.mimeTypeCache[url] && !ignoreCache) {
return Promise.resolve(this.mimeTypeCache[url]); return this.mimeTypeCache[url]!;
} }
return this.performHead(url).then((response) => { try {
const response = await this.performHead(url);
let mimeType = response.headers.get('Content-Type'); let mimeType = response.headers.get('Content-Type');
if (mimeType) { if (mimeType) {
// Remove "parameters" like charset. // Remove "parameters" like charset.
@ -331,10 +341,10 @@ export class CoreWSProvider {
this.mimeTypeCache[url] = mimeType; this.mimeTypeCache[url] = mimeType;
return mimeType || ''; return mimeType || '';
}).catch(() => } catch (error) {
// Error, resolve with empty mimetype. // Error, resolve with empty mimetype.
'', return '';
); }
} }
/** /**
@ -345,17 +355,15 @@ export class CoreWSProvider {
*/ */
getRemoteFileSize(url: string): Promise<number> { getRemoteFileSize(url: string): Promise<number> {
return this.performHead(url).then((response) => { return this.performHead(url).then((response) => {
const size = parseInt(response.headers.get('Content-Length'), 10); const contentLength = response.headers.get('Content-Length');
const size = contentLength ? parseInt(contentLength, 10) : 0;
if (size) { if (size) {
return size; return size;
} }
return -1; return -1;
}).catch(() => }).catch(() => -1);
// Error, return -1.
-1,
);
} }
/** /**
@ -389,19 +397,16 @@ export class CoreWSProvider {
* @param method The WebService method to be called. * @param method The WebService method to be called.
* @param data Arguments to pass to the method. * @param data Arguments to pass to the method.
* @param preSets Extra settings and information. Only some * @param preSets Extra settings and information. Only some
* @return Promise resolved with the response data in success and rejected with an object containing: * @return Promise resolved with the response data in success and rejected with CoreAjaxError.
* - error: Error message.
* - errorcode: Error code returned by the site (if any).
* - available: 0 if unknown, 1 if available, -1 if not available.
*/ */
protected performAjax<T = unknown>(method: string, data: Record<string, unknown>, preSets: CoreWSAjaxPreSets): Promise<T> { protected performAjax<T = unknown>(method: string, data: Record<string, unknown>, preSets: CoreWSAjaxPreSets): Promise<T> {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
let promise: Promise<HttpResponse<any>>; let promise: Promise<HttpResponse<any>>;
if (typeof preSets.siteUrl == 'undefined') { if (typeof preSets.siteUrl == 'undefined') {
return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.unexpectederror'))); throw new CoreAjaxError(Translate.instance.instant('core.unexpectederror'));
} else if (!CoreApp.instance.isOnline()) { } else if (!CoreApp.instance.isOnline()) {
return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.networkerrormsg'))); throw new CoreAjaxError(Translate.instance.instant('core.networkerrormsg'));
} }
if (typeof preSets.responseExpected == 'undefined') { if (typeof preSets.responseExpected == 'undefined') {
@ -446,23 +451,23 @@ export class CoreWSProvider {
// Check if error. Ajax layer should always return an object (if error) or an array (if success). // Check if error. Ajax layer should always return an object (if error) or an array (if success).
if (!data || typeof data != 'object') { if (!data || typeof data != 'object') {
return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.serverconnection'))); throw new CoreAjaxError(Translate.instance.instant('core.serverconnection'));
} else if (data.error) { } else if (data.error) {
return Promise.reject(new CoreAjaxWSError(data)); throw new CoreAjaxWSError(data);
} }
// Get the first response since only one request was done. // Get the first response since only one request was done.
data = data[0]; data = data[0];
if (data.error) { if (data.error) {
return Promise.reject(new CoreAjaxWSError(data.exception)); throw new CoreAjaxWSError(data.exception);
} }
return data.data; return data.data;
}, (data) => { }, (data) => {
const available = data.status == 404 ? -1 : 0; const available = data.status == 404 ? -1 : 0;
return Promise.reject(new CoreAjaxError(Translate.instance.instant('core.serverconnection'), available)); throw new CoreAjaxError(Translate.instance.instant('core.serverconnection'), available);
}); });
} }
@ -522,7 +527,7 @@ export class CoreWSProvider {
} }
if (!data) { if (!data) {
return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection'))); throw new CoreError(Translate.instance.instant('core.serverconnection'));
} else if (typeof data != preSets.typeExpected) { } else if (typeof data != preSets.typeExpected) {
// If responseType is text an string will be returned, parse before returning. // If responseType is text an string will be returned, parse before returning.
if (typeof data == 'string') { if (typeof data == 'string') {
@ -531,7 +536,7 @@ export class CoreWSProvider {
if (isNaN(data)) { if (isNaN(data)) {
this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`); this.logger.warn(`Response expected type "${preSets.typeExpected}" cannot be parsed to number`);
return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); throw new CoreError(Translate.instance.instant('core.errorinvalidresponse'));
} }
} else if (preSets.typeExpected == 'boolean') { } else if (preSets.typeExpected == 'boolean') {
if (data === 'true') { if (data === 'true') {
@ -541,17 +546,17 @@ export class CoreWSProvider {
} else { } else {
this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`); this.logger.warn(`Response expected type "${preSets.typeExpected}" is not true or false`);
return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); throw new CoreError(Translate.instance.instant('core.errorinvalidresponse'));
} }
} else { } else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); throw new CoreError(Translate.instance.instant('core.errorinvalidresponse'));
} }
} else { } else {
this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`); this.logger.warn('Response of type "' + typeof data + `" received, expecting "${preSets.typeExpected}"`);
return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); throw new CoreError(Translate.instance.instant('core.errorinvalidresponse'));
} }
} }
@ -561,11 +566,11 @@ export class CoreWSProvider {
this.logger.error('Error calling WS', method, data); this.logger.error('Error calling WS', method, data);
} }
return Promise.reject(new CoreWSError(data)); throw new CoreWSError(data);
} }
if (typeof data.debuginfo != 'undefined') { if (typeof data.debuginfo != 'undefined') {
return Promise.reject(new CoreError('Error. ' + data.message)); throw new CoreError('Error. ' + data.message);
} }
return data; return data;
@ -593,7 +598,7 @@ export class CoreWSProvider {
return retryPromise; return retryPromise;
} }
return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection'))); throw new CoreError(Translate.instance.instant('core.serverconnection'));
}); });
} }
@ -606,7 +611,7 @@ export class CoreWSProvider {
const call = this.retryCalls.shift(); const call = this.retryCalls.shift();
// Add a delay between calls. // Add a delay between calls.
setTimeout(() => { setTimeout(() => {
call.deferred.resolve(this.performPost(call.method, call.siteUrl, call.data, call.preSets)); call!.deferred.resolve(this.performPost(call!.method, call!.siteUrl, call!.data, call!.preSets));
this.processRetryQueue(); this.processRetryQueue();
}, 200); }, 200);
} else { } else {
@ -623,8 +628,12 @@ export class CoreWSProvider {
* @param params Params of the HTTP request. * @param params Params of the HTTP request.
* @return The promise saved. * @return The promise saved.
*/ */
protected setPromiseHttp<T = unknown>(promise: Promise<T>, method: string, url: string, params?: Record<string, unknown>): protected setPromiseHttp<T = unknown>(
Promise<T> { promise: Promise<T>,
method: string,
url: string,
params?: Record<string, unknown>,
): Promise<T> {
const queueItemId = this.getQueueItemId(method, url, params); const queueItemId = this.getQueueItemId(method, url, params);
this.ongoingCalls[queueItemId] = promise; this.ongoingCalls[queueItemId] = promise;
@ -652,7 +661,7 @@ export class CoreWSProvider {
* @return Promise resolved with the response data in success and rejected with the error message if it fails. * @return Promise resolved with the response data in success and rejected with the error message if it fails.
* @return Request response. * @return Request response.
*/ */
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types // eslint-disable-next-line @typescript-eslint/no-explicit-any
syncCall<T = unknown>(method: string, data: any, preSets: CoreWSPreSets): T { syncCall<T = unknown>(method: string, data: any, preSets: CoreWSPreSets): T {
if (!preSets) { if (!preSets) {
throw new CoreError(Translate.instance.instant('core.unexpectederror')); throw new CoreError(Translate.instance.instant('core.unexpectederror'));
@ -728,22 +737,26 @@ export class CoreWSProvider {
* @param onProgress Function to call on progress. * @param onProgress Function to call on progress.
* @return Promise resolved when uploaded. * @return Promise resolved when uploaded.
*/ */
uploadFile<T = unknown>(filePath: string, options: CoreWSFileUploadOptions, preSets: CoreWSPreSets, async uploadFile<T = unknown>(
onProgress?: (event: ProgressEvent) => void): Promise<T> { filePath: string,
options: CoreWSFileUploadOptions,
preSets: CoreWSPreSets,
onProgress?: (event: ProgressEvent) => void,
): Promise<T> {
this.logger.debug(`Trying to upload file: ${filePath}`); this.logger.debug(`Trying to upload file: ${filePath}`);
if (!filePath || !options || !preSets) { if (!filePath || !options || !preSets) {
return Promise.reject(new CoreError('Invalid options passed to upload file.')); throw new CoreError('Invalid options passed to upload file.');
} }
if (!CoreApp.instance.isOnline()) { if (!CoreApp.instance.isOnline()) {
return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); throw new CoreError(Translate.instance.instant('core.networkerrormsg'));
} }
const uploadUrl = preSets.siteUrl + '/webservice/upload.php'; const uploadUrl = preSets.siteUrl + '/webservice/upload.php';
const transfer = FileTransfer.instance.create(); const transfer = FileTransfer.instance.create();
transfer.onProgress(onProgress); onProgress && transfer.onProgress(onProgress);
options.httpMethod = 'POST'; options.httpMethod = 'POST';
options.params = { options.params = {
@ -755,45 +768,51 @@ export class CoreWSProvider {
options.headers = {}; options.headers = {};
options['Connection'] = 'close'; options['Connection'] = 'close';
return transfer.upload(filePath, uploadUrl, options, true).then((success) => { try {
const data = CoreTextUtils.instance.parseJSON(success.response, null, const success = await transfer.upload(filePath, uploadUrl, options, true);
this.logger.error.bind(this.logger, 'Error parsing response from upload', success.response));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data = CoreTextUtils.instance.parseJSON<any>(
success.response,
null,
this.logger.error.bind(this.logger, 'Error parsing response from upload', success.response),
);
if (data === null) { if (data === null) {
return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); throw new CoreError(Translate.instance.instant('core.errorinvalidresponse'));
} }
if (!data) { if (!data) {
return Promise.reject(new CoreError(Translate.instance.instant('core.serverconnection'))); throw new CoreError(Translate.instance.instant('core.serverconnection'));
} else if (typeof data != 'object') { } else if (typeof data != 'object') {
this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"'); this.logger.warn('Upload file: Response of type "' + typeof data + '" received, expecting "object"');
return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); throw new CoreError(Translate.instance.instant('core.errorinvalidresponse'));
} }
if (typeof data.exception !== 'undefined') { if (typeof data.exception !== 'undefined') {
return Promise.reject(new CoreWSError(data)); throw new CoreWSError(data);
} else if (typeof data.error !== 'undefined') { } else if (typeof data.error !== 'undefined') {
return Promise.reject(new CoreWSError({ throw new CoreWSError({
errorcode: data.errortype, errorcode: data.errortype,
message: data.error, message: data.error,
})); });
} else if (data[0] && typeof data[0].error !== 'undefined') { } else if (data[0] && typeof data[0].error !== 'undefined') {
return Promise.reject(new CoreWSError({ throw new CoreWSError({
errorcode: data[0].errortype, errorcode: data[0].errortype,
message: data[0].error, message: data[0].error,
})); });
} }
// We uploaded only 1 file, so we only return the first file returned. // We uploaded only 1 file, so we only return the first file returned.
this.logger.debug('Successfully uploaded file', filePath); this.logger.debug('Successfully uploaded file', filePath);
return data[0]; return data[0];
}).catch((error) => { } catch (error) {
this.logger.error('Error while uploading file', filePath, error); this.logger.error('Error while uploading file', filePath, error);
return Promise.reject(new CoreError(Translate.instance.instant('core.errorinvalidresponse'))); throw new CoreError(Translate.instance.instant('core.errorinvalidresponse'));
}); }
} }
/** /**
@ -842,7 +861,7 @@ export class CoreWSProvider {
return new HttpResponse<T>({ return new HttpResponse<T>({
body: <T> content, body: <T> content,
headers: null, headers: undefined,
status: 200, status: 200,
statusText: 'OK', statusText: 'OK',
url, url,
@ -890,7 +909,7 @@ export class CoreWSProvider {
break; break;
default: default:
return Promise.reject(new CoreError('Method not implemented yet.')); throw new CoreError('Method not implemented yet.');
} }
if (angularOptions.timeout) { if (angularOptions.timeout) {
@ -966,6 +985,11 @@ export type CoreWSExternalWarning = {
* Structure of files returned by WS. * Structure of files returned by WS.
*/ */
export type CoreWSExternalFile = { export type CoreWSExternalFile = {
/**
* Downloadable file url.
*/
fileurl: string;
/** /**
* File name. * File name.
*/ */
@ -981,11 +1005,6 @@ export type CoreWSExternalFile = {
*/ */
filesize?: number; filesize?: number;
/**
* Downloadable file url.
*/
fileurl?: string;
/** /**
* Time modified. * Time modified.
*/ */
@ -1108,7 +1127,7 @@ export type HttpRequestOptions = {
/** /**
* Timeout for the request in seconds. If undefined, the default value will be used. If null, no timeout. * Timeout for the request in seconds. If undefined, the default value will be used. If null, no timeout.
*/ */
timeout?: number | null; timeout?: number;
/** /**
* Serializer to use. Defaults to 'urlencoded'. Only for mobile environments. * Serializer to use. Defaults to 'urlencoded'. Only for mobile environments.
@ -1162,6 +1181,6 @@ type RetryCall = {
* Downloaded file entry. It includes some calculated data. * Downloaded file entry. It includes some calculated data.
*/ */
export type CoreWSDownloadedFileEntry = FileEntry & { export type CoreWSDownloadedFileEntry = FileEntry & {
extension?: string; // File extension. extension: string; // File extension.
path?: string; // File path. path: string; // File path.
}; };

View File

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

View File

@ -5,7 +5,7 @@
*/ */
function initCache () { function initCache () {
const store = [] const store: any[] = []
// cache only first element, second is length to jump ahead for the parser // cache only first element, second is length to jump ahead for the parser
const cache = function cache (value) { const cache = function cache (value) {
store.push(value[0]) store.push(value[0])
@ -316,7 +316,7 @@ function expectArrayItems (str, expectedItems = 0, cache) {
let hasStringKeys = false let hasStringKeys = false
let item let item
let totalOffset = 0 let totalOffset = 0
let items = [] let items: any[] = []
cache([items]) cache([items])
for (let i = 0; i < expectedItems; i++) { for (let i = 0; i < expectedItems; i++) {

View File

@ -141,7 +141,7 @@ export class CoreUrl {
// If nothing else worked, parse the domain. // If nothing else worked, parse the domain.
const urlParts = CoreUrl.parse(url); const urlParts = CoreUrl.parse(url);
return urlParts && urlParts.domain ? urlParts.domain : null; return urlParts?.domain ? urlParts.domain : null;
} }
/** /**
@ -196,8 +196,8 @@ export class CoreUrl {
const partsA = CoreUrl.parse(urlA); const partsA = CoreUrl.parse(urlA);
const partsB = CoreUrl.parse(urlB); const partsB = CoreUrl.parse(urlB);
return partsA.domain == partsB.domain && return partsA?.domain == partsB?.domain &&
CoreTextUtils.instance.removeEndingSlash(partsA.path) == CoreTextUtils.instance.removeEndingSlash(partsB.path); CoreTextUtils.instance.removeEndingSlash(partsA?.path) == CoreTextUtils.instance.removeEndingSlash(partsB?.path);
} }
} }

View File

@ -60,7 +60,7 @@ export class CoreWindow {
await CoreUtils.instance.openFile(url); await CoreUtils.instance.openFile(url);
} else { } else {
let treated: boolean; let treated = false;
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
options = options || {}; options = options || {};
@ -76,7 +76,7 @@ export class CoreWindow {
// Not logged in, cannot auto-login. // Not logged in, cannot auto-login.
CoreUtils.instance.openInBrowser(url); CoreUtils.instance.openInBrowser(url);
} else { } else {
await CoreSites.instance.getCurrentSite().openInBrowserWithAutoLoginIfSameSite(url); await CoreSites.instance.getCurrentSite()!.openInBrowserWithAutoLoginIfSameSite(url);
} }
} }
} }