From b975e6f0d8f6eec3b46142c2a952dbce45c68779 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 20 Jul 2023 13:03:29 +0200 Subject: [PATCH 1/4] MOBILE-4323 enrol: Create enrol delegates Co-authored: dpalou --- scripts/langindex.json | 15 +- src/addons/addons.module.ts | 2 + .../services/handlers/course-option.ts | 4 +- .../services/handlers/course-option.ts | 4 +- src/addons/enrol/enrol.module.ts | 30 ++ src/addons/enrol/fee/fee.module.ts | 30 ++ .../enrol/fee/services/enrol-handler.ts | 38 ++ src/addons/enrol/guest/guest.module.ts | 30 ++ src/addons/enrol/guest/lang.json | 5 + .../enrol/guest/services/enrol-handler.ts | 142 ++++++++ src/addons/enrol/guest/services/guest.ts | 158 +++++++++ src/addons/enrol/paypal/paypal.module.ts | 30 ++ .../enrol/paypal/services/enrol-handler.ts | 38 ++ src/addons/enrol/self/lang.json | 7 + src/addons/enrol/self/self.module.ts | 30 ++ .../enrol/self/services/enrol-handler.ts | 176 ++++++++++ src/addons/enrol/self/services/self.ts | 152 ++++++++ .../notes/services/handlers/course-option.ts | 4 +- .../password-modal/password-modal.ts | 7 +- .../components/course-format/course-format.ts | 2 +- src/core/features/course/lang.json | 3 - .../pages/course-summary/course-summary.html | 30 +- .../course-summary/course-summary.page.ts | 331 +++++++----------- .../features/course/services/course-helper.ts | 15 +- .../services/course-options-delegate.ts | 6 +- .../course/tests/behat/guest_access.feature | 1 - .../core-courses-course-list-item.html | 2 +- .../course-list-item/course-list-item.ts | 29 +- src/core/features/courses/lang.json | 4 - src/core/features/courses/services/courses.ts | 237 ++----------- src/core/features/enrol/enrol.module.ts | 24 ++ .../features/enrol/services/enrol-delegate.ts | 240 +++++++++++++ .../features/enrol/services/enrol-helper.ts | 128 +++++++ src/core/features/enrol/services/enrol.ts | 138 ++++++++ src/core/features/features.module.ts | 2 + src/core/services/utils/dom.ts | 3 + 36 files changed, 1599 insertions(+), 498 deletions(-) create mode 100644 src/addons/enrol/enrol.module.ts create mode 100644 src/addons/enrol/fee/fee.module.ts create mode 100644 src/addons/enrol/fee/services/enrol-handler.ts create mode 100644 src/addons/enrol/guest/guest.module.ts create mode 100644 src/addons/enrol/guest/lang.json create mode 100644 src/addons/enrol/guest/services/enrol-handler.ts create mode 100644 src/addons/enrol/guest/services/guest.ts create mode 100644 src/addons/enrol/paypal/paypal.module.ts create mode 100644 src/addons/enrol/paypal/services/enrol-handler.ts create mode 100644 src/addons/enrol/self/lang.json create mode 100644 src/addons/enrol/self/self.module.ts create mode 100644 src/addons/enrol/self/services/enrol-handler.ts create mode 100644 src/addons/enrol/self/services/self.ts create mode 100644 src/core/features/enrol/enrol.module.ts create mode 100644 src/core/features/enrol/services/enrol-delegate.ts create mode 100644 src/core/features/enrol/services/enrol-helper.ts create mode 100644 src/core/features/enrol/services/enrol.ts diff --git a/scripts/langindex.json b/scripts/langindex.json index 5b5458bc8cf..5c81137e069 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -237,6 +237,14 @@ "addon.coursecompletion.requirement": "block_completionstatus", "addon.coursecompletion.status": "moodle", "addon.coursecompletion.viewcoursereport": "completion", + "addon.enrol_guest.guestaccess_withoutpassword": "enrol_guest", + "addon.enrol_guest.guestaccess_withpassword": "enrol_guest", + "addon.enrol_guest.passwordinvalid": "enrol_guest", + "addon.enrol_self.confirmselfenrol": "local_moodlemobileapp", + "addon.enrol_self.errorselfenrol": "local_moodlemobileapp", + "addon.enrol_self.nopassword": "enrol_self", + "addon.enrol_self.password": "enrol_self", + "addon.enrol_self.pluginname": "enrol_self", "addon.messageoutput_airnotifier.processorsettingsdesc": "local_moodlemobileapp", "addon.messageoutput_airnotifier.pushdisabledwarning": "local_moodlemobileapp", "addon.messages.acceptandaddcontact": "message", @@ -1590,9 +1598,6 @@ "core.course.errordownloadingsection": "local_moodlemobileapp", "core.course.errorgetmodule": "local_moodlemobileapp", "core.course.failed": "completion", - "core.course.guestaccess": "enrol_guest/pluginname", - "core.course.guestaccess_passwordinvalid": "enrol_guest/passwordinvalid", - "core.course.guestaccess_withpassword": "enrol_guest", "core.course.hiddenfromstudents": "moodle", "core.course.hiddenoncoursepage": "moodle", "core.course.highlighted": "moodle", @@ -1623,7 +1628,6 @@ "core.coursedetails": "moodle", "core.coursenogroups": "local_moodlemobileapp", "core.courses.addtofavourites": "block_myoverview", - "core.courses.allowguests": "enrol_guest", "core.courses.aria:coursecategory": "course", "core.courses.aria:coursename": "course", "core.courses.aria:courseprogress": "block_myoverview", @@ -1633,7 +1637,6 @@ "core.courses.cannotretrievemorecategories": "local_moodlemobileapp", "core.courses.categories": "moodle", "core.courses.completeenrolmentbrowser": "local_moodlemobileapp", - "core.courses.confirmselfenrol": "local_moodlemobileapp", "core.courses.courses": "moodle", "core.courses.downloadcourses": "local_moodlemobileapp", "core.courses.enrolme": "local_moodlemobileapp", @@ -1641,7 +1644,6 @@ "core.courses.errorloadcourses": "local_moodlemobileapp", "core.courses.errorloadplugins": "local_moodlemobileapp", "core.courses.errorsearching": "local_moodlemobileapp", - "core.courses.errorselfenrol": "local_moodlemobileapp", "core.courses.favourite": "course", "core.courses.filtermycourses": "local_moodlemobileapp", "core.courses.frontpage": "admin", @@ -1663,7 +1665,6 @@ "core.courses.search": "moodle", "core.courses.searchcourses": "moodle", "core.courses.searchcoursesadvice": "local_moodlemobileapp", - "core.courses.selfenrolment": "local_moodlemobileapp", "core.courses.show": "block_myoverview", "core.courses.showonlyenrolled": "local_moodlemobileapp", "core.courses.therearecourses": "moodle", diff --git a/src/addons/addons.module.ts b/src/addons/addons.module.ts index e74659659e1..9d643867a92 100644 --- a/src/addons/addons.module.ts +++ b/src/addons/addons.module.ts @@ -20,6 +20,7 @@ import { AddonBlogModule } from './blog/blog.module'; import { AddonCalendarModule } from './calendar/calendar.module'; import { AddonCompetencyModule } from './competency/competency.module'; import { AddonCourseCompletionModule } from './coursecompletion/coursecompletion.module'; +import { AddonEnrolModule } from './enrol/enrol.module'; import { AddonFilterModule } from './filter/filter.module'; import { AddonMessageOutputModule } from './messageoutput/messageoutput.module'; import { AddonMessagesModule } from './messages/messages.module'; @@ -42,6 +43,7 @@ import { AddonUserProfileFieldModule } from './userprofilefield/userprofilefield AddonCalendarModule, AddonCompetencyModule, AddonCourseCompletionModule, + AddonEnrolModule, AddonFilterModule, AddonMessageOutputModule, AddonMessagesModule, diff --git a/src/addons/competency/services/handlers/course-option.ts b/src/addons/competency/services/handlers/course-option.ts index 0db2a268704..e46b49b784e 100644 --- a/src/addons/competency/services/handlers/course-option.ts +++ b/src/addons/competency/services/handlers/course-option.ts @@ -50,8 +50,8 @@ export class AddonCompetencyCourseOptionHandlerService implements CoreCourseOpti accessData: CoreCourseAccess, navOptions?: CoreCourseUserAdminOrNavOptionIndexed, ): Promise { - if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { - return false; // Not enabled for guests. + if (accessData && accessData.type === CoreCourseProvider.ACCESS_GUEST) { + return false; // Not enabled for guest access. } if (navOptions && navOptions.competencies !== undefined) { diff --git a/src/addons/coursecompletion/services/handlers/course-option.ts b/src/addons/coursecompletion/services/handlers/course-option.ts index 6d738947771..8b1419a6bc9 100644 --- a/src/addons/coursecompletion/services/handlers/course-option.ts +++ b/src/addons/coursecompletion/services/handlers/course-option.ts @@ -43,8 +43,8 @@ export class AddonCourseCompletionCourseOptionHandlerService implements CoreCour * @inheritdoc */ async isEnabledForCourse(courseId: number, accessData: CoreCourseAccess): Promise { - if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { - return false; // Not enabled for guests. + if (accessData && accessData.type === CoreCourseProvider.ACCESS_GUEST) { + return false; // Not enabled for guest access. } const courseEnabled = await AddonCourseCompletion.isPluginViewEnabledForCourse(courseId); diff --git a/src/addons/enrol/enrol.module.ts b/src/addons/enrol/enrol.module.ts new file mode 100644 index 00000000000..7320b649132 --- /dev/null +++ b/src/addons/enrol/enrol.module.ts @@ -0,0 +1,30 @@ +// (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 { NgModule } from '@angular/core'; + +import { AddonEnrolFeeModule } from './fee/fee.module'; +import { AddonEnrolGuestModule } from './guest/guest.module'; +import { AddonEnrolPaypalModule } from './paypal/paypal.module'; +import { AddonEnrolSelfModule } from './self/self.module'; + +@NgModule({ + imports: [ + AddonEnrolFeeModule, + AddonEnrolGuestModule, + AddonEnrolPaypalModule, + AddonEnrolSelfModule, + ], +}) +export class AddonEnrolModule {} diff --git a/src/addons/enrol/fee/fee.module.ts b/src/addons/enrol/fee/fee.module.ts new file mode 100644 index 00000000000..9bb880b0065 --- /dev/null +++ b/src/addons/enrol/fee/fee.module.ts @@ -0,0 +1,30 @@ +// (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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { AddonEnrolFeeHandler } from './services/enrol-handler'; +import { CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate'; + +@NgModule({ + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreEnrolDelegate.registerHandler(AddonEnrolFeeHandler.instance); + }, + }, + ], +}) +export class AddonEnrolFeeModule {} diff --git a/src/addons/enrol/fee/services/enrol-handler.ts b/src/addons/enrol/fee/services/enrol-handler.ts new file mode 100644 index 00000000000..15afd5ad840 --- /dev/null +++ b/src/addons/enrol/fee/services/enrol-handler.ts @@ -0,0 +1,38 @@ +// (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 { CoreEnrolAction, CoreEnrolHandler } from '@features/enrol/services/enrol-delegate'; +import { makeSingleton } from '@singletons'; + +/** + * Enrol handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonEnrolFeeHandlerService implements CoreEnrolHandler { + + name = 'AddonEnrolFee'; + type = 'fee'; + enrolmentAction = CoreEnrolAction.BROWSER; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + +} + +export const AddonEnrolFeeHandler = makeSingleton(AddonEnrolFeeHandlerService); diff --git a/src/addons/enrol/guest/guest.module.ts b/src/addons/enrol/guest/guest.module.ts new file mode 100644 index 00000000000..4fbf2017922 --- /dev/null +++ b/src/addons/enrol/guest/guest.module.ts @@ -0,0 +1,30 @@ +// (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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { AddonEnrolGuestHandler } from './services/enrol-handler'; +import { CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate'; + +@NgModule({ + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreEnrolDelegate.registerHandler(AddonEnrolGuestHandler.instance); + }, + }, + ], +}) +export class AddonEnrolGuestModule {} diff --git a/src/addons/enrol/guest/lang.json b/src/addons/enrol/guest/lang.json new file mode 100644 index 00000000000..484eb14b64b --- /dev/null +++ b/src/addons/enrol/guest/lang.json @@ -0,0 +1,5 @@ +{ + "guestaccess_withpassword": "Guest access requires password", + "guestaccess_withoutpassword": "Guest access", + "passwordinvalid": "Incorrect access password, please try again" +} diff --git a/src/addons/enrol/guest/services/enrol-handler.ts b/src/addons/enrol/guest/services/enrol-handler.ts new file mode 100644 index 00000000000..d8b6f5ef0c0 --- /dev/null +++ b/src/addons/enrol/guest/services/enrol-handler.ts @@ -0,0 +1,142 @@ +// (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 { CoreEnrolAction, CoreEnrolGuestHandler, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate'; +import { makeSingleton } from '@singletons'; +import { AddonEnrolGuest } from './guest'; +import { CorePasswordModalResponse } from '@components/password-modal/password-modal'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreWSError } from '@classes/errors/wserror'; +import { CoreEnrol, CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol'; + +/** + * Enrol handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonEnrolGuestHandlerService implements CoreEnrolGuestHandler { + + name = 'AddonEnrolGuest'; + type = 'guest'; + enrolmentAction = CoreEnrolAction.GUEST; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + async getInfoIcons(courseId: number): Promise { + const guestEnrolments = await CoreEnrol.getSupportedCourseEnrolmentMethods(courseId, { type: this.type }); + + for (const guestEnrolment of guestEnrolments) { + const info = await AddonEnrolGuest.getGuestEnrolmentInfo(guestEnrolment.id); + // Don't allow guest access if it requires a password if not supported. + if (!info.passwordrequired) { + return [{ + label: 'addon.enrol_guest.guestaccess_withoutpassword', + icon: 'fas-unlock', + }]; + } else { + return [{ + label: 'addon.enrol_guest.guestaccess_withpassword', + icon: 'fas-key', + }]; + } + } + + return []; + } + + /** + * @inheritdoc + */ + async canAccess(method: CoreEnrolEnrolmentMethod): Promise { + const info = await AddonEnrolGuest.getGuestEnrolmentInfo(method.id); + + return info.status && (!info.passwordrequired || AddonEnrolGuest.isValidateGuestAccessPasswordAvailable()); + } + + /** + * @inheritdoc + */ + async validateAccess(method: CoreEnrolEnrolmentMethod): Promise { + const info = await AddonEnrolGuest.getGuestEnrolmentInfo(method.id); + + if (!info.status) { + return false; + } + + if (!info.passwordrequired) { + return true; + } + + if (!AddonEnrolGuest.isValidateGuestAccessPasswordAvailable()) { + return false; + } + + const validatePassword = async (password: string): Promise => { + const modal = await CoreDomUtils.showModalLoading('core.loading', true); + + try { + const response = await AddonEnrolGuest.validateGuestAccessPassword(method.id, password); + + let error = response.hint; + if (!response.validated && !error) { + error = 'addon.enrol_guest.passwordinvalid'; + } + + return { + password, validated: response.validated, error, + }; + } finally { + modal.dismiss(); + } + }; + + try { + const response = await CoreDomUtils.promptPassword({ + title: method.name, + validator: validatePassword, + }); + + if (!response.validated) { + return false; + } + } catch (error) { + if (error instanceof CoreWSError) { + throw error; + } + + // Cancelled, return + return false; + } + + return true; + } + + /** + * @inheritdoc + */ + async invalidate(method: CoreEnrolEnrolmentMethod): Promise { + return AddonEnrolGuest.invalidateGuestEnrolmentInfo(method.id); + } + +} + +export const AddonEnrolGuestHandler = makeSingleton(AddonEnrolGuestHandlerService); diff --git a/src/addons/enrol/guest/services/guest.ts b/src/addons/enrol/guest/services/guest.ts new file mode 100644 index 00000000000..c9b77b7d518 --- /dev/null +++ b/src/addons/enrol/guest/services/guest.ts @@ -0,0 +1,158 @@ +// (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 { CoreSiteWSPreSets, CoreSite } from '@classes/site'; +import { CoreEnrolEnrolmentInfo } from '@features/enrol/services/enrol'; +import { CoreSites } from '@services/sites'; +import { CoreWSExternalWarning } from '@services/ws'; +import { makeSingleton } from '@singletons'; + +/** + * Service that provides some features to manage guest enrolment. + */ +@Injectable({ providedIn: 'root' }) +export class AddonEnrolGuestService { + + protected static readonly ROOT_CACHE_KEY = 'AddonEnrolGuest:'; + + /** + * Get info from a course guest enrolment method. + * + * @param instanceId Guest instance ID. + * @param siteId Site ID. If not defined, use current site. + * @returns Promise resolved when the info is retrieved. + */ + async getGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonEnrolGuestGetInstanceInfoWSParams = { + instanceid: instanceId, + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getGuestEnrolmentInfoCacheKey(instanceId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + + const response = + await site.read('enrol_guest_get_instance_info', params, preSets); + + return response.instanceinfo; + } + + /** + * Get cache key for get course guest enrolment methods WS call. + * + * @param instanceId Guest instance ID. + * @returns Cache key. + */ + protected getGuestEnrolmentInfoCacheKey(instanceId: number): string { + return AddonEnrolGuestService.ROOT_CACHE_KEY + instanceId; + } + + /** + * Invalidates get course guest enrolment info WS call. + * + * @param instanceId Guest instance ID. + * @param siteId Site Id. If not defined, use current site. + * @returns Promise resolved when the data is invalidated. + */ + async invalidateGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await Promise.all([ + site.invalidateWsCacheForKey(this.getGuestEnrolmentInfoCacheKey(instanceId)), + site.invalidateWsCacheForKey(`mmCourses:guestinfo:${instanceId}`), // @todo Remove after 4.3 release. + ]); + } + + /** + * Check if guest password validation WS is available on the current site. + * + * @returns Whether guest password validation WS is available. + */ + isValidateGuestAccessPasswordAvailable(): boolean { + return CoreSites.wsAvailableInCurrentSite('enrol_guest_validate_password'); + } + + /** + * Perform password validation of guess access. + * + * @param enrolmentInstanceId Instance id of guest enrolment plugin. + * @param password Course Password. + * @returns Wether the password is valid. + */ + async validateGuestAccessPassword( + enrolmentInstanceId: number, + password: string, + ): Promise { + const site = CoreSites.getCurrentSite(); + + if (!site) { + return { + validated: false, + }; + } + + const params: AddonEnrolGuestValidatePasswordWSParams = { + instanceid: enrolmentInstanceId, + password, + }; + + return await site.write('enrol_guest_validate_password', params); + } + +} +export const AddonEnrolGuest = makeSingleton(AddonEnrolGuestService); + +/** + * Params of enrol_guest_get_instance_info WS. + */ +type AddonEnrolGuestGetInstanceInfoWSParams = { + instanceid: number; // Instance id of guest enrolment plugin. +}; + +/** + * Data returned by enrol_guest_get_instance_info WS. + */ +export type AddonEnrolGuestGetInstanceInfoWSResponse = { + instanceinfo: AddonEnrolGuestInfo; + warnings?: CoreWSExternalWarning[]; +}; + +/** + * Course guest enrolment method. + */ +export type AddonEnrolGuestInfo = CoreEnrolEnrolmentInfo & { + passwordrequired: boolean; // Is a password required? + status: boolean; // Is the enrolment enabled? +}; + +/** + * Params of enrol_guest_validate_password WS. + */ +type AddonEnrolGuestValidatePasswordWSParams = { + instanceid: number; // instance id of guest enrolment plugin + password: string; // the course password +}; + +/** + * Data returned by enrol_guest_get_instance_info WS. + */ +export type AddonEnrolGuestValidatePasswordWSResponse = { + validated: boolean; // Whether the password was successfully validated + hint?: string; // Password hint (if enabled) + warnings?: CoreWSExternalWarning[]; +}; diff --git a/src/addons/enrol/paypal/paypal.module.ts b/src/addons/enrol/paypal/paypal.module.ts new file mode 100644 index 00000000000..c8175f2a323 --- /dev/null +++ b/src/addons/enrol/paypal/paypal.module.ts @@ -0,0 +1,30 @@ +// (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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { AddonEnrolPaypalHandler } from './services/enrol-handler'; +import { CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate'; + +@NgModule({ + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreEnrolDelegate.registerHandler(AddonEnrolPaypalHandler.instance); + }, + }, + ], +}) +export class AddonEnrolPaypalModule {} diff --git a/src/addons/enrol/paypal/services/enrol-handler.ts b/src/addons/enrol/paypal/services/enrol-handler.ts new file mode 100644 index 00000000000..6f4a3ee7e7d --- /dev/null +++ b/src/addons/enrol/paypal/services/enrol-handler.ts @@ -0,0 +1,38 @@ +// (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 { CoreEnrolAction, CoreEnrolHandler } from '@features/enrol/services/enrol-delegate'; +import { makeSingleton } from '@singletons'; + +/** + * Enrol handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonEnrolPaypalHandlerService implements CoreEnrolHandler { + + name = 'AddonEnrolPaypal'; + type = 'paypal'; + enrolmentAction = CoreEnrolAction.BROWSER; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + +} + +export const AddonEnrolPaypalHandler = makeSingleton(AddonEnrolPaypalHandlerService); diff --git a/src/addons/enrol/self/lang.json b/src/addons/enrol/self/lang.json new file mode 100644 index 00000000000..89407c2d7a2 --- /dev/null +++ b/src/addons/enrol/self/lang.json @@ -0,0 +1,7 @@ +{ + "pluginname": "Self enrolment", + "confirmselfenrol": "Are you sure you want to enrol yourself in this course?", + "errorselfenrol": "An error occurred while self enrolling.", + "nopassword": "No enrolment key required.", + "password": "Enrolment key" +} diff --git a/src/addons/enrol/self/self.module.ts b/src/addons/enrol/self/self.module.ts new file mode 100644 index 00000000000..2947a8366f2 --- /dev/null +++ b/src/addons/enrol/self/self.module.ts @@ -0,0 +1,30 @@ +// (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 { APP_INITIALIZER, NgModule } from '@angular/core'; +import { AddonEnrolSelfHandler } from './services/enrol-handler'; +import { CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate'; + +@NgModule({ + providers: [ + { + provide: APP_INITIALIZER, + multi: true, + useValue: () => { + CoreEnrolDelegate.registerHandler(AddonEnrolSelfHandler.instance); + }, + }, + ], +}) +export class AddonEnrolSelfModule {} diff --git a/src/addons/enrol/self/services/enrol-handler.ts b/src/addons/enrol/self/services/enrol-handler.ts new file mode 100644 index 00000000000..5cc0589f004 --- /dev/null +++ b/src/addons/enrol/self/services/enrol-handler.ts @@ -0,0 +1,176 @@ +// (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 { CoreEnrolAction, CoreEnrolSelfHandler, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate'; +import { Translate, makeSingleton } from '@singletons'; +import { AddonEnrolSelf } from './self'; +import { CorePasswordModalResponse } from '@components/password-modal/password-modal'; +import { CoreCoursesProvider } from '@features/courses/services/courses'; +import { CoreDomUtils } from '@services/utils/dom'; +import { CoreEnrol, CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol'; + +/** + * Enrol handler. + */ +@Injectable({ providedIn: 'root' }) +export class AddonEnrolSelfHandlerService implements CoreEnrolSelfHandler { + + name = 'AddonEnrolSelf'; + type = 'self'; + enrolmentAction = CoreEnrolAction.SELF; + + /** + * @inheritdoc + */ + async isEnabled(): Promise { + return true; + } + + /** + * @inheritdoc + */ + async getInfoIcons(courseId: number): Promise { + const selfEnrolments = await CoreEnrol.getSupportedCourseEnrolmentMethods(courseId, { type: this.type }); + let passwordRequired = false; + let noPasswordRequired = false; + + for (const selfEnrolment of selfEnrolments) { + const info = await AddonEnrolSelf.getSelfEnrolmentInfo(selfEnrolment.id); + // Don't allow self access if it requires a password if not supported. + if (!info.enrolpassword) { + noPasswordRequired = true; + } else { + passwordRequired = true; + } + if (noPasswordRequired && passwordRequired) { + break; + } + } + + const icons: CoreEnrolInfoIcon[] = []; + if (noPasswordRequired) { + icons.push({ + label: 'addon.enrol_self.pluginname', + icon: 'fas-right-to-bracket', + }); + } + + if (passwordRequired) { + icons.push({ + label: 'addon.enrol_self.pluginname', + icon: 'fas-key', + }); + } + + return icons; + } + + /** + * @inheritdoc + */ + async enrol(method: CoreEnrolEnrolmentMethod): Promise { + const info = await AddonEnrolSelf.getSelfEnrolmentInfo(method.id); + // Don't allow self access if it requires a password if not supported. + if (!info.enrolpassword) { + try { + await CoreDomUtils.showConfirm( + Translate.instant('addon.enrol_self.confirmselfenrol') + '
' + + Translate.instant('addon.enrol_self.nopassword'), + method.name, + ); + } catch { + // User cancelled. + return false; + } + } + + try { + return await this.performEnrol(method); + } catch { + return false; + } + } + + /** + * Self enrol in a course. + * + * @param method Enrolment method + * @returns Promise resolved when self enrolled. + */ + protected async performEnrol(method: CoreEnrolEnrolmentMethod): Promise { + const validatePassword = async (password = ''): Promise => { + const modal = await CoreDomUtils.showModalLoading('core.loading', true); + + const response: CorePasswordModalResponse = { + password, + }; + + try { + response.validated = await AddonEnrolSelf.selfEnrol(method.courseid, password, method.id); + } catch (error) { + if (error && error.errorcode === CoreCoursesProvider.ENROL_INVALID_KEY) { + response.validated = false; + response.error = error.message; + } else { + CoreDomUtils.showErrorModalDefault(error, 'addon.enrol_self.errorselfenrol', true); + + throw error; + } + } finally { + modal.dismiss(); + } + + return response; + }; + + let response: CorePasswordModalResponse | undefined; + + try { + response = await validatePassword(); + } catch { + return false; + } + + if (!response.validated) { + try { + const response = await CoreDomUtils.promptPassword({ + validator: validatePassword, + title: method.name, + placeholder: 'addon.enrol_self.password', + submit: 'core.courses.enrolme', + }); + + if (!response.validated) { + return false; + } + } catch { + // Cancelled, return + return false; + } + } + + return true; + } + + /** + * @inheritdoc + */ + async invalidate(method: CoreEnrolEnrolmentMethod): Promise { + return AddonEnrolSelf.invalidateSelfEnrolmentInfo(method.id); + } + +} + +export const AddonEnrolSelfHandler = makeSingleton(AddonEnrolSelfHandlerService); diff --git a/src/addons/enrol/self/services/self.ts b/src/addons/enrol/self/services/self.ts new file mode 100644 index 00000000000..178268decc8 --- /dev/null +++ b/src/addons/enrol/self/services/self.ts @@ -0,0 +1,152 @@ +// (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 { CoreWSError } from '@classes/errors/wserror'; +import { CoreSiteWSPreSets, CoreSite } from '@classes/site'; +import { CoreCoursesProvider } from '@features/courses/services/courses'; +import { CoreSites } from '@services/sites'; +import { CoreStatusWithWarningsWSResponse } from '@services/ws'; +import { makeSingleton } from '@singletons'; + +/** + * Service that provides some features to manage self enrolment. + */ +@Injectable({ providedIn: 'root' }) +export class AddonEnrolSelfService { + + protected static readonly ROOT_CACHE_KEY = 'AddonEnrolSelf:'; + + /** + * Get info from a course self enrolment method. + * + * @param instanceId Self instance ID. + * @param siteId Site ID. If not defined, use current site. + * @returns Promise resolved when the info is retrieved. + */ + async getSelfEnrolmentInfo(instanceId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const params: AddonEnrolSelfGetInstanceInfoWSParams = { + instanceid: instanceId, + }; + + const preSets: CoreSiteWSPreSets = { + cacheKey: this.getSelfEnrolmentInfoCacheKey(instanceId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + + return await site.read('enrol_self_get_instance_info', params, preSets); + } + + /** + * Get cache key for get course self enrolment methods WS call. + * + * @param instanceId Self instance ID. + * @returns Cache key. + */ + protected getSelfEnrolmentInfoCacheKey(instanceId: number): string { + return AddonEnrolSelfService.ROOT_CACHE_KEY + instanceId; + } + + /** + * Invalidates get course self enrolment info WS call. + * + * @param instanceId Self instance ID. + * @param siteId Site Id. If not defined, use current site. + * @returns Promise resolved when the data is invalidated. + */ + async invalidateSelfEnrolmentInfo(instanceId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await site.invalidateWsCacheForKey(this.getSelfEnrolmentInfoCacheKey(instanceId)); + } + + /** + * Self enrol current user in a certain course. + * + * @param courseId Course ID. + * @param password Password to use. + * @param instanceId Enrol instance ID. + * @param siteId Site ID. If not defined, use current site. + * @returns Promise resolved if the user is enrolled. If the password is invalid, the promise is rejected + * with an object with errorcode = CoreCoursesProvider.ENROL_INVALID_KEY. + */ + async selfEnrol(courseId: number, password: string = '', instanceId?: number, siteId?: string): Promise { + + const site = await CoreSites.getSite(siteId); + + const params: AddonEnrolSelfEnrolUserWSParams = { + courseid: courseId, + password: password, + }; + if (instanceId) { + params.instanceid = instanceId; + } + + const response = await site.write('enrol_self_enrol_user', params); + + if (!response) { + throw Error('WS enrol_self_enrol_user failed'); + } + + if (response.status) { + return true; + } + + if (response.warnings && response.warnings.length) { + // Invalid password warnings. + const warning = response.warnings.find((warning) => + warning.warningcode == '2' || warning.warningcode == '3' || warning.warningcode == '4'); + + if (warning) { + throw new CoreWSError({ errorcode: CoreCoursesProvider.ENROL_INVALID_KEY, message: warning.message }); + } else { + throw new CoreWSError(response.warnings[0]); + } + } + + throw Error('WS enrol_self_enrol_user failed without warnings'); + } + +} +export const AddonEnrolSelf = makeSingleton(AddonEnrolSelfService); + +/** + * Params of enrol_self_get_instance_info WS. + */ +type AddonEnrolSelfGetInstanceInfoWSParams = { + instanceid: number; // Instance id of self enrolment plugin. +}; + +/** + * Data returned by enrol_self_get_instance_info WS. + */ +export type AddonEnrolSelfGetInstanceInfoWSResponse = { + id: number; // Id of course enrolment instance. + courseid: number; // Id of course. + type: string; // Type of enrolment plugin. + name: string; // Name of enrolment plugin. + status: string; // Status of enrolment plugin. + enrolpassword?: string; // Password required for enrolment. +}; + +/** + * Params of enrol_self_enrol_user WS. + */ +type AddonEnrolSelfEnrolUserWSParams = { + courseid: number; // Id of the course. + password?: string; // Enrolment key. + instanceid?: number; // Instance id of self enrolment plugin. +}; diff --git a/src/addons/notes/services/handlers/course-option.ts b/src/addons/notes/services/handlers/course-option.ts index 77c7d54e44b..736e9e6da03 100644 --- a/src/addons/notes/services/handlers/course-option.ts +++ b/src/addons/notes/services/handlers/course-option.ts @@ -47,8 +47,8 @@ export class AddonNotesCourseOptionHandlerService implements CoreCourseOptionsHa accessData: CoreCourseAccess, navOptions?: CoreCourseUserAdminOrNavOptionIndexed, ): Promise { - if (accessData && accessData.type == CoreCourseProvider.ACCESS_GUEST) { - return false; // Not enabled for guests. + if (accessData && accessData.type === CoreCourseProvider.ACCESS_GUEST) { + return false; // Not enabled for guest access. } if (navOptions && navOptions.notes !== undefined) { diff --git a/src/core/components/password-modal/password-modal.ts b/src/core/components/password-modal/password-modal.ts index dd7e43cd7fd..60a19efa07e 100644 --- a/src/core/components/password-modal/password-modal.ts +++ b/src/core/components/password-modal/password-modal.ts @@ -61,7 +61,12 @@ export class CorePasswordModalComponent { ModalController.dismiss(response); } - this.error = response.error; + if (typeof response.error === 'string') { + this.error = response.error; + } else if (response.error) { + ModalController.dismiss(response.error); + } + } /** diff --git a/src/core/features/course/components/course-format/course-format.ts b/src/core/features/course/components/course-format/course-format.ts index 381c6040a84..e8aafa64d2b 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -76,7 +76,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { @Input() initialSectionId?: number; // The section to load first (by ID). @Input() initialSectionNumber?: number; // The section to load first (by number). @Input() moduleId?: number; // The module ID to scroll to. Must be inside the initial selected section. - @Input() isGuest?: boolean; // If user is accessing as a guest. + @Input() isGuest?: boolean; // If user is accessing using an ACCESS_GUEST enrolment method. // eslint-disable-next-line @typescript-eslint/no-explicit-any @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList>; diff --git a/src/core/features/course/lang.json b/src/core/features/course/lang.json index a8bd74a199a..d1501f66e93 100644 --- a/src/core/features/course/lang.json +++ b/src/core/features/course/lang.json @@ -35,9 +35,6 @@ "errordownloadingsection": "Error downloading section.", "errorgetmodule": "Error getting activity data.", "failed": "Failed", - "guestaccess_passwordinvalid": "Incorrect access password, please try again", - "guestaccess_withpassword": "Guest access requires password", - "guestaccess": "Guest access", "hiddenfromstudents": "Hidden from students", "hiddenoncoursepage": "Available but not shown on course page", "highlighted": "Highlighted", diff --git a/src/core/features/course/pages/course-summary/course-summary.html b/src/core/features/course/pages/course-summary/course-summary.html index e639fea870f..373dda5ae30 100644 --- a/src/core/features/course/pages/course-summary/course-summary.html +++ b/src/core/features/course/pages/course-summary/course-summary.html @@ -138,26 +138,18 @@

