merged from master to sync release
John Napiorkowski [Wed, 25 Mar 2015 20:24:33 +0000 (15:24 -0500)]
13 files changed:
Changes
lib/Catalyst.pm
lib/Catalyst/Controller.pm
lib/Catalyst/DispatchType/Chained.pm
lib/Catalyst/Engine.pm
lib/Catalyst/Runtime.pm
lib/Catalyst/UTF8.pod
lib/Catalyst/Upgrading.pod
t/args0_bug.t [new file with mode: 0644]
t/lib/TestApp/Controller/HTTPMethods.pm
t/no_test_stash_bug.t [new file with mode: 0644]
t/not_utf8_query_bug.t [new file with mode: 0644]
t/utf_incoming.t

diff --git a/Changes b/Changes
index 0d1c586..e1eaea0 100644 (file)
--- a/Changes
+++ b/Changes
@@ -6,6 +6,20 @@
     CaptureArg.
   - New top level document on Route matching. (Catalyst::RouteMatching).
 
+5.90085 - 2015-03-25
+  - Small change to Catalyst::Action to prevent autovivication of Args value (dim1++)
+  - Minor typo fixes (Abraxxa++)
+  - Make sure than when using chained actions and when more than one action
+    matches the same path specification AND has Args(0), that we follow the
+    "in a tie, the last action defined wins" rule.  There is a small chance
+    this is a breaking change for you.  See Catalyst::Upgrading for more.
+    You may use the application configuration setting "use_chained_args_0_special_case"
+    to disable this new behavior, if you must for back-compat reasons.
+  - Added PATCH HTTP Method action attribute shortcut.
+  - Several new configuration options aimed to give improved backwards compatibility
+    for when your URL query parameters or keywords have non UTF-8 encodings.
+    See Catalyst::Upgrading.
+
 5.90084 - 2015-02-23
   - Small change to the way body parameters are created in order to prevent
     trying to create parameters twice.
index 0d8a817..03acccc 100644 (file)
@@ -3904,6 +3904,51 @@ parameter to true.
 
 =item *
 
+C<do_not_decode_query>
+
+If true, then do not try to character decode any wide characters in your
+request URL query or keywords.  Most readings of the relevent specifications
+suggest these should be UTF-* encoded, which is the default that L<Catalyst>
+will use, hwoever if you are creating a lot of URLs manually or have external
+evil clients, this might cause you trouble.  If you find the changes introduced
+in Catalyst version 5.90080+ break some of your query code, you may disable 
+the UTF-8 decoding globally using this configuration.
+
+This setting takes precedence over C<default_query_encoding> and
+C<decode_query_using_global_encoding>
+
+=item *
+
+C<default_query_encoding>
+
+By default we decode query and keywords in your request URL using UTF-8, which
+is our reading of the relevent specifications.  This setting allows one to
+specify a fixed value for how to decode your query.  You might need this if
+you are doing a lot of custom encoding of your URLs and not using UTF-8.
+
+This setting take precedence over C<decode_query_using_global_encoding>.
+
+=item *
+
+C<decode_query_using_global_encoding>
+
+Setting this to true will default your query decoding to whatever your
+general global encoding is (the default is UTF-8).
+
+=item *
+
+C<use_chained_args_0_special_case>
+
+In older versions of Catalyst, when more than one action matched the same path
+AND all those matching actions declared Args(0), we'd break the tie by choosing
+the first action defined.  We now normalized how Args(0) works so that it
+follows the same rule as Args(N), which is to say when we need to break a tie
+we choose the LAST action defined.  If this breaks your code and you don't
+have time to update to follow the new normalized approach, you may set this
+value to true and it will globally revert to the original chaining behavior.
+
+=item *
+
 C<psgi_middleware> - See L<PSGI MIDDLEWARE>.
 
 =item *
