--- rat/lonpageflip.pm 2000/10/03 20:33:06 1.1 +++ rat/lonpageflip.pm 2022/10/19 00:03:12 1.112 @@ -2,354 +2,943 @@ # # Page flip handler # -# (Page Handler +# $Id: lonpageflip.pm,v 1.112 2022/10/19 00:03:12 raeburn Exp $ # -# (TeX Content Handler +# Copyright Michigan State University Board of Trustees # -# 05/29/00,05/30 Gerd Kortemeyer) -# 08/30,08/31,09/06,09/14,09/15,09/16,09/19,09/20,09/21,09/23, -# 10/02 Gerd Kortemeyer) +# This file is part of the LearningOnline Network with CAPA (LON-CAPA). +# +# LON-CAPA is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# LON-CAPA is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with LON-CAPA; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA +# +# /home/httpd/html/adm/gpl.txt +# +# http://www.lon-capa.org/ # -# 10/03 Gerd Kortemeyer + + package Apache::lonpageflip; use strict; -use Apache::Constants qw(:common :http); -use Apache::lonnet(); +use LONCAPA; +use Apache::Constants qw(:common :http REDIRECT); +use Apache::lonnet; +use Apache::loncommon(); +use Apache::lonnavmaps(); +use Apache::lonuserstate; +use Apache::lonlocal; use HTML::TokeParser; use GDBM_File; -# -------------------------------------------------------------- Module Globals +# ========================================================== Module Global Hash + my %hash; -my @rows; -# ------------------------------------------------------------------ Euclid gcd - -sub euclid { - my ($e,$f)=@_; - my $a; my $b; my $r; - if ($e>$f) { $b=$e; $r=$f; } else { $r=$e; $b=$f; } - while ($r!=0) { - $a=$b; $b=$r; - $r=$a%$b; - } - return $b; -} - -# ------------------------------------------------------------ Build page table - -sub tracetable { - my ($sofar,$rid,$beenhere)=@_; - my $further=$sofar; - unless ($beenhere=~/\&$rid\&/) { - $beenhere.=$rid.'&'; - - if (defined($hash{'is_map_'.$rid})) { - if ((defined($hash{'map_start_'.$hash{'src_'.$rid}})) && - (defined($hash{'map_finish_'.$hash{'src_'.$rid}}))) { - my $frid=$hash{'map_finish_'.$hash{'src_'.$rid}}; - $sofar= - &tracetable($sofar,$hash{'map_start_'.$hash{'src_'.$rid}}, - '&'.$frid.'&'); - $sofar++; - if ($hash{'src_'.$frid}) { - my $brepriv=&Apache::lonnet::allowed('bre',$hash{'src_'.$frid}); - if (($brepriv eq '2') || ($brepriv eq 'F')) { - if (defined($rows[$sofar])) { - $rows[$sofar].='&'.$frid; - } else { - $rows[$sofar]=$frid; - } - } - } - } - } else { - $sofar++; - if ($hash{'src_'.$rid}) { - my $brepriv=&Apache::lonnet::allowed('bre',$hash{'src_'.$rid}); - if (($brepriv eq '2') || ($brepriv eq 'F')) { - if (defined($rows[$sofar])) { - $rows[$sofar].='&'.$rid; - } else { - $rows[$sofar]=$rid; - } - } - } - } - - if (defined($hash{'to_'.$rid})) { - map { - my $now=&tracetable($sofar,$hash{'goesto_'.$_},$beenhere); - if ($now>$further) { $further=$now; } - } split(/\,/,$hash{'to_'.$rid}); - } +sub cleanup { + if (tied(%hash)){ + &Apache::lonnet::logthis('Cleanup pageflip: hash'); + unless (untie(%hash)) { + &Apache::lonnet::logthis('Failed cleanup pageflip: hash'); + } } - return $further; + return OK; } -# ================================================================ Main Handler - -sub handler { - my $r=shift; +sub addrid { + my ($current,$new,$condid)=@_; + unless ($condid) { $condid=0; } + + if ($current) { + $current.=','.$new; + } else { + $current=''.$new; + } -# ------------------------------------------- Set document type for header only - -# if ($r->headers_only) { - $r->content_type('text/html'); - $r->send_http_header; -# return OK; -# } + return $current; +} +sub fullmove { + my ($rid,$mapurl,$direction)=@_; + if (tie(%hash,'GDBM_File',$env{'request.course.fn'}.'.db', + &GDBM_READER(),0640)) { + ($rid,$mapurl)=&move($rid,$mapurl,$direction); + untie(%hash); + } + return($rid,$mapurl); +} +sub hash_src { + my ($id)=@_; + my ($mapid,$resid)=split(/\./,$id); + my $symb=&Apache::lonnet::encode_symb($hash{'map_id_'.$mapid}, + $resid,$hash{'src_'.$id}); + my $anchor; + if ($hash{'ext_'.$id} eq 'true:') { + if ($hash{'src_'.$id} =~ /(\#.+)$/) { + $anchor = $1; + } + } + if ($hash{'encrypted_'.$id}) { + return (&Apache::lonenc::encrypted($hash{'src_'.$id}), + &Apache::lonenc::encrypted($symb), + $hash{'encrypted_'.$id},$anchor); + } + return ($hash{'src_'.$id},$symb,$hash{'encrypted_'.$id},$anchor); +} +sub move { + my ($next,$endupmap,$direction,$firstres) = @_; + my $safecount=0; + my $allowed=0; + my $deeplinkonly=0; + my $deeplinkchecked; + my $deeplink_login_pc; + my $prev=$next; + my ($prevmapid)=split(/\./,$next); + do { + ($next,$endupmap)=&get_next_possible_move($next,$endupmap,$direction); + + my $url = $hash{'src_'.$next}; + my ($mapid,$resid)=split(/\./,$next); + my $symb = &Apache::lonnet::encode_symb($hash{'map_id_'.$mapid}, + $resid,$url); + if ($url eq '' || $symb eq '') { + $allowed = 0; + } else { + my $nodeeplinkcheck = 0; + if ($hash{'is_map_'.$next}) { + $nodeeplinkcheck = 1; + } + my $priv = &Apache::lonnet::allowed('bre',$url,$symb,'','','','',$nodeeplinkcheck); + $allowed = (($priv eq 'F') || ($priv eq '2') || ($priv eq 'A')); + } + $deeplinkonly = 0; + if ($hash{'deeplinkonly_'.$next}) { + my ($value,$level) = map { &unescape($_); } split(/:/,$hash{'deeplinkonly_'.$next}); + my ($state,$others,$listed,$scope,$protect) = split(/,/,$value); + unless (($state eq 'both') || ($hash{'is_map_'.$next})) { + if ($level eq 'resource') { + $deeplinkonly = 1; + } elsif ($level eq 'map') { + if ($scope eq 'rec') { + unless ($mapid == $prevmapid) { + unless ($deeplinkchecked) { + $deeplink_login_pc = &get_deeplink_login_pc(); + $deeplinkchecked = 1; + } + if ($deeplink_login_pc) { + my $poss_map_pc; + if ($hash{'is_map_'.$next}) { + $poss_map_pc = $hash{'map_pc_'.$url}; + } else { + $poss_map_pc = $hash{'map_pc_'.$hash{'map_id_'.$mapid}}; + } + unless ($deeplink_login_pc == $poss_map_pc) { + unless (grep(/^$deeplink_login_pc$/,split(/,/,$hash{'map_hierarchy_'.$poss_map_pc}))) { + $deeplinkonly = 1; + } + } + } else { + $deeplinkonly = 1; + } + } + } elsif ($mapid != $prevmapid) { + $deeplinkonly = 1; + } + } + } + } elsif (($hash{'deeplinkonly_'.$prev}) && (!$firstres)) { + my ($value,$level) = map { &unescape($_); } split(/:/,$hash{'deeplinkonly_'.$prev}); + my ($state,$others,$listed,$scope,$protect) = split(/,/,$value); + unless (($state eq 'both') || ($hash{'is_map_'.$prev})) { + if ($level eq 'resource') { + $deeplinkonly = 1; + } elsif ($level eq 'map') { + if ($scope eq 'rec') { + unless ($mapid == $prevmapid) { + unless ($deeplinkchecked) { + $deeplink_login_pc = &get_deeplink_login_pc(); + $deeplinkchecked = 1; + } + if ($deeplink_login_pc) { + my $poss_map_pc; + if ($hash{'is_map_'.$prev}) { + $poss_map_pc = $hash{'map_pc_'.$url}; + } else { + $poss_map_pc = $hash{'map_pc_'.$hash{'map_id_'.$mapid}}; + } + unless ($deeplink_login_pc == $poss_map_pc) { + unless (grep(/^$deeplink_login_pc$/,split(/,/,$hash{'map_hierarchy_'.$poss_map_pc}))) { + $deeplinkonly = 1; + } + } + } + } + } else { + if ($mapid != $prevmapid) { + $deeplinkonly = 1; + } + } + } + } + } + $safecount++; + } while ( ($next) + && ($next!~/\,/) + && ( + (!$hash{'src_'.$next}) + || ( + (!$env{'request.role.adv'}) + && (($hash{'randomout_'.$next}) + || ($deeplinkonly) + || ($hash{'deeplinkout_'.$next})) + ) + || (!$allowed) + ) + && ($safecount<10000)); + return ($next,$endupmap); +} - $r->print('
'.$ENV{'form.postdata'}.''); - return OK; +sub get_next_possible_move { + my ($rid,$mapurl,$direction)=@_; + my $startoutrid=$rid; + + my $next=''; + + my $mincond=1; + my $posnext=''; + if ($direction eq 'forward') { +# --------------------------------------------------------------------- Forward + while ($hash{'type_'.$rid} eq 'finish') { + $rid=$hash{'ids_'.$hash{'map_id_'.(split(/\./,$rid))[0]}}; + } + foreach my $id (split(/\,/,$hash{'to_'.$rid})) { + my $condition= $hash{'conditions_'.$hash{'goesto_'.$id}}; + my $rescond = &Apache::lonnet::docondval($condition); + my $linkcond = &Apache::lonnet::directcondval($hash{'condid_'.$hash{'undercond_'.$id}}); + my $thiscond = ($rescond<$linkcond)?$rescond:$linkcond; + if ($thiscond>=$mincond) { + if ($posnext) { + $posnext.=','.$id.':'.$thiscond; + } else { + $posnext=$id.':'.$thiscond; + } + if ($thiscond>$mincond) { $mincond=$thiscond; } + } + } + foreach my $id (split(/\,/,$posnext)) { + my ($linkid,$condval)=split(/\:/,$id); + if ($condval>=$mincond) { + $next=&addrid($next,$hash{'goesto_'.$linkid}, + $hash{'condid_'.$hash{'undercond_'.$linkid}}); + } + } + if ($hash{'is_map_'.$next}) { +# This jumps to the beginning of a new map (going down level) + if ( + $hash{'map_type_'.$hash{'map_pc_'.$hash{'src_'.$next}}} eq 'sequence') { + $mapurl=$hash{'src_'.$next}; + $next=$hash{'map_start_'.$hash{'src_'.$next}}; + } elsif ( +# This jumps back up from an empty sequence, to a page up one level + $hash{'map_type_'.$hash{'map_pc_'.$hash{'src_'.$next}}} eq 'page') { + $mapurl=$hash{'map_id_'.(split(/\./,$next))[0]}; + } + } elsif + ((split(/\./,$startoutrid))[0]!=(split(/\./,$next))[0]) { +# This comes up from a map (coming up one level); + $mapurl=$hash{'map_id_'.(split(/\./,$next))[0]}; + } + } elsif ($direction eq 'back') { +# ------------------------------------------------------------------- Backwards + while ($hash{'type_'.$rid} eq 'start') { + $rid=$hash{'ids_'.$hash{'map_id_'.(split(/\./,$rid))[0]}}; + } + foreach my $id (split(/\,/,$hash{'from_'.$rid})) { + my $condition= $hash{'conditions_'.$hash{'comesfrom_'.$id}}; + my $rescond = &Apache::lonnet::docondval($condition); + my $linkcond = &Apache::lonnet::directcondval($hash{'condid_'.$hash{'undercond_'.$id}}); + my $thiscond = ($rescond<$linkcond)?$rescond:$linkcond; + if ($thiscond>=$mincond) { + if ($posnext) { + $posnext.=','.$id.':'.$thiscond; + } else { + $posnext=$id.':'.$thiscond; + } + if ($thiscond>$mincond) { $mincond=$thiscond; } + } + } + foreach my $id (split(/\,/,$posnext)) { + my ($linkid,$condval)=split(/\:/,$id); + if ($condval>=$mincond) { + $next=&addrid($next,$hash{'comesfrom_'.$linkid}, + $hash{'condid_'.$hash{'undercond_'.$linkid}}); + } + } + if ($hash{'is_map_'.$next}) { +# This jumps to the end of a new map (going down one level) + if ( + $hash{'map_type_'.$hash{'map_pc_'.$hash{'src_'.$next}}} eq 'sequence') { + $mapurl=$hash{'src_'.$next}; + $next=$hash{'map_finish_'.$hash{'src_'.$next}}; + } elsif ( + $hash{'map_type_'.$hash{'map_pc_'.$hash{'src_'.$next}}} eq 'page') { +# This jumps back up from an empty sequence, to a page up one level + $mapurl=$hash{'map_id_'.(split(/\./,$next))[0]}; + } + } elsif + ((split(/\./,$startoutrid))[0]!=(split(/\./,$next))[0]) { +# This comes back up from a map (going up one level); + $mapurl=$hash{'map_id_'.(split(/\./,$next))[0]}; + } + } + return ($next,$mapurl); +} +sub first_accessible_resource { + my $furl; + if (tie(%hash,'GDBM_File',$env{'request.course.fn'}.'.db', + &GDBM_READER(),0640)) { + $furl=$hash{'first_url'}; + my (%args,$url,$argstr); + if ($furl =~ m{^/enc/}) { + ($url,$argstr) = split(/\?/,&Apache::lonenc::unencrypted($furl)); + } else { + ($url,$argstr) = split(/\?/,$furl); + } + foreach my $pair (split(/\&/,$argstr)) { + my ($name,$value) = split(/=/,$pair); + $args{&unescape($name)} = &unescape($value); + } + my $priv = &Apache::lonnet::allowed('bre',$url,$args{'symb'}); + my $allowed = (($priv eq 'F') || ($priv eq '2') || ($priv eq 'A')); + if (!$allowed) { +# Wow, we cannot see this ... move forward to the next one that we can see + my ($newrid,$newmap)=&move($hash{'first_rid'},$hash{'first_mapurl'},'forward',1); +# Build the new URL + if ($newrid eq '') { + $furl = '/adm/navmaps'; + } else { + my ($newmapid,$newresid)=split(/\./,$newrid); + my $symb=&Apache::lonnet::encode_symb($newmap,$newresid,$hash{'src_'.$newrid}); + $furl=&add_get_param($hash{'src_'.$newrid},{ 'symb' => $symb }); + if ($hash{'encrypted_'.$newrid}) { + $furl=&Apache::lonenc::encrypted($furl); + } + } + } + untie(%hash); + return $furl; + } else { + return '/adm/navmaps'; + } +} +sub first_answerable_ressymb { + my $navmap = Apache::lonnavmaps::navmap->new; + return unless (ref($navmap)); + my $iterator = $navmap->getIterator(undef,undef,undef,1); + return unless (ref($iterator)); + my ($curRes,$result); + while ($curRes = $iterator->next()) { + if (ref($curRes) && $curRes->is_problem()) { + foreach my $part (@{$curRes->parts()}) { + if ($curRes->tries($part) < $curRes->maxtries($part)) { + $result = $curRes->link().'?symb='.$curRes->shown_symb(); + last; + } + } + } + } + if ($result) { + return $result; + } else { + return &first_accessible_resource(); + } +} - my $requrl=$r->uri; -# ----------------------------------------------------------------- Tie db file - if ($ENV{'request.course.fn'}) { - my $fn=$ENV{'request.course.fn'}; - if (-e "$fn.db") { - if (tie(%hash,'GDBM_File',"$fn.db",&GDBM_READER,0640)) { -# ------------------------------------------------------------------- Hash tied - my $firstres=$hash{'map_start_'.$requrl}; - my $lastres=$hash{'map_finish_'.$requrl}; - if (($firstres) && ($lastres)) { -# ----------------------------------------------------------------- Render page +sub check_http_req { + my ($srcref,$hostname) = @_; + return unless (ref($srcref) eq 'SCALAR'); + my $usehttp; + if ($env{'request.course.id'}) { + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + if (($$srcref =~ m{^\Q/public/$cdom/$cnum/syllabus\E($|\?)}) && + ($ENV{'SERVER_PORT'} == 443) && + ($env{'course.'.$env{'request.course.id'}.'.externalsyllabus'} =~ m{^http://})) { + unless ((&Apache::lonnet::uses_sts()) || + (&Apache::lonnet::waf_allssl($hostname))) { + $$srcref .= (($$srcref =~/\?/)? '&':'?') . 'usehttp=1'; + $usehttp = 1; + } + } elsif (($$srcref =~ m{^\Q/adm/wrapper/ext/\E(?!https:)}) && + ($ENV{'SERVER_PORT'} == 443)) { + unless ((&Apache::lonnet::uses_sts()) || + (&Apache::lonnet::waf_allssl($hostname))) { + my ($url,$anchor) = ($$srcref =~ /^([^\#]+)(?:|(\#[^\#]+))$/); + $$srcref = $url . (($$srcref =~/\?/)? '&':'?') . 'usehttp=1' .$anchor; + $usehttp = 1; + } + } + } + return $usehttp; +} - @rows=(); +sub reinited_js { + my ($url,$cid,$timeout) = @_; + if (!$timeout) { + $timeout = 0; + } + return <<"END"; + +END +} - &tracetable(0,$firstres,'&'.$lastres.'&'); - if ($hash{'src_'.$lastres}) { - my $brepriv= - &Apache::lonnet::allowed('bre',$hash{'src_'.$lastres}); - if (($brepriv eq '2') || ($brepriv eq 'F')) { - $rows[$#rows+1]=''.$lastres; - } - } +sub get_deeplink_login_pc { + my $deeplink_login_pc; + if (($env{'request.deeplink.login'}) && ($env{'request.course.id'})) { + my $cdom = $env{'course.'.$env{'request.course.id'}.'.domain'}; + my $cnum = $env{'course.'.$env{'request.course.id'}.'.num'}; + my $deeplink_symb = &Apache::loncommon::deeplink_login_symb($cnum,$cdom); + if ($deeplink_symb) { + my $loginmap; + if ($deeplink_symb =~ /\.(page|sequence)$/) { + $loginmap = &Apache::lonnet::clutter((&Apache::lonnet::decode_symb($deeplink_symb))[2]); + } else { + $loginmap = &Apache::lonnet::clutter((&Apache::lonnet::decode_symb($deeplink_symb))[0]); + } + $deeplink_login_pc = $hash{'map_pc_'.$loginmap}; + } + } + return $deeplink_login_pc; +} -# ------------------------------------------------------------ Add to symb list +# ================================================================ Main Handler - my $i; - my %symbhash=(); - for ($i=0;$i<=$#rows;$i++) { - if ($rows[$i]) { - my @colcont=split(/\&/,$rows[$i]); - map { - $symbhash{$hash{'src_'.$_}}=''; - } @colcont; - } - } - &Apache::lonnet::symblist($requrl,%symbhash); +sub handler { + my $r=shift; -# ------------------------------------------------------------------ Page parms +# ------------------------------------------- Set document type for header only - my $j; - my $lcm=1; - my $contents=0; - my $nforms=0; - - my %ssibody=(); - my %ssibgcolor=(); - my %ssitext=(); - my %ssilink=(); - my %ssivlink=(); - my %ssialink=(); - my %cellemb=(); - - my $allscript=''; - my $allmeta=''; - - my $isxml=0; - my $xmlheader=''; - my $xmlbody=''; - -# --------------------------------------------- Get SSI output, post parameters - - for ($i=0;$i<=$#rows;$i++) { - if ($rows[$i]) { - $contents++; - my @colcont=split(/\&/,$rows[$i]); - $lcm*=($#colcont+1)/euclid($lcm,($#colcont+1)); - map { - my $src=$hash{'src_'.$_}; - $src=~/\.(\w+)$/; - $cellemb{$_}=Apache::lonnet::fileembstyle($1); - if ($cellemb{$_} eq 'ssi') { -# --------------------------------------------------------- This is an SSI cell - my $prefix=$_.'_'; - my %posthash=('request.prefix' => $prefix); - if (($ENV{'form.'.$prefix.'submit'}) - || ($ENV{'form.all_submit'})) { - map { - if ($_=~/^form.$prefix/) { - my $name=$_; - $name=~s/^form.$prefix//; - $posthash{$name}=$ENV{$_}; - } - } keys %ENV; - } - my $output=Apache::lonnet::ssi($src,%posthash); - my $parser=HTML::TokeParser->new(\$output); - my $token; - my $bodydef=0; - my $thisxml=0; - if ($output=~/\?xml/) { - $isxml=1; - $thisxml=1; - $output=~ - /((?:\<(?:\?xml|\!DOC|html)[^\>]*(?:\>|\>\]\>)\s*)+)\]*\>/si; - $xmlheader=$1; - } - while (($bodydef==0) && - ($token=$parser->get_token)) { - if ($token->[1] eq 'body') { - $bodydef=1; - $ssibgcolor{$_}=$token->[2]->{'bgcolor'}; - $ssitext{$_}=$token->[2]->{'text'}; - $ssilink{$_}=$token->[2]->{'link'}; - $ssivlink{$_}=$token->[2]->{'vlink'}; - $ssialink{$_}=$token->[2]->{'alink'}; - if ($thisxml) { - $xmlbody=$token->[4]; - } - } - if ($token->[1] eq 'meta') { - $allmeta.="\n".$token->[4].''; - } - if ($token->[1] eq 'script') { - $allscript.="\n\n" - .$parser->get_text('/script'); - } - } - if ($output=~/\]*\>(.*)/si) { - $output=$1; - } - $output=~s/\<\/body\>.*//si; - if ($output=~/\'); + &Apache::lonhtmlcommon::actionbox( + [''.$lt{'back'}.'', + ''.$lt{'nav'}.'' + ])); } - $r->print(''); -# -------------------------------------------------------------------- End page - } -# ------------------------------------------------------------- End render page - } else { - $r->content_type('text/html'); - $r->send_http_header; - $r->print('Page undefined.'); - } -# ------------------------------------------------------------------ Untie hash - unless (untie(%hash)) { - &Apache::lonnet::logthis("WARNING: ". - "Could not untie coursemap $fn (browse)."); - } -# -------------------------------------------------------------------- All done - return OK; -# ----------------------------------------------- Errors, hash could no be tied + + } + $r->print(&Apache::loncommon::end_page()); + + return OK; + } + } else { +# ------------------------------------------------- Problem, could not tie hash + if ($reinitcheck eq 'update') { + &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); + $r->print(&Apache::loncommon::end_page()); + } + $env{'user.error.msg'}="/adm/flip:bre:0:1:Course Data Missing"; + return HTTP_NOT_ACCEPTABLE; } - } - } - $ENV{'user.error.msg'}="$requrl:bre:0:0:Course not initialized"; - return HTTP_NOT_ACCEPTABLE; + } else { +# ---------------------------------------- No, could not determine where we are + my $newloc = '/adm/ambiguous'; + if ($reinitcheck eq 'update') { + &Apache::lonhtmlcommon::Close_PrgWin($r,\%prog_state); + $r->print(&reinited_js($newloc,$env{'request.course.id'},100)); + $r->print(&Apache::loncommon::end_page()); + } else { + $r->internal_redirect($newloc); + } + return OK; + } + } else { +# -------------------------- Class was not initialized or page fliped strangely + $env{'user.error.msg'}="/adm/flip:bre:0:0:Choose Course"; + return HTTP_NOT_ACCEPTABLE; + } } 1; __END__ +=pod + +=head1 NAME + +Apache::lonpageflip + +=head1 SYNOPSIS + +Deals with forward, backward, and other page flips. + +This is part of the LearningOnline Network with CAPA project +described at http://www.lon-capa.org. + +=head1 OVERVIEW + +(empty) + +=head1 SUBROUTINES + +=over cleanup() + +=item addrid() + +=item fullmove() + +=item hash_src() + +=item move() + +=item get_next_possible_move() + +=item first_accessible_resource() + +=item handler() + +=back + +=cut