merged after conflict resolution
John Napiorkowski [Wed, 12 Jun 2013 14:27:20 +0000 (10:27 -0400)]
31 files changed:
Makefile.PL
lib/Catalyst.pm
lib/Catalyst/Engine/HTTP.pm
lib/Catalyst/Plugin/Unicode/Encoding.pm [new file with mode: 0644]
t/aggregate/live_plugin_loaded.t
t/aggregate/unit_core_plugin.t
t/author/podcoverage.t
t/lib/ACLTestApp.pm
t/lib/ChainedActionsApp.pm
t/lib/PluginTestApp.pm
t/lib/PluginTestApp/Controller/Root.pm
t/lib/TestApp.pm
t/lib/TestApp/Controller/Root.pm
t/lib/TestApp2.pm [new file with mode: 0644]
t/lib/TestApp2/Controller/Root.pm [new file with mode: 0644]
t/lib/TestAppDoubleAutoBug.pm
t/lib/TestAppIndexDefault.pm
t/lib/TestAppMatchSingleArg.pm
t/lib/TestAppOneView.pm
t/lib/TestAppUnicode.pm [new file with mode: 0644]
t/lib/TestAppUnicode/Controller/Root.pm [new file with mode: 0644]
t/lib/TestAppWithoutUnicode.pm [new file with mode: 0644]
t/lib/TestAppWithoutUnicode/Controller/Root.pm [new file with mode: 0644]
t/lib/TestLogger.pm [new file with mode: 0644]
t/live_redirect_body.t
t/unicode_plugin_charset_utf8.t [new file with mode: 0644]
t/unicode_plugin_config.t [new file with mode: 0644]
t/unicode_plugin_live.t [new file with mode: 0644]
t/unicode_plugin_nested_params.t [new file with mode: 0644]
t/unicode_plugin_no_encoding.t [new file with mode: 0644]
t/unicode_plugin_request_decode.t [new file with mode: 0644]

index a37f617..b50b74f 100644 (file)
@@ -48,7 +48,6 @@ requires 'HTTP::Headers' => '1.64';
 requires 'HTTP::Request' => '5.814';
 requires 'HTTP::Response' => '5.813';
 requires 'HTTP::Request::AsCGI' => '1.0';
-requires 'LWP::UserAgent';
 requires 'Module::Pluggable' => '3.9';
 requires 'Path::Class' => '0.09';
 requires 'Scalar::Util';
@@ -59,7 +58,6 @@ requires 'Tree::Simple' => '1.15';
 requires 'Tree::Simple::Visitor::FindByPath';
 requires 'Try::Tiny';
 requires 'Safe::Isa';
-requires 'URI' => '1.35';
 requires 'Task::Weaken';
 requires 'Text::Balanced'; # core in 5.8.x but mentioned for completeness
 requires 'MRO::Compat';
@@ -69,16 +67,21 @@ requires 'Devel::InnerPackage'; # No longer core in blead
 requires 'Plack' => '0.9991'; # IIS6+7 fix middleware
 requires 'Plack::Middleware::ReverseProxy' => '0.04';
 requires 'Plack::Test::ExternalServer';
+requires 'Class::Data::Inheritable';
+requires 'Encode' => '2.49';
+requires 'LWP' => '5.837'; # LWP had unicode fail in 5.8.26
+requires 'URI' => '1.36';
 
 # Install the standalone Regex dispatch modules in order to ease the
 # depreciation transition
 requires 'Catalyst::DispatchType::Regex' => '5.90021';
 
-test_requires 'Class::Data::Inheritable';
 test_requires 'Test::Exception';
 test_requires 'Test::More' => '0.88';
 test_requires 'Data::Dump';
 test_requires 'HTTP::Request::Common';
+test_requires 'IO::Scalar';
+test_requires 'HTTP::Status';
 
 # aggregate tests if AGGREGATE_TESTS is set and a recent Test::Aggregate and a Test::Simple it works with is available
 my @author_requires;
@@ -94,6 +97,7 @@ else {
 
 push(@author_requires, 'CatalystX::LeakChecker', '0.05');
 push(@author_requires, 'Catalyst::Devel', '1.0'); # For http server test
+push(@author_requires, 'Test::WWW::Mechanize::Catalyst', '0.51');
 push(@author_requires, 'Test::TCP', '1.27'); # ditto, ships Net::EmptyPort
 
 author_tests('t/author');
@@ -110,6 +114,7 @@ author_requires(
   Test::Pod::Coverage
   Test::Spelling
   Pod::Coverage::TrustPod
+  Catalyst::Plugin::Params::Nested
 ));
 
 if ($Module::Install::AUTHOR) {
index 2e03326..17605d4 100644 (file)
@@ -2982,10 +2982,26 @@ the plugin name does not begin with C<Catalyst::Plugin::>.
         return $class;
     }
 
