2
0
Fork 0
Vmeda.Online/src/core/services/sites.ts

1903 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 { Inject, Injectable, InjectionToken, Optional } from '@angular/core';
import { Md5 } from 'ts-md5/dist/md5';
import { timeout } from 'rxjs/operators';
import { CoreApp, CoreStoreConfig } from '@services/app';
import { CoreEvents } from '@singletons/events';
import { CoreWS } from '@services/ws';
import { CoreDomUtils } from '@services/utils/dom';
import { CoreTextUtils } from '@services/utils/text';
import { CoreUrlUtils } from '@services/utils/url';
import { CoreUtils } from '@services/utils/utils';
import { CoreConstants } from '@/core/constants';
import {
CoreSite,
CoreSiteWSPreSets,
LocalMobileResponse,
CoreSiteInfo,
CoreSiteConfig,
CoreSitePublicConfigResponse,
CoreSiteInfoResponse,
} from '@classes/site';
import { SQLiteDB, SQLiteDBTableSchema } from '@classes/sqlitedb';
import { CoreError } from '@classes/errors/error';
import { CoreSiteError } from '@classes/errors/siteerror';
import { makeSingleton, Translate, Http } from '@singletons';
import { CoreLogger } from '@singletons/logger';
import {
APP_SCHEMA,
SCHEMA_VERSIONS_TABLE_SCHEMA,
SITES_TABLE_NAME,
CURRENT_SITE_TABLE_NAME,
SCHEMA_VERSIONS_TABLE_NAME,
SiteDBEntry,
CurrentSiteDBEntry,
SchemaVersionsDBEntry,
} from '@services/database/sites';
import { CoreArray } from '../singletons/array';
import { CoreNetworkError } from '@classes/errors/network-error';
export const CORE_SITE_SCHEMAS = new InjectionToken<CoreSiteSchema[]>('CORE_SITE_SCHEMAS');
/*
* 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 calling the registerCoreSiteSchema method.
*/
@Injectable({ providedIn: 'root' })
export class CoreSitesProvider {
// Constants to validate a site version.
protected static readonly WORKPLACE_APP = 3;
protected static readonly MOODLE_APP = 2;
protected static readonly VALID_VERSION = 1;
protected static readonly INVALID_VERSION = -1;
protected isWPApp = false;
protected logger: CoreLogger;
protected services = {};
protected sessionRestored = false;
protected currentSite?: CoreSite;
protected sites: { [s: string]: CoreSite } = {};
protected siteSchemasMigration: { [siteId: string]: Promise<void> } = {};
protected siteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {};
protected pluginsSiteSchemas: { [name: string]: CoreRegisteredSiteSchema } = {};
// Variables for DB.
protected appDB: Promise<SQLiteDB>;
protected resolveAppDB!: (appDB: SQLiteDB) => void;
constructor(@Optional() @Inject(CORE_SITE_SCHEMAS) siteSchemas: CoreSiteSchema[][] = []) {
this.appDB = new Promise(resolve => this.resolveAppDB = resolve);
this.logger = CoreLogger.getInstance('CoreSitesProvider');
this.siteSchemas = CoreArray.flatten(siteSchemas).reduce(
(siteSchemas, schema) => {
siteSchemas[schema.name] = schema;
return siteSchemas;
},
this.siteSchemas,
);
}
/**
* Initialize database.
*/
async initializeDatabase(): Promise<void> {
try {
await CoreApp.createTablesFromSchema(APP_SCHEMA);
} catch (e) {
// Ignore errors.
}
this.resolveAppDB(CoreApp.getDB());
}
/**
* 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): CoreSitesDemoSiteData | undefined {
const demoSites = CoreConstants.CONFIG.demo_sites;
name = name.toLowerCase();
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.
*/
async checkSite(siteUrl: string, protocol: string = 'https://'): Promise<CoreSiteCheckResponse> {
// The formatURL function adds the protocol if is missing.
siteUrl = CoreUrlUtils.formatURL(siteUrl);
if (!CoreUrlUtils.isHttpURL(siteUrl)) {
throw new CoreError(Translate.instant('core.login.invalidsite'));
} else if (!CoreApp.isOnline()) {
throw new CoreNetworkError();
}
try {
return await this.checkSiteWithProtocol(siteUrl, protocol);
} catch (error) {
// Do not continue checking if a critical error happened.
if (error.critical) {
throw error;
}
// Retry with the other protocol.
protocol = protocol == 'https://' ? 'http://' : 'https://';
try {
return await this.checkSiteWithProtocol(siteUrl, protocol);
} catch (secondError) {
if (secondError.critical) {
throw secondError;
}
// Site doesn't exist. Return the error message.
if (CoreTextUtils.getErrorMessageFromError(error)) {
throw error;
} else if (CoreTextUtils.getErrorMessageFromError(secondError)) {
throw secondError;
} else {
throw new CoreError(Translate.instant('core.cannotconnecttrouble'));
}
}
}
}
/**
* 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.
*/
async checkSiteWithProtocol(siteUrl: string, protocol: string): Promise<CoreSiteCheckResponse> {
let publicConfig: CoreSitePublicConfigResponse | undefined;
// Now, replace the siteUrl with the protocol.
siteUrl = siteUrl.replace(/^https?:\/\//i, protocol);
try {
await this.siteExists(siteUrl);
} catch (error) {
// Do not continue checking if WS are not enabled.
if (error.errorcode == 'enablewsdescription') {
error.critical = true;
throw error;
}
// Site doesn't exist. Try to add or remove 'www'.
const treatedUrl = CoreUrlUtils.addOrRemoveWWW(siteUrl);
try {
await this.siteExists(treatedUrl);
// 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') {
secondError.critical = true;
throw secondError;
}
// Return the error.
if (CoreTextUtils.getErrorMessageFromError(error)) {
throw error;
} else {
throw secondError;
}
}
}
// Site exists. Create a temporary site to check if local_mobile is installed.
const temporarySite = new CoreSite(undefined, siteUrl);
let data: LocalMobileResponse;
try {
data = await temporarySite.checkLocalMobilePlugin();
} catch (error) {
// Local mobile check returned an error. This only happens if the plugin is installed and it returns an error.
throw new CoreSiteError({
message: error.message,
critical: true,
});
}
data.service = data.service || CoreConstants.CONFIG.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.
try {
const config = await temporarySite.getPublicConfig();
publicConfig = config;
// Check that the user can authenticate.
if (!config.enablewebservices) {
throw new CoreSiteError({
message: Translate.instant('core.login.webservicesnotenabled'),
});
} else if (!config.enablemobilewebservice) {
throw new CoreSiteError({
message: Translate.instant('core.login.mobileservicesnotenabled'),
});
} else if (config.maintenanceenabled) {
let message = Translate.instant('core.sitemaintenance');
if (config.maintenancemessage) {
message += config.maintenancemessage;
}
throw new CoreSiteError({
message,
});
}
// Everything ok.
if (data.code === 0) {
data.code = config.typeoflogin;
}
} catch (error) {
// Error, check if not supported.
if (error.available === 1) {
// Service supported but an error happened. Return error.
if (error.errorcode == 'codingerror') {
// This could be caused by a redirect. Check if it's the case.
const redirect = await CoreUtils.checkRedirect(siteUrl);
if (redirect) {
error.error = Translate.instant('core.login.sitehasredirect');
} else {
// We can't be sure if there is a redirect or not. Display cannot connect error.
error.error = Translate.instant('core.cannotconnecttrouble');
}
}
throw new CoreSiteError({
message: error.error,
errorcode: error.errorcode,
critical: true,
});
}
}
}
siteUrl = temporarySite.getURL();
return { siteUrl, code: data.code, warning: data.warning, service: data.service, config: publicConfig };
}
/**
* Check if a site exists.
*
* @param siteUrl URL of the site to check.
* @return A promise to be resolved if the site exists.
*/
async siteExists(siteUrl: string): Promise<void> {
let data: CoreSitesLoginTokenResponse;
// Use a valid path first.
siteUrl = CoreUrlUtils.removeUrlParams(siteUrl);
try {
data = await Http.post(siteUrl + '/login/token.php', { appsitecheck: 1 }).pipe(timeout(CoreWS.getRequestTimeout()))
.toPromise();
} catch (error) {
// Default error messages are kinda bad, return our own message.
throw new CoreSiteError({
message: Translate.instant('core.cannotconnecttrouble'),
});
}
if (data === null) {
// Cannot connect.
throw new CoreSiteError({
message: Translate.instant('core.cannotconnect', { $a: CoreSite.MINIMUM_MOODLE_VERSION }),
});
}
if (data.errorcode && (data.errorcode == 'enablewsdescription' || data.errorcode == 'requirecorrectaccess')) {
throw new CoreSiteError({
errorcode: data.errorcode,
message: data.error!,
});
}
if (data.error && data.error == 'Web services must be enabled in Advanced features.') {
throw new CoreSiteError({
errorcode: 'enablewsdescription',
message: 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.
*/
async getUserToken(
siteUrl: string,
username: string,
password: string,
service?: string,
retry?: boolean,
): Promise<CoreSiteUserTokenResponse> {
if (!CoreApp.isOnline()) {
throw new CoreNetworkError();
}
if (!service) {
service = this.determineService(siteUrl);
}
const params = {
username,
password,
service,
};
const loginUrl = siteUrl + '/login/token.php';
let data: CoreSitesLoginTokenResponse;
try {
data = await Http.post(loginUrl, params).pipe(timeout(CoreWS.getRequestTimeout())).toPromise();
} catch (error) {
throw new CoreError(Translate.instant('core.cannotconnecttrouble'));
}
if (typeof data == 'undefined') {
throw new CoreError(Translate.instant('core.cannotconnecttrouble'));
} else {
if (typeof data.token != 'undefined') {
return { token: data.token, siteUrl, privateToken: data.privatetoken };
} else {
if (typeof data.error != 'undefined') {
// We only allow one retry (to avoid loops).
if (!retry && data.errorcode == 'requirecorrectaccess') {
siteUrl = CoreUrlUtils.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.
const redirect = await CoreUtils.checkRedirect(loginUrl);
if (redirect) {
throw new CoreSiteError({
message: Translate.instant('core.login.sitehasredirect'),
});
}
}
throw new CoreSiteError({
message: data.error,
errorcode: data.errorcode,
});
}
throw new CoreError(Translate.instant('core.login.invalidaccount'));
}
}
}
/**
* 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.
* @param oauthId OAuth ID. Only if the authentication was using an OAuth method.
* @return A promise resolved with siteId when the site is added and the user is authenticated.
*/
async newSite(
siteUrl: string,
token: string,
privateToken: string = '',
login: boolean = true,
oauthId?: number,
): Promise<string> {
if (typeof login != 'boolean') {
login = true;
}
// Create a "candidate" site to fetch the site info.
let candidateSite = new CoreSite(undefined, siteUrl, token, undefined, privateToken, undefined, undefined);
let isNewSite = true;
try {
const info = await candidateSite.fetchSiteInfo();
const result = this.isValidMoodleVersion(info);
if (result != CoreSitesProvider.VALID_VERSION) {
return this.treatInvalidAppVersion(result, siteUrl);
}
const siteId = this.createSiteID(info.siteurl, info.username);
// Check if the site already exists.
const site = await CoreUtils.ignoreErrors<CoreSite>(this.getSite(siteId));
if (site) {
// Site already exists, update its data and use it.
isNewSite = false;
candidateSite = site;
candidateSite.setToken(token);
candidateSite.setPrivateToken(privateToken);
candidateSite.setInfo(info);
candidateSite.setOAuthId(oauthId);
candidateSite.setLoggedOut(false);
} else {
// New site, set site ID and info.
isNewSite = true;
candidateSite.setId(siteId);
candidateSite.setInfo(info);
candidateSite.setOAuthId(oauthId);
// Create database tables before login and before any WS call.
await this.migrateSiteSchemas(candidateSite);
}
// Try to get the site config.
let config: CoreSiteConfig | undefined;
try {
config = await this.getSiteConfig(candidateSite);
} catch (error) {
// Ignore errors if it's not a new site, we'll use the config already stored.
if (isNewSite) {
throw error;
}
}
if (typeof config != 'undefined') {
candidateSite.setConfig(config);
}
// Add site to sites list.
this.addSite(siteId, siteUrl, token, info, privateToken, config, oauthId);
this.sites[siteId] = candidateSite;
if (login) {
// Turn candidate site into current site.
this.currentSite = candidateSite;
// Store session.
this.login(siteId);
} else if (this.currentSite && this.currentSite.getId() == siteId) {
// Current site has just been updated, trigger the event.
CoreEvents.trigger(CoreEvents.SITE_UPDATED, info, siteId);
}
CoreEvents.trigger(CoreEvents.SITE_ADDED, info, siteId);
return siteId;
} catch (error) {
// Error invaliddevice is returned by Workplace server meaning the same as connecttoworkplaceapp.
if (error && error.errorcode == 'invaliddevice') {
return this.treatInvalidAppVersion(CoreSitesProvider.WORKPLACE_APP, siteUrl);
}
throw error;
}
}
/**
* 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 async treatInvalidAppVersion(result: number, siteUrl: string, siteId?: string): Promise<never> {
let errorCode: string | undefined;
let errorKey: string | undefined;
let translateParams;
switch (result) {
case CoreSitesProvider.MOODLE_APP:
errorKey = 'core.login.connecttomoodleapp';
errorCode = 'connecttomoodleapp';
break;
case CoreSitesProvider.WORKPLACE_APP:
errorKey = 'core.login.connecttoworkplaceapp';
errorCode = 'connecttoworkplaceapp';
break;
default:
errorCode = 'invalidmoodleversion';
errorKey = 'core.login.invalidmoodleversion';
translateParams = { $a: CoreSite.MINIMUM_MOODLE_VERSION };
}
if (siteId) {
await this.setSiteLoggedOut(siteId, true);
}
throw new CoreSiteError({
message: 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 CoreConstants.CONFIG.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: CoreSiteInfoResponse): number {
if (!info) {
return CoreSitesProvider.INVALID_VERSION;
}
const version31 = 2016052300;
const 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 CoreSitesProvider.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: CoreSiteInfoResponse): number {
const isWorkplace = !!info.functions && info.functions.some((func) => func.name == 'tool_program_get_user_programs');
if (typeof this.isWPApp == 'undefined') {
this.isWPApp = false; // @todo
}
if (!this.isWPApp && isWorkplace) {
return CoreSitesProvider.WORKPLACE_APP;
}
if (this.isWPApp && !isWorkplace) {
return CoreSitesProvider.MOODLE_APP;
}
return CoreSitesProvider.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 '';
}
/**
* 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).
* @param oauthId OAuth ID. Only if the authentication was using an OAuth method.
* @return Promise resolved when done.
*/
async addSite(
id: string,
siteUrl: string,
token: string,
info: CoreSiteInfoResponse,
privateToken: string = '',
config?: CoreSiteConfig,
oauthId?: number,
): Promise<void> {
const db = await this.appDB;
const entry: SiteDBEntry = {
id,
siteUrl,
token,
info: info ? JSON.stringify(info) : undefined,
privateToken,
config: config ? JSON.stringify(config) : undefined,
loggedOut: 0,
oauthId,
};
await db.insertRecord(SITES_TABLE_NAME, entry);
}
/**
* Check the app for a site and show a download dialogs if necessary.
*
* @param response Data obtained during site check.
*/
async checkApplication(response: CoreSiteCheckResponse): Promise<void> {
await this.checkRequiredMinimumVersion(response.config);
}
/**
* 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.
*/
async checkRequiredMinimumVersion(config?: CoreSitePublicConfigResponse, siteId?: string): Promise<void> {
if (!config || !config.tool_mobile_minimumversion) {
return;
}
const requiredVersion = this.convertVersionName(config.tool_mobile_minimumversion);
const appVersion = this.convertVersionName(CoreConstants.CONFIG.versionname);
if (requiredVersion > appVersion) {
const storesConfig: CoreStoreConfig = {
android: config.tool_mobile_androidappid,
ios: config.tool_mobile_iosappid,
mobile: config.tool_mobile_setuplink || 'https://download.moodle.org/mobile/',
default: config.tool_mobile_setuplink,
};
siteId = siteId || this.getCurrentSiteId();
const downloadUrl = CoreApp.getAppStoreUrl(storesConfig);
if (downloadUrl != null) {
// Do not block interface.
CoreDomUtils.showConfirm(
Translate.instant('core.updaterequireddesc', { $a: config.tool_mobile_minimumversion }),
Translate.instant('core.updaterequired'),
Translate.instant('core.download'),
Translate.instant(siteId ? 'core.mainmenu.logout' : 'core.cancel'),
).then(() => CoreUtils.openInBrowser(downloadUrl)).catch(() => {
// Do nothing.
});
} else {
CoreDomUtils.showAlert(
Translate.instant('core.updaterequired'),
Translate.instant('core.updaterequireddesc', { $a: config.tool_mobile_minimumversion }),
);
}
if (siteId) {
// Logout if it's the currentSite.
if (siteId == this.getCurrentSiteId()) {
await this.logout();
}
// Always expire the token.
await this.setSiteLoggedOut(siteId, true);
}
throw new CoreError('Current app version is lower than required version.');
}
}
/**
* 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.
*/
async loadSite(siteId: string, pageName?: string, params?: Record<string, unknown>): Promise<boolean> {
this.logger.debug(`Load site ${siteId}`);
const site = await this.getSite(siteId);
this.currentSite = site;
if (site.isLoggedOut()) {
// Logged out, trigger session expired event and stop.
CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {
pageName,
params,
}, site.getId());
return false;
}
// Check if local_mobile was installed to Moodle.
try {
await site.checkIfLocalMobileInstalledAndNotUsed();
// Local mobile was added. Throw invalid session to force reconnect and create a new token.
CoreEvents.trigger(CoreEvents.SESSION_EXPIRED, {
pageName,
params,
}, siteId);
return false;
} catch (error) {
let config: CoreSitePublicConfigResponse | undefined;
try {
config = await site.getPublicConfig();
} catch (error) {
// Error getting config, probably the site doesn't have the WS
}
try {
await this.checkRequiredMinimumVersion(config);
this.login(siteId);
// Update site info. We don't block the UI.
this.updateSiteInfo(siteId);
return true;
} catch (error) {
return false;
}
}
}
/**
* Get current site.
*
* @return Current site.
*/
getCurrentSite(): CoreSite | undefined {
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 {
return this.currentSite?.getUserId() || 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.
*/
async deleteSite(siteId: string): Promise<void> {
this.logger.debug(`Delete site ${siteId}`);
if (typeof this.currentSite != 'undefined' && this.currentSite.id == siteId) {
this.logout();
}
const site = await this.getSite(siteId);
await site.deleteDB();
// Site DB deleted, now delete the app from the list of sites.
delete this.sites[siteId];
try {
const db = await this.appDB;
await db.deleteRecords(SITES_TABLE_NAME, { id: siteId });
} catch (err) {
// DB remove shouldn't fail, but we'll go ahead even if it does.
}
// Site deleted from sites list, now delete the folder.
await site.deleteFolder();
CoreEvents.trigger(CoreEvents.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.
*/
async hasSites(): Promise<boolean> {
const db = await this.appDB;
const count = await db.countRecords(SITES_TABLE_NAME);
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.
*/
async getSite(siteId?: string): Promise<CoreSite> {
if (!siteId) {
if (this.currentSite) {
return this.currentSite;
}
throw new CoreError('No current site found.');
} else if (this.currentSite && this.currentSite.getId() == siteId) {
return this.currentSite;
} else if (typeof this.sites[siteId] != 'undefined') {
return this.sites[siteId];
} else {
// Retrieve and create the site.
const db = await this.appDB;
const data = await db.getRecord<SiteDBEntry>(SITES_TABLE_NAME, { id: siteId });
return this.makeSiteFromSiteListEntry(data);
}
}
/**
* Finds a site with a certain URL. It will return the first site found.
*
* @param siteUrl The site URL.
* @return Promise resolved with the site.
*/
async getSiteByUrl(siteUrl: string): Promise<CoreSite> {
const db = await this.appDB;
const data = await db.getRecord<SiteDBEntry>(SITES_TABLE_NAME, { siteUrl });
if (typeof this.sites[data.id] != 'undefined') {
return this.sites[data.id];
}
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: SiteDBEntry): Promise<CoreSite> {
// Parse info and config.
const info = entry.info ? <CoreSiteInfo> CoreTextUtils.parseJSON(entry.info) : undefined;
const config = entry.config ? <CoreSiteConfig> CoreTextUtils.parseJSON(entry.config) : undefined;
const site = new CoreSite(entry.id, entry.siteUrl, entry.token, info, entry.privateToken, config, entry.loggedOut == 1);
site.setOAuthId(entry.oauthId || undefined);
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.
*/
async getSiteDb(siteId?: string): Promise<SQLiteDB> {
const site = await this.getSite(siteId);
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) => 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.
*/
async getSites(ids?: string[]): Promise<CoreSiteBasicInfo[]> {
const db = await this.appDB;
const sites = await db.getAllRecords<SiteDBEntry>(SITES_TABLE_NAME);
const formattedSites: CoreSiteBasicInfo[] = [];
sites.forEach((site) => {
if (!ids || ids.indexOf(site.id) > -1) {
// Parse info.
const siteInfo = site.info ? <CoreSiteInfo> CoreTextUtils.parseJSON(site.info) : undefined;
const basicInfo: CoreSiteBasicInfo = {
id: site.id,
siteUrl: site.siteUrl,
fullName: siteInfo?.fullname,
siteName: CoreConstants.CONFIG.sitename == '' ? siteInfo?.sitename: CoreConstants.CONFIG.sitename,
avatar: siteInfo?.userpictureurl,
siteHomeId: 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.
*/
async getSortedSites(ids?: string[]): Promise<CoreSiteBasicInfo[]> {
const sites = await this.getSites(ids);
// Sort sites by url and ful lname.
sites.sort((a, b) => {
// First compare by site url without the protocol.
const urlA = a.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
const urlB = b.siteUrl.replace(/^https?:\/\//, '').toLowerCase();
const compare = urlA.localeCompare(urlB);
if (compare !== 0) {
return compare;
}
// If site url is the same, use fullname instead.
const fullNameA = a.fullName?.toLowerCase().trim();
const fullNameB = b.fullName?.toLowerCase().trim();
if (!fullNameA || !fullNameB) {
return 0;
}
return fullNameA.localeCompare(fullNameB);
});
return sites;
}
/**
* Get the list of IDs of sites stored and not logged out.
*
* @return Promise resolved when the sites IDs are retrieved.
*/
async getLoggedInSitesIds(): Promise<string[]> {
const db = await this.appDB;
const sites = await db.getRecords<SiteDBEntry>(SITES_TABLE_NAME, { loggedOut : 0 });
return sites.map((site) => site.id);
}
/**
* Get the list of IDs of sites stored.
*
* @return Promise resolved when the sites IDs are retrieved.
*/
async getSitesIds(): Promise<string[]> {
const db = await this.appDB;
const sites = await db.getAllRecords<SiteDBEntry>(SITES_TABLE_NAME);
return sites.map((site) => 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.
*/
async login(siteId: string): Promise<void> {
const db = await this.appDB;
const entry = {
id: 1,
siteId,
};
await db.insertRecord(CURRENT_SITE_TABLE_NAME, entry);
CoreEvents.trigger(CoreEvents.LOGIN, {}, siteId);
}
/**
* Logout the user.
*
* @return Promise resolved when the user is logged out.
*/
async logout(): Promise<void> {
let siteId: string | undefined;
const promises: Promise<unknown>[] = [];
if (this.currentSite) {
const db = await this.appDB;
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(db.deleteRecords(CURRENT_SITE_TABLE_NAME, { id: 1 }));
}
try {
await Promise.all(promises);
} finally {
CoreEvents.trigger(CoreEvents.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.
*/
async restoreSession(): Promise<void> {
if (this.sessionRestored) {
return Promise.reject(new CoreError('Session already restored.'));
}
const db = await this.appDB;
this.sessionRestored = true;
try {
const currentSite = await db.getRecord<CurrentSiteDBEntry>(CURRENT_SITE_TABLE_NAME, { id: 1 });
const siteId = currentSite.siteId;
this.logger.debug(`Restore session in site ${siteId}`);
await this.loadSite(siteId);
} catch (err) {
// 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.
*/
async setSiteLoggedOut(siteId: string, loggedOut: boolean): Promise<void> {
const db = await this.appDB;
const site = await this.getSite(siteId);
const newValues: Partial<SiteDBEntry> = {
loggedOut: loggedOut ? 1 : 0,
};
if (loggedOut) {
// Erase the token for security.
newValues.token = '';
site.token = '';
}
site.setLoggedOut(loggedOut);
await db.updateRecords(SITES_TABLE_NAME, 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.
*/
async updateSiteToken(siteUrl: string, username: string, token: string, privateToken: string = ''): Promise<void> {
const siteId = this.createSiteID(siteUrl, username);
await this.updateSiteTokenBySiteId(siteId, token, privateToken);
await 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.
*/
async updateSiteTokenBySiteId(siteId: string, token: string, privateToken: string = ''): Promise<void> {
const db = await this.appDB;
const site = await this.getSite(siteId);
const newValues: Partial<SiteDBEntry> = {
token,
privateToken,
loggedOut: 0,
};
site.token = token;
site.privateToken = privateToken;
site.setLoggedOut(false); // Token updated means the user authenticated again, not logged out anymore.
await db.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId });
}
/**
* Updates a site's info.
*
* @param siteid Site's ID.
* @return A promise resolved when the site is updated.
*/
async updateSiteInfo(siteId: string): Promise<void> {
const site = await this.getSite(siteId);
try {
const info = await site.fetchSiteInfo();
site.setInfo(info);
const versionCheck = this.isValidMoodleVersion(info);
if (versionCheck != CoreSitesProvider.VALID_VERSION) {
// The Moodle version is not supported, reject.
return this.treatInvalidAppVersion(versionCheck, site.getURL(), site.getId());
}
// Try to get the site config.
let config: CoreSiteConfig | undefined;
try {
config = await this.getSiteConfig(site);
} catch (error) {
// Error getting config, keep the current one.
}
const newValues: Partial<SiteDBEntry> = {
info: JSON.stringify(info),
loggedOut: site.isLoggedOut() ? 1 : 0,
};
if (typeof config != 'undefined') {
site.setConfig(config);
newValues.config = JSON.stringify(config);
}
try {
const db = await this.appDB;
await db.updateRecords(SITES_TABLE_NAME, newValues, { id: siteId });
} finally {
CoreEvents.trigger(CoreEvents.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<void> {
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).
*/
async 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 [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 (CoreUrlUtils.isAbsoluteURL(url)) {
// It has some protocol. Return empty array.
return [];
} else {
// No protocol, probably a relative URL. Return current site.
if (this.currentSite) {
return [this.currentSite.getId()];
} else {
return [];
}
}
}
try {
const db = await this.appDB;
const siteEntries = await db.getAllRecords<SiteDBEntry>(SITES_TABLE_NAME);
const ids: string[] = [];
const promises: Promise<unknown>[] = [];
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);
}
}
});
await Promise.all(promises);
return ids;
} catch (error) {
// Shouldn't happen.
return [];
}
}
/**
* Get the site ID stored in DB as current site.
*
* @return Promise resolved with the site ID.
*/
async getStoredCurrentSiteId(): Promise<string> {
const db = await this.appDB;
const currentSite = await db.getRecord<CurrentSiteDBEntry>(CURRENT_SITE_TABLE_NAME, { id: 1 });
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<CoreSitePublicConfigResponse> {
const temporarySite = new CoreSite(undefined, siteUrl);
return temporarySite.getPublicConfig();
}
/**
* Get site config.
*
* @param site The site to get the config.
* @return Promise resolved with config if available.
*/
protected async getSiteConfig(site: CoreSite): Promise<CoreSiteConfig | undefined> {
if (!site.wsAvailable('tool_mobile_get_config')) {
// WS not available, cannot get config.
return;
}
return await 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) => site.isFeatureDisabled(name));
}
/**
* 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) : false;
}
/**
* Register a site schema in current site.
* This function is meant for site plugins to create DB tables in current site. Tables created from within the app
* should use the registerCoreSiteSchema method instead.
*
* @param schema The schema to register.
* @return Promise resolved when done.
*/
async registerSiteSchema(schema: CoreSiteSchema): Promise<void> {
if (!this.currentSite) {
return;
}
try {
// Site has already been created, apply the schema directly.
const schemas: {[name: string]: CoreRegisteredSiteSchema} = {};
schemas[schema.name] = schema;
// Apply it to the specified site only.
(schema as CoreRegisteredSiteSchema).siteId = this.currentSite.getId();
await this.applySiteSchemas(this.currentSite, schemas);
} finally {
this.pluginsSiteSchemas[schema.name] = schema;
}
}
/**
* Install and upgrade all the registered schemas and tables.
*
* @param site Site.
* @return Promise resolved when done.
*/
migrateSiteSchemas(site: CoreSite): Promise<void> {
if (!site.id) {
return Promise.resolve();
}
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 = site.getDb().createTableFromSchema(SCHEMA_VERSIONS_TABLE_SCHEMA)
.then(() => this.applySiteSchemas(site, this.siteSchemas));
this.siteSchemasMigration[site.id] = promise;
return promise.finally(() => {
delete this.siteSchemasMigration[site.id!];
});
}
/**
* Install and upgrade the supplied schemas for a certain site.
*
* @param site Site.
* @param schemas Schemas to migrate.
* @return Promise resolved when done.
*/
protected async applySiteSchemas(site: CoreSite, schemas: {[name: string]: CoreRegisteredSiteSchema}): Promise<void> {
const db = site.getDb();
// Fetch installed versions of the schema.
const records = await db.getAllRecords<SchemaVersionsDBEntry>(SCHEMA_VERSIONS_TABLE_NAME);
const versions: {[name: string]: number} = {};
records.forEach((record) => {
versions[record.name] = record.version;
});
const promises: Promise<void>[] = [];
for (const name in schemas) {
const schema = schemas[name];
const oldVersion = versions[name] || 0;
if (oldVersion >= schema.version || (schema.siteId && site.getId() != schema.siteId)) {
// Version already applied or the schema shouldn't be registered to this site.
continue;
}
this.logger.debug(`Migrating schema '${name}' of ${site.id} from version ${oldVersion} to ${schema.version}`);
promises.push(this.applySiteSchema(site, schema, oldVersion));
}
await Promise.all(promises);
}
/**
* Install and upgrade the supplied schema for a certain site.
*
* @param site Site.
* @param schema Schema to migrate.
* @param oldVersion Old version of the schema.
* @return Promise resolved when done.
*/
protected async applySiteSchema(site: CoreSite, schema: CoreRegisteredSiteSchema, oldVersion: number): Promise<void> {
if (!site.id) {
return;
}
const db = site.getDb();
if (schema.tables) {
await db.createTablesFromSchema(schema.tables);
}
if (schema.migrate && oldVersion > 0) {
await schema.migrate(db, oldVersion, site.id);
}
// Set installed version.
await db.insertRecord(SCHEMA_VERSIONS_TABLE_NAME, { name: schema.name, version: schema.version });
}
/**
* 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.
*/
async isStoredRootURL(url: string, username?: string): Promise<{site?: CoreSite; siteIds: string[]}> {
// Check if the site is stored.
const siteIds = await this.getSiteIdsFromUrl(url, true, username);
const result: {site?: CoreSite; siteIds: string[]} = {
siteIds,
};
if (!siteIds.length) {
return result;
}
// If more than one site is returned it usually means there are different users stored. Use any of them.
const site = await this.getSite(siteIds[0]);
const siteUrl = CoreTextUtils.removeEndingSlash(
CoreUrlUtils.removeProtocolAndWWW(site.getURL()),
);
const treatedUrl = CoreTextUtils.removeEndingSlash(CoreUrlUtils.removeProtocolAndWWW(url));
if (siteUrl == treatedUrl) {
result.site = site;
}
return result;
}
/**
* Returns the Site Schema names that can be cleared on space storage.
*
* @param site The site that will be cleared.
* @return Name of the site schemas.
*/
getSiteTableSchemasToClear(site: CoreSite): string[] {
let reset: string[] = [];
const schemas = Object.values(this.siteSchemas).concat(Object.values(this.pluginsSiteSchemas));
schemas.forEach((schema) => {
if (schema.canBeCleared && (!schema.siteId || site.getId() == schema.siteId)) {
reset = reset.concat(schema.canBeCleared);
}
});
return reset;
}
/**
* Returns presets for a given reading strategy.
*
* @param strategy Reading strategy.
* @return PreSets options object.
*/
getReadingStrategyPreSets(strategy?: CoreSitesReadingStrategy): CoreSiteWSPreSets {
switch (strategy) {
case CoreSitesReadingStrategy.PreferCache:
return {
omitExpires: true,
};
case CoreSitesReadingStrategy.OnlyCache:
return {
omitExpires: true,
forceOffline: true,
};
case CoreSitesReadingStrategy.PreferNetwork:
return {
getFromCache: false,
};
case CoreSitesReadingStrategy.OnlyNetwork:
return {
getFromCache: false,
emergencyCache: false,
};
default:
return {};
}
}
/**
* Returns site info found on the backend.
*
* @param search Searched text.
* @return Site info list.
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async findSites(search: string): Promise<CoreLoginSiteInfo[]> {
return [];
}
}
export const CoreSites = makeSingleton(CoreSitesProvider);
/**
* Response of checking if a site exists and its configuration.
*/
export type 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?: CoreSitePublicConfigResponse;
};
/**
* Response of getting user token.
*/
export type CoreSiteUserTokenResponse = {
/**
* User token.
*/
token: string;
/**
* Site URL to use.
*/
siteUrl: string;
/**
* User private token.
*/
privateToken?: string;
};
/**
* Site's basic info.
*/
export type 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 type 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<void> | void;
};
/**
* Data about sites to be listed.
*/
export type CoreLoginSiteInfo = {
/**
* Site name.
*/
name: string;
/**
* Site alias.
*/
alias?: string;
/**
* URL of the site.
*/
url: string;
/**
* Image URL of the site.
*/
imageurl?: string;
/**
* City of the site.
*/
city?: string;
/**
* Countrycode of the site.
*/
countrycode?: string;
};
/**
* Registered site schema.
*/
export type CoreRegisteredSiteSchema = CoreSiteSchema & {
/**
* Site ID to apply the schema to. If not defined, all sites.
*/
siteId?: string;
};
/**
* Possible reading strategies (for cache).
*/
export const enum CoreSitesReadingStrategy {
OnlyCache,
PreferCache,
OnlyNetwork,
PreferNetwork,
}
/**
* Common options used when calling a WS through CoreSite.
*/
export type CoreSitesCommonWSOptions = {
readingStrategy?: CoreSitesReadingStrategy; // Reading strategy.
siteId?: string; // Site ID. If not defined, current site.
};
/**
* Data about a certain demo site.
*/
export type CoreSitesDemoSiteData = {
url: string;
username: string;
password: string;
};
/**
* Response of calls to login/token.php.
*/
export type CoreSitesLoginTokenResponse = {
token?: string;
privatetoken?: string;
error?: string;
errorcode?: string;
stacktrace?: string;
debuginfo?: string;
reproductionlink?: string;
};