[system] / branches / rel-2-4-dev / webwork2 / lib / WeBWorK / ContentGenerator / Instructor / FileManager.pm Repository:
ViewVC logotype

View of /branches/rel-2-4-dev/webwork2/lib/WeBWorK/ContentGenerator/Instructor/FileManager.pm

Parent Directory Parent Directory | Revision Log Revision Log


Revision 5417 - (download) (as text) (annotate)
Sat Aug 25 18:23:08 2007 UTC (5 years, 8 months ago) by sh002i
File size: 37680 byte(s)
backport (dpvc): Work around IE bug where pressing return submits a form
but does not set the submit button's value properly in some
circumstances.

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

aubreyja at gmail dot com
ViewVC Help
Powered by ViewVC 1.0.9