index c94f22e..9b8b037 100644 (file)
@@ -550,6 +550,7 @@ sub _parse_PUT_attr     { Method => 'PUT'     }
 sub _parse_DELETE_attr  { Method => 'DELETE'  }
 sub _parse_OPTIONS_attr { Method => 'OPTIONS' }
 sub _parse_HEAD_attr    { Method => 'HEAD'    }
+sub _parse_PATCH_attr  { Method => 'PATCH'  }
 
 sub _expand_role_shortname {
     my ($self, @shortnames) = @_;
index 421175f..b9d6d07 100644 (file)
@@ -298,13 +298,29 @@ sub recurse_match {
                 #    The current best action might also be Args(0),
                 #    but we couldn't chose between then anyway so we'll take the last seen
 
-                if (!$best_action                       ||
+                if (
+                    !$best_action                       ||
                     @parts < @{$best_action->{parts}}   ||
-                    (!@parts && defined($args_attr) && $args_attr eq "0")){
+                    (
+                        !@parts && 
+                        defined($args_attr) && 
+                        (
+                            $args_attr eq "0" &&
+                            (
+                              ($c->config->{use_chained_args_0_special_case}||0) || 
+                                (
+                                  exists($best_action->{args_attr}) && defined($best_action->{args_attr}) ?
+                                  ($best_action->{args_attr} ne 0) : 1
+                                )
+                            )
+                        )
+                    )
+                ){
                     $best_action = {
                         actions => [ $action ],
                         captures=> [],
                         parts   => \@parts,
+                        args_attr => $args_attr,
                         n_pathparts => scalar(@pathparts),
                     };
                 }
index 44c9c12..e4deb9e 100644 (file)
@@ -10,7 +10,7 @@ use HTML::Entities;
 use HTTP::Headers;
 use Plack::Loader;
 use Catalyst::EngineLoader;
-use Encode 2.21 'decode_utf8';
+use Encode 2.21 'decode_utf8', 'encode', 'decode';
 use Plack::Request::Upload;
 use Hash::MultiValue;
 use namespace::clean -except => 'meta';
@@ -573,6 +573,17 @@ process the query string and extract query parameters.
 sub prepare_query_parameters {
     my ($self, $c) = @_;
     my $env = $c->request->env;
+    my $do_not_decode_query = $c->config->{do_not_decode_query};
+    my $default_query_encoding = $c->config->{default_query_encoding} || 
+      ($c->config->{decode_query_using_global_encoding} ?
+        $c->encoding : 'UTF-8');
+
+    my $decoder = sub {
+      my $str = shift;
+      return $str if $do_not_decode_query;
+      return $str unless $default_query_encoding;
+      return decode( $default_query_encoding, $str);
+    };
 
     my $query_string = exists $env->{QUERY_STRING}
         ? $env->{QUERY_STRING}
@@ -582,7 +593,7 @@ sub prepare_query_parameters {
     # (yes, index() is faster than a regex :))
     if ( index( $query_string, '=' ) < 0 ) {
         my $keywords = $self->unescape_uri($query_string);
-        $keywords = decode_utf8 $keywords;
+        $keywords = $decoder->($keywords);
         $c->request->query_keywords($keywords);
         return;
     }
@@ -590,7 +601,7 @@ sub prepare_query_parameters {
     $query_string =~ s/\A[&;]+//;
 
     my $p = Hash::MultiValue->new(
-        map { defined $_ ? decode_utf8($self->unescape_uri($_)) : $_ }
+        map { defined $_ ? $decoder->($self->unescape_uri($_)) : $_ }
         map { ( split /=/, $_, 2 )[0,1] } # slice forces two elements
         split /[&;]+/, $query_string
     );
index cc02f87..572f50f 100644 (file)
@@ -1,4 +1,3 @@
-package Catalyst::Runtime;
 
 use strict;
 use warnings;
index 91aeaed..eefb181 100644 (file)
@@ -196,6 +196,27 @@ to send over the wire via HTTP needs to be bytes (not unicode characters).
 Remember if you use any utf8 literals in your source code, you should use the
 C<use utf8> pragma.
 
+B<NOTE:> Assuming UTF-8 in your query parameters and keywords may be an issue if you have
+legacy code where you created URL in templates manually and used an encoding other than UTF-8.
+In these cases you may find versions of Catalyst after 5.90080+ will incorrectly decode.  For
+backwards compatibility we offer three configurations settings, here described in order of
+precedence:
+
+C<do_not_decode_query>
+
+If true, then do not try to character decode any wide characters in your
+request URL query or keywords.  You will need gto handle this manually in your action code
+(although if you choose this setting, chances are you already do this).
+
+C<default_query_encoding>
+
+This setting allows one to specify a fixed value for how to decode your query, instead of using
+the default, UTF-8.
+
+C<decode_query_using_global_encoding>
+
+If this is true we decode using whatever you set C<encoding> to.
+
 =head2 UTF8 in Form POST
 
 In general most modern browsers will follow the specification, which says that POSTed
index ebfa2a3..e210bbb 100644 (file)
@@ -2,6 +2,104 @@
 
 Catalyst::Upgrading - Instructions for upgrading to the latest Catalyst
 
+=head1 Upgrading to Catalyst 5.90085
+
+In this version of Catalyst we made a small change to Chained Dispatching so
+that when two or more actions all have the same path specification AND they
+all have Args(0), we break the tie by choosing the last action defined, and
+not the first one defined.  This was done to normalize Chaining to following
+the 'longest Path wins, and when several actions match the same Path specification
+we choose the last defined.' rule. Previously Args(0) was hard coded to be a special
+case such that the first action defined would match (which is not the case when
+Args is not zero.)
+
+Its possible that this could be a breaking change for you, if you had used
+action roles (custom or otherwise) to add additional matching rules to differentiate
+between several Args(0) actions that share the same root action chain.  For
+example if you have code now like this:
+
+    sub check_default :Chained(/) CaptureArgs(0) { ... }
+
+      sub default_get :Chained('check_default') PathPart('') Args(0) GET {
+          pop->res->body('get3');
+      }
+
+      sub default_post :Chained('check_default') PathPart('') Args(0) POST {
+          pop->res->body('post3');
+      }
+
+      sub chain_default :Chained('check_default') PathPart('') Args(0) {
+          pop->res->body('chain_default');
+      }
+
+The way that chaining will work previous is that when two or more equal actions can
+match, the 'top' one wins.  So if the request is "GET .../check_default" BOTH
+actions 'default_get' AND 'chain_default' would match.  To break the tie in
+the case when Args is 0, we'd previous take the 'top' (or first defined) action.
+Unfortunately this treatment of Args(0) is special case.  In all other cases
+we choose the 'last defined' action to break a tie.  So this version of
+Catalyst changed the dispatcher to make Args(0) no longer a special case for
+breaking ties.  This means that the above code must now become:
+
+    sub check_default :Chained(/) CaptureArgs(0) { ... }
+
+      sub chain_default :Chained('check_default') PathPart('') Args(0) {
+          pop->res->body('chain_default');
+      }
+
+      sub default_get :Chained('check_default') PathPart('') Args(0) GET {
+          pop->res->body('get3');
+      }
+
+      sub default_post :Chained('check_default') PathPart('') Args(0) POST {
+          pop->res->body('post3');
+      }
+
+If we want it to work as expected (for example we we GET to match 'default_get' and
+POST to match 'default_post' and any other http Method to match 'chain_default').
+
+In other words Arg(0) and chained actions must now follow the normal rule where
+in a tie the last defined action wins and you should place all your less defined
+or 'catch all' actions first.
+
+If this causes you trouble and you can't fix your code to conform, you may set the
+application configuration setting "use_chained_args_0_special_case" to true and
+that will revert you code to the previous behavior.
+
+=head2 More backwards compatibility options with UTF-8 changes
+
+In order to give better backwards compatiblity with the 5.90080+ UTF-8 changes
+we've added several configuration options around control of how we try to decode
+your URL keywords / query parameters.
+
+C<do_not_decode_query>
+
+If true, then do not try to character decode any wide characters in your
+request URL query or keywords.  Most readings of the relevent specifications
+suggest these should be UTF-* encoded, which is the default that L<Catalyst>
+will use, hwoever if you are creating a lot of URLs manually or have external
+evil clients, this might cause you trouble.  If you find the changes introduced
+in Catalyst version 5.90080+ break some of your query code, you may disable 
+the UTF-8 decoding globally using this configuration.
+
+This setting takes precedence over C<default_query_encoding> and
+C<decode_query_using_global_encoding>
+
+C<default_query_encoding>
+
+By default we decode query and keywords in your request URL using UTF-8, which
+is our reading of the relevent specifications.  This setting allows one to
+specify a fixed value for how to decode your query.  You might need this if
+you are doing a lot of custom encoding of your URLs and not using UTF-8.
+
+This setting take precedence over C<decode_query_using_global_encoding>.
+
+C<decode_query_using_global_encoding>
+
+Setting this to true will default your query decoding to whatever your
+general global encoding is (the default is UTF-8).
+
+
 =head1 Upgrading to Catalyst 5.90080
 
 UTF8 encoding is now default.  For temporary backwards compatibility, if this
diff --git a/t/args0_bug.t b/t/args0_bug.t
new file mode 100644 (file)
index 0000000..71f2512
--- /dev/null
@@ -0,0 +1,63 @@
+use warnings;
+use strict;
+use Test::More;
+
+{
+  package MyApp::Controller::Root;
+  $INC{'MyApp/Controller/Root.pm'} = __FILE__;
+
+  use Moose;
+  use MooseX::MethodAttributes;
+
+  extends 'Catalyst::Controller';
+
+  sub chain_base :Chained(/) CaptureArgs(1) { }
+
+    sub chained_one_args_0  : Chained(chain_base) PathPart('') Args(1) { $_[1]->res->body('chained_one_args_0') }
+    sub chained_one_args_1  : Chained(chain_base) PathPart('') Args(1) { $_[1]->res->body('chained_one_args_1') }
+
+    sub chained_zero_args_0 : Chained(chain_base) PathPart('') Args(0) { $_[1]->res->body('chained_zero_args_0') }
+    sub chained_zero_args_1 : Chained(chain_base) PathPart('') Args(0) { $_[1]->res->body('chained_zero_args_1') }
+
+  MyApp::Controller::Root->config(namespace=>'');
+
+  package MyApp;
+  use Catalyst;
+
+  #MyApp->config(use_chained_args_0_special_case=>1);
+  MyApp->setup;
+}
+
+=over
+
+[debug] Loaded Chained actions:
+.-----------------------------------------+---------------------------------------------------.
+| Path Spec                               | Private                                           |
++-----------------------------------------+---------------------------------------------------+
+| /chain_base/*/*                         | /chain_base (1)                                   |
+|                                         | => /chained_one_args_0 (1)                        |
+| /chain_base/*/*                         | /chain_base (1)                                   |
+|                                         | => /chained_one_args_1 (1)                        |
+| /chain_base/*                           | /chain_base (1)                                   |
+|                                         | => /chained_zero_args_0 (0)                       |
+| /chain_base/*                           | /chain_base (1)                                   |
+|                                         | => /chained_zero_args_1 (0)                       |
+'-----------------------------------------+---------------------------------------------------'
+
+=cut
+
+use Catalyst::Test 'MyApp';
+{
+   my $res = request '/chain_base/capturearg/arg';
+  is $res->content, 'chained_one_args_1', "request '/chain_base/capturearg/arg'";
+}
+
+{
+    my $res = request '/chain_base/capturearg';
+    is $res->content, 'chained_zero_args_1', "request '/chain_base/capturearg'";
+}
+
+done_testing;
+
+__END__
+
index 2f7476d..5d42858 100644 (file)
@@ -80,6 +80,10 @@ sub delete2 :Chained('post_or_delete') PathPart('') Args(0) DELETE {
 
 sub check_default :Chained('base') CaptureArgs(0) { }
 
+sub chain_default :Chained('check_default') PathPart('') Args(0) {
+    pop->res->body('chain_default');
+}
+
 sub default_get :Chained('check_default') PathPart('') Args(0) GET {
     pop->res->body('get3');
 }
@@ -88,8 +92,6 @@ sub default_post :Chained('check_default') PathPart('') Args(0) POST {
     pop->res->body('post3');
 }
 
-sub chain_default :Chained('check_default') PathPart('') Args(0) {
-    pop->res->body('chain_default');
-}
+
 
 __PACKAGE__->meta->make_immutable;
diff --git a/t/no_test_stash_bug.t b/t/no_test_stash_bug.t
new file mode 100644 (file)
index 0000000..72549df
--- /dev/null
@@ -0,0 +1,28 @@
+use warnings;
+use strict;
+
+# For reported: https://rt.cpan.org/Ticket/Display.html?id=97948
+
+{
+  package MyApp::Controller::Root;
+  $INC{'MyApp/Controller/Root.pm'} = __FILE__;
+
+  use base 'Catalyst::Controller';
+
+  sub example :Local Args(0) {
+    pop->stash->{testing1} = 'testing2';
+  }
+
+  package MyApp;
+  use Catalyst;
+
+  MyApp->setup;
+}
+
+use Test::More;
+use Catalyst::Test 'MyApp';
+
+my ($res, $c) = ctx_request('/root/example');
+is $c->stash->{testing1}, 'testing2', 'got expected stash value';
+
+done_testing;
diff --git a/t/not_utf8_query_bug.t b/t/not_utf8_query_bug.t
new file mode 100644 (file)
index 0000000..52578bc
--- /dev/null
@@ -0,0 +1,45 @@
+use utf8;
+use warnings;
+use strict;
+
+# For reported: https://rt.cpan.org/Ticket/Display.html?id=103063
+
+{
+  package MyApp::Controller::Root;
+  $INC{'MyApp/Controller/Root.pm'} = __FILE__;
+
+  use base 'Catalyst::Controller';
+
+  sub example :Local Args(0) {
+    pop->stash->{testing1} = 'testing2';
+  }
+
+  package MyApp;
+  use Catalyst;
+
+  #MyApp->config(decode_query_using_global_encoding=>1, encoding => 'SHIFT_JIS');
+  #MyApp->config(do_not_decode_query=>1);
+  #MyApp->config(decode_query_using_global_encoding=>1, encoding => undef);
+  MyApp->config(default_query_encoding=>'SHIFT_JIS');
+
+  MyApp->setup;
+}
+
+use Test::More;
+use Catalyst::Test 'MyApp';
+use Encode;
+use HTTP::Request::Common;
+
+{
+  my $shiftjs = 'test テスト';
+  my $encoded = Encode::encode('SHIFT_JIS', $shiftjs);
+
+  ok my $req = GET "/root/example?a=$encoded";
+  my ($res, $c) = ctx_request $req;
+
+  is $c->req->query_parameters->{'a'}, $shiftjs, 'got expected value';
+}
+
+
+done_testing;
+
index c144a44..6342025 100644 (file)
@@ -479,6 +479,16 @@ SKIP: {
 
 }
 
+{
+  my $shiftjs = 'test テスト';
+  my $encoded = Encode::encode('UTF-8', $shiftjs);
+
+  ok my $req = GET "/root/echo_arg?a=$encoded";
+  my ($res, $c) = ctx_request $req;
+
+  is $c->req->query_parameters->{'a'}, $shiftjs, 'got expected value';
+}
+
 ## should we use binmode on filehandles to force the encoding...?
 ## Not sure what else to do with multipart here, if docs are enough...