Subversion Repositories DevTools

Rev

Rev 1295 | Blame | Compare with Previous | Last modification | View Log | RSS feed

###############################################################################
# Codestriker: Copyright (c) 2001, 2002 David Sitsky.  All rights reserved.
# sits@users.sourceforge.net
#
# This program is free software; you can redistribute it and modify it under
# the terms of the GPL.

# Collection of routines for creating HTTP responses, including headers and
# error indications.

package Codestriker::Http::Response;

use strict;
use Codestriker::Http::Cookie;
use HTML::Entities ();

# Constructor for this class.  Indicate that the response header hasn't been
# generated yet.
sub new($$) {
    my ($type, $query) = @_;
    
    my $self = {};
    $self->{header_generated} = 0;
    $self->{query} = $query;
    $self->{format} = $query->param('format');
    $self->{action} = $query->param('action');
    return bless $self, $type;
}

# Return the query object associated with the response.
sub get_query($) {
    my ($self) = @_;

    return $self->{query};
}

# Generate the initial HTTP response header, with the initial HTML header.
# Most of the input parameters are used for storage into the user's cookie.
sub generate_header {
    
    my ($self, %params) = @_;

    my $topic = undef;
    my $topic_title = "";
    my $email = "";
    my $reviewers = "";
    my $cc = "";
    my $mode = "";
    my $tabwidth = "";
    my $repository = "";
    my $projectid = "";
    my $load_anchor = "";
    my $topicsort = "";
    my $fview = -1;

    my $reload = $params{reload};
    my $cache = $params{cache};

    $load_anchor = $params{load_anchor};
    $load_anchor = "" if ! defined $load_anchor;

    # If the header has already been generated, do nothing.
    return if ($self->{header_generated});
    $self->{header_generated} = 1;
    my $query = $self->{query};

    # Set the topic and title parameters.
    $topic = $params{topic};
    $topic_title = $params{topic_title};

    # Set the fview parameter if defined.
    $fview = $params{fview} if defined $params{fview};

    # Set the cookie in the HTTP header for the $email, $cc, $reviewers and
    # $tabwidth parameters.
    my %cookie = ();

    if (!exists $params{email} || $params{email} eq "") {
        $email = Codestriker::Http::Cookie->get_property($query, 'email');
    }
    else {
        $email = $params{email};
    }

    if (!exists $params{reviewers} || $params{reviewers} eq "") {
        $reviewers = Codestriker::Http::Cookie->get_property($query,
                                                             'reviewers');
    }
    else {
        $reviewers = $params{reviewers};
    }

    if (!exists $params{cc} || $params{cc} eq "") {
        $cc = Codestriker::Http::Cookie->get_property($query, 'cc');
    }
    else {
        $cc = $params{cc};
    }

    if (!exists $params{tabwidth} || $params{tabwidth} eq "") {
        $tabwidth = Codestriker::Http::Cookie->get_property($query,
                                                            'tabwidth');
    }
    else {
        $tabwidth = $params{tabwidth};
    }

    if (!exists $params{mode} || $params{mode} eq "") {
        $mode = Codestriker::Http::Cookie->get_property($query, 'mode');
    }
    else {
        $mode = $params{mode};
    }

    if (!exists $params{repository} || $params{repository} eq "") {
        $repository = Codestriker::Http::Cookie->get_property($query,
                                                             'repository');
    }
    else {
        $repository = $params{repository};
    }

    if (!exists $params{projectid} || $params{projectid} eq "") {
        $projectid = Codestriker::Http::Cookie->get_property($query,
                                                             'projectid');
    }
    else {
        $projectid = $params{projectid};
    }

    if (!exists $params{topicsort} || $params{topicsort} eq "") {
        $topicsort = Codestriker::Http::Cookie->get_property($query,
                                                             'topicsort');
    }
    else {
        $topicsort = $params{topicsort};
    }

    $cookie{'email'} = $email if $email ne "";
    $cookie{'reviewers'} = $reviewers if $reviewers ne "";
    $cookie{'cc'} = $cc if $cc ne "";
    $cookie{'tabwidth'} = $tabwidth if $tabwidth ne "";
    $cookie{'mode'} = $mode if $mode ne "";
    $cookie{'repository'} = $repository if $repository ne "";
    $cookie{'projectid'} = $projectid if $projectid ne "";
    $cookie{'topicsort'} = $topicsort if $topicsort ne "";

    my $cookie_obj = Codestriker::Http::Cookie->make($query, \%cookie);

    # This logic is taken from cvsweb.  There is _reason_ behind this logic...
    # Basically mozilla supports gzip regardless even though some versions
    # don't state this.  IE claims it does, but doesn't support it.  Using
    # the gzip binary technique doesn't work apparently under mod_perl.
    
    # Determine if the client browser is capable of handled compressed HTML.
    eval {
        require Compress::Zlib;
    };
    my $output_compressed = 0;
    my $has_zlib = !$@;
    my $browser = $ENV{'HTTP_USER_AGENT'};
    my $can_compress = ($Codestriker::use_compression &&
                        ((defined($ENV{'HTTP_ACCEPT_ENCODING'})
                          && $ENV{'HTTP_ACCEPT_ENCODING'} =~ m|gzip|)
                         || $browser =~ m%^Mozilla/3%)
                        && ($browser !~ m/MSIE/)
                        && !(defined($ENV{'MOD_PERL'}) && !$has_zlib));

    # Output the appropriate header if compression is allowed to the client.
    if ($can_compress &&
        ($has_zlib || ($Codestriker::gzip ne "" &&
                       open(GZIP, "| $Codestriker::gzip -1 -c")))) {
        if ($cache) {
            print $query->header(-cookie=>$cookie_obj,
                                 -content_encoding=>'x-gzip',
                                 -charset=>"UTF-8",
                                 -vary=>'Accept-Encoding');
        } else {
            print $query->header(-cookie=>$cookie_obj,
                                 -expires=>'+1d',
                                 -charset=>"UTF-8",
                                 -cache_control=>'no-store',
                                 -pragma=>'no-cache',
                                 -content_encoding=>'x-gzip',
                                 -vary=>'Accept-Encoding');
        }

        # Flush header output, and switch STDOUT to GZIP.
        $| = 1; $| = 0;
        if ($has_zlib) {
            tie *GZIP, __PACKAGE__, \*STDOUT;
        }
        select(GZIP);
        $output_compressed = 1;
    } else {
        # Make sure the STDOUT encoding is set to UTF8.  Not needed
        # when the data is being sent as compressed bytes.
        binmode STDOUT, ':utf8';
        if ($cache) {
            print $query->header(-cookie=>$cookie_obj,
                                         -charset=>"UTF-8");
        } else {
            print $query->header(-cookie=>$cookie_obj,
                                 -expires=>'+1d',
                                 -charset=>"UTF-8",
                                 -cache_control=>'no-store',
                                 -pragma=>'no-cache');
        }
    }

    my $title = "Codestriker";
    if (defined $topic_title && $topic_title ne "") {
        $title .= ": \"$topic_title\"";
    }
    $title = HTML::Entities::encode($title);

    # Generate the URL to the codestriker CSS file.
    my $codestriker_css;
    if (defined $Codestriker::codestriker_css &&
        $Codestriker::codestriker_css ne "") {
        $codestriker_css = $Codestriker::codestriker_css;
    } else {
        $codestriker_css = $query->url();
        $codestriker_css =~ s/\/[\w\-]+\/codestriker\.pl/\/codestrikerhtml\/codestriker\.css/;
    }

    my $overlib_js = $codestriker_css;
    $overlib_js =~ s/codestriker.css/overlib.js/o;
    my $overlib_centerpopup_js = $codestriker_css;
    $overlib_centerpopup_js =~ s/codestriker.css/overlib_centerpopup.js/o;
    my $overlib_draggable_js = $codestriker_css;
    $overlib_draggable_js =~ s/codestriker.css/overlib_draggable.js/o;
    my $xbdhtml_js = $codestriker_css;
    $xbdhtml_js =~ s/codestriker.css/xbdhtml.js/o;
    my $codestriker_js = $codestriker_css;
    $codestriker_js =~ s/codestriker.css/codestriker.js/o;

    # Print the basic HTML header header, with the inclusion of the scripts.
    print '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">';
    print "\n";
    print '<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="en-US">';
    print "\n";
    print "<head><title>$title</title>\n";
    print "<link rel=\"stylesheet\" type=\"text/css\" href=\"$codestriker_css\" />\n";
    print "<script src=\"$overlib_js\" type=\"text/javascript\"></script>\n";
    print "<script src=\"$overlib_centerpopup_js\" type=\"text/javascript\"></script>\n";
    print "<script src=\"$overlib_draggable_js\" type=\"text/javascript\"></script>\n";
    print "<script src=\"$xbdhtml_js\" type=\"text/javascript\"></script>\n";
    print "<script src=\"$codestriker_js\" type=\"text/javascript\"></script>\n";
    print "<script type=\"text/javascript\">\n";
    print "    var cs_load_anchor = '$load_anchor';\n";
    print "    var cs_reload = $reload;\n";
    print "    var cs_topicid = $topic->{topicid};\n" if defined $topic;
    print "    var cs_email = '$email';\n" if defined $email;
    print "    var cs_css = '$codestriker_css';\n";
    print "    var cs_xbdhtml_js = '$xbdhtml_js';\n";

    # Now output all of the comment metric information.
    print "    var cs_metric_data = new Array();\n";
    my $i = 0;
    foreach my $metric_config (@{ $Codestriker::comment_state_metrics }) {
        print "    cs_metric_data[$i] = new Object();\n";
        print "    cs_metric_data[$i].name = '" .
            $metric_config->{name} . "';\n";
        print "    cs_metric_data[$i].values = new Array();\n";
        my $j = 0;
        foreach my $value (@{ $metric_config->{values} }) {
            print "    cs_metric_data[$i].values[$j] = '$value';\n";
            $j++;
        }
        if (defined $metric_config->{default_value}) {
            print "    cs_metric_data[$i].default_value = '" .
                $metric_config->{default_value} . "';\n";
        }
        $i++;
    }

    # Check that the external javascript files were loaded, and if not
    # output an error message.  This is usually due to a
    # misconfiguration.
    print "    if ('function' != typeof window.add_comment_html) {\n";
    print "        alert('Oh oh... can\\'t find codestriker.js, please check your apache config.');\n";
    print "    }\n";

    print "</script>\n";

    # Output the comment declarations if the $comments array is defined.
    my $comments = $params{comments};
    if (defined $comments) {
        print generate_comment_declarations($topic, $comments, $query,
                                            $fview, $tabwidth);
    }

    # Write an HTML comment indicating if response was sent compressed or not.
    $self->{output_compressed} = $output_compressed;
    print "\n<!-- Source was" . (!$output_compressed ? " not" : "") .
        " sent compressed. -->\n";
}

