@@ -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 }
@@ -182,6 +277,13 @@ sub launch ($c) {
182277 ? (
183278 courseID => $c -> stash-> {courseID },
184279 initial_request => 1,
280+ $c -> stash-> {courseChoices }
281+ ? (
282+ course_choices => $c -> stash-> {courseChoices },
283+ lms_context_id =>
284+ $c -> stash-> {lti_jwt_claims }{' https://purl.imsglobal.org/spec/lti/claim/context' }{id }
285+ )
286+ : (),
185287 accept_multiple =>
186288 $c -> stash-> {lti_jwt_claims }{' https://purl.imsglobal.org/spec/lti-dl/claim/deep_linking_settings' }
187289 {accept_multiple },
@@ -209,6 +311,61 @@ sub content_selection ($c) {
209311 unless $c -> authz-> hasPermissions($c -> authen-> {user_id }, ' modify_problem_sets' );
210312
211313 if ($c -> param(' initial_request' )) {
314+ my @courseChoices = $c -> param(' course_choices' );
315+ if (@courseChoices > 1) {
316+ return $c -> render(
317+ ' ContentGenerator/LTI/content_item_course_selection' ,
318+ courseChoices => \@courseChoices ,
319+ forwardParams => {
320+ accept_multiple => $c -> param(' accept_multiple' ),
321+ deep_link_return_url => $c -> param(' deep_link_return_url' ),
322+ lms_context_id => $c -> param(' lms_context_id' ),
323+ $c -> param(' data' ) ? (data => $c -> param(' data' )) : (),
324+ }
325+ );
326+ } elsif (@courseChoices ) {
327+ # If only one course matched for this user, then just use it.
328+ # Add it to the course map and skip course selection.
329+ $c -> db-> setLTICourseMap($courseChoices [0], $c -> param(' lms_context_id' ));
330+ }
331+
332+ my $selectedCourse = $c -> param(' selected_course' );
333+ if ($selectedCourse && $selectedCourse ne $c -> ce-> {courseName }) {
334+ # The user has selected a course that is not the initial guess (the first course found that the user fit
335+ # into), and the user was authenticated into that inital guess course. So sign the user out of that course,
336+ # and sign the user into the selected course. This does not go through the entire authentication process,
337+ # but presumably that would succeed since the authhentication parameters for the two courses match and the
338+ # permissions of the user were checked in both courses already.
339+ my $key = $c -> db-> getKey($c -> authen-> {user_id });
340+ $c -> db-> deleteKey($c -> authen-> {user_id });
341+ $c -> signed_cookie(
342+ ' WeBWorKCourseSession.' . $c -> ce-> {courseName },
343+ ' ' ,
344+ {
345+ domain => $c -> app-> sessions-> cookie_domain,
346+ expires => time ,
347+ httponly => 1,
348+ path => $c -> app-> sessions-> cookie_path,
349+ samesite => $c -> app-> sessions-> samesite,
350+ secure => $c -> app-> sessions-> secure
351+ }
352+ );
353+
354+ $c -> stash-> {courseID } = $selectedCourse ;
355+ my $ce = eval { WeBWorK::CourseEnvironment-> new({ courseName => $selectedCourse }) };
356+ if ($@ ) {
357+ $c -> log -> error(" Failed to initialize course environment for $selectedCourse : $@ " );
358+ return $c -> render(' ContentGenerator/LTI/content_item_selection_error' ,
359+ errorMessage => $c -> maketext(' The course [_1] is not correctly configured.' , $selectedCourse ));
360+ }
361+ $c -> ce($ce );
362+ my $db = WeBWorK::DB-> new($ce );
363+ $c -> db($db );
364+ $c -> setSessionParams;
365+
366+ $c -> db-> setLTICourseMap($selectedCourse , $c -> param(' lms_context_id' ));
367+ }
368+
212369 return $c -> render(
213370 ' ContentGenerator/LTI/content_item_selection' ,
214371 visibleSets => [ $c -> db-> getGlobalSetsWhere({ visible => 1 }, [qw( due_date set_id) ]) ],
0 commit comments