Subversion Repositories DevTools

Rev

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

Rev Author Line No. Line
1293 dpurdie 1
###############################################################################
2
# Codestriker: Copyright (c) 2001, 2002 David Sitsky.  All rights reserved.
3
# sits@users.sourceforge.net
4
#
5
# This program is free software; you can redistribute it and modify it under
6
# the terms of the GPL.
7
 
8
# Collection of routines for creating HTTP responses, including headers and
9
# error indications.
10
 
11
package Codestriker::Http::Response;
12
 
13
use strict;
14
use Codestriker::Http::Cookie;
15
use HTML::Entities ();
16
 
17
# Constructor for this class.  Indicate that the response header hasn't been
18
# generated yet.
19
sub new($$) {
20
    my ($type, $query) = @_;
21
 
22
    my $self = {};
23
    $self->{header_generated} = 0;
24
    $self->{query} = $query;
25
    $self->{format} = $query->param('format');
26
    $self->{action} = $query->param('action');
27
    return bless $self, $type;
28
}
29
 
30
# Return the query object associated with the response.
31
sub get_query($) {
32
    my ($self) = @_;
33
 
34
    return $self->{query};
35
}
36
 
37
# Generate the initial HTTP response header, with the initial HTML header.
38
# Most of the input parameters are used for storage into the user's cookie.
39
sub generate_header {
40
 
41
    my ($self, %params) = @_;
42
 
43
    my $topic = undef;
44
    my $topic_title = "";
45
    my $email = "";
46
    my $reviewers = "";
47
    my $cc = "";
48
    my $mode = "";
49
    my $tabwidth = "";
50
    my $repository = "";
51
    my $projectid = "";
52
    my $load_anchor = "";
53
    my $topicsort = "";
54
    my $fview = -1;
55
 
56
    my $reload = $params{reload};
57
    my $cache = $params{cache};
58
 
59
    $load_anchor = $params{load_anchor};
60
    $load_anchor = "" if ! defined $load_anchor;
61
 
62
    # If the header has already been generated, do nothing.
63
    return if ($self->{header_generated});
64
    $self->{header_generated} = 1;
65
    my $query = $self->{query};
66
 
67
    # Set the topic and title parameters.
68
    $topic = $params{topic};
69
    $topic_title = $params{topic_title};
70
 
71
    # Set the fview parameter if defined.
72
    $fview = $params{fview} if defined $params{fview};
73
 
74
    # Set the cookie in the HTTP header for the $email, $cc, $reviewers and
75
    # $tabwidth parameters.
76
    my %cookie = ();
77
 
78
    if (!exists $params{email} || $params{email} eq "") {
79
	$email = Codestriker::Http::Cookie->get_property($query, 'email');
80
    }
81
    else {
82
        $email = $params{email};
83
    }
84
 
85
    if (!exists $params{reviewers} || $params{reviewers} eq "") {
86
	$reviewers = Codestriker::Http::Cookie->get_property($query,
87
							     'reviewers');
88
    }
89
    else {
90
        $reviewers = $params{reviewers};
91
    }
92
 
93
    if (!exists $params{cc} || $params{cc} eq "") {
94
	$cc = Codestriker::Http::Cookie->get_property($query, 'cc');
95
    }
96
    else {
97
        $cc = $params{cc};
98
    }
99
 
100
    if (!exists $params{tabwidth} || $params{tabwidth} eq "") {
101
	$tabwidth = Codestriker::Http::Cookie->get_property($query,
102
							    'tabwidth');
103
    }
104
    else {
105
        $tabwidth = $params{tabwidth};
106
    }
107
 
108
    if (!exists $params{mode} || $params{mode} eq "") {
109
	$mode = Codestriker::Http::Cookie->get_property($query, 'mode');
110
    }
111
    else {
112
        $mode = $params{mode};
113
    }
114
 
115
    if (!exists $params{repository} || $params{repository} eq "") {
116
	$repository = Codestriker::Http::Cookie->get_property($query,
117
							     'repository');
118
    }
119
    else {
120
        $repository = $params{repository};
121
    }
122
 
123
    if (!exists $params{projectid} || $params{projectid} eq "") {
124
	$projectid = Codestriker::Http::Cookie->get_property($query,
125
							     'projectid');
126
    }
127
    else {
128
        $projectid = $params{projectid};
129
    }
130
 
131
    if (!exists $params{topicsort} || $params{topicsort} eq "") {
132
	$topicsort = Codestriker::Http::Cookie->get_property($query,
133
							     'topicsort');
134
    }
135
    else {
136
        $topicsort = $params{topicsort};
137
    }
138
 
139
    $cookie{'email'} = $email if $email ne "";
140
    $cookie{'reviewers'} = $reviewers if $reviewers ne "";
141
    $cookie{'cc'} = $cc if $cc ne "";
142
    $cookie{'tabwidth'} = $tabwidth if $tabwidth ne "";
143
    $cookie{'mode'} = $mode if $mode ne "";
144
    $cookie{'repository'} = $repository if $repository ne "";
145
    $cookie{'projectid'} = $projectid if $projectid ne "";
146
    $cookie{'topicsort'} = $topicsort if $topicsort ne "";
147
 
148
    my $cookie_obj = Codestriker::Http::Cookie->make($query, \%cookie);
149
 
150
    # This logic is taken from cvsweb.  There is _reason_ behind this logic...
151
    # Basically mozilla supports gzip regardless even though some versions
152
    # don't state this.  IE claims it does, but doesn't support it.  Using
153
    # the gzip binary technique doesn't work apparently under mod_perl.
154
 
155
    # Determine if the client browser is capable of handled compressed HTML.
156
    eval {
157
	require Compress::Zlib;
158
    };
159
    my $output_compressed = 0;
160
    my $has_zlib = !$@;
161
    my $browser = $ENV{'HTTP_USER_AGENT'};
162
    my $can_compress = ($Codestriker::use_compression &&
163
			((defined($ENV{'HTTP_ACCEPT_ENCODING'})
164
			  && $ENV{'HTTP_ACCEPT_ENCODING'} =~ m|gzip|)
165
			 || $browser =~ m%^Mozilla/3%)
166
			&& ($browser !~ m/MSIE/)
167
			&& !(defined($ENV{'MOD_PERL'}) && !$has_zlib));
168
 
169
    # Output the appropriate header if compression is allowed to the client.
170
    if ($can_compress &&
171
	($has_zlib || ($Codestriker::gzip ne "" &&
172
		       open(GZIP, "| $Codestriker::gzip -1 -c")))) {
173
	if ($cache) {
174
	    print $query->header(-cookie=>$cookie_obj,
175
				 -content_encoding=>'x-gzip',
176
				 -charset=>"UTF-8",
177
				 -vary=>'Accept-Encoding');
178
	} else {
179
	    print $query->header(-cookie=>$cookie_obj,
180
				 -expires=>'+1d',
181
				 -charset=>"UTF-8",
182
				 -cache_control=>'no-store',
183
				 -pragma=>'no-cache',
184
				 -content_encoding=>'x-gzip',
185
				 -vary=>'Accept-Encoding');
186
	}
187
 
188
	# Flush header output, and switch STDOUT to GZIP.
189
	$| = 1; $| = 0;
190
	if ($has_zlib) {
191
	    tie *GZIP, __PACKAGE__, \*STDOUT;
192
	}
193
	select(GZIP);
194
	$output_compressed = 1;
195
    } else {
196
        # Make sure the STDOUT encoding is set to UTF8.  Not needed
197
        # when the data is being sent as compressed bytes.
198
	binmode STDOUT, ':utf8';
199
	if ($cache) {
200
	    print $query->header(-cookie=>$cookie_obj,
201
					 -charset=>"UTF-8");
202
	} else {
203
	    print $query->header(-cookie=>$cookie_obj,
204
				 -expires=>'+1d',
205
				 -charset=>"UTF-8",
206
				 -cache_control=>'no-store',
207
				 -pragma=>'no-cache');
208
	}
209
    }
210
 
211
    my $title = "Codestriker";
212
    if (defined $topic_title && $topic_title ne "") {
213
	$title .= ": \"$topic_title\"";
214
    }
215
    $title = HTML::Entities::encode($title);
216
 
217
    # Generate the URL to the codestriker CSS file.
218
    my $codestriker_css;
219
    if (defined $Codestriker::codestriker_css &&
220
	$Codestriker::codestriker_css ne "") {
221
	$codestriker_css = $Codestriker::codestriker_css;
222
    } else {
223
	$codestriker_css = $query->url();
224
	$codestriker_css =~ s/\/[\w\-]+\/codestriker\.pl/\/codestrikerhtml\/codestriker\.css/;
225
    }
226
 
227
    my $overlib_js = $codestriker_css;
228
    $overlib_js =~ s/codestriker.css/overlib.js/o;
229
    my $overlib_centerpopup_js = $codestriker_css;
230
    $overlib_centerpopup_js =~ s/codestriker.css/overlib_centerpopup.js/o;
231
    my $overlib_draggable_js = $codestriker_css;
232
    $overlib_draggable_js =~ s/codestriker.css/overlib_draggable.js/o;
233
    my $xbdhtml_js = $codestriker_css;
234
    $xbdhtml_js =~ s/codestriker.css/xbdhtml.js/o;
235
    my $codestriker_js = $codestriker_css;
236
    $codestriker_js =~ s/codestriker.css/codestriker.js/o;
237
 
238
    # Print the basic HTML header header, with the inclusion of the scripts.
239
    print '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">';
240
    print "\n";
241
    print '<html xmlns="http://www.w3.org/1999/xhtml" lang="en-US" xml:lang="en-US">';
242
    print "\n";
243
    print "<head><title>$title</title>\n";
244
    print "<link rel=\"stylesheet\" type=\"text/css\" href=\"$codestriker_css\" />\n";
245
    print "<script src=\"$overlib_js\" type=\"text/javascript\"></script>\n";
246
    print "<script src=\"$overlib_centerpopup_js\" type=\"text/javascript\"></script>\n";
247
    print "<script src=\"$overlib_draggable_js\" type=\"text/javascript\"></script>\n";
248
    print "<script src=\"$xbdhtml_js\" type=\"text/javascript\"></script>\n";
249
    print "<script src=\"$codestriker_js\" type=\"text/javascript\"></script>\n";
250
    print "<script type=\"text/javascript\">\n";
251
    print "    var cs_load_anchor = '$load_anchor';\n";
252
    print "    var cs_reload = $reload;\n";
253
    print "    var cs_topicid = $topic->{topicid};\n" if defined $topic;
254
    print "    var cs_email = '$email';\n" if defined $email;
255
    print "    var cs_css = '$codestriker_css';\n";
256
    print "    var cs_xbdhtml_js = '$xbdhtml_js';\n";
257
 
258
    # Now output all of the comment metric information.
259
    print "    var cs_metric_data = new Array();\n";
260
    my $i = 0;
261
    foreach my $metric_config (@{ $Codestriker::comment_state_metrics }) {
262
	print "    cs_metric_data[$i] = new Object();\n";
263
	print "    cs_metric_data[$i].name = '" .
264
	    $metric_config->{name} . "';\n";
265
	print "    cs_metric_data[$i].values = new Array();\n";
266
	my $j = 0;
267
	foreach my $value (@{ $metric_config->{values} }) {
268
	    print "    cs_metric_data[$i].values[$j] = '$value';\n";
269
	    $j++;
270
	}
271
	if (defined $metric_config->{default_value}) {
272
	    print "    cs_metric_data[$i].default_value = '" .
273
		$metric_config->{default_value} . "';\n";
274
	}
275
	$i++;
276
    }
277
 
278
    # Check that the external javascript files were loaded, and if not
279
    # output an error message.  This is usually due to a
280
    # misconfiguration.
281
    print "    if ('function' != typeof window.add_comment_html) {\n";
282
    print "        alert('Oh oh... can\\'t find codestriker.js, please check your apache config.');\n";
283
    print "    }\n";
284
 
285
    print "</script>\n";
286
 
287
    # Output the comment declarations if the $comments array is defined.
288
    my $comments = $params{comments};
289
    if (defined $comments) {
290
	print generate_comment_declarations($topic, $comments, $query,
291
					    $fview, $tabwidth);
292
    }
293
 
294
    # Write an HTML comment indicating if response was sent compressed or not.
295
    $self->{output_compressed} = $output_compressed;
296
    print "\n<!-- Source was" . (!$output_compressed ? " not" : "") .
297
	" sent compressed. -->\n";
298
}
299
 