# Return the javascript code necessary to support viewing/modification of
# comments.
sub generate_comment_declarations
{
    my ($topic, $comments, $query, $fview, $tabwidth) = @_;

    # The output html to return.
    my $html = "";

    # Build a hash from filenumber|fileline|new -> comment array, to record
    # what comments are associated with what locations.  Also record the
    # order of comment_locations found.
    my %comment_hash = ();
    my @comment_locations = ();
    for (my $i = 0; $i <= $#$comments; $i++) {
        my $comment = $$comments[$i];
        my $key = $comment->{filenumber} . "|" . $comment->{fileline} . "|" .
            $comment->{filenew};
        if (! exists $comment_hash{$key}) {
            push @comment_locations, $key;
        }
        push @{ $comment_hash{$key} }, $comment;
    }

    # Precompute the overlib HTML for each comment location.
    $html .= "\n<script language=\"JavaScript\" type=\"text/javascript\">\n";

    # Add the reviewers for the review here.
    $html .= "    var topic_reviewers = '" . $topic->{reviewers} . "';\n";

    # Now record all the comments made so far in the topic.
    $html .= "    var comment_text = new Array();\n";
    $html .= "    var comment_hash = new Array();\n";
    $html .= "    var comment_metrics = new Array();\n";
    my $index;
    for ($index = 0; $index <= $#comment_locations; $index++) {

        # Contains the overlib HTML text.
        my $overlib_html = "";

        # Determine what the previous and next comment locations are.
        my $previous = undef;
        my $next = undef;
        if ($index > 0) {
            $previous = $comment_locations[$index-1];
        }
        if ($index < $#comment_locations) {
            $next = $comment_locations[$index+1];
        }

        # Compute the previous link if required.
        my $current_url = $query->self_url();
        if (defined $previous && $previous =~ /^(\-?\d+)|\-?\d+|\d+$/o) {
            my $previous_fview = $1;
            my $previous_index = $index - 1;
            my $previous_url = $current_url;
            $previous_url =~ s/fview=\d+/fview=$previous_fview/o if $fview != -1;
            $previous_url .= '#' . $previous;
            $overlib_html .= "<a href=\"javascript:window.location=\\'$previous_url\\'; ";
            if ($fview == -1 || $fview == $previous_fview) {
                $overlib_html .= "overlib(comment_text[$previous_index], STICKY, DRAGGABLE, ALTCUT, FIXX, getEltPageLeft(getElt(\\'c$previous_index\\')), FIXY, getEltPageTop(getElt(\\'c$previous_index\\'))); ";
}
            $overlib_html .= "void(0);\">Previous</a>";
        }

        # Compute the next link if required.
        if (defined $next && $next =~ /^(\-?\d+)|\-?\d+|\d+$/o) {
            my $next_fview = $1;
            $overlib_html .= " | " if defined $previous;
            my $next_index = $index + 1;
            my $next_url = $current_url;
            $next_url =~ s/fview=\d+/fview=$next_fview/o if $fview != -1;
            $next_url .= '#' . $next;
            $overlib_html .= "<a href=\"javascript:window.location=\\'$next_url\\'; ";
            if ($fview == -1 || $fview == $next_fview) {
                $overlib_html .= "overlib(comment_text[$next_index], STICKY, DRAGGABLE, ALTCUT, FIXX, getEltPageLeft(getElt(\\'c$next_index\\')), FIXY, getEltPageTop(getElt(\\'c$next_index\\'))); ";
            }
            $overlib_html .= "void(0);\">Next</a>";
        }
        if (defined $previous || defined $next) {
            $overlib_html .= " | ";
        }

        # Add an add comment link.
        my $key = $comment_locations[$index];
        $key =~ /^(\-?\d+)\|(\-?\d+)\|(\d+)$/o;
        if (!Codestriker::topic_readonly($topic->{topic_state})) {
            $overlib_html .= "<a href=\"javascript:add_comment_tooltip($1,$2,$3)" .
                "; void(0);\">Add Comment<\\/a> | ";
        }

        # Add a close link.
        $overlib_html .= "<a href=\"javascript:hideElt(getElt(\\'overDiv\\')); void(0);\">Close<\\/a><p>";

        # Create the actual comment text.
        my @comments = @{ $comment_hash{$key} };

        for (my $i = 0; $i <= $#comments; $i++) {
            my $comment = $comments[$i];

            # Need to format the data appropriately for HTML display.
            my $data = HTML::Entities::encode($comment->{data});
            $data =~ s/\\/\\\\/mgo;
            $data =~ s/\'/\\\'/mgo;
            $data =~ s/\n/<br>/mgo;
            $data =~ s/ \s+/'&nbsp;' x (length($&)-1)/emgo;
            $data = Codestriker::Http::Render::tabadjust($tabwidth, $data, 1);

            # Show each comment with the author and date in bold.
            $overlib_html .= "<b>Comment from $comment->{author} ";
            $overlib_html .= "on $comment->{date}<\\/b><br>";
            $overlib_html .= "$data";

            # Add a newline at the end if required.
            if ($i < $#comments &&
                substr($overlib_html, length($overlib_html)-4, 4) ne '<br>') {
                $overlib_html .= '<br>';
            }
        }

        $html .= "    comment_text[$index] = '$overlib_html';\n";
        $html .= "    comment_hash['" . $comment_locations[$index] .
            "'] = $index;\n";

        # Store the current metric values for this comment.
        $html .= "    comment_metrics[$index] = new Array();\n";
        my $comment_metrics = $comments[0]->{metrics};
        foreach my $metric_config (@{ $Codestriker::comment_state_metrics }) {
            my $value = $comment_metrics->{$metric_config->{name}};
            $value = "" unless defined $value;
            $html .= "    comment_metrics[${index}]['" .
                $metric_config->{name} . "'] = '" . $value . "';\n";
        }

    }
    $html .= "</script>\n";

    # Now declare the CSS positional elements for each comment location.
    $html .= "<style type=\"text/css\">\n";
    for (my $i = 0; $i <= $#$comments; $i++) {
        $html .= '#c' . $i . ' { position: absolute; }' . "\n";
    }
    $html .= "</style>\n";

    # Return the generated HTML.
    return $html;
}


