1390 lines
52 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// (C) Copyright 2015 Martin Dougiamas
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injector } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { HttpClient } from '@angular/common/http';
import { SQLiteDB } from './sqlitedb';
import { CoreAppProvider } from '../providers/app';
import { CoreDbProvider } from '../providers/db';
import { CoreEventsProvider } from '../providers/events';
import { CoreFileProvider } from '../providers/file';
import { CoreLoggerProvider } from '../providers/logger';
import { CoreWSProvider, CoreWSPreSets, CoreWSFileUploadOptions } from '../providers/ws';
import { CoreDomUtilsProvider } from '../providers/utils/dom';
import { CoreTextUtilsProvider } from '../providers/utils/text';
import { CoreTimeUtilsProvider } from '../providers/utils/time';
import { CoreUrlUtilsProvider } from '../providers/utils/url';
import { CoreUtilsProvider } from '../providers/utils/utils';
import { CoreConstants } from '../core/constants';
import { CoreConfigConstants } from '../configconstants';
import { Md5 } from 'ts-md5/dist/md5';
export interface CoreSiteWSPreSets {
getFromCache?: boolean; // Get the value from the cache if it's still valid.
saveToCache?: boolean; // Save the result to the cache.
omitExpires?: boolean; // Ignore cache expiration.
emergencyCache?: boolean; // Use the cache when a request fails. Defaults to true.
cacheKey?: string; // Extra key to add to the cache when storing this call, to identify the entry.
getCacheUsingCacheKey?: boolean; // Whether it should use cache key to retrieve the cached data instead of the request params.
getEmergencyCacheUsingCacheKey?: boolean; // Same as getCacheUsingCacheKey, but for emergency cache.
uniqueCacheKey?: boolean; // Whether it should only be 1 entry for this cache key (all entries with same key will be deleted).
filter?: boolean; // Whether to filter WS response (moodlewssettingfilter). Defaults to true.
rewriteurls?: boolean; // Whether to rewrite URLs (moodlewssettingfileurl). Defaults to true.
responseExpected?: boolean; // Defaults to true. Set to false when the expected response is null.
typeExpected?: string; // Defaults to 'object'. Use it when you expect a type that's not an object|array.
};
export interface LocalMobileResponse {
code: number; // Code to identify the authentication method to use.
service?: string; // Name of the service to use.
warning?: string; // Code of the warning message.
coreSupported?: boolean; // Whether core SSO is supported.
}
/**
* Class that represents a site (combination of site + user).
* It will have all the site data and provide utility functions regarding a site.
* To add tables to the site's database, please use CoreSitesProvider.createTablesFromSchema. This will make sure that
* the tables are created in all the sites, not just the current one.
*/
export class CoreSite {
// List of injected services. This class isn't injectable, so it cannot use DI.
protected appProvider;
protected dbProvider;
protected domUtils;
protected eventsProvider;
protected fileProvider;
protected http;
protected textUtils;
protected timeUtils;
protected translate;
protected utils;
protected urlUtils;
protected wsProvider;
// Variables for the database.
protected WS_CACHE_TABLE = 'wscache';
protected tableSchema = {
name: this.WS_CACHE_TABLE,
columns: [
{
name: 'id',
type: 'TEXT',
primaryKey: true
},
{
name: 'data',
type: 'TEXT'
},
{
name: 'key',
type: 'TEXT'
},
{
name: 'expirationTime',
type: 'INTEGER'
}
]
};
// Rest of variables.
protected logger;
protected db: SQLiteDB;
protected cleanUnicode = false;
protected lastAutoLogin = 0;
protected moodleReleases = {
'3.1': 2016052300,
'3.2': 2016120500,
'3.3': 2017051503,
'3.4': 2017111300
};
protected deprecatedFunctions = {
"core_grade_get_definitions": "core_grading_get_definitions",
"moodle_course_create_courses": "core_course_create_courses",
"moodle_course_get_courses": "core_course_get_courses",
"moodle_enrol_get_users_courses": "core_enrol_get_users_courses",
"moodle_file_get_files": "core_files_get_files",
"moodle_file_upload": "core_files_upload",
"moodle_group_add_groupmembers": "core_group_add_group_members",
"moodle_group_create_groups": "core_group_create_groups",
"moodle_group_delete_groupmembers": "core_group_delete_group_members",
"moodle_group_delete_groups": "core_group_delete_groups",
"moodle_group_get_course_groups": "core_group_get_course_groups",
"moodle_group_get_groupmembers": "core_group_get_group_members",
"moodle_group_get_groups": "core_group_get_groups",
"moodle_message_send_instantmessages": "core_message_send_instant_messages",
"moodle_notes_create_notes": "core_notes_create_notes",
"moodle_role_assign": "core_role_assign_role",
"moodle_role_unassign": "core_role_unassign_role",
"moodle_user_create_users": "core_user_create_users",
"moodle_user_delete_users": "core_user_delete_users",
"moodle_user_get_course_participants_by_id": "core_user_get_course_user_profiles",
"moodle_user_get_users_by_courseid": "core_enrol_get_enrolled_users",
// Both *_user_get_users_by_id are deprecated, but there is no equivalent available in the Mobile service.
"moodle_user_get_users_by_id": "core_user_get_users_by_id",
"moodle_user_update_users": "core_user_update_users",
"moodle_webservice_get_siteinfo": "core_webservice_get_site_info",
};
/**
* Create a site.
*
* @param {Injector} injector Angular injector to prevent having to pass all the required services.
* @param {string} id Site ID.
* @param {string} siteUrl Site URL.
* @param {string} [token] Site's WS token.
* @param {any} [info] Site info.
* @param {string} [privateToken] Private token.
* @param {any} [config] Site public config.
* @param {boolean} [loggedOut] Whether user is logged out.
*/
constructor(injector: Injector, public id: string, public siteUrl: string, public token?: string, public infos?: any,
public privateToken?: string, public config?: any, public loggedOut?: boolean) {
// Inject the required services.
let logger = injector.get(CoreLoggerProvider);
this.appProvider = injector.get(CoreAppProvider);
this.dbProvider = injector.get(CoreDbProvider);
this.domUtils = injector.get(CoreDomUtilsProvider);
this.eventsProvider = injector.get(CoreEventsProvider);
this.fileProvider = injector.get(CoreFileProvider);
this.http = injector.get(HttpClient);
this.textUtils = injector.get(CoreTextUtilsProvider);
this.timeUtils = injector.get(CoreTimeUtilsProvider);
this.translate = injector.get(TranslateService);
this.utils = injector.get(CoreUtilsProvider);
this.urlUtils = injector.get(CoreUrlUtilsProvider);
this.wsProvider = injector.get(CoreWSProvider);
this.logger = logger.getInstance('CoreWSProvider');
if (this.id) {
this.initDB();
}
}
/**
* Initialize the database.
*/
initDB() : void {
this.db = this.dbProvider.getDB('Site-' + this.id);
this.db.createTableFromSchema(this.tableSchema);
}
/**
* Get site ID.
*
* @return {string} Site ID.
*/
getId() : string {
return this.id;
}
/**
* Get site URL.
*
* @return {string} Site URL.
*/
getURL() : string {
return this.siteUrl;
}
/**
* Get site token.
*
* @return {string} Site token.
*/
getToken() : string {
return this.token;
}
/**
* Get site info.
*
* @return {any} Site info.
*/
getInfo() : any {
return this.infos;
}
/**
* Get site private token.
*
* @return {string} Site private token.
*/
getPrivateToken() : string {
return this.privateToken;
}
/**
* Get site DB.
*
* @return {SQLiteDB} Site DB.
*/
getDb() : SQLiteDB {
return this.db;
}
/**
* Get site user's ID.
*
* @return {number} User's ID.
*/
getUserId() : number {
if (typeof this.infos != 'undefined' && typeof this.infos.userid != 'undefined') {
return this.infos.userid;
}
}
/**
* Get site Course ID for frontpage course. If not declared it will return 1 as default.
*
* @return {number} Site Home ID.
*/
getSiteHomeId() : number {
return this.infos && this.infos.siteid || 1;
}
/**
* Set site ID.
*
* @param {string} New ID.
*/
setId(id: string) : void {
this.id = id;
this.initDB();
}
/**
* Set site token.
*
* @param {string} New token.
*/
setToken(token: string) : void {
this.token = token;
}
/**
* Set site private token.
*
* @param {string} 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 {boolean} Whether is logged out.
*/
isLoggedOut() : boolean {
return !!this.loggedOut;
}
/**
* Set site info.
*
* @param {any} New info.
*/
setInfo(infos: any) : void {
this.infos = infos;
}
/**
* Set site config.
*
* @param {any} Config.
*/
setConfig(config: any) : void {
this.config = config;
}
/**
* Set site logged out.
*
* @param {boolean} loggedOut True if logged out and needs to authenticate again, false otherwise.
*/
setLoggedOut(loggedOut: boolean) : void {
this.loggedOut = !!loggedOut;
}
/**
* Can the user access their private files?
*
* @return {boolean} Whether can access my files.
*/
canAccessMyFiles() : boolean {
const infos = this.getInfo();
return infos && (typeof infos.usercanmanageownfiles === 'undefined' || infos.usercanmanageownfiles);
}
/**
* Can the user download files?
*
* @return {boolean} Whether can download files.
*/
canDownloadFiles() : boolean {
const infos = this.getInfo();
return infos && infos.downloadfiles;
}
/**
* Can the user use an advanced feature?
*
* @param {string} feature The name of the feature.
* @param {boolean} [whenUndefined=true] The value to return when the parameter is undefined.
* @return {boolean} Whether can use advanced feature.
*/
canUseAdvancedFeature(feature: string, whenUndefined = true) : boolean {
let infos = this.getInfo(),
canUse = true;
if (typeof infos.advancedfeatures === 'undefined') {
canUse = whenUndefined;
} else {
for (let i in infos.advancedfeatures) {
let item = infos.advancedfeatures[i];
if (item.name === feature && parseInt(item.value, 10) === 0) {
canUse = false;
}
}
}
return canUse;
}
/**
* Can the user upload files?
*
* @return {boolean} Whether can upload files.
*/
canUploadFiles() : boolean {
const infos = this.getInfo();
return infos && infos.uploadfiles;
}
/**
* Fetch site info from the Moodle site.
*
* @return {Promise<any>} A promise to be resolved when the site info is retrieved.
*/
fetchSiteInfo() : Promise<any> {
// get_site_info won't be cached.
let preSets = {
getFromCache: false,
saveToCache: false
}
// 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 {string} method WS method to use.
* @param {any} data Data to send to the WS.
* @param {CoreSiteWSPreSets} [preSets] Extra options.
* @return {Promise<any>} Promise resolved with the response, rejected with CoreWSError if it fails.
*/
read(method: string, data: any, preSets?: CoreSiteWSPreSets) : Promise<any> {
preSets = preSets || {};
if (typeof preSets.getFromCache == 'undefined') {
preSets.getFromCache = true;
}
if (typeof preSets.saveToCache == 'undefined') {
preSets.saveToCache = true;
}
return this.request(method, data, preSets);
}
/**
* Sends some data to the Moodle site using WS. Requests are NOT cached by default.
*
* @param {string} method WS method to use.
* @param {any} data Data to send to the WS.
* @param {CoreSiteWSPreSets} [preSets] Extra options.
* @return {Promise<any>} Promise resolved with the response, rejected with CoreWSError if it fails.
*/
write(method: string, data: any, preSets?: CoreSiteWSPreSets) : Promise<any> {
preSets = preSets || {};
if (typeof preSets.getFromCache == 'undefined') {
preSets.getFromCache = false;
}
if (typeof preSets.saveToCache == 'undefined') {
preSets.saveToCache = false;
}
if (typeof preSets.emergencyCache == 'undefined') {
preSets.emergencyCache = false;
}
return this.request(method, data, preSets);
}
/**
* WS request to the site.
*
* @param {string} method The WebService method to be called.
* @param {any} data Arguments to pass to the method.
* @param {CoreSiteWSPreSets} preSets Extra options.
* @param {boolean} [retrying] True if we're retrying the call for some reason. This is to prevent infinite loops.
* @return {Promise<any>} Promise resolved with the response, rejected with CoreWSError if it fails.
* @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.
*
* This method is smart which means that it will try to map the method to a compatibility one if need be, usually this
* means that it will fallback on the 'local_mobile_' prefixed function if it is available and the non-prefixed is not.
*/
request(method: string, data: any, preSets: CoreSiteWSPreSets, retrying?: boolean) : Promise<any> {
let initialToken = this.token;
data = data || {};
// Get the method to use based on the available ones.
method = this.getCompatibleFunction(method);
// Check if the method is available, use a prefixed version if possible.
// We ignore this check when we do not have the site info, as the list of functions is not loaded yet.
if (this.getInfo() && !this.wsAvailable(method, false)) {
const compatibilityMethod = CoreConstants.wsPrefix + method;
if (this.wsAvailable(compatibilityMethod, false)) {
this.logger.info(`Using compatibility WS method '${compatibilityMethod}'`);
method = compatibilityMethod;
} else {
this.logger.error(`WS function '${method}' is not available, even in compatibility mode.`);
return Promise.reject(this.wsProvider.createFakeWSError('mm.core.wsfunctionnotavailable', true));
}
}
let wsPreSets: CoreWSPreSets = {
wsToken: this.token,
siteUrl: this.siteUrl,
cleanUnicode: this.cleanUnicode,
typeExpected: preSets.typeExpected,
responseExpected: preSets.responseExpected
};
if (wsPreSets.cleanUnicode && this.textUtils.hasUnicodeData(data)) {
// Data will be cleaned, notify the user.
this.domUtils.showToast('mm.core.unicodenotsupported', true, 3000);
} else {
// No need to clean data in this call.
wsPreSets.cleanUnicode = false;
}
// Enable text filtering by default.
data.moodlewssettingfilter = preSets.filter === false ? false : true;
data.moodlewssettingfileurl = preSets.rewriteurls === false ? false : true;
// Convert the values to string before starting the cache process.
try {
data = this.wsProvider.convertValuesToString(data, wsPreSets.cleanUnicode);
} catch (e) {
// Empty cleaned text found.
return Promise.reject(this.wsProvider.createFakeWSError('mm.core.unicodenotsupportedcleanerror', true));
}
return this.getFromCache(method, data, preSets).catch(() => {
// Do not pass those options to the core WS factory.
return this.wsProvider.call(method, data, wsPreSets).then((response) => {
if (preSets.saveToCache) {
this.saveToCache(method, data, response, preSets);
}
// We pass back a clone of the original object, this may
// prevent errors if in the callback the object is modified.
return Object.assign({}, response);
}).catch((error) => {
if (error.errorcode == 'invalidtoken' ||
(error.errorcode == 'accessexception' && error.message.indexOf('Invalid token - token expired') > -1)) {
if (initialToken !== this.token && !retrying) {
// Token has changed, retry with the new token.
return this.request(method, data, preSets, true);
} else if (this.appProvider.isSSOAuthenticationOngoing()) {
// There's an SSO authentication ongoing, wait for it to finish and try again.
return this.appProvider.waitForSSOAuthentication().then(() => {
return this.request(method, data, preSets, true);
});
}
// Session expired, trigger event.
this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, {siteId: this.id});
// Change error message. We'll try to get data from cache.
error.message = this.translate.instant('mm.core.lostconnection');
} else if (error.errorcode === 'userdeleted') {
// User deleted, trigger event.
this.eventsProvider.trigger(CoreEventsProvider.USER_DELETED, {siteId: this.id, params: data});
error.message = this.translate.instant('mm.core.userdeleted');
return Promise.reject(error);
} else if (error.errorcode === 'forcepasswordchangenotice') {
// Password Change Forced, trigger event.
this.eventsProvider.trigger(CoreEventsProvider.PASSWORD_CHANGE_FORCED, {siteId: this.id});
error.message = this.translate.instant('mm.core.forcepasswordchangenotice');
return Promise.reject(error);
} else if (error.errorcode === 'usernotfullysetup') {
// User not fully setup, trigger event.
this.eventsProvider.trigger(CoreEventsProvider.USER_NOT_FULLY_SETUP, {siteId: this.id});
error.message = this.translate.instant('mm.core.usernotfullysetup');
return Promise.reject(error);
} else if (error.errorcode === 'sitepolicynotagreed') {
// Site policy not agreed, trigger event.
this.eventsProvider.trigger(CoreEventsProvider.SITE_POLICY_NOT_AGREED, {siteId: this.id});
error.message = this.translate.instant('mm.core.sitepolicynotagreederror');
return Promise.reject(error);
} else if (error.errorcode === 'dmlwriteexception' && this.textUtils.hasUnicodeData(data)) {
if (!this.cleanUnicode) {
// Try again cleaning unicode.
this.cleanUnicode = true;
return this.request(method, data, preSets);
}
// This should not happen.
error.message = this.translate.instant('mm.core.unicodenotsupported');
return Promise.reject(error);
} else if (typeof preSets.emergencyCache !== 'undefined' && !preSets.emergencyCache) {
this.logger.debug(`WS call '${method}' failed. Emergency cache is forbidden, rejecting.`);
return Promise.reject(error);
}
this.logger.debug(`WS call '${method}' failed. Trying to use the emergency cache.`);
preSets.omitExpires = true;
preSets.getFromCache = true;
return this.getFromCache(method, data, preSets, true).catch(() => {
return Promise.reject(error);
});
});
});
}
/**
* Check if a WS is available in this site.
*
* @param {string} method WS name.
* @param {boolean} [checkPrefix=true] When true also checks with the compatibility prefix.
* @return {boolean} Whether the WS is available.
*/
wsAvailable(method: string, checkPrefix = true) : boolean {
if (typeof this.infos == 'undefined') {
return false;
}
for (let i = 0; i < this.infos.functions.length; i++) {
const func = this.infos.functions[i];
if (func.name == method) {
return true;
}
}
// Let's try again with the compatibility prefix.
if (checkPrefix) {
return this.wsAvailable(CoreConstants.wsPrefix + method, false);
}
return false;
}
/**
* Get cache ID.
*
* @param {string} method The WebService method.
* @param {any} data Arguments to pass to the method.
* @return {string} Cache ID.
*/
protected getCacheId(method: string, data: any) : string {
return <string>Md5.hashAsciiStr(method + ':' + this.utils.sortAndStringify(data));
}
/**
* Get a WS response from cache.
*
* @param {string} method The WebService method to be called.
* @param {any} data Arguments to pass to the method.
* @param {CoreSiteWSPreSets} preSets Extra options.
* @param {boolean} emergency Whether it's an "emergency" cache call (WS call failed).
* @return {Promise<any>} Promise resolved with the WS response.
*/
protected getFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, emergency?: boolean) : Promise<any> {
let id = this.getCacheId(method, data),
promise;
if (!this.db || !preSets.getFromCache) {
return Promise.reject(null);
}
if (preSets.getCacheUsingCacheKey || (emergency && preSets.getEmergencyCacheUsingCacheKey)) {
promise = this.db.getRecords(this.WS_CACHE_TABLE, {key: preSets.cacheKey}).then((entries) => {
if (!entries.length) {
// Cache key not found, get by params sent.
return this.db.getRecord(this.WS_CACHE_TABLE, {id: id});
} else if (entries.length > 1) {
// More than one entry found. Search the one with same ID as this call.
for (let i = 0, len = entries.length; i < len; i++) {
let entry = entries[i];
if (entry.id == id) {
return entry;
}
}
}
return entries[0];
});
} else {
promise = this.db.getRecord(this.WS_CACHE_TABLE, {id: id});
}
return promise.then((entry) => {
const now = Date.now();
preSets.omitExpires = preSets.omitExpires || !this.appProvider.isOnline();
if (!preSets.omitExpires) {
if (now > entry.expirationTime) {
this.logger.debug('Cached element found, but it is expired');
return Promise.reject(null);
}
}
if (typeof entry != 'undefined' && typeof entry.data != 'undefined') {
const expires = (entry.expirationTime - now) / 1000;
this.logger.info(`Cached element found, id: ${id} expires in ${expires} seconds`);
return JSON.parse(entry.data);
}
return Promise.reject(null);
});
}
/**
* Save a WS response to cache.
*
* @param {string} method The WebService method.
* @param {any} data Arguments to pass to the method.
* @param {any} response The WS response.
* @param {CoreSiteWSPreSets} preSets Extra options.
* @return {Promise<any>} Promise resolved when the response is saved.
*/
protected saveToCache(method: string, data: any, response: any, preSets: CoreSiteWSPreSets) : Promise<any> {
let id = this.getCacheId(method, data),
cacheExpirationTime = CoreConfigConstants.cache_expiration_time,
promise,
entry: any = {
id: id,
data: JSON.stringify(response)
}
if (!this.db) {
return Promise.reject(null);
} else {
if (preSets.uniqueCacheKey) {
// Cache key must be unique, delete all entries with same cache key.
promise = this.deleteFromCache(method, data, preSets, true).catch(() => {
// Ignore errors.
});
} else {
promise = Promise.resolve();
}
return promise.then(() => {
cacheExpirationTime = isNaN(cacheExpirationTime) ? 300000 : cacheExpirationTime;
entry.expirationTime = new Date().getTime() + cacheExpirationTime;
if (preSets.cacheKey) {
entry.key = preSets.cacheKey;
}
return this.db.insertOrUpdateRecord(this.WS_CACHE_TABLE, entry, {id: id});
});
}
}
/**
* Delete a WS cache entry or entries.
*
* @param {string} method The WebService method to be called.
* @param {any} data Arguments to pass to the method.
* @param {CoreSiteWSPreSets} preSets Extra options.
* @param {boolean} [allCacheKey] True to delete all entries with the cache key, false to delete only by ID.
* @return {Promise<any>} Promise resolved when the entries are deleted.
*/
protected deleteFromCache(method: string, data: any, preSets: CoreSiteWSPreSets, allCacheKey?: boolean) : Promise<any> {
const id = this.getCacheId(method, data);
if (!this.db) {
return Promise.reject(null);
} else {
if (allCacheKey) {
return this.db.deleteRecords(this.WS_CACHE_TABLE, {key: preSets.cacheKey});
} else {
return this.db.deleteRecords(this.WS_CACHE_TABLE, {id: id});
}
}
}
/*
* Uploads a file using Cordova File API.
*
* @param {string} filePath File path.
* @param {CoreWSFileUploadOptions} options File upload options.
* @return {Promise<any>} Promise resolved when uploaded.
*/
uploadFile(filePath: string, options: CoreWSFileUploadOptions) : Promise<any> {
if (!options.fileArea) {
options.fileArea = 'draft';
}
return this.wsProvider.uploadFile(filePath, options, {
siteUrl: this.siteUrl,
wsToken: this.token
});
}
/**
* Invalidates all the cache entries.
*
* @return {Promise<any>} Promise resolved when the cache entries are invalidated.
*/
invalidateWsCache() : Promise<any> {
if (!this.db) {
return Promise.reject(null);
}
this.logger.debug('Invalidate all the cache for site: ' + this.id);
return this.db.updateRecords(this.WS_CACHE_TABLE, {expirationTime: 0});
}
/**
* Invalidates all the cache entries with a certain key.
*
* @param {string} key Key to search.
* @return {Promise<any>} Promise resolved when the cache entries are invalidated.
*/
invalidateWsCacheForKey(key: string) : Promise<any> {
if (!this.db) {
return Promise.reject(null);
}
if (!key) {
return Promise.resolve();
}
this.logger.debug('Invalidate cache for key: ' + key);
return this.db.updateRecords(this.WS_CACHE_TABLE, {expirationTime: 0}, {key: key});
}
/**
* Invalidates all the cache entries in an array of keys.
*
* @param {string[]} keys Keys to search.
* @return {Promise<any>} Promise resolved when the cache entries are invalidated.
*/
invalidateMultipleWsCacheForKey(keys: string[]) : Promise<any> {
if (!this.db) {
return Promise.reject(null);
}
if (!keys || !keys.length) {
return Promise.resolve();
}
let promises = [];
this.logger.debug('Invalidating multiple cache keys');
keys.forEach((key) => {
promises.push(this.invalidateWsCacheForKey(key));
});
return Promise.all(promises);
}
/**
* Invalidates all the cache entries whose key starts with a certain value.
*
* @param {string} key Key to search.
* @return {Promise} Promise resolved when the cache entries are invalidated.
*/
invalidateWsCacheForKeyStartingWith(key: string) : Promise<any> {
if (!this.db) {
return Promise.reject(null);
}
if (!key) {
return Promise.resolve();
}
this.logger.debug('Invalidate cache for key starting with: ' + key);
let sql = 'UPDATE ' + this.WS_CACHE_TABLE + ' SET expirationTime=0 WHERE key LIKE ?%';
return this.db.execute(sql, [key]);
}
/**
* Generic function for adding the wstoken to Moodle urls and for pointing to the correct script.
* Uses CoreUtilsProvider.fixPluginfileURL, passing site's token.
*
* @param {string} url The url to be fixed.
* @return {string} Fixed URL.
*/
fixPluginfileURL(url: string) : string {
return this.urlUtils.fixPluginfileURL(url, this.token);
}
/**
* Deletes site's DB.
*
* @return {Promise<any>} Promise to be resolved when the DB is deleted.
*/
deleteDB() : Promise<any> {
return this.dbProvider.deleteDB('Site-' + this.id);
}
/**
* Deletes site's folder.
*
* @return {Promise<any>} Promise to be resolved when the DB is deleted.
*/
deleteFolder() : Promise<any> {
if (this.fileProvider.isAvailable()) {
const siteFolder = this.fileProvider.getSiteFolder(this.id);
return this.fileProvider.removeDir(siteFolder).catch(() => {
// Ignore any errors, $mmFS.removeDir fails if folder doesn't exists.
});
} else {
return Promise.resolve();
}
}
/**
* Get space usage of the site.
*
* @return {Promise<number>} Promise resolved with the site space usage (size).
*/
getSpaceUsage() : Promise<number> {
if (this.fileProvider.isAvailable()) {
const siteFolderPath = this.fileProvider.getSiteFolder(this.id);
return this.fileProvider.getDirectorySize(siteFolderPath).catch(() => {
return 0;
});
} else {
return Promise.resolve(0);
}
}
/**
* Returns the URL to the documentation of the app, based on Moodle version and current language.
*
* @param {string} [page] Docs page to go to.
* @return {Promise} Promise resolved with the Moodle docs URL.
*/
getDocsUrl(page: string) : Promise<string> {
const release = this.infos.release ? this.infos.release : undefined;
return this.urlUtils.getDocsUrl(release, page);
}
/**
* Check if the local_mobile plugin is installed in the Moodle site.
*
* @param {boolean} [retrying] True if we're retrying the check.
* @return {Promise<LocalMobileResponse>} Promise resolved when the check is done.
*/
checkLocalMobilePlugin(retrying?: boolean) : Promise<LocalMobileResponse> {
const checkUrl = this.siteUrl + '/local/mobile/check.php',
service = CoreConfigConstants.wsextservice;
if (!service) {
// External service not defined.
return Promise.resolve({code: 0});
}
let observable = this.http.post(checkUrl, {service: service}).timeout(CoreConstants.wsTimeout);
return this.utils.observableToPromise(observable).then((data: any) => {
if (typeof data != 'undefined' && data.errorcode === 'requirecorrectaccess') {
if (!retrying) {
this.siteUrl = this.urlUtils.addOrRemoveWWW(this.siteUrl);
return this.checkLocalMobilePlugin(true);
} else {
return Promise.reject(data.error);
}
} else if (typeof data == 'undefined' || typeof data.code == 'undefined') {
// local_mobile returned something we didn't expect. Let's assume it's not installed.
return {code: 0, warning: 'mm.login.localmobileunexpectedresponse'};
}
const code = parseInt(data.code, 10);
if (data.error) {
switch (code) {
case 1:
// Site in maintenance mode.
return Promise.reject(this.translate.instant('mm.login.siteinmaintenance'));
case 2:
// Web services not enabled.
return Promise.reject(this.translate.instant('mm.login.webservicesnotenabled'));
case 3:
// Extended service not enabled, but the official is enabled.
return {code: 0};
case 4:
// Neither extended or official services enabled.
return Promise.reject(this.translate.instant('mm.login.mobileservicesnotenabled'));
default:
return Promise.reject(this.translate.instant('mm.core.unexpectederror'));
}
} else {
return {code: code, service: service, coresupported: !!data.coresupported};
}
}, () => {
return {code: 0};
});
}
/**
* Check if local_mobile has been installed in Moodle.
*
* @return {boolean} Whether the App is able to use local_mobile plugin for this site.
*/
checkIfAppUsesLocalMobile() : boolean {
let appUsesLocalMobile = false;
if (!this.infos || !this.infos.functions) {
return appUsesLocalMobile;
}
this.infos.functions.forEach((func) => {
if (func.name.indexOf(CoreConstants.wsPrefix) != -1) {
appUsesLocalMobile = true;
}
});
return appUsesLocalMobile;
}
/**
* Check if local_mobile has been installed in Moodle but the app is not using it.
*
* @return {Promise<any>} Promise resolved it local_mobile was added, rejected otherwise.
*/
checkIfLocalMobileInstalledAndNotUsed() : Promise<any> {
const appUsesLocalMobile = this.checkIfAppUsesLocalMobile();
if (appUsesLocalMobile) {
// App already uses local_mobile, it wasn't added.
return Promise.reject(null);
}
return this.checkLocalMobilePlugin().then((data: LocalMobileResponse) : any => {
if (typeof data.service == 'undefined') {
// local_mobile NOT installed. Reject.
return Promise.reject(null);
}
return data;
});
}
/**
* Check if a URL belongs to this site.
*
* @param {string} url URL to check.
* @return {boolean} Whether the URL belongs to this site.
*/
containsUrl(url: string) : boolean {
if (!url) {
return false;
}
const siteUrl = this.urlUtils.removeProtocolAndWWW(this.siteUrl);
url = this.urlUtils.removeProtocolAndWWW(url);
return url.indexOf(siteUrl) == 0;
}
/**
* Get the public config of this site.
*
* @return {Promise<any>} Promise resolved with public config. Rejected with an object if error, see CoreWSProvider.callAjax.
*/
getPublicConfig() : Promise<any> {
return this.wsProvider.callAjax('tool_mobile_get_public_config', {}, {siteUrl: this.siteUrl}).then((config) => {
// Use the wwwroot returned by the server.
if (config.httpswwwroot) {
this.siteUrl = config.httpswwwroot;
}
return config;
});
}
/**
* Open a URL in browser using auto-login in the Moodle site if available.
*
* @param {string} url The URL to open.
* @param {string} [alertMessage] If defined, an alert will be shown before opening the browser.
* @return {Promise<any>} Promise resolved when done, rejected otherwise.
*/
openInBrowserWithAutoLogin(url: string, alertMessage?: string) : Promise<any> {
return this.openWithAutoLogin(false, url, undefined, alertMessage);
}
/**
* Open a URL in browser using auto-login in the Moodle site if available and the URL belongs to the site.
*
* @param {string} url The URL to open.
* @param {string} [alertMessage] If defined, an alert will be shown before opening the browser.
* @return {Promise<any>} Promise resolved when done, rejected otherwise.
*/
openInBrowserWithAutoLoginIfSameSite(url: string, alertMessage?: string) : Promise<any> {
return this.openWithAutoLoginIfSameSite(false, url, undefined, alertMessage);
}
/**
* Open a URL in inappbrowser using auto-login in the Moodle site if available.
*
* @param {string} url The URL to open.
* @param {any} [options] Override default options passed to InAppBrowser.
* @param {string} [alertMessage] If defined, an alert will be shown before opening the inappbrowser.
* @return {Promise<any>} Promise resolved when done, rejected otherwise.
*/
openInAppWithAutoLogin(url: string, options?: any, alertMessage?: string) : Promise<any> {
return this.openWithAutoLogin(true, url, options, alertMessage);
}
/**
* Open a URL in inappbrowser using auto-login in the Moodle site if available and the URL belongs to the site.
*
* @param {string} url The URL to open.
* @param {object} [options] Override default options passed to inappbrowser.
* @param {string} [alertMessage] If defined, an alert will be shown before opening the inappbrowser.
* @return {Promise<any>} Promise resolved when done, rejected otherwise.
*/
openInAppWithAutoLoginIfSameSite(url: string, options: any, alertMessage?: string) : Promise<any> {
return this.openWithAutoLoginIfSameSite(true, url, options, alertMessage);
}
/**
* Open a URL in browser or InAppBrowser using auto-login in the Moodle site if available.
*
* @param {boolean} inApp True to open it in InAppBrowser, false to open in browser.
* @param {string} url The URL to open.
* @param {object} [options] Override default options passed to $cordovaInAppBrowser#open.
* @param {string} [alertMessage] If defined, an alert will be shown before opening the browser/inappbrowser.
* @return {Promise<any>} Promise resolved when done, rejected otherwise.
*/
openWithAutoLogin(inApp: boolean, url: string, options?: any, alertMessage?: string) : Promise<any> {
// Convenience function to open the URL.
let open = (url) => {
if (modal) {
modal.dismiss();
}
let promise;
if (alertMessage) {
promise = this.domUtils.showAlert('mm.core.notice', alertMessage, null, 3000);
} else {
promise = Promise.resolve();
}
return promise.finally(() => {
if (inApp) {
this.utils.openInApp(url, options);
} else {
this.utils.openInBrowser(url);
}
});
};
if (!this.privateToken || !this.wsAvailable('tool_mobile_get_autologin_key') ||
(this.lastAutoLogin && this.timeUtils.timestamp() - this.lastAutoLogin < 6 * CoreConstants.secondsMinute)) {
// No private token, WS not available or last auto-login was less than 6 minutes ago.
// Open the final URL without auto-login.
return open(url);
}
const userId = this.getUserId(),
params = {
privatetoken: this.privateToken
},
modal = this.domUtils.showModalLoading();
// Use write to not use cache.
return this.write('tool_mobile_get_autologin_key', params).then((data) => {
if (!data.autologinurl || !data.key) {
// Not valid data, open the final URL without auto-login.
return open(url);
}
this.lastAutoLogin = this.timeUtils.timestamp();
return open(data.autologinurl + '?userid=' + userId + '&key=' + data.key + '&urltogo=' + url);
}).catch(() => {
// Couldn't get autologin key, open the final URL without auto-login.
return open(url);
});
}
/**
* Open a URL in browser or InAppBrowser using auto-login in the Moodle site if available and the URL belongs to the site.
*
* @param {boolean} inApp True to open it in InAppBrowser, false to open in browser.
* @param {string} url The URL to open.
* @param {object} [options] Override default options passed to inappbrowser.
* @param {string} [alertMessage] If defined, an alert will be shown before opening the browser/inappbrowser.
* @return {Promise<any>} Promise resolved when done, rejected otherwise.
*/
openWithAutoLoginIfSameSite(inApp: boolean, url: string, options?: any, alertMessage?: string) : Promise<any> {
if (this.containsUrl(url)) {
return this.openWithAutoLogin(inApp, url, options, alertMessage);
} else {
if (inApp) {
this.utils.openInApp(url, options);
} else {
this.utils.openInBrowser(url);
}
return Promise.resolve();
}
}
/**
* Get the config of this site.
* It is recommended to use getStoredConfig instead since it's faster and doesn't use network.
*
* @param {string} [name] Name of the setting to get. If not set or false, all settings will be returned.
* @param {boolean} [ignoreCache] True if it should ignore cached data.
* @return {Promise<any>} Promise resolved with site config.
*/
getConfig(name?: string, ignoreCache?: boolean) {
let preSets: CoreSiteWSPreSets = {
cacheKey: this.getConfigCacheKey()
}
if (ignoreCache) {
preSets.getFromCache = false;
preSets.emergencyCache = false;
}
return this.read('tool_mobile_get_config', {}, preSets).then((config) => {
if (name) {
// Return the requested setting.
for (let x in config.settings) {
if (config.settings[x].name == name) {
return config.settings[x].value;
}
}
return Promise.reject(null);
} else {
// Return all settings in the same array.
let settings = {};
config.settings.forEach((setting) => {
settings[setting.name] = setting.value;
});
return settings;
}
});
}
/**
* Invalidates config WS call.
*
* @return {Promise<any>} Promise resolved when the data is invalidated.
*/
invalidateConfig() : Promise<any> {
return this.invalidateWsCacheForKey(this.getConfigCacheKey());
}
/**
* Get cache key for getConfig WS calls.
*
* @return {string} Cache key.
*/
protected getConfigCacheKey() : string {
return 'tool_mobile_get_config';
}
/**
* Get the stored config of this site.
*
* @param {string} [name] Name of the setting to get. If not set, all settings will be returned.
* @return {any} Site config or a specific setting.
*/
getStoredConfig(name?: string) : any {
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 {string} name Name of the feature to check.
* @return {boolean} Whether it's disabled.
*/
isFeatureDisabled(name: string) : boolean {
const disabledFeatures = this.getStoredConfig('tool_mobile_disabledfeatures');
if (!disabledFeatures) {
return false;
}
const regEx = new RegExp('(,|^)' + this.textUtils.escapeForRegex(name) + '(,|$)', 'g');
return !!disabledFeatures.match(regEx);
}
/**
* Return the function to be used, based on the available functions in the site. It'll try to use non-deprecated
* functions first, and fallback to deprecated ones if needed.
*
* @param {string} method WS function to check.
* @return {string} Method to use based in the available functions.
*/
getCompatibleFunction(method: string) : string {
const newFunction = this.deprecatedFunctions[method];
if (typeof newFunction != 'undefined') {
// Deprecated function is being used. Warn the developer.
if (this.wsAvailable(newFunction)) {
this.logger.warn('You are using deprecated Web Services: ' + method +
' you must replace it with the newer function: ' + newFunction);
return newFunction;
} else {
this.logger.warn('You are using deprecated Web Services. ' +
'Your remote site seems to be outdated, consider upgrade it to the latest Moodle version.');
}
} else if (!this.wsAvailable(method)) {
// Method not available. Check if there is a deprecated method to use.
for (let oldFunc in this.deprecatedFunctions) {
if (this.deprecatedFunctions[oldFunc] === method && this.wsAvailable(oldFunc)) {
this.logger.warn('Your remote site doesn\'t support the function ' + method +
', it seems to be outdated, consider upgrade it to the latest Moodle version.');
return oldFunc; // Use deprecated function.
}
}
}
return method;
}
/**
* 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 {string | string[]} versions Version or list of versions to check.
* @return {boolean} 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 {
const siteVersion = parseInt(this.getInfo().version, 10);
if (Array.isArray(versions)) {
if (!versions.length) {
return false;
}
for (let i = 0; i < versions.length; i++) {
let 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;
}
/**
* 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 {string} version Release version to convert to version number.
* @return {number} Version number, 0 if invalid.
*/
protected getVersionNumber(version: string) : number {
let data = this.getMajorAndMinor(version);
if (!data) {
// Invalid version.
return 0;
}
if (typeof this.moodleReleases[data.major] == 'undefined') {
// Major version not found. Use the last one.
data.major = Object.keys(this.moodleReleases).slice(-1);
}
return this.moodleReleases[data.major] + data.minor;
}
/**
* Given a release version, return the major and minor versions.
*
* @param {string} version Release version (e.g. '3.1.0').
* @return {object} Object with major and minor. Returns false if invalid version.
*/
protected getMajorAndMinor(version) : any {
const match = version.match(/(\d)+(?:\.(\d)+)?(?:\.(\d)+)?/);
if (!match || !match[1]) {
// Invalid version.
return false;
}
return {
major: match[1] + '.' + (match[2] || '0'),
minor: parseInt(match[3] || 0, 10)
}
}
/**
* Given a release version, return the next major version number.
*
* @param {string} version Release version (e.g. '3.1.0').
* @return {number} Next major version number.
*/
protected getNextMajorVersionNumber(version: string) : number {
let data = this.getMajorAndMinor(version),
position,
releases = Object.keys(this.moodleReleases);
if (!data) {
// Invalid version.
return 0;
}
position = releases.indexOf(data.major);
if (position == -1 || position == releases.length -1) {
// Major version not found or it's the last one. Use the last one.
return this.moodleReleases[releases[position]];
}
return this.moodleReleases[releases[position + 1]];
}
}