2
0
Fork 0
Vmeda.Online/src/providers/sites.ts

1737 lines
60 KiB
TypeScript

// (C) Copyright 2015 Moodle Pty Ltd.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { Injectable, Injector } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { TranslateService } from '@ngx-translate/core';
import { CoreAppProvider } from './app';
import { CoreEventsProvider } from './events';
import { CoreLoggerProvider } from './logger';
import { CoreSitesFactoryProvider } from './sites-factory';
import { CoreDomUtilsProvider } from './utils/dom';
import { CoreTextUtilsProvider } from './utils/text';
import { CoreUrlUtilsProvider } from './utils/url';
import { CoreUtilsProvider } from './utils/utils';
import { CoreWSProvider } from './ws';
import { CoreConstants } from '@core/constants';
import { CoreConfigConstants } from '../configconstants';
import { CoreSite } from '@classes/site';
import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb';
import { Md5 } from 'ts-md5/dist/md5';
import { WP_PROVIDER } from '@app/app.module';
/**
* Response of checking if a site exists and its configuration.
*/
export interface CoreSiteCheckResponse {
/**
* Code to identify the authentication method to use.
*/
code: number;
/**
* Site url to use (might have changed during the process).
*/
siteUrl: string;
/**
* Service used.
*/
service: string;
/**
* Code of the warning message to show to the user.
*/
warning?: string;
/**
* Site public config (if available).
*/
config?: any;
}
/**
* Response of getting user token.
*/
export interface CoreSiteUserTokenResponse {
/**
* User token.
*/
token: string;
/**
* Site URL to use.
*/
siteUrl: string;
/**
* User private token.
*/
privateToken?: string;
}
/**
* Site's basic info.
*/
export interface CoreSiteBasicInfo {
/**
* Site ID.
*/
id: string;
/**
* Site URL.
*/
siteUrl: string;
/**
* User's full name.
*/
fullName: string;
/**
* Site's name.
*/
siteName: string;
/**
* User's avatar.
*/
avatar: string;
/**
* Badge to display in the site.
*/
badge?: number;
/**
* Site home ID.
*/
siteHomeId?: number;
}
/**
* Site schema and migration function.
*/
export interface CoreSiteSchema {
/**
* Name of the schema.
*/
name: string;
/**
* Latest version of the schema (integer greater than 0).
*/
version: number;
/**
* Names of the tables of the site schema that can be cleared.
*/
canBeCleared?: string[];
/**
* Tables to create when installing or upgrading the schema.
*/
tables?: SQLiteDBTableSchema[];
/**
* Migrates the schema in a site to the latest version.
*
* Called when installing and upgrading the schema, after creating the defined tables.
*
* @param db Site database.
* @param oldVersion Old version of the schema or 0 if not installed.
* @param siteId Site Id to migrate.
* @return Promise resolved when done.
*/
migrate?(db: SQLiteDB, oldVersion: number, siteId: string): Promise<any> | void;
}
/*
* Service to manage and interact with sites.
* It allows creating tables in the databases of all sites. Each service or component should be responsible of creating
* their own database tables. Example:
*
* constructor(sitesProvider: CoreSitesProvider) {
* this.sitesProvider.createTableFromSchema(this.tableSchema);
*
* This provider will automatically create the tables in the databases of all the instantiated sites, and also to the
* databases of sites instantiated from now on.
*/
@Injectable()
export class CoreSitesProvider {
// Variables for the database.
protected SITES_TABLE = 'sites';
protected CURRENT_SITE_TABLE = 'current_site';
protected SCHEMA_VERSIONS_TABLE = 'schema_versions';
protected appTablesSchema: SQLiteDBTableSchema[] = [
{
name: this.SITES_TABLE,
columns: [
{
name: 'id',
type: 'TEXT',
primaryKey: true
},
{
name: 'siteUrl',
type: 'TEXT',
notNull: true
},
{
name: 'token',
type: 'TEXT'
},
{
name: 'info',
type: 'TEXT'
},
{
name: 'privateToken',
type: 'TEXT'
},
{
name: 'config',
type: 'TEXT'
},
{
name: 'loggedOut',
type: 'INTEGER'
}
]
},
{
name: this.CURRENT_SITE_TABLE,
columns: [
{
name: 'id',
type: 'INTEGER',
primaryKey: true
},
{
name: 'siteId',
type: 'TEXT',
notNull: true,
unique: true
}
]
}
];
// Constants to validate a site version.
protected WORKPLACE_APP = 3;
protected MOODLE_APP = 2;
protected VALID_VERSION = 1;
protected INVALID_VERSION = -1;
protected isWPApp: boolean;
protected logger;
protected services = {};
protected sessionRestored = false;
protected currentSite: CoreSite;
protected sites: { [s: string]: CoreSite } = {};
protected appDB: SQLiteDB;
protected siteSchemasMigration: { [siteId: string]: Promise<any> } = {};
// Schemas for site tables. Other providers can add schemas in here.
protected siteSchemas: { [name: string]: CoreSiteSchema } = {};
protected siteTablesSchemas: SQLiteDBTableSchema[] = [
{
name: this.SCHEMA_VERSIONS_TABLE,
columns: [
{
name: 'name',
type: 'TEXT',
primaryKey: true,
},
{
name: 'version',
type: 'INTEGER'
}
]
}
];
// Site schema for this provider.
protected siteSchema: CoreSiteSchema = {
name: 'CoreSitesProvider',
version: 1,
canBeCleared: [ CoreSite.WS_CACHE_TABLE ],
tables: [
{
name: CoreSite.WS_CACHE_TABLE,
columns: [
{
name: 'id',
type: 'TEXT',
primaryKey: true
},
{
name: 'data',
type: 'TEXT'
},
{
name: 'key',
type: 'TEXT'
},
{
name: 'expirationTime',
type: 'INTEGER'
}
]
},
{
name: CoreSite.CONFIG_TABLE,
columns: [
{
name: 'name',
type: 'TEXT',
unique: true,
notNull: true
},
{
name: 'value'
}
]
}
]
};
constructor(logger: CoreLoggerProvider, private http: HttpClient, private sitesFactory: CoreSitesFactoryProvider,
private appProvider: CoreAppProvider, private translate: TranslateService, private urlUtils: CoreUrlUtilsProvider,
private eventsProvider: CoreEventsProvider, private textUtils: CoreTextUtilsProvider,
private utils: CoreUtilsProvider, private injector: Injector, private wsProvider: CoreWSProvider,
protected domUtils: CoreDomUtilsProvider) {
this.logger = logger.getInstance('CoreSitesProvider');
this.appDB = appProvider.getDB();
this.appDB.createTablesFromSchema(this.appTablesSchema);
this.registerSiteSchema(this.siteSchema);
}
/**
* Get the demo data for a certain "name" if it is a demo site.
*
* @param name Name of the site to check.
* @return Site data if it's a demo site, undefined otherwise.
*/
getDemoSiteData(name: string): any {
const demoSites = CoreConfigConstants.demo_sites;
if (typeof demoSites != 'undefined' && typeof demoSites[name] != 'undefined') {
return demoSites[name];
}
}
/**
* Check if a site is valid and if it has specifics settings for authentication (like force to log in using the browser).
* It will test both protocols if the first one fails: http and https.
*
* @param siteUrl URL of the site to check.
* @param protocol Protocol to use first.
* @return A promise resolved when the site is checked.
*/
checkSite(siteUrl: string, protocol: string = 'https://'): Promise<CoreSiteCheckResponse> {
// The formatURL function adds the protocol if is missing.
siteUrl = this.urlUtils.formatURL(siteUrl);
if (!this.urlUtils.isHttpURL(siteUrl)) {
return Promise.reject(this.translate.instant('core.login.invalidsite'));
} else if (!this.appProvider.isOnline()) {
return Promise.reject(this.translate.instant('core.networkerrormsg'));
} else {
return this.checkSiteWithProtocol(siteUrl, protocol).catch((error) => {
// Do not continue checking if a critical error happened.
if (error.critical) {
return Promise.reject(error);
}
// Retry with the other protocol.
protocol = protocol == 'https://' ? 'http://' : 'https://';
return this.checkSiteWithProtocol(siteUrl, protocol).catch((secondError) => {
if (secondError.critical) {
return Promise.reject(secondError);
}
// Site doesn't exist. Return the error message.
if (this.textUtils.getErrorMessageFromError(error)) {
return Promise.reject(error);
} else if (this.textUtils.getErrorMessageFromError(secondError)) {
return Promise.reject(secondError);
} else {
return this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION});
}
});
});
}
}
/**
* Helper function to check if a site is valid and if it has specifics settings for authentication.
*
* @param siteUrl URL of the site to check.
* @param protocol Protocol to use.
* @return A promise resolved when the site is checked.
*/
checkSiteWithProtocol(siteUrl: string, protocol: string): Promise<CoreSiteCheckResponse> {
let publicConfig;
// Now, replace the siteUrl with the protocol.
siteUrl = siteUrl.replace(/^http(s)?\:\/\//i, protocol);
return this.siteExists(siteUrl).catch((error) => {
// Do not continue checking if WS are not enabled.
if (error.errorcode == 'enablewsdescription') {
return rejectWithCriticalError(error.error, error.errorcode);
}
// Site doesn't exist. Try to add or remove 'www'.
const treatedUrl = this.urlUtils.addOrRemoveWWW(siteUrl);
return this.siteExists(treatedUrl).then(() => {
// Success, use this new URL as site url.
siteUrl = treatedUrl;
}).catch((secondError) => {
// Do not continue checking if WS are not enabled.
if (secondError.errorcode == 'enablewsdescription') {
return rejectWithCriticalError(secondError.error, secondError.errorcode);
}
// Return the error message.
if (this.textUtils.getErrorMessageFromError(error)) {
return Promise.reject(error);
} else {
return Promise.reject(secondError);
}
});
}).then(() => {
// Create a temporary site to check if local_mobile is installed.
const temporarySite = this.sitesFactory.makeSite(undefined, siteUrl);
return temporarySite.checkLocalMobilePlugin().then((data) => {
data.service = data.service || CoreConfigConstants.wsservice;
this.services[siteUrl] = data.service; // No need to store it in DB.
if (data.coreSupported ||
(data.code != CoreConstants.LOGIN_SSO_CODE && data.code != CoreConstants.LOGIN_SSO_INAPP_CODE)) {
// SSO using local_mobile not needed, try to get the site public config.
return temporarySite.getPublicConfig().then((config): any => {
publicConfig = config;
// Check that the user can authenticate.
if (!config.enablewebservices) {
return rejectWithCriticalError(this.translate.instant('core.login.webservicesnotenabled'));
} else if (!config.enablemobilewebservice) {
return rejectWithCriticalError(this.translate.instant('core.login.mobileservicesnotenabled'));
} else if (config.maintenanceenabled) {
let message = this.translate.instant('core.sitemaintenance');
if (config.maintenancemessage) {
message += config.maintenancemessage;
}
return rejectWithCriticalError(message);
}
// Everything ok.
if (data.code === 0) {
data.code = config.typeoflogin;
}
return data;
}, (error): any => {
// Error, check if not supported.
if (error.available === 1) {
// Service supported but an error happened. Return error.
error.critical = true;
if (error.errorcode == 'codingerror') {
// This could be caused by a redirect. Check if it's the case.
return this.utils.checkRedirect(siteUrl).then((redirect) => {
if (redirect) {
error.error = this.translate.instant('core.login.sitehasredirect');
} else {
// We can't be sure if there is a redirect or not. Display cannot connect error.
error.error = this.translate.instant('core.cannotconnect',
{$a: CoreSite.MINIMUM_MOODLE_VERSION});
}
return Promise.reject(error);
});
}
return Promise.reject(error);
}
return data;
});
}
return data;
}, (error) => {
// Local mobile check returned an error. This only happens if the plugin is installed and it returns an error.
return rejectWithCriticalError(error);
}).then((data) => {
siteUrl = temporarySite.getURL();
return { siteUrl: siteUrl, code: data.code, warning: data.warning, service: data.service, config: publicConfig };
});
});
// Return a rejected promise with a "critical" error.
function rejectWithCriticalError(message: string, errorCode?: string): Promise<never> {
return Promise.reject({
error: message,
errorcode: errorCode,
critical: true
});
}
}
/**
* Check if a site exists.
*
* @param siteUrl URL of the site to check.
* @return A promise to be resolved if the site exists.
*/
siteExists(siteUrl: string): Promise<void> {
return this.http.post(siteUrl + '/login/token.php', {}).timeout(this.wsProvider.getRequestTimeout()).toPromise()
.catch(() => {
// Default error messages are kinda bad, return our own message.
return Promise.reject({error: this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION})});
}).then((data: any) => {
if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) {
return Promise.reject({ errorcode: data.errorcode, error: data.error });
} else if (data.error && data.error == 'Web services must be enabled in Advanced features.') {
return Promise.reject({ errorcode: 'enablewsdescription', error: data.error });
}
// Other errors are not being checked because invalid login will be always raised and we cannot differ them.
});
}
/**
* Gets a user token from the server.
*
* @param siteUrl The site url.
* @param username User name.
* @param password Password.
* @param service Service to use. If not defined, it will be searched in memory.
* @param retry Whether we are retrying with a prefixed URL.
* @return A promise resolved when the token is retrieved.
*/
getUserToken(siteUrl: string, username: string, password: string, service?: string, retry?: boolean)
: Promise<CoreSiteUserTokenResponse> {
if (!this.appProvider.isOnline()) {
return Promise.reject(this.translate.instant('core.networkerrormsg'));
}
if (!service) {
service = this.determineService(siteUrl);
}
const params = {
username: username,
password: password,
service: service
},
loginUrl = siteUrl + '/login/token.php',
promise = this.http.post(loginUrl, params).timeout(this.wsProvider.getRequestTimeout()).toPromise();
return promise.then((data: any): any => {
if (typeof data == 'undefined') {
return Promise.reject(this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION}));
} else {
if (typeof data.token != 'undefined') {
return { token: data.token, siteUrl: siteUrl, privateToken: data.privatetoken };
} else {
if (typeof data.error != 'undefined') {
// We only allow one retry (to avoid loops).
if (!retry && data.errorcode == 'requirecorrectaccess') {
siteUrl = this.urlUtils.addOrRemoveWWW(siteUrl);
return this.getUserToken(siteUrl, username, password, service, true);
} else if (data.errorcode == 'missingparam') {
// It seems the server didn't receive all required params, it could be due to a redirect.
return this.utils.checkRedirect(loginUrl).then((redirect) => {
if (redirect) {
return Promise.reject({ error: this.translate.instant('core.login.sitehasredirect') });
} else {
return Promise.reject({ error: data.error, errorcode: data.errorcode });
}
});
} else if (typeof data.errorcode != 'undefined') {
return Promise.reject({ error: data.error, errorcode: data.errorcode });
} else {
return Promise.reject(data.error);
}
} else {
return Promise.reject(this.translate.instant('core.login.invalidaccount'));
}
}
}
}, () => {
return Promise.reject(this.translate.instant('core.cannotconnect', {$a: CoreSite.MINIMUM_MOODLE_VERSION}));
});
}
/**
* Add a new site to the site list and authenticate the user in this site.
*
* @param siteUrl The site url.
* @param token User's token.
* @param privateToken User's private token.
* @param login Whether to login the user in the site. Defaults to true.
* @return A promise resolved with siteId when the site is added and the user is authenticated.
*/
newSite(siteUrl: string, token: string, privateToken: string = '', login: boolean = true): Promise<string> {
if (typeof login != 'boolean') {
login = true;
}
// Create a "candidate" site to fetch the site info.
let candidateSite = this.sitesFactory.makeSite(undefined, siteUrl, token, undefined, privateToken),
isNewSite = true;
return candidateSite.fetchSiteInfo().then((info) => {
const result = this.isValidMoodleVersion(info);
if (result == this.VALID_VERSION) {
const siteId = this.createSiteID(info.siteurl, info.username);
// Check if the site already exists.
return this.getSite(siteId).catch(() => {
// Not exists.
}).then((site) => {
if (site) {
// Site already exists, update its data and use it.
isNewSite = false;
candidateSite = site;
candidateSite.setToken(token);
candidateSite.setPrivateToken(privateToken);
candidateSite.setInfo(info);
} else {
// New site, set site ID and info.
isNewSite = true;
candidateSite.setId(siteId);
candidateSite.setInfo(info);
// Create database tables before login and before any WS call.
return this.migrateSiteSchemas(candidateSite);
}
}).then(() => {
// Try to get the site config.
return this.getSiteConfig(candidateSite).catch((error) => {
// Ignore errors if it's not a new site, we'll use the config already stored.
if (isNewSite) {
return Promise.reject(error);
}
}).then((config) => {
if (typeof config != 'undefined') {
candidateSite.setConfig(config);
}
// Add site to sites list.
this.addSite(siteId, siteUrl, token, info, privateToken, config);
this.sites[siteId] = candidateSite;
if (login) {
// Turn candidate site into current site.
this.currentSite = candidateSite;
// Store session.
this.login(siteId);
}
this.eventsProvider.trigger(CoreEventsProvider.SITE_ADDED, info, siteId);
return siteId;
});
});
}
return this.treatInvalidAppVersion(result, siteUrl);
});
}
/**
* Having the result of isValidMoodleVersion, it treats the error message to be shown.
*
* @param result Result returned by isValidMoodleVersion function.
* @param siteUrl The site url.
* @param siteId If site is already added, it will invalidate the token.
* @return A promise rejected with the error info.
*/
protected treatInvalidAppVersion(result: number, siteUrl: string, siteId?: string): Promise<any> {
let errorCode,
errorKey,
translateParams;
switch (result) {
case this.MOODLE_APP:
errorKey = 'core.login.connecttomoodleapp';
errorCode = 'connecttomoodleapp';
break;
case this.WORKPLACE_APP:
errorKey = 'core.login.connecttoworkplaceapp';
errorCode = 'connecttoworkplaceapp';
break;
default:
errorCode = 'invalidmoodleversion';
errorKey = 'core.login.invalidmoodleversion';
translateParams = {$a: CoreSite.MINIMUM_MOODLE_VERSION};
}
let promise;
if (siteId) {
promise = this.setSiteLoggedOut(siteId, true);
} else {
promise = Promise.resolve();
}
return promise.then(() => {
return Promise.reject({
error: this.translate.instant(errorKey, translateParams),
errorcode: errorCode,
loggedout: true
});
});
}
/**
* Create a site ID based on site URL and username.
*
* @param siteUrl The site url.
* @param username Username.
* @return Site ID.
*/
createSiteID(siteUrl: string, username: string): string {
return <string> Md5.hashAsciiStr(siteUrl + username);
}
/**
* Function for determine which service we should use (default or extended plugin).
*
* @param siteUrl The site URL.
* @return The service shortname.
*/
determineService(siteUrl: string): string {
// We need to try siteUrl in both https or http (due to loginhttps setting).
// First http://
siteUrl = siteUrl.replace('https://', 'http://');
if (this.services[siteUrl]) {
return this.services[siteUrl];
}
// Now https://
siteUrl = siteUrl.replace('http://', 'https://');
if (this.services[siteUrl]) {
return this.services[siteUrl];
}
// Return default service.
return CoreConfigConstants.wsservice;
}
/**
* Check for the minimum required version.
*
* @param info Site info.
* @return Either VALID_VERSION, WORKPLACE_APP, MOODLE_APP or INVALID_VERSION.
*/
protected isValidMoodleVersion(info: any): number {
if (!info) {
return this.INVALID_VERSION;
}
const version31 = 2016052300,
release31 = CoreSite.MINIMUM_MOODLE_VERSION;
// Try to validate by version.
if (info.version) {
const version = parseInt(info.version, 10);
if (!isNaN(version)) {
if (version >= version31) {
return this.validateWorkplaceVersion(info);
}
}
}
// We couldn't validate by version number. Let's try to validate by release number.
const release = this.getReleaseNumber(info.release || '');
if (release) {
if (release >= release31) {
return this.validateWorkplaceVersion(info);
}
}
// Couldn't validate it.
return this.INVALID_VERSION;
}
/**
* Check if needs to be redirected to specific Workplace App or general Moodle App.
*
* @param info Site info.
* @return Either VALID_VERSION, WORKPLACE_APP or MOODLE_APP.
*/
protected validateWorkplaceVersion(info: any): number {
const isWorkplace = !!info.functions && info.functions.some((func) => {
return func.name == 'tool_program_get_user_programs';
});
if (typeof this.isWPApp == 'undefined') {
this.isWPApp = !!WP_PROVIDER && WP_PROVIDER.name == 'AddonBlockProgramsOverviewModule' &&
!!this.injector.get(WP_PROVIDER, false);
}
if (!this.isWPApp && isWorkplace) {
return this.WORKPLACE_APP;
}
if (this.isWPApp && !isWorkplace) {
return this.MOODLE_APP;
}
return this.VALID_VERSION;
}
/**
* Returns the release number from site release info.
*
* @param rawRelease Raw release info text.
* @return Release number or empty.
*/
getReleaseNumber(rawRelease: string): string {
const matches = rawRelease.match(/^\d(\.\d(\.\d+)?)?/);
if (matches) {
return matches[0];
}
return '';
}
/**
* Check if site info is valid. If it's not, return error message.
*
* @param info Site info.
* @return True if valid, object with error message to show and its params if not valid.
*/
protected validateSiteInfo(info: any): any {
if (!info.firstname || !info.lastname) {
const moodleLink = `<a core-link href="${info.siteurl}">${info.siteurl}</a>`;
return { error: 'core.requireduserdatamissing', params: { $a: moodleLink } };
}
return true;
}
/**
* Saves a site in local DB.
*
* @param id Site ID.
* @param siteUrl Site URL.
* @param token User's token in the site.
* @param info Site's info.
* @param privateToken User's private token.
* @param config Site config (from tool_mobile_get_config).
* @return Promise resolved when done.
*/
addSite(id: string, siteUrl: string, token: string, info: any, privateToken: string = '', config?: any): Promise<any> {
const entry = {
id: id,
siteUrl: siteUrl,
token: token,
info: info ? JSON.stringify(info) : info,
privateToken: privateToken,
config: config ? JSON.stringify(config) : config,
loggedOut: 0
};
return this.appDB.insertRecord(this.SITES_TABLE, entry);
}
/**
* Check the required minimum version of the app for a site and shows a download dialog.
*
* @param config Config object of the site.
* @param siteId ID of the site to check. Current site id will be used otherwise.
* @return Resolved with if meets the requirements, rejected otherwise.
*/
checkRequiredMinimumVersion(config: any, siteId?: string): Promise<void> {
if (config && config.tool_mobile_minimumversion) {
const requiredVersion = this.convertVersionName(config.tool_mobile_minimumversion),
appVersion = this.convertVersionName(CoreConfigConstants.versionname);
if (requiredVersion > appVersion) {
let downloadUrl = '';
if (this.appProvider.isAndroid() && config.tool_mobile_androidappid) {
downloadUrl = 'market://details?id=' + config.tool_mobile_androidappid;
} else if (this.appProvider.isIOS() && config.tool_mobile_iosappid) {
downloadUrl = 'itms-apps://itunes.apple.com/app/id' + config.tool_mobile_iosappid;
} else if (config.tool_mobile_setuplink) {
downloadUrl = config.tool_mobile_setuplink;
} else if (this.appProvider.isMobile()) {
downloadUrl = 'https://download.moodle.org/mobile/';
} else {
downloadUrl = 'https://download.moodle.org/desktop/';
}
siteId = siteId || this.getCurrentSiteId();
// Do not block interface.
this.domUtils.showConfirm(
this.translate.instant('core.updaterequireddesc', { $a: config.tool_mobile_minimumversion }),
this.translate.instant('core.updaterequired'),
this.translate.instant('core.download'),
this.translate.instant(siteId ? 'core.mainmenu.logout' : 'core.cancel')).then(() => {
this.utils.openInBrowser(downloadUrl);
}).catch(() => {
// Do nothing.
});
if (siteId) {
// Logout if it's the currentSite.
const promise = siteId == this.getCurrentSiteId() ? this.logout() : Promise.resolve();
return promise.then(() => {
// Always expire the token.
return this.setSiteLoggedOut(siteId, true);
}).then(() => {
return Promise.reject(null);
});
}
return Promise.reject(null);
}
}
return Promise.resolve();
}
/**
* Convert version name to numbers.
*
* @param name Version name (dot separated).
* @return Version translated to a comparable number.
*/
protected convertVersionName(name: string): number {
let version = 0;
const parts = name.split('-')[0].split('.', 3);
parts.forEach((num) => {
version = (version * 100) + Number(num);
});
if (parts.length < 3) {
version = version * Math.pow(100, 3 - parts.length);
}
return version;
}
/**
* Login a user to a site from the list of sites.
*
* @param siteId ID of the site to load.
* @param pageName Name of the page to go once authenticated if logged out. If not defined, site initial page.
* @param params Params of the page to go once authenticated if logged out.
* @return Promise resolved with true if site is loaded, resolved with false if cannot login.
*/
loadSite(siteId: string, pageName?: string, params?: any): Promise<boolean> {
this.logger.debug(`Load site ${siteId}`);
return this.getSite(siteId).then((site) => {
this.currentSite = site;
if (site.isLoggedOut()) {
// Logged out, trigger session expired event and stop.
this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, {
pageName: pageName,
params: params
}, site.getId());
return false;
}
// Check if local_mobile was installed to Moodle.
return site.checkIfLocalMobileInstalledAndNotUsed().then(() => {
// Local mobile was added. Throw invalid session to force reconnect and create a new token.
this.eventsProvider.trigger(CoreEventsProvider.SESSION_EXPIRED, {
pageName: pageName,
params: params
}, siteId);
return false;
}, () => {
return site.getPublicConfig().catch(() => {
return {};
}).then((config) => {
return this.checkRequiredMinimumVersion(config).then(() => {
this.login(siteId);
// Update site info. We don't block the UI.
this.updateSiteInfo(siteId);
return true;
}).catch(() => {
return false;
});
});
});
});
}
/**
* Get current site.
*
* @return Current site.
*/
getCurrentSite(): CoreSite {
return this.currentSite;
}
/**
* Get the site home ID of the current site.
*
* @return Current site home ID.
*/
getCurrentSiteHomeId(): number {
if (this.currentSite) {
return this.currentSite.getSiteHomeId();
} else {
return 1;
}
}
/**
* Get current site ID.
*
* @return Current site ID.
*/
getCurrentSiteId(): string {
if (this.currentSite) {
return this.currentSite.getId();
} else {
return '';
}
}
/**
* Get current site User ID.
*
* @return Current site User ID.
*/
getCurrentSiteUserId(): number {
if (this.currentSite) {
return this.currentSite.getUserId();
} else {
return 0;
}
}
/**
* Check if the user is logged in a site.
*
* @return Whether the user is logged in a site.
*/
isLoggedIn(): boolean {
return typeof this.currentSite != 'undefined' && typeof this.currentSite.token != 'undefined' &&
this.currentSite.token != '';
}
/**
* Delete a site from the sites list.
*
* @param siteId ID of the site to delete.
* @return Promise to be resolved when the site is deleted.
*/
deleteSite(siteId: string): Promise<any> {
this.logger.debug(`Delete site ${siteId}`);
if (typeof this.currentSite != 'undefined' && this.currentSite.id == siteId) {
this.logout();
}
return this.getSite(siteId).then((site: CoreSite) => {
return site.deleteDB().then(() => {
// Site DB deleted, now delete the app from the list of sites.
delete this.sites[siteId];
return this.appDB.deleteRecords(this.SITES_TABLE, { id: siteId }).then(() => {
// Site deleted from sites list, now delete the folder.
return site.deleteFolder();
}, () => {
// DB remove shouldn't fail, but we'll go ahead even if it does.
return site.deleteFolder();
}).then(() => {
this.eventsProvider.trigger(CoreEventsProvider.SITE_DELETED, site, siteId);
});
});
});
}
/**
* Check if there are sites stored.
*
* @return Promise resolved with true if there are sites and false if there aren't.
*/
hasSites(): Promise<boolean> {
return this.appDB.countRecords(this.SITES_TABLE).then((count) => {
return count > 0;
});
}
/**
* Returns a site object.
*
* @param siteId The site ID. If not defined, current site (if available).
* @return Promise resolved with the site.
*/
getSite(siteId?: string): Promise<CoreSite> {
if (!siteId) {
return this.currentSite ? Promise.resolve(this.currentSite) : Promise.reject(null);
} else if (this.currentSite && this.currentSite.getId() == siteId) {
return Promise.resolve(this.currentSite);
} else if (typeof this.sites[siteId] != 'undefined') {
return Promise.resolve(this.sites[siteId]);
} else {
// Retrieve and create the site.
return this.appDB.getRecord(this.SITES_TABLE, { id: siteId }).then((data) => {
return this.makeSiteFromSiteListEntry(data);
});
}
}
/**
* Create a site from an entry of the sites list DB. The new site is added to the list of "cached" sites: this.sites.
*
* @param entry Site list entry.
* @return Promised resolved with the created site.
*/
makeSiteFromSiteListEntry(entry: any): Promise<CoreSite> {
let site: CoreSite,
info = entry.info,
config = entry.config;
// Parse info and config.
info = info ? this.textUtils.parseJSON(info) : info;
config = config ? this.textUtils.parseJSON(config) : config;
site = this.sitesFactory.makeSite(entry.id, entry.siteUrl, entry.token,
info, entry.privateToken, config, entry.loggedOut == 1);
return this.migrateSiteSchemas(site).then(() => {
// Set site after migrating schemas, or a call to getSite could get the site while tables are being created.
this.sites[entry.id] = site;
return site;
});
}
/**
* Returns if the site is the current one.
*
* @param site Site object or siteId to be compared. If not defined, use current site.
* @return Whether site or siteId is the current one.
*/
isCurrentSite(site: string | CoreSite): boolean {
if (!site || !this.currentSite) {
return !!this.currentSite;
}
const siteId = typeof site == 'object' ? site.getId() : site;
return this.currentSite.getId() === siteId;
}
/**
* Returns the database object of a site.
*
* @param siteId The site ID. If not defined, current site (if available).
* @return Promise resolved with the database.
*/
getSiteDb(siteId: string): Promise<SQLiteDB> {
return this.getSite(siteId).then((site) => {
return site.getDb();
});
}
/**
* Returns the site home ID of a site.
*
* @param siteId The site ID. If not defined, current site (if available).
* @return Promise resolved with site home ID.
*/
getSiteHomeId(siteId?: string): Promise<number> {
return this.getSite(siteId).then((site) => {
return site.getSiteHomeId();
});
}
/**
* Get the list of sites stored.
*
* @param ids IDs of the sites to get. If not defined, return all sites.
* @return Promise resolved when the sites are retrieved.
*/
getSites(ids?: string[]): Promise<CoreSiteBasicInfo[]> {
return this.appDB.getAllRecords(this.SITES_TABLE).then((sites) => {
const formattedSites = [];
sites.forEach((site) => {
if (!ids || ids.indexOf(site.id) > -1) {
// Parse info.
const siteInfo = site.info ? this.textUtils.parseJSON(site.info) : site.info,
basicInfo: CoreSiteBasicInfo = {
id: site.id,
siteUrl: site.siteUrl,
fullName: siteInfo && siteInfo.fullname,
siteName: CoreConfigConstants.sitename ? CoreConfigConstants.sitename : siteInfo && siteInfo.sitename,
avatar: siteInfo && siteInfo.userpictureurl,
siteHomeId: siteInfo && siteInfo.siteid || 1
};
formattedSites.push(basicInfo);
}
});
return formattedSites;
});
}
/**
* Get the list of sites stored, sorted by URL and full name.
*
* @param ids IDs of the sites to get. If not defined, return all sites.
* @return Promise resolved when the sites are retrieved.
*/
getSortedSites(ids?: string[]): Promise<CoreSiteBasicInfo[]> {
return this.getSites(ids).then((sites) => {
// Sort sites by url and ful lname.
sites.sort((a, b) => {
// First compare by site url without the protocol.
let compareA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase(),
compareB = b.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
const compare = compareA.localeCompare(compareB);
if (compare !== 0) {
return compare;
}
// If site url is the same, use fullname instead.
compareA = a.fullName.toLowerCase().trim();
compareB = b.fullName.toLowerCase().trim();
return compareA.localeCompare(compareB);
});
return sites;
});
}
/**
* Get the list of IDs of sites stored and not logged out.
*
* @return Promise resolved when the sites IDs are retrieved.
*/
getLoggedInSitesIds(): Promise<string[]> {
return this.appDB.getRecords(this.SITES_TABLE, {loggedOut : 0}).then((sites) => {
return sites.map((site) => {
return site.id;
});
});
}
/**
* Get the list of IDs of sites stored.
*
* @return Promise resolved when the sites IDs are retrieved.
*/
getSitesIds(): Promise<string[]> {
return this.appDB.getAllRecords(this.SITES_TABLE).then((sites) => {
return sites.map((site) => {
return site.id;
});
});
}
/**
* Login the user in a site.
*
* @param siteid ID of the site the user is accessing.
* @return Promise resolved when current site is stored.
*/
login(siteId: string): Promise<void> {
const entry = {
id: 1,
siteId: siteId
};
return this.appDB.insertRecord(this.CURRENT_SITE_TABLE, entry).then(() => {
this.eventsProvider.trigger(CoreEventsProvider.LOGIN, {}, siteId);
});
}
/**
* Logout the user.
*
* @return Promise resolved when the user is logged out.
*/
logout(): Promise<any> {
let siteId;
const promises = [];
if (this.currentSite) {
const siteConfig = this.currentSite.getStoredConfig();
siteId = this.currentSite.getId();
this.currentSite = undefined;
if (siteConfig && siteConfig.tool_mobile_forcelogout == '1') {
promises.push(this.setSiteLoggedOut(siteId, true));
}
promises.push(this.appDB.deleteRecords(this.CURRENT_SITE_TABLE, { id: 1 }));
}
return Promise.all(promises).finally(() => {
this.eventsProvider.trigger(CoreEventsProvider.LOGOUT, {}, siteId);
});
}
/**
* Restores the session to the previous one so the user doesn't has to login everytime the app is started.
*
* @return Promise resolved if a session is restored.
*/
restoreSession(): Promise<any> {
if (this.sessionRestored) {
return Promise.reject(null);
}
this.sessionRestored = true;
return this.appDB.getRecord(this.CURRENT_SITE_TABLE, { id: 1 }).then((currentSite) => {
const siteId = currentSite.siteId;
this.logger.debug(`Restore session in site ${siteId}`);
return this.loadSite(siteId);
}).catch(() => {
// No current session.
});
}
/**
* Mark or unmark a site as logged out so the user needs to authenticate again.
*
* @param siteId ID of the site.
* @param loggedOut True to set the site as logged out, false otherwise.
* @return Promise resolved when done.
*/
setSiteLoggedOut(siteId: string, loggedOut: boolean): Promise<any> {
return this.getSite(siteId).then((site) => {
const newValues = {
token: '', // Erase the token for security.
loggedOut: loggedOut ? 1 : 0
};
site.setLoggedOut(loggedOut);
return this.appDB.updateRecords(this.SITES_TABLE, newValues, { id: siteId });
});
}
/**
* Unset current site.
*/
unsetCurrentSite(): void {
this.currentSite = undefined;
}
/**
* Updates a site's token.
*
* @param siteUrl Site's URL.
* @param username Username.
* @param token User's new token.
* @param privateToken User's private token.
* @return A promise resolved when the site is updated.
*/
updateSiteToken(siteUrl: string, username: string, token: string, privateToken: string = ''): Promise<any> {
const siteId = this.createSiteID(siteUrl, username);
return this.updateSiteTokenBySiteId(siteId, token, privateToken).then(() => {
return this.login(siteId);
});
}
/**
* Updates a site's token using siteId.
*
* @param siteId Site Id.
* @param token User's new token.
* @param privateToken User's private token.
* @return A promise resolved when the site is updated.
*/
updateSiteTokenBySiteId(siteId: string, token: string, privateToken: string = ''): Promise<any> {
return this.getSite(siteId).then((site) => {
const newValues = {
token: token,
privateToken: privateToken,
loggedOut: 0
};
site.token = token;
site.privateToken = privateToken;
site.setLoggedOut(false); // Token updated means the user authenticated again, not logged out anymore.
return this.appDB.updateRecords(this.SITES_TABLE, newValues, { id: siteId });
});
}
/**
* Updates a site's info.
*
* @param siteid Site's ID.
* @return A promise resolved when the site is updated.
*/
updateSiteInfo(siteId: string): Promise<any> {
return this.getSite(siteId).then((site) => {
return site.fetchSiteInfo().then((info) => {
site.setInfo(info);
const versionCheck = this.isValidMoodleVersion(info);
if (versionCheck != this.VALID_VERSION) {
// The Moodle version is not supported, reject.
return this.treatInvalidAppVersion(versionCheck, site.getURL(), site.getId());
}
// Try to get the site config.
return this.getSiteConfig(site).catch(() => {
// Error getting config, keep the current one.
}).then((config) => {
const newValues: any = {
info: JSON.stringify(info),
loggedOut: site.isLoggedOut() ? 1 : 0
};
if (typeof config != 'undefined') {
site.setConfig(config);
newValues.config = JSON.stringify(config);
}
return this.appDB.updateRecords(this.SITES_TABLE, newValues, { id: siteId }).finally(() => {
this.eventsProvider.trigger(CoreEventsProvider.SITE_UPDATED, info, siteId);
});
});
}).catch((error) => {
// Ignore that we cannot fetch site info. Probably the auth token is invalid.
});
});
}
/**
* Updates a site's info.
*
* @param siteUrl Site's URL.
* @param username Username.
* @return A promise to be resolved when the site is updated.
*/
updateSiteInfoByUrl(siteUrl: string, username: string): Promise<any> {
const siteId = this.createSiteID(siteUrl, username);
return this.updateSiteInfo(siteId);
}
/**
* Get the site IDs a URL belongs to.
* Someone can have more than one account in the same site, that's why this function returns an array of IDs.
*
* @param url URL to check.
* @param prioritize True if it should prioritize current site. If the URL belongs to current site then it won't
* check any other site, it will only return current site.
* @param username If set, it will return only the sites where the current user has this username.
* @return Promise resolved with the site IDs (array).
*/
getSiteIdsFromUrl(url: string, prioritize?: boolean, username?: string): Promise<string[]> {
// If prioritize is true, check current site first.
if (prioritize && this.currentSite && this.currentSite.containsUrl(url)) {
if (!username || this.currentSite.getInfo().username == username) {
return Promise.resolve([this.currentSite.getId()]);
}
}
// Check if URL has http(s) protocol.
if (!url.match(/^https?:\/\//i)) {
// URL doesn't have http(s) protocol. Check if it has any protocol.
if (this.urlUtils.isAbsoluteURL(url)) {
// It has some protocol. Return empty array.
return Promise.resolve([]);
} else {
// No protocol, probably a relative URL. Return current site.
if (this.currentSite) {
return Promise.resolve([this.currentSite.getId()]);
} else {
return Promise.resolve([]);
}
}
}
return this.appDB.getAllRecords(this.SITES_TABLE).then((siteEntries) => {
const ids = [];
const promises = [];
siteEntries.forEach((site) => {
if (!this.sites[site.id]) {
promises.push(this.makeSiteFromSiteListEntry(site));
}
if (this.sites[site.id].containsUrl(url)) {
if (!username || this.sites[site.id].getInfo().username == username) {
ids.push(site.id);
}
}
});
return Promise.all(promises).then(() => {
return ids;
});
}).catch(() => {
// Shouldn't happen.
return [];
});
}
/**
* Get the site ID stored in DB as current site.
*
* @return Promise resolved with the site ID.
*/
getStoredCurrentSiteId(): Promise<string> {
return this.appDB.getRecord(this.CURRENT_SITE_TABLE, { id: 1 }).then((currentSite) => {
return currentSite.siteId;
});
}
/**
* Get the public config of a certain site.
*
* @param siteUrl URL of the site.
* @return Promise resolved with the public config.
*/
getSitePublicConfig(siteUrl: string): Promise<any> {
const temporarySite = this.sitesFactory.makeSite(undefined, siteUrl);
return temporarySite.getPublicConfig();
}
/**
* Get site config.
*
* @param site The site to get the config.
* @return Promise resolved with config if available.
*/
protected getSiteConfig(site: CoreSite): Promise<any> {
if (!site.wsAvailable('tool_mobile_get_config')) {
// WS not available, cannot get config.
return Promise.resolve();
}
return site.getConfig(undefined, true);
}
/**
* Check if a certain feature is disabled in a site.
*
* @param name Name of the feature to check.
* @param siteId The site ID. If not defined, current site (if available).
* @return Promise resolved with true if disabled.
*/
isFeatureDisabled(name: string, siteId?: string): Promise<boolean> {
return this.getSite(siteId).then((site) => {
return site.isFeatureDisabled(name);
});
}
/**
* Create a table in all the sites databases.
*
* @param table Table schema.
*/
createTableFromSchema(table: SQLiteDBTableSchema): void {
this.createTablesFromSchema([table]);
}
/**
* Create several tables in all the sites databases.
*
* @param tables List of tables schema.
*/
createTablesFromSchema(tables: SQLiteDBTableSchema[]): void {
// Add the tables to the list of schemas. This list is to create all the tables in new sites.
this.siteTablesSchemas = this.siteTablesSchemas.concat(tables);
// Now create these tables in current sites.
for (const id in this.sites) {
this.sites[id].getDb().createTablesFromSchema(tables);
}
}
/**
* Check if a WS is available in the current site, if any.
*
* @param method WS name.
* @param checkPrefix When true also checks with the compatibility prefix.
* @return Whether the WS is available.
*/
wsAvailableInCurrentSite(method: string, checkPrefix: boolean = true): boolean {
const site = this.getCurrentSite();
return site && site.wsAvailable(method, checkPrefix);
}
/**
* Check if a site is a legacy site by its info.
*
* @param info The site info.
* @return Whether it's a legacy Moodle.
* @deprecated since 3.7.1
*/
isLegacyMoodleByInfo(info: any): boolean {
return false;
}
/**
* Register a site schema.
*/
registerSiteSchema(schema: CoreSiteSchema): void {
this.siteSchemas[schema.name] = schema;
}
/**
* Install and upgrade all the registered schemas and tables.
*
* @param site Site.
* @return Promise resolved when done.
*/
migrateSiteSchemas(site: CoreSite): Promise<any> {
const db = site.getDb();
if (this.siteSchemasMigration[site.id]) {
return this.siteSchemasMigration[site.id];
}
this.logger.debug(`Migrating all schemas of ${site.id}`);
// First create tables not registerd with name/version.
const promise = db.createTablesFromSchema(this.siteTablesSchemas).then(() => {
// Fetch installed versions of the schema.
return db.getAllRecords(this.SCHEMA_VERSIONS_TABLE).then((records) => {
const versions = {};
records.forEach((record) => {
versions[record.name] = record.version;
});
const promises = [];
for (const name in this.siteSchemas) {
const schema = this.siteSchemas[name];
const oldVersion = versions[name] || 0;
if (oldVersion >= schema.version) {
continue;
}
this.logger.debug(`Migrating schema '${name}' of ${site.id} from version ${oldVersion} to ${schema.version}`);
let promise: Promise<any> = Promise.resolve();
if (schema.tables) {
promise = promise.then(() => db.createTablesFromSchema(schema.tables));
}
if (schema.migrate) {
promise = promise.then(() => schema.migrate(db, oldVersion, site.id));
}
// Set installed version.
promise = promise.then(() => db.insertRecord(this.SCHEMA_VERSIONS_TABLE, {name, version: schema.version}));
promises.push(promise);
}
return Promise.all(promises);
});
});
this.siteSchemasMigration[site.id] = promise;
return promise.finally(() => {
delete this.siteSchemasMigration[site.id];
});
}
/**
* Check if a URL is the root URL of any of the stored sites.
*
* @param url URL to check.
* @param username Username to check.
* @return Promise resolved with site to use and the list of sites that have
* the URL. Site will be undefined if it isn't the root URL of any stored site.
*/
isStoredRootURL(url: string, username?: string): Promise<{site: CoreSite, siteIds: string[]}> {
// Check if the site is stored.
return this.getSiteIdsFromUrl(url, true, username).then((siteIds) => {
const result = {
siteIds: siteIds,
site: undefined
};
if (siteIds.length > 0) {
// If more than one site is returned it usually means there are different users stored. Use any of them.
return this.getSite(siteIds[0]).then((site) => {
const siteUrl = this.textUtils.removeEndingSlash(this.urlUtils.removeProtocolAndWWW(site.getURL())),
treatedUrl = this.textUtils.removeEndingSlash(this.urlUtils.removeProtocolAndWWW(url));
if (siteUrl == treatedUrl) {
result.site = site;
}
return result;
});
}
return result;
});
}
/**
* Returns the Site Schema names that can be cleared on space storage.
*
* @return Name of the site schemas.
*/
getSiteTableSchemasToClear(): string[] {
let reset = [];
for (const name in this.siteSchemas) {
if (this.siteSchemas[name].canBeCleared) {
reset = reset.concat(this.siteSchemas[name].canBeCleared);
}
}
return reset;
}
}