# Close the response, which only requires work if we are dealing with
# compressed streams.
sub generate_footer($) {
    my ($self) = @_;

    if ($self->{output_compressed}) {
        select(STDOUT);
        close(GZIP);
        untie *GZIP;
    }
}

# Generate an error page response if bad input was passed in.
sub error($$) {
    my ($self, $error_message) = @_;

    my $query = $self->{query};

    # Check if the expected format is XML.
    if (defined $self->{format} && $self->{format} eq "xml") {
        print $query->header(-content_type=>'text/xml');
        print "<?xml version=\"1.0\" encoding=\"UTF-8\" " .
                    "standalone=\"yes\"?>\n";
        print "<response><method>" . $self->{action} . "</method>" .
            "<result>" . HTML::Entities::encode($error_message) .
            "</result></response>\n";
    }
    else {
        if (! $self->{header_generated}) {
            print $query->header,
            $query->start_html(-title=>'Codestriker error',
                               -bgcolor=>'white');
        }

        print $query->p, "<FONT COLOR='red'>$error_message</FONT>", $query->p;
        print $query->end_html();

        $self->generate_footer();
    }

    exit;
}

# Implement a gzipped file handle via the Compress:Zlib compression
# library.  This code was stolen from CVSweb.

sub MAGIC1() { 0x1f }
sub MAGIC2() { 0x8b }
sub OSCODE() { 3    }

