--- loncom/interface/portfolio.pm	2013/08/21 15:42:22	1.250
+++ loncom/interface/portfolio.pm	2024/12/29 21:44:16	1.254.2.7
@@ -1,7 +1,7 @@
 # The LearningOnline Network
 # portfolio browser
 #
-# $Id: portfolio.pm,v 1.250 2013/08/21 15:42:22 bisitz Exp $
+# $Id: portfolio.pm,v 1.254.2.7 2024/12/29 21:44:16 raeburn Exp $
 #
 # Copyright Michigan State University Board of Trustees
 #
@@ -112,13 +112,17 @@ sub display_common {
 END
         }
 
+        # Find space available before uploading
+        my $free_space = &free_space($group);
+
         # Upload File
         $r->print('<div class="LC_left_float">'
                  .'<form method="post" enctype="multipart/form-data" action="'.$escuri.'">'
                  .'<fieldset>'
                  .'<legend>'.$lt{'upload_label'}.'</legend>'
                  .$groupitem 
-                 .'<input name="uploaddoc" type="file" />'
+                 .'<input name="uploaddoc" type="file" class="LC_flUpload" />'
+                 .'<input type="hidden" id="LC_free_space" value="'.$free_space.'" />'
                  .'<input type="hidden" name="currentpath" value="'.$current_path.'" />'
                  .'<input type="hidden" name="action" value="'.$env{"form.action"}.'" />'
                  .'<input type="hidden" name="symb" value="'.$env{"form.symb"}.'" />'
@@ -199,7 +203,8 @@ sub display_portfolio_usage {
                     .$helpitem
                     .'</div>'
                     .'<div>'
-                    .&Apache::lonhtmlcommon::display_usage($current_disk_usage,$disk_quota)
+                    .&Apache::lonhtmlcommon::display_usage($current_disk_usage,
+                                                           $disk_quota,'portfolio')
                     .'</div>');
 }
 
@@ -262,7 +267,7 @@ sub display_directory_line {
 
 sub display_directory {
     my ($r,$url,$current_path,$is_empty,$dir_list,$group,$can_upload,
-        $can_modify,$can_delete,$can_setacl)=@_;
+        $can_modify,$can_delete,$can_setacl,$caller)=@_;
     my $iconpath= $r->dir_config('lonIconsURL') . "/";
     my $select_mode;
     my $checked_files;
@@ -306,6 +311,15 @@ sub display_directory {
                 &mt('Using the portfolio file list'))
            .'</p>'
         );
+        my ($acl_helpfile,$acl_helplink);
+        if ($caller eq 'coursegrp_portfolio') {
+            $acl_helpfile = 'Portfolio ShareFile Group';
+        } elsif ($can_setacl) {
+            $acl_helpfile = 'Portfolio ShareFile';
+        }
+        if ($acl_helpfile) {
+            $acl_helplink = &Apache::loncommon::help_open_topic($acl_helpfile);
+        }
         $r->print(&Apache::loncommon::start_data_table()
                  .&Apache::loncommon::start_data_table_header_row()
                  .'<th colspan="2">'.&mt('Actions'). &Apache::loncommon::help_open_topic('Portfolio FileAction').'</th>'
@@ -315,7 +329,7 @@ sub display_directory {
                  .'<th>'.&mt('Size').'</th>'
                  .'<th>'.&mt('Last Modified').'</th>'
                  .'<th>&nbsp;</th>'
-                 .'<th>'.&mt('Current Access Status').&Apache::loncommon::help_open_topic('Portfolio ShareFile').'</th>'
+                 .'<th>'.&mt('Current Access Status').$acl_helplink.'</th>'
                  .&Apache::loncommon::end_data_table_header_row());
     }
 