300
# Return the javascript code necessary to support viewing/modification of
301
# comments.
302
sub generate_comment_declarations
303
{
304
    my ($topic, $comments, $query, $fview, $tabwidth) = @_;
305
 
306
    # The output html to return.
307
    my $html = "";
308
 
309
    # Build a hash from filenumber|fileline|new -> comment array, to record
310
    # what comments are associated with what locations.  Also record the
311
    # order of comment_locations found.
312
    my %comment_hash = ();
313
    my @comment_locations = ();
314
    for (my $i = 0; $i <= $#$comments; $i++) {
315
	my $comment = $$comments[$i];
316
	my $key = $comment->{filenumber} . "|" . $comment->{fileline} . "|" .
317
	    $comment->{filenew};
318
	if (! exists $comment_hash{$key}) {
319
	    push @comment_locations, $key;
320
	}
321
        push @{ $comment_hash{$key} }, $comment;
322
    }
323
 
324
    # Precompute the overlib HTML for each comment location.
325
    $html .= "\n<script language=\"JavaScript\" type=\"text/javascript\">\n";
326
 
327
    # Add the reviewers for the review here.
328
    $html .= "    var topic_reviewers = '" . $topic->{reviewers} . "';\n";
329
 
330
    # Now record all the comments made so far in the topic.
331
    $html .= "    var comment_text = new Array();\n";
332
    $html .= "    var comment_hash = new Array();\n";
333
    $html .= "    var comment_metrics = new Array();\n";
334
    my $index;
335
    for ($index = 0; $index <= $#comment_locations; $index++) {
336
 
337
	# Contains the overlib HTML text.
338
	my $overlib_html = "";
339
 
340
	# Determine what the previous and next comment locations are.
341
	my $previous = undef;
342
	my $next = undef;
343
	if ($index > 0) {
344
	    $previous = $comment_locations[$index-1];
345
	}
346
	if ($index < $#comment_locations) {
347
	    $next = $comment_locations[$index+1];
348
	}
349
 
350
	# Compute the previous link if required.
351
	my $current_url = $query->self_url();
352
	if (defined $previous && $previous =~ /^(\-?\d+)|\-?\d+|\d+$/o) {
353
	    my $previous_fview = $1;
354
	    my $previous_index = $index - 1;
355
	    my $previous_url = $current_url;
356
	    $previous_url =~ s/fview=\d+/fview=$previous_fview/o if $fview != -1;
357
	    $previous_url .= '#' . $previous;
358
	    $overlib_html .= "<a href=\"javascript:window.location=\\'$previous_url\\'; ";
359
	    if ($fview == -1 || $fview == $previous_fview) {
360
		$overlib_html .= "overlib(comment_text[$previous_index], STICKY, DRAGGABLE, ALTCUT, FIXX, getEltPageLeft(getElt(\\'c$previous_index\\')), FIXY, getEltPageTop(getElt(\\'c$previous_index\\'))); ";
361
}
362
	    $overlib_html .= "void(0);\">Previous</a>";
363
	}
364
 
365
	# Compute the next link if required.
366
	if (defined $next && $next =~ /^(\-?\d+)|\-?\d+|\d+$/o) {
367
	    my $next_fview = $1;
368
	    $overlib_html .= " | " if defined $previous;
369
	    my $next_index = $index + 1;
370
	    my $next_url = $current_url;
371
	    $next_url =~ s/fview=\d+/fview=$next_fview/o if $fview != -1;
372
	    $next_url .= '#' . $next;
373
	    $overlib_html .= "<a href=\"javascript:window.location=\\'$next_url\\'; ";
374
	    if ($fview == -1 || $fview == $next_fview) {
375
		$overlib_html .= "overlib(comment_text[$next_index], STICKY, DRAGGABLE, ALTCUT, FIXX, getEltPageLeft(getElt(\\'c$next_index\\')), FIXY, getEltPageTop(getElt(\\'c$next_index\\'))); ";
376
	    }
377
	    $overlib_html .= "void(0);\">Next</a>";
378
	}
379
	if (defined $previous || defined $next) {
380
	    $overlib_html .= " | ";
381
	}
382
 
383
	# Add an add comment link.
384
	my $key = $comment_locations[$index];
385
	$key =~ /^(\-?\d+)\|(\-?\d+)\|(\d+)$/o;
386
	if (!Codestriker::topic_readonly($topic->{topic_state})) {
387
	    $overlib_html .= "<a href=\"javascript:add_comment_tooltip($1,$2,$3)" .
388
		"; void(0);\">Add Comment<\\/a> | ";
389
	}
390
 
391
	# Add a close link.
392
	$overlib_html .= "<a href=\"javascript:hideElt(getElt(\\'overDiv\\')); void(0);\">Close<\\/a><p>";
393
 
394
	# Create the actual comment text.
395
	my @comments = @{ $comment_hash{$key} };
396
 
397
	for (my $i = 0; $i <= $#comments; $i++) {
398
	    my $comment = $comments[$i];
399
 
400
	    # Need to format the data appropriately for HTML display.
401
	    my $data = HTML::Entities::encode($comment->{data});
402
	    $data =~ s/\\/\\\\/mgo;
403
	    $data =~ s/\'/\\\'/mgo;
404
	    $data =~ s/\n/<br>/mgo;
405
	    $data =~ s/ \s+/'&nbsp;' x (length($&)-1)/emgo;
406
	    $data = Codestriker::Http::Render::tabadjust($tabwidth, $data, 1);
407
 
408
	    # Show each comment with the author and date in bold.
409
	    $overlib_html .= "<b>Comment from $comment->{author} ";
410
	    $overlib_html .= "on $comment->{date}<\\/b><br>";
411
	    $overlib_html .= "$data";
412
 
413
	    # Add a newline at the end if required.
414
	    if ($i < $#comments &&
415
		substr($overlib_html, length($overlib_html)-4, 4) ne '<br>') {
416
		$overlib_html .= '<br>';
417
	    }
418
	}
419
 
420
	$html .= "    comment_text[$index] = '$overlib_html';\n";
421
        $html .= "    comment_hash['" . $comment_locations[$index] .
422
	    "'] = $index;\n";
423
 
424
	# Store the current metric values for this comment.
425
	$html .= "    comment_metrics[$index] = new Array();\n";
426
	my $comment_metrics = $comments[0]->{metrics};
427
	foreach my $metric_config (@{ $Codestriker::comment_state_metrics }) {
428
	    my $value = $comment_metrics->{$metric_config->{name}};
429
	    $value = "" unless defined $value;
430
	    $html .= "    comment_metrics[${index}]['" .
431
		$metric_config->{name} . "'] = '" . $value . "';\n";
432
	}
433
 
434
    }
435
    $html .= "</script>\n";
436
 
437
    # Now declare the CSS positional elements for each comment location.
438
    $html .= "<style type=\"text/css\">\n";
439
    for (my $i = 0; $i <= $#$comments; $i++) {
440
	$html .= '#c' . $i . ' { position: absolute; }' . "\n";
441
    }
442
    $html .= "</style>\n";
443
 
444
    # Return the generated HTML.
445
    return $html;
446
}
447
 
