From f4e6613f2c19bfc4d96ae2a58aad98ae7723f1b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Tue, 20 Jun 2023 15:38:09 +0200 Subject: [PATCH 1/7] MOBILE-4009 courses: Remove unused options of prefetch courses --- .../features/course/services/course-helper.ts | 71 +------------------ 1 file changed, 3 insertions(+), 68 deletions(-) diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index f9830defa..3931a5e5e 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -469,12 +469,6 @@ export class CoreCourseHelperProvider { let handlers: CoreCourseOptionsHandlerToDisplay[] = []; let menuHandlers: CoreCourseOptionsMenuHandlerToDisplay[] = []; let success = true; - let isGuest = false; - - if (options.canHaveGuestCourses) { - // Check if the user can only access as guest. - isGuest = await this.courseUsesGuestAccess(course.id, siteId); - } // Get the sections and the handlers. subPromises.push(CoreCourse.getSections(course.id, false, true).then((courseSections) => { @@ -483,12 +477,12 @@ export class CoreCourseHelperProvider { return; })); - subPromises.push(CoreCourseOptionsDelegate.getHandlersToDisplay(course, false, isGuest).then((cHandlers) => { + subPromises.push(CoreCourseOptionsDelegate.getHandlersToDisplay(course, false).then((cHandlers) => { handlers = cHandlers; return; })); - subPromises.push(CoreCourseOptionsDelegate.getMenuHandlersToDisplay(course, false, isGuest).then((mHandlers) => { + subPromises.push(CoreCourseOptionsDelegate.getMenuHandlersToDisplay(course, false).then((mHandlers) => { menuHandlers = mHandlers; return; @@ -597,55 +591,6 @@ export class CoreCourseHelperProvider { await CoreDomUtils.confirmDownloadSize(sizeSum, undefined, undefined, undefined, undefined, alwaysConfirm); } - /** - * Check whether a course is accessed using guest access. - * - * @param courseId Course ID. - * @param siteId Site ID. If not defined, current site. - * @returns Promise resolved with boolean: whether course is accessed using guest access. - */ - async courseUsesGuestAccess(courseId: number, siteId?: string): Promise { - try { - try { - // Check if user is enrolled. If enrolled, no guest access. - await CoreCourses.getUserCourse(courseId, false, siteId); - - return false; - } catch { - // Ignore errors. - } - - try { - // 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 false; - } catch { - // Ignore errors. - } - - // Check if guest access is enabled. - const enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(courseId, siteId); - - const method = enrolmentMethods.find((method) => method.type === 'guest'); - - if (!method) { - return false; - } - - const info = await CoreCourses.getCourseGuestEnrolmentInfo(method.id); - if (!info.status) { - // Not active, reject. - return false; - } - - // Don't allow guest access if it requires a password. - return !info.passwordrequired; - } catch { - return false; - } - } - /** * Create and return a section for "All sections". * @@ -1215,20 +1160,17 @@ export class CoreCourseHelperProvider { * * @param courses Courses array to prefetch. * @param prefetch Prefetch information to be updated. - * @param options Other options. * @returns Promise resolved when done. */ async prefetchCourses( courses: CoreCourseAnyCourseData[], prefetch: CorePrefetchStatusInfo, - options: CoreCoursePrefetchCoursesOptions = {}, ): Promise { prefetch.loading = true; prefetch.icon = CoreConstants.ICON_DOWNLOADING; prefetch.badge = ''; const prefetchOptions = { - ...options, onProgress: (progress) => { prefetch.badge = progress.count + ' / ' + progress.total; prefetch.badgeA11yText = Translate.instant('core.course.downloadcoursesprogressdescription', progress); @@ -2143,17 +2085,10 @@ export type CoreCoursePrefetchCourseOptions = { isGuest?: boolean; // Whether the user is guest. }; -/** - * Options for prefetch courses function. - */ -export type CoreCoursePrefetchCoursesOptions = { - canHaveGuestCourses?: boolean; // Whether the list of courses can contain courses with only guest access. -}; - /** * Options for confirm and prefetch courses function. */ -export type CoreCourseConfirmPrefetchCoursesOptions = CoreCoursePrefetchCoursesOptions & { +export type CoreCourseConfirmPrefetchCoursesOptions = { onProgress?: (data: CoreCourseCoursesProgress) => void; }; From e8e2b94b15d2c14d0b82c31b994dfb418580809f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 28 Jun 2023 11:40:26 +0200 Subject: [PATCH 2/7] MOBILE-4009 courses: Pass isGuest param on navigation --- .../pages/course-storage/course-storage.ts | 5 +- .../components/course-format/course-format.ts | 2 + .../course/pages/contents/contents.html | 2 +- .../course/pages/contents/contents.ts | 2 + src/core/features/course/pages/index/index.ts | 4 +- .../features/course/services/course-helper.ts | 51 +++++++++++++++++++ 6 files changed, 63 insertions(+), 3 deletions(-) diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.ts b/src/addons/storagemanager/pages/course-storage/course-storage.ts index 7cc02bd19..2b0a4ff0a 100644 --- a/src/addons/storagemanager/pages/course-storage/course-storage.ts +++ b/src/addons/storagemanager/pages/course-storage/course-storage.ts @@ -25,6 +25,7 @@ import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate'; import { CoreCourses } from '@features/courses/services/courses'; +import { CoreCoursesHelper } from '@features/courses/services/courses-helper'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; @@ -103,7 +104,9 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { this.title = Translate.instant('core.sitehome.sitehome'); } - this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest'); + this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest') ?? + (await CoreCourseHelper.courseUsesGuestAccessInfo(this.courseId)).guestAccess; + this.initialSectionId = CoreNavigator.getRouteNumberParam('sectionId'); this.downloadCourseEnabled = !CoreCourses.isDownloadCourseDisabledInSite(); 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 15d60a938..697cdd331 100644 --- a/src/core/features/course/components/course-format/course-format.ts +++ b/src/core/features/course/components/course-format/course-format.ts @@ -75,6 +75,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. // eslint-disable-next-line @typescript-eslint/no-explicit-any @ViewChildren(CoreDynamicComponent) dynamicComponents?: QueryList>; @@ -462,6 +463,7 @@ export class CoreCourseFormatComponent implements OnInit, OnChanges, OnDestroy { params: { title: this.course.fullname, sectionId: selectedId, + isGuest: this.isGuest, }, }, ); diff --git a/src/core/features/course/pages/contents/contents.html b/src/core/features/course/pages/contents/contents.html index ecafa525a..056ab073f 100644 --- a/src/core/features/course/pages/contents/contents.html +++ b/src/core/features/course/pages/contents/contents.html @@ -5,7 +5,7 @@ + [moduleId]="moduleId" class="core-course-format-{{course.format}}" *ngIf="dataLoaded && sections" [isGuest]="isGuest"> diff --git a/src/core/features/course/pages/contents/contents.ts b/src/core/features/course/pages/contents/contents.ts index 45a36e8c0..dac4a6212 100644 --- a/src/core/features/course/pages/contents/contents.ts +++ b/src/core/features/course/pages/contents/contents.ts @@ -64,6 +64,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon moduleId?: number; displayEnableDownload = false; displayRefresher = false; + isGuest?: boolean; protected formatOptions?: Record; protected completionObserver?: CoreEventObserver; @@ -92,6 +93,7 @@ export class CoreCourseContentsPage implements OnInit, OnDestroy, CoreRefreshCon this.sectionId = CoreNavigator.getRouteNumberParam('sectionId'); this.sectionNumber = CoreNavigator.getRouteNumberParam('sectionNumber'); this.moduleId = CoreNavigator.getRouteNumberParam('moduleId'); + this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest'); this.debouncedUpdateCachedCompletion = CoreUtils.debounce(() => { if (this.modulesHaveCompletion) { diff --git a/src/core/features/course/pages/index/index.ts b/src/core/features/course/pages/index/index.ts index 1f451a6e5..b472cdaff 100644 --- a/src/core/features/course/pages/index/index.ts +++ b/src/core/features/course/pages/index/index.ts @@ -143,7 +143,9 @@ export class CoreCourseIndexPage implements OnInit, OnDestroy { this.firstTabName = CoreNavigator.getRouteParam('selectedTab'); this.module = CoreNavigator.getRouteParam('module'); - this.isGuest = !!CoreNavigator.getRouteBooleanParam('isGuest'); + this.isGuest = CoreNavigator.getRouteBooleanParam('isGuest') ?? + (!!this.course && (await CoreCourseHelper.courseUsesGuestAccessInfo(this.course.id)).guestAccess); + this.modNavOptions = CoreNavigator.getRouteParam('modNavOptions'); this.openModule = CoreNavigator.getRouteBooleanParam('openModule') ?? true; // If false, just scroll to module. if (!this.modNavOptions) { diff --git a/src/core/features/course/services/course-helper.ts b/src/core/features/course/services/course-helper.ts index 3931a5e5e..5032933cc 100644 --- a/src/core/features/course/services/course-helper.ts +++ b/src/core/features/course/services/course-helper.ts @@ -591,6 +591,57 @@ export class CoreCourseHelperProvider { await CoreDomUtils.confirmDownloadSize(sizeSum, undefined, undefined, undefined, undefined, alwaysConfirm); } + /** + * Check whether a course is accessed using guest access and if requires password to enter. + * + * @param courseId Course ID. + * @param siteId Site ID. If not defined, current site. + * @returns Promise resolved with guestAccess and passwordRequired booleans. + */ + async courseUsesGuestAccessInfo( + courseId: number, + siteId?: string, + ): Promise<{guestAccess: boolean; passwordRequired?: boolean}> { + try { + try { + // Check if user is enrolled. If enrolled, no guest access. + await CoreCourses.getUserCourse(courseId, false, siteId); + + return { guestAccess: false }; + } catch { + // Ignore errors. + } + + try { + // 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 }; + } catch { + // Ignore errors. + } + + // Check if guest access is enabled. + const enrolmentMethods = await CoreCourses.getCourseEnrolmentMethods(courseId, siteId); + + const method = enrolmentMethods.find((method) => method.type === 'guest'); + + if (!method) { + return { guestAccess: false }; + } + + const info = await CoreCourses.getCourseGuestEnrolmentInfo(method.id); + + // Don't allow guest access if it requires a password and it's available. + return { + guestAccess: !!info.status && !info.passwordrequired, + passwordRequired: info.passwordrequired, + }; + } catch { + return { guestAccess: false }; + } + } + /** * Create and return a section for "All sections". * From c819f2eedd1a7aed642db46b196e8f8302c4163b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 28 Jun 2023 11:51:39 +0200 Subject: [PATCH 3/7] MOBILE-4009 storage: Fix downloading guest courses --- .../pages/course-storage/course-storage.ts | 30 ++++++++++++++----- 1 file changed, 23 insertions(+), 7 deletions(-) diff --git a/src/addons/storagemanager/pages/course-storage/course-storage.ts b/src/addons/storagemanager/pages/course-storage/course-storage.ts index 2b0a4ff0a..f0b1e18ea 100644 --- a/src/addons/storagemanager/pages/course-storage/course-storage.ts +++ b/src/addons/storagemanager/pages/course-storage/course-storage.ts @@ -24,8 +24,7 @@ import { import { CoreCourseModulePrefetchDelegate, CoreCourseModulePrefetchHandler } from '@features/course/services/module-prefetch-delegate'; -import { CoreCourses } from '@features/courses/services/courses'; -import { CoreCoursesHelper } from '@features/courses/services/courses-helper'; +import { CoreCourseAnyCourseData, CoreCourses } from '@features/courses/services/courses'; import { CoreNavigator } from '@services/navigator'; import { CoreSites } from '@services/sites'; import { CoreDomUtils } from '@services/utils/dom'; @@ -660,6 +659,25 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { this.changeDetectorRef.markForCheck(); } + protected async getCourse(courseId: number): Promise { + try { + // Check if user is enrolled. If enrolled, no guest access. + return await CoreCourses.getUserCourse(courseId, true); + } catch { + // Ignore errors. + } + + try { + // The user is not enrolled in the course. Use getCourses to see if it's an admin/manager and can see the course. + return await CoreCourses.getCourse(courseId); + } catch { + // Ignore errors. + } + + return await CoreCourses.getCourseByField('id', this.courseId); + + } + /** * Prefetch the whole course. * @@ -669,12 +687,10 @@ export class AddonStorageManagerCourseStoragePage implements OnInit, OnDestroy { event.stopPropagation(); event.preventDefault(); - const courses = await CoreCourses.getUserCourses(true); - let course = courses.find((course) => course.id == this.courseId); - if (!course) { - course = await CoreCourses.getCourse(this.courseId); - } + const course = await this.getCourse(this.courseId); if (!course) { + CoreDomUtils.showErrorModal('core.course.errordownloadingcourse', true); + return; } From 28049518106901fb84c4634b5f6229d5b62c4355 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 6 Jul 2023 10:14:57 +0200 Subject: [PATCH 4/7] MOBILE-4009 core: Create a generic password modal --- .../lesson/components/components.module.ts | 3 - .../mod/lesson/services/handlers/prefetch.ts | 14 ++--- .../password-modal/password-modal.html | 17 +++-- .../password-modal/password-modal.module.ts | 29 +++++++++ .../password-modal/password-modal.ts | 23 ++++--- .../course-summary/course-summary.page.ts | 29 ++++----- .../courses/components/components.module.ts | 3 - .../self-enrol-password.html | 27 -------- .../self-enrol-password.ts | 62 ------------------- src/core/services/utils/dom.ts | 28 +++++++++ src/theme/theme.base.scss | 17 ++++- 11 files changed, 113 insertions(+), 139 deletions(-) rename src/{addons/mod/lesson => core}/components/password-modal/password-modal.html (53%) create mode 100644 src/core/components/password-modal/password-modal.module.ts rename src/{addons/mod/lesson => core}/components/password-modal/password-modal.ts (61%) delete mode 100644 src/core/features/courses/components/self-enrol-password/self-enrol-password.html delete mode 100644 src/core/features/courses/components/self-enrol-password/self-enrol-password.ts diff --git a/src/addons/mod/lesson/components/components.module.ts b/src/addons/mod/lesson/components/components.module.ts index 1db1ad2f5..6daa4ec51 100644 --- a/src/addons/mod/lesson/components/components.module.ts +++ b/src/addons/mod/lesson/components/components.module.ts @@ -18,13 +18,11 @@ import { CoreSharedModule } from '@/core/shared.module'; import { CoreCourseComponentsModule } from '@features/course/components/components.module'; import { AddonModLessonIndexComponent } from './index/index'; import { AddonModLessonMenuModalPage } from './menu-modal/menu-modal'; -import { AddonModLessonPasswordModalComponent } from './password-modal/password-modal'; @NgModule({ declarations: [ AddonModLessonIndexComponent, AddonModLessonMenuModalPage, - AddonModLessonPasswordModalComponent, ], imports: [ CoreSharedModule, @@ -35,7 +33,6 @@ import { AddonModLessonPasswordModalComponent } from './password-modal/password- exports: [ AddonModLessonIndexComponent, AddonModLessonMenuModalPage, - AddonModLessonPasswordModalComponent, ], }) export class AddonModLessonComponentsModule {} diff --git a/src/addons/mod/lesson/services/handlers/prefetch.ts b/src/addons/mod/lesson/services/handlers/prefetch.ts index e5e354f96..af6005197 100644 --- a/src/addons/mod/lesson/services/handlers/prefetch.ts +++ b/src/addons/mod/lesson/services/handlers/prefetch.ts @@ -13,7 +13,6 @@ // limitations under the License. import { Injectable } from '@angular/core'; -import { CoreCanceledError } from '@classes/errors/cancelederror'; import { CoreError } from '@classes/errors/error'; import { CoreCourseActivityPrefetchHandlerBase } from '@features/course/classes/activity-prefetch-handler'; @@ -26,7 +25,6 @@ import { CoreDomUtils } from '@services/utils/dom'; import { CoreUtils } from '@services/utils/utils'; import { CoreWSFile } from '@services/ws'; import { makeSingleton, Translate } from '@singletons'; -import { AddonModLessonPasswordModalComponent } from '../../components/password-modal/password-modal'; import { AddonModLesson, AddonModLessonGetAccessInformationWSResponse, @@ -55,15 +53,11 @@ export class AddonModLessonPrefetchHandlerService extends CoreCourseActivityPref */ protected async askUserPassword(): Promise { // Create and show the modal. - const modalData = await CoreDomUtils.openModal({ - component: AddonModLessonPasswordModalComponent, + return CoreDomUtils.promptPassword({ + title: 'addon.mod_lesson.enterpassword', + placeholder: 'core.login.password', + submit: 'addon.mod_lesson.continue', }); - - if (typeof modalData != 'string') { - throw new CoreCanceledError(); - } - - return modalData; } /** diff --git a/src/addons/mod/lesson/components/password-modal/password-modal.html b/src/core/components/password-modal/password-modal.html similarity index 53% rename from src/addons/mod/lesson/components/password-modal/password-modal.html rename to src/core/components/password-modal/password-modal.html index 4a6ea9fe2..e54e70810 100644 --- a/src/addons/mod/lesson/components/password-modal/password-modal.html +++ b/src/core/components/password-modal/password-modal.html @@ -1,7 +1,7 @@ -

{{ 'core.login.password' | translate }}

+

{{ title | translate }}

@@ -10,19 +10,18 @@
- -
+ + - {{ 'addon.mod_lesson.enterpassword' | translate }} + {{ placeholder | translate }} - + - - {{ 'addon.mod_lesson.continue' | translate }} - + + {{ submit | translate }} diff --git a/src/core/components/password-modal/password-modal.module.ts b/src/core/components/password-modal/password-modal.module.ts new file mode 100644 index 000000000..77018a384 --- /dev/null +++ b/src/core/components/password-modal/password-modal.module.ts @@ -0,0 +1,29 @@ +// (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 { CorePasswordModalComponent } from './password-modal'; +import { CoreSharedModule } from '@/core/shared.module'; + +export { CorePasswordModalComponent }; + +@NgModule({ + declarations: [ + CorePasswordModalComponent, + ], + imports: [ + CoreSharedModule, + ], +}) +export class CorePasswordModalModule {} diff --git a/src/addons/mod/lesson/components/password-modal/password-modal.ts b/src/core/components/password-modal/password-modal.ts similarity index 61% rename from src/addons/mod/lesson/components/password-modal/password-modal.ts rename to src/core/components/password-modal/password-modal.ts index a163b454e..af2e53b4f 100644 --- a/src/addons/mod/lesson/components/password-modal/password-modal.ts +++ b/src/core/components/password-modal/password-modal.ts @@ -12,37 +12,42 @@ // See the License for the specific language governing permissions and // limitations under the License. -import { Component, ViewChild, ElementRef } from '@angular/core'; -import { IonInput } from '@ionic/angular'; +import { Component, ViewChild, ElementRef, Input } from '@angular/core'; import { CoreSites } from '@services/sites'; import { CoreForms } from '@singletons/form'; import { ModalController } from '@singletons'; /** - * Modal that asks the password for a lesson. + * Modal that asks the password. + * + * WARNING: This component is not loaded with components.module.ts. */ @Component({ - selector: 'page-addon-mod-lesson-password-modal', + selector: 'core-password-modal', templateUrl: 'password-modal.html', }) -export class AddonModLessonPasswordModalComponent { +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. + /** * Send the password back. * * @param e Event. - * @param password The input element. */ - submitPassword(e: Event, password: IonInput): void { + submitPassword(e: Event): void { e.preventDefault(); e.stopPropagation(); CoreForms.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId()); - ModalController.dismiss(password.value); + ModalController.dismiss(this.password); } /** @@ -55,3 +60,5 @@ export class AddonModLessonPasswordModalComponent { } } + +export type CorePasswordModalParams = Pick; 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 502174240..68dd0cb2a 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 @@ -31,7 +31,6 @@ import { } from '@features/course/services/course-options-delegate'; import { CoreCourseHelper } from '@features/course/services/course-helper'; import { ActionSheetController, ModalController, NgZone, Translate } from '@singletons'; -import { CoreCoursesSelfEnrolPasswordComponent } from '../../../courses/components/self-enrol-password/self-enrol-password'; import { CoreNavigator } from '@services/navigator'; import { CoreUtils } from '@services/utils/utils'; import { CoreCoursesHelper, CoreCourseWithImageAndColor } from '@features/courses/services/courses-helper'; @@ -374,25 +373,23 @@ export class CoreCourseSummaryPage implements OnInit, OnDestroy { modal?.dismiss(); if (error && error.errorcode === CoreCoursesProvider.ENROL_INVALID_KEY) { - // Initialize the self enrol modal. - // Invalid password, show the modal to enter the password. - const modalData = await CoreDomUtils.openModal( - { - component: CoreCoursesSelfEnrolPasswordComponent, - componentProps: { password }, - }, - ); - if (modalData !== undefined) { + 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); - - return; - } - - if (!password) { + } catch { // No password entered, don't show error. - return; } + + return; } CoreDomUtils.showErrorModalDefault(error, 'core.courses.errorselfenrol', true); diff --git a/src/core/features/courses/components/components.module.ts b/src/core/features/courses/components/components.module.ts index 24068556c..1b596c8fc 100644 --- a/src/core/features/courses/components/components.module.ts +++ b/src/core/features/courses/components/components.module.ts @@ -18,14 +18,12 @@ import { CoreSharedModule } from '@/core/shared.module'; import { CoreCoursesCourseListItemComponent } from './course-list-item/course-list-item'; import { CoreCoursesCourseProgressComponent } from './course-progress/course-progress'; import { CoreCoursesCourseOptionsMenuComponent } from './course-options-menu/course-options-menu'; -import { CoreCoursesSelfEnrolPasswordComponent } from './self-enrol-password/self-enrol-password'; @NgModule({ declarations: [ CoreCoursesCourseListItemComponent, CoreCoursesCourseProgressComponent, CoreCoursesCourseOptionsMenuComponent, - CoreCoursesSelfEnrolPasswordComponent, ], imports: [ CoreSharedModule, @@ -34,7 +32,6 @@ import { CoreCoursesSelfEnrolPasswordComponent } from './self-enrol-password/sel CoreCoursesCourseListItemComponent, CoreCoursesCourseProgressComponent, CoreCoursesCourseOptionsMenuComponent, - CoreCoursesSelfEnrolPasswordComponent, ], }) export class CoreCoursesComponentsModule {} diff --git a/src/core/features/courses/components/self-enrol-password/self-enrol-password.html b/src/core/features/courses/components/self-enrol-password/self-enrol-password.html deleted file mode 100644 index 4a7455323..000000000 --- a/src/core/features/courses/components/self-enrol-password/self-enrol-password.html +++ /dev/null @@ -1,27 +0,0 @@ - - - -

{{ 'core.courses.selfenrolment' | translate }}

-
- - - - - -
-
- - - - {{ 'core.courses.password' | translate }} - - - - - -
- {{ 'core.courses.enrolme' | translate }} -
- -
diff --git a/src/core/features/courses/components/self-enrol-password/self-enrol-password.ts b/src/core/features/courses/components/self-enrol-password/self-enrol-password.ts deleted file mode 100644 index 12d9db4a6..000000000 --- a/src/core/features/courses/components/self-enrol-password/self-enrol-password.ts +++ /dev/null @@ -1,62 +0,0 @@ -// (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 { Component, ViewChild, ElementRef } from '@angular/core'; -import { NavParams } from '@ionic/angular'; -import { CoreSites } from '@services/sites'; -import { ModalController } from '@singletons'; -import { CoreForms } from '@singletons/form'; - -/** - * Modal that displays a form to enter a password to self enrol in a course. - */ -@Component({ - selector: 'page-core-courses-self-enrol-password', - templateUrl: 'self-enrol-password.html', -}) -export class CoreCoursesSelfEnrolPasswordComponent { - - @ViewChild('enrolPasswordForm') formElement!: ElementRef; - password = ''; - - constructor( - navParams: NavParams, - ) { - this.password = navParams.get('password') || ''; - } - - /** - * Close help modal. - */ - close(): void { - CoreForms.triggerFormCancelledEvent(this.formElement, CoreSites.getCurrentSiteId()); - - ModalController.dismiss(); - } - - /** - * Submit password. - * - * @param e Event. - */ - submitPassword(e: Event): void { - e.preventDefault(); - e.stopPropagation(); - - CoreForms.triggerFormSubmittedEvent(this.formElement, false, CoreSites.getCurrentSiteId()); - - ModalController.dismiss(this.password); - } - -} diff --git a/src/core/services/utils/dom.ts b/src/core/services/utils/dom.ts index 83eb3c62c..9b93de35e 100644 --- a/src/core/services/utils/dom.ts +++ b/src/core/services/utils/dom.ts @@ -59,6 +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'; /* * "Utils" service with helper functions for UI, DOM elements and HTML code. @@ -1880,6 +1881,33 @@ export class CoreDomUtilsProvider { } } + /** + * Prompts password to the user and returns the entered text. + * + * @param passwordParams Params to show the modal. + * @returns Entered password. + */ + async promptPassword(passwordParams?: CorePasswordModalParams): Promise { + const { CorePasswordModalComponent } = + await import('@/core/components/password-modal/password-modal.module'); + + const modalData = await CoreDomUtils.openModal( + { + cssClass: 'core-password-modal', + showBackdrop: true, + backdropDismiss: true, + component: CorePasswordModalComponent, + componentProps: passwordParams, + }, + ); + + if (typeof modalData !== 'string') { + throw new CoreCanceledError(); + } + + return modalData; + } + /** * View an image in a modal. * diff --git a/src/theme/theme.base.scss b/src/theme/theme.base.scss index fb1f62a78..8d2c00e1c 100644 --- a/src/theme/theme.base.scss +++ b/src/theme/theme.base.scss @@ -729,6 +729,21 @@ body.core-iframe-fullscreen ion-router-outlet { } } +.core-password-modal { + --border-radius: var(--medium-radius); + --min-width: auto; + --min-height: 260px; + --width: 320px; + --height: auto; + + form { + display: flex; + flex-direction: column; + height: 100%; + justify-content: space-between; + } +} + // Hidden submit button. .core-submit-hidden-enter { position: absolute; @@ -871,7 +886,7 @@ img.large-avatar, max-width: var(--core-large-avatar-size); max-height: var(--core-large-avatar-size); margin-bottom: 10px; - border-radius : 50%; + border-radius: 50%; padding: 4px; border: 1px solid var(--stroke); background-color: transparent; From 73addbf42ebe5a2658c28e61b3394a192b892221 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 28 Jun 2023 11:40:52 +0200 Subject: [PATCH 5/7] MOBILE-4009 course: Validate guest access password and enter the course --- scripts/langindex.json | 3 + .../mod/lesson/services/handlers/prefetch.ts | 22 +-- .../password-modal/password-modal.html | 23 ++- .../password-modal/password-modal.ts | 61 ++++++- src/core/features/course/lang.json | 3 + .../pages/course-summary/course-summary.html | 7 + .../course-summary/course-summary.page.ts | 164 ++++++++++++------ .../features/course/services/course-helper.ts | 5 +- src/core/features/courses/services/courses.ts | 84 ++++++++- src/core/services/utils/dom.ts | 8 +- src/theme/theme.base.scss | 4 +- 11 files changed, 284 insertions(+), 100 deletions(-) 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 { From 4e2cb51c538c73a29c7ef90038ccf3cc3bea31b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Wed, 28 Jun 2023 15:40:44 +0200 Subject: [PATCH 6/7] MOBILE-4009 course: Add behats to test guest access --- .../course/tests/behat/basic_usage.feature | 35 -------- .../course/tests/behat/guest_access.feature | 84 +++++++++++++++++++ 2 files changed, 84 insertions(+), 35 deletions(-) create mode 100644 src/core/features/course/tests/behat/guest_access.feature diff --git a/src/core/features/course/tests/behat/basic_usage.feature b/src/core/features/course/tests/behat/basic_usage.feature index 979edbbe9..f6d12bcb9 100755 --- a/src/core/features/course/tests/behat/basic_usage.feature +++ b/src/core/features/course/tests/behat/basic_usage.feature @@ -442,41 +442,6 @@ Feature: Test basic usage of one course in app And I should find "Test scorm name" in the app And I should find "Test workshop name" in the app - @lms_from4.0 - Scenario: Guest access - Given I entered the course "Course 1" as "teacher1" in the app - And I press "Course summary" in the app - And I press "Open in browser" in the app - And I switch to the browser tab opened by the app - And I log in as "teacher1" - And I click on "Participants" "link" - And I select "Enrolment methods" from the "jump" singleselect - And I click on "Enable" "icon" in the "Guest access" "table_row" - And I close the browser tab opened by the app - Given I entered the app as "student2" - When I press "Site home" in the app - And I press "Available courses" in the app - And I press "Course 1" in the app - - Then I should find "Course summary" in the app - And I should find "Course" in the app - - When I press "View course" "ion-button" in the app - Then the header should be "Course 1" in the app - And I should find "Choice course 1" in the app - And I should find "assignment" in the app - And I should find "Test forum name" in the app - And I should find "Test chat name" in the app - And I should find "Web links" in the app - And I should find "Test feedback name" in the app - And I should find "Test glossary" in the app - And I should find "Quiz 1" in the app - And I should find "Test survey name" in the app - And I should find "Test wiki name" in the app - And I should find "Test lesson name" in the app - And I should find "Test scorm name" in the app - And I should find "Test workshop name" in the app - Scenario: View blocks on drawer Given the following "blocks" exist: | blockname | contextlevel | reference | pagetypepattern | defaultregion | configdata | diff --git a/src/core/features/course/tests/behat/guest_access.feature b/src/core/features/course/tests/behat/guest_access.feature new file mode 100644 index 000000000..c472f358d --- /dev/null +++ b/src/core/features/course/tests/behat/guest_access.feature @@ -0,0 +1,84 @@ +@core @core_course @app @javascript @enrol @enrol_guest +Feature: Test basic usage of guest access course in app + + Background: + Given the following "users" exist: + | username | firstname | lastname | email | + | teacher1 | Teacher | teacher | teacher1@example.com | + | student1 | Student | student | student1@example.com | + And the following "courses" exist: + | fullname | shortname | category | + | Course 1 | C1 | 0 | + And the following "course enrolments" exist: + | user | course | role | + | teacher1 | C1 | editingteacher | + And the following "activities" exist: + | activity | course | idnumber | name | intro | assignsubmission_onlinetext_enabled | section | + | assign | C1 | assign1 | assignment | Test assignment description | 1 | 1 | + And the following "activities" exist: + | activity | name | intro | course | idnumber | groupmode | + | wiki | Test wiki name | Test wiki | C1 | wiki | 0 | + + + @lms_from4.0 + Scenario: Guest access without password (student) + Given I log in as "teacher1" + And I am on the "Course 1" "enrolment methods" page + And I click on "Edit" "link" in the "Guest access" "table_row" + And I set the following fields to these values: + | Allow guest access | Yes | + And I press "Save changes" + And I entered the app as "student1" + + When I press "Site home" in the app + And I press "Available courses" in the app + And I press "Course 1" in the app + Then I should find "Course summary" in the app + And I should find "Course" in the app + + When I press "View course" "ion-button" in the app + Then the header should be "Course 1" in the app + And I should find "assignment" in the app + And I should find "Test wiki name" in the app + + When I press "assignment" in the app + Then I should not find "Add submission" in the app + + @lms_from4.3 + Scenario: Guest access with password (student) + Given I log in as "teacher1" + And I am on the "Course 1" "enrolment methods" page + And I click on "Edit" "link" in the "Guest access" "table_row" + And I set the following fields to these values: + | Allow guest access | Yes | + | Password | moodle_rules | + And I press "Save changes" + And I entered the app as "student1" + + When I press "Site home" in the app + And I press "Available courses" in the app + And I press "Course 1" in the app + Then I should find "Course summary" in the app + And I should find "Course" in the app + And I should find "Guest access requires password" in the app + + When I press "View course" "ion-button" in the app + And I set the following fields to these values in the app: + | Password | wrong | + And I press "Submit" "ion-button" in the app + Then I should find "Incorrect access password, please try again" in the app + + # Show the hint. + Given the following config values are set as admin: + | showhint | 1 | enrol_guest | + When I press "Submit" "ion-button" in the app + Then I should find "That access password was incorrect, please try again" in the app + When I set the following fields to these values in the app: + | Password | moodle_rules | + And I press "Submit" "ion-button" in the app + Then the header should be "Course 1" in the app + And I should find "assignment" in the app + And I should find "Test wiki name" in the app + + When I press "assignment" in the app + Then I should not find "Add submission" in the app From 7d19bb9ca78ea248ab13db975c0cbc59ba732191 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pau=20Ferrer=20Oca=C3=B1a?= Date: Thu, 6 Jul 2023 10:34:05 +0200 Subject: [PATCH 7/7] MOBILE-4362 scripts: Change langpack server and follow redirects --- scripts/create_langindex_functions.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/create_langindex_functions.sh b/scripts/create_langindex_functions.sh index 80d19bae4..bc6eb3c52 100644 --- a/scripts/create_langindex_functions.sh +++ b/scripts/create_langindex_functions.sh @@ -3,7 +3,7 @@ # Functions used to create langidex. # -SERVER_URL='https://download.moodle.org/' +SERVER_URL='https://packaging.moodle.org/' # Downloads a file and if it's a zip file, unzip it. function download_file { @@ -12,7 +12,7 @@ function download_file { pushd "$LANGPACKS_PATH" > /dev/null - curl -s "$url" --output "$filename" > /dev/null + curl -L -s "$url" --output "$filename" > /dev/null size=$(du -k "$filename" | cut -f 1) if [ ! -n "$filename" ] || [ "$size" -le 1 ]; then echo "Wrong or corrupt file $filename" @@ -45,7 +45,7 @@ function get_english { get_app_version echo "Getting English language..." - download_file "$SERVER_URL/download.php/direct/langpack/$LANGVERSION/en.zip" + download_file "$SERVER_URL/langpack/$LANGVERSION/en.zip" } #Saves or updates a key on langindex_old.json