gecko-dev/webtools/doctor/doctor.cgi

341 lines
12 KiB
Perl
Executable File

#!/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 <myk@mozilla.org>
# Frédéric Buclin <LpSolit@gmail.com>
################################################################################
# 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;
################################################################################
# Script Configuration
################################################################################
# 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
################################################################################
# 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") { 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.";
print $request->header(-type=>"text/plain");
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();
my $password = ValidatePassword();
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 ValidatePassword {
$request->param('password')
|| ThrowUserError("You must enter your password.");
return $request->param('password');
}
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 != $newversion) {
ThrowCodeError("You edited version <em>$oldversion</em> of the file,
but version <em>$newversion</em> is in the repository. Reload the edit
page and make your changes again (and bother the authors of this script
to implement change merging
(<a href=\"http://bugzilla.mozilla.org/show_bug.cgi?id=164342\">bug 164342</a>).");
}
}
################################################################################
# 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;
}