commit
						51c1e423fd
					
				| @ -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.'); | ||||||
|  | |||||||
| @ -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); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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({ | ||||||
|  | |||||||
| @ -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; | ||||||
|  | |||||||
| @ -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
											
										
									
								
							| @ -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 = { | ||||||
|  | |||||||
| @ -13,15 +13,33 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||||
|  | import { CommonModule } from '@angular/common'; | ||||||
|  | import { IonicModule } from '@ionic/angular'; | ||||||
|  | import { TranslateModule } from '@ngx-translate/core'; | ||||||
|  | 
 | ||||||
| import { CoreIconComponent } from './icon/icon'; | import { CoreIconComponent } from './icon/icon'; | ||||||
|  | import { CoreLoadingComponent } from './loading/loading'; | ||||||
|  | import { CoreShowPasswordComponent } from './show-password/show-password'; | ||||||
|  | import { CoreDirectivesModule } from '@app/directives/directives.module'; | ||||||
|  | import { CorePipesModule } from '@app/pipes/pipes.module'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
|         CoreIconComponent, |         CoreIconComponent, | ||||||
|  |         CoreLoadingComponent, | ||||||
|  |         CoreShowPasswordComponent, | ||||||
|  |     ], | ||||||
|  |     imports: [ | ||||||
|  |         CommonModule, | ||||||
|  |         IonicModule.forRoot(), | ||||||
|  |         TranslateModule.forChild(), | ||||||
|  |         CoreDirectivesModule, | ||||||
|  |         CorePipesModule, | ||||||
|     ], |     ], | ||||||
|     imports: [], |  | ||||||
|     exports: [ |     exports: [ | ||||||
|         CoreIconComponent, |         CoreIconComponent, | ||||||
|  |         CoreLoadingComponent, | ||||||
|  |         CoreShowPasswordComponent, | ||||||
|     ], |     ], | ||||||
| }) | }) | ||||||
| export class CoreComponentsModule {} | export class CoreComponentsModule {} | ||||||
|  | |||||||
| @ -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; | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
							
								
								
									
										10
									
								
								src/app/components/loading/core-loading.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/app/components/loading/core-loading.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | |||||||
|  | <div class="core-loading-container" *ngIf="!hideUntil" role="status"> <!-- @todo [@coreShowHideAnimation]  --> | ||||||
|  |     <span class="core-loading-spinner"> | ||||||
|  |         <ion-spinner></ion-spinner> | ||||||
|  |         <p class="core-loading-message" *ngIf="message" role="status">{{message}}</p> | ||||||
|  |     </span> | ||||||
|  | </div> | ||||||
|  | <div #content class="core-loading-content" [id]="uniqueId" [attr.aria-busy]="hideUntil"> | ||||||
|  |     <ng-content *ngIf="hideUntil"> | ||||||
|  |     </ng-content> <!-- @todo [@coreShowHideAnimation]  --> | ||||||
|  | </div> | ||||||
							
								
								
									
										67
									
								
								src/app/components/loading/loading.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										67
									
								
								src/app/components/loading/loading.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,67 @@ | |||||||