448
 
449
# Close the response, which only requires work if we are dealing with
450
# compressed streams.
451
sub generate_footer($) {
452
    my ($self) = @_;
453
 
454
    if ($self->{output_compressed}) {
455
	select(STDOUT);
456
	close(GZIP);
457
	untie *GZIP;
458
    }
459
}
460
 
461
# Generate an error page response if bad input was passed in.
462
sub error($$) {
463
    my ($self, $error_message) = @_;
464
 
465
    my $query = $self->{query};
466
 
467
    # Check if the expected format is XML.
468
    if (defined $self->{format} && $self->{format} eq "xml") {
469
	print $query->header(-content_type=>'text/xml');
470
	print "<?xml version=\"1.0\" encoding=\"UTF-8\" " .
471
	            "standalone=\"yes\"?>\n";
472
	print "<response><method>" . $self->{action} . "</method>" .
473
	    "<result>" . HTML::Entities::encode($error_message) .
474
	    "</result></response>\n";
475
    }
476
    else {
477
	if (! $self->{header_generated}) {
478
	    print $query->header,
479
	    $query->start_html(-title=>'Codestriker error',
480
			       -bgcolor=>'white');
481
	}
482
 
483
	print $query->p, "<FONT COLOR='red'>$error_message</FONT>", $query->p;
484
	print $query->end_html();
485
 
486
	$self->generate_footer();
487
    }
488
 
489
    exit;
490
}
491
 