{{item.data.title | translate }} + + + {{ 'core.courses.enrolme' | translate }} + - - {{ 'core.courses.enrolme' | translate }} - - - - - - {{ 'core.courses.notenrollable' | translate }} - - - - - - - {{ 'core.course.guestaccess_withpassword' | translate }} - - - + + + + {{ 'core.courses.notenrollable' | translate }} + + + {{ 'core.course.viewcourse' | translate }} diff --git a/src/core/features/course/pages/course-summary/course-summary.page.ts b/src/core/features/course/pages/course-summary/course-summary.page.ts index 878c0d2a0f6..e354df78ec2 100644 --- a/src/core/features/course/pages/course-summary/course-summary.page.ts +++ b/src/core/features/course/pages/course-summary/course-summary.page.ts @@ -19,7 +19,6 @@ import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; import { CoreCourseCustomField, - CoreCourseEnrolmentMethod, CoreCourses, CoreCourseSearchedData, CoreCoursesProvider, @@ -37,13 +36,12 @@ import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '@features/course import { Subscription } from 'rxjs'; import { CoreColors } from '@singletons/colors'; import { CorePath } from '@singletons/path'; -import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; -import { CorePasswordModalResponse } from '@components/password-modal/password-modal'; import { CoreTime } from '@singletons/time'; import { CoreAnalytics, CoreAnalyticsEventType } from '@services/analytics'; - -const ENROL_BROWSER_METHODS = ['fee', 'paypal']; +import { CoreEnrolHelper } from '@features/enrol/services/enrol-helper'; +import { CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate'; +import { CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol'; /** * Page that shows the summary of a course including buttons to enrol and other available options. @@ -61,21 +59,21 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { @ViewChild('courseThumb') courseThumb?: ElementRef; isEnrolled = false; + canAccessCourse = true; - selfEnrolInstances: CoreCourseEnrolmentMethod[] = []; - otherEnrolments = false; + useGuestAccess = false; + + selfEnrolInstances: CoreEnrolEnrolmentMethod[] = []; + guestEnrolInstances: CoreEnrolEnrolmentMethod[] = []; + hasBrowserEnrolments = false; dataLoaded = false; isModal = false; contactsExpanded = false; - useGuestAccess = false; - guestAccessPasswordRequired = false; courseUrl = ''; progress?: number; courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = []; protected actionSheet?: HTMLIonActionSheetElement; - protected guestInstanceId = new CorePromisedValue(); - protected courseData = new CorePromisedValue(); protected waitStart = 0; protected enrolUrl = ''; protected pageDestroyed = false; @@ -143,40 +141,14 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { await this.getCourse(); } - /** - * Check if the user can access as guest. - * - * @returns Promise resolved if can access as guest, rejected otherwise. Resolve param indicates if - * password is required for guest access. - */ - protected async canAccessAsGuest(): Promise { - const guestInstanceId = await this.guestInstanceId; - if (guestInstanceId === undefined) { - return false; - } - - const info = await CoreCourses.getCourseGuestEnrolmentInfo(guestInstanceId); - - // Don't allow guest access if it requires a password if not supported. - this.guestAccessPasswordRequired = info.passwordrequired; - - return info.status && (!info.passwordrequired || CoreCourses.isValidateGuestAccessPasswordAvailable()); - } - /** * Convenience function to get course. We use this to determine if a user can see the course or not. * * @param refresh If it's refreshing content. */ protected async getCourse(refresh = false): Promise { - this.otherEnrolments = false; - try { - await Promise.all([ - this.getEnrolmentMethods(), - this.getCourseData(), - this.loadCourseExtraData(), - ]); + await this.getCourseData(); this.logView(); } catch (error) { @@ -201,40 +173,13 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { this.dataLoaded = true; } - /** - * Get course enrolment methods. - */ - protected async getEnrolmentMethods(): Promise { - this.selfEnrolInstances = []; - this.guestInstanceId.reset(); - - const enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(this.courseId); - - enrolmentMethods.forEach((method) => { - if (!CoreUtils.isTrueOrOne(method.status)) { - return; - } - - if (method.type === 'self') { - this.selfEnrolInstances.push(method); - } else if (method.type === 'guest') { - this.guestInstanceId.resolve(method.id); - } else { - // Other enrolments that comes from that WS should need user action. - this.otherEnrolments = true; - } - }); - - if (!this.guestInstanceId.isSettled()) { - // No guest instance found. - this.guestInstanceId.resolve(undefined); - } - } - /** * Get course data. */ protected async getCourseData(): Promise { + this.canAccessCourse = false; + this.useGuestAccess = false; + try { // Check if user is enrolled in the course. try { @@ -250,40 +195,51 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { this.canAccessCourse = true; this.useGuestAccess = false; } catch { - // The user is not an admin/manager. Check if we can provide guest access to the course. - this.canAccessCourse = await this.canAccessAsGuest(); - this.useGuestAccess = this.canAccessCourse; + // Ignore errors. + } + + const courseByField = await CoreUtils.ignoreErrors(CoreCourses.getCourseByField('id', this.courseId)); + if (courseByField) { + if (this.course) { + this.course.customfields = courseByField.customfields; + this.course.contacts = courseByField.contacts; + this.course.displayname = courseByField.displayname; + this.course.categoryname = courseByField.categoryname; + this.course.overviewfiles = courseByField.overviewfiles; + } else { + this.course = courseByField; + } } - this.courseData.resolve(this.course); + await this.getEnrolmentInfo(); } /** - * Load some extra data for the course. + * Get course enrolment methods. */ - protected async loadCourseExtraData(): Promise { - try { - const courseByField = await CoreCourses.getCourseByField('id', this.courseId); - const courseData = await this.courseData; - - if (courseData) { - courseData.customfields = courseByField.customfields; - courseData.contacts = courseByField.contacts; - courseData.displayname = courseByField.displayname; - courseData.categoryname = courseByField.categoryname; - courseData.overviewfiles = courseByField.overviewfiles; - } else { - this.course = courseByField; - this.courseData.resolve(courseByField); - } + protected async getEnrolmentInfo(): Promise { + if (this.isEnrolled) { + return; + } - // enrollmentmethods contains ALL enrolment methods including manual. - if (!this.isEnrolled && courseByField.enrollmentmethods?.some((method) => ENROL_BROWSER_METHODS.includes(method))) { - this.otherEnrolments = true; - } + const enrolByType = await CoreEnrolHelper.getEnrolmentsByType(this.courseId); - } catch { - // Ignore errors. + this.hasBrowserEnrolments = enrolByType.hasBrowser; + this.selfEnrolInstances = enrolByType.self; + this.guestEnrolInstances = enrolByType.guest; + + if (!this.canAccessCourse) { + // The user is not an admin/manager. Check if we can provide guest access to the course. + const promises = this.guestEnrolInstances.map(async (method) => { + const canAccess = await CoreEnrolDelegate.canAccess(method); + if (canAccess) { + this.canAccessCourse = true; + } + }); + + await Promise.all(promises); + + this.useGuestAccess = this.canAccessCourse; } } @@ -302,6 +258,33 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { await CoreCourseOptionsDelegate.getMenuHandlersToDisplay(this.course, refresh, this.useGuestAccess); } + /** + * Validates if the user has access to the course and opens it. + * + * @param enrolMethod The enrolment method. + * @param replaceCurrentPage If current place should be replaced in the navigation stack. + */ + async validateAccessAndOpen(enrolMethod: CoreEnrolEnrolmentMethod, replaceCurrentPage: boolean): Promise { + if (!this.canAccessCourse || !this.course || this.isModal) { + return; + } + + let validated = false; + try { + validated = await CoreEnrolDelegate.validateAccess(enrolMethod); + } catch { + this.refreshData(); + + return; + } + + if (!validated) { + return; + } + + CoreCourseHelper.openCourse(this.course, { params: { isGuest: this.useGuestAccess }, replace: replaceCurrentPage }); + } + /** * Open the course. * @@ -312,50 +295,34 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { return; } - const guestInstanceId = await this.guestInstanceId; - if (this.useGuestAccess && this.guestAccessPasswordRequired && guestInstanceId) { - // Check if the user has access to the course as guest with a previous sent password. - let validated = await CoreCourseHelper.userHasAccessToCourse(this.courseId); - - if (!validated) { - try { - type ValidatorResponse = CorePasswordModalResponse & { cancel?: boolean }; - const validatePassword = async (password: string): Promise => { - try { - const response = await CoreCourses.validateGuestAccessPassword(guestInstanceId, password); - - validated = response.validated; - let error = response.hint; - if (!validated && !error) { - error = 'core.course.guestaccess_passwordinvalid'; - } - - return { - password, validated, error, - }; - } catch { - this.refreshData(); - - return { - password, - cancel: true, - }; - } - }; - - const response = await CoreDomUtils.promptPassword({ - title: 'core.course.guestaccess', - validator: validatePassword, - }); - - if (!response.validated || response.cancel) { - return; - } - } catch { - // Cancelled, return - return; - } + const hasAccess = await CoreCourseHelper.userHasAccessToCourse(this.courseId); + if (!hasAccess && this.guestEnrolInstances.length) { + if (this.guestEnrolInstances.length == 1) { + this.validateAccessAndOpen(this.guestEnrolInstances[0], replaceCurrentPage); + + return; } + + const buttons: ActionSheetButton[] = this.guestEnrolInstances.map((enrolMethod) => ({ + text: enrolMethod.name, + handler: (): void => { + this.validateAccessAndOpen(enrolMethod, replaceCurrentPage); + }, + })); + + buttons.push({ + text: Translate.instant('core.cancel'), + role: 'cancel', + }); + + this.actionSheet = await ActionSheetController.create({ + header: Translate.instant('core.course.viewcourse'), + buttons: buttons, + }); + + await this.actionSheet.present(); + + return; } CoreCourseHelper.openCourse(this.course, { params: { isGuest: this.useGuestAccess }, replace: replaceCurrentPage }); @@ -389,74 +356,22 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { } /** - * Confirm user to Self enrol in course. + * Self enrol in a course. * * @param enrolMethod The enrolment method. */ - async selfEnrolConfirm(enrolMethod: CoreCourseEnrolmentMethod): Promise { + async selfEnrolInCourse(enrolMethod: CoreEnrolEnrolmentMethod): Promise { + let enrolled = false; try { - await CoreDomUtils.showConfirm(Translate.instant('core.courses.confirmselfenrol'), enrolMethod.name); - - this.selfEnrolInCourse(enrolMethod.id); + enrolled = await CoreEnrolDelegate.enrol(enrolMethod); } catch { - // User cancelled. - } - } + this.refreshData(); - /** - * Self enrol in a course. - * - * @param instanceId The instance ID. - * @returns Promise resolved when self enrolled. - */ - async selfEnrolInCourse(instanceId: number): Promise { - const validatePassword = async (password = ''): Promise => { - const response: CorePasswordModalResponse = { - password, - }; - try { - response.validated = await CoreCourses.selfEnrol(this.courseId, password, instanceId); - } catch (error) { - if (error && error.errorcode === CoreCoursesProvider.ENROL_INVALID_KEY) { - response.validated = false; - response.error = error.message; - } else { - CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorselfenrol', true); - - throw error; - } - } - - return response; - }; - - const modal = await CoreDomUtils.showModalLoading('core.loading', true); - let response: CorePasswordModalResponse | undefined; - - try { - response = await validatePassword(); - } catch { return; - } finally { - modal.dismiss(); } - if (!response.validated) { - try { - const response = await CoreDomUtils.promptPassword({ - validator: validatePassword, - title: 'core.courses.selfenrolment', - placeholder: 'core.courses.password', - submit: 'core.courses.enrolme', - }); - - if (!response.validated) { - return; - } - } catch { - // Cancelled, return - return; - } + if (!enrolled) { + return; } // Refresh data. @@ -488,12 +403,18 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { promises.push(CoreCourses.invalidateUserCourses()); promises.push(CoreCourses.invalidateCourse(this.courseId)); - promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.courseId)); promises.push(CoreCourseOptionsDelegate.clearAndInvalidateCoursesOptions(this.courseId)); promises.push(CoreCourses.invalidateCoursesByField('id', this.courseId)); - if (this.guestInstanceId.value) { - promises.push(CoreCourses.invalidateCourseGuestEnrolmentInfo(this.guestInstanceId.value)); - } + + promises.push(CoreCourses.invalidateCourseEnrolmentMethods(this.courseId)); + + this.selfEnrolInstances.forEach((method) => { + promises.push(CoreEnrolDelegate.invalidate(method)); + }); + + this.guestEnrolInstances.forEach((method) => { + promises.push(CoreEnrolDelegate.invalidate(method)); + }); await Promise.all(promises).finally(() => this.getCourse()).finally(() => { refresher?.complete(); @@ -587,13 +508,13 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { * Open enrol action sheet. */ async enrolMe(): Promise { - if (this.selfEnrolInstances.length == 1 && !this.otherEnrolments) { - this.selfEnrolConfirm(this.selfEnrolInstances[0]); + if (this.selfEnrolInstances.length == 1 && !this.hasBrowserEnrolments) { + this.selfEnrolInCourse(this.selfEnrolInstances[0]); return; } - if (this.selfEnrolInstances.length == 0 && this.otherEnrolments) { + if (this.selfEnrolInstances.length == 0 && this.hasBrowserEnrolments) { this.browserEnrol(); return; @@ -602,11 +523,11 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { const buttons: ActionSheetButton[] = this.selfEnrolInstances.map((enrolMethod) => ({ text: enrolMethod.name, handler: (): void => { - this.selfEnrolConfirm(enrolMethod); + this.selfEnrolInCourse(enrolMethod); }, })); - if (this.otherEnrolments) { + if (this.hasBrowserEnrolments) { buttons.push({ text: Translate.instant('core.courses.completeenrolmentbrowser'), handler: (): void => { diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 40a5c87d9cf..74eaa9f5488 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -74,6 +74,8 @@ import { CoreCourseWithImageAndColor } from '@features/courses/services/courses- import { CoreCourseSummaryPage } from '../pages/course-summary/course-summary.page'; import { CoreRemindersPushNotificationData } from '@features/reminders/services/reminders'; import { CoreLocalNotifications } from '@services/local-notifications'; +import { AddonEnrolGuest } from '@addons/enrol/guest/services/guest'; +import { CoreEnrol } from '@features/enrol/services/enrol'; /** * Prefetch info of a module. @@ -622,19 +624,16 @@ export class CoreCourseHelperProvider { } // Check if guest access is enabled. - const enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(courseId, siteId); - - const method = enrolmentMethods.find((method) => method.type === 'guest'); - - if (!method) { + const enrolmentMethods = await CoreEnrol.getSupportedCourseEnrolmentMethods(courseId, { type: 'guest', siteId }); + if (!enrolmentMethods) { return { guestAccess: false }; } - const info = await CoreCourses.getCourseGuestEnrolmentInfo(method.id); + const info = await AddonEnrolGuest.getGuestEnrolmentInfo(enrolmentMethods[0].id); // Don't allow guest access if it requires a password and it's available. return { - guestAccess: info.status && (!info.passwordrequired || CoreCourses.isValidateGuestAccessPasswordAvailable()), + guestAccess: info.status && (!info.passwordrequired || AddonEnrolGuest.isValidateGuestAccessPasswordAvailable()), passwordRequired: info.passwordrequired, }; } catch { @@ -2151,7 +2150,7 @@ export type CoreCoursePrefetchCourseOptions = { sections?: CoreCourseWSSection[]; // List of course sections. courseHandlers?: CoreCourseOptionsHandlerToDisplay[]; // List of course handlers. menuHandlers?: CoreCourseOptionsMenuHandlerToDisplay[]; // List of course menu handlers. - isGuest?: boolean; // Whether the user is guest. + isGuest?: boolean; // Whether the user is using an ACCESS_GUEST enrolment method. }; /** diff --git a/src/core/features/course/services/course-options-delegate.ts b/src/core/features/course/services/course-options-delegate.ts index 24b5ab1cd62..2d200c95c5f 100644 --- a/src/core/features/course/services/course-options-delegate.ts +++ b/src/core/features/course/services/course-options-delegate.ts @@ -357,7 +357,7 @@ export class CoreCourseOptionsDelegateService extends CoreDelegate + [attr.aria-label]="icon.label | translate" [ngClass]="[icon.className]"> diff --git a/src/core/features/courses/components/course-list-item/course-list-item.ts b/src/core/features/courses/components/course-list-item/course-list-item.ts index 7d603ef4935..449fbf5b86f 100644 --- a/src/core/features/courses/components/course-list-item/course-list-item.ts +++ b/src/core/features/courses/components/course-list-item/course-list-item.ts @@ -26,6 +26,7 @@ import { CoreEventCourseStatusChanged, CoreEventObserver, CoreEvents } from '@si import { CoreCourseListItem, CoreCourses, CoreCoursesProvider } from '../../services/courses'; import { CoreCoursesHelper, CoreEnrolledCourseDataWithExtraInfoAndOptions } from '../../services/courses-helper'; import { CoreCoursesCourseOptionsMenuComponent } from '../course-options-menu/course-options-menu'; +import { CoreEnrolHelper } from '@features/enrol/services/enrol-helper'; /** * This directive is meant to display an item for a list of courses. @@ -98,33 +99,7 @@ export class CoreCoursesCourseListItemComponent implements OnInit, OnDestroy, On this.initPrefetchCourse(); } else if ('enrollmentmethods' in this.course) { - this.enrolmentIcons = []; - - this.course.enrollmentmethods.forEach((instance) => { - if (instance === 'self') { - this.enrolmentIcons.push({ - label: 'core.courses.selfenrolment', - icon: 'fas-key', - }); - } else if (instance === 'guest') { - this.enrolmentIcons.push({ - label: 'core.courses.allowguests', - icon: 'fas-unlock', - }); - } else if (instance === 'paypal' || instance === 'fee') { - this.enrolmentIcons.push({ - label: 'core.courses.otherenrolments', - icon: 'fas-up-right-from-square', - }); - } - }); - - if (this.enrolmentIcons.length == 0) { - this.enrolmentIcons.push({ - label: 'core.courses.notenrollable', - icon: 'fas-lock', - }); - } + this.enrolmentIcons = await CoreEnrolHelper.getEnrolmentIcons(this.course.enrollmentmethods, this.course.id); } } diff --git a/src/core/features/courses/lang.json b/src/core/features/courses/lang.json index 71ba772dda4..1f93ed37cd4 100644 --- a/src/core/features/courses/lang.json +++ b/src/core/features/courses/lang.json @@ -1,6 +1,5 @@ { "addtofavourites": "Star this course", - "allowguests": "This course allows guest users to enter", "aria:coursecategory": "Course category", "aria:coursename": "Course name", "aria:courseprogress": "Course progress:", @@ -10,7 +9,6 @@ "cannotretrievemorecategories": "Categories deeper than level {{$a}} cannot be retrieved.", "categories": "Course categories", "completeenrolmentbrowser": "Complete enrolment in browser", - "confirmselfenrol": "Are you sure you want to enrol yourself in this course?", "courses": "Courses", "downloadcourses": "Download all courses", "enrolme": "Enrol me", @@ -18,7 +16,6 @@ "errorloadcourses": "An error occurred while loading courses.", "errorloadplugins": "The plugins required by this course could not be loaded correctly. Please reload the app to try again.", "errorsearching": "An error occurred while searching.", - "errorselfenrol": "An error occurred while self enrolling.", "favourite": "Starred course", "filtermycourses": "Filter my courses", "frontpage": "Site home", @@ -40,7 +37,6 @@ "search": "Search", "searchcourses": "Search courses", "searchcoursesadvice": "You can use the search courses button to find courses to access as a guest or enrol yourself in courses that allow it.", - "selfenrolment": "Self enrolment", "show": "Restore to view", "showonlyenrolled": "Show only my courses", "therearecourses": "There are {{$a}} courses", diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index e812d3b32c9..8960cba950b 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -16,13 +16,15 @@ import { Injectable } from '@angular/core'; import { CoreSites, CoreSitesCommonWSOptions, CoreSitesReadingStrategy } from '@services/sites'; import { CoreSite, CoreSiteWSPreSets, WSObservable } from '@classes/site'; import { makeSingleton } from '@singletons'; -import { CoreStatusWithWarningsWSResponse, CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; +import { CoreWarningsWSResponse, CoreWSExternalFile, CoreWSExternalWarning } from '@services/ws'; import { CoreEvents } from '@singletons/events'; -import { CoreWSError } from '@classes/errors/wserror'; import { CoreCourseAnyCourseDataWithExtraInfoAndOptions, CoreCourseWithImageAndColor } from './courses-helper'; import { asyncObservable, firstValueFrom, ignoreErrors, zipIncludingComplete } from '@/core/utils/rxjs'; import { of } from 'rxjs'; import { map } from 'rxjs/operators'; +import { AddonEnrolGuest, AddonEnrolGuestInfo } from '@addons/enrol/guest/services/guest'; +import { AddonEnrolSelf } from '@addons/enrol/self/services/self'; +import { CoreEnrol, CoreEnrolEnrolmentInfo, CoreEnrolEnrolmentMethod } from '@features/enrol/services/enrol'; const ROOT_CACHE_KEY = 'mmCourses:'; @@ -309,33 +311,13 @@ export class CoreCoursesProvider { /** * Get the enrolment methods from a course. * - * @param id ID of the course. + * @param courseId ID of the course. * @param siteId Site ID. If not defined, use current site. * @returns Promise resolved with the methods. + * @deprecated since 4.3. Use CoreEnrol.getSupportedCourseEnrolmentMethods instead. */ - async getCourseEnrolmentMethods(id: number, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - - const params: CoreEnrolGetCourseEnrolmentMethodsWSParams = { - courseid: id, - }; - const preSets = { - cacheKey: this.getCourseEnrolmentMethodsCacheKey(id), - updateFrequency: CoreSite.FREQUENCY_RARELY, - }; - - return site.read - ('core_enrol_get_course_enrolment_methods', params, preSets); - } - - /** - * Get cache key for get course enrolment methods WS call. - * - * @param id Course ID. - * @returns Cache key. - */ - protected getCourseEnrolmentMethodsCacheKey(id: number): string { - return ROOT_CACHE_KEY + 'enrolmentmethods:' + id; + async getCourseEnrolmentMethods(courseId: number, siteId?: string): Promise { + return CoreEnrol.getSupportedCourseEnrolmentMethods(courseId, { siteId }); } /** @@ -344,70 +326,10 @@ export class CoreCoursesProvider { * @param instanceId Guest instance ID. * @param siteId Site ID. If not defined, use current site. * @returns Promise resolved when the info is retrieved. + * @deprecated since 4.3 use AddonEnrolGuest.getCourseGuestEnrolmentInfo instead. */ - async getCourseGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - const params: EnrolGuestGetInstanceInfoWSParams = { - instanceid: instanceId, - }; - const preSets: CoreSiteWSPreSets = { - cacheKey: this.getCourseGuestEnrolmentInfoCacheKey(instanceId), - updateFrequency: CoreSite.FREQUENCY_RARELY, - }; - const response = await site.read('enrol_guest_get_instance_info', params, preSets); - - return response.instanceinfo; - } - - /** - * Get cache key for get course guest enrolment methods WS call. - * - * @param instanceId Guest instance ID. - * @returns Cache key. - */ - protected getCourseGuestEnrolmentInfoCacheKey(instanceId: number): string { - return ROOT_CACHE_KEY + 'guestinfo:' + instanceId; - } - - /** - * Check if guest password validation WS is available on the current site. - * - * @returns Whether guest password validation WSget courses by field is available. - */ - isValidateGuestAccessPasswordAvailable(): boolean { - return CoreSites.wsAvailableInCurrentSite('enrol_guest_validate_password'); - } - - /** - * Perform password validation of guess access. - * - * @param enrolmentInstanceId Instance id of guest enrolment plugin. - * @param password Course Password. - * @returns Wether the password is valid. - */ - async validateGuestAccessPassword( - enrolmentInstanceId: number, - password: string, - ): Promise { - const site = CoreSites.getCurrentSite(); - - if (!site) { - return { - validated: false, - }; - } - const preSets: CoreSiteWSPreSets = { - getFromCache: false, - saveToCache: false, - emergencyCache: false, - }; - - const params: EnrolGuestValidatePasswordWSParams = { - instanceid: enrolmentInstanceId, - password, - }; - - return await site.read('enrol_guest_validate_password', params, preSets); + async getCourseGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise { + return AddonEnrolGuest.getGuestEnrolmentInfo(instanceId, siteId); } /** @@ -1116,14 +1038,13 @@ export class CoreCoursesProvider { /** * Invalidates get course enrolment methods WS call. * - * @param id Course ID. + * @param courseId Course ID. * @param siteId Site Id. If not defined, use current site. * @returns Promise resolved when the data is invalidated. + * @deprecated since 4.3, use CoreEnrol.invalidateCourseEnrolmentMethods instead. */ - async invalidateCourseEnrolmentMethods(id: number, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - - await site.invalidateWsCacheForKey(this.getCourseEnrolmentMethodsCacheKey(id)); + async invalidateCourseEnrolmentMethods(courseId: number, siteId?: string): Promise { + return CoreEnrol.invalidateCourseEnrolmentMethods(courseId, siteId); } /** @@ -1132,11 +1053,10 @@ export class CoreCoursesProvider { * @param instanceId Guest instance ID. * @param siteId Site Id. If not defined, use current site. * @returns Promise resolved when the data is invalidated. + * @deprecated since 4.3, use CoreEnrolDelegate.invalidate instead. */ async invalidateCourseGuestEnrolmentInfo(instanceId: number, siteId?: string): Promise { - const site = await CoreSites.getSite(siteId); - - await site.invalidateWsCacheForKey(this.getCourseGuestEnrolmentInfoCacheKey(instanceId)); + return AddonEnrolGuest.invalidateGuestEnrolmentInfo(instanceId, siteId); } /** @@ -1270,16 +1190,6 @@ export class CoreCoursesProvider { await site.invalidateWsCacheForKey(this.getUserNavigationOptionsCacheKey(courseIds)); } - /** - * Check if WS to retrieve guest enrolment data is available. - * - * @returns Whether guest WS is available. - * @deprecated since app 3.9.5 - */ - isGuestWSAvailable(): boolean { - return true; - } - /** * Report a dashboard or my courses page view event. * @@ -1339,42 +1249,10 @@ export class CoreCoursesProvider { * @param siteId Site ID. If not defined, use current site. * @returns Promise resolved if the user is enrolled. If the password is invalid, the promise is rejected * with an object with errorcode = CoreCoursesProvider.ENROL_INVALID_KEY. + * @deprecated since 4.3, use CoreEnrolDelegate.enrol instead. */ async selfEnrol(courseId: number, password: string = '', instanceId?: number, siteId?: string): Promise { - - const site = await CoreSites.getSite(siteId); - - const params: EnrolSelfEnrolUserWSParams = { - courseid: courseId, - password: password, - }; - if (instanceId) { - params.instanceid = instanceId; - } - - const response = await site.write('enrol_self_enrol_user', params); - - if (!response) { - throw Error('WS enrol_self_enrol_user failed'); - } - - if (response.status) { - return true; - } - - if (response.warnings && response.warnings.length) { - // Invalid password warnings. - const warning = response.warnings.find((warning) => - warning.warningcode == '2' || warning.warningcode == '3' || warning.warningcode == '4'); - - if (warning) { - throw new CoreWSError({ errorcode: CoreCoursesProvider.ENROL_INVALID_KEY, message: warning.message }); - } else { - throw new CoreWSError(response.warnings[0]); - } - } - - throw Error('WS enrol_self_enrol_user failed without warnings'); + return AddonEnrolSelf.selfEnrol(courseId, password, instanceId, siteId); } /** @@ -1823,50 +1701,19 @@ export type CoreCourseUserAdminOrNavOptionIndexed = { boolean; // Whether the option is available or not. }; -/** - * Params of core_enrol_get_course_enrolment_methods WS. - */ -type CoreEnrolGetCourseEnrolmentMethodsWSParams = { - courseid: number; // Course id. -}; - -/** - * Data returned by core_enrol_get_course_enrolment_methods WS. - */ -type CoreEnrolGetCourseEnrolmentMethodsWSResponse = CoreCourseEnrolmentMethod[]; - /** * Course enrolment basic info. + * + * @deprecated since 4.3 use CoreEnrolEnrolmentInfo instead. */ -export type CoreCourseEnrolmentInfo = { - id: number; // Id of course enrolment instance. - courseid: number; // Id of course. - type: string; // Type of enrolment plugin. - name: string; // Name of enrolment plugin. -}; +export type CoreCourseEnrolmentInfo = CoreEnrolEnrolmentInfo; /** * Course enrolment method. + * + * @deprecated since 4.3 use CoreEnrolEnrolmentMethod instead. */ -export type CoreCourseEnrolmentMethod = CoreCourseEnrolmentInfo & { - wsfunction?: string; // Webservice function to get more information. - status: string; // Status of enrolment plugin. True if successful, else error message or false. -}; - -/** - * Params of enrol_guest_get_instance_info WS. - */ -type EnrolGuestGetInstanceInfoWSParams = { - instanceid: number; // Instance id of guest enrolment plugin. -}; - -/** - * Data returned by enrol_guest_get_instance_info WS. - */ -export type EnrolGuestGetInstanceInfoWSResponse = { - instanceinfo: CoreCourseEnrolmentGuestMethod; - warnings?: CoreWSExternalWarning[]; -}; +export type CoreCourseEnrolmentMethod = CoreEnrolEnrolmentMethod; /** * Params of core_course_get_recent_courses WS. @@ -1888,23 +1735,6 @@ export type CoreCourseGetRecentCoursesOptions = CoreSitesCommonWSOptions & { sort?: string; // Sort string. }; -/** - * Course guest enrolment method. - */ -export type CoreCourseEnrolmentGuestMethod = CoreCourseEnrolmentInfo & { - passwordrequired: boolean; // Is a password required? - status: boolean; // Is the enrolment enabled? -}; - -/** - * Params of enrol_self_enrol_user WS. - */ -type EnrolSelfEnrolUserWSParams = { - courseid: number; // Id of the course. - password?: string; // Enrolment key. - instanceid?: number; // Instance id of self enrolment plugin. -}; - /** * Params of core_course_set_favourite_courses WS. */ @@ -1928,23 +1758,6 @@ export type CoreCourseAnyCourseDataWithOptions = CoreCourseAnyCourseData & { admOptions?: CoreCourseUserAdminOrNavOptionIndexed; }; -/** - * Params of enrol_guest_validate_password WS. - */ -type EnrolGuestValidatePasswordWSParams = { - instanceid: number; // instance id of guest enrolment plugin - password: string; // the course password -}; - -/** - * Data returned by enrol_guest_get_instance_info WS. - */ -export type EnrolGuestValidatePasswordWSResponse = { - validated: boolean; // Whether the password was successfully validated - hint?: string; // Password hint (if enabled) - warnings?: CoreWSExternalWarning[]; -}; - /** * Params of core_my_view_page WS. */ diff --git a/src/core/features/enrol/enrol.module.ts b/src/core/features/enrol/enrol.module.ts new file mode 100644 index 00000000000..c211f6849db --- /dev/null +++ b/src/core/features/enrol/enrol.module.ts @@ -0,0 +1,24 @@ +// (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 { NgModule, Type } from '@angular/core'; + +import { CoreEnrolDelegateService } from './services/enrol-delegate'; + +export const CORE_ENROL_SERVICES: Type[] = [ + CoreEnrolDelegateService, +]; + +@NgModule({}) +export class CoreEnrolModule {} diff --git a/src/core/features/enrol/services/enrol-delegate.ts b/src/core/features/enrol/services/enrol-delegate.ts new file mode 100644 index 00000000000..a728ef96e6f --- /dev/null +++ b/src/core/features/enrol/services/enrol-delegate.ts @@ -0,0 +1,240 @@ +// (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 { CoreDelegate, CoreDelegateHandler } from '@classes/delegate'; +import { makeSingleton } from '@singletons'; +import { CoreEnrolEnrolmentMethod } from './enrol'; + +/** + * Enrolment actions. + */ +export enum CoreEnrolAction { + BROWSER = 'browser', // User should use the browser to enrol. Ie. paypal + SELF = 'self', // User can enrol himself or herself. + GUEST = 'guest', // User can view the course without enrolling, like guest enrolment. + NOT_SUPPORTED = 'not_supported', // Enrolment method is not supported by the app. +} + +/** + * Interface that all enrolment plugins must implement. + */ +export interface CoreEnrolHandler extends CoreDelegateHandler { + /** + * Name of the enrol the handler supports. E.g. 'self'. + */ + type: string; + + /** + * Action to take when enroling. + */ + enrolmentAction: CoreEnrolAction; + + /** + * Returns the data needed to render the icon. + * + * @param courseId Course Id. + * @returns Icons data. + */ + getInfoIcons?(courseId: number): Promise; + + /** + * Invalidates the enrolment info. + * + * @param method Course enrolment method. + * @returns Promise resolved when done + */ + invalidate?(method: CoreEnrolEnrolmentMethod): Promise; +} + +/** + * Interface that all self enrolment plugins must implement. + */ +export interface CoreEnrolSelfHandler extends CoreEnrolHandler { + /** + * @inheritdoc + */ + enrolmentAction: CoreEnrolAction.SELF; + + /** + * Enrols the user and returns if it has been enrolled or not. + * + * @param method Course enrolment method. + * @returns If the user has been enrolled. + */ + enrol(method: CoreEnrolEnrolmentMethod): Promise; +} + +/** + * Interface that all guest enrolment plugins must implement. + */ +export interface CoreEnrolGuestHandler extends CoreEnrolHandler { + /** + * @inheritdoc + */ + enrolmentAction: CoreEnrolAction.GUEST; + + /** + * Check if the user can access to the course. + * + * @param method Course enrolment method. + * @returns Whether the user can access. + */ + canAccess(method: CoreEnrolEnrolmentMethod): Promise; + + /** + * Validates the access to a course + * + * @param method Course enrolment method. + * @returns Whether the user has validated the access to the course. + */ + validateAccess(method: CoreEnrolEnrolmentMethod): Promise; + +} + +/** + * Data needed to render a enrolment icons. It's returned by the handler. + */ +export interface CoreEnrolInfoIcon { + label: string; + icon: string; + className?: string; +} + +/** + * Delegate to register enrol handlers. + */ +@Injectable({ providedIn: 'root' }) +export class CoreEnrolDelegateService extends CoreDelegate { + + protected handlerNameProperty = 'type'; + protected featurePrefix = 'CoreEnrolDelegate_'; + + constructor() { + super('CoreEnrolDelegate', true); + } + + /** + * Check if an enrolment plugin is supported. + * + * @param methodType Enrol method type. + * @returns Whether it's supported. + */ + isEnrolSupported(methodType: string): boolean { + return this.hasHandler(methodType, true); + } + + /** + * Get enrolment action. + * + * @param methodType Enrol method type. + * @returns The enrolment action to take. + */ + getEnrolmentAction(methodType: string): CoreEnrolAction { + const handler = this.getHandler(methodType, false); + if (!handler) { + return CoreEnrolAction.NOT_SUPPORTED; + } + + return handler.enrolmentAction; + } + + /** + * Get the enrol icon for a certain enrolment method. + * + * @param methodType The methodType to get the icon. + * @param courseId Course Id. + * @returns Promise resolved with the display data. + */ + async getInfoIcons(methodType: string, courseId: number): Promise { + const icons = await this.executeFunctionOnEnabled( + methodType, + 'getInfoIcons', + [courseId], + ); + + icons?.forEach((icon) => { + if (!icon.className) { + icon.className = `addon-enrol-${methodType}`; + } + }); + + return icons || []; + } + + /** + * Enrols the user and returns if it has been enrolled or not. + * + * @param method Course enrolment method. + * @returns If the user has been enrolled. + */ + async enrol(method: CoreEnrolEnrolmentMethod): Promise { + const enrolled = await this.executeFunctionOnEnabled( + method.type, + 'enrol', + [method], + ); + + return !!enrolled; + } + + /** + * Check if the user can access to the course. + * + * @param method Course enrolment method. + * @returns Whether the user can access. + */ + async canAccess(method: CoreEnrolEnrolmentMethod): Promise { + const canAccess = await this.executeFunctionOnEnabled( + method.type, + 'canAccess', + [method], + ); + + return !!canAccess; + } + + /** + * Validates the access to a course. + * + * @param method Course enrolment method. + * @returns Whether the user has validated the access to the course. + */ + async validateAccess(method: CoreEnrolEnrolmentMethod): Promise { + const validated = await this.executeFunctionOnEnabled( + method.type, + 'validateAccess', + [method], + ); + + return !!validated; + } + + /** + * Invalidates the enrolment info. + * + * @param method Course enrolment method. + * @returns Promise resolved when done + */ + async invalidate(method: CoreEnrolEnrolmentMethod): Promise { + await this.executeFunctionOnEnabled( + method.type, + 'invalidate', + [method], + ); + } + +} + +export const CoreEnrolDelegate = makeSingleton(CoreEnrolDelegateService); diff --git a/src/core/features/enrol/services/enrol-helper.ts b/src/core/features/enrol/services/enrol-helper.ts new file mode 100644 index 00000000000..1954254fddc --- /dev/null +++ b/src/core/features/enrol/services/enrol-helper.ts @@ -0,0 +1,128 @@ +// (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 { makeSingleton } from '@singletons'; +import { CoreEnrolAction, CoreEnrolDelegate, CoreEnrolInfoIcon } from './enrol-delegate'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreEnrol, CoreEnrolEnrolmentMethod } from './enrol'; + +/** + * Service that provides helper functions for enrolment plugins. + */ +@Injectable({ providedIn: 'root' }) +export class CoreEnrolHelperService { + + /** + * Get enrolment icons to show enrol status. + * + * @param methodTypes List of enrolment types to show. + * @param courseId Course Id. + * @returns Enrolment icons to show. + */ + async getEnrolmentIcons(methodTypes: string[], courseId: number): Promise { + methodTypes = CoreUtils.uniqueArray(methodTypes); + + let enrolmentIcons: CoreEnrolInfoIcon[] = []; + let addBrowserOption = false; + + const promises = methodTypes.map(async (type) => { + const enrolIcons = await CoreEnrolDelegate.getInfoIcons(type, courseId); + + if (enrolIcons.length) { + enrolmentIcons = enrolmentIcons.concat(enrolIcons); + + return; + } + + const action = CoreEnrolDelegate.getEnrolmentAction(type); + addBrowserOption = addBrowserOption || action === CoreEnrolAction.BROWSER; + }); + + await Promise.all(promises); + + if (addBrowserOption) { + enrolmentIcons.push({ + className: 'enrol_browser', + label: 'core.courses.otherenrolments', + icon: 'fas-up-right-from-square', + }); + } + + if (enrolmentIcons.length == 0) { + enrolmentIcons.push({ + className: 'enrol_locked', + label: 'core.courses.notenrollable', + icon: 'fas-lock', + }); + } + + return enrolmentIcons; + } + + /** + * Get enrolment methods divided by type. + * + * @param courseId Course Id. + * @returns Enrolment info divided by types. + */ + async getEnrolmentsByType(courseId: number): Promise { + const enrolmentMethods = await CoreEnrol.getSupportedCourseEnrolmentMethods(courseId); + + const self: CoreEnrolEnrolmentMethod[] = []; + const guest: CoreEnrolEnrolmentMethod[] = []; + let hasBrowser = false; + let hasNotSupported = false; + + enrolmentMethods.forEach((method) => { + if (!CoreUtils.isTrueOrOne(method.status)) { + return; + } + + const action = CoreEnrolDelegate.getEnrolmentAction(method.type); + + switch (action) { + case CoreEnrolAction.SELF: + self.push(method); + break; + case CoreEnrolAction.GUEST: + guest.push(method); + break; + case CoreEnrolAction.BROWSER: + hasBrowser = true; + break; + case CoreEnrolAction.NOT_SUPPORTED: + hasNotSupported = true; + break; + } + }); + + return { + self, + guest, + hasBrowser, + hasNotSupported, + }; + } + +} + +export const CoreEnrolHelper = makeSingleton(CoreEnrolHelperService); + +export type CoreEnrolmentsByType = { + self: CoreEnrolEnrolmentMethod[]; + guest: CoreEnrolEnrolmentMethod[]; + hasBrowser: boolean; + hasNotSupported: boolean; +}; diff --git a/src/core/features/enrol/services/enrol.ts b/src/core/features/enrol/services/enrol.ts new file mode 100644 index 00000000000..b18a3a2e24b --- /dev/null +++ b/src/core/features/enrol/services/enrol.ts @@ -0,0 +1,138 @@ +// (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 { makeSingleton } from '@singletons'; +import { CoreSite } from '@classes/site'; +import { CoreSites } from '@services/sites'; +import { CoreUtils } from '@services/utils/utils'; +import { CoreEnrolAction, CoreEnrolDelegate } from './enrol-delegate'; + +/** + * Service that provides functions for enrolment plugins. + */ +@Injectable({ providedIn: 'root' }) +export class CoreEnrolService { + + protected static readonly ROOT_CACHE_KEY = 'CoreEnrol:'; + + /** + * Get the enrolment methods from a course. + * + * @param courseId ID of the course. + * @param siteId Site ID. If not defined, use current site. + * @returns Promise resolved with the methods. + */ + protected async getCourseEnrolmentMethods(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + const params: CoreEnrolGetCourseEnrolmentMethodsWSParams = { + courseid: courseId, + }; + const preSets = { + cacheKey: this.getCourseEnrolmentMethodsCacheKey(courseId), + updateFrequency: CoreSite.FREQUENCY_RARELY, + }; + + return site.read('core_enrol_get_course_enrolment_methods', params, preSets); + } + + /** + * Get the enrolment methods from a course that are enabled and supported by the app. + * + * @param courseId ID of the course. + * @param options Options. + * @returns Promise resolved with the methods. + */ + async getSupportedCourseEnrolmentMethods( + courseId: number, + options: CoreEnrolGetSupportedMethodsOptions = {}, + ): Promise { + const methods = await CoreEnrol.getCourseEnrolmentMethods(courseId, options.siteId); + + return methods.filter((method) => { + if (options.type && method.type !== options.type) { + return false; + } + + return CoreEnrolDelegate.isEnrolSupported(method.type) && CoreUtils.isTrueOrOne(method.status) && + (!options.action || CoreEnrolDelegate.getEnrolmentAction(method.type) === options.action); + }); + } + + /** + * Get cache key for get course enrolment methods WS call. + * + * @param courseId Course ID. + * @returns Cache key. + */ + protected getCourseEnrolmentMethodsCacheKey(courseId: number): string { + return CoreEnrolService.ROOT_CACHE_KEY + 'enrolmentmethods:' + courseId; + } + + /** + * Invalidates get course enrolment methods WS call. + * + * @param courseId Course ID. + * @param siteId Site Id. If not defined, use current site. + * @returns Promise resolved when the data is invalidated. + */ + async invalidateCourseEnrolmentMethods(courseId: number, siteId?: string): Promise { + const site = await CoreSites.getSite(siteId); + + await Promise.all([ + site.invalidateWsCacheForKey(this.getCourseEnrolmentMethodsCacheKey(courseId)), + site.invalidateWsCacheForKey(`mmCourses:enrolmentmethods:${courseId}`), // @todo: Remove after 4.3 release. + ]); + } + +} + +export const CoreEnrol = makeSingleton(CoreEnrolService); + +/** + * Params of core_enrol_get_course_enrolment_methods WS. + */ +type CoreEnrolGetCourseEnrolmentMethodsWSParams = { + courseid: number; // Course id. +}; + +/** + * Data returned by core_enrol_get_course_enrolment_methods WS. + */ +type CoreEnrolGetCourseEnrolmentMethodsWSResponse = CoreEnrolEnrolmentMethod[]; + +/** + * Course enrolment method. + */ +export type CoreEnrolEnrolmentMethod = CoreEnrolEnrolmentInfo & { + wsfunction?: string; // Webservice function to get more information. + status: string; // Status of enrolment plugin. True if successful, else error message or false. +}; + +/** + * Course enrolment basic info. + */ +export type CoreEnrolEnrolmentInfo = { + id: number; // Id of course enrolment instance. + courseid: number; // Id of course. + type: string; // Type of enrolment plugin. + name: string; // Name of enrolment plugin. +}; + +export type CoreEnrolGetSupportedMethodsOptions = { + type?: string; // If set, only get methods of a certain type. + action?: CoreEnrolAction; // If set, only get methods that use a certain action. + siteId?: string; // Site ID. If not defined, use current site. +}; diff --git a/src/core/features/features.module.ts b/src/core/features/features.module.ts index 6c4e3240e58..61c97cbe730 100644 --- a/src/core/features/features.module.ts +++ b/src/core/features/features.module.ts @@ -21,6 +21,7 @@ import { CoreCourseModule } from './course/course.module'; import { CoreCoursesModule } from './courses/courses.module'; import { CoreEditorModule } from './editor/editor.module'; import { CoreEmulatorModule } from './emulator/emulator.module'; +import { CoreEnrolModule } from './enrol/enrol.module'; import { CoreFileUploaderModule } from './fileuploader/fileuploader.module'; import { CoreFilterModule } from './filter/filter.module'; import { CoreGradesModule } from './grades/grades.module'; @@ -53,6 +54,7 @@ import { CoreReportBuilderModule } from './reportbuilder/reportbuilder.module'; CoreCourseModule, CoreCoursesModule, CoreEditorModule, + CoreEnrolModule, CoreFileUploaderModule, CoreFilterModule, CoreGradesModule, diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index d17696b342f..9197a9b5141 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -60,6 +60,7 @@ import { CorePlatform } from '@services/platform'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreLang } from '@services/lang'; import { CorePasswordModalParams, CorePasswordModalResponse } from '@components/password-modal/password-modal'; +import { CoreWSError } from '@classes/errors/wserror'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -1903,6 +1904,8 @@ export class CoreDomUtilsProvider { if (modalData === undefined) { throw new CoreCanceledError(); + } else if (modalData instanceof CoreWSError) { + throw modalData; } return modalData; From c20df40bdc7942bd88e60e5f6e2b7c21e8be8592 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 23 Aug 2023 16:05:20 +0200 Subject: [PATCH 2/4] MOBILE-4323 enrol: Support enrolment action on siteplugins Co-authored: dpalou --- src/core/features/compile/services/compile.ts | 2 + src/core/features/enrol/enrol.module.ts | 4 + .../classes/handlers/enrol-handler.ts | 77 +++++++++++++++++++ .../services/siteplugins-helper.ts | 70 +++++++++++++++++ .../siteplugins/services/siteplugins.ts | 11 ++- 5 files changed, 163 insertions(+), 1 deletion(-) create mode 100644 src/core/features/siteplugins/classes/handlers/enrol-handler.ts diff --git a/src/core/features/compile/services/compile.ts b/src/core/features/compile/services/compile.ts index 0ba9a739c20..30eb5d31b83 100644 --- a/src/core/features/compile/services/compile.ts +++ b/src/core/features/compile/services/compile.ts @@ -47,6 +47,7 @@ import { CORE_CONTENTLINKS_SERVICES } from '@features/contentlinks/contentlinks. import { CORE_COURSE_SERVICES } from '@features/course/course.module'; import { CORE_COURSES_SERVICES } from '@features/courses/courses.module'; import { CORE_EDITOR_SERVICES } from '@features/editor/editor.module'; +import { CORE_ENROL_SERVICES } from '@features/enrol/enrol.module'; import { CORE_NATIVE_SERVICES } from '@features/native/native.module'; import { CORE_FILEUPLOADER_SERVICES } from '@features/fileuploader/fileuploader.module'; import { CORE_FILTER_SERVICES } from '@features/filter/filter.module'; @@ -269,6 +270,7 @@ export class CoreCompileProvider { ...CORE_COURSE_SERVICES, ...CORE_COURSES_SERVICES, ...CORE_EDITOR_SERVICES, + ...CORE_ENROL_SERVICES, ...CORE_FILEUPLOADER_SERVICES, ...CORE_FILTER_SERVICES, ...CORE_GRADES_SERVICES, diff --git a/src/core/features/enrol/enrol.module.ts b/src/core/features/enrol/enrol.module.ts index c211f6849db..d1f8472b6c4 100644 --- a/src/core/features/enrol/enrol.module.ts +++ b/src/core/features/enrol/enrol.module.ts @@ -15,8 +15,12 @@ import { NgModule, Type } from '@angular/core'; import { CoreEnrolDelegateService } from './services/enrol-delegate'; +import { CoreEnrolService } from './services/enrol'; +import { CoreEnrolHelperService } from './services/enrol-helper'; export const CORE_ENROL_SERVICES: Type[] = [ + CoreEnrolService, + CoreEnrolHelperService, CoreEnrolDelegateService, ]; diff --git a/src/core/features/siteplugins/classes/handlers/enrol-handler.ts b/src/core/features/siteplugins/classes/handlers/enrol-handler.ts new file mode 100644 index 00000000000..f387804bb2b --- /dev/null +++ b/src/core/features/siteplugins/classes/handlers/enrol-handler.ts @@ -0,0 +1,77 @@ +// (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 { CoreLogger } from '@singletons/logger'; +import { CoreSitePluginsBaseHandler } from './base-handler'; +import { CoreEnrolAction, CoreEnrolHandler, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate'; +import { CoreSitePluginsContent, CoreSitePluginsEnrolHandlerData } from '@features/siteplugins/services/siteplugins'; + +/** + * Handler to support a enrol using a site plugin. + */ +export class CoreSitePluginsEnrolHandler extends CoreSitePluginsBaseHandler implements CoreEnrolHandler { + + protected logger: CoreLogger; + + constructor( + name: string, + public type: string, + public enrolmentAction: CoreEnrolAction, + protected handlerSchema: CoreSitePluginsEnrolHandlerData, + protected initResult: CoreSitePluginsContent | null, + ) { + super(name); + + this.logger = CoreLogger.getInstance('CoreSitePluginsEnrolHandler'); + } + + /** + * @inheritdoc + */ + async getInfoIcons(): Promise { + return this.handlerSchema.infoIcons ?? []; + } + + /** + * @inheritdoc + */ + async invalidate(): Promise { + // To be overridden. + } + + /** + * @inheritdoc + */ + async enrol(): Promise { + // To be overridden. + return false; + } + + /** + * @inheritdoc + */ + async canAccess(): Promise { + // To be overridden. + return false; + } + + /** + * @inheritdoc + */ + async validateAccess(): Promise { + // To be overridden. + return false; + } + +} diff --git a/src/core/features/siteplugins/services/siteplugins-helper.ts b/src/core/features/siteplugins/services/siteplugins-helper.ts index 9b485bf3a71..93e96eddabb 100644 --- a/src/core/features/siteplugins/services/siteplugins-helper.ts +++ b/src/core/features/siteplugins/services/siteplugins-helper.ts @@ -74,6 +74,7 @@ import { CoreSitePluginsHandlerCommonData, CoreSitePluginsInitHandlerData, CoreSitePluginsMainMenuHomeHandlerData, + CoreSitePluginsEnrolHandlerData, } from './siteplugins'; import { makeSingleton } from '@singletons'; import { CoreMainMenuHomeDelegate } from '@features/mainmenu/services/home-delegate'; @@ -86,6 +87,8 @@ import { CoreContentLinksModuleListHandler } from '@features/contentlinks/classe import { CoreObject } from '@singletons/object'; import { CoreUrlUtils } from '@services/utils/url'; import { CorePath } from '@singletons/path'; +import { CoreEnrolAction, CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate'; +import { CoreSitePluginsEnrolHandler } from '../classes/handlers/enrol-handler'; const HANDLER_DISABLED = 'core_site_plugins_helper_handler_disabled'; @@ -561,6 +564,10 @@ export class CoreSitePluginsHelperProvider { uniqueName = this.registerMainMenuHomeHandler(plugin, handlerName, handlerSchema, initResult); break; + case 'CoreEnrolDelegate': + uniqueName = await this.registerEnrolHandler(plugin, handlerName, handlerSchema, initResult); + break; + default: // Nothing to do. } @@ -800,6 +807,69 @@ export class CoreSitePluginsHelperProvider { return uniqueName; } + /** + * Given a handler in a plugin, register it in the enrol delegate. + * + * @param plugin Data of the plugin. + * @param handlerName Name of the handler in the plugin. + * @param handlerSchema Data about the handler. + * @param initResult Result of init function. + * @returns A string to identify the handler. + */ + protected async registerEnrolHandler( + plugin: CoreSitePluginsPlugin, + handlerName: string, + handlerSchema: CoreSitePluginsEnrolHandlerData, + initResult: CoreSitePluginsContent | null, + ): Promise { + const uniqueName = CoreSitePlugins.getHandlerUniqueName(plugin, handlerName); + const type = (handlerSchema.moodlecomponent || plugin.component).replace('enrol_', ''); + const action = handlerSchema.enrolmentAction ?? CoreEnrolAction.BROWSER; + const handler = new CoreSitePluginsEnrolHandler(uniqueName, type, action, handlerSchema, initResult); + + if (!handlerSchema.method && (action === CoreEnrolAction.SELF || action === CoreEnrolAction.GUEST)) { + this.logger.error('"self" or "guest" enrol plugins must implement a method to override the required JS functions.'); + + return; + } + + if (handlerSchema.method) { + // Execute the main method and its JS to allow implementing the handler functions. + const result = await this.executeMethodAndJS(plugin, handlerSchema.method); + + if (action === CoreEnrolAction.SELF && !result.jsResult?.enrol) { + this.logger.error('"self" enrol plugins must implement an "enrol" function in the JS returned by the method.'); + + return; + } + + if (action === CoreEnrolAction.GUEST && (!result.jsResult?.canAccess || !result.jsResult?.validateAccess)) { + this.logger.error('"guest" enrol plugins must implement "canAccess" and "validateAccess" functions in the JS ' + + 'returned by the method.'); + + return; + } + + if (result.jsResult) { + // Override default handler functions with the result of the method JS. + const jsResult = > result.jsResult; + const handlerProperties = CoreObject.getAllPropertyNames(handler); + + for (const property of handlerProperties) { + if (property !== 'constructor' && typeof handler[property] === 'function' && + typeof jsResult[property] === 'function') { + // eslint-disable-next-line @typescript-eslint/ban-types + handler[property] = ( jsResult[property]).bind(handler); + } + } + } + } + + CoreEnrolDelegate.registerHandler(handler); + + return uniqueName; + } + /** * Given a handler in a plugin, register it in the main menu delegate. * diff --git a/src/core/features/siteplugins/services/siteplugins.ts b/src/core/features/siteplugins/services/siteplugins.ts index 856f8ccfe79..76dc1569b49 100644 --- a/src/core/features/siteplugins/services/siteplugins.ts +++ b/src/core/features/siteplugins/services/siteplugins.ts @@ -31,6 +31,7 @@ import { CoreLogger } from '@singletons/logger'; import { CoreSitePluginsModuleHandler } from '../classes/handlers/module-handler'; import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; +import { CoreEnrolAction, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate'; const ROOT_CACHE_KEY = 'CoreSitePlugins:'; @@ -825,7 +826,7 @@ export type CoreSitePluginsPlugin = CoreSitePluginsWSPlugin & { export type CoreSitePluginsHandlerData = CoreSitePluginsInitHandlerData | CoreSitePluginsCourseOptionHandlerData | CoreSitePluginsMainMenuHandlerData | CoreSitePluginsCourseModuleHandlerData | CoreSitePluginsCourseFormatHandlerData | CoreSitePluginsUserHandlerData | CoreSitePluginsSettingsHandlerData | CoreSitePluginsMessageOutputHandlerData | -CoreSitePluginsBlockHandlerData | CoreSitePluginsMainMenuHomeHandlerData; +CoreSitePluginsBlockHandlerData | CoreSitePluginsMainMenuHomeHandlerData | CoreSitePluginsEnrolHandlerData; /** * Plugin handler data common to all delegates. @@ -960,6 +961,14 @@ export type CoreSitePluginsBlockHandlerData = CoreSitePluginsHandlerCommonData & fallback?: string; }; +/** + * Enrol handler specific data. + */ +export type CoreSitePluginsEnrolHandlerData = CoreSitePluginsHandlerCommonData & { + enrolmentAction?: CoreEnrolAction; + infoIcons?: CoreEnrolInfoIcon[]; +}; + /** * Common handler data with some data from the init method. */ From 6862f4e73a9582e42837e8f603ced3ff7dfc0a31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 25 Jul 2023 17:09:07 +0200 Subject: [PATCH 3/4] MOBILE-4323 styles: Reinforce the format-text styles when inside an item --- src/theme/components/format-text.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/src/theme/components/format-text.scss b/src/theme/components/format-text.scss index 10135e76dc5..13ba068aed4 100644 --- a/src/theme/components/format-text.scss +++ b/src/theme/components/format-text.scss @@ -211,6 +211,7 @@ core-format-text { } core-format-text, +.item core-format-text, core-rich-text-editor .core-rte-editor { @include core-headings(); From 77d4f53d2f0ceb6afa48fb095c7540d919820971 Mon Sep 17 00:00:00 2001 From: Dani Palou Date: Wed, 23 Aug 2023 09:51:57 +0200 Subject: [PATCH 4/4] MOBILE-4323 enrol: Check all methods in courseUsesGuestAccessInfo --- .../enrol/guest/services/enrol-handler.ts | 14 ++++- .../course-summary/course-summary.page.ts | 2 +- .../features/course/services/course-helper.ts | 55 ++++++++++++++----- .../courses/services/handlers/course-link.ts | 2 +- .../features/enrol/services/enrol-delegate.ts | 21 ++++--- .../classes/handlers/enrol-handler.ts | 11 +++- 6 files changed, 75 insertions(+), 30 deletions(-) diff --git a/src/addons/enrol/guest/services/enrol-handler.ts b/src/addons/enrol/guest/services/enrol-handler.ts index d8b6f5ef0c0..0a0b174c4af 100644 --- a/src/addons/enrol/guest/services/enrol-handler.ts +++ b/src/addons/enrol/guest/services/enrol-handler.ts @@ -13,7 +13,12 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreEnrolAction, CoreEnrolGuestHandler, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate'; +import { + CoreEnrolAction, + CoreEnrolCanAccessData, + CoreEnrolGuestHandler, + CoreEnrolInfoIcon, +} from '@features/enrol/services/enrol-delegate'; import { makeSingleton } from '@singletons'; import { AddonEnrolGuest } from './guest'; import { CorePasswordModalResponse } from '@components/password-modal/password-modal'; @@ -66,10 +71,13 @@ export class AddonEnrolGuestHandlerService implements CoreEnrolGuestHandler { /** * @inheritdoc */ - async canAccess(method: CoreEnrolEnrolmentMethod): Promise { + async canAccess(method: CoreEnrolEnrolmentMethod): Promise { const info = await AddonEnrolGuest.getGuestEnrolmentInfo(method.id); - return info.status && (!info.passwordrequired || AddonEnrolGuest.isValidateGuestAccessPasswordAvailable()); + return { + canAccess: info.status && (!info.passwordrequired || AddonEnrolGuest.isValidateGuestAccessPasswordAvailable()), + requiresUserInput: info.passwordrequired, + }; } /** diff --git a/src/core/features/course/pages/course-summary/course-summary.page.ts b/src/core/features/course/pages/course-summary/course-summary.page.ts index e354df78ec2..d3c5700dca9 100644 --- a/src/core/features/course/pages/course-summary/course-summary.page.ts +++ b/src/core/features/course/pages/course-summary/course-summary.page.ts @@ -231,7 +231,7 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { if (!this.canAccessCourse) { // The user is not an admin/manager. Check if we can provide guest access to the course. const promises = this.guestEnrolInstances.map(async (method) => { - const canAccess = await CoreEnrolDelegate.canAccess(method); + const { canAccess } = await CoreEnrolDelegate.canAccess(method); if (canAccess) { this.canAccessCourse = true; } diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 74eaa9f5488..283d3e6d2e5 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -74,8 +74,8 @@ import { CoreCourseWithImageAndColor } from '@features/courses/services/courses- import { CoreCourseSummaryPage } from '../pages/course-summary/course-summary.page'; import { CoreRemindersPushNotificationData } from '@features/reminders/services/reminders'; import { CoreLocalNotifications } from '@services/local-notifications'; -import { AddonEnrolGuest } from '@addons/enrol/guest/services/guest'; import { CoreEnrol } from '@features/enrol/services/enrol'; +import { CoreEnrolAction, CoreEnrolDelegate } from '@features/enrol/services/enrol-delegate'; /** * Prefetch info of a module. @@ -594,22 +594,26 @@ export class CoreCourseHelperProvider { } /** - * Check whether a course is accessed using guest access and if it requires password to enter. + * Check whether a course is accessed using guest access and if it requires user input to enter. * * @param courseId Course ID. * @param siteId Site ID. If not defined, current site. - * @returns Promise resolved with guestAccess and passwordRequired booleans. + * @returns Data about guest access info. */ async courseUsesGuestAccessInfo( courseId: number, siteId?: string, - ): Promise<{guestAccess: boolean; passwordRequired?: boolean}> { + ): Promise { + const accessData: CoreCourseGuestAccessInfo = { + guestAccess: false, + }; + try { try { // Check if user is enrolled. If enrolled, no guest access. await CoreCourses.getUserCourse(courseId, false, siteId); - return { guestAccess: false }; + return accessData; } catch { // Ignore errors. } @@ -618,26 +622,35 @@ export class CoreCourseHelperProvider { // The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course. await CoreCourses.getCourse(courseId, siteId); - return { guestAccess: false }; + return accessData; } catch { // Ignore errors. } // Check if guest access is enabled. - const enrolmentMethods = await CoreEnrol.getSupportedCourseEnrolmentMethods(courseId, { type: 'guest', siteId }); + const enrolmentMethods = await CoreEnrol.getSupportedCourseEnrolmentMethods(courseId, { + action: CoreEnrolAction.GUEST, + siteId, + }); + if (!enrolmentMethods) { - return { guestAccess: false }; + return accessData; } - const info = await AddonEnrolGuest.getGuestEnrolmentInfo(enrolmentMethods[0].id); + const results = await Promise.all(enrolmentMethods.map(method => CoreEnrolDelegate.canAccess(method))); - // Don't allow guest access if it requires a password and it's available. - return { - guestAccess: info.status && (!info.passwordrequired || AddonEnrolGuest.isValidateGuestAccessPasswordAvailable()), - passwordRequired: info.passwordrequired, - }; + results.forEach(result => { + accessData.guestAccess = accessData.guestAccess || result.canAccess; + if (accessData.requiresUserInput !== false && result.canAccess) { + accessData.requiresUserInput = result.requiresUserInput ?? accessData.requiresUserInput; + } + }); + + accessData.passwordRequired = accessData.requiresUserInput; // For backwards compatibility. + + return accessData; } catch { - return { guestAccess: false }; + return accessData; } } @@ -2192,3 +2205,15 @@ export type CoreCourseOpenModuleOptions = { sectionId?: number; // Section the module belongs to. modNavOptions?: CoreNavigationOptions; // Navigation options to open the module, including params to pass to the module. }; + +/** + * Result of courseUsesGuestAccessInfo. + */ +export type CoreCourseGuestAccessInfo = { + guestAccess: boolean; // Whether guest access is enabled for a course. + requiresUserInput?: boolean; // Whether the first guest access enrolment method requires user input. + /** + * @deprecated since 4.3. Use requiresUserInput instead. + */ + passwordRequired?: boolean; +}; diff --git a/src/core/features/courses/services/handlers/course-link.ts b/src/core/features/courses/services/handlers/course-link.ts index 4c60c16b6fe..d29db57e62b 100644 --- a/src/core/features/courses/services/handlers/course-link.ts +++ b/src/core/features/courses/services/handlers/course-link.ts @@ -132,7 +132,7 @@ export class CoreCoursesCourseLinkHandlerService extends CoreContentLinksHandler const guestInfo = await CoreCourseHelper.courseUsesGuestAccessInfo(courseId); pageParams.isGuest = guestInfo.guestAccess; - if (hasAccess && !guestInfo.guestAccess && !guestInfo.passwordRequired) { + if (hasAccess && !guestInfo.guestAccess && !guestInfo.requiresUserInput) { // Direct access. const course = await CoreUtils.ignoreErrors(CoreCourses.getUserCourse(courseId), { id: courseId }); diff --git a/src/core/features/enrol/services/enrol-delegate.ts b/src/core/features/enrol/services/enrol-delegate.ts index a728ef96e6f..3ef216fda9e 100644 --- a/src/core/features/enrol/services/enrol-delegate.ts +++ b/src/core/features/enrol/services/enrol-delegate.ts @@ -89,9 +89,9 @@ export interface CoreEnrolGuestHandler extends CoreEnrolHandler { * Check if the user can access to the course. * * @param method Course enrolment method. - * @returns Whether the user can access. + * @returns Access info. */ - canAccess(method: CoreEnrolEnrolmentMethod): Promise; + canAccess(method: CoreEnrolEnrolmentMethod): Promise; /** * Validates the access to a course @@ -100,7 +100,6 @@ export interface CoreEnrolGuestHandler extends CoreEnrolHandler { * @returns Whether the user has validated the access to the course. */ validateAccess(method: CoreEnrolEnrolmentMethod): Promise; - } /** @@ -112,6 +111,14 @@ export interface CoreEnrolInfoIcon { className?: string; } +/** + * Data about course access using a GUEST enrolment method. + */ +export interface CoreEnrolCanAccessData { + canAccess: boolean; // Whether the user can access the course using this enrolment method. + requiresUserInput?: boolean; // Whether the user needs to input some data to access the course using this enrolment method. +} + /** * Delegate to register enrol handlers. */ @@ -193,16 +200,16 @@ export class CoreEnrolDelegateService extends CoreDelegate { * Check if the user can access to the course. * * @param method Course enrolment method. - * @returns Whether the user can access. + * @returns Access data. */ - async canAccess(method: CoreEnrolEnrolmentMethod): Promise { - const canAccess = await this.executeFunctionOnEnabled( + async canAccess(method: CoreEnrolEnrolmentMethod): Promise { + const canAccess = await this.executeFunctionOnEnabled( method.type, 'canAccess', [method], ); - return !!canAccess; + return canAccess ?? { canAccess: false }; } /** diff --git a/src/core/features/siteplugins/classes/handlers/enrol-handler.ts b/src/core/features/siteplugins/classes/handlers/enrol-handler.ts index f387804bb2b..188e63e8506 100644 --- a/src/core/features/siteplugins/classes/handlers/enrol-handler.ts +++ b/src/core/features/siteplugins/classes/handlers/enrol-handler.ts @@ -14,7 +14,12 @@ import { CoreLogger } from '@singletons/logger'; import { CoreSitePluginsBaseHandler } from './base-handler'; -import { CoreEnrolAction, CoreEnrolHandler, CoreEnrolInfoIcon } from '@features/enrol/services/enrol-delegate'; +import { + CoreEnrolAction, + CoreEnrolCanAccessData, + CoreEnrolHandler, + CoreEnrolInfoIcon, +} from '@features/enrol/services/enrol-delegate'; import { CoreSitePluginsContent, CoreSitePluginsEnrolHandlerData } from '@features/siteplugins/services/siteplugins'; /** @@ -61,9 +66,9 @@ export class CoreSitePluginsEnrolHandler extends CoreSitePluginsBaseHandler impl /** * @inheritdoc */ - async canAccess(): Promise { + async canAccess(): Promise { // To be overridden. - return false; + return { canAccess: false }; } /**