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.
=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 *
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) = @_;
# 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),
};
}
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';
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}
# (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;
}
$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
);
-package Catalyst::Runtime;
use strict;
use warnings;
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
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
--- /dev/null
+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__
+
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');
}
pop->res->body('post3');
}
-sub chain_default :Chained('check_default') PathPart('') Args(0) {
- pop->res->body('chain_default');
-}
+
__PACKAGE__->meta->make_immutable;
--- /dev/null
+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;
--- /dev/null
+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;
+
}
+{
+ 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...