// (C) Copyright 2015 Moodle Pty Ltd. // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. import { Injectable } from '@angular/core'; import { CoreError } from '@classes/errors/error'; import { CoreWSError } from '@classes/errors/wserror'; import { CoreLoginHelper } from '@features/login/services/login-helper'; import { CoreNavigator } from '@services/navigator'; import { CoreSites, CoreSitesCommonWSOptions } from '@services/sites'; import { CoreWSExternalWarning } from '@services/ws'; import { makeSingleton } from '@singletons'; import { POLICY_PAGE_NAME, SITE_POLICY_PAGE_NAME } from '../constants'; import { CoreSite } from '@classes/sites/site'; /** * Service that provides some common features regarding policies. */ @Injectable({ providedIn: 'root' }) export class CorePolicyService { protected static readonly ROOT_CACHE_KEY = 'CorePolicy:'; /** * Accept all mandatory site policies. * * @param siteId Site ID. If not defined, current site. * @returns Promise resolved if success, rejected if failure. */ async acceptMandatorySitePolicies(siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const result = await site.write('core_user_agree_site_policy', {}); if (result.status) { return; } if (!result.warnings?.length) { throw new CoreError('Cannot agree site policy'); } // Check if there is a warning 'alreadyagreed'. const found = result.warnings.some((warning) => warning.warningcode === 'alreadyagreed'); if (found) { // Policy already agreed, treat it as a success. return; } // Another warning, reject. throw new CoreWSError(result.warnings[0]); } /** * Get the URL to view the site policy (or all the site policies in a single page if there's more than one). * * @param siteId Site ID. If not defined, current site. * @returns Promise resolved with the site policy. */ async getSitePoliciesURL(siteId?: string): Promise { const site = await CoreSites.getSite(siteId); let sitePolicy: string | undefined; try { // Try to get the latest config, maybe the site policy was just added or has changed. sitePolicy = await site.getConfig('sitepolicy', true); } catch (error) { // Cannot get config, try to get the site policy using signup settings. const settings = await CoreLoginHelper.getEmailSignupSettings(site.getURL()); sitePolicy = settings.sitepolicy; } if (!sitePolicy) { throw new CoreError('Cannot retrieve site policy'); } return sitePolicy; } /** * Get the next policies to accept. * * @param options Options * @returns Next pending policies */ async getNextPendingPolicies(options: CoreSitesCommonWSOptions = {}): Promise { const policies = await this.getUserAcceptances(options); const pendingPolicies: CorePolicySitePolicy[] = []; for (const i in policies) { const policy = policies[i]; const hasAccepted = policy.acceptance?.status === 1; const hasDeclined = policy.acceptance?.status === 0; if (hasAccepted || (hasDeclined && policy.optional === 1)) { // Policy already answered, ignore. continue; } if (policy.agreementstyle === CorePolicyAgreementStyle.OwnPage) { // Policy needs to be accepted on its own page, it's the next policy to accept. return [policy]; } pendingPolicies.push(policy); } return pendingPolicies; } /** * Get user acceptances. * * @param options Options * @returns List of policies with their acceptances. */ async getUserAcceptances(options: CorePolicyGetAcceptancesOptions = {}): Promise { const site = await CoreSites.getSite(options.siteId); const userId = options.userId || site.getUserId(); const data: CorePolicyGetUserAcceptancesWSParams = { userid: userId, }; const preSets = { cacheKey: this.getUserAcceptancesCacheKey(userId), updateFrequency: CoreSite.FREQUENCY_RARELY, ...CoreSites.getReadingStrategyPreSets(options.readingStrategy), }; const response = await site.read('tool_policy_get_user_acceptances', data, preSets); if (response.warnings?.length) { throw new CoreWSError(response.warnings[0]); } return response.policies; } /** * Get the cache key for the get user acceptances call. * * @param userId ID of the user to get the badges from. * @returns Cache key. */ protected getUserAcceptancesCacheKey(userId: number): string { return CorePolicyService.ROOT_CACHE_KEY + 'userAcceptances:' + userId; } /** * Open page to accept site policies. * * @param siteId Site ID. If not defined, current site. */ goToAcceptSitePolicies(siteId?: string): void { siteId = siteId || CoreSites.getCurrentSiteId(); if (!siteId || siteId != CoreSites.getCurrentSiteId()) { // Only current site allowed. return; } const routePath = `/${POLICY_PAGE_NAME}/${SITE_POLICY_PAGE_NAME}`; // If current page is already site policy, stop. if (CoreNavigator.isCurrent(routePath)) { return; } CoreNavigator.navigate(routePath, { params: { siteId }, reset: true }); } /** * Invalidate acceptances WS call. * * @param options Options. * @returns Promise resolved when data is invalidated. */ async invalidateAcceptances(options: {userId?: number; siteId?: string} = {}): Promise { const site = await CoreSites.getSite(options.siteId); await site.invalidateWsCacheForKey(this.getUserAcceptancesCacheKey(options.userId || site.getUserId())); } /** * Check whether a site allows getting and setting acceptances. * * @param siteId Site Id. * @returns Whether the site allows getting and setting acceptances. */ async isManageAcceptancesAvailable(siteId?: string): Promise { const site = await CoreSites.getSite(siteId); return site.wsAvailable('tool_policy_get_user_acceptances') && site.wsAvailable('tool_policy_set_acceptances_status'); } /** * Set user acceptances. * * @param policies Policies to accept or decline. Keys are policy version id, value is whether to accept or decline. * @param siteId Site ID. If not defined, current site. * @returns New value for policyagreed. */ async setUserAcceptances(policies: Record, siteId?: string): Promise { const site = await CoreSites.getSite(siteId); const data: CorePolicySetAcceptancesWSParams = { userid: site.getUserId(), policies: Object.keys(policies).map((versionId) => ({ versionid: Number(versionId), status: policies[versionId], })), }; const response = await site.write('tool_policy_set_acceptances_status', data); if (response.warnings?.length) { throw new CoreWSError(response.warnings[0]); } return response.policyagreed; } } export const CorePolicy = makeSingleton(CorePolicyService); /** * Options for get policy acceptances. */ type CorePolicyGetAcceptancesOptions = CoreSitesCommonWSOptions & { userId?: number; // User ID. If not defined, current user. }; /** * Result of WS core_user_agree_site_policy. */ type CorePolicyAgreeSitePolicyResult = { status: boolean; // Status: true only if we set the policyagreed to 1 for the user. warnings?: CoreWSExternalWarning[]; }; /** * Params of tool_policy_get_user_acceptances WS. */ type CorePolicyGetUserAcceptancesWSParams = { userid?: number; // The user id we want to retrieve the acceptances. }; /** * Data returned by tool_policy_get_user_acceptances WS. */ type CorePolicyGetUserAcceptancesWSResponse = { policies: CorePolicySitePolicy[]; warnings?: CoreWSExternalWarning[]; }; /** * Policy data returned by tool_policy_get_user_acceptances WS. */ export type CorePolicySitePolicy = { policyid: number; // The policy id. versionid: number; // The policy version id. agreementstyle: number; // The policy agreement style. 0: consent page, 1: own page. optional: number; // Whether the policy is optional. 0: compulsory, 1: optional. revision: string; // The policy revision. status: number; // The policy status. 0: draft, 1: active, 2: archived. name: string; // The policy name. summary?: string; // The policy summary. summaryformat: number; // Summary format (1 = HTML, 0 = MOODLE, 2 = PLAIN, or 4 = MARKDOWN). content?: string; // The policy content. contentformat: number; // Content format (1 = HTML, 0 = MOODLE, 2 = PLAIN, or 4 = MARKDOWN). acceptance?: CorePolicySitePolicyAcceptance; // Acceptance status for the given user. canaccept: boolean; // Whether the policy can be accepted. candecline: boolean; // Whether the policy can be declined. canrevoke: boolean; // Whether the policy can be revoked. }; /** * Policy acceptance data returned by tool_policy_get_user_acceptances WS. */ export type CorePolicySitePolicyAcceptance = { status: number; // The acceptance status. 0: declined, 1: accepted. lang: string; // The policy lang. timemodified: number; // The time the acceptance was set. usermodified: number; // The user who accepted. note?: string; // The policy note/remarks. modfullname?: string; // The fullname who accepted on behalf. }; /** * Params of tool_policy_set_acceptances_status WS. */ type CorePolicySetAcceptancesWSParams = { policies: { versionid: number; // The policy version id. status: number; // The policy acceptance status. 0: decline, 1: accept. }[]; // Policies acceptances for the given user. userid?: number; // The user id we want to set the acceptances. Default is the current user. }; /** * Data returned by tool_policy_set_acceptances_status WS. */ type CorePolicySetAcceptancesWSResponse = { policyagreed: number; // Whether the user has provided acceptance to all current site policies. 1 if yes, 0 if not. warnings?: CoreWSExternalWarning[]; }; /** * Agreement style. */ export enum CorePolicyAgreementStyle { ConsentPage = 0, // Policy to be accepted together with others on the consent page. OwnPage = 1, // Policy to be accepted on its own page before reaching the consent page. } /** * Status of a policy. */ export enum CorePolicyStatus { Draft = 0, Active = 1, Archived = 2, }