// (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 { Injectable } 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 { CoreUrlUtilsProvider } from './utils/url'; import { CoreUtilsProvider } from './utils/utils'; import { CoreConstants } from '../core/constants'; import { CoreConfigConstants } from '../configconstants'; import { CoreSite } from '../classes/site'; import { SQLiteDB } from '../classes/sqlitedb'; import { Md5 } from 'ts-md5/dist/md5'; export interface CoreSiteCheckResponse { code: number; // Code to identify the authentication method to use. siteUrl: string; // Site url to use (might have changed during the process). service: string; // Service used. warning?: string; // Code of the warning message to show to the user. config?: any; // Site public config (if available). }; export interface CoreSiteUserTokenResponse { token: string; // User token. siteUrl: string; // Site URL to use. privateToken?: string; // User private token. }; export interface CoreSiteBasicInfo { id: string; siteUrl: string; fullName: string; siteName: string; avatar: string; badge?: number; }; /* * 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 appTablesSchema = [ { 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 VALID_VERSION = 1; protected LEGACY_APP_VERSION = 0; protected INVALID_VERSION = -1; protected logger; protected services = {}; protected sessionRestored = false; protected currentSite: CoreSite; protected sites: {[s: string]: CoreSite} = {}; protected appDB: SQLiteDB; protected siteTablesSchemas = []; // Schemas for site tables. Other providers can add schemas in here. constructor(logger: CoreLoggerProvider, private http: HttpClient, private sitesFactory: CoreSitesFactoryProvider, private appProvider: CoreAppProvider, private utils: CoreUtilsProvider, private translate: TranslateService, private eventsProvider: CoreEventsProvider, private urlUtils: CoreUrlUtilsProvider) { this.logger = logger.getInstance('CoreSitesProvider'); this.appDB = appProvider.getDB(); this.appDB.createTablesFromSchema(this.appTablesSchema); } /** * Get the demo data for a certain "name" if it is a demo site. * * @param {string} name Name of the site to check. * @return {any} Site data if it's a demo site, undefined otherwise. */ getDemoSiteData(name: string) { 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 {string} siteUrl URL of the site to check. * @param {string} [protocol=https://] Protocol to use first. * @return {Promise} A promise resolved when the site is checked. */ checkSite(siteUrl: string, protocol = 'https://') : Promise { // formatURL 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.error); } // Retry with the other protocol. protocol = protocol == 'https://' ? 'http://' : 'https://'; return this.checkSiteWithProtocol(siteUrl, protocol).catch((secondError) => { // Site doesn't exist. if (secondError.error) { return Promise.reject(secondError.error); } else if (error.error) { return Promise.reject(error.error); } return Promise.reject(this.translate.instant('core.login.checksiteversion')); }); }); } } /** * Helper function to check if a site is valid and if it has specifics settings for authentication. * * @param {string} siteUrl URL of the site to check. * @param {string} protocol Protocol to use. * @return {Promise} A promise resolved when the site is checked. */ checkSiteWithProtocol(siteUrl: string, protocol: string) : Promise { 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 && 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 && secondError.errorcode == 'enablewsdescription') { return rejectWithCriticalError(secondError.error, secondError.errorcode); } error = secondError || error; return Promise.reject({error: typeof error == 'object' ? error.error : error}); }); }).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.loginSSOCode && data.code != CoreConstants.loginSSOInAppCode)) { // 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. return Promise.reject({error: error.error}); } return data; }); } return data; }).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) { return Promise.reject({ error: message, errorcode: errorCode, critical: true }); } } /** * Check if a site exists. * * @param {string} siteUrl URL of the site to check. * @return {Promise} A promise to be resolved if the site exists. */ siteExists(siteUrl: string) : Promise { let data: any = {}; if (!this.appProvider.isMobile()) { // Send fake parameters for CORS. This is only needed in browser. data.username = 'a'; data.password = 'b'; data.service = 'c'; } const observable = this.http.post(siteUrl + '/login/token.php', data).timeout(CoreConstants.wsTimeout); return this.utils.observableToPromise(observable).catch((error) => { return Promise.reject(error.message); }).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 {string} siteUrl The site url. * @param {string} username User name. * @param {string} password Password. * @param {string} [service] Service to use. If not defined, it will be searched in memory. * @param {boolean} [retry] Whether we are retrying with a prefixed URL. * @return {Promise} A promise resolved when the token is retrieved. */ getUserToken(siteUrl: string, username: string, password: string, service?: string, retry?: boolean) : Promise { 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 }, observable = this.http.post(siteUrl + '/login/token.php', params).timeout(CoreConstants.wsTimeout); return this.utils.observableToPromise(observable).then((data: any) : any => { if (typeof data == 'undefined') { return Promise.reject(this.translate.instant('core.cannotconnect')); } 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 (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')); }); } /** * Add a new site to the site list and authenticate the user in this site. * * @param {string} siteUrl The site url. * @param {string} token User's token. * @param {string} [privateToken=''] User's private token. * @return {Promise} A promise resolved when the site is added and the user is authenticated. */ newSite(siteUrl: string, token: string, privateToken = '') : Promise { // Create a "candidate" site to fetch the site info. const candidateSite = this.sitesFactory.makeSite(undefined, siteUrl, token, undefined, privateToken); return candidateSite.fetchSiteInfo().then((info) => { let result = this.isValidMoodleVersion(info); if (result == this.VALID_VERSION) { // Set site ID and info. const siteId = this.createSiteID(info.siteurl, info.username); candidateSite.setId(siteId); candidateSite.setInfo(info); // Try to get the site config. return this.getSiteConfig(candidateSite).then((config) => { candidateSite.setConfig(config); // Add site to sites list. this.addSite(siteId, siteUrl, token, info, privateToken, config); // Turn candidate site into current site. this.currentSite = candidateSite; this.sites[siteId] = candidateSite; // Store session. this.login(siteId); this.eventsProvider.trigger(CoreEventsProvider.SITE_ADDED, siteId); if (this.siteTablesSchemas.length) { // Create tables in the site's database. candidateSite.getDb().createTablesFromSchema(this.siteTablesSchemas); } return siteId; }); } else if (result == this.LEGACY_APP_VERSION) { return Promise.reject(this.translate.instant('mm.login.legacymoodleversion')); } else { return Promise.reject(this.translate.instant('core.login.invalidmoodleversion')); } }); } /** * Create a site ID based on site URL and username. * * @param {string} siteUrl The site url. * @param {string} username Username. * @return {string} Site ID. */ createSiteID(siteUrl: string, username: string) : string { return Md5.hashAsciiStr(siteUrl + username); } /** * Function for determine which service we should use (default or extended plugin). * * @param {string} siteUrl The site URL. * @return {string} 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 {any} info Site info. * @return {number} Either VALID_VERSION, LEGACY_APP_VERSION or INVALID_VERSION. */ protected isValidMoodleVersion(info: any) : number { if (!info) { return this.INVALID_VERSION; } const version24 = 2012120300, // Moodle 2.4 version. release24 = '2.4', version31 = 2016052300, release31 = '3.1'; // Try to validate by version. if (info.version) { const version = parseInt(info.version, 10); if (!isNaN(version)) { if (version >= version31) { return this.VALID_VERSION; } else if (version >= version24) { return this.LEGACY_APP_VERSION; } else { return this.INVALID_VERSION; } } } // We couldn't validate by version number. Let's try to validate by release number. if (info.release) { const matches = info.release.match(/^([\d|\.]*)/); if (matches && matches.length > 1) { if (matches[1] >= release31) { return this.VALID_VERSION; } else if (matches[1] >= release24) { return this.LEGACY_APP_VERSION; } else { return this.INVALID_VERSION; } } } // Couldn't validate it. return this.INVALID_VERSION; } /** * Check if site info is valid. If it's not, return error message. * * @param {any} info Site info. * @return {any} 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 = `${info.siteurl}`; return {error: 'core.requireduserdatamissing', params: {'$a': moodleLink}}; } return true; } /** * Saves a site in local DB. * * @param {string} id Site ID. * @param {string} siteUrl Site URL. * @param {string} token User's token in the site. * @param {any} info Site's info. * @param {string} [privateToken=''] User's private token. * @param {any} [config] Site config (from tool_mobile_get_config). * @return {Promise} Promise resolved when done. */ addSite(id: string, siteUrl: string, token: string, info: any, privateToken = '', config?: any) : Promise { 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.insertOrUpdateRecord(this.SITES_TABLE, entry, {id: id}); } /** * Login a user to a site from the list of sites. * * @param {string} siteId ID of the site to load. * @return {Promise} Promise to be resolved when the site is loaded. */ loadSite(siteId: string) : Promise { this.logger.debug(`Load site ${siteId}`); return this.getSite(siteId).then((site) => { this.currentSite = site; this.login(siteId); if (site.isLoggedOut()) { // Logged out, nothing else to do. return; } // 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, {siteId: siteId}); }, () => { // Update site info. We don't block the UI. this.updateSiteInfo(siteId); }); }); } /** * Get current site. * * @return {CoreSite} Current site. */ getCurrentSite() : CoreSite { return this.currentSite; } /** * Get current site ID. * * @return {string} Current site ID. */ getCurrentSiteId() : string { if (this.currentSite) { return this.currentSite.getId(); } else { return ''; } } /** * Check if the user is logged in a site. * * @return {boolean} 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 {string} siteId ID of the site to delete. * @return {Promise} Promise to be resolved when the site is deleted. */ deleteSite(siteId: string) : Promise { 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); }); }); }); } /** * Check if there are no sites stored. * * @return {Promise} Promise resolved if there are no sites, and rejected if there is at least one. */ hasNoSites() : Promise { return this.appDB.countRecords(this.SITES_TABLE).then((count) => { if (count > 0) { return Promise.reject(null); } }); } /** * Check if there are sites stored. * * @return {Promise} Promise resolved if there is at least one site, and rejected if there aren't. */ hasSites() : Promise { return this.appDB.countRecords(this.SITES_TABLE).then((count) => { if (count == 0) { return Promise.reject(null); } }); } /** * Returns a site object. * * @param {string} [siteId] The site ID. If not defined, current site (if available). * @return {Promise} Promise resolved with the site. */ getSite(siteId?: string) : Promise { 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 {any} entry Site list entry. * @return {CoreSite} Created site. */ makeSiteFromSiteListEntry(entry) : CoreSite { let site, info = entry.info, config = entry.config; // Try to parse info and config. try { info = info ? JSON.parse(info) : info; } catch(ex) {} try { config = config ? JSON.parse(config) : config; } catch(ex) {} site = this.sitesFactory.makeSite(entry.id, entry.siteUrl, entry.token, info, entry.privateToken, config, entry.loggedOut == 1); this.sites[entry.id] = site; if (this.siteTablesSchemas.length) { // Create tables in the site's database. site.getDb().createTablesFromSchema(this.siteTablesSchemas); } return site; } /** * Returns if the site is the current one. * * @param {string|CoreSite} [site] Site object or siteId to be compared. If not defined, use current site. * @return {boolean} 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 {string} [siteId] The site ID. If not defined, current site (if available). * @return {Promise} Promise resolved with the database. */ getSiteDb(siteId: string) : Promise { return this.getSite(siteId).then((site) => { return site.getDb(); }); } /** * Returns the site home ID of a site. * * @param {number} [siteId] The site ID. If not defined, current site (if available). * @return {Promise} Promise resolved with site home ID. */ getSiteHomeId(siteId: string) : Promise { return this.getSite(siteId).then((site) => { return site.getSiteHomeId(); }); } /** * Get the list of sites stored. * * @param {String[]} [ids] IDs of the sites to get. If not defined, return all sites. * @return {Promise} Promise resolved when the sites are retrieved. */ getSites(ids?: string[]) : Promise { return this.appDB.getAllRecords(this.SITES_TABLE).then((sites) => { let formattedSites = []; sites.forEach((site) => { if (!ids || ids.indexOf(site.id) > -1) { // Try to parse info. let siteInfo = site.info; try { siteInfo = siteInfo ? JSON.parse(siteInfo) : siteInfo; } catch(ex) {} const basicInfo: CoreSiteBasicInfo = { id: site.id, siteUrl: site.siteUrl, fullName: siteInfo && siteInfo.fullname, siteName: siteInfo && siteInfo.sitename, avatar: siteInfo && siteInfo.userpictureurl }; formattedSites.push(basicInfo); } }); return formattedSites; }); } /** * Get the list of IDs of sites stored. * * @return {Promise} Promise resolved when the sites IDs are retrieved. */ getSitesIds(): Promise { return this.appDB.getAllRecords(this.SITES_TABLE).then((sites) => { return sites.map((site) => { return site.id; }); }); } /** * Login the user in a site. * * @param {string} siteid ID of the site the user is accessing. * @return {Promise} Promise resolved when current site is stored. */ login(siteId: string) : Promise { const entry = { id: 1, siteId: siteId }; return this.appDB.insertOrUpdateRecord(this.CURRENT_SITE_TABLE, entry, {id: 1}).then(() => { this.eventsProvider.trigger(CoreEventsProvider.LOGIN, {siteId: siteId}); }); } /** * Logout the user. * * @return {Promise} Promise resolved when the user is logged out. */ logout() : Promise { if (!this.currentSite) { // Already logged out. return Promise.resolve(); } const siteId = this.currentSite.getId(), siteConfig = this.currentSite.getStoredConfig(), promises = []; 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: siteId}); }); } /** * Restores the session to the previous one so the user doesn't has to login everytime the app is started. * * @return {Promise} Promise resolved if a session is restored. */ restoreSession() : Promise { 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); }); } /** * Mark or unmark a site as logged out so the user needs to authenticate again. * * @param {string} siteId ID of the site. * @param {boolean} loggedOut True to set the site as logged out, false otherwise. * @return {Promise} Promise resolved when done. */ setSiteLoggedOut(siteId: string, loggedOut: boolean) : Promise { 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}); }); } /** * Updates a site's token. * * @param {string} siteUrl Site's URL. * @param {string} username Username. * @param {string} token User's new token. * @param {string} [privateToken=''] User's private token. * @return {Promise} A promise resolved when the site is updated. */ updateSiteToken(siteUrl: string, username: string, token: string, privateToken = '') : Promise { const siteId = this.createSiteID(siteUrl, username); return this.updateSiteTokenBySiteId(siteId, token, privateToken); } /** * Updates a site's token using siteId. * * @param {string} siteId Site Id. * @param {string} token User's new token. * @param {string} [privateToken=''] User's private token. * @return {Promise} A promise resolved when the site is updated. */ updateSiteTokenBySiteId(siteId: string, token: string, privateToken = '') : Promise { 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 {string} siteid Site's ID. * @return {Promise} A promise resolved when the site is updated. */ updateSiteInfo(siteId: string) : Promise { return this.getSite(siteId).then((site) => { return site.fetchSiteInfo().then((info) => { site.setInfo(info); // Try to get the site config. return this.getSiteConfig(site).catch(() => { // Error getting config, keep the current one. }).then((config) => { let 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, {siteId: siteId}); }); }); }); }); } /** * Updates a site's info. * * @param {string} siteUrl Site's URL. * @param {string} username Username. * @return {Promise} A promise to be resolved when the site is updated. */ updateSiteInfoByUrl(siteUrl: string, username: string) : Promise { 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 {string} url URL to check. * @param {boolean} [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 {string} [username] If set, it will return only the sites where the current user has this username. * @return {Promise} Promise resolved with the site IDs (array). */ getSiteIdsFromUrl(url: string, prioritize?: boolean, username?: string) : Promise { // 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) => { let ids = []; siteEntries.forEach((site) => { if (!this.sites[site.id]) { this.makeSiteFromSiteListEntry(site); } if (this.sites[site.id].containsUrl(url)) { if (!username || this.sites[site.id].getInfo().username == username) { ids.push(site.id); } } }); return ids; }).catch(() => { // Shouldn't happen. return []; }); } /** * Get the site ID stored in DB as current site. * * @return {Promise} Promise resolved with the site ID. */ getStoredCurrentSiteId() : Promise { return this.appDB.getRecord(this.CURRENT_SITE_TABLE, {id: 1}).then((currentSite) => { return currentSite.siteId; }); } /** * Get the public config of a certain site. * * @param {string} siteUrl URL of the site. * @return {Promise} Promise resolved with the public config. */ getSitePublicConfig(siteUrl: string) : Promise { const temporarySite = this.sitesFactory.makeSite(undefined, siteUrl); return temporarySite.getPublicConfig(); } /** * Get site config. * * @param {any} site The site to get the config. * @return {Promise} Promise resolved with config if available. */ protected getSiteConfig(site: CoreSite) : Promise { 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 {string} name Name of the feature to check. * @param {string} [siteId] The site ID. If not defined, current site (if available). * @return {Promise} Promise resolved with true if disabled. */ isFeatureDisabled(name: string, siteId?: string) : Promise { return this.getSite(siteId).then((site) => { return site.isFeatureDisabled(name); }); } /** * Create a table in all the sites databases. * * @param {any} table Table schema. */ createTableFromSchema(table: any) : void { this.createTablesFromSchema([table]); } /** * Create several tables in all the sites databases. * * @param {any[]} tables List of tables schema. */ createTablesFromSchema(tables: any[]) : 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 (let id in this.sites) { this.sites[id].getDb().createTablesFromSchema(tables); } } }