Pau Ferrer Ocaña 5053d579f3 MOBILE-3635 login: Send extra parameter on token.php check
The extra parameter can be used by Moodle to avoid throwing an error
in server logs because other parameters e.g. username are not supplied.
2021-03-17 10:58:45 +01:00

1896 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 = {
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);
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) => 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 = {
token: '', // Erase the token for security.
loggedOut: loggedOut ? 1 : 0,
};
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 = {
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: Record<string, string | number> = {
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) {
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;
};