Merge 'trunk' into 'param_filtering'
Tomas Doran [Mon, 1 Mar 2010 19:10:02 +0000 (19:10 +0000)]
r12135@t0mlaptop (orig r12100):  t0m | 2009-12-01 02:16:08 +0000
Fix bug in Catalyst::Engine which could cause it to all go wrong if read returned '0' as per thread on mailing list re 'Wrong Content-Length value' and clarify docs about read and read_chunk methods in Catalyst::Engine
r12149@t0mlaptop (orig r12114):  t0m | 2009-12-01 04:19:44 +0000
The documentation example had the variables the opposite way round to my working nginx config. CLEARLY this meant the documentation needed the variables (but not their values) transposing. Yes, yes - I really am _that_ dumb.
r12181@t0mlaptop (orig r12146):  t0m | 2009-12-02 15:10:52 +0000
Changes, bump version
r12184@t0mlaptop (orig r12149):  t0m | 2009-12-02 18:58:14 +0000
Pod nits, rt#52370
r12185@t0mlaptop (orig r12150):  t0m | 2009-12-02 19:13:19 +0000
And that's what I get for not really applying the patch and doing it manually. Fail..
r12186@t0mlaptop (orig r12151):  gshank | 2009-12-02 21:52:21 +0000
add another failing test for Chained CaptureArgs preference

r12203@t0mlaptop (orig r12168):  t0m | 2009-12-03 17:15:17 +0000
I can't stand the over-long debug screens any more. Suggestions on how to do this in a less gross way?
r12244@t0mlaptop (orig r12209):  t0m | 2009-12-06 12:47:59 +0000
Win32 fix, kmx++
r12245@t0mlaptop (orig r12210):  t0m | 2009-12-06 13:04:50 +0000
Skip on win32, hopefully we'll get a better answer than this, but lets stop failing
r12253@t0mlaptop (orig r12218):  rafl | 2009-12-06 16:31:52 +0000
Undocument $request->user.
r12254@t0mlaptop (orig r12219):  autarch | 2009-12-06 18:44:36 +0000
Add restartdirectory as alias for restart_directory, for backcompat

r12255@t0mlaptop (orig r12220):  autarch | 2009-12-06 18:48:34 +0000
fix pod for Server script so options match actual attr name
r12257@t0mlaptop (orig r12222):  autarch | 2009-12-06 18:50:57 +0000
Doc change for --restartdirectory

r12260@t0mlaptop (orig r12225):  kmx | 2009-12-06 20:11:43 +0000
Win32 fix: reverting commit 12210 (after a discussion with t0m) - all tests pass on Win32/strawberry perl 5.8.9 and 5.10.1
r12287@t0mlaptop (orig r12252):  autarch | 2009-12-08 05:38:40 +0000
Don't default to localhost for --host

Tweak docs for --host to say it accepts a name or IP

r12288@t0mlaptop (orig r12253):  autarch | 2009-12-08 05:39:25 +0000
Doc --host change

