2020-10-07 08:52:51 +00:00
|
|
|
// (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.
|
|
|
|
|
2020-10-14 06:28:02 +00:00
|
|
|
import { InAppBrowserObject, InAppBrowserOptions } from '@ionic-native/in-app-browser';
|
2020-10-07 08:52:51 +00:00
|
|
|
import { Md5 } from 'ts-md5/dist/md5';
|
|
|
|
|
|
|
|
import { CoreApp } from '@services/app';
|
2022-05-11 12:06:42 +00:00
|
|
|
import { CoreNetwork } from '@services/network';
|
2020-10-07 08:52:51 +00:00
|
|
|
import { CoreDB } from '@services/db';
|
2021-03-10 11:30:18 +00:00
|
|
|
import { CoreEvents } from '@singletons/events';
|
2020-10-07 08:52:51 +00:00
|
|
|
import { CoreFile } from '@services/file';
|
2020-11-06 14:32:00 +00:00
|
|
|
import {
|
|
|
|
CoreWS,
|
|
|
|
CoreWSPreSets,
|
|
|
|
CoreWSFileUploadOptions,
|
|
|
|
CoreWSAjaxPreSets,
|
|
|
|
CoreWSExternalWarning,
|
|
|
|
CoreWSUploadFileResult,
|
2021-01-19 09:00:40 +00:00
|
|
|
CoreWSPreSetsSplitRequest,
|
2020-11-06 14:32:00 +00:00
|
|
|
} from '@services/ws';
|
2022-05-30 13:18:01 +00:00
|
|
|
import { CoreDomUtils, ToastDuration } from '@services/utils/dom';
|
2020-10-07 08:52:51 +00:00
|
|
|
import { CoreTextUtils } from '@services/utils/text';
|
|
|
|
import { CoreTimeUtils } from '@services/utils/time';
|
2020-10-14 14:38:24 +00:00
|
|
|
import { CoreUrlUtils, CoreUrlParams } from '@services/utils/url';
|
2022-05-30 16:13:43 +00:00
|
|
|
import { CoreUtils, CoreUtilsOpenInBrowserOptions } from '@services/utils/utils';
|
2020-11-19 11:40:18 +00:00
|
|
|
import { CoreConstants } from '@/core/constants';
|
2020-10-14 06:28:02 +00:00
|
|
|
import { SQLiteDB } from '@classes/sqlitedb';
|
|
|
|
import { CoreError } from '@classes/errors/error';
|
|
|
|
import { CoreWSError } from '@classes/errors/wserror';
|
2020-10-07 08:52:51 +00:00
|
|
|
import { CoreLogger } from '@singletons/logger';
|
2020-11-24 08:31:11 +00:00
|
|
|
import { Translate } from '@singletons';
|
2020-10-21 14:32:27 +00:00
|
|
|
import { CoreIonLoadingElement } from './ion-loading';
|
2021-10-14 11:13:27 +00:00
|
|
|
import { CoreLang } from '@services/lang';
|
2022-04-08 10:19:54 +00:00
|
|
|
import { CoreSites, CoreSitesReadingStrategy } from '@services/sites';
|
2022-02-03 12:56:36 +00:00
|
|
|
import { asyncInstance, AsyncInstance } from '../utils/async-instance';
|
|
|
|
import { CoreDatabaseTable } from './database/database-table';
|
2022-02-07 12:46:09 +00:00
|
|
|
import { CoreDatabaseCachingStrategy } from './database/database-table-proxy';
|
2022-02-24 15:10:51 +00:00
|
|
|
import { CoreSilentError } from './errors/silenterror';
|
2022-05-30 16:13:43 +00:00
|
|
|
import { CorePromisedValue } from '@classes/promised-value';
|
2022-06-13 06:27:59 +00:00
|
|
|
import {
|
|
|
|
CONFIG_TABLE,
|
|
|
|
CoreSiteConfigDBRecord,
|
|
|
|
CoreSiteLastViewedDBRecord,
|
|
|
|
CoreSiteWSCacheRecord,
|
|
|
|
LAST_VIEWED_TABLE,
|
|
|
|
WSGroups,
|
|
|
|
WS_CACHE_TABLES_PREFIX,
|
|
|
|
} from '@services/database/sites';
|
2022-07-01 12:48:55 +00:00
|
|
|
import { Observable, ObservableInput, ObservedValueOf, OperatorFunction, Subject } from 'rxjs';
|
|
|
|
import { finalize, map, mergeMap } from 'rxjs/operators';
|
2022-07-18 10:22:38 +00:00
|
|
|
import { firstValueFrom } from '../utils/rxjs';
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2021-09-15 09:45:48 +00:00
|
|
|
/**
|
|
|
|
* QR Code type enumeration.
|
|
|
|
*/
|
|
|
|
export enum CoreSiteQRCodeType {
|
|
|
|
QR_CODE_DISABLED = 0, // QR code disabled value
|
|
|
|
QR_CODE_URL = 1, // QR code type URL value
|
|
|
|
QR_CODE_LOGIN = 2, // QR code type login value
|
|
|
|
}
|
|
|
|
|
2021-11-17 09:36:57 +00:00
|
|
|
// WS that we allow to call even if the site is logged out.
|
|
|
|
const ALLOWED_LOGGEDOUT_WS = [
|
|
|
|
'core_user_remove_user_device',
|
|
|
|
];
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
/**
|
|
|
|
* Class that represents a site (combination of site + user).
|
|
|
|
* It will have all the site data and provide utility functions regarding a site.
|
2020-10-28 13:25:18 +00:00
|
|
|
* To add tables to the site's database, please use registerSiteSchema exported in @services/sites.ts. This will make sure that
|
2020-10-07 08:52:51 +00:00
|
|
|
* the tables are created in all the sites, not just the current one.
|
2020-10-21 14:32:27 +00:00
|
|
|
*
|
|
|
|
* @todo: Refactor this class to improve "temporary" sites support (not fully authenticated).
|
2020-10-07 08:52:51 +00:00
|
|
|
*/
|
|
|
|
export class CoreSite {
|
2020-10-14 06:28:02 +00:00
|
|
|
|
|
|
|
static readonly REQUEST_QUEUE_FORCE_WS = false; // Use "tool_mobile_call_external_functions" even for calling a single function.
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
// Constants for cache update frequency.
|
2020-10-14 06:28:02 +00:00
|
|
|
static readonly FREQUENCY_USUALLY = 0;
|
|
|
|
static readonly FREQUENCY_OFTEN = 1;
|
|
|
|
static readonly FREQUENCY_SOMETIMES = 2;
|
|
|
|
static readonly FREQUENCY_RARELY = 3;
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2021-09-03 07:26:02 +00:00
|
|
|
static readonly MINIMUM_MOODLE_VERSION = '3.5';
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
// Versions of Moodle releases.
|
2021-09-03 07:26:02 +00:00
|
|
|
static readonly MOODLE_RELEASES = {
|
2021-02-18 08:19:38 +00:00
|
|
|
'3.5': 2018051700,
|
|
|
|
'3.6': 2018120300,
|
|
|
|
'3.7': 2019052000,
|
|
|
|
'3.8': 2019111800,
|
|
|
|
'3.9': 2020061500,
|
|
|
|
'3.10': 2020110900,
|
2021-05-18 15:17:48 +00:00
|
|
|
'3.11': 2021051700,
|
2021-10-06 06:41:39 +00:00
|
|
|
'4.0': 2021100300, // @todo [4.0] replace with right value when released. Using a tmp value to be able to test new things.
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Possible cache update frequencies.
|
2020-10-14 06:28:02 +00:00
|
|
|
protected readonly UPDATE_FREQUENCIES = [
|
2020-10-21 15:56:01 +00:00
|
|
|
CoreConstants.CONFIG.cache_update_frequency_usually || 420000,
|
|
|
|
CoreConstants.CONFIG.cache_update_frequency_often || 1200000,
|
|
|
|
CoreConstants.CONFIG.cache_update_frequency_sometimes || 3600000,
|
|
|
|
CoreConstants.CONFIG.cache_update_frequency_rarely || 43200000,
|
2020-10-07 08:52:51 +00:00
|
|
|
];
|
|
|
|
|
|
|
|
// Rest of variables.
|
|
|
|
protected logger: CoreLogger;
|
2020-10-21 14:32:27 +00:00
|
|
|
protected db?: SQLiteDB;
|
2022-06-13 06:27:59 +00:00
|
|
|
protected cacheTables: Record<WSGroups, AsyncInstance<CoreDatabaseTable<CoreSiteWSCacheRecord>>>;
|
2022-02-10 14:59:54 +00:00
|
|
|
protected configTable: AsyncInstance<CoreDatabaseTable<CoreSiteConfigDBRecord, 'name'>>;
|
2022-02-28 11:38:46 +00:00
|
|
|
protected lastViewedTable: AsyncInstance<CoreDatabaseTable<CoreSiteLastViewedDBRecord, 'component' | 'id'>>;
|
2020-10-07 08:52:51 +00:00
|
|
|
protected cleanUnicode = false;
|
|
|
|
protected lastAutoLogin = 0;
|
|
|
|
protected offlineDisabled = false;
|
2020-10-14 06:28:02 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2022-06-21 08:58:10 +00:00
|
|
|
protected ongoingRequests: { [cacheId: string]: Observable<any> } = {};
|
2020-10-07 08:52:51 +00:00
|
|
|
protected requestQueue: RequestQueueItem[] = [];
|
2020-10-21 14:32:27 +00:00
|
|
|
protected requestQueueTimeout: number | null = null;
|
|
|
|
protected tokenPluginFileWorks?: boolean;
|
|
|
|
protected tokenPluginFileWorksPromise?: Promise<boolean>;
|
|
|
|
protected oauthId?: number;
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Create a site.
|
|
|
|
*
|
|
|
|
* @param id Site ID.
|
|
|
|
* @param siteUrl Site URL.
|
|
|
|
* @param token Site's WS token.
|
|
|
|
* @param info Site info.
|
|
|
|
* @param privateToken Private token.
|
|
|
|
* @param config Site public config.
|
|
|
|
* @param loggedOut Whether user is logged out.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
constructor(
|
|
|
|
public id: string | undefined,
|
|
|
|
public siteUrl: string,
|
|
|
|
public token?: string,
|
|
|
|
public infos?: CoreSiteInfo,
|
|
|
|
public privateToken?: string,
|
|
|
|
public config?: CoreSiteConfig,
|
|
|
|
public loggedOut?: boolean,
|
|
|
|
) {
|
|
|
|
this.logger = CoreLogger.getInstance('CoreSite');
|
2021-07-06 06:21:10 +00:00
|
|
|
this.siteUrl = CoreUrlUtils.removeUrlParams(this.siteUrl); // Make sure the URL doesn't have params.
|
2022-06-13 06:27:59 +00:00
|
|
|
|
|
|
|
this.cacheTables = Object.values(WSGroups).reduce((tables, group) => {
|
|
|
|
tables[group] = asyncInstance(() => CoreSites.getSiteTable(WS_CACHE_TABLES_PREFIX + group, {
|
|
|
|
siteId: this.getId(),
|
|
|
|
database: this.getDb(),
|
|
|
|
config: { cachingStrategy: CoreDatabaseCachingStrategy.None },
|
|
|
|
}));
|
|
|
|
|
|
|
|
return tables;
|
|
|
|
}, <Record<WSGroups, AsyncInstance<CoreDatabaseTable<CoreSiteWSCacheRecord>>>> {});
|
|
|
|
|
|
|
|
this.configTable = asyncInstance(() => CoreSites.getSiteTable(CONFIG_TABLE, {
|
2022-02-10 14:59:54 +00:00
|
|
|
siteId: this.getId(),
|
|
|
|
database: this.getDb(),
|
|
|
|
config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
|
|
|
|
primaryKeyColumns: ['name'],
|
|
|
|
}));
|
2022-06-13 06:27:59 +00:00
|
|
|
|
|
|
|
this.lastViewedTable = asyncInstance(() => CoreSites.getSiteTable(LAST_VIEWED_TABLE, {
|
2022-02-28 11:38:46 +00:00
|
|
|
siteId: this.getId(),
|
|
|
|
database: this.getDb(),
|
2022-03-09 13:38:00 +00:00
|
|
|
config: { cachingStrategy: CoreDatabaseCachingStrategy.Eager },
|
2022-02-28 11:38:46 +00:00
|
|
|
primaryKeyColumns: ['component', 'id'],
|
|
|
|
}));
|
2020-10-07 08:52:51 +00:00
|
|
|
this.setInfo(infos);
|
|
|
|
this.calculateOfflineDisabled();
|
|
|
|
|
|
|
|
if (this.id) {
|
|
|
|
this.initDB();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Initialize the database.
|
|
|
|
*/
|
|
|
|
initDB(): void {
|
2021-03-02 10:41:04 +00:00
|
|
|
this.db = CoreDB.getDB('Site-' + this.id);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get site ID.
|
|
|
|
*
|
|
|
|
* @return Site ID.
|
|
|
|
*/
|
|
|
|
getId(): string {
|
2021-05-19 13:46:21 +00:00
|
|
|
if (this.id === undefined) {
|
2020-10-21 14:32:27 +00:00
|
|
|
// Shouldn't happen for authenticated sites.
|
|
|
|
throw new CoreError('This site doesn\'t have an ID');
|
|
|
|
}
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
return this.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get site URL.
|
|
|
|
*
|
|
|
|
* @return Site URL.
|
|
|
|
*/
|
|
|
|
getURL(): string {
|
|
|
|
return this.siteUrl;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get site token.
|
|
|
|
*
|
|
|
|
* @return Site token.
|
|
|
|
*/
|
|
|
|
getToken(): string {
|
2021-05-19 13:46:21 +00:00
|
|
|
if (this.token === undefined) {
|
2020-10-21 14:32:27 +00:00
|
|
|
// Shouldn't happen for authenticated sites.
|
|
|
|
throw new CoreError('This site doesn\'t have a token');
|
|
|
|
}
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
return this.token;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get site info.
|
|
|
|
*
|
|
|
|
* @return Site info.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
getInfo(): CoreSiteInfo | undefined {
|
2020-10-07 08:52:51 +00:00
|
|
|
return this.infos;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get site private token.
|
|
|
|
*
|
|
|
|
* @return Site private token.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
getPrivateToken(): string | undefined {
|
2020-10-07 08:52:51 +00:00
|
|
|
return this.privateToken;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get site DB.
|
|
|
|
*
|
|
|
|
* @return Site DB.
|
|
|
|
*/
|
|
|
|
getDb(): SQLiteDB {
|
2020-10-21 14:32:27 +00:00
|
|
|
if (!this.db) {
|
|
|
|
// Shouldn't happen for authenticated sites.
|
|
|
|
throw new CoreError('Site DB doesn\'t exist');
|
|
|
|
}
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
return this.db;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get site user's ID.
|
|
|
|
*
|
|
|
|
* @return User's ID.
|
|
|
|
*/
|
|
|
|
getUserId(): number {
|
2020-10-21 14:32:27 +00:00
|
|
|
if (!this.infos) {
|
|
|
|
// Shouldn't happen for authenticated sites.
|
|
|
|
throw new CoreError('Site info could not be fetched.');
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
2020-10-21 14:32:27 +00:00
|
|
|
|
|
|
|
return this.infos.userid;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get site Course ID for frontpage course. If not declared it will return 1 as default.
|
|
|
|
*
|
|
|
|
* @return Site Home ID.
|
|
|
|
*/
|
|
|
|
getSiteHomeId(): number {
|
2020-10-21 14:32:27 +00:00
|
|
|
return this.infos?.siteid || 1;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get site name.
|
|
|
|
*
|
|
|
|
* @return Site name.
|
|
|
|
*/
|
|
|
|
getSiteName(): string {
|
2020-10-21 15:56:01 +00:00
|
|
|
if (CoreConstants.CONFIG.sitename) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Overridden by config.
|
2020-10-21 15:56:01 +00:00
|
|
|
return CoreConstants.CONFIG.sitename;
|
2020-10-07 08:52:51 +00:00
|
|
|
} else {
|
2020-10-21 14:32:27 +00:00
|
|
|
return this.infos?.sitename || '';
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set site ID.
|
|
|
|
*
|
|
|
|
* @param New ID.
|
|
|
|
*/
|
|
|
|
setId(id: string): void {
|
|
|
|
this.id = id;
|
|
|
|
this.initDB();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set site token.
|
|
|
|
*
|
|
|
|
* @param New token.
|
|
|
|
*/
|
|
|
|
setToken(token: string): void {
|
|
|
|
this.token = token;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set site private token.
|
|
|
|
*
|
|
|
|
* @param privateToken New private token.
|
|
|
|
*/
|
|
|
|
setPrivateToken(privateToken: string): void {
|
|
|
|
this.privateToken = privateToken;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if user logged out from the site and needs to authenticate again.
|
|
|
|
*
|
|
|
|
* @return Whether is logged out.
|
|
|
|
*/
|
|
|
|
isLoggedOut(): boolean {
|
|
|
|
return !!this.loggedOut;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get OAuth ID.
|
|
|
|
*
|
|
|
|
* @return OAuth ID.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
getOAuthId(): number | undefined {
|
2020-10-07 08:52:51 +00:00
|
|
|
return this.oauthId;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set site info.
|
|
|
|
*
|
|
|
|
* @param New info.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
setInfo(infos?: CoreSiteInfo): void {
|
2020-10-07 08:52:51 +00:00
|
|
|
this.infos = infos;
|
|
|
|
|
|
|
|
// Index function by name to speed up wsAvailable method.
|
2020-10-21 14:32:27 +00:00
|
|
|
if (infos?.functions) {
|
2021-10-14 13:03:13 +00:00
|
|
|
infos.functionsByName = CoreUtils.arrayToObject(infos.functions, 'name');
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set site config.
|
|
|
|
*
|
|
|
|
* @param config Config.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
setConfig(config: CoreSiteConfig): void {
|
2020-10-07 08:52:51 +00:00
|
|
|
if (config) {
|
2021-03-02 10:41:04 +00:00
|
|
|
config.tool_mobile_disabledfeatures = CoreTextUtils.treatDisabledFeatures(config.tool_mobile_disabledfeatures);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.config = config;
|
|
|
|
this.calculateOfflineDisabled();
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set site logged out.
|
|
|
|
*
|
|
|
|
* @param loggedOut True if logged out and needs to authenticate again, false otherwise.
|
|
|
|
*/
|
|
|
|
setLoggedOut(loggedOut: boolean): void {
|
|
|
|
this.loggedOut = !!loggedOut;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set OAuth ID.
|
|
|
|
*
|
|
|
|
* @param oauth OAuth ID.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
setOAuthId(oauthId: number | undefined): void {
|
2020-10-07 08:52:51 +00:00
|
|
|
this.oauthId = oauthId;
|
|
|
|
}
|
|
|
|
|
2022-08-19 11:15:06 +00:00
|
|
|
/**
|
|
|
|
* Check if current user is Admin.
|
|
|
|
* Works properly since v3.8. See more in: {@link} https://tracker.moodle.org/browse/MDL-65550
|
|
|
|
*
|
|
|
|
* @returns Whether the user is Admin.
|
|
|
|
*/
|
|
|
|
isAdmin(): boolean {
|
|
|
|
return this.getInfo()?.userissiteadmin ?? false;
|
|
|
|
}
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
/**
|
|
|
|
* Check if the user authenticated in the site using an OAuth method.
|
|
|
|
*
|
|
|
|
* @return Whether the user authenticated in the site using an OAuth method.
|
|
|
|
*/
|
|
|
|
isOAuth(): boolean {
|
2021-12-16 09:46:40 +00:00
|
|
|
return this.oauthId != null && this.oauthId !== undefined;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Can the user access their private files?
|
|
|
|
*
|
|
|
|
* @return Whether can access my files.
|
|
|
|
*/
|
|
|
|
canAccessMyFiles(): boolean {
|
2020-10-21 14:32:27 +00:00
|
|
|
const info = this.getInfo();
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2021-12-16 09:46:40 +00:00
|
|
|
return !!(info && (info.usercanmanageownfiles === undefined || info.usercanmanageownfiles));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Can the user download files?
|
|
|
|
*
|
|
|
|
* @return Whether can download files.
|
|
|
|
*/
|
|
|
|
canDownloadFiles(): boolean {
|
2020-10-21 14:32:27 +00:00
|
|
|
const info = this.getInfo();
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
return !!info?.downloadfiles && info?.downloadfiles > 0;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Can the user use an advanced feature?
|
|
|
|
*
|
|
|
|
* @param feature The name of the feature.
|
|
|
|
* @param whenUndefined The value to return when the parameter is undefined.
|
|
|
|
* @return Whether can use advanced feature.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
canUseAdvancedFeature(featureName: string, whenUndefined: boolean = true): boolean {
|
|
|
|
const info = this.getInfo();
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2021-12-16 09:46:40 +00:00
|
|
|
if (info?.advancedfeatures === undefined) {
|
2020-10-21 14:32:27 +00:00
|
|
|
return whenUndefined;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
const feature = info.advancedfeatures.find((item) => item.name === featureName);
|
|
|
|
|
|
|
|
if (!feature) {
|
|
|
|
return whenUndefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
return feature.value !== 0;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Can the user upload files?
|
|
|
|
*
|
|
|
|
* @return Whether can upload files.
|
|
|
|
*/
|
|
|
|
canUploadFiles(): boolean {
|
2020-10-21 14:32:27 +00:00
|
|
|
const info = this.getInfo();
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
return !!info?.uploadfiles && info?.uploadfiles > 0;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Fetch site info from the Moodle site.
|
|
|
|
*
|
|
|
|
* @return A promise to be resolved when the site info is retrieved.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
fetchSiteInfo(): Promise<CoreSiteInfoResponse> {
|
2020-10-07 08:52:51 +00:00
|
|
|
// The get_site_info WS call won't be cached.
|
|
|
|
const preSets = {
|
|
|
|
getFromCache: false,
|
|
|
|
saveToCache: false,
|
2020-10-14 06:28:02 +00:00
|
|
|
skipQueue: true,
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
// Reset clean Unicode to check if it's supported again.
|
|
|
|
this.cleanUnicode = false;
|
|
|
|
|
|
|
|
return this.read('core_webservice_get_site_info', {}, preSets);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Read some data from the Moodle site using WS. Requests are cached by default.
|
|
|
|
*
|
|
|
|
* @param method WS method to use.
|
|
|
|
* @param data Data to send to the WS.
|
|
|
|
* @param preSets Extra options.
|
|
|
|
* @return Promise resolved with the response, rejected with CoreWSError if it fails.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2020-10-14 06:28:02 +00:00
|
|
|
read<T = unknown>(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise<T> {
|
2022-06-23 08:42:20 +00:00
|
|
|
return firstValueFrom(this.readObservable<T>(method, data, preSets));
|
2022-06-21 08:58:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Read some data from the Moodle site using WS. Requests are cached by default.
|
|
|
|
*
|
|
|
|
* @param method WS method to use.
|
|
|
|
* @param data Data to send to the WS.
|
|
|
|
* @param preSets Extra options.
|
|
|
|
* @return Observable.
|
|
|
|
*/
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
readObservable<T = unknown>(method: string, data: any, preSets?: CoreSiteWSPreSets): Observable<T> {
|
2020-10-07 08:52:51 +00:00
|
|
|
preSets = preSets || {};
|
2022-06-21 08:58:10 +00:00
|
|
|
preSets.getFromCache = preSets.getFromCache ?? true;
|
|
|
|
preSets.saveToCache = preSets.saveToCache ?? true;
|
|
|
|
preSets.reusePending = preSets.reusePending ?? true;
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-21 08:58:10 +00:00
|
|
|
return this.requestObservable<T>(method, data, preSets);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends some data to the Moodle site using WS. Requests are NOT cached by default.
|
|
|
|
*
|
|
|
|
* @param method WS method to use.
|
|
|
|
* @param data Data to send to the WS.
|
|
|
|
* @param preSets Extra options.
|
|
|
|
* @return Promise resolved with the response, rejected with CoreWSError if it fails.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2020-10-14 06:28:02 +00:00
|
|
|
write<T = unknown>(method: string, data: any, preSets?: CoreSiteWSPreSets): Promise<T> {
|
2022-06-23 08:42:20 +00:00
|
|
|
return firstValueFrom(this.writeObservable<T>(method, data, preSets));
|
2022-06-21 08:58:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Sends some data to the Moodle site using WS. Requests are NOT cached by default.
|
|
|
|
*
|
|
|
|
* @param method WS method to use.
|
|
|
|
* @param data Data to send to the WS.
|
|
|
|
* @param preSets Extra options.
|
|
|
|
* @return Observable.
|
|
|
|
*/
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
writeObservable<T = unknown>(method: string, data: any, preSets?: CoreSiteWSPreSets): Observable<T> {
|
2020-10-07 08:52:51 +00:00
|
|
|
preSets = preSets || {};
|
2022-06-21 08:58:10 +00:00
|
|
|
preSets.getFromCache = preSets.getFromCache ?? false;
|
|
|
|
preSets.saveToCache = preSets.saveToCache ?? false;
|
|
|
|
preSets.emergencyCache = preSets.emergencyCache ?? false;
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-21 08:58:10 +00:00
|
|
|
return this.requestObservable<T>(method, data, preSets);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* WS request to the site.
|
|
|
|
*
|
|
|
|
* @param method The WebService method to be called.
|
|
|
|
* @param data Arguments to pass to the method.
|
|
|
|
* @param preSets Extra options.
|
|
|
|
* @return Promise resolved with the response, rejected with CoreWSError if it fails.
|
2022-06-21 08:58:10 +00:00
|
|
|
*/
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
async request<T = unknown>(method: string, data: any, preSets: CoreSiteWSPreSets): Promise<T> {
|
2022-06-23 08:42:20 +00:00
|
|
|
return firstValueFrom(this.requestObservable<T>(method, data, preSets));
|
2022-06-21 08:58:10 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* WS request to the site.
|
|
|
|
*
|
|
|
|
* @param method The WebService method to be called.
|
|
|
|
* @param data Arguments to pass to the method.
|
|
|
|
* @param preSets Extra options.
|
|
|
|
* @return Observable
|
2020-10-07 08:52:51 +00:00
|
|
|
* @description
|
|
|
|
*
|
|
|
|
* Sends a webservice request to the site. This method will automatically add the
|
|
|
|
* required parameters and pass it on to the low level API in CoreWSProvider.call().
|
|
|
|
*
|
|
|
|
* Caching is also implemented, when enabled this method will returned a cached version of the request if the
|
|
|
|
* data hasn't expired.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2022-06-21 08:58:10 +00:00
|
|
|
requestObservable<T = unknown>(method: string, data: any, preSets: CoreSiteWSPreSets): Observable<T> {
|
2021-11-17 09:36:57 +00:00
|
|
|
if (this.isLoggedOut() && !ALLOWED_LOGGEDOUT_WS.includes(method)) {
|
2021-09-03 07:26:02 +00:00
|
|
|
// Site is logged out, it cannot call WebServices.
|
|
|
|
CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {}, this.id);
|
|
|
|
|
2022-02-24 15:10:51 +00:00
|
|
|
// Use a silent error, the SESSION_EXPIRED event will display a message if needed.
|
|
|
|
throw new CoreSilentError(Translate.instant('core.lostconnection'));
|
2021-09-03 07:26:02 +00:00
|
|
|
}
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
data = data || {};
|
|
|
|
|
2022-05-11 12:06:42 +00:00
|
|
|
if (!CoreNetwork.isOnline() && this.offlineDisabled) {
|
2021-03-02 10:41:04 +00:00
|
|
|
throw new CoreError(Translate.instant('core.errorofflinedisabled'));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2021-09-03 11:33:03 +00:00
|
|
|
// Check if the method is available.
|
2020-10-07 08:52:51 +00:00
|
|
|
// We ignore this check when we do not have the site info, as the list of functions is not loaded yet.
|
2021-09-03 11:33:03 +00:00
|
|
|
if (this.getInfo() && !this.wsAvailable(method)) {
|
|
|
|
this.logger.error(`WS function '${method}' is not available.`);
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2021-09-03 11:33:03 +00:00
|
|
|
throw new CoreError(Translate.instant('core.wsfunctionnotavailable'));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const wsPreSets: CoreWSPreSets = {
|
2020-10-21 14:32:27 +00:00
|
|
|
wsToken: this.token || '',
|
2020-10-07 08:52:51 +00:00
|
|
|
siteUrl: this.siteUrl,
|
|
|
|
cleanUnicode: this.cleanUnicode,
|
|
|
|
typeExpected: preSets.typeExpected,
|
2020-10-14 06:28:02 +00:00
|
|
|
responseExpected: preSets.responseExpected,
|
2021-01-19 09:00:40 +00:00
|
|
|
splitRequest: preSets.splitRequest,
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
if (wsPreSets.cleanUnicode && CoreTextUtils.hasUnicodeData(data)) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Data will be cleaned, notify the user.
|
2022-05-30 13:18:01 +00:00
|
|
|
CoreDomUtils.showToast('core.unicodenotsupported', true, ToastDuration.LONG);
|
2020-10-07 08:52:51 +00:00
|
|
|
} else {
|
|
|
|
// No need to clean data in this call.
|
|
|
|
wsPreSets.cleanUnicode = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (this.offlineDisabled) {
|
|
|
|
// Offline is disabled, don't use cache.
|
|
|
|
preSets.getFromCache = false;
|
|
|
|
preSets.saveToCache = false;
|
|
|
|
preSets.emergencyCache = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Enable text filtering by default.
|
|
|
|
data.moodlewssettingfilter = preSets.filter === false ? false : true;
|
|
|
|
data.moodlewssettingfileurl = preSets.rewriteurls === false ? false : true;
|
|
|
|
|
|
|
|
// Convert arguments to strings before starting the cache process.
|
2021-03-02 10:41:04 +00:00
|
|
|
data = CoreWS.convertValuesToString(data, wsPreSets.cleanUnicode);
|
2020-10-07 08:52:51 +00:00
|
|
|
if (data == null) {
|
|
|
|
// Empty cleaned text found.
|
2021-03-02 10:41:04 +00:00
|
|
|
throw new CoreError(Translate.instant('core.unicodenotsupportedcleanerror'));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const cacheId = this.getCacheId(method, data);
|
|
|
|
|
|
|
|
// Check for an ongoing identical request if we're not ignoring cache.
|
2022-06-13 06:27:59 +00:00
|
|
|
if (preSets.getFromCache && this.ongoingRequests[cacheId] !== undefined) {
|
2022-06-21 08:58:10 +00:00
|
|
|
return this.ongoingRequests[cacheId];
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2022-06-21 08:58:10 +00:00
|
|
|
const observable = this.performRequest<T>(method, data, preSets, wsPreSets).pipe(
|
|
|
|
// Return a clone of the original object, this may prevent errors if in the callback the object is modified.
|
|
|
|
map((data) => CoreUtils.clone(data)),
|
|
|
|
);
|
2022-06-16 13:08:17 +00:00
|
|
|
|
2022-06-21 08:58:10 +00:00
|
|
|
this.ongoingRequests[cacheId] = observable;
|
2022-06-16 13:08:17 +00:00
|
|
|
|
2022-06-21 08:58:10 +00:00
|
|
|
return observable.pipe(
|
|
|
|
finalize(() => {
|
|
|
|
// Clear the ongoing request unless it has changed (e.g. a new request that ignores cache).
|
|
|
|
if (this.ongoingRequests[cacheId] === observable) {
|
|
|
|
delete this.ongoingRequests[cacheId];
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
);
|
2022-06-16 13:08:17 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
/**
|
|
|
|
* Perform a request, getting the response either from cache or WebService.
|
|
|
|
*
|
|
|
|
* @param method The WebService method to be called.
|
|
|
|
* @param data Arguments to pass to the method.
|
|
|
|
* @param preSets Extra options related to the site.
|
|
|
|
* @param wsPreSets Extra options related to the WS call.
|
2022-06-21 08:58:10 +00:00
|
|
|
* @return Observable.
|
2022-06-16 13:08:17 +00:00
|
|
|
*/
|
2022-06-21 08:58:10 +00:00
|
|
|
protected performRequest<T = unknown>(
|
2022-06-16 13:08:17 +00:00
|
|
|
method: string,
|
|
|
|
data: unknown,
|
|
|
|
preSets: CoreSiteWSPreSets,
|
|
|
|
wsPreSets: CoreWSPreSets,
|
2022-06-21 08:58:10 +00:00
|
|
|
): Observable<T> {
|
|
|
|
const subject = new Subject<T>();
|
2021-10-14 11:13:27 +00:00
|
|
|
|
2022-06-21 08:58:10 +00:00
|
|
|
const run = async () => {
|
|
|
|
try {
|
2022-06-21 12:50:08 +00:00
|
|
|
let response: T | WSCachedError;
|
|
|
|
let cachedData: WSCachedData<T> | undefined;
|
2021-05-06 06:57:10 +00:00
|
|
|
|
2022-06-21 08:58:10 +00:00
|
|
|
try {
|
2022-06-21 12:50:08 +00:00
|
|
|
cachedData = await this.getFromCache<T>(method, data, preSets, false);
|
|
|
|
response = cachedData.response;
|
2022-06-21 08:58:10 +00:00
|
|
|
} catch {
|
|
|
|
// Not found or expired, call WS.
|
2022-06-21 12:50:08 +00:00
|
|
|
response = await this.getFromWS<T>(method, data, preSets, wsPreSets);
|
2022-06-21 08:58:10 +00:00
|
|
|
}
|
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
if (
|
|
|
|
typeof response === 'object' && response !== null &&
|
|
|
|
(
|
|
|
|
('exception' in response && response.exception !== undefined) ||
|
|
|
|
('errorcode' in response && response.errorcode !== undefined)
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
subject.error(new CoreWSError(response));
|
|
|
|
} else {
|
|
|
|
subject.next(<T> response);
|
2022-06-21 08:58:10 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
if (
|
|
|
|
preSets.updateInBackground &&
|
|
|
|
!CoreConstants.CONFIG.disableCallWSInBackground &&
|
|
|
|
cachedData &&
|
|
|
|
!cachedData.expirationIgnored &&
|
|
|
|
cachedData.expirationTime !== undefined &&
|
|
|
|
Date.now() > cachedData.expirationTime
|
|
|
|
) {
|
|
|
|
// Update the data in background.
|
|
|
|
setTimeout(async () => {
|
|
|
|
try {
|
|
|
|
preSets = {
|
|
|
|
...preSets,
|
|
|
|
emergencyCache: false,
|
|
|
|
};
|
|
|
|
|
|
|
|
const newData = await this.getFromWS<T>(method, data, preSets, wsPreSets);
|
|
|
|
|
|
|
|
subject.next(newData);
|
|
|
|
} catch (error) {
|
|
|
|
// Ignore errors when updating in background.
|
|
|
|
this.logger.error('Error updating WS data in background', error);
|
|
|
|
} finally {
|
|
|
|
subject.complete();
|
|
|
|
}
|
|
|
|
});
|
|
|
|
} else {
|
|
|
|
// No need to update in background, complete the observable.
|
|
|
|
subject.complete();
|
|
|
|
}
|
2022-06-21 08:58:10 +00:00
|
|
|
} catch (error) {
|
|
|
|
subject.error(error);
|
|
|
|
}
|
|
|
|
};
|
|
|
|
|
|
|
|
run();
|
|
|
|
|
|
|
|
return subject;
|
2022-06-16 13:08:17 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
/**
|
|
|
|
* Get a request response from WS, if it fails it might try to get it from emergency cache.
|
|
|
|
*
|
|
|
|
* @param method The WebService method to be called.
|
|
|
|
* @param data Arguments to pass to the method.
|
|
|
|
* @param preSets Extra options related to the site.
|
|
|
|
* @param wsPreSets Extra options related to the WS call.
|
|
|
|
* @return Promise resolved with the response.
|
|
|
|
*/
|
2022-06-21 12:50:08 +00:00
|
|
|
protected async getFromWS<T = unknown>(
|
2022-06-16 13:08:17 +00:00
|
|
|
method: string,
|
|
|
|
data: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
|
|
preSets: CoreSiteWSPreSets,
|
|
|
|
wsPreSets: CoreWSPreSets,
|
|
|
|
): Promise<T> {
|
|
|
|
if (preSets.forceOffline) {
|
|
|
|
// Don't call the WS, just fail.
|
|
|
|
throw new CoreError(
|
|
|
|
Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
try {
|
2022-06-21 12:50:08 +00:00
|
|
|
const response = await this.callOrEnqueueWS<T>(method, data, preSets, wsPreSets);
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
if (preSets.saveToCache) {
|
|
|
|
this.saveToCache(method, data, response, preSets);
|
|
|
|
}
|
|
|
|
|
|
|
|
return response;
|
|
|
|
} catch (error) {
|
|
|
|
let useSilentError = false;
|
|
|
|
|
|
|
|
if (CoreUtils.isExpiredTokenError(error)) {
|
|
|
|
// Session expired, trigger event.
|
|
|
|
CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {}, this.id);
|
|
|
|
// Change error message. Try to get data from cache, the event will handle the error.
|
|
|
|
error.message = Translate.instant('core.lostconnection');
|
|
|
|
useSilentError = true; // Use a silent error, the SESSION_EXPIRED event will display a message if needed.
|
|
|
|
} else if (error.errorcode === 'userdeleted' || error.errorcode === 'wsaccessuserdeleted') {
|
|
|
|
// User deleted, trigger event.
|
|
|
|
CoreEvents.trigger(CoreEvents.USER_DELETED, { params: data }, this.id);
|
|
|
|
error.message = Translate.instant('core.userdeleted');
|
|
|
|
|
|
|
|
throw new CoreWSError(error);
|
|
|
|
} else if (error.errorcode === 'wsaccessusersuspended') {
|
|
|
|
// User suspended, trigger event.
|
|
|
|
CoreEvents.trigger(CoreEvents.USER_SUSPENDED, { params: data }, this.id);
|
|
|
|
error.message = Translate.instant('core.usersuspended');
|
|
|
|
|
|
|
|
throw new CoreWSError(error);
|
|
|
|
} else if (error.errorcode === 'wsaccessusernologin') {
|
|
|
|
// User suspended, trigger event.
|
|
|
|
CoreEvents.trigger(CoreEvents.USER_NO_LOGIN, { params: data }, this.id);
|
|
|
|
error.message = Translate.instant('core.usernologin');
|
|
|
|
|
|
|
|
throw new CoreWSError(error);
|
|
|
|
} else if (error.errorcode === 'forcepasswordchangenotice') {
|
|
|
|
// Password Change Forced, trigger event. Try to get data from cache, the event will handle the error.
|
|
|
|
CoreEvents.trigger(CoreEvents.PASSWORD_CHANGE_FORCED, {}, this.id);
|
|
|
|
error.message = Translate.instant('core.forcepasswordchangenotice');
|
|
|
|
useSilentError = true; // Use a silent error, the change password page already displays the appropiate info.
|
|
|
|
} else if (error.errorcode === 'usernotfullysetup') {
|
|
|
|
// User not fully setup, trigger event. Try to get data from cache, the event will handle the error.
|
|
|
|
CoreEvents.trigger(CoreEvents.USER_NOT_FULLY_SETUP, {}, this.id);
|
|
|
|
error.message = Translate.instant('core.usernotfullysetup');
|
|
|
|
useSilentError = true; // Use a silent error, the complete profile page already displays the appropiate info.
|
|
|
|
} else if (error.errorcode === 'sitepolicynotagreed') {
|
|
|
|
// Site policy not agreed, trigger event.
|
|
|
|
CoreEvents.trigger(CoreEvents.SITE_POLICY_NOT_AGREED, {}, this.id);
|
|
|
|
error.message = Translate.instant('core.login.sitepolicynotagreederror');
|
|
|
|
|
|
|
|
throw new CoreWSError(error);
|
|
|
|
} else if (error.errorcode === 'dmlwriteexception' && CoreTextUtils.hasUnicodeData(data)) {
|
|
|
|
if (!this.cleanUnicode) {
|
|
|
|
// Try again cleaning unicode.
|
|
|
|
this.cleanUnicode = true;
|
|
|
|
|
|
|
|
return this.request<T>(method, data, preSets);
|
|
|
|
}
|
|
|
|
// This should not happen.
|
|
|
|
error.message = Translate.instant('core.unicodenotsupported');
|
|
|
|
|
|
|
|
throw new CoreWSError(error);
|
|
|
|
} else if (error.exception === 'required_capability_exception' || error.errorcode === 'nopermission' ||
|
|
|
|
error.errorcode === 'notingroup') {
|
|
|
|
// Translate error messages with missing strings.
|
|
|
|
if (error.message === 'error/nopermission') {
|
|
|
|
error.message = Translate.instant('core.nopermissionerror');
|
|
|
|
} else if (error.message === 'error/notingroup') {
|
|
|
|
error.message = Translate.instant('core.notingroup');
|
|
|
|
}
|
|
|
|
|
|
|
|
if (preSets.saveToCache) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Save the error instead of deleting the cache entry so the same content is displayed in offline.
|
|
|
|
this.saveToCache(method, data, error, preSets);
|
2022-06-16 13:08:17 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
throw new CoreWSError(error);
|
|
|
|
} else if (preSets.cacheErrors && preSets.cacheErrors.indexOf(error.errorcode) != -1) {
|
|
|
|
// Save the error instead of deleting the cache entry so the same content is displayed in offline.
|
|
|
|
this.saveToCache(method, data, error, preSets);
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
throw new CoreWSError(error);
|
|
|
|
} else if (preSets.emergencyCache !== undefined && !preSets.emergencyCache) {
|
|
|
|
this.logger.debug(`WS call '${method}' failed. Emergency cache is forbidden, rejecting.`);
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
throw new CoreWSError(error);
|
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
if (preSets.deleteCacheIfWSError && CoreUtils.isWebServiceError(error)) {
|
|
|
|
// Delete the cache entry and return the entry. Don't block the user with the delete.
|
|
|
|
CoreUtils.ignoreErrors(this.deleteFromCache(method, data, preSets));
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
throw new CoreWSError(error);
|
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
this.logger.debug(`WS call '${method}' failed. Trying to use the emergency cache.`);
|
2022-06-21 12:50:08 +00:00
|
|
|
preSets = {
|
|
|
|
...preSets,
|
|
|
|
omitExpires: true,
|
|
|
|
getFromCache: true,
|
|
|
|
};
|
2022-02-24 15:10:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
try {
|
2022-06-21 12:50:08 +00:00
|
|
|
const cachedData = await this.getFromCache<T>(method, data, preSets, true);
|
|
|
|
|
|
|
|
if (
|
|
|
|
typeof cachedData.response === 'object' && cachedData.response !== null &&
|
|
|
|
(
|
|
|
|
('exception' in cachedData.response && cachedData.response.exception !== undefined) ||
|
|
|
|
('errorcode' in cachedData.response && cachedData.response.errorcode !== undefined)
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
throw new CoreWSError(cachedData.response);
|
|
|
|
}
|
|
|
|
|
|
|
|
return <T> cachedData.response;
|
2022-06-16 13:08:17 +00:00
|
|
|
} catch {
|
|
|
|
if (useSilentError) {
|
|
|
|
throw new CoreSilentError(error.message);
|
2021-05-06 06:57:10 +00:00
|
|
|
}
|
2022-06-16 13:08:17 +00:00
|
|
|
|
|
|
|
throw new CoreWSError(error);
|
2021-05-06 06:57:10 +00:00
|
|
|
}
|
2022-06-16 13:08:17 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
/**
|
|
|
|
* Get a request response from WS.
|
|
|
|
*
|
|
|
|
* @param method The WebService method to be called.
|
|
|
|
* @param data Arguments to pass to the method.
|
|
|
|
* @param preSets Extra options related to the site.
|
|
|
|
* @param wsPreSets Extra options related to the WS call.
|
|
|
|
* @return Promise resolved with the response.
|
|
|
|
*/
|
2022-06-21 12:50:08 +00:00
|
|
|
protected async callOrEnqueueWS<T = unknown>(
|
2022-06-16 13:08:17 +00:00
|
|
|
method: string,
|
|
|
|
data: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
|
|
preSets: CoreSiteWSPreSets,
|
|
|
|
wsPreSets: CoreWSPreSets,
|
|
|
|
): Promise<T> {
|
|
|
|
// Call the WS.
|
|
|
|
const initialToken = this.token ?? '';
|
|
|
|
|
|
|
|
// Call the WS.
|
|
|
|
if (method !== 'core_webservice_get_site_info') {
|
|
|
|
// Send the language to use. Do it after checking cache to prevent losing offline data when changing language.
|
|
|
|
// Don't send it to core_webservice_get_site_info, that WS is used to check if Moodle version is supported.
|
|
|
|
data = {
|
|
|
|
...data,
|
|
|
|
moodlewssettinglang: preSets.lang ?? await CoreLang.getCurrentLanguage(),
|
|
|
|
};
|
|
|
|
// Moodle uses underscore instead of dash.
|
|
|
|
data.moodlewssettinglang = data.moodlewssettinglang.replace('-', '_');
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-16 13:08:17 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
try {
|
2022-06-16 13:08:17 +00:00
|
|
|
return await this.callOrEnqueueRequest<T>(method, data, preSets, wsPreSets);
|
|
|
|
} catch (error) {
|
|
|
|
if (CoreUtils.isExpiredTokenError(error)) {
|
|
|
|
if (initialToken !== this.token) {
|
|
|
|
// Token has changed, retry with the new token.
|
|
|
|
wsPreSets.wsToken = this.token ?? '';
|
|
|
|
|
|
|
|
return await this.callOrEnqueueRequest<T>(method, data, preSets, wsPreSets);
|
|
|
|
} else if (CoreApp.isSSOAuthenticationOngoing()) {
|
|
|
|
// There's an SSO authentication ongoing, wait for it to finish and try again.
|
|
|
|
await CoreApp.waitForSSOAuthentication();
|
|
|
|
|
|
|
|
return await this.callOrEnqueueRequest<T>(method, data, preSets, wsPreSets);
|
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
2022-06-16 13:08:17 +00:00
|
|
|
|
|
|
|
throw error;
|
2020-10-21 14:32:27 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a request to the queue or calls it immediately when not using the queue.
|
|
|
|
*
|
|
|
|
* @param method The WebService method to be called.
|
|
|
|
* @param data Arguments to pass to the method.
|
|
|
|
* @param preSets Extra options related to the site.
|
|
|
|
* @param wsPreSets Extra options related to the WS call.
|
|
|
|
* @return Promise resolved with the response when the WS is called.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
protected callOrEnqueueRequest<T = unknown>(
|
|
|
|
method: string,
|
2020-10-21 14:32:27 +00:00
|
|
|
data: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
2020-10-14 06:28:02 +00:00
|
|
|
preSets: CoreSiteWSPreSets,
|
|
|
|
wsPreSets: CoreWSPreSets,
|
|
|
|
): Promise<T> {
|
2020-10-07 08:52:51 +00:00
|
|
|
if (preSets.skipQueue || !this.wsAvailable('tool_mobile_call_external_functions')) {
|
2021-03-02 10:41:04 +00:00
|
|
|
return CoreWS.call<T>(method, data, wsPreSets);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const cacheId = this.getCacheId(method, data);
|
|
|
|
|
|
|
|
// Check if there is an identical request waiting in the queue (read requests only by default).
|
|
|
|
if (preSets.reusePending) {
|
|
|
|
const request = this.requestQueue.find((request) => request.cacheId == cacheId);
|
|
|
|
if (request) {
|
2022-05-30 16:13:43 +00:00
|
|
|
return request.deferred;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
const request: RequestQueueItem<T> = {
|
2020-10-07 08:52:51 +00:00
|
|
|
cacheId,
|
|
|
|
method,
|
|
|
|
data,
|
|
|
|
preSets,
|
|
|
|
wsPreSets,
|
2022-05-30 16:13:43 +00:00
|
|
|
deferred: new CorePromisedValue(),
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return this.enqueueRequest(request);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Adds a request to the queue.
|
|
|
|
*
|
|
|
|
* @param request The request to enqueue.
|
|
|
|
* @return Promise resolved with the response when the WS is called.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
protected enqueueRequest<T>(request: RequestQueueItem<T>): Promise<T> {
|
2020-10-07 08:52:51 +00:00
|
|
|
this.requestQueue.push(request);
|
|
|
|
|
2021-12-03 13:18:22 +00:00
|
|
|
if (this.requestQueue.length >= CoreConstants.CONFIG.wsrequestqueuelimit) {
|
2020-10-07 08:52:51 +00:00
|
|
|
this.processRequestQueue();
|
|
|
|
} else if (!this.requestQueueTimeout) {
|
2021-12-03 13:18:22 +00:00
|
|
|
this.requestQueueTimeout = window.setTimeout(
|
|
|
|
this.processRequestQueue.bind(this),
|
|
|
|
CoreConstants.CONFIG.wsrequestqueuedelay,
|
|
|
|
);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2022-05-30 16:13:43 +00:00
|
|
|
return request.deferred;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Call the enqueued web service requests.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
protected async processRequestQueue(): Promise<void> {
|
2020-10-07 08:52:51 +00:00
|
|
|
this.logger.debug(`Processing request queue (${this.requestQueue.length} requests)`);
|
|
|
|
|
|
|
|
// Clear timeout if set.
|
|
|
|
if (this.requestQueueTimeout) {
|
|
|
|
clearTimeout(this.requestQueueTimeout);
|
|
|
|
this.requestQueueTimeout = null;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Extract all requests from the queue.
|
|
|
|
const requests = this.requestQueue;
|
|
|
|
this.requestQueue = [];
|
|
|
|
|
|
|
|
if (requests.length == 1 && !CoreSite.REQUEST_QUEUE_FORCE_WS) {
|
|
|
|
// Only one request, do a regular web service call.
|
2020-10-21 14:32:27 +00:00
|
|
|
try {
|
2021-03-02 10:41:04 +00:00
|
|
|
const data = await CoreWS.call(requests[0].method, requests[0].data, requests[0].wsPreSets);
|
2020-10-21 14:32:27 +00:00
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
requests[0].deferred.resolve(data);
|
2020-10-21 14:32:27 +00:00
|
|
|
} catch (error) {
|
2020-10-07 08:52:51 +00:00
|
|
|
requests[0].deferred.reject(error);
|
2020-10-21 14:32:27 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2021-10-14 11:13:27 +00:00
|
|
|
let lang: string | undefined;
|
|
|
|
const requestsData: Record<string, unknown> = {
|
2020-10-07 08:52:51 +00:00
|
|
|
requests: requests.map((request) => {
|
|
|
|
const args = {};
|
|
|
|
const settings = {};
|
|
|
|
|
|
|
|
// Separate WS settings from function arguments.
|
|
|
|
Object.keys(request.data).forEach((key) => {
|
|
|
|
let value = request.data[key];
|
|
|
|
const match = /^moodlews(setting.*)$/.exec(key);
|
|
|
|
if (match) {
|
|
|
|
if (match[1] == 'settingfilter' || match[1] == 'settingfileurl') {
|
|
|
|
// Undo special treatment of these settings in CoreWSProvider.convertValuesToString.
|
|
|
|
value = (value == 'true' ? '1' : '0');
|
2021-10-14 11:13:27 +00:00
|
|
|
} else if (match[1] == 'settinglang') {
|
|
|
|
// Use the lang globally to avoid exceptions with languages not installed.
|
|
|
|
lang = value;
|
|
|
|
|
|
|
|
return;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
settings[match[1]] = value;
|
|
|
|
} else {
|
|
|
|
args[key] = value;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return {
|
|
|
|
function: request.method,
|
|
|
|
arguments: JSON.stringify(args),
|
2020-10-14 06:28:02 +00:00
|
|
|
...settings,
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
2020-10-14 06:28:02 +00:00
|
|
|
}),
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
2021-10-14 11:13:27 +00:00
|
|
|
requestsData.moodlewssettinglang = lang;
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
const wsPresets: CoreWSPreSets = {
|
|
|
|
siteUrl: this.siteUrl,
|
2020-10-21 14:32:27 +00:00
|
|
|
wsToken: this.token || '',
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
try {
|
2021-03-02 10:41:04 +00:00
|
|
|
const data = await CoreWS.call<CoreSiteCallExternalFunctionsResult>(
|
2020-10-21 14:32:27 +00:00
|
|
|
'tool_mobile_call_external_functions',
|
|
|
|
requestsData,
|
|
|
|
wsPresets,
|
|
|
|
);
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
if (!data || !data.responses) {
|
2021-03-02 10:41:04 +00:00
|
|
|
throw new CoreError(Translate.instant('core.errorinvalidresponse'));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
requests.forEach((request, i) => {
|
|
|
|
const response = data.responses[i];
|
|
|
|
|
|
|
|
if (!response) {
|
|
|
|
// Request not executed, enqueue again.
|
|
|
|
this.enqueueRequest(request);
|
|
|
|
} else if (response.error) {
|
2021-03-02 10:41:04 +00:00
|
|
|
request.deferred.reject(CoreTextUtils.parseJSON(response.exception || ''));
|
2020-10-07 08:52:51 +00:00
|
|
|
} else {
|
2021-03-02 10:41:04 +00:00
|
|
|
let responseData = response.data ? CoreTextUtils.parseJSON(response.data) : {};
|
2020-10-07 08:52:51 +00:00
|
|
|
// Match the behaviour of CoreWSProvider.call when no response is expected.
|
2021-12-16 09:46:40 +00:00
|
|
|
const responseExpected = wsPresets.responseExpected === undefined || wsPresets.responseExpected;
|
2020-10-07 08:52:51 +00:00
|
|
|
if (!responseExpected && (responseData == null || responseData === '')) {
|
|
|
|
responseData = {};
|
|
|
|
}
|
|
|
|
request.deferred.resolve(responseData);
|
|
|
|
}
|
|
|
|
});
|
2020-10-21 14:32:27 +00:00
|
|
|
} catch (error) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Error not specific to a single request, reject all promises.
|
|
|
|
requests.forEach((request) => {
|
|
|
|
request.deferred.reject(error);
|
|
|
|
});
|
2020-10-21 14:32:27 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a WS is available in this site.
|
|
|
|
*
|
|
|
|
* @param method WS name.
|
|
|
|
* @return Whether the WS is available.
|
|
|
|
*/
|
2021-09-03 11:33:03 +00:00
|
|
|
wsAvailable(method: string): boolean {
|
|
|
|
return !!this.infos?.functionsByName?.[method];
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get cache ID.
|
|
|
|
*
|
|
|
|
* @param method The WebService method.
|
|
|
|
* @param data Arguments to pass to the method.
|
|
|
|
* @return Cache ID.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2020-10-07 08:52:51 +00:00
|
|
|
protected getCacheId(method: string, data: any): string {
|
2021-03-02 10:41:04 +00:00
|
|
|
return <string> Md5.hashAsciiStr(method + ':' + CoreUtils.sortAndStringify(data));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a WS response from cache.
|
|
|
|
*
|
|
|
|
* @param method The WebService method to be called.
|
|
|
|
* @param data Arguments to pass to the method.
|
|
|
|
* @param preSets Extra options.
|
|
|
|
* @param emergency Whether it's an "emergency" cache call (WS call failed).
|
|
|
|
* @param originalData Arguments to pass to the method before being converted to strings.
|
2022-06-21 12:50:08 +00:00
|
|
|
* @return Cached data.
|
2020-10-07 08:52:51 +00:00
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
protected async getFromCache<T = unknown>(
|
|
|
|
method: string,
|
|
|
|
data: any, // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
|
|
preSets: CoreSiteWSPreSets,
|
|
|
|
emergency?: boolean,
|
2022-06-21 12:50:08 +00:00
|
|
|
): Promise<WSCachedData<T>> {
|
2022-02-03 12:43:10 +00:00
|
|
|
if (!this.db || !preSets.getFromCache) {
|
2020-10-21 14:32:27 +00:00
|
|
|
throw new CoreError('Get from cache is disabled.');
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const id = this.getCacheId(method, data);
|
2022-06-13 06:27:59 +00:00
|
|
|
const group = this.getWSGroupFromWSName(method);
|
2020-10-21 14:32:27 +00:00
|
|
|
let entry: CoreSiteWSCacheRecord | undefined;
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) {
|
2022-06-13 06:27:59 +00:00
|
|
|
const entries = await this.cacheTables[group].getMany({ key: preSets.cacheKey });
|
2020-10-21 14:32:27 +00:00
|
|
|
|
|
|
|
if (!entries.length) {
|
|
|
|
// Cache key not found, get by params sent.
|
2022-06-13 06:27:59 +00:00
|
|
|
entry = await this.cacheTables[group].getOneByPrimaryKey({ id });
|
2020-10-21 14:32:27 +00:00
|
|
|
} else {
|
|
|
|
if (entries.length > 1) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// More than one entry found. Search the one with same ID as this call.
|
2020-10-21 14:32:27 +00:00
|
|
|
entry = entries.find((entry) => entry.id == id);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
if (!entry) {
|
|
|
|
entry = entries[0];
|
|
|
|
}
|
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
} else {
|
2022-06-13 06:27:59 +00:00
|
|
|
entry = await this.cacheTables[group].getOneByPrimaryKey({ id });
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2021-12-16 09:46:40 +00:00
|
|
|
if (entry === undefined) {
|
2020-10-21 14:32:27 +00:00
|
|
|
throw new CoreError('Cache entry not valid.');
|
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
const now = Date.now();
|
|
|
|
let expirationTime: number | undefined;
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
const forceCache = preSets.omitExpires || preSets.forceOffline || !CoreNetwork.isOnline();
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
if (!forceCache) {
|
2020-10-21 14:32:27 +00:00
|
|
|
expirationTime = entry.expirationTime + this.getExpirationDelay(preSets.updateFrequency);
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
if (preSets.updateInBackground && !CoreConstants.CONFIG.disableCallWSInBackground) {
|
|
|
|
// Use a extended expiration time.
|
|
|
|
const extendedTime = entry.expirationTime +
|
|
|
|
(CoreConstants.CONFIG.callWSInBackgroundExpirationTime ?? CoreConstants.SECONDS_WEEK * 1000);
|
|
|
|
|
|
|
|
if (now > extendedTime) {
|
|
|
|
this.logger.debug('Cached element found, but it is expired even for call WS in background.');
|
|
|
|
|
|
|
|
throw new CoreError('Cache entry is expired.');
|
|
|
|
}
|
|
|
|
} else if (now > expirationTime) {
|
2020-10-21 14:32:27 +00:00
|
|
|
this.logger.debug('Cached element found, but it is expired');
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
throw new CoreError('Cache entry is expired.');
|
|
|
|
}
|
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2021-12-16 09:46:40 +00:00
|
|
|
if (entry.data !== undefined) {
|
2020-10-21 14:32:27 +00:00
|
|
|
if (!expirationTime) {
|
|
|
|
this.logger.info(`Cached element found, id: ${id}. Expiration time ignored.`);
|
|
|
|
} else {
|
|
|
|
const expires = (expirationTime - now) / 1000;
|
|
|
|
this.logger.info(`Cached element found, id: ${id}. Expires in expires in ${expires} seconds`);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
return {
|
|
|
|
response: <T> CoreTextUtils.parseJSON(entry.data, {}),
|
|
|
|
expirationIgnored: forceCache,
|
|
|
|
expirationTime,
|
|
|
|
};
|
2020-10-21 14:32:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
throw new CoreError('Cache entry not valid.');
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2022-06-13 06:27:59 +00:00
|
|
|
/**
|
|
|
|
* Get WS group based on a WS name.
|
|
|
|
*
|
|
|
|
* @return WS group.
|
|
|
|
*/
|
|
|
|
protected getWSGroupFromWSName(name: string): WSGroups {
|
|
|
|
if (name.startsWith('mod_')) {
|
|
|
|
return WSGroups.MOD;
|
|
|
|
} else if (name.startsWith('tool_')) {
|
|
|
|
return WSGroups.TOOL;
|
|
|
|
} else if (name.startsWith('block_') || name.startsWith('core_block_')) {
|
|
|
|
return WSGroups.BLOCK;
|
|
|
|
} else if (name.startsWith('core_')) {
|
|
|
|
return WSGroups.CORE;
|
|
|
|
} else {
|
|
|
|
return WSGroups.OTHER;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
/**
|
|
|
|
* Gets the size of cached data for a specific component or component instance.
|
|
|
|
*
|
|
|
|
* @param component Component name
|
|
|
|
* @param componentId Optional component id (if not included, returns sum for whole component)
|
|
|
|
* @return Promise resolved when we have calculated the size
|
|
|
|
*/
|
2020-10-14 14:38:24 +00:00
|
|
|
async getComponentCacheSize(component: string, componentId?: number): Promise<number> {
|
2020-10-14 06:28:02 +00:00
|
|
|
const params: Array<string | number> = [component];
|
2020-10-07 08:52:51 +00:00
|
|
|
let extraClause = '';
|
|
|
|
if (componentId !== undefined && componentId !== null) {
|
|
|
|
params.push(componentId);
|
|
|
|
extraClause = ' AND componentId = ?';
|
|
|
|
}
|
|
|
|
|
2022-06-13 06:27:59 +00:00
|
|
|
const sizes = await Promise.all(Object.values(this.cacheTables).map(table => table.reduce(
|
2022-02-03 12:43:10 +00:00
|
|
|
{
|
|
|
|
sql: 'SUM(length(data))',
|
|
|
|
js: (size, record) => size + record.data.length,
|
|
|
|
jsInitialValue: 0,
|
|
|
|
},
|
|
|
|
{
|
|
|
|
sql: 'WHERE component = ?' + extraClause,
|
|
|
|
sqlParams: params,
|
|
|
|
js: record => record.component === component && (params.length === 1 || record.componentId === componentId),
|
|
|
|
},
|
2022-06-13 06:27:59 +00:00
|
|
|
)));
|
|
|
|
|
|
|
|
return sizes.reduce((totalSize, size) => totalSize + size, 0);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Save a WS response to cache.
|
|
|
|
*
|
|
|
|
* @param method The WebService method.
|
|
|
|
* @param data Arguments to pass to the method.
|
|
|
|
* @param response The WS response.
|
|
|
|
* @param preSets Extra options.
|
|
|
|
* @return Promise resolved when the response is saved.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2020-10-14 06:28:02 +00:00
|
|
|
protected async saveToCache(method: string, data: any, response: any, preSets: CoreSiteWSPreSets): Promise<void> {
|
2020-10-07 08:52:51 +00:00
|
|
|
if (preSets.uniqueCacheKey) {
|
|
|
|
// Cache key must be unique, delete all entries with same cache key.
|
2021-03-02 10:41:04 +00:00
|
|
|
await CoreUtils.ignoreErrors(this.deleteFromCache(method, data, preSets, true));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2020-10-14 06:28:02 +00:00
|
|
|
// Since 3.7, the expiration time contains the time the entry is modified instead of the expiration time.
|
|
|
|
// We decided to reuse this field to prevent modifying the database table.
|
|
|
|
const id = this.getCacheId(method, data);
|
2022-06-13 06:27:59 +00:00
|
|
|
const group = this.getWSGroupFromWSName(method);
|
2020-10-14 06:28:02 +00:00
|
|
|
const entry = {
|
|
|
|
id,
|
|
|
|
data: JSON.stringify(response),
|
|
|
|
expirationTime: Date.now(),
|
|
|
|
};
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2020-10-14 06:28:02 +00:00
|
|
|
if (preSets.cacheKey) {
|
|
|
|
entry['key'] = preSets.cacheKey;
|
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2020-10-14 06:28:02 +00:00
|
|
|
if (preSets.component) {
|
|
|
|
entry['component'] = preSets.component;
|
|
|
|
if (preSets.componentId) {
|
|
|
|
entry['componentId'] = preSets.componentId;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
2020-10-14 06:28:02 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2022-06-13 06:27:59 +00:00
|
|
|
await this.cacheTables[group].insert(entry);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Delete a WS cache entry or entries.
|
|
|
|
*
|
|
|
|
* @param method The WebService method to be called.
|
|
|
|
* @param data Arguments to pass to the method.
|
|
|
|
* @param preSets Extra options.
|
|
|
|
* @param allCacheKey True to delete all entries with the cache key, false to delete only by ID.
|
|
|
|
* @return Promise resolved when the entries are deleted.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
2020-10-14 06:28:02 +00:00
|
|
|
protected async deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean): Promise<void> {
|
2020-10-07 08:52:51 +00:00
|
|
|
const id = this.getCacheId(method, data);
|
2022-06-13 06:27:59 +00:00
|
|
|
const group = this.getWSGroupFromWSName(method);
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
if (allCacheKey) {
|
2022-06-13 06:27:59 +00:00
|
|
|
await this.cacheTables[group].delete({ key: preSets.cacheKey });
|
2020-10-14 06:28:02 +00:00
|
|
|
} else {
|
2022-06-13 06:27:59 +00:00
|
|
|
await this.cacheTables[group].deleteByPrimaryKey({ id });
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deletes WS cache entries for all methods relating to a specific component (and
|
|
|
|
* optionally component id).
|
|
|
|
*
|
|
|
|
* @param component Component name.
|
|
|
|
* @param componentId Component id.
|
|
|
|
* @return Promise resolved when the entries are deleted.
|
|
|
|
*/
|
|
|
|
async deleteComponentFromCache(component: string, componentId?: number): Promise<void> {
|
|
|
|
if (!component) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2022-02-03 12:43:10 +00:00
|
|
|
const params = { component };
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
if (componentId) {
|
2020-10-14 06:28:02 +00:00
|
|
|
params['componentId'] = componentId;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2022-06-13 06:27:59 +00:00
|
|
|
await Promise.all(Object.values(this.cacheTables).map(table => table.delete(params)));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Uploads a file using Cordova File API.
|
|
|
|
*
|
|
|
|
* @param filePath File path.
|
|
|
|
* @param options File upload options.
|
|
|
|
* @param onProgress Function to call on progress.
|
|
|
|
* @return Promise resolved when uploaded.
|
|
|
|
*/
|
2020-11-06 14:32:00 +00:00
|
|
|
uploadFile(
|
2020-10-14 06:28:02 +00:00
|
|
|
filePath: string,
|
|
|
|
options: CoreWSFileUploadOptions,
|
|
|
|
onProgress?: (event: ProgressEvent) => void,
|
2020-11-06 14:32:00 +00:00
|
|
|
): Promise<CoreWSUploadFileResult> {
|
2020-10-07 08:52:51 +00:00
|
|
|
if (!options.fileArea) {
|
|
|
|
options.fileArea = 'draft';
|
|
|
|
}
|
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
return CoreWS.uploadFile(filePath, options, {
|
2020-10-07 08:52:51 +00:00
|
|
|
siteUrl: this.siteUrl,
|
2020-10-21 14:32:27 +00:00
|
|
|
wsToken: this.token || '',
|
2020-10-07 08:52:51 +00:00
|
|
|
}, onProgress);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidates all the cache entries.
|
|
|
|
*
|
|
|
|
* @return Promise resolved when the cache entries are invalidated.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
async invalidateWsCache(): Promise<void> {
|
2020-10-07 08:52:51 +00:00
|
|
|
this.logger.debug('Invalidate all the cache for site: ' + this.id);
|
|
|
|
|
2020-10-14 06:28:02 +00:00
|
|
|
try {
|
2022-06-13 06:27:59 +00:00
|
|
|
await Promise.all(Object.values(this.cacheTables).map(table => table.update({ expirationTime: 0 })));
|
2020-10-14 06:28:02 +00:00
|
|
|
} finally {
|
2020-10-22 10:48:23 +00:00
|
|
|
CoreEvents.trigger(CoreEvents.WS_CACHE_INVALIDATED, {}, this.getId());
|
2020-10-14 06:28:02 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidates all the cache entries with a certain key.
|
|
|
|
*
|
|
|
|
* @param key Key to search.
|
|
|
|
* @return Promise resolved when the cache entries are invalidated.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
async invalidateWsCacheForKey(key: string): Promise<void> {
|
2020-10-07 08:52:51 +00:00
|
|
|
if (!key) {
|
2020-10-14 06:28:02 +00:00
|
|
|
return;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.logger.debug('Invalidate cache for key: ' + key);
|
|
|
|
|
2022-06-13 06:27:59 +00:00
|
|
|
await Promise.all(Object.values(this.cacheTables).map(table => table.update({ expirationTime: 0 }, { key })));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidates all the cache entries in an array of keys.
|
|
|
|
*
|
|
|
|
* @param keys Keys to search.
|
|
|
|
* @return Promise resolved when the cache entries are invalidated.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
async invalidateMultipleWsCacheForKey(keys: string[]): Promise<void> {
|
2020-10-07 08:52:51 +00:00
|
|
|
if (!this.db) {
|
2020-10-14 06:28:02 +00:00
|
|
|
throw new CoreError('Site DB not initialized');
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
if (!keys || !keys.length) {
|
2020-10-14 06:28:02 +00:00
|
|
|
return;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.logger.debug('Invalidating multiple cache keys');
|
2020-10-14 06:28:02 +00:00
|
|
|
await Promise.all(keys.map((key) => this.invalidateWsCacheForKey(key)));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidates all the cache entries whose key starts with a certain value.
|
|
|
|
*
|
|
|
|
* @param key Key to search.
|
|
|
|
* @return Promise resolved when the cache entries are invalidated.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
async invalidateWsCacheForKeyStartingWith(key: string): Promise<void> {
|
2020-10-07 08:52:51 +00:00
|
|
|
if (!key) {
|
2020-10-14 06:28:02 +00:00
|
|
|
return;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
this.logger.debug('Invalidate cache for key starting with: ' + key);
|
|
|
|
|
2022-06-13 06:27:59 +00:00
|
|
|
await Promise.all(Object.values(this.cacheTables).map(table => table.updateWhere({ expirationTime: 0 }, {
|
2022-02-03 12:43:10 +00:00
|
|
|
sql: 'key LIKE ?',
|
2022-03-01 07:20:58 +00:00
|
|
|
sqlParams: [key + '%'],
|
2022-02-03 12:43:10 +00:00
|
|
|
js: record => !!record.key?.startsWith(key),
|
2022-06-13 06:27:59 +00:00
|
|
|
})));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if tokenpluginfile can be used, and fix the URL afterwards.
|
|
|
|
*
|
|
|
|
* @param url The url to be fixed.
|
|
|
|
* @return Promise resolved with the fixed URL.
|
|
|
|
*/
|
|
|
|
checkAndFixPluginfileURL(url: string): Promise<string> {
|
2020-10-14 06:28:02 +00:00
|
|
|
return this.checkTokenPluginFile(url).then(() => this.fixPluginfileURL(url));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Generic function for adding the wstoken to Moodle urls and for pointing to the correct script.
|
|
|
|
* Uses CoreUtilsProvider.fixPluginfileURL, passing site's token.
|
|
|
|
*
|
|
|
|
* @param url The url to be fixed.
|
|
|
|
* @return Fixed URL.
|
|
|
|
*/
|
|
|
|
fixPluginfileURL(url: string): string {
|
2021-12-16 09:46:40 +00:00
|
|
|
const accessKey = this.tokenPluginFileWorks || this.tokenPluginFileWorks === undefined ?
|
2020-10-14 06:28:02 +00:00
|
|
|
this.infos && this.infos.userprivateaccesskey : undefined;
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
return CoreUrlUtils.fixPluginfileURL(url, this.token || '', this.siteUrl, accessKey);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deletes site's DB.
|
|
|
|
*
|
|
|
|
* @return Promise to be resolved when the DB is deleted.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
async deleteDB(): Promise<void> {
|
2021-03-02 10:41:04 +00:00
|
|
|
await CoreDB.deleteDB('Site-' + this.id);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deletes site's folder.
|
|
|
|
*
|
|
|
|
* @return Promise to be resolved when the DB is deleted.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
async deleteFolder(): Promise<void> {
|
2021-03-02 10:41:04 +00:00
|
|
|
if (!CoreFile.isAvailable() || !this.id) {
|
2020-10-14 06:28:02 +00:00
|
|
|
return;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
2020-10-14 06:28:02 +00:00
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
const siteFolder = CoreFile.getSiteFolder(this.id);
|
2020-10-14 06:28:02 +00:00
|
|
|
|
|
|
|
// Ignore any errors, removeDir fails if folder doesn't exists.
|
2021-03-02 10:41:04 +00:00
|
|
|
await CoreUtils.ignoreErrors(CoreFile.removeDir(siteFolder));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get space usage of the site.
|
|
|
|
*
|
|
|
|
* @return Promise resolved with the site space usage (size).
|
|
|
|
*/
|
|
|
|
getSpaceUsage(): Promise<number> {
|
2021-03-02 10:41:04 +00:00
|
|
|
if (CoreFile.isAvailable() && this.id) {
|
|
|
|
const siteFolderPath = CoreFile.getSiteFolder(this.id);
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
return CoreFile.getDirectorySize(siteFolderPath).catch(() => 0);
|
2020-10-07 08:52:51 +00:00
|
|
|
} else {
|
|
|
|
return Promise.resolve(0);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2022-06-13 06:27:59 +00:00
|
|
|
* Gets an approximation of the cache tables usage of the site.
|
2020-10-07 08:52:51 +00:00
|
|
|
*
|
2022-06-13 06:27:59 +00:00
|
|
|
* Currently this is just the total length of the data fields in the cache tables.
|
2020-10-07 08:52:51 +00:00
|
|
|
*
|
2022-06-13 06:27:59 +00:00
|
|
|
* @return Promise resolved with the total size of all data in the cache tables (bytes)
|
2020-10-07 08:52:51 +00:00
|
|
|
*/
|
2020-10-14 14:38:24 +00:00
|
|
|
async getCacheUsage(): Promise<number> {
|
2022-06-13 06:27:59 +00:00
|
|
|
const sizes = await Promise.all(Object.values(this.cacheTables).map(table => table.reduce({
|
2022-02-03 12:43:10 +00:00
|
|
|
sql: 'SUM(length(data))',
|
|
|
|
js: (size, record) => size + record.data.length,
|
|
|
|
jsInitialValue: 0,
|
2022-06-13 06:27:59 +00:00
|
|
|
})));
|
|
|
|
|
|
|
|
return sizes.reduce((totalSize, size) => totalSize + size, 0);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Gets a total of the file and cache usage.
|
|
|
|
*
|
|
|
|
* @return Promise with the total of getSpaceUsage and getCacheUsage
|
|
|
|
*/
|
|
|
|
async getTotalUsage(): Promise<number> {
|
|
|
|
const space = await this.getSpaceUsage();
|
|
|
|
const cache = await this.getCacheUsage();
|
|
|
|
|
|
|
|
return space + cache;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns the URL to the documentation of the app, based on Moodle version and current language.
|
|
|
|
*
|
|
|
|
* @param page Docs page to go to.
|
|
|
|
* @return Promise resolved with the Moodle docs URL.
|
|
|
|
*/
|
|
|
|
getDocsUrl(page?: string): Promise<string> {
|
2020-10-21 14:32:27 +00:00
|
|
|
const release = this.infos?.release ? this.infos.release : undefined;
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
return CoreUrlUtils.getDocsUrl(release, page);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Returns a url to link an specific page on the site.
|
|
|
|
*
|
|
|
|
* @param path Path of the url to go to.
|
|
|
|
* @param params Object with the params to add.
|
|
|
|
* @param anchor Anchor text if needed.
|
|
|
|
* @return URL with params.
|
|
|
|
*/
|
2020-10-14 14:38:24 +00:00
|
|
|
createSiteUrl(path: string, params?: CoreUrlParams, anchor?: string): string {
|
2021-03-02 10:41:04 +00:00
|
|
|
return CoreUrlUtils.addParamsToUrl(this.siteUrl + path, params, anchor);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the local_mobile plugin is installed in the Moodle site.
|
|
|
|
*
|
|
|
|
* @return Promise resolved when the check is done.
|
2021-09-03 11:33:03 +00:00
|
|
|
* @deprecated since app 4.0
|
2020-10-07 08:52:51 +00:00
|
|
|
*/
|
2021-09-03 11:33:03 +00:00
|
|
|
async checkLocalMobilePlugin(): Promise<LocalMobileResponse> {
|
|
|
|
// Not used anymore.
|
|
|
|
return { code: 0, coreSupported: true };
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if local_mobile has been installed in Moodle.
|
|
|
|
*
|
|
|
|
* @return Whether the App is able to use local_mobile plugin for this site.
|
2021-09-03 11:33:03 +00:00
|
|
|
* @deprecated since app 4.0
|
2020-10-07 08:52:51 +00:00
|
|
|
*/
|
|
|
|
checkIfAppUsesLocalMobile(): boolean {
|
2021-09-03 11:33:03 +00:00
|
|
|
return false;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if local_mobile has been installed in Moodle but the app is not using it.
|
|
|
|
*
|
|
|
|
* @return Promise resolved it local_mobile was added, rejected otherwise.
|
2021-09-03 11:33:03 +00:00
|
|
|
* @deprecated since app 4.0
|
2020-10-07 08:52:51 +00:00
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
async checkIfLocalMobileInstalledAndNotUsed(): Promise<void> {
|
2021-09-03 11:33:03 +00:00
|
|
|
throw new CoreError('Deprecated.');
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a URL belongs to this site.
|
|
|
|
*
|
|
|
|
* @param url URL to check.
|
|
|
|
* @return Whether the URL belongs to this site.
|
|
|
|
*/
|
2021-03-02 16:26:13 +00:00
|
|
|
containsUrl(url?: string): boolean {
|
2020-10-07 08:52:51 +00:00
|
|
|
if (!url) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
const siteUrl = CoreTextUtils.addEndingSlash(CoreUrlUtils.removeProtocolAndWWW(this.siteUrl));
|
|
|
|
url = CoreTextUtils.addEndingSlash(CoreUrlUtils.removeProtocolAndWWW(url));
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
return url.indexOf(siteUrl) == 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the public config of this site.
|
|
|
|
*
|
2022-04-08 10:19:54 +00:00
|
|
|
* @param options Options.
|
2020-10-07 08:52:51 +00:00
|
|
|
* @return Promise resolved with public config. Rejected with an object if error, see CoreWSProvider.callAjax.
|
|
|
|
*/
|
2022-04-08 10:19:54 +00:00
|
|
|
async getPublicConfig(options: { readingStrategy?: CoreSitesReadingStrategy } = {}): Promise<CoreSitePublicConfigResponse> {
|
|
|
|
if (!this.db) {
|
|
|
|
return this.requestPublicConfig();
|
|
|
|
}
|
|
|
|
|
|
|
|
const method = 'tool_mobile_get_public_config';
|
|
|
|
const cacheId = this.getCacheId(method, {});
|
|
|
|
const cachePreSets: CoreSiteWSPreSets = {
|
|
|
|
getFromCache: true,
|
|
|
|
saveToCache: true,
|
|
|
|
emergencyCache: true,
|
2022-05-31 10:55:43 +00:00
|
|
|
cacheKey: this.getPublicConfigCacheKey(),
|
2022-04-08 10:19:54 +00:00
|
|
|
...CoreSites.getReadingStrategyPreSets(options.readingStrategy),
|
|
|
|
};
|
|
|
|
|
|
|
|
if (this.offlineDisabled) {
|
|
|
|
// Offline is disabled, don't use cache.
|
|
|
|
cachePreSets.getFromCache = false;
|
|
|
|
cachePreSets.saveToCache = false;
|
|
|
|
cachePreSets.emergencyCache = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check for an ongoing identical request if we're not ignoring cache.
|
2022-06-13 06:27:59 +00:00
|
|
|
if (cachePreSets.getFromCache && this.ongoingRequests[cacheId] !== undefined) {
|
2022-06-23 08:42:20 +00:00
|
|
|
return await firstValueFrom(this.ongoingRequests[cacheId]);
|
2022-04-08 10:19:54 +00:00
|
|
|
}
|
|
|
|
|
2022-06-21 08:58:10 +00:00
|
|
|
const subject = new Subject<CoreSitePublicConfigResponse>();
|
|
|
|
const observable = subject.pipe(
|
|
|
|
// Return a clone of the original object, this may prevent errors if in the callback the object is modified.
|
|
|
|
map((data) => CoreUtils.clone(data)),
|
|
|
|
finalize(() => {
|
|
|
|
// Clear the ongoing request unless it has changed (e.g. a new request that ignores cache).
|
|
|
|
if (this.ongoingRequests[cacheId] === observable) {
|
|
|
|
delete this.ongoingRequests[cacheId];
|
|
|
|
}
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
|
|
|
|
this.ongoingRequests[cacheId] = observable;
|
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
this.getFromCache<CoreSitePublicConfigResponse>(method, {}, cachePreSets, false)
|
|
|
|
.then(cachedData => cachedData.response)
|
|
|
|
.catch(async () => {
|
|
|
|
if (cachePreSets.forceOffline) {
|
|
|
|
// Don't call the WS, just fail.
|
|
|
|
throw new CoreError(
|
|
|
|
Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }),
|
|
|
|
);
|
|
|
|
}
|
2022-04-08 10:19:54 +00:00
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
// Call the WS.
|
|
|
|
try {
|
|
|
|
const config = await this.requestPublicConfig();
|
2022-04-08 10:19:54 +00:00
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
if (cachePreSets.saveToCache) {
|
|
|
|
this.saveToCache(method, {}, config, cachePreSets);
|
|
|
|
}
|
2022-04-08 10:19:54 +00:00
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
return config;
|
|
|
|
} catch (error) {
|
|
|
|
cachePreSets.omitExpires = true;
|
|
|
|
cachePreSets.getFromCache = true;
|
2022-04-08 10:19:54 +00:00
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
try {
|
|
|
|
const cachedData = await this.getFromCache<CoreSitePublicConfigResponse>(method, {}, cachePreSets, true);
|
|
|
|
|
|
|
|
return cachedData.response;
|
|
|
|
} catch {
|
|
|
|
throw error;
|
|
|
|
}
|
2022-04-08 10:19:54 +00:00
|
|
|
}
|
2022-06-21 12:50:08 +00:00
|
|
|
}).then((response) => {
|
|
|
|
// The app doesn't store exceptions for this call, it's safe to assume type CoreSitePublicConfigResponse.
|
|
|
|
subject.next(<CoreSitePublicConfigResponse> response);
|
|
|
|
subject.complete();
|
2022-04-08 10:19:54 +00:00
|
|
|
|
2022-06-21 12:50:08 +00:00
|
|
|
return;
|
|
|
|
}).catch((error) => {
|
|
|
|
subject.error(error);
|
|
|
|
});
|
2022-04-08 10:19:54 +00:00
|
|
|
|
2022-06-23 08:42:20 +00:00
|
|
|
return firstValueFrom(observable);
|
2022-04-08 10:19:54 +00:00
|
|
|
}
|
|
|
|
|
2022-05-31 10:55:43 +00:00
|
|
|
/**
|
|
|
|
* Get cache key for getPublicConfig WS calls.
|
|
|
|
*
|
|
|
|
* @return Cache key.
|
|
|
|
*/
|
|
|
|
protected getPublicConfigCacheKey(): string {
|
|
|
|
return 'tool_mobile_get_public_config';
|
|
|
|
}
|
|
|
|
|
2022-04-08 10:19:54 +00:00
|
|
|
/**
|
|
|
|
* Perform a request to the server to get the public config of this site.
|
|
|
|
*
|
|
|
|
* @return Promise resolved with public config.
|
|
|
|
*/
|
|
|
|
protected async requestPublicConfig(): Promise<CoreSitePublicConfigResponse> {
|
2020-10-07 08:52:51 +00:00
|
|
|
const preSets: CoreWSAjaxPreSets = {
|
2020-10-14 06:28:02 +00:00
|
|
|
siteUrl: this.siteUrl,
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
|
|
|
|
2021-09-14 13:18:49 +00:00
|
|
|
let config: CoreSitePublicConfigResponse;
|
2020-10-21 14:32:27 +00:00
|
|
|
|
|
|
|
try {
|
2021-09-14 13:18:49 +00:00
|
|
|
config = await CoreWS.callAjax<CoreSitePublicConfigResponse>('tool_mobile_get_public_config', {}, preSets);
|
2020-10-21 14:32:27 +00:00
|
|
|
} catch (error) {
|
2021-09-14 13:18:49 +00:00
|
|
|
if (!error || error.errorcode !== 'codingerror' || (this.getInfo() && !this.isVersionGreaterEqualThan('3.8'))) {
|
|
|
|
throw error;
|
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2021-09-14 13:18:49 +00:00
|
|
|
// This error probably means that there is a redirect in the site. Try to use a GET request.
|
|
|
|
preSets.noLogin = true;
|
|
|
|
preSets.useGet = true;
|
|
|
|
|
|
|
|
try {
|
|
|
|
config = await CoreWS.callAjax<CoreSitePublicConfigResponse>('tool_mobile_get_public_config', {}, preSets);
|
|
|
|
} catch (error2) {
|
|
|
|
if (this.getInfo() && this.isVersionGreaterEqualThan('3.8')) {
|
|
|
|
// GET is supported, return the second error.
|
|
|
|
throw error2;
|
|
|
|
} else {
|
|
|
|
// GET not supported or we don't know if it's supported. Return first error.
|
|
|
|
throw error;
|
2020-10-21 14:32:27 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
2020-10-21 14:32:27 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
// Use the wwwroot returned by the server.
|
2021-09-14 13:18:49 +00:00
|
|
|
if (config.httpswwwroot) {
|
|
|
|
this.siteUrl = CoreUrlUtils.removeUrlParams(config.httpswwwroot); // Make sure the URL doesn't have params.
|
2020-10-21 14:32:27 +00:00
|
|
|
}
|
|
|
|
|
2021-09-14 13:18:49 +00:00
|
|
|
return config;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open a URL in browser using auto-login in the Moodle site if available.
|
|
|
|
*
|
|
|
|
* @param url The URL to open.
|
|
|
|
* @param alertMessage If defined, an alert will be shown before opening the browser.
|
2021-10-07 06:58:05 +00:00
|
|
|
* @param options Other options.
|
2020-10-07 08:52:51 +00:00
|
|
|
* @return Promise resolved when done, rejected otherwise.
|
|
|
|
*/
|
2021-10-07 06:58:05 +00:00
|
|
|
async openInBrowserWithAutoLogin(
|
|
|
|
url: string,
|
|
|
|
alertMessage?: string,
|
|
|
|
options: CoreUtilsOpenInBrowserOptions = {},
|
|
|
|
): Promise<void> {
|
|
|
|
await this.openWithAutoLogin(false, url, options, alertMessage);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open a URL in browser using auto-login in the Moodle site if available and the URL belongs to the site.
|
|
|
|
*
|
|
|
|
* @param url The URL to open.
|
|
|
|
* @param alertMessage If defined, an alert will be shown before opening the browser.
|
2021-10-07 06:58:05 +00:00
|
|
|
* @param options Other options.
|
2020-10-07 08:52:51 +00:00
|
|
|
* @return Promise resolved when done, rejected otherwise.
|
2022-07-01 13:42:35 +00:00
|
|
|
* @deprecated since 4.1. Use openInBrowserWithAutoLogin instead, now it always checks that URL belongs to same site.
|
2020-10-07 08:52:51 +00:00
|
|
|
*/
|
2021-10-07 06:58:05 +00:00
|
|
|
async openInBrowserWithAutoLoginIfSameSite(
|
|
|
|
url: string,
|
|
|
|
alertMessage?: string,
|
|
|
|
options: CoreUtilsOpenInBrowserOptions = {},
|
|
|
|
): Promise<void> {
|
2022-07-01 13:42:35 +00:00
|
|
|
return this.openInBrowserWithAutoLogin(url, alertMessage, options);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open a URL in inappbrowser using auto-login in the Moodle site if available.
|
|
|
|
*
|
|
|
|
* @param url The URL to open.
|
|
|
|
* @param options Override default options passed to InAppBrowser.
|
|
|
|
* @param alertMessage If defined, an alert will be shown before opening the inappbrowser.
|
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
async openInAppWithAutoLogin(url: string, options?: InAppBrowserOptions, alertMessage?: string): Promise<InAppBrowserObject> {
|
|
|
|
const iabInstance = <InAppBrowserObject> await this.openWithAutoLogin(true, url, options, alertMessage);
|
|
|
|
|
|
|
|
return iabInstance;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open a URL in inappbrowser using auto-login in the Moodle site if available and the URL belongs to the site.
|
|
|
|
*
|
|
|
|
* @param url The URL to open.
|
|
|
|
* @param options Override default options passed to inappbrowser.
|
|
|
|
* @param alertMessage If defined, an alert will be shown before opening the inappbrowser.
|
|
|
|
* @return Promise resolved when done.
|
2022-07-01 13:42:35 +00:00
|
|
|
* @deprecated since 4.1. Use openInAppWithAutoLogin instead, now it always checks that URL belongs to same site.
|
2020-10-07 08:52:51 +00:00
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
async openInAppWithAutoLoginIfSameSite(
|
|
|
|
url: string,
|
|
|
|
options?: InAppBrowserOptions,
|
|
|
|
alertMessage?: string,
|
|
|
|
): Promise<InAppBrowserObject> {
|
2022-07-01 13:42:35 +00:00
|
|
|
return this.openInAppWithAutoLogin(url, options, alertMessage);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open a URL in browser or InAppBrowser using auto-login in the Moodle site if available.
|
|
|
|
*
|
|
|
|
* @param inApp True to open it in InAppBrowser, false to open in browser.
|
|
|
|
* @param url The URL to open.
|
|
|
|
* @param options Override default options passed to $cordovaInAppBrowser#open.
|
|
|
|
* @param alertMessage If defined, an alert will be shown before opening the browser/inappbrowser.
|
|
|
|
* @return Promise resolved when done. Resolve param is returned only if inApp=true.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
async openWithAutoLogin(
|
|
|
|
inApp: boolean,
|
|
|
|
url: string,
|
2021-10-07 06:58:05 +00:00
|
|
|
options: InAppBrowserOptions & CoreUtilsOpenInBrowserOptions = {},
|
2020-10-21 14:32:27 +00:00
|
|
|
alertMessage?: string,
|
|
|
|
): Promise<InAppBrowserObject | void> {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Get the URL to open.
|
2022-03-01 08:43:30 +00:00
|
|
|
const autoLoginUrl = await this.getAutoLoginUrl(url);
|
2020-10-07 08:52:51 +00:00
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
if (alertMessage) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Show an alert first.
|
2021-03-02 10:41:04 +00:00
|
|
|
const alert = await CoreDomUtils.showAlert(
|
|
|
|
Translate.instant('core.notice'),
|
2020-10-21 14:32:27 +00:00
|
|
|
alertMessage,
|
|
|
|
undefined,
|
|
|
|
3000,
|
|
|
|
);
|
|
|
|
|
|
|
|
await alert.onDidDismiss();
|
2021-10-07 06:58:05 +00:00
|
|
|
options.showBrowserWarning = false; // A warning already shown, no need to show another.
|
2020-10-21 14:32:27 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Open the URL.
|
|
|
|
if (inApp) {
|
2022-03-01 08:43:30 +00:00
|
|
|
return CoreUtils.openInApp(autoLoginUrl, options);
|
2020-10-21 14:32:27 +00:00
|
|
|
} else {
|
2022-04-14 09:47:39 +00:00
|
|
|
options.browserWarningUrl = url;
|
2022-03-01 08:43:30 +00:00
|
|
|
|
|
|
|
return CoreUtils.openInBrowser(autoLoginUrl, options);
|
2020-10-21 14:32:27 +00:00
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Open a URL in browser or InAppBrowser using auto-login in the Moodle site if available and the URL belongs to the site.
|
|
|
|
*
|
|
|
|
* @param inApp True to open it in InAppBrowser, false to open in browser.
|
|
|
|
* @param url The URL to open.
|
|
|
|
* @param options Override default options passed to inappbrowser.
|
|
|
|
* @param alertMessage If defined, an alert will be shown before opening the browser/inappbrowser.
|
|
|
|
* @return Promise resolved when done. Resolve param is returned only if inApp=true.
|
2022-07-01 13:42:35 +00:00
|
|
|
* @deprecated since 4.1. Use openWithAutoLogin instead, now it always checks that URL belongs to same site.
|
2020-10-07 08:52:51 +00:00
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
async openWithAutoLoginIfSameSite(
|
|
|
|
inApp: boolean,
|
|
|
|
url: string,
|
2021-10-07 06:58:05 +00:00
|
|
|
options: InAppBrowserOptions & CoreUtilsOpenInBrowserOptions = {},
|
2020-10-21 14:32:27 +00:00
|
|
|
alertMessage?: string,
|
|
|
|
): Promise<InAppBrowserObject | void> {
|
2022-07-01 13:42:35 +00:00
|
|
|
return this.openWithAutoLogin(inApp, url, options, alertMessage);
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the config of this site.
|
|
|
|
* It is recommended to use getStoredConfig instead since it's faster and doesn't use network.
|
|
|
|
*
|
|
|
|
* @param name Name of the setting to get. If not set or false, all settings will be returned.
|
|
|
|
* @param ignoreCache True if it should ignore cached data.
|
|
|
|
* @return Promise resolved with site config.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
getConfig(name?: undefined, ignoreCache?: boolean): Promise<CoreSiteConfig>;
|
|
|
|
getConfig(name: string, ignoreCache?: boolean): Promise<string>;
|
2020-10-14 06:28:02 +00:00
|
|
|
getConfig(name?: string, ignoreCache?: boolean): Promise<string | CoreSiteConfig> {
|
2022-07-06 06:02:41 +00:00
|
|
|
return firstValueFrom(
|
|
|
|
this.getConfigObservable(<string> name, ignoreCache ? CoreSitesReadingStrategy.ONLY_NETWORK : undefined),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the config of this site.
|
|
|
|
* It is recommended to use getStoredConfig instead since it's faster and doesn't use network.
|
|
|
|
*
|
|
|
|
* @param name Name of the setting to get. If not set or false, all settings will be returned.
|
|
|
|
* @param readingStrategy Reading strategy.
|
|
|
|
* @return Observable returning site config.
|
|
|
|
*/
|
|
|
|
getConfigObservable(name?: undefined, readingStrategy?: CoreSitesReadingStrategy): Observable<CoreSiteConfig>;
|
|
|
|
getConfigObservable(name: string, readingStrategy?: CoreSitesReadingStrategy): Observable<string>;
|
|
|
|
getConfigObservable(name?: string, readingStrategy?: CoreSitesReadingStrategy): Observable<string | CoreSiteConfig> {
|
2020-10-07 08:52:51 +00:00
|
|
|
const preSets: CoreSiteWSPreSets = {
|
2020-10-14 06:28:02 +00:00
|
|
|
cacheKey: this.getConfigCacheKey(),
|
2022-07-06 06:02:41 +00:00
|
|
|
...CoreSites.getReadingStrategyPreSets(readingStrategy),
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
|
|
|
|
2022-07-06 06:02:41 +00:00
|
|
|
return this.readObservable<CoreSiteConfigResponse>('tool_mobile_get_config', {}, preSets).pipe(map(config => {
|
2020-10-07 08:52:51 +00:00
|
|
|
if (name) {
|
|
|
|
// Return the requested setting.
|
|
|
|
for (const x in config.settings) {
|
|
|
|
if (config.settings[x].name == name) {
|
|
|
|
return config.settings[x].value;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-14 06:28:02 +00:00
|
|
|
throw new CoreError('Site config not found: ' + name);
|
2020-10-07 08:52:51 +00:00
|
|
|
} else {
|
|
|
|
// Return all settings in the same array.
|
|
|
|
const settings = {};
|
|
|
|
config.settings.forEach((setting) => {
|
|
|
|
settings[setting.name] = setting.value;
|
|
|
|
});
|
|
|
|
|
|
|
|
return settings;
|
|
|
|
}
|
2022-07-06 06:02:41 +00:00
|
|
|
}));
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Invalidates config WS call.
|
|
|
|
*
|
|
|
|
* @return Promise resolved when the data is invalidated.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
async invalidateConfig(): Promise<void> {
|
|
|
|
await this.invalidateWsCacheForKey(this.getConfigCacheKey());
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get cache key for getConfig WS calls.
|
|
|
|
*
|
|
|
|
* @return Cache key.
|
|
|
|
*/
|
|
|
|
protected getConfigCacheKey(): string {
|
|
|
|
return 'tool_mobile_get_config';
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get the stored config of this site.
|
|
|
|
*
|
|
|
|
* @param name Name of the setting to get. If not set, all settings will be returned.
|
|
|
|
* @return Site config or a specific setting.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
getStoredConfig(): CoreSiteConfig | undefined;
|
|
|
|
getStoredConfig(name: string): string | undefined;
|
|
|
|
getStoredConfig(name?: string): string | CoreSiteConfig | undefined {
|
2020-10-07 08:52:51 +00:00
|
|
|
if (!this.config) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (name) {
|
|
|
|
return this.config[name];
|
|
|
|
} else {
|
|
|
|
return this.config;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a certain feature is disabled in the site.
|
|
|
|
*
|
|
|
|
* @param name Name of the feature to check.
|
|
|
|
* @return Whether it's disabled.
|
|
|
|
*/
|
|
|
|
isFeatureDisabled(name: string): boolean {
|
2020-10-21 14:32:27 +00:00
|
|
|
const disabledFeatures = this.getStoredConfig('tool_mobile_disabledfeatures');
|
2020-10-07 08:52:51 +00:00
|
|
|
if (!disabledFeatures) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
const regEx = new RegExp('(,|^)' + CoreTextUtils.escapeForRegex(name) + '(,|$)', 'g');
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
return !!disabledFeatures.match(regEx);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Calculate if offline is disabled in the site.
|
|
|
|
*/
|
|
|
|
calculateOfflineDisabled(): void {
|
|
|
|
this.offlineDisabled = this.isFeatureDisabled('NoDelegate_CoreOffline');
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get whether offline is disabled in the site.
|
|
|
|
*
|
|
|
|
* @return Whether it's disabled.
|
|
|
|
*/
|
|
|
|
isOfflineDisabled(): boolean {
|
|
|
|
return this.offlineDisabled;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if the site version is greater than one or several versions.
|
|
|
|
* This function accepts a string or an array of strings. If array, the last version must be the highest.
|
|
|
|
*
|
|
|
|
* @param versions Version or list of versions to check.
|
|
|
|
* @return Whether it's greater or equal, false otherwise.
|
|
|
|
* @description
|
|
|
|
* If a string is supplied (e.g. '3.2.1'), it will check if the site version is greater or equal than this version.
|
|
|
|
*
|
|
|
|
* If an array of versions is supplied, it will check if the site version is greater or equal than the last version,
|
|
|
|
* or if it's higher or equal than any of the other releases supplied but lower than the next major release. The last
|
|
|
|
* version of the array must be the highest version.
|
|
|
|
* For example, if the values supplied are ['3.0.5', '3.2.3', '3.3.1'] the function will return true if the site version
|
|
|
|
* is either:
|
|
|
|
* - Greater or equal than 3.3.1.
|
|
|
|
* - Greater or equal than 3.2.3 but lower than 3.3.
|
|
|
|
* - Greater or equal than 3.0.5 but lower than 3.1.
|
|
|
|
*
|
|
|
|
* This function only accepts versions from 2.4.0 and above. If any of the versions supplied isn't found, it will assume
|
|
|
|
* it's the last released major version.
|
|
|
|
*/
|
|
|
|
isVersionGreaterEqualThan(versions: string | string[]): boolean {
|
2020-10-21 14:32:27 +00:00
|
|
|
const info = this.getInfo();
|
|
|
|
|
|
|
|
if (!info || !info.version) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
const siteVersion = Number(info.version);
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
if (Array.isArray(versions)) {
|
|
|
|
if (!versions.length) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
for (let i = 0; i < versions.length; i++) {
|
|
|
|
const versionNumber = this.getVersionNumber(versions[i]);
|
|
|
|
if (i == versions.length - 1) {
|
|
|
|
// It's the last version, check only if site version is greater than this one.
|
|
|
|
return siteVersion >= versionNumber;
|
|
|
|
} else {
|
|
|
|
// Check if site version if bigger than this number but lesser than next major.
|
|
|
|
if (siteVersion >= versionNumber && siteVersion < this.getNextMajorVersionNumber(versions[i])) {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
} else if (typeof versions == 'string') {
|
|
|
|
// Compare with this version.
|
|
|
|
return siteVersion >= this.getVersionNumber(versions);
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a URL, convert it to a URL that will auto-login if supported.
|
|
|
|
*
|
|
|
|
* @param url The URL to convert.
|
|
|
|
* @param showModal Whether to show a loading modal.
|
|
|
|
* @return Promise resolved with the converted URL.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
async getAutoLoginUrl(url: string, showModal: boolean = true): Promise<string> {
|
2022-04-05 13:52:24 +00:00
|
|
|
if (!this.privateToken) {
|
|
|
|
// No private token, don't change the URL.
|
2020-10-21 14:32:27 +00:00
|
|
|
return url;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2022-07-01 13:42:35 +00:00
|
|
|
if (!this.containsUrl(url)) {
|
|
|
|
// URL doesn't belong to the site, don't auto login.
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
2022-04-05 13:52:24 +00:00
|
|
|
if (this.lastAutoLogin > 0) {
|
|
|
|
const timeBetweenRequests = await CoreUtils.ignoreErrors(
|
|
|
|
this.getConfig('tool_mobile_autologinmintimebetweenreq'),
|
|
|
|
CoreConstants.SECONDS_MINUTE * 6,
|
|
|
|
);
|
|
|
|
|
|
|
|
if (CoreTimeUtils.timestamp() - this.lastAutoLogin < timeBetweenRequests) {
|
|
|
|
// Not enough time has passed since last auto login.
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
const userId = this.getUserId();
|
|
|
|
const params = {
|
|
|
|
privatetoken: this.privateToken,
|
|
|
|
};
|
2020-10-21 14:32:27 +00:00
|
|
|
let modal: CoreIonLoadingElement | undefined;
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
if (showModal) {
|
2021-03-02 10:41:04 +00:00
|
|
|
modal = await CoreDomUtils.showModalLoading();
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
try {
|
|
|
|
// Use write to not use cache.
|
|
|
|
const data = await this.write<CoreSiteAutologinKeyResult>('tool_mobile_get_autologin_key', params);
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
if (!data.autologinurl || !data.key) {
|
|
|
|
// Not valid data, return the same URL.
|
|
|
|
return url;
|
|
|
|
}
|
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
this.lastAutoLogin = CoreTimeUtils.timestamp();
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
return data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + encodeURIComponent(url);
|
2020-10-21 14:32:27 +00:00
|
|
|
} catch (error) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Couldn't get autologin key, return the same URL.
|
2020-10-21 14:32:27 +00:00
|
|
|
return url;
|
|
|
|
} finally {
|
|
|
|
modal?.dismiss();
|
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a version number from a release version.
|
|
|
|
* If release version is valid but not found in the list of Moodle releases, it will use the last released major version.
|
|
|
|
*
|
|
|
|
* @param version Release version to convert to version number.
|
|
|
|
* @return Version number, 0 if invalid.
|
|
|
|
*/
|
|
|
|
protected getVersionNumber(version: string): number {
|
|
|
|
const data = this.getMajorAndMinor(version);
|
|
|
|
|
|
|
|
if (!data) {
|
|
|
|
// Invalid version.
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2021-09-03 07:26:02 +00:00
|
|
|
if (CoreSite.MOODLE_RELEASES[data.major] === undefined) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Major version not found. Use the last one.
|
2021-10-14 13:03:13 +00:00
|
|
|
const major = Object.keys(CoreSite.MOODLE_RELEASES).pop();
|
|
|
|
if (!major) {
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
data.major = major;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2021-09-03 07:26:02 +00:00
|
|
|
return CoreSite.MOODLE_RELEASES[data.major] + data.minor;
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a release version, return the major and minor versions.
|
|
|
|
*
|
|
|
|
* @param version Release version (e.g. '3.1.0').
|
|
|
|
* @return Object with major and minor. Returns false if invalid version.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
protected getMajorAndMinor(version: string): {major: string; minor: number} | false {
|
2021-05-06 10:35:32 +00:00
|
|
|
const match = version.match(/^(\d+)(\.(\d+)(\.\d+)?)?/);
|
2020-10-07 08:52:51 +00:00
|
|
|
if (!match || !match[1]) {
|
|
|
|
// Invalid version.
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return {
|
2021-05-06 10:35:32 +00:00
|
|
|
major: match[1] + '.' + (match[3] || '0'),
|
|
|
|
minor: parseInt(match[5], 10) || 0,
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Given a release version, return the next major version number.
|
|
|
|
*
|
|
|
|
* @param version Release version (e.g. '3.1.0').
|
|
|
|
* @return Next major version number.
|
|
|
|
*/
|
|
|
|
protected getNextMajorVersionNumber(version: string): number {
|
2020-10-14 06:28:02 +00:00
|
|
|
const data = this.getMajorAndMinor(version);
|
2021-09-03 07:26:02 +00:00
|
|
|
const releases = Object.keys(CoreSite.MOODLE_RELEASES);
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
if (!data) {
|
|
|
|
// Invalid version.
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
|
2020-10-14 06:28:02 +00:00
|
|
|
const position = releases.indexOf(data.major);
|
2020-10-07 08:52:51 +00:00
|
|
|
|
|
|
|
if (position == -1 || position == releases.length - 1) {
|
|
|
|
// Major version not found or it's the last one. Use the last one.
|
2021-09-03 07:26:02 +00:00
|
|
|
return CoreSite.MOODLE_RELEASES[releases[position]];
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2021-09-03 07:26:02 +00:00
|
|
|
return CoreSite.MOODLE_RELEASES[releases[position + 1]];
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Deletes a site setting.
|
|
|
|
*
|
|
|
|
* @param name The config name.
|
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
async deleteSiteConfig(name: string): Promise<void> {
|
2022-02-10 14:59:54 +00:00
|
|
|
await this.configTable.deleteByPrimaryKey({ name });
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a site setting on local device.
|
|
|
|
*
|
|
|
|
* @param name The config name.
|
|
|
|
* @param defaultValue Default value to use if the entry is not found.
|
|
|
|
* @return Resolves upon success along with the config data. Reject on failure.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
async getLocalSiteConfig<T extends number | string>(name: string, defaultValue?: T): Promise<T> {
|
|
|
|
try {
|
2022-02-10 14:59:54 +00:00
|
|
|
const entry = await this.configTable.getOneByPrimaryKey({ name });
|
2020-10-21 14:32:27 +00:00
|
|
|
|
|
|
|
return <T> entry.value;
|
|
|
|
} catch (error) {
|
2021-12-16 09:46:40 +00:00
|
|
|
if (defaultValue !== undefined) {
|
2020-10-07 08:52:51 +00:00
|
|
|
return defaultValue;
|
|
|
|
}
|
|
|
|
|
2020-10-21 14:32:27 +00:00
|
|
|
throw error;
|
|
|
|
}
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Set a site setting on local device.
|
|
|
|
*
|
|
|
|
* @param name The config name.
|
|
|
|
* @param value The config value. Can only store number or strings.
|
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
2020-10-14 06:28:02 +00:00
|
|
|
async setLocalSiteConfig(name: string, value: number | string): Promise<void> {
|
2022-02-10 14:59:54 +00:00
|
|
|
await this.configTable.insert({ name, value });
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a certain cache expiration delay.
|
|
|
|
*
|
|
|
|
* @param updateFrequency The update frequency of the entry.
|
|
|
|
* @return Expiration delay.
|
|
|
|
*/
|
|
|
|
getExpirationDelay(updateFrequency?: number): number {
|
2020-10-21 14:32:27 +00:00
|
|
|
updateFrequency = updateFrequency || CoreSite.FREQUENCY_USUALLY;
|
2020-10-07 08:52:51 +00:00
|
|
|
let expirationDelay = this.UPDATE_FREQUENCIES[updateFrequency] || this.UPDATE_FREQUENCIES[CoreSite.FREQUENCY_USUALLY];
|
|
|
|
|
2022-05-11 12:06:42 +00:00
|
|
|
if (CoreNetwork.isNetworkAccessLimited()) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Not WiFi, increase the expiration delay a 50% to decrease the data usage in this case.
|
|
|
|
expirationDelay *= 1.5;
|
|
|
|
}
|
|
|
|
|
|
|
|
return expirationDelay;
|
|
|
|
}
|
|
|
|
|
|
|
|
/*
|
|
|
|
* Check if tokenpluginfile script works in the site.
|
|
|
|
*
|
|
|
|
* @param url URL to check.
|
|
|
|
* @return Promise resolved with boolean: whether it works or not.
|
|
|
|
*/
|
|
|
|
checkTokenPluginFile(url: string): Promise<boolean> {
|
2021-03-02 10:41:04 +00:00
|
|
|
if (!CoreUrlUtils.canUseTokenPluginFile(url, this.siteUrl, this.infos && this.infos.userprivateaccesskey)) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Cannot use tokenpluginfile.
|
|
|
|
return Promise.resolve(false);
|
2021-12-16 09:46:40 +00:00
|
|
|
} else if (this.tokenPluginFileWorks !== undefined) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Already checked.
|
|
|
|
return Promise.resolve(this.tokenPluginFileWorks);
|
|
|
|
} else if (this.tokenPluginFileWorksPromise) {
|
|
|
|
// Check ongoing, use the same promise.
|
|
|
|
return this.tokenPluginFileWorksPromise;
|
2022-05-11 12:06:42 +00:00
|
|
|
} else if (!CoreNetwork.isOnline()) {
|
2020-10-07 08:52:51 +00:00
|
|
|
// Not online, cannot check it. Assume it's working, but don't save the result.
|
|
|
|
return Promise.resolve(true);
|
|
|
|
}
|
|
|
|
|
|
|
|
url = this.fixPluginfileURL(url);
|
|
|
|
|
2021-03-02 10:41:04 +00:00
|
|
|
this.tokenPluginFileWorksPromise = CoreWS.urlWorks(url).then((result) => {
|
2020-10-07 08:52:51 +00:00
|
|
|
this.tokenPluginFileWorks = result;
|
|
|
|
|
|
|
|
return result;
|
|
|
|
});
|
|
|
|
|
|
|
|
return this.tokenPluginFileWorksPromise;
|
|
|
|
}
|
2020-10-14 06:28:02 +00:00
|
|
|
|
2021-11-03 11:59:00 +00:00
|
|
|
/**
|
|
|
|
* Check if a URL to a file belongs to the site and uses the pluginfileurl or tokenpluginfileurl endpoints.
|
|
|
|
*
|
|
|
|
* @param url File URL to check.
|
|
|
|
* @return Whether it's a site file URL.
|
|
|
|
*/
|
|
|
|
isSitePluginFileUrl(url: string): boolean {
|
|
|
|
const isPluginFileUrl = CoreUrlUtils.isPluginFileUrl(url) || CoreUrlUtils.isTokenPluginFileUrl(url);
|
|
|
|
if (!isPluginFileUrl) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.containsUrl(url);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Check if a URL to a file belongs to the site and is a theme image file.
|
|
|
|
*
|
|
|
|
* @param url File URL to check.
|
|
|
|
* @return Whether it's a site theme image URL.
|
|
|
|
*/
|
|
|
|
isSiteThemeImageUrl(url: string): boolean {
|
|
|
|
if (!CoreUrlUtils.isThemeImageUrl(url)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return this.containsUrl(url);
|
|
|
|
}
|
|
|
|
|
2022-02-28 11:38:46 +00:00
|
|
|
/**
|
|
|
|
* Deletes last viewed records based on some conditions.
|
|
|
|
*
|
|
|
|
* @param conditions Conditions.
|
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
|
|
|
async deleteLastViewed(conditions?: Partial<CoreSiteLastViewedDBRecord>): Promise<void> {
|
|
|
|
await this.lastViewedTable.delete(conditions);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Get a last viewed record for a component+id.
|
|
|
|
*
|
|
|
|
* @param component The component.
|
|
|
|
* @param id ID.
|
|
|
|
* @return Resolves with last viewed record, undefined if not found.
|
|
|
|
*/
|
|
|
|
async getLastViewed(component: string, id: number): Promise<CoreSiteLastViewedDBRecord | undefined> {
|
|
|
|
try {
|
|
|
|
return await this.lastViewedTable.getOneByPrimaryKey({ component, id });
|
2022-08-31 14:49:31 +00:00
|
|
|
} catch {
|
2022-02-28 11:38:46 +00:00
|
|
|
// Not found.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-03-09 13:38:00 +00:00
|
|
|
/**
|
|
|
|
* Get several last viewed for a certain component.
|
|
|
|
*
|
|
|
|
* @param component The component.
|
|
|
|
* @param ids IDs. If not provided or empty, return all last viewed for a component.
|
|
|
|
* @return Resolves with last viewed records, undefined if error.
|
|
|
|
*/
|
|
|
|
async getComponentLastViewed(component: string, ids: number[] = []): Promise<CoreSiteLastViewedDBRecord[] | undefined> {
|
|
|
|
try {
|
|
|
|
if (!ids.length) {
|
|
|
|
return await this.lastViewedTable.getMany({ component });
|
|
|
|
}
|
|
|
|
|
|
|
|
const whereAndParams = SQLiteDB.getInOrEqual(ids);
|
|
|
|
|
|
|
|
whereAndParams.sql = 'id ' + whereAndParams.sql + ' AND component = ?';
|
|
|
|
whereAndParams.params.push(component);
|
|
|
|
|
|
|
|
return await this.lastViewedTable.getManyWhere({
|
|
|
|
sql: whereAndParams.sql,
|
|
|
|
sqlParams: whereAndParams.params,
|
|
|
|
js: (record) => record.component === component && ids.includes(record.id),
|
|
|
|
});
|
2022-08-31 14:49:31 +00:00
|
|
|
} catch {
|
2022-03-09 13:38:00 +00:00
|
|
|
// Not found.
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-02-28 11:38:46 +00:00
|
|
|
/**
|
|
|
|
* Store a last viewed record.
|
|
|
|
*
|
|
|
|
* @param component The component.
|
|
|
|
* @param id ID.
|
|
|
|
* @param value Last viewed item value.
|
2022-03-09 13:38:00 +00:00
|
|
|
* @param options Options.
|
2022-02-28 11:38:46 +00:00
|
|
|
* @return Promise resolved when done.
|
|
|
|
*/
|
2022-03-09 13:38:00 +00:00
|
|
|
async storeLastViewed(
|
|
|
|
component: string,
|
|
|
|
id: number,
|
|
|
|
value: string | number,
|
|
|
|
options: CoreSiteStoreLastViewedOptions = {},
|
|
|
|
): Promise<void> {
|
2022-02-28 11:38:46 +00:00
|
|
|
await this.lastViewedTable.insert({
|
|
|
|
component,
|
|
|
|
id,
|
|
|
|
value: String(value),
|
2022-03-09 13:38:00 +00:00
|
|
|
data: options.data,
|
|
|
|
timeaccess: options.timeaccess ?? Date.now(),
|
2022-02-28 11:38:46 +00:00
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
}
|
|
|
|
|
2022-07-01 12:48:55 +00:00
|
|
|
/**
|
|
|
|
* Operator to chain requests when using observables.
|
|
|
|
*
|
|
|
|
* @param readingStrategy Reading strategy used for the current request.
|
|
|
|
* @param callback Callback called with the result of current request and the reading strategy to use in next requests.
|
|
|
|
* @return Operator.
|
|
|
|
*/
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
export function chainRequests<T, O extends ObservableInput<any>>(
|
|
|
|
readingStrategy: CoreSitesReadingStrategy | undefined,
|
|
|
|
callback: (data: T, readingStrategy?: CoreSitesReadingStrategy) => O,
|
|
|
|
): OperatorFunction<T, ObservedValueOf<O>> {
|
|
|
|
return (source: Observable<T>) => new Observable<{ data: T; readingStrategy?: CoreSitesReadingStrategy }>(subscriber => {
|
|
|
|
let firstValue = true;
|
|
|
|
let isCompleted = false;
|
|
|
|
|
|
|
|
return source.subscribe({
|
|
|
|
next: async (value) => {
|
|
|
|
if (readingStrategy !== CoreSitesReadingStrategy.UPDATE_IN_BACKGROUND) {
|
|
|
|
// Just use same strategy.
|
|
|
|
subscriber.next({ data: value, readingStrategy });
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!firstValue) {
|
|
|
|
// Second (last) value. Chained requests should have used cached data already, just return 1 value now.
|
|
|
|
subscriber.next({
|
|
|
|
data: value,
|
|
|
|
});
|
|
|
|
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
firstValue = false;
|
|
|
|
|
|
|
|
// Wait to see if the observable is completed (no more values).
|
|
|
|
await CoreUtils.nextTick();
|
|
|
|
|
|
|
|
if (isCompleted) {
|
|
|
|
// Current request only returns cached data. Let chained requests update in background.
|
|
|
|
subscriber.next({ data: value, readingStrategy });
|
|
|
|
} else {
|
|
|
|
// Current request will update in background. Prefer cached data in the chained requests.
|
|
|
|
subscriber.next({
|
|
|
|
data: value,
|
|
|
|
readingStrategy: CoreSitesReadingStrategy.PREFER_CACHE,
|
|
|
|
});
|
|
|
|
}
|
|
|
|
},
|
|
|
|
error: (error) => subscriber.error(error),
|
|
|
|
complete: async () => {
|
|
|
|
isCompleted = true;
|
|
|
|
|
|
|
|
await CoreUtils.nextTick();
|
|
|
|
|
|
|
|
subscriber.complete();
|
|
|
|
},
|
|
|
|
});
|
|
|
|
}).pipe(
|
|
|
|
mergeMap(({ data, readingStrategy }) => callback(data, readingStrategy)),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
/**
|
|
|
|
* PreSets accepted by the WS call.
|
|
|
|
*/
|
|
|
|
export type CoreSiteWSPreSets = {
|
|
|
|
/**
|
|
|
|
* Get the value from the cache if it's still valid.
|
|
|
|
*/
|
|
|
|
getFromCache?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Save the result to the cache.
|
|
|
|
*/
|
|
|
|
saveToCache?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Ignore cache expiration.
|
|
|
|
*/
|
|
|
|
omitExpires?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Use the cache when a request fails. Defaults to true.
|
|
|
|
*/
|
|
|
|
emergencyCache?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If true, the app won't call the WS. If the data isn't cached, the call will fail.
|
|
|
|
*/
|
|
|
|
forceOffline?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Extra key to add to the cache when storing this call, to identify the entry.
|
|
|
|
*/
|
|
|
|
cacheKey?: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether it should use cache key to retrieve the cached data instead of the request params.
|
|
|
|
*/
|
|
|
|
getCacheUsingCacheKey?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Same as getCacheUsingCacheKey, but for emergency cache.
|
|
|
|
*/
|
|
|
|
getEmergencyCacheUsingCacheKey?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If true, the cache entry will be deleted if the WS call returns an exception.
|
|
|
|
*/
|
|
|
|
deleteCacheIfWSError?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether it should only be 1 entry for this cache key (all entries with same key will be deleted).
|
|
|
|
*/
|
|
|
|
uniqueCacheKey?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether to filter WS response (moodlewssettingfilter). Defaults to true.
|
|
|
|
*/
|
|
|
|
filter?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether to rewrite URLs (moodlewssettingfileurl). Defaults to true.
|
|
|
|
*/
|
|
|
|
rewriteurls?: boolean;
|
|
|
|
|
2021-10-14 11:13:27 +00:00
|
|
|
/**
|
|
|
|
* Language to send to the WebService (moodlewssettinglang). Defaults to app's language.
|
|
|
|
*/
|
|
|
|
lang?: string;
|
|
|
|
|
2020-10-07 08:52:51 +00:00
|
|
|
/**
|
|
|
|
* Defaults to true. Set to false when the expected response is null.
|
|
|
|
*/
|
|
|
|
responseExpected?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Defaults to 'object'. Use it when you expect a type that's not an object|array.
|
|
|
|
*/
|
|
|
|
typeExpected?: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Wehther a pending request in the queue matching the same function and arguments can be reused instead of adding
|
|
|
|
* a new request to the queue. Defaults to true for read requests.
|
|
|
|
*/
|
|
|
|
reusePending?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether the request will be be sent immediately as a single request. Defaults to false.
|
|
|
|
*/
|
|
|
|
skipQueue?: boolean;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Cache the response if it returns an errorcode present in this list.
|
|
|
|
*/
|
|
|
|
cacheErrors?: string[];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update frequency. This value determines how often the cached data will be updated. Possible values:
|
|
|
|
* CoreSite.FREQUENCY_USUALLY, CoreSite.FREQUENCY_OFTEN, CoreSite.FREQUENCY_SOMETIMES, CoreSite.FREQUENCY_RARELY.
|
|
|
|
* Defaults to CoreSite.FREQUENCY_USUALLY.
|
|
|
|
*/
|
|
|
|
updateFrequency?: number;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component name. Optionally included if this request is being made on behalf of a specific
|
|
|
|
* component (e.g. activity).
|
|
|
|
*/
|
|
|
|
component?: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Component id. Optionally included when 'component' is set.
|
|
|
|
*/
|
|
|
|
componentId?: number;
|
2021-01-19 09:00:40 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether to split a request if it has too many parameters. Sending too many parameters to the site
|
|
|
|
* can cause the request to fail (see PHP's max_input_vars).
|
|
|
|
*/
|
|
|
|
splitRequest?: CoreWSPreSetsSplitRequest;
|
2022-06-21 12:50:08 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* If true, the app will return cached data even if it's expired and then it'll call the WS in the background.
|
|
|
|
* Only enabled if CoreConstants.CONFIG.disableCallWSInBackground isn't true.
|
|
|
|
*/
|
|
|
|
updateInBackground?: boolean;
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Response of checking local_mobile status.
|
2021-09-03 11:33:03 +00:00
|
|
|
*
|
|
|
|
* @deprecated since app 4.0
|
2020-10-07 08:52:51 +00:00
|
|
|
*/
|
|
|
|
export type LocalMobileResponse = {
|
|
|
|
/**
|
|
|
|
* Code to identify the authentication method to use.
|
|
|
|
*/
|
|
|
|
code: number;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Name of the service to use.
|
|
|
|
*/
|
|
|
|
service?: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Code of the warning message.
|
|
|
|
*/
|
|
|
|
warning?: string;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Whether core SSO is supported.
|
|
|
|
*/
|
|
|
|
coreSupported?: boolean;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Info of a request waiting in the queue.
|
|
|
|
*/
|
2020-10-21 14:32:27 +00:00
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
type RequestQueueItem<T = any> = {
|
2020-10-07 08:52:51 +00:00
|
|
|
cacheId: string;
|
|
|
|
method: string;
|
2020-10-21 14:32:27 +00:00
|
|
|
data: any; // eslint-disable-line @typescript-eslint/no-explicit-any
|
2020-10-07 08:52:51 +00:00
|
|
|
preSets: CoreSiteWSPreSets;
|
|
|
|
wsPreSets: CoreWSPreSets;
|
2022-05-30 16:13:43 +00:00
|
|
|
deferred: CorePromisedValue<T>;
|
2020-10-07 08:52:51 +00:00
|
|
|
};
|
2020-10-14 06:28:02 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Result of WS core_webservice_get_site_info.
|
|
|
|
*/
|
|
|
|
export type CoreSiteInfoResponse = {
|
|
|
|
sitename: string; // Site name.
|
|
|
|
username: string; // Username.
|
|
|
|
firstname: string; // First name.
|
|
|
|
lastname: string; // Last name.
|
|
|
|
fullname: string; // User full name.
|
|
|
|
lang: string; // Current language.
|
|
|
|
userid: number; // User id.
|
|
|
|
siteurl: string; // Site url.
|
|
|
|
userpictureurl: string; // The user profile picture.
|
|
|
|
functions: {
|
|
|
|
name: string; // Function name.
|
|
|
|
version: string; // The version number of the component to which the function belongs.
|
|
|
|
}[];
|
|
|
|
downloadfiles?: number; // 1 if users are allowed to download files, 0 if not.
|
|
|
|
uploadfiles?: number; // 1 if users are allowed to upload files, 0 if not.
|
|
|
|
release?: string; // Moodle release number.
|
|
|
|
version?: string; // Moodle version number.
|
|
|
|
mobilecssurl?: string; // Mobile custom CSS theme.
|
|
|
|
advancedfeatures?: { // Advanced features availability.
|
|
|
|
name: string; // Feature name.
|
|
|
|
value: number; // Feature value. Usually 1 means enabled.
|
|
|
|
}[];
|
|
|
|
usercanmanageownfiles?: boolean; // True if the user can manage his own files.
|
|
|
|
userquota?: number; // User quota (bytes). 0 means user can ignore the quota.
|
|
|
|
usermaxuploadfilesize?: number; // User max upload file size (bytes). -1 means the user can ignore the upload file size.
|
2021-12-15 11:30:35 +00:00
|
|
|
userhomepage?: CoreSiteInfoUserHomepage; // The default home page for the user.
|
2020-10-14 06:28:02 +00:00
|
|
|
userprivateaccesskey?: string; // Private user access key for fetching files.
|
|
|
|
siteid?: number; // Site course ID.
|
|
|
|
sitecalendartype?: string; // Calendar type set in the site.
|
|
|
|
usercalendartype?: string; // Calendar typed used by the user.
|
|
|
|
userissiteadmin?: boolean; // Whether the user is a site admin or not.
|
|
|
|
theme?: string; // Current theme for the user.
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Site info, including some calculated data.
|
|
|
|
*/
|
|
|
|
export type CoreSiteInfo = CoreSiteInfoResponse & {
|
|
|
|
functionsByName?: {
|
|
|
|
[name: string]: {
|
|
|
|
name: string; // Function name.
|
|
|
|
version: string; // The version number of the component to which the function belongs.
|
|
|
|
};
|
|
|
|
};
|
|
|
|
};
|
|
|
|
|
2021-12-15 11:30:35 +00:00
|
|
|
/**
|
|
|
|
* Enum constants that define default user home page.
|
|
|
|
*/
|
|
|
|
export enum CoreSiteInfoUserHomepage {
|
|
|
|
HOMEPAGE_SITE = 0, // Site home.
|
|
|
|
HOMEPAGE_MY = 1, // Dashboard.
|
|
|
|
HOMEPAGE_MYCOURSES = 3, // My courses.
|
2022-09-02 15:57:22 +00:00
|
|
|
}
|
2021-12-15 11:30:35 +00:00
|
|
|
|
2020-10-14 06:28:02 +00:00
|
|
|
/**
|
|
|
|
* Result of WS tool_mobile_get_config.
|
|
|
|
*/
|
|
|
|
export type CoreSiteConfigResponse = {
|
|
|
|
settings: { // Settings.
|
|
|
|
name: string; // The name of the setting.
|
|
|
|
value: string; // The value of the setting.
|
|
|
|
}[];
|
|
|
|
warnings?: CoreWSExternalWarning[];
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Site config indexed by name.
|
|
|
|
*/
|
|
|
|
export type CoreSiteConfig = {[name: string]: string};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Result of WS tool_mobile_get_public_config.
|
|
|
|
*/
|
|
|
|
export type CoreSitePublicConfigResponse = {
|
|
|
|
wwwroot: string; // Site URL.
|
|
|
|
httpswwwroot: string; // Site https URL (if httpslogin is enabled).
|
|
|
|
sitename: string; // Site name.
|
|
|
|
guestlogin: number; // Whether guest login is enabled.
|
|
|
|
rememberusername: number; // Values: 0 for No, 1 for Yes, 2 for optional.
|
|
|
|
authloginviaemail: number; // Whether log in via email is enabled.
|
|
|
|
registerauth: string; // Authentication method for user registration.
|
|
|
|
forgottenpasswordurl: string; // Forgotten password URL.
|
|
|
|
authinstructions: string; // Authentication instructions.
|
|
|
|
authnoneenabled: number; // Whether auth none is enabled.
|
|
|
|
enablewebservices: number; // Whether Web Services are enabled.
|
|
|
|
enablemobilewebservice: number; // Whether the Mobile service is enabled.
|
|
|
|
maintenanceenabled: number; // Whether site maintenance is enabled.
|
|
|
|
maintenancemessage: string; // Maintenance message.
|
|
|
|
logourl?: string; // The site logo URL.
|
|
|
|
compactlogourl?: string; // The site compact logo URL.
|
|
|
|
typeoflogin: number; // The type of login. 1 for app, 2 for browser, 3 for embedded.
|
|
|
|
launchurl?: string; // SSO login launch URL.
|
|
|
|
mobilecssurl?: string; // Mobile custom CSS theme.
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
|
|
tool_mobile_disabledfeatures?: string; // Disabled features in the app.
|
2020-10-19 06:55:46 +00:00
|
|
|
identityproviders?: CoreSiteIdentityProvider[]; // Identity providers.
|
2020-10-14 06:28:02 +00:00
|
|
|
country?: string; // Default site country.
|
|
|
|
agedigitalconsentverification?: boolean; // Whether age digital consent verification is enabled.
|
|
|
|
supportname?: string; // Site support contact name (only if age verification is enabled).
|
|
|
|
supportemail?: string; // Site support contact email (only if age verification is enabled).
|
|
|
|
autolang?: number; // Whether to detect default language from browser setting.
|
|
|
|
lang?: string; // Default language for the site.
|
|
|
|
langmenu?: number; // Whether the language menu should be displayed.
|
|
|
|
langlist?: string; // Languages on language menu.
|
|
|
|
locale?: string; // Sitewide locale.
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
|
|
tool_mobile_minimumversion?: string; // Minimum required version to access.
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
|
|
tool_mobile_iosappid?: string; // IOS app's unique identifier.
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
|
|
tool_mobile_androidappid?: string; // Android app's unique identifier.
|
|
|
|
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
|
|
tool_mobile_setuplink?: string; // App download page.
|
2021-09-15 09:45:48 +00:00
|
|
|
tool_mobile_qrcodetype?: CoreSiteQRCodeType; // eslint-disable-line @typescript-eslint/naming-convention
|
2020-10-14 06:28:02 +00:00
|
|
|
warnings?: CoreWSExternalWarning[];
|
|
|
|
};
|
|
|
|
|
2020-10-19 06:55:46 +00:00
|
|
|
/**
|
|
|
|
* Identity provider.
|
|
|
|
*/
|
|
|
|
export type CoreSiteIdentityProvider = {
|
|
|
|
name: string; // The identity provider name.
|
|
|
|
iconurl: string; // The icon URL for the provider.
|
|
|
|
url: string; // The URL of the provider.
|
|
|
|
};
|
|
|
|
|
2020-10-14 06:28:02 +00:00
|
|
|
/**
|
|
|
|
* Result of WS tool_mobile_get_autologin_key.
|
|
|
|
*/
|
|
|
|
export type CoreSiteAutologinKeyResult = {
|
|
|
|
key: string; // Auto-login key for a single usage with time expiration.
|
|
|
|
autologinurl: string; // Auto-login URL.
|
|
|
|
warnings?: CoreWSExternalWarning[];
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Result of WS tool_mobile_call_external_functions.
|
|
|
|
*/
|
|
|
|
export type CoreSiteCallExternalFunctionsResult = {
|
|
|
|
responses: {
|
|
|
|
error: boolean; // Whether an exception was thrown.
|
|
|
|
data?: string; // JSON-encoded response data.
|
|
|
|
exception?: string; // JSON-encoed exception info.
|
|
|
|
}[];
|
|
|
|
};
|
2020-10-14 14:38:24 +00:00
|
|
|
|
2022-03-09 13:38:00 +00:00
|
|
|
/**
|
|
|
|
* Options for storeLastViewed.
|
|
|
|
*/
|
|
|
|
export type CoreSiteStoreLastViewedOptions = {
|
|
|
|
data?: string; // Other data.
|
|
|
|
timeaccess?: number; // Accessed time. If not set, current time.
|
|
|
|
};
|
2022-06-21 12:50:08 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Info about cached data.
|
|
|
|
*/
|
|
|
|
type WSCachedData<T> = {
|
|
|
|
response: T | WSCachedError; // The WS response data, or an error if the WS returned an error and it was cached.
|
|
|
|
expirationIgnored: boolean; // Whether the expiration time was ignored.
|
|
|
|
expirationTime?: number; // Entry expiration time (only if not ignored).
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Error data stored in cache.
|
|
|
|
*/
|
|
|
|
type WSCachedError = {
|
|
|
|
exception?: string;
|
|
|
|
errorcode?: string;
|
|
|
|
};
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Observable returned when calling WebServices.
|
|
|
|
* If the request uses the "update in background" feature, it will return 2 values: first the cached one, and then the one
|
|
|
|
* coming from the server. After this, it will complete.
|
|
|
|
* Otherwise, it will only return 1 value, either coming from cache or from the server. After this, it will complete.
|
|
|
|
*/
|
|
|
|
export type WSObservable<T> = Observable<T>;
|