File:  [LON-CAPA] / rat / map.pm
Revision 1.16: download - view: text, annotated - select for diffs
Sat Oct 22 17:24:55 2022 UTC (2 years ago) by raeburn
Branches: MAIN
CVS tags: version_2_12_X, version_2_11_5_msu, version_2_11_4_msu, HEAD
- Bug 6975
  Code to trigger updates of cached Supplemental Content information for a
  course moved from londocs.pm to lonnet.pm to facilitate reuse, and called
  after uploading file and/or extracting archive file using Course Editor.

# The LearningOnline Network with CAPA
# routines for modyfing .sequence and .page files
#
# $Id: map.pm,v 1.16 2022/10/22 17:24:55 raeburn Exp $
#
# Copyright Michigan State University Board of Trustees
#
# 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/
#

package LONCAPA::map;
use strict;
use HTML::TokeParser;
use HTML::Entities();
use Apache::lonnet;
use Apache::lonlocal;
use File::Copy;
use LONCAPA;

use vars qw(@order @resources @resparms @zombies);

# Mapread read maps into global arrays @links and @resources, determines status
# sets @order - pointer to resources in right order
# sets @resources - array with the resources with correct idx
#
sub mapread {
    my ($fn)= @_;

    my @links;

    @resources=('');
    @order=();
    @resparms=();
    @zombies=();

    my ($outtext,$errtext)=&loadmap($fn,'');
    if ($errtext) { return ($errtext,2); }

# -------------------------------------------------------------------- Read map
    foreach (split(/\<\&\>/,$outtext)) {
	my ($command,$number,$content)=split(/\<\:\>/,$_);
        if ($command eq 'objcont') {
	    my ($title,$src,$ext,$type)=split(/\:/,$content);
	    if ($ext eq 'cond') { next; }
	    if ($type ne 'zombie') {
		$resources[$number]=$content;
	    } else {
		$zombies[$number]=$content;
	    }
        }
        if ($command eq 'objlinks') {
            $links[$number]=$content;
        }
        if ($command eq 'objparms') {
	    if ($resparms[$number]) {
		$resparms[$number].='&&&'.$content;
	    } else {
		$resparms[$number]=$content;
	    }
        }
    }
# ------------------------------------------------------- Is this a linear map?
    my @starters;
    my @endings;

    foreach (@links) {
        if (defined($_)) {
	    my ($start,$end,$cond)=split(/\:/,$_);
            if ((defined($starters[$start])) || (defined($endings[$end]))) { 
		return
		    (&mt('Map has branchings. Use advanced editor.'),1);
            }
	    $starters[$start]=1;
	    $endings[$end]=1;
	    if ($cond) {
		return
		    (&mt('Map has conditions. Use advanced editor.'),1);
            }
	}
    }

    for (my $i=1; $i<=$#resources; $i++) {
        if (defined($resources[$i])) {
	    unless (($starters[$i]) || ($endings[$i])) {
                return
		    (&mt('Map has unconnected resources. Use advanced editor.'),1);
            }
        }
    }
# ---------------------------------------------- Did we just read an empty map?
    if ($#resources<1) {
        undef $resources[0];
	$resources[1]=':::start';
        $resources[2]=':::finish';
    }
# -------------------------------------------------- This is a linear map, sort

    my $startidx=0;
    my $endidx=0;
    for (my $i=0; $i<=$#resources; $i++) {
        if (defined($resources[$i])) {
            my ($title,$url,$ext,$type)=split(/\:/,$resources[$i]);
	    if ($type eq 'start') { $startidx=$i; }
            if ($type eq 'finish') { $endidx=$i; }
        }
    }
    my $k=0;
    my $currentidx=$startidx;
    $order[$k]=$currentidx;
    for (my $i=0; $i<=$#resources; $i++) {
        foreach (@links) {
	    my ($start,$end)=split(/\:/,$_);
            if ($start==$currentidx) {
		$currentidx=$end;
                $k++;
                $order[$k]=$currentidx;
                last;
            }
        }
        if ($currentidx==$endidx) { last; }
    }
    return $errtext;
}