+    sub _default_plugins { return qw(Unicode::Encoding) }
+
     sub setup_plugins {
         my ( $class, $plugins ) = @_;
 
         $class->_plugins( {} ) unless $class->_plugins;
+        $plugins = [ grep {
+            m/Unicode::Encoding/ ? do {
+                $class->log->warn(
+                    'Unicode::Encoding plugin is auto-applied,'
+                    . ' please remove this from your appclass'
+                    . ' and make sure to define "encoding" config'
+                );
+                unless (exists $class->config->{'encoding'}) {
+                  $class->config->{'encoding'} = 'UTF-8';
+                }
+                () }
+                : $_
+        } @$plugins ];
+        unshift @$plugins, $class->_default_plugins;
         $plugins = Data::OptList::mkopt($plugins || []);
 
         my @plugins = map {
@@ -3183,6 +3199,10 @@ C<< $c->request->base >> will be incorrect.
 
 C<using_frontend_proxy> - See L</PROXY SUPPORT>.
 
+=item *
+
+C<encoding> - See L</ENCODING>
+
 =back
 
 =item abort_chain_on_error_fix => 1
@@ -3285,6 +3305,53 @@ If you plan to operate in a threaded environment, remember that all other
 modules you are using must also be thread-safe. Some modules, most notably
 L<DBD::SQLite>, are not thread-safe.
 
+=head1 ENCODING
+
+On request, decodes all params from encoding into a sequence of
+logical characters. On response, encodes body into encoding.
+
+=head2 Methods
+
+=over 4
+
+=item encoding
+
+Returns an instance of an C<Encode> encoding
+
+    print $c->encoding->name
+
+=item handle_unicode_encoding_exception ($exception_context)
+
+Method called when decoding process for a request fails.
+
+An C<$exception_context> hashref is provided to allow you to override the
+behaviour of your application when given data with incorrect encodings.
+
+The default method throws exceptions in the case of invalid request parameters
+(resulting in a 500 error), but ignores errors in upload filenames.
+
+The keys passed in the C<$exception_context> hash are:
+
+=over
+
+=item param_value
+
+The value which was not able to be decoded.
+
+=item error_msg
+
+The exception received from L<Encode>.
+
+=item encoding_step
+
+What type of data was being decoded. Valid values are (currently)
+C<params> - for request parameters / arguments / captures
+and C<uploads> - for request upload filenames.
+
+=back
+
+=back
+
 =head1 SUPPORT
 
 IRC:
@@ -3458,7 +3525,7 @@ Will Hawes C<info@whawes.co.uk>
 
 willert: Sebastian Willert <willert@cpan.org>
 
-wreis: Wallace Reis <wallace@reis.org.br>
+wreis: Wallace Reis <wreis@cpan.org>
 
 Yuval Kogman, C<nothingmuch@woobling.org>
 
index b708a7f..7b506e6 100644 (file)
@@ -18,6 +18,27 @@ to update your scripts to not do this.\n") unless $ENV{HARNESS_ACTIVE};
 
 1;
 
-# This is here only as some old generated scripts require Catalyst::Engine::HTTP
+__END__
 
+=head1 NAME
 
+Catalyst::Engine::HTTP
+
+=head1 SYNOPSIS
+
+See L<Catalyst>.
+
+=head1 DESCRIPTION
+
+This is here only as some old generated scripts require Catalyst::Engine::HTTP
+
+=head1 AUTHORS
+
+Catalyst Contributors, see Catalyst.pm
+
+=head1 COPYRIGHT
+
+This library is free software. You can redistribute it and/or modify it under
+the same terms as Perl itself.
+
+=cut
diff --git a/lib/Catalyst/Plugin/Unicode/Encoding.pm b/lib/Catalyst/Plugin/Unicode/Encoding.pm
new file mode 100644 (file)
index 0000000..402087e
--- /dev/null
@@ -0,0 +1,209 @@
+package Catalyst::Plugin::Unicode::Encoding;
+
+use strict;
+use base 'Class::Data::Inheritable';
+
+use Carp ();
+use MRO::Compat;
+use Try::Tiny;
+
+use Encode 2.21 ();
+our $CHECK = Encode::FB_CROAK | Encode::LEAVE_SRC;
+
+our $VERSION = '2.1';
+
+__PACKAGE__->mk_classdata('_encoding');
+
+sub encoding {
+    my $c = shift;
+    my $encoding;
+
+    if ( scalar @_ ) {
+        # Let it be set to undef
+        if (my $wanted = shift)  {
+            $encoding = Encode::find_encoding($wanted)
+              or Carp::croak( qq/Unknown encoding '$wanted'/ );
+        }
+
+        $encoding = ref $c
+                  ? $c->{encoding} = $encoding
+                  : $c->_encoding($encoding);
+    } else {
+      $encoding = ref $c && exists $c->{encoding}
+                ? $c->{encoding}
+                : $c->_encoding;
+    }
+
+    return $encoding;
+}
+
+sub finalize_headers {
+    my $c = shift;
+
+    my $body = $c->response->body;
+
+    return $c->next::method(@_)
+      unless defined($body);
+
+    my $enc = $c->encoding;
+
+    return $c->next::method(@_)
+      unless $enc;
+
+    my ($ct, $ct_enc) = $c->response->content_type;
+
+    # Only touch 'text-like' contents
+    return $c->next::method(@_)
+      unless $c->response->content_type =~ /^text|xml$|javascript$/;
+
+    if ($ct_enc && $ct_enc =~ /charset=([^;]*)/) {
+        if (uc($1) ne uc($enc->mime_name)) {
+            $c->log->debug("Unicode::Encoding is set to encode in '" .
+                           $enc->mime_name .
+                           "', content type is '$1', not encoding ");
+            return $c->next::method(@_);
+        }
+    } else {
+        $c->res->content_type($c->res->content_type . "; charset=" . $enc->mime_name);
+    }
+
+    # Encode expects plain scalars (IV, NV or PV) and segfaults on ref's
+    $c->response->body( $c->encoding->encode( $body, $CHECK ) )
+        if ref(\$body) eq 'SCALAR';
+
+    $c->next::method(@_);
+}
+
+# Note we have to hook here as uploads also add to the request parameters
+sub prepare_uploads {
+    my $c = shift;
+
+    $c->next::method(@_);
+
+    my $enc = $c->encoding;
+
+    for my $key (qw/ parameters query_parameters body_parameters /) {
+        for my $value ( values %{ $c->request->{$key} } ) {
+            # N.B. Check if already a character string and if so do not try to double decode.
+            #      http://www.mail-archive.com/catalyst@lists.scsys.co.uk/msg02350.html
+            #      this avoids exception if we have already decoded content, and is _not_ the
+            #      same as not encoding on output which is bad news (as it does the wrong thing
+            #      for latin1 chars for example)..
+            $value = $c->_handle_unicode_decoding($value);
+        }
+    }
+    for my $value ( values %{ $c->request->uploads } ) {
+        # skip if it fails for uploads, as we don't usually want uploads touched
+        # in any way
+        $_->{filename} = try {
+        $enc->decode( $_->{filename}, $CHECK )
+    } catch {
+        $c->handle_unicode_encoding_exception({
+            param_value => $_->{filename},
+            error_msg => $_,
+            encoding_step => 'uploads',
+        });
+    } for ( ref($value) eq 'ARRAY' ? @{$value} : $value );
+    }
+}
+
+sub prepare_action {
+    my $c = shift;
+
+    my $ret = $c->next::method(@_);
+
+    foreach (@{$c->req->arguments}, @{$c->req->captures}) {
+      $_ = $c->_handle_param_unicode_decoding($_);
+    }
+
+    return $ret;
+}
+
+sub setup {
+    my $self = shift;
+
+    my $conf = $self->config;
+
+    # Allow an explict undef encoding to disable default of utf-8
+    my $enc = delete $conf->{encoding};
+    $self->encoding( $enc );
+
+    return $self->next::method(@_);
+}
+
+sub _handle_unicode_decoding {
+    my ( $self, $value ) = @_;
+
+    return unless defined $value;
+
+    if ( ref $value eq 'ARRAY' ) {
+        foreach ( @$value ) {
+            $_ = $self->_handle_unicode_decoding($_);
+        }
+        return $value;
+    }
+    elsif ( ref $value eq 'HASH' ) {
+        foreach ( values %$value ) {
+            $_ = $self->_handle_unicode_decoding($_);
+        }
+        return $value;
+    }
+    else {
+        return $self->_handle_param_unicode_decoding($value);
+    }
+}
+
+sub _handle_param_unicode_decoding {
+    my ( $self, $value ) = @_;
+    my $enc = $self->encoding;
+    return try {
+        Encode::is_utf8( $value ) ?
+            $value
+        : $enc->decode( $value, $CHECK );
+    }
+    catch {
+        $self->handle_unicode_encoding_exception({
+            param_value => $value,
+            error_msg => $_,
+            encoding_step => 'params',
+        });
+    };
+}
+
+sub handle_unicode_encoding_exception {
+    my ( $self, $exception_ctx ) = @_;
+    $self->log->warn($exception_ctx->{error_msg});
+    return $exception_ctx->{'param_value'};
+}
+
+1;
+
+__END__
+
+=head1 NAME
+
+Catalyst::Plugin::Unicode::Encoding - Unicode aware Catalyst
+
+=head1 SYNOPSIS
+
+    use Catalyst;
+
+    MyApp->config( encoding => 'UTF-8' ); # A valid Encode encoding
+
+
+=head1 DESCRIPTION
+
+This plugin is automatically loaded by apps. Even though is not a core component
+yet, it will vanish as soon as the code is fully integrated. For more
+information, please refer to C<ENCODING> section at L<Catalyst>.
+
+=head1 AUTHORS
+
+Catalyst Contributors, see Catalyst.pm
+
+=head1 COPYRIGHT
+
+This library is free software. You can redistribute it and/or modify
+it under the same terms as Perl itself.
+
+=cut
index 6795043..64161aa 100644 (file)
@@ -15,6 +15,7 @@ my @expected = qw[
   Catalyst::Plugin::Test::Inline
   Catalyst::Plugin::Test::MangleDollarUnderScore
   Catalyst::Plugin::Test::Plugin
+  Catalyst::Plugin::Unicode::Encoding
   TestApp::Plugin::AddDispatchTypes
   TestApp::Plugin::FullyQualified
 ];
index 16a5e24..becc3c1 100644 (file)
@@ -53,6 +53,7 @@ my @expected = qw(
   Catalyst::Plugin::Test::Inline
   Catalyst::Plugin::Test::MangleDollarUnderScore
   Catalyst::Plugin::Test::Plugin
+  Catalyst::Plugin::Unicode::Encoding
   TestApp::Plugin::AddDispatchTypes
   TestApp::Plugin::FullyQualified
 );
index bee250c..894f122 100644 (file)
@@ -8,6 +8,7 @@ use Test::Pod::Coverage 1.04;
 my @modules = all_modules;
 our @private = ( 'BUILD' );
 foreach my $module (@modules) {
+    next if $module =~ /Unicode::Encoding/;
     local @private = (@private, 'run', 'dont_close_all_files') if $module =~ /^Catalyst::Script::/;
     local @private = (@private, 'plugin') if $module =~ /^Catalyst$/;
     local @private = (@private, 'snippets') if $module =~ /^Catalyst::Request$/;
index ec87027..2b7a010 100644 (file)
@@ -5,10 +5,13 @@ use strict;
 use warnings;
 use MRO::Compat;
 use Scalar::Util ();
+use TestLogger;
 
 use base qw/Catalyst Catalyst::Controller/;
 use Catalyst qw//;
 
+__PACKAGE__->log(TestLogger->new);
+
 sub execute {
     my $c = shift;
     my ( $class, $action ) = @_;
index 375ce10..3be4faf 100644 (file)
@@ -1,6 +1,7 @@
 package ChainedActionsApp;
 use Moose;
 use namespace::autoclean;
+use TestLogger;
 
 use Catalyst::Runtime 5.80;
 
@@ -16,6 +17,8 @@ __PACKAGE__->config(
   disable_component_regex_fallback => 1,
 );
 
+__PACKAGE__->log(TestLogger->new);
+
 __PACKAGE__->setup;
 
 1;
index 29a02cd..7af690d 100644 (file)
@@ -14,8 +14,9 @@ sub _test_plugins {
     is_deeply [ $c->registered_plugins ],
     [
         qw/Catalyst::Plugin::Test::Plugin
+        Catalyst::Plugin::Unicode::Encoding
         TestApp::Plugin::FullyQualified/
-        ],
+    ],
     '... and it should report the correct plugins';
     ok $c->registered_plugins('Catalyst::Plugin::Test::Plugin'),
     '... or if we have a particular plugin';
index 7bec366..94f4378 100644 (file)
@@ -49,6 +49,7 @@ sub run_time_plugins : Local {
     is_deeply [ $c->registered_plugins ],
     [
         qw/Catalyst::Plugin::Test::Plugin
+        Catalyst::Plugin::Unicode::Encoding
         Faux::Plugin
         TestApp::Plugin::FullyQualified/
         ],
index 89332ba..b06880c 100644 (file)
@@ -50,6 +50,7 @@ TestApp->config(
             action_action_nine => { another_extra_arg => 13 }
         }
     },
+    encoding => 'UTF-8',
     abort_chain_on_error_fix => 1,
 );
 
index f2acb21..b626bdb 100644 (file)
@@ -2,6 +2,7 @@ package TestApp::Controller::Root;
 use strict;
 use warnings;
 use base 'Catalyst::Controller';
+use utf8;
 
 __PACKAGE__->config->{namespace} = '';
 
diff --git a/t/lib/TestApp2.pm b/t/lib/TestApp2.pm
new file mode 100644 (file)
index 0000000..53b483f
--- /dev/null
@@ -0,0 +1,19 @@
+package TestApp2;
+use strict;
+use warnings;
+use base qw/Catalyst/;
+use Catalyst qw/Params::Nested/;
+
+__PACKAGE__->config(
+  'name' => 'TestApp2',
+  encoding => 'UTF-8',
+);
+
+__PACKAGE__->setup;
+
+sub handle_unicode_encoding_exception {
+  my ( $self, $param_value, $error_msg ) = @_;
+  return $param_value;
+}
+
+1;
diff --git a/t/lib/TestApp2/Controller/Root.pm b/t/lib/TestApp2/Controller/Root.pm
new file mode 100644 (file)
index 0000000..0fefe63
--- /dev/null
@@ -0,0 +1,16 @@
+package TestApp2::Controller::Root;
+use strict;
+use warnings;
+use utf8;
+
+__PACKAGE__->config(namespace => q{});
+
+use base 'Catalyst::Controller';
+
+# your actions replace this one
+sub main :Path('') { 
+    $_[1]->res->body('<h1>It works</h1>');
+    $_[1]->res->content_type('text/html');
+}
+
+1;
index 524ed8b..1044a30 100644 (file)
@@ -3,6 +3,7 @@ use warnings;
 
 package TestAppDoubleAutoBug;
 
+use TestLogger;
 use Catalyst qw/
     Test::Errors
     Test::Headers
@@ -13,6 +14,8 @@ our $VERSION = '0.01';
 
 __PACKAGE__->config( name => 'TestAppDoubleAutoBug', root => '/some/dir' );
 
+__PACKAGE__->log(TestLogger->new);
+
 __PACKAGE__->setup;
 
 sub execute {
index 9a129cb..57e3f85 100644 (file)
@@ -1,8 +1,11 @@
 package TestAppIndexDefault;
 use strict;
 use warnings;
+use TestLogger;
 use Catalyst;
 
+__PACKAGE__->log(TestLogger->new);
+
 __PACKAGE__->setup;
 
 1;
index 8f87993..6687eac 100644 (file)
@@ -1,8 +1,11 @@
 package TestAppMatchSingleArg;
 use strict;
 use warnings;
+use TestLogger;
 use Catalyst;
 
+__PACKAGE__->log(TestLogger->new);
+
 __PACKAGE__->setup;
 
 1;
index 59354b3..33fafea 100644 (file)
@@ -1,8 +1,11 @@
 package TestAppOneView;
 use strict;
 use warnings;
+use TestLogger;
 use Catalyst;
 
+__PACKAGE__->log(TestLogger->new);
+
 __PACKAGE__->setup;
 
 1;
diff --git a/t/lib/TestAppUnicode.pm b/t/lib/TestAppUnicode.pm
new file mode 100644 (file)
index 0000000..7d66522
--- /dev/null
@@ -0,0 +1,22 @@
+package TestAppUnicode;
+use strict;
+use warnings;
+use TestLogger;
+use base qw/Catalyst/;
+use Catalyst qw/Unicode::Encoding Params::Nested/;
+
+__PACKAGE__->config(
+  'name' => 'TestAppUnicode',
+  $ENV{TESTAPP_ENCODING} ? ( encoding => $ENV{TESTAPP_ENCODING} ) : (),
+);
+
+__PACKAGE__->log(TestLogger->new);
+
+__PACKAGE__->setup;
+
+sub handle_unicode_encoding_exception {
+  my ( $self, $param_value, $error_msg ) = @_;
+  return $param_value;
+}
+
+1;
diff --git a/t/lib/TestAppUnicode/Controller/Root.pm b/t/lib/TestAppUnicode/Controller/Root.pm
new file mode 100644 (file)
index 0000000..a944b95
--- /dev/null
@@ -0,0 +1,80 @@
+package TestAppUnicode::Controller::Root;
+use strict;
+use warnings;
+use utf8;
+
+__PACKAGE__->config(namespace => q{});
+
+use base 'Catalyst::Controller';
+
+sub main :Path('') { 
+    my ($self, $ctx, $charset) = @_;
+    my $content_type = 'text/html';
+    if ($ctx->stash->{charset}) {
+        $content_type .= ";charset=" . $ctx->stash->{charset};
+    }
+    $ctx->res->body('<h1>It works</h1>');
+    $ctx->res->content_type($content_type);
+}
+
+sub unicode_no_enc :Local {
+    my ($self, $c) = @_;
+    my $data = "ほげ"; # hoge!
+    utf8::encode($data);
+    $c->response->body($data);
+    $c->res->content_type('text/plain');
+    $c->encoding(undef);
+}
+
+sub unicode :Local {
+    my ($self, $c) = @_;
+    my $data = "ほげ"; # hoge!
+    $c->response->body($data); # should be decoded
+    $c->res->content_type('text/plain');
+}
+
+sub not_unicode :Local {
+    my ($self, $c) = @_;
+    my $data = "\x{1234}\x{5678}";
+    utf8::encode($data); # DO NOT WANT unicode
+    $c->response->body($data); # just some octets
+    $c->res->content_type('text/plain');
+    $c->encoding(undef);
+}
+
+sub latin1 :Local {
+  my ($self, $c) = @_;
+
+  $c->res->content_type('text/plain');
+  $c->response->body('LATIN SMALL LETTER E WITH ACUTE: é');
+}
+
+sub file :Local {
+    my ($self, $c) = @_;
+    close *STDERR; # i am evil.
+    $c->response->body($main::TEST_FILE); # filehandle from test file
+}
+
+sub capture : Chained('/') CaptureArgs(1) {}
+
+sub decode_capture : Chained('capture') PathPart('') Args(0) {
+    my ( $self, $c, $cap_arg ) = @_;
+    $c->forward('main');
+}
+
+sub capture_charset : Chained('/') Args(1) {
+    my ( $self, $c, $cap_arg ) = @_;
+    $c->stash(charset => $cap_arg);
+    $c->forward('main');
+}
+
+sub shift_jis :Local {
+    my ($self, $c) = @_;
+    my $data = "ほげ"; # hoge!
+    $c->response->body($data); # should be decoded
+    $c->res->content_type('text/plain; charset=Shift_JIS');
+    $c->encoding("Shift_JIS");
+}
+
+1;
+
diff --git a/t/lib/TestAppWithoutUnicode.pm b/t/lib/TestAppWithoutUnicode.pm
new file mode 100644 (file)
index 0000000..5cb3d81
--- /dev/null
@@ -0,0 +1,14 @@
+package TestAppWithoutUnicode;
+use strict;
+use warnings;
+use TestLogger;
+use base qw/Catalyst/;
+use Catalyst qw/Params::Nested/;
+
+__PACKAGE__->config('name' => 'TestAppWithoutUnicode');
+
+__PACKAGE__->log(TestLogger->new);
+
+__PACKAGE__->setup;
+
+1;
diff --git a/t/lib/TestAppWithoutUnicode/Controller/Root.pm b/t/lib/TestAppWithoutUnicode/Controller/Root.pm
new file mode 100644 (file)
index 0000000..4328fb9
--- /dev/null
@@ -0,0 +1,17 @@
+package TestAppWithoutUnicode::Controller::Root;
+
+use Moose;
+BEGIN { extends 'Catalyst::Controller' }
+use Encode qw(encode_utf8 decode_utf8);
+
+__PACKAGE__->config( namespace => q{} );
+
+sub default : Private {
+    my ( $self, $c ) = @_;
+    my $param = decode_utf8($c->request->parameters->{'myparam'});
+    $c->response->body( encode_utf8($param) );
+}
+
+__PACKAGE__->meta->make_immutable;
+
+1;
diff --git a/t/lib/TestLogger.pm b/t/lib/TestLogger.pm
new file mode 100644 (file)
index 0000000..f1dc7e6
--- /dev/null
@@ -0,0 +1,23 @@
+package TestLogger;
+use strict;
+use warnings;
+
+our @LOGS;
+our @ELOGS;
+
+sub new {
+    return bless {}, __PACKAGE__;
+}
+
+sub debug {
+    shift;
+    push(@LOGS, shift());
+}
+
+sub warn {
+    shift;
+    push(@ELOGS, shift());
+}
+
+1;
+
index 913f0e9..8b9d62c 100644 (file)
@@ -40,7 +40,7 @@ use Test::More;
     is( $response->code, 302, 'Response Code' );
 
     # When the developer sets both the content body and content type, the set content body and content_type should get through.
-    is( $response->header( 'Content-Type' ), 'text/plain', 'Content Type' );
+    like( $response->header( 'Content-Type' ), qr{text/plain}, 'Content Type' );
     like( $response->content, qr/kind sir/, 'Content contains content set by the Controller' );
 }
 
diff --git a/t/unicode_plugin_charset_utf8.t b/t/unicode_plugin_charset_utf8.t
new file mode 100644 (file)
index 0000000..81ba9f7
--- /dev/null
@@ -0,0 +1,32 @@
+use strict;
+use warnings;
+use Test::More;
+use FindBin qw/ $Bin /;
+use lib "$Bin/lib";
+use Data::Dumper;
+
+BEGIN {
+    $ENV{TESTAPP_ENCODING} = 'UTF-8';
+    $ENV{TESTAPP_DEBUG} = 0;
+    $ENV{CATALYST_DEBUG} = 0;
+}
+
+use Catalyst::Test 'TestAppUnicode';
+
+ok request('/capture_charset/utf-8');
+is scalar(@TestLogger::LOGS), 0;
+
+ok request('/capture_charset/latin1');
+is scalar(@TestLogger::LOGS), 1
+    or diag Dumper(\@TestLogger::LOGS);
+
+@TestLogger::LOGS = ();
+
+ok request('/capture_charset/iso-8859-1; header=present');
+is scalar(@TestLogger::LOGS), 1
+    or diag Dumper(\@TestLogger::LOGS);
+like $TestLogger::LOGS[0], qr/content type is 'iso-8859-1'/;
+
+like $TestLogger::ELOGS[0], qr/Unicode::Encoding plugin/;
+
+done_testing;
diff --git a/t/unicode_plugin_config.t b/t/unicode_plugin_config.t
new file mode 100644 (file)
index 0000000..513c978
--- /dev/null
@@ -0,0 +1,31 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use Test::More;
+
+BEGIN { $ENV{TESTAPP_ENCODING} = 'UTF-8' };
+
+# setup library path
+use FindBin qw($Bin);
+use lib "$Bin/lib";
+
+BEGIN {
+if ( !eval { require Test::WWW::Mechanize::Catalyst } || ! Test::WWW::Mechanize::Catalyst->VERSION('0.51') ) {
+    plan skip_all => 'Need Test::WWW::Mechanize::Catalyst for this test';
+}
+}
+
+# make sure testapp works
+use_ok('TestAppUnicode');
+
+use Test::WWW::Mechanize::Catalyst 'TestAppUnicode';
+my $mech = Test::WWW::Mechanize::Catalyst->new;
+
+{
+    TestAppUnicode->encoding('UTF-8');
+    $mech->get_ok('http://localhost/unicode', 'encoding configured ok');
+}
+
+done_testing;
+
diff --git a/t/unicode_plugin_live.t b/t/unicode_plugin_live.t
new file mode 100644 (file)
index 0000000..de810aa
--- /dev/null
@@ -0,0 +1,93 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use Test::More;
+use IO::Scalar;
+
+# setup library path
+use FindBin qw($Bin);
+use lib "$Bin/lib";
+
+BEGIN {
+if ( !eval { require Test::WWW::Mechanize::Catalyst } || ! Test::WWW::Mechanize::Catalyst->VERSION('0.51') ) {
+    plan skip_all => 'Need Test::WWW::Mechanize::Catalyst for this test';
+}
+}
+
+# make sure testapp works
+use_ok('TestAppUnicode') or BAIL_OUT($@);
+
+our $TEST_FILE = IO::Scalar->new(\"this is a test");
+sub IO::Scalar::FILENO { -1 }; # needed?
+
+# a live test against TestAppUnicode, the test application
+use Test::WWW::Mechanize::Catalyst 'TestAppUnicode';
+my $mech = Test::WWW::Mechanize::Catalyst->new;
+$mech->get_ok('http://localhost/', 'get main page');
+$mech->content_like(qr/it works/i, 'see if it has our text');
+is ($mech->response->header('Content-Type'), 'text/html; charset=UTF-8',
+    'Content-Type with charset'
+);
+
+{
+    $mech->get_ok('http://localhost/unicode_no_enc', 'get unicode_no_enc');
+
+    my $exp = "\xE3\x81\xBB\xE3\x81\x92";
+    my $got = Encode::encode_utf8($mech->content);
+
+    is ($mech->response->header('Content-Type'), 'text/plain',
+        'Content-Type with no charset');
+
+    is($got, $exp, 'content contains hoge');
+}
+
+{
+    $mech->get_ok('http://localhost/unicode', 'get unicode');
+
+    is ($mech->response->header('Content-Type'), 'text/plain; charset=UTF-8',
+        'Content-Type with charset');
+
+    my $exp = "\xE3\x81\xBB\xE3\x81\x92";
+    my $got = Encode::encode_utf8($mech->content);
+
+    is($got, $exp, 'content contains hoge');
+}
+
+{
+    $mech->get_ok('http://localhost/not_unicode', 'get bytes');
+    my $exp = "\xE1\x88\xB4\xE5\x99\xB8";
+    my $got = Encode::encode_utf8($mech->content);
+
+    is($got, $exp, 'got 1234 5678');
+}
+
+{
+    $mech->get_ok('http://localhost/file', 'get file');
+    $mech->content_like(qr/this is a test/, 'got filehandle contents');
+}
+
+{
+    # The latin 1 case is the one everyone forgets. I want to really make sure
+    # its right, so lets check the damn bytes.
+    $mech->get_ok('http://localhost/latin1', 'get latin1');
+    is ($mech->response->header('Content-Type'), 'text/plain; charset=UTF-8',
+        'Content-Type with charset');
+
+
+    my $exp = "LATIN SMALL LETTER E WITH ACUTE: \xC3\xA9";
+    my $got = Encode::encode_utf8($mech->content);
+
+    is ($got, $exp, 'content octets are UTF-8');
+}
+
+{
+    $mech->get_ok('http://localhost/shift_jis', 'get shift_jis');
+    is ($mech->response->header('Content-Type'), 'text/plain; charset=Shift_JIS', 'Content-Type with charset');
+    my $exp = "\xE3\x81\xBB\xE3\x81\x92";
+    my $got = Encode::encode_utf8($mech->content);
+    is ($got, $exp, 'content octets are Shift_JIS');
+}
+
+done_testing;
+
diff --git a/t/unicode_plugin_nested_params.t b/t/unicode_plugin_nested_params.t
new file mode 100644 (file)
index 0000000..b31f4ce
--- /dev/null
@@ -0,0 +1,71 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use Test::More;
+use utf8;
+
+# setup library path
+use FindBin qw($Bin);
+use lib "$Bin/lib";
+
+BEGIN { eval { require Catalyst::Plugin::Params::Nested; 1; } ||
+    plan skip_all => 'Need Catalyst::Plugin::Params::Nested' }
+
+use Catalyst::Test 'TestApp2';
+use Encode;
+use HTTP::Request::Common;
+use URI::Escape qw/uri_escape_utf8/;
+use HTTP::Status 'is_server_error';
+
+my $encode_str = "\x{e3}\x{81}\x{82}"; # e38182 is japanese 'あ'
+my $decode_str = Encode::decode('utf-8' => $encode_str);
+my $escape_str = uri_escape_utf8($decode_str);
+
+BEGIN {
+    eval 'require Catalyst::Plugin::Params::Nested';
+    plan skip_all => 'Catalyst::Plugin::Params::Nested is required' if $@;
+}
+
+{
+    my ($res, $c) = ctx_request("/?foo.1=bar&foo.2=$escape_str");
+    is( $c->res->output, '<h1>It works</h1>', 'Content displayed' );
+    
+    my $got = $c->request->parameters;
+    my $expected = {
+        'foo.1' => 'bar',
+        'foo.2' => $decode_str,
+        'foo'   => [undef, 'bar', $decode_str],
+    };
+    
+    is( $got->{foo}->[0], undef, '{foo}->[0] is undef' );
+    is( $got->{foo}->[1], 'bar', '{foo}->[1] is bar' );
+    ok( utf8::is_utf8( $got->{'foo.2'}       ), '{foo.2} is utf8' );
+    ok( utf8::is_utf8( $got->{foo}->[2]      ), '{foo}->[2] is utf8' );
+    is_deeply($got, $expected, 'nested params' );
+}
+
+{
+    my ($res, $c) = ctx_request("/?foo.1=bar&foo.2=$escape_str&bar.baz=$escape_str&baz.bar.foo=$escape_str&&arr.0.1=$escape_str");
+    
+    my $got = $c->request->parameters;
+    my $expected = {
+        'foo.1'       => 'bar',
+        'foo.2'       => $decode_str,
+        'bar.baz'     => $decode_str,
+        'baz.bar.foo' => $decode_str,
+        'arr.0.1'     => $decode_str,
+        'arr'         => [ [undef, $decode_str] ],
+        'foo'         => [undef, 'bar', $decode_str],
+        'bar'         => { baz => $decode_str },
+        'baz'         => { bar => { foo => $decode_str } },
+    };
+    
+    is( ref $got->{arr}->[0], 'ARRAY', '{arr}->[0] is ARRAY' );
+    ok( utf8::is_utf8( $got->{arr}->[0]->[1] ), '{arr}->[0]->[1] is utf8' );
+    ok( utf8::is_utf8( $got->{bar}{baz}      ), '{bar}{baz} is utf8' );
+    ok( utf8::is_utf8( $got->{baz}{bar}{foo} ), '{baz}{bar}{foo} is utf8' );
+    is_deeply($got, $expected, 'nested params' );
+}
+
+done_testing();
diff --git a/t/unicode_plugin_no_encoding.t b/t/unicode_plugin_no_encoding.t
new file mode 100644 (file)
index 0000000..5d0dfe3
--- /dev/null
@@ -0,0 +1,37 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use Test::More;
+use utf8;
+
+# setup library path
+use FindBin qw($Bin);
+use lib "$Bin/lib";
+
+use Catalyst::Test 'TestAppWithoutUnicode';
+use Encode;
+use HTTP::Request::Common;
+use URI::Escape qw/uri_escape_utf8/;
+use HTTP::Status 'is_server_error';
+
+my $encode_str = "\x{e3}\x{81}\x{82}"; # e38182 is japanese 'あ'
+my $decode_str = Encode::decode('utf-8' => $encode_str);
+my $escape_str = uri_escape_utf8($decode_str);
+
+check_parameter(GET "/?myparam=$escape_str");
+
+sub check_parameter {
+    my ( undef, $c ) = ctx_request(shift);
+    is $c->res->output => $encode_str;
+
+    my $myparam = $c->req->param('myparam');
+    ok !utf8::is_utf8($myparam);
+    is $myparam => $encode_str;
+
+    is scalar(@TestLogger::ELOGS), 2
+        or diag Dumper(\@TestLogger::ELOGS);
+    like $TestLogger::ELOGS[0], qr/method \"decode\"/;
+}
+
+done_testing;
diff --git a/t/unicode_plugin_request_decode.t b/t/unicode_plugin_request_decode.t
new file mode 100644 (file)
index 0000000..a3bab97
--- /dev/null
@@ -0,0 +1,82 @@
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use Test::More tests => 5 * 5;
+use utf8;
+
+# setup library path
+use FindBin qw($Bin);
+use lib "$Bin/lib";
+
+use Catalyst::Test 'TestAppUnicode';
+use Encode;
+use HTTP::Request::Common;
+use URI::Escape qw/uri_escape_utf8/;
+use HTTP::Status 'is_server_error';
+
+my $encode_str = "\x{e3}\x{81}\x{82}"; # e38182 is japanese 'あ'
+my $decode_str = Encode::decode('utf-8' => $encode_str);
+my $escape_str = uri_escape_utf8($decode_str);
+
+check_parameter(GET "/?foo=$escape_str");
+check_parameter(POST '/', ['foo' => $encode_str]);
+check_parameter(POST '/',
+    Content_Type => 'form-data',
+    Content => [
+        'foo' => [
+            "$Bin/unicode_plugin_request_decode.t",
+            $encode_str,
+        ]
+    ],
+);
+
+check_argument(GET "/$escape_str");
+check_capture(GET "/capture/$escape_str");
+
+# sending non-utf8 data
+my $non_utf8_data = "%C3%E6%CB%AA";
+check_fallback(GET "/?q=${non_utf8_data}");
+check_fallback(GET "/${non_utf8_data}");
+check_fallback(GET "/capture/${non_utf8_data}");
+check_fallback(POST '/', ['foo' => $non_utf8_data]);
+
+sub check_parameter {
+    my ( undef, $c ) = ctx_request(shift);
+    is $c->res->output => '<h1>It works</h1>';
+
+    my $foo = $c->req->param('foo');
+    ok utf8::is_utf8($foo);
+    is $foo => $decode_str;
+
+    my $other_foo = $c->req->method eq 'POST'
+        ? $c->req->upload('foo')
+            ? $c->req->upload('foo')->filename
+            : $c->req->body_parameters->{foo}
+        : $c->req->query_parameters->{foo};
+    ok utf8::is_utf8($other_foo);
+    is $other_foo => $decode_str;
+}
+
+sub check_argument {
+    my ( undef, $c ) = ctx_request(shift);
+    is $c->res->output => '<h1>It works</h1>';
+
+    my $foo = $c->req->args->[0];
+    ok utf8::is_utf8($foo);
+    is $foo => $decode_str;
+}
+
+sub check_capture {
+    my ( undef, $c ) = ctx_request(shift);
+    is $c->res->output => '<h1>It works</h1>';
+
+    my $foo = $c->req->captures->[0];
+    ok utf8::is_utf8($foo);
+    is $foo => $decode_str;
+}
+
+sub check_fallback {
+  my ( $res, $c ) = ctx_request(shift);
+  ok(!is_server_error($res->code)) or diag('Response code is: ' . $res->code);
+}