File:  [LON-CAPA] / loncom / homework / lonr.pm
Revision 1.9: download - view: text, annotated - select for diffs
Fri Aug 14 01:58:52 2009 UTC (14 years, 8 months ago) by www
Branches: MAIN
CVS tags: HEAD
Saving my work - correctly serialize only the output of the last line of a script

    1: # The LearningOnline Network with CAPA
    2: # Interface routines to R CAS
    3: #
    4: # $Id: lonr.pm,v 1.9 2009/08/14 01:58:52 www 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::lonr;
   30:  
   31: use strict;
   32: use IO::Socket;
   33: use Apache::lonnet;
   34: use Apache::response();
   35: use LONCAPA;
   36: 
   37: ### You need to install the libraries below for this to work!
   38: 
   39: ###use Tie::IxHash::Easy; # autoties all subhashes to keep index order
   40: ###use Data::Dumper;  # used to output hash contents
   41: 
   42: my $errormsg='';
   43: 
   44: #
   45: # Rcroak: for use with R-error messages
   46: #
   47: sub Rcroak {
   48:    $errormsg=$_[0];
   49: }
   50: 
   51: #
   52: #
   53: # Rpeel takes a string containing serialized values from R, 
   54: # peels off the first syntactically complete unit (number, string or array),
   55: # and returns a list (first unit, remainder).
   56: #
   57: sub Rpeel {
   58:         my $x = $_[0];  # the string containing the serialized R object(s)
   59:         if ($x =~ /^((?:i|d):(.+?);)(.*)$/) {
   60:                 return ($1, $+);  # x starts with a number
   61:         } elsif ($x =~ /^s:(\d+):/) {
   62:                 my $n = $1;  # x starts with a string of length n
   63:                 if ($x =~ /^(s:\d+:\"(.{$n})\";)(.*)$/) {
   64:                         return ($1, $+);  # x starts with a valid string
   65:                 } else {
   66:                         &Rcroak('invalid string detected');
   67:                 }
   68:         } elsif ($x =~ /^a:/) {
   69:                 # x starts with an array -- need to find the closing brace
   70:                 my $i = index $x, '{', 0;  # position of first opening brace
   71:                 if ($i < 0) {
   72:                         &Rcroak('array with no opening brace');
   73:                 }
   74:                 my $open = 1;  # counts open braces
   75:                 my $j = index $x, '}', $i; # position of first closing brace
   76:                 $i = index $x, '{', $i + 1; # position of next opening brace (if any)
   77:                 my $pos = -1;  # position of final closing brace
   78:                 do {
   79:                         if (($i < $j) && ($i > 0)) {
   80:                                 # encounter another opening brace before next closing brace
   81:                                 $open++;
   82:                                 $i = index $x, '{', $i + 1;  # find the next opening brace
   83:                         } elsif ($j > 0) {
   84:                                 # next brace encountered is a closing brace
   85:                                 $open--;
   86:                                 $pos = $j;
   87:                                 $j = index $x, '}', $j + 1;
   88:                         } else {
   89:                                 &Rcroak('unmatched left brace');
   90:                         }
   91:                 } until ($open eq 0);
   92:                 # array runs from start to $pos
   93:                 my $a = substr $x, 0, $pos + 1;  # array
   94:                 my $b = substr $x, $pos + 1;     # remainder
   95:                 return ($a, $b);
   96:         } else {
   97:                 &Rcroak('unrecognized R value');
   98:         }
   99: }
  100: # --- end Rpeel ---
  101: 
  102: #
  103: # Rreturn accepts a string containing a serialized R object
  104: # and returns either the object's value (if it is scalar) or a reference
  105: # to a hash containing the contents of the object.  Any null keys in the hash
  106: # are replaced by 'resultNNN' where NNN is the index of the entry in the original
  107: # R array.
  108: #
  109: sub Rreturn {
  110:     my $x = $_[0];  # the string containing the serialized R object(s)
  111:     $x=~s/^\"//;
  112:     $x=~s/\"$//;
  113:     $x=~s/\\\"/\"/g;
  114:     $errormsg='';
  115:     if ($x =~ /^(?:i|d):(.+?);$/) {
  116:         return $1;  # return the value of the number
  117:     } elsif ($x =~ /^s:(\d+):\"(.*)\";$/) {
  118:         # string -- verify the length
  119:         if (length($2) eq $1) {
  120:            return $2;  # return the string
  121:         } else {
  122:            return 'mismatch in string length';
  123:         }
  124:     } elsif ($x =~ /^a:(\d+):\{(.*)\}$/) {
  125:         # array
  126:         my $dim = $1;  # array size
  127:         $x = $2;  # array contents
  128:         tie(my %h,'Tie::IxHash::Easy'); # start a hash
  129:         keys(%h) = $dim; # allocate space for the hash
  130:         my $key;
  131:         my $y;
  132:         for (my $i = 0; $i < $dim; $i++) {
  133:            ($y, $x) = &Rpeel($x);  # strip off the entry for the key
  134:            if ($y eq '') {
  135:               &Rcroak('ran out of keys');
  136:            }
  137:            $key = &Rreturn($y);
  138:            if ($key eq '') {
  139:               $key = "result$i";  # correct null key
  140:            }
  141:            ($y, $x) = &Rpeel($x);  # strip off the value
  142:            if ($y eq '') {
  143:                &Rcroak('ran out of values');
  144:            }
  145:            if ($y =~ /^a:/) {
  146:                $h{$key} = \&Rreturn($y);  # array value: store as reference
  147:            } else {
  148:                $h{$key} = &Rreturn($y);  # scalar value: store the entry in the hash
  149:            }
  150:         }
  151:         if ($errormsg) { return $errormsg; }
  152:         return \%h;  # return a reference to the hash
  153:     } else {
  154:         return 'Unrecognized output';
  155:     }
  156: }
  157: # --- end Rreturn ---
  158: 
  159: sub Rentry {
  160:     my $hash = shift;  # pointer to tied hash
  161:     my $i;
  162:     if (ref($hash) ne 'HASH') {
  163:        return 'Argument to cas_hashref_entry is not a hash!';
  164:     }
  165:     while ($i = shift) {
  166:        if (exists($hash->{$i})) {
  167:           $hash = $hash->{$i};
  168:        } else {
  169:           return undef;
  170:        }
  171:        if (ref($hash) eq 'REF') {
  172:           $hash = $$hash;  # dereference one layer
  173:        } elsif (ref($hash) ne 'HASH') {
  174:           return $hash;  # drilled down to a scalar
  175:        }
  176:     }
  177: }
  178: 
  179: sub Rarray {
  180:     my $hash = shift;  # pointer to tied hash
  181:     my $i;
  182:     if (ref($hash) ne 'HASH') {
  183:        return 'Argument to cas_hashref_array is not a hash!';
  184:     }
  185:     while ($i = shift) {
  186:        if (exists($hash->{$i})) {
  187:           $hash = $hash->{$i};
  188:        } else {
  189:           return undef;
  190:        }
  191:        if (ref($hash) eq 'REF') {
  192:           $hash = $$hash;  # dereference one layer
  193:        }
  194:     }
  195:     my @returnarray=();
  196:     foreach my $key (keys(%{$hash})) {
  197:         $returnarray[$key-1]=$$hash{$key};
  198:     }
  199:     return @returnarray;
  200: }
  201: 
  202: sub connect {
  203:    return IO::Socket::UNIX->new(Peer    => $Apache::lonnet::perlvar{'lonSockDir'}.'/rsock',
  204: 				Type    => SOCK_STREAM,
  205: 				Timeout => 10);
  206: }
  207: 
  208: sub disconnect {
  209:     my ($socket)=@_;
  210:     if ($socket) { close($socket); }
  211: }
  212: 
  213: sub rreply {
  214:     my ($socket,$cmd)=@_;
  215:     if ($socket) {
  216: 	print $socket &escape($cmd)."\n";
  217:         my $reply=<$socket>;
  218:         chomp($reply);
  219:         if ($reply=~/^Incorrect/) { $reply='Error: '.$reply; }
  220:         return &unescape($reply);
  221:     } else {
  222:         return 'Error: no connection.';
  223:     }
  224: }
  225: 
  226: sub blacklisted {
  227:     my ($cmd)=@_;
  228:     foreach my $forbidden (
  229:         'read','write','scan','save','socket','connections',
  230:         'open','close',
  231:         'plot','X11','windows','quartz',
  232:         'postscript','pdf','png','jpeg',
  233:         'dev\.list','dev\.next','dev\.prev','dev\.set',
  234:         'dev\.off','dev\.copy','dev\.print','graphics',
  235:         'library','package','source','sink','objects',
  236:         'Sys\.','unlink','file\.','on\.exit','error',
  237:         'q\(\)'
  238:      ) {
  239: 	if ($cmd=~/$forbidden/s) { return 1; }
  240:     } 
  241:     return 0;
  242: }
  243: 
  244: sub r_allowed_libraries {
  245:    return ('alr3','boot','car','class','cluster','datasets','Hmisc','KernSmooth','leaps','lmtest',
  246:            'MASS','methods','mgcv','nlme','nnet','qAnalyst','quadprog','rpart','SuppDists','spatial',
  247:            'splines','stats','stats4','survival','tseries','zoo');
  248: }
  249: 
  250: sub r_is_allowed_library {
  251:     my ($library)=@_;
  252:     foreach my $allowed_library (&r_allowed_libraries()) {
  253:        if ($library eq $allowed_library) { return 1; }
  254:     }
  255:     return 0;
  256: }
  257: 
  258: sub runscript {
  259:     my ($socket,$fullscript,$libraries)=@_;
  260:     if (&blacklisted($fullscript)) { return 'Error: blacklisted'; }
  261:     my $reply='';
  262:     $fullscript=~s/[\n\r\l]//gs;
  263:     if ($libraries) {
  264:        foreach my $library (split(/\s*\,\s*/,$libraries)) {
  265:           unless ($library=~/\w/) { next; }
  266:           if (&r_is_allowed_library($library)) {
  267:               $reply=&rreply($socket,'library('.$library.');');
  268:               if ($reply=~/^Error\:/) { return $reply; }
  269:           } else { 
  270:              return 'Error: blacklisted'; 
  271:           }
  272:        }
  273:     }
  274:     $fullscript=~s/\;+\s*$//s;
  275:     if ($fullscript=~/\w/) { $reply=&rreply($socket,$fullscript.';'); }
  276:     if ($reply=~/^Error\:/) { return $reply; }
  277:     $reply=~s/^\s*//gs;
  278:     $reply=~s/\s*$//gs;
  279:     &Apache::lonxml::debug("r $fullscript \n reply $reply");
  280:     return $reply;
  281: }
  282: 
  283: sub runserializedscript {
  284:     my ($socket,$fullscript,$libraries)=@_;
  285:     if (&blacklisted($fullscript)) { return 'Error: blacklisted'; }
  286:     my $reply;
  287:     $fullscript=~s/[\n\r\l]//gs;
  288:     if ($libraries) {
  289:        foreach my $library (split(/\s*\,\s*/,$libraries)) {
  290:           unless ($library=~/\w/) { next; }
  291:           if (&r_is_allowed_library($library)) {
  292:               $reply=&rreply($socket,'library('.$library.');');
  293:               if ($reply=~/^Error\:/) { return($reply,$reply); }
  294:           } else {
  295:              return 'Error: blacklisted';
  296:           }
  297:        }
  298:     }
  299:     $fullscript=~s/\;+\s*$//s;
  300:     my $lastline='';
  301:     my $firstpart='';
  302:     if ($fullscript=~/\;/) {
  303:        ($firstpart,$lastline)=($fullscript=~/^(.*\;)([^\;]+)$/);
  304:     } else {
  305:        $lastline=$fullscript;
  306:     }
  307:     if ($firstpart) {
  308:         $firstpart=~s/\;+\s*$//s;
  309:         $reply=&rreply($socket,$firstpart.';');
  310:         if ($reply=~/^Error\:/) { return($reply,$reply); }
  311:     }
  312: # The last line needs to be serialized
  313:     $reply=&Rreturn(&rreply($socket,"phpSerialize($lastline);"));
  314:     return($reply,&Dumper($reply));
  315: }
  316: 
  317: sub r_cas_formula_fix {
  318:    my ($expression)=@_;
  319:    return &Apache::response::implicit_multiplication($expression);
  320: }
  321: 
  322: sub r_run {
  323:     my ($script,$submission,$argument,$libraries) = @_;
  324:     my $socket=&connect();
  325:     my @submissionarray=split(/\s*\,\s*/,$submission);
  326:     for (my $i=0;$i<=$#submissionarray;$i++) {
  327:         my $n=$i+1;
  328:         my $fixedsubmission=&r_cas_formula_fix($submissionarray[$i]);
  329:         $script=~s/RESPONSE\[$n\]/$fixedsubmission/gs;
  330:     }
  331:     my @argumentarray=@{$argument};
  332:     for (my $i=0;$i<=$#argumentarray;$i++) {
  333:         my $n=$i+1;
  334:         my $fixedargument=&r_cas_formula_fix($argumentarray[$i]);
  335:         $script=~s/LONCAPALIST\[$n\]/$fixedargument/gs;
  336:     }
  337:     my $reply=&runscript($socket,$script,$libraries);
  338:     &disconnect($socket);
  339:     if ($reply=~/^\s*true\s*$/i) { return 'EXACT_ANS'; }
  340:     if ($reply=~/^\s*false\s*$/i) { return 'INCORRECT'; } 
  341:     return 'BAD_FORMULA';
  342: }
  343: 
  344: sub r_eval {
  345:     my ($script,$libraries,$hashflag) = @_;
  346:     my $socket=&connect();
  347:     my $reply;
  348:     my $dump='';
  349:     if ($hashflag) {
  350:        ($reply,$dump)=&runserializedscript($socket,$script,$libraries);
  351:     } else {
  352:        $reply=&runscript($socket,$script,$libraries);
  353:     }
  354:     &disconnect($socket);
  355:     return ($reply,$dump);
  356: }
  357: 
  358: 
  359: sub compareterms {
  360:     my ($socket,$terma,$termb)=@_;
  361:     my $difference=$terma.'-('.$termb.')';
  362:     if (&blacklisted($difference)) { return 'Error: blacklisted'; }
  363:     my $reply=&rreply($socket,$difference.';');
  364:     if ($reply=~/^\s*0\s*$/) { return 'true'; }
  365:     if ($reply=~/^Error\:/) { return $reply; }
  366:     return 'false';
  367: }
  368: 
  369: sub r_check {
  370:     my ($response,$answer,$reterror) = @_;
  371:     my $socket=&connect();
  372:     my $reply=&compareterms($socket,$response,$answer);
  373:     &disconnect($socket);
  374:     # integer to string mappings come from capaParser.h
  375:     # 1 maps to 'EXACT_ANS'
  376:     if ($reply eq 'true') { return 1; }
  377:     # 11 maps to 'BAD_FORMULA'
  378:     if ($reply=~/^Error\:/) { return 11; }
  379:     # 7 maps to 'INCORRECT'
  380:     return 7;
  381: }
  382:  
  383: 1;
  384: __END__;

FreeBSD-CVSweb <freebsd-cvsweb@FreeBSD.org>