From 3fd7d541f0cb4d6784daae4108d351c517f7b049 Mon Sep 17 00:00:00 2001 From: Daniel Mejia Date: Tue, 25 Aug 2026 21:04:25 -0400 Subject: [PATCH] com_courses: let admins view course forms instead of 422ing them out Form::execute() resolved a #__courses_members row at section then offering scope and aborted 422 "No user found" when neither matched. Site admins and course managers who are not enrolled have no such row, so any form task -- including a read-only PDF layout view reached from the outline builder -- turned them away. authorize() already exempts Super Administrators, and the manager check it performs is access('manage'), not membership. But authorize() runs inside the task and execute() gets there first, so that exemption was never reachable. execute() now applies the same test: a user with no member row is let through when they are a super admin or can manage the course, with $this->member left null and $this->unenrolled set. $this->member is only consumed by showDeployment, complete, startWork, saveProgress and submit, so those five now assert it themselves. An admin can lay out and read a form but still cannot deploy one or submit responses, which keeps respondent records tied to real members. The authoring tasks (layout, index, upload, saveLayout) never read it. The layout view shows a warning when unenrolled. It has to render in the view rather than through Notify: this route is requested with tmpl=component, and neither the form views nor the component template render the notification queue, so a Notify call would silently surface on some later page. Ticket: https://nanohub.org/support/ticket/510363 Co-Authored-By: Claude Opus 5 --- .../com_courses/site/controllers/form.php | 61 +++++++++++++++++-- .../site/language/en-GB/en-GB.com_courses.ini | 1 + .../site/views/form/tmpl/layout.php | 5 ++ 3 files changed, 61 insertions(+), 6 deletions(-) diff --git a/core/components/com_courses/site/controllers/form.php b/core/components/com_courses/site/controllers/form.php index f17116ceea1..6ea62b5052c 100644 --- a/core/components/com_courses/site/controllers/form.php +++ b/core/components/com_courses/site/controllers/form.php @@ -30,6 +30,13 @@ */ class Form extends SiteController { + /** + * Viewing without being enrolled (admins and course managers) + * + * @var boolean + */ + protected $unenrolled = false; + /** * Execute a task * @@ -44,10 +51,21 @@ public function execute() if (!$this->member || !is_numeric($this->member)) { $this->member = $this->course->offering()->member(User::get('id'))->get('id'); - if (!$this->member || !is_numeric($this->member)) + } + + if (!$this->member || !is_numeric($this->member)) + { + // authorize() already exempts super admins and course managers, but + // it runs inside the task and execute() gets here first. Let them + // through without a member id rather than turning them away, and + // flag it so the view can say so. + if (User::get('usertype') != 'Super Administrator' && !$this->course->access('manage')) { App::abort(422, Lang::txt('No user found')); } + + $this->member = null; + $this->unenrolled = true; } // Set the base path @@ -198,11 +216,12 @@ public function layoutTask() $this->_buildTitle(); $this->_buildPathway(); - $this->view->pdf = new PdfForm($this->assertFormId()); - $this->view->title = $this->view->pdf->getTitle(); - $this->view->readonly = Request::getInt('readonly', false); - $this->view->base = $this->base; - $this->view->course = $this->course; + $this->view->pdf = new PdfForm($this->assertFormId()); + $this->view->title = $this->view->pdf->getTitle(); + $this->view->readonly = Request::getInt('readonly', false); + $this->view->unenrolled = $this->unenrolled; + $this->view->base = $this->base; + $this->view->course = $this->course; $this->view->display(); } @@ -353,6 +372,12 @@ public function updateDeploymentTask() */ public function showDeploymentTask($dep=null) { + // These need a real member record; viewing as an admin is not enough + if (!$this->member) + { + App::abort(422, Lang::txt('No user found')); + } + if (!$id = Request::getInt('id', false)) { App::abort(422, Lang::txt('COM_COURSES_ERROR_MISSING_IDENTIFIER')); @@ -379,6 +404,12 @@ public function showDeploymentTask($dep=null) */ public function completeTask() { + // These need a real member record; viewing as an admin is not enough + if (!$this->member) + { + App::abort(422, Lang::txt('No user found')); + } + if (!$crumb = Request::getString('crumb', false)) { App::abort(422); @@ -475,6 +506,12 @@ public function completeTask() */ public function startWorkTask() { + // These need a real member record; viewing as an admin is not enough + if (!$this->member) + { + App::abort(422, Lang::txt('No user found')); + } + if (!$crumb = Request::getString('crumb', false)) { App::abort(422); @@ -500,6 +537,12 @@ public function startWorkTask() */ public function saveProgressTask() { + // These need a real member record; viewing as an admin is not enough + if (!$this->member) + { + App::abort(422, Lang::txt('No user found')); + } + if (!isset($_POST['crumb']) || !isset($_POST['question']) || !isset($_POST['answer'])) { echo Lang::txt('COM_COURSES_ERROR_MISSING_CRUMB_QUESTION_OR_ANSWER'); @@ -526,6 +569,12 @@ public function saveProgressTask() */ public function submitTask() { + // These need a real member record; viewing as an admin is not enough + if (!$this->member) + { + App::abort(422, Lang::txt('No user found')); + } + if (!$crumb = Request::getString('crumb', false)) { App::abort(422); diff --git a/core/components/com_courses/site/language/en-GB/en-GB.com_courses.ini b/core/components/com_courses/site/language/en-GB/en-GB.com_courses.ini index 2b75fb889bb..862b62fc0cd 100644 --- a/core/components/com_courses/site/language/en-GB/en-GB.com_courses.ini +++ b/core/components/com_courses/site/language/en-GB/en-GB.com_courses.ini @@ -172,6 +172,7 @@ COM_COURSES_HEADER_END_DATE="End date" COM_COURSES_SELECT_PREVIOUS_PDF="Select a previous PDF" COM_COURSES_DEPLOY="Deploy" COM_COURSES_ERROR_MISSING_DEPLOYMENT="No deployment provided" +COM_COURSES_FORM_NOT_ENROLLED="You are viewing this form as an administrator. You are not enrolled in this course, so you cannot deploy it or respond to it." COM_COURSES_ERROR_MISSING_DEPLOYMENT_ID="No deployment ID provided" COM_COURSES_ERROR_UNKNOWN_IDENTIFIER="No form matches identifier" COM_COURSES_ERROR_MISSING_IDENTIFIER="No form identifier supplied" diff --git a/core/components/com_courses/site/views/form/tmpl/layout.php b/core/components/com_courses/site/views/form/tmpl/layout.php index f40c8465682..ef41a51df57 100644 --- a/core/components/com_courses/site/views/form/tmpl/layout.php +++ b/core/components/com_courses/site/views/form/tmpl/layout.php @@ -17,6 +17,11 @@ ?>
+ unenrolled)) : ?> +
+ +
+