@@ -904,15 +918,15 @@ sub display_access {
         $info .= '</li><li>'.&mt('Passphrase-protected files do not require log-in, but will require the viewer to enter the passphrase you set.');
         $info .= '</li><li>'.&explain_conditionals();
         $info .= '</li></ul>'.
-                  &mt('A listing of files viewable without log-in is available at: ')."<a href=\"/adm/$udom/$uname/aboutme/portfolio\">".&Apache::lonnet::absolute_url($ENV{'SERVER_NAME'})."/adm/$udom/$uname/aboutme/portfolio</a>.<br />";
+                  &mt('A listing of files viewable without log-in is available at: ')."<a href=\"/adm/$udom/$uname/aboutme/portfolio\">".&Apache::lonnet::absolute_url()."/adm/$udom/$uname/aboutme/portfolio</a>.<br />";
         if ($group eq '') {
             $info .= &mt("For logged in users a 'Display file listing' link will also appear (when there are viewable files) on your personal information page:");
         } else {
             $info .= &mt("For logged in users a 'Display file listing' link will also appear (when there are viewable files) on the course information page:");
         }
-        $info .= "<br /><a href=\"/adm/$udom/$uname/aboutme\">".&Apache::lonnet::absolute_url($ENV{'SERVER_NAME'})."/adm/$udom/$uname/aboutme</a><br />";
+        $info .= "<br /><a href=\"/adm/$udom/$uname/aboutme\">".&Apache::lonnet::absolute_url()."/adm/$udom/$uname/aboutme</a><br />";
         if ($group ne '') {
-            $info .= &mt("Users with privileges to edit course contents may add a course information page to a course using the 'Course Info' button in DOCS").'<br />';
+            $info .= &mt("Users with course editing rights may add a 'Group Portfolio' item using the Course Editor (Collaboration tab), to provide access to viewable group portfolio files.").'<br />';
         }
     } else {
         $header = '<h3>'.&mt('Conditional access controls for file: [_1]',$port_path.$env{'form.currentpath'}.$env{'form.access'}).'</h3>'.
@@ -942,9 +956,11 @@ sub display_access {
 
 sub explain_conditionals {
     return
-        &mt('Conditional files are accessible to logged-in users with accounts in the LON-CAPA network, who satisfy the conditions you set.').'<br />'."\n".
-        &mt('The conditions can include affiliation with a particular course or community, or a user account in a specific domain.').'<br />'."\n".
-        &mt('Alternatively access can be granted to people with specific LON-CAPA usernames and domains.');
+        &mt('Conditional files are accessible to users who satisfy the conditions you set.').'<br /><ul>'.
+        '<li>'.&mt('Conditions can be IP-based, in which case no log-in is required').'</li>'.
+        '<li>'.&mt("Conditions can also be based on a user's status, in which case the user needs an account in the LON-CAPA network, and needs to be logged in.").'<br />'."\n".
+        &mt('The status-based conditions can include affiliation with a particular course or community, or a user account in a specific domain.').'<br />'."\n".
+        &mt('Alternatively access can be granted to people with specific LON-CAPA usernames and domains.').'</li></ul>';
 }
 
 sub view_access_settings {
@@ -985,8 +1001,9 @@ sub build_access_summary {
                       domains => 'Conditional: domain-based',
                       users => 'Conditional: user-based',
                       course => 'Conditional: course/community-based',
+                      ip     => 'Conditional: IP-based',
                      );
-    my @allscopes = ('public','guest','domains','users','course');
+    my @allscopes = ('public','guest','domains','users','course','ip');
     foreach my $scope (@allscopes) {
         if ((!(exists($todisplay{$scope}))) || (ref($todisplay{$scope}) ne 'HASH')) {
             next;
@@ -1063,6 +1080,9 @@ sub build_access_summary {
                 } elsif ($scope eq 'users') {
                     my $curr_user_list = &sort_users($content->{'users'});
                     $r->print(&mt('Users: ').$curr_user_list);
+                } elsif ($scope eq 'ip') {
+                    my $curr_ips_list = &sort_ips($content->{'ip'});
+                    $r->print(&mt('IP(s):').' '.$curr_ips_list);
                 } else {
                     $r->print('&nbsp;');
                 }
@@ -1171,7 +1191,7 @@ sub update_access {
     my $totalnew = 0;
     my $status = 'new';
     my ($firstitem,$lastitem);
-    my @types = ('course','domains','users');
+    my @types = ('course','domains','users','ip');
     foreach my $newitem (@types) {
         $allnew += $env{'form.new'.$newitem};
     }
@@ -1183,6 +1203,7 @@ sub update_access {
            course  => 'course/community',
            domains => 'domain',
            users   => 'user',
+           ip      => 'IP',
         );
         foreach my $newitem (@types) {
             next if ($env{'form.new'.$newitem} <= 0);
@@ -1286,6 +1307,13 @@ sub build_access_record {
 		'udom'  => $udom
 		});
 	}
+    } elsif ($scope eq 'ip') {
+        my $ipslist = $env{'form.ips_'.$num};
+        $ipslist =~ s/\s+//sg;
+        my %ipshash = map { ($_,1) } (split(/,/,$ipslist));
+        foreach my $ip (keys(%ipshash)) {
+            push(@{$record->{'ip'}},$ip);
+        }
     }
     return $record;
 }
@@ -1311,6 +1339,13 @@ sub sort_users {
     return $curr_user_list;
 }
 
+sub sort_ips {
+    my ($ips) = @_;
+    if (ref($ips) eq 'ARRAY') {
+        return join(",\n",sort(@{$ips}));
+    }
+}
+
 sub access_setting_table {
     my ($r,$url,$filename,$access_controls,$action) = @_;
     my ($public,$publictext);
@@ -1320,6 +1355,7 @@ sub access_setting_table {
     my @courses = ();
     my @domains = ();
     my @users = ();
+    my @ips = ();
     my $now = time;
     my $then = $now + (60*60*24*180); # six months approx.
     my ($num,$scope,$publicnum,$guestnum);
@@ -1342,6 +1378,8 @@ sub access_setting_table {
                 push(@domains,$key);
             } elsif ($scope eq 'users') {
                 push(@users,$key);
+            } elsif ($scope eq 'ip') {
+                push(@ips,$key);
             }
         }
         $acl_count{$scope} ++;
@@ -1353,7 +1391,7 @@ sub access_setting_table {
                            $guesttext,$access_controls,%conditionals);
     } else {
         &condition_setting($r,$access_controls,$now,$then,\%acl_count,
-                           \@domains,\@users,\@courses);
+                           \@domains,\@users,\@courses,\@ips);
     }
     $r->print('</td></tr></table>');
 }
@@ -1398,14 +1436,14 @@ sub standard_settings {
     $r->print(&Apache::loncommon::end_data_table_row());
     $r->print(&Apache::loncommon::end_data_table());
     $r->print('</td></tr><tr><td colspan="3">&nbsp;</td></tr>'.
-              '<tr><td colspan="3">');
+              '<tr><td colspan="3" valign="top">');
     my $numconditionals = 0;
     my $conditionstext;
     my %cond_status;
-    foreach my $scope ('domains','users','course') {
+    foreach my $scope ('domains','users','course','ip') {
         $numconditionals += $acl_count->{$scope}; 
         if ($acl_count->{$scope} > 0) {
-            if ($conditionstext ne 'Active') { 
+            if ($conditionstext ne 'Active') {
                 foreach my $key (keys(%{$conditionals{$scope}})) {
                     $conditionstext = &acl_status($start->{$key},$end->{$key},$now);
                     if ($conditionstext eq 'Active') {
@@ -1436,24 +1474,20 @@ sub standard_settings {
         &build_access_summary($r,$count,$chg,%conditionals);
         $r->print(&Apache::loncommon::end_data_table());
     } else {
-        $r->print(&make_anchor($url,\%anchor_fields,&mt('Add conditional access')).' '.&mt('based on domain, username, or course/community affiliation.'));
+        $r->print(&make_anchor($url,\%anchor_fields,&mt('Add conditional access')).' '.&mt("based on domain, username, course/community affiliation or user's IP address."));
     }
 }
 
 sub condition_setting {
-    my ($r,$access_controls,$now,$then,$acl_count,$domains,$users,$courses) = @_;
+    my ($r,$access_controls,$now,$then,$acl_count,$domains,$users,$courses,$ips) = @_;
     $r->print('<tr><td valign="top">');
     &access_element($r,'domains',$acl_count,$domains,$access_controls,$now,$then);
     $r->print('</td><td>&nbsp;</td><td valign="top">');
     &access_element($r,'users',$acl_count,$users,$access_controls,$now,$then);
-    $r->print('</td></tr><tr><td colspan="3"></td></tr><tr>');
-    if ($acl_count->{course} > 0) {
-        $r->print('<td colspan="3" valign="top">');
-    } else {
-        $r->print('<td valign="top">');
-    }
+    $r->print('</td></tr><tr><td colspan="3"></td></tr><tr><td valign="top">');
     &access_element($r,'course',$acl_count,$courses,$access_controls,$now,$then);
-    $r->print('</td>');
+    $r->print('</td><td>&nbsp;</td><td valign="top">');
+    &access_element($r,'ip',$acl_count,$ips,$access_controls,$now,$then);
     $r->print('</td></tr></table>');
 }
 
@@ -1473,7 +1507,8 @@ sub access_element {
     my %typetext = (
         domains => 'Domain',
         users   => 'User',
-        course  => 'Course/Community'
+        course  => 'Course/Community',
+        ip      => 'IP',
     );
     $r->print('<h3>'.&mt($typetext{$type}.'-based conditional access:').' ');
     if ($$acl_count{$type}) {
@@ -1498,6 +1533,9 @@ sub display_access_row {
     } elsif ($type eq 'users') {
         $showtype = &mt('Users');
         $infotype = 'User';
+    } elsif ($type eq 'ip') {
+        $showtype = &mt('IP-based');
+        $infotype = 'IP';  
     }
     if (@{$items} > 0) {
         my @all_doms;
@@ -1507,7 +1545,6 @@ sub display_access_row {
         $r->print('<th>'.&mt('Action?').'</th><th>'.$showtype.'</th><th>'.
               &mt('Dates available').'</th>');
         if ($type eq 'course' && $status eq 'old') {
-            
             $r->print('<th>'.&mt('Allowed course/community affiliations').
                       '</th>');
             $colspan ++;
@@ -1524,6 +1561,8 @@ sub display_access_row {
                             $then);
             } elsif ($type eq 'users') {
                 &users_row($r,$status,$key,$access_controls,$now,$then);
+            } elsif ($type eq 'ip') {
+                &ips_row($r,$status,$key,$access_controls,$now,$then);
             }
 	    $r->print(&Apache::loncommon::end_data_table_row());
         }
@@ -1702,6 +1741,23 @@ sub users_row {
     $r->print('<td>'.&actionbox($status,$num,$scope).'</td><td>'.&mt("Format for users' username:domain information:").'<br /><tt>sparty:msu,illini:uiuc  ... etc.</tt><br /><textarea name="users_'.$num.'" cols="30"  rows="5">'.$curr_user_list.'</textarea></td><td>'.&dateboxes($num,$start,$end).'</td>');
 }
 
+sub ips_row {
+    my ($r,$status,$item,$access_controls,$now,$then) = @_;
+    my ($num,$scope,$end,$start) = &set_identifiers($status,$item,$now,$then,
+                                                    'ip');
+    my $curr_ips_list;
+    if ($status eq 'old') {
+        my $content = $$access_controls{$item};
+        $curr_ips_list = &sort_ips($content->{'ip'});
+    }
+    $r->print('<td>'.&actionbox($status,$num,$scope).'</td><td>'.&mt('Format for IP controls').'<br />'.
+              &mt('[_1] or [_2] or [_3] or [_4] or [_5]','<tt>35.8.*</tt>','<tt>35.8.3.[34-56]</tt>',
+                  '<tt>*.msu.edu</tt>','<tt>35.8.3.34</tt>','<tt>somehostname.pa.msu.edu</tt>').'<br />'.
+              &mt('Use a comma to separate different ranges.').'</br/>'.
+              '<textarea name="ips_'.$num.'" cols="30"  rows="5">'.$curr_ips_list.'</textarea></td>'.
+              '<td>'.&dateboxes($num,$start,$end).'</td>');
+}
+
 sub additional_item {
     my ($type) = @_;
     my $showtype;
@@ -1711,6 +1767,8 @@ sub additional_item {
         $showtype = 'domain';
     } elsif ($type eq 'users') {
         $showtype = 'user';
+    } elsif ($type eq 'ip') {
+        $showtype = 'IP';
     }
     return
         &mt('Add new '.$showtype.'-based condition(s)?')
@@ -1723,7 +1781,7 @@ sub actionbox {
     my $output = '<span class="LC_nobreak"><label>';
     if ($status eq 'new') {
         my $checkstate;
-        if ($scope eq 'domains' || $scope eq 'users' || $scope eq 'course') {
+        if ($scope eq 'domains' || $scope eq 'users' || $scope eq 'course' || $scope eq 'ip') {
             $checkstate = 'checked="checked"';
         }
         $output .= '<input type="checkbox" name="activate" value="'.$num.'" '.
@@ -1735,7 +1793,7 @@ sub actionbox {
                    '<label><input type="checkbox" name="update" value="'.
                    $num.'" />'.&mt('Update');
     }
-    $output .= '</label></span><input type="hidden" name="scope_'.$num.                '" value="'.$scope.'" />';
+    $output .= '</label></span><input type="hidden" name="scope_'.$num.'" value="'.$scope.'" />';
     return $output;
 }
                                                                                    
@@ -2400,7 +2458,7 @@ sub get_group_quota {
     if (%curr_groups) {
         my %group_info =  &Apache::longroup::get_group_settings(
                                                     $curr_groups{$group});
-        $group_quota = $group_info{'quota'}; #expressed in Mb
+        $group_quota = $group_info{'quota'}; #expressed in MB
         if ($group_quota) {
             $group_quota = 1000 * $group_quota; #expressed in k
         }
@@ -2500,13 +2558,15 @@ sub coursegrp_portfolio_header {
     }
     &Apache::lonhtmlcommon::add_breadcrumb
         ({href=>"/adm/$cdom/$cnum/$env{'form.group'}/smppg?ref=$env{'form.ref'}",
-          text=>"$ucgpterm: $grp_desc",
-          title=>"Go to group's home page"},
+          text=>&mt('Group').": $grp_desc",
+          title=>&mt("Go to group's home page"),
+          no_mt=>1},
          {href=>"/adm/coursegrp_portfolio?".&group_args(),
           text=>"Group Portfolio",
           title=>"Display group portfolio"});
     my $output = &Apache::lonhtmlcommon::breadcrumbs(
-                         &mt('[_1] portfolio files - [_2]',$gpterm,$grp_desc));
+                         &mt('Group portfolio files - [_1]',$grp_desc),
+                                                     undef,undef,undef,undef,1);
     return $output;
 }
 
@@ -2522,8 +2582,8 @@ sub get_quota {
         }
     } else {
         $disk_quota = &Apache::loncommon::get_user_quota($env{'user.name'},
-                                    $env{'user.domain'}); #expressed in Mb
-        $disk_quota = 1000 * $disk_quota; # convert from Mb to kb
+                                    $env{'user.domain'}); #expressed in MB
+        $disk_quota = 1024 * $disk_quota; # convert from MB to kB
     }
     return $disk_quota;
 }
@@ -2553,6 +2613,58 @@ STATE
     return $state;
 }
 
+# Find space available in a user's portfolio (convert to bytes)
+sub free_space {
+    my ($group) = @_;
+    my $disk_quota = &get_quota($group); # Expressed in kB
+    my ($uname,$udom) = &get_name_dom($group);
+    my $portfolio_root = &get_portfolio_root();
+    my $getpropath = 1;
+    my $current_disk_usage = &Apache::lonnet::diskusage($udom, $uname,
+                             $portfolio_root, $getpropath); # Expressed in kB
+    my $free_space = 1024 * ($disk_quota - $current_disk_usage);
+    return $free_space;
+}
+
+sub valid_container {
+    my ($uname,$udom,$group) = @_;
+    my $container_prefix;
+    if ($group ne '') {
+        $container_prefix = "/uploaded/$udom/$uname/groups/$group/portfolio";
+    } else {
+        $container_prefix = "/uploaded/$udom/$uname/portfolio";
+    }
+    if ($env{'form.currentpath'}) {
+        $container_prefix .= $env{'form.currentpath'};
+    } else {
+        $container_prefix .= '/';
+    }
+    if ($env{'form.container'} =~ m{^\Q$container_prefix\E(.+)$}) {
+        my $filename = $1;
+        if ($filename eq &Apache::lonnet::clean_filename($filename)) {
+            return 1;
+        }
+    }
+    return;
+}
+
+sub invalid_parms {
+    my ($r,$url,$currentpath) = @_;
+    my $escpath = &HTML::Entities::encode($currentpath,'&<>"');
+    my $rtnlink = '<a href="'.$url;
+    if ($url =~ /\?/) {
+        $rtnlink .= '&';
+    } else {
+        $rtnlink .= '?';
+    }
+    $rtnlink .= 'currentpath='.$escpath;
+    $rtnlink .= '">'.&mt('Return to directory').'</a>';
+    $r->print('<h3>'.&mt('Action disallowed').'</h3>');
+    $r->print(&mt('Some of the data included with this request were invalid'));
+    $r->print('<br />'.$rtnlink);
+    return;
+}
+
 sub handler {
     # this handles file management
     my $r = shift;
@@ -2640,16 +2752,19 @@ sub handler {
     # Give the LON-CAPA page header
     my $brcrum = [{href=>"/adm/portfolio",text=>"Portfolio Manager"}];
 
+    my $js = '<script type="text/javascript"
+                src="/res/adm/includes/file_upload.js"></script>';
+
     if ($env{"form.mode"} eq 'selectfile'){
-        $r->print(&Apache::loncommon::start_page($title,undef,
+        $r->print(&Apache::loncommon::start_page($title, $js,
 						 {'only_body' => 1}));
     } elsif ($env{'form.action'} eq 'rolepicker') {
-        $r->print(&Apache::loncommon::start_page('New role-based condition',undef,
+        $r->print(&Apache::loncommon::start_page('New role-based condition', $js,
                                                  {'no_nav_bar'  => 1, }));
     } elsif ($caller eq 'coursegrp_portfolio') {
-        $r->print(&Apache::loncommon::start_page($title));
+        $r->print(&Apache::loncommon::start_page($title, $js));
     } else {
-        $r->print(&Apache::loncommon::start_page($title,undef,
+        $r->print(&Apache::loncommon::start_page($title, $js,
                                                  {'bread_crumbs' => $brcrum}));
         if (!&Apache::lonnet::usertools_access($uname,$udom,'portfolio')) {
             $r->print('<h2>'.&mt('No user portfolio available') .'</h2>'.
@@ -2663,8 +2778,9 @@ sub handler {
     }
     $r->rflush();
     # Check if access to portfolio is blocked by one or more blocking events in courses.
+    my $clientip = &Apache::lonnet::get_requestor_ip($r);
     my ($blocked,$blocktext) = 
-        &Apache::loncommon::blocking_status('port',$uname,$udom);
+        &Apache::loncommon::blocking_status('port',$clientip,$uname,$udom);
     if ($blocked) {
         my $evade_block;
         # If portfolio display is in a window popped up from a "Select Portfolio Files"
@@ -2699,6 +2815,21 @@ sub handler {
             return OK;
         }
     }
+    if (($env{'form.currentpath'}) && ($env{'form.currentpath'} ne '/')) {
+        my $clean_currentpath = '/'.&Apache::loncommon::clean_path($env{'form.currentpath'}).'/';
+        unless ($env{'form.currentpath'} eq $clean_currentpath) {
+            &invalid_parms($r,$url);
+            $r->print(&Apache::loncommon::end_page());
+            return OK;
+        }
+    }
+    if ($env{'form.container'}) {
+        unless (&valid_container($uname,$udom,$group)) {
+            &invalid_parms($r,$url,$env{'form.currentpath'});
+            $r->print(&Apache::loncommon::end_page());
+            return OK;
+        }
+    }
     if (($env{'form.storeupl'}) & (!$env{'form.uploaddoc.filename'})){
    	$r->print(
             '<p><span class="LC_warning">'
@@ -2711,7 +2842,7 @@ sub handler {
     }
     if ($env{'form.meta'}) {
         &open_form($r,$url);
-        $r->print(&mt('Edit the meta data').'<br />');
+        $r->print(&mt('Edit Metadata').'<br />');
         &close_form($r,$url);
     }
     if ($env{'form.uploaddoc.filename'}) {
@@ -2869,7 +3000,8 @@ sub handler {
 	&display_common($r,$url,$current_path,$is_empty,$dirlistref,
 			$can_upload,$group);
         &display_directory($r,$url,$current_path,$is_empty,$dirlistref,$group,
-                           $can_upload,$can_modify,$can_delete,$can_setacl);
+                           $can_upload,$can_modify,$can_delete,$can_setacl,
+                           $caller);
     }
     $r->print(&Apache::loncommon::end_page());
     return OK;