// (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 { CoreConstants } from '@/core/constants'; import { CoreError } from '@classes/errors/error'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreSitesReadingStrategy } from '@services/sites'; import { CoreTextUtils } from '@services/utils/text'; import { CoreUrl, CoreUrlPartNames } from '@singletons/url'; import { CoreWS, CoreWSAjaxPreSets, CoreWSExternalWarning } from '@services/ws'; import { CorePath } from '@singletons/path'; /** * Class that represents a Moodle site where the user still hasn't authenticated. */ export class CoreUnauthenticatedSite { siteUrl: string; protected publicConfig?: CoreSitePublicConfigResponse; /** * Create a site. * * @param siteUrl Site URL. * @param publicConfig Site public config. */ constructor(siteUrl: string, publicConfig?: CoreSitePublicConfigResponse) { this.siteUrl = CoreUrl.removeUrlParts( siteUrl, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], ); // Make sure the URL doesn't have params. if (publicConfig) { this.setPublicConfig(publicConfig); } } /** * Get site URL. * * @returns Site URL. */ getURL(): string { return this.siteUrl; } /** * Set site URL. * * @param url Site URL. */ setURL(url: string): void { this.siteUrl = url; } /** * Get site info. * * @returns Site info. */ getInfo(): CoreSiteInfo | undefined { // Cannot retrieve info for unauthenticated sites. return undefined; } /** * Check if the site has info with the given key and it doesn't contain an empty value. * * @param key Info key. * @returns Whether the key is filled within site info. */ hasInfo(key: string): boolean { const info = this.getInfo()?.[key] ?? null; return info === false || info === 0 || !!info; } /** * Get site name. * * @returns Site name. */ async getSiteName(): Promise { if (this.isDemoModeSite()) { return CoreConstants.CONFIG.appname; } const siteName = this.getInfo()?.sitename || this.publicConfig?.sitename; if (siteName) { return siteName; } // Fallback. const isSingleFixedSite = await CoreLoginHelper.isSingleFixedSite(); if (isSingleFixedSite) { const sites = await CoreLoginHelper.getAvailableSites(); return sites[0].name; } return ''; } /** * Check whether the app should use the local logo instead of the remote one. * * @returns Whether local logo is forced. */ forcesLocalLogo(): boolean { return CoreConstants.CONFIG.forceLoginLogo || this.isDemoModeSite(); } /** * Get logo URL from a site public config. * * @param config Site public config. * @returns Logo URL. */ getLogoUrl(config?: CoreSitePublicConfigResponse): string | undefined { config = config ?? this.publicConfig; if (!config || this.forcesLocalLogo()) { return 'assets/img/login_logo.png'; } return config.logourl || config.compactlogourl || 'assets/img/login_logo.png'; } /** * Returns a url to link an specific page on the site. * * @param path Path of the url to go to. * @param params Object with the params to add. * @param anchor Anchor text if needed. * @returns URL with params. */ createSiteUrl(path: string, params?: Record, anchor?: string): string { return CoreUrl.addParamsToUrl(CorePath.concatenatePaths(this.siteUrl, path), params, anchor); } /** * Check if a URL belongs to this site. * * @param url URL to check. * @returns Whether the URL belongs to this site. */ containsUrl(url?: string): boolean { if (!url) { return false; } const siteUrl = CoreTextUtils.addEndingSlash( CoreUrl.removeUrlParts(this.siteUrl, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain]), ); url = CoreTextUtils.addEndingSlash(CoreUrl.removeUrlParts(url, [CoreUrlPartNames.Protocol, CoreUrlPartNames.WWWInDomain])); return url.indexOf(siteUrl) == 0; } /** * Get the public config of this site. * * @param options Options. * @returns Promise resolved with public config. Rejected with an object if error, see CoreWSProvider.callAjax. */ async getPublicConfig(options: { readingStrategy?: CoreSitesReadingStrategy } = {}): Promise { const ignoreCache = options.readingStrategy === CoreSitesReadingStrategy.ONLY_NETWORK || options.readingStrategy === CoreSitesReadingStrategy.PREFER_NETWORK; if (!ignoreCache && this.publicConfig) { return this.publicConfig; } if (options.readingStrategy === CoreSitesReadingStrategy.ONLY_CACHE) { throw new CoreError('Cache not available to read public config'); } try { const config = await this.requestPublicConfig(); this.setPublicConfig(config); return config; } catch (error) { if (options.readingStrategy === CoreSitesReadingStrategy.ONLY_NETWORK || !this.publicConfig) { throw error; } return this.publicConfig; } } /** * Set public config. * * @param publicConfig Public config. */ setPublicConfig(publicConfig: CoreSitePublicConfigResponse): void { publicConfig.tool_mobile_disabledfeatures = CoreTextUtils.treatDisabledFeatures(publicConfig.tool_mobile_disabledfeatures ?? ''); this.publicConfig = publicConfig; } /** * Perform a request to the server to get the public config of this site. * * @returns Promise resolved with public config. */ protected async requestPublicConfig(): Promise { const preSets: CoreWSAjaxPreSets = { siteUrl: this.siteUrl, }; let config: CoreSitePublicConfigResponse; try { config = await CoreWS.callAjax('tool_mobile_get_public_config', {}, preSets); } catch (error) { if (!error || error.errorcode !== 'codingerror' || (this.getInfo() && !this.isAjaxGetSupported())) { throw error; } // This error probably means that there is a redirect in the site. Try to use a GET request. preSets.noLogin = true; preSets.useGet = true; try { config = await CoreWS.callAjax('tool_mobile_get_public_config', {}, preSets); } catch (error2) { if (this.isAjaxGetSupported()) { // GET is supported, return the second error. throw error2; } else { // GET not supported or we don't know if it's supported. Return first error. throw error; } } } // Use the wwwroot returned by the server. if (config.httpswwwroot) { this.siteUrl = CoreUrl.removeUrlParts( config.httpswwwroot, [CoreUrlPartNames.Query, CoreUrlPartNames.Fragment], ); // Make sure the URL doesn't have params. } return config; } /** * Check if GET method is supported for AJAX calls. * * @returns Whether it's supported. * @since Moodle 3.8 */ protected isAjaxGetSupported(): boolean { // We don't know if it's supported, assume it's not. return false; } /** * Check if a URL to a file belongs to the site and uses the pluginfileurl or tokenpluginfileurl endpoints. * * @param url File URL to check. * @returns Whether it's a site file URL. */ isSitePluginFileUrl(url: string): boolean { const isPluginFileUrl = CoreUrl.isPluginFileUrl(url) || CoreUrl.isTokenPluginFileUrl(url); if (!isPluginFileUrl) { return false; } return this.containsUrl(url); } /** * Check if a URL to a file belongs to the site and is a theme image file. * * @param url File URL to check. * @returns Whether it's a site theme image URL. */ isSiteThemeImageUrl(url: string): boolean { if (!CoreUrl.isThemeImageUrl(url)) { return false; } return this.containsUrl(url); } /** * Check if the site is a demo mode site. * * @returns Whether the site is a demo mode site. */ isDemoModeSite(): boolean { const demoSiteData = CoreLoginHelper.getDemoModeSiteInfo(); return this.containsUrl(demoSiteData?.url); } /** * Check whether informative links should be displayed for this site. * * @returns Whether informative links should be displayed. */ shouldDisplayInformativeLinks(): boolean { return !CoreConstants.CONFIG.hideInformativeLinks && !this.isDemoModeSite(); } /** * Check if a certain feature is disabled in the site. * * @param name Name of the feature to check. * @returns Whether it's disabled. */ isFeatureDisabled(name: string): boolean { const disabledFeatures = this.getDisabledFeatures(); if (!disabledFeatures) { return false; } const regEx = new RegExp('(,|^)' + CoreTextUtils.escapeForRegex(name) + '(,|$)', 'g'); return !!disabledFeatures.match(regEx); } /** * Get disabled features string. * * @returns Disabled features. */ protected getDisabledFeatures(): string | undefined { return this.publicConfig?.tool_mobile_disabledfeatures; } } /** * Result of WS core_webservice_get_site_info. */ export type CoreSiteInfoResponse = { sitename: string; // Site name. username: string; // Username. firstname: string; // First name. lastname: string; // Last name. fullname: string; // User full name. lang: string; // Current language. userid: number; // User id. siteurl: string; // Site url. userpictureurl: string; // The user profile picture. functions: { name: string; // Function name. version: string; // The version number of the component to which the function belongs. }[]; downloadfiles?: number; // 1 if users are allowed to download files, 0 if not. uploadfiles?: number; // 1 if users are allowed to upload files, 0 if not. release?: string; // Moodle release number. version?: string; // Moodle version number. mobilecssurl?: string; // Mobile custom CSS theme. advancedfeatures?: { // Advanced features availability. name: string; // Feature name. value: number; // Feature value. Usually 1 means enabled. }[]; usercanmanageownfiles?: boolean; // True if the user can manage his own files. userquota?: number; // User quota (bytes). 0 means user can ignore the quota. usermaxuploadfilesize?: number; // User max upload file size (bytes). -1 means the user can ignore the upload file size. userhomepage?: CoreSiteInfoUserHomepage; // The default home page for the user. userprivateaccesskey?: string; // Private user access key for fetching files. siteid?: number; // Site course ID. sitecalendartype?: string; // Calendar type set in the site. usercalendartype?: string; // Calendar typed used by the user. userissiteadmin?: boolean; // Whether the user is a site admin or not. theme?: string; // Current theme for the user. limitconcurrentlogins?: number; // @since 4.0. Number of concurrent sessions allowed. usersessionscount?: number; // @since 4.0. Number of active sessions for current user. Only if limitconcurrentlogins is used. policyagreed?: number; // @since 4.4. Whether user accepted all the policies. }; /** * Site info, including some calculated data. */ export type CoreSiteInfo = CoreSiteInfoResponse & { functionsByName?: { [name: string]: { name: string; // Function name. version: string; // The version number of the component to which the function belongs. }; }; }; /** * Enum constants that define default user home page. */ export enum CoreSiteInfoUserHomepage { HOMEPAGE_SITE = 0, // Site home. HOMEPAGE_MY = 1, // Dashboard. HOMEPAGE_MYCOURSES = 3, // My courses. } /** * Possible values for 'supportavailability' config. */ export const enum CoreSiteConfigSupportAvailability { Disabled = 0, Authenticated = 1, Anyone = 2, } /** * Result of WS tool_mobile_get_public_config. */ export type CoreSitePublicConfigResponse = { wwwroot: string; // Site URL. httpswwwroot: string; // Site https URL (if httpslogin is enabled). sitename: string; // Site name. guestlogin: number; // Whether guest login is enabled. rememberusername: number; // Values: 0 for No, 1 for Yes, 2 for optional. authloginviaemail: number; // Whether log in via email is enabled. registerauth: string; // Authentication method for user registration. forgottenpasswordurl: string; // Forgotten password URL. authinstructions: string; // Authentication instructions. authnoneenabled: number; // Whether auth none is enabled. enablewebservices: number; // Whether Web Services are enabled. enablemobilewebservice: number; // Whether the Mobile service is enabled. maintenanceenabled: number; // Whether site maintenance is enabled. maintenancemessage: string; // Maintenance message. logourl?: string; // The site logo URL. compactlogourl?: string; // The site compact logo URL. typeoflogin: TypeOfLogin; // The type of login. 1 for app, 2 for browser, 3 for embedded. launchurl?: string; // SSO login launch URL. mobilecssurl?: string; // Mobile custom CSS theme. // eslint-disable-next-line @typescript-eslint/naming-convention tool_mobile_disabledfeatures?: string; // Disabled features in the app. identityproviders?: CoreSiteIdentityProvider[]; // Identity providers. country?: string; // Default site country. agedigitalconsentverification?: boolean; // Whether age digital consent verification is enabled. supportname?: string; // Site support contact name (only if age verification is enabled). supportemail?: string; // Site support contact email (only if age verification is enabled). supportavailability?: CoreSiteConfigSupportAvailability; supportpage?: string; // Site support contact url. autolang?: number; // Whether to detect default language from browser setting. lang?: string; // Default language for the site. langmenu?: number; // Whether the language menu should be displayed. langlist?: string; // Languages on language menu. locale?: string; // Sitewide locale. // eslint-disable-next-line @typescript-eslint/naming-convention tool_mobile_minimumversion?: string; // Minimum required version to access. // eslint-disable-next-line @typescript-eslint/naming-convention tool_mobile_iosappid?: string; // IOS app's unique identifier. // eslint-disable-next-line @typescript-eslint/naming-convention tool_mobile_androidappid?: string; // Android app's unique identifier. // eslint-disable-next-line @typescript-eslint/naming-convention tool_mobile_setuplink?: string; // App download page. tool_mobile_qrcodetype?: CoreSiteQRCodeType; // eslint-disable-line @typescript-eslint/naming-convention warnings?: CoreWSExternalWarning[]; showloginform?: number; // @since 4.5. Display default login form. }; /** * QR Code type enumeration. */ export enum CoreSiteQRCodeType { QR_CODE_DISABLED = 0, // QR code disabled value QR_CODE_URL = 1, // QR code type URL value QR_CODE_LOGIN = 2, // QR code type login value } /** * Identity provider. */ export type CoreSiteIdentityProvider = { name: string; // The identity provider name. iconurl: string; // The icon URL for the provider. url: string; // The URL of the provider. }; /** * The type of login. 1 for app, 2 for browser, 3 for embedded. */ export enum TypeOfLogin { APP = 1, BROWSER = 2, // SSO in browser window is required. EMBEDDED = 3, // SSO in embedded browser is required. }