File Coverage

blib/lib/CGI/WebGzip.pm
Criterion Covered Total %
statement 14 105 13.3
branch 5 56 8.9
condition 0 15 0.0
subroutine 7 19 36.8
pod 8 10 80.0
total 34 205 16.5


line stmt bran cond sub pod time code
1             package CGI::WebGzip;
2             our $VERSION = '0.13';
3 1     1   8851 use strict;
  1         3  
  1         1662  
4            
5             # Compression level.
6             my $level = 9;
7             # Compression status (undef if compression is used).
8             my $status = undef;
9             # Callback.
10             my $callback = undef;
11            
12            
13             ##
14             ## Interface.
15             ##
16            
17             # Import code.
18             # Usage:
19             # use CGI::WebGzip(level);
20             sub import {
21 1     1   14 setLevel($_[1]);
22 1 50       3 if (!defined getAbility()) {
23 0         0 startCapture();
24             }
25             }
26            
27             # Finish code.
28             END {
29 1     1   222 flush();
30             }
31            
32            
33             # void flush()
34             # Flushes the compressed buffer immediately and releases STDOUT capture.
35             sub flush {
36             #CGI::WebOut::_Debug("Finished %s, %s, %s", __PACKAGE__, $capture->line_pointer, tied(*STDOUT));
37 1 50   1 1 7 my $data = stopCapture(); return if !defined $data;
  1         11  
38 0         0 my ($headers, $body) = split /\r?\n\r?\n/, $data, 2;
39            
40             # Run compression.
41 0         0 my ($newBody, $newHeaders, $stat);
42 0 0       0 if (length($body) == 0) {
43 0         0 ($newBody, $newHeaders) = ($body, $headers);
44             } else {
45 0         0 ($newBody, $newHeaders, $stat) = ob_gzhandler($body, $headers);
46 0         0 $status = $stat;
47             }
48            
49             # Run callback if defined. Callback may set additional cookies
50             # printing Set-Cookie header. If callback returns 0, no data
51             # is output by this function (presume callback did it itself).
52 0 0       0 if ($callback) {
53 0 0       0 $callback->($newBody, $newHeaders, $body) or return;
54             }
55 0         0 binmode(STDOUT);
56 0         0 print $newHeaders;
57 0         0 print "\r\n\r\n";
58 0         0 print $newBody;
59             }
60            
61            
62             # bool getAbility()
63             # Returns non-false is we are in CGI mode and browser understands compression
64             # Also loads Compress::Zlib and silently returns false if not found.
65             # Returns undef if compression ca be used.
66             sub getAbility {
67 1 50   1 1 6 if (!$ENV{SCRIPT_NAME}) {
68 1         1741 return "no: not a CGI script";
69             }
70 0   0     0 my $acc = $ENV{HTTP_ACCEPT_ENCODING}||"";
71 0 0       0 if ($acc !~ /\bgzip\b/i) {
72 0         0 return "no: incompatible browser";
73             }
74 0 0       0 if (!eval { require Compress::Zlib }) {
  0         0  
75 0         0 return "no: Compress::Zlib not found";
76             }
77 0         0 return undef;
78             }
79            
80            
81             # bool isCompressibleType($type)
82             # Returns true if MIME type $type is compressible.
83             sub isCompressibleType {
84 0     0 1 0 my ($type) = @_;
85 0         0 return $type =~ m{^text/}i;
86             }
87            
88            
89             # CODE setCallback(CODE $func)
90             # Sets the new callback function. Returns previous.
91             sub setCallback {
92 0     0 1 0 my $prev = $callback;
93 0         0 $callback = $_[0];
94 0         0 return $prev;
95             }
96            
97            
98             # int setLevel($level)
99             # Sets compression level. Returns previous.
100             sub setLevel {
101 1     1 1 2 my $prev = $level;
102 1 50       4 $level = defined $_[0]? $_[0] : 9;
103 1         2 return $prev;
104             }
105            
106            
107             # string getStatus()
108             # Returns status string. If compression is failed, status string is
109             # non-empty and contains diagnostic message. Otherwise it is undef.
110             sub getStatus {
111 0     0 1 0 return $status;
112             }
113            
114            
115            
116             ##
117             ## Compression abstraction level.
118             ##
119            
120             # ($compressedBody, $modifiedHeaders, $status) ob_gzhandler(string $body, [string $headers])
121             # Returns compressed data (additionally analysing headers, if present).
122             # In scalar context returns $compressedBody only.
123             # Input headers can be modified, thus this function returns $modifiedHeaders.
124             # Compression error message is returned in $status (or undef if everything is OK).
125             # This function can be used exactly as PHP's ob_gzhandler().
126             sub ob_gzhandler {
127 0     0 1 0 my ($body, $h) = @_;
128 0   0     0 $h ||= "";
129 0         0 my $status = undef;
130            
131             # Process all the headers.
132 0         0 my $ContentEncoding = undef;
133 0         0 my $ContentType = undef;
134 0         0 my $Status = undef;
135 0         0 my @headers = ();
136 0         0 foreach (split /\r?\n/, $h) {
137 0 0       0 if (/^Content[-_]Encoding:\s*(.*)/i) {
138 0         0 $ContentEncoding = $1;
139 0         0 next;
140             }
141 0 0       0 if (/^Content[-_]Type:\s*(.*)/i) {
142 0         0 $ContentType = $1;
143             }
144 0 0       0 if (/^Status:\s*(\d+)/i) {
145 0         0 $Status = $1;
146             }
147 0 0       0 push @headers, $_ if $_;
148             }
149            
150             # Determine if we need to compress.
151 0         0 my $needCompress = 1;
152 0 0 0     0 if (defined $ContentType && !isCompressibleType($ContentType)) {
153 0   0     0 $ContentType ||= "undef";
154 0         0 $status = "no: incompatible Content-type ($ContentType)";
155 0         0 $needCompress = undef;
156             }
157 0 0 0     0 if ($Status && $Status ne 200) {
158 0         0 $status = "no: Status must be 200 (given $Status)";
159 0         0 $needCompress = undef;
160             }
161 0 0       0 if (defined($status=getAbility())) {
162 0         0 $needCompress = undef;
163             }
164            
165             # Echo compression header.
166 0 0       0 if ($needCompress) {
167 0 0 0     0 $ContentEncoding = "gzip" . ($ContentEncoding? ", $ContentEncoding" : "")
    0          
168             if !$ContentEncoding || $ContentEncoding !~ /\bgzip\b/i;
169 0         0 push @headers, "Content-Encoding: $ContentEncoding";
170 0         0 push @headers, "Vary: Accept-Encoding";
171             }
172            
173             # Compress output.
174 0         0 my $headers = join "\r\n", @headers;
175 0 0       0 my $out = $needCompress? deflate_gzip($body, $level) : $body;
176            
177 0 0       0 return wantarray? ($out, $headers, $status) : $out;
178             }
179            
180            
181             # string deflate_gzip($text, $level);
182             # Compresses the input string and returns result.
183             sub deflate_gzip {
184 0 0   0 1 0 my ($d, $st) = Compress::Zlib::deflateInit(-Level => defined $_[1]? $_[1] : 9);
185 0         0 my ($out, $Status) = $d->deflate($_[0]);
186 0         0 my ($outF, $StatusF) = $d->flush();
187 0         0 $out = $out.$outF;
188            
189             # Shamanian code - without them nothing works! Hmmm...
190 0         0 my $pre = pack('CCCCCCCC', 0x1f,0x8b,0x08,0x00,0x00,0x00,0x00,0x00);
191 0         0 $out = $pre . substr($out, 0, -4) . pack('V', Compress::Zlib::crc32($_[0])) . pack('V', length($_[0]));
192            
193 0         0 return $out;
194             }
195            
196            
197            
198             ##
199             ## STDOUT capture abstraction level.
200             ##
201            
202             # Capture object.
203             my $capture = undef;
204            
205             # Starts STDOUT capturing.
206             sub startCapture {
207             # Tie STDOUT only once.
208 0 0   0 0 0 return if $capture;
209 0         0 $capture = tie *STDOUT, "CGI::WebGzip::Tie";
210             }
211            
212             # Finishes STDOUT capturing.
213             sub stopCapture {
214 1 50   1 0 6 return undef if !$capture;
215 0           my $obj = tied *STDOUT;
216 0           my $data = join "", @$obj;
217 0           $obj = $capture = undef;
218 0           untie(*STDOUT);
219 0           return $data;
220             }
221            
222             # Package to tie STOUT. Captures all the output.
223             package CGI::WebGzip::Tie;
224 0     0     sub TIEHANDLE { return bless [], $_[0] }
225             sub PRINT {
226 0     0     my $th = shift;
227             push @$th, map {
228 0 0         if (!defined $_) {
  0            
229 0 0         eval { require Carp } and Carp::carp("Use of uninitialized value in print");
  0            
230 0           ""
231             } else {
232 0           $_
233             }
234             } @_;
235             }
236             sub PRINTF {
237 0     0     my $th = shift;
238             push @$th, sprintf map {
239 0 0         if (!defined $_) {
  0            
240 0 0         eval { require Carp } and Carp::carp("Use of uninitialized value in printf");
  0            
241 0           ""
242             } else {
243 0           $_
244             }
245             } @_;
246             }
247 0     0     sub WRITE { goto &PRINT; }
248 0     0     sub CLOSE { CGI::WebGzip::flush() }
249 0     0     sub BINMODE { }
250            
251             return 1;
252             __END__