r12291@t0mlaptop (orig r12256):  t0m | 2009-12-08 11:16:44 +0000
Fix tests from r12252, add comments to make the behavior explicit
r12306@t0mlaptop (orig r12271):  t0m | 2009-12-09 18:30:53 +0000
Regression for 5.80015 when rewriting an app, nanonyme++, Khisanth++
r12311@t0mlaptop (orig r12276):  t0m | 2009-12-09 20:21:13 +0000
Fixes port environment, RT#52604
r12312@t0mlaptop (orig r12277):  t0m | 2009-12-09 20:35:13 +0000
And more tests and fixing for the same thing
r12313@t0mlaptop (orig r12278):  t0m | 2009-12-09 20:50:18 +0000
Changelog
r12314@t0mlaptop (orig r12279):  t0m | 2009-12-09 20:54:53 +0000
More tests for the prepare_path thing, fix said tests, changelog
r12316@t0mlaptop (orig r12281):  t0m | 2009-12-09 21:36:13 +0000
Additional test
r12325@t0mlaptop (orig r12290):  t0m | 2009-12-10 09:46:10 +0000
Fix RT#52630
r12342@t0mlaptop (orig r12307):  t0m | 2009-12-11 12:44:55 +0000
Unfuck that as well
r12350@t0mlaptop (orig r12315):  t0m | 2009-12-11 23:25:35 +0000
Remove warning when running tests aggregated
r12351@t0mlaptop (orig r12316):  t0m | 2009-12-11 23:26:08 +0000
Changelog, bump version
r12352@t0mlaptop (orig r12317):  rafl | 2009-12-11 23:28:09 +0000
moar better warnings fix.
r12422@t0mlaptop (orig r12387):  t0m | 2009-12-15 10:21:32 +0000
Bug fix for issue reported on the mailing list by Toby Corkindale
r12466@t0mlaptop (orig r12431):  t0m | 2009-12-18 19:55:03 +0000
Fix RT#52898, __MOP__ removal breaking debug screen with C::P::Session
r12497@t0mlaptop (orig r12462):  t0m | 2009-12-22 14:19:36 +0000
Someone think of a less fugly way of doing this please? Fixes using rewrite rules to ask for a sub-path in your app with apache in some combinations..
r12528@t0mlaptop (orig r12493):  t0m | 2009-12-30 15:55:43 +0000
Fix regex special characters screwing things up by not using regexes
r12559@t0mlaptop (orig r12524):  t0m | 2010-01-04 20:52:10 +0000
Doc fix
r12560@t0mlaptop (orig r12525):  t0m | 2010-01-04 20:55:25 +0000
Clarify comment
r12564@t0mlaptop (orig r12529):  rafl | 2010-01-05 00:28:35 +0000
Stop supressing Adopt::NEXT warnings.
r12570@t0mlaptop (orig r12535):  rafl | 2010-01-06 15:59:41 +0000
Clarify comment.
r12589@t0mlaptop (orig r12554):  t0m | 2010-01-09 15:37:00 +0000
Don't screw over people using --detach, <sigh>
r12590@t0mlaptop (orig r12555):  t0m | 2010-01-09 15:57:27 +0000
Back out r12493, use \Q instead
r12591@t0mlaptop (orig r12556):  t0m | 2010-01-09 15:58:18 +0000
Changelog Adopt::NEXT warnings
r12592@t0mlaptop (orig r12557):  t0m | 2010-01-09 16:43:25 +0000
Correctly pass argv option into Catalyst::Engine::HTTP
r12593@t0mlaptop (orig r12558):  t0m | 2010-01-09 16:54:08 +0000
Un stupid
r12594@t0mlaptop (orig r12559):  t0m | 2010-01-09 17:38:59 +0000
Bump dep
r12608@t0mlaptop (orig r12573):  t0m | 2010-01-09 18:22:02 +0000
Bump version of ::Role::WithOverloading
r12610@t0mlaptop (orig r12575):  t0m | 2010-01-09 19:01:59 +0000
require autoclean once only
r12622@t0mlaptop (orig r12587):  rafl | 2010-01-10 02:00:03 +0000
Version 5.80017.
r12631@t0mlaptop (orig r12596):  t0m | 2010-01-10 14:22:15 +0000
Apply patch to clarify uri_for action from Octavian Rasnita on list
r12640@t0mlaptop (orig r12605):  t0m | 2010-01-11 21:11:05 +0000
Deprecate bare imports of Catalyst::Test - either use an app name or don't run the import method. As-per r12564
r12648@t0mlaptop (orig r12613):  t0m | 2010-01-11 23:18:08 +0000
Fix URI bug masked by HTTP::Request::AsCGI
r12652@t0mlaptop (orig r12617):  rafl | 2010-01-12 21:37:31 +0000
Fix a deprecation warning in the tests.
r12653@t0mlaptop (orig r12618):  rafl | 2010-01-12 21:37:39 +0000
canonical() is a no-op for the base uri.
r12654@t0mlaptop (orig r12619):  rafl | 2010-01-12 21:37:46 +0000
Version 5.80018.
r12669@t0mlaptop (orig r12634):  rafl | 2010-01-14 02:26:03 +0000
Depend on n:c 0.12 to work on perl >= 5.11.2.
r12673@t0mlaptop (orig r12638):  rafl | 2010-01-14 05:21:24 +0000
Exception stuff is fixed for a while now.
r12674@t0mlaptop (orig r12639):  rafl | 2010-01-14 05:45:09 +0000
Only set up the leakchecker for the tests that need it.

