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;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -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,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -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 {}
|
||||||
|
|
|
@ -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>
|
|
@ -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%);
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -0,0 +1,3 @@
|
||||||
|
.item-ios .item-button[icon-only] ion-icon {
|
||||||
|
font-size: 2.1em;
|
||||||
|
}
|
File diff suppressed because it is too large
Load Diff
|
@ -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 {}
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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;
|
||||||
|
};
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
Loading…
Reference in New Issue