[system] / trunk / webwork2 / lib / WeBWorK / ContentGenerator / Instructor / FileManager.pm Repository:
ViewVC logotype

View of /trunk/webwork2/lib/WeBWorK/ContentGenerator/Instructor/FileManager.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 3330 - (download) (as text) (annotate)
Sat Jul 2 16:52:31 2005 UTC (7 years, 10 months ago) by dpvc
File size: 35617 byte(s)
Major updates to the file manager to allow it to:

1.  Show dates and sizes of files (optionally, since some browsers
    don't handle the CSS to change to a monospaced font).

2.  Provide better control over renaming of uploaded files whose
    names already exist (there is a checkbox for overwriting them
    automatically; if unchecked, the user is prompted for a new name).

3.  Allow the creation or gzipped tar archives from files in the
    course directory.  Multiple files and directories can be selected
    to be included in the archive.  If only one file is selected, the
    archive will have it's name with ".tgz" appended; if mulitple
    files are selected, the archive will get a unique name starting
    with the course ID.

4.  Provide a checkbox that controls whether uploaded .tgz archives
    are unpacked automatically, and a second that controls whether the
    unpacked archive file is deleted afterward.  Files from the
    archive will be unpacked into the current directory, and will
    overwrite existing files silently.

5.  Follow symbolic links that are to files or directories within the
    course hierarchy.  In addition, there is a new variable in
    global.conf that provides a list of "valid links"; these are
    directories to which the FileManager is allowed to follow symbolic
    links.  The system administator can add directories to this list
    in order to allow professors to access limited areas outside their
    course directory (but they still need to have a symblic link
    within their course to those areas in order to view them).

I think this covers all the current FileManager requests, and this
closes bug#791.

    1 ################################################################################
    2 # WeBWorK Online Homework Delivery System
    3 # Copyright © 2000-2003 The WeBWorK Project, http://openwebwork.sf.net/
    4 # $CVSHeader: webwork-modperl/lib/WeBWorK/ContentGenerator/Instructor/FileManager.pm,v 1.8 2005/01/27 04:26:37 dpvc Exp $
    5 #
    6 # This program is free software; you can redistribute it and/or modify it under
    7 # the terms of either: (a) the GNU General Public License as published by the
    8 # Free Software Foundation; either version 2, or (at your option) any later
    9 # version, or (b) the "Artistic License" which comes with this package.
   10 #
   11 # This program is distributed in the hope that it will be useful, but WITHOUT
   12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
   13 # FOR A PARTICULAR PURPOSE. See either the GNU General Public License or the
   14 # Artistic License for more details.
   15 ################################################################################
   16 
   17 package WeBWorK::ContentGenerator::Instructor::FileManager;
   18 use base qw(WeBWorK::ContentGenerator::Instructor);
   19 
   20 use WeBWorK::Utils qw(readDirectory readFile sortByName);
   21 use WeBWorK::Upload;
   22 use File::Path;
   23 use File::Copy;
   24 use File::Spec;
   25 
   26 =head1 NAME
   27 
   28 WeBWorK::ContentGenerator::Instructor::FileManager.pm -- simple directory manager for WW files
   29 
   30 =cut
   31 
   32 use strict;
   33 use warnings;
   34 use CGI;
   35 
   36 #
   37 #  The list of file extensions and the directories they usually go in.
   38 #
   39 my %uploadDir = (
   40   csv  => 'scoring',
   41   lst  => 'templates',
   42   pg   => 'templates/.*',
   43   pl   => 'templates/macros',
   44   def  => 'templates',
   45   html => 'html/.*',
   46 );
   47 
   48 ##################################################
   49 #
   50 # Check that the user is authorized, and then
   51 # see if there is a download to perform.
   52 #
   53 sub pre_header_initialize {
   54   my $self = shift;
   55   my $r = $self->r;
   56   my $authz = $r->authz;
   57   my $user = $r->param('user');
   58 
   59   unless ($authz->hasPermissions($user, "access_instructor_tools")) {
   60     $self->addbadmessage("You aren't authorized to manage course files");
   61     return;
   62   }
   63 
   64   my $action = $r->param('action');
   65   $self->Download if ($action && $action eq 'Download');
   66   my $file = $r->param('download');
   67   $self->downloadFile($file) if (defined $file);
   68 }
   69 
   70 ##################################################
   71 #
   72 # Download a given file
   73 #
   74 sub downloadFile {
   75   my $self = shift;
   76   my $file = checkName(shift);
   77   my $pwd = $self->checkPWD(shift || $self->r->param('pwd') || '.');
   78   return unless $pwd;
   79   $pwd = $self->{ce}{courseDirs}{root} . '/' . $pwd;
   80   unless (-e "$pwd/$file") {
   81     $self->addbadmessage("The file you are trying to download doesn't exist");
   82     return;
   83   }
   84   unless (-f "$pwd/$file") {
   85     $self->addbadmessage("You can only download regular files.");
   86     return;
   87   }
   88   my $type = "application/octet-stream";
   89   $type = "text/plain" if $file =~ m/\.(pg|pl|pm|txt|def|csv|lst)/;
   90   $type = "image/gif"  if $file =~ m/\.gif/;
   91   $type = "image/jpeg" if $file =~ m/\.(jpg|jpeg)/;
   92   $type = "image/png"  if $file =~ m/\.png/;
   93   $self->reply_with_file($type, "$pwd/$file", $file, 0);
   94 }
   95 
   96 ##################################################
   97 #
   98 # The main body of the page
   99 #
  100 sub body {
  101   my $self       = shift;
  102   my $r          = $self->r;
  103   my $urlpath    = $r->urlpath;
  104   my $db         = $r->db;
  105   my $ce         = $r->ce;
  106   my $authz      = $r->authz;
  107   my $courseRoot = $ce->{courseDirs}{root};
  108   my $courseName = $urlpath->arg('courseID');
  109   my $user       = $r->param('user');
  110   my $key        = $r->param('key');
  111 
  112   return CGI::em("You are not authorized to access the instructor tools")
  113     unless $authz->hasPermissions($user, "access_instructor_tools");
  114 
  115   $self->{pwd} = $self->checkPWD($r->param('pwd') || '.');
  116   return CGI::em("You have specified an illegal working directory!") unless defined $self->{pwd};
  117 
  118   my $fileManagerPage = $urlpath->newFromModule($urlpath->module, courseID => $courseName);
  119   my $fileManagerURL  = $self->systemLink($fileManagerPage, authen => 0);
  120 
  121   print CGI::start_multipart_form(
  122     -method=>"POST",
  123     -action=>$fileManagerURL,
  124     -id=>"FileManager",
  125     -name=>"FileManager"
  126   );
  127   print $self->hidden_authen_fields;
  128 
  129   $self->{courseRoot} = $courseRoot;
  130   $self->{courseName} = $courseName;
  131 
  132   my $action = $r->param('action') || $r->param('formAction') || 'Init';
  133 
  134   for ($action) {
  135     /^Refresh/i    and do {$self->Refresh; last};
  136     /^Cancel/i     and do {$self->Refresh; last};
  137     /^\^/i         and do {$self->ParentDir; last};
  138     /^Directory/i  and do {$self->Go; last};
  139     /^Go/i         and do {$self->Go; last};
  140     /^View/i       and do {$self->View; last};
  141     /^Edit/i       and do {$self->Edit; last};
  142     /^Download/i   and do {$self->Refresh; last};
  143     /^Copy/i       and do {$self->Copy; last};
  144     /^Rename/i     and do {$self->Rename; last};
  145     /^Delete/i     and do {$self->Delete; last};
  146     /^GZIP/i       and do {$self->GZIP; last};
  147     /^UNGZIP/i     and do {$self->UNGZIP; last};
  148     /^New Folder/i and do {$self->NewFolder; last};
  149     /^New File/i   and do {$self->NewFile; last};
  150     /^Upload/i     and do {$self->Upload; last};
  151     /^Revert/i     and do {$self->Edit; last};
  152     /^Save As/i    and do {$self->SaveAs; last};
  153     /^Save/i       and do {$self->Save; last};
  154     /^Init/i       and do {$self->Init; last};
  155     $self->addbadmessage("Unknown action.");
  156     $self->Refresh;
  157   }
  158 
  159   print CGI::hidden({name=>'pwd',value=>$self->{pwd}});
  160   print CGI::hidden({name=>'formAction'});
  161   print CGI::end_multipart_form();
  162 
  163   return "";
  164 }
  165 
  166 
  167 ##################################################
  168 #
  169 #  First time through
  170 #
  171 sub Init {
  172   my $self = shift;
  173   $self->r->param('unpack',1);
  174   $self->r->param('autodelete',1);
  175   $self->r->param('format','Automatic');
  176   $self->Refresh;
  177 }
  178 
  179 sub HiddenFlags {
  180   my $self = shift;
  181   print CGI::hidden({name=>"dates",     value=>$self->getFlag('dates')});
  182   print CGI::hidden({name=>"overwrite", value=>$self->getFlag('overwrite')});
  183   print CGI::hidden({name=>"unpack",    value=>$self->getFlag('unpack')});
  184   print CGI::hidden({name=>"autodelete",value=>$self->getFlag('autodelete')});
  185   print CGI::hidden({name=>"format",    value=>$self->getFlag('format','Automatic')});
  186 }
  187 
  188 ##################################################
  189 #
  190 # Display the directory listing and associated buttons
  191 #
  192 sub Refresh {
  193   my $self = shift;
  194   my $pwd = shift || $self->{pwd};
  195   my $isTop = $pwd eq '.' || $pwd eq '';
  196 
  197   my ($dirs,$dirlabels) = directoryMenu($self->{courseName},$pwd);
  198   my ($files,$filelabels) = directoryListing($self->{courseRoot},$pwd,$self->getFlag('dates'));
  199 
  200   unless ($files) {
  201     $self->addbadmessage("The directory you specified doesn't exist");
  202     $files = []; $filelabels = {};
  203   }
  204 
  205   #
  206   # Some JavaScript to make things easier for the user
  207   #
  208   print CGI::script(<<EOF);
  209     function doForm(action) {
  210       var form = window.document.getElementById('FileManager');
  211       form.formAction.value = action;
  212       form.submit();
  213     }
  214     function disableButton(id,state) {
  215       var element = window.document.getElementById(id);
  216       element.disabled = state;
  217     }
  218     function checkFiles() {
  219       var files = window.document.getElementById('files');
  220       var state = files.selectedIndex < 0;
  221       disableButton('View',state);
  222       disableButton('Edit',state);
  223       disableButton('Download',state);
  224       disableButton('Rename',state);
  225       disableButton('Copy',state);
  226       disableButton('Delete',state);
  227       disableButton('GZIP',state);
  228       checkGZIP(files,state);
  229     }
  230     function checkFile() {
  231       var file = window.document.getElementById('file');
  232       var state = (file.value == "");
  233       disableButton('Upload',state);
  234     }
  235     function checkGZIP(files,disabled) {
  236       var gzip = document.getElementById('GZIP');
  237       gzip.value = 'GZIP';
  238       if (disabled) return;
  239       if (!files.childNodes[files.selectedIndex].value.match(/\\.tgz\$/)) return;
  240       for (var i = files.selectedIndex+1; i < files.length; i++)
  241         {if (files.childNodes[i].selected) return}
  242       gzip.value = 'UNGZIP';
  243     }
  244 EOF
  245 
  246   #
  247   # Start the table
  248   #
  249   print CGI::start_table({border=>0,cellpadding=>0,cellspacing=>3, style=>"margin:1em 0 0 3em"});
  250 
  251   #
  252   # Directory menu and date/size checkbox
  253   #
  254   print CGI::Tr(
  255     CGI::td({colspan=>2},
  256       CGI::input({type=>"submit", name=>"action", value => "^", ($isTop? (disabled=>1): ())}),
  257       CGI::popup_menu(
  258         -name => "directory",
  259         -values => $dirs,
  260         -labels => $dirlabels,
  261         -style => "width:25em",
  262         -onChange => "doForm('Go')"
  263       ),
  264       CGI::noscript(CGI::input({type=>"submit",name=>"action",value=>"Go"}))
  265     ),
  266     CGI::td(CGI::small(CGI::checkbox(
  267       -name => 'dates',
  268       -checked => $self->getFlag('dates'),
  269       -value => 1,
  270       -label => 'Show Date & Size',
  271       -onClick => 'doForm("Refresh")',
  272     ))),
  273   );
  274 
  275   #
  276   # Directory Listing and column of buttons
  277   #
  278   my %button = (type=>"submit",name=>"action",style=>"width:10em");
  279   my $width = ($self->getFlag('dates') && scalar(@{$files}) > 0) ? "": " width:30em";
  280   print CGI::Tr({valign=>"middle"},
  281     fixSpaces(CGI::td(CGI::scrolling_list(
  282       -name => "files", id => "files",
  283       -style => "font-family:monospace; $width",
  284       -size => 17,
  285       -multiple => 1,
  286       -values => $files,
  287       -labels => $filelabels,
  288       -onDblClick => "doForm('View')",
  289       -onChange => "checkFiles()"
  290     ))),
  291     CGI::td({width=>15}),
  292     CGI::td(
  293       CGI::start_table({border=>0,cellpadding=>0,cellspacing=>3}),
  294       CGI::Tr([
  295         CGI::td(CGI::input({%button,value=>"View",id=>"View"})),
  296         CGI::td(CGI::input({%button,value=>"Edit",id=>"Edit"})),
  297         CGI::td(CGI::input({%button,value=>"Download",id=>"Download"})),
  298         CGI::td(CGI::input({%button,value=>"Rename",id=>"Rename"})),
  299         CGI::td(CGI::input({%button,value=>"Copy",id=>"Copy"})),
  300         CGI::td(CGI::input({%button,value=>"Delete",id=>"Delete"})),
  301         CGI::td(CGI::input({%button,value=>"GZIP",id=>"GZIP"})),
  302         CGI::td({height=>10}),
  303         CGI::td(CGI::input({%button,value=>"New File"})),
  304         CGI::td(CGI::input({%button,value=>"New Folder"})),
  305         CGI::td(CGI::input({%button,value=>"Refresh"})),
  306       ]),
  307       CGI::end_table(),
  308     ),
  309   );
  310 
  311   #
  312   # Upload button and checkboxes
  313   #
  314   print CGI::Tr([
  315     CGI::td(),
  316     CGI::td({colspan=>3},
  317       CGI::input({type=>"submit",name=>"action",style=>"width:7em",value=>"Upload:",id=>"Upload"}),
  318       CGI::input({type=>"file",name=>"file",id=>"file",size=>40,onChange=>"checkFile()"}),
  319       CGI::br(),
  320       CGI::small(join(' &nbsp; ',"Format:",
  321         CGI::radio_group('format',['Text','Binary','Automatic'],
  322              $self->getFlag('format','Automatic')))),
  323     ),
  324   ]);
  325   print CGI::Tr([
  326     CGI::td(),
  327     CGI::td({colspan=>3},
  328       CGI::small(CGI::checkbox('overwrite',$self->getFlag('overwrite'),1,
  329              'Overwrite existing files silently')),
  330       CGI::br(),
  331       CGI::small(CGI::checkbox('unpack',$self->getFlag('unpack'),1,
  332              'Unpack archives automatically')),
  333       CGI::small(CGI::checkbox('autodelete',$self->getFlag('autodelete'),1,
  334              'then delete them')),
  335     ),
  336   ]);
  337 
  338   #
  339   # End the table
  340   #
  341   print CGI::end_table();
  342   print CGI::script("checkFiles(); checkFile();");
  343 }
  344 
  345 ##################################################
  346 #
  347 # Move to the parent directory
  348 #
  349 sub ParentDir {
  350   my $self = shift;
  351   $self->{pwd} = '.' unless ($self->{pwd} =~ s!/[^/]*$!!);
  352   $self->Refresh;
  353 }
  354 
  355 ##################################################
  356 #
  357 # Move to the parent directory
  358 #
  359 sub Go {
  360   my $self = shift;
  361   $self->{pwd} = $self->r->param('directory');
  362   $self->Refresh;
  363 }
  364 
  365 ##################################################
  366 #
  367 # Open a directory or view a file
  368 #
  369 sub View {
  370   my $self = shift; my $pwd = $self->{pwd};
  371   my $filename = $self->getFile("view"); return unless $filename;
  372   my $name = "$pwd/$filename"; $name =~ s!^\./?!!;
  373   my $file = "$self->{courseRoot}/$pwd/$filename";
  374 
  375   #
  376   # Don't follow symbolic links
  377   #
  378   if ($self->isSymLink($file)) {
  379     $self->addbadmessage("That symbolic link takes you outside your course directory");
  380     $self->Refresh; return;
  381   }
  382 
  383   #
  384   # Handle directories by making them the working directory
  385   #
  386   if (-d $file) {
  387     $self->{pwd} .= '/'.$filename;
  388     $self->Refresh; return;
  389   }
  390 
  391   unless (-f $file) {
  392     $self->addbadmessage("You can't view files of that type");
  393     $self->Refresh; return;
  394   }
  395 
  396   #
  397   # Include a download link
  398   #
  399   my $urlpath = $self->r->urlpath;
  400   my $fileManagerPage = $urlpath->newFromModule($urlpath->module, courseID => $self->{courseName});
  401   my $fileManagerURL  = $self->systemLink($fileManagerPage, params => {download => $filename, pwd => $pwd});
  402   print CGI::div({style=>"float:right"},
  403      CGI::a({href=>$fileManagerURL},"Download"));
  404   print CGI::p(),CGI::b($name),CGI::p();
  405   print CGI::hr();
  406 
  407   #
  408   # For files, display the file, if possible.
  409   # If the file is an image, display it as an image.
  410   #
  411   my $data = readFile($file);
  412   if (isText($data)) {
  413     print CGI::pre(showHTML($data));
  414   } elsif ($file =~ m/\.(gif|jpg|png)/i) {
  415     print CGI::img({src=>$fileManagerURL, border=>0});
  416   } else {
  417     print CGI::div({class=>"ResultsWithError"},
  418       "The file does not appear to be a text file.");
  419   }
  420 }
  421 
  422 ##################################################
  423 #
  424 # Edit a file
  425 #
  426 sub Edit {
  427   my $self = shift;
  428   my $filename = $self->getFile('edit'); return unless $filename;
  429   my $file = "$self->{courseRoot}/$self->{pwd}/$filename";
  430 
  431   if (-d $file) {
  432     $self->addbadmessage("You can't edit a directory");
  433     $self->Refresh; return;
  434   }
  435   unless (-f $file) {
  436     $self->addbadmessage("You can only edit text files");
  437     $self->Refresh; return;
  438   }
  439   my $data = readFile($file);
  440   if (!isText($data)) {
  441     $self->addbadmessage("The file does not appear to be a text file");
  442     $self->Refresh; return;
  443   }
  444 
  445   $self->RefreshEdit($data,$filename);
  446 }
  447 
  448 ##################################################
  449 #
  450 # Save the edited file
  451 #
  452 sub Save {
  453   my $self = shift; my $filename = shift;
  454   my $pwd = $self->{pwd};
  455   if ($filename) {
  456     $pwd = substr($filename,length($self->{courseRoot})+1);
  457     $pwd =~ s!(/|^)([^/]*)$!!; $filename = $2;
  458                 $pwd = '.' if $pwd eq '';
  459   } else {
  460     $filename = $self->getFile("save"); return unless $filename;
  461   }
  462   my $file = "$self->{courseRoot}/$pwd/$filename";
  463   my $data = $self->r->param("data");
  464 
  465   if (defined($data)) {
  466     if (open(OUTFILE,">$file")) {
  467       eval {print OUTFILE $data; close(OUTFILE)};
  468       if ($@) {$self->addbadmessage("Failed to save: $@")}
  469          else {$self->addgoodmessage("File saved")}
  470     } else {$self->addbadmessage("Can't write to file: $!")}
  471   } else {$data = ""; $self->addbadmessage("Error: no file data was submitted!")}
  472 
  473   $self->{pwd} = $pwd;
  474   $self->RefreshEdit($data,$filename);
  475 }
  476 
  477 ##################################################
  478 #
  479 # Save the edited file under a new name
  480 #
  481 sub SaveAs {
  482   my $self = shift;
  483 
  484   my $newfile = $self->r->param('name');
  485   my $original = $self->r->param('files');
  486   $newfile = $self->verifyPath($newfile,$original);
  487   if ($newfile) {$self->Save($newfile); return}
  488   $self->RefreshEdit($self->r->param('data'),$original);
  489 }
  490 
  491 ##################################################
  492 #
  493 # Display the Edit page
  494 #
  495 sub RefreshEdit {
  496   my $self = shift; my $data = shift; my $file = shift;
  497   my $pwd = shift || $self->{pwd};
  498   my $name = "$pwd/$file"; $name =~ s!^\./?!!;
  499 
  500   my %button = (type=>"submit",name=>"action",style=>"width:6em");
  501   print CGI::p();
  502   print CGI::start_table({border=>0,cellspacing=>0,cellpadding=>2, width=>"95%", align=>"center"});
  503   print CGI::Tr([
  504     CGI::td({align=>"center",style=>"background-color:#CCCCCC"},CGI::b($name)),
  505     CGI::td(CGI::textarea(-name=>"data",-default=>$data,-override=>1,-rows=>30,-columns=>80,
  506         -style=>"width:100%")), ## can't seem to get variable height to work
  507     CGI::td({align=>"center", nowrap=>1},
  508       CGI::input({%button,value=>"Cancel"}),"&nbsp;",
  509       CGI::input({%button,value=>"Revert"}),"&nbsp;",
  510       CGI::input({%button,value=>"Save As:"}),
  511       CGI::input({type=>"text",name=>"name",size=>20,style=>"width:50%"}),"&nbsp;",
  512       CGI::input({%button,value=>"Save"}),
  513     ),
  514   ]);
  515   print CGI::end_table();
  516   print CGI::hidden({name=>"files",     value=>$file});
  517   $self->SaveHiddenFlags;
  518 }
  519 
  520 ##################################################
  521 #
  522 # Copy a file
  523 #
  524 sub Copy {
  525   my $self = shift;
  526         my $dir = "$self->{courseRoot}/$self->{pwd}";
  527   my $original = $self->getFile('copy'); return unless $original;
  528   my $oldfile = "$dir/$original";
  529 
  530   if (-d $oldfile) {
  531     # FIXME: need to do recursive directory copy
  532     $self->addbadmessage("Directory copies are not yet implemented");
  533     $self->Refresh;
  534     return;
  535   }
  536 
  537   if ($self->r->param('confirmed')) {
  538     my $newfile = $self->r->param('name');
  539     if ($newfile = $self->verifyPath($newfile,$original)) {
  540       if (copy($oldfile, $newfile)) {
  541         $self->addgoodmessage("File successfully copied");
  542         $self->Refresh; return;
  543       } else {$self->addbadmessage("Can't copy file: $!")}
  544     }
  545   }
  546 
  547   $self->Confirm("Copy file as:",$original,"Copy");
  548   print CGI::hidden({name=>"files",value=>$original});
  549 }
  550 
  551 ##################################################
  552 #
  553 # Rename a file
  554 #
  555 sub Rename {
  556   my $self = shift;
  557         my $dir = "$self->{courseRoot}/$self->{pwd}";
  558   my $original = $self->getFile('rename'); return unless $original;
  559   my $oldfile = "$dir/$original";
  560 
  561   if ($self->r->param('confirmed')) {
  562     my $newfile = $self->r->param('name');
  563     if ($newfile = $self->verifyPath($newfile,$original)) {
  564       if (rename $oldfile, $newfile) {
  565         $self->addgoodmessage("File successfully renamed");
  566         $self->Refresh; return;
  567       } else {$self->addbadmessage("Can't rename file: $!")}
  568     }
  569   }
  570 
  571   $self->Confirm("Rename file as:",uniqueName($dir,$original),"Rename");
  572   print CGI::hidden({name=>"files",value=>$original});
  573 }
  574 
  575 ##################################################
  576 #
  577 # Delete a file
  578 #
  579 sub Delete {
  580   my $self = shift;
  581   my @files = $self->r->param('files');
  582   if (scalar(@files) == 0) {
  583     $self->addbadmessage("You must select at least one file to delete");
  584     $self->Refresh; return;
  585   }
  586 
  587   my $pwd = $self->{pwd};
  588   my $dir = $self->{courseRoot}.'/'.$pwd;
  589   if ($self->r->param('confirmed')) {
  590 
  591     #
  592     # If confirmed, go ahead and delete the files
  593     #
  594     foreach my $file (@files) {
  595       if (defined $self->checkPWD("$pwd/$file",1)) {
  596         if (-d "$dir/$file") {
  597           my $removed = eval {rmtree("$dir/$file",0,1)};
  598           if ($removed) {$self->addgoodmessage("Directory '$file' removed (items deleted: $removed)")}
  599           else {$self->addbadmessage("Directory '$file' not removed: $!")}
  600         } else {
  601           if (unlink("$dir/$file")) {$self->addgoodmessage("File '$file' successfully removed")}
  602           else {$self->addbadmessage("File '$file' not removed: $!")}
  603         }
  604       } else {$self->addbadmessage("Illegal file '$file' specified"); last}
  605     }
  606     $self->Refresh;
  607 
  608   } else {
  609 
  610     #
  611     # Put up the confirmation dialog box
  612     #
  613     print CGI::start_table({border=>1,cellspacing=>2,cellpadding=>20, style=>"margin: 1em 0 0 5em"});
  614     print CGI::Tr(
  615       CGI::td(
  616         CGI::b("Warning:")," You have requested that the following items be deleted\n",
  617         CGI::ul(CGI::li(\@files)),
  618           ((grep { -d "$dir/$_" } @files)?
  619         CGI::p({style=>"width:500"},"Some of these files are directories. ",
  620          "Only delete directories if you really know what you are doing. ",
  621          "You can seriously damage your course if you delete the wrong thing."): ""),
  622         CGI::p({style=>"color:red"},"There is no undo for deleting files or directories!"),
  623         CGI::p("Really delete the items listed above?"),
  624         CGI::div({style=>"float:left; padding-left:3ex"},
  625           CGI::input({type=>"submit",name=>"action",value=>"Cancel"})),
  626         CGI::div({style=>"float:right; padding-right:3ex"},
  627           CGI::input({type=>"submit",name=>"action",value=>"Delete"})),
  628       ),
  629     );
  630     print CGI::end_table();
  631 
  632     print CGI::hidden({name=>"confirmed",value=>1});
  633     foreach my $file (@files) {print CGI::hidden({name=>"files",value=>$file})}
  634     $self->HiddenFlags;
  635   }
  636 }
  637 
  638 ##################################################
  639 #
  640 # Make a gzipped tar archive
  641 #
  642 sub GZIP {
  643   my $self = shift;
  644   my @files = $self->r->param('files');
  645   if (scalar(@files) == 0) {
  646     $self->addbadmessage("You must select at least one file to GZIP");
  647     $self->Refresh; return;
  648   }
  649 
  650   my $dir = $self->{courseRoot}.'/'.$self->{pwd};
  651   my $archive = uniqueName($dir,(scalar(@files) == 1)?
  652          $files[0].".tgz": $self->{courseName}.".tgz");
  653   my $tar = "cd '$dir' && $self->{ce}{externalPrograms}{tar} -czf $archive ";
  654   my $files = `$tar`; chomp($files);
  655   if ($? == 0) {
  656     my @files = split(/\n/,$files);
  657     my $n = scalar(@files); my $s = ($n == 1? "": "s");
  658     $self->addgoodmessage("Archive '$archive' created successfully ($n file$s)");
  659   } else {
  660     $self->addbadmessage("Can't create archive '$archive': comand returned ".systemError($?));
  661   }
  662   $self->Refresh;
  663 }
  664 
  665 ##################################################
  666 #
  667 # Unpack a gzipped tar archive
  668 #
  669 sub UNGZIP {
  670   my $self = shift;
  671   my $archive = $self->getFile("UNGZIP"); return unless $archive;
  672   if ($archive !~ m/\.tgz$/) {
  673     $self->addbadmessage("You can only unpack files ending in '.tgz'");
  674   } else {
  675     $self->ungzip($archive);
  676   }
  677   $self->Refresh;
  678 }
  679 
  680 sub ungzip {
  681   my $self = shift;
  682   my $archive = shift;
  683   my $dir = $self->{courseRoot}.'/'.$self->{pwd};
  684   my $tar = "cd '$dir' && $self->{ce}{externalPrograms}{tar} -vxzf $archive";
  685                my $files = `$tar`; chomp($files);
  686   if ($? == 0) {
  687     my @files = split(/\n/,$files);
  688     my $n = scalar(@files); my $s = ($n == 1? "": "s");
  689     $self->addgoodmessage("$n file$s unpacked successfully");
  690     return 1;
  691   } else {
  692     $self->addbadmessage("Can't unpack '$archive': command returned ".systemError($?));
  693     return 0;
  694   }
  695 }
  696 
  697 ##################################################
  698 #
  699 # Make a new file and edit it
  700 #
  701 sub NewFile {
  702   my $self = shift;
  703 
  704   if ($self->r->param('confirmed')) {
  705     my $name = $self->r->param('name');
  706     if (my $file = $self->verifyName($name,"file")) {
  707       if (open(NEWFILE,">$file")) {
  708         close(NEWFILE);
  709         $self->RefreshEdit("",$name);
  710         return;
  711       } else {$self->addbadmessage("Can't create file: $!")}
  712     }
  713   }
  714 
  715   $self->Confirm("New file name:","","New File");
  716 }
  717 
  718 ##################################################
  719 #
  720 # Make a new directory
  721 #
  722 sub NewFolder {
  723   my $self = shift;
  724 
  725   if ($self->r->param('confirmed')) {
  726     my $name = $self->r->param('name');
  727     if (my $dir = $self->verifyName($name,"directory")) {
  728       if (mkdir $dir, 0750) {
  729         $self->{pwd} .= '/'.$name;
  730         $self->Refresh; return;
  731       } else {$self->addbadmessage("Can't create directory: $!")}
  732     }
  733   }
  734 
  735   $self->Confirm("New folder name:","","New Folder");
  736 }
  737 
  738 ##################################################
  739 #
  740 # Download a file
  741 #
  742 sub Download {
  743   my $self = shift;
  744   my $pwd = $self->checkPWD($self->r->param('pwd') || '.');
  745   return unless $pwd;
  746   my $filename = $self->getFile("download"); return unless $filename;
  747   my $file = $self->{ce}{courseDirs}{root}.'/'.$pwd.'/'.$filename;
  748 
  749   if (-d $file) {$self->addbadmessage("You can't download directories"); return}
  750   unless (-f $file) {$self->addbadmessage("You can't download files of that type"); return}
  751 
  752   $self->r->param('download',$filename);
  753 }
  754 
  755 ##################################################
  756 #
  757 # Upload a file to the server
  758 #
  759 sub Upload {
  760   my $self = shift;
  761   my $dir = "$self->{courseRoot}/$self->{pwd}";
  762   my $fileIDhash = $self->r->param('file');
  763   unless ($fileIDhash) {
  764     $self->addbadmessage("You have not chosen a file to upload.");
  765     $self->Refresh;
  766     return;
  767   }
  768 
  769   my ($id,$hash) = split(/\s+/,$fileIDhash);
  770   my $upload = WeBWorK::Upload->retrieve($id,$hash,dir=>$self->{ce}{webworkDirs}{uploadCache});
  771 
  772   my $name = checkName($upload->filename);
  773   my $action = $self->r->param("formAction") || "Cancel";
  774   if ($self->r->param("confirmed")) {
  775     if ($action eq "Cancel") {
  776       $upload->dispose;
  777       $self->Refresh;
  778       return;
  779     }
  780     $name = checkName($self->r->param('name')) if ($action eq "Rename");
  781   }
  782 
  783   if (-e "$dir/$name") {
  784     unless ($self->r->param('overwrite') || $action eq "Overwrite") {
  785       $self->Confirm("File ".CGI::b($name)." already exists. Overwrite it, or rename it as:".
  786                CGI::p(),uniqueName($dir,$name),"Rename","Overwrite");
  787       print CGI::hidden({name=>"action",value=>"Upload"});
  788       print CGI::hidden({name=>"file",value=>$fileIDhash});
  789       return;
  790     }
  791   }
  792   $self->checkFileLocation($name,$self->{pwd});
  793 
  794   my $file = "$dir/$name";
  795   my $type = $self->getFlag('format','Automatic');
  796   my $data;
  797 
  798   #
  799   #  Check if we need to convert linebreaks
  800   #
  801   if ($type ne 'Binary') {
  802     my $fh = $upload->fileHandle;
  803     my @lines = <$fh>; $data = join('',@lines);
  804     if ($type eq 'Automatic') {$type = isText($data) ? 'Text' : 'Binary'}
  805   }
  806   if ($type eq 'Text') {
  807     $upload->dispose;
  808     $data =~ s/\r\n?/\n/g;
  809     open(UPLOAD,">$file") || $self->addbadmessage("Can't create file '$name'");
  810     print UPLOAD $data; close(UPLOAD);
  811   } else {
  812     $upload->disposeTo($file);
  813   }
  814 
  815   if (-e $file) {
  816     $self->addgoodmessage("$type file '$name' uploaded successfully");
  817     if ($name =~ m/\.tgz$/ && $self->getFlag('unpack')) {
  818       if ($self->ungzip($name) && $self->getFlag('autodelete')) {
  819         if (unlink($file)) {$self->addgoodmessage("Archive '$name' deleted")}
  820           else {$self->addbadmessage("Can't delete archive '$name': $!")}
  821       }
  822     }
  823   }
  824 
  825   $self->Refresh;
  826 }
  827 
  828 ##################################################
  829 ##################################################
  830 #
  831 # Print a confirmation dialog box
  832 #
  833 sub Confirm {
  834   my $self = shift;
  835   my $message = shift; my $value = shift;
  836   my $button = shift; my $button2 = shift;
  837 
  838   print CGI::p();
  839   print CGI::start_table({border=>1,cellspacing=>2,cellpadding=>20, style=>"margin: 1em 0 0 3em"});
  840   print CGI::Tr(
  841     CGI::td({align=>"CENTER"},
  842       $message,
  843       CGI::input({type=>"text",name=>"name",size=>50,value=>$value}),
  844       CGI::p(), CGI::center(
  845         CGI::div({style=>"float:right; padding-right:3ex"},
  846           CGI::input({type=>"submit",name=>"formAction",value=>$button})), # this will be the default
  847         CGI::div({style=>"float:left; padding-left:3ex"},
  848         CGI::input({type=>"submit",name=>"formAction",value=>"Cancel"})),
  849         ($button2 ? CGI::input({type=>"submit",name=>"formAction",value=>$button2}): ()),
  850       ),
  851     ),
  852         );
  853   print CGI::end_table();
  854   print CGI::hidden({name=>"confirmed", value=>1});
  855   $self->HiddenFlags;
  856   print CGI::script("window.document.FileManager.name.focus()");
  857 }
  858 
  859 ##################################################
  860 ##################################################
  861 #
  862 # Check that there is exactly one valid file
  863 #
  864 sub getFile {
  865   my $self = shift; my $action = shift;
  866   my @files = $self->r->param("files");
  867   if (scalar(@files) > 1) {
  868     $self->addbadmessage("You can only $action one file at a time.");
  869     $self->Refresh unless $action eq 'download';
  870     return;
  871   }
  872   if (scalar(@files) == 0 || $files[0] eq "") {
  873     $self->addbadmessage("You need to select a file to $action.");
  874     $self->Refresh unless $action eq 'download';
  875     return;
  876   }
  877   my $pwd = $self->checkPWD($self->{pwd} || $self->r->param('pwd') || '.') || '.';
  878   if ($self->isSymLink($pwd.'/'.$files[0])) {
  879     $self->addbadmessage("That symbolic link takes you outside your course directory");
  880     $self->Refresh unless $action eq 'download';
  881     return;
  882   }
  883   unless ($self->checkPWD($pwd.'/'.$files[0],1)) {
  884     $self->addbadmessage("You have specified an illegal file");
  885     $self->Refresh unless $action eq 'download';
  886     return;
  887   }
  888   return $files[0];
  889 }
  890 
  891 ##################################################
  892 #
  893 # Get the entries for the directory menu
  894 #
  895 sub directoryMenu {
  896   my $course = shift;
  897   my $dir  = shift; $dir =~ s!^\.(/|$)!!;
  898   my @dirs = split('/',$dir);
  899   my $menu = ""; my $pwd;
  900 
  901   my (@values,%labels);
  902   while (scalar(@dirs)) {
  903     $pwd = join('/',(@dirs)[0..$#dirs]);
  904     $dir = pop(@dirs);
  905     push(@values,$pwd); $labels{$pwd} = $dir;
  906   }
  907   push(@values,'.'); $labels{'.'} = $course;
  908   return (\@values,\%labels);
  909 }
  910 
  911 ##################################################
  912 #
  913 # Get the directory listing
  914 #
  915 sub directoryListing {
  916   my $root = shift; my $pwd = shift; my $showdates = shift;
  917   my $dir = $root.'/'.$pwd;
  918   my (@values,%labels,$size,$data);
  919 
  920   return unless -d $dir;
  921         my $len = 24;
  922   my @names = sortByName(undef,grep(/^[^.]/,readDirectory($dir)));
  923   foreach my $name (@names) {
  924     unless ($name eq 'DATA') {   #FIXME don't view the DATA directory
  925       my $file = "$dir/$name";
  926       push(@values,$name); $labels{$name} = $name;
  927       $labels{$name} .= '@' if (-l $file);
  928       $labels{$name} .= '/' if (-d $file && !-l $file);
  929       $len = length($labels{$name}) if length($labels{$name}) > $len;
  930     }
  931   }
  932   if ($showdates) {
  933     $len += 3;
  934     foreach my $name (@values) {
  935       my $file = "$dir/$name";
  936       my ($size,$date) = (lstat($file))[7,9];
  937       $labels{$name} = sprintf("%-${len}s%-16s%10s",$labels{$name},
  938              ((-d $file)? ("",""):
  939               (getDate($date),getSize($size))));
  940     }
  941   }
  942   return (\@values,\%labels);
  943 }
  944 
  945 sub getDate {
  946   my ($sec,$min,$hour,$day,$month,$year) = localtime(shift);
  947   sprintf("%02d-%02d-%04d %02d:%02d",$month+1,$day,$year+1900,$hour,$min);
  948 }
  949 
  950 sub getSize {
  951   my $size = shift;
  952   return $size." B "                        if $size < 1024;
  953   return sprintf("%.1f KB",$size/1024)      if $size < 1024*100;
  954   return sprintf("%d KB",int($size/1024))   if $size < 1024*1024;
  955   return sprintf("%.1f MB",$size/1024/1024) if $size < 1024*1024*100;
  956   return sprintf("%d MB",$size/1024/1024);
  957 }
  958 
  959 ##################################################
  960 #
  961 #  Check if a file is a symbolic link that we
  962 #  are not allowed to follow.
  963 #
  964 sub isSymLink {
  965   my $self = shift; my $file = shift;
  966   return 0 unless -l $file;
  967 
  968   my $courseRoot = $self->{ce}{courseDirs}{root};
  969   $courseRoot = readlink($courseRoot) if -l $courseRoot;
  970   my $pwd = $self->{pwd} || $self->r->param('pwd') || '.';
  971   my $link = File::Spec->rel2abs(readlink($file),"$courseRoot/$pwd");
  972   #
  973   # Remove /./ and dir/../ constructs
  974   #
  975   $link =~ s!(^|/)(\.(/|$))+!$1!g;
  976   while ($link =~ s!((\.[^./]+|\.\.[^/]+|[^./][^/]*)/\.\.(/|$))!!) {};
  977 
  978   #
  979   # Link is OK if it is in the course directory
  980   #
  981   return 0 if substr($link,0,length($courseRoot)) eq $courseRoot;
  982 
  983   #
  984   # Look through the list of valid paths to see if this link is OK
  985   #
  986   my $valid = $self->{ce}{webworkDirs}{valid_symlinks};
  987   if (defined $valid && $valid) {
  988     foreach my $path (@{$valid}) {
  989       return 0 if substr($link,0,length($path)) eq $path;
  990     }
  991   }
  992 
  993   return 1;
  994 }
  995 
  996 ##################################################
  997 #
  998 # Normalize the working directory and check if it is OK.
  999 #
 1000 sub checkPWD {
 1001   my $self = shift;
 1002   my $pwd = shift;
 1003   my $renameError = shift;
 1004 
 1005   $pwd =~ s!//+!/!g;               # remove duplicate slashes
 1006   $pwd =~ s!(^|/)~!$1_!g;          # remove ~user references
 1007   $pwd =~ s!(^|/)(\.(/|$))+!$1!g;  # remove dot directories
 1008 
 1009   # remove dir/.. constructions
 1010   while ($pwd =~ s!((\.[^./]+|\.\.[^/]+|[^./][^/]*)/\.\.(/|$))!!) {};
 1011 
 1012   $pwd =~ s!/$!!;                        # remove trailing /
 1013   return if ($pwd =~ m!(^|/)\.\.(/|$)!); # Error if outside the root
 1014 
 1015   # check for bad symbolic links
 1016   my @dirs = split('/',$pwd);
 1017   pop(@dirs) if $renameError;      # don't check file iteself in this case
 1018   my @path = ($self->{ce}{courseDirs}{root});
 1019   foreach my $dir (@dirs) {
 1020     push @path,$dir;
 1021     return if ($self->isSymLink(join('/',@path)));
 1022   }
 1023 
 1024   my $original = $pwd;
 1025   $pwd =~ s!(^|/)\.!$1_!g;         # don't enter hidden directories
 1026   $pwd =~ s!^/!!;                  # remove leading /
 1027   $pwd =~ s![^-_./A-Z0-9~ ]!_!gi;  # no illegal characters
 1028   return if $renameError && $original ne $pwd;
 1029 
 1030   $pwd = '.' if $pwd eq '';
 1031   return $pwd;
 1032 }
 1033 
 1034 ##################################################
 1035 #
 1036 # Check that a file is uploaded to the correct directory
 1037 #
 1038 sub checkFileLocation {
 1039     my $self = shift;
 1040   my $extension = shift; $extension =~ s/.*\.//;
 1041   my $dir = shift;
 1042   return unless defined($uploadDir{$extension});
 1043   return if $dir =~ m/^$uploadDir{$extension}$/;
 1044   $dir = $uploadDir{$extension}; $dir =~ s!/\.\*!!;
 1045   $self->addbadmessage("Files with extension '.$extension' usually belong in '$dir'");
 1046 }
 1047 
 1048 ##################################################
 1049 #
 1050 # Check a name for bad characters, etc.
 1051 #
 1052 sub checkName {
 1053   my $file = shift;
 1054   $file =~ s!.*[/\\]!!;               # remove directory
 1055   $file =~ s/[^-_.a-zA-Z0-9 ]/_/g;    # no illegal characters
 1056   $file =~ s/^\./_/;                  # no initial dot
 1057   $file = "newfile.txt" unless $file; # no blank names
 1058   return $file;
 1059 }
 1060 
 1061 ##################################################
 1062 #
 1063 # Get a unique name (in case it already exists)
 1064 #
 1065 sub uniqueName {
 1066   my $dir = shift; my $name = shift;
 1067   return $name unless (-e "$dir/$name");
 1068   my $type = ""; my $n = 1;
 1069   $type = $1 if ($name =~ s/(\.[^.]*)$//);
 1070   $n = $1 if ($name =~ s/_(\d+)$/_/);
 1071   while (-e "$dir/${name}_$n$type") {$n++}
 1072   return "${name}_$n$type";
 1073 }
 1074 
 1075 ##################################################
 1076 #
 1077 # Verify that a name can be added tot he current
 1078 # directory.
 1079 #
 1080 sub verifyName {
 1081   my $self = shift; my $name = shift; my $object = shift;
 1082   if ($name) {
 1083     unless ($name =~ m!/!) {
 1084       unless ($name =~ m!^\.!) {
 1085         unless ($name =~ m![^-_.a-zA-Z0-9 ]!) {
 1086           my $file = "$self->{courseRoot}/$self->{pwd}/$name";
 1087           return $file unless (-e $file);
 1088           $self->addbadmessage("A file with that name already exists");
 1089         } else {$self->addbadmessage("Your $object name contains illegal characters")}
 1090       } else {$self->addbadmessage("Your $object name may not begin with a dot")}
 1091     } else {$self->addbadmessage("Your $object name may not contain a path component")}
 1092   } else {$self->addbadmessage("You must specify a $object name")}
 1093   return
 1094 }
 1095 
 1096 ##################################################
 1097 #
 1098 # Verify that a file path is valid
 1099 #
 1100 sub verifyPath {
 1101   my $self = shift; my $path = shift; my $name = shift;
 1102 
 1103   if ($path) {
 1104     unless ($path =~ m![^-_.a-zA-Z0-9 /]!) {
 1105       unless ($path =~ m!^/!) {
 1106         $path = $self->checkPWD($self->{pwd}.'/'.$path,1);
 1107         if ($path) {
 1108           $path = $self->{courseRoot}.'/'.$path;
 1109           $path .= '/'.$name if -d $path && $name;
 1110           return $path unless (-e $path);
 1111           $self->addbadmessage("A file with that name already exists");
 1112         } else {$self->addbadmessage("You have specified an illegal path")}
 1113       } else {$self->addbadmessage("You can not specify an absolute path")}
 1114     } else {$self->addbadmessage("Your file name contains illegal characters")}
 1115   } else {$self->addbadmessage("You must specify a file name")}
 1116   return
 1117 }
 1118 
 1119 ##################################################
 1120 #
 1121 # Get the value of a parameter flag
 1122 #
 1123 sub getFlag {
 1124   my $self = shift; my $flag = shift;
 1125   my $default = shift; $default = 0 unless defined $default;
 1126   my $value = $self->r->param($flag);
 1127   $value = $default unless defined $value;
 1128   return $value;
 1129 }
 1130 
 1131 ##################################################
 1132 #
 1133 # Make HTML symbols printable
 1134 #
 1135 sub showHTML {
 1136   my $string = shift;
 1137   return '' unless defined $string;
 1138   $string =~ s/&/\&amp;/g;
 1139   $string =~ s/</\&lt;/g;
 1140   $string =~ s/>/\&gt;/g;
 1141   $string;
 1142 }
 1143 
 1144 ##################################################
 1145 #
 1146 # Check if a string is plain text
 1147 # (i.e., doesn't contain three non-regular
 1148 # characters in a row.)
 1149 #
 1150 sub isText {
 1151   my $string = shift;
 1152   return $string !~ m/[^\s\x20-\x7E]{3,}/;
 1153 }
 1154 
 1155 ##################################################
 1156 #
 1157 #  Convert spaces to &nbsp;, but only REAL spaces
 1158 #
 1159 sub sp2nbsp {
 1160   my $s = shift;
 1161   $s =~ s/ /\&nbsp;/g;
 1162   return $s;
 1163 }
 1164 
 1165 ##################################################
 1166 #
 1167 #  Hack to convert multiple spaces in the file
 1168 #  selection box into &nbsp; so that the columns
 1169 #  will allign properly in fixed-width fonts.
 1170 #  We have to do it agter the fact, since CGI::
 1171 #  is being "helpful" by turning & in the labels
 1172 #  into &amp; for us.  So we have to convert
 1173 #  after the <SELECT> is created (ugh).
 1174 #
 1175 sub fixSpaces {
 1176   my $s = shift;
 1177   $s =~ s!(<option[^>]*>)(.*?)(</option>)!$1.sp2nbsp($2).$3!gei;
 1178   return $s;
 1179 }
 1180 
 1181 ##################################################
 1182 #
 1183 #  Interpret command return errors
 1184 #
 1185 sub systemError {
 1186   my $status = shift;
 1187   return "error: $!" if $status == 0xFF00;
 1188   return "exit status ".($status >> 8) if ($status & 0xFF) == 0;
 1189   return "signal ".($status &= ~0x80);
 1190 }
 1191 
 1192 ##################################################
 1193 
 1194 1;

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9