# ---------------------------------------------- Read a map as well as possible
# Also used by the sequence handler
# Call lonsequence::attemptread to read from resource space
#
sub attemptread {
    my ($fn,$unsorted)=@_;

    my @links;
    my @theseres;

    my ($outtext,$errtext)=&loadmap($fn,'');
    if ($errtext) { return @theseres }

# -------------------------------------------------------------------- Read map
    my ($start,$finish);
    foreach (split(/\<\&\>/,$outtext)) {
	my ($command,$number,$content)=split(/\<\:\>/,$_);
        if ($command eq 'objcont') {
	    my ($title,$src,$ext,$type)=split(/\:/,$content);	    
	    if ($type ne 'zombie' && $ext ne 'cond') {
		$theseres[$number]=$content;
	    }
	    if ($type eq 'start') {
		$start = $number;
	    }
	    if ($type eq 'finish') {
		$finish = $number;
	    }
        }
        if ($command eq 'objlinks') {
            $links[$number]=$content;
        }
    }
    if ($unsorted) {
	return @theseres;
    }

# ---------------------------- attempt to flatten the map into a 'sorted' order

    my %path_length = ($start => 0);
    my @todo = @links;

    while (@todo) {
	my $link = shift(@todo);
	next if (!defined($link));
	my ($from,$to) = split(':',$link);
	if (!exists($path_length{$from})) {
	    # don't know how long it takes to get to this link,
	    # save away to retry
	    push(@todo,$link);
	    next;
	}
	# already have a length, keep it
	next if (exists($path_length{$to}));
	$path_length{$to}=$path_length{$from}+1;
    }
    # invert hash so we have the ids in depth order now
    my @by_depth;
    while (my ($key,$value) = each(%path_length)) {
	push(@{$by_depth[$value]},$key);
    }
    # reorder resources
    my @outres;
    foreach my $ids_at_depth (@by_depth) {
	foreach my $id (sort(@{$ids_at_depth})) {
	    # skip the finish resource
	    next if ($id == $finish);
	    push(@outres, $theseres[$id]);
	}
    }
    # make sure finish is last (in case there are cycles or bypass routes
    # finish can end up with a rather short possible path)
    push(@outres, $theseres[$finish]);
    return @outres;
}

# ------------------------------------- Revive zombie idx or get unused number