sub TIEHANDLE {
        my ($class, $out) = @_;
        my $level = Compress::Zlib::Z_BEST_COMPRESSION();
        my $wbits = -Compress::Zlib::MAX_WBITS();
        my ($d) = Compress::Zlib::deflateInit(-Level => $level,
                                              -WindowBits => $wbits)
            or return undef;
        my ($o) = {
                handle => $out,
                dh => $d,
                crc => 0,
                len => 0,
        };
        my ($header) = pack("C10", MAGIC1, MAGIC2,
                            Compress::Zlib::Z_DEFLATED(),
                            0,0,0,0,0,0, OSCODE);
        print {$o->{handle}} $header;
        return bless($o, $class);
}

sub PRINT {
        my ($o) = shift;
        my ($buf) = join(defined $, ? $, : "",@_);
        my ($len) = length($buf);
        my ($compressed, $status) = $o->{dh}->deflate($buf);
        print {$o->{handle}} $compressed if defined($compressed);
        $o->{crc} = Compress::Zlib::crc32($buf, $o->{crc});
        $o->{len} += $len;
        return $len;
}

sub CLOSE {
        my ($o) = @_;
        return if !defined( $o->{dh});
        my ($buf) = $o->{dh}->flush();
        $buf .= pack("V V", $o->{crc}, $o->{len});
        print {$o->{handle}} $buf;
        undef $o->{dh};
}

sub DESTROY {
        my ($o) = @_;
        CLOSE($o);
}

1;