--- loncom/lonnet/perl/lonnet.pm	2006/06/07 21:15:10	1.746
+++ loncom/lonnet/perl/lonnet.pm	2006/08/04 19:42:59	1.768
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # TCP networking package
 #
-# $Id: lonnet.pm,v 1.746 2006/06/07 21:15:10 raeburn Exp $
+# $Id: lonnet.pm,v 1.768 2006/08/04 19:42:59 albertel Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -281,6 +281,17 @@ sub critical {
     return $answer;
 }
 
+# ------------------------------------------- check if return value is an error
+
+sub error {
+    my ($result) = @_;
+    if ($result =~ /^(con_lost|no_such_host|error: (\d+) (.*))/) {
+	if ($2 == 2) { return undef; }
+	return $1;
+    }
+    return undef;
+}
+
 # ------------------------------------------- Transfer profile into environment
 
 sub transfer_profile_to_env {
@@ -1874,9 +1885,6 @@ sub get_course_adv_roles {
 	    (!$nothide{$username.':'.$domain})) { next; }
 	if ($role eq 'cr') { next; }
         my $key=&plaintext($role);
-	if ($role =~ /^cr/) {
-	    $key=(split('/',$role))[3];
-	}
         if ($section) { $key.=' (Sec/Grp '.$section.')'; }
         if ($returnhash{$key}) {
 	    $returnhash{$key}.=','.$username.':'.$domain;
@@ -2922,23 +2930,25 @@ sub del {
 # -------------------------------------------------------------- dump interface
 
 sub dump {
-   my ($namespace,$udomain,$uname,$regexp,$range)=@_;
-   if (!$udomain) { $udomain=$env{'user.domain'}; }
-   if (!$uname) { $uname=$env{'user.name'}; }
-   my $uhome=&homeserver($uname,$udomain);
-   if ($regexp) {
-       $regexp=&escape($regexp);
-   } else {
-       $regexp='.';
-   }
-   my $rep=reply("dump:$udomain:$uname:$namespace:$regexp:$range",$uhome);
-   my @pairs=split(/\&/,$rep);
-   my %returnhash=();
-   foreach (@pairs) {
-      my ($key,$value)=split(/=/,$_,2);
-      $returnhash{unescape($key)}=&thaw_unescape($value);
-   }
-   return %returnhash;
+    my ($namespace,$udomain,$uname,$regexp,$range)=@_;
+    if (!$udomain) { $udomain=$env{'user.domain'}; }
+    if (!$uname) { $uname=$env{'user.name'}; }
+    my $uhome=&homeserver($uname,$udomain);
+    if ($regexp) {
+	$regexp=&escape($regexp);
+    } else {
+	$regexp='.';
+    }
+    my $rep=&reply("dump:$udomain:$uname:$namespace:$regexp:$range",$uhome);
+    my @pairs=split(/\&/,$rep);
+    my %returnhash=();
+    foreach my $item (@pairs) {
+	my ($key,$value)=split(/=/,$item,2);
+	$key = &unescape($key);
+	next if ($key =~ /^error: 2 /);
+	$returnhash{$key}=&thaw_unescape($value);
+    }
+    return %returnhash;
 }
 
 # --------------------------------------------------------- dumpstore interface
@@ -3207,6 +3217,218 @@ sub tmpdel {
     return &reply("tmpdel:$token",$server);
 }
 
+# -------------------------------------------------- portfolio access checking
+
+sub portfolio_access {
+    my ($requrl) = @_;
+    my (undef,$udom,$unum,$file_name,$group) = &parse_portfolio_url($requrl);
+    my $result = &get_portfolio_access($udom,$unum,$file_name,$group);
+    if ($result eq 'ok') {
+       return 'F';
+    } elsif ($result =~ /^[^:]+:guest_/) {
+       return 'A';
+    }
+    return '';
+}
+
+sub get_portfolio_access {
+    my ($udom,$unum,$file_name,$group,$access_hash) = @_;
+
+    if (!ref($access_hash)) {
+	my $current_perms = &get_portfile_permissions($udom,$unum);
+	my %access_controls = &get_access_controls($current_perms,$group,
+						   $file_name);
+	$access_hash = $access_controls{$file_name};
+    }
+
+    my ($public,$guest,@domains,@users,@courses,@groups);
+    my $now = time;
+    if (ref($access_hash) eq 'HASH') {
+        foreach my $key (keys(%{$access_hash})) {
+            my ($num,$scope,$end,$start) = ($key =~ /^([^:]+):([a-z]+)_(\d*)_?(\d*)$/);
+            if ($start > $now) {
+                next;
+            }
+            if ($end && $end<$now) {
+                next;
+            }
+            if ($scope eq 'public') {
+                $public = $key;
+                last;
+            } elsif ($scope eq 'guest') {
+                $guest = $key;
+            } elsif ($scope eq 'domains') {
+                push(@domains,$key);
+            } elsif ($scope eq 'users') {
+                push(@users,$key);
+            } elsif ($scope eq 'course') {
+                push(@courses,$key);
+            } elsif ($scope eq 'group') {
+                push(@groups,$key);
+            }
+        }
+        if ($public) {
+            return 'ok';
+        }
+        if ($env{'user.name'} eq 'public' && $env{'user.domain'} eq 'public') {
+            if ($guest) {
+                return $guest;
+            }
+        } else {
+            if (@domains > 0) {
+                foreach my $domkey (@domains) {
+                    if (ref($access_hash->{$domkey}{'dom'}) eq 'ARRAY') {
+                        if (grep(/^\Q$env{'user.domain'}\E$/,@{$access_hash->{$domkey}{'dom'}})) {
+                            return 'ok';
+                        }
+                    }
+                }
+            }
+            if (@users > 0) {
+                foreach my $userkey (@users) {
+                    if (exists($access_hash->{$userkey}{'users'}{$env{'user.name'}.':'.$env{'user.domain'}})) {
+                        return 'ok';
+                    }
+                }
+            }
+            my %roleshash;
+            my @courses_and_groups = @courses;
+            push(@courses_and_groups,@groups); 
+            if (@courses_and_groups > 0) {
+                my (%allgroups,%allroles); 
+                my ($start,$end,$role,$sec,$group);
+                foreach my $envkey (%env) {
+                    if ($envkey =~ m-^user\.role\.(gr|cc|in|ta|ep|st)\./([^/]+)/([^/]+)/?([^/]*)$-) {
+                        my $cid = $2.'_'.$3; 
+                        if ($1 eq 'gr') {
+                            $group = $4;
+                            $allgroups{$cid}{$group} = $env{$envkey};
+                        } else {
+                            if ($4 eq '') {
+                                $sec = 'none';
+                            } else {
+                                $sec = $4;
+                            }
+                            $allroles{$cid}{$1}{$sec} = $env{$envkey};
+                        }
+                    } elsif ($envkey =~ m-^user\.role\./cr/(\w+/\w+/\w*)./([^/]+)/([^/]+)/?([^/]*)$-) {
+                        my $cid = $2.'_'.$3;
+                        if ($4 eq '') {
+                            $sec = 'none';
+                        } else {
+                            $sec = $4;
+                        }
+                        $allroles{$cid}{$1}{$sec} = $env{$envkey};
+                    }
+                }
+                if (keys(%allroles) == 0) {
+                    return;
+                }
+                foreach my $key (@courses_and_groups) {
+                    my %content = %{$$access_hash{$key}};
+                    my $cnum = $content{'number'};
+                    my $cdom = $content{'domain'};
+                    my $cid = $cdom.'_'.$cnum;
+                    if (!exists($allroles{$cid})) {
+                        next;
+                    }    
+                    foreach my $role_id (keys(%{$content{'roles'}})) {
+                        my @sections = @{$content{'roles'}{$role_id}{'section'}};
+                        my @groups = @{$content{'roles'}{$role_id}{'group'}};
+                        my @status = @{$content{'roles'}{$role_id}{'access'}};
+                        my @roles = @{$content{'roles'}{$role_id}{'role'}};
+                        foreach my $role (keys(%{$allroles{$cid}})) {
+                            if ((grep/^all$/,@roles) || (grep/^\Q$role\E$/,@roles)) {
+                                foreach my $sec (keys(%{$allroles{$cid}{$role}})) {
+                                    if (&course_group_datechecker($allroles{$cid}{$role}{$sec},$now,\@status) eq 'ok') {
+                                        if (grep/^all$/,@sections) {
+                                            return 'ok';
+                                        } else {
+                                            if (grep/^$sec$/,@sections) {
+                                                return 'ok';
+                                            }
+                                        }
+                                    }
+                                }
+                                if (keys(%{$allgroups{$cid}}) == 0) {
+                                    if (grep/^none$/,@groups) {
+                                        return 'ok';
+                                    }
+                                } else {
+                                    if (grep/^all$/,@groups) {
+                                        return 'ok';
+                                    } 
+                                    foreach my $group (keys(%{$allgroups{$cid}})) {
+                                        if (grep/^$group$/,@groups) {
+                                            return 'ok';
+                                        }
+                                    }
+                                } 
+                            }
+                        }
+                    }
+                }
+            }
+            if ($guest) {
+                return $guest;
+            }
+        }
+    }
+    return;
+}
+
+sub course_group_datechecker {
+    my ($dates,$now,$status) = @_;
+    my ($start,$end) = split(/\./,$dates);
+    if (!$start && !$end) {
+        return 'ok';
+    }
+    if (grep/^active$/,@{$status}) {
+        if (((!$start) || ($start && $start <= $now)) && ((!$end) || ($end && $end >= $now))) {
+            return 'ok';
+        }
+    }
+    if (grep/^previous$/,@{$status}) {
+        if ($end > $now ) {
+            return 'ok';
+        }
+    }
+    if (grep/^future$/,@{$status}) {
+        if ($start > $now) {
+            return 'ok';
+        }
+    }
+    return; 
+}
+
+sub parse_portfolio_url {
+    my ($url) = @_;
+
+    my ($type,$udom,$unum,$group,$file_name);
+    
+    if ($url =~  m-^/*uploaded/([^/]+)/([^/]+)/portfolio(/.+)$-) {
+	$type = 1;
+        $udom = $1;
+        $unum = $2;
+        $file_name = $3;
+    } elsif ($url =~ m-^/*uploaded/([^/]+)/([^/]+)/groups/([^/]+)/portfolio/(.+)$-) {
+	$type = 2;
+        $udom = $1;
+        $unum = $2;
+        $group = $3;
+        $file_name = $3.'/'.$4;
+    }
+    if (wantarray) {
+	return ($type,$udom,$unum,$file_name,$group);
+    }
+    return $type;
+}
+
+sub is_portfolio_url {
+    my ($url) = @_;
+    return scalar(&parse_portfolio_url($url));
+}
+
 # ---------------------------------------------- Custom access rule evaluation
 
 sub customaccess {
@@ -3264,7 +3486,7 @@ sub allowed {
         return 'F';
     }
 
-# bre access to group if user has rgf priv for this group and course.
+# bre access to group portfolio for rgf priv in group, or mdg or vcg in course.
     if (($space=~/^(uploaded|editupload)$/) && ($dir[0] eq 'groups') 
          && ($dir[2] eq 'portfolio') && ($priv eq 'bre')) {
         if (exists($env{'request.course.id'})) {
@@ -3276,6 +3498,14 @@ sub allowed {
                 if ($env{'user.priv.'.$env{'request.role'}.'./'.$courseprivid
                     .'/'.$dir[1]} =~/rgf\&([^\:]*)/) {
                     return $1; 
+                } else {
+                    if ($env{'request.course.sec'}) {
+                        $courseprivid.='/'.$env{'request.course.sec'};
+                    }
+                    if ($env{'user.priv.'.$env{'request.role'}.'./'.
+                        $courseprivid} =~/(mdg|vcg)\&([^\:]*)/) {
+                        return $2;
+                    }
                 }
             }
         }
@@ -3344,14 +3574,6 @@ sub allowed {
        $thisallowed.=$1;
     }
 
-# Group: uri itself is a group
-    my $groupuri=$uri;
-    $groupuri=~s/^([^\/])/\/$1/;
-    if ($env{'user.priv.'.$env{'request.role'}.'.'.$groupuri}
-       =~/\Q$priv\E\&([^\:]*)/) {
-       $thisallowed.=$1;
-    }
-
 # URI is an uploaded document for this course, default permissions don't matter
 # not allowing 'edit' access (editupload) to uploaded course docs
     if (($priv eq 'bre') && ($uri=~m|^uploaded/|)) {
@@ -3378,6 +3600,13 @@ sub allowed {
         }
     }
 
+    if ($priv eq 'bre'
+	&& $thisallowed ne 'F' 
+	&& $thisallowed ne '2'
+	&& &is_portfolio_url($uri)) {
+	$thisallowed = &portfolio_access($uri);
+    }
+    
 # Full access at system, domain or course-wide level? Exit.
 
     if ($thisallowed=~/F/) {
@@ -3528,7 +3757,11 @@ sub allowed {
 #
 
     unless ($env{'request.course.id'}) {
-       return '1';
+	if ($thisallowed eq 'A') {
+	    return 'A';
+	} else {
+	    return '1';
+	}
     }
 
 #
@@ -3591,6 +3824,9 @@ sub allowed {
       }
    }
 
+    if ($thisallowed eq 'A') {
+	return 'A';
+    }
    return 'F';
 }
 
@@ -4100,6 +4336,9 @@ sub devalidate_getgroups_cache {
 
 sub plaintext {
     my ($short,$type,$cid) = @_;
+    if ($short =~ /^cr/) {
+	return (split('/',$short))[-1];
+    }
     if (!defined($cid)) {
         $cid = $env{'request.course.id'};
     }
@@ -4569,6 +4808,14 @@ sub is_locked {
     }
 }
 
+sub declutter_portfile {
+    my ($file) = @_;
+    &logthis("got $file");
+    $file =~ s-^(/portfolio/|portfolio/)-/-;
+    &logthis("ret $file");
+    return $file;
+}
+
 # ------------------------------------------------------------- Mark as Read Only
 
 sub mark_as_readonly {
@@ -4577,6 +4824,7 @@ sub mark_as_readonly {
     my ($tmp)=keys(%current_permissions);
     if ($tmp=~/^error:/) { undef(%current_permissions); }
     foreach my $file (@{$files}) {
+	$file = &declutter_portfile($file);
         push(@{$current_permissions{$file}},$what);
     }
     &put('file_permissions',\%current_permissions,$domain,$user);
@@ -4655,7 +4903,6 @@ sub files_not_in_path {
 #----------------------------------------------Get portfolio file permissions
 
 sub get_portfile_permissions {
-    # returns a reference to a hash containing contents of file_permissions.db 
     my ($domain,$user) = @_;
     my %current_permissions = &dump('file_permissions',$domain,$user);
     my ($tmp)=keys(%current_permissions);
@@ -4665,45 +4912,121 @@ sub get_portfile_permissions {
 
 #---------------------------------------------Get portfolio file access controls
 
-sub get_access_controls  {
-    # returns a hash containing access control information retrieved from
-    # file_permissions.db. The hash contains key=value pairs where key is
-    # the control type, end date and start date, in the form type_end_start
-    # and value is a string containing access control settings (in XML),
-    #
-    # Internally access_controls are stored in file_permissions.db in an
-    # array of arrays and a hash, where arrays are locks set when a portfolio   
-    # file has been uploaded to an essayresponse problem in a course, and
-    # the hash contains other data. Two keys are currently defined in the hash:  
-    # access and accesscount. The value for accesscount is a scalar - equal to 
-    # the next number to use as the first part of an access control key
-    # when defining a new control. The value for access is an anonymous hash
-    # where keys are access controls and values are settings.
-    #    
+sub get_access_controls {
     my ($current_permissions,$group,$file) = @_;
-    my @access_checks = ();
     my %access; 
     if (defined($file)) {
-        @access_checks = ($file);
+        if (ref($$current_permissions{$file."\0".'accesscontrol'}) eq 'HASH') {
+            foreach my $control (keys(%{$$current_permissions{$file."\0".'accesscontrol'}})) {
+                $access{$file}{$control} = $$current_permissions{$file."\0".$control};
+            }
+        }
     } else {
-        @access_checks = keys(%{$current_permissions});
+        foreach my $key (keys(%{$current_permissions})) {
+            if ($key =~ /\0accesscontrol$/) {
+                if (defined($group)) {
+                    if ($key !~ m-^\Q$group\E/-) {
+                        next;
+                    }
+                }
+                my ($fullpath) = split(/\0/,$key);
+                if (ref($$current_permissions{$key}) eq 'HASH') {
+                    foreach my $control (keys(%{$$current_permissions{$key}})) {
+                        $access{$fullpath}{$control}=$$current_permissions{$fullpath."\0".$control};
+                    }
+                }
+            }
+        }
     }
-    foreach my $file_name (@access_checks) {
-        my $value = $$current_permissions{$file_name};
-        if (defined($group)) {
-            if ($file_name !~ m-^\Q$group\E/-) {
-                next;
+    return %access;
+}
+
+sub modify_access_controls {
+    my ($file_name,$changes,$domain,$user)=@_;
+    my ($outcome,$deloutcome);
+    my %store_permissions;
+    my %new_values;
+    my %new_control;
+    my %translation;
+    my @deletions = ();
+    my $now = time;
+    if (exists($$changes{'activate'})) {
+        if (ref($$changes{'activate'}) eq 'HASH') {
+            my @newitems = sort(keys(%{$$changes{'activate'}}));
+            my $numnew = scalar(@newitems);
+            for (my $i=0; $i<$numnew; $i++) {
+                my $newkey = $newitems[$i];
+                my $newid = &Apache::loncommon::get_cgi_id();
+                $newkey =~ s/^(\d+)/$newid/;
+                $translation{$1} = $newid;
+                $new_values{$file_name."\0".$newkey} = 
+                                          $$changes{'activate'}{$newitems[$i]};
+                $new_control{$newkey} = $now;
             }
         }
-        if (ref($value) eq "ARRAY") {
-            foreach my $stored_what (@{$value}) {
-                if (ref($stored_what) eq 'HASH') {
-                    $access{$file_name} = $$stored_what{'access'};
+    }
+    my %todelete;
+    my %changed_items;
+    foreach my $action ('delete','update') {
+        if (exists($$changes{$action})) {
+            if (ref($$changes{$action}) eq 'HASH') {
+                foreach my $key (keys(%{$$changes{$action}})) {
+                    my ($itemnum) = ($key =~ /^([^:]+):/);
+                    if ($action eq 'delete') { 
+                        $todelete{$itemnum} = 1;
+                    } else {
+                        $changed_items{$itemnum} = $key;
+                    }
                 }
             }
         }
     }
-    return %access;
+    # get lock on access controls for file.
+    my $lockhash = {
+                  $file_name."\0".'locked_access_records' => $env{'user.name'}.
+                                                       ':'.$env{'user.domain'},
+                   }; 
+    my $tries = 0;
+    my $gotlock = &newput('file_permissions',$lockhash,$domain,$user);
+   
+    while (($gotlock ne 'ok') && $tries <3) {
+        $tries ++;
+        sleep 1;
+        $gotlock = &newput('file_permissions',$lockhash,$domain,$user);
+    }
+    if ($gotlock eq 'ok') {
+        my %curr_permissions = &dump('file_permissions',$domain,$user,$file_name);
+        my ($tmp)=keys(%curr_permissions);
+        if ($tmp=~/^error:/) { undef(%curr_permissions); }
+        if (exists($curr_permissions{$file_name."\0".'accesscontrol'})) {
+            my $curr_controls = $curr_permissions{$file_name."\0".'accesscontrol'};
+            if (ref($curr_controls) eq 'HASH') {
+                foreach my $control_item (keys(%{$curr_controls})) {
+                    my ($itemnum) = ($control_item =~ /^([^:]+):/);
+                    if (defined($todelete{$itemnum})) {
+                        push(@deletions,$file_name."\0".$control_item);
+                    } else {
+                        if (defined($changed_items{$itemnum})) {
+                            $new_control{$changed_items{$itemnum}} = $now;
+                            push(@deletions,$file_name."\0".$control_item);
+                            $new_values{$file_name."\0".$changed_items{$itemnum}} = $$changes{'update'}{$changed_items{$itemnum}};
+                        } else {
+                            $new_control{$control_item} = $$curr_controls{$control_item};
+                        }
+                    }
+                }
+            }
+        }
+        $deloutcome = &del('file_permissions',\@deletions,$domain,$user);
+        $new_values{$file_name."\0".'accesscontrol'} = \%new_control;
+        $outcome = &put('file_permissions',\%new_values,$domain,$user);
+        #  remove lock
+        my @del_lock = ($file_name."\0".'locked_access_records');
+        my $dellockoutcome = &del('file_permissions',\@del_lock,$domain,$user);
+    } else {
+        $outcome = "error: could not obtain lockfile\n";  
+    }
+    return ($outcome,$deloutcome,\%new_values,\%translation);
 }
 
 #------------------------------------------------------Get Marked as Read Only
@@ -4723,9 +5046,7 @@ sub get_marked_as_readonly {
         if (ref($value) eq "ARRAY"){
             foreach my $stored_what (@{$value}) {
                 my $cmp2=$stored_what;
-                if (ref($stored_what eq 'HASH')) {
-                    next;
-                } elsif (ref($stored_what eq 'ARRAY')) {
+                if (ref($stored_what) eq 'ARRAY') {
                     $cmp2=join('',@{$stored_what});
                 }
                 if ($cmp1 eq $cmp2) {
@@ -4754,12 +5075,18 @@ sub get_marked_as_readonly_hash {
         if (ref($value) eq "ARRAY"){
             foreach my $stored_what (@{$value}) {
                 if (ref($stored_what) eq 'ARRAY') {
-                    if ($stored_what eq $what) {
-                        $readonly_files{$file_name} = 'locked';
-                    } elsif (!defined($what)) {
-                        $readonly_files{$file_name} = 'locked';
+                    foreach my $lock_descriptor(@{$stored_what}) {
+                        if ($lock_descriptor eq 'graded') {
+                            $readonly_files{$file_name} = 'graded';
+                        } elsif ($lock_descriptor eq 'handback') {
+                            $readonly_files{$file_name} = 'handback';
+                        } else {
+                            if (!exists($readonly_files{$file_name})) {
+                                $readonly_files{$file_name} = 'locked';
+                            }
+                        }
                     }
-                }
+                } 
             }
         } 
     }
@@ -4771,6 +5098,7 @@ sub unmark_as_readonly {
     # unmarks $file_name (if $file_name is defined), or all files locked by $what 
     # for portfolio submissions, $what contains [$symb,$crsid] 
     my ($domain,$user,$what,$file_name,$group) = @_;
+    $file_name = &declutter_portfile($file_name);
     my $symb_crs = $what;
     if (ref($what)) { $symb_crs=join('',@$what); }
     my %current_permissions = &dump('file_permissions',$domain,$user,$group);
@@ -4778,16 +5106,15 @@ sub unmark_as_readonly {
     if ($tmp=~/^error:/) { undef(%current_permissions); }
     my @readonly_files = &get_marked_as_readonly($domain,$user,$what,$group);
     foreach my $file (@readonly_files) {
-	if (defined($file_name) && ($file_name ne $file)) { next; }
+	my $clean_file = &declutter_portfile($file);
+	if (defined($file_name) && ($file_name ne $clean_file)) { next; }
 	my $current_locks = $current_permissions{$file};
         my @new_locks;
         my @del_keys;
         if (ref($current_locks) eq "ARRAY"){
             foreach my $locker (@{$current_locks}) {
                 my $compare=$locker;
-                if (!ref($locker) eq 'ARRAY') {
-                    push(@new_locks,$locker);
-                } else {   
+                if (ref($locker) eq 'ARRAY') {
                     $compare=join('',@{$locker});
                     if ($compare ne $symb_crs) {
                         push(@new_locks, $locker);
@@ -5054,6 +5381,7 @@ sub devalidatecourseresdata {
     &devalidate_cache_new('courseres',$hashid);
 }
 
+
 # --------------------------------------------------- Course Resourcedata Query
 
 sub get_courseresdata {
@@ -5556,7 +5884,7 @@ sub metadata {
         my %metathesekeys=();
         unless ($filename=~/\.meta$/) { $filename.='.meta'; }
 	my $metastring;
-	if ($uri !~ m -^(uploaded|editupload)/-) {
+	if ($uri !~ m -^(editupload)/-) {
 	    my $file=&filelocation('',&clutter($filename));
 	    #push(@{$metaentry{$uri.'.file'}},$file);
 	    $metastring=&getfile($file);
@@ -5775,6 +6103,17 @@ sub metadata_generate_part0 {
     }
 }
 
+# ------------------------------------------------------ Devalidate title cache
+
+sub devalidate_title_cache {
+    my ($url)=@_;
+    if (!$env{'request.course.id'}) { return; }
+    my $symb=&symbread($url);
+    if (!$symb) { return; }
+    my $key=$env{'request.course.id'}."\0".$symb;
+    &devalidate_cache_new('title',$key);
+}
+
 # ------------------------------------------------- Get the title of a resource
 
 sub gettitle {
@@ -7181,6 +7520,7 @@ actions
  '': forbidden
  1: user needs to choose course
  2: browse allowed
+ A: passphrase authentication needed
 
 =item *
 
@@ -7580,6 +7920,31 @@ cput($namespace,$storehash,$udom,$uname)
 
 =item *
 
+newput($namespace,$storehash,$udom,$uname) :
+
+Attempts to store the items in the $storehash, but only if they don't
+currently exist, if this succeeds you can be certain that you have 
+successfully created a new key value pair in the $namespace db.
+
+
+Args:
+ $namespace: name of database to store values to
+ $storehash: hashref to store to the db
+ $udom: (optional) domain of user containing the db
+ $uname: (optional) name of user caontaining the db
+
+Returns:
+ 'ok' -> succeeded in storing all keys of $storehash
+ 'key_exists: <key>' -> failed to anything out of $storehash, as at
+                        least <key> already existed in the db (other
+                        requested keys may also already exist)
+ 'error: <msg>' -> unable to tie the DB or other erorr occured
+ 'con_lost' -> unable to contact request server
+ 'refused' -> action was not allowed by remote machine
+
+
+=item *
+
 eget($namespace,$storearr,$udom,$uname) : returns hash with keys from array
 reference filled in from namesp (encrypts the return communication)
 ($udom and $uname are optional)
@@ -7814,6 +8179,94 @@ removeuploadedurl(): convience function
   Args:
    url:  a full /uploaded/... url to delete
 
+=item * 
+
+get_portfile_permissions():
+  Args:
+    domain: domain of user or course contain the portfolio files
+    user: name of user or num of course contain the portfolio files
+  Returns:
+    hashref of a dump of the proper file_permissions.db
+   
+
+=item * 
+
+get_access_controls():
+
+Args:
+  current_permissions: the hash ref returned from get_portfile_permissions()
+  group: (optional) the group you want the files associated with
+  file: (optional) the file you want access info on
+
+Returns:
+    a hash (keys are file names) of hashes containing
+        keys are: path to file/file_name\0uniqueID:scope_end_start (see below)
+        values are XML containing access control settings (see below) 
+
+Internal notes:
+
+ access controls are stored in file_permissions.db as key=value pairs.
+    key -> path to file/file_name\0uniqueID:scope_end_start
+        where scope -> public,guest,course,group,domains or users.
+              end -> UNIX time for end of access (0 -> no end date)
+              start -> UNIX time for start of access
+
+    value -> XML description of access control
+           <scope type=""> (type =1 of: public,guest,course,group,domains,users">
+            <start></start>
+            <end></end>
+
+            <password></password>  for scope type = guest
+
+            <domain></domain>     for scope type = course or group
+            <number></number>
+            <roles id="">
+             <role></role>
+             <access></access>
+             <section></section>
+             <group></group>
+            </roles>
+
+            <dom></dom>         for scope type = domains
+
+            <users>             for scope type = users
+             <user>
+              <uname></uname>
+              <udom></udom>
+             </user>
+            </users>
+           </scope> 
+              
+ Access data is also aggregated for each file in an additional key=value pair:
+ key -> path to file/file_name\0accesscontrol 
+ value -> reference to hash
+          hash contains key = value pairs
+          where key = uniqueID:scope_end_start
+                value = UNIX time record was last updated
+
+          Used to improve speed of look-ups of access controls for each file.  
+ 
+ Locks on files (resulting from submission of portfolio file to a homework problem stored in array of arrays.
+
+modify_access_controls():
+
+Modifies access controls for a portfolio file
+Args
+1. file name
+2. reference to hash of required changes,
+3. domain
+4. username
+  where domain,username are the domain of the portfolio owner 
+  (either a user or a course) 
+
+Returns:
+1. result of additions or updates ('ok' or 'error', with error message). 
+2. result of deletions ('ok' or 'error', with error message).
+3. reference to hash of any new or updated access controls.
+4. reference to hash used to map incoming IDs to uniqueIDs assigned to control.
+   key = integer (inbound ID)
+   value = uniqueID  
+
 =back
 
 =head2 HTTP Helper Routines