#!/export2/gnu/files/perl-5.6.0/bin/perl -w # -*- Mode: perl; indent-tabs-mode: nil -*- # # The contents of this file are subject to the Mozilla Public # License Version 1.1 (the "License"); you may not use this file # except in compliance with the License. You may obtain a copy of # the License at http://www.mozilla.org/MPL/ # # Software distributed under the License is distributed on an "AS # IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or # implied. See the License for the specific language governing # rights and limitations under the License. # # The Original Code is Doctor. # # The Initial Developer of the Original Code is Netscape # Communications Corporation. Portions created by Netscape # are Copyright (C) 2002 Netscape Communications Corporation. # All Rights Reserved. # # Contributor(s): Myk Melez ################################################################################ # Script Initialization ################################################################################ # Make it harder to do dangerous things in Perl. use diagnostics; use strict; # Make it easier to access Perl's built-in variables by turning on the option # of referencing them by sensible name instead of punctuation mark. use English; # Include the standard Perl CGI library and create a new request object # to handle this CGI request. use CGI; my $request = new CGI; # Use a useful library for capturing filehandle output into a variable. # http://groups.google.com/groups?selm=yr0e8.12924%24ZC3.1033373%40newsread2.prod.itd.earthlink.net use IO::Capture; # Include the Perl library for creating and deleting directory hierarchies. use File::Path; # Use the Template Toolkit (http://www.template-toolkit.org/) to generate # the user interface using templates in the "template/" subdirectory. use Template; # Create the global template object that processes templates and specify # configuration parameters that apply to templates processed in this script. my $template = Template->new( { # Colon-separated list of directories containing templates. INCLUDE_PATH => "templates" , FILTERS => { linebreak => sub { my ($var) = @_; $var =~ s/\\/\\\\/g; $var =~ s/\n/\\n/g; $var =~ s/\r/\\r/g; return $var; } } } ); # Define the global variables and functions that will be passed to the UI # template. Individual functions add their own values to this hash before # sending them to the templates they process. my $vars = {}; ################################################################################ # Script Configuration ################################################################################ # Note: Look in the configuration file for descriptions of each parameter. # Use the AppConfig module for handling script configuration. use AppConfig qw(:expand :argcount); # Create an AppConfig object and populate it with parameters defined # in the configuration file. my $config = AppConfig->new( { CREATE => 1 , GLOBAL => { ARGCOUNT => ARGCOUNT_ONE , } } ); $config->file("doctor.conf"); # Write parameters into global variables. my $TEMP_DIR = $config->get('TEMP_DIR'); my $READ_CVS_SERVER = $config->get('READ_CVS_SERVER'); my $READ_CVS_USERNAME = $config->get('READ_CVS_USERNAME'); my $READ_CVS_PASSWORD = $config->get('READ_CVS_PASSWORD'); my $WRITE_CVS_SERVER = $config->get('WRITE_CVS_SERVER'); my $WEB_BASE_URI = $config->get('WEB_BASE_URI'); my $WEB_BASE_URI_PATTERN = $config->get('WEB_BASE_URI_PATTERN'); my $WEB_BASE_PATH = $config->get('WEB_BASE_PATH'); # Store the home directory so we can get back to it after changing directories # in certain places in the code. use Cwd; my $HOME = cwd; ################################################################################ # Main Body Execution ################################################################################ # Note: All calls to this script should contain an "action" variable # whose value determines what the user wants to do. The code below # checks the value of that variable and runs the appropriate code. # Determine whether to use the action specified by the user or the default. my $action = lc($request->param('action')) || "edit"; # If the user wants to edit a file, but they haven't specified the name # of the file, prompt them for it. if ($action eq "edit" && !$request->param('file')) { $action = "select" } if ($action eq "select") { print $request->header; $template->process("select.tmpl", $vars) || DisplayError("Template Process Failed", $template->error()) && exit; } elsif ($action eq "edit") { ValidateFile(); $vars->{'file'} = $request->param('file'); if ($request->param('content')) { ValidateContent(); $vars->{'content'} = $request->param('content'); $vars->{'version'} = $request->param('version'); $vars->{'line_endings'} = $request->param('line_endings'); } else { ($vars->{'content'}, $vars->{'version'}, $vars->{'line_endings'}) = RetrieveFile($request->param('file')); } print $request->header; $template->process("edit.tmpl", $vars) || DisplayError("Template Process Failed", $template->error()) && exit; } elsif ($action eq "review") { ValidateFile(); ReviewChanges(); } elsif ($action eq "commit") { ValidateContent(); ValidateUsername(); ValidatePassword(); ValidateComment(); CommitFile(); } else { DisplayError("I could not figure out what you wanted to do.", "Unknown action"); } exit; ################################################################################ # Input Validation ################################################################################ sub ValidateFile { # Make sure a path was entered. my $file = $request->param("file"); $file || DisplayError("You must include the name of the file.") && exit; # URL -> Path Conversion # Remove the absolute URI for files on the web site (if any) # from the beginning of the path. if ($WEB_BASE_URI_PATTERN) { $file =~ s/^$WEB_BASE_URI_PATTERN//i } else { $file =~ s/^\Q$WEB_BASE_URI\E//i } # Entire Path Issues # Collapse multiple consecutive slashes (i.e. dir//file.txt) into a single slash. $file =~ s:/{2,}:/:; # Beginning of Path Issues # Remove a preceding slash. $file =~ s:^/::; # Add the base path of the file in the cvs repository if necessary. # (i.e. if the user entered a URL or a path based on the URL). if ($file !~ /^\Q$WEB_BASE_PATH\E/) { $file = $WEB_BASE_PATH . $file } # End of Path Issues # If the filename (the last name in the path) contains no period, # it is probably a directory, so add a slash. if ($file =~ m:^[^\./]+$: || $file =~ m:/[^\./]+$:) { $file .= "/" } # If the file ends with a forward slash, it is a directory, # so add the name of the default file. if ($file =~ m:/$:) { $file .= "index.html" } # Set the file path. $request->param("file", $file); # Note: we don't need to make sure the file exists at this point # because CVS will tell us that. # Construct a URL to the file if possible. my $url = $file; if ($url =~ s/^\Q$WEB_BASE_PATH\E(.*)$/$1/i) { $vars->{'file_url'} = $WEB_BASE_URI . $url } } sub ValidateUsername { $request->param('username') || DisplayError("You must enter a username.") && exit; my $username = $request->param('username'); # If the username has an at sign in it, convert it to a percentage sign, # since that is probably what the user meant (CVS usernames are often # email addresses in which the at sign has been converted to a percentage sign. $username =~ s/@/%/; $request->param('username', $username); } sub ValidatePassword { $request->param('password') || DisplayError("You must enter a password.") && exit; } sub ValidateComment { $request->param('comment') || DisplayError("You must enter a check-in comment.") && exit; } sub ValidateContent { my $content = $request->param('content'); # The HTML spec tells browsers to remove line breaks (carriage return # and newline characters) from form values in some cases. This happens # even if those characters are encoded into entities ( and ). # To prevent corruption of file content in this case, we have to escape # those characters as \r and \n, so here we convert them back. # # An escaped line break is the literal string "\r" or "\n" with zero # or an even number of slashes before it, because if there are an odd # number of slashes before it, then the total number of slashes is even, # so the slashes are actually escaped slashes followed by a literal # "r" or "n" character, rather than an escaped line break character. # In other words, "\n" and "\\\n" are a line break and a slash followed # by a line break, respectively, but "\\n" is a slash followed by # a literal "n". # $content =~ s/(^|[^\\])((\\\\)*)\\r/$1$2\r/g; $content =~ s/(^|[^\\])((\\\\)*)\\n/$1$2\n/g; $content =~ s/\\\\/\\/g; $request->param('content', $content); } ################################################################################ # Utility Functions ################################################################################ sub RetrieveFile { my ($name) = @_; my @args = ("-d", ":pserver:$READ_CVS_USERNAME:$READ_CVS_PASSWORD\@$READ_CVS_SERVER", "checkout", "-p", # write to STDOUT $name); # Check out the file from the CVS repository, capturing the file content # and any errors/notices. my $CAPTURE_ERR = IO::Capture->new(\*STDERR); my $CAPTURE_OUT = IO::Capture->new(\*STDOUT); my $error_code = system("cvs", @args); my $output = $CAPTURE_OUT->capture; my $errors = $CAPTURE_ERR->capture; if ($error_code != 0) { # Include the command in the error message (but hide the username/password). my $command = join(" ", "cvs", @args); $command =~ s/\Q$READ_CVS_PASSWORD\E/[password]/g; $errors =~ s/\Q$READ_CVS_PASSWORD\E/[password]/g; DisplayCVSError("I could not check the file out from the repository.", $command, $error_code, $errors); exit; } # Extract the file version from the errors/notices. my $version = ""; if ($errors =~ /VERS:\s([0-9.]+)\s/) { $version = $1 } # Figure out the line-ending style by examining the content of the file. my $line_endings = "unix"; if ($output =~ /\r\n/s) { $line_endings = "windows" } elsif ($output =~ /\r[^\n]/s) { $line_endings = "mac" } return ($output, $version, $line_endings); } sub ReviewChanges { # Displays a diff between the version of a file in the repository # and the version submitted by the user so the user can review the # changes they are making to the file. Offers the user the option # to either commit their changes or edit the file some more. # Create and change to a temporary sub-directory into which # we will check out the file being reviewed. CreateTempDir(); # Check out the file from the repository. my $file = $request->param('file'); my $oldversion = $request->param('version'); my $newversion = CheckOutFile($file); # Throw an error if the version of the file that was edited # does not match the version in the repository. In the future # we should try to merge the user's changes if possible. if ($oldversion && $newversion && $oldversion != $newversion) { DisplayError("You edited version $oldversion of the file, but version $newversion is in the repository. Reload the edit page and make your changes again (and bother the authors of this script to implement change merging)."); DeleteTempDir(); exit; } # Replace the checked out file with the edited version, generate # a diff between the edited file, and display it to the user. ReplaceFile($file); $vars->{'diff'} = DiffFile($file); # Delete the temporary directory into which we checked out the file. DeleteTempDir(); $vars->{'file'} = $file; $vars->{'content'} = $request->param('content'); $vars->{'version'} = $request->param('version'); $vars->{'line_endings'} = $request->param('line_endings'); print $request->header; $template->process("review.tmpl", $vars) || DisplayError("Template Process Failed", $template->error()) && exit; } sub CommitFile { # Commit a file to the repository. Checks out the file via cvs, # applies the changes, generates a diff, and then checks the file # back in. It would be easier if we could commit a file by piping # it into standard input, but cvs does not provide for this. # Create and change to a temporary sub-directory into which # we will check out the file being committed. CreateTempDir(); # Check out the file from the repository. my $file = $request->param('file'); my $oldversion = $request->param('version'); my $newversion = CheckOutFile($file); # Throw an error if the version of the file that was edited # does not match the version in the repository. In the future # we should try to merge the user's changes if possible. if ($oldversion && $newversion && $oldversion != $newversion) { DisplayError("You edited version $oldversion of the file, but version $newversion is in the repository. Reload the edit page and make your changes again (and bother the authors of this script to implement change merging)."); DeleteTempDir(); exit; } # Replace the checked out file with the edited version, generate # a diff between the edited file and the version in the repository, # and check in the edited file. ReplaceFile($file); $vars->{'diff'} = DiffFile($file); $vars->{'checkin_results'} = CheckInFile($file); # Delete the temporary directory into which we checked out the file. DeleteTempDir(); $vars->{'file'} = $file; print $request->header; $template->process("committed.tmpl", $vars) || DisplayError("Template Process Failed", $template->error()) && exit; } sub CheckOutFile { # Checks out a file from the repository. my ($file) = @_; my @args = ("-t", # debugging messages from which to extract the version "-d", ":pserver:$READ_CVS_USERNAME:$READ_CVS_PASSWORD\@$READ_CVS_SERVER", "checkout", $file); # Check out the file from the repository, capturing the output of the # command and any error messages/notices. my $CAPTURE_ERR = IO::Capture->new(\*STDERR); my $CAPTURE_OUT = IO::Capture->new(\*STDOUT); my $error_code = system("cvs", @args); my $output = $CAPTURE_OUT->capture; my $errors = $CAPTURE_ERR->capture; if ($error_code != 0) { # Include the command in the error message (but hide the password). my $command = join(" ", "cvs", @args); $command =~ s/\Q$READ_CVS_PASSWORD\E/[password]/g; $errors =~ s/\Q$READ_CVS_PASSWORD\E/[password]/g; DisplayCVSError("I could not check the file out from the repository.", $command, undef, $errors); DeleteTempDir(); exit; } # Extract the file version from the errors/notices. my $version = ""; if ($errors =~ /\Q$file\E,v,\s([0-9.]+),/) { $version = $1 } return $version; } sub ReplaceFile { # Replaces the file checked out from the repository with the edited version. my ($file) = @_; my $content = $request->param('content'); my $line_endings = $request->param('line_endings'); # Replace the Windows-style line endings in which browsers send content # with line endings appropriate to the file being replaced if the file # has Unix- or Mac-style line endings. if ($line_endings eq "unix") { $content =~ s/\r\n/\n/g } if ($line_endings eq "mac") { $content =~ s/\r\n/\r/g } if (!open(DOC, ">$file")) { DisplayError("I could not open the temporary file $file for writing: $!"); DeleteTempDir(); exit; } print DOC $content; close(DOC); } sub DiffFile { # Returns the results of diffing a file against the version in the repository. my ($file) = @_; my @args = ("-d", ":pserver:$READ_CVS_USERNAME:$READ_CVS_PASSWORD\@$READ_CVS_SERVER", "diff", "-u", $file); # Diff the file against the version in the repository, capturing the diff # and any error messages/notices. my $CAPTURE_ERR = IO::Capture->new(\*STDERR); my $CAPTURE_OUT = IO::Capture->new(\*STDOUT); my $error_code = system("cvs", @args); my $output = $CAPTURE_OUT->capture; my $errors = $CAPTURE_ERR->capture; # Check the error messages/notices in addition to the error code # returned by the system call, because for some reason the call # sometimes returns a non-zero error code (256) upon success. if ($error_code && $errors) { # Include the command in the error message (but hide the password). my $command = join(" ", "cvs", @args); $command =~ s/\Q$READ_CVS_PASSWORD\E/[password]/g; $errors =~ s/\Q$READ_CVS_PASSWORD\E/[password]/g; DisplayCVSError("I could not diff your version of the file against the version in the repository.", $command, undef, $errors); DeleteTempDir(); exit; } if (!$output) { DisplayError("You didn't change anything!"); DeleteTempDir(); exit; } return $output; } sub CheckInFile { # Checks a file into the repository. my ($file) = @_; my $username = $request->param('username'); my $password = $request->param('password'); my $comment = $request->param('comment'); my @args = ("-d" , ":pserver:$username:$password\@$WRITE_CVS_SERVER" , "commit" , "-m" , $comment , $file); # Check the file into the repository and capture the results. my $CAPTURE_ERR = IO::Capture->new(\*STDERR); my $CAPTURE_OUT = IO::Capture->new(\*STDOUT); my $error_code = system("cvs", @args); my $output = $CAPTURE_OUT->capture; my $errors = $CAPTURE_ERR->capture; if ($error_code != 0) { # Include the command in the error message (but hide the password). my $command = join(" ", "cvs", @args); $command =~ s/\Q$password\E/[password]/g; $errors =~ s/\Q$password\E/[password]/g; DisplayCVSError("I could not check your version of the file into the repository.", $command, undef, $errors); DeleteTempDir(); exit; } return $output; } sub DisplayError { ($vars->{'message'}, $vars->{'title'}) = @_; chdir($HOME) || die("Could not change back to original working directory to deliver error message $vars->{'title'}: $vars->{'message'}."); print $request->header; $template->process("error.tmpl", $vars) || die("Template Error: " . $template->error() . " while trying to deliver error message $vars->{'title'}: $vars->{'message'}."); } sub DisplayCVSError { ($vars->{'message'}, $vars->{'cvs_command'}, $vars->{'cvs_error_code'}, $vars->{'cvs_error_message'}, $vars->{'title'}) = @_; $vars->{'title'} ||= "CVS Error"; chdir($HOME) || die("Could not change back to original working directory to deliver error message $vars->{'title'}: $vars->{'message'}."); print $request->header; $template->process("cvs-error.tmpl", $vars) || die("Template Error: " . $template->error() . " while trying to deliver error message $vars->{'title'}: $vars->{'message'}."); } sub CreateTempDir { # Creates and changes to a temporary sub-directory unique to this process # that can be used for CVS activities requiring the checking out of files. if (!-e $TEMP_DIR and !mkdir($TEMP_DIR)) { DisplayError("I could not create the temporary directory $TEMP_DIR: $!"); exit; } elsif (!-d $TEMP_DIR) { DisplayError("I could not use the temporary directory $TEMP_DIR because it is not a directory."); exit; } # Create and change to a unique sub-directory in the temporary directory. mkdir("$TEMP_DIR/$PID") || DisplayError("I could not create a temporary sub-directory $TEMP_DIR/$PID: $!") && exit; chdir("$TEMP_DIR/$PID") || DisplayError("I could not change to the temporary sub-directory $TEMP_DIR/$PID: $!") && exit; } sub DeleteTempDir { # Changes back to the home directory and deletes the temporary directory. chdir($HOME); rmtree("$TEMP_DIR/$PID", 0, 1) || DisplayError("I could not delete the temporary sub-directory $TEMP_DIR/$PID: $!") && exit; }