492
# Implement a gzipped file handle via the Compress:Zlib compression
493
# library.  This code was stolen from CVSweb.
494
 
495
sub MAGIC1() { 0x1f }
496
sub MAGIC2() { 0x8b }
497
sub OSCODE() { 3    }
498
 
499
sub TIEHANDLE {
500
	my ($class, $out) = @_;
501
	my $level = Compress::Zlib::Z_BEST_COMPRESSION();
502
	my $wbits = -Compress::Zlib::MAX_WBITS();
503
	my ($d) = Compress::Zlib::deflateInit(-Level => $level,
504
					      -WindowBits => $wbits)
505
	    or return undef;
506
	my ($o) = {
507
		handle => $out,
508
		dh => $d,
509
		crc => 0,
510
		len => 0,
511
	};
512
	my ($header) = pack("C10", MAGIC1, MAGIC2,
513
			    Compress::Zlib::Z_DEFLATED(),
514
			    0,0,0,0,0,0, OSCODE);
515
	print {$o->{handle}} $header;
516
	return bless($o, $class);
517
}
518
 
519
sub PRINT {
520
	my ($o) = shift;
521
	my ($buf) = join(defined $, ? $, : "",@_);
522
	my ($len) = length($buf);
523
	my ($compressed, $status) = $o->{dh}->deflate($buf);
524
	print {$o->{handle}} $compressed if defined($compressed);
525
	$o->{crc} = Compress::Zlib::crc32($buf, $o->{crc});
526
	$o->{len} += $len;
527
	return $len;
528
}
529
 
530
sub CLOSE {
531
	my ($o) = @_;
532
	return if !defined( $o->{dh});
533
	my ($buf) = $o->{dh}->flush();
534
	$buf .= pack("V V", $o->{crc}, $o->{len});
535
	print {$o->{handle}} $buf;
536
	undef $o->{dh};
537
}
538
 
539
sub DESTROY {
540
	my ($o) = @_;
541
	CLOSE($o);
542
}
543
 
544
1;