@@ -61,16 +61,19 @@ sub initializeRoute ($c, $routeCaptures) {
6161 {
6262 $c -> stash-> {isContentSelection } = 1;
6363
64+ my $siteEnvironment = WeBWorK::CourseEnvironment-> new;
65+
6466 # The database object used here is not associated to any course,
6567 # and so the only has access to non-native tables.
66- my @matchingCourses = WeBWorK::DB-> new(WeBWorK::CourseEnvironment-> new)-> getLTICourseMapsWhere({
68+ my $nonNativeDB = WeBWorK::DB-> new($siteEnvironment );
69+ my @matchingCourses = $nonNativeDB -> getLTICourseMapsWhere({
6770 lms_context_id =>
6871 $c -> stash-> {lti_jwt_claims }{' https://purl.imsglobal.org/spec/lti/claim/context' }{id }
6972 });
7073
7174 if (@matchingCourses == 1) {
7275 $c -> stash-> {courseID } = $matchingCourses [0]-> course_id;
73- } else {
76+ } elsif ( $siteEnvironment -> { LTI }{ v1p3 }{ allowCourseSelection }) {
7477 for (@matchingCourses ) {
7578 my $ce = eval { WeBWorK::CourseEnvironment-> new({ courseName => $_ -> course_id }) };
7679 if ($@ ) { warn " Failed to initialize course environment for $_ : $@ \n " ; next ; }
@@ -86,6 +89,98 @@ sub initializeRoute ($c, $routeCaptures) {
8689 last ;
8790 }
8891 }
92+
93+ # If a matching course was not found in the LTI course map and the site is configured to allow
94+ # course selection, then construct a list of all courses for which the LTI 1.3 authentication
95+ # parameters match and that have a user that has the email address set that matches the email
96+ # address sent from the LMS, and that has access_instructor_tools and modify_problem_sets
97+ # permissions.
98+ unless (defined $c -> stash-> {courseID }) {
99+ my @userCourses ;
100+
101+ my $claims = $c -> stash-> {lti_jwt_claims };
102+ my $extract_claim = sub ($key ) {
103+ my $value = $claims ;
104+ for (split ' #' , $key ) {
105+ if (defined $value -> {$_ }) {
106+ $value = $value -> {$_ };
107+ } else {
108+ return ;
109+ }
110+ }
111+ return $value ;
112+ };
113+
114+ my %mappedCourses = map { $_ -> course_id => 1 } $nonNativeDB -> getLTICourseMapsWhere;
115+
116+ my $firstFoundUserId ;
117+
118+ for (listCourses(WeBWorK::CourseEnvironment-> new)) {
119+ next if $mappedCourses {$_ };
120+
121+ my $ce = eval { WeBWorK::CourseEnvironment-> new({ courseName => $_ }) };
122+ if ($@ ) { $c -> log -> error(" Failed to initialize course environment for $_ : $@ " ); next ; }
123+
124+ if (($ce -> {LTIVersion } // ' ' ) eq ' v1p3'
125+ && $ce -> {LTI }{v1p3 }{PlatformID }
126+ && $ce -> {LTI }{v1p3 }{PlatformID } eq $c -> stash-> {LTILaunchData }-> data-> {PlatformID }
127+ && $ce -> {LTI }{v1p3 }{ClientID }
128+ && $ce -> {LTI }{v1p3 }{ClientID } eq $c -> stash-> {LTILaunchData }-> data-> {ClientID }
129+ && $ce -> {LTI }{v1p3 }{DeploymentID }
130+ && $ce -> {LTI }{v1p3 }{DeploymentID } eq
131+ $c -> stash-> {LTILaunchData }-> data-> {DeploymentID }
132+ && $ce -> {LTI }{v1p3 }{preferred_source_of_username })
133+ {
134+ my $userIdSource = ' ' ;
135+ my $userId = $extract_claim -> ($ce -> {LTI }{v1p3 }{preferred_source_of_username });
136+ $userIdSource = $ce -> {LTI }{v1p3 }{preferred_source_of_username } if $userId ;
137+ if (!defined $userId && $ce -> {LTI }{v1p3 }{fallback_source_of_username }) {
138+ $userId = $extract_claim -> ($ce -> {LTI }{v1p3 }{fallback_source_of_username });
139+ $userIdSource = $ce -> {LTI }{v1p3 }{fallback_source_of_username } if $userId ;
140+ }
141+ next unless defined $userId ;
142+ $userId =~ s / @.*$//
143+ if $userIdSource eq ' email' && $ce -> {LTI }{v1p3 }{strip_domain_from_email };
144+ $userId = lc ($userId ) if $ce -> {LTI }{v1p3 }{lowercase_username };
145+
146+ # Assert that the user id for the user is the same in all courses offered for selection.
147+ # Otherwise things will fall apart when the content_selection method attempts to sign
148+ # the user out of the intial guess course and into a different selected course.
149+ $firstFoundUserId = $userId unless defined $firstFoundUserId ;
150+ next unless $userId eq $firstFoundUserId ;
151+
152+ my $db = WeBWorK::DB-> new($ce );
153+ my $user = $db -> getUser($userId );
154+
155+ # Only allow courses for which the user has the email address set, and the email address
156+ # matches the email address sent from the LMS. This means that if one course has the LTI
157+ # user selection parameters set differently than another, then not all courses for the
158+ # user will actually be listed. This should be considered a configuration error, and the
159+ # system administrator should not set courses up this way.
160+ next unless $user && $user -> email_address && $claims -> {email } eq $user -> email_address;
161+
162+ # Only allow courses for which the user has the access_instructor_tools and
163+ # modify_problem_sets permissions. The WeBWorK::Authz object for this request has not
164+ # yet been constructed, so the permissions check has to be performed manually.
165+ my $permission = $db -> getPermissionLevel($userId );
166+ next
167+ unless $permission
168+ && $permission -> permission >=
169+ $ce -> {userRoles }{ $ce -> {permissionLevels }{access_instructor_tools } }
170+ && $permission -> permission >=
171+ $ce -> {userRoles }{ $ce -> {permissionLevels }{modify_problem_sets } };
172+
173+ push (@userCourses , $_ );
174+ }
175+ }
176+ if (@userCourses ) {
177+ # Use the first matching course for initial authentication. All matching courses have the
178+ # same LTI 1.3 authentication parameters, and so presumably authentication will work an any
179+ # of them.
180+ $c -> stash-> {courseID } = $userCourses [0];
181+ $c -> stash-> {courseChoices } = \@userCourses ;
182+ }
183+ }
89184 }
90185 } else {
91186 $c -> stash-> {courseID } = $c -> stash-> {LTILaunchData }-> data-> {courseID }
@@ -183,6 +278,13 @@ sub launch ($c) {
183278 ? (
184279 courseID => $c -> stash-> {courseID },
185280 initial_request => 1,
281+ $c -> stash-> {courseChoices }
282+ ? (
283+ course_choices => $c -> stash-> {courseChoices },
284+ lms_context_id =>
285+ $c -> stash-> {lti_jwt_claims }{' https://purl.imsglobal.org/spec/lti/claim/context' }{id }
286+ )
287+ : (),
186288 accept_multiple =>
187289 $c -> stash-> {lti_jwt_claims }{' https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings' }
188290 {accept_multiple },
@@ -210,6 +312,61 @@ sub content_selection ($c) {
210312 unless $c -> authz-> hasPermissions($c -> authen-> {user_id }, ' modify_problem_sets' );
211313
212314 if ($c -> param(' initial_request' )) {
315+ my @courseChoices = $c -> param(' course_choices' );
316+ if (@courseChoices > 1) {
317+ return $c -> render(
318+ ' ContentGenerator/LTI/content_item_course_selection' ,
319+ courseChoices => \@courseChoices ,
320+ forwardParams => {
321+ accept_multiple => $c -> param(' accept_multiple' ),
322+ deep_link_return_url => $c -> param(' deep_link_return_url' ),
323+ lms_context_id => $c -> param(' lms_context_id' ),
324+ $c -> param(' data' ) ? (data => $c -> param(' data' )) : (),
325+ }
326+ );
327+ } elsif (@courseChoices ) {
328+ # If only one course matched for this user, then just use it.
329+ # Add it to the course map and skip course selection.
330+ $c -> db-> setLTICourseMap($courseChoices [0], $c -> param(' lms_context_id' ));
331+ }
332+
333+ my $selectedCourse = $c -> param(' selected_course' );
334+ if ($selectedCourse && $selectedCourse ne $c -> ce-> {courseName }) {
335+ # The user has selected a course that is not the initial guess (the first course found that the user fit
336+ # into), and the user was authenticated into that inital guess course. So sign the user out of that course,
337+ # and sign the user into the selected course. This does not go through the entire authentication process,
338+ # but presumably that would succeed since the authhentication parameters for the two courses match and the
339+ # permissions of the user were checked in both courses already.
340+ my $key = $c -> db-> getKey($c -> authen-> {user_id });
341+ $c -> db-> deleteKey($c -> authen-> {user_id });
342+ $c -> signed_cookie(
343+ ' WeBWorKCourseSession.' . $c -> ce-> {courseName },
344+ ' ' ,
345+ {
346+ domain => $c -> app-> sessions-> cookie_domain,
347+ expires => time ,
348+ httponly => 1,
349+ path => $c -> app-> sessions-> cookie_path,
350+ samesite => $c -> app-> sessions-> samesite,
351+ secure => $c -> app-> sessions-> secure
352+ }
353+ );
354+
355+ $c -> stash-> {courseID } = $selectedCourse ;
356+ my $ce = eval { WeBWorK::CourseEnvironment-> new({ courseName => $selectedCourse }) };
357+ if ($@ ) {
358+ $c -> log -> error(" Failed to initialize course environment for $selectedCourse : $@ " );
359+ return $c -> render(' ContentGenerator/LTI/content_item_selection_error' ,
360+ errorMessage => $c -> maketext(' The course [_1] is not correctly configured.' , $selectedCourse ));
361+ }
362+ $c -> ce($ce );
363+ my $db = WeBWorK::DB-> new($ce );
364+ $c -> db($db );
365+ $c -> setSessionParams;
366+
367+ $c -> db-> setLTICourseMap($selectedCourse , $c -> param(' lms_context_id' ));
368+ }
369+
213370 return $c -> render(
214371 ' ContentGenerator/LTI/content_item_selection' ,
215372 visibleSets => [ $c -> db-> getGlobalSetsWhere({ visible => 1 }, [qw( due_date set_id) ]) ],
0 commit comments