#!/usr/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 # Frédéric Buclin ################################################################################ # Script Initialization ################################################################################ # Make it harder to do dangerous things in Perl. use strict; use lib "."; use Doctor qw(%CONFIG); use Doctor::File; use Doctor::Error; my $request = Doctor->cgi; my $template = Doctor->template; my $vars = {}; $vars->{'config'} = \%CONFIG; ################################################################################ # Main Body Execution ################################################################################ # 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. my $action = lc($request->param('action')); if (!$action) { $action = $request->param('file') ? "edit" : "choose"; } # Displays a form for choosing the page you want to edit. if ($action eq "choose") { choose() } # Displays a page with UI for editing the page online, downloading it # for local editing, viewing the original and the user's modified versions, # getting a diff of the user's changes, and committing the user's changes # or submitting them to the editors for review. This is the principle # interface to Doctor functionality, and many other actions are called # from links, forms, and JavaScript on this page. elsif ($action eq "edit") { edit() } # Retrieves a page from CVS and returns it. Called by the "download the page" # link on the Edit page for users who want to download and edit pages locally # instead of via the embedded textarea. Also called by the View Original panel # of the Edit page to show the original version of the page being edited. elsif ($action eq "download" || $action eq "display") { retrieve() } # Generates and returns a diff of changes between the submitted version # of a page and the version in CVS. Called by the Show Diff panel of the # Edit page. elsif ($action eq "diff" || $action eq "download-diff") { diff() } # Returns the content that was submitted to it. Useful for displaying # the modified version of a page the user downloaded and edited locally, # since for security reasons there's no way to get access to a file # in a file upload control on the client side. Called by JavaScript # when the user focuses the View Edited panel on the Edit page after entering # a filename into the file upload control. elsif ($action eq "regurgitate") { regurgitate() } # Submits a change to the editors for review. Requires the EDITOR_EMAIL config # parameter to be set to the editors' email addresses. elsif ($action eq "queue") { queue() } # Commits changes to a (possibly new) page to the repository. elsif ($action eq "create" || $action eq "commit") { commit() } else { ThrowCodeError("couldn't recognize the value of the action parameter", "Unknown Action"); } exit; ################################################################################ # Main Execution Functions ################################################################################ sub choose { print $request->header; $template->process("select.tmpl", $vars) || ThrowCodeError($template->error(), "Template Processing Failed"); } sub edit { $vars->{file} = Doctor::File->new($request->param('file')); print $request->header; $template->process("edit.tmpl", $vars) || ThrowCodeError($template->error(), "Template Processing Failed"); } sub retrieve { my $file = Doctor::File->new($request->param('file')); my $disposition = $action eq "download" ? "attachment" : "inline"; print $request->header( -type => "text/html; name=\"" . $file->name . "\"", -content_disposition => "$disposition; filename=\"" . $file->name . "\"", -content_length => length($file->content) ); print $file->content; } sub diff { my $file = Doctor::File->new($request->param('file')); ValidateVersions($request->param('version'), $file->version); my $diff = $file->diff(GetContent()) || "There are no differences between the version in CVS and your revision."; if ($action eq "diff") { print $request->header(-type=>"text/plain"); } else { print $request->header( -type => "text/html; name=\"" . $file->name . ".diff\"", -content_disposition => "attachment; filename=\"" . $file->name . ".diff\"", -content_length => length($file->content) ); } print $diff; } sub regurgitate { # Returns the content that was submitted to it. Useful for displaying # the modified version of a document the user downloaded and edited locally # in the "View Edited" tab, since for security reasons there's no way # to get access to a file being uploaded on the client side. When the user # clicks on the "View Edited" tab, client-side JS checks to see if there is # a value in the file upload control, and if so it adds an iframe # to the "View Edited" panel and posts the file to it with action=regurgitate. my $content = GetContent(); my $filename = $request->param('content_file') || $request->param('file') || "modified.html"; $filename =~ s/^(.*[\/\\])?([^\/\\]+)$/$2/; print $request->header( -type => qq|text/html; name="$filename"|, -content_disposition => qq|inline; filename="$filename"|, -content_length => length($content)); print $content; } sub queue { # Sends the diff or new file to an editors mailing list for review. if (!$CONFIG{EDITOR_EMAIL}) { ThrowCodeError("The administrator has not enabled submission of patches for review.", "Review Not Enabled"); } my $file = Doctor::File->new($request->param('file')); my $comment = $request->param('comment') || "No comment."; my $content = GetContent() || ""; my $email; use Email::Valid; if (!($email = Email::Valid->address($request->param('email')))) { ThrowUserError("address $email invalid: $Email::Valid::Details"); } # Prefer the name of the file being uploaded, if any; otherwise append # ".diff" to the name of the file in CVS. my $filename = $request->param('content_file') || $file->name . ".diff"; $filename =~ s/^(.*[\/\\])?([^\/\\]+)$/$2/; my ($patch, $version); if ($file->version eq "new") { $patch = $content; $version = "(new file)"; } else { ValidateVersions($request->param('version'), $file->version); $patch = $file->diff($content); if (!$patch) { ThrowUserError("There are no differences between the version in CVS and your revision.", "No Differences Found"); } $version = "v" . $file->version; } my $subject = "patch: " . $file->spec . " $version"; eval { use MIME::Entity; my $mail = MIME::Entity->build(Type =>"multipart/mixed", From => $email, To => $CONFIG{EDITOR_EMAIL}, Subject => $subject); $mail->attach(Data => \$comment, Encoding => "quoted-printable"); $mail->attach(Data => \$patch, Encoding => "quoted-printable", Disposition => "inline", Filename => $filename); # Set the record separator because otherwise MIME::Entity seems # to get stuck in an infinite loop. In a block to localize $/. { local $/ = "\n"; $mail->send(); } }; if ($@) { ThrowCodeError($@, "Mail Failure"); } $vars->{file} = $file; print $request->header; $template->process("queued.tmpl", $vars) || ThrowCodeError($template->error(), "Template Processing Failed"); } sub commit { # Commits a (possibly new) file to the repository. my $file = Doctor::File->new($request->param('file')); my $username = ValidateUsername(); # The password is optional. my $password = $request->param('password'); my $comment = ValidateComment(); if ($action eq "commit") { ValidateVersions($request->param('version'), $file->version); $file->patch(GetContent()); } else { $file->add(GetContent()); } my ($rv, $output, $errors) = $file->commit($username, $password, $comment); if ($rv != 0) { ThrowUserError("An error occurred while committing the file: $output $errors", undef, $output, $rv, $errors); } $vars->{'file'} = $file; $vars->{'output'} = $output; $vars->{'errors'} = $errors; print $request->header; $template->process("committed.tmpl", $vars) || ThrowCodeError($template->error(), "Template Processing Failed"); } ################################################################################ # Input Validation ################################################################################ sub ValidateUsername { $request->param('username') || ThrowUserError("You must enter your username."); my $username = $request->param('username'); # If the username has an at sign in it, convert it to a percentage sign, # since that's probably what the user meant. CVS usernames are often email # addresses in which the at sign has been converted to a percentage sign, # and since at signs aren't legal characters in CVS usernames, it's a good # bet that any occurrences are supposed to be percentage signs. $username =~ s/@/%/; return $username; } sub ValidateComment { $request->param('comment') || ThrowUserError("You must enter a check-in comment describing your changes."); return $request->param('comment'); } sub ValidateVersions() { # Throws 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. my ($oldversion, $newversion) = @_; if ($oldversion && $newversion && $oldversion ne $newversion) { ThrowCodeError("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 (bug 164342)."); } } ################################################################################ # Misc ################################################################################ sub GetContent { my $fh = $request->upload('content_file'); my $content; if ($fh) { local $/ = undef; $content = <$fh>; } if (!$content) { $content = $request->param('content'); } return $content; }