sub getresidx {
    my ($url,$residx)= @_;
    my $max=1+($#resources>$#zombies?$#resources:$#zombies);
    unless ($url) { return $max; }
    for (my $i=0; $i<=$#zombies; $i++) {
	my ($title,$src,$ext,$type)=split(/\:/,$zombies[$i]);
	if ($src eq $url) {
	    if ($residx) {
		if ($i == $residx) {
		    undef($zombies[$i]);
		    return $i;
		}
	    } else {
		undef($zombies[$i]);
		return $i;
	    }
	}
    }
    return $max;
}

# --------------------------------------------------------------- Make a zombie

sub makezombie {
    my $idx=shift;
    my ($name,$url,$ext)=split(/\:/,$resources[$idx]);
    my $now=time;
    $zombies[$idx]=$name.
	' [('.$now.','.$env{'user.name'}.','.$env{'user.domain'}.')]:'.
	$url.':'.$ext.':zombie';
}

# ----------------------------------------------------------- Paste into target
# modifies @order, @resources

sub pastetarget {
    my ($after,@which)=@_;
    my @insertorder=();
    foreach (@which) {
        if (defined($_)) {
	    my ($name,$url,$residx)=split(/\=/,$_);
            $name=&unescape($name);
            $url=&unescape($url);
            if ($url) {
		my $idx=&getresidx($url,$residx);
		$insertorder[$#insertorder+1]=$idx;
		my $ext='false';
		if ($url=~/^https?\:\/\//) { $ext='true'; }
		$url=~s/\:/\&colon;/g;
		$name=~s/\:/\&colon;/g;
		$resources[$idx]=$name.':'.$url.':'.$ext.':normal:res';
	    }
        }
    }
    my @oldorder=splice(@order,$after);
    @order=(@order,@insertorder,@oldorder);
}

# ------------------------------------------------ Get start and finish correct
# modifies @resources

sub startfinish {
# Remove all start and finish
    foreach (@order) {
	my ($name,$url,$ext)=split(/\:/,$resources[$_]);
        if ($url=~/https?\&colon\:\/\//) { $ext='true'; }
        $resources[$_]=$name.':'.$url.':'.$ext.':normal:res';
    }
# Garbage collection
    my $stillchange=1;
    while (($#order>1) && ($stillchange)) {
	$stillchange=0;
	for (my $i=0;$i<=$#order;$i++) {
	    my ($name,$url,$ext)=split(/\:/,$resources[$order[$i]]);
	    unless ($url) {
# Take out empty resource
		for (my $j=$i+1;$j<=$#order;$j++) {
		    $order[$j-1]=$order[$j];
		}
		$#order--;
		$stillchange=1;
		last;
	    }
	}
    }
# Make sure this has at least start and finish
    if ($#order==-1) {
        $resources[&getresidx()]='::false';
        $order[0]=$#resources;
    }
# Put in a start resource
    my ($name,$url,$ext)=split(/\:/,$resources[$order[0]]);
    $resources[$order[0]]=$name.':'.$url.':'.$ext.':start:res';
    if ($#order==0) {
	$resources[&getresidx()]='::false';
	$order[1]=$#resources;
    }
# Make the last one a finish resource
    ($name,$url,$ext)=split(/\:/,$resources[$order[$#order]]);
    $resources[$order[$#order]]=$name.':'.$url.':'.$ext.':finish:res';
}

# ------------------------------------------------------------------- Store map

sub storemap {
    my ($realfn,$useorig,$dotimeupdate) = @_;
    my $fn=$realfn;
# unless this is forced to work from the original file, use a temporary file
# instead
    unless ($useorig) {
	$fn=$realfn.'.tmp';
	unless (-e $fn) {
	    copy($realfn,$fn);
	}
    }
# store data either into tmp or real file
    &startfinish();
    my $output='graphdef<:>no';
    my $k=1;
    for (my $i=0; $i<=$#order; $i++) {
        if (defined($resources[$order[$i]])) {
	    $output.='<&>objcont<:>'.$order[$i].'<:>'.$resources[$order[$i]];
        }
	if (defined($resparms[$order[$i]])) {
	    foreach (split('&&&',$resparms[$order[$i]])) {
		if ($_) {
		    $output.='<&>objparms<:>'.$order[$i].'<:>'.$_;
		}
	    }
	}
        if (defined($order[$i+1])) {
	    if (defined($resources[$order[$i+1]])) {
		$output.='<&>objlinks<:>'.$k.'<:>'.
		    $order[$i].':'.$order[$i+1].':0';
		$k++;
            }
        }
    }
    for (my $i=0; $i<=$#zombies; $i++) {
        if (defined($zombies[$i])) {
	    $output.='<&>objcont<:>'.$i.'<:>'.$zombies[$i];
        }
    }
    $output=~s/http\&colon\;\/\///g;
    $env{'form.output'}=$output;
    return &loadmap($fn,&savemap($fn,'',$dotimeupdate));
}

# ------------------------------------------ Store and get parameters in global

sub storeparameter {
    my ($to,$name,$value,$ptype)=@_;
    my $newentry='';
    my $nametype='';
    foreach (split('&&&',$resparms[$to])) {
	my ($thistype,$thisname,$thisvalue)=split('___',$_);
	if ($thisname) {
	    unless ($thisname eq $name) {
		$newentry.=$_.'&&&';
	    } else {
		$nametype=$thistype;
	    }
	}
    }
    unless ($ptype) { $ptype=$nametype; }
    unless ($ptype) { $ptype='string'; }
    $newentry.=$ptype.'___'.$name.'___'.$value;
    $resparms[$to]=$newentry;
}

sub delparameter {
    my ($to,$name)=@_;
    my $newentry='';
    my $nametype='';
    foreach (split('&&&',$resparms[$to])) {
	my ($thistype,$thisname,$thisvalue)=split('___',$_);
	if ($thisname) {
	    unless ($thisname eq $name) {
		$newentry.=$_.'&&&';
	    }
	}
    }
    $resparms[$to]=$newentry;
}

sub getparameter {
    my ($to,$name)=@_;
    my $value=undef;
    my $ptype=undef;
    foreach (split('&&&',$resparms[$to])) {
	my ($thistype,$thisname,$thisvalue)=split('___',$_);
	if ($thisname eq $name) {
	    $value=$thisvalue;
	    $ptype=$thistype;
	}
    }
    return ($value,$ptype);
}

# ------------------------------------------------------------- From RAT to XML

sub qtescape {
    my $str=shift;
    $str=~s/\&colon;/\:/g;
    $str=~s/\&\#58\;/\:/g;
    $str=~s/\&\#39\;/\'/g;
    $str=~s/\&\#44\;/\,/g;
    $str=~s/\&\#34\;/\"/g;
    return $str;
}

# ------------------------------------------------------------- From XML to RAT

sub qtunescape {
    my $str=shift;
    $str=~s/\:/\&colon\;/g;
    $str=~s/\'/\&\#39\;/g;
    $str=~s/\,/\&\#44\;/g;
    $str=~s/\"/\&\#34\;/g;
    return $str;
}

# --------------------------------------------------------- Loads map from disk

sub loadmap {
    my ($fn,$errtext,$infotext)=@_;
    if ($errtext) { return('',$errtext); }
    my $outstr='';
    my @obj=();
    my @links=();
    my $instr='';
    if ($fn=~/^\/*uploaded\//) {
        $instr=&Apache::lonnet::getfile($fn);
    } elsif (-e $fn) {
        my @content=();
        {
	    open(my $fh,"<$fn");
            @content=<$fh>;
        }
        $instr=join('',@content);
    }
    if ($instr eq -2) {
        $errtext.='Map not loaded: An error occurred while trying to load the map.';
    } elsif ($instr eq '-1') {
	# Map doesn't exist 
    } elsif ($instr) {
        my $parser = HTML::TokeParser->new(\$instr);
        my $token;
        my $graphmode=0;

        $fn=~/\.(\w+)$/;
        $outstr="mode<:>$1";

        while ($token = $parser->get_token) {
	    if ($token->[0] eq 'S') {
                if ($token->[1] eq 'map') {
		    $graphmode=($token->[2]->{'mode'} eq 'rat/graphical');
                } elsif ($token->[1] eq 'resource') {
# -------------------------------------------------------------------- Resource
                    $outstr.='<&>objcont';
                    if (defined($token->[2]->{'id'})) {
			$outstr.='<:>'.$token->[2]->{'id'};
                        if ($obj[$token->[2]->{'id'}]==1) {
			    $errtext.='Error: multiple use of ID '.
				$token->[2]->{'id'}.'. ';
                        }
                        $obj[$token->[2]->{'id'}]=1; 
                    } else {
                        my $i=1;
                        while (($i<=$#obj) && ($obj[$i]!=0)) { $i++; }
                        $outstr.='<:>'.$i;
                        $obj[$i]=1;
                    }
                    $outstr.='<:>';
                    $outstr.=qtunescape($token->[2]->{'title'}).":";
                    $outstr.=qtunescape($token->[2]->{'src'}).":";
                    if ($token->[2]->{'external'} eq 'true') {
                        $outstr.='true:';
                    } else {
                        $outstr.='false:';
                    }
                    if (defined($token->[2]->{'type'})) {
			$outstr.=$token->[2]->{'type'}.':';
                    }  else {
                        $outstr.='normal:';
                    }
		    if ($token->[2]->{'type'} ne 'zombie') {
			$outstr.='res';
		    } else {
                        $outstr.='zombie';
		    }
                } elsif ($token->[1] eq 'condition') {
# ------------------------------------------------------------------- Condition
                    $outstr.='<&>objcont';
                    if (defined($token->[2]->{'id'})) {
			$outstr.='<:>'.$token->[2]->{'id'};
                        if ($obj[$token->[2]->{'id'}]==1) {
			    $errtext.='Error: multiple use of ID '.
				$token->[2]->{'id'}.'. ';
                        }
                        $obj[$token->[2]->{'id'}]=1; 
                    } else {
                        my $i=1;
                        while (($i<=$#obj) && ($obj[$i]!=0)) { $i++; }
                        $outstr.='<:>'.$i;
                        $obj[$i]=1;
                    }
                    $outstr.='<:>';
                    $outstr.=qtunescape($token->[2]->{'value'}).':';
                    if (defined($token->[2]->{'type'})) {
			$outstr.=$token->[2]->{'type'}.':';
                    } else {
                        $outstr.='normal:';
                    }
                    $outstr.='cond';
                } elsif ($token->[1] eq 'link') {
# ----------------------------------------------------------------------- Links
                    $outstr.='<&>objlinks';
		    
		    if (defined($token->[2]->{'index'})) {
			if ($links[$token->[2]->{'index'}]) {
			    $errtext.='Error: multiple use of link index '.
				$token->[2]->{'index'}.'. ';
			}
			$outstr.='<:>'.$token->[2]->{'index'};
			$links[$token->[2]->{'index'}]=1;
		    } else {
			my $i=1;
			while (($i<=$#links) && ($links[$i]==1)) { $i++; }
			$outstr.='<:>'.$i;
			$links[$i]=1;
		    }
		    
                    $outstr.='<:>'.$token->[2]->{'from'}.
			':'.$token->[2]->{'to'};
                    if (defined($token->[2]->{'condition'})) {
			$outstr.=':'.$token->[2]->{'condition'};
                    } else {
 			$outstr.=':0';
                    }
# ------------------------------------------------------------------- Parameter
                } elsif ($token->[1] eq 'param') {
                    $outstr.='<&>objparms<:>'.$token->[2]->{'to'}.'<:>'.
			$token->[2]->{'type'}.'___'.$token->[2]->{'name'}.
			'___'.$token->[2]->{'value'};
                } elsif ($graphmode) {
# --------------------------------------------- All other tags (graphical only)
                    $outstr.='<&>'.$token->[1];
                    if (defined($token->[2]->{'index'})) {
			$outstr.='<:>'.$token->[2]->{'index'};
                        if ($token->[1] eq 'obj') {
			    $obj[$token->[2]->{'index'}]=2;
                        }
                    }
                    $outstr.='<:>'.$token->[2]->{'value'};
                }
            }
        }

    } else {
        $errtext.='Map not loaded: The file does not exist. ';
    }
    return($outstr,$errtext,$infotext);
}


# ----------------------------------------------------------- Saves map to disk

sub savemap {
    my ($fn,$errtext,$dotimeupdate)=@_;
    my $infotext='';
    my %alltypes;
    my %allvalues;
    if (($fn=~/\.sequence(\.tmp)*$/) ||
        ($fn=~/\.page(\.tmp)*$/)) {
	
# ------------------------------------------------------------- Deal with input
        my @tags=split(/<&>/,$env{'form.output'});
        my $outstr='';
        my $graphdef=0;
        if ($tags[0] eq 'graphdef<:>yes') {
	    $outstr='<map mode="rat/graphical">'."\n";
            $graphdef=1;
        } else {
            $outstr="<map>\n";
        }
        foreach (@tags) {
	    my @parts=split(/<:>/,$_);
	    if ($parts[0] eq 'objcont') {
		my @comp=split(/:/,$parts[$#parts]);
# --------------------------------------------------------------- Logical input
		if (($comp[$#comp] eq 'res') || ($comp[$#comp] eq 'zombie')) {
		    $comp[0]=qtescape($comp[0]);
		    $comp[0] = &HTML::Entities::encode($comp[0],'&<>"');
		    
		    $comp[1]=qtescape($comp[1]);
		    if ($comp[2] eq 'true') {
			if ($comp[1]!~/^http\:\/\//) {
			    $comp[1]='http://'.$comp[1];
			}
			$comp[1].='" external="true';
		    } else {
			if ($comp[1]=~/^http\:\/\//) {
			    $comp[1]=~s/^http\:\/\/[^\/]*\//\//;
			}
		    }
		    $outstr.='<resource id="'.$parts[1].'" src="'
			.$comp[1].'"';
		    
		    if (($comp[3] ne '') && ($comp[3] ne 'normal')) {
			$outstr.=' type="'.$comp[3].'"';
		    }
		    if ($comp[0] ne '') {
			$outstr.=' title="'.$comp[0].'"';
		    }
		    $outstr.=" />\n";
		} elsif ($comp[$#comp] eq 'cond') {
		    $outstr.='<condition id="'.$parts[1].'"';
		    if (($comp[1] ne '') && ($comp[1] ne 'normal')) {
			$outstr.=' type="'.$comp[1].'"';
		    }
		    $outstr.=' value="'.qtescape($comp[0]).'"';
		    $outstr.=" />\n";
		}
	    } elsif ($parts[0] eq 'objlinks') {
		my @comp=split(/:/,$parts[$#parts]);
		$outstr.='<link';
		$outstr.=' from="'.$comp[0].'"';
		$outstr.=' to="'.$comp[1].'"';
		if (($comp[2] ne '') && ($comp[2]!=0)) {
		    $outstr.=' condition="'.$comp[2].'"';
		}
		$outstr.=' index="'.$parts[1].'"';
		$outstr.=" />\n";
	    } elsif ($parts[0] eq 'objparms') {
		undef %alltypes;
		undef %allvalues;
		foreach (split(/:/,$parts[$#parts])) {
		    my ($type,$name,$value)=split(/\_\_\_/,$_);
		    $alltypes{$name}=$type;
		    $allvalues{$name}=$value;
		}
		foreach (keys %allvalues) {
		    if ($allvalues{$_} ne '') {
			$outstr.='<param to="'.$parts[1].'" type="'
			    .$alltypes{$_}.'" name="'.$_
			    .'" value="'.$allvalues{$_}.'" />'
			    ."\n";
		    }
		}
	    } elsif (($parts[0] ne '') && ($graphdef)) {
# ------------------------------------------------------------- Graphical input
		$outstr.='<'.$parts[0];
		if ($#parts==2) {
		    $outstr.=' index="'.$parts[1].'"';
		}
		$outstr.=' value="'.qtescape($parts[$#parts]).'" />'."\n";
	    }
        }
        $outstr.="</map>\n";
        my ($cdom,$cnum,$mapname);
	if ($fn=~m{^/*uploaded/($LONCAPA::domain_re)/($LONCAPA::courseid_re)/(.*)$}) {
            ($cdom,$cnum,$mapname) = ($1,$2,$3);
	    $env{'form.output'}=$outstr;
            my $result=&Apache::lonnet::finishuserfileupload($cnum,$cdom,
							     'output',$mapname);
	    if ($result != m|^/uploaded/|) {
		$errtext.='Map not saved: A network error occurred when trying to save the map. ';
	    }
        } else {
	    if (open(my $fh,">$fn")) {
		print $fh $outstr;
		$infotext.="Map saved as $fn. ";
	    } else {
		$errtext.='Could not write file '.$fn.'.  Map not saved. ';
	    }
        }
        if ($dotimeupdate) {
            unless ($errtext) {
                if ($env{'request.course.id'}) {
                    if (($cdom eq $env{'course.'.$env{'request.course.id'}.'.domain'}) &&
                        ($cnum eq $env{'course.'.$env{'request.course.id'}.'.num'}) &&
                        ($mapname =~ /^supplemental(|_\d+)\.sequence$/)) {
                        &Apache::lonnet::update_supp_caches($cdom,$cnum);
                    } else {
                        my $now = time;
                        &Apache::lonnet::put('environment',{'internal.contentchange' => $now},
                                             $env{'course.'.$env{'request.course.id'}.'.domain'},
                                             $env{'course.'.$env{'request.course.id'}.'.num'});
                        &Apache::lonnet::appenv(
                            {'course.'.$env{'request.course.id'}.'.internal.contentchange' => $now});
                        &Apache::lonnet::do_cache_new('crschange',$env{'request.course.id'},$now,600);
                    }
                }
            }
        }
    } else {
# -------------------------------------------- Cannot write to that file, error
        $errtext.='Map not saved: The specified path does not exist. ';
    }
    return ($errtext,$infotext);
}

1;
__END__

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