That way we avoid the useless Devel::Cycle glob warnings.
r12676@t0mlaptop (orig r12641):  dandv | 2010-01-14 08:15:10 +0000
Typo fix
r12678@t0mlaptop (orig r12643):  dandv | 2010-01-14 09:23:50 +0000
Cosmetic: wrapped long code line
r12690@t0mlaptop (orig r12655):  dandv | 2010-01-14 23:52:44 +0000
Passing test case for RT #53678 - Catalyst::Test::get currently doesn't decode the response body octets

r12691@t0mlaptop (orig r12656):  karpet | 2010-01-15 06:35:30 +0000
in what case is a numeric comparison called for? I cannot think of one. Is this the best way to test?
r12698@t0mlaptop (orig r12663):  t0m | 2010-01-15 16:39:55 +0000
Clarify that it's an app, not a ctx here
r12713@t0mlaptop (orig r12678):  rafl | 2010-01-18 09:00:20 +0000
Depend on a namespace::clean that isn't broken on <= 5.8.8.
r12725@t0mlaptop (orig r12690):  rafl | 2010-01-19 16:07:45 +0000
Merge branch 'action_args'

* action_args:
  And another minor tweak.
  Some more doc tweaking.
  tweaked docs based on IRC suggestions
  added documentation for the configuration option "action_args".
  Allow passing extra args to action constructors using action_args config.
  Add tests for passing extra arguments to action constructors.
  Create branch action_args
r12726@t0mlaptop (orig r12691):  rafl | 2010-01-19 16:08:22 +0000
Changelog action_args.
r12728@t0mlaptop (orig r12693):  aristotle | 2010-01-19 21:41:23 +0000
fix $c->error doc in Catalyst.pm POD
r12761@t0mlaptop (orig r12726):  aCiD2 | 2010-01-25 01:03:52 +0000
Improve the documentation about -Home and how Catalyst finds the home path for apps
r12762@t0mlaptop (orig r12727):  rafl | 2010-01-25 18:59:52 +0000
Revert "in what case is a numeric comparison called for? I cannot think of one. Is this the best way to test?"

This reverts commit 279b2f1abb0a8e056b8c905e5a4ecb66537ee194.
r12763@t0mlaptop (orig r12728):  rafl | 2010-01-25 19:00:17 +0000
Remove apparently useless Component::BUILDARGS conditional.
r12778@t0mlaptop (orig r12743):  t0m | 2010-01-27 20:12:46 +0000
Clarrify debug documentation
r12779@t0mlaptop (orig r12744):  t0m | 2010-01-27 20:15:52 +0000
Fix failing test related to missing g in regex
r12781@t0mlaptop (orig r12746):  t0m | 2010-01-27 21:04:17 +0000
Fix / => %2F in uri_for so that it only works for action objects as previously advertised as this has been screwing people. Also fix multiple / => %2F conversion in the same arg/capture
r12793@t0mlaptop (orig r12758):  t0m | 2010-01-28 15:47:31 +0000
Fix paths with URI encoding as the first path part
r12794@t0mlaptop (orig r12759):  t0m | 2010-01-28 18:15:10 +0000
And test the non root app case
r12804@t0mlaptop (orig r12769):  t0m | 2010-01-28 23:28:15 +0000
Changelog up to date
r12805@t0mlaptop (orig r12770):  rafl | 2010-01-29 00:16:42 +0000
Changelog tweaking.
r12806@t0mlaptop (orig r12771):  rafl | 2010-01-29 00:16:48 +0000
Version 5.80019.
r12845@t0mlaptop (orig r12810):  rafl | 2010-02-04 05:50:05 +0000
Merge branch 'expand_modules'

* expand_modules:
  Allow models and components to specify the names of any components they generate
  Branching to allow components to specify any modules they may have created
r12846@t0mlaptop (orig r12811):  rafl | 2010-02-04 06:18:46 +0000
Version 5.80020.
r12870@t0mlaptop (orig r12835):  t0m | 2010-02-08 21:46:15 +0000
 r12572@t0mlaptop (orig r12537):  caelum | 2010-01-06 22:33:51 +0000
 branch to utf8-decode captures and args
 r12573@t0mlaptop (orig r12538):  caelum | 2010-01-06 22:56:59 +0000
 utf8::decode captures and args, and uri-escape captures
 r12574@t0mlaptop (orig r12539):  caelum | 2010-01-07 00:04:45 +0000
 add a test for the uri_for utf8 stuff
 r12869@t0mlaptop (orig r12834):  t0m | 2010-02-08 21:20:05 +0000
 Unfuck

