source: trunk/core/lib/Foswiki/Response.pm @ 14536

Revision 14536, 13.0 KB checked in by FlorianSchlichting, 14 months ago (diff)

Item11702: $status regex was too rigid, Reason-Phrase (*<TEXT, excluding CR, LF>) may be included

  • Property svn:keywords set to Revision Date
Line 
1# See bottom of file for license and copyright information
2
3=begin TML
4
5---+!! package Foswiki::Response
6
7Class to encapsulate response data.
8
9Fields:
10    * =status=  - response status
11    * =headers= - hashref to response headers
12    * =body=    - response body
13    * =cookies= - hashref to response cookies
14
15=cut
16
17package Foswiki::Response;
18
19use strict;
20use warnings;
21use Assert;
22
23use CGI::Util ();
24
25=begin TML
26
27---++ ClassMethod new() -> $response
28
29Constructs a Foswiki::Response object.
30
31=cut
32
33sub new {
34    my $proto = shift;
35    my $class = ref($proto) || $proto;
36    my $this  = {
37
38#status needs to default to 'unset' so the web server can set the status to whatever it needs (think basic auth, or other magics)
39        status           => undef,
40        headers          => {},
41        body             => undef,
42        charset          => $Foswiki::cfg{Site}{CharSet},
43        cookies          => [],
44        outputHasStarted => 0,
45    };
46
47    return bless $this, $class;
48}
49
50=begin TML
51
52---++ ObjectMethod status( $status ) -> $status
53
54Gets/Sets response status.
55   * =$status= is a three digit code, optionally followed by a status string
56
57=cut
58
59sub status {
60    my ( $this, $status ) = @_;
61    if ($status) {
62        ASSERT( !$this->{outputHasStarted}, 'Too late to change status' )
63          if DEBUG;
64        $this->{status} = $status =~ /^\d{3}/ ? $status : undef;
65    }
66    return $this->{status};
67}
68
69=begin TML
70
71---++ ObjectMethod charset([$charset]) -> $charset
72
73Gets/Sets response charset. If not defined, defaults to ISO-8859-1,
74just like CGI.pm
75
76=cut
77
78sub charset {
79    return @_ == 1 ? $_[0]->{charset} : ( $_[0]->{charset} = $_[1] );
80}
81
82=begin TML
83
84---++ ObjectMethod header(-type       => $type,
85                          -status     => $status,
86                          -cookie     => $cookie || \@cookies,
87                          -attachment => $attachName,
88                          -charset    => $charset,
89                          -expires    => $expires,
90                          -HeaderN    => ValueN )
91
92Sets response header. Resonably compatible with CGI.
93Doesn't support -nph, -target and -p3p.
94
95=cut
96
97sub header {
98    my ( $this, @p ) = @_;
99    my (@header);
100
101    ASSERT( !$this->{outputHasStarted}, 'Too late to change headers' ) if DEBUG;
102
103    # Ugly hack to avoid html escape in CGI::Util::rearrange
104    local $CGI::Q = { escape => 0 };
105
106    # SMELL: CGI::Util is documented as not having any public subroutines
107    my ( $type, $status, $cookie, $charset, $expires, @other ) =
108      CGI::Util::rearrange(
109        [
110            [ 'TYPE',   'CONTENT_TYPE', 'CONTENT-TYPE' ], 'STATUS',
111            [ 'COOKIE', 'COOKIES' ],    'CHARSET',
112            'EXPIRES',
113        ],
114        @p
115      );
116
117    if ( defined $charset ) {
118        $this->charset($charset);
119    }
120    else {
121        $charset = $this->charset;
122    }
123
124    foreach (@other) {
125
126        # Don't use \s because of perl bug 21951
127        next unless my ( $header, $value ) = /([^ \r\n\t=]+)=\"?(.+?)\"?$/;
128
129        $header = lc($header);
130        $header =~ s/\b(\w)/\u$1/g;
131        if ( exists $this->{headers}->{$header} ) {
132            if ( ref $this->{headers}->{$header} ) {
133                push @{ $this->{headers}->{$header} }, $value;
134            }
135            else {
136                $this->{headers}->{$header} =
137                  [ $this->{headers}->{$header}, $value ];
138            }
139        }
140        else {
141            $this->{headers}->{$header} = $value;
142        }
143    }
144
145    $type ||= 'text/html' unless defined($type);
146    $type .= "; charset=$charset"
147      if $type ne ''
148          and $type =~ m!^text/!
149          and $type !~ /\bcharset\b/
150          and $charset ne '';
151
152    if ($status) {
153        $this->{headers}->{Status} = $status;
154        $this->status($status);
155    }
156
157    # push all the cookies -- there may be several
158    if ($cookie) {
159        my @cookies = ref($cookie) eq 'ARRAY' ? @$cookie : ($cookie);
160        $this->cookies( \@cookies );
161    }
162    $this->{headers}->{Expires} = CGI::Util::expires( $expires, 'http' )
163      if ( defined $expires );
164    $this->{headers}->{Date} = CGI::Util::expires( 0, 'http' )
165      if defined $expires || $cookie;
166
167    $this->{headers}->{'Content-Type'} = $type if $type ne '';
168}
169
170=begin TML
171
172---++ ObjectMethod headers( { ... } ) -> $headersHashRef
173
174Gets/Sets all response headers. Keys are headers name and values
175are scalars for single-valued headers or arrayref for multivalued ones.
176
177=cut
178
179sub headers {
180    my ( $this, $hdr ) = @_;
181    if ($hdr) {
182        ASSERT( !$this->{outputHasStarted}, 'Too late to change headers' )
183          if DEBUG;
184        my %headers = ();
185        while ( my ( $key, $value ) = each %$hdr ) {
186            $key =~ s/(?:^|(?<=-))(.)([^-]*)/\u$1\L$2\E/g;
187            $headers{$key} = $value;
188        }
189        $headers{Expires} = CGI::Util::expires( $headers{Expires}, 'http' )
190          if defined $headers{Expires};
191        $headers{Date} = CGI::Util::expires( 0, 'http' )
192          if defined $headers{'Set-Cookie'} || defined $headers{Expires};
193        if ( defined $headers{'Set-Cookie'} ) {
194            my @cookies =
195              ref( $headers{'Set-Cookie'} ) eq 'ARRAY'
196              ? @{ $headers{'Set-Cookie'} }
197              : ( $headers{'Set-Cookie'} );
198            $this->cookies( \@cookies );
199        }
200        $this->status( $headers{Status} ) if defined $headers{Status};
201        $this->{headers} = \%headers;
202    }
203    return $this->{headers};
204}
205
206=begin TML
207
208---++ ObjectMethod getHeader( [ $name ] ) -> $value
209
210If called without parameters returns all present header names,
211otherwise returns a list (maybe with a single element) of values
212associated with $name.
213
214=cut
215
216sub getHeader {
217    my ( $this, $hdr ) = @_;
218    return keys %{ $this->{headers} } unless $hdr;
219    $hdr =~ s/(?:^|(?<=-))(.)([^-]*)/\u$1\L$2\E/g;
220    if ( exists $this->{headers}->{$hdr} ) {
221        my $value = $this->{headers}->{$hdr};
222        return ref $value ? @$value : ($value);
223    }
224    else {
225        return;
226    }
227}
228
229=begin TML
230
231---++ ObjectMethod setDefaultHeaders( { $name => $value, ... } )
232
233Sets the header corresponding to the key => value pairs passed in the
234hash, if the key doesn't already exist, otherwise does nothing.
235This ensures some default values are entered, but they can be overridden
236by plugins or other parts in the code.
237
238=cut
239
240sub setDefaultHeaders {
241    my ( $this, $hopt ) = @_;
242    return unless $hopt && keys %$hopt;
243    while ( my ( $hdr, $value ) = each %$hopt ) {
244        $hdr =~ s/(?:^|(?<=-))(.)([^-]*)/\u$1\L$2\E/g;
245        unless ( exists $this->{headers}->{$hdr} ) {
246            if ( $hdr eq 'Status' ) {
247                $this->status($hdr);
248            }
249            elsif ( $hdr eq 'Expires' ) {
250                $value = CGI::Util::expires( $value, 'http' );
251            }
252            elsif ( $hdr eq 'Set-Cookie' ) {
253                my @cookies = ref($value) eq 'ARRAY' ? @$value : ($value);
254                $this->cookies( \@cookies );
255            }
256            $this->{headers}->{$hdr} = $value;
257        }
258    }
259    $this->{headers}{Date} = CGI::Util::expires( 0, 'http' )
260      if !exists $this->{headers}{Date}
261          && (   defined $this->{headers}{Expires}
262              || defined $this->{headers}{'Set-Cookie'} );
263}
264
265=begin TML
266
267---++ ObjectMethod printHeaders()
268
269Return a string of all headers, separated by CRLF
270
271=cut
272
273sub printHeaders {
274    my ($this) = shift;
275    my $CRLF   = "\x0D\x0A";
276    my $hdr    = '';
277
278    # make sure we always generate a status for the response
279    $this->{headers}->{Status} = $this->status()
280      if ( $this->status() && !defined( $this->headers->{Status} ) );
281    foreach my $header ( keys %{ $this->{headers} } ) {
282        $hdr .= $header . ': ' . $_ . $CRLF foreach $this->getHeader($header);
283    }
284    $hdr .= $CRLF;
285    return $hdr;
286}
287
288=begin TML
289
290---++ ObjectMethod deleteHeader($h1, $h2, ...)
291
292Deletes headers whose names are passed.
293
294=cut
295
296sub deleteHeader {
297    my $this = shift;
298
299    ASSERT( !$this->{outputHasStarted}, 'Too late to change headers' ) if DEBUG;
300
301    foreach (@_) {
302        ( my $hdr = $_ ) =~ s/(?:^|(?<=-))(.)([^-]*)/\u$1\L$2\E/g;
303        delete $this->{headers}->{$hdr};
304    }
305}
306
307=begin TML
308
309---++ ObjectMethod pushHeader( $name, $value )
310
311Adds $value to list of values associated with header $name.
312
313=cut
314
315sub pushHeader {
316    my ( $this, $hdr, $value ) = @_;
317
318    ASSERT( !$this->{outputHasStarted}, 'Too late to change headers' ) if DEBUG;
319
320    $hdr =~ s/(?:^|(?<=-))(.)([^-]*)/\u$1\L$2\E/g;
321    my $cur = $this->{headers}->{$hdr};
322    if ($cur) {
323        if ( ref $cur ) {
324            push @{ $this->{headers}->{$hdr} }, $value;
325        }
326        else {
327            $this->{headers}->{$hdr} = [ $cur, $value ];
328        }
329    }
330    else {
331        $this->{headers}->{$hdr} = $value;
332    }
333}
334
335=begin TML
336
337---++ ObjectMethod cookies( [ \@cookies ] ) -> @cookies
338
339Gets/Sets response cookies. Parameter, if passed, *must* be an arrayref.
340
341Elements may be CGI::Cookie objects or raw cookie strings.
342
343WARNING: cookies set this way are *not* passed in redirects.
344
345=cut
346
347sub cookies {
348    return @_ == 1 ? @{ $_[0]->{cookies} } : @{ $_[0]->{cookies} = $_[1] };
349}
350
351=begin TML
352
353---++ ObjectMethod body( [ $body ] ) -> $body
354
355Gets/Sets response body. Note: do not use this method for output, use
356=print= instead.
357
358=cut
359
360sub body {
361    my ( $this, $body ) = @_;
362    if ( defined $body ) {
363
364        # There *is* a risk that a unicode string could reach this far - for
365        # example, if it comes from a plugin. We need to force such strings
366        # into the "Foswiki canonical" representation of a string of bytes.
367        # The output may be crap, but at least it won't trigger a
368        # "Wide character in print" error.
369        if ( utf8::is_utf8($body)
370            and ( $Foswiki::cfg{Site}{CharSet} ne 'utf-8' ) )
371        {
372            require Encode;
373
374#used to encode to 'iso-8859-1', but that seems wrong in light of the cfg settings
375            $body = Encode::encode( $Foswiki::cfg{Site}{CharSet}, $body, 0 );
376        }
377        $this->{headers}->{'Content-Length'} = length($body);
378        $this->{body} = $body;
379    }
380    return $this->{body};
381}
382
383=begin TML
384
385---++ ObjectMethod redirect( $uri, $status, $cookies |
386                             -Location => $uri,
387                             -Status   => $status,
388                             -Cookies  => $cookies )
389
390Populate object with redirect response headers.
391
392=$uri= *must* be passed. Others are optional.
393
394CGI Compatibility Note: It doesn't support -target or -nph
395
396=cut
397
398sub redirect {
399    my ( $this, @p ) = @_;
400    ASSERT( !$this->{outputHasStarted}, 'Too late to redirect' ) if DEBUG;
401    my ( $url, $status, $cookies ) = CGI::Util::rearrange(
402        [ [qw(LOCATION URL URI)], 'STATUS', [qw(COOKIE COOKIES)], ], @p );
403
404    return unless $url;
405
406    $status = 302 unless $status;
407    ASSERT( $status =~ /^30\d( [^\r\n]*)?$/, "Not a valid redirect status: '$status'" ) if DEBUG;
408    return if ( $status && $status !~ /^\s*3\d\d.*/ );
409
410    my @headers = ( -Location => $url );
411    push @headers, '-Status' => $status;
412    push @headers, '-Cookie' => $cookies if $cookies;
413    $this->header(@headers);
414}
415
416=begin TML
417
418---++ ObjectMethod print(...)
419
420Add content to the end of the body.
421
422=cut
423
424sub print {
425    my $this = shift;
426    $this->{body} = '' unless defined $this->{body};
427    $this->body( $this->{body} . join( '', @_ ) );
428}
429
430=begin TML
431
432---++ ObjectMethod outputHasStarted([$boolean])
433
434Get/set the output-has-started flag. This is used by the Foswiki::Engine
435to separate header and body output. Once output has started, the headers
436cannot be changed (though the body can be modified)
437
438=cut
439
440sub outputHasStarted {
441    my ( $this, $flag ) = @_;
442    $this->{outputHasStarted} = $flag if defined $flag;
443    return $this->{outputHasStarted};
444}
445
4461;
447__END__
448Foswiki - The Free and Open Source Wiki, http://foswiki.org/
449
450Copyright (C) 2008-2010 Foswiki Contributors. Foswiki Contributors
451are listed in the AUTHORS file in the root of this distribution.
452NOTE: Please extend that file, not this notice.
453
454Additional copyrights apply to some or all of the code in this
455file as follows:
456
457Copyright (C) 1999-2007 Peter Thoeny, peter@thoeny.org
458and TWiki Contributors. All Rights Reserved. TWiki Contributors
459are listed in the AUTHORS file in the root of this distribution.
460
461This module is based/inspired on Catalyst framework, and also CGI,
462CGI::Simple and HTTP::Headers modules. Refer to
463http://search.cpan.org/~mramberg/Catalyst-Runtime-5.7010/lib/Catalyst.pm,
464http://search.cpan.org/~lds/CGI.pm-3.29/CGI.pm and
465http://search.cpan.org/author/ANDYA/CGI-Simple-1.103/lib/CGI/Simple.pm
466http://search.cpan.org/~gaas/libwww-perl-5.808/lib/HTTP/Headers.pm
467for credits and liscence details.
468
469This program is free software; you can redistribute it and/or
470modify it under the terms of the GNU General Public License
471as published by the Free Software Foundation; either version 2
472of the License, or (at your option) any later version. For
473more details read LICENSE in the root of this distribution.
474
475This program is distributed in the hope that it will be useful,
476but WITHOUT ANY WARRANTY; without even the implied warranty of
477MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
478
479As per the GPL, removal of this notice is prohibited.
Note: See TracBrowser for help on using the repository browser.