|  | ion-app.app-root { | ||||||
|  |     core-loading { | ||||||
|  |         // @todo @include core-transition(height, 200ms); | ||||||
|  | 
 | ||||||
|  |         .core-loading-container { | ||||||
|  |             width: 100%; | ||||||
|  |             text-align: center; | ||||||
|  |             padding-top: 10px; | ||||||
|  |             clear: both; | ||||||
|  |             /* @todo @include darkmode() { | ||||||
|  |                 color: $core-dark-text-color; | ||||||
|  |             } */ | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         .core-loading-content { | ||||||
|  |             display: inline; | ||||||
|  |             padding-bottom: 1px; /* This makes height be real */ | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         &.core-loading-noheight .core-loading-content { | ||||||
|  |             height: auto; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         &.safe-area-page { | ||||||
|  |             padding-left: 0 !important; | ||||||
|  |             padding-right: 0 !important; | ||||||
|  | 
 | ||||||
|  |             > .core-loading-content > *:not[padding], | ||||||
|  |             > .core-loading-content-loading > *:not[padding] { | ||||||
|  |                 // @todo @include safe-area-padding-horizontal(0px, 0px); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .scroll-content > core-loading, | ||||||
|  |     ion-content > .scroll-content > core-loading, | ||||||
|  |     core-tab core-loading, | ||||||
|  |     .core-loading-center { | ||||||
|  |         position: static !important; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .scroll-content > core-loading, | ||||||
|  |     ion-content > .scroll-content > core-loading, | ||||||
|  |     core-tab core-loading, | ||||||
|  |     .core-loading-center, | ||||||
|  |     core-loading.core-loading-loaded { | ||||||
|  |         position: relative; | ||||||
|  | 
 | ||||||
|  |         > .core-loading-container { | ||||||
|  |             position: absolute; | ||||||
|  |             // @todo @include position(0, 0, 0, 0); | ||||||
|  |             display: table; | ||||||
|  |             height: 100%; | ||||||
|  |             width: 100%; | ||||||
|  |             z-index: 1; | ||||||
|  |             margin: 0; | ||||||
|  |             padding: 0; | ||||||
|  |             clear: both; | ||||||
|  | 
 | ||||||
|  |             .core-loading-spinner { | ||||||
|  |                 display: table-cell; | ||||||
|  |                 text-align: center; | ||||||
|  |                 vertical-align: middle; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										120
									
								
								src/app/components/loading/loading.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										120
									
								
								src/app/components/loading/loading.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,120 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component, Input, OnInit, OnChanges, SimpleChange, ViewChild, ElementRef, AfterViewInit } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | import { CoreEventLoadingChangedData, CoreEvents, CoreEventsProvider } from '@services/events'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { Translate } from '@singletons/core.singletons'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Component to show a loading spinner and message while data is being loaded. | ||||||
|  |  * | ||||||
|  |  * It will show a spinner with a message and hide all the content until 'hideUntil' variable is set to a truthy value (!!hideUntil). | ||||||
|  |  * If 'message' isn't set, default message "Loading" is shown. | ||||||
|  |  * 'message' attribute accepts hardcoded strings, variables, filters, etc. E.g. [message]="'core.loading' | translate". | ||||||
|  |  * | ||||||
|  |  * Usage: | ||||||
|  |  * <core-loading [message]="loadingMessage" [hideUntil]="dataLoaded"> | ||||||
|  |  *     <!-- CONTENT TO HIDE UNTIL LOADED --> | ||||||
|  |  * </core-loading> | ||||||
|  |  * | ||||||
|  |  * IMPORTANT: Due to how ng-content works in Angular, the content of core-loading will be executed as soon as your view | ||||||
|  |  * is loaded, even if the content hidden. So if you have the following code: | ||||||
|  |  * <core-loading [hideUntil]="dataLoaded"><my-component></my-component></core-loading> | ||||||
|  |  * | ||||||
|  |  * The component "my-component" will be initialized immediately, even if dataLoaded is false, but it will be hidden. If you want | ||||||
|  |  * your component to be initialized only if dataLoaded is true, then you should use ngIf: | ||||||
|  |  * <core-loading [hideUntil]="dataLoaded"><my-component *ngIf="dataLoaded"></my-component></core-loading> | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-loading', | ||||||
|  |     templateUrl: 'core-loading.html', | ||||||
|  |     styleUrls: ['loading.scss'], | ||||||
|  |     // @todo animations: [coreShowHideAnimation],
 | ||||||
|  | }) | ||||||
|  | export class CoreLoadingComponent implements OnInit, OnChanges, AfterViewInit { | ||||||
|  | 
 | ||||||
|  |     @Input() hideUntil: unknown; // Determine when should the contents be shown.
 | ||||||
|  |     @Input() message?: string; // Message to show while loading.
 | ||||||
|  |     @ViewChild('content') content?: ElementRef; | ||||||
|  | 
 | ||||||
|  |     protected uniqueId!: string; | ||||||
|  |     protected element: HTMLElement; // Current element.
 | ||||||
|  | 
 | ||||||
|  |     constructor(element: ElementRef) { | ||||||
|  |         this.element = element.nativeElement; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component being initialized. | ||||||
|  |      */ | ||||||
|  |     ngOnInit(): void { | ||||||
|  |         // Calculate the unique ID.
 | ||||||
|  |         this.uniqueId = 'core-loading-content-' + CoreUtils.instance.getUniqueId('CoreLoadingComponent'); | ||||||
|  | 
 | ||||||
|  |         if (!this.message) { | ||||||
|  |             // Default loading message.
 | ||||||
|  |             this.message = Translate.instance.instant('core.loading'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * View has been initialized. | ||||||
|  |      */ | ||||||
|  |     ngAfterViewInit(): void { | ||||||
|  |         // Add class if loaded on init.
 | ||||||
|  |         if (this.hideUntil) { | ||||||
|  |             this.element.classList.add('core-loading-loaded'); | ||||||
|  |             this.content?.nativeElement.classList.add('core-loading-content'); | ||||||
|  |         } else { | ||||||
|  |             this.content?.nativeElement.classList.remove('core-loading-content'); | ||||||
|  |             this.content?.nativeElement.classList.add('core-loading-content-loading'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component input changed. | ||||||
|  |      * | ||||||
|  |      * @param changes Changes. | ||||||
|  |      */ | ||||||
|  |     ngOnChanges(changes: { [name: string]: SimpleChange }): void { | ||||||
|  |         if (changes.hideUntil) { | ||||||
|  |             if (this.hideUntil) { | ||||||
|  |                 setTimeout(() => { | ||||||
|  |                     // Content is loaded so, center the spinner on the content itself.
 | ||||||
|  |                     this.element.classList.add('core-loading-loaded'); | ||||||
|  |                     setTimeout(() => { | ||||||
|  |                         // Change CSS to force calculate height.
 | ||||||
|  |                         this.content?.nativeElement.classList.add('core-loading-content'); | ||||||
|  |                         this.content?.nativeElement.classList.remove('core-loading-content-loading'); | ||||||
|  |                     }, 500); | ||||||
|  |                 }); | ||||||
|  |             } else { | ||||||
|  |                 this.element.classList.remove('core-loading-loaded'); | ||||||
|  |                 this.content?.nativeElement.classList.remove('core-loading-content'); | ||||||
|  |                 this.content?.nativeElement.classList.add('core-loading-content-loading'); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Trigger the event after a timeout since the elements inside ngIf haven't been added to DOM yet.
 | ||||||
|  |             setTimeout(() => { | ||||||
|  |                 CoreEvents.instance.trigger(CoreEventsProvider.CORE_LOADING_CHANGED, <CoreEventLoadingChangedData> { | ||||||
|  |                     loaded: !!this.hideUntil, | ||||||
|  |                     uniqueId: this.uniqueId, | ||||||
|  |                 }); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										4
									
								
								src/app/components/show-password/core-show-password.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								src/app/components/show-password/core-show-password.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,4 @@ | |||||||
|  | <ng-content></ng-content> | ||||||
|  | <ion-button icon-only clear [attr.aria-label]="label | translate" [core-suppress-events] (onClick)="toggle($event)"> | ||||||
|  |     <core-icon [name]="iconName"></core-icon> | ||||||
|  | </ion-button> | ||||||
							
								
								
									
										38
									
								
								src/app/components/show-password/show-password.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/app/components/show-password/show-password.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,38 @@ | |||||||
|  | ion-app.app-root core-show-password { | ||||||
|  |     padding: 0px; | ||||||
|  |     width: 100%; | ||||||
|  |     position: relative; | ||||||
|  | 
 | ||||||
|  |     ion-input input.text-input { | ||||||
|  |         // @todo @include padding(null, 47px, null, null); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .button[icon-only] { | ||||||
|  |         background: transparent; | ||||||
|  |         // @todo padding: 0 ($content-padding / 2); | ||||||
|  |         position: absolute; | ||||||
|  |         // @todo @include position(null, 0, $content-padding / 2, null); | ||||||
|  |         margin-top: 0; | ||||||
|  |         margin-bottom: 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     .core-ioninput-password { | ||||||
|  |         padding-top: 0; | ||||||
|  |         padding-bottom: 0; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ion-app.app-root.md { | ||||||
|  |     .item-label-stacked core-show-password .button[icon-only] { | ||||||
|  |         bottom: 0; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | ion-app.app-root.ios { | ||||||
|  |     .item-label-stacked core-show-password .button[icon-only] { | ||||||
|  |         bottom: -5px; | ||||||
|  |     } | ||||||
|  |     core-show-password .button[icon-only] { | ||||||
|  |         bottom: 0; | ||||||
|  |     } | ||||||
|  | } | ||||||
							
								
								
									
										136
									
								
								src/app/components/show-password/show-password.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								src/app/components/show-password/show-password.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,136 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component, OnInit, AfterViewInit, Input, ElementRef, ContentChild } from '@angular/core'; | ||||||
|  | import { IonInput } from '@ionic/angular'; | ||||||
|  | 
 | ||||||
|  | import { CoreApp } from '@services/app'; | ||||||
|  | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Component to allow showing and hiding a password. The affected input MUST have a name to identify it. | ||||||
|  |  * | ||||||
|  |  * @description | ||||||
|  |  * This directive needs to surround the input with the password. | ||||||
|  |  * | ||||||
|  |  * You need to supply the name of the input. | ||||||
|  |  * | ||||||
|  |  * Example: | ||||||
|  |  * | ||||||
|  |  * <core-show-password item-content [name]="'password'"> | ||||||
|  |  *     <ion-input type="password" name="password"></ion-input> | ||||||
|  |  * </core-show-password> | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'core-show-password', | ||||||
|  |     templateUrl: 'core-show-password.html', | ||||||
|  |     styleUrls: ['show-password.scss'], | ||||||
|  | }) | ||||||
|  | export class CoreShowPasswordComponent implements OnInit, AfterViewInit { | ||||||
|  | 
 | ||||||
|  |     @Input() name?: string; // Name of the input affected.
 | ||||||
|  |     @Input() initialShown?: boolean | string; // Whether the password should be shown at start.
 | ||||||
|  |     @ContentChild(IonInput) ionInput?: IonInput; | ||||||
|  | 
 | ||||||
|  |     shown!: boolean; // Whether the password is shown.
 | ||||||
|  |     label?: string; // Label for the button to show/hide.
 | ||||||
|  |     iconName?: string; // Name of the icon of the button to show/hide.
 | ||||||
|  |     selector = ''; // Selector to identify the input.
 | ||||||
|  | 
 | ||||||
|  |     protected input?: HTMLInputElement | null; // Input affected.
 | ||||||
|  |     protected element: HTMLElement; // Current element.
 | ||||||
|  | 
 | ||||||
|  |     constructor(element: ElementRef) { | ||||||
|  |         this.element = element.nativeElement; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component being initialized. | ||||||
|  |      */ | ||||||
|  |     ngOnInit(): void { | ||||||
|  |         this.shown = CoreUtils.instance.isTrueOrOne(this.initialShown); | ||||||
|  |         this.selector = 'input[name="' + this.name + '"]'; | ||||||
|  |         this.setData(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * View has been initialized. | ||||||
|  |      */ | ||||||
|  |     ngAfterViewInit(): void { | ||||||
|  |         this.searchInput(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Search the input to show/hide. | ||||||
|  |      */ | ||||||
|  |     protected async searchInput(): Promise<void> { | ||||||
|  |         if (this.ionInput) { | ||||||
|  |             // It's an ion-input, use it to get the native element.
 | ||||||
|  |             this.input = await this.ionInput.getInputElement(); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Search the input.
 | ||||||
|  |         this.input = <HTMLInputElement> this.element.querySelector(this.selector); | ||||||
|  | 
 | ||||||
|  |         if (this.input) { | ||||||
|  |             // Input found. Set the right type.
 | ||||||
|  |             this.input.type = this.shown ? 'text' : 'password'; | ||||||
|  | 
 | ||||||
|  |             // By default, don't autocapitalize and autocorrect.
 | ||||||
|  |             if (!this.input.getAttribute('autocorrect')) { | ||||||
|  |                 this.input.setAttribute('autocorrect', 'off'); | ||||||
|  |             } | ||||||
|  |             if (!this.input.getAttribute('autocapitalize')) { | ||||||
|  |                 this.input.setAttribute('autocapitalize', 'none'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Set label, icon name and input type. | ||||||
|  |      */ | ||||||
|  |     protected setData(): void { | ||||||
|  |         this.label = this.shown ? 'core.hide' : 'core.show'; | ||||||
|  |         this.iconName = this.shown ? 'eye-off' : 'eye'; | ||||||
|  |         if (this.input) { | ||||||
|  |             this.input.type = this.shown ? 'text' : 'password'; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Toggle show/hide password. | ||||||
|  |      * | ||||||
|  |      * @param event The mouse event. | ||||||
|  |      */ | ||||||
|  |     toggle(event: Event): void { | ||||||
|  |         event.preventDefault(); | ||||||
|  |         event.stopPropagation(); | ||||||
|  | 
 | ||||||
|  |         const isFocused = document.activeElement === this.input; | ||||||
|  | 
 | ||||||
|  |         this.shown = !this.shown; | ||||||
|  |         this.setData(); | ||||||
|  | 
 | ||||||
|  |         if (isFocused && CoreApp.instance.isAndroid()) { | ||||||
|  |             // In Android, the keyboard is closed when the input type changes. Focus it again.
 | ||||||
|  |             setTimeout(() => { | ||||||
|  |                 CoreDomUtils.instance.focusElement(this.input!); | ||||||
|  |             }, 400); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -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.
 | ||||||
|  | |||||||
| @ -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(); | ||||||
|     } |     } | ||||||
|  | |||||||
| @ -15,8 +15,10 @@ | |||||||
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||||
| import { RouterModule, Routes } from '@angular/router'; | import { RouterModule, Routes } from '@angular/router'; | ||||||
| 
 | 
 | ||||||
|  | import { CoreLoginCredentialsPage } from './pages/credentials/credentials.page'; | ||||||
| import { CoreLoginInitPage } from './pages/init/init.page'; | import { CoreLoginInitPage } from './pages/init/init.page'; | ||||||
| import { CoreLoginSitePage } from './pages/site/site.page'; | import { CoreLoginSitePage } from './pages/site/site.page'; | ||||||
|  | import { CoreLoginSitesPage } from './pages/sites/sites.page'; | ||||||
| 
 | 
 | ||||||
| const routes: Routes = [ | const routes: Routes = [ | ||||||
|     { |     { | ||||||
| @ -27,6 +29,14 @@ const routes: Routes = [ | |||||||
|         path: 'site', |         path: 'site', | ||||||
|         component: CoreLoginSitePage, |         component: CoreLoginSitePage, | ||||||
|     }, |     }, | ||||||
|  |     { | ||||||
|  |         path: 'credentials', | ||||||
|  |         component: CoreLoginCredentialsPage, | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |         path: 'sites', | ||||||
|  |         component: CoreLoginSitesPage, | ||||||
|  |     }, | ||||||
| ]; | ]; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|  | |||||||
| @ -14,13 +14,20 @@ | |||||||
| 
 | 
 | ||||||
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||||
| import { CommonModule } from '@angular/common'; | import { CommonModule } from '@angular/common'; | ||||||
|  | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; | ||||||
| 
 | 
 | ||||||
| import { IonicModule } from '@ionic/angular'; | import { IonicModule } from '@ionic/angular'; | ||||||
| import { TranslateModule } from '@ngx-translate/core'; | import { TranslateModule } from '@ngx-translate/core'; | ||||||
| 
 | 
 | ||||||
|  | import { CoreComponentsModule } from '@/app/components/components.module'; | ||||||
|  | import { CoreDirectivesModule } from '@/app/directives/directives.module'; | ||||||
|  | 
 | ||||||
| import { CoreLoginRoutingModule } from './login-routing.module'; | import { CoreLoginRoutingModule } from './login-routing.module'; | ||||||
|  | import { CoreLoginCredentialsPage } from './pages/credentials/credentials.page'; | ||||||
| import { CoreLoginInitPage } from './pages/init/init.page'; | import { CoreLoginInitPage } from './pages/init/init.page'; | ||||||
| import { CoreLoginSitePage } from './pages/site/site.page'; | import { CoreLoginSitePage } from './pages/site/site.page'; | ||||||
|  | import { CoreLoginSitesPage } from './pages/sites/sites.page'; | ||||||
|  | import { CoreLoginHelperProvider } from './services/helper'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     imports: [ |     imports: [ | ||||||
| @ -28,10 +35,19 @@ import { CoreLoginSitePage } from './pages/site/site.page'; | |||||||
|         IonicModule, |         IonicModule, | ||||||
|         CoreLoginRoutingModule, |         CoreLoginRoutingModule, | ||||||
|         TranslateModule.forChild(), |         TranslateModule.forChild(), | ||||||
|  |         FormsModule, | ||||||
|  |         ReactiveFormsModule, | ||||||
|  |         CoreComponentsModule, | ||||||
|  |         CoreDirectivesModule, | ||||||
|     ], |     ], | ||||||
|     declarations: [ |     declarations: [ | ||||||
|  |         CoreLoginCredentialsPage, | ||||||
|         CoreLoginInitPage, |         CoreLoginInitPage, | ||||||
|         CoreLoginSitePage, |         CoreLoginSitePage, | ||||||
|  |         CoreLoginSitesPage, | ||||||
|  |     ], | ||||||
|  |     providers: [ | ||||||
|  |         CoreLoginHelperProvider, | ||||||
|     ], |     ], | ||||||
| }) | }) | ||||||
| export class CoreLoginModule {} | export class CoreLoginModule {} | ||||||
|  | |||||||
							
								
								
									
										75
									
								
								src/app/core/login/pages/credentials/credentials.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/app/core/login/pages/credentials/credentials.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | |||||||
|  | <ion-header> | ||||||
|  |     <ion-toolbar> | ||||||
|  |         <ion-buttons slot="start"> | ||||||
|  |             <ion-back-button></ion-back-button> | ||||||
|  |         </ion-buttons> | ||||||
|  | 
 | ||||||
|  |         <ion-title>{{ 'core.login.login' | translate }}</ion-title> | ||||||
|  | 
 | ||||||
|  |         <ion-buttons slot="end"> | ||||||
|  |             <!-- @todo: Settings button. --> | ||||||
|  |         </ion-buttons> | ||||||
|  |     </ion-toolbar> | ||||||
|  | </ion-header> | ||||||
|  | <ion-content padding> | ||||||
|  |     <core-loading [hideUntil]="pageLoaded"> | ||||||
|  |         <div text-wrap text-center margin-bottom> | ||||||
|  |             <div class="core-login-site-logo"> | ||||||
|  |                 <!-- Show site logo or a default image. --> | ||||||
|  |                 <img *ngIf="logoUrl" [src]="logoUrl" role="presentation" onError="this.src='assets/img/login_logo.png'"> | ||||||
|  |                 <img *ngIf="!logoUrl" src="assets/img/login_logo.png" role="presentation"> | ||||||
|  |             </div> | ||||||
|  | 
 | ||||||
|  |             <h3 *ngIf="siteName" padding class="core-sitename"><core-format-text [text]="siteName" [filter]="false"></core-format-text></h3> | ||||||
|  |             <p class="core-siteurl">{{siteUrl}}</p> | ||||||
|  |         </div> | ||||||
|  | 
 | ||||||
|  |         <form ion-list [formGroup]="credForm" (ngSubmit)="login($event)" class="core-login-form" #credentialsForm> | ||||||
|  |             <ion-item *ngIf="siteChecked && !isBrowserSSO"> | ||||||
|  |                 <ion-input type="text" name="username" placeholder="{{ 'core.login.username' | translate }}" formControlName="username" autocapitalize="none" autocorrect="off"></ion-input> | ||||||
|  |             </ion-item> | ||||||
|  |             <ion-item *ngIf="siteChecked && !isBrowserSSO" margin-bottom> | ||||||
|  |                 <core-show-password item-content [name]="'password'"> | ||||||
|  |                     <ion-input class="core-ioninput-password" name="password" type="password" placeholder="{{ 'core.login.password' | translate }}" formControlName="password" core-show-password [clearOnEdit]="false"></ion-input> | ||||||
|  |                 </core-show-password> | ||||||
|  |             </ion-item> | ||||||
|  |             <div padding> | ||||||
|  |                 <ion-button block type="submit" [disabled]="siteChecked && !isBrowserSSO && !credForm.valid" class="core-login-login-button">{{ 'core.login.loginbutton' | translate }}</ion-button> | ||||||
|  |                 <input type="submit" className="core-submit-enter" /> <!-- Remove this once Ionic fixes this bug: https://github.com/ionic-team/ionic-framework/issues/19368 --> | ||||||
|  |             </div> | ||||||
|  | 
 | ||||||
|  |             <ng-container *ngIf="showScanQR"> | ||||||
|  |                 <div class="core-login-site-qrcode-separator">{{ 'core.login.or' | translate }}</div> | ||||||
|  |                 <ion-item class="core-login-site-qrcode" no-lines> | ||||||
|  |                     <ion-button block color="light" margin-top icon-start text-wrap (click)="showInstructionsAndScanQR()"> | ||||||
|  |                         <core-icon name="fa-qrcode" aria-hidden="true"></core-icon> | ||||||
|  |                         {{ 'core.scanqr' | translate }} | ||||||
|  |                     </ion-button> | ||||||
|  |                 </ion-item> | ||||||
|  |             </ng-container> | ||||||
|  |         </form> | ||||||
|  | 
 | ||||||
|  |         <!-- Forgotten password button. --> | ||||||
|  |         <ion-list no-lines *ngIf="showForgottenPassword" class="core-login-forgotten-password"> | ||||||
|  |             <ion-item text-center text-wrap (click)="forgottenPassword()" detail-none> | ||||||
|  |                 {{ 'core.login.forgotten' | translate }} | ||||||
|  |             </ion-item> | ||||||
|  |         </ion-list> | ||||||
|  | 
 | ||||||
|  |         <ion-list *ngIf="identityProviders && identityProviders.length" padding-top class="core-login-identity-providers"> | ||||||
|  |             <ion-item text-wrap no-lines><h3 class="item-heading">{{ 'core.login.potentialidps' | translate }}</h3></ion-item> | ||||||
|  |             <ion-item *ngFor="let provider of identityProviders" text-wrap class="core-oauth-icon" (click)="oauthClicked(provider)" title="{{provider.name}}"> | ||||||
|  |                 <img [src]="provider.iconurl" alt="" width="32" height="32" item-start> | ||||||
|  |                 {{provider.name}} | ||||||
|  |             </ion-item> | ||||||
|  |         </ion-list> | ||||||
|  | 
 | ||||||
|  |         <ion-list *ngIf="canSignup" padding-top class="core-login-sign-up"> | ||||||
|  |             <ion-item text-wrap no-lines><h3 class="item-heading">{{ 'core.login.firsttime' | translate }}</h3></ion-item> | ||||||
|  |             <ion-item no-lines text-wrap *ngIf="authInstructions"> | ||||||
|  |                 <p><core-format-text [text]="authInstructions" [filter]="false"></core-format-text></p> | ||||||
|  |             </ion-item> | ||||||
|  |             <ion-button block color="light" (onClick)="signup()">{{ 'core.login.startsignup' | translate }}</ion-button> | ||||||
|  |         </ion-list> | ||||||
|  |     </core-loading> | ||||||
|  | </ion-content> | ||||||
							
								
								
									
										336
									
								
								src/app/core/login/pages/credentials/credentials.page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										336
									
								
								src/app/core/login/pages/credentials/credentials.page.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,336 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Component, OnInit, OnDestroy, ViewChild, ElementRef } from '@angular/core'; | ||||||
|  | import { ActivatedRoute } from '@angular/router'; | ||||||
|  | import { FormBuilder, FormGroup, Validators } from '@angular/forms'; | ||||||
|  | import { NavController } from '@ionic/angular'; | ||||||
|  | 
 | ||||||
|  | import { CoreApp } from '@services/app'; | ||||||
|  | import { CoreSites } from '@services/sites'; | ||||||
|  | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { CoreLoginHelper, CoreLoginHelperProvider } from '@core/login/services/helper'; | ||||||
|  | import CoreConfigConstants from '@app/config.json'; | ||||||
|  | import { Translate } from '@singletons/core.singletons'; | ||||||
|  | import { CoreSiteIdentityProvider, CoreSitePublicConfigResponse } from '@/app/classes/site'; | ||||||
|  | import { CoreEvents, CoreEventsProvider } from '@/app/services/events'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Page that displays a "splash screen" while the app is being initialized. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'page-core-login-credentials', | ||||||
|  |     templateUrl: 'credentials.html', | ||||||
|  | }) | ||||||
|  | export class CoreLoginCredentialsPage implements OnInit, OnDestroy { | ||||||
|  | 
 | ||||||
|  |     @ViewChild('credentialsForm') formElement?: ElementRef; | ||||||
|  | 
 | ||||||
|  |     credForm!: FormGroup; | ||||||
|  |     siteUrl!: string; | ||||||
|  |     siteChecked = false; | ||||||
|  |     siteName?: string; | ||||||
|  |     logoUrl?: string; | ||||||
|  |     authInstructions?: string; | ||||||
|  |     canSignup?: boolean; | ||||||
|  |     identityProviders?: CoreSiteIdentityProvider[]; | ||||||
|  |     pageLoaded = false; | ||||||
|  |     isBrowserSSO = false; | ||||||
|  |     isFixedUrlSet = false; | ||||||
|  |     showForgottenPassword = true; | ||||||
|  |     showScanQR: boolean; | ||||||
|  | 
 | ||||||
|  |     protected siteConfig?: CoreSitePublicConfigResponse; | ||||||
|  |     protected eventThrown = false; | ||||||
|  |     protected viewLeft = false; | ||||||
|  |     protected siteId?: string; | ||||||
|  |     protected urlToOpen?: string; | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         protected fb: FormBuilder, | ||||||
|  |         protected route: ActivatedRoute, | ||||||
|  |         protected navCtrl: NavController, | ||||||
|  |     ) { | ||||||
|  | 
 | ||||||
|  |         const canScanQR = CoreUtils.instance.canScanQR(); | ||||||
|  |         if (canScanQR) { | ||||||
|  |             if (typeof CoreConfigConstants['displayqroncredentialscreen'] == 'undefined') { | ||||||
|  |                 this.showScanQR = CoreLoginHelper.instance.isFixedUrlSet(); | ||||||
|  |             } else { | ||||||
|  |                 this.showScanQR = !!CoreConfigConstants['displayqroncredentialscreen']; | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             this.showScanQR = false; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize the component. | ||||||
|  |      */ | ||||||
|  |     ngOnInit(): void { | ||||||
|  |         this.route.queryParams.subscribe(params => { | ||||||
|  |             this.siteUrl = params['siteUrl']; | ||||||
|  |             this.siteName = params['siteName'] || undefined; | ||||||
|  |             this.logoUrl = !CoreConfigConstants.forceLoginLogo && params['logoUrl'] || undefined; | ||||||
|  |             this.siteConfig = params['siteConfig']; | ||||||
|  |             this.urlToOpen = params['urlToOpen']; | ||||||
|  | 
 | ||||||
|  |             this.credForm = this.fb.group({ | ||||||
|  |                 username: [params['username'] || '', Validators.required], | ||||||
|  |                 password: ['', Validators.required], | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         this.treatSiteConfig(); | ||||||
|  |         this.isFixedUrlSet = CoreLoginHelper.instance.isFixedUrlSet(); | ||||||
|  | 
 | ||||||
|  |         if (this.isFixedUrlSet) { | ||||||
|  |             // Fixed URL, we need to check if it uses browser SSO login.
 | ||||||
|  |             this.checkSite(this.siteUrl); | ||||||
|  |         } else { | ||||||
|  |             this.siteChecked = true; | ||||||
|  |             this.pageLoaded = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Check if a site uses local_mobile, requires SSO login, etc. | ||||||
|  |      * This should be used only if a fixed URL is set, otherwise this check is already performed in CoreLoginSitePage. | ||||||
|  |      * | ||||||
|  |      * @param siteUrl Site URL to check. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     protected async checkSite(siteUrl: string): Promise<void> { | ||||||
|  |         this.pageLoaded = false; | ||||||
|  | 
 | ||||||
|  |         // If the site is configured with http:// protocol we force that one, otherwise we use default mode.
 | ||||||
|  |         const protocol = siteUrl.indexOf('http://') === 0 ? 'http://' : undefined; | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const result = await CoreSites.instance.checkSite(siteUrl, protocol); | ||||||
|  | 
 | ||||||
|  |             this.siteChecked = true; | ||||||
|  |             this.siteUrl = result.siteUrl; | ||||||
|  | 
 | ||||||
|  |             this.siteConfig = result.config; | ||||||
|  |             this.treatSiteConfig(); | ||||||
|  | 
 | ||||||
|  |             if (result && result.warning) { | ||||||
|  |                 CoreDomUtils.instance.showErrorModal(result.warning, true, 4000); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (CoreLoginHelper.instance.isSSOLoginNeeded(result.code)) { | ||||||
|  |                 // SSO. User needs to authenticate in a browser.
 | ||||||
|  |                 this.isBrowserSSO = true; | ||||||
|  | 
 | ||||||
|  |                 // Check that there's no SSO authentication ongoing and the view hasn't changed.
 | ||||||
|  |                 if (!CoreApp.instance.isSSOAuthenticationOngoing() && !this.viewLeft) { | ||||||
|  |                     CoreLoginHelper.instance.confirmAndOpenBrowserForSSOLogin( | ||||||
|  |                         result.siteUrl, | ||||||
|  |                         result.code, | ||||||
|  |                         result.service, | ||||||
|  |                         result.config?.launchurl, | ||||||
|  |                     ); | ||||||
|  |                 } | ||||||
|  |             } else { | ||||||
|  |                 this.isBrowserSSO = false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } catch (error) { | ||||||
|  |             CoreDomUtils.instance.showErrorModal(error); | ||||||
|  |         } finally { | ||||||
|  |             this.pageLoaded = true; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Treat the site configuration (if it exists). | ||||||
|  |      */ | ||||||
|  |     protected treatSiteConfig(): void { | ||||||
|  |         if (this.siteConfig) { | ||||||
|  |             this.siteName = CoreConfigConstants.sitename ? CoreConfigConstants.sitename : this.siteConfig.sitename; | ||||||
|  |             this.logoUrl = CoreLoginHelper.instance.getLogoUrl(this.siteConfig); | ||||||
|  |             this.authInstructions = this.siteConfig.authinstructions || Translate.instance.instant('core.login.loginsteps'); | ||||||
|  | 
 | ||||||
|  |             const disabledFeatures = CoreLoginHelper.instance.getDisabledFeatures(this.siteConfig); | ||||||
|  |             this.identityProviders = CoreLoginHelper.instance.getValidIdentityProviders(this.siteConfig, disabledFeatures); | ||||||
|  |             this.canSignup = this.siteConfig.registerauth == 'email' && | ||||||
|  |                     !CoreLoginHelper.instance.isEmailSignupDisabled(this.siteConfig, disabledFeatures); | ||||||
|  |             this.showForgottenPassword = !CoreLoginHelper.instance.isForgottenPasswordDisabled(this.siteConfig, disabledFeatures); | ||||||
|  | 
 | ||||||
|  |             if (!this.eventThrown && !this.viewLeft) { | ||||||
|  |                 this.eventThrown = true; | ||||||
|  |                 CoreEvents.instance.trigger(CoreEventsProvider.LOGIN_SITE_CHECKED, { config: this.siteConfig }); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             this.authInstructions = undefined; | ||||||
|  |             this.canSignup = false; | ||||||
|  |             this.identityProviders = []; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Tries to authenticate the user. | ||||||
|  |      * | ||||||
|  |      * @param e Event. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async login(e?: Event): Promise<void> { | ||||||
|  |         if (e) { | ||||||
|  |             e.preventDefault(); | ||||||
|  |             e.stopPropagation(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         CoreApp.instance.closeKeyboard(); | ||||||
|  | 
 | ||||||
|  |         // Get input data.
 | ||||||
|  |         const siteUrl = this.siteUrl; | ||||||
|  |         const username = this.credForm.value.username; | ||||||
|  |         const password = this.credForm.value.password; | ||||||
|  | 
 | ||||||
|  |         if (!this.siteChecked || this.isBrowserSSO) { | ||||||
|  |             // Site wasn't checked (it failed) or a previous check determined it was SSO. Let's check again.
 | ||||||
|  |             await this.checkSite(siteUrl); | ||||||
|  | 
 | ||||||
|  |             if (!this.isBrowserSSO) { | ||||||
|  |                 // Site doesn't use browser SSO, throw app's login again.
 | ||||||
|  |                 return this.login(); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!username) { | ||||||
|  |             CoreDomUtils.instance.showErrorModal('core.login.usernamerequired', true); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         if (!password) { | ||||||
|  |             CoreDomUtils.instance.showErrorModal('core.login.passwordrequired', true); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!CoreApp.instance.isOnline()) { | ||||||
|  |             CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const modal = await CoreDomUtils.instance.showModalLoading(); | ||||||
|  | 
 | ||||||
|  |         // Start the authentication process.
 | ||||||
|  |         try { | ||||||
|  |             const data = await CoreSites.instance.getUserToken(siteUrl, username, password); | ||||||
|  | 
 | ||||||
|  |             const id = await CoreSites.instance.newSite(data.siteUrl, data.token, data.privateToken); | ||||||
|  | 
 | ||||||
|  |             // Reset fields so the data is not in the view anymore.
 | ||||||
|  |             this.credForm.controls['username'].reset(); | ||||||
|  |             this.credForm.controls['password'].reset(); | ||||||
|  | 
 | ||||||
|  |             this.siteId = id; | ||||||
|  | 
 | ||||||
|  |             await CoreLoginHelper.instance.goToSiteInitialPage(undefined, undefined, undefined, undefined, this.urlToOpen); | ||||||
|  |         } catch (error) { | ||||||
|  |             CoreLoginHelper.instance.treatUserTokenError(siteUrl, error, username, password); | ||||||
|  | 
 | ||||||
|  |             if (error.loggedout) { | ||||||
|  |                 this.navCtrl.navigateRoot('/login/sites'); | ||||||
|  |             } else if (error.errorcode == 'forcepasswordchangenotice') { | ||||||
|  |                 // Reset password field.
 | ||||||
|  |                 this.credForm.controls.password.reset(); | ||||||
|  |             } | ||||||
|  |         } finally { | ||||||
|  |             modal.dismiss(); | ||||||
|  | 
 | ||||||
|  |             CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Forgotten password button clicked. | ||||||
|  |      */ | ||||||
|  |     forgottenPassword(): void { | ||||||
|  |         CoreLoginHelper.instance.forgottenPasswordClicked( | ||||||
|  |             this.navCtrl, | ||||||
|  |             this.siteUrl, | ||||||
|  |             this.credForm.value.username, | ||||||
|  |             this.siteConfig, | ||||||
|  |         ); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * An OAuth button was clicked. | ||||||
|  |      * | ||||||
|  |      * @param provider The provider that was clicked. | ||||||
|  |      */ | ||||||
|  |     oauthClicked(provider: CoreSiteIdentityProvider): void { | ||||||
|  |         if (!CoreLoginHelper.instance.openBrowserForOAuthLogin(this.siteUrl, provider, this.siteConfig?.launchurl)) { | ||||||
|  |             CoreDomUtils.instance.showErrorModal('Invalid data.'); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Signup button was clicked. | ||||||
|  |      */ | ||||||
|  |     signup(): void { | ||||||
|  |         // @todo Go to signup.
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show instructions and scan QR code. | ||||||
|  |      */ | ||||||
|  |     showInstructionsAndScanQR(): void { | ||||||
|  |         // Show some instructions first.
 | ||||||
|  |         CoreDomUtils.instance.showAlertWithOptions({ | ||||||
|  |             header: Translate.instance.instant('core.login.faqwhereisqrcode'), | ||||||
|  |             message: Translate.instance.instant( | ||||||
|  |                 'core.login.faqwhereisqrcodeanswer', | ||||||
|  |                 { $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML }, | ||||||
|  |             ), | ||||||
|  |             buttons: [ | ||||||
|  |                 { | ||||||
|  |                     text: Translate.instance.instant('core.cancel'), | ||||||
|  |                     role: 'cancel', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     text: Translate.instance.instant('core.next'), | ||||||
|  |                     handler: (): void => { | ||||||
|  |                         this.scanQR(); | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Scan a QR code and put its text in the URL input. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async scanQR(): Promise<void> { | ||||||
|  |         // @todo Scan for a QR code.
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * View destroyed. | ||||||
|  |      */ | ||||||
|  |     ngOnDestroy(): void { | ||||||
|  |         this.viewLeft = true; | ||||||
|  |         CoreEvents.instance.trigger(CoreEventsProvider.LOGIN_SITE_UNCHECKED, { config: this.siteConfig }, this.siteId); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -13,7 +13,7 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component, OnInit } from '@angular/core'; | import { Component, OnInit } from '@angular/core'; | ||||||
| import { Router } from '@angular/router'; | import { NavController } from '@ionic/angular'; | ||||||
| 
 | 
 | ||||||
| import { CoreApp } from '@services/app'; | import { CoreApp } from '@services/app'; | ||||||
| import { CoreInit } from '@services/init'; | import { CoreInit } from '@services/init'; | ||||||
| @ -29,49 +29,49 @@ import { SplashScreen } from '@singletons/core.singletons'; | |||||||
| }) | }) | ||||||
| export class CoreLoginInitPage implements OnInit { | export class CoreLoginInitPage implements OnInit { | ||||||
| 
 | 
 | ||||||
|     constructor(protected router: Router) {} |     constructor(protected navCtrl: NavController) {} | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|      * 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.
 |  | ||||||
|             const redirectData = CoreApp.instance.getRedirect(); |  | ||||||
|             if (redirectData.siteId) { |  | ||||||
|                 // Unset redirect data.
 |  | ||||||
|                 CoreApp.instance.storeRedirect('', '', {}); |  | ||||||
| 
 | 
 | ||||||
|                 // Only accept the redirect if it was stored less than 20 seconds ago.
 |         // Check if there was a pending redirect.
 | ||||||
|                 if (Date.now() - redirectData.timemodified < 20000) { |         const redirectData = CoreApp.instance.getRedirect(); | ||||||
|                     // if (redirectData.siteId != CoreConstants.NO_SITE_ID) {
 |         if (redirectData.siteId) { | ||||||
|                     //     // The redirect is pointing to a site, load it.
 |             // Unset redirect data.
 | ||||||
|                     //     return this.sitesProvider.loadSite(redirectData.siteId, redirectData.page, redirectData.params)
 |             CoreApp.instance.storeRedirect('', '', {}); | ||||||
|                     //             .then((loggedIn) => {
 |  | ||||||
| 
 | 
 | ||||||
|                     //         if (loggedIn) {
 |             // Only accept the redirect if it was stored less than 20 seconds ago.
 | ||||||
|                     //             return this.loginHelper.goToSiteInitialPage(this.navCtrl, redirectData.page, redirectData.params,
 |             if (redirectData.timemodified && Date.now() - redirectData.timemodified < 20000) { | ||||||
|                     //                     { animate: false });
 |                 // if (redirectData.siteId != CoreConstants.NO_SITE_ID) {
 | ||||||
|                     //         }
 |                 //     // The redirect is pointing to a site, load it.
 | ||||||
|                     //     }).catch(() => {
 |                 //     return this.sitesProvider.loadSite(redirectData.siteId, redirectData.page, redirectData.params)
 | ||||||
|                     //         // Site doesn't exist.
 |                 //             .then((loggedIn) => {
 | ||||||
|                     //         return this.loadPage();
 | 
 | ||||||
|                     //     });
 |                 //         if (loggedIn) {
 | ||||||
|                     // } else {
 |                 //             return this.loginHelper.goToSiteInitialPage(this.navCtrl, redirectData.page, redirectData.params,
 | ||||||
|                     //     // No site to load, open the page.
 |                 //                     { animate: false });
 | ||||||
|                     //     return this.loginHelper.goToNoSitePage(this.navCtrl, redirectData.page, redirectData.params);
 |                 //         }
 | ||||||
|                     // }
 |                 //     }).catch(() => {
 | ||||||
|                 } |                 //         // Site doesn't exist.
 | ||||||
|  |                 //         return this.loadPage();
 | ||||||
|  |                 //     });
 | ||||||
|  |                 // } else {
 | ||||||
|  |                 //     // No site to load, open the page.
 | ||||||
|  |                 //     return this.loginHelper.goToNoSitePage(this.navCtrl, redirectData.page, redirectData.params);
 | ||||||
|  |                 // }
 | ||||||
|             } |             } | ||||||
|  |         } | ||||||
| 
 | 
 | ||||||
|             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); | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -90,6 +90,7 @@ export class CoreLoginInitPage implements OnInit { | |||||||
|         //     return this.loginHelper.goToSiteInitialPage();
 |         //     return this.loginHelper.goToSiteInitialPage();
 | ||||||
|         // }
 |         // }
 | ||||||
| 
 | 
 | ||||||
|         await this.router.navigate(['/login/site']); |         await this.navCtrl.navigateRoot('/login/sites'); | ||||||
|     } |     } | ||||||
|  | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -1,3 +1,105 @@ | |||||||
| <ion-content> | <ion-header> | ||||||
|     {{ 'core.login.yourenteredsite' | translate }} |     <ion-toolbar> | ||||||
|  |         <ion-buttons slot="start"> | ||||||
|  |             <ion-back-button></ion-back-button> | ||||||
|  |         </ion-buttons> | ||||||
|  | 
 | ||||||
|  |         <ion-title>{{ 'core.login.connecttomoodle' | translate }}</ion-title> | ||||||
|  | 
 | ||||||
|  |         <ion-buttons slot="end"> | ||||||
|  |             <!-- @todo: Settings button. --> | ||||||
|  |         </ion-buttons> | ||||||
|  |     </ion-toolbar> | ||||||
|  | </ion-header> | ||||||
|  | <ion-content padding> | ||||||
|  |     <div> | ||||||
|  |         <div text-center padding margin-bottom [class.hidden]="hasSites || enteredSiteUrl" class="core-login-site-logo"> | ||||||
|  |             <img src="assets/img/login_logo.png" class="avatar-full login-logo" role="presentation"> | ||||||
|  |         </div> | ||||||
|  |         <form ion-list [formGroup]="siteForm" (ngSubmit)="connect($event, siteForm.value.siteUrl)" *ngIf="!fixedSites" #siteFormEl> | ||||||
|  |             <!-- Form to input the site URL if there are no fixed sites. --> | ||||||
|  |             <ng-container *ngIf="siteSelector == 'url'"> | ||||||
|  |                 <ion-item> | ||||||
|  |                     <ion-label position="stacked"><h2>{{ 'core.login.siteaddress' | translate }}</h2></ion-label> | ||||||
|  |                     <ion-input name="url" type="url" placeholder="{{ 'core.login.siteaddressplaceholder' | translate }}" formControlName="siteUrl" [core-auto-focus]="showKeyboard && !showScanQR"></ion-input> | ||||||
|  |                 </ion-item> | ||||||
|  |             </ng-container> | ||||||
|  |             <ng-container *ngIf="siteSelector != 'url'"> | ||||||
|  |                 <ion-item> | ||||||
|  |                     <ion-label position="stacked"><h2>{{ 'core.login.siteaddress' | translate }}</h2></ion-label> | ||||||
|  |                     <ion-input name="url" placeholder="{{ 'core.login.siteaddressplaceholder' | translate }}" formControlName="siteUrl" [core-auto-focus]="showKeyboard && !showScanQR" (ionChange)="searchSite($event, siteForm.value.siteUrl)"></ion-input> | ||||||
|  |                 </ion-item> | ||||||
|  | 
 | ||||||
|  |                 <ion-list [class.hidden]="!hasSites && !enteredSiteUrl" class="core-login-site-list"> | ||||||
|  |                     <ion-item no-lines class="core-login-site-list-title"><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item> | ||||||
|  |                     <ion-item *ngIf="enteredSiteUrl" (click)="connect($event, enteredSiteUrl.url)" [attr.aria-label]="'core.login.connect' | translate" detail-push class="core-login-entered-site"> | ||||||
|  |                         <ion-thumbnail item-start> | ||||||
|  |                             <core-icon name="fa-pencil"></core-icon> | ||||||
|  |                         </ion-thumbnail> | ||||||
|  |                         <h2 text-wrap>{{ 'core.login.yourenteredsite' | translate }}</h2> | ||||||
|  |                         <p>{{enteredSiteUrl.noProtocolUrl}}</p> | ||||||
|  |                     </ion-item> | ||||||
|  | 
 | ||||||
|  |                     <div class="core-login-site-list-found" [class.hidden]="!hasSites" [class.dimmed]="loadingSites"> | ||||||
|  |                         <div *ngIf="loadingSites" class="core-login-site-list-loading"><ion-spinner></ion-spinner></div> | ||||||
|  |                         <ion-item *ngFor="let site of sites" (click)="connect($event, site.url, site)" [attr.aria-label]="site.name" detail-push> | ||||||
|  |                             <ion-thumbnail item-start *ngIf="siteFinderSettings.displayimage"> | ||||||
|  |                                 <img [src]="site.imageurl" *ngIf="site.imageurl" onError="this.src='assets/icon/icon.png'"> | ||||||
|  |                                 <img src="assets/icon/icon.png" *ngIf="!site.imageurl" class="core-login-default-icon"> | ||||||
|  |                             </ion-thumbnail> | ||||||
|  |                             <h2 *ngIf="site.title" text-wrap>{{site.title}}</h2> | ||||||
|  |                             <p *ngIf="site.noProtocolUrl">{{site.noProtocolUrl}}</p> | ||||||
|  |                             <p *ngIf="site.location">{{site.location}}</p> | ||||||
|  |                         </ion-item> | ||||||
|  |                     </div> | ||||||
|  |                 </ion-list> | ||||||
|  | 
 | ||||||
|  |                 <div *ngIf="!hasSites && loadingSites" class="core-login-site-nolist-loading"><ion-spinner></ion-spinner></div> | ||||||
|  |             </ng-container> | ||||||
|  | 
 | ||||||
|  |             <ion-item *ngIf="siteSelector == 'url'" no-lines> | ||||||
|  |                 <ion-button block [disabled]="!siteForm.valid" text-wrap>{{ 'core.login.connect' | translate }}</ion-button> | ||||||
|  |             </ion-item> | ||||||
|  |         </form> | ||||||
|  | 
 | ||||||
|  |         <ng-container *ngIf="fixedSites"> | ||||||
|  |             <!-- Pick the site from a list of fixed sites. --> | ||||||
|  |             <ion-list *ngIf="siteSelector == 'list'"> | ||||||
|  |                 <ion-item no-lines><h2 class="item-heading">{{ 'core.login.selectsite' | translate }}</h2></ion-item> | ||||||
|  |                 <ion-searchbar *ngIf="fixedSites.length > 4" [(ngModel)]="filter" (ionInput)="filterChanged($event)" (ionCancel)="filterChanged()" [placeholder]="'core.login.findyoursite' | translate"></ion-searchbar> | ||||||
|  |                 <ion-item *ngFor="let site of filteredSites" (click)="connect($event, site.url)" [title]="site.name" detail-push> | ||||||
|  |                     <ion-thumbnail item-start *ngIf="siteFinderSettings.displayimage"> | ||||||
|  |                         <img [src]="site.imageurl" *ngIf="site.imageurl" onError="this.src='assets/icon/icon.png'"> | ||||||
|  |                         <img src="assets/icon/icon.png" *ngIf="!site.imageurl" class="core-login-default-icon"> | ||||||
|  |                     </ion-thumbnail> | ||||||
|  |                     <h2 *ngIf="site.title" text-wrap>{{site.title}}</h2> | ||||||
|  |                     <p *ngIf="site.noProtocolUrl">{{site.noProtocolUrl}}</p> | ||||||
|  |                     <p *ngIf="site.location">{{site.location}}</p> | ||||||
|  |                 </ion-item> | ||||||
|  |             </ion-list> | ||||||
|  | 
 | ||||||
|  |             <!-- Display them using buttons. --> | ||||||
|  |             <div *ngIf="siteSelector == 'buttons'"> | ||||||
|  |                 <p class="padding no-padding-bottom">{{ 'core.login.selectsite' | translate }}</p> | ||||||
|  |                 <ion-button *ngFor="let site of fixedSites" text-wrap block (click)="connect($event, site.url)" [title]="site.name" margin-bottom>{{site.title}}</ion-button> | ||||||
|  |             </div> | ||||||
|  |         </ng-container> | ||||||
|  | 
 | ||||||
|  |         <ng-container *ngIf="showScanQR && !hasSites && !enteredSiteUrl"> | ||||||
|  |             <div class="core-login-site-qrcode-separator">{{ 'core.login.or' | translate }}</div> | ||||||
|  |             <ion-item class="core-login-site-qrcode" no-lines> | ||||||
|  |                 <ion-button block color="light" margin-top icon-start (click)="showInstructionsAndScanQR()" text-wrap> | ||||||
|  |                     <core-icon name="fa-qrcode" aria-hidden="true"></core-icon> | ||||||
|  |                     {{ 'core.scanqr' | translate }} | ||||||
|  |                 </ion-button> | ||||||
|  |             </ion-item> | ||||||
|  |         </ng-container> | ||||||
|  | 
 | ||||||
|  |         <!-- Help. --> | ||||||
|  |         <ion-list no-lines margin-top> | ||||||
|  |             <ion-item text-center text-wrap class="core-login-need-help" (click)="showHelp()" detail-none> | ||||||
|  |                 {{ 'core.needhelp' | translate }} | ||||||
|  |             </ion-item> | ||||||
|  |         </ion-list> | ||||||
|  |     </div> | ||||||
| </ion-content> | </ion-content> | ||||||
|  | |||||||
| @ -12,7 +12,23 @@ | |||||||
| // See the License for the specific language governing permissions and
 | // See the License for the specific language governing permissions and
 | ||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Component, OnInit } from '@angular/core'; | import { Component, OnInit, ViewChild, ElementRef } from '@angular/core'; | ||||||
|  | import { ActivatedRoute } from '@angular/router'; | ||||||
|  | import { FormBuilder, FormGroup, ValidatorFn, AbstractControl, ValidationErrors } from '@angular/forms'; | ||||||
|  | 
 | ||||||
|  | import { CoreApp } from '@services/app'; | ||||||
|  | import { CoreConfig } from '@services/config'; | ||||||
|  | import { CoreSites, CoreSiteCheckResponse, CoreLoginSiteInfo, CoreSitesDemoSiteData } from '@services/sites'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { CoreLoginHelper, CoreLoginHelperProvider } from '@core/login/services/helper'; | ||||||
|  | import { CoreSite } from '@classes/site'; | ||||||
|  | import { CoreError } from '@classes/errors/error'; | ||||||
|  | import CoreConfigConstants from '@app/config.json'; | ||||||
|  | import { Translate } from '@singletons/core.singletons'; | ||||||
|  | import { CoreUrl } from '@singletons/url'; | ||||||
|  | import { CoreUrlUtils } from '@/app/services/utils/url'; | ||||||
|  | import { NavController } from '@ionic/angular'; | ||||||
| 
 | 
 | ||||||
| /** | /** | ||||||
|  * Page that displays a "splash screen" while the app is being initialized. |  * Page that displays a "splash screen" while the app is being initialized. | ||||||
| @ -24,11 +40,476 @@ import { Component, OnInit } from '@angular/core'; | |||||||
| }) | }) | ||||||
| export class CoreLoginSitePage implements OnInit { | export class CoreLoginSitePage implements OnInit { | ||||||
| 
 | 
 | ||||||
|  |     @ViewChild('siteFormEl') formElement?: ElementRef; | ||||||
|  | 
 | ||||||
|  |     siteForm: FormGroup; | ||||||
|  |     fixedSites?: CoreLoginSiteInfoExtended[]; | ||||||
|  |     filteredSites?: CoreLoginSiteInfoExtended[]; | ||||||
|  |     siteSelector = 'sitefinder'; | ||||||
|  |     showKeyboard = false; | ||||||
|  |     filter = ''; | ||||||
|  |     sites: CoreLoginSiteInfoExtended[] = []; | ||||||
|  |     hasSites = false; | ||||||
|  |     loadingSites = false; | ||||||
|  |     searchFunction: (search: string) => void; | ||||||
|  |     showScanQR: boolean; | ||||||
|  |     enteredSiteUrl?: CoreLoginSiteInfoExtended; | ||||||
|  |     siteFinderSettings: SiteFinderSettings; | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         protected route: ActivatedRoute, | ||||||
|  |         protected formBuilder: FormBuilder, | ||||||
|  |         protected navCtrl: NavController, | ||||||
|  |     ) { | ||||||
|  | 
 | ||||||
|  |         let url = ''; | ||||||
|  |         this.siteSelector = CoreConfigConstants.multisitesdisplay; | ||||||
|  | 
 | ||||||
|  |         const siteFinderSettings: Partial<SiteFinderSettings> = CoreConfigConstants['sitefindersettings'] || {}; | ||||||
|  |         this.siteFinderSettings = { | ||||||
|  |             displaysitename: true, | ||||||
|  |             displayimage: true, | ||||||
|  |             displayalias: true, | ||||||
|  |             displaycity: true, | ||||||
|  |             displaycountry: true, | ||||||
|  |             displayurl: true, | ||||||
|  |             ...siteFinderSettings, | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         // Load fixed sites if they're set.
 | ||||||
|  |         if (CoreLoginHelper.instance.hasSeveralFixedSites()) { | ||||||
|  |             url = this.initSiteSelector(); | ||||||
|  |         } else if (CoreConfigConstants.enableonboarding && !CoreApp.instance.isIOS() && !CoreApp.instance.isMac()) { | ||||||
|  |             this.initOnboarding(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.showScanQR = CoreUtils.instance.canScanQR() && (typeof CoreConfigConstants['displayqronsitescreen'] == 'undefined' || | ||||||
|  |             !!CoreConfigConstants['displayqronsitescreen']); | ||||||
|  | 
 | ||||||
|  |         this.siteForm = this.formBuilder.group({ | ||||||
|  |             siteUrl: [url, this.moodleUrlValidator()], | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         this.searchFunction = CoreUtils.instance.debounce(async (search: string) => { | ||||||
|  |             search = search.trim(); | ||||||
|  | 
 | ||||||
|  |             if (search.length >= 3) { | ||||||
|  |                 // Update the sites list.
 | ||||||
|  |                 const sites = await CoreSites.instance.findSites(search); | ||||||
|  | 
 | ||||||
|  |                 // Add UI tweaks.
 | ||||||
|  |                 this.sites = this.extendCoreLoginSiteInfo(<CoreLoginSiteInfoExtended[]> sites); | ||||||
|  | 
 | ||||||
|  |                 this.hasSites = !!this.sites.length; | ||||||
|  |             } else { | ||||||
|  |                 // Not reseting the array to allow animation to be displayed.
 | ||||||
|  |                 this.hasSites = false; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             this.loadingSites = false; | ||||||
|  |         }, 1000); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|     /** |     /** | ||||||
|      * Initialize the component. |      * Initialize the component. | ||||||
|      */ |      */ | ||||||
|     ngOnInit(): void { |     ngOnInit(): void { | ||||||
|         //
 |         this.route.queryParams.subscribe(params => { | ||||||
|  |             this.showKeyboard = !!params['showKeyboard']; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize the site selector. | ||||||
|  |      * | ||||||
|  |      * @return URL of the first site. | ||||||
|  |      */ | ||||||
|  |     protected initSiteSelector(): string { | ||||||
|  |         // Deprecate listnourl on 3.9.3, remove this block on the following release.
 | ||||||
|  |         if (this.siteSelector == 'listnourl') { | ||||||
|  |             this.siteSelector = 'list'; | ||||||
|  |             this.siteFinderSettings.displayurl = false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.fixedSites = this.extendCoreLoginSiteInfo(<CoreLoginSiteInfoExtended[]> CoreLoginHelper.instance.getFixedSites()); | ||||||
|  | 
 | ||||||
|  |         // Do not show images if none are set.
 | ||||||
|  |         if (!this.fixedSites.some((site) => !!site.imageurl)) { | ||||||
|  |             this.siteFinderSettings.displayimage = false; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Autoselect if not defined.
 | ||||||
|  |         if (this.siteSelector != 'list' && this.siteSelector != 'buttons') { | ||||||
|  |             this.siteSelector = this.fixedSites.length > 3 ? 'list' : 'buttons'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.filteredSites = this.fixedSites; | ||||||
|  | 
 | ||||||
|  |         return this.fixedSites[0].url; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize and show onboarding if needed. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     protected async initOnboarding(): Promise<void> { | ||||||
|  |         const onboardingDone = await CoreConfig.instance.get(CoreLoginHelperProvider.ONBOARDING_DONE, false); | ||||||
|  | 
 | ||||||
|  |         if (!onboardingDone) { | ||||||
|  |             // Check onboarding.
 | ||||||
|  |             this.showOnboarding(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Extend info of Login Site Info to get UI tweaks. | ||||||
|  |      * | ||||||
|  |      * @param  sites Sites list. | ||||||
|  |      * @return Sites list with extended info. | ||||||
|  |      */ | ||||||
|  |     protected extendCoreLoginSiteInfo(sites: CoreLoginSiteInfoExtended[]): CoreLoginSiteInfoExtended[] { | ||||||
|  |         return sites.map((site) => { | ||||||
|  |             site.noProtocolUrl = this.siteFinderSettings.displayurl && site.url ? CoreUrl.removeProtocol(site.url) : ''; | ||||||
|  | 
 | ||||||
|  |             const name = this.siteFinderSettings.displaysitename ? site.name : ''; | ||||||
|  |             const alias = this.siteFinderSettings.displayalias && site.alias ? site.alias : ''; | ||||||
|  | 
 | ||||||
|  |             // Set title with parenthesis if both name and alias are present.
 | ||||||
|  |             site.title = name && alias ? name + ' (' + alias + ')' : name + alias; | ||||||
|  | 
 | ||||||
|  |             const country = this.siteFinderSettings.displaycountry && site.countrycode ? | ||||||
|  |                 CoreUtils.instance.getCountryName(site.countrycode) : ''; | ||||||
|  |             const city = this.siteFinderSettings.displaycity && site.city ? | ||||||
|  |                 site.city : ''; | ||||||
|  | 
 | ||||||
|  |             // Separate location with hiphen if both country and city are present.
 | ||||||
|  |             site.location = city && country ? city + ' - ' + country : city + country; | ||||||
|  | 
 | ||||||
|  |             return site; | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Validate Url. | ||||||
|  |      * | ||||||
|  |      * @return {ValidatorFn} Validation results. | ||||||
|  |      */ | ||||||
|  |     protected moodleUrlValidator(): ValidatorFn { | ||||||
|  |         return (control: AbstractControl): ValidationErrors | null => { | ||||||
|  |             const value = control.value.trim(); | ||||||
|  |             let valid = value.length >= 3 && CoreUrl.isValidMoodleUrl(value); | ||||||
|  | 
 | ||||||
|  |             if (!valid) { | ||||||
|  |                 const demo = !!CoreSites.instance.getDemoSiteData(value); | ||||||
|  | 
 | ||||||
|  |                 if (demo) { | ||||||
|  |                     valid = true; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             return valid ? null : { siteUrl: { value: control.value } }; | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show a help modal. | ||||||
|  |      */ | ||||||
|  |     showHelp(): void { | ||||||
|  |         // @todo
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show an onboarding modal. | ||||||
|  |      */ | ||||||
|  |     showOnboarding(): void { | ||||||
|  |         // @todo
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Try to connect to a site. | ||||||
|  |      * | ||||||
|  |      * @param e Event. | ||||||
|  |      * @param url The URL to connect to. | ||||||
|  |      * @param foundSite The site clicked, if any, from the found sites list. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async connect(e: Event, url: string, foundSite?: CoreLoginSiteInfoExtended): Promise<void> { | ||||||
|  |         e.preventDefault(); | ||||||
|  |         e.stopPropagation(); | ||||||
|  | 
 | ||||||
|  |         CoreApp.instance.closeKeyboard(); | ||||||
|  | 
 | ||||||
|  |         if (!url) { | ||||||
|  |             CoreDomUtils.instance.showErrorModal('core.login.siteurlrequired', true); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!CoreApp.instance.isOnline()) { | ||||||
|  |             CoreDomUtils.instance.showErrorModal('core.networkerrormsg', true); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         url = url.trim(); | ||||||
|  | 
 | ||||||
|  |         if (url.match(/^(https?:\/\/)?campus\.example\.edu/)) { | ||||||
|  |             this.showLoginIssue(null, new CoreError(Translate.instance.instant('core.login.errorexampleurl'))); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const siteData = CoreSites.instance.getDemoSiteData(url); | ||||||
|  | 
 | ||||||
|  |         if (siteData) { | ||||||
|  |             // It's a demo site.
 | ||||||
|  |             await this.loginDemoSite(siteData); | ||||||
|  | 
 | ||||||
|  |         } else { | ||||||
|  |             // Not a demo site.
 | ||||||
|  |             const modal = await CoreDomUtils.instance.showModalLoading(); | ||||||
|  | 
 | ||||||
|  |             let checkResult: CoreSiteCheckResponse; | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  |                 checkResult = await CoreSites.instance.checkSite(url); | ||||||
|  |             } catch (error) { | ||||||
|  |                 // Attempt guessing the domain if the initial check failed
 | ||||||
|  |                 const domain = CoreUrl.guessMoodleDomain(url); | ||||||
|  | 
 | ||||||
|  |                 if (domain && domain != url) { | ||||||
|  |                     try { | ||||||
|  |                         checkResult = await CoreSites.instance.checkSite(domain); | ||||||
|  |                     } catch (secondError) { | ||||||
|  |                         // Try to use the first error.
 | ||||||
|  |                         modal.dismiss(); | ||||||
|  | 
 | ||||||
|  |                         return this.showLoginIssue(url, error || secondError); | ||||||
|  |                     } | ||||||
|  |                 } else { | ||||||
|  |                     modal.dismiss(); | ||||||
|  | 
 | ||||||
|  |                     return this.showLoginIssue(url, error); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             await this.login(checkResult, foundSite); | ||||||
|  | 
 | ||||||
|  |             modal.dismiss(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Authenticate in a demo site. | ||||||
|  |      * | ||||||
|  |      * @param siteData Site data. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     protected async loginDemoSite(siteData: CoreSitesDemoSiteData): Promise<void> { | ||||||
|  |         const modal = await CoreDomUtils.instance.showModalLoading(); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const data = await CoreSites.instance.getUserToken(siteData.url, siteData.username, siteData.password); | ||||||
|  | 
 | ||||||
|  |             await CoreSites.instance.newSite(data.siteUrl, data.token, data.privateToken); | ||||||
|  | 
 | ||||||
|  |             CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true); | ||||||
|  | 
 | ||||||
|  |             return CoreLoginHelper.instance.goToSiteInitialPage(); | ||||||
|  |         } catch (error) { | ||||||
|  |             CoreLoginHelper.instance.treatUserTokenError(siteData.url, error, siteData.username, siteData.password); | ||||||
|  | 
 | ||||||
|  |             if (error.loggedout) { | ||||||
|  |                 this.navCtrl.navigateRoot('/login/sites'); | ||||||
|  |             } | ||||||
|  |         } finally { | ||||||
|  |             modal.dismiss(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Process login to a site. | ||||||
|  |      * | ||||||
|  |      * @param response Response obtained from the site check request. | ||||||
|  |      * @param foundSite The site clicked, if any, from the found sites list. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved after logging in. | ||||||
|  |      */ | ||||||
|  |     protected async login(response: CoreSiteCheckResponse, foundSite?: CoreLoginSiteInfoExtended): Promise<void> { | ||||||
|  |         await CoreUtils.instance.ignoreErrors(CoreSites.instance.checkApplication(response)); | ||||||
|  | 
 | ||||||
|  |         CoreDomUtils.instance.triggerFormSubmittedEvent(this.formElement, true); | ||||||
|  | 
 | ||||||
|  |         if (response.warning) { | ||||||
|  |             CoreDomUtils.instance.showErrorModal(response.warning, true, 4000); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (CoreLoginHelper.instance.isSSOLoginNeeded(response.code)) { | ||||||
|  |             // SSO. User needs to authenticate in a browser.
 | ||||||
|  |             CoreLoginHelper.instance.confirmAndOpenBrowserForSSOLogin( | ||||||
|  |                 response.siteUrl, | ||||||
|  |                 response.code, | ||||||
|  |                 response.service, | ||||||
|  |                 response.config?.launchurl, | ||||||
|  |             ); | ||||||
|  |         } else { | ||||||
|  |             const pageParams = { siteUrl: response.siteUrl, siteConfig: response.config }; | ||||||
|  |             if (foundSite) { | ||||||
|  |                 pageParams['siteName'] = foundSite.name; | ||||||
|  |                 pageParams['logoUrl'] = foundSite.imageurl; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // @todo Navigate to credentials.
 | ||||||
|  |             this.navCtrl.navigateForward('/login/credentials', { | ||||||
|  |                 queryParams: pageParams, | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show an error that aims people to solve the issue. | ||||||
|  |      * | ||||||
|  |      * @param url The URL the user was trying to connect to. | ||||||
|  |      * @param error Error to display. | ||||||
|  |      */ | ||||||
|  |     protected showLoginIssue(url: string | null, error: CoreError): void { | ||||||
|  |         let errorMessage = CoreDomUtils.instance.getErrorMessage(error); | ||||||
|  | 
 | ||||||
|  |         if (errorMessage == Translate.instance.instant('core.cannotconnecttrouble')) { | ||||||
|  |             const found = this.sites.find((site) => site.url == url); | ||||||
|  | 
 | ||||||
|  |             if (!found) { | ||||||
|  |                 errorMessage += ' ' + Translate.instance.instant('core.cannotconnectverify'); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let message = '<p>' + errorMessage + '</p>'; | ||||||
|  |         if (url) { | ||||||
|  |             const fullUrl = CoreUrlUtils.instance.isAbsoluteURL(url) ? url : 'https://' + url; | ||||||
|  |             message += '<p padding><a href="' + fullUrl + '" core-link>' + url + '</a></p>'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const buttons = [ | ||||||
|  |             { | ||||||
|  |                 text: Translate.instance.instant('core.needhelp'), | ||||||
|  |                 handler: (): void => { | ||||||
|  |                     this.showHelp(); | ||||||
|  |                 }, | ||||||
|  |             }, | ||||||
|  |             { | ||||||
|  |                 text: Translate.instance.instant('core.tryagain'), | ||||||
|  |                 role: 'cancel', | ||||||
|  |             }, | ||||||
|  |         ]; | ||||||
|  | 
 | ||||||
|  |         // @TODO: Remove CoreSite.MINIMUM_MOODLE_VERSION, not used on translations since 3.9.0.
 | ||||||
|  |         CoreDomUtils.instance.showAlertWithOptions({ | ||||||
|  |             header: Translate.instance.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }), | ||||||
|  |             message, | ||||||
|  |             buttons, | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * The filter has changed. | ||||||
|  |      * | ||||||
|  |      * @param event Received Event. | ||||||
|  |      */ | ||||||
|  |     // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
|  |     filterChanged(event: any): void { | ||||||
|  |         const newValue = event.target.value?.trim().toLowerCase(); | ||||||
|  |         if (!newValue || !this.fixedSites) { | ||||||
|  |             this.filteredSites = this.fixedSites; | ||||||
|  |         } else { | ||||||
|  |             this.filteredSites = this.fixedSites.filter((site) => | ||||||
|  |                 site.title.toLowerCase().indexOf(newValue) > -1 || site.noProtocolUrl.toLowerCase().indexOf(newValue) > -1 || | ||||||
|  |                 site.location.toLowerCase().indexOf(newValue) > -1); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Find a site on the backend. | ||||||
|  |      * | ||||||
|  |      * @param e Event. | ||||||
|  |      * @param search Text to search. | ||||||
|  |      */ | ||||||
|  |     searchSite(e: Event, search: string): void { | ||||||
|  |         this.loadingSites = true; | ||||||
|  | 
 | ||||||
|  |         search = search.trim(); | ||||||
|  | 
 | ||||||
|  |         if (this.siteForm.valid && search.length >= 3) { | ||||||
|  |             this.enteredSiteUrl = { | ||||||
|  |                 url: search, | ||||||
|  |                 name: 'connect', | ||||||
|  |                 title: '', | ||||||
|  |                 location: '', | ||||||
|  |                 noProtocolUrl: CoreUrl.removeProtocol(search), | ||||||
|  |             }; | ||||||
|  |         } else { | ||||||
|  |             this.enteredSiteUrl = undefined; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.searchFunction(search.trim()); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Show instructions and scan QR code. | ||||||
|  |      */ | ||||||
|  |     showInstructionsAndScanQR(): void { | ||||||
|  |         // Show some instructions first.
 | ||||||
|  |         CoreDomUtils.instance.showAlertWithOptions({ | ||||||
|  |             header: Translate.instance.instant('core.login.faqwhereisqrcode'), | ||||||
|  |             message: Translate.instance.instant( | ||||||
|  |                 'core.login.faqwhereisqrcodeanswer', | ||||||
|  |                 { $image: CoreLoginHelperProvider.FAQ_QRCODE_IMAGE_HTML }, | ||||||
|  |             ), | ||||||
|  |             buttons: [ | ||||||
|  |                 { | ||||||
|  |                     text: Translate.instance.instant('core.cancel'), | ||||||
|  |                     role: 'cancel', | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                     text: Translate.instance.instant('core.next'), | ||||||
|  |                     handler: (): void => { | ||||||
|  |                         this.scanQR(); | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |             ], | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Scan a QR code and put its text in the URL input. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async scanQR(): Promise<void> { | ||||||
|  |         // Scan for a QR code.
 | ||||||
|  |         const text = await CoreUtils.instance.scanQR(); | ||||||
|  | 
 | ||||||
|  |         if (text) { | ||||||
|  |             // @todo
 | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Extended data for UI implementation. | ||||||
|  |  */ | ||||||
|  | type CoreLoginSiteInfoExtended = CoreLoginSiteInfo & { | ||||||
|  |     noProtocolUrl: string; // Url wihtout protocol.
 | ||||||
|  |     location: string; // City + country.
 | ||||||
|  |     title: string; // Name + alias.
 | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | type SiteFinderSettings = { | ||||||
|  |     displayalias: boolean; | ||||||
|  |     displaycity: boolean; | ||||||
|  |     displaycountry: boolean; | ||||||
|  |     displayimage: boolean; | ||||||
|  |     displaysitename: boolean; | ||||||
|  |     displayurl: boolean; | ||||||
|  | }; | ||||||
|  | |||||||
| @ -1,2 +1,130 @@ | |||||||
| app-root page-core-login-init { | .item-input:last-child { | ||||||
|  |     margin-bottom: 20px; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .searchbar-ios { | ||||||
|  |     background: transparent; | ||||||
|  | 
 | ||||||
|  |     .searchbar-input { | ||||||
|  |         background-color: white; // @todo $searchbar-ios-toolbar-input-background; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .item.item-block { | ||||||
|  |     &.core-login-need-help.item { | ||||||
|  |         text-decoration: underline; | ||||||
|  |     } | ||||||
|  |     &.core-login-site-qrcode { | ||||||
|  |         .item-inner { | ||||||
|  |             border-bottom: 0; | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .core-login-site-connect { | ||||||
|  |     margin-top: 1.4rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .item ion-thumbnail { | ||||||
|  |     min-width: 50px; | ||||||
|  |     min-height: 50px; | ||||||
|  |     width: 50px; | ||||||
|  |     height: 50px; | ||||||
|  |     border-radius: 20%; | ||||||
|  |     box-shadow: 0 0 4px #eee; | ||||||
|  |     text-align: center; | ||||||
|  |     overflow: hidden; | ||||||
|  | 
 | ||||||
|  |     img { | ||||||
|  |         max-height: 50px; | ||||||
|  |         max-width: fit-content; | ||||||
|  |         width: auto; | ||||||
|  |         height: auto; | ||||||
|  |         margin: 0 auto; | ||||||
|  |         margin-left: 50%; | ||||||
|  |         transform: translateX(-50%); | ||||||
|  |         object-fit: cover; | ||||||
|  |         object-position: 50% 50%; | ||||||
|  |     } | ||||||
|  |     ion-icon { | ||||||
|  |         margin: 0 auto; | ||||||
|  |         font-size: 40px; | ||||||
|  |         line-height: 50px; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .core-login-site-logo, | ||||||
|  | .core-login-site-list, | ||||||
|  | .core-login-site-list-found { | ||||||
|  |     transition-delay: 0s; | ||||||
|  |     visibility: visible; | ||||||
|  |     opacity: 1; | ||||||
|  |     transition: all 0.7s ease-in-out; | ||||||
|  |     max-height: 9999px; | ||||||
|  | 
 | ||||||
|  |     &.hidden { | ||||||
|  |         opacity: 0; | ||||||
|  |         visibility: hidden; | ||||||
|  |         margin-top: 0; | ||||||
|  |         margin-bottom: 0; | ||||||
|  |         padding: 0; | ||||||
|  |         max-height: 0; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .core-login-site-list-found.dimmed { | ||||||
|  |     pointer-events: none; | ||||||
|  |     position: relative; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .core-login-site-list-loading { | ||||||
|  |     position: absolute; | ||||||
|  |     //@todo @include position(0, 0, 0, 0); | ||||||
|  |     width: 100%; | ||||||
|  |     height: 100%; | ||||||
|  |     display: flex; | ||||||
|  |     align-content: center; | ||||||
|  |     align-items: center; | ||||||
|  |     background-color: rgba(255, 255, 255, 0.5); | ||||||
|  |     z-index: 1; | ||||||
|  |     ion-spinner { | ||||||
|  |         flex: 1; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .core-login-site-nolist-loading { | ||||||
|  |     text-align: center; | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | .item.core-login-site-list-title { | ||||||
|  |     ion-label, ion-label h2.item-heading { | ||||||
|  |         margin-top: 0; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | /* @todo | ||||||
|  | @include media-breakpoint-up(md) { | ||||||
|  |     .scroll-content > * { | ||||||
|  |         max-width: 600px; | ||||||
|  |         margin: 0 auto; | ||||||
|  |         width: 100%; | ||||||
|  |     } | ||||||
|  |     .core-login-site-logo { | ||||||
|  |         margin-top: 20%; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     &.hidden { | ||||||
|  |         margin: 0; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | */ | ||||||
|  | .core-login-entered-site { | ||||||
|  |     background-color: gray; // @todo $gray-lighter; | ||||||
|  |     ion-thumbnail { | ||||||
|  |         box-shadow: 0 0 4px #ddd; | ||||||
|  |     } | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | .core-login-default-icon { | ||||||
|  |     filter: grayscale(100%); | ||||||
| } | } | ||||||
|  | |||||||
							
								
								
									
										37
									
								
								src/app/core/login/pages/sites/sites.html
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								src/app/core/login/pages/sites/sites.html
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,37 @@ | |||||||
|  | <ion-header> | ||||||
|  |     <ion-toolbar> | ||||||
|  |         <ion-buttons slot="start"> | ||||||
|  |             <ion-back-button></ion-back-button> | ||||||
|  |         </ion-buttons> | ||||||
|  | 
 | ||||||
|  |         <ion-title>{{ 'core.settings.sites' | translate }}</ion-title> | ||||||
|  | 
 | ||||||
|  |         <ion-buttons slot="end"> | ||||||
|  |             <!-- @todo: Settings button. --> | ||||||
|  |             <ion-button *ngIf="sites && sites.length > 0" icon-only (click)="toggleDelete()" [attr.aria-label]="'core.delete' | translate"> | ||||||
|  |                 <ion-icon name="create" ios="md-create"></ion-icon> | ||||||
|  |             </ion-button> | ||||||
|  |         </ion-buttons> | ||||||
|  |     </ion-toolbar> | ||||||
|  | </ion-header> | ||||||
|  | <ion-content> | ||||||
|  |     <ion-list> | ||||||
|  |         <ion-item (click)="login(site.id)" *ngFor="let site of sites; let idx = index" detail-none> | ||||||
|  |             <ion-avatar item-start> | ||||||
|  |                 <img [src]="site.avatar" core-external-content [siteId]="site.id" alt="{{ 'core.pictureof' | translate:{$a: site.fullName} }}" role="presentation" onError="this.src='assets/img/user-avatar.png'"> | ||||||
|  |             </ion-avatar> | ||||||
|  |             <h2>{{site.fullName}}</h2> | ||||||
|  |             <p><core-format-text [text]="site.siteName" clean="true" [siteId]="site.id"></core-format-text></p> | ||||||
|  |             <p>{{site.siteUrl}}</p> | ||||||
|  |             <ion-badge item-end *ngIf="!showDelete && site.badge">{{site.badge}}</ion-badge> | ||||||
|  |             <ion-button *ngIf="showDelete" item-end icon-only clear color="danger" (click)="deleteSite($event, idx)" [attr.aria-label]="'core.delete' | translate"> | ||||||
|  |                 <ion-icon name="trash"></ion-icon> | ||||||
|  |             </ion-button> | ||||||
|  |         </ion-item> | ||||||
|  |     </ion-list> | ||||||
|  |     <ion-fab slot="fixed" core-fab vertical="bottom" horizontal="end"> | ||||||
|  |         <ion-fab-button (click)="add()" [attr.aria-label]="'core.add' | translate"> | ||||||
|  |             <ion-icon name="add"></ion-icon> | ||||||
|  |         </ion-fab-button> | ||||||
|  |     </ion-fab> | ||||||
|  | </ion-content> | ||||||
							
								
								
									
										145
									
								
								src/app/core/login/pages/sites/sites.page.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										145
									
								
								src/app/core/login/pages/sites/sites.page.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,145 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { CoreDomUtils } from '@/app/services/utils/dom'; | ||||||
|  | import { CoreUtils } from '@/app/services/utils/utils'; | ||||||
|  | import { Component, OnInit } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | import { CoreSiteBasicInfo, CoreSites } from '@services/sites'; | ||||||
|  | import { CoreLogger } from '@singletons/logger'; | ||||||
|  | import { CoreLoginHelper } from '../../services/helper'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Page that displays a "splash screen" while the app is being initialized. | ||||||
|  |  */ | ||||||
|  | @Component({ | ||||||
|  |     selector: 'page-core-login-sites', | ||||||
|  |     templateUrl: 'sites.html', | ||||||
|  |     styleUrls: ['sites.scss'], | ||||||
|  | }) | ||||||
|  | export class CoreLoginSitesPage implements OnInit { | ||||||
|  | 
 | ||||||
|  |     sites: CoreSiteBasicInfo[] = []; | ||||||
|  |     showDelete = false; | ||||||
|  | 
 | ||||||
|  |     protected logger: CoreLogger; | ||||||
|  | 
 | ||||||
|  |     constructor() { | ||||||
|  |         this.logger = CoreLogger.getInstance('CoreLoginSitesPage'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component being initialized. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async ngOnInit(): Promise<void> { | ||||||
|  |         const sites = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSortedSites()); | ||||||
|  | 
 | ||||||
|  |         if (!sites || sites.length == 0) { | ||||||
|  |             CoreLoginHelper.instance.goToAddSite(true); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Remove protocol from the url to show more url text.
 | ||||||
|  |         this.sites = sites.map((site) => { | ||||||
|  |             site.siteUrl = site.siteUrl.replace(/^https?:\/\//, ''); | ||||||
|  |             site.badge = 0; | ||||||
|  |             // @todo: getSiteCounter.
 | ||||||
|  | 
 | ||||||
|  |             return site; | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         this.showDelete = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Go to the page to add a site. | ||||||
|  |      */ | ||||||
|  |     add(): void { | ||||||
|  |         CoreLoginHelper.instance.goToAddSite(false, true); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Delete a site. | ||||||
|  |      * | ||||||
|  |      * @param e Click event. | ||||||
|  |      * @param index Position of the site. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async deleteSite(e: Event, index: number): Promise<void> { | ||||||
|  |         e.stopPropagation(); | ||||||
|  | 
 | ||||||
|  |         const site = this.sites[index]; | ||||||
|  |         const siteName = site.siteName || ''; | ||||||
|  | 
 | ||||||
|  |         // @todo: Format text: siteName.
 | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             await CoreDomUtils.instance.showDeleteConfirm('core.login.confirmdeletesite', { sitename: siteName }); | ||||||
|  |         } catch (error) { | ||||||
|  |             // User cancelled, stop.
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             await CoreSites.instance.deleteSite(site.id); | ||||||
|  | 
 | ||||||
|  |             this.sites.splice(index, 1); | ||||||
|  |             this.showDelete = false; | ||||||
|  | 
 | ||||||
|  |             // If there are no sites left, go to add site.
 | ||||||
|  |             const hasSites = await CoreSites.instance.hasSites(); | ||||||
|  | 
 | ||||||
|  |             if (!hasSites) { | ||||||
|  |                 CoreLoginHelper.instance.goToAddSite(true, true); | ||||||
|  |             } | ||||||
|  |         } catch (error) { | ||||||
|  |             this.logger.error('Error deleting site ' + site.id, error); | ||||||
|  |             CoreDomUtils.instance.showErrorModalDefault(error, 'core.login.errordeletesite', true); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Login in a site. | ||||||
|  |      * | ||||||
|  |      * @param siteId The site ID. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     async login(siteId: string): Promise<void> { | ||||||
|  |         const modal = await CoreDomUtils.instance.showModalLoading(); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             const loggedIn = await CoreSites.instance.loadSite(siteId); | ||||||
|  | 
 | ||||||
|  |             if (loggedIn) { | ||||||
|  |                 return CoreLoginHelper.instance.goToSiteInitialPage(); | ||||||
|  |             } | ||||||
|  |         } catch (error) { | ||||||
|  |             this.logger.error('Error loading site ' + siteId, error); | ||||||
|  |             CoreDomUtils.instance.showErrorModalDefault(error, 'Error loading site.'); | ||||||
|  |         } finally { | ||||||
|  |             modal.dismiss(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Toggle delete. | ||||||
|  |      */ | ||||||
|  |     toggleDelete(): void { | ||||||
|  |         this.showDelete = !this.showDelete; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										3
									
								
								src/app/core/login/pages/sites/sites.scss
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								src/app/core/login/pages/sites/sites.scss
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,3 @@ | |||||||
|  | .item-ios .item-button[icon-only] ion-icon { | ||||||
|  |     font-size: 2.1em; | ||||||
|  | } | ||||||
							
								
								
									
										1372
									
								
								src/app/core/login/services/helper.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1372
									
								
								src/app/core/login/services/helper.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										75
									
								
								src/app/directives/auto-focus.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								src/app/directives/auto-focus.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,75 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Directive, Input, OnInit, ElementRef } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Directive to auto focus an element when a view is loaded. | ||||||
|  |  * | ||||||
|  |  * You can apply it conditionallity assigning it a boolean value: <ion-input [core-auto-focus]="{{showKeyboard}}"> | ||||||
|  |  */ | ||||||
|  | @Directive({ | ||||||
|  |     selector: '[core-auto-focus]', | ||||||
|  | }) | ||||||
|  | export class CoreAutoFocusDirective implements OnInit { | ||||||
|  | 
 | ||||||
|  |     @Input('core-auto-focus') coreAutoFocus: boolean | string = true; | ||||||
|  | 
 | ||||||
|  |     protected element: HTMLElement; | ||||||
|  | 
 | ||||||
|  |     constructor(element: ElementRef) { | ||||||
|  |         this.element = element.nativeElement; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Component being initialized. | ||||||
|  |      */ | ||||||
|  |     ngOnInit(): void { | ||||||
|  |         // @todo
 | ||||||
|  |         // if (this.navCtrl.isTransitioning()) {
 | ||||||
|  |         //     // Navigating to a new page. Wait for the transition to be over.
 | ||||||
|  |         //     const subscription = this.navCtrl.viewDidEnter.subscribe(() => {
 | ||||||
|  |         //         this.autoFocus();
 | ||||||
|  |         //         subscription.unsubscribe();
 | ||||||
|  |         //     });
 | ||||||
|  |         // } else {
 | ||||||
|  |         this.autoFocus(); | ||||||
|  |         // }
 | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Function after the view is initialized. | ||||||
|  |      */ | ||||||
|  |     protected autoFocus(): void { | ||||||
|  |         const autoFocus = CoreUtils.instance.isTrueOrOne(this.coreAutoFocus); | ||||||
|  |         if (autoFocus) { | ||||||
|  |             // Wait a bit to make sure the view is loaded.
 | ||||||
|  |             setTimeout(() => { | ||||||
|  |                 // If it's a ion-input or ion-textarea, search the right input to use.
 | ||||||
|  |                 let element = this.element; | ||||||
|  |                 if (this.element.tagName == 'ION-INPUT') { | ||||||
|  |                     element = this.element.querySelector('input') || element; | ||||||
|  |                 } else if (this.element.tagName == 'ION-TEXTAREA') { | ||||||
|  |                     element = this.element.querySelector('textarea') || element; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 CoreDomUtils.instance.focusElement(element); | ||||||
|  |             }, 200); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -13,15 +13,28 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { NgModule } from '@angular/core'; | import { NgModule } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | import { CoreAutoFocusDirective } from './auto-focus'; | ||||||
|  | import { CoreExternalContentDirective } from './external-content'; | ||||||
|  | import { CoreFormatTextDirective } from './format-text'; | ||||||
| import { CoreLongPressDirective } from './long-press.directive'; | import { CoreLongPressDirective } from './long-press.directive'; | ||||||
|  | import { CoreSupressEventsDirective } from './supress-events'; | ||||||
| 
 | 
 | ||||||
| @NgModule({ | @NgModule({ | ||||||
|     declarations: [ |     declarations: [ | ||||||
|  |         CoreAutoFocusDirective, | ||||||
|  |         CoreExternalContentDirective, | ||||||
|  |         CoreFormatTextDirective, | ||||||
|         CoreLongPressDirective, |         CoreLongPressDirective, | ||||||
|  |         CoreSupressEventsDirective, | ||||||
|     ], |     ], | ||||||
|     imports: [], |     imports: [], | ||||||
|     exports: [ |     exports: [ | ||||||
|  |         CoreAutoFocusDirective, | ||||||
|  |         CoreExternalContentDirective, | ||||||
|  |         CoreFormatTextDirective, | ||||||
|         CoreLongPressDirective, |         CoreLongPressDirective, | ||||||
|  |         CoreSupressEventsDirective, | ||||||
|     ], |     ], | ||||||
| }) | }) | ||||||
| export class CoreDirectivesModule {} | export class CoreDirectivesModule {} | ||||||
|  | |||||||
							
								
								
									
										364
									
								
								src/app/directives/external-content.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										364
									
								
								src/app/directives/external-content.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,364 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Directive, Input, AfterViewInit, ElementRef, OnChanges, SimpleChange, Output, EventEmitter } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | import { CoreApp } from '@services/app'; | ||||||
|  | import { CoreFile } from '@services/file'; | ||||||
|  | import { CoreFilepool } from '@services/filepool'; | ||||||
|  | import { CoreSites } from '@services/sites'; | ||||||
|  | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { CoreUrlUtils } from '@services/utils/url'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { Platform } from '@singletons/core.singletons'; | ||||||
|  | import { CoreLogger } from '@singletons/logger'; | ||||||
|  | import { CoreError } from '@classes/errors/error'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Directive to handle external content. | ||||||
|  |  * | ||||||
|  |  * This directive should be used with any element that links to external content | ||||||
|  |  * which we want to have available when the app is offline. Typically media and links. | ||||||
|  |  * | ||||||
|  |  * If a file is downloaded, its URL will be replaced by the local file URL. | ||||||
|  |  * | ||||||
|  |  * From v3.5.2 this directive will also download inline styles, so it can be used in any element as long as it has inline styles. | ||||||
|  |  */ | ||||||
|  | @Directive({ | ||||||
|  |     selector: '[core-external-content]', | ||||||
|  | }) | ||||||
|  | export class CoreExternalContentDirective implements AfterViewInit, OnChanges { | ||||||
|  | 
 | ||||||
|  |     @Input() siteId?: string; // Site ID to use.
 | ||||||
|  |     @Input() component?: string; // Component to link the file to.
 | ||||||
|  |     @Input() componentId?: string | number; // Component ID to use in conjunction with the component.
 | ||||||
|  |     @Input() src?: string; | ||||||
|  |     @Input() href?: string; | ||||||
|  |     @Input('target-src') targetSrc?: string; // eslint-disable-line @angular-eslint/no-input-rename
 | ||||||
|  |     @Input() poster?: string; | ||||||
|  |     // eslint-disable-next-line @angular-eslint/no-output-on-prefix
 | ||||||
|  |     @Output() onLoad = new EventEmitter(); // Emitted when content is loaded. Only for images.
 | ||||||
|  | 
 | ||||||
|  |     loaded = false; | ||||||
|  |     invalid = false; | ||||||
|  |     protected element: Element; | ||||||
|  |     protected logger: CoreLogger; | ||||||
|  |     protected initialized = false; | ||||||
|  | 
 | ||||||
|  |     constructor(element: ElementRef) { | ||||||
|  | 
 | ||||||
|  |         this.element = element.nativeElement; | ||||||
|  |         this.logger = CoreLogger.getInstance('CoreExternalContentDirective'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * View has been initialized | ||||||
|  |      */ | ||||||
|  |     ngAfterViewInit(): void { | ||||||
|  |         this.checkAndHandleExternalContent(); | ||||||
|  | 
 | ||||||
|  |         this.initialized = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Listen to changes. | ||||||
|  |      * | ||||||
|  |      * * @param {{[name: string]: SimpleChange}} changes Changes. | ||||||
|  |      */ | ||||||
|  |     ngOnChanges(changes: { [name: string]: SimpleChange }): void { | ||||||
|  |         if (changes && this.initialized) { | ||||||
|  |             // If any of the inputs changes, handle the content again.
 | ||||||
|  |             this.checkAndHandleExternalContent(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add a new source with a certain URL as a sibling of the current element. | ||||||
|  |      * | ||||||
|  |      * @param url URL to use in the source. | ||||||
|  |      */ | ||||||
|  |     protected addSource(url: string): void { | ||||||
|  |         if (this.element.tagName !== 'SOURCE') { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const newSource = document.createElement('source'); | ||||||
|  |         const type = this.element.getAttribute('type'); | ||||||
|  | 
 | ||||||
|  |         newSource.setAttribute('src', url); | ||||||
|  | 
 | ||||||
|  |         if (type) { | ||||||
|  |             if (CoreApp.instance.isAndroid() && type == 'video/quicktime') { | ||||||
|  |                 // Fix for VideoJS/Chrome bug https://github.com/videojs/video.js/issues/423 .
 | ||||||
|  |                 newSource.setAttribute('type', 'video/mp4'); | ||||||
|  |             } else { | ||||||
|  |                 newSource.setAttribute('type', type); | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.element.parentNode?.insertBefore(newSource, this.element); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Get the URL that should be handled and, if valid, handle it. | ||||||
|  |      */ | ||||||
|  |     protected async checkAndHandleExternalContent(): Promise<void> { | ||||||
|  |         const currentSite = CoreSites.instance.getCurrentSite(); | ||||||
|  |         const siteId = this.siteId || currentSite?.getId(); | ||||||
|  |         const tagName = this.element.tagName.toUpperCase(); | ||||||
|  |         let targetAttr; | ||||||
|  |         let url; | ||||||
|  | 
 | ||||||
|  |         // Always handle inline styles (if any).
 | ||||||
|  |         this.handleInlineStyles(siteId); | ||||||
|  | 
 | ||||||
|  |         if (tagName === 'A' || tagName == 'IMAGE') { | ||||||
|  |             targetAttr = 'href'; | ||||||
|  |             url = this.href; | ||||||
|  | 
 | ||||||
|  |         } else if (tagName === 'IMG') { | ||||||
|  |             targetAttr = 'src'; | ||||||
|  |             url = this.src; | ||||||
|  | 
 | ||||||
|  |         } else if (tagName === 'AUDIO' || tagName === 'VIDEO' || tagName === 'SOURCE' || tagName === 'TRACK') { | ||||||
|  |             targetAttr = 'src'; | ||||||
|  |             url = this.targetSrc || this.src; | ||||||
|  | 
 | ||||||
|  |             if (tagName === 'VIDEO') { | ||||||
|  |                 if (this.poster) { | ||||||
|  |                     // Handle poster.
 | ||||||
|  |                     this.handleExternalContent('poster', this.poster, siteId).catch(() => { | ||||||
|  |                         // Ignore errors.
 | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } else { | ||||||
|  |             this.invalid = true; | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Avoid handling data url's.
 | ||||||
|  |         if (url && url.indexOf('data:') === 0) { | ||||||
|  |             this.invalid = true; | ||||||
|  |             this.onLoad.emit(); | ||||||
|  |             this.loaded = true; | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             await this.handleExternalContent(targetAttr, url, siteId); | ||||||
|  |         } catch (error) { | ||||||
|  |             // Error handling content. Make sure the loaded event is triggered for images.
 | ||||||
|  |             if (tagName === 'IMG') { | ||||||
|  |                 if (url) { | ||||||
|  |                     this.waitForLoad(); | ||||||
|  |                 } else { | ||||||
|  |                     this.onLoad.emit(); | ||||||
|  |                     this.loaded = true; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handle external content, setting the right URL. | ||||||
|  |      * | ||||||
|  |      * @param targetAttr Attribute to modify. | ||||||
|  |      * @param url Original URL to treat. | ||||||
|  |      * @param siteId Site ID. | ||||||
|  |      * @return Promise resolved if the element is successfully treated. | ||||||
|  |      */ | ||||||
|  |     protected async handleExternalContent(targetAttr: string, url: string, siteId?: string): Promise<void> { | ||||||
|  | 
 | ||||||
|  |         const tagName = this.element.tagName; | ||||||
|  | 
 | ||||||
|  |         if (tagName == 'VIDEO' && targetAttr != 'poster') { | ||||||
|  |             const video = <HTMLVideoElement> this.element; | ||||||
|  |             if (video.textTracks) { | ||||||
|  |                 // It's a video with subtitles. In iOS, subtitles position is wrong so it needs to be fixed.
 | ||||||
|  |                 video.textTracks.onaddtrack = (event): void => { | ||||||
|  |                     const track = <TextTrack> event.track; | ||||||
|  |                     if (track) { | ||||||
|  |                         track.oncuechange = (): void => { | ||||||
|  |                             if (!track.cues) { | ||||||
|  |                                 return; | ||||||
|  |                             } | ||||||
|  | 
 | ||||||
|  |                             const line = Platform.instance.is('tablet') || CoreApp.instance.isAndroid() ? 90 : 80; | ||||||
|  |                             // Position all subtitles to a percentage of video height.
 | ||||||
|  |                             // eslint-disable-next-line @typescript-eslint/no-explicit-any
 | ||||||
|  |                             Array.from(track.cues).forEach((cue: any) => { | ||||||
|  |                                 cue.snapToLines = false; | ||||||
|  |                                 cue.line = line; | ||||||
|  |                                 cue.size = 100; // This solves some Android issue.
 | ||||||
|  |                             }); | ||||||
|  |                             // Delete listener.
 | ||||||
|  |                             track.oncuechange = null; | ||||||
|  |                         }; | ||||||
|  |                     } | ||||||
|  |                 }; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!url || !url.match(/^https?:\/\//i) || CoreUrlUtils.instance.isLocalFileUrl(url) || | ||||||
|  |                 (tagName === 'A' && !CoreUrlUtils.instance.isDownloadableUrl(url))) { | ||||||
|  | 
 | ||||||
|  |             this.logger.debug('Ignoring non-downloadable URL: ' + url); | ||||||
|  |             if (tagName === 'SOURCE') { | ||||||
|  |                 // Restoring original src.
 | ||||||
|  |                 this.addSource(url); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             throw new CoreError('Non-downloadable URL'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const site = await CoreSites.instance.getSite(siteId); | ||||||
|  | 
 | ||||||
|  |         if (!site.canDownloadFiles() && CoreUrlUtils.instance.isPluginFileUrl(url)) { | ||||||
|  |             this.element.parentElement?.removeChild(this.element); // Remove element since it'll be broken.
 | ||||||
|  | 
 | ||||||
|  |             throw 'Site doesn\'t allow downloading files.'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Download images, tracks and posters if size is unknown.
 | ||||||
|  |         const downloadUnknown = tagName == 'IMG' || tagName == 'TRACK' || targetAttr == 'poster'; | ||||||
|  |         let finalUrl: string; | ||||||
|  | 
 | ||||||
|  |         if (targetAttr === 'src' && tagName !== 'SOURCE' && tagName !== 'TRACK' && tagName !== 'VIDEO' && tagName !== 'AUDIO') { | ||||||
|  |             finalUrl = await CoreFilepool.instance.getSrcByUrl( | ||||||
|  |                 site.getId(), | ||||||
|  |                 url, | ||||||
|  |                 this.component, | ||||||
|  |                 this.componentId, | ||||||
|  |                 0, | ||||||
|  |                 true, | ||||||
|  |                 downloadUnknown, | ||||||
|  |             ); | ||||||
|  |         } else { | ||||||
|  |             finalUrl = await CoreFilepool.instance.getUrlByUrl( | ||||||
|  |                 site.getId(), | ||||||
|  |                 url, | ||||||
|  |                 this.component, | ||||||
|  |                 this.componentId, | ||||||
|  |                 0, | ||||||
|  |                 true, | ||||||
|  |                 downloadUnknown, | ||||||
|  |             ); | ||||||
|  | 
 | ||||||
|  |             finalUrl = CoreFile.instance.convertFileSrc(finalUrl); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (!CoreUrlUtils.instance.isLocalFileUrl(finalUrl)) { | ||||||
|  |             /* In iOS, if we use the same URL in embedded file and background download then the download only | ||||||
|  |                downloads a few bytes (cached ones). Add a hash to the URL so both URLs are different. */ | ||||||
|  |             finalUrl = finalUrl + '#moodlemobile-embedded'; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.logger.debug('Using URL ' + finalUrl + ' for ' + url); | ||||||
|  |         if (tagName === 'SOURCE') { | ||||||
|  |             // The browser does not catch changes in SRC, we need to add a new source.
 | ||||||
|  |             this.addSource(finalUrl); | ||||||
|  |         } else { | ||||||
|  |             if (tagName === 'IMG') { | ||||||
|  |                 this.loaded = false; | ||||||
|  |                 this.waitForLoad(); | ||||||
|  |             } | ||||||
|  |             this.element.setAttribute(targetAttr, finalUrl); | ||||||
|  |             this.element.setAttribute('data-original-' + targetAttr, url); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Set events to download big files (not downloaded automatically).
 | ||||||
|  |         if (!CoreUrlUtils.instance.isLocalFileUrl(finalUrl) && targetAttr != 'poster' && | ||||||
|  |             (tagName == 'VIDEO' || tagName == 'AUDIO' || tagName == 'A' || tagName == 'SOURCE')) { | ||||||
|  |             const eventName = tagName == 'A' ? 'click' : 'play'; | ||||||
|  |             let clickableEl = this.element; | ||||||
|  | 
 | ||||||
|  |             if (tagName == 'SOURCE') { | ||||||
|  |                 clickableEl = <HTMLElement> CoreDomUtils.instance.closest(this.element, 'video,audio'); | ||||||
|  |                 if (!clickableEl) { | ||||||
|  |                     return; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             clickableEl.addEventListener(eventName, () => { | ||||||
|  |                 // User played media or opened a downloadable link.
 | ||||||
|  |                 // Download the file if in wifi and it hasn't been downloaded already (for big files).
 | ||||||
|  |                 if (CoreApp.instance.isWifi()) { | ||||||
|  |                     // We aren't using the result, so it doesn't matter which of the 2 functions we call.
 | ||||||
|  |                     CoreFilepool.instance.getUrlByUrl(site.getId(), url, this.component, this.componentId, 0, false); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Handle inline styles, trying to download referenced files. | ||||||
|  |      * | ||||||
|  |      * @param siteId Site ID. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     protected async handleInlineStyles(siteId?: string): Promise<void> { | ||||||
|  |         if (!siteId) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let inlineStyles = this.element.getAttribute('style'); | ||||||
|  | 
 | ||||||
|  |         if (!inlineStyles) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         let urls = inlineStyles.match(/https?:\/\/[^"') ;]*/g); | ||||||
|  |         if (!urls || !urls.length) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         urls = CoreUtils.instance.uniqueArray(urls); // Remove duplicates.
 | ||||||
|  | 
 | ||||||
|  |         const promises = urls.map(async (url) => { | ||||||
|  |             const finalUrl = await CoreFilepool.instance.getUrlByUrl(siteId, url, this.component, this.componentId, 0, true, true); | ||||||
|  | 
 | ||||||
|  |             this.logger.debug('Using URL ' + finalUrl + ' for ' + url + ' in inline styles'); | ||||||
|  |             inlineStyles = inlineStyles!.replace(new RegExp(url, 'gi'), finalUrl); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             await CoreUtils.instance.allPromises(promises); | ||||||
|  | 
 | ||||||
|  |             this.element.setAttribute('style', inlineStyles); | ||||||
|  |         } catch (error) { | ||||||
|  |             this.logger.error('Error treating inline styles.', this.element); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Wait for the image to be loaded or error, and emit an event when it happens. | ||||||
|  |      */ | ||||||
|  |     protected waitForLoad(): void { | ||||||
|  |         const listener = (): void => { | ||||||
|  |             this.element.removeEventListener('load', listener); | ||||||
|  |             this.element.removeEventListener('error', listener); | ||||||
|  |             this.onLoad.emit(); | ||||||
|  |             this.loaded = true; | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         this.element.addEventListener('load', listener); | ||||||
|  |         this.element.addEventListener('error', listener); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
							
								
								
									
										745
									
								
								src/app/directives/format-text.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										745
									
								
								src/app/directives/format-text.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,745 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | import { Directive, ElementRef, Input, Output, EventEmitter, OnChanges, SimpleChange, Optional } from '@angular/core'; | ||||||
|  | import { NavController, IonContent } from '@ionic/angular'; | ||||||
|  | 
 | ||||||
|  | import { CoreEventLoadingChangedData, CoreEventObserver, CoreEvents, CoreEventsProvider } from '@services/events'; | ||||||
|  | import { CoreSites } from '@services/sites'; | ||||||
|  | import { CoreDomUtils } from '@services/utils/dom'; | ||||||
|  | import { CoreIframeUtils, CoreIframeUtilsProvider } from '@services/utils/iframe'; | ||||||
|  | import { CoreTextUtils } from '@services/utils/text'; | ||||||
|  | import { CoreUtils } from '@services/utils/utils'; | ||||||
|  | import { CoreSite } from '@classes/site'; | ||||||
|  | import { Translate } from '@singletons/core.singletons'; | ||||||
|  | import { CoreExternalContentDirective } from './external-content'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Directive to format text rendered. It renders the HTML and treats all links and media, using CoreLinkDirective | ||||||
|  |  * and CoreExternalContentDirective. It also applies filters if needed. | ||||||
|  |  * | ||||||
|  |  * Please use this directive if your text needs to be filtered or it can contain links or media (images, audio, video). | ||||||
|  |  * | ||||||
|  |  * Example usage: | ||||||
|  |  * <core-format-text [text]="myText" [component]="component" [componentId]="componentId"></core-format-text> | ||||||
|  |  */ | ||||||
|  | @Directive({ | ||||||
|  |     selector: 'core-format-text', | ||||||
|  | }) | ||||||
|  | export class CoreFormatTextDirective implements OnChanges { | ||||||
|  | 
 | ||||||
|  |     @Input() text?: string; // The text to format.
 | ||||||
|  |     @Input() siteId?: string; // Site ID to use.
 | ||||||
|  |     @Input() component?: string; // Component for CoreExternalContentDirective.
 | ||||||
|  |     @Input() componentId?: string | number; // Component ID to use in conjunction with the component.
 | ||||||
|  |     @Input() adaptImg?: boolean | string = true; // Whether to adapt images to screen width.
 | ||||||
|  |     @Input() clean?: boolean | string; // Whether all the HTML tags should be removed.
 | ||||||
|  |     @Input() singleLine?: boolean | string; // Whether new lines should be removed (all text in single line). Only if clean=true.
 | ||||||
|  |     @Input() maxHeight?: number; // Max height in pixels to render the content box. It should be 50 at least to make sense.
 | ||||||
|  |                                  // Using this parameter will force display: block to calculate height better.
 | ||||||
|  |                                  // If you want to avoid this use class="inline" at the same time to use display: inline-block.
 | ||||||
|  |     @Input() fullOnClick?: boolean | string; // Whether it should open a new page with the full contents on click.
 | ||||||
|  |     @Input() fullTitle?: string; // Title to use in full view. Defaults to "Description".
 | ||||||
|  |     @Input() highlight?: string; // Text to highlight.
 | ||||||
|  |     @Input() filter?: boolean | string; // Whether to filter the text. If not defined, true if contextLevel and instanceId are set.
 | ||||||
|  |     @Input() contextLevel?: string; // The context level of the text.
 | ||||||
|  |     @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() 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.
 | ||||||
|  | 
 | ||||||
|  |     protected element: HTMLElement; | ||||||
|  |     protected showMoreDisplayed = false; | ||||||
|  |     protected loadingChangedListener?: CoreEventObserver; | ||||||
|  | 
 | ||||||
|  |     constructor( | ||||||
|  |         element: ElementRef, | ||||||
|  |         @Optional() protected navCtrl: NavController, | ||||||
|  |         @Optional() protected content: IonContent, | ||||||
|  |     ) { | ||||||
|  | 
 | ||||||
|  |         this.element = element.nativeElement; | ||||||
|  |         this.element.classList.add('opacity-hide'); // Hide contents until they're treated.
 | ||||||
|  |         this.afterRender = new EventEmitter<void>(); | ||||||
|  | 
 | ||||||
|  |         this.element.addEventListener('click', this.elementClicked.bind(this)); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Detect changes on input properties. | ||||||
|  |      */ | ||||||
|  |     ngOnChanges(changes: { [name: string]: SimpleChange }): void { | ||||||
|  |         if (changes.text || changes.filter || changes.contextLevel || changes.contextInstanceId) { | ||||||
|  |             this.hideShowMore(); | ||||||
|  |             this.formatAndRenderContents(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Apply CoreExternalContentDirective to a certain element. | ||||||
|  |      * | ||||||
|  |      * @param element Element to add the attributes to. | ||||||
|  |      * @return External content instance. | ||||||
|  |      */ | ||||||
|  |     protected addExternalContent(element: Element): CoreExternalContentDirective { | ||||||
|  |         // Angular doesn't let adding directives dynamically. Create the CoreExternalContentDirective manually.
 | ||||||
|  |         const extContent = new CoreExternalContentDirective(new ElementRef(element)); | ||||||
|  | 
 | ||||||
|  |         extContent.component = this.component; | ||||||
|  |         extContent.componentId = this.componentId; | ||||||
|  |         extContent.siteId = this.siteId; | ||||||
|  |         extContent.src = element.getAttribute('src') || undefined; | ||||||
|  |         extContent.href = element.getAttribute('href') || element.getAttribute('xlink:href') || undefined; | ||||||
|  |         extContent.targetSrc = element.getAttribute('target-src') || undefined; | ||||||
|  |         extContent.poster = element.getAttribute('poster') || undefined; | ||||||
|  | 
 | ||||||
|  |         extContent.ngAfterViewInit(); | ||||||
|  | 
 | ||||||
|  |         return extContent; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add class to adapt media to a certain element. | ||||||
|  |      * | ||||||
|  |      * @param element Element to add the class to. | ||||||
|  |      */ | ||||||
|  |     protected addMediaAdaptClass(element: HTMLElement): void { | ||||||
|  |         element.classList.add('core-media-adapt-width'); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Wrap an image with a container to adapt its width. | ||||||
|  |      * | ||||||
|  |      * @param img Image to adapt. | ||||||
|  |      */ | ||||||
|  |     protected adaptImage(img: HTMLElement): void { | ||||||
|  |         // Element to wrap the image.
 | ||||||
|  |         const container = document.createElement('span'); | ||||||
|  |         const originalWidth = img.attributes.getNamedItem('width'); | ||||||
|  | 
 | ||||||
|  |         const forcedWidth = Number(originalWidth?.value); | ||||||
|  |         if (!isNaN(forcedWidth)) { | ||||||
|  |             if (originalWidth!.value.indexOf('%') < 0) { | ||||||
|  |                 img.style.width = forcedWidth  + 'px'; | ||||||
|  |             } else { | ||||||
|  |                 img.style.width = forcedWidth  + '%'; | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         container.classList.add('core-adapted-img-container'); | ||||||
|  |         container.style.cssFloat = img.style.cssFloat; // Copy the float to correctly position the search icon.
 | ||||||
|  |         if (img.classList.contains('atto_image_button_right')) { | ||||||
|  |             container.classList.add('atto_image_button_right'); | ||||||
|  |         } else if (img.classList.contains('atto_image_button_left')) { | ||||||
|  |             container.classList.add('atto_image_button_left'); | ||||||
|  |         } else if (img.classList.contains('atto_image_button_text-top')) { | ||||||
|  |             container.classList.add('atto_image_button_text-top'); | ||||||
|  |         } else if (img.classList.contains('atto_image_button_middle')) { | ||||||
|  |             container.classList.add('atto_image_button_middle'); | ||||||
|  |         } else if (img.classList.contains('atto_image_button_text-bottom')) { | ||||||
|  |             container.classList.add('atto_image_button_text-bottom'); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         CoreDomUtils.instance.wrapElement(img, container); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add magnifying glass icons to view adapted images at full size. | ||||||
|  |      */ | ||||||
|  |     addMagnifyingGlasses(): void { | ||||||
|  |         const imgs = Array.from(this.element.querySelectorAll('.core-adapted-img-container > img')); | ||||||
|  |         if (!imgs.length) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // If cannot calculate element's width, use viewport width to avoid false adapt image icons appearing.
 | ||||||
|  |         const elWidth = this.getElementWidth(this.element) || window.innerWidth; | ||||||
|  | 
 | ||||||
|  |         imgs.forEach((img: HTMLImageElement) => { | ||||||
|  |             // Skip image if it's inside a link.
 | ||||||
|  |             if (img.closest('a')) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             let imgWidth = Number(img.getAttribute('width')); | ||||||
|  |             if (!imgWidth) { | ||||||
|  |                 // No width attribute, use real size.
 | ||||||
|  |                 imgWidth = img.naturalWidth; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (imgWidth <= elWidth) { | ||||||
|  |                 return; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             const imgSrc = CoreTextUtils.instance.escapeHTML(img.getAttribute('data-original-src') || img.getAttribute('src')); | ||||||
|  |             const label = Translate.instance.instant('core.openfullimage'); | ||||||
|  |             const anchor = document.createElement('a'); | ||||||
|  | 
 | ||||||
|  |             anchor.classList.add('core-image-viewer-icon'); | ||||||
|  |             anchor.setAttribute('aria-label', label); | ||||||
|  |             // Add an ion-icon item to apply the right styles, but the ion-icon component won't be executed.
 | ||||||
|  |             anchor.innerHTML = '<ion-icon name="search" class="icon icon-md ion-md-search"></ion-icon>'; | ||||||
|  | 
 | ||||||
|  |             anchor.addEventListener('click', (e: Event) => { | ||||||
|  |                 e.preventDefault(); | ||||||
|  |                 e.stopPropagation(); | ||||||
|  |                 CoreDomUtils.instance.viewImage(imgSrc, img.getAttribute('alt'), this.component, this.componentId, true); | ||||||
|  |             }); | ||||||
|  | 
 | ||||||
|  |             img.parentNode?.appendChild(anchor); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Calculate the height and check if we need to display show more or not. | ||||||
|  |      */ | ||||||
|  |     protected calculateHeight(): void { | ||||||
|  |         // @todo: Work on calculate this height better.
 | ||||||
|  |         if (!this.maxHeight) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Remove max-height (if any) to calculate the real height.
 | ||||||
|  |         const initialMaxHeight = this.element.style.maxHeight; | ||||||
|  |         this.element.style.maxHeight = ''; | ||||||
|  | 
 | ||||||
|  |         const height = this.getElementHeight(this.element); | ||||||
|  | 
 | ||||||
|  |         // Restore the max height now.
 | ||||||
|  |         this.element.style.maxHeight = initialMaxHeight; | ||||||
|  | 
 | ||||||
|  |         // If cannot calculate height, shorten always.
 | ||||||
|  |         if (!height || height > this.maxHeight) { | ||||||
|  |             if (!this.showMoreDisplayed) { | ||||||
|  |                 this.displayShowMore(); | ||||||
|  |             } | ||||||
|  |         } else if (this.showMoreDisplayed) { | ||||||
|  |             this.hideShowMore(); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Display the "Show more" in the element. | ||||||
|  |      */ | ||||||
|  |     protected displayShowMore(): void { | ||||||
|  |         const expandInFullview = CoreUtils.instance.isTrueOrOne(this.fullOnClick) || false; | ||||||
|  |         const showMoreDiv = document.createElement('div'); | ||||||
|  | 
 | ||||||
|  |         showMoreDiv.classList.add('core-show-more'); | ||||||
|  |         showMoreDiv.innerHTML = Translate.instance.instant('core.showmore'); | ||||||
|  |         this.element.appendChild(showMoreDiv); | ||||||
|  | 
 | ||||||
|  |         if (expandInFullview) { | ||||||
|  |             this.element.classList.add('core-expand-in-fullview'); | ||||||
|  |         } | ||||||
|  |         this.element.classList.add('core-text-formatted'); | ||||||
|  |         this.element.classList.add('core-shortened'); | ||||||
|  |         this.element.style.maxHeight = this.maxHeight + 'px'; | ||||||
|  | 
 | ||||||
|  |         this.showMoreDisplayed = true; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Listener to call when the element is clicked. | ||||||
|  |      * | ||||||
|  |      * @param e Click event. | ||||||
|  |      */ | ||||||
|  |     protected elementClicked(e: MouseEvent): void { | ||||||
|  |         if (e.defaultPrevented) { | ||||||
|  |             // Ignore it if the event was prevented by some other listener.
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  |         if (!this.text) { | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const expandInFullview = CoreUtils.instance.isTrueOrOne(this.fullOnClick) || false; | ||||||
|  | 
 | ||||||
|  |         if (!expandInFullview && !this.showMoreDisplayed) { | ||||||
|  |             // Nothing to do on click, just stop.
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         e.preventDefault(); | ||||||
|  |         e.stopPropagation(); | ||||||
|  | 
 | ||||||
|  |         if (!expandInFullview) { | ||||||
|  |             // Change class.
 | ||||||
|  |             this.element.classList.toggle('core-shortened'); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } else { | ||||||
|  |             // Open a new state with the contents.
 | ||||||
|  |             const filter = typeof this.filter != 'undefined' ? CoreUtils.instance.isTrueOrOne(this.filter) : undefined; | ||||||
|  | 
 | ||||||
|  |             CoreTextUtils.instance.viewText( | ||||||
|  |                 this.fullTitle || Translate.instance.instant('core.description'), | ||||||
|  |                 this.text, | ||||||
|  |                 { | ||||||
|  |                     component: this.component, | ||||||
|  |                     componentId: this.componentId, | ||||||
|  |                     filter: filter, | ||||||
|  |                     contextLevel: this.contextLevel, | ||||||
|  |                     instanceId: this.contextInstanceId, | ||||||
|  |                     courseId: this.courseId, | ||||||
|  |                 }, | ||||||
|  |             ); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Finish the rendering, displaying the element again and calling afterRender. | ||||||
|  |      */ | ||||||
|  |     protected finishRender(): void { | ||||||
|  |         // Show the element again.
 | ||||||
|  |         this.element.classList.remove('opacity-hide'); | ||||||
|  |         // Emit the afterRender output.
 | ||||||
|  |         this.afterRender.emit(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Format contents and render. | ||||||
|  |      */ | ||||||
|  |     protected async formatAndRenderContents(): Promise<void> { | ||||||
|  |         if (!this.text) { | ||||||
|  |             this.element.innerHTML = ''; // Remove current contents.
 | ||||||
|  |             this.finishRender(); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // In AOT the inputs and ng-reflect aren't in the DOM sometimes. Add them so styles are applied.
 | ||||||
|  |         if (this.maxHeight && !this.element.getAttribute('maxHeight')) { | ||||||
|  |             this.element.setAttribute('maxHeight', String(this.maxHeight)); | ||||||
|  |         } | ||||||
|  |         if (!this.element.getAttribute('singleLine')) { | ||||||
|  |             this.element.setAttribute('singleLine', String(CoreUtils.instance.isTrueOrOne(this.singleLine))); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.text = this.text ? this.text.trim() : ''; | ||||||
|  | 
 | ||||||
|  |         const result = await this.formatContents(); | ||||||
|  | 
 | ||||||
|  |         // Disable media adapt to correctly calculate the height.
 | ||||||
|  |         this.element.classList.add('core-disable-media-adapt'); | ||||||
|  | 
 | ||||||
|  |         this.element.innerHTML = ''; // Remove current contents.
 | ||||||
|  |         if (this.maxHeight && result.div.innerHTML != '' && | ||||||
|  |                 (this.fullOnClick || (window.innerWidth < 576 || window.innerHeight < 576))) { // Don't collapse in big screens.
 | ||||||
|  | 
 | ||||||
|  |             // Move the children to the current element to be able to calculate the height.
 | ||||||
|  |             CoreDomUtils.instance.moveChildren(result.div, this.element); | ||||||
|  | 
 | ||||||
|  |             // Calculate the height now.
 | ||||||
|  |             this.calculateHeight(); | ||||||
|  | 
 | ||||||
|  |             // Add magnifying glasses to images.
 | ||||||
|  |             this.addMagnifyingGlasses(); | ||||||
|  | 
 | ||||||
|  |             if (!this.loadingChangedListener) { | ||||||
|  |                 // Recalculate the height if a parent core-loading displays the content.
 | ||||||
|  |                 this.loadingChangedListener = | ||||||
|  |                     CoreEvents.instance.on(CoreEventsProvider.CORE_LOADING_CHANGED, (data: CoreEventLoadingChangedData) => { | ||||||
|  |                         if (data.loaded && CoreDomUtils.instance.closest(this.element.parentElement, '#' + data.uniqueId)) { | ||||||
|  |                             // The format-text is inside the loading, re-calculate the height.
 | ||||||
|  |                             this.calculateHeight(); | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |             } | ||||||
|  |         } else { | ||||||
|  |             CoreDomUtils.instance.moveChildren(result.div, this.element); | ||||||
|  | 
 | ||||||
|  |             // Add magnifying glasses to images.
 | ||||||
|  |             this.addMagnifyingGlasses(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (result.options.filter) { | ||||||
|  |             // Let filters hnadle HTML. We do it here because we don't want them to block the render of the text.
 | ||||||
|  |             // @todo
 | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.element.classList.remove('core-disable-media-adapt'); | ||||||
|  |         this.finishRender(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Apply formatText and set sub-directives. | ||||||
|  |      * | ||||||
|  |      * @return Promise resolved with a div element containing the code. | ||||||
|  |      */ | ||||||
|  |     protected async formatContents(): Promise<FormatContentsResult> { | ||||||
|  |         // Retrieve the site since it might be needed later.
 | ||||||
|  |         const site = await CoreUtils.instance.ignoreErrors(CoreSites.instance.getSite(this.siteId)); | ||||||
|  | 
 | ||||||
|  |         if (site && this.contextLevel == 'course' && this.contextInstanceId !== undefined && this.contextInstanceId <= 0) { | ||||||
|  |             this.contextInstanceId = site.getSiteHomeId(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         const filter = typeof this.filter == 'undefined' ? | ||||||
|  |             !!(this.contextLevel && typeof this.contextInstanceId != 'undefined') : CoreUtils.instance.isTrueOrOne(this.filter); | ||||||
|  | 
 | ||||||
|  |         const options = { | ||||||
|  |             clean: CoreUtils.instance.isTrueOrOne(this.clean), | ||||||
|  |             singleLine: CoreUtils.instance.isTrueOrOne(this.singleLine), | ||||||
|  |             highlight: this.highlight, | ||||||
|  |             courseId: this.courseId, | ||||||
|  |             wsNotFiltered: CoreUtils.instance.isTrueOrOne(this.wsNotFiltered), | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         let formatted: string; | ||||||
|  | 
 | ||||||
|  |         if (filter) { | ||||||
|  |             // @todo
 | ||||||
|  |             formatted = this.text!; | ||||||
|  |         } else { | ||||||
|  |             // @todo
 | ||||||
|  |             formatted = this.text!; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         formatted = this.treatWindowOpen(formatted); | ||||||
|  | 
 | ||||||
|  |         const div = document.createElement('div'); | ||||||
|  | 
 | ||||||
|  |         div.innerHTML = formatted; | ||||||
|  | 
 | ||||||
|  |         this.treatHTMLElements(div, site); | ||||||
|  | 
 | ||||||
|  |         return { | ||||||
|  |             div, | ||||||
|  |             filters: [], | ||||||
|  |             options, | ||||||
|  |             siteId: site?.getId(), | ||||||
|  |         }; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Treat HTML elements when formatting contents. | ||||||
|  |      * | ||||||
|  |      * @param div Div element. | ||||||
|  |      * @param site Site instance. | ||||||
|  |      * @return Promise resolved when done. | ||||||
|  |      */ | ||||||
|  |     protected async treatHTMLElements(div: HTMLElement, site?: CoreSite): Promise<void> { | ||||||
|  |         const canTreatVimeo = site?.isVersionGreaterEqualThan(['3.3.4', '3.4']) || false; | ||||||
|  |         const navCtrl = this.navCtrl; // @todo this.svComponent ? this.svComponent.getMasterNav() : this.navCtrl;
 | ||||||
|  | 
 | ||||||
|  |         const images = Array.from(div.querySelectorAll('img')); | ||||||
|  |         const anchors = Array.from(div.querySelectorAll('a')); | ||||||
|  |         const audios = Array.from(div.querySelectorAll('audio')); | ||||||
|  |         const videos = Array.from(div.querySelectorAll('video')); | ||||||
|  |         const iframes = Array.from(div.querySelectorAll('iframe')); | ||||||
|  |         const buttons = Array.from(div.querySelectorAll('.button')); | ||||||
|  |         const elementsWithInlineStyles = Array.from(div.querySelectorAll('*[style]')); | ||||||
|  |         const stopClicksElements = Array.from(div.querySelectorAll('button,input,select,textarea')); | ||||||
|  |         const frames = Array.from(div.querySelectorAll(CoreIframeUtilsProvider.FRAME_TAGS.join(',').replace(/iframe,?/, ''))); | ||||||
|  |         const svgImages = Array.from(div.querySelectorAll('image')); | ||||||
|  | 
 | ||||||
|  |         // Walk through the content to find the links and add our directive to it.
 | ||||||
|  |         // Important: We need to look for links first because in 'img' we add new links without core-link.
 | ||||||
|  |         anchors.forEach((anchor) => { | ||||||
|  |             // Angular 2 doesn't let adding directives dynamically. Create the CoreLinkDirective manually.
 | ||||||
|  |             // @todo
 | ||||||
|  | 
 | ||||||
|  |             this.addExternalContent(anchor); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         const externalImages: CoreExternalContentDirective[] = []; | ||||||
|  |         if (images && images.length > 0) { | ||||||
|  |             // Walk through the content to find images, and add our directive.
 | ||||||
|  |             images.forEach((img: HTMLElement) => { | ||||||
|  |                 this.addMediaAdaptClass(img); | ||||||
|  | 
 | ||||||
|  |                 const externalImage = this.addExternalContent(img); | ||||||
|  |                 if (!externalImage.invalid) { | ||||||
|  |                     externalImages.push(externalImage); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (CoreUtils.instance.isTrueOrOne(this.adaptImg) && !img.classList.contains('icon')) { | ||||||
|  |                     this.adaptImage(img); | ||||||
|  |                 } | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         audios.forEach((audio) => { | ||||||
|  |             this.treatMedia(audio); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         videos.forEach((video) => { | ||||||
|  |             this.treatMedia(video); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         iframes.forEach((iframe) => { | ||||||
|  |             this.treatIframe(iframe, site, canTreatVimeo, navCtrl); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         svgImages.forEach((image) => { | ||||||
|  |             this.addExternalContent(image); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // Handle buttons with inner links.
 | ||||||
|  |         buttons.forEach((button: HTMLElement) => { | ||||||
|  |             // Check if it has a link inside.
 | ||||||
|  |             if (button.querySelector('a')) { | ||||||
|  |                 button.classList.add('core-button-with-inner-link'); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // Handle inline styles.
 | ||||||
|  |         elementsWithInlineStyles.forEach((el: HTMLElement) => { | ||||||
|  |             // Only add external content for tags that haven't been treated already.
 | ||||||
|  |             if (el.tagName != 'A' && el.tagName != 'IMG' && el.tagName != 'AUDIO' && el.tagName != 'VIDEO' | ||||||
|  |                     && el.tagName != 'SOURCE' && el.tagName != 'TRACK') { | ||||||
|  |                 this.addExternalContent(el); | ||||||
|  |             } | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // Stop propagating click events.
 | ||||||
|  |         stopClicksElements.forEach((element: HTMLElement) => { | ||||||
|  |             element.addEventListener('click', (e) => { | ||||||
|  |                 e.stopPropagation(); | ||||||
|  |             }); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // Handle all kind of frames.
 | ||||||
|  |         frames.forEach((frame: HTMLFrameElement | HTMLObjectElement | HTMLEmbedElement) => { | ||||||
|  |             CoreIframeUtils.instance.treatFrame(frame, false, navCtrl); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         CoreDomUtils.instance.handleBootstrapTooltips(div); | ||||||
|  | 
 | ||||||
|  |         if (externalImages.length) { | ||||||
|  |             // Wait for images to load.
 | ||||||
|  |             const promise = CoreUtils.instance.allPromises(externalImages.map((externalImage) => { | ||||||
|  |                 if (externalImage.loaded) { | ||||||
|  |                     // Image has already been loaded, no need to wait.
 | ||||||
|  |                     return Promise.resolve(); | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 return new Promise((resolve): void => { | ||||||
|  |                     const subscription = externalImage.onLoad.subscribe(() => { | ||||||
|  |                         subscription.unsubscribe(); | ||||||
|  |                         resolve(); | ||||||
|  |                     }); | ||||||
|  |                 }); | ||||||
|  |             })); | ||||||
|  | 
 | ||||||
|  |             // Automatically reject the promise after 5 seconds to prevent blocking the user forever.
 | ||||||
|  |             await CoreUtils.instance.ignoreErrors(CoreUtils.instance.timeoutPromise(promise, 5000)); | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns the element width in pixels. | ||||||
|  |      * | ||||||
|  |      * @param element Element to get width from. | ||||||
|  |      * @return The width of the element in pixels. When 0 is returned it means the element is not visible. | ||||||
|  |      */ | ||||||
|  |     protected getElementWidth(element: HTMLElement): number { | ||||||
|  |         let width = CoreDomUtils.instance.getElementWidth(element); | ||||||
|  | 
 | ||||||
|  |         if (!width) { | ||||||
|  |             // All elements inside are floating or inline. Change display mode to allow calculate the width.
 | ||||||
|  |             const parentWidth = element.parentElement ? | ||||||
|  |                 CoreDomUtils.instance.getElementWidth(element.parentElement, true, false, false, true) : 0; | ||||||
|  |             const previousDisplay = getComputedStyle(element, null).display; | ||||||
|  | 
 | ||||||
|  |             element.style.display = 'inline-block'; | ||||||
|  | 
 | ||||||
|  |             width = CoreDomUtils.instance.getElementWidth(element); | ||||||
|  | 
 | ||||||
|  |             // If width is incorrectly calculated use parent width instead.
 | ||||||
|  |             if (parentWidth > 0 && (!width || width > parentWidth)) { | ||||||
|  |                 width = parentWidth; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             element.style.display = previousDisplay; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return width; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Returns the element height in pixels. | ||||||
|  |      * | ||||||
|  |      * @param elementAng Element to get height from. | ||||||
|  |      * @return The height of the element in pixels. When 0 is returned it means the element is not visible. | ||||||
|  |      */ | ||||||
|  |     protected getElementHeight(element: HTMLElement): number { | ||||||
|  |         return CoreDomUtils.instance.getElementHeight(element) || 0; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * "Hide" the "Show more" in the element if it's shown. | ||||||
|  |      */ | ||||||
|  |     protected hideShowMore(): void { | ||||||
|  |         const showMoreDiv = this.element.querySelector('div.core-show-more'); | ||||||
|  | 
 | ||||||
|  |         if (showMoreDiv) { | ||||||
|  |             showMoreDiv.remove(); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         this.element.classList.remove('core-expand-in-fullview'); | ||||||
|  |         this.element.classList.remove('core-text-formatted'); | ||||||
|  |         this.element.classList.remove('core-shortened'); | ||||||
|  |         this.element.style.maxHeight = ''; | ||||||
|  |         this.showMoreDisplayed = false; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add media adapt class and apply CoreExternalContentDirective to the media element and its sources and tracks. | ||||||
|  |      * | ||||||
|  |      * @param element Video or audio to treat. | ||||||
|  |      */ | ||||||
|  |     protected treatMedia(element: HTMLElement): void { | ||||||
|  |         this.addMediaAdaptClass(element); | ||||||
|  |         this.addExternalContent(element); | ||||||
|  | 
 | ||||||
|  |         const sources = Array.from(element.querySelectorAll('source')); | ||||||
|  |         const tracks = Array.from(element.querySelectorAll('track')); | ||||||
|  | 
 | ||||||
|  |         sources.forEach((source) => { | ||||||
|  |             source.setAttribute('target-src', source.getAttribute('src') || ''); | ||||||
|  |             source.removeAttribute('src'); | ||||||
|  |             this.addExternalContent(source); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         tracks.forEach((track) => { | ||||||
|  |             this.addExternalContent(track); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // Stop propagating click events.
 | ||||||
|  |         element.addEventListener('click', (e) => { | ||||||
|  |             e.stopPropagation(); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Add media adapt class and treat the iframe source. | ||||||
|  |      * | ||||||
|  |      * @param iframe Iframe to treat. | ||||||
|  |      * @param site Site instance. | ||||||
|  |      * @param canTreatVimeo Whether Vimeo videos can be treated in the site. | ||||||
|  |      * @param navCtrl NavController to use. | ||||||
|  |      */ | ||||||
|  |     protected async treatIframe( | ||||||
|  |         iframe: HTMLIFrameElement, | ||||||
|  |         site: CoreSite | undefined, | ||||||
|  |         canTreatVimeo: boolean, | ||||||
|  |         navCtrl: NavController, | ||||||
|  |     ): Promise<void> { | ||||||
|  |         const src = iframe.src; | ||||||
|  |         const currentSite = CoreSites.instance.getCurrentSite(); | ||||||
|  | 
 | ||||||
|  |         this.addMediaAdaptClass(iframe); | ||||||
|  | 
 | ||||||
|  |         if (currentSite?.containsUrl(src)) { | ||||||
|  |             // URL points to current site, try to use auto-login.
 | ||||||
|  |             const finalUrl = await currentSite.getAutoLoginUrl(src, false); | ||||||
|  | 
 | ||||||
|  |             iframe.src = finalUrl; | ||||||
|  | 
 | ||||||
|  |             CoreIframeUtils.instance.treatFrame(iframe, false, navCtrl); | ||||||
|  | 
 | ||||||
|  |             return; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (site && src && canTreatVimeo) { | ||||||
|  |             // 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]+)/); | ||||||
|  |             if (matches && matches[1]) { | ||||||
|  |                 let newUrl = CoreTextUtils.instance.concatenatePaths(site.getURL(), '/media/player/vimeo/wsplayer.php?video=') + | ||||||
|  |                     matches[1] + '&token=' + site.getToken(); | ||||||
|  | 
 | ||||||
|  |                 // Width and height are mandatory, we need to calculate them.
 | ||||||
|  |                 let width; | ||||||
|  |                 let height; | ||||||
|  | 
 | ||||||
|  |                 if (iframe.width) { | ||||||
|  |                     width = iframe.width; | ||||||
|  |                 } else { | ||||||
|  |                     width = this.getElementWidth(iframe); | ||||||
|  |                     if (!width) { | ||||||
|  |                         width = window.innerWidth; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 if (iframe.height) { | ||||||
|  |                     height = iframe.height; | ||||||
|  |                 } else { | ||||||
|  |                     height = this.getElementHeight(iframe); | ||||||
|  |                     if (!height) { | ||||||
|  |                         height = width; | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Width and height parameters are required in 3.6 and older sites.
 | ||||||
|  |                 if (site && !site.isVersionGreaterEqualThan('3.7')) { | ||||||
|  |                     newUrl += '&width=' + width + '&height=' + height; | ||||||
|  |                 } | ||||||
|  |                 iframe.src = newUrl; | ||||||
|  | 
 | ||||||
|  |                 if (!iframe.width) { | ||||||
|  |                     iframe.width = width; | ||||||
|  |                 } | ||||||
|  |                 if (!iframe.height) { | ||||||
|  |                     iframe.height = height; | ||||||
|  |                 } | ||||||
|  | 
 | ||||||
|  |                 // Do the iframe responsive.
 | ||||||
|  |                 if (iframe.parentElement?.classList.contains('embed-responsive')) { | ||||||
|  |                     iframe.addEventListener('load', () => { | ||||||
|  |                         if (iframe.contentDocument) { | ||||||
|  |                             const css = document.createElement('style'); | ||||||
|  |                             css.setAttribute('type', 'text/css'); | ||||||
|  |                             css.innerHTML = 'iframe {width: 100%;height: 100%;}'; | ||||||
|  |                             iframe.contentDocument.head.appendChild(css); | ||||||
|  |                         } | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         CoreIframeUtils.instance.treatFrame(iframe, false, navCtrl); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Convert window.open to window.openWindowSafely inside HTML tags. | ||||||
|  |      * | ||||||
|  |      * @param text Text to treat. | ||||||
|  |      * @return Treated text. | ||||||
|  |      */ | ||||||
|  |     protected treatWindowOpen(text: string): string { | ||||||
|  |         // Get HTML tags that include window.open. Script tags aren't executed so there's no need to treat them.
 | ||||||
|  |         const matches = text.match(/<[^>]+window\.open\([^)]*\)[^>]*>/g); | ||||||
|  | 
 | ||||||
|  |         if (matches) { | ||||||
|  |             matches.forEach((match) => { | ||||||
|  |                 // Replace all the window.open inside the tag.
 | ||||||
|  |                 const treated = match.replace(/window\.open\(/g, 'window.openWindowSafely('); | ||||||
|  | 
 | ||||||
|  |                 text = text.replace(match, treated); | ||||||
|  |             }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         return text; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type FormatContentsResult = { | ||||||
|  |     div: HTMLElement; | ||||||
|  |     filters: any[]; | ||||||
|  |     options: any; | ||||||
|  |     siteId?: string; | ||||||
|  | }; | ||||||
							
								
								
									
										97
									
								
								src/app/directives/supress-events.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/app/directives/supress-events.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,97 @@ | |||||||
|  | // (C) Copyright 2015 Moodle Pty Ltd.
 | ||||||
|  | //
 | ||||||
|  | // Licensed under the Apache License, Version 2.0 (the "License");
 | ||||||
|  | // you may not use this file except in compliance with the License.
 | ||||||
|  | // You may obtain a copy of the License at
 | ||||||
|  | //
 | ||||||
|  | //     http://www.apache.org/licenses/LICENSE-2.0
 | ||||||
|  | //
 | ||||||
|  | // Unless required by applicable law or agreed to in writing, software
 | ||||||
|  | // distributed under the License is distributed on an "AS IS" BASIS,
 | ||||||
|  | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 | ||||||
|  | // See the License for the specific language governing permissions and
 | ||||||
|  | // limitations under the License.
 | ||||||
|  | 
 | ||||||
|  | // Based on http://roblouie.com/article/198/using-gestures-in-the-ionic-2-beta/
 | ||||||
|  | 
 | ||||||
|  | import { Directive, ElementRef, OnInit, Input, Output, EventEmitter } from '@angular/core'; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Directive to suppress all events on an element. This is useful to prevent keyboard closing when clicking this element. | ||||||
|  |  * | ||||||
|  |  * This directive is based on some code posted by johnthackstonanderson in | ||||||
|  |  * https://github.com/ionic-team/ionic-plugin-keyboard/issues/81
 | ||||||
|  |  * | ||||||
|  |  * @description | ||||||
|  |  * | ||||||
|  |  * If nothing is supplied or string 'all', then all the default events will be suppressed. This is the recommended usage. | ||||||
|  |  * | ||||||
|  |  * If you only want to suppress a single event just pass the name of the event. If you want to suppress a set of events, | ||||||
|  |  * pass an array with the names of the events to suppress. | ||||||
|  |  * | ||||||
|  |  * Example usage: | ||||||
|  |  * | ||||||
|  |  * <a ion-button [core-suppress-events] (onClick)="toggle($event)"> | ||||||
|  |  */ | ||||||
|  | @Directive({ | ||||||
|  |     selector: '[core-suppress-events]', | ||||||
|  | }) | ||||||
|  | export class CoreSupressEventsDirective implements OnInit { | ||||||
|  | 
 | ||||||
|  |     @Input('core-suppress-events') suppressEvents?: string | string[]; | ||||||
|  |     @Output() onClick = new EventEmitter(); // eslint-disable-line @angular-eslint/no-output-on-prefix
 | ||||||
|  | 
 | ||||||
|  |     protected element: HTMLElement; | ||||||
|  | 
 | ||||||
|  |     constructor(el: ElementRef) { | ||||||
|  |         this.element = el.nativeElement; | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Initialize event listeners. | ||||||
|  |      */ | ||||||
|  |     ngOnInit(): void { | ||||||
|  |         let events: string[]; | ||||||
|  | 
 | ||||||
|  |         if (this.suppressEvents == 'all' || typeof this.suppressEvents == 'undefined' || this.suppressEvents === null) { | ||||||
|  |             // Suppress all events.
 | ||||||
|  |             events = ['click', 'mousedown', 'touchdown', 'touchmove', 'touchstart']; | ||||||
|  | 
 | ||||||
|  |         } else if (typeof this.suppressEvents == 'string') { | ||||||
|  |             // It's a string, just suppress this event.
 | ||||||
|  |             events = [this.suppressEvents]; | ||||||
|  | 
 | ||||||
|  |         } else if (Array.isArray(this.suppressEvents)) { | ||||||
|  |             // Array supplied.
 | ||||||
|  |             events = this.suppressEvents; | ||||||
|  |         } else { | ||||||
|  |             events = []; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Suppress the events.
 | ||||||
|  |         for (const evName of events) { | ||||||
|  |             this.element.addEventListener(evName, this.stopBubble.bind(this)); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // Now listen to "click" events.
 | ||||||
|  |         this.element.addEventListener('mouseup', (event) => { // Triggered in Android & iOS.
 | ||||||
|  |             this.onClick.emit(event); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         this.element.addEventListener('touchend', (event) => { // Triggered desktop & browser.
 | ||||||
|  |             this.stopBubble(event); | ||||||
|  |             this.onClick.emit(event); | ||||||
|  |         }); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     /** | ||||||
|  |      * Stop event default and propagation. | ||||||
|  |      * | ||||||
|  |      * @param event Event. | ||||||
|  |      */ | ||||||
|  |     protected stopBubble(event: Event): void { | ||||||
|  |         event.preventDefault(); | ||||||
|  |         event.stopPropagation(); | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  | } | ||||||
| @ -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. | ||||||
|      */ |      */ | ||||||
|  | |||||||
| @ -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.
 | ||||||
|  |         this.queuePromise = CoreUtils.instance.ignoreErrors(this.queuePromise).then(async () => { | ||||||
|  |             try { | ||||||
|  |                 await this.executeHandler(name, force, siteId); | ||||||
| 
 | 
 | ||||||
|             // Add the execution to the queue.
 |  | ||||||
|             this.queuePromise = this.queuePromise.catch(() => { |  | ||||||
|                 // Ignore errors in previous handlers.
 |  | ||||||
|             }).then(() => this.executeHandler(name, force, siteId).then(() => { |  | ||||||
|                 this.logger.debug(`Execution of handler '${name}' was a success.`); |                 this.logger.debug(`Execution of handler '${name}' was a success.`); | ||||||
| 
 | 
 | ||||||
|                 return this.setHandlerLastExecutionTime(name, Date.now()).then(() => { |                 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); |  | ||||||
|         }); |  | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -13,6 +13,7 @@ | |||||||
| // limitations under the License.
 | // limitations under the License.
 | ||||||
| 
 | 
 | ||||||
| import { Injectable } from '@angular/core'; | import { Injectable } from '@angular/core'; | ||||||
|  | import { Params } from '@angular/router'; | ||||||
| import { Subject } from 'rxjs'; | import { Subject } from 'rxjs'; | ||||||
| 
 | 
 | ||||||
| import { CoreLogger } from '@singletons/logger'; | import { CoreLogger } from '@singletons/logger'; | ||||||
| @ -199,3 +200,20 @@ export class CoreEventsProvider { | |||||||
| } | } | ||||||
| 
 | 
 | ||||||
| export class CoreEvents extends makeSingleton(CoreEventsProvider) {} | export class CoreEvents extends makeSingleton(CoreEventsProvider) {} | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Data passed to SESSION_EXPIRED event. | ||||||
|  |  */ | ||||||
|  | export type CoreEventSessionExpiredData = { | ||||||
|  |     pageName?: string; | ||||||
|  |     params?: Params; | ||||||
|  |     siteId?: string; | ||||||
|  | }; | ||||||
|  | 
 | ||||||
|  | /** | ||||||
|  |  * Data passed to CORE_LOADING_CHANGED event. | ||||||
|  |  */ | ||||||
|  | export type CoreEventLoadingChangedData = { | ||||||
|  |     loaded: boolean; | ||||||
|  |     uniqueId: string; | ||||||
|  | }; | ||||||
|  | |||||||
| @ -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,70 +117,76 @@ 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); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         // The file system is available.
 | ||||||
|  |         const isWifi = CoreApp.instance.isWifi(); | ||||||
|  |         const isOnline = CoreApp.instance.isOnline(); | ||||||
|  | 
 | ||||||
|  |         if (state == CoreConstants.DOWNLOADED) { | ||||||
|  |             // File is downloaded, get the local file URL.
 | ||||||
|  |             return CoreFilepool.instance.getUrlByUrl(siteId, fileUrl, component, componentId, timemodified, false, false, file); | ||||||
|  |         } else { | ||||||
|  |             if (!isOnline && !this.isStateDownloaded(state)) { | ||||||
|  |                 // Not downloaded and user is offline, reject.
 | ||||||
|  |                 throw new CoreError(Translate.instance.instant('core.networkerrormsg')); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (onProgress) { | ||||||
|  |                 // This call can take a while. Send a fake event to notify that we're doing some calculations.
 | ||||||
|  |                 onProgress({ calculating: true }); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  |                 await CoreFilepool.instance.shouldDownloadBeforeOpen(fixedUrl, file.filesize || 0); | ||||||
|  |             } catch (error) { | ||||||
|  |                 // Start the download if in wifi, but return the URL right away so the file is opened.
 | ||||||
|  |                 if (isWifi) { | ||||||
|  |                     this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 return promise.then((state) => { |                 if (!this.isStateDownloaded(state) || isOnline) { | ||||||
|                     // The file system is available.
 |                     // Not downloaded or online, return the online URL.
 | ||||||
|                     const isWifi = CoreApp.instance.isWifi(); |                     return fixedUrl; | ||||||
|                     const isOnline = CoreApp.instance.isOnline(); |                 } else { | ||||||
|  |                     // Outdated but offline, so we return the local URL.
 | ||||||
|  |                     return CoreFilepool.instance.getUrlByUrl( | ||||||
|  |                         siteId, fileUrl, component, componentId, timemodified, false, false, file); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
| 
 | 
 | ||||||
|                     if (state == CoreConstants.DOWNLOADED) { |             // Download the file first.
 | ||||||
|                         // File is downloaded, get the local file URL.
 |             if (state == CoreConstants.DOWNLOADING) { | ||||||
|                         return CoreFilepool.instance.getUrlByUrl( |                 // It's already downloading, stop.
 | ||||||
|                             siteId, fileUrl, component, componentId, timemodified, false, false, file); |  | ||||||
|                     } else { |  | ||||||
|                         if (!isOnline && !this.isStateDownloaded(state)) { |  | ||||||
|                             // Not downloaded and user is offline, reject.
 |  | ||||||
|                             return Promise.reject(new CoreError(Translate.instance.instant('core.networkerrormsg'))); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         if (onProgress) { |  | ||||||
|                             // This call can take a while. Send a fake event to notify that we're doing some calculations.
 |  | ||||||
|                             onProgress({ calculating: true }); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         return CoreFilepool.instance.shouldDownloadBeforeOpen(fixedUrl, file.filesize).then(() => { |  | ||||||
|                             if (state == CoreConstants.DOWNLOADING) { |  | ||||||
|                                 // It's already downloading, stop.
 |  | ||||||
|                                 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.
 |  | ||||||
|                             if (isWifi) { |  | ||||||
|                                 this.downloadFile(fileUrl, component, componentId, timemodified, onProgress, file, siteId); |  | ||||||
|                             } |  | ||||||
| 
 |  | ||||||
|                             if (!this.isStateDownloaded(state) || isOnline) { |  | ||||||
|                                 // Not downloaded or online, return the online URL.
 |  | ||||||
|                                 return fixedUrl; |  | ||||||
|                             } else { |  | ||||||
|                                 // Outdated but offline, so we return the local URL.
 |  | ||||||
|                                 return CoreFilepool.instance.getUrlByUrl( |  | ||||||
|                                     siteId, fileUrl, component, componentId, timemodified, false, false, file); |  | ||||||
|                             } |  | ||||||
|                         }); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             } else { |  | ||||||
|                 // Use the online URL.
 |  | ||||||
|                 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()) { | 
 | ||||||
|                 return Promise.reject(new CoreError(Translate.instance.instant('core.cannotdownloadfiles'))); |         if (!site.canDownloadFiles()) { | ||||||
|  |             throw new CoreError(Translate.instance.instant('core.cannotdownloadfiles')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         try { | ||||||
|  |             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.
 | ||||||
|  |             const state = await CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified); | ||||||
|  | 
 | ||||||
|  |             if (this.isStateDownloaded(state)) { | ||||||
|  |                 return CoreFilepool.instance.getInternalUrlByUrl(siteId, fileUrl); | ||||||
|  |             } else { | ||||||
|  |                 throw error; | ||||||
|             } |             } | ||||||
| 
 |         } | ||||||
|             return 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.
 |  | ||||||
|                 CoreFilepool.instance.getFileStateByUrl(siteId, fileUrl, timemodified).then((state) => { |  | ||||||
|                     if (this.isStateDownloaded(state)) { |  | ||||||
|                         return CoreFilepool.instance.getInternalUrlByUrl(siteId, fileUrl); |  | ||||||
|                     } else { |  | ||||||
|                         return Promise.reject(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; | ||||||
|  | |||||||
| @ -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] = {}; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -117,25 +117,25 @@ 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()) { |  | ||||||
|                 this.basePath = File.instance.externalApplicationStorageDirectory || this.basePath; |  | ||||||
|             } else if (CoreApp.instance.isIOS()) { |  | ||||||
|                 this.basePath = File.instance.documentsDirectory || this.basePath; |  | ||||||
|             } else if (!this.isAvailable() || this.basePath === '') { |  | ||||||
|                 this.logger.error('Error getting device OS.'); |  | ||||||
| 
 | 
 | ||||||
|                 return Promise.reject(new CoreError('Error getting device OS to initialize file system.')); |         if (CoreApp.instance.isAndroid()) { | ||||||
|             } |             this.basePath = File.instance.externalApplicationStorageDirectory || this.basePath; | ||||||
|  |         } else if (CoreApp.instance.isIOS()) { | ||||||
|  |             this.basePath = File.instance.documentsDirectory || this.basePath; | ||||||
|  |         } else if (!this.isAvailable() || this.basePath === '') { | ||||||
|  |             this.logger.error('Error getting device OS.'); | ||||||
| 
 | 
 | ||||||
|             this.initialized = true; |             return Promise.reject(new CoreError('Error getting device OS to initialize file system.')); | ||||||
|             this.logger.debug('FS initialized: ' + this.basePath); |         } | ||||||
|         }); | 
 | ||||||
|  |         this.initialized = true; | ||||||
|  |         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.
 |  | ||||||
|             path = this.removeStartingSlash(path.replace(this.basePath, '')); |  | ||||||
|             this.logger.debug('Write file: ' + path); |  | ||||||
| 
 | 
 | ||||||
|             // Create file (and parent folders) to prevent errors.
 |         // Remove basePath if it's in the path.
 | ||||||
|             return this.createFile(path).then((fileEntry) => { |         path = this.removeStartingSlash(path.replace(this.basePath, '')); | ||||||
|                 if (this.isHTMLAPI && !CoreApp.instance.isDesktop() && |         this.logger.debug('Write file: ' + path); | ||||||
|                     (typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) { |  | ||||||
|                     // We need to write Blobs.
 |  | ||||||
|                     const type = CoreMimetypeUtils.instance.getMimeType(CoreMimetypeUtils.instance.getFileExtension(path)); |  | ||||||
|                     data = new Blob([data], { type: type || 'text/plain' }); |  | ||||||
|                 } |  | ||||||
| 
 | 
 | ||||||
|                 return File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append }) |         // Create file (and parent folders) to prevent errors.
 | ||||||
|                     .then(() => fileEntry); |         const fileEntry = await this.createFile(path); | ||||||
|             }); | 
 | ||||||
|         }); |         if (this.isHTMLAPI && !CoreApp.instance.isDesktop() && | ||||||
|  |                 (typeof data == 'string' || data.toString() == '[object ArrayBuffer]')) { | ||||||
|  |             // We need to write Blobs.
 | ||||||
|  |             const extension = CoreMimetypeUtils.instance.getFileExtension(path); | ||||||
|  |             const type = extension ? CoreMimetypeUtils.instance.getMimeType(extension) : ''; | ||||||
|  |             data = new Blob([data], { type: type || 'text/plain' }); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         await File.instance.writeFile(this.basePath, path, data, { replace: !append, append: !!append }); | ||||||
|  | 
 | ||||||
|  |         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()) { | 
 | ||||||
|                 // In iOS we want the internal URL (cdvfile://localhost/persistent/...).
 |         if (CoreApp.instance.isIOS()) { | ||||||
|                 return File.instance.resolveDirectoryUrl(this.basePath).then((dirEntry) => dirEntry.toInternalURL()); |             // In iOS we want the internal URL (cdvfile://localhost/persistent/...).
 | ||||||
|             } else { |             const dirEntry = await File.instance.resolveDirectoryUrl(this.basePath); | ||||||
|                 // In the other platforms we use the basePath as it is (file://...).
 | 
 | ||||||
|                 return this.basePath; |             return dirEntry.toInternalURL(); | ||||||
|             } |         } else { | ||||||
|         }); |             // In the other platforms we use the basePath as it is (file://...).
 | ||||||
|  |             return this.basePath; | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -773,18 +790,22 @@ 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)); |  | ||||||
| 
 | 
 | ||||||
|         await this.init(); |         await this.init(); | ||||||
| 
 | 
 | ||||||
| @ -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) => { |         if (destFolder && recreateDir) { | ||||||
|             fileEntry = fe; |             // Make sure the dest dir doesn't exist already.
 | ||||||
|  |             await CoreUtils.instance.ignoreErrors(this.removeDir(destFolder)); | ||||||
| 
 | 
 | ||||||
|             if (destFolder && recreateDir) { |             // Now create the dir, otherwise if any of the ancestor dirs doesn't exist the unzip would fail.
 | ||||||
|                 // Make sure the dest dir doesn't exist already.
 |             await this.createDir(destFolder); | ||||||
|                 return this.removeDir(destFolder).catch(() => { |         } | ||||||
|                     // Ignore errors.
 |  | ||||||
|                 }).then(() => |  | ||||||
|                     // Now create the dir, otherwise if any of the ancestor dirs doesn't exist the unzip would fail.
 |  | ||||||
|                     this.createDir(destFolder), |  | ||||||
|                 ); |  | ||||||
|             } |  | ||||||
|         }).then(() => { |  | ||||||
|             // 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)); |  | ||||||
| 
 | 
 | ||||||
|             return Zip.instance.unzip(fileEntry.toURL(), destFolder, onProgress); |         // If destFolder is not set, use same location as ZIP file. We need to use absolute paths (including basePath).
 | ||||||
|         }).then((result) => { |         destFolder = this.addBasePathIfNeeded(destFolder || CoreMimetypeUtils.instance.removeExtension(path)); | ||||||
|             if (result == -1) { | 
 | ||||||
|                 return Promise.reject(new CoreError('Unzip failed.')); |         const result = await Zip.instance.unzip(fileEntry.toURL(), destFolder, onProgress); | ||||||
|             } | 
 | ||||||
|         }); |         if (result == -1) { | ||||||
|  |             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.
 |  | ||||||
|             const dirAndFile = this.getFileAndDirectoryFromPath(to); |  | ||||||
| 
 | 
 | ||||||
|             return this.createDir(dirAndFile.directory).then((dirEntry) => |         // Create the destination dir if it doesn't exist.
 | ||||||
|                 // Now copy/move the file.
 |         const dirAndFile = this.getFileAndDirectoryFromPath(to); | ||||||
|                 new Promise((resolve, reject): void => { | 
 | ||||||
|                     if (copy) { |         const dirEntry = await this.createDir(dirAndFile.directory); | ||||||
|                         fileEntry.copyTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject); | 
 | ||||||
|                     } else { |         // Now copy/move the file.
 | ||||||
|                         fileEntry.moveTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject); |         return new Promise((resolve, reject): void => { | ||||||
|                     } |             if (copy) { | ||||||
|                 }), |                 fileEntry.copyTo(dirEntry, dirAndFile.name, (entry: FileEntry) => resolve(entry), reject); | ||||||
|             ); |             } else { | ||||||
|  |                 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
											
										
									
								
							| @ -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,46 +116,53 @@ export class CoreLocalNotificationsProvider { | |||||||
|             // Ignore errors.
 |             // Ignore errors.
 | ||||||
|         }); |         }); | ||||||
| 
 | 
 | ||||||
|         Platform.instance.ready().then(() => { |         this.init(); | ||||||
|             // Listen to events.
 |     } | ||||||
|             this.triggerSubscription = LocalNotifications.instance.on('trigger').subscribe((notification: ILocalNotification) => { |  | ||||||
|                 this.trigger(notification); |  | ||||||
| 
 | 
 | ||||||
|                 this.handleEvent('trigger', notification); |     /** | ||||||
|             }); |      * Init some properties. | ||||||
|  |      */ | ||||||
|  |     protected async init(): Promise<void> { | ||||||
|  |         await Platform.instance.ready(); | ||||||
| 
 | 
 | ||||||
|             this.clickSubscription = LocalNotifications.instance.on('click').subscribe((notification: ILocalNotification) => { |         // Listen to events.
 | ||||||
|                 this.handleEvent('click', notification); |         this.triggerSubscription = LocalNotifications.instance.on('trigger').subscribe((notification: ILocalNotification) => { | ||||||
|             }); |             this.trigger(notification); | ||||||
| 
 | 
 | ||||||
|             this.clearSubscription = LocalNotifications.instance.on('clear').subscribe((notification: ILocalNotification) => { |             this.handleEvent('trigger', notification); | ||||||
|                 this.handleEvent('clear', notification); |         }); | ||||||
|             }); |  | ||||||
| 
 | 
 | ||||||
|             this.cancelSubscription = LocalNotifications.instance.on('cancel').subscribe((notification: ILocalNotification) => { |         this.clickSubscription = LocalNotifications.instance.on('click').subscribe((notification: ILocalNotification) => { | ||||||
|                 this.handleEvent('cancel', notification); |             this.handleEvent('click', notification); | ||||||
|             }); |         }); | ||||||
| 
 | 
 | ||||||
|             this.addSubscription = LocalNotifications.instance.on('schedule').subscribe((notification: ILocalNotification) => { |         this.clearSubscription = LocalNotifications.instance.on('clear').subscribe((notification: ILocalNotification) => { | ||||||
|                 this.handleEvent('schedule', notification); |             this.handleEvent('clear', notification); | ||||||
|             }); |         }); | ||||||
| 
 | 
 | ||||||
|             this.updateSubscription = LocalNotifications.instance.on('update').subscribe((notification: ILocalNotification) => { |         this.cancelSubscription = LocalNotifications.instance.on('cancel').subscribe((notification: ILocalNotification) => { | ||||||
|                 this.handleEvent('update', notification); |             this.handleEvent('cancel', notification); | ||||||
|             }); |         }); | ||||||
| 
 | 
 | ||||||
|             // Create the default channel for local notifications.
 |         this.addSubscription = LocalNotifications.instance.on('schedule').subscribe((notification: ILocalNotification) => { | ||||||
|  |             this.handleEvent('schedule', notification); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         this.updateSubscription = LocalNotifications.instance.on('update').subscribe((notification: ILocalNotification) => { | ||||||
|  |             this.handleEvent('update', notification); | ||||||
|  |         }); | ||||||
|  | 
 | ||||||
|  |         // Create the default channel for local notifications.
 | ||||||
|  |         this.createDefaultChannel(); | ||||||
|  | 
 | ||||||
|  |         Translate.instance.onLangChange.subscribe(() => { | ||||||
|  |             // Update the channel name.
 | ||||||
|             this.createDefaultChannel(); |             this.createDefaultChannel(); | ||||||
| 
 |  | ||||||
|             Translate.instance.onLangChange.subscribe(() => { |  | ||||||
|                 // Update the channel name.
 |  | ||||||
|                 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]; | ||||||
| 
 | 
 | ||||||
|         // Check if request is valid.
 |         try { | ||||||
|         if (typeof request == 'object' && typeof request.table != 'undefined' && typeof request.id != 'undefined') { |             // Check if request is valid.
 | ||||||
|             // Get the code and resolve/reject all the promises of this request.
 |             if (typeof request != 'object' || request.table === undefined || request.id === undefined) { | ||||||
|             promise = this.getCode(request.table, request.id).then((code) => { |                 return; | ||||||
|                 request.deferreds.forEach((p) => { |             } | ||||||
|                     p.resolve(code); |  | ||||||
|                 }); |  | ||||||
|             }).catch((error) => { |  | ||||||
|                 request.deferreds.forEach((p) => { |  | ||||||
|                     p.reject(error); |  | ||||||
|                 }); |  | ||||||
|             }); |  | ||||||
|         } else { |  | ||||||
|             promise = Promise.resolve(); |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
|         // Once this item is treated, remove it and process next.
 |             // Get the code and resolve/reject all the promises of this request.
 | ||||||
|         promise.finally(() => { |             const code = await this.getCode(request.table, request.id); | ||||||
|  | 
 | ||||||
|  |             request.deferreds.forEach((p) => { | ||||||
|  |                 p.resolve(code); | ||||||
|  |             }); | ||||||
|  |         } catch (error) { | ||||||
|  |             request.deferreds.forEach((p) => { | ||||||
|  |                 p.reject(error); | ||||||
|  |             }); | ||||||
|  |         } finally { | ||||||
|  |             // Once this item is treated, remove it and process next.
 | ||||||
|             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); | ||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|  | |||||||
| @ -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]; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -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): {[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) => { | 
 | ||||||
|                 // Do not continue checking if a critical error happened.
 |         try { | ||||||
|                 if (error.critical) { |             return await this.checkSiteWithProtocol(siteUrl, protocol); | ||||||
|                     return Promise.reject(error); |         } catch (error) { | ||||||
|  |             // Do not continue checking if a critical error happened.
 | ||||||
|  |             if (error.critical) { | ||||||
|  |                 throw error; | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Retry with the other protocol.
 | ||||||
|  |             protocol = protocol == 'https://' ? 'http://' : 'https://'; | ||||||
|  | 
 | ||||||
|  |             try { | ||||||
|  |                 return await this.checkSiteWithProtocol(siteUrl, protocol); | ||||||
|  |             } catch (secondError) { | ||||||
|  |                 if (secondError.critical) { | ||||||
|  |                     throw secondError; | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 // Retry with the other protocol.
 |                 // Site doesn't exist. Return the error message.
 | ||||||
|                 protocol = protocol == 'https://' ? 'http://' : 'https://'; |                 if (CoreTextUtils.instance.getErrorMessageFromError(error)) { | ||||||
| 
 |                     throw error; | ||||||
|                 return this.checkSiteWithProtocol(siteUrl, protocol).catch((secondError: CoreSiteError) => { |                 } else if (CoreTextUtils.instance.getErrorMessageFromError(secondError)) { | ||||||
|                     if (secondError.critical) { |                     throw secondError; | ||||||
|                         return Promise.reject(secondError); |                 } else { | ||||||
|                     } |                     throw new CoreError(Translate.instance.instant('core.cannotconnecttrouble')); | ||||||
| 
 |                 } | ||||||
|                     // Site doesn't exist. Return the error message.
 |             } | ||||||
|                     if (CoreTextUtils.instance.getErrorMessageFromError(error)) { |  | ||||||
|                         return Promise.reject(error); |  | ||||||
|                     } else if (CoreTextUtils.instance.getErrorMessageFromError(secondError)) { |  | ||||||
|                         return Promise.reject(secondError); |  | ||||||
|                     } else { |  | ||||||
|                         return Translate.instance.instant('core.cannotconnecttrouble'); |  | ||||||
|                     } |  | ||||||
|                 }); |  | ||||||
|             }); |  | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -336,121 +341,123 @@ 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.
 |             try { | ||||||
|                     return temporarySite.getPublicConfig().then((config) => { |                 const config = await temporarySite.getPublicConfig(); | ||||||
|                         publicConfig = config; |  | ||||||
| 
 | 
 | ||||||
|                         // Check that the user can authenticate.
 |                 publicConfig = config; | ||||||
|                         if (!config.enablewebservices) { |  | ||||||
|                             return Promise.reject(new CoreSiteError({ |  | ||||||
|                                 message: Translate.instance.instant('core.login.webservicesnotenabled'), |  | ||||||
|                             })); |  | ||||||
|                         } else if (!config.enablemobilewebservice) { |  | ||||||
|                             return Promise.reject(new CoreSiteError({ |  | ||||||
|                                 message: Translate.instance.instant('core.login.mobileservicesnotenabled'), |  | ||||||
|                             })); |  | ||||||
|                         } else if (config.maintenanceenabled) { |  | ||||||
|                             let message = Translate.instance.instant('core.sitemaintenance'); |  | ||||||
|                             if (config.maintenancemessage) { |  | ||||||
|                                 message += config.maintenancemessage; |  | ||||||
|                             } |  | ||||||
| 
 | 
 | ||||||
|                             return Promise.reject(new CoreSiteError({ |                 // Check that the user can authenticate.
 | ||||||
|                                 message, |                 if (!config.enablewebservices) { | ||||||
|                             })); |                     throw new CoreSiteError({ | ||||||
|                         } |                         message: Translate.instance.instant('core.login.webservicesnotenabled'), | ||||||
|  |                     }); | ||||||
|  |                 } else if (!config.enablemobilewebservice) { | ||||||
|  |                     throw new CoreSiteError({ | ||||||
|  |                         message: Translate.instance.instant('core.login.mobileservicesnotenabled'), | ||||||
|  |                     }); | ||||||
|  |                 } else if (config.maintenanceenabled) { | ||||||
|  |                     let message = Translate.instance.instant('core.sitemaintenance'); | ||||||
|  |                     if (config.maintenancemessage) { | ||||||
|  |                         message += config.maintenancemessage; | ||||||
|  |                     } | ||||||
| 
 | 
 | ||||||
|                         // Everything ok.
 |                     throw new CoreSiteError({ | ||||||
|                         if (data.code === 0) { |                         message, | ||||||
|                             data.code = config.typeoflogin; |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         return data; |  | ||||||
|                     }, async (error) => { |  | ||||||
|                         // Error, check if not supported.
 |  | ||||||
|                         if (error.available === 1) { |  | ||||||
|                             // Service supported but an error happened. Return error.
 |  | ||||||
|                             if (error.errorcode == 'codingerror') { |  | ||||||
|                                 // This could be caused by a redirect. Check if it's the case.
 |  | ||||||
|                                 const redirect = await CoreUtils.instance.checkRedirect(siteUrl); |  | ||||||
| 
 |  | ||||||
|                                 if (redirect) { |  | ||||||
|                                     error.error = Translate.instance.instant('core.login.sitehasredirect'); |  | ||||||
|                                 } else { |  | ||||||
|                                     // We can't be sure if there is a redirect or not. Display cannot connect error.
 |  | ||||||
|                                     error.error = Translate.instance.instant('core.cannotconnecttrouble'); |  | ||||||
|                                 } |  | ||||||
|                             } |  | ||||||
| 
 |  | ||||||
|                             return Promise.reject(new CoreSiteError({ |  | ||||||
|                                 message: error.error, |  | ||||||
|                                 errorcode: error.errorcode, |  | ||||||
|                                 critical: true, |  | ||||||
|                             })); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         return data; |  | ||||||
|                     }); |                     }); | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 return data; |                 // Everything ok.
 | ||||||
|             }, (error: CoreError) => |                 if (data.code === 0) { | ||||||
|                 // Local mobile check returned an error. This only happens if the plugin is installed and it returns an error.
 |                     data.code = config.typeoflogin; | ||||||
|                 Promise.reject(new CoreSiteError({ |                 } | ||||||
|                     message: error.message, |             } catch (error) { | ||||||
|                     critical: true, |                 // Error, check if not supported.
 | ||||||
|                 })), |                 if (error.available === 1) { | ||||||
|             ).then((data: LocalMobileResponse) => { |                     // Service supported but an error happened. Return error.
 | ||||||
|                 siteUrl = temporarySite.getURL(); |                     if (error.errorcode == 'codingerror') { | ||||||
|  |                         // This could be caused by a redirect. Check if it's the case.
 | ||||||
|  |                         const redirect = await CoreUtils.instance.checkRedirect(siteUrl); | ||||||
| 
 | 
 | ||||||
|                 return { siteUrl, code: data.code, warning: data.warning, service: data.service, config: publicConfig }; |                         if (redirect) { | ||||||
|             }); |                             error.error = Translate.instance.instant('core.login.sitehasredirect'); | ||||||
|         }); |                         } else { | ||||||
|  |                             // We can't be sure if there is a redirect or not. Display cannot connect error.
 | ||||||
|  |                             error.error = Translate.instance.instant('core.cannotconnecttrouble'); | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  | 
 | ||||||
|  |                     throw new CoreSiteError({ | ||||||
|  |                         message: error.error, | ||||||
|  |                         errorcode: error.errorcode, | ||||||
|  |                         critical: true, | ||||||
|  |                     }); | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         siteUrl = temporarySite.getURL(); | ||||||
|  | 
 | ||||||
|  |         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,47 +534,46 @@ 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; | ||||||
| 
 | 
 | ||||||
|         return promise.then((data: CoreSitesLoginTokenResponse) => { |         try { | ||||||
|             if (typeof data == 'undefined') { |             data = await Http.instance.post(loginUrl, params).pipe(timeout(CoreWS.instance.getRequestTimeout())).toPromise(); | ||||||
|                 return Promise.reject(new CoreError(Translate.instance.instant('core.cannotconnecttrouble'))); |         } catch (error) { | ||||||
|  |             throw new CoreError(Translate.instance.instant('core.cannotconnecttrouble')); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         if (typeof data == 'undefined') { | ||||||
|  |             throw new CoreError(Translate.instance.instant('core.cannotconnecttrouble')); | ||||||
|  |         } else { | ||||||
|  |             if (typeof data.token != 'undefined') { | ||||||
|  |                 return { token: data.token, siteUrl, privateToken: data.privatetoken }; | ||||||
|             } else { |             } else { | ||||||
|                 if (typeof data.token != 'undefined') { |                 if (typeof data.error != 'undefined') { | ||||||
|                     return { token: data.token, siteUrl, privateToken: data.privatetoken }; |                     // We only allow one retry (to avoid loops).
 | ||||||
|                 } else { |                     if (!retry && data.errorcode == 'requirecorrectaccess') { | ||||||
|                     if (typeof data.error != 'undefined') { |                         siteUrl = CoreUrlUtils.instance.addOrRemoveWWW(siteUrl); | ||||||
|                         // We only allow one retry (to avoid loops).
 |  | ||||||
|                         if (!retry && data.errorcode == 'requirecorrectaccess') { |  | ||||||
|                             siteUrl = CoreUrlUtils.instance.addOrRemoveWWW(siteUrl); |  | ||||||
| 
 | 
 | ||||||
|                             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) { | 
 | ||||||
|                                     return Promise.reject(new CoreSiteError({ |                         if (redirect) { | ||||||
|                                         message: Translate.instance.instant('core.login.sitehasredirect'), |                             throw new CoreSiteError({ | ||||||
|                                     })); |                                 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({ |  | ||||||
|                                 message: data.error, |  | ||||||
|                                 errorcode: data.errorcode, |  | ||||||
|                             })); |  | ||||||
|                         } |                         } | ||||||
|                     } else { |  | ||||||
|                         return Promise.reject(new CoreError(Translate.instance.instant('core.login.invalidaccount'))); |  | ||||||
|                     } |                     } | ||||||
|  | 
 | ||||||
|  |                     throw new CoreSiteError({ | ||||||
|  |                         message: data.error, | ||||||
|  |                         errorcode: data.errorcode, | ||||||
|  |                     }); | ||||||
|                 } |                 } | ||||||
|  | 
 | ||||||
|  |                 throw new CoreError(Translate.instance.instant('core.login.invalidaccount')); | ||||||
|             } |             } | ||||||
|         }, () => Promise.reject(new CoreError(Translate.instance.instant('core.cannotconnecttrouble')))); |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -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,74 +601,77 @@ 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) { | ||||||
|                 const siteId = this.createSiteID(info.siteurl, info.username); |                 return this.treatInvalidAppVersion(result, siteUrl); | ||||||
| 
 |  | ||||||
|                 // Check if the site already exists.
 |  | ||||||
|                 return this.getSite(siteId).catch(() => { |  | ||||||
|                     // Not exists.
 |  | ||||||
|                 }).then((site) => { |  | ||||||
|                     if (site) { |  | ||||||
|                         // Site already exists, update its data and use it.
 |  | ||||||
|                         isNewSite = false; |  | ||||||
|                         candidateSite = site; |  | ||||||
|                         candidateSite.setToken(token); |  | ||||||
|                         candidateSite.setPrivateToken(privateToken); |  | ||||||
|                         candidateSite.setInfo(info); |  | ||||||
|                         candidateSite.setOAuthId(oauthId); |  | ||||||
|                         candidateSite.setLoggedOut(false); |  | ||||||
|                     } else { |  | ||||||
|                         // New site, set site ID and info.
 |  | ||||||
|                         isNewSite = true; |  | ||||||
|                         candidateSite.setId(siteId); |  | ||||||
|                         candidateSite.setInfo(info); |  | ||||||
|                         candidateSite.setOAuthId(oauthId); |  | ||||||
| 
 |  | ||||||
|                         // Create database tables before login and before any WS call.
 |  | ||||||
|                         return this.migrateSiteSchemas(candidateSite); |  | ||||||
|                     } |  | ||||||
|                 }).then(() => |  | ||||||
| 
 |  | ||||||
|                     // Try to get the site config.
 |  | ||||||
|                     this.getSiteConfig(candidateSite).catch((error) => { |  | ||||||
|                         // Ignore errors if it's not a new site, we'll use the config already stored.
 |  | ||||||
|                         if (isNewSite) { |  | ||||||
|                             return Promise.reject(error); |  | ||||||
|                         } |  | ||||||
|                     }).then((config) => { |  | ||||||
|                         if (typeof config != 'undefined') { |  | ||||||
|                             candidateSite.setConfig(config); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         // Add site to sites list.
 |  | ||||||
|                         this.addSite(siteId, siteUrl, token, info, privateToken, config, oauthId); |  | ||||||
|                         this.sites[siteId] = candidateSite; |  | ||||||
| 
 |  | ||||||
|                         if (login) { |  | ||||||
|                             // Turn candidate site into current site.
 |  | ||||||
|                             this.currentSite = candidateSite; |  | ||||||
|                             // Store session.
 |  | ||||||
|                             this.login(siteId); |  | ||||||
|                         } |  | ||||||
| 
 |  | ||||||
|                         CoreEvents.instance.trigger(CoreEventsProvider.SITE_ADDED, info, siteId); |  | ||||||
| 
 |  | ||||||
|                         return siteId; |  | ||||||
|                     }), |  | ||||||
|                 ); |  | ||||||
|             } |             } | ||||||
| 
 | 
 | ||||||
|             return this.treatInvalidAppVersion(result, siteUrl); |             const siteId = this.createSiteID(info.siteurl, info.username); | ||||||
|         }).catch((error) => { | 
 | ||||||
|  |             // Check if the site already exists.
 | ||||||
|  |             const site = await CoreUtils.instance.ignoreErrors<CoreSite>(this.getSite(siteId)); | ||||||
|  | 
 | ||||||
|  |             if (site) { | ||||||
|  |                 // Site already exists, update its data and use it.
 | ||||||
|  |                 isNewSite = false; | ||||||
|  |                 candidateSite = site; | ||||||
|  |                 candidateSite.setToken(token); | ||||||
|  |                 candidateSite.setPrivateToken(privateToken); | ||||||
|  |                 candidateSite.setInfo(info); | ||||||
|  |                 candidateSite.setOAuthId(oauthId); | ||||||
|  |                 candidateSite.setLoggedOut(false); | ||||||
|  |             } else { | ||||||
|  |                 // New site, set site ID and info.
 | ||||||
|  |                 isNewSite = true; | ||||||
|  |                 candidateSite.setId(siteId); | ||||||
|  |                 candidateSite.setInfo(info); | ||||||
|  |                 candidateSite.setOAuthId(oauthId); | ||||||
|  | 
 | ||||||
|  |                 // Create database tables before login and before any WS call.
 | ||||||
|  |                 await this.migrateSiteSchemas(candidateSite); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Try to get the site config.
 | ||||||
|  |             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.
 | ||||||
|  |                 if (isNewSite) { | ||||||
|  |                     throw error; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             if (typeof config != 'undefined') { | ||||||
|  |                 candidateSite.setConfig(config); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             // Add site to sites list.
 | ||||||
|  |             this.addSite(siteId, siteUrl, token, info, privateToken, config, oauthId); | ||||||
|  |             this.sites[siteId] = candidateSite; | ||||||
|  | 
 | ||||||
|  |             if (login) { | ||||||
|  |                 // Turn candidate site into current site.
 | ||||||
|  |                 this.currentSite = candidateSite; | ||||||
|  |                 // Store session.
 | ||||||
|  |                 this.login(siteId); | ||||||
|  |             } | ||||||
|  | 
 | ||||||
|  |             CoreEvents.instance.trigger(CoreEventsProvider.SITE_ADDED, info, siteId); | ||||||
|  | 
 | ||||||
|  |             return siteId; | ||||||
|  |         } 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,47 +877,55 @@ 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) { | ||||||
|             const requiredVersion = this.convertVersionName(config.tool_mobile_minimumversion); |             return; | ||||||
|             const appVersion = this.convertVersionName(CoreConfigConstants.versionname); |         } | ||||||
| 
 | 
 | ||||||
|             if (requiredVersion > appVersion) { |         const requiredVersion = this.convertVersionName(config.tool_mobile_minimumversion); | ||||||
|                 const storesConfig: CoreStoreConfig = { |         const appVersion = this.convertVersionName(CoreConfigConstants.versionname); | ||||||
|                     android: config.tool_mobile_androidappid || null, |  | ||||||
|                     ios: config.tool_mobile_iosappid || null, |  | ||||||
|                     desktop: config.tool_mobile_setuplink || 'https://download.moodle.org/desktop/', |  | ||||||
|                     mobile: config.tool_mobile_setuplink || 'https://download.moodle.org/mobile/', |  | ||||||
|                     default: config.tool_mobile_setuplink, |  | ||||||
|                 }; |  | ||||||
| 
 | 
 | ||||||
|                 const downloadUrl = CoreApp.instance.getAppStoreUrl(storesConfig); |         if (requiredVersion > appVersion) { | ||||||
|  |             const storesConfig: CoreStoreConfig = { | ||||||
|  |                 android: config.tool_mobile_androidappid, | ||||||
|  |                 ios: config.tool_mobile_iosappid, | ||||||
|  |                 desktop: config.tool_mobile_setuplink || 'https://download.moodle.org/desktop/', | ||||||
|  |                 mobile: config.tool_mobile_setuplink || 'https://download.moodle.org/mobile/', | ||||||
|  |                 default: config.tool_mobile_setuplink, | ||||||
|  |             }; | ||||||
| 
 | 
 | ||||||
|                 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.
 | ||||||
|                     if (siteId == this.getCurrentSiteId()) { |                 if (siteId == this.getCurrentSiteId()) { | ||||||
|                         await this.logout(); |                     await this.logout(); | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     // Always expire the token.
 |  | ||||||
|                     await this.setSiteLoggedOut(siteId, true); |  | ||||||
|                 } |                 } | ||||||
| 
 | 
 | ||||||
|                 throw new CoreError('Current app version is lower than required version.'); |                 // Always expire the token.
 | ||||||
|  |                 await this.setSiteLoggedOut(siteId, true); | ||||||
|             } |             } | ||||||
|  | 
 | ||||||
|  |             throw new CoreError('Current app version is lower than required version.'); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -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,31 +1781,31 @@ 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 = { |  | ||||||
|                 siteIds, |  | ||||||
|                 site: undefined, |  | ||||||
|             }; |  | ||||||
| 
 | 
 | ||||||
|             if (siteIds.length > 0) { |         const result: {site?: CoreSite; siteIds: string[]} = { | ||||||
|                 // If more than one site is returned it usually means there are different users stored. Use any of them.
 |             siteIds, | ||||||
|                 return this.getSite(siteIds[0]).then((site) => { |         }; | ||||||
|                     const siteUrl = CoreTextUtils.instance.removeEndingSlash( |  | ||||||
|                         CoreUrlUtils.instance.removeProtocolAndWWW(site.getURL())); |  | ||||||
|                     const treatedUrl = CoreTextUtils.instance.removeEndingSlash(CoreUrlUtils.instance.removeProtocolAndWWW(url)); |  | ||||||
| 
 |  | ||||||
|                     if (siteUrl == treatedUrl) { |  | ||||||
|                         result.site = site; |  | ||||||
|                     } |  | ||||||
| 
 |  | ||||||
|                     return result; |  | ||||||
|                 }); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|  |         if (!siteIds.length) { | ||||||
|             return result; |             return result; | ||||||
|         }); |         } | ||||||
|  | 
 | ||||||
|  |         // If more than one site is returned it usually means there are different users stored. Use any of them.
 | ||||||
|  |         const site = await this.getSite(siteIds[0]); | ||||||
|  | 
 | ||||||
|  |         const siteUrl = CoreTextUtils.instance.removeEndingSlash( | ||||||
|  |             CoreUrlUtils.instance.removeProtocolAndWWW(site.getURL()), | ||||||
|  |         ); | ||||||
|  |         const treatedUrl = CoreTextUtils.instance.removeEndingSlash(CoreUrlUtils.instance.removeProtocolAndWWW(url)); | ||||||
|  | 
 | ||||||
|  |         if (siteUrl == treatedUrl) { | ||||||
|  |             result.site = site; | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|  |         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. | ||||||
|  | |||||||
| @ -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,73 +143,88 @@ 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) => { |  | ||||||
|                     if (CoreApp.instance.isAndroid()) { |  | ||||||
|                         return availableBytes; |  | ||||||
|                     } else { |  | ||||||
|                         // Space calculation is not accurate on iOS, but it gets more accurate when space is lower.
 |  | ||||||
|                         // We'll only use it when space is <500MB, or we're downloading more than twice the reported space.
 |  | ||||||
|                         if (availableBytes < CoreConstants.IOS_FREE_SPACE_THRESHOLD || size.size > availableBytes / 2) { |  | ||||||
|                             return availableBytes; |  | ||||||
|                         } else { |  | ||||||
|                             return null; |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }).then((availableBytes) => { |  | ||||||
|                     resolve(availableBytes); |  | ||||||
|                 }); |  | ||||||
|             } |             } | ||||||
|         }); |  | ||||||
| 
 | 
 | ||||||
|         const getAvailableSpace = getAvailableBytes.then((availableBytes: number) => { |             const availableBytes = await CoreFile.instance.calculateFreeSpace(); | ||||||
|  | 
 | ||||||
|  |             if (CoreApp.instance.isAndroid()) { | ||||||
|  |                 return availableBytes; | ||||||
|  |             } else { | ||||||
|  |                 // Space calculation is not accurate on iOS, but it gets more accurate when space is lower.
 | ||||||
|  |                 // We'll only use it when space is <500MB, or we're downloading more than twice the reported space.
 | ||||||
|  |                 if (availableBytes < CoreConstants.IOS_FREE_SPACE_THRESHOLD || size.size > availableBytes / 2) { | ||||||
|  |                     return availableBytes; | ||||||
|  |                 } else { | ||||||
|  |                     return null; | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         }; | ||||||
|  | 
 | ||||||
|  |         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 }); | ||||||
|             } |             } | ||||||
|         }); |         }; | ||||||
| 
 | 
 | ||||||
|         return getAvailableSpace.then((availableSpace) => { |         const availableBytes = await getAvailableBytes(); | ||||||
|             wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold; |  | ||||||
|             limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.DOWNLOAD_THRESHOLD : limitedThreshold; |  | ||||||
| 
 | 
 | ||||||
|             let wifiPrefix = ''; |         const availableSpace = getAvailableSpace(availableBytes); | ||||||
|             if (CoreApp.instance.isNetworkAccessLimited()) { |  | ||||||
|                 wifiPrefix = Translate.instance.instant('core.course.confirmlimiteddownload'); |  | ||||||
|             } |  | ||||||
| 
 | 
 | ||||||
|             if (size.size < 0 || (size.size == 0 && !size.total)) { |         wifiThreshold = typeof wifiThreshold == 'undefined' ? CoreConstants.WIFI_DOWNLOAD_THRESHOLD : wifiThreshold; | ||||||
|                 // Seems size was unable to be calculated. Show a warning.
 |         limitedThreshold = typeof limitedThreshold == 'undefined' ? CoreConstants.DOWNLOAD_THRESHOLD : limitedThreshold; | ||||||
|                 unknownMessage = unknownMessage || 'core.course.confirmdownloadunknownsize'; |  | ||||||
| 
 | 
 | ||||||
|                 return this.showConfirm(wifiPrefix + Translate.instance.instant( |         let wifiPrefix = ''; | ||||||
|                     unknownMessage, { availableSpace: availableSpace })); |         if (CoreApp.instance.isNetworkAccessLimited()) { | ||||||
|             } else if (!size.total) { |             wifiPrefix = Translate.instance.instant('core.course.confirmlimiteddownload'); | ||||||
|                 // Filesize is only partial.
 |         } | ||||||
| 
 | 
 | ||||||
|                 return this.showConfirm(wifiPrefix + Translate.instance.instant('core.course.confirmpartialdownloadsize', |         if (size.size < 0 || (size.size == 0 && !size.total)) { | ||||||
|                     { size: readableSize, availableSpace: availableSpace })); |             // Seems size was unable to be calculated. Show a warning.
 | ||||||
|             } else if (alwaysConfirm || size.size >= wifiThreshold || |             unknownMessage = unknownMessage || 'core.course.confirmdownloadunknownsize'; | ||||||
|  | 
 | ||||||
|  |             return this.showConfirm( | ||||||
|  |                 wifiPrefix + Translate.instance.instant( | ||||||
|  |                     unknownMessage, | ||||||
|  |                     { availableSpace: availableSpace }, | ||||||
|  |                 ), | ||||||
|  |             ); | ||||||
|  |         } else if (!size.total) { | ||||||
|  |             // Filesize is only partial.
 | ||||||
|  | 
 | ||||||
|  |             return this.showConfirm( | ||||||
|  |                 wifiPrefix + Translate.instance.instant( | ||||||
|  |                     'core.course.confirmpartialdownloadsize', | ||||||
|  |                     { size: readableSize, availableSpace: availableSpace }, | ||||||
|  |                 ), | ||||||
|  |             ); | ||||||
|  |         } 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, | ||||||
|  |         ); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
| @ -1304,6 +1359,11 @@ export class CoreDomUtilsProvider { | |||||||
|         needsTranslate?: boolean, |         needsTranslate?: boolean, | ||||||
|         autocloseTime?: number, |         autocloseTime?: number, | ||||||
|     ): Promise<HTMLIonAlertElement | null> { |     ): Promise<HTMLIonAlertElement | null> { | ||||||
|  |         if (this.isCanceledError(error)) { | ||||||
|  |             // It's a canceled error, don't display an error.
 | ||||||
|  |             return Promise.resolve(null); | ||||||
|  |         } | ||||||
|  | 
 | ||||||
|         const message = this.getErrorMessage(error, needsTranslate); |         const message = this.getErrorMessage(error, needsTranslate); | ||||||
| 
 | 
 | ||||||
|         if (message === null) { |         if (message === null) { | ||||||
| @ -1334,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); | ||||||
| @ -1423,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); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -1443,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'); | ||||||
| 
 | 
 | ||||||
| @ -1532,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++); | ||||||
| 
 | 
 | ||||||
| @ -1602,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
 | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -1614,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) => { | ||||||
| @ -1646,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); | ||||||
|     } |     } | ||||||
| @ -1675,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; | ||||||
|         } |         } | ||||||
| @ -1690,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; | ||||||
|  | |||||||
| @ -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.
 | ||||||
|  | |||||||
| @ -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 = ''; | ||||||
|  | |||||||
| @ -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 ''; | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -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(); | ||||||
|             if (CoreApp.instance.isIOS()) { |     } | ||||||
|                 NativeHttp.instance.setHeader('*', 'User-Agent', navigator.userAgent); | 
 | ||||||
|             } |     /** | ||||||
|         }); |      * Initialize some data. | ||||||
|  |      */ | ||||||
|  |     protected async init(): Promise<void> { | ||||||
|  |         await Platform.instance.ready(); | ||||||
|  | 
 | ||||||
|  |         if (CoreApp.instance.isIOS()) { | ||||||
|  |             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'; | ||||||
| @ -113,9 +119,9 @@ export class CoreWSProvider { | |||||||
|         if (this.retryCalls.length > 0) { |         if (this.retryCalls.length > 0) { | ||||||
|             this.logger.warn('Calls locked, trying later...'); |             this.logger.warn('Calls locked, trying later...'); | ||||||
| 
 | 
 | ||||||
|             return this.addToRetryQueue<T>(method, siteUrl, data, preSets); |             return this.addToRetryQueue<T>(method, siteUrl, dataToSend, preSets); | ||||||
|         } else { |         } else { | ||||||
|             return this.performPost<T>(method, siteUrl, data, preSets); |             return this.performPost<T>(method, siteUrl, dataToSend, preSets); | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| @ -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.
 | ||||||
| }; | }; | ||||||
|  | |||||||
| @ -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); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     /** |     /** | ||||||
|  | |||||||
| @ -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++) { | ||||||
|  | |||||||
| @ -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); | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| } | } | ||||||
|  | |||||||
| @ -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); | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  | |||||||
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/login_logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/img/login_logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 16 KiB | 
							
								
								
									
										
											BIN
										
									
								
								src/assets/img/user-avatar.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								src/assets/img/user-avatar.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.2 KiB | 
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user