Annotation of loncom/lti/ltiroster.pm, revision 1.1
1.1 ! raeburn 1: # The LearningOnline Network with CAPA
! 2: # LTI Consumer Module to respond to a course roster request.
! 3: #
! 4: # $Id: ltiroster.pm,v 1.1 2017/12/01 11:30:20 raeburn Exp $
! 5: #
! 6: # Copyright Michigan State University Board of Trustees
! 7: #
! 8: # This file is part of the LearningOnline Network with CAPA (LON-CAPA).
! 9: #
! 10: # LON-CAPA is free software; you can redistribute it and/or modify
! 11: # it under the terms of the GNU General Public License as published by
! 12: # the Free Software Foundation; either version 2 of the License, or
! 13: # (at your option) any later version.
! 14: #
! 15: # LON-CAPA is distributed in the hope that it will be useful,
! 16: # but WITHOUT ANY WARRANTY; without even the implied warranty of
! 17: # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
! 18: # GNU General Public License for more details.
! 19: #
! 20: # You should have received a copy of the GNU General Public License
! 21: # along with LON-CAPA; if not, write to the Free Software
! 22: # Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
! 23: #
! 24: # /home/httpd/html/adm/gpl.txt
! 25: #
! 26: # http://www.lon-capa.org/
! 27: #
! 28:
! 29: package Apache::ltiroster;
! 30:
! 31: use strict;
! 32: use Apache::Constants qw(:common :http);
! 33: use Encode;
! 34: use Digest::SHA;
! 35: use Apache::lonnet;
! 36: use Apache::loncommon;
! 37: use Apache::lonacc;
! 38: use Apache::loncoursedata;
! 39: use LONCAPA::ltiutils;
! 40:
! 41: sub handler {
! 42: my $r = shift;
! 43: my %errors;
! 44: #
! 45: # Retrieve data POSTed by LTI Provider
! 46: #
! 47: &Apache::lonacc::get_posted_cgi($r);
! 48: my $params = {};
! 49: foreach my $key (sort(keys(%env))) {
! 50: if ($key =~ /^form\.(.+)$/) {
! 51: $params->{$1} = $env{$key};
! 52: }
! 53: }
! 54:
! 55: unless (keys(%{$params})) {
! 56: $errors{1} = 1;
! 57: &invalid_request($r,\%errors);
! 58: return OK;
! 59: }
! 60:
! 61: #
! 62: # Retrieve the signature, digested symb, and LON-CAPA courseID
! 63: # from the ext_ims_lis_memberships_id in the POSTed data
! 64: #
! 65:
! 66: unless ($params->{'ext_ims_lis_memberships_id'}) {
! 67: $errors{2} = 1;
! 68: &invalid_request($r,\%errors);
! 69: return OK;
! 70: }
! 71:
! 72: my ($rostersig,$digsymb,$cid) = split(/\Q:::\E/,$params->{'ext_ims_lis_memberships_id'});
! 73: unless ($rostersig && $digsymb && $cid) {
! 74: $errors{3} = 1;
! 75: &invalid_request($r,\%errors);
! 76: return OK;
! 77: }
! 78:
! 79: my ($cdom,$cnum,$marker,$symb);
! 80:
! 81: #
! 82: # Determine the domain and the courseID of the LON-CAPA course to which the
! 83: # launch of LON-CAPA should provide access.
! 84: #
! 85: ($cdom,$cnum) = &LONCAPA::ltiutils::get_loncapa_course($r->dir_config('lonHostID'),
! 86: $cid,\%errors);
! 87: unless ($cdom && $cnum) {
! 88: $errors{4} = 1;
! 89: &invalid_request($r,\%errors);
! 90: return OK;
! 91: }
! 92:
! 93: #
! 94: # Use the digested symb to lookup the real symb in exttools.db
! 95: #
! 96:
! 97: ($marker,$symb) =
! 98: &LONCAPA::ltiutils::get_tool_instance($cdom,$cnum,$digsymb,undef,\%errors);
! 99:
! 100: unless ($marker) {
! 101: $errors{5} = 1;
! 102: &invalid_request($r,\%errors);
! 103: return OK;
! 104: }
! 105:
! 106: #
! 107: # Retrieve the Consumer key and Consumer secret from the domain configuration
! 108: # for the Tool Provider ID stored in the exttool_$marker.db
! 109: #
! 110:
! 111: my (%toolsettings,%ltitools);
! 112: my ($consumer_secret,$nonce_lifetime) =
! 113: &LONCAPA::ltiutils::get_tool_secret($params->{'oauth_consumer_key'},
! 114: $marker,$symb,$cdom,$cnum,
! 115: \%toolsettings,\%ltitools,\%errors);
! 116:
! 117: #
! 118: # Verify the signed request using the consumer_key and
! 119: # secret for the specific LTI Provider.
! 120: #
! 121:
! 122: my $protocol = 'http';
! 123: if ($ENV{'SERVER_PORT'} == 443) {
! 124: $protocol = 'https';
! 125: }
! 126: unless (LONCAPA::ltiutils::verify_request($params,$protocol,$r->hostname,$r->uri,
! 127: $env{'request.method'},$consumer_secret,
! 128: \%errors)) {
! 129: $errors{6} = 1;
! 130: &invalid_request($r,\%errors);
! 131: return OK;
! 132: }
! 133:
! 134: #
! 135: # Determine if nonce in POSTed data has expired.
! 136: # If unexpired, confirm it has not already been used.
! 137:
! 138: unless (&LONCAPA::ltiutils::check_nonce($params->{'oauth_nonce'},$params->{'oauth_timestamp'},
! 139: $nonce_lifetime,$cdom,$r->dir_config('lonLTIDir'))) {
! 140: $errors{7} = 1;
! 141: &invalid_request($r,\%errors);
! 142: return OK;
! 143: }
! 144:
! 145: #
! 146: # Verify that the ext_ims_lis_memberships_id has not been tampered
! 147: # with, and the rostersecret used to create it is still valid.
! 148: #
! 149:
! 150: unless (&LONCAPA::ltiutils::verify_lis_item($rostersig,'roster',$digsymb,undef,$cdom,$cnum,
! 151: \%toolsettings,\%ltitools,\%errors)) {
! 152: $errors{8} = 1;
! 153: &invalid_request($r,\%errors);
! 154: return OK;
! 155: }
! 156:
! 157: #
! 158: # Retrieve users with active roles in course for all roles for which roles have been mapped
! 159: # in domain configuration for the Tool Provider requesting the roster.
! 160: #
! 161: my %maproles;
! 162:
! 163: if (ref($ltitools{'roles'}) eq 'HASH') {
! 164: %maproles = %{$ltitools{'roles'}};
! 165: }
! 166:
! 167: unless (keys(%maproles)) {
! 168: $errors{9} = 1;
! 169: &invalid_request($r,\%errors);
! 170: return OK;
! 171: }
! 172:
! 173: my $crstype;
! 174: my @allroles = &Apache::lonuserutils::roles_by_context('course',0,$crstype);
! 175:
! 176: my (%availableroles,$coursepersonnel,$includestudents,%userdata,
! 177: @needpersenv,@needstuenv,$needemail,$needfullname,$needuser,
! 178: $needroles,$needsresult,$gradesecret);
! 179:
! 180: if ($ltitools{'passback'}) {
! 181: my $now = time;
! 182: if (&LONCAPA::ltiutils::set_service_secret($cdom,$cnum,$marker,'grade',$now,
! 183: \%toolsettings,\%ltitools) eq 'ok') {
! 184: if ($toolsettings{'gradesecret'} ne '') {
! 185: $needsresult = 1;
! 186: $gradesecret = $ltitools{'gradesecret'};
! 187: }
! 188: }
! 189: }
! 190:
! 191: foreach my $role (@allroles) {
! 192: if (exists($maproles{$role})) {
! 193: $availableroles{$role} = 1;
! 194: if ($role eq 'st') {
! 195: $includestudents = 1;
! 196: } else {
! 197: $coursepersonnel = 1;
! 198: }
! 199: }
! 200: }
! 201: if (keys(%availableroles)) {
! 202: $needroles = 1;
! 203: }
! 204: if (ref($ltitools{'fields'}) eq 'HASH') {
! 205: foreach my $field (keys(%{$ltitools{'fields'}})) {
! 206: if (($field eq 'lastname') || ($field eq 'firstname')) {
! 207: push(@needstuenv,$field);
! 208: push(@needpersenv,$field);
! 209: } elsif ($field eq 'email') {
! 210: $needemail = 1;
! 211: push(@needpersenv,'permanentemail');
! 212: } elsif ($field eq 'fullname') {
! 213: $needfullname = 1;
! 214: } elsif ($field eq 'user') {
! 215: $needuser = 1;
! 216: }
! 217: }
! 218: }
! 219:
! 220: my $statusidx = &Apache::loncoursedata::CL_STATUS();
! 221: my $emailidx = &Apache::loncoursedata::CL_PERMANENTEMAIL();
! 222:
! 223: my %students;
! 224: if ($includestudents) {
! 225: my $classlist = &Apache::loncoursedata::get_classlist($cdom,$cnum);
! 226: if (ref($classlist) eq 'HASH') {
! 227: %students = %{$classlist};
! 228: }
! 229: }
! 230:
! 231: &Apache::loncommon::content_type($r,'text/xml');
! 232: $r->send_http_header;
! 233: if ($r->header_only) {
! 234: return;
! 235: }
! 236: $r->print(<<"END");
! 237: <message_response>
! 238: <lti_message_type>basic-lis-readmembershipsforcontext</lti_message_type>
! 239: <statusinfo>
! 240: <codemajor>Success</codemajor>
! 241: <severity>Status</severity>
! 242: <codeminor>fullsuccess</codeminor>
! 243: <description>Roster retrieved</description>
! 244: </statusinfo>
! 245: <memberships>
! 246: END
! 247:
! 248: my %skipstu;
! 249: if ($coursepersonnel) {
! 250: my %personnel = &Apache::lonnet::get_my_roles($cnum,$cdom);
! 251: foreach my $key (sort(keys(%personnel))) {
! 252: my ($uname,$udom,$role) = split(/:/,$key);
! 253: if ($availableroles{$role}) {
! 254: $userdata{$uname.':'.$udom}{'ltiroles'}{$maproles{$role}} = 1;
! 255: }
! 256: }
! 257: foreach my $user (sort(keys(%userdata))) {
! 258: if (exists($students{$user})) {
! 259: $skipstu{$user} = 1;
! 260: }
! 261: $r->print(" <member>\n");
! 262: my ($uname,$udom) = split(/:/,$user);
! 263: my $digest_user = &Encode::decode_utf8($uname.':'.$udom);
! 264: $digest_user = &Digest::SHA::sha1_hex($digest_user);
! 265: $r->print(' <user_id>'.$digest_user.'</user_id>'."\n");
! 266: if (exists($students{$user})) {
! 267: if (ref($students{$user}) eq 'ARRAY') {
! 268: if ($students{$user}[$statusidx] eq 'Active') {
! 269: $userdata{$uname.':'.$udom}{'ltiroles'}{$maproles{'st'}} = 1;
! 270: }
! 271: }
! 272: }
! 273: if ($needroles) {
! 274: if (ref($userdata{$uname.':'.$udom}{'ltiroles'}) eq 'HASH') {
! 275: $r->print(' <roles>'.join(',',sort(keys(%{$userdata{$uname.':'.$udom}{'ltiroles'}}))).'</roles>'."\n");
! 276: } else {
! 277: $r->print(" <roles></roles>\n");
! 278: }
! 279: } else {
! 280: $r->print(" <roles></roles>\n");
! 281: }
! 282: if ($needuser) {
! 283: $r->print(' <person_sourcedid>'.$user.'</person_sourcedid>'."\n");
! 284: } else {
! 285: $r->print(" <person_sourcedid></person_sourcedid>\n");
! 286: }
! 287: my %userinfo;
! 288: if (@needpersenv) {
! 289: %userinfo = &Apache::lonnet::userenvironment($udom,$uname,@needpersenv);
! 290: }
! 291: foreach my $item ('firstname','lastname','permanentemail') {
! 292: my $info;
! 293: if ((@needpersenv) && (grep(/^\Q$item\E$/,@needpersenv))) {
! 294: $info = $userinfo{$item};
! 295: }
! 296: if ($item eq 'firstname') {
! 297: $r->print(' <person_name_given>'.$info.'</person_name_given>'."\n");
! 298: } elsif ($item eq 'lastname') {
! 299: $r->print(' <person_name_family>'.$info.'</person_name_family>'."\n");
! 300: } elsif ($item eq 'permanentemail') {
! 301: $r->print(' <person_contact_email_primary>'.$info.'</person_contact_email_primary>'."\n");
! 302: }
! 303: }
! 304: if ($needfullname) {
! 305: my $info = &Apache::loncommon::plainname($uname,$udom);
! 306: if ($info eq $uname.':'.$udom) {
! 307: $info = '';
! 308: }
! 309: $r->print(' <person_name_full>'.$info.'</person_name_full>'."\n");
! 310: } else {
! 311: $r->print(' <person_name_full></person_name_full>'."\n");
! 312: }
! 313: if ($needsresult) {
! 314: my $uniqid = $digsymb.':::'.$digest_user.':::'.$cid;
! 315: my $sourcedid = &LONCAPA::ltiutils::get_service_id($gradesecret,$uniqid);
! 316: $r->print(' <lis_result_sourcedid>'.$sourcedid.'</lis_result_sourcedid>'."\n");
! 317: } else {
! 318: $r->print(" <lis_result_sourcedid></lis_result_sourcedid>\n");
! 319: }
! 320: $r->print(" </member>\n");
! 321: }
! 322: }
! 323:
! 324: if (($includestudents) && (keys(%students))) {
! 325: foreach my $user (keys(%students)) {
! 326: next if ($skipstu{$user});
! 327: if (ref($students{$user}) eq 'ARRAY') {
! 328: next unless ($students{$user}[$statusidx] eq 'Active');
! 329: $r->print(" <member>\n");
! 330: my ($uname,$udom) = split(/:/,$user);
! 331: my $digest_user = &Encode::decode_utf8($uname.':'.$udom);
! 332: $digest_user = &Digest::SHA::sha1_hex($digest_user);
! 333: $r->print(' <user_id>'.$digest_user.'</user_id>'."\n");
! 334: if ($needroles) {
! 335: $r->print(' <roles>'.$maproles{'st'}.'</roles>'."\n");
! 336: } else {
! 337: $r->print(" <roles></roles>\n");
! 338: }
! 339: if ($needuser) {
! 340: $r->print(' <person_sourcedid>'.$user.'</person_sourcedid>'."\n");
! 341: } else {
! 342: $r->print(" <person_sourcedid></person_sourcedid>\n");
! 343: }
! 344: my %userinfo;
! 345: if (@needstuenv) {
! 346: %userinfo = &Apache::lonnet::userenvironment($udom,$uname,@needstuenv);
! 347: }
! 348: foreach my $item ('firstname','lastname') {
! 349: my $info;
! 350: if ((@needstuenv) && (grep(/^\Q$item\E$/,@needstuenv))) {
! 351: $info = $userinfo{$item};
! 352: }
! 353: if ($item eq 'firstname') {
! 354: $r->print(' <person_name_given>'.$info.'</person_name_given>'."\n");
! 355: } elsif ($item eq 'lastname') {
! 356: $r->print(' <person_name_family>'.$info.'</person_name_family>'."\n");
! 357: }
! 358: }
! 359: if ($needemail) {
! 360: $r->print(' <person_contact_email_primary>'.$students{$user}[$emailidx].'</person_contact_email_primary>'."\n");
! 361: } else {
! 362: $r->print(' <person_contact_email_primary></person_contact_email_primary>'."\n");
! 363: }
! 364: if ($needfullname) {
! 365: my $info = &Apache::loncommon::plainname($uname,$udom);
! 366: if ($info eq $uname.':'.$udom) {
! 367: $info = '';
! 368: }
! 369: $r->print(' <person_name_full>'.$info.'</person_name_full>'."\n");
! 370: } else {
! 371: $r->print(' <person_name_full></person_name_full>'."\n");
! 372: }
! 373: if ($needsresult) {
! 374: my $uniqid = $digsymb.':::'.$digest_user.':::'.$cid;
! 375: my $sourcedid = &LONCAPA::ltiutils::get_service_id($gradesecret,$uniqid);
! 376: $r->print(' <lis_result_sourcedid>'.$sourcedid.'</lis_result_sourcedid>'."\n");
! 377: } else {
! 378: $r->print(" <lis_result_sourcedid></lis_result_sourcedid>\n");
! 379: }
! 380: $r->print(" </member>\n");
! 381: }
! 382: }
! 383: }
! 384: $r->print(<<"END");
! 385: </memberships>
! 386: </message_response>
! 387: END
! 388: return OK;
! 389: }
! 390:
! 391: sub invalid_request {
! 392: my ($r,$errors) = @_;
! 393: my $errormsg;
! 394: if (ref($errors) eq 'HASH') {
! 395: $errormsg = join('&&',keys(%{$errors}));
! 396: }
! 397: &Apache::loncommon::content_type($r,'text/xml');
! 398: $r->send_http_header;
! 399: if ($r->header_only) {
! 400: return;
! 401: }
! 402: $r->print(<<"END");
! 403: <message_response>
! 404: <lti_message_type>basic-lis-updateresult</lti_message_type>
! 405: <statusinfo>
! 406: <codemajor>Failure</codemajor>
! 407: <severity>Error</severity>
! 408: <codeminor>$errormsg</codeminor>
! 409: </statusinfo>
! 410: </message_response>
! 411: END
! 412: return;
! 413: }
! 414:
! 415: 1;
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>