Annotation of loncom/lti/ltipassback.pm, revision 1.1
1.1 ! raeburn 1: # The LearningOnline Network with CAPA
! 2: # LTI Consumer Module to receive grades passed back by Provider
! 3: #
! 4: # $Id: ltipassback.pm,v 1.1 2017/11/30 22:41: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::ltipassback;
! 30:
! 31: use strict;
! 32: use Apache::Constants qw(:common :http);
! 33: use Apache::lonnet;
! 34: use Apache::loncommon;
! 35: use Apache::lonacc;
! 36: use LONCAPA::ltiutils;
! 37:
! 38: sub handler {
! 39: my $r = shift;
! 40: my %errors;
! 41: #
! 42: # Retrieve data POSTed by LTI Provider
! 43: #
! 44: &Apache::lonacc::get_posted_cgi($r);
! 45: my $params = {};
! 46: foreach my $key (sort(keys(%env))) {
! 47: if ($key =~ /^form\.(.+)$/) {
! 48: $params->{$1} = $env{$key};
! 49: }
! 50: }
! 51:
! 52: unless (keys(%{$params})) {
! 53: $errors{1} = 1;
! 54: &invalid_request($r,$params,\%errors);
! 55: return OK;
! 56: }
! 57:
! 58: unless ($params->{'oauth_consumer_key'} &&
! 59: $params->{'oauth_nonce'} &&
! 60: $params->{'oauth_timestamp'} &&
! 61: $params->{'oauth_version'} &&
! 62: $params->{'oauth_signature'} &&
! 63: $params->{'oauth_signature_method'}) {
! 64: $errors{2} = 1;
! 65: &invalid_request($r,$params,\%errors);
! 66: return OK;
! 67: }
! 68:
! 69: #
! 70: # Retrieve the signature, digested symb, digested user, and LON-CAPA
! 71: # courseID from the sourcedid in the POSTed data
! 72: #
! 73: unless ($params->{'sourcedid'}) {
! 74: $errors{3} = 1;
! 75: &invalid_request($r,$params,\%errors);
! 76: return OK;
! 77: }
! 78:
! 79: my ($resultsig,$digsymb,$diguser,$cid) = split(/\Q:::\E/,$params->{'sourcedid'});
! 80: unless ($resultsig && $digsymb && $diguser && $cid) {
! 81: $errors{4} = 1;
! 82: &invalid_request($r,$params,\%errors);
! 83: return OK;
! 84: }
! 85:
! 86: my ($cdom,$cnum,$marker,$symb,$uname,$udom);
! 87:
! 88: #
! 89: # Determine the domain and the courseID of the LON-CAPA course to which the
! 90: # launch of LON-CAPA should provide access.
! 91: #
! 92: ($cdom,$cnum) = &LONCAPA::ltiutils::get_loncapa_course($r->dir_config('lonHostID'),
! 93: $cid,\%errors);
! 94: unless ($cdom && $cnum) {
! 95: &invalid_request($r,$params,\%errors);
! 96: return OK;
! 97: }
! 98:
! 99: #
! 100: # Use the digested symb to lookup the real symb in exttools.db
! 101: #
! 102:
! 103: ($marker,$symb,$uname,$udom) =
! 104: &LONCAPA::ltiutils::get_tool_instance($cdom,$cnum,$digsymb,$diguser,\%errors);
! 105:
! 106: unless ($marker) {
! 107: &invalid_request($r,$params,\%errors);
! 108: return OK;
! 109: }
! 110:
! 111: #
! 112: # Retrieve the Consumer key and Consumer secret from the domain configuration
! 113: # for the Tool Provider ID stored in the exttool_$marker.db
! 114: #
! 115:
! 116: my (%toolsettings,%ltitools);
! 117: my ($consumer_secret,$nonce_lifetime) =
! 118: &LONCAPA::ltiutils::get_tool_secret($params->{'oauth_consumer_key'},
! 119: $marker,$symb,$cdom,$cnum,
! 120: \%toolsettings,\%ltitools,\%errors);
! 121:
! 122: #
! 123: # Verify the signed request using the consumer_key and
! 124: # secret for the specific LTI Provider.
! 125: #
! 126:
! 127: my $protocol = 'http';
! 128: if ($ENV{'SERVER_PORT'} == 443) {
! 129: $protocol = 'https';
! 130: }
! 131: unless (LONCAPA::ltiutils::verify_request($params,$protocol,$r->hostname,$r->uri,
! 132: $env{'request.method'},$consumer_secret,
! 133: \%errors)) {
! 134: &invalid_request($r,$params,\%errors);
! 135: return OK;
! 136: }
! 137:
! 138: #
! 139: # Determine if nonce in POSTed data has expired.
! 140: # If unexpired, confirm it has not already been used.
! 141:
! 142: unless (&LONCAPA::ltiutils::check_nonce($params->{'oauth_nonce'},$params->{'oauth_timestamp'},
! 143: $ltitools{'lifetime'},$cdom,$r->dir_config('lonLTIDir'))) {
! 144: $errors{15} = 1;
! 145: &invalid_request($r,$params,\%errors);
! 146: return OK;
! 147: }
! 148:
! 149: #
! 150: # Verify that the sourcedid has not been tampered with,
! 151: # and the gradesecret used to create it is still valid.
! 152: #
! 153:
! 154: unless (&LONCAPA::ltiutils::verify_lis_item($resultsig,'grade',$digsymb,$diguser,$cdom,
! 155: $cnum,\%toolsettings,\%ltitools,\%errors)) {
! 156: &invalid_request($r,$params,\%errors);
! 157: return OK;
! 158: }
! 159:
! 160: #
! 161: # Does the user have an active role in the course which maps to one of
! 162: # the supported LTI roles
! 163: #
! 164:
! 165: if (($uname ne '') && ($udom ne '')) {
! 166: my %maproles;
! 167: if (ref($ltitools{'roles'}) eq 'HASH') {
! 168: %maproles = %{$ltitools{'roles'}};
! 169: }
! 170: unless (keys(%maproles)) {
! 171: $errors{20} = 1;
! 172: &invalid_request($r,$params,\%errors);
! 173: return OK;
! 174: }
! 175: my ($crstype,$hasrole);
! 176: my @allroles = &Apache::lonuserutils::roles_by_context('course',0,$crstype);
! 177: my (%availableroles,$coursepersonnel,$includestudents,%users);
! 178: foreach my $role (@allroles) {
! 179: if (exists($maproles{$role})) {
! 180: $availableroles{$role} = 1;
! 181: if ($role eq 'st') {
! 182: $includestudents = 1;
! 183: } else {
! 184: $coursepersonnel = 1;
! 185: }
! 186: }
! 187: }
! 188: if (keys(%availableroles)) {
! 189: my $courseurl = "/$cdom/$cnum";
! 190: my %roleshash = &Apache::lonnet::dump('roles',$udom,$uname,$courseurl);
! 191: if (keys(%roleshash)) {
! 192: my $now = time;
! 193: foreach my $key (keys(%roleshash)) {
! 194: if ($key =~ m{^\Q$courseurl\E(|/\w+)_(\w+)$}) {
! 195: my ($secgroup,$rolecode) = ($1,$2);
! 196: next if ($rolecode eq 'gr');
! 197: next unless ($availableroles{$rolecode});
! 198: my ($dummy,$end,$start)=split(/\_/,$roleshash{$key});
! 199: next if (defined($end) && $end && ($now > $end));
! 200: next if (defined($start) && $start && ($now < $start));
! 201: $hasrole = 1;
! 202: last;
! 203: }
! 204: }
! 205: }
! 206: }
! 207: unless ($hasrole) {
! 208: $errors{21} = 1;
! 209: &invalid_request($r,$params,\%errors);
! 210: return OK;
! 211: }
! 212: } else {
! 213: $errors{22} = 1;
! 214: &invalid_request($r,$params,\%errors);
! 215: return OK;
! 216: }
! 217:
! 218: #
! 219: # Store result if one was sent in a valid format.
! 220: #
! 221:
! 222:
! 223: my ($result,$resulttype,$lang,$pcf);
! 224: if (exists($params->{'result_resultvaluesourcedid'})) {
! 225: $resulttype = 'decimal';
! 226: } else {
! 227: $resulttype = $params->{'result_resultvaluesourcedid'};
! 228: $resulttype =~ s/(^\s+|\s+)$//g;
! 229: }
! 230: $result = $params->{'result_resultscore_textstring'};
! 231: $result =~ s/(^\s+|\s+)$//g;
! 232: my $posslang = $params->{'result_resultscore_language'};
! 233: $posslang =~ s/(^\s+|\s+)$//g;
! 234: if ($posslang =~ /^\w+(|\-\w+(|\-w+))$/) {
! 235: $lang = $posslang;
! 236: }
! 237: if (($resulttype eq 'ratio') || ($resulttype eq 'decimal') || ($resulttype eq 'percentage')) {
! 238: if ($resulttype eq 'ratio') {
! 239: my ($numerator,$denominator) = split(/\s*\/\s*/,$result,2);
! 240: $numerator =~ s/(^\s+|\s+)$//g;
! 241: $denominator =~ s/(^\s+|\s+)$//g;
! 242: if (($numerator =~ /^\d+$/) && ($denominator =~ /^\d+$/) && ($denominator !=0)) {
! 243: eval {
! 244: $pcf = $numerator/$denominator;
! 245: };
! 246: }
! 247: if ($@) {
! 248: $errors{22} = 1;
! 249: &invalid_request($r,$params,\%errors);
! 250: return OK;
! 251: }
! 252: } elsif ($resulttype eq 'decimal') {
! 253: if (($result ne '') && ($result =~ /^\d*\.?\d*$/)) {
! 254: if ($result eq '.') {
! 255: $result = 0;
! 256: }
! 257: if (($result >= 0) && ($result <= 1)) {
! 258: $pcf = $result;
! 259: }
! 260: }
! 261: } elsif ($resulttype eq 'percentage') {
! 262: if ($result =~ /^(\d+)\s*\%?$/) {
! 263: my $percent = $1;
! 264: if (($percent >= 0) && ($percent <= 100)) {
! 265: $pcf = $percent/100.0;
! 266: }
! 267: }
! 268: }
! 269: if ($pcf ne '') {
! 270: my %newrecord=();
! 271: my $reckey = 'resource.0.solved';
! 272: my %record = &Apache::lonnet::restore($symb,$cdom.'_'.$cnum,$udom,$uname);
! 273: if ($record{'resource.0.awarded'} ne $pcf) {
! 274: $newrecord{'resource.0.awarded'} = $pcf;
! 275: }
! 276: if ($pcf == 0) {
! 277: if ($record{$reckey} ne 'incorrect_by_override') {
! 278: $newrecord{$reckey} = 'incorrect_by_override';
! 279: }
! 280: } else {
! 281: if ($record{$reckey} ne 'correct_by_override') {
! 282: $newrecord{$reckey} = 'correct_by_override';
! 283: }
! 284: }
! 285: if (%newrecord) {
! 286: my $result = &Apache::lonnet::cstore(\%newrecord,$symb,$cdom.'_'.$cnum,
! 287: $udom,$uname);
! 288: if (($result eq 'ok') || ($result eq 'con_delayed')) {
! 289: &success($r,$params->{'sourcedid'},$resulttype,$result,$lang);
! 290: } else {
! 291: $errors{23} = 1;
! 292: &invalid_request($r,$params,\%errors);
! 293: }
! 294: }
! 295: } else {
! 296: $errors{24} = 1;
! 297: &invalid_request($r,$params,\%errors);
! 298: }
! 299: } else {
! 300: $errors{25} = 1;
! 301: &invalid_request($r,$params,\%errors);
! 302: }
! 303: return OK;
! 304: }
! 305:
! 306: sub success {
! 307: my ($r,$sourcedid,$scoretype,$score,$lang) = @_;
! 308: my $date = &Apache::loncommon::utc_string(time);
! 309: &Apache::loncommon::content_type($r,'text/xml');
! 310: $r->send_http_header;
! 311: if ($r->header_only) {
! 312: return;
! 313: }
! 314: $r->print(<<"END");
! 315: <?xml version="1.0" encoding="UTF-8" ?>
! 316: <message_response>
! 317: <lti_message_type>basic-lis-updateresult</lti_message_type>
! 318: <statusinfo>
! 319: <codemajor>Success</codemajor>
! 320: <severity>Status</severity>
! 321: <codeminor>fullsuccess</codeminor>
! 322: <description>Grade updated</description>
! 323: </statusinfo>
! 324: <result>
! 325: <sourcedid>$sourcedid</sourcedid>
! 326: <date>$date</date>
! 327: <resultscore>
! 328: <resultvaluesourcedid>$scoretype</resultvaluesourcedid>
! 329: <textstring>$score</textstring>
! 330: <language>$lang</language>
! 331: </resultscore>
! 332: </result>
! 333: </message_response>
! 334: END
! 335: return;
! 336: }
! 337:
! 338: sub invalid_request {
! 339: my ($r,$params,$errors) = @_;
! 340: my $date = &Apache::loncommon::utc_string(time);
! 341: my ($scoretype,$score,$lang);
! 342: if (ref($params) eq 'HASH') {
! 343: if ($params->{'result_resultvaluesourcedid'} =~ /^\s*(decimal|percentage|ratio)\s*$/) {
! 344: $scoretype = $1;
! 345: }
! 346: if ($scoretype eq 'decimal') {
! 347: if ($params->{'result_resultscore_textstring'} =~ /^\s*(\d*\.?\d*)\s*$/) {
! 348: $score = $1;
! 349: }
! 350: } elsif ($scoretype eq 'ratio') {
! 351: if ($params->{'result_resultscore_textstring'} =~ m{^\s*(\d+)\s*/\s*(\d+)\s*$}) {
! 352: $score = $1.'/'.$2;
! 353: }
! 354: } elsif ($scoretype eq 'percentage') {
! 355: if ($params->{'result_resultscore_textstring'} =~ /^\s*(\d+)\s*(\%?)\s*$/) {
! 356: $score = $1.$2;
! 357: }
! 358: }
! 359: my $posslang = $params->{'result_resultscore_language'};
! 360: $posslang =~ s/(^\s+|\s+)$//g;
! 361: if ($posslang =~ /^\w+(|\-\w+(|\-w+))$/) {
! 362: $lang = $posslang;
! 363: }
! 364: }
! 365: my $errormsg;
! 366: if (ref($errors) eq 'HASH') {
! 367: $errormsg = join(',',keys(%{$errors}));
! 368: }
! 369: &Apache::loncommon::content_type($r,'text/xml');
! 370: $r->send_http_header;
! 371: if ($r->header_only) {
! 372: return;
! 373: }
! 374: $r->print(<<"END");
! 375: <message_response>
! 376: <lti_message_type>basic-lis-updateresult</lti_message_type>
! 377: <statusinfo>
! 378: <codemajor>Failure</codemajor>
! 379: <severity>Error</severity>
! 380: <codeminor>$errormsg</codeminor>
! 381: </statusinfo>
! 382: <result>
! 383: <sourcedid>$params->{'sourcedid'}</sourcedid>
! 384: <statusofresult>interim</statusofresult>
! 385: <date>$date</date>
! 386: <resultscore>
! 387: <resultvaluesourcedid>$scoretype</resultvaluesourcedid>
! 388: <textstring>$score</textstring>
! 389: <language>$lang</language>
! 390: </resultscore>
! 391: </result>
! 392: </message_response>
! 393: END
! 394: return;
! 395: }
! 396:
! 397: 1;
! 398:
FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>