diff --git a/scripts/langindex.json b/scripts/langindex.json index fc50d343f..8d4bc3940 100644 --- a/scripts/langindex.json +++ b/scripts/langindex.json @@ -1582,6 +1582,9 @@ "core.course.errordownloadingsection": "local_moodlemobileapp", "core.course.errorgetmodule": "local_moodlemobileapp", "core.course.failed": "completion", + "core.course.guestaccess_passwordinvalid": "enrol_guest/passwordinvalid", + "core.course.guestaccess_withpassword": "enrol_guest", + "core.course.guestaccess": "enrol_guest/pluginname", "core.course.hiddenfromstudents": "moodle", "core.course.hiddenoncoursepage": "moodle", "core.course.highlighted": "moodle", diff --git a/src/addons/mod/lesson/services/handlers/prefetch.ts b/src/addons/mod/lesson/services/handlers/prefetch.ts index af6005197..f9a7e124e 100644 --- a/src/addons/mod/lesson/services/handlers/prefetch.ts +++ b/src/addons/mod/lesson/services/handlers/prefetch.ts @@ -46,20 +46,6 @@ export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPref // Don't check timers to decrease positives. If a user performs some action it will be reflected in other items. updatesNames = /^configuration$|^.*files$|^grades$|^gradeitems$|^pages$|^answers$|^questionattempts$|^pagesviewed$/; - /** - * Ask password. - * - * @returns Promise resolved with the password. - */ - protected async askUserPassword(): Promise { - // Create and show the modal. - return CoreDomUtils.promptPassword({ - title: 'addon.mod_lesson.enterpassword', - placeholder: 'core.login.password', - submit: 'addon.mod_lesson.continue', - }); - } - /** * Get the download size of a module. * @@ -149,7 +135,13 @@ export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPref throw new CoreError(accessInfo.preventaccessreasons[0].message); } - password = await this.askUserPassword(); + // Create and show the modal. + const response = await CoreDomUtils.promptPassword({ + title: 'addon.mod_lesson.enterpassword', + placeholder: 'core.login.password', + submit: 'addon.mod_lesson.continue', + }); + password = response.password; return this.validatePassword(lessonId, accessInfo, password, options); } diff --git a/src/core/components/password-modal/password-modal.html b/src/core/components/password-modal/password-modal.html index e54e70810..9a37010a7 100644 --- a/src/core/components/password-modal/password-modal.html +++ b/src/core/components/password-modal/password-modal.html @@ -11,15 +11,20 @@ -
- - {{ placeholder | translate }} - - - - - + +
+ + {{ placeholder | translate }} + + + + + + + + +
{{ submit | translate }} diff --git a/src/core/components/password-modal/password-modal.ts b/src/core/components/password-modal/password-modal.ts index af2e53b4f..dd7e43cd7 100644 --- a/src/core/components/password-modal/password-modal.ts +++ b/src/core/components/password-modal/password-modal.ts @@ -17,6 +17,7 @@ import { Component, ViewChild, ElementRef, Input } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreForms } from '@singletons/form'; import { ModalController } from '@singletons'; +import { CoreDomUtils } from '@services/utils/dom'; /** * Modal that asks the password. @@ -31,23 +32,63 @@ export class CorePasswordModalComponent { @ViewChild('passwordForm') formElement?: ElementRef; - @Input() title? = 'core.login.password'; // Translatable string to be shown on modal title. - @Input() placeholder? = 'core.login.password'; // Translatable string to be shown on password input as placeholder. - @Input() submit? = 'core.submit'; // Translatable string to be shown on submit button. - @Input() password? = ''; // Previous entered password. + @Input() title = 'core.login.password'; // Translatable string to be shown on modal title. + @Input() placeholder = 'core.login.password'; // Translatable string to be shown on password input as placeholder. + @Input() submit = 'core.submit'; // Translatable string to be shown on submit button. + @Input() validator?: (password?: string) => Promise; // Function to validate the password. + + password = ''; // Previous entered password. + error?: string; // Error message to be shown. /** * Send the password back. * * @param e Event. */ - submitPassword(e: Event): void { + async submitPassword(e: Event): Promise { e.preventDefault(); e.stopPropagation(); CoreForms.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId()); - ModalController.dismiss(this.password); + const response = await this.validatePassword(this.password); + + if (response.validated === undefined) { + ModalController.dismiss(response); + } + + if (response.validated) { + ModalController.dismiss(response); + } + + this.error = response.error; + } + + /** + * Validates the entered password if validator is available. + * + * @param password Entered password. + * @returns Response of the modal. + */ + protected async validatePassword(password: string): Promise { + const response: CorePasswordModalResponse = { password }; + + if (!this.validator) { + return response; + } + + const modal = await CoreDomUtils.showModalLoading('core.loading', true); + try { + return await this.validator(password); + } catch (error) { + response.validated = false; + response.error = error; + } finally { + modal.dismiss(); + } + + return response; + } /** @@ -61,4 +102,10 @@ export class CorePasswordModalComponent { } -export type CorePasswordModalParams = Pick; +export type CorePasswordModalParams = Partial>; + +export type CorePasswordModalResponse = { + password: string; + validated?: boolean; + error?: string; +}; diff --git a/src/core/features/course/lang.json b/src/core/features/course/lang.json index d1501f66e..a8bd74a19 100644 --- a/src/core/features/course/lang.json +++ b/src/core/features/course/lang.json @@ -35,6 +35,9 @@ "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 d4ac2dd2d..e639fea87 100644 --- a/src/core/features/course/pages/course-summary/course-summary.html +++ b/src/core/features/course/pages/course-summary/course-summary.html @@ -151,6 +151,13 @@ + + + + {{ 'core.course.guestaccess_withpassword' | 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 68dd0cb2a..deffdadcd 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 @@ -39,6 +39,8 @@ import { CoreColors } from '@singletons/colors'; import { CorePath } from '@singletons/path'; import { CorePromisedValue } from '@classes/promised-value'; import { CorePlatform } from '@services/platform'; +import { CoreCourse } from '@features/course/services/course'; +import { CorePasswordModalResponse } from '@components/password-modal/password-modal'; const ENROL_BROWSER_METHODS = ['fee', 'paypal']; @@ -64,15 +66,13 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { dataLoaded = false; isModal = false; contactsExpanded = false; - + useGuestAccess = false; + guestAccessPasswordRequired = false; courseUrl = ''; progress?: number; - - protected actionSheet?: HTMLIonActionSheetElement; - courseMenuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = []; - protected useGuestAccess = false; + protected actionSheet?: HTMLIonActionSheetElement; protected guestInstanceId = new CorePromisedValue(); protected courseData = new CorePromisedValue(); protected waitStart = 0; @@ -141,8 +141,10 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { const info = await CoreCourses.getCourseGuestEnrolmentInfo(guestInstanceId); - // Guest access with password is not supported by the app. - return !!info.status && !info.passwordrequired; + // Don't allow guest access if it requires a password if not supported. + this.guestAccessPasswordRequired = info.passwordrequired; + + return info.status === true && (!info.passwordrequired || CoreCourses.isValidateGuestAccessPasswordAvailable()); } /** @@ -287,11 +289,49 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { * * @param replaceCurrentPage If current place should be replaced in the navigation stack. */ - openCourse(replaceCurrentPage = false): void { + async openCourse(replaceCurrentPage = false): Promise { if (!this.canAccessCourse || !this.course || this.isModal) { 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 CoreUtils.promiseWorks( + CoreCourse.getSections(this.courseId, true, true, undefined, undefined, false), + ); + + if (!validated) { + try { + const validatePassword = async (password: string): Promise => { + 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, + }; + }; + + const response = await CoreDomUtils.promptPassword({ + title: 'core.course.guestaccess', + validator: validatePassword, + }); + + if (!response.validated) { + return; + } + } catch { + // Cancelled, return + return; + } + } + } + CoreCourseHelper.openCourse(this.course, { params: { isGuest: this.useGuestAccess }, replace: replaceCurrentPage }); } @@ -341,59 +381,75 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { * Self enrol in a course. * * @param instanceId The instance ID. - * @param password Password to use. * @returns Promise resolved when self enrolled. */ - async selfEnrolInCourse(instanceId: number, password = ''): Promise { - const modal = await CoreDomUtils.showModalLoading('core.loading', true); + 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); - try { - await CoreCourses.selfEnrol(this.courseId, password, instanceId); - - // Close modal and refresh data. - this.isEnrolled = true; - this.dataLoaded = false; - - // Sometimes the list of enrolled courses takes a while to be updated. Wait for it. - await this.waitForEnrolled(true); - - await this.refreshData().finally(() => { - // My courses have been updated, trigger event. - CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { - courseId: this.courseId, - course: this.course, - action: CoreCoursesProvider.ACTION_ENROL, - }, CoreSites.getCurrentSiteId()); - }); - - this.openCourse(true); - - modal?.dismiss(); - } catch (error) { - modal?.dismiss(); - - if (error && error.errorcode === CoreCoursesProvider.ENROL_INVALID_KEY) { - - try { - // Initialize the self enrol modal. - // Invalid password, show the modal to enter the password. - const modalData = await CoreDomUtils.promptPassword({ - password, - title: 'core.courses.selfenrolment', - placeholder: 'core.courses.password', - submit: 'core.courses.enrolme', - }); - - this.selfEnrolInCourse(instanceId, modalData); - } catch { - // No password entered, don't show error. + throw error; } - - return; } - CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorselfenrol', true); + 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; + } + } + + // Refresh data. + this.isEnrolled = true; + this.dataLoaded = false; + + // Sometimes the list of enrolled courses takes a while to be updated. Wait for it. + await this.waitForEnrolled(true); + + await this.refreshData().finally(() => { + // My courses have been updated, trigger event. + CoreEvents.trigger(CoreCoursesProvider.EVENT_MY_COURSES_UPDATED, { + courseId: this.courseId, + course: this.course, + action: CoreCoursesProvider.ACTION_ENROL, + }, CoreSites.getCurrentSiteId()); + }); + + this.openCourse(true); } /** diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 5032933cc..87ab3c580 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -592,7 +592,7 @@ export class CoreCourseHelperProvider { } /** - * Check whether a course is accessed using guest access and if requires password to enter. + * Check whether a course is accessed using guest access and if it requires password to enter. * * @param courseId Course ID. * @param siteId Site ID. If not defined, current site. @@ -634,7 +634,8 @@ export class CoreCourseHelperProvider { // Don't allow guest access if it requires a password and it's available. return { - guestAccess: !!info.status && !info.passwordrequired, + guestAccess: info.status === true && + (!info.passwordrequired || CoreCourses.isValidateGuestAccessPasswordAvailable()), passwordRequired: info.passwordrequired, }; } catch { diff --git a/src/core/features/courses/services/courses.ts b/src/core/features/courses/services/courses.ts index 4979562c9..f65faaf96 100644 --- a/src/core/features/courses/services/courses.ts +++ b/src/core/features/courses/services/courses.ts @@ -290,7 +290,7 @@ export class CoreCoursesProvider { } /** - * Get course. + * Get course information if user has persmissions to view. * * @param id ID of the course to get. * @param siteId Site to get the courses from. If not defined, use current site. @@ -324,7 +324,8 @@ export class CoreCoursesProvider { updateFrequency: CoreSite.FREQUENCY_RARELY, }; - return site.read('core_enrol_get_course_enrolment_methods', params, preSets); + return site.read + ('core_enrol_get_course_enrolment_methods', params, preSets); } /** @@ -368,6 +369,47 @@ export class CoreCoursesProvider { 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); + } + /** * Get courses. * Warning: if the user doesn't have permissions to view some of the courses passed the WS call will fail. @@ -1773,14 +1815,25 @@ type CoreEnrolGetCourseEnrolmentMethodsWSParams = { }; /** - * Course enrolment method. + * Data returned by core_enrol_get_course_enrolment_methods WS. */ -export type CoreCourseEnrolmentMethod = { +type CoreEnrolGetCourseEnrolmentMethodsWSResponse = CoreCourseEnrolmentMethod[]; + +/** + * Course enrolment basic info. + */ +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. - status: string; // Status of enrolment plugin. + status: boolean | string; // Available status of enrolment plugin. True if successful, else error message or false. +}; + +/** + * Course enrolment method. + */ +export type CoreCourseEnrolmentMethod = CoreCourseEnrolmentInfo & { wsfunction?: string; // Webservice function to get more information. }; @@ -1822,8 +1875,8 @@ export type CoreCourseGetRecentCoursesOptions = CoreSitesCommonWSOptions & { /** * Course guest enrolment method. */ -export type CoreCourseEnrolmentGuestMethod = CoreCourseEnrolmentMethod & { - passwordrequired: boolean; // Is a password required?. +export type CoreCourseEnrolmentGuestMethod = CoreCourseEnrolmentInfo & { + passwordrequired: boolean; // Is a password required? }; /** @@ -1857,3 +1910,20 @@ export type CoreCourseAnyCourseDataWithOptions = CoreCourseAnyCourseData & { navOptions?: CoreCourseUserAdminOrNavOptionIndexed; 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[]; +}; diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 9b93de35e..caa533f89 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -59,7 +59,7 @@ import { CoreErrorInfoComponent } from '@components/error-info/error-info'; import { CorePlatform } from '@services/platform'; import { CoreCancellablePromise } from '@classes/cancellable-promise'; import { CoreLang } from '@services/lang'; -import { CorePasswordModalParams } from '@components/password-modal/password-modal'; +import { CorePasswordModalParams, CorePasswordModalResponse } from '@components/password-modal/password-modal'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -1885,13 +1885,13 @@ export class CoreDomUtilsProvider { * Prompts password to the user and returns the entered text. * * @param passwordParams Params to show the modal. - * @returns Entered password. + * @returns Entered password, error and validation. */ async promptPassword(passwordParams?: CorePasswordModalParams): Promise { const { CorePasswordModalComponent } = await import('@/core/components/password-modal/password-modal.module'); - const modalData = await CoreDomUtils.openModal( + const modalData = await CoreDomUtils.openModal( { cssClass: 'core-password-modal', showBackdrop: true, @@ -1901,7 +1901,7 @@ export class CoreDomUtilsProvider { }, ); - if (typeof modalData !== 'string') { + if (modalData === undefined) { throw new CoreCanceledError(); } diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index 8d2c00e1c..cc0b1a292 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -732,8 +732,8 @@ body.core-iframe-fullscreen ion-router-outlet { .core-password-modal { --border-radius: var(--medium-radius); --min-width: auto; - --min-height: 260px; - --width: 320px; + --min-height: 300px; + --width: 384px; --height: auto; form {