r12889@t0mlaptop (orig r12853):  caelum | 2010-02-09 22:26:23 +0000
update Changes, put comment back in
r12890@t0mlaptop (orig r12854):  t0m | 2010-02-11 20:06:15 +0000
Fix typo
r12891@t0mlaptop (orig r12855):  t0m | 2010-02-11 20:36:22 +0000
First attempt to make this make more sense.
r12897@t0mlaptop (orig r12861):  rafl | 2010-02-14 11:30:23 +0000
Make the debug log say the cat version with all its digits.
r13014@t0mlaptop (orig r12978):  rafl | 2010-02-23 17:52:30 +0000
fix doc typo.

lib/Catalyst.pm
t/unit_core_debug_filtering.t [new file with mode: 0644]

index 4d53a0d..ef99798 100644 (file)
@@ -1743,6 +1743,8 @@ sub finalize {
         $c->finalize_body;
     }
 
+       $c->log_response;
+
     if ($c->use_stats) {
         my $elapsed = sprintf '%f', $c->stats->elapsed;
         my $av = $elapsed == 0 ? '??' : sprintf '%.3f', 1 / $elapsed;
@@ -1967,8 +1969,7 @@ sub prepare {
     $path       = '/' unless length $path;
     my $address = $c->req->address || '';
 
-    $c->log->debug(qq/"$method" request for "$path" from "$address"/)
-      if $c->debug;
+    $c->log_request;
 
     $c->prepare_action;
 
@@ -1998,17 +1999,6 @@ sub prepare_body {
     $c->engine->prepare_body( $c, @_ );
     $c->prepare_parameters;
     $c->prepare_uploads;
-
-    if ( $c->debug && keys %{ $c->req->body_parameters } ) {
-        my $t = Text::SimpleTable->new( [ 35, 'Parameter' ], [ 36, 'Value' ] );
-        for my $key ( sort keys %{ $c->req->body_parameters } ) {
-            my $param = $c->req->body_parameters->{$key};
-            my $value = defined($param) ? $param : '';
-            $t->row( $key,
-                ref $value eq 'ARRAY' ? ( join ', ', @$value ) : $value );
-        }
-        $c->log->debug( "Body Parameters are:\n" . $t->draw );
-    }
 }
 
 =head2 $c->prepare_body_chunk( $chunk )
@@ -2092,55 +2082,328 @@ sub prepare_query_parameters {
     my $c = shift;
 
     $c->engine->prepare_query_parameters( $c, @_ );
+}
 
-    if ( $c->debug && keys %{ $c->request->query_parameters } ) {
-        my $t = Text::SimpleTable->new( [ 35, 'Parameter' ], [ 36, 'Value' ] );
-        for my $key ( sort keys %{ $c->req->query_parameters } ) {
-            my $param = $c->req->query_parameters->{$key};
-            my $value = defined($param) ? $param : '';
-            $t->row( $key,
-                ref $value eq 'ARRAY' ? ( join ', ', @$value ) : $value );
+=head2 $c->apply_parameter_debug_filters($params)
+
+=cut
+
+sub _apply_parameter_debug_filters {
+    my $c      = shift;
+    my $type   = shift;
+    my $params = shift;
+
+    # take a copy since we don't want to modify the original
+    my $filtered_params = {%$params};
+
+    my @filters;
+
+    my $filter_param_config = $c->config->{Debug}->{param_filters};
+    if ( ref($filter_param_config) eq 'HASH' ) {
+
+        # filters broken out by parameter type (i.e. body, query, all)
+        my $type_filters = $filter_param_config->{$type} || [];
+        $type_filters = [$type_filters] if ref $type_filters ne 'ARRAY';
+
+        my $all_filters = $filter_param_config->{'all'} || [];
+        $all_filters = [$all_filters] if ref $all_filters ne 'ARRAY';
+
+        @filters = $c->_normalize_debug_filters( [ @$type_filters, @$all_filters ] );
+    } elsif ($filter_param_config) {
+        @filters = $c->_normalize_debug_filters($filter_param_config);
+    }
+
+    # allow callback to modify each parameter
+    foreach my $k ( keys %$filtered_params ) {
+
+        # apply filters to each param
+        foreach my $f (@filters) {
+
+            # take a copy of the key to avoid the callback inadvertantly
+            # modifying things
+            my $copy_key = $k;
+
+            my $returned = $f->( $copy_key => $filtered_params->{$k} );
+
+            if ( defined $returned ) {
+
+                # if no value is returned, we assume the filter chose not to modify anything
+                # otherwise, the returned value is the logged value
+                $filtered_params->{$k} = $returned;
+
+                last;    # skip the rest of the filters since this one matched
+            }
         }
-        $c->log->debug( "Query Parameters are:\n" . $t->draw );
     }
+    return $filtered_params;
 }
 
-=head2 $c->prepare_read
+# turn debug filters into a list of CodeRef's
+sub _normalize_debug_filters {
+    my $c = shift;
 
-Prepares the input for reading.
+    my @filters = ref( $_[0] ) eq 'ARRAY' ? @{ $_[0] } : grep { defined $_ } @_;
 
-=cut
+    my @normalized = map { _make_filter_callback($_) } @filters;
 
-sub prepare_read { my $c = shift; $c->engine->prepare_read( $c, @_ ) }
+    return @normalized;
+}
 
-=head2 $c->prepare_request
+sub _make_filter_callback {
+    my $filter = shift;
 
-Prepares the engine request.
+    my $filter_str = '[FILTERED]';
+    if ( ref($filter) eq 'Regexp' ) {
+        return sub { return $_[0] =~ $filter ? $filter_str  : undef };
+    } elsif ( ref($filter) eq 'CODE' ) {
+        return $filter;
+    } else {
+        return sub { return $_[0] eq $filter ? $filter_str : undef };
+    }
+}
+
+=head2 $c->log_request
+
+Writes information about the request to the debug logs.  This includes:
+
+=over 4
+
+=item * Request method, path, and remote IP address
+
+=item * Query keywords (see L<Catalyst::Request/query_keywords>)
+
+=item * Request parameters (see L</log_request_parameters>)
+
+=item * File uploads
+
+=back
 
 =cut
 
-sub prepare_request { my $c = shift; $c->engine->prepare_request( $c, @_ ) }
+sub log_request {
+    my $c = shift;
 
-=head2 $c->prepare_uploads
+    return unless $c->debug;
 
-Prepares uploads.
+    my ( $method, $path, $address ) = ( $c->req->method, $c->req->path, $c->req->address );
+    $method ||= '';
+    $path = '/' unless length $path;
+    $address ||= '';
+    $c->log->debug(qq/"$method" request for "$path" from "$address"/);
+
+    if ( my $keywords = $c->req->query_keywords ) {
+        $c->log->debug("Query keywords are: $keywords");
+    }
+
+    $c->log_request_parameters( query => $c->req->query_parameters, body => $c->req->body_parameters );
+
+    $c->log_request_uploads;
+}
+
+=head2 $c->log_response
+
+Writes information about the response to the debug logs.  This includes:
+
+=over 4
+
+=item * Response status code
+
+=item * Response headers (see L</log_headers>)
+
+=back
+
+This logging is not enabled by default.  To enable it, you must set a flag in your Catalyst config:
+
+       __PACKAGE__->config( Debug => { log_response => 1 } );
 
 =cut
 
-sub prepare_uploads {
+sub log_response {
     my $c = shift;
 
-    $c->engine->prepare_uploads( $c, @_ );
+    return unless $c->debug && $c->config->{Debug}->{log_response};
+
+    $c->log->debug('Response Status: ' . $c->response->status);
+    $c->log_headers('response', $c->response->headers);
+}
+
+=head2 $c->log_request_parameters( query => {}, body => {} )
+
+Logs request parameters to debug logs
+
+If you have sensitive data that you do not want written to the Catalyst
+debug logs, you can set options in your config to filter those values out.
+There are a few different ways you can set these up depending on what
+exactly you need to filter.
+
+=head3 Filtering parameters by name
+
+The most basic means of filtering is to add an entry into your config
+as shown below.  You can have a simple scalar to just filter a
+single parameter or an ARRAY ref to filter out multiple params.
+
+    # filters a single param
+    __PACKAGE__->config( Debug => { param_filters => 'param_name' } );
+
+    # filters multiple params
+    __PACKAGE__->config( Debug => { param_filters => [qw(param1 param2)] } );
+
+When the debug logs are generated for a given request, any parameters
+(query or body) that exactly match the specified value(s) will have
+their values replaced with '[FILTERED]'.  For instance:
+
+    [debug] Query Parameters are:
+    .-------------------------------------+--------------------------------------.
+    | Parameter                           | Value                                |
+    +-------------------------------------+--------------------------------------+
+    | password                            | [FILTERED]                           |
+    .-------------------------------------+--------------------------------------.
+
+=head3 Filtering parameters by regular expression
+
+If you have a set of parameters you need to filter, you can specify a
+regular expression that will be used to match against parameter names.
+
+    # filters parameters starting with "private."
+    __PACKAGE__->config( Debug => { param_filters => qr/^private\./ } );
+
+    # filters parameters named "param1" or starting with "private." or "secret."
+    __PACKAGE__->config( Debug => { param_filters => [ 'param1', qr/^private\./, qr/^secret\./ ] } );
+
+Notice on the second example, the arrayref contains a string as well
+as two regular expressions.  This should DWIM and filter parameters that
+match any of the filters specified.
+
+=head3 Filtering parameters by callback
+
+If you want even more flexible filtering, you can specify an anonymous
+subroutine.  The subroutine is given the parameter name and value and
+is expected to return the new value that will be shown in the debug log.
+An C<undef> return value indicates that no change should be made to
+the value.
+
+    # transform any "password" param to "********"
+    __PACKAGE__->config(
+        Debug => {
+            param_filters => sub { my ( $k, $v ) = @_; return unless $k eq 'password'; return '*' x 8; }
+        }
+    );
+
+    # combine several param filtering methods
+    __PACKAGE__->config(
+        Debug => {
+            param_filters => [
+                'simple_param_name',
+                qr/^private\./,
+                sub { my ( $k, $v ) = @_; return unless $k eq 'password'; return '*' x 8; },
+            ]
+        }
+    );
+
+An example of the debug log for a request with 
+C<password=secret&some_other_param=some_other_value> would be:
+
+    [debug] Body Parameters are:
+    .-------------------------------------+--------------------------------------.
+    | Parameter                           | Value                                |
+    +-------------------------------------+--------------------------------------+
+    | some_other_param                    | some_other_value                     |
+    | password                            | ********                             |
+    .-------------------------------------+--------------------------------------.
+
+=head3 Filtering by parameter location
+
+If you have different filters that depend on whether a param was passed
+as a query or body param (or as either), you can specify a hashref with
+different sets of filters:
+
+    # filters all body parameters
+    __PACKAGE__->config( Debug => { param_filters => { body => qr// } } );
+
+    # filters query parameters starting with 'private'.
+    __PACKAGE__->config( Debug => { param_filters => { query => qr/^private\./ } } );
+
+    # filters all parameters (query or body) through the specified callback
+    __PACKAGE__->config(
+        Debug => {
+            param_filters => {
+                all => sub { return unless $_[0] eq 'fizzbuzz'; return 'FIZZBUZZ FILTER' }
+            }
+        }
+    );
+
+Of course, you can use any of the above filtering methods with these
+"location-specific" filters:
+
+    # body parameter filters
+    __PACKAGE__->config(
+        Debug => {
+            param_filters => {
+                body => [
+                    'some_param',
+                    qr/^private\./,
+                    sub { return 'XXX' if shift eq 'other_param' }
+                ]
+            }
+        }
+    );
+
+    # query parameter filters
+    __PACKAGE__->config(
+        Debug => {
+            param_filters => {
+                body => [
+                    'some_param',
+                    qr/^private\./,
+                    sub { return 'XXX' if shift eq 'other_param' }
+                ]
+            }
+        }
+    );
 
-    if ( $c->debug && keys %{ $c->request->uploads } ) {
+    # query parameter filters
+    __PACKAGE__->config( Debug => { param_filters => { all => [qw(foo bar)] } } );
+
+=cut
+
+sub log_request_parameters {
+    my $c          = shift;
+    my %all_params = @_;
+
+    my $column_width = Catalyst::Utils::term_width() - 44;
+    foreach my $type (qw(query body)) {
+        my $filtered_params = $c->_apply_parameter_debug_filters( $type, $all_params{$type} || {} );
+        next unless keys %$filtered_params;
+        my $t = Text::SimpleTable->new( [ 35, 'Parameter' ], [ $column_width, 'Value' ] );
+        for my $key ( sort keys %$filtered_params ) {
+            my $param = $filtered_params->{$key};
+            my $value = defined($param) ? $param : '';
+            $t->row( $key, ref $value eq 'ARRAY' ? ( join ', ', @$value ) : $value );
+        }
+        $c->log->debug( ucfirst($type) . " Parameters are:\n" . $t->draw );
+    }
+}
+
+=head2 $c->log_request_uploads
+
+Logs file uploads included in the request to the debug logs.
+The parameter name, filename, file type, and file size are all included in
+the debug logs.
+
+=cut
+
+sub log_request_uploads {
+    my $c = shift;
+    my $uploads = $c->req->uploads;
+    if ( keys %$uploads ) {
         my $t = Text::SimpleTable->new(
             [ 12, 'Parameter' ],
             [ 26, 'Filename' ],
             [ 18, 'Type' ],
             [ 9,  'Size' ]
         );
-        for my $key ( sort keys %{ $c->request->uploads } ) {
-            my $upload = $c->request->uploads->{$key};
+        for my $key ( sort keys %$uploads ) {
+            my $upload = $uploads->{$key};
             for my $u ( ref $upload eq 'ARRAY' ? @{$upload} : ($upload) ) {
                 $t->row( $key, $u->filename, $u->type, $u->size );
             }
@@ -2149,6 +2412,106 @@ sub prepare_uploads {
     }
 }
 
+=head2 $c->log_headers($type => $headers)
+
+Writes HTTP::Headers to debug logs, applying filters as configured.
+
+Similarly to how L</log_request_parameters> is configured, you can
+configure Catalyst to filter response header values to avoid writing
+sensitive data to your logs (e.g. cookie values, etc.). The configuration
+works in virtually the same way as the examples in
+L</log_request_parameters>.  Here are a few specific examples:
+
+    # filters all "Set-Cookie" headers from response logging
+    __PACKAGE__->config(Debug => { response_header_filters => 'Set-Cookie' } );
+
+    # filters only the value of the cookie (and leaves the name, path, expiration)
+    __PACKAGE__->config(
+        Debug => {
+            response_header_filters => sub {
+                my ( $n, $v ) = @_;
+                return unless $n eq 'Set-Cookie';
+                $v =~ s/^.*?;//;
+                return $v;
+            },
+        }
+    );
+
+=cut
+
+sub log_headers {
+    my $c       = shift;
+    my $type    = shift;
+    my $headers = shift;    # an HTTP::Headers instance
+
+    my $filtered = $c->_apply_header_debug_filters( $type, $headers );
+
+    my $t = Text::SimpleTable->new( [ 35, 'Header Name' ], [ 40, 'Value' ] );
+    $filtered->scan(
+        sub {
+            my ( $name, $value ) = @_;
+            $t->row( $name, $value );
+        }
+    );
+    $c->log->debug( ucfirst($type) . " Headers:\n" . $t->draw );
+}
+
+# Applies debug filters to $headers and returns a new HTTP::Headers object which has (potentially) filtered values.
+sub _apply_header_debug_filters {
+    my $c    = shift;
+    my $type    = shift;
+    my $headers = shift;
+
+    my @header_filters   = $c->_normalize_debug_filters( $c->config->{Debug}->{ $type . '_header_filters' } );
+    my $filtered_headers = HTTP::Headers->new();
+    foreach my $name ( $headers->header_field_names ) {
+        my @values = $headers->header($name);
+
+        # headers can be multi-valued
+        foreach my $value (@values) {
+            foreach my $f (@header_filters) {
+                my $new_value = $f->( $name, $value );
+
+                # if a defined value is returned, we use that
+                if ( defined $new_value ) {
+                    $value = $new_value;
+                    last;    # skip the rest of the filters
+                }
+            }
+            $filtered_headers->push_header( $name, $value );
+        }
+    }
+    return $filtered_headers;
+}
+
+=head2 $c->prepare_read
+
+Prepares the input for reading.
+
+=cut
+
+sub prepare_read { my $c = shift; $c->engine->prepare_read( $c, @_ ) }
+
+=head2 $c->prepare_request
+
+Prepares the engine request.
+
+=cut
+
+sub prepare_request { my $c = shift; $c->engine->prepare_request( $c, @_ ) }
+
+=head2 $c->prepare_uploads
+
+Prepares uploads.
+
+=cut
+
+sub prepare_uploads {
+    my $c = shift;
+
+    $c->engine->prepare_uploads( $c, @_ );
+}
+
 =head2 $c->prepare_write
 
 Prepares the output for writing.
diff --git a/t/unit_core_debug_filtering.t b/t/unit_core_debug_filtering.t
new file mode 100644 (file)
index 0000000..babaf63
--- /dev/null
@@ -0,0 +1,71 @@
+use strict;
+use warnings;
+use Test::More tests=>15;
+
+use Catalyst;
+use HTTP::Headers;
+my $c = Catalyst->new( {} );
+$c->config(Debug => {param_filters => 'simple_str'});
+
+isa_ok( $c, 'Catalyst' );
+my $params = $c->_apply_parameter_debug_filters( 'query', {} );
+is_deeply( $params, {}, 'empty param list' );
+my $filter_str = '[FILTERED]';
+
+$params = $c->_apply_parameter_debug_filters( 'body', { simple_str => 1, other_str => 2 } );
+is( $params->{simple_str}, $filter_str, 'filtered simple_str' );
+is( $params->{other_str},  '2',         "didn't filter other_str" );
+
+$c->config( Debug => { param_filters => [qw(a b)] } );
+$params = $c->_apply_parameter_debug_filters( 'query', { a => 1, b => 2, c => 3 }, );
+
+is_deeply( $params, { a => $filter_str, b => $filter_str, c => 3 }, 'list of simple param names' );
+
+$c->config( Debug => { param_filters => qr/^foo/ } );
+$params = $c->_apply_parameter_debug_filters( 'query', { foo => 1, foobar => 2, c => 3 }, );
+is_deeply( $params, { foo => $filter_str, foobar => $filter_str, c => 3 }, 'single regex' );
+
+$c->config(Debug => {param_filters => [qr/^foo/, qr/bar/, 'simple']});
+$params = $c->_apply_parameter_debug_filters( 'query', { foo => 1, foobar => 2, bar => 3, c => 3, simple => 4 }, );
+is_deeply( $params, { foo => $filter_str, foobar => $filter_str, bar => $filter_str, c => 3, simple => $filter_str }, 'array of regexes and a simple filter' );
+
+$c->config(
+    Debug => {
+        param_filters => sub { return unless shift eq 'password'; return '*' x 8 }
+    }
+);
+$params = $c->_apply_parameter_debug_filters( 'query', { password => 'secret', other => 'public' }, );
+is_deeply( $params, { other => 'public', password => '********' }, 'single CODE ref' );
+
+$c->config( Debug => { param_filters => { body => qr// } } );
+$params = $c->_apply_parameter_debug_filters( 'query', { a=>1, b=>2 } );
+is_deeply( $params, { a=>1, b=>2 }, 'body filters do not modify query params' );
+$params = $c->_apply_parameter_debug_filters( 'body', { a=>1, b=>2 } );
+is_deeply( $params, { a => $filter_str, b => $filter_str }, 'all body params filtered' );
+
+$c->config( Debug => { param_filters => undef } );
+$c->config( Debug => { param_filters => { all => [qw(foo bar)] } } );
+$params = $c->_apply_parameter_debug_filters( 'body', { foo=>1, bar=>2, baz=>3 } );
+is_deeply( $params, { foo => $filter_str, bar => $filter_str, baz => 3 }, 'using the "all" type filter on body params' );
+$params = $c->_apply_parameter_debug_filters( 'query', { foo=>1, bar=>2, baz=>3 } );
+is_deeply( $params, { foo => $filter_str, bar => $filter_str, baz => 3 }, 'using the "all" type filter on query params' );
+
+my $headers = HTTP::Headers->new(
+    Content_type => 'text/html',
+    Set_Cookie => 'session_id=abc123; expires=Fri, 31-Dec-2010 23:59:59 GMT; path=/; domain=.example.org.',
+    Set_Cookie => 'something_else=xyz890; expires=Fri, 31-Dec-2010 23:59:59 GMT; path=/; domain=.example.org.',
+);
+$c->config(
+    Debug => {
+        response_header_filters => sub {
+            my ( $n, $v ) = @_;
+            return unless $n eq 'Set-Cookie';
+            $v =~ s/session_id=.*?;/session_id=SECRET/;
+            return $v;
+        },
+    }
+);
+my $filtered = $c->_apply_header_debug_filters(response => $headers);
+is($filtered->header('Content-Type'), 'text/html', 'Content-Type header left alone');
+like($filtered->as_string, qr/session_id=SECRET/, 'Set-Cookie value filtered');
+like($filtered->as_string, qr/something_else=xyz890/, 'non-session_id cookie not filtered');