diff --git a/Changes b/Changes index f1343cd..afc1509 100644 --- a/Changes +++ b/Changes @@ -1,3 +1,5 @@ +v3.89 2013-03-14 00:00:00 + - Rebased on Mojolicious v3.89 v3.84 2013-02-04 00:00:00 - Rebased on Mojolicious v3.84 v3.70 2012-12-24 00:00:00 diff --git a/MANIFEST.SKIP b/MANIFEST.SKIP index aebfd65..0ca2d44 100644 --- a/MANIFEST.SKIP +++ b/MANIFEST.SKIP @@ -1,5 +1,7 @@ ^\.(?!perltidyrc) .*\.old$ +\.tar\.gz$ ^Makefile$ +^MYMETA\. ^blib ^pm_to_blib diff --git a/examples/connect-proxy.pl b/examples/connect-proxy.pl index 3aac297..cd3ed8f 100644 --- a/examples/connect-proxy.pl +++ b/examples/connect-proxy.pl @@ -4,7 +4,7 @@ use Mojo::IOLoop; -# Minimal connect proxy server to test TLS tunneling +# Minimal CONNECT proxy server to test TLS tunneling my %buffer; Mojo::IOLoop->server( {port => 3000} => sub { @@ -79,7 +79,7 @@ ) or die "Couldn't create listen socket!\n"; print <<'EOF'; -Starting connect proxy on port 3000. +Starting CONNECT proxy on port 3000. For testing use something like "HTTPS_PROXY=http://127.0.0.1:3000". EOF diff --git a/examples/microhttpd.pl b/examples/microhttpd.pl index d176e93..04841a6 100644 --- a/examples/microhttpd.pl +++ b/examples/microhttpd.pl @@ -25,7 +25,7 @@ # Write a minimal HTTP response # (the "Hello World!" message has been optimized away!) - $stream->write("HTTP/1.1 200 OK\x0d\x0a" + $stream->write("HTTP/1.1 200 OK\x0d\x0aContent-Length: 0\x0d\x0a" . "Connection: keep-alive\x0d\x0a\x0d\x0a"); } } @@ -36,7 +36,7 @@ print <<'EOF'; Starting server on port 8080. -Try something like "ab -c 30 -n 100000 -k http://127.0.0.1:8080/" for testing. +Try something like "wrk -c 100 -d 10s http://127.0.0.1:8080/" for testing. On a MacBook Air this results in about 18k req/s. EOF diff --git a/examples/websocket.pl b/examples/websocket.pl index b847ddc..e252b17 100644 --- a/examples/websocket.pl +++ b/examples/websocket.pl @@ -1,16 +1,14 @@ use FindBin; use lib "$FindBin::Bin/../lib"; use Mojolicious::Lite; -use Mojo::JSON 'j'; -websocket '/' => sub { +websocket '/test' => sub { my $self = shift; $self->on( - text => sub { - my ($self, $data) = @_; - my $hash = j($data); + json => sub { + my ($self, $hash) = @_; $hash->{test} = "♥ $hash->{test}"; - $self->send({text => j($hash)}); + $self->send({json => $hash}); } ); }; @@ -25,29 +23,24 @@ - WebSocket - % my $url = url_for->to_abs->scheme('ws'); + WebSocket Test %= javascript begin var ws; if ("WebSocket" in window) { - ws = new WebSocket('<%= $url %>'); + ws = new WebSocket('<%= url_for('test')->to_abs %>'); } if(typeof(ws) !== 'undefined') { - function wsmessage(event) { - alert(JSON.parse(event.data).test); - } - function wsopen(event) { - ws.send(JSON.stringify({test: "WebSocket support works! ♥"})); - } - ws.onmessage = wsmessage; - ws.onopen = wsopen; + ws.onmessage = function (event) { + document.body.innerHTML += JSON.parse(event.data).test; + }; + ws.onopen = function (event) { + ws.send(JSON.stringify({test: 'WebSocket support works! ♥'})); + }; } else { - alert("Sorry, your browser does not support WebSockets."); + document.body.innerHTML += 'Browser does not support WebSockets.'; } % end - - Testing WebSockets, please make sure you have JavaScript enabled. - + Testing WebSockets: diff --git a/lib/Mojo.pm b/lib/Mojo.pm index 45ae93e..c797340 100644 --- a/lib/Mojo.pm +++ b/lib/Mojo.pm @@ -58,6 +58,8 @@ sub _dict { 1; +=encoding utf8 + =head1 NAME Mojo - Duct tape for the HTML5 web! @@ -130,7 +132,7 @@ plugins, since non-blocking requests that are already in progress will interfere with new blocking ones. # Perform blocking request - my $body = $app->ua->get('mojolicio.us')->res->body; + my $body = $app->ua->get('example.com')->res->body; =head1 METHODS @@ -153,17 +155,15 @@ object. =head2 config - my $config = $app->config; - my $foo = $app->config('foo'); - $app = $app->config({foo => 'bar'}); - $app = $app->config(foo => 'bar'); + my $hash = $app->config; + my $foo = $app->config('foo'); + $app = $app->config({foo => 'bar'}); + $app = $app->config(foo => 'bar'); Application configuration. - # Manipulate configuration - $app->config->{foo} = 'bar'; - my $foo = $app->config->{foo}; - delete $app->config->{foo}; + # Remove value + my $foo = delete $app->config->{foo}; =head2 handler diff --git a/lib/Mojo/Asset.pm b/lib/Mojo/Asset.pm index aff4ad0..5fd0468 100644 --- a/lib/Mojo/Asset.pm +++ b/lib/Mojo/Asset.pm @@ -20,6 +20,8 @@ sub slurp { croak 'Method "slurp" not implemented by subclass' } 1; +=encoding utf8 + =head1 NAME Mojo::Asset - HTTP content storage base class @@ -83,9 +85,10 @@ subclass. =head2 get_chunk my $bytes = $asset->get_chunk($offset); + my $bytes = $asset->get_chunk($offset, $max); -Get chunk of data starting from a specific position. Meant to be overloaded -in a subclass. +Get chunk of data starting from a specific position, defaults to a maximum +chunk size of C<131072> bytes. Meant to be overloaded in a subclass. =head2 is_file diff --git a/lib/Mojo/Asset/File.pm b/lib/Mojo/Asset/File.pm index dada732..efc71a8 100644 --- a/lib/Mojo/Asset/File.pm +++ b/lib/Mojo/Asset/File.pm @@ -22,11 +22,11 @@ has handle => sub { } # Open new or temporary file - my $base = catfile File::Spec::Functions::tmpdir, 'mojo.tmp'; + my $base = catfile $self->tmpdir, 'mojo.tmp'; my $name = defined $path ? $path : $base; until ($handle->open($name, O_CREAT | O_EXCL | O_RDWR)) { croak qq{Can't open file "$name": $!} if defined $path || $! != $!{EEXIST}; - $name = "$base." . md5_sum(time . $$ . rand 9999999); + $name = "$base." . md5_sum(time . $$ . rand 9 x 7); } $self->path($name); @@ -57,14 +57,14 @@ sub add_chunk { } sub contains { - my ($self, $string) = @_; + my ($self, $str) = @_; my $handle = $self->handle; $handle->sysseek($self->start_range, SEEK_SET); # Calculate window size my $end = defined $self->end_range ? $self->end_range : $self->size; - my $len = length $string; + my $len = length $str; my $size = $len > 131072 ? $len : 131072; $size = $end - $self->start_range if $size > $end - $self->start_range; @@ -79,7 +79,7 @@ sub contains { $window .= $buffer; # Search window - my $pos = index $window, $string; + my $pos = index $window, $str; return $offset + $pos if $pos >= 0; $offset += $read; return -1 if $read == 0 || $offset == $end; @@ -92,19 +92,20 @@ sub contains { } sub get_chunk { - my ($self, $start) = @_; + my ($self, $offset, $max) = @_; + $max = defined $max ? $max : 131072; - $start += $self->start_range; + $offset += $self->start_range; my $handle = $self->handle; - $handle->sysseek($start, SEEK_SET); + $handle->sysseek($offset, SEEK_SET); my $buffer; if (defined(my $end = $self->end_range)) { - my $chunk = $end + 1 - $start; + my $chunk = $end + 1 - $offset; return '' if $chunk <= 0; - $handle->sysread($buffer, $chunk > 131072 ? 131072 : $chunk); + $handle->sysread($buffer, $chunk > $max ? $max : $chunk); } - else { $handle->sysread($buffer, 131072) } + else { $handle->sysread($buffer, $max) } return $buffer; } @@ -139,6 +140,8 @@ sub slurp { 1; +=encoding utf8 + =head1 NAME Mojo::Asset::File - File storage for HTTP content @@ -183,7 +186,7 @@ Delete file automatically once it's not used anymore. my $handle = $file->handle; $file = $file->handle(IO::File->new); -File handle, created on demand. +Filehandle, created on demand. =head2 path @@ -199,7 +202,7 @@ necessary. $file = $file->tmpdir('/tmp'); Temporary directory used to generate C, defaults to the value of the -C environment variable or auto detection. +MOJO_TMPDIR environment variable or auto detection. =head1 METHODS @@ -220,9 +223,11 @@ Check if asset contains a specific string. =head2 get_chunk - my $bytes = $file->get_chunk($start); + my $bytes = $file->get_chunk($offset); + my $bytes = $file->get_chunk($offset, $max); -Get chunk of data starting from a specific position. +Get chunk of data starting from a specific position, defaults to a maximum +chunk size of C<131072> bytes. =head2 is_file diff --git a/lib/Mojo/Asset/Memory.pm b/lib/Mojo/Asset/Memory.pm index 24b20f4..d517075 100644 --- a/lib/Mojo/Asset/Memory.pm +++ b/lib/Mojo/Asset/Memory.pm @@ -21,26 +21,26 @@ sub add_chunk { } sub contains { - my ($self, $string) = @_; + my ($self, $str) = @_; my $start = $self->start_range; - my $pos = index $self->{content}, $string, $start; + my $pos = index $self->{content}, $str, $start; $pos -= $start if $start && $pos >= 0; my $end = $self->end_range; - return $end && ($pos + length $string) >= $end ? -1 : $pos; + return $end && ($pos + length $str) >= $end ? -1 : $pos; } sub get_chunk { - my ($self, $start) = @_; + my ($self, $offset, $max) = @_; + $max = defined $max ? $max : 131072; - $start += $self->start_range; - my $size = 131072; + $offset += $self->start_range; if (my $end = $self->end_range) { - $size = $end + 1 - $start if ($start + $size) > $end; + $max = $end + 1 - $offset if ($offset + $max) > $end; } - return substr shift->{content}, $start, $size; + return substr shift->{content}, $offset, $max; } sub move_to { @@ -55,6 +55,8 @@ sub slurp { shift->{content} } 1; +=encoding utf8 + =head1 NAME Mojo::Asset::Memory - In-memory storage for HTTP content @@ -110,7 +112,7 @@ automatically upgrade to a L object. Maximum size in bytes of data to keep in memory before automatically upgrading to a L object, defaults to the value of the -C environment variable or C<262144>. +MOJO_MAX_MEMORY_SIZE environment variable or C<262144>. =head1 METHODS @@ -139,8 +141,10 @@ Check if asset contains a specific string. =head2 get_chunk my $bytes = $mem->get_chunk($offset); + my $bytes = $mem->get_chunk($offset, $max); -Get chunk of data starting from a specific position. +Get chunk of data starting from a specific position, defaults to a maximum +chunk size of C<131072> bytes. =head2 move_to diff --git a/lib/Mojo/Base.pm b/lib/Mojo/Base.pm index f4c34d4..250e030 100644 --- a/lib/Mojo/Base.pm +++ b/lib/Mojo/Base.pm @@ -23,10 +23,9 @@ sub import { elsif ($flag eq '-strict') { $flag = undef } # Module - else { - my $file = $flag; - $file =~ s/::|'/\//g; - require "$file.pm" unless $flag->can('new'); + elsif ((my $file = $flag) && !$flag->can('new')) { + $file =~ s!::|'!/!g; + require "$file.pm"; } # ISA @@ -35,7 +34,7 @@ sub import { push @{"${caller}::ISA"}, $flag; *{"${caller}::has"} = sub { attr($caller, @_) }; } - + my $caller = caller; *{"${caller}::say"} = sub { say(@_) }; @@ -43,7 +42,6 @@ sub import { strict->import; warnings->import; utf8->import; - feature->import(':5.10'); } sub new { @@ -104,6 +102,8 @@ sub tap { 1; +=encoding utf8 + =head1 NAME Mojo::Base - Minimal base class for Mojo projects @@ -213,7 +213,7 @@ pass it either a hash or a hash reference with attribute values. Create attribute accessor for hash-based objects, an array reference can be used to create more than one at a time. Pass an optional second argument to set a default value, it should be a constant or a callback. The callback will -be excuted at accessor read time if there's no set value. Accessors can be +be executed at accessor read time if there's no set value. Accessors can be chained, that means they return their invocant when they are called with an argument. @@ -222,15 +222,12 @@ argument. $object = $object->tap(sub {...}); K combinator, tap into a method chain to perform operations on an object -within the chain. - -=head2 C - -Backported from perl-5.10.1 +within the chain. The object will be the first argument passed to the callback +and is also available as C<$_>. =head1 DEBUGGING -You can set the C environment variable to get some advanced +You can set the MOJO_BASE_DEBUG environment variable to get some advanced diagnostics information printed to C. MOJO_BASE_DEBUG=1 diff --git a/lib/Mojo/ByteStream.pm b/lib/Mojo/ByteStream.pm index 4966621..140a776 100644 --- a/lib/Mojo/ByteStream.pm +++ b/lib/Mojo/ByteStream.pm @@ -1,5 +1,5 @@ package Mojo::ByteStream; -use Mojo::Base -base; +use Mojo::Base -strict; use overload '""' => sub { shift->to_string }, fallback => 1; use Exporter 'import'; @@ -10,12 +10,11 @@ our @EXPORT_OK = ('b'); # Turn most functions from Mojo::Util into methods my @UTILS = ( - qw(b64_decode b64_encode camelize decamelize hmac_md5_sum hmac_sha1_sum), - qw(html_unescape md5_bytes md5_sum punycode_decode punycode_encode quote), - qw(sha1_bytes sha1_sum slurp spurt squish trim unquote url_escape), - qw(url_unescape xml_escape xor_encode) + qw(b64_decode b64_encode camelize decamelize hmac_sha1_sum html_unescape), + qw(md5_bytes md5_sum punycode_decode punycode_encode quote sha1_bytes), + qw(sha1_sum slurp spurt squish trim unquote url_escape url_unescape), + qw(xml_escape xor_encode) ); -push @UTILS, 'html_escape'; # DEPRECATED in Rainbow! for my $name (@UTILS) { my $sub = Mojo::Util->can($name); Mojo::Util::monkey_patch __PACKAGE__, $name, sub { @@ -64,10 +63,14 @@ sub split { return Mojo::Collection->new(map { $self->new($_) } split $pattern, $$self); } +sub tap { shift->Mojo::Base::tap(@_) } + sub to_string { ${$_[0]} } 1; +=encoding utf8 + =head1 NAME Mojo::ByteStream - ByteStream @@ -105,8 +108,7 @@ Construct a new scalar-based L object. =head1 METHODS -L inherits all methods from L and implements the -following new ones. +L implements the following methods. =head2 new @@ -165,12 +167,6 @@ Encode bytestream with L, defaults to C. $stream->trim->quote->encode->say; -=head2 hmac_md5_sum - - $stream = $stream->hmac_md5_sum('passw0rd'); - -Generate HMAC-MD5 checksum for bytestream with L. - =head2 hmac_sha1_sum $stream = $stream->hmac_sha1_sum('passw0rd'); @@ -226,7 +222,7 @@ Print bytestream to handle and append a newline, defaults to C. =head2 secure_compare - my $success = $stream->secure_compare($string); + my $success = $stream->secure_compare($str); Compare bytestream with L. @@ -270,9 +266,10 @@ Write all data from bytestream at once to file with L. my $collection = $stream->split(','); -Turn bytestream into L. +Turn bytestream into L object containing L +objects. - b('a,b,c')->split(',')->pluck('quote')->join(',')->say; + b('a,b,c')->split(',')->quote->join(',')->say; =head2 squish @@ -282,10 +279,16 @@ Trim whitespace characters from both ends of bytestream and then change all consecutive groups of whitespace into one space each with L. +=head2 tap + + $stream = $stream->tap(sub {...}); + +Alias for L. + =head2 to_string - my $string = $stream->to_string; - my $string = "$stream"; + my $str = $stream->to_string; + my $str = "$stream"; Stringify bytestream. @@ -334,6 +337,12 @@ bytestream with L. XOR encode bytestream with L. +=head1 BYTESTREAM + +Direct scalar reference access to the bytestream is also possible. + + $$stream .= 'foo'; + =head1 SEE ALSO L, L, L. diff --git a/lib/Mojo/Cache.pm b/lib/Mojo/Cache.pm index d136296..681d4d7 100644 --- a/lib/Mojo/Cache.pm +++ b/lib/Mojo/Cache.pm @@ -19,6 +19,8 @@ sub set { 1; +=encoding utf8 + =head1 NAME Mojo::Cache - Naive in-memory cache diff --git a/lib/Mojo/Collection.pm b/lib/Mojo/Collection.pm index 4e006a1..75f117e 100644 --- a/lib/Mojo/Collection.pm +++ b/lib/Mojo/Collection.pm @@ -1,16 +1,29 @@ package Mojo::Collection; -use Mojo::Base -base; -use overload - 'bool' => sub {1}, - '""' => sub { shift->join("\n") }, - fallback => 1; +use Mojo::Base -strict; +use overload bool => sub {1}, '""' => sub { shift->join("\n") }, fallback => 1; +use Carp 'croak'; use Exporter 'import'; use List::Util; use Mojo::ByteStream; +use Scalar::Util 'blessed'; our @EXPORT_OK = ('c'); +sub AUTOLOAD { + my $self = shift; + + my ($package, $method) = our $AUTOLOAD =~ /^([\w:]+)::(\w+)$/; + croak "Undefined subroutine &${package}::$method called" + unless blessed $self && $self->isa(__PACKAGE__); + + croak qq{Can't locate object method "$method" via package "$package"} + unless @$self; + return $self->pluck($method, @_); +} + +sub DESTROY { } + sub new { my $class = shift; return bless [@_], ref $class || $class; @@ -18,6 +31,10 @@ sub new { sub c { __PACKAGE__->new(@_) } +sub compact { + shift->grep(sub {length}); +} + sub each { my ($self, $cb) = @_; return @$self unless $cb; @@ -28,16 +45,15 @@ sub each { sub first { my ($self, $cb) = @_; - return $cb ? do { (ref $cb) eq 'CODE' ? List::Util::first { $cb->($_) } @$self : List::Util::first { $_ =~ $cb } @$self } : $self->[0]; + return $self->[0] unless $cb; + return List::Util::first { $cb->($_) } @$self if ref $cb eq 'CODE'; + return List::Util::first { $_ =~ $cb } @$self; } sub grep { my ($self, $cb) = @_; - if ((ref $cb) eq 'CODE') { - return $self->new(grep { $cb->($_) } @$self); - } else { - return $self->new(grep { $_ =~ $cb } @$self); - } + return $self->new(grep { $cb->($_) } @$self) if ref $cb eq 'CODE'; + return $self->new(grep { $_ =~ $cb } @$self); } sub join { @@ -77,6 +93,8 @@ sub sort { return $self->new($cb ? sort { $a->$cb($b) } @$self : sort @$self); } +sub tap { shift->Mojo::Base::tap(@_) } + sub uniq { my $self = shift; my %seen; @@ -85,6 +103,8 @@ sub uniq { 1; +=encoding utf8 + =head1 NAME Mojo::Collection - Collection @@ -120,8 +140,7 @@ Construct a new array-based L object. =head1 METHODS -L inherits all methods from L and implements the -following new ones. +L implements the following methods. =head2 new @@ -129,12 +148,20 @@ following new ones. Construct a new array-based L object. +=head2 compact + + my $new = $collection->compact; + +Create a new collection with all elements that are defined and not an empty +string. + =head2 each my @elements = $collection->each; $collection = $collection->each(sub {...}); -Evaluate callback for each element in collection. +Evaluate callback for each element in collection. The element will be the +first argument passed to the callback and is also available as C<$_>. $collection->each(sub { my ($e, $count) = @_; @@ -149,7 +176,8 @@ Evaluate callback for each element in collection. Evaluate regular expression or callback for each element in collection and return the first one that matched the regular expression, or for which the -callback returned true. +callback returned true. The element will be the first argument passed to the +callback and is also available as C<$_>. my $five = $collection->first(sub { $_ == 5 }); @@ -160,7 +188,8 @@ callback returned true. Evaluate regular expression or callback for each element in collection and create a new collection with all elements that matched the regular expression, -or for which the callback returned true. +or for which the callback returned true. The element will be the first +argument passed to the callback and is also available as C<$_>. my $interesting = $collection->grep(qr/mojo/i); @@ -177,7 +206,8 @@ Turn collection into L. my $new = $collection->map(sub {...}); Evaluate callback for each element in collection and create a new collection -from the results. +from the results. The element will be the first argument passed to the +callback and is also available as C<$_>. my $doubled = $collection->map(sub { $_ * 2 }); @@ -226,12 +256,34 @@ from the results. my $insensitive = $collection->sort(sub { uc(shift) cmp uc(shift) }); +=head2 tap + + $collection = $collection->tap(sub {...}); + +Alias for L. + =head2 uniq my $new = $collection->uniq; Create a new collection without duplicate elements. +=head1 ELEMENT METHODS + +In addition to the methods above, you can also call methods provided by all +elements in the collection directly and create a new collection from the +results, similar to C. + + push @$collection, Mojo::ByteStream->new("/home/sri/$_.txt") for 1 .. 9; + say $collection->slurp->b64_encode(''); + +=head1 ELEMENTS + +Direct array reference access to elements is also possible. + + say $collection->[23]; + say for @$collection; + =head1 SEE ALSO L, L, L. diff --git a/lib/Mojo/Content.pm b/lib/Mojo/Content.pm index 08a7f83..18f636b 100644 --- a/lib/Mojo/Content.pm +++ b/lib/Mojo/Content.pm @@ -18,7 +18,7 @@ sub body_size { croak 'Method "body_size" not implemented by subclass' } sub boundary { return undef unless my $type = shift->headers->content_type; - $type =~ m!multipart.*boundary=(?:"([^"]+)"|([\w'(),.:?\-+/]+))!i + $type =~ m!multipart.*boundary\s*=\s*(?:"([^"]+)"|([\w'(),.:?\-+/]+))!i and return defined $1 ? $1 : $2; return undef; } @@ -27,8 +27,8 @@ sub build_body { shift->_build('get_body_chunk') } sub build_headers { shift->_build('get_header_chunk') } sub charset { - my $type = shift->headers->content_type || ''; - return $type =~ /charset="?([^"\s;]+)"?/i ? $1 : undef; + my $type = do {my $tmp = shift->headers->content_type; defined $tmp ? $tmp : ''}; + return $type =~ /charset\s*=\s*"?([^"\s;]+)"?/i ? $1 : undef; } sub clone { @@ -64,15 +64,13 @@ sub get_header_chunk { return substr $self->{header_buffer}, $offset, 131072; } -sub has_leftovers { !!length shift->leftovers } - sub header_size { length shift->build_headers } sub is_chunked { !!shift->headers->transfer_encoding } -sub is_compressed { (shift->headers->content_encoding || '') =~ /^gzip$/i } +sub is_compressed { (do {my $tmp = shift->headers->content_encoding; defined $tmp ? $tmp : ''}) =~ /^gzip$/i } -sub is_dynamic { $_[0]->{dynamic} && !defined $_[0]->headers->content_length } +sub is_dynamic { $_[0]{dynamic} && !defined $_[0]->headers->content_length } sub is_finished { my $tmp = shift->{state}; (defined $tmp ? $tmp : '') eq 'finished' } @@ -117,8 +115,8 @@ sub parse { # Relaxed parsing my $headers = $self->headers; if ($self->auto_relax) { - my $connection = $headers->connection || ''; - my $len = defined $headers->content_length ? $headers->content_length : ''; + my $connection = defined $headers->connection ? $headers->connection : ''; + my $len = defined $headers->content_length ? $headers->content_length : ''; $self->relaxed(1) if !length $len && ($connection =~ /close/i || $headers->content_type); } @@ -155,7 +153,7 @@ sub parse_body { sub progress { my $self = shift; return 0 unless my $state = $self->{state}; - return 0 unless grep { $_ eq $state } qw(body finished); + return 0 unless $state eq 'body' || $state eq 'finished'; return $self->{raw_size} - ($self->{header_size} || 0); } @@ -222,7 +220,7 @@ sub _parse_chunked { # Start new chunk (ignore the chunk extension) unless ($self->{chunk_len}) { last - unless $self->{pre_buffer} =~ s/^(?:\x0d?\x0a)?([[:xdigit:]]+).*\x0a//; + unless $self->{pre_buffer} =~ s/^(?:\x0d?\x0a)?([0-9a-fA-F]+).*\x0a//; next if $self->{chunk_len} = hex $1; # Last chunk @@ -293,13 +291,13 @@ sub _uncompress { # Uncompress $self->{post_buffer} .= $chunk; my $gz = $self->{gz} = defined $self->{gz} ? $self->{gz} : - Compress::Raw::Zlib::Inflate->new(WindowBits => WANT_GZIP()); + Compress::Raw::Zlib::Inflate->new(WindowBits => WANT_GZIP); my $status = $gz->inflate(\$self->{post_buffer}, my $out); $self->emit(read => $out) if defined $out; # Replace Content-Encoding with Content-Length $self->headers->content_length($gz->total_out)->remove('Content-Encoding') - if $status == Z_STREAM_END(); + if $status == Z_STREAM_END; # Check buffer size $self->{limit} = $self->{state} = 'finished' @@ -308,6 +306,8 @@ sub _uncompress { 1; +=encoding utf8 + =head1 NAME Mojo::Content - HTTP content base class @@ -398,7 +398,7 @@ Content headers, defaults to a L object. $content = $content->max_buffer_size(1024); Maximum size in bytes of buffer for content parser, defaults to the value of -the C environment variable or C<262144>. +the MOJO_MAX_BUFFER_SIZE environment variable or C<262144>. =head2 max_leftover_size @@ -406,7 +406,7 @@ the C environment variable or C<262144>. $content = $content->max_leftover_size(1024); Maximum size in bytes of buffer for pipelined HTTP requests, defaults to the -value of the C environment variable or C<262144>. +value of the MOJO_MAX_LEFTOVER_SIZE environment variable or C<262144>. =head2 relaxed @@ -449,13 +449,13 @@ Extract multipart boundary from C header. =head2 build_body - my $string = $content->build_body; + my $str = $content->build_body; Render whole body. =head2 build_headers - my $string = $content->build_headers; + my $str = $content->build_headers; Render all headers. @@ -481,20 +481,14 @@ Generate dynamic content. my $bytes = $content->get_body_chunk(0); -Get a chunk of content starting from a specfic position. Meant to be +Get a chunk of content starting from a specific position. Meant to be overloaded in a subclass. =head2 get_header_chunk my $bytes = $content->get_header_chunk(13); -Get a chunk of the headers starting from a specfic position. - -=head2 has_leftovers - - my $success = $content->has_leftovers; - -Check if there are leftovers. +Get a chunk of the headers starting from a specific position. =head2 header_size diff --git a/lib/Mojo/Content/MultiPart.pm b/lib/Mojo/Content/MultiPart.pm index 6667d5f..b13b43c 100644 --- a/lib/Mojo/Content/MultiPart.pm +++ b/lib/Mojo/Content/MultiPart.pm @@ -45,14 +45,14 @@ sub build_boundary { my $boundary; my $size = 1; while (1) { - $boundary = b64_encode join('', map chr(rand(256)), 1 .. $size++ * 3); + $boundary = b64_encode join('', map chr(rand 256), 1 .. $size++ * 3); $boundary =~ s/\W/X/g; last unless $self->body_contains($boundary); } # Add boundary to Content-Type header my $headers = $self->headers; - ($headers->content_type || '') =~ m!^(.*multipart/[^;]+)(.*)$!; + (defined $headers->content_type ? $headers->content_type : '') =~ m!^(.*multipart/[^;]+)(.*)$!; my $before = $1 || 'multipart/mixed'; my $after = $2 || ''; $headers->content_type("$before; boundary=$boundary$after"); @@ -199,6 +199,8 @@ sub _read { 1; +=encoding utf8 + =head1 NAME Mojo::Content::MultiPart - HTTP multipart content @@ -289,7 +291,7 @@ Clone content if possible, otherwise return C. my $bytes = $multi->get_body_chunk(0); -Get a chunk of content starting from a specfic position. +Get a chunk of content starting from a specific position. =head2 is_multipart diff --git a/lib/Mojo/Content/Single.pm b/lib/Mojo/Content/Single.pm index 995f202..b67ba76 100644 --- a/lib/Mojo/Content/Single.pm +++ b/lib/Mojo/Content/Single.pm @@ -53,6 +53,8 @@ sub parse { 1; +=encoding utf8 + =head1 NAME Mojo::Content::Single - HTTP content @@ -145,7 +147,7 @@ Clone content if possible, otherwise return C. my $bytes = $single->get_body_chunk(0); -Get a chunk of content starting from a specfic position. +Get a chunk of content starting from a specific position. =head2 parse @@ -154,7 +156,7 @@ Get a chunk of content starting from a specfic position. = $single->parse("Content-Type: multipart/form-data\x0d\x0a\x0d\x0a"); Parse content chunk and upgrade to L object if -possible. +necessary. =head1 SEE ALSO diff --git a/lib/Mojo/Cookie.pm b/lib/Mojo/Cookie.pm index ff8bd8b..e5ea183 100644 --- a/lib/Mojo/Cookie.pm +++ b/lib/Mojo/Cookie.pm @@ -1,51 +1,18 @@ package Mojo::Cookie; use Mojo::Base -base; -use overload - 'bool' => sub {1}, - '""' => sub { shift->to_string }, - fallback => 1; +use overload bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1; use Carp 'croak'; -use Mojo::Util 'unquote'; has [qw(name value)]; sub parse { croak 'Method "parse" not implemented by subclass' } sub to_string { croak 'Method "to_string" not implemented by subclass' } -sub _tokenize { - my ($self, $string) = @_; - - # Nibbling parser - my (@tree, @token); - while ($string) { - - # Name - last unless $string =~ s/^\s*([^=;,]+)\s*=?\s*//; - my $name = $1; - - # "expires" is a special case, thank you Netscape... - $string =~ s/^([^;,]+,?[^;,]+)/"$1"/ if $name =~ /^expires$/i; - - # Value - my $value; - $value = unquote $1 if $string =~ s/^("(?:\\\\|\\"|[^"])+"|[^;,]+)\s*//; - push @token, [$name, $value]; - - # Separator - $string =~ s/^\s*;\s*//; - if ($string =~ s/^\s*,\s*//) { - push @tree, [@token]; - @token = (); - } - } - - # Take care of final token - return @token ? (@tree, \@token) : @tree; -} - 1; +=encoding utf8 + =head1 NAME Mojo::Cookie - HTTP cookie base class @@ -60,7 +27,8 @@ Mojo::Cookie - HTTP cookie base class =head1 DESCRIPTION -L is an abstract base class for HTTP cookies. +L is an abstract base class for HTTP cookies as described in RFC +6265. =head1 ATTRIBUTES @@ -87,14 +55,14 @@ following new ones. =head2 parse - my $cookies = $cookie->parse($string); + my $cookies = $cookie->parse($str); Parse cookies. Meant to be overloaded in a subclass. =head2 to_string - my $string = $cookie->to_string; - my $string = "$cookie"; + my $str = $cookie->to_string; + my $str = "$cookie"; Render cookie. Meant to be overloaded in a subclass. diff --git a/lib/Mojo/Cookie/Request.pm b/lib/Mojo/Cookie/Request.pm index 0764d1e..8fbac8b 100644 --- a/lib/Mojo/Cookie/Request.pm +++ b/lib/Mojo/Cookie/Request.pm @@ -1,17 +1,17 @@ package Mojo::Cookie::Request; use Mojo::Base 'Mojo::Cookie'; -use Mojo::Util 'quote'; +use Mojo::Util qw(quote split_header); sub parse { - my ($self, $string) = @_; + my ($self, $str) = @_; my @cookies; - for my $token (map {@$_} $self->_tokenize($string)) { - my ($name, $value) = @$token; + my @pairs = map {@$_} @{split_header(defined $str ? $str : '')}; + while (@pairs) { + my ($name, $value) = (shift @pairs, shift @pairs); next if $name =~ /^\$/; - push @cookies, - Mojo::Cookie::Request->new(name => $name, value => defined $value ? $value : ''); + push @cookies, $self->new(name => $name, value => defined $value ? $value : ''); } return \@cookies; @@ -21,12 +21,14 @@ sub to_string { my $self = shift; return '' unless my $name = $self->name; my $value = defined $self->value ? $self->value : ''; - $value = $value =~ /[,;"]/ ? quote($value) : $value; + $value = $value =~ /[,;" ]/ ? quote($value) : $value; return "$name=$value"; } 1; +=encoding utf8 + =head1 NAME Mojo::Cookie::Request - HTTP request cookie @@ -42,7 +44,8 @@ Mojo::Cookie::Request - HTTP request cookie =head1 DESCRIPTION -L is a container for HTTP request cookies. +L is a container for HTTP request cookies as described +in RFC 6265. =head1 ATTRIBUTES @@ -55,13 +58,13 @@ implements the following new ones. =head2 parse - my $cookies = $cookie->parse('f=b; g=a'); + my $cookies = Mojo::Cookie::Request->parse('f=b; g=a'); Parse cookies. =head2 to_string - my $string = $cookie->to_string; + my $str = $cookie->to_string; Render cookie. diff --git a/lib/Mojo/Cookie/Response.pm b/lib/Mojo/Cookie/Response.pm index f24354a..f80f951 100644 --- a/lib/Mojo/Cookie/Response.pm +++ b/lib/Mojo/Cookie/Response.pm @@ -2,7 +2,7 @@ package Mojo::Cookie::Response; use Mojo::Base 'Mojo::Cookie'; use Mojo::Date; -use Mojo::Util 'quote'; +use Mojo::Util qw(quote split_header); has [qw(domain httponly max_age path secure)]; @@ -10,10 +10,8 @@ sub expires { my $self = shift; # Upgrade - return $self->{expires} - = defined $self->{expires} && !ref $self->{expires} - ? Mojo::Date->new($self->{expires}) - : $self->{expires} + my $e = $self->{expires}; + return $self->{expires} = defined $e && !ref $e ? Mojo::Date->new($e) : $e unless @_; $self->{expires} = shift; @@ -21,26 +19,33 @@ sub expires { } sub parse { - my ($self, $string) = @_; + my ($self, $str) = @_; my @cookies; - for my $token ($self->_tokenize($string)) { - for my $i (0 .. $#$token) { - my ($name, $value) = @{$token->[$i]}; + my $tree = split_header(defined $str ? $str : ''); + while (my $pairs = shift @$tree) { + my $i = 0; + while (@$pairs) { + my ($name, $value) = (shift @$pairs, shift @$pairs); + + # "expires" is a special case, thank you Netscape... + if ($name =~ /^expires$/i) { + my $tmp = shift @$tree; + push @$pairs, @{ defined $tmp ? $tmp : [] }; + my $len = (defined $pairs->[0] ? $pairs->[0] : '') =~ /-/ ? 6 : 10; + $value .= join ' ', ',', grep {defined} splice @$pairs, 0, $len; + } # This will only run once - push(@cookies, - Mojo::Cookie::Response->new(name => $name, value => defined $value ? $value : '')) - and next - unless $i; + push @cookies, $self->new(name => $name, value => defined $value ? $value : '') and next + unless $i++; # Attributes (Netscape and RFC 6265) - next - unless my @match - = $name =~ /^(expires|domain|path|secure|Max-Age|HttpOnly)$/msi; - my $attr = lc $match[0]; - $attr =~ tr/-/_/; - $cookies[-1]->$attr($attr =~ /(?:secure|HttpOnly)/i ? 1 : $value); + next unless $name =~ /^(expires|domain|path|secure|max-age|httponly)$/i; + my $attr = lc $1; + $attr = 'max_age' if $attr eq 'max-age'; + $cookies[-1] + ->$attr($attr eq 'secure' || $attr eq 'httponly' ? 1 : $value); } } @@ -53,7 +58,7 @@ sub to_string { # Name and value (Netscape) return '' unless my $name = $self->name; my $value = defined $self->value ? $self->value : ''; - $value = $value =~ /[,;"]/ ? quote($value) : $value; + $value = $value =~ /[,;" ]/ ? quote($value) : $value; my $cookie = "$name=$value"; # "expires" (Netscape) @@ -79,6 +84,8 @@ sub to_string { 1; +=encoding utf8 + =head1 NAME Mojo::Cookie::Response - HTTP response cookie @@ -94,12 +101,13 @@ Mojo::Cookie::Response - HTTP response cookie =head1 DESCRIPTION -L is a container for HTTP response cookies. +L is a container for HTTP response cookies as +described in RFC 6265. =head1 ATTRIBUTES L inherits all attributes from L and -implements the followign new ones. +implements the following new ones. =head2 domain @@ -153,13 +161,13 @@ Expiration for cookie. =head2 parse - my $cookies = $cookie->parse('f=b; path=/'); + my $cookies = Mojo::Cookie::Response->parse('f=b; path=/'); Parse cookies. =head2 to_string - my $string = $cookie->to_string; + my $str = $cookie->to_string; Render cookie. diff --git a/lib/Mojo/DOM.pm b/lib/Mojo/DOM.pm index 333acf4..405142f 100644 --- a/lib/Mojo/DOM.pm +++ b/lib/Mojo/DOM.pm @@ -1,8 +1,8 @@ package Mojo::DOM; -use Mojo::Base -base; +use Mojo::Base -strict; use overload - '%{}' => sub { shift->attrs }, - 'bool' => sub {1}, + '%{}' => sub { shift->attr }, + bool => sub {1}, '""' => sub { shift->to_xml }, fallback => 1; @@ -12,18 +12,17 @@ use Carp 'croak'; use Mojo::Collection; use Mojo::DOM::CSS; use Mojo::DOM::HTML; -use Mojo::Util 'squish'; +use Mojo::Util qw(deprecated squish); use Scalar::Util qw(blessed weaken); sub AUTOLOAD { my $self = shift; - # Method my ($package, $method) = our $AUTOLOAD =~ /^([\w:]+)::(\w+)$/; croak "Undefined subroutine &${package}::$method called" unless blessed $self && $self->isa(__PACKAGE__); - # Search children + # Search children of current element my $children = $self->children($method); return @$children > 1 ? $children : $children->[0] if @$children; croak qq{Can't locate object method "$method" via package "$package"}; @@ -37,24 +36,22 @@ sub new { return @_ ? $self->parse(@_) : $self; } -sub all_text { - my ($self, $trim) = @_; - my $tree = $self->tree; - return _text(_elements($tree), 1, _trim($tree, $trim)); -} +sub all_text { shift->_content(1, @_) } + +sub ancestors { $_[0]->_collection(_ancestors($_[0]->tree)) } sub append { shift->_add(1, @_) } sub append_content { my ($self, $new) = @_; my $tree = $self->tree; - push @$tree, @{_parent($self->_parse("$new"), $tree)}; + push @$tree, _link($self->_parse("$new"), $tree); return $self; } sub at { shift->find(@_)->[0] } -sub attrs { +sub attr { my $self = shift; # Hash @@ -71,21 +68,22 @@ sub attrs { return $self; } -sub charset { shift->_html(charset => @_) } +# DEPRECATED in Top Hat! +sub attrs { + deprecated 'Mojo::DOM::attrs is DEPRECATED in favor of Mojo::DOM::attr'; + shift->attr(@_); +} sub children { my ($self, $type) = @_; my @children; - my $charset = $self->charset; - my $xml = $self->xml; - my $tree = $self->tree; - for my $e (@$tree[($tree->[0] eq 'root' ? 1 : 4) .. $#$tree]) { + my $xml = $self->xml; + for my $n (@{_nodes($self->tree)}) { # Make sure child is the right type - next unless $e->[0] eq 'tag'; - next if defined $type && $e->[1] ne $type; - push @children, $self->new->charset($charset)->tree($e)->xml($xml); + next if $n->[0] ne 'tag' || (defined $type && $n->[1] ne $type); + push @children, $self->new->tree($n)->xml($xml); } return Mojo::Collection->new(@children); @@ -93,34 +91,24 @@ sub children { sub content_xml { my $self = shift; - - # Render children - my $tree = $self->tree; - my $charset = $self->charset; - my $xml = $self->xml; - return join '', map { - Mojo::DOM::HTML->new(charset => $charset, tree => $_, xml => $xml)->render - } @$tree[($tree->[0] eq 'root' ? 1 : 4) .. $#$tree]; + my $xml = $self->xml; + return join '', map { _render($_, $xml) } @{_nodes($self->tree)}; } sub find { - my ($self, $selector) = @_; - - my $charset = $self->charset; - my $xml = $self->xml; - return Mojo::Collection->new( - map { $self->new->charset($charset)->tree($_)->xml($xml) } - @{Mojo::DOM::CSS->new(tree => $self->tree)->select($selector)}); + my $self = shift; + my $results = Mojo::DOM::CSS->new(tree => $self->tree)->select(@_); + return $self->_collection(@$results); } sub namespace { my $self = shift; - # Extract namespace prefix and search parents return '' if (my $current = $self->tree)->[0] eq 'root'; + + # Extract namespace prefix and search parents my $ns = $current->[1] =~ /^(.*?):/ ? "xmlns:$1" : undef; - while ($current) { - last if $current->[0] eq 'root'; + while ($current->[0] ne 'root') { # Namespace for prefix my $attrs = $current->[2]; @@ -129,7 +117,6 @@ sub namespace { # Namespace attribute elsif (defined $attrs->{xmlns}) { return $attrs->{xmlns} } - # Parent $current = $current->[3]; } @@ -141,23 +128,17 @@ sub next { shift->_sibling(1) } sub parent { my $self = shift; return undef if (my $tree = $self->tree)->[0] eq 'root'; - return $self->new->charset($self->charset)->tree($tree->[3]) - ->xml($self->xml); + return $self->new->tree($tree->[3])->xml($self->xml); } -sub parse { - my $self = shift; - $self->[0]->parse(@_); - return $self; -} +sub parse { shift->_html(parse => shift) } sub prepend { shift->_add(0, @_) } sub prepend_content { my ($self, $new) = @_; my $tree = $self->tree; - splice @$tree, $tree->[0] eq 'root' ? 1 : 4, 0, - @{_parent($self->_parse("$new"), $tree)}; + splice @$tree, _offset($tree), 0, _link($self->_parse("$new"), $tree); return $self; } @@ -167,79 +148,64 @@ sub remove { shift->replace('') } sub replace { my ($self, $new) = @_; - - # Parse my $tree = $self->tree; - if ($tree->[0] eq 'root') { return $self->xml(undef)->parse($new) } - else { $new = $self->_parse("$new") } - - # Find and replace - my $parent = $tree->[3]; - my $i = $parent->[0] eq 'root' ? 1 : 4; - for my $e (@$parent[$i .. $#$parent]) { - last if $e == $tree; - $i++; - } - splice @$parent, $i, 1, @{_parent($new, $parent)}; - - return $self; + return $self->xml(undef)->parse($new) if $tree->[0] eq 'root'; + return $self->_replace($tree, $self->_parse("$new")); } sub replace_content { my ($self, $new) = @_; my $tree = $self->tree; - splice @$tree, $tree->[0] eq 'root' ? 1 : 4, $#$tree, - @{_parent($self->_parse("$new"), $tree)}; + splice @$tree, _offset($tree), $#$tree, _link($self->_parse("$new"), $tree); return $self; } sub root { my $self = shift; - - my $root = $self->tree; - while ($root->[0] eq 'tag') { - last unless my $parent = $root->[3]; - $root = $parent; - } - - return $self->new->charset($self->charset)->tree($root)->xml($self->xml); + return $self unless my $tree = _ancestors($self->tree, 1); + return $self->new->tree($tree)->xml($self->xml); } -sub text { - my ($self, $trim) = @_; +sub strip { + my $self = shift; my $tree = $self->tree; - return _text(_elements($tree), 0, _trim($tree, $trim)); + return $self if $tree->[0] eq 'root'; + return $self->_replace($tree, ['root', @{_nodes($tree)}]); } +sub tap { shift->Mojo::Base::tap(@_) } + +sub text { shift->_content(0, @_) } + sub text_after { my ($self, $trim) = @_; - # Find following text elements return '' if (my $tree = $self->tree)->[0] eq 'root'; - my (@elements, $started); - for my $e (@{_elements($tree->[3])}) { - ++$started and next if $e eq $tree; + + my (@nodes, $started); + for my $n (@{_nodes($tree->[3])}) { + ++$started and next if $n eq $tree; next unless $started; - last if $e->[0] eq 'tag'; - push @elements, $e; + last if $n->[0] eq 'tag'; + push @nodes, $n; } - return _text(\@elements, 0, _trim($tree->[3], $trim)); + return _text(\@nodes, 0, _trim($tree->[3], $trim)); } sub text_before { my ($self, $trim) = @_; - # Find preceding text elements return '' if (my $tree = $self->tree)->[0] eq 'root'; - my @elements; - for my $e (@{_elements($tree->[3])}) { - last if $e eq $tree; - push @elements, $e; - @elements = () if $e->[0] eq 'tag'; + + my @nodes; + for my $n (@{_nodes($tree->[3])}) { + last if $n eq $tree; + push @nodes, $n; + @nodes = () if $n->[0] eq 'tag'; } - return _text(\@elements, 0, _trim($tree->[3], $trim)); + return _text(\@nodes, 0, _trim($tree->[3], $trim)); } sub to_xml { shift->[0]->render } @@ -248,14 +214,9 @@ sub tree { shift->_html(tree => @_) } sub type { my ($self, $type) = @_; - - # Get return '' if (my $tree = $self->tree)->[0] eq 'root'; return $tree->[1] unless $type; - - # Set $tree->[1] = $type; - return $self; } @@ -264,26 +225,32 @@ sub xml { shift->_html(xml => @_) } sub _add { my ($self, $offset, $new) = @_; - # Not a tag return $self if (my $tree = $self->tree)->[0] eq 'root'; - # Find parent my $parent = $tree->[3]; - my $i = $parent->[0] eq 'root' ? 1 : 4; - for my $e (@$parent[$i .. $#$parent]) { - last if $e == $tree; - $i++; - } - - # Add children - splice @$parent, $i + $offset, 0, @{_parent($self->_parse("$new"), $parent)}; + splice @$parent, _parent($parent, $tree) + $offset, 0, + _link($self->_parse("$new"), $parent); return $self; } -sub _elements { - return [] unless my $e = shift; - return [@$e[($e->[0] eq 'root' ? 1 : 4) .. $#$e]]; +sub _ancestors { + my ($tree, $root) = @_; + my @ancestors; + push @ancestors, $tree while ($tree->[0] eq 'tag') && ($tree = $tree->[3]); + return $root ? $ancestors[-1] : @ancestors[0 .. $#ancestors - 1]; +} + +sub _collection { + my $self = shift; + my $xml = $self->xml; + return Mojo::Collection->new(@_) + ->map(sub { $self->new->tree($_)->xml($xml) }); +} + +sub _content { + my $tree = shift->tree; + return _text(_nodes($tree), shift, _trim($tree, @_)); } sub _html { @@ -293,26 +260,50 @@ sub _html { return $self; } -sub _parent { +sub _link { my ($children, $parent) = @_; # Link parent to children my @new; - for my $e (@$children[1 .. $#$children]) { - if ($e->[0] eq 'tag') { - $e->[3] = $parent; - weaken $e->[3]; - } - push @new, $e; + for my $n (@$children[1 .. $#$children]) { + push @new, $n; + next unless $n->[0] eq 'tag'; + $n->[3] = $parent; + weaken $n->[3]; } - return \@new; + return @new; } -sub _parse { - my $self = shift; - Mojo::DOM::HTML->new(charset => $self->charset, xml => $self->xml) - ->parse(shift)->tree; +sub _nodes { + return [] unless my $n = shift; + return [@$n[_offset($n) .. $#$n]]; +} + +sub _offset { $_[0][0] eq 'root' ? 1 : 4 } + +sub _parent { + my ($parent, $child) = @_; + + # Find parent offset for child + my $i = _offset($parent); + for my $n (@$parent[$i .. $#$parent]) { + last if $n == $child; + $i++; + } + + return $i; +} + +sub _parse { Mojo::DOM::HTML->new(xml => shift->xml)->parse(shift)->tree } + +sub _render { Mojo::DOM::HTML->new(tree => shift, xml => shift)->render } + +sub _replace { + my ($self, $tree, $new) = @_; + my $parent = $tree->[3]; + splice @$parent, _parent($parent, $tree), 1, _link($new, $parent); + return $self->parent; } sub _sibling { @@ -334,23 +325,23 @@ sub _sibling { } sub _text { - my ($elements, $recurse, $trim) = @_; + my ($nodes, $recurse, $trim) = @_; my $text = ''; - for my $e (@$elements) { - my $type = $e->[0]; + for my $n (@$nodes) { + my $type = $n->[0]; # Nested tag my $content = ''; if ($type eq 'tag' && $recurse) { - $content = _text(_elements($e), 1, _trim($e, $trim)); + $content = _text(_nodes($n), 1, _trim($n, $trim)); } # Text - elsif ($type eq 'text') { $content = $trim ? squish($e->[1]) : $e->[1] } + elsif ($type eq 'text') { $content = $trim ? squish($n->[1]) : $n->[1] } # CDATA or raw text - elsif ($type eq 'cdata' || $type eq 'raw') { $content = $e->[1] } + elsif ($type eq 'cdata' || $type eq 'raw') { $content = $n->[1] } # Add leading whitespace if punctuation allows it $content = " $content" if $text =~ /\S\z/ && $content =~ /^[^.!?,;:\s]+/; @@ -379,6 +370,8 @@ sub _trim { 1; +=encoding utf8 + =head1 NAME Mojo::DOM - Minimalistic HTML/XML DOM parser with CSS selectors @@ -392,7 +385,8 @@ Mojo::DOM - Minimalistic HTML/XML DOM parser with CSS selectors # Find say $dom->at('#b')->text; - say $dom->find('p')->pluck('text'); + say $dom->find('p')->text; + say $dom->find('[id]')->attr('id'); # Walk say $dom->div->p->[0]->text; @@ -408,9 +402,10 @@ Mojo::DOM - Minimalistic HTML/XML DOM parser with CSS selectors # Modify $dom->div->p->[1]->append('

C

'); + $dom->find(':not(p)')->strip; # Render - say $dom; + say "$dom"; =head1 DESCRIPTION @@ -421,7 +416,7 @@ use it for validation. =head1 CASE SENSITIVITY L defaults to HTML semantics, that means all tags and attributes -are lowercased and selectors need to be lower case as well. +are lowercased and selectors need to be lowercase as well. my $dom = Mojo::DOM->new('

Hi!

'); say $dom->at('p')->text; @@ -444,15 +439,15 @@ XML detection can also be disabled with the C method. =head1 METHODS -L inherits all methods from L and implements the -following new ones. +L implements the following methods. =head2 new my $dom = Mojo::DOM->new; my $dom = Mojo::DOM->new('test'); -Construct a new array-based L object. +Construct a new array-based L object and C HTML/XML document +if necessary. =head2 all_text @@ -468,11 +463,21 @@ enabled by default. # "foo\nbarbaz\n" $dom->parse("
foo\n

bar

baz\n
")->div->all_text(0); +=head2 ancestors + + my $collection = $dom->ancestors; + +Return a L object containing the ancestors of this element +as L objects, similar to C. + + # List types of ancestor elements + say $dom->ancestors->type; + =head2 append $dom = $dom->append('

Hi!

'); -Append to element. +Append HTML/XML to element. # "

A

B

" $dom->parse('

A

')->at('h1')->append('

B

')->root; @@ -481,7 +486,7 @@ Append to element. $dom = $dom->append_content('

Hi!

'); -Append to element content. +Append HTML/XML to element content. # "

AB

" $dom->parse('

A

')->at('h1')->append_content('B')->root; @@ -490,35 +495,32 @@ Append to element content. my $result = $dom->at('html title'); -Find a single element with CSS selectors. All selectors from L -are supported. +Find first element matching the CSS selector and return it as a L +object or return C if none could be found. All selectors from +L are supported. # Find first element with "svg" namespace definition my $namespace = $dom->at('[xmlns\:svg]')->{'xmlns:svg'}; -=head2 attrs +=head2 attr - my $attrs = $dom->attrs; - my $foo = $dom->attrs('foo'); - $dom = $dom->attrs({foo => 'bar'}); - $dom = $dom->attrs(foo => 'bar'); + my $attrs = $dom->attr; + my $foo = $dom->attr('foo'); + $dom = $dom->attr({foo => 'bar'}); + $dom = $dom->attr(foo => 'bar'); Element attributes. -=head2 charset - - my $charset = $dom->charset; - $dom = $dom->charset('UTF-8'); - -Charset used for decoding and encoding HTML/XML. + # List id attributes + say $dom->find('*')->attr('id')->compact; =head2 children my $collection = $dom->children; my $collection = $dom->children('div'); -Return a L object containing the children of this element, -similar to C. +Return a L object containing the children of this element as +L objects, similar to C. # Show type of random child element say $dom->children->shuffle->first->type; @@ -527,8 +529,7 @@ similar to C. my $xml = $dom->content_xml; -Render content of this element to XML. Note that the XML will be encoded if a -C has been defined. +Render content of this element to XML. # "test" $dom->parse('
test
')->div->content_xml; @@ -537,14 +538,16 @@ C has been defined. my $collection = $dom->find('html title'); -Find elements with CSS selectors and return a L object. All -selectors from L are supported. +Find all elements matching the CSS selector and return a L +object containing these elements as L objects. All selectors from +L are supported. # Find a specific element and extract information my $id = $dom->find('div')->[23]{id}; # Extract information from multiple elements - my @headers = $dom->find('h1, h2, h3')->pluck('text')->each; + my @headers = $dom->find('h1, h2, h3')->text->each; + my @links = $dom->find('a[href]')->attr('href')->each; =head2 namespace @@ -562,7 +565,8 @@ Find element namespace. my $sibling = $dom->next; -Next sibling of element. +Return L object for next sibling of element or C if there +are no more siblings. # "

B

" $dom->parse('

A

B

')->at('h1')->next; @@ -571,7 +575,8 @@ Next sibling of element. my $parent = $dom->parent; -Parent of element. +Return L object for parent of element or C if this element +has no parent. =head2 parse @@ -579,14 +584,14 @@ Parent of element. Parse HTML/XML document with L. - # Parse UTF-8 encoded XML - my $dom = Mojo::DOM->new->charset('UTF-8')->xml(1)->parse($xml); + # Parse XML + my $dom = Mojo::DOM->new->xml(1)->parse($xml); =head2 prepend $dom = $dom->prepend('

Hi!

'); -Prepend to element. +Prepend HTML/XML to element. # "

A

B

" $dom->parse('

B

')->at('h2')->prepend('

A

')->root; @@ -595,7 +600,7 @@ Prepend to element. $dom = $dom->prepend_content('

Hi!

'); -Prepend to element content. +Prepend HTML/XML to element content. # "

AB

" $dom->parse('

B

')->at('h2')->prepend_content('A')->root; @@ -604,37 +609,39 @@ Prepend to element content. my $sibling = $dom->previous; -Previous sibling of element. +Return L object for previous sibling of element or C if +there are no more siblings. # "

A

" $dom->parse('

A

B

')->at('h2')->previous; =head2 remove - my $old = $dom->remove; + my $parent = $dom->remove; -Remove element. +Remove element and return L object for parent of element. # "
" - $dom->parse('

A

')->at('h1')->remove->root; + $dom->parse('

A

')->at('h1')->remove; =head2 replace - my $old = $dom->replace('
test
'); + my $parent = $dom->replace('
test
'); -Replace element. +Replace element with HTML/XML and return L object for parent of +element. # "

B

" - $dom->parse('

A

')->at('h1')->replace('

B

')->root; + $dom->parse('

A

')->at('h1')->replace('

B

'); # "
" - $dom->parse('

A

')->at('h1')->replace('')->root; + $dom->parse('

A

')->at('h1')->replace(''); =head2 replace_content - $dom = $dom->replace_content('test'); + $dom = $dom->replace_content('

test

'); -Replace element content. +Replace element content with HTML/XML. # "

B

" $dom->parse('

A

')->at('h1')->replace_content('B')->root; @@ -646,7 +653,23 @@ Replace element content. my $root = $dom->root; -Find root node. +Return L object for root node. + +=head2 strip + + my $parent = $dom->strip; + +Remove element while preserving its content and return L object for +parent of element. + + # "
A
" + $dom->parse('

A

')->at('h1')->strip; + +=head2 tap + + $dom = $dom->tap(sub {...}); + +Alias for L. =head2 text @@ -695,8 +718,7 @@ is enabled by default. my $xml = $dom->to_xml; my $xml = "$dom"; -Render this element and its content to XML. Note that the XML will be encoded -if a C has been defined. +Render this element and its content to XML. # "test" $dom->parse('
test
')->div->b->to_xml; @@ -704,9 +726,10 @@ if a C has been defined. =head2 tree my $tree = $dom->tree; - $dom = $dom->tree(['root', [qw(text lalala)]]); + $dom = $dom->tree(['root', ['text', 'foo']]); -Document Object Model. +Document Object Model. Note that this structure should only be used very +carefully since it is very dynamic. =head2 type @@ -716,7 +739,7 @@ Document Object Model. Element type. # List types of child elements - say $dom->children->pluck('type'); + say $dom->children->type; =head2 xml @@ -734,7 +757,7 @@ L object, depending on number of children. say $dom->p->text; say $dom->div->[23]->text; - say $dom->div->pluck('text'); + say $dom->div->text; =head1 ELEMENT ATTRIBUTES diff --git a/lib/Mojo/DOM/CSS.pm b/lib/Mojo/DOM/CSS.pm index dae5b29..ae5479d 100644 --- a/lib/Mojo/DOM/CSS.pm +++ b/lib/Mojo/DOM/CSS.pm @@ -3,14 +3,14 @@ use Mojo::Base -base; has 'tree'; -my $ESCAPE_RE = qr/\\[^[:xdigit:]]|\\[[:xdigit:]]{1,6}/; +my $ESCAPE_RE = qr/\\[^0-9a-fA-F]|\\[0-9a-fA-F]{1,6}/; my $ATTR_RE = qr/ \[ ((?:$ESCAPE_RE|[\w\-])+) # Key (?: (\W)? # Operator = - (?:"((?:\\"|[^"])+)"|(\S+)) # Value + (?:"((?:\\"|[^"])*)"|(\S+)) # Value )? \] /x; @@ -52,7 +52,7 @@ sub select { # Try all selectors with element for my $part (@$pattern) { - push(@results, $current) and last + push @results, $current and last if $self->_combinator([reverse @$part], $current, $tree); } } @@ -125,7 +125,6 @@ sub _compile { my ($separator, $element, $pc, $attrs, $combinator) = ($1, defined $2 ? $2 : '', $3, $6, $11); - # Trash next unless $separator || $element || $pc || $attrs || $combinator; # New selector @@ -251,10 +250,9 @@ sub _pc { # Siblings my $parent = $current->[3]; - my $start = $parent->[0] eq 'root' ? 1 : 4; my @siblings; my $type = $class =~ /of-type$/ ? $current->[1] : undef; - for my $i ($start .. $#$parent) { + for my $i (($parent->[0] eq 'root' ? 1 : 4) .. $#$parent) { my $sibling = $parent->[$i]; next unless $sibling->[0] eq 'tag'; next if defined $type && $type ne $sibling->[1]; @@ -279,8 +277,7 @@ sub _pc { # Siblings my $parent = $current->[3]; - my $start = $parent->[0] eq 'root' ? 1 : 4; - for my $i ($start .. $#$parent) { + for my $i (($parent->[0] eq 'root' ? 1 : 4) .. $#$parent) { my $sibling = $parent->[$i]; next if $sibling->[0] ne 'tag' || $sibling eq $current; return undef unless defined $type && $sibling->[1] ne $type; @@ -345,8 +342,7 @@ sub _sibling { my $parent = $current->[3]; my $found; - my $start = $parent->[0] eq 'root' ? 1 : 4; - for my $e (@$parent[$start .. $#$parent]) { + for my $e (@$parent[($parent->[0] eq 'root' ? 1 : 4) .. $#$parent]) { return $found if $e eq $current; next unless $e->[0] eq 'tag'; @@ -367,7 +363,7 @@ sub _unescape { $value =~ s/\\\n//g; # Unescape Unicode characters - $value =~ s/\\([[:xdigit:]]{1,6})\s?/pack('U', hex $1)/ge; + $value =~ s/\\([0-9a-fA-F]{1,6})\s?/pack('U', hex $1)/ge; # Remove backslash $value =~ s/\\//g; @@ -377,6 +373,8 @@ sub _unescape { 1; +=encoding utf8 + =head1 NAME Mojo::DOM::CSS - CSS selector engine @@ -600,9 +598,10 @@ L implements the following attributes. =head2 tree my $tree = $css->tree; - $css = $css->tree(['root', [qw(text lalala)]]); + $css = $css->tree(['root', ['text', 'foo']]); -Document Object Model. +Document Object Model. Note that this structure should only be used very +carefully since it is very dynamic. =head1 METHODS diff --git a/lib/Mojo/DOM/HTML.pm b/lib/Mojo/DOM/HTML.pm index a28a1e5..b7fee4c 100644 --- a/lib/Mojo/DOM/HTML.pm +++ b/lib/Mojo/DOM/HTML.pm @@ -1,15 +1,14 @@ package Mojo::DOM::HTML; use Mojo::Base -base; -use Mojo::Util qw(decode encode html_unescape xml_escape); +use Mojo::Util qw(html_unescape xml_escape); use Scalar::Util 'weaken'; -has [qw(charset xml)]; +has 'xml'; has tree => sub { ['root'] }; my $ATTR_RE = qr/ - \s* - ([^=\s>]+) # Key + ([^<>=\s]+) # Key (?: \s*=\s* (?: @@ -41,9 +40,12 @@ my $TOKEN_RE = qr/ | <( \s* - [^>\s]+ # Tag + [^<>\s]+ # Tag + \s* (?:$ATTR_RE)* # Attributes )> + | + (<) # Runaway "<" )?? /xis; @@ -76,32 +78,34 @@ my %INLINE = map { $_ => 1 } ( sub parse { my ($self, $html) = @_; - my $charset = $self->charset; - defined ($html = decode($charset, $html)) || return $self->charset(undef) if $charset; - - my $tree = ['root']; - my $current = $tree; + my $current = my $tree = ['root']; while ($html =~ m/\G$TOKEN_RE/gcs) { - my ($text, $pi, $comment, $cdata, $doctype, $tag) - = ($1, $2, $3, $4, $5, $6); + my ($text, $pi, $comment, $cdata, $doctype, $tag, $runaway) + = ($1, $2, $3, $4, $5, $6, $11); - # Text + # Text (and runaway "<") + $text .= '<' if defined $runaway; if (length $text) { - $text = html_unescape $text if (index $text, '&') >= 0; - $self->_text($text, \$current); + $text = html_unescape $text; + my $sibling = $current->[-1]; + if (ref $sibling && $sibling->[0] eq 'text') { $sibling->[1] .= $text } + else { push @$current, ['text', $text] } } # DOCTYPE - if ($doctype) { $self->_doctype($doctype, \$current) } + if ($doctype) { push @$current, ['doctype', $doctype] } # Comment - elsif ($comment) { $self->_comment($comment, \$current) } + elsif ($comment) { push @$current, ['comment', $comment] } # CDATA - elsif ($cdata) { $self->_cdata($cdata, \$current) } + elsif ($cdata) { push @$current, ['cdata', $cdata] } - # Processing instruction - elsif ($pi) { $self->_pi($pi, \$current) } + # Processing instruction (try to detect XML) + elsif ($pi) { + $self->xml(1) if !defined $self->xml && $pi =~ /xml/i; + push @$current, ['pi', $pi]; + } # End next unless $tag; @@ -121,12 +125,10 @@ sub parse { # Empty tag next if $key eq '/'; - # Add unescaped value - $value = html_unescape $value if $value && (index $value, '&') >= 0; - $attrs{$key} = $value; + $attrs{$key} = defined $value ? html_unescape($value) : $value; } - # Start + # Tag $self->_start($start, \%attrs, \$current); # Empty element @@ -134,9 +136,9 @@ sub parse { if (!$self->xml && $VOID{$start}) || $attr =~ m!/\s*$!; # Relaxed "script" or "style" - if (grep { $_ eq $start } qw(script style)) { + if ($start eq 'script' || $start eq 'style') { if ($html =~ m!\G(.*?)<\s*/\s*$start\s*>!gcsi) { - $self->_raw($1, \$current); + push @$current, ['raw', $1]; $self->_end($start, \$current); } } @@ -146,17 +148,7 @@ sub parse { return $self->tree($tree); } -sub render { - my $self = shift; - my $content = $self->_render($self->tree); - my $charset = $self->charset; - return $charset ? encode($charset, $content) : $content; -} - -sub _cdata { - my ($self, $cdata, $current) = @_; - push @$$current, ['cdata', $cdata]; -} +sub render { $_[0]->_render($_[0]->tree) } sub _close { my ($self, $current, $tags, $stop) = @_; @@ -165,8 +157,7 @@ sub _close { # Check if parents need to be closed my $parent = $$current; - while ($parent) { - last if $parent->[0] eq 'root' || $parent->[1] eq $stop; + while ($parent->[0] ne 'root' && $parent->[1] ne $stop) { # Close $tags->{$parent->[1]} and $self->_end($parent->[1], $current); @@ -176,27 +167,13 @@ sub _close { } } -sub _comment { - my ($self, $comment, $current) = @_; - push @$$current, ['comment', $comment]; -} - -sub _doctype { - my ($self, $doctype, $current) = @_; - push @$$current, ['doctype', $doctype]; -} - sub _end { my ($self, $end, $current) = @_; - # Not a tag - return if $$current->[0] eq 'root'; - # Search stack for start tag my $found = 0; my $next = $$current; - while ($next) { - last if $next->[0] eq 'root'; + while ($next->[0] ne 'root') { # Right tag ++$found and last if $next->[1] eq $end; @@ -204,7 +181,6 @@ sub _end { # Inline elements can only cross other inline elements return if !$self->xml && $INLINE{$end} && !$INLINE{$next->[1]}; - # Parent $next = $next->[3]; } @@ -213,17 +189,14 @@ sub _end { # Walk backwards $next = $$current; - while ($$current = $next) { - last if $$current->[0] eq 'root'; + while (($$current = $next) && $$current->[0] ne 'root') { $next = $$current->[3]; # Match if ($end eq $$current->[1]) { return $$current = $$current->[3] } # Optional elements - elsif ($OPTIONAL{$$current->[1]}) { - $self->_end($$current->[1], $current); - } + elsif ($OPTIONAL{$$current->[1]}) { $self->_end($$current->[1], $current) } # Table elsif ($end eq 'table') { $self->_close($current) } @@ -233,18 +206,6 @@ sub _end { } } -# Try to detect XML from processing instructions -sub _pi { - my ($self, $pi, $current) = @_; - $self->xml(1) if !defined $self->xml && $pi =~ /xml/i; - push @$$current, ['pi', $pi]; -} - -sub _raw { - my ($self, $raw, $current) = @_; - push @$$current, ['raw', $raw]; -} - sub _render { my ($self, $tree) = @_; @@ -256,16 +217,16 @@ sub _render { return $tree->[1] if $e eq 'raw'; # DOCTYPE - return "[1] . ">" if $e eq 'doctype'; + return '[1] . '>' if $e eq 'doctype'; # Comment - return "" if $e eq 'comment'; + return '' if $e eq 'comment'; # CDATA - return "[1] . "]]>" if $e eq 'cdata'; + return '[1] . ']]>' if $e eq 'cdata'; # Processing instruction - return "[1] . "?>" if $e eq 'pi'; + return '[1] . '?>' if $e eq 'pi'; # Start tag my $start = $e eq 'root' ? 1 : 2; @@ -338,21 +299,18 @@ sub _start { elsif ($start eq 'tr') { $self->_close($current, {tr => 1}) } # "" and "" - elsif (grep { $_ eq $start } qw(th td)) { - $self->_close($current, {th => 1}); - $self->_close($current, {td => 1}); + elsif ($start eq 'th' || $start eq 'td') { + $self->_close($current, {$_ => 1}) for qw(th td); } # "
" and "
" - elsif (grep { $_ eq $start } qw(dt dd)) { - $self->_end('dt', $current); - $self->_end('dd', $current); + elsif ($start eq 'dt' || $start eq 'dd') { + $self->_end($_, $current) for qw(dt dd); } # "" and "" - elsif (grep { $_ eq $start } qw(rt rp)) { - $self->_end('rt', $current); - $self->_end('rp', $current); + elsif ($start eq 'rt' || $start eq 'rp') { + $self->_end($_, $current) for qw(rt rp); } } @@ -363,13 +321,10 @@ sub _start { $$current = $new; } -sub _text { - my ($self, $text, $current) = @_; - push @$$current, ['text', $text]; -} - 1; +=encoding utf8 + =head1 NAME Mojo::DOM::HTML - HTML/XML engine @@ -391,19 +346,13 @@ L is the HTML/XML engine used by L. L implements the following attributes. -=head2 charset - - my $charset = $html->charset; - $html = $html->charset('UTF-8'); - -Charset used for decoding and encoding HTML/XML. - =head2 tree my $tree = $html->tree; - $html = $html->tree(['root', [qw(text lalala)]]); + $html = $html->tree(['root', ['text', 'foo']]); -Document Object Model. +Document Object Model. Note that this structure should only be used very +carefully since it is very dynamic. =head2 xml diff --git a/lib/Mojo/Date.pm b/lib/Mojo/Date.pm index eb32628..be0ad1a 100644 --- a/lib/Mojo/Date.pm +++ b/lib/Mojo/Date.pm @@ -1,9 +1,6 @@ package Mojo::Date; use Mojo::Base -base; -use overload - 'bool' => sub {1}, - '""' => sub { shift->to_string }, - fallback => 1; +use overload bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1; use Time::Local 'timegm'; @@ -63,6 +60,8 @@ sub to_string { 1; +=encoding utf8 + =head1 NAME Mojo::Date - HTTP date @@ -109,7 +108,7 @@ following new ones. my $date = Mojo::Date->new; my $date = Mojo::Date->new('Sun Nov 6 08:49:37 1994'); -Construct a new L object. +Construct a new L object and C date if necessary. =head2 parse @@ -131,8 +130,8 @@ Parse date. =head2 to_string - my $string = $date->to_string; - my $string = "$date"; + my $str = $date->to_string; + my $str = "$date"; Render date suitable for HTTP messages. diff --git a/lib/Mojo/EventEmitter.pm b/lib/Mojo/EventEmitter.pm index f9ecb08..9fb1d69 100644 --- a/lib/Mojo/EventEmitter.pm +++ b/lib/Mojo/EventEmitter.pm @@ -86,6 +86,8 @@ sub unsubscribe { 1; +=encoding utf8 + =head1 NAME Mojo::EventEmitter - Event emitter base class @@ -198,7 +200,7 @@ Unsubscribe from event. =head1 DEBUGGING -You can set the C environment variable to get some +You can set the MOJO_EVENTEMITTER_DEBUG environment variable to get some advanced diagnostics information printed to C. MOJO_EVENTEMITTER_DEBUG=1 diff --git a/lib/Mojo/Exception.pm b/lib/Mojo/Exception.pm index a42d858..60708fc 100644 --- a/lib/Mojo/Exception.pm +++ b/lib/Mojo/Exception.pm @@ -1,15 +1,12 @@ package Mojo::Exception; use Mojo::Base -base; -use overload - 'bool' => sub {1}, - '""' => sub { shift->to_string }, - fallback => 1; +use overload bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1; use Scalar::Util 'blessed'; has [qw(frames line lines_before lines_after)] => sub { [] }; has message => 'Exception!'; -has verbose => sub { $ENV{MOJO_EXCEPTION_VERBOSE} || 0 }; +has 'verbose'; sub new { my $self = shift->SUPER::new; @@ -22,19 +19,19 @@ sub to_string { my $self = shift; return $self->message unless $self->verbose; - my $string = $self->message ? $self->message : ''; + my $str = $self->message ? $self->message : ''; # Before - $string .= $_->[0] . ': ' . $_->[1] . "\n" for @{$self->lines_before}; + $str .= $_->[0] . ': ' . $_->[1] . "\n" for @{$self->lines_before}; # Line - $string .= ($self->line->[0] . ': ' . $self->line->[1] . "\n") + $str .= ($self->line->[0] . ': ' . $self->line->[1] . "\n") if $self->line->[0]; # After - $string .= $_->[0] . ': ' . $_->[1] . "\n" for @{$self->lines_after}; + $str .= $_->[0] . ': ' . $_->[1] . "\n" for @{$self->lines_after}; - return $string; + return $str; } sub trace { @@ -46,40 +43,36 @@ sub trace { } sub _context { - my ($self, $line, $lines) = @_; - - # Wrong file - return unless defined $lines->[0][$line - 1]; + my ($self, $num, $lines) = @_; # Line - $self->line([$line]); - for my $l (@$lines) { - chomp(my $code = $l->[$line - 1]); + return unless defined $lines->[0][$num - 1]; + $self->line([$num]); + for my $line (@$lines) { + chomp(my $code = $line->[$num - 1]); push @{$self->line}, $code; } # Before for my $i (2 .. 6) { - last if ((my $previous = $line - $i) < 0); - if (defined $lines->[0][$previous]) { - unshift @{$self->lines_before}, [$previous + 1]; - for my $l (@$lines) { - chomp(my $code = $l->[$previous]); - push @{$self->lines_before->[0]}, $code; - } + last if ((my $previous = $num - $i) < 0); + next unless defined $lines->[0][$previous]; + unshift @{$self->lines_before}, [$previous + 1]; + for my $line (@$lines) { + chomp(my $code = $line->[$previous]); + push @{$self->lines_before->[0]}, $code; } } # After for my $i (0 .. 4) { - next if ((my $next = $line + $i) < 0); - if (defined $lines->[0][$next]) { - push @{$self->lines_after}, [$next + 1]; - for my $l (@$lines) { - next unless defined(my $code = $l->[$next]); - chomp $code; - push @{$self->lines_after->[-1]}, $code; - } + next if ((my $next = $num + $i) < 0); + next unless defined $lines->[0][$next]; + push @{$self->lines_after}, [$next + 1]; + for my $line (@$lines) { + last unless defined(my $code = $line->[$next]); + chomp $code; + push @{$self->lines_after->[-1]}, $code; } } } @@ -100,8 +93,7 @@ sub _detect { # Search for context in files for my $frame (@trace) { - next unless -r $frame->[0]; - open my $handle, '<:utf8', $frame->[0]; + next unless -r $frame->[0] && open my $handle, '<:utf8', $frame->[0]; $self->_context($frame->[1], [[<$handle>]]); return $self; } @@ -114,6 +106,8 @@ sub _detect { 1; +=encoding utf8 + =head1 NAME Mojo::Exception - Exceptions with context @@ -148,21 +142,21 @@ Stacktrace. my $line = $e->line; $e = $e->line([3 => 'foo']); -The line where the exception occured. +The line where the exception occurred. =head2 lines_after my $lines = $e->lines_after; $e = $e->lines_after([[1 => 'bar'], [2 => 'baz']]); -Lines after the line where the exception occured. +Lines after the line where the exception occurred. =head2 lines_before my $lines = $e->lines_before; $e = $e->lines_before([[4 => 'bar'], [5 => 'baz']]); -Lines before the line where the exception occured. +Lines before the line where the exception occurred. =head2 message @@ -176,8 +170,7 @@ Exception message. my $verbose = $e->verbose; $e = $e->verbose(1); -Activate verbose rendering, defaults to the value of the -C environment variable or C<0>. +Render exception with context. =head1 METHODS @@ -200,8 +193,8 @@ Throw exception with stacktrace. =head2 to_string - my $string = $e->to_string; - my $string = "$e"; + my $str = $e->to_string; + my $str = "$e"; Render exception. diff --git a/lib/Mojo/Headers.pm b/lib/Mojo/Headers.pm index 4d3d02c..97ffe93 100644 --- a/lib/Mojo/Headers.pm +++ b/lib/Mojo/Headers.pm @@ -8,13 +8,13 @@ has max_line_size => sub { $ENV{MOJO_MAX_LINE_SIZE} || 10240 }; # Common headers my @HEADERS = ( qw(Accept Accept-Charset Accept-Encoding Accept-Language Accept-Ranges), - qw(Authorization Cache-Control Connection Content-Disposition), + qw(Allow Authorization Cache-Control Connection Content-Disposition), qw(Content-Encoding Content-Length Content-Range Content-Type Cookie DNT), - qw(Date ETag Expect Expires Host If-Modified-Since Last-Modified Location), - qw(Origin Proxy-Authenticate Proxy-Authorization Range), + qw(Date ETag Expect Expires Host If-Modified-Since Last-Modified Link), + qw(Location Origin Proxy-Authenticate Proxy-Authorization Range), qw(Sec-WebSocket-Accept Sec-WebSocket-Extensions Sec-WebSocket-Key), qw(Sec-WebSocket-Protocol Sec-WebSocket-Version Server Set-Cookie Status), - qw(TE Trailer Transfer-Encoding Upgrade User-Agent WWW-Authenticate) + qw(TE Trailer Transfer-Encoding Upgrade User-Agent Vary WWW-Authenticate) ); for my $header (@HEADERS) { my $name = lc $header; @@ -22,28 +22,31 @@ for my $header (@HEADERS) { monkey_patch __PACKAGE__, $name, sub { scalar shift->header($header => @_) }; } -# Lower case headers +# Lowercase headers my %NORMALCASE = map { lc($_) => $_ } @HEADERS; sub add { my ($self, $name) = (shift, shift); # Make sure we have a normal case entry for name - my $lcname = lc $name; - $self->{normalcase}{$lcname} = $self->{normalcase}{$lcname} ? $self->{normalcase}{$lcname} : $name unless $NORMALCASE{$lcname}; + my $key = lc $name; + $self->{normalcase}{$key} = defined $self->{normalcase}{$key} ? $self->{normalcase}{$key} : $name unless $NORMALCASE{$key}; # Add lines - push @{$self->{headers}{$lcname}}, map { ref $_ eq 'ARRAY' ? $_ : [$_] } @_; + push @{$self->{headers}{$key}}, map { ref $_ eq 'ARRAY' ? $_ : [$_] } @_; return $self; } +sub append { + my ($self, $name, $value) = @_; + my $old = $self->header($name); + return $self->header($name => defined $old ? "$old, $value" : $value); +} + sub clone { - my $self = shift; - my $clone = $self->new; - $clone->{headers}{$_} = [@{$self->{headers}{$_}}] - for keys %{$self->{headers}}; - return $clone; + my $self = shift; + return $self->new->from_hash($self->to_hash(1)); } sub from_hash { @@ -102,10 +105,10 @@ sub parse { } # New header - if ($line =~ /^(\S+)\s*:\s*(.*)$/) { push @$headers, $1, $2 } + if ($line =~ /^(\S+)\s*:\s*(.*)$/) { push @$headers, $1, [$2] } # Multiline - elsif (@$headers && $line =~ s/^\s+//) { $headers->[-1] .= " $line" } + elsif (@$headers && $line =~ s/^\s+//) { push @{$headers->[-1]}, $line } # Empty line else { @@ -132,30 +135,16 @@ sub remove { sub to_hash { my ($self, $multi) = @_; - my %hash; - for my $header (@{$self->names}) { - my @headers = $self->header($header); - - # Multi line - if ($multi) { $hash{$header} = [@headers] } - - # Flat - else { - - # Turn single value arrays into strings - @$_ == 1 and $_ = $_->[0] for @headers; - $hash{$header} = @headers > 1 ? [@headers] : $headers[0]; - } - } - + $hash{$_} = $multi ? [$self->header($_)] : scalar $self->header($_) + for @{$self->names}; return \%hash; } sub to_string { my $self = shift; - # Format multiline values + # Make sure multiline values are formatted correctly my @headers; for my $name (@{$self->names}) { push @headers, "$name: " . join("\x0d\x0a ", @$_) for $self->header($name); @@ -166,6 +155,8 @@ sub to_string { 1; +=encoding utf8 + =head1 NAME Mojo::Headers - Headers @@ -201,7 +192,7 @@ L implements the following attributes. $headers = $headers->max_line_size(1024); Maximum header line size in bytes, defaults to the value of the -C environment variable or C<10240>. +MOJO_MAX_LINE_SIZE environment variable or C<10240>. =head1 METHODS @@ -245,9 +236,34 @@ Shortcut for the C header. =head2 add - $headers = $headers->add('Content-Type', 'text/plain'); + $headers = $headers->add(Foo => 'one value'); + $headers = $headers->add(Foo => 'first value', 'second value'); + $headers = $headers->add(Foo => ['first line', 'second line']); + +Add one or more header values with one or more lines. + + # "Vary: Accept" + # "Vary: Accept-Encoding" + $headers->vary('Accept')->add(Vary => 'Accept-Encoding')->to_string; -Add one or more header lines. +=head2 allow + + my $allow = $headers->allow; + $headers = $headers->allow('GET, POST'); + +Shortcut for the C header. + +=head2 append + + $headers = $headers->append(Vary => 'Accept-Encoding'); + +Append value to header and flatten it if necessary. + + # "Vary: Accept" + $headers->append(Vary => 'Accept')->to_string; + + # "Vary: Accept, Accept-Encoding" + $headers->vary('Accept')->append(Vary => 'Accept-Encoding')->to_string; =head2 authorization @@ -359,13 +375,15 @@ Shortcut for the C header. $headers = $headers->from_hash({'Content-Type' => 'text/html'}); $headers = $headers->from_hash({}); -Parse headers from a hash reference. +Parse headers from a hash reference, an empty hash removes all headers. =head2 header - my $string = $headers->header('Content-Type'); - my @lines = $headers->header('Content-Type'); - $headers = $headers->header('Content-Type' => 'text/plain'); + my $value = $headers->header('Foo'); + my @values = $headers->header('Foo'); + $headers = $headers->header(Foo => 'one value'); + $headers = $headers->header(Foo => 'first value', 'second value'); + $headers = $headers->header(Foo => ['first line', 'second line']); Get or replace the current header values. @@ -416,6 +434,13 @@ Shortcut for the C header. Get leftover data from header parser. +=head2 link + + my $link = $headers->link; + $headers = $headers->link('; rel="next"'); + +Shortcut for the C header from RFC 5988. + =head2 location my $location = $headers->location; @@ -427,7 +452,10 @@ Shortcut for the C header. my $names = $headers->names; -Generate a list of all currently defined headers. +Return a list of all currently defined headers. + + # Names of all headers + say for @{$headers->names}; =head2 origin @@ -466,14 +494,14 @@ Shortcut for the C header. =head2 referrer my $referrer = $headers->referrer; - $headers = $headers->referrer('http://mojolicio.us'); + $headers = $headers->referrer('http://example.com'); Shortcut for the C header, there was a typo in RFC 2068 which resulted in C becoming an official header. =head2 remove - $headers = $headers->remove('Content-Type'); + $headers = $headers->remove('Foo'); Remove a header. @@ -500,8 +528,8 @@ Shortcut for the C header from RFC 6455. =head2 sec_websocket_protocol - my $protocol = $headers->sec_websocket_protocol; - $headers = $headers->sec_websocket_protocol('sample'); + my $proto = $headers->sec_websocket_protocol; + $headers = $headers->sec_websocket_protocol('sample'); Shortcut for the C header from RFC 6455. @@ -545,14 +573,14 @@ Shortcut for the C header. my $single = $headers->to_hash; my $multi = $headers->to_hash(1); -Turn headers into hash reference, nested array references to represent multi -line values are disabled by default. +Turn headers into hash reference, nested array references to represent +multiline values are disabled by default. say $headers->to_hash->{DNT}; =head2 to_string - my $string = $headers->to_string; + my $str = $headers->to_string; Turn headers into a string, suitable for HTTP messages. @@ -584,6 +612,13 @@ Shortcut for the C header. Shortcut for the C header. +=head2 vary + + my $vary = $headers->vary; + $headers = $headers->vary('*'); + +Shortcut for the C header. + =head2 www_authenticate my $authenticate = $headers->www_authenticate; diff --git a/lib/Mojo/HelloWorld.pm b/lib/Mojo/HelloWorld.pm index 62da868..b7de97c 100644 --- a/lib/Mojo/HelloWorld.pm +++ b/lib/Mojo/HelloWorld.pm @@ -7,6 +7,8 @@ any '/*whatever' => {whatever => '', text => 'Your Mojo is working!'}; 1; +=encoding utf8 + =head1 NAME Mojo::HelloWorld - Hello World! diff --git a/lib/Mojo/Home.pm b/lib/Mojo/Home.pm index 2fd932e..7b12366 100644 --- a/lib/Mojo/Home.pm +++ b/lib/Mojo/Home.pm @@ -1,9 +1,6 @@ package Mojo::Home; use Mojo::Base -base; -use overload - 'bool' => sub {1}, - '""' => sub { shift->to_string }, - fallback => 1; +use overload bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1; use Cwd 'abs_path'; use File::Basename 'dirname'; @@ -80,18 +77,12 @@ sub parse { sub rel_dir { catdir(@{shift->{parts} || []}, split '/', shift) } sub rel_file { catfile(@{shift->{parts} || []}, split '/', shift) } -# DEPRECATED in Rainbow! -sub slurp_rel_file { - warn <slurp_rel_file is DEPRECATED in favor of Mojo::Util->slurp!!! -EOF - slurp shift->rel_file(@_); -} - sub to_string { catdir(@{shift->{parts} || []}) } 1; +=encoding utf8 + =head1 NAME Mojo::Home - Home sweet home! @@ -121,15 +112,15 @@ following new ones. my $home = Mojo::Home->new; my $home = Mojo::Home->new('/home/sri/myapp'); -Construct a new L object. +Construct a new L object and C home directory if necessary. =head2 detect $home = $home->detect; $home = $home->detect('My::App'); -Detect home directory from the value of the C environment variable -or application class. +Detect home directory from the value of the MOJO_HOME environment variable or +application class. =head2 lib_dir @@ -143,9 +134,9 @@ Path to C directory of application. my $files = $home->list_files('foo/bar'); Portably list all files recursively in directory relative to the home -diectory. +directory. - $home->rel_file($home->list_files('templates/layouts')->[1]); + say $home->rel_file($home->list_files('templates/layouts')->[1]); =head2 mojo_lib_dir @@ -174,8 +165,8 @@ Portably generate an absolute path for a file relative to the home directory. =head2 to_string - my $string = $home->to_string; - my $string = "$home"; + my $str = $home->to_string; + my $str = "$home"; Home directory. diff --git a/lib/Mojo/IOLoop.pm b/lib/Mojo/IOLoop.pm index 9561a3b..eeccb3c 100644 --- a/lib/Mojo/IOLoop.pm +++ b/lib/Mojo/IOLoop.pm @@ -9,9 +9,8 @@ use Mojo::IOLoop::Delay; use Mojo::IOLoop::Server; use Mojo::IOLoop::Stream; use Mojo::Reactor::Poll; -use Mojo::Util 'md5_sum'; +use Mojo::Util qw(md5_sum steady_time); use Scalar::Util 'weaken'; -use Time::HiRes 'time'; use constant DEBUG => $ENV{MOJO_IOLOOP_DEBUG} || 0; @@ -33,50 +32,39 @@ $SIG{PIPE} = 'IGNORE'; __PACKAGE__->singleton->reactor; sub acceptor { - my ($self, $acceptor) = @_; - $self = $self->singleton unless ref $self; + my ($self, $acceptor) = (_instance(shift), @_); # Find acceptor for id return $self->{acceptors}{$acceptor} unless ref $acceptor; - # Make sure connection manager is running - $self->_manager; - # Connect acceptor with reactor my $id = $self->_id; $self->{acceptors}{$id} = $acceptor; weaken $acceptor->reactor($self->reactor)->{reactor}; $self->{accepts} = $self->max_accepts if $self->max_accepts; - # Stop accepting so new acceptor can get picked up + # Allow new acceptor to get picked up $self->_not_accepting; return $id; } sub client { - my ($self, $cb) = (shift, pop); - $self = $self->singleton unless ref $self; + my ($self, $cb) = (_instance(shift), pop); - # Make sure connection manager is running - $self->_manager; + # Make sure timers are running + $self->_recurring; - my $id = $self->_id; - my $c = $self->{connections}{$id} ||= {}; - my $client = $c->{client} = Mojo::IOLoop::Client->new; + my $id = $self->_id; + my $client = $self->{connections}{$id}{client} = Mojo::IOLoop::Client->new; weaken $client->reactor($self->reactor)->{reactor}; weaken $self; $client->on( connect => sub { - my $handle = pop; - - # Turn handle into stream - my $c = $self->{connections}{$id}; - delete $c->{client}; - my $stream = $c->{stream} = Mojo::IOLoop::Stream->new($handle); + delete $self->{connections}{$id}{client}; + my $stream = Mojo::IOLoop::Stream->new(pop); $self->_stream($stream => $id); - $self->$cb(undef, $stream); } ); @@ -92,8 +80,7 @@ sub client { } sub delay { - my $self = shift; - $self = $self->singleton unless ref $self; + my $self = _instance(shift); my $delay = Mojo::IOLoop::Delay->new; weaken $delay->ioloop($self)->{ioloop}; @@ -107,41 +94,24 @@ sub generate_port { Mojo::IOLoop::Server->generate_port } sub is_running { (ref $_[0] ? $_[0] : $_[0]->singleton)->reactor->is_running } sub one_tick { (ref $_[0] ? $_[0] : $_[0]->singleton)->reactor->one_tick } -sub recurring { - my ($self, $after, $cb) = @_; - $self = $self->singleton unless ref $self; - weaken $self; - return $self->reactor->recurring($after => sub { $self->$cb }); -} +sub recurring { shift->_timer(recurring => @_) } sub remove { - my ($self, $id) = @_; - $self = $self->singleton unless ref $self; - if (my $c = $self->{connections}{$id}) { return $c->{finish} = 1 } + my ($self, $id) = (_instance(shift), @_); + my $c = $self->{connections}{$id}; + if ($c && (my $stream = $c->{stream})) { return $stream->close_gracefully } $self->_remove($id); } sub server { - my ($self, $cb) = (shift, pop); - $self = $self->singleton unless ref $self; + my ($self, $cb) = (_instance(shift), pop); my $server = Mojo::IOLoop::Server->new; weaken $self; $server->on( accept => sub { - my $handle = pop; - - # Turn handle into stream - my $stream = Mojo::IOLoop::Stream->new($handle); + my $stream = Mojo::IOLoop::Stream->new(pop); $self->$cb($stream, $self->stream($stream)); - - # Enforce connection limit (randomize to improve load balancing) - $self->max_connections(0) - if defined $self->{accepts} - && ($self->{accepts} -= int(rand 2) + 1) <= 0; - - # Stop accepting to release accept mutex - $self->_not_accepting; } ); $server->listen(@_); @@ -161,39 +131,40 @@ sub start { sub stop { (ref $_[0] ? $_[0] : $_[0]->singleton)->reactor->stop } sub stream { - my ($self, $stream) = @_; - $self = $self->singleton unless ref $self; - - # Connect stream with reactor - return $self->_stream($stream, $self->_id) if ref $stream; + my ($self, $stream) = (_instance(shift), @_); # Find stream for id - return undef unless my $c = $self->{connections}{$stream}; - return $c->{stream}; -} + return ($self->{connections}{$stream} || {})->{stream} unless ref $stream; -sub timer { - my ($self, $after, $cb) = @_; - $self = $self->singleton unless ref $self; - weaken $self; - return $self->reactor->timer($after => sub { $self->$cb }); + # Release accept mutex + $self->_not_accepting; + + # Enforce connection limit (randomize to improve load balancing) + $self->max_connections(0) + if defined $self->{accepts} && ($self->{accepts} -= int(rand 2) + 1) <= 0; + + return $self->_stream($stream, $self->_id); } +sub timer { shift->_timer(timer => @_) } + sub _accepting { my $self = shift; - # Check connection limit - return if $self->{accepting}; + # Check if we have acceptors my $acceptors = $self->{acceptors} ||= {}; - return unless keys %$acceptors; + return $self->_remove(delete $self->{accept}) unless keys %$acceptors; + + # Check connection limit my $i = keys %{$self->{connections}}; my $max = $self->max_connections; return unless $i < $max; # Acquire accept mutex if (my $cb = $self->lock) { return unless $self->$cb(!$i) } + $self->_remove(delete $self->{accept}); - # Check if multi-accept is desirable and start accepting + # Check if multi-accept is desirable my $multi = $self->multi_accept; $_->multi_accept($max < $multi ? 1 : $multi)->start for values %$acceptors; $self->{accepting}++; @@ -202,38 +173,19 @@ sub _accepting { sub _id { my $self = shift; my $id; - do { $id = md5_sum('c' . time . rand 999) } + do { $id = md5_sum('c' . steady_time . rand 999) } while $self->{connections}{$id} || $self->{acceptors}{$id}; return $id; } -sub _manage { - my $self = shift; - - # Try to acquire accept mutex - $self->_accepting; - - # Close connections gracefully - my $connections = $self->{connections} ||= {}; - while (my ($id, $c) = each %$connections) { - $self->_remove($id) - if $c->{finish} && (!$c->{stream} || !$c->{stream}->is_writing); - } - - # Graceful stop - $self->_remove(delete $self->{manager}) - unless keys %$connections || keys %{$self->{acceptors}}; - $self->stop if $self->max_connections == 0 && keys %$connections == 0; -} - -sub _manager { - my $self = shift; - $self->{manager} ||= $self->recurring($self->accept_interval => \&_manage); -} +sub _instance { ref $_[0] ? $_[0] : $_[0]->singleton } sub _not_accepting { my $self = shift; + # Make sure timers are running + $self->_recurring; + # Release accept mutex return unless delete $self->{accepting}; return unless my $cb = $self->unlock; @@ -242,6 +194,12 @@ sub _not_accepting { $_->stop for values %{$self->{acceptors} || {}}; } +sub _recurring { + my $self = shift; + $self->{accept} ||= $self->recurring($self->accept_interval => \&_accepting); + $self->{stop} ||= $self->recurring(1 => \&_stop); +} + sub _remove { my ($self, $id) = @_; @@ -250,35 +208,46 @@ sub _remove { return if $reactor->remove($id); # Acceptor - if (delete $self->{acceptors}{$id}) { delete $self->{accepting} } + if (delete $self->{acceptors}{$id}) { $self->_not_accepting } - # Connection (stream needs to be deleted first) - else { - delete(($self->{connections}{$id} || {})->{stream}); - delete $self->{connections}{$id}; - } + # Connection + else { delete $self->{connections}{$id} } +} + +sub _stop { + my $self = shift; + return if keys %{$self->{connections}}; + $self->stop if $self->max_connections == 0; + return if keys %{$self->{acceptors}}; + $self->{$_} && $self->_remove(delete $self->{$_}) for qw(accept stop); } sub _stream { my ($self, $stream, $id) = @_; - # Make sure connection manager is running - $self->_manager; + # Make sure timers are running + $self->_recurring; # Connect stream with reactor $self->{connections}{$id}{stream} = $stream; weaken $stream->reactor($self->reactor)->{reactor}; - - # Start streaming weaken $self; - $stream->on(close => sub { $self->{connections}{$id}{finish} = 1 }); + $stream->on(close => sub { $self && $self->_remove($id) }); $stream->start; return $id; } +sub _timer { + my ($self, $method, $after, $cb) = (_instance(shift), @_); + weaken $self; + return $self->reactor->$method($after => sub { $self->$cb }); +} + 1; +=encoding utf8 + =head1 NAME Mojo::IOLoop - Minimalistic event loop @@ -332,14 +301,17 @@ L is a very minimalistic event loop based on L, it has been reduced to the absolute minimal feature set required to build solid and scalable non-blocking TCP clients and servers. -Optional modules L (4.0+), L (0.16+) and -L (1.75+) are supported transparently, and used if installed. -Individual features can also be disabled with the C and -C environment variables. +The event loop will be resilient to time jumps if a monotonic clock is +available through L. A TLS certificate and key are also built +right in, to make writing test servers as easy as possible. Also note that for +convenience the C signal will be set to C when L +is loaded. -A TLS certificate and key are also built right in, to make writing test -servers as easy as possible. Also note that for convenience the C signal -will be set to C when L is loaded. +For better scalability (epoll, kqueue) and to provide IPv6 as well as TLS +support, the optional modules L (4.0+), L (0.16+) and +L (1.75+) will be used automatically if they are installed. +Individual features can also be disabled with the MOJO_NO_IPV6 and MOJO_NO_TLS +environment variables. See L for more. @@ -352,9 +324,9 @@ L implements the following attributes. my $interval = $loop->accept_interval; $loop = $loop->accept_interval(0.5); -Interval in seconds for trying to reacquire the accept mutex and connection -management, defaults to C<0.025>. Note that changing this value can affect -performance and idle CPU usage. +Interval in seconds for trying to reacquire the accept mutex, defaults to +C<0.025>. Note that changing this value can affect performance and idle CPU +usage. =head2 lock @@ -462,19 +434,20 @@ L. my $delay = $loop->delay(sub {...}); my $delay = $loop->delay(sub {...}, sub {...}); -Get L object to control the flow of events. A single -callback will be treated as a subscriber to the C event, and multiple -ones as a chain of steps. +Get L object to manage callbacks and control the flow of +events. A single callback will be treated as a subscriber to the C +event, and multiple ones as a chain of steps. # Synchronize multiple events my $delay = Mojo::IOLoop->delay(sub { say 'BOOM!' }); for my $i (1 .. 10) { - $delay->begin; + my $end = $delay->begin; Mojo::IOLoop->timer($i => sub { say 10 - $i; - $delay->end; + $end->(); }); } + $delay->wait unless Mojo::IOLoop->is_running; # Sequentialize multiple events my $delay = Mojo::IOLoop->delay( @@ -497,8 +470,6 @@ ones as a chain of steps. # Third step (the end) sub { say 'And done after 5 seconds total.' } ); - - # Wait for events if necessary $delay->wait unless Mojo::IOLoop->is_running; =head2 generate_port @@ -570,6 +541,10 @@ loop object from everywhere inside the process. Mojo::IOLoop->timer(2 => sub { Mojo::IOLoop->stop }); Mojo::IOLoop->start; + # Restart active timer + my $id = Mojo::IOLoop->timer(3 => sub { say 'Timeout!' }); + Mojo::IOLoop->singleton->reactor->again($id); + =head2 start Mojo::IOLoop->start; @@ -614,7 +589,7 @@ seconds. =head1 DEBUGGING -You can set the C environment variable to get some advanced +You can set the MOJO_IOLOOP_DEBUG environment variable to get some advanced diagnostics information printed to C. MOJO_IOLOOP_DEBUG=1 diff --git a/lib/Mojo/IOLoop/Client.pm b/lib/Mojo/IOLoop/Client.pm index 8196313..7efca4b 100644 --- a/lib/Mojo/IOLoop/Client.pm +++ b/lib/Mojo/IOLoop/Client.pm @@ -34,7 +34,7 @@ sub connect { sub _cleanup { my $self = shift; - return $self unless my $reactor = $self->{reactor}; + return $self unless my $reactor = $self->reactor; $self->{$_} && $reactor->remove(delete $self->{$_}) for qw(delay timer handle); return $self; @@ -56,7 +56,7 @@ sub _connect { $options{LocalAddr} = $args->{local_address} if $args->{local_address}; $options{PeerAddr} =~ s/[\[\]]//g if $options{PeerAddr}; my $class = IPV6 ? 'IO::Socket::IP' : 'IO::Socket::INET'; - return $self->emit_safe(error => "Couldn't connect") + return $self->emit_safe(error => "Couldn't connect: $@") unless $self->{handle} = $handle = $class->new(%options); # Timeout @@ -73,16 +73,15 @@ sub _connect { sub _tls { my $self = shift; - # Switch between reading and writing + # Connected my $handle = $self->{handle}; - if ($self->{tls} && !$handle->connect_SSL) { - my $err = $IO::Socket::SSL::SSL_ERROR; - if ($err == TLS_READ) { $self->reactor->watch($handle, 1, 0) } - elsif ($err == TLS_WRITE) { $self->reactor->watch($handle, 1, 1) } - return; - } + return $self->_cleanup->emit_safe(connect => $handle) + if $handle->connect_SSL; - $self->_cleanup->emit_safe(connect => $handle); + # Switch between reading and writing + my $err = $IO::Socket::SSL::SSL_ERROR; + if ($err == TLS_READ) { $self->reactor->watch($handle, 1, 0) } + elsif ($err == TLS_WRITE) { $self->reactor->watch($handle, 1, 1) } } sub _try { @@ -98,39 +97,37 @@ sub _try { # Disable Nagle's algorithm setsockopt $handle, IPPROTO_TCP, TCP_NODELAY, 1; - # TLS - if ($args->{tls} && !$handle->isa('IO::Socket::SSL')) { - return $self->emit_safe( - error => 'IO::Socket::SSL 1.75 required for TLS support') - unless TLS; - - # Upgrade - weaken $self; - my %options = ( - SSL_ca_file => $args->{tls_ca} - && -T $args->{tls_ca} ? $args->{tls_ca} : undef, - SSL_cert_file => $args->{tls_cert}, - SSL_error_trap => sub { $self->_cleanup->emit_safe(error => $_[1]) }, - SSL_hostname => $args->{address}, - SSL_key_file => $args->{tls_key}, - SSL_startHandshake => 0, - SSL_verify_mode => $args->{tls_ca} ? 0x01 : 0x00, - SSL_verifycn_name => $args->{address}, - SSL_verifycn_scheme => $args->{tls_ca} ? 'http' : undef - ); - $self->{tls} = 1; - my $reactor = $self->reactor; - $reactor->remove($handle); - return $self->emit_safe(error => 'TLS upgrade failed') - unless $handle = IO::Socket::SSL->start_SSL($handle, %options); - return $reactor->io($handle => sub { $self->_tls })->watch($handle, 0, 1); - } + return $self->_cleanup->emit_safe(connect => $handle) + if !$args->{tls} || $handle->isa('IO::Socket::SSL'); + return $self->emit_safe( + error => 'IO::Socket::SSL 1.75 required for TLS support') + unless TLS; - $self->_cleanup->emit_safe(connect => $handle); + # Upgrade + weaken $self; + my %options = ( + SSL_ca_file => $args->{tls_ca} + && -T $args->{tls_ca} ? $args->{tls_ca} : undef, + SSL_cert_file => $args->{tls_cert}, + SSL_error_trap => sub { $self->_cleanup->emit_safe(error => $_[1]) }, + SSL_hostname => $args->{address}, + SSL_key_file => $args->{tls_key}, + SSL_startHandshake => 0, + SSL_verify_mode => $args->{tls_ca} ? 0x01 : 0x00, + SSL_verifycn_name => $args->{address}, + SSL_verifycn_scheme => $args->{tls_ca} ? 'http' : undef + ); + my $reactor = $self->reactor; + $reactor->remove($handle); + return $self->emit_safe(error => 'TLS upgrade failed') + unless $handle = IO::Socket::SSL->start_SSL($handle, %options); + $reactor->io($handle => sub { $self->_tls })->watch($handle, 0, 1); } 1; +=encoding utf8 + =head1 NAME Mojo::IOLoop::Client - Non-blocking TCP client @@ -149,7 +146,7 @@ Mojo::IOLoop::Client - Non-blocking TCP client my ($client, $err) = @_; ... }); - $client->connect(address => 'mojolicio.us', port => 80); + $client->connect(address => 'example.com', port => 80); # Start reactor if necessary $client->reactor->start unless $client->reactor->is_running; @@ -211,39 +208,57 @@ These options are currently available: =item address + address => 'mojolicio.us' + Address or host name of the peer to connect to, defaults to C. =item handle + handle => $handle + Use an already prepared handle. =item local_address + local_address => '127.0.0.1' + Local address to bind to. =item port -Port to connect to. + port => 80 + +Port to connect to, defaults to C<80> or C<443> with C option. =item timeout + timeout => 15 + Maximum amount of time in seconds establishing connection may take before getting canceled, defaults to C<10>. =item tls + tls => 1 + Enable TLS. =item tls_ca + tls_ca => '/etc/tls/ca.crt' + Path to TLS certificate authority file. Also activates hostname verification. =item tls_cert + tls_cert => '/etc/tls/client.crt' + Path to the TLS certificate file. =item tls_key + tls_key => '/etc/tls/client.key' + Path to the TLS key file. =back diff --git a/lib/Mojo/IOLoop/Delay.pm b/lib/Mojo/IOLoop/Delay.pm index ef17ae5..c418b5f 100644 --- a/lib/Mojo/IOLoop/Delay.pm +++ b/lib/Mojo/IOLoop/Delay.pm @@ -6,13 +6,12 @@ use Mojo::IOLoop; has ioloop => sub { Mojo::IOLoop->singleton }; sub begin { - my $self = shift; - my $id = $self->{counter}++; - return sub { shift; $self->_step($id, @_) }; + my ($self, $ignore) = @_; + $self->{pending}++; + my $id = $self->{counter}++; + return sub { (defined $ignore ? $ignore : 1) and shift; $self->_step($id, @_) }; } -sub end { shift->_step(undef, @_) } - sub steps { my $self = shift; $self->{steps} = [@_]; @@ -31,33 +30,26 @@ sub wait { sub _step { my ($self, $id) = (shift, shift); - # Arguments - my $ordered = $self->{ordered} ||= []; - my $unordered = $self->{unordered} ||= []; - if (defined $id) { $ordered->[$id] = [@_] } - else { push @$unordered, @_ } - - # Wait for more events - return $self->{counter} if --$self->{counter}; + $self->{args}[$id] = [@_]; + return $self->{pending} if --$self->{pending} || $self->{lock}; + local $self->{lock} = 1; + my @args = map {@$_} @{delete $self->{args}}; - # Next step - my $cb = shift @{$self->{steps} ||= []}; - $self->{$_} = [] for qw(ordered unordered); - my @args = ((map {@$_} grep {defined} @$ordered), @$unordered); - $self->$cb(@args) if $cb; - - # Finished - $self->emit('finish', @args) - if !$self->{counter} && !@{$self->{steps}} && !$self->{finished}++; + $self->{counter} = 0; + if (my $cb = shift @{$self->{steps} ||= []}) { $self->$cb(@args) } + if (!$self->{counter}) { $self->emit(finish => @args) } + elsif (!$self->{pending}) { $self->ioloop->timer(0 => $self->begin) } return 0; } 1; +=encoding utf8 + =head1 NAME -Mojo::IOLoop::Delay - Control the flow of events +Mojo::IOLoop::Delay - Manage callbacks and control the flow of events =head1 SYNOPSIS @@ -67,12 +59,13 @@ Mojo::IOLoop::Delay - Control the flow of events my $delay = Mojo::IOLoop::Delay->new; $delay->on(finish => sub { say 'BOOM!' }); for my $i (1 .. 10) { - $delay->begin; + my $end = $delay->begin; Mojo::IOLoop->timer($i => sub { say 10 - $i; - $delay->end; + $end->(); }); } + $delay->wait unless Mojo::IOLoop->is_running; # Sequentialize multiple events my $delay = Mojo::IOLoop::Delay->new; @@ -99,13 +92,12 @@ Mojo::IOLoop::Delay - Control the flow of events say 'And done after 5 seconds total.'; } ); - - # Wait for events if necessary $delay->wait unless Mojo::IOLoop->is_running; =head1 DESCRIPTION -L controls the flow of events for L. +L manages callbacks and controls the flow of events for +L. =head1 EVENTS @@ -142,22 +134,17 @@ implements the following new ones. =head2 begin my $cb = $delay->begin; + my $cb = $delay->begin(0); -Increment active event counter, the returned callback can be used instead of -C, which has the advantage of preserving the order of arguments. Note -that the first argument passed to the callback will be ignored. +Increment active event counter, the returned callback can be used to decrement +the active event counter again. Arguments passed to the callback are queued in +the right order for the next step or C event and C method, the +first argument will be ignored by default. + # Capture all arguments my $delay = Mojo::IOLoop->delay; - Mojo::UserAgent->new->get('mojolicio.us' => $delay->begin); - my $tx = $delay->wait; - -=head2 end - - my $remaining = $delay->end; - my $remaining = $delay->end(@args); - -Decrement active event counter, all arguments are queued for the next step or -C event and C method. + Mojo::IOLoop->client({port => 3000} => $delay->begin(0)); + my ($loop, $err, $stream) = $delay->wait; =head2 steps @@ -165,7 +152,8 @@ C event and C method. Sequentialize multiple events, the first callback will run right away, and the next one once the active event counter reaches zero, this chain will continue -until there are no more callbacks left. +until there are no more callbacks or a callback does not increment the active +event counter. =head2 wait diff --git a/lib/Mojo/IOLoop/Server.pm b/lib/Mojo/IOLoop/Server.pm index 21cd794..8dbfebd 100644 --- a/lib/Mojo/IOLoop/Server.pm +++ b/lib/Mojo/IOLoop/Server.pm @@ -34,7 +34,7 @@ has reactor => sub { sub DESTROY { my $self = shift; if (my $port = $self->{port}) { $ENV{MOJO_REUSE} =~ s/(?:^|\,)${port}:\d+// } - return unless my $reactor = $self->{reactor}; + return unless my $reactor = $self->reactor; $self->stop if $self->{handle}; $reactor->remove($_) for values %{$self->{handles}}; } @@ -56,8 +56,8 @@ sub listen { my $handle; my $class = IPV6 ? 'IO::Socket::IP' : 'IO::Socket::INET'; if (defined $fd) { - $handle = $class->new; - $handle->fdopen($fd, 'r') or croak "Can't open file descriptor $fd: $!"; + $handle = $class->new_from_fd($fd, 'r') + or croak "Can't open file descriptor $fd: $!"; } # New socket @@ -68,10 +68,11 @@ sub listen { LocalPort => $port, Proto => 'tcp', ReuseAddr => 1, + ReusePort => $args->{reuse}, Type => SOCK_STREAM ); $options{LocalAddr} =~ s/[\[\]]//g; - $handle = $class->new(%options) or croak "Can't create listen socket: $!"; + $handle = $class->new(%options) or croak "Can't create listen socket: $@"; $fd = fileno $handle; $ENV{MOJO_REUSE} .= length $ENV{MOJO_REUSE} ? ",$reuse:$fd" : "$reuse:$fd"; } @@ -81,11 +82,11 @@ sub listen { return unless $args->{tls}; croak "IO::Socket::SSL 1.75 required for TLS support" unless TLS; - # Options (Prioritize RC4 to mitigate BEAST attack) + # Prioritize RC4 to mitigate BEAST attack and use Perfect Forward Secrecy my $options = $self->{tls} = { SSL_cert_file => $args->{tls_cert} || $CERT, SSL_cipher_list => - '!aNULL:!eNULL:!EXPORT:!DSS:!DES:!SSLv2:!LOW:RC4-SHA:RC4-MD5:ALL', + 'ECDHE-RSA-AES128-SHA256:AES128-GCM-SHA256:RC4:HIGH:!MD5:!aNULL:!EDH', SSL_honor_cipher_order => 1, SSL_key_file => $args->{tls_key} || $KEY, SSL_startHandshake => 0, @@ -102,6 +103,8 @@ sub generate_port { ->sockport; } +sub handle { shift->{handle} } + sub start { my $self = shift; weaken $self; @@ -109,7 +112,7 @@ sub start { $self->{handle} => sub { $self->_accept for 1 .. $self->multi_accept }); } -sub stop { $_[0]->reactor->remove($_[0]->{handle}) } +sub stop { $_[0]->reactor->remove($_[0]{handle}) } sub _accept { my $self = shift; @@ -139,8 +142,7 @@ sub _tls { # Accepted if ($handle->accept_SSL) { $self->reactor->remove($handle); - delete $self->{handles}{$handle}; - return $self->emit_safe(accept => $handle); + return $self->emit_safe(accept => delete $self->{handles}{$handle}); } # Switch between reading and writing @@ -151,6 +153,8 @@ sub _tls { 1; +=encoding utf8 + =head1 NAME Mojo::IOLoop::Server - Non-blocking TCP server @@ -229,34 +233,57 @@ These options are currently available: =item address + address => '127.0.0.1' + Local address to listen on, defaults to all. =item backlog + backlog => 128 + Maximum backlog size, defaults to C. =item port + port => 80 + Port to listen on. +=item reuse + + reuse => 1 + +Allow multiple servers to use the same port with the C socket +option. + =item tls + tls => 1 + Enable TLS. =item tls_ca + tls_ca => '/etc/tls/ca.crt' + Path to TLS certificate authority file. =item tls_cert + tls_cert => '/etc/tls/server.crt' + Path to the TLS cert file, defaults to a built-in test certificate. =item tls_key + tls_key => '/etc/tls/server.key' + Path to the TLS key file, defaults to a built-in test key. =item tls_verify + tls_verify => 0x00 + TLS verification mode, defaults to C<0x03>. =back @@ -267,6 +294,12 @@ TLS verification mode, defaults to C<0x03>. Find a free TCP port, this is a utility function primarily used for tests. +=head2 handle + + my $handle = $server->handle; + +Get handle for server. + =head2 start $server->start; diff --git a/lib/Mojo/IOLoop/Stream.pm b/lib/Mojo/IOLoop/Stream.pm index f515ada..4157e2e 100644 --- a/lib/Mojo/IOLoop/Stream.pm +++ b/lib/Mojo/IOLoop/Stream.pm @@ -3,56 +3,63 @@ use Mojo::Base 'Mojo::EventEmitter'; use Errno qw(EAGAIN ECONNRESET EINTR EPIPE EWOULDBLOCK); use Scalar::Util 'weaken'; -use Time::HiRes 'time'; has reactor => sub { require Mojo::IOLoop; Mojo::IOLoop->singleton->reactor; }; -has timeout => 15; sub DESTROY { shift->close } -sub new { shift->SUPER::new(handle => shift, buffer => '', active => time) } +sub new { shift->SUPER::new(handle => shift, buffer => '', timeout => 15) } sub close { my $self = shift; - # Cleanup - return unless my $reactor = $self->{reactor}; - $reactor->remove(delete $self->{timer}) if $self->{timer}; - return unless my $handle = delete $self->{handle}; + return unless my $reactor = $self->reactor; + return unless my $handle = delete $self->timeout(0)->{handle}; $reactor->remove($handle); - close $handle; $self->emit_safe('close'); } +sub close_gracefully { + my $self = shift; + return $self->{graceful} = 1 if $self->is_writing; + $self->close; +} + sub handle { shift->{handle} } sub is_readable { my $self = shift; - $self->{active} = time; + $self->_again; return $self->{handle} && $self->reactor->is_readable($self->{handle}); } sub is_writing { my $self = shift; - return undef unless exists $self->{handle}; + return undef unless $self->{handle}; return !!length($self->{buffer}) || $self->has_subscribers('drain'); } sub start { my $self = shift; - return $self->_startup unless $self->{startup}++; - return unless delete $self->{paused}; - $self->reactor->watch($self->{handle}, 1, $self->is_writing); + + # Resume + my $reactor = $self->reactor; + return $reactor->watch($self->{handle}, 1, $self->is_writing) + if delete $self->{paused}; + + weaken $self; + my $cb = sub { pop() ? $self->_write : $self->_read }; + $reactor->io($self->timeout($self->{timeout})->{handle} => $cb); } sub stop { my $self = shift; - return if $self->{paused}++; - $self->reactor->watch($self->{handle}, 0, $self->is_writing); + $self->reactor->watch($self->{handle}, 0, $self->is_writing) + unless $self->{paused}++; } sub steal_handle { @@ -61,6 +68,21 @@ sub steal_handle { return delete $self->{handle}; } +sub timeout { + my $self = shift; + + return $self->{timeout} unless @_; + + my $reactor = $self->reactor; + $reactor->remove(delete $self->{timer}) if $self->{timer}; + return $self unless my $timeout = $self->{timeout} = shift; + weaken $self; + $self->{timer} + = $reactor->timer($timeout => sub { $self->emit_safe('timeout')->close }); + + return $self; +} + sub write { my ($self, $chunk, $cb) = @_; @@ -73,42 +95,27 @@ sub write { return $self; } -sub _read { - my $self = shift; - - my $read = $self->{handle}->sysread(my $buffer, 131072, 0); - unless (defined $read) { +sub _again { $_[0]->reactor->again($_[0]{timer}) if $_[0]{timer} } - # Retry - return if grep { $_ == $! } EAGAIN, EINTR, EWOULDBLOCK; - - # Closed - return $self->close if grep { $_ == $! } ECONNRESET, EPIPE; +sub _error { + my $self = shift; - # Read error - return $self->emit_safe(error => $!)->close; - } + # Retry + return if $! == EAGAIN || $! == EINTR || $! == EWOULDBLOCK; - # EOF - return $self->close if $read == 0; + # Closed + return $self->close if $! == ECONNRESET || $! == EPIPE; - $self->emit_safe(read => $buffer)->{active} = time; + # Error + $self->emit_safe(error => $!)->close; } -sub _startup { +sub _read { my $self = shift; - - # Timeout (ignore 0 timeout) - my $reactor = $self->reactor; - weaken $self; - $self->{timer} = $reactor->recurring( - 0.5 => sub { - return unless my $t = $self->timeout; - $self->emit_safe('timeout')->close if (time - $self->{active}) >= $t; - } - ); - - $reactor->io($self->{handle}, sub { pop() ? $self->_write : $self->_read }); + my $read = $self->{handle}->sysread(my $buffer, 131072, 0); + return $self->_error unless defined $read; + return $self->close if $read == 0; + $self->emit_safe(read => $buffer)->_again; } sub _write { @@ -117,29 +124,21 @@ sub _write { my $handle = $self->{handle}; if (length $self->{buffer}) { my $written = $handle->syswrite($self->{buffer}); - unless (defined $written) { - - # Retry - return if grep { $_ == $! } EAGAIN, EINTR, EWOULDBLOCK; - - # Closed - return $self->close if grep { $_ == $! } ECONNRESET, EPIPE; - - # Write error - return $self->emit_safe(error => $!)->close; - } - + return $self->_error unless defined $written; $self->emit_safe(write => substr($self->{buffer}, 0, $written, '')); - $self->{active} = time; + $self->_again; } - $self->emit_safe('drain') if !length $self->{buffer}; + $self->emit_safe('drain') unless length $self->{buffer}; return if $self->is_writing; - $self->reactor->watch($handle, !$self->{paused}, 0); + return $self->close if $self->{graceful}; + $self->reactor->watch($handle, !$self->{paused}, 0) if $self->{handle}; } 1; +=encoding utf8 + =head1 NAME Mojo::IOLoop::Stream - Non-blocking I/O stream @@ -247,15 +246,6 @@ L implements the following attributes. Low level event reactor, defaults to the C attribute value of the global L singleton. -=head2 timeout - - my $timeout = $stream->timeout; - $stream = $stream->timeout(45); - -Maximum amount of time in seconds stream can be inactive before getting closed -automatically, defaults to C<15>. Setting the value to C<0> will allow this -stream to be inactive indefinitely. - =head1 METHODS L inherits all methods from L and @@ -273,6 +263,12 @@ Construct a new L object. Close stream immediately. +=head2 close_gracefully + + $stream->close_gracefully; + +Close stream gracefully. + =head2 handle my $handle = $stream->handle; @@ -310,6 +306,15 @@ Stop watching for new data on the stream. Steal handle from stream and prevent it from getting closed automatically. +=head2 timeout + + my $timeout = $stream->timeout; + $stream = $stream->timeout(45); + +Maximum amount of time in seconds stream can be inactive before getting closed +automatically, defaults to C<15>. Setting the value to C<0> will allow this +stream to be inactive indefinitely. + =head2 write $stream = $stream->write($bytes); diff --git a/lib/Mojo/JSON.pm b/lib/Mojo/JSON.pm index f08d17a..42cae14 100644 --- a/lib/Mojo/JSON.pm +++ b/lib/Mojo/JSON.pm @@ -20,22 +20,22 @@ my %ESCAPE = ( '\\' => '\\', '/' => '/', 'b' => "\x07", - 'f' => "\x0C", - 'n' => "\x0A", - 'r' => "\x0D", + 'f' => "\x0c", + 'n' => "\x0a", + 'r' => "\x0d", 't' => "\x09", 'u2028' => "\x{2028}", 'u2029' => "\x{2029}" ); my %REVERSE = map { $ESCAPE{$_} => "\\$_" } keys %ESCAPE; -for (0x00 .. 0x1F, 0x7F) { $REVERSE{pack 'C', $_} = do {my $tmp = $REVERSE{pack 'C', $_}; defined $tmp ? $tmp : sprintf '\u%.4X', $_} } +for (0x00 .. 0x1f, 0x7f) { $REVERSE{pack 'C', $_} = do { my $tmp = $REVERSE{pack 'C', $_}; defined $tmp ? $tmp : sprintf '\u%.4X', $_ } } # Unicode encoding detection my $UTF_PATTERNS = { - 'UTF-32BE' => qr/^\0\0\0[^\0]/, - 'UTF-16BE' => qr/^\0[^\0]\0[^\0]/, - 'UTF-32LE' => qr/^[^\0]\0\0\0/, - 'UTF-16LE' => qr/^[^\0]\0[^\0]\0/ + 'UTF-32BE' => qr/^\x00{3}[^\x00]/, + 'UTF-32LE' => qr/^[^\x00]\x00{3}/, + 'UTF-16BE' => qr/^(?:\x00[^\x00]){2}/, + 'UTF-16LE' => qr/^(?:[^\x00]\x00){2}/ }; my $WHITESPACE_RE = qr/[\x20\x09\x0a\x0d]*/; @@ -98,7 +98,7 @@ sub decode { sub encode { my ($self, $ref) = @_; - return Mojo::Util::encode 'UTF-8', _encode_values($ref); + return Mojo::Util::encode 'UTF-8', _encode_value($ref); } sub false {$FALSE} @@ -166,13 +166,13 @@ sub _decode_string { my $pos = pos; # Extract string with escaped characters - m#\G(((?:[^\x00-\x1F\\"]|\\(?:["\\/bfnrt]|u[[:xdigit:]]{4})){0,32766})*)#gc; + m!\G((?:(?:[^\x00-\x1f\\"]|\\(?:["\\/bfnrt]|u[0-9a-fA-F]{4})){0,32766})*)!gc; my $str = $1; # Missing quote unless (m/\G"/gc) { _exception('Unexpected character or invalid escape while parsing string') - if m/\G[\x00-\x1F\\]/; + if m/\G[\x00-\x1f\\]/; _exception('Unterminated string'); } @@ -195,18 +195,17 @@ sub _decode_string { my $ord = hex $3; # Surrogate pair - if (($ord & 0xF800) == 0xD800) { + if (($ord & 0xf800) == 0xd800) { # High surrogate - ($ord & 0xFC00) == 0xD800 + ($ord & 0xfc00) == 0xd800 or pos($_) = $pos + pos($str), _exception('Missing high-surrogate'); # Low surrogate $str =~ m/\G\\u([Dd][C-Fc-f]..)/gc or pos($_) = $pos + pos($str), _exception('Missing low-surrogate'); - # Pair - $ord = 0x10000 + ($ord - 0xD800) * 0x400 + (hex($1) - 0xDC00); + $ord = 0x10000 + ($ord - 0xd800) * 0x400 + (hex($1) - 0xdc00); } # Character @@ -251,23 +250,23 @@ sub _decode_value { sub _encode_array { my $array = shift; - return '[' . join(',', map { _encode_values($_) } @$array) . ']'; + return '[' . join(',', map { _encode_value($_) } @$array) . ']'; } sub _encode_object { my $object = shift; - my @pairs = map { _encode_string($_) . ':' . _encode_values($object->{$_}) } + my @pairs = map { _encode_string($_) . ':' . _encode_value($object->{$_}) } keys %$object; return '{' . join(',', @pairs) . '}'; } sub _encode_string { - my $string = shift; - $string =~ s!([\x00-\x1F\x7F\x{2028}\x{2029}\\"/\b\f\n\r\t])!$REVERSE{$1}!gs; - return "\"$string\""; + my $str = shift; + $str =~ s!([\x00-\x1f\x7f\x{2028}\x{2029}\\"/\b\f\n\r\t])!$REVERSE{$1}!gs; + return "\"$str\""; } -sub _encode_values { +sub _encode_value { my $value = shift; # Reference @@ -285,7 +284,7 @@ sub _encode_values { # Blessed reference with TO_JSON method if (blessed $value && (my $sub = $value->can('TO_JSON'))) { - return _encode_values($value->$sub); + return _encode_value($value->$sub); } } @@ -294,8 +293,7 @@ sub _encode_values { # Number my $flags = B::svref_2object(\$value)->FLAGS; - return $value - if $flags & (B::SVp_IOK | B::SVp_NOK) && !($flags & B::SVp_POK); + return 0 + $value if $flags & (B::SVp_IOK | B::SVp_NOK) && $value * 0 == 0; # String return _encode_string($value); @@ -323,6 +321,8 @@ use overload '0+' => sub { ${$_[0]} }, '""' => sub { ${$_[0]} }, fallback => 1; 1; +=encoding utf8 + =head1 NAME Mojo::JSON - Minimalistic JSON @@ -353,7 +353,11 @@ it for validation. It supports normal Perl data types like C, C reference, C reference and will try to call the C method on blessed references, or -stringify them if it doesn't exist. +stringify them if it doesn't exist. Differentiating between strings and +numbers in Perl is hard, depending on how it has been used, a C can be +both at the same time. Since numeric comparisons on strings are very unlikely +to happen intentionally, the numeric value always gets priority, so any +C that has been used in numeric context is considered a number. [1, -2, 3] -> [1, -2, 3] {"foo": "bar"} -> {foo => 'bar'} diff --git a/lib/Mojo/JSON/Pointer.pm b/lib/Mojo/JSON/Pointer.pm index 676140d..964df11 100644 --- a/lib/Mojo/JSON/Pointer.pm +++ b/lib/Mojo/JSON/Pointer.pm @@ -1,7 +1,6 @@ package Mojo::JSON::Pointer; use Mojo::Base -base; -use Mojo::Util qw(decode url_unescape); use Scalar::Util 'looks_like_number'; sub contains { shift->_pointer(1, @_) } @@ -10,9 +9,8 @@ sub get { shift->_pointer(0, @_) } sub _pointer { my ($self, $contains, $data, $pointer) = @_; - $pointer = decode('UTF-8', url_unescape $pointer); return $data unless $pointer =~ s!^/!!; - for my $p (split '/', $pointer) { + for my $p ($pointer eq '' ? ($pointer) : (split '/', $pointer)) { $p =~ s/~0/~/g; $p =~ s!~1!/!g; @@ -33,6 +31,8 @@ sub _pointer { 1; +=encoding utf8 + =head1 NAME Mojo::JSON::Pointer - JSON Pointers @@ -47,7 +47,7 @@ Mojo::JSON::Pointer - JSON Pointers =head1 DESCRIPTION -L implements JSON Pointers. +L is a relaxed implementation of RFC 6901. =head1 METHODS @@ -59,10 +59,12 @@ Check if data structure contains a value that can be identified with the given JSON Pointer. # True + $pointer->contains({'♥' => 'mojolicious'}, '/♥'); $pointer->contains({foo => 'bar', baz => [4, 5, 6]}, '/foo'); $pointer->contains({foo => 'bar', baz => [4, 5, 6]}, '/baz/2'); # False + $pointer->contains({'♥' => 'mojolicious'}, '/☃'); $pointer->contains({foo => 'bar', baz => [4, 5, 6]}, '/bar'); $pointer->contains({foo => 'bar', baz => [4, 5, 6]}, '/baz/9'); @@ -72,6 +74,9 @@ JSON Pointer. Extract value identified by the given JSON Pointer. + # "mojolicious" + $pointer->get({'♥' => 'mojolicious'}, '/♥'); + # "bar" $pointer->get({foo => 'bar', baz => [4, 5, 6]}, '/foo'); diff --git a/lib/Mojo/Loader.pm b/lib/Mojo/Loader.pm index 2b3aeca..a854e61 100644 --- a/lib/Mojo/Loader.pm +++ b/lib/Mojo/Loader.pm @@ -82,6 +82,8 @@ sub _all { 1; +=encoding utf8 + =head1 NAME Mojo::Loader - Loader @@ -128,7 +130,7 @@ Load a class and catch exceptions. Note that classes are checked for a C method to see if they are already loaded. if (my $e = $loader->load('Foo::Bar')) { - die ref $e ? "Exception: $e" : 'Already loaded!'; + die ref $e ? "Exception: $e" : 'Not found!'; } =head2 search diff --git a/lib/Mojo/Log.pm b/lib/Mojo/Log.pm index bb33ffe..30b5dc0 100644 --- a/lib/Mojo/Log.pm +++ b/lib/Mojo/Log.pm @@ -3,18 +3,18 @@ use Mojo::Base 'Mojo::EventEmitter'; use Carp 'croak'; use Fcntl ':flock'; +use Mojo::Util 'encode'; has handle => sub { # File if (my $path = shift->path) { croak qq{Can't open log file "$path": $!} - unless open my $file, '>>:utf8', $path; + unless open my $file, '>>', $path; return $file; } # STDERR - binmode STDERR, ':utf8'; return \*STDERR; }; has level => 'debug'; @@ -35,7 +35,8 @@ sub fatal { shift->log(fatal => @_) } sub format { my ($self, $level, @lines) = @_; - return '[' . localtime(time) . "] [$level] " . join("\n", @lines) . "\n"; + return encode 'UTF-8', + '[' . localtime(time) . "] [$level] " . join("\n", @lines, ''); } sub info { shift->log(info => @_) } @@ -57,18 +58,19 @@ sub log { shift->emit('message', lc(shift), @_) } sub warn { shift->log(warn => @_) } sub _message { - my ($self, $level, @lines) = @_; + my ($self, $level) = (shift, shift); return unless $self->is_level($level) && (my $handle = $self->handle); flock $handle, LOCK_EX; - croak "Can't write to log: $!" - unless defined $handle->syswrite($self->format($level, @lines)); + $handle->print($self->format($level, @_)) or croak "Can't write to log: $!"; flock $handle, LOCK_UN; } 1; +=encoding utf8 + =head1 NAME Mojo::Log - Simple logger @@ -123,7 +125,7 @@ L implements the following attributes. my $handle = $log->handle; $log = $log->handle(IO::Handle->new); -Log file handle used by default C event, defaults to opening C +Log filehandle used by default C event, defaults to opening C or C. =head2 level @@ -131,8 +133,8 @@ or C. my $level = $log->level; $log = $log->level('debug'); -Active log level, defaults to the value of the C environment -variable or C. +Active log level, defaults to C. Note that the MOJO_LOG_LEVEL +environment variable can override this value. These levels are currently available: @@ -171,32 +173,36 @@ default logger. =head2 debug - $log = $log->debug('You screwed up, but that is ok'); + $log = $log->debug('You screwed up, but that is ok.'); + $log = $log->debug('All', 'cool!'); Log debug message. =head2 error - $log = $log->error('You really screwed up this time'); + $log = $log->error('You really screwed up this time.'); + $log = $log->error('Wow', 'seriously!'); Log error message. =head2 fatal $log = $log->fatal('Its over...'); + $log = $log->fatal('Bye', 'bye!'); Log fatal message. =head2 format - my $msg = $log->format('debug', 'Hi there!'); - my $msg = $log->format('debug', 'Hi', 'there!'); + my $msg = $log->format(debug => 'Hi there!'); + my $msg = $log->format(debug => 'Hi', 'there!'); Format log message. =head2 info - $log = $log->info('You are bad, but you prolly know already'); + $log = $log->info('You are bad, but you prolly know already.'); + $log = $log->info('Ok', 'then!'); Log info message. @@ -238,13 +244,15 @@ Check for warn log level. =head2 log - $log = $log->log(debug => 'This should work'); + $log = $log->log(debug => 'This should work.'); + $log = $log->log(debug => 'This', 'too!'); Emit C event. =head2 warn $log = $log->warn('Dont do that Dave...'); + $log = $log->warn('No', 'really!'); Log warn message. diff --git a/lib/Mojo/Message.pm b/lib/Mojo/Message.pm index f988d8e..c78affd 100644 --- a/lib/Mojo/Message.pm +++ b/lib/Mojo/Message.pm @@ -10,32 +10,26 @@ use Mojo::JSON::Pointer; use Mojo::Parameters; use Mojo::Upload; use Mojo::Util 'decode'; -use Scalar::Util 'weaken'; has content => sub { Mojo::Content::Single->new }; has default_charset => 'UTF-8'; has max_line_size => sub { $ENV{MOJO_MAX_LINE_SIZE} || 10240 }; -has max_message_size => sub { $ENV{MOJO_MAX_MESSAGE_SIZE} || 5242880 }; +has max_message_size => sub { $ENV{MOJO_MAX_MESSAGE_SIZE} || 10485760 }; has version => '1.1'; sub body { my $self = shift; # Downgrade multipart content - $self->content(Mojo::Content::Single->new) if $self->content->is_multipart; my $content = $self->content; + $content = $self->content(Mojo::Content::Single->new)->content + if $content->is_multipart; # Get - return $content->asset->slurp unless defined(my $new = shift); - - # Callback - if (ref $new eq 'CODE') { - weaken $self; - return $content->unsubscribe('read')->on(read => sub { $self->$new(pop) }); - } + return $content->asset->slurp unless @_; # Set raw content - else { $content->asset(Mojo::Asset::Memory->new->add_chunk($new)) } + $content->asset(Mojo::Asset::Memory->new->add_chunk(@_)); return $self; } @@ -47,20 +41,15 @@ sub body_params { my $params = $self->{body_params} = Mojo::Parameters->new; $params->charset($self->content->charset || $self->default_charset); - # "x-application-urlencoded" and "application/x-www-form-urlencoded" - my $type = $self->headers->content_type || ''; - if ($type =~ m!(?:x-application|application/x-www-form)-urlencoded!i) { + # "application/x-www-form-urlencoded" + my $type = defined $self->headers->content_type ? $self->headers->content_type : ''; + if ($type =~ m!application/x-www-form-urlencoded!i) { $params->parse($self->content->asset->slurp); } - # "multipart/formdata" + # "multipart/form-data" elsif ($type =~ m!multipart/form-data!i) { - my $formdata = $self->_parse_formdata; - for my $data (@$formdata) { - my ($name, $filename, $value) = @$data; - next if defined $filename; - $params->append($name, $value); - } + $params->append(@$_[0, 1]) for @{$self->_parse_formdata}; } return $params; @@ -72,23 +61,18 @@ sub build_body { shift->_build('get_body_chunk') } sub build_headers { shift->_build('get_header_chunk') } sub build_start_line { shift->_build('get_start_line_chunk') } -sub cookie { - my ($self, $name) = @_; - $self->{cookies} ||= _nest($self->cookies); - return unless my $cookies = $self->{cookies}{$name}; - my @cookies = ref $cookies eq 'ARRAY' ? @$cookies : ($cookies); - return wantarray ? @cookies : $cookies[0]; -} +sub cookie { shift->_cache(cookies => @_) } sub cookies { croak 'Method "cookies" not implemented by subclass' } sub dom { my $self = shift; - return undef if $self->is_multipart; - my $dom = $self->{dom} - ||= Mojo::DOM->new->charset(defined $self->content->charset ? $self->content->charset : undef) - ->parse($self->body); + return undef if $self->content->is_multipart; + my $html = $self->body; + my $charset = $self->content->charset; + $html = do { my $tmp = decode($charset, $html); defined $tmp ? $tmp : $html } if $charset; + my $dom = $self->{dom} ||= Mojo::DOM->new($html); return @_ ? $dom->find(@_) : $dom; } @@ -121,9 +105,10 @@ sub fix_headers { my $self = shift; # Content-Length or Connection (unless chunked transfer encoding is used) - return $self if $self->{fix}++ || $self->is_chunked; + my $content = $self->content; + return $self if $self->{fix}++ || $content->is_chunked; my $headers = $self->headers; - $self->is_dynamic + $content->is_dynamic ? $headers->connection('close') : $headers->content_length($self->body_size) unless $headers->content_length; @@ -152,39 +137,28 @@ sub get_start_line_chunk { croak 'Method "get_start_line_chunk" not implemented by subclass'; } -sub has_leftovers { shift->content->has_leftovers } - sub header_size { shift->fix_headers->content->header_size } -sub headers { shift->content->headers } -sub is_chunked { shift->content->is_chunked } -sub is_dynamic { shift->content->is_dynamic } +sub headers { shift->content->headers } -sub is_finished { do { my $tmp = shift->{state}; defined $tmp ? $tmp : ''} eq 'finished' } +sub is_finished { do { my $tmp = shift->{state}; defined $tmp ? $tmp : '' } eq 'finished' } -sub is_limit_exceeded { - return undef unless my $code = (shift->error)[1]; - return !!grep { $_ eq $code } 413, 431; -} - -sub is_multipart { shift->content->is_multipart } +sub is_limit_exceeded { !!shift->{limit} } sub json { my ($self, $pointer) = @_; - return undef if $self->is_multipart; + return undef if $self->content->is_multipart; my $data = $self->{json} ||= Mojo::JSON->new->decode($self->body); return $pointer ? Mojo::JSON::Pointer->new->get($data, $pointer) : $data; } -sub leftovers { shift->content->leftovers } - sub param { shift->body_params->param(@_) } sub parse { my ($self, $chunk) = @_; # Check message size - return $self->error('Maximum message size exceeded', 413) + return $self->_limit('Maximum message size exceeded', 413) if ($self->{raw_size} += length($chunk = defined $chunk ? $chunk : '')) > $self->max_message_size; $self->{buffer} .= $chunk; @@ -195,18 +169,19 @@ sub parse { # Check line size my $len = index $self->{buffer}, "\x0a"; $len = length $self->{buffer} if $len < 0; - return $self->error('Maximum line size exceeded', 431) + return $self->_limit('Maximum line size exceeded', 431) if $len > $self->max_line_size; $self->{state} = 'content' if $self->extract_start_line(\$self->{buffer}); } # Content + my $state = defined $self->{state} ? $self->{state} : ''; $self->content($self->content->parse(delete $self->{buffer})) - if grep { $_ eq (defined $self->{state} ? $self->{state} : '') } qw(content finished); + if $state eq 'content' || $state eq 'finished'; # Check line size - return $self->error('Maximum line size exceeded', 431) + return $self->_limit('Maximum line size exceeded', 431) if $self->headers->is_limit_exceeded; # Check buffer size @@ -223,31 +198,18 @@ sub to_string { return $self->build_start_line . $self->build_headers . $self->build_body; } -sub upload { - my ($self, $name) = @_; - $self->{uploads} ||= _nest($self->uploads); - return unless my $uploads = $self->{uploads}{$name}; - my @uploads = ref $uploads eq 'ARRAY' ? @$uploads : ($uploads); - return wantarray ? @uploads : $uploads[0]; -} +sub upload { shift->_cache(uploads => @_) } sub uploads { my $self = shift; my @uploads; - my $formdata = $self->_parse_formdata; - for my $data (@$formdata) { - my ($name, $filename, $part) = @$data; - - # Just a form value - next unless defined $filename; - - # Uploaded file + for my $data (@{$self->_parse_formdata(1)}) { my $upload = Mojo::Upload->new( - name => $name, - asset => $part->asset, - filename => $filename, - headers => $part->headers + name => $data->[0], + filename => $data->[2], + asset => $data->[1]->asset, + headers => $data->[1]->headers ); push @uploads, $upload; } @@ -255,9 +217,6 @@ sub uploads { return \@uploads; } -sub write { shift->_write(write => @_) } -sub write_chunk { shift->_write(write_chunk => @_) } - sub _build { my ($self, $method) = @_; @@ -278,79 +237,66 @@ sub _build { return $buffer; } -sub _nest { - my $array = shift; - - # Turn array of objects into hash - my $hash = {}; - for my $object (@$array) { - my $name = $object->name; - - # Multiple objects with same name - if (exists $hash->{$name}) { - $hash->{$name} = [$hash->{$name}] unless ref $hash->{$name} eq 'ARRAY'; - push @{$hash->{$name}}, $object; - } +sub _cache { + my ($self, $method, $name) = @_; - # Single object - else { $hash->{$name} = $object } + # Cache objects by name + unless ($self->{$method}) { + $self->{$method} = {}; + push @{$self->{$method}{$_->name}}, $_ for @{$self->$method}; } - return $hash; + return unless my $objects = $self->{$method}{$name}; + return wantarray ? @$objects : $objects->[0]; } -sub _parse_formdata { +sub _limit { my $self = shift; + $self->{limit} = 1; + $self->error(@_); +} + +sub _parse_formdata { + my ($self, $upload) = @_; - # Check for multipart content my @formdata; my $content = $self->content; return \@formdata unless $content->is_multipart; my $charset = $content->charset || $self->default_charset; - # Check all parts for form data - my @parts; - push @parts, $content; + # Check all parts recursively + my @parts = ($content); while (my $part = shift @parts) { - # Nested multipart content if ($part->is_multipart) { unshift @parts, @{$part->parts}; next; } - # Extract information from Content-Disposition header - my $disposition = $part->headers->content_disposition; - next unless $disposition; - my ($name) = $disposition =~ /[; ]name="?([^";]+)"?/; - my ($filename) = $disposition =~ /[; ]filename="?([^"]*)"?/; + next unless my $disposition = $part->headers->content_disposition; + my ($filename) = $disposition =~ /[; ]filename\s*=\s*"?([^"]*)"?/; + next if ($upload && !defined $filename) || (!$upload && defined $filename); + my ($name) = $disposition =~ /[; ]name\s*=\s*"?([^";]+)"?/; if ($charset) { - $name = do {my $tmp = decode($charset, $name); defined $tmp ? $tmp : $name} if $name; - $filename = do {my $tmp = decode($charset, $filename); defined $tmp ? $tmp : $filename} if $filename; + $name = do { my $tmp = decode($charset, $name); defined $tmp ? $tmp : $name } if $name; + $filename = do { my $tmp = decode($charset, $filename); defined $tmp ? $tmp : $filename } if $filename; } - # Check for file upload - my $value = $part; - unless (defined $filename) { - $value = $part->asset->slurp; - $value = do {my $tmp = decode($charset, $value); defined $tmp ? $tmp : $value} if $charset; + unless ($upload) { + $part = $part->asset->slurp; + $part = do { my $tmp = decode($charset, $part); defined $tmp ? $tmp : $part } if $charset; } - push @formdata, [$name, $filename, $value]; + push @formdata, [$name, $part, $filename]; } return \@formdata; } -sub _write { - my ($self, $method, $chunk, $cb) = @_; - weaken $self; - $self->content->$method($chunk => sub { shift and $self->$cb(@_) if $cb }); - return $self; -} - 1; +=encoding utf8 + =head1 NAME Mojo::Message - HTTP message base class @@ -428,7 +374,7 @@ Message content, defaults to a L object. my $charset = $msg->default_charset; $msg = $msg->default_charset('UTF-8'); -Default charset used for form data parsing, defaults to C. +Default charset used for form-data parsing, defaults to C. =head2 max_line_size @@ -436,7 +382,7 @@ Default charset used for form data parsing, defaults to C. $msg = $msg->max_line_size(1024); Maximum start line size in bytes, defaults to the value of the -C environment variable or C<10240>. +MOJO_MAX_LINE_SIZE environment variable or C<10240>. =head2 max_message_size @@ -444,7 +390,7 @@ C environment variable or C<10240>. $msg = $msg->max_message_size(1024); Maximum message size in bytes, defaults to the value of the -C environment variable or C<5242880>. Note that +MOJO_MAX_MESSAGE_SIZE environment variable or C<10485760>. Note that increasing this value can also drastically increase memory usage, should you for example attempt to parse an excessively large message body with the C, C or C methods. @@ -465,23 +411,19 @@ implements the following new ones. my $bytes = $msg->body; $msg = $msg->body('Hello!'); - my $cb = $msg->body(sub {...}); -Access C data or replace all subscribers of the C event. - - $msg->body(sub { - my ($msg, $bytes) = @_; - say "Streaming: $bytes"; - }); +Slurp or replace C. =head2 body_params my $params = $msg->body_params; -C parameters extracted from C, -C or C message body, -usually a L object. Note that this method caches all data, -so it should not be called before the entire message body has been received. +POST parameters extracted from C or +C message body, usually a L object. +Note that this method caches all data, so it should not be called before the +entire message body has been received. Parts of the message body need to be +loaded into memory to parse POST parameters, so you have to make sure it is +not excessively large. # Get POST parameter value say $msg->body_params->param('foo'); @@ -536,14 +478,16 @@ Access message cookies. Meant to be overloaded in a subclass. Turns message body into a L object and takes an optional selector to perform a C on it right away, which returns a L object. Note that this method caches all data, so it should not be called -before the entire message body has been received. +before the entire message body has been received. The whole message body needs +to be loaded into memory to parse it, so you have to make sure it is not +excessively large. # Perform "find" right away - say $msg->dom('h1, h2, h3')->pluck('text'); + say $msg->dom('h1, h2, h3')->text; # Use everything else Mojo::DOM has to offer say $msg->dom->at('title')->text; - say $msg->dom->html->body->children->pluck('type')->uniq; + say $msg->dom->html->body->children->type->uniq; =head2 error @@ -556,7 +500,7 @@ Error and code. =head2 extract_start_line - my $success = $msg->extract_start_line(\$string); + my $success = $msg->extract_start_line(\$str); Extract start line from string. Meant to be overloaded in a subclass. @@ -591,12 +535,6 @@ Get a chunk of header data, starting from a specific position. Get a chunk of start line data starting from a specific position. Meant to be overloaded in a subclass. -=head2 has_leftovers - - my $success = $msg->has_leftovers; - -Check if there are leftovers. - =head2 header_size my $size = $msg->header_size; @@ -609,19 +547,6 @@ Size of headers in bytes. Message headers, usually a L object. -=head2 is_chunked - - my $success = $msg->is_chunked; - -Check if content is chunked. - -=head2 is_dynamic - - my $success = $msg->is_dynamic; - -Check if content will be dynamically generated, which prevents C from -working. - =head2 is_finished my $success = $msg->is_finished; @@ -634,12 +559,6 @@ Check if message parser/generator is finished. Check if message has exceeded C or C. -=head2 is_multipart - - my $success = $msg->is_multipart; - -Check if content is a L object. - =head2 json my $hash = $msg->json; @@ -650,25 +569,23 @@ Decode JSON message body directly using L if possible, returns C otherwise. An optional JSON Pointer can be used to extract a specific value with L. Note that this method caches all data, so it should not be called before the entire message body has been received. +The whole message body needs to be loaded into memory to parse it, so you have +to make sure it is not excessively large. # Extract JSON values say $msg->json->{foo}{bar}[23]; say $msg->json('/foo/bar/23'); -=head2 leftovers - - my $bytes = $msg->leftovers; - -Get leftover data from content parser. - =head2 param my @names = $msg->param; my $foo = $msg->param('foo'); my @foo = $msg->param('foo'); -Access C parameters. Note that this method caches all data, so it should -not be called before the entire message body has been received. +Access POST parameters. Note that this method caches all data, so it should +not be called before the entire message body has been received. Parts of the +message body need to be loaded into memory to parse POST parameters, so you +have to make sure it is not excessively large. =head2 parse @@ -684,7 +601,7 @@ Size of the start line in bytes. =head2 to_string - my $string = $msg->to_string; + my $str = $msg->to_string; Render whole message. @@ -706,22 +623,6 @@ entire message body has been received. All C file uploads, usually L objects. -=head2 write - - $msg = $msg->write($bytes); - $msg = $msg->write($bytes => sub {...}); - -Write dynamic content non-blocking, the optional drain callback will be -invoked once all data has been written. - -=head2 write_chunk - - $msg = $msg->write_chunk($bytes); - $msg = $msg->write_chunk($bytes => sub {...}); - -Write dynamic content non-blocking with C transfer encoding, the -optional drain callback will be invoked once all data has been written. - =head1 SEE ALSO L, L, L. diff --git a/lib/Mojo/Message/Request.pm b/lib/Mojo/Message/Request.pm index ca652c3..193900d 100644 --- a/lib/Mojo/Message/Request.pm +++ b/lib/Mojo/Message/Request.pm @@ -2,7 +2,6 @@ package Mojo::Message::Request; use Mojo::Base 'Mojo::Message'; use Mojo::Cookie::Request; -use Mojo::Parameters; use Mojo::Util qw(b64_encode b64_decode get_line); use Mojo::URL; @@ -10,14 +9,14 @@ has env => sub { {} }; has method => 'GET'; has url => sub { Mojo::URL->new }; -my $START_LINE_RE = qr| - ^\s* - ([a-zA-Z]+) # Method +my $START_LINE_RE = qr/ + ^ + ([a-zA-Z]+) # Method \s+ - ([0-9a-zA-Z\-._~:/?#[\]\@!\$&'()*+,;=\%]+) # Path - (?:\s+HTTP/(\d\.\d))? # Version + ([0-9a-zA-Z!#\$\%&'()*+,\-.\/:;=?\@[\\\]^_`\{|\}~]+) # URL + (?:\s+HTTP\/(\d\.\d))? # Version $ -|x; +/x; sub clone { my $self = shift; @@ -79,11 +78,8 @@ sub fix_headers { $headers->authorization('Basic ' . b64_encode($auth, '')) if $auth && !$headers->authorization; - # Proxy + # Basic proxy authentication if (my $proxy = $self->proxy) { - $url = $proxy if $self->method eq 'CONNECT'; - - # Basic proxy authentication my $proxy_auth = $proxy->userinfo; $headers->proxy_authorization('Basic ' . b64_encode($proxy_auth, '')) if $proxy_auth && !$headers->proxy_authorization; @@ -119,7 +115,7 @@ sub get_start_line_chunk { # Proxy elsif ($self->proxy) { my $clone = $url = $url->clone->userinfo(undef); - my $upgrade = lc($self->headers->upgrade || ''); + my $upgrade = lc(defined $self->headers->upgrade ? $self->headers->upgrade : ''); $path = $clone unless $upgrade eq 'websocket' || $url->protocol eq 'https'; } @@ -137,7 +133,7 @@ sub is_secure { } sub is_xhr { - (shift->headers->header('X-Requested-With') || '') =~ /XMLHttpRequest/i; + (do {my $tmp = shift->headers->header('X-Requested-With'); defined $tmp ? $tmp : ''}) =~ /XMLHttpRequest/i; } sub param { shift->params->param(@_) } @@ -145,7 +141,7 @@ sub param { shift->params->param(@_) } sub params { my $self = shift; return $self->{params} - ||= Mojo::Parameters->new->merge($self->body_params, $self->query_params); + ||= $self->body_params->clone->merge($self->query_params); } sub parse { @@ -233,7 +229,7 @@ sub _parse_env { $self->method($env->{REQUEST_METHOD}) if $env->{REQUEST_METHOD}; # Scheme/Version - if (($env->{SERVER_PROTOCOL} || '') =~ m!^([^/]+)/([^/]+)$!) { + if ((defined $env->{SERVER_PROTOCOL} ? $env->{SERVER_PROTOCOL} : '') =~ m!^([^/]+)/([^/]+)$!) { $base->scheme($1); $self->version($2); } @@ -264,6 +260,8 @@ sub _parse_env { 1; +=encoding utf8 + =head1 NAME Mojo::Message::Request - HTTP request @@ -291,7 +289,7 @@ Mojo::Message::Request - HTTP request =head1 DESCRIPTION L is a container for HTTP requests as described in RFC -2616. +2616 and RFC 2817. =head1 EVENTS @@ -329,8 +327,10 @@ HTTP request method, defaults to C. HTTP request URL, defaults to a L object. - # Get request path - say $req->url->path; + # Get request information + say $req->url->to_abs->userinfo; + say $req->url->to_abs->host; + say $req->url->to_abs->path; =head1 METHODS @@ -353,7 +353,7 @@ Access request cookies, usually L objects. =head2 extract_start_line - my $success = $req->extract_start_line(\$string); + my $success = $req->extract_start_line(\$str); Extract request line from string. @@ -387,16 +387,20 @@ Check C header for C value. my $foo = $req->param('foo'); my @foo = $req->param('foo'); -Access C and C parameters. Note that this method caches all data, -so it should not be called before the entire request body has been received. +Access GET and POST parameters. Note that this method caches all data, so it +should not be called before the entire request body has been received. Parts +of the request body need to be loaded into memory to parse POST parameters, so +you have to make sure it is not excessively large. =head2 params my $params = $req->params; -All C and C parameters, usually a L object. Note -that this method caches all data, so it should not be called before the entire -request body has been received. +All GET and POST parameters, usually a L object. Note that +this method caches all data, so it should not be called before the entire +request body has been received. Parts of the request body need to be loaded +into memory to parse POST parameters, so you have to make sure it is not +excessively large. # Get parameter value say $req->params->param('foo'); @@ -424,7 +428,7 @@ Proxy URL for request. my $params = $req->query_params; -All C parameters, usually a L object. +All GET parameters, usually a L object. # Turn GET parameters to hash and extract value say $req->query_params->to_hash->{foo}; diff --git a/lib/Mojo/Message/Response.pm b/lib/Mojo/Message/Response.pm index 9f0ceb7..3b934a8 100644 --- a/lib/Mojo/Message/Response.pm +++ b/lib/Mojo/Message/Response.pm @@ -129,7 +129,7 @@ sub get_start_line_chunk { sub is_empty { my $self = shift; return undef unless my $code = $self->code; - return $self->is_status_class(100) || grep { $_ eq $code } qw(204 304); + return $self->is_status_class(100) || $code eq 204 || $code eq 304; } sub is_status_class { @@ -140,6 +140,8 @@ sub is_status_class { 1; +=encoding utf8 + =head1 NAME Mojo::Message::Response - HTTP response @@ -149,7 +151,7 @@ Mojo::Message::Response - HTTP response use Mojo::Message::Response; # Parse - my $res = Mojo::Message::Reponse->new; + my $res = Mojo::Message::Response->new; $res->parse("HTTP/1.0 200 OK\x0a\x0d"); $res->parse("Content-Length: 12\x0a\x0d\x0a\x0d"); $res->parse("Content-Type: text/plain\x0a\x0d\x0a\x0d"); @@ -214,7 +216,7 @@ Generate default response message for code. =head2 extract_start_line - my $success = $req->extract_start_line(\$string); + my $success = $res->extract_start_line(\$str); Extract status line from string. diff --git a/lib/Mojo/Parameters.pm b/lib/Mojo/Parameters.pm index 1bc5017..49461fb 100644 --- a/lib/Mojo/Parameters.pm +++ b/lib/Mojo/Parameters.pm @@ -1,14 +1,14 @@ package Mojo::Parameters; use Mojo::Base -base; use overload - 'bool' => sub {1}, + '@{}' => sub { shift->params }, + bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1; use Mojo::Util qw(decode encode url_escape url_unescape); -has charset => 'UTF-8'; -has pair_separator => '&'; +has charset => 'UTF-8'; sub new { shift->SUPER::new->parse(@_) } @@ -33,8 +33,7 @@ sub append { sub clone { my $self = shift; - my $clone = Mojo::Parameters->new->charset($self->charset) - ->pair_separator($self->pair_separator); + my $clone = $self->new->charset($self->charset); if (defined $self->{string}) { $clone->{string} = $self->{string} } else { $clone->params([@{$self->params}]) } @@ -78,18 +77,15 @@ sub params { } # Parse string - if (defined(my $string = delete $self->{string})) { + if (defined(my $str = delete $self->{string})) { my $params = $self->{params} = []; - - # Detect pair separator for reconstruction - return $params unless length(defined $string ? $string : ''); - $self->pair_separator(';') if $string =~ /;/ && $string !~ /\&/; + return $params unless length $str; # W3C suggests to also accept ";" as a separator my $charset = $self->charset; - for my $pair (split /[\&\;]+/, $string) { - $pair =~ /^([^=]*)(?:=(.*))?$/; - my $name = defined $1 ? $1 : ''; + for my $pair (split /&|;/, $str) { + next unless $pair =~ /^([^=]+)(?:=(.*))?$/; + my $name = $1; my $value = defined $2 ? $2 : ''; # Replace "+" with whitespace, unescape and decode @@ -157,9 +153,9 @@ sub to_string { # String my $charset = $self->charset; - if (defined(my $string = $self->{string})) { - $string = encode $charset, $string if $charset; - return url_escape $string, '^A-Za-z0-9\-._~!$&\'()*+,;=%:@/?'; + if (defined(my $str = $self->{string})) { + $str = encode $charset, $str if $charset; + return url_escape $str, '^A-Za-z0-9\-._~!$&\'()*+,;=%:@/?'; } # Build pairs @@ -170,23 +166,22 @@ sub to_string { my ($name, $value) = @{$params}[$i, $i + 1]; # Escape and replace whitespace with "+" - $name = encode $charset, $name if $charset; - $name = url_escape $name, '^A-Za-z0-9\-._~!$\'()*,:@/?'; - $name =~ s/\%20/\+/g; - if ($value) { - $value = encode $charset, $value if $charset; - $value = url_escape $value, '^A-Za-z0-9\-._~!$\'()*,:@/?'; - $value =~ s/\%20/\+/g; - } + $name = encode $charset, $name if $charset; + $name = url_escape $name, '^A-Za-z0-9\-._~!$\'()*,:@/?'; + $value = encode $charset, $value if $charset; + $value = url_escape $value, '^A-Za-z0-9\-._~!$\'()*,:@/?'; + s/\%20/\+/g for $name, $value; - push @pairs, defined $value ? "$name=$value" : $name; + push @pairs, "$name=$value"; } - return join $self->pair_separator, @pairs; + return join '&', @pairs; } 1; +=encoding utf8 + =head1 NAME Mojo::Parameters - Parameters @@ -201,11 +196,12 @@ Mojo::Parameters - Parameters # Build my $params = Mojo::Parameters->new(foo => 'bar', baz => 23); + push @$params, i => '♥ mojolicious'; say "$params"; =head1 DESCRIPTION -L is a container for form parameters. +L is a container for form parameters used by L. =head1 ATTRIBUTES @@ -221,13 +217,6 @@ Charset used for encoding and decoding parameters, defaults to C. # Disable encoding and decoding $params->charset(undef); -=head2 pair_separator - - my $separator = $params->pair_separator; - $params = $params->pair_separator(';'); - -Separator for parameter pairs, defaults to C<&>. - =head1 METHODS L inherits all methods from L and implements the @@ -241,7 +230,8 @@ following new ones. my $params = Mojo::Parameters->new(foo => ['ba;r', 'b;az']); my $params = Mojo::Parameters->new(foo => ['ba;r', 'b;az'], bar => 23); -Construct a new L object. +Construct a new L object and C parameters if +necessary. =head2 append @@ -249,7 +239,7 @@ Construct a new L object. $params = $params->append(foo => ['ba;r', 'b;az']); $params = $params->append(foo => ['ba;r', 'b;az'], bar => 23); -Append parameters. +Append parameters. Note that this method will normalize the parameters. # "foo=bar&foo=baz" Mojo::Parameters->new('foo=bar')->append(foo => 'baz'); @@ -270,7 +260,8 @@ Clone parameters. $params = $params->merge(Mojo::Parameters->new(foo => 'b;ar', baz => 23)); -Merge L objects. +Merge L objects. Note that this method will normalize the +parameters. =head2 param @@ -280,14 +271,18 @@ Merge L objects. my $foo = $params->param(foo => 'ba;r'); my @foo = $params->param(foo => qw(ba;r ba;z)); -Check and replace parameter values. +Check and replace parameter value. Be aware that if you request a parameter by +name in scalar context, you will receive only the I value for that +parameter, if there are multiple values for that name. In list context you +will receive I of the values for that name. Note that this method will +normalize the parameters. =head2 params my $array = $params->params; $params = $params->params([foo => 'b;ar', baz => 23]); -Parsed parameters. +Parsed parameters. Note that this method will normalize the parameters. =head2 parse @@ -299,7 +294,7 @@ Parse parameters. $params = $params->remove('foo'); -Remove parameters. +Remove parameters. Note that this method will normalize the parameters. # "bar=yada" Mojo::Parameters->new('foo=bar&foo=baz&bar=yada')->remove('foo'); @@ -308,18 +303,27 @@ Remove parameters. my $hash = $params->to_hash; -Turn parameters into a hash reference. +Turn parameters into a hash reference. Note that this method will normalize +the parameters. # "baz" Mojo::Parameters->new('foo=bar&foo=baz')->to_hash->{foo}[1]; =head2 to_string - my $string = $params->to_string; - my $string = "$params"; + my $str = $params->to_string; + my $str = "$params"; Turn parameters into a string. +=head1 PARAMETERS + +Direct array reference access to the parsed parameters is also possible. Note +that this will normalize the parameters. + + say $params->[0]; + say for @$params; + =head1 SEE ALSO L, L, L. diff --git a/lib/Mojo/Path.pm b/lib/Mojo/Path.pm index a0b6388..21a0612 100644 --- a/lib/Mojo/Path.pm +++ b/lib/Mojo/Path.pm @@ -1,15 +1,14 @@ package Mojo::Path; use Mojo::Base -base; use overload - 'bool' => sub {1}, + '@{}' => sub { shift->parts }, + bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1; use Mojo::Util qw(decode encode url_escape url_unescape); has charset => 'UTF-8'; -has [qw(leading_slash trailing_slash)]; -has parts => sub { [] }; sub new { shift->SUPER::new->parse(@_) } @@ -21,15 +20,11 @@ sub canonicalize { # ".." if ($part eq '..') { - unless (@parts && $parts[-1] ne '..') { push @parts, '..' } - else { pop @parts } - next; + (@parts && $parts[-1] ne '..') ? pop @parts : push @parts, '..'; } - # "." - next if grep { $_ eq $part } '.', ''; - - push @parts, $part; + # Something else than "." + elsif ($part ne '.' && $part ne '') { push @parts, $part } } $self->trailing_slash(undef) unless @parts; @@ -37,25 +32,25 @@ sub canonicalize { } sub clone { - my $self = shift; - my $clone = Mojo::Path->new; - $clone->leading_slash($self->leading_slash); - $clone->trailing_slash($self->trailing_slash); - return $clone->charset($self->charset)->parts([@{$self->parts}]); + my $self = shift; + + my $clone = $self->new->charset($self->charset); + if (my $parts = $self->{parts}) { + $clone->{$_} = $self->{$_} for qw(leading_slash trailing_slash); + $clone->{parts} = [@$parts]; + } + else { $clone->{path} = $self->{path} } + + return $clone; } sub contains { my ($self, $path) = @_; - - my $parts = $self->new($path)->parts; - for my $part (@{$self->parts}) { - return 1 unless defined(my $try = shift @$parts); - return undef unless $part eq $try; - } - - return !@$parts; + return $path eq '/' || $self->to_route =~ m!^\Q$path\E(?:/|$)!; } +sub leading_slash { shift->_parse(leading_slash => @_) } + sub merge { my ($self, $path) = @_; @@ -70,19 +65,19 @@ sub merge { } sub parse { - my ($self, $path) = @_; + my $self = shift; + $self->{path} = shift; + delete $self->{$_} for qw(leading_slash parts trailing_slash); + return $self; +} - $path = url_unescape defined $path ? $path : ''; - my $charset = $self->charset; - $path = do { my $tmp = decode($charset, $path); defined $tmp ? $tmp : $path } if $charset; - $self->leading_slash($path =~ s!^/!! ? 1 : undef); - $self->trailing_slash($path =~ s!/$!! ? 1 : undef); +sub parts { shift->_parse(parts => @_) } - return $self->parts([split '/', $path, -1]); +sub to_abs_string { + my $path = shift->to_string; + return $path =~ m!^/! ? $path : "/$path"; } -sub to_abs_string { $_[0]->leading_slash ? "$_[0]" : "/$_[0]" } - sub to_dir { my $clone = shift->clone; pop @{$clone->parts} unless $clone->trailing_slash; @@ -90,24 +85,50 @@ sub to_dir { } sub to_route { - my $self = shift; - return '/' . join('/', @{$self->parts}) . ($self->trailing_slash ? '/' : ''); + my $clone = shift->clone; + my $route = join '/', @{$clone->parts}; + return "/$route" . ($clone->trailing_slash ? '/' : ''); } sub to_string { my $self = shift; - my @parts = @{$self->parts}; + # Path my $charset = $self->charset; + if (defined(my $path = $self->{path})) { + $path = encode $charset, $path if $charset; + return url_escape $path, '^A-Za-z0-9\-._~!$&\'()*+,;=%:@/'; + } + + # Build path + my @parts = @{$self->parts}; @parts = map { encode $charset, $_ } @parts if $charset; my $path = join '/', map { url_escape $_, '^A-Za-z0-9\-._~!$&\'()*+,;=:@' } @parts; $path = "/$path" if $self->leading_slash; $path = "$path/" if $self->trailing_slash; - return $path; } +sub trailing_slash { shift->_parse(trailing_slash => @_) } + +sub _parse { + my ($self, $name) = (shift, shift); + + unless ($self->{parts}) { + my $path = url_unescape do {my $tmp = delete($self->{path}); defined $tmp ? $tmp : ''}; + my $charset = $self->charset; + $path = do {my $tmp = decode($charset, $path); defined $tmp ? $tmp : $path} if $charset; + $self->{leading_slash} = $path =~ s!^/!! ? 1 : undef; + $self->{trailing_slash} = $path =~ s!/$!! ? 1 : undef; + $self->{parts} = [split '/', $path, -1]; + } + + return $self->{$name} unless @_; + $self->{$name} = shift; + return $self; +} + 1; =encoding utf8 @@ -120,13 +141,18 @@ Mojo::Path - Path use Mojo::Path; + # Parse my $path = Mojo::Path->new('/foo%2Fbar%3B/baz.html'); - shift @{$path->parts}; + say $path->[0]; + + # Build + my $path = Mojo::Path->new('/i/♥'); + push @$path, 'mojolicious'; say "$path"; =head1 DESCRIPTION -L is a container for URL paths. +L is a container for paths used by L. =head1 ATTRIBUTES @@ -142,30 +168,6 @@ Charset used for encoding and decoding, defaults to C. # Disable encoding and decoding $path->charset(undef); -=head2 leading_slash - - my $leading_slash = $path->leading_slash; - $path = $path->leading_slash(1); - -Path has a leading slash. - -=head2 parts - - my $parts = $path->parts; - $path = $path->parts([qw(foo bar baz)]); - -The path parts. - - # Part with slash - push @{$path->parts}, 'foo/bar'; - -=head2 trailing_slash - - my $trailing_slash = $path->trailing_slash; - $path = $path->trailing_slash(1); - -Path has a trailing slash. - =head1 METHODS L inherits all methods from L and implements the @@ -176,7 +178,7 @@ following new ones. my $path = Mojo::Path->new; my $path = Mojo::Path->new('/foo%2Fbar%3B/baz.html'); -Construct a new L object. +Construct a new L object and C path if necessary. =head2 canonicalize @@ -185,7 +187,7 @@ Construct a new L object. Canonicalize path. # "/foo/baz" - Mojo::Path->new('/foo/bar/../baz')->canonicalize; + Mojo::Path->new('/foo/./bar/../baz')->canonicalize; =head2 clone @@ -209,13 +211,22 @@ Check if path contains given prefix. Mojo::Path->new('/foo/bar')->contains('/bar'); Mojo::Path->new('/foo/bar')->contains('/whatever'); +=head2 leading_slash + + my $slash = $path->leading_slash; + $path = $path->leading_slash(1); + +Path has a leading slash. Note that this method will normalize the path and +that C<%2F> will be treated as C for security reasons. + =head2 merge $path = $path->merge('/foo/bar'); $path = $path->merge('foo/bar'); $path = $path->merge(Mojo::Path->new('foo/bar')); -Merge paths. +Merge paths. Note that this method will normalize both paths if necessary and +that C<%2F> will be treated as C for security reasons. # "/baz/yada" Mojo::Path->new('/foo/bar')->merge('/baz/yada'); @@ -230,17 +241,29 @@ Merge paths. $path = $path->parse('/foo%2Fbar%3B/baz.html'); -Parse path. Note that C<%2F> will be treated as C for security reasons. +Parse path. =head2 to_abs_string - my $string = $path->to_abs_string; + my $str = $path->to_abs_string; Turn path into an absolute string. # "/i/%E2%99%A5/mojolicious" + Mojo::Path->new('/i/%E2%99%A5/mojolicious')->to_abs_string; Mojo::Path->new('i/%E2%99%A5/mojolicious')->to_abs_string; +=head2 parts + + my $parts = $path->parts; + $path = $path->parts([qw(foo bar baz)]); + +The path parts. Note that this method will normalize the path and that C<%2F> +will be treated as C for security reasons. + + # Part with slash + push @{$path->parts}, 'foo/bar'; + =head2 to_dir my $dir = $route->to_dir; @@ -248,6 +271,9 @@ Turn path into an absolute string. Clone path and remove everything after the right-most slash. # "/i/%E2%99%A5/" + Mojo::Path->new('/i/%E2%99%A5/mojolicious')->to_dir->to_abs_string; + + # "i/%E2%99%A5/" Mojo::Path->new('i/%E2%99%A5/mojolicious')->to_dir->to_abs_string; =head2 to_route @@ -257,18 +283,39 @@ Clone path and remove everything after the right-most slash. Turn path into a route. # "/i/♥/mojolicious" + Mojo::Path->new('/i/%E2%99%A5/mojolicious')->to_route; Mojo::Path->new('i/%E2%99%A5/mojolicious')->to_route; =head2 to_string - my $string = $path->to_string; - my $string = "$path"; + my $str = $path->to_string; + my $str = "$path"; Turn path into a string. + # "/i/%E2%99%A5/mojolicious" + Mojo::Path->new('/i/%E2%99%A5/mojolicious')->to_string; + # "i/%E2%99%A5/mojolicious" Mojo::Path->new('i/%E2%99%A5/mojolicious')->to_string; +=head2 trailing_slash + + my $slash = $path->trailing_slash; + $path = $path->trailing_slash(1); + +Path has a trailing slash. Note that this method will normalize the path and +that C<%2F> will be treated as C for security reasons. + +=head1 PATH PARTS + +Direct array reference access to path parts is also possible. Note that this +will normalize the path and that C<%2F> will be treated as C for security +reasons. + + say $path->[0]; + say for @$path; + =head1 SEE ALSO L, L, L. diff --git a/lib/Mojo/Reactor.pm b/lib/Mojo/Reactor.pm index 4ebaacc..3b7ff58 100644 --- a/lib/Mojo/Reactor.pm +++ b/lib/Mojo/Reactor.pm @@ -5,6 +5,8 @@ use Carp 'croak'; use IO::Poll qw(POLLERR POLLHUP POLLIN); use Mojo::Loader; +sub again { croak 'Method "again" not implemented by subclass' } + sub detect { my $try = $ENV{MOJO_REACTOR} || 'Mojo::Reactor::EV'; return Mojo::Loader->new->load($try) ? 'Mojo::Reactor::Poll' : $try; @@ -35,6 +37,8 @@ sub watch { croak 'Method "watch" not implemented by subclass' } 1; +=encoding utf8 + =head1 NAME Mojo::Reactor - Low level event reactor base class @@ -46,6 +50,7 @@ Mojo::Reactor - Low level event reactor base class $ENV{MOJO_REACTOR} ||= 'Mojo::Reactor::MyEventLoop'; + sub again {...} sub io {...} sub is_running {...} sub one_tick {...} @@ -84,12 +89,18 @@ Emitted safely for exceptions caught in callbacks. L inherits all methods from L and implements the following new ones. +=head2 again + + $reactor->again($id); + +Restart active timer. Meant to be overloaded in a subclass. + =head2 detect my $class = Mojo::Reactor->detect; Detect and load the best reactor implementation available, will try the value -of the C environment variable, L or +of the MOJO_REACTOR environment variable, L or L. # Instantiate best reactor implementation available diff --git a/lib/Mojo/Reactor/EV.pm b/lib/Mojo/Reactor/EV.pm index e2ea74a..df4c9e9 100644 --- a/lib/Mojo/Reactor/EV.pm +++ b/lib/Mojo/Reactor/EV.pm @@ -13,6 +13,8 @@ sub DESTROY { undef $EV } # We have to fall back to Mojo::Reactor::Poll, since EV is unique sub new { $EV++ ? Mojo::Reactor::Poll->new : shift->SUPER::new } +sub again { shift->{timers}{shift()}{watcher}->again } + sub is_running { !!EV::depth } sub one_tick { EV::run(EV::RUN_ONCE) } @@ -59,7 +61,7 @@ sub _timer { my $id = $self->SUPER::_timer(0, 0, $cb); weaken $self; $self->{timers}{$id}{watcher} = EV::timer( - $after => ($recurring ? $after : 0) => sub { + $after => $after => sub { $self->_sandbox("Timer $id", $self->{timers}{$id}{cb}); delete $self->{timers}{$id} unless $recurring; } @@ -70,6 +72,8 @@ sub _timer { 1; +=encoding utf8 + =head1 NAME Mojo::Reactor::EV - Low level event reactor with libev support @@ -117,6 +121,12 @@ implements the following new ones. Construct a new L object. +=head2 again + + $reactor->again($id); + +Restart active timer. + =head2 is_running my $success = $reactor->is_running; diff --git a/lib/Mojo/Reactor/Poll.pm b/lib/Mojo/Reactor/Poll.pm index 8ab5ede..f08914a 100644 --- a/lib/Mojo/Reactor/Poll.pm +++ b/lib/Mojo/Reactor/Poll.pm @@ -3,8 +3,13 @@ use Mojo::Base 'Mojo::Reactor'; use IO::Poll qw(POLLERR POLLHUP POLLIN POLLOUT); use List::Util 'min'; -use Mojo::Util 'md5_sum'; -use Time::HiRes qw(time usleep); +use Mojo::Util qw(md5_sum steady_time); +use Time::HiRes 'usleep'; + +sub again { + my $timer = shift->{timers}{shift()}; + $timer->{time} = steady_time + $timer->{after}; +} sub io { my ($self, $handle, $cb) = @_; @@ -31,7 +36,7 @@ sub one_tick { # Calculate ideal timeout based on timers my $min = min map { $_->{time} } values %{$self->{timers}}; - my $timeout = defined $min ? ($min - time) : 0.5; + my $timeout = defined $min ? ($min - steady_time) : 0.5; $timeout = 0 if $timeout < 0; # I/O @@ -46,12 +51,14 @@ sub one_tick { # Wait for timeout if poll can't be used elsif ($timeout) { usleep $timeout * 1000000 } - # Timers - while (my ($id, $t) = each %{$self->{timers} || {}}) { - next unless $t->{time} <= (my $time = time); + # Timers (time should not change in between timers) + my $now = steady_time; + for my $id (keys %{$self->{timers}}) { + next unless my $t = $self->{timers}{$id}; + next unless $t->{time} <= $now; # Recurring timer - if (exists $t->{recurring}) { $t->{time} = $time + $t->{recurring} } + if (exists $t->{recurring}) { $t->{time} = $now + $t->{recurring} } # Normal timer else { $self->remove($id) } @@ -105,16 +112,20 @@ sub _sandbox { sub _timer { my ($self, $recurring, $after, $cb) = @_; + my $timers = defined $self->{timers} ? $self->{timers} : ($self->{timers} = {}); my $id; - do { $id = md5_sum('t' . time . rand 999) } while $self->{timers}{$id}; - my $t = $self->{timers}{$id} = {cb => $cb, time => time + $after}; - $t->{recurring} = $after if $recurring; + do { $id = md5_sum('t' . steady_time . rand 999) } while $timers->{$id}; + my $timer = $timers->{$id} + = {cb => $cb, after => $after, time => steady_time + $after}; + $timer->{recurring} = $after if $recurring; return $id; } 1; +=encoding utf8 + =head1 NAME Mojo::Reactor::Poll - Low level event reactor with poll support @@ -145,9 +156,7 @@ Mojo::Reactor::Poll - Low level event reactor with poll support =head1 DESCRIPTION -L is a low level event reactor based on L. Note -that this reactor was designed for maximum portability, and therefore does not -use a monotonic clock to handle time jumps. +L is a low level event reactor based on L. =head1 EVENTS @@ -158,6 +167,12 @@ L inherits all events from L. L inherits all methods from L and implements the following new ones. +=head2 again + + $reactor->again($id); + +Restart active timer. + =head2 io $reactor = $reactor->io($handle => sub {...}); diff --git a/lib/Mojo/Server.pm b/lib/Mojo/Server.pm index c0fa671..50120e8 100644 --- a/lib/Mojo/Server.pm +++ b/lib/Mojo/Server.pm @@ -2,7 +2,6 @@ package Mojo::Server; use Mojo::Base 'Mojo::EventEmitter'; use Carp 'croak'; -use FindBin; use Mojo::Loader; use Mojo::Util 'md5_sum'; use Scalar::Util 'blessed'; @@ -27,15 +26,16 @@ sub build_tx { shift->app->build_tx } sub load_app { my ($self, $path) = @_; - # Clean environment (reset FindBin) + # Clean environment (reset FindBin defensively) { local $0 = $path; + require FindBin; FindBin->again; local $ENV{MOJO_APP_LOADER} = 1; local $ENV{MOJO_EXE}; # Try to load application from script into sandbox - $self->app(my $app = eval sprintf <<'EOF', md5_sum($path . $$)); + my $app = eval sprintf <<'EOF', md5_sum($path . $$); package Mojo::Server::SandBox::%s; my $app = do $path; if (!$app && (my $e = $@ || $!)) { die $e } @@ -44,6 +44,7 @@ EOF die qq{Couldn't load application from file "$path": $@} if !$app && $@; die qq{File "$path" did not return an application object.\n} unless blessed $app && $app->isa('Mojo'); + $self->app($app); }; FindBin->again; @@ -54,6 +55,8 @@ sub run { croak 'Method "run" not implemented by subclass' } 1; +=encoding utf8 + =head1 NAME Mojo::Server - HTTP server base class diff --git a/lib/Mojo/Server/CGI.pm b/lib/Mojo/Server/CGI.pm index 4a8540a..6b9f3e7 100644 --- a/lib/Mojo/Server/CGI.pm +++ b/lib/Mojo/Server/CGI.pm @@ -34,7 +34,7 @@ sub run { return undef unless _write($res, 'get_header_chunk'); # Response body - return undef unless _write($res, 'get_body_chunk'); + $tx->is_empty or _write($res, 'get_body_chunk') or return undef; # Finish transaction $tx->server_close; @@ -65,6 +65,8 @@ sub _write { 1; +=encoding utf8 + =head1 NAME Mojo::Server::CGI - CGI server @@ -112,7 +114,7 @@ implements the following new ones. my $nph = $cgi->nph; $cgi = $cgi->nph(1); -Activate non parsed header mode. +Activate non-parsed header mode. =head1 METHODS diff --git a/lib/Mojo/Server/Daemon.pm b/lib/Mojo/Server/Daemon.pm index a3a26c9..cc716a8 100644 --- a/lib/Mojo/Server/Daemon.pm +++ b/lib/Mojo/Server/Daemon.pm @@ -9,6 +9,7 @@ use Scalar::Util 'weaken'; use constant DEBUG => $ENV{MOJO_DAEMON_DEBUG} || 0; +has acceptors => sub { [] }; has [qw(backlog group silent user)]; has inactivity_timeout => sub { defined $ENV{MOJO_INACTIVITY_TIMEOUT} ? $ENV{MOJO_INACTIVITY_TIMEOUT} : 15 }; has ioloop => sub { Mojo::IOLoop->singleton }; @@ -20,7 +21,7 @@ sub DESTROY { my $self = shift; return unless my $loop = $self->ioloop; $self->_remove($_) for keys %{$self->{connections} || {}}; - $loop->remove($_) for @{$self->{acceptors} || []}; + $loop->remove($_) for @{$self->acceptors}; } sub run { @@ -54,9 +55,9 @@ sub start { # Resume accepting connections my $loop = $self->ioloop; - if (my $acceptors = $self->{acceptors}) { - push @$acceptors, $loop->acceptor(delete $self->{servers}{$_}) - for keys %{$self->{servers}}; + if (my $servers = $self->{servers}) { + push @{$self->acceptors}, $loop->acceptor(delete $servers->{$_}) + for keys %$servers; } # Start listening @@ -71,7 +72,7 @@ sub stop { # Suspend accepting connections but keep listen sockets open my $loop = $self->ioloop; - while (my $id = shift @{$self->{acceptors}}) { + while (my $id = shift @{$self->acceptors}) { my $server = $self->{servers}{$id} = $loop->acceptor($id); $loop->remove($id); $server->stop; @@ -108,9 +109,7 @@ sub _build_tx { ); # Kept alive if we have more than one request on the connection - $tx->kept_alive(1) if ++$c->{requests} > 1; - - return $tx; + return ++$c->{requests} > 1 ? $tx->kept_alive(1) : $tx; } sub _close { @@ -153,9 +152,9 @@ sub _finish { return $self->_remove($id) if $req->error || !$tx->keep_alive; # Build new transaction for leftovers - return unless $req->has_leftovers; + return unless length(my $leftovers = $req->content->leftovers); $tx = $c->{tx} = $self->_build_tx($id, $c); - $tx->server_read($req->leftovers); + $tx->server_read($leftovers); } sub _listen { @@ -167,6 +166,7 @@ sub _listen { address => $url->host, backlog => $self->backlog, port => $url->port, + reuse => scalar $query->param('reuse'), tls_ca => scalar $query->param('ca'), tls_cert => scalar $query->param('cert'), tls_key => scalar $query->param('key') @@ -177,7 +177,7 @@ sub _listen { my $tls = $options->{tls} = $url->protocol eq 'https' ? 1 : undef; weaken $self; - my $id = $self->ioloop->server( + push @{$self->acceptors}, $self->ioloop->server( $options => sub { my ($loop, $stream, $id) = @_; @@ -198,24 +198,24 @@ sub _listen { sub { $self->app->log->debug('Inactivity timeout.') if $c->{tx} }); } ); - push @{$self->{acceptors} ||= []}, $id; return if $self->silent; - $self->app->log->info(qq{Listening at "$listen".}); - $listen =~ s!//\*!//127.0.0.1!i; - say "Server available at $listen."; + $self->app->log->info(qq{Listening at "$url".}); + $query->params([]); + $url->host('127.0.0.1') if $url->host eq '*'; + say "Server available at $url."; } sub _read { my ($self, $id, $chunk) = @_; # Make sure we have a transaction and parse chunk - my $c = $self->{connections}{$id}; + return unless my $c = $self->{connections}{$id}; my $tx = $c->{tx} ||= $self->_build_tx($id, $c); warn "-- Server <<< Client (@{[$tx->req->url->to_abs]})\n$chunk\n" if DEBUG; $tx->server_read($chunk); - # Last keep alive request or corrupted connection + # Last keep-alive request or corrupted connection $tx->res->headers->connection('close') if (($c->{requests} || 0) >= $self->max_requests) || $tx->req->error; @@ -234,7 +234,7 @@ sub _write { my ($self, $id) = @_; # Not writing - my $c = $self->{connections}{$id}; + return unless my $c = $self->{connections}{$id}; return unless my $tx = $c->{tx}; return unless $tx->is_writing; @@ -257,11 +257,13 @@ sub _write { return unless $c->{tx}; } } - $stream->write('', $cb); + $stream->write('' => $cb); } 1; +=encoding utf8 + =head1 NAME Mojo::Server::Daemon - Non-blocking I/O HTTP and WebSocket server @@ -292,13 +294,15 @@ Mojo::Server::Daemon - Non-blocking I/O HTTP and WebSocket server =head1 DESCRIPTION L is a full featured, highly portable non-blocking I/O -HTTP and WebSocket server, with C, C, C (long polling) and -multiple event loop support. +HTTP and WebSocket server, with IPv6, TLS, Comet (long polling), keep-alive, +connection pooling, timeout, cookie, multipart and multiple event loop +support. -Optional modules L (4.0+), L (0.16+) and -L (1.75+) are supported transparently through -L, and used if installed. Individual features can also be -disabled with the C and C environment variables. +For better scalability (epoll, kqueue) and to provide IPv6 as well as TLS +support, the optional modules L (4.0+), L (0.16+) and +L (1.75+) will be used automatically by L if +they are installed. Individual features can also be disabled with the +MOJO_NO_IPV6 and MOJO_NO_TLS environment variables. See L for more. @@ -311,6 +315,13 @@ L inherits all events from L. L inherits all attributes from L and implements the following new ones. +=head2 acceptors + + my $acceptors = $daemon->acceptors; + $daemon = $daemon->acceptors([]); + +Active acceptors. + =head2 backlog my $backlog = $daemon->backlog; @@ -331,7 +342,7 @@ Group for server process. $daemon = $daemon->inactivity_timeout(5); Maximum amount of time in seconds a connection can be inactive before getting -closed, defaults to the value of the C environment +closed, defaults to the value of the MOJO_INACTIVITY_TIMEOUT environment variable or C<15>. Setting the value to C<0> will allow connections to be inactive indefinitely. @@ -349,7 +360,10 @@ L singleton. $daemon = $daemon->listen(['https://localhost:3000']); List of one or more locations to listen on, defaults to the value of the -C environment variable or C. +MOJO_LISTEN environment variable or C. + + # Allow multiple servers to use the same port (SO_REUSEPORT) + $daemon->listen(['http://*:8080?reuse=1']); # Listen on IPv6 interface $daemon->listen(['http://[::1]:4000']); @@ -370,18 +384,33 @@ These parameters are currently available: =item ca + ca=/etc/tls/ca.crt + Path to TLS certificate authority file. =item cert + cert=/etc/tls/server.crt + Path to the TLS cert file, defaults to a built-in test certificate. =item key + key=/etc/tls/server.key + Path to the TLS key file, defaults to a built-in test key. +=item reuse + + reuse=1 + +Allow multiple servers to use the same port with the C socket +option. + =item verify + verify=0x00 + TLS verification mode, defaults to C<0x03>. =back @@ -398,7 +427,7 @@ Maximum number of parallel client connections, defaults to C<1000>. my $max = $daemon->max_requests; $daemon = $daemon->max_requests(100); -Maximum number of keep alive requests per connection, defaults to C<25>. +Maximum number of keep-alive requests per connection, defaults to C<25>. =head2 silent @@ -445,7 +474,7 @@ Stop accepting connections. =head1 DEBUGGING -You can set the C environment variable to get some advanced +You can set the MOJO_DAEMON_DEBUG environment variable to get some advanced diagnostics information printed to C. MOJO_DAEMON_DEBUG=1 diff --git a/lib/Mojo/Server/Hypnotoad.pm b/lib/Mojo/Server/Hypnotoad.pm index a7a74b5..1a2474b 100644 --- a/lib/Mojo/Server/Hypnotoad.pm +++ b/lib/Mojo/Server/Hypnotoad.pm @@ -7,6 +7,7 @@ use Cwd 'abs_path'; use File::Basename 'dirname'; use File::Spec::Functions 'catfile'; use Mojo::Server::Prefork; +use Mojo::Util 'steady_time'; use POSIX 'setsid'; use Scalar::Util 'weaken'; @@ -55,14 +56,14 @@ sub run { exit 0 if $pid; setsid or die "Can't start a new session: $!"; - # Close file handles + # Close filehandles open STDIN, '/dev/null'; open STDERR, '>&STDOUT'; } # Start accepting connections - local $SIG{USR2} = sub { $self->{upgrade} ||= time }; + local $SIG{USR2} = sub { $self->{upgrade} ||= steady_time }; $prefork->run; } @@ -123,7 +124,7 @@ sub _manage { # Timeout kill 'KILL', $self->{new} - if $self->{upgrade} + $self->{upgrade_timeout} <= time; + if $self->{upgrade} + $self->{upgrade_timeout} <= steady_time; } } @@ -145,6 +146,8 @@ sub _stop { 1; +=encoding utf8 + =head1 NAME Mojo::Server::Hypnotoad - ALL GLORY TO THE HYPNOTOAD! @@ -160,10 +163,11 @@ Mojo::Server::Hypnotoad - ALL GLORY TO THE HYPNOTOAD! L is a full featured, UNIX optimized, preforking non-blocking I/O HTTP and WebSocket server, built around the very well tested -and reliable L, with C, C, C (long -polling), multiple event loop and hot deployment support that just works. Note -that the server uses signals for process management, so you should avoid -modifying signal handlers in your applications. +and reliable L, with IPv6, TLS, Comet (long polling), +keep-alive, connection pooling, timeout, cookie, multipart, multiple event +loop and hot deployment support that just works. Note that the server uses +signals for process management, so you should avoid modifying signal handlers +in your applications. To start applications with it you can use the L script. @@ -178,10 +182,11 @@ You can run the same command again for automatic hot deployment. For L and L applications it will default to C mode. -Optional modules L (4.0+), L (0.16+) and -L (1.75+) are supported transparently through -L, and used if installed. Individual features can also be -disabled with the C and C environment variables. +For better scalability (epoll, kqueue) and to provide IPv6 as well as TLS +support, the optional modules L (4.0+), L (0.16+) and +L (1.75+) will be used automatically by L if +they are installed. Individual features can also be disabled with the +MOJO_NO_IPV6 and MOJO_NO_TLS environment variables. See L for more. @@ -254,9 +259,9 @@ L for examples. accept_interval => 0.5 -Interval in seconds for trying to reacquire the accept mutex and connection -management, defaults to C<0.025>. Note that changing this value can affect -performance and idle CPU usage. +Interval in seconds for trying to reacquire the accept mutex, defaults to +C<0.025>. Note that changing this value can affect performance and idle CPU +usage. =head2 accepts @@ -321,7 +326,7 @@ be inactive indefinitely. keep_alive_requests => 50 -Number of keep alive requests per connection, defaults to C<25>. +Number of keep-alive requests per connection, defaults to C<25>. =head2 listen @@ -339,10 +344,11 @@ appended, defaults to a random temporary path. =head2 lock_timeout - lock_timeout => 1 + lock_timeout => 0.5 Maximum amount of time in seconds a worker may block when waiting for the -accept mutex, defaults to C<0.5>. +accept mutex, defaults to C<1>. Note that changing this value can affect +performance and idle CPU usage. =head2 multi_accept @@ -364,7 +370,7 @@ the server has been stopped. Activate reverse proxy support, which allows for the C and C headers to be picked up automatically, defaults to the -value of the C environment variable. +value of the MOJO_REVERSE_PROXY environment variable. =head2 upgrade_timeout diff --git a/lib/Mojo/Server/Morbo.pm b/lib/Mojo/Server/Morbo.pm index c406dba..65c16e5 100644 --- a/lib/Mojo/Server/Morbo.pm +++ b/lib/Mojo/Server/Morbo.pm @@ -25,7 +25,7 @@ sub check_file { sub run { my ($self, $app) = @_; - # Prepare environment + # Clean manager environment local $SIG{CHLD} = sub { $self->_reap }; local $SIG{INT} = local $SIG{TERM} = local $SIG{QUIT} = sub { $self->{finished} = 1; @@ -37,7 +37,6 @@ sub run { # Prepare and cache listen sockets for smooth restarting my $daemon = Mojo::Server::Daemon->new(silent => 1)->start->stop; - # Watch files and manage worker $self->_manage while !$self->{finished} || $self->{running}; exit 0; } @@ -101,6 +100,8 @@ sub _spawn { 1; +=encoding utf8 + =head1 NAME Mojo::Server::Morbo - DOOOOOOOOOOOOOOOOOOM! @@ -116,18 +117,21 @@ Mojo::Server::Morbo - DOOOOOOOOOOOOOOOOOOM! L is a full featured, self-restart capable non-blocking I/O HTTP and WebSocket server, built around the very well tested and reliable -L, with C, C, C (long polling) and -multiple event loop support. +L, with IPv6, TLS, Comet (long polling), keep-alive, +connection pooling, timeout, cookie, multipart and multiple event loop +support. Note that the server uses signals for process management, so you +should avoid modifying signal handlers in your applications. To start applications with it you can use the L script. $ morbo myapp.pl Server available at http://127.0.0.1:3000. -Optional modules L (4.0+), L (0.16+) and -L (1.75+) are supported transparently through -L, and used if installed. Individual features can also be -disabled with the C and C environment variables. +For better scalability (epoll, kqueue) and to provide IPv6 as well as TLS +support, the optional modules L (4.0+), L (0.16+) and +L (1.75+) will be used automatically by L if +they are installed. Individual features can also be disabled with the +MOJO_NO_IPV6 and MOJO_NO_TLS environment variables. See L for more. diff --git a/lib/Mojo/Server/PSGI.pm b/lib/Mojo/Server/PSGI.pm index 8abbbbd..f929b27 100644 --- a/lib/Mojo/Server/PSGI.pm +++ b/lib/Mojo/Server/PSGI.pm @@ -29,8 +29,8 @@ sub run { } # PSGI response - return [$res->code || 404, - \@headers, Mojo::Server::PSGI::_IO->new(tx => $tx)]; + my $io = Mojo::Server::PSGI::_IO->new(tx => $tx, empty => $tx->is_empty); + return [$res->code || 404, \@headers, $io]; } sub to_psgi_app { @@ -50,6 +50,9 @@ sub close { shift->{tx}->server_close } sub getline { my $self = shift; + # Empty + return undef if $self->{empty}; + # No content yet, try again later my $chunk = $self->{tx}->res->get_body_chunk($self->{offset} = defined $self->{offset} ? $self->{offset} : 0); return '' unless defined $chunk; @@ -63,6 +66,8 @@ sub getline { 1; +=encoding utf8 + =head1 NAME Mojo::Server::PSGI - PSGI server diff --git a/lib/Mojo/Server/Prefork.pm b/lib/Mojo/Server/Prefork.pm index 46036e9..2e06412 100644 --- a/lib/Mojo/Server/Prefork.pm +++ b/lib/Mojo/Server/Prefork.pm @@ -5,6 +5,7 @@ use Fcntl ':flock'; use File::Spec::Functions qw(catfile tmpdir); use IO::Poll 'POLLIN'; use List::Util 'shuffle'; +use Mojo::Util 'steady_time'; use POSIX 'WNOHANG'; use Scalar::Util 'weaken'; use Time::HiRes (); @@ -14,7 +15,7 @@ has accept_interval => 0.025; has [qw(graceful_timeout heartbeat_timeout)] => 20; has heartbeat_interval => 5; has lock_file => sub { catfile tmpdir, 'prefork.lock' }; -has lock_timeout => 0.5; +has lock_timeout => 1; has multi_accept => 50; has pid_file => sub { catfile tmpdir, 'prefork.pid' }; has workers => 4; @@ -63,17 +64,20 @@ sub run { # Clean manager environment local $SIG{INT} = local $SIG{TERM} = sub { $self->_term }; local $SIG{CHLD} = sub { - while ((my $pid = waitpid -1, WNOHANG) > 0) { $self->_reap($pid) } + while ((my $pid = waitpid -1, WNOHANG) > 0) { + $self->app->log->debug("Worker $pid stopped.") + if delete $self->emit(reap => $pid)->{pool}{$pid}; + } }; local $SIG{QUIT} = sub { $self->_term(1) }; local $SIG{TTIN} = sub { $self->workers($self->workers + 1) }; local $SIG{TTOU} = sub { $self->workers($self->workers - 1) if $self->workers > 0; return unless $self->workers; - $self->{pool}{shuffle keys %{$self->{pool}}}{graceful} ||= time; + $self->{pool}{shuffle keys %{$self->{pool}}}{graceful} ||= steady_time; }; - # Preload application and start accepting connections + # Preload application before starting workers $self->start->app->log->info("Manager $$ started."); $self->{running} = 1; $self->_manage while $self->{running}; @@ -89,7 +93,8 @@ sub _heartbeat { return unless $self->{reader}->sysread(my $chunk, 4194304); # Update heartbeats - $self->{pool}{$1} and $self->emit(heartbeat => $1)->{pool}{$1}{time} = time + my $time = steady_time; + $self->{pool}{$1} and $self->emit(heartbeat => $1)->{pool}{$1}{time} = $time while $chunk =~ /(\d+)\n/g; } @@ -108,22 +113,24 @@ sub _manage { # Manage workers $self->emit('wait')->_heartbeat; my $log = $self->app->log; - while (my ($pid, $w) = each %{$self->{pool}}) { + for my $pid (keys %{$self->{pool}}) { + next unless my $w = $self->{pool}{$pid}; # No heartbeat (graceful stop) my $interval = $self->heartbeat_interval; my $timeout = $self->heartbeat_timeout; - if (!$w->{graceful} && ($w->{time} + $interval + $timeout <= time)) { + my $time = steady_time; + if (!$w->{graceful} && ($w->{time} + $interval + $timeout <= $time)) { $log->info("Worker $pid has no heartbeat, restarting."); - $w->{graceful} = time; + $w->{graceful} = $time; } # Graceful stop with timeout - $w->{graceful} ||= time if $self->{graceful}; + $w->{graceful} ||= $time if $self->{graceful}; if ($w->{graceful}) { $log->debug("Trying to stop worker $pid gracefully."); kill 'QUIT', $pid; - $w->{force} = 1 if $w->{graceful} + $self->graceful_timeout <= time; + $w->{force} = 1 if $w->{graceful} + $self->graceful_timeout <= $time; } # Normal stop @@ -148,20 +155,13 @@ sub _pid_file { print $handle $$; } -sub _reap { - my ($self, $pid) = @_; - - # CLean up dead worker - $self->app->log->debug("Worker $pid stopped.") - if delete $self->emit(reap => $pid)->{pool}{$pid}; -} - sub _spawn { my $self = shift; # Manager die "Can't fork: $!" unless defined(my $pid = fork); - return $self->emit(spawn => $pid)->{pool}{$pid} = {time => time} if $pid; + return $self->emit(spawn => $pid)->{pool}{$pid} = {time => steady_time} + if $pid; # Prepare lock file my $file = $self->{lock_file}; @@ -175,21 +175,21 @@ sub _spawn { sub { # Blocking ("ualarm" can't be imported on Windows) - my $l; + my $lock; if ($_[1]) { eval { local $SIG{ALRM} = sub { die "alarm\n" }; my $old = Time::HiRes::ualarm $self->lock_timeout * 1000000; - $l = flock $handle, LOCK_EX; + $lock = flock $handle, LOCK_EX; Time::HiRes::ualarm $old; }; - if ($@) { $l = $@ eq "alarm\n" ? 0 : die($@) } + if ($@) { $lock = $@ eq "alarm\n" ? 0 : die($@) } } # Non blocking - else { $l = flock $handle, LOCK_EX | LOCK_NB } + else { $lock = flock $handle, LOCK_EX | LOCK_NB } - return $l; + return $lock; } ); $loop->unlock(sub { flock $handle, LOCK_UN }); @@ -208,7 +208,6 @@ sub _spawn { $SIG{QUIT} = sub { $loop->max_connections(0) }; delete $self->{$_} for qw(poll reader); - # Start event loop $self->app->log->debug("Worker $$ started."); $loop->start; exit 0; @@ -222,6 +221,8 @@ sub _term { 1; +=encoding utf8 + =head1 NAME Mojo::Server::Prefork - Preforking non-blocking I/O HTTP and WebSocket server @@ -253,15 +254,16 @@ Mojo::Server::Prefork - Preforking non-blocking I/O HTTP and WebSocket server L is a full featured, UNIX optimized, preforking non-blocking I/O HTTP and WebSocket server, built around the very well tested -and reliable L, with C, C, C (long -polling) and multiple event loop support. Note that the server uses signals -for process management, so you should avoid modifying signal handlers in your -applications. +and reliable L, with IPv6, TLS, Comet (long polling), +keep-alive, connection pooling, timeout, cookie, multipart and multiple event +loop support. Note that the server uses signals for process management, so you +should avoid modifying signal handlers in your applications. -Optional modules L (4.0+), L (0.16+) and -L (1.75+) are supported transparently through -L, and used if installed. Individual features can also be -disabled with the C and C environment variables. +For better scalability (epoll, kqueue) and to provide IPv6 as well as TLS +support, the optional modules L (4.0+), L (0.16+) and +L (1.75+) will be used automatically by L if +they are installed. Individual features can also be disabled with the +MOJO_NO_IPV6 and MOJO_NO_TLS environment variables. See L for more. @@ -392,9 +394,9 @@ and implements the following new ones. my $interval = $prefork->accept_interval; $prefork = $prefork->accept_interval(0.5); -Interval in seconds for trying to reacquire the accept mutex and connection -management, defaults to C<0.025>. Note that changing this value can affect -performance and idle CPU usage. +Interval in seconds for trying to reacquire the accept mutex, defaults to +C<0.025>. Note that changing this value can affect performance and idle CPU +usage. =head2 accepts @@ -441,10 +443,11 @@ appended, defaults to a random temporary path. =head2 lock_timeout my $timeout = $prefork->lock_timeout; - $prefork = $prefork->lock_timeout(1); + $prefork = $prefork->lock_timeout(0.5); Maximum amount of time in seconds a worker may block when waiting for the -accept mutex, defaults to C<0.5>. +accept mutex, defaults to C<1>. Note that changing this value can affect +performance and idle CPU usage. =head2 multi_accept diff --git a/lib/Mojo/Template.pm b/lib/Mojo/Template.pm index 28118e8..df627b2 100644 --- a/lib/Mojo/Template.pm +++ b/lib/Mojo/Template.pm @@ -27,6 +27,7 @@ sub build { my $self = shift; my (@lines, $cpst, $multi); + my $escape = $self->auto_escape; for my $line (@{$self->tree}) { push @lines, ''; for (my $j = 0; $j < @{$line}; $j += 2) { @@ -58,13 +59,12 @@ sub build { if ($type eq 'code' || $multi) { $lines[-1] .= "$value" } # Expression - if (grep { $_ eq $type } qw(expr escp)) { + if ($type eq 'expr' || $type eq 'escp') { # Start unless ($multi) { # Escaped - my $escape = $self->auto_escape; if (($type eq 'escp' && !$escape) || ($type eq 'expr' && $escape)) { $lines[-1] .= "\$_M .= _escape"; $lines[-1] .= " scalar $value" if length $value; @@ -99,7 +99,9 @@ sub compile { # Compile with line directive return undef unless my $code = $self->code; - my $compiled = eval qq{#line 1 "@{[$self->name]}"\n$code}; + my $name = $self->name; + $name =~ s/"//g; + my $compiled = eval qq{#line 1 "$name"\n$code}; $self->compiled($compiled) and return undef unless $@; # Use local stacktrace for compile exceptions @@ -124,10 +126,10 @@ sub interpret { } sub parse { - my ($self, $tmpl) = @_; + my ($self, $template) = @_; # Clean start - delete $self->template($tmpl)->{tree}; + my $tree = $self->template($template)->tree([])->tree; my $tag = $self->tag_start; my $replace = $self->replace_mark; @@ -140,7 +142,6 @@ sub parse { my $end = $self->tag_end; my $start = $self->line_start; - # Precompile my $token_re = qr/ ( \Q$tag$replace\E # Replace @@ -174,28 +175,23 @@ sub parse { # Split lines my $state = 'text'; my ($trimming, @capture_token); - for my $line (split /\n/, $tmpl) { + for my $line (split /\n/, $template) { $trimming = 0 if $state eq 'text'; - # Perl line + # Turn Perl line into mixed line if ($state eq 'text' && $line !~ s/^(\s*)\Q$start$replace\E/$1$start/) { - $line =~ s/^(\s*)\Q$start\E(\Q$expr\E)?// - and $line = $2 ? "$1$tag$2$line $end" : "$tag$line $trim$end"; - } - - # Escaped line ending - if ($line =~ /(\\+)$/) { - my $len = length $1; + if ($line =~ s/^(\s*)\Q$start\E(?:(\Q$cmnt\E)|(\Q$expr\E))?//) { - # Newline - if ($len == 1) { $line =~ s/\\$// } + # Comment + if ($2) { $line = "$tag$2 $trim$end" } - # Backslash - elsif ($len > 1) { $line =~ s/\\\\$/\\\n/ } + # Expression or code + else { $line = $3 ? "$1$tag$3$line $end" : "$tag$line $trim$end" } + } } - # Normal line ending - else { $line .= "\n" } + # Escaped line ending + $line .= "\n" unless $line =~ s/\\\\$/\\\n/ || $line =~ s/\\$//; # Mixed line my @token; @@ -233,7 +229,7 @@ sub parse { # Comment elsif ($token =~ /^\Q$tag$cmnt\E$/) { $state = 'cmnt' } - # Value + # Text else { # Replace @@ -251,27 +247,27 @@ sub parse { @capture_token = (); } } - push @{$self->tree}, \@token; + push @$tree, \@token; } return $self; } sub render { - my $self = shift->parse(shift)->build; - return $self->compile || $self->interpret(@_); + my $self = shift; + return $self->parse(shift)->build->compile || $self->interpret(@_); } sub render_file { my ($self, $path) = (shift, shift); $self->name($path) unless defined $self->{name}; - my $tmpl = slurp $path; + my $template = slurp $path; my $encoding = $self->encoding; croak qq{Template "$path" has invalid encoding.} - if $encoding && !defined($tmpl = decode $encoding, $tmpl); + if $encoding && !defined($template = decode $encoding, $template); - return $self->render($tmpl, @_); + return $self->render($template, @_); } sub _trim { @@ -280,8 +276,8 @@ sub _trim { # Walk line backwards for (my $j = @$line - 4; $j >= 0; $j -= 2) { - # Skip capture - next if grep { $_ eq $line->[$j] } qw(cpst cpen); + # Skip captures + next if $line->[$j] eq 'cpst' || $line->[$j] eq 'cpen'; # Only trim text return unless $line->[$j] eq 'text'; @@ -321,6 +317,8 @@ sub _wrap { 1; +=encoding utf8 + =head1 NAME Mojo::Template - Perl-ish templates! @@ -344,14 +342,14 @@ Mojo::Template - Perl-ish templates! # More advanced my $output = $mt->render(<<'EOF', 23, 'foo bar'); - % my ($number, $text) = @_; + % my ($num, $text) = @_; %= 5 * 5 More advanced test 123 - foo <% my $i = $number + 2; %> + foo <% my $i = $num + 2; %> % for (1 .. 23) { * some text <%= $i++ %> % } @@ -383,7 +381,7 @@ automatically enabled. % Perl code line, treated as "<% line =%>" %= Perl expression line, treated as "<%= line %>" %== Perl expression line, treated as "<%== line %>" - %# Comment line, treated as "<%# line =%>" + %# Comment line, useful for debugging %% Replaced with "%", useful for generating templates Escaping behavior can be reversed with the C attribute, this is @@ -616,14 +614,15 @@ Characters indicating the end of a tag, defaults to C<%E>. my $template = $mt->template; $mt = $mt->template($template); -Raw template. +Raw unparsed template. =head2 tree my $tree = $mt->tree; - $mt = $mt->tree($tree); + $mt = $mt->tree([['text', 'foo']]); -Parsed tree. +Template in parsed form. Note that this structure should only be used very +carefully since it is very dynamic. =head2 trim_mark @@ -639,12 +638,6 @@ Character activating automatic whitespace trimming, defaults to C<=>. L inherits all methods from L and implements the following new ones. -=head2 new - - my $mt = Mojo::Template->new; - -Construct a new L object. - =head2 build $mt = $mt->build; @@ -693,8 +686,8 @@ Render template file. =head1 DEBUGGING -You can set the C environment variable to get some -advanced diagnostics information printed to C. +You can set the MOJO_TEMPLATE_DEBUG environment variable to get some advanced +diagnostics information printed to C. MOJO_TEMPLATE_DEBUG=1 diff --git a/lib/Mojo/Transaction.pm b/lib/Mojo/Transaction.pm index 86b4243..e34deff 100644 --- a/lib/Mojo/Transaction.pm +++ b/lib/Mojo/Transaction.pm @@ -5,14 +5,14 @@ use Carp 'croak'; use Mojo::Message::Request; use Mojo::Message::Response; -has [qw(kept_alive local_address local_port previous remote_port)]; +has [qw(kept_alive local_address local_port remote_port)]; has req => sub { Mojo::Message::Request->new }; has res => sub { Mojo::Message::Response->new }; sub client_close { my $self = shift; $self->res->finish; - return $self->server_close(@_); + return $self->server_close; } sub client_read { croak 'Method "client_read" not implemented by subclass' } @@ -36,11 +36,7 @@ sub is_finished { do {my $tmp = shift->{state}; defined $tmp ? $tmp : ''} eq 'fi sub is_websocket {undef} -sub is_writing { - return 1 unless my $state = shift->{state}; - return !!grep { $_ eq $state } - qw(write write_start_line write_headers write_body); -} +sub is_writing { do {my $tmp = shift->{state}; defined $tmp ? $tmp : 'write'} eq 'write' } sub remote_address { my $self = shift; @@ -54,33 +50,31 @@ sub remote_address { # Reverse proxy if ($ENV{MOJO_REVERSE_PROXY}) { return $self->{forwarded_for} if $self->{forwarded_for}; - my $forwarded = $self->req->headers->header('X-Forwarded-For') || ''; + my $forwarded = defined $self->req->headers->header('X-Forwarded-For') ? $self->req->headers->header('X-Forwarded-For') : ''; $forwarded =~ /([^,\s]+)$/ and return $self->{forwarded_for} = $1; } return $self->{remote_address}; } -sub resume { - my $self = shift; - if ((defined $self->{state} ? $self->{state} : '') eq 'paused') { $self->{state} = 'write_body' } - elsif (!$self->is_writing) { $self->{state} = 'write' } - return $self->emit('resume'); -} - -sub server_close { - my $self = shift; - $self->{state} = 'finished'; - return $self->emit('finish'); -} +sub resume { shift->_state(qw(write resume)) } +sub server_close { shift->_state(qw(finished finish)) } sub server_read { croak 'Method "server_read" not implemented by subclass' } sub server_write { croak 'Method "server_write" not implemented by subclass' } sub success { $_[0]->error ? undef : $_[0]->res } +sub _state { + my ($self, $state, $event) = @_; + $self->{state} = $state; + return $self->emit($event); +} + 1; +=encoding utf8 + =head1 NAME Mojo::Transaction - Transaction base class @@ -156,16 +150,6 @@ Local interface address. Local interface port. -=head2 previous - - my $previous = $tx->previous; - $tx = $tx->previous(Mojo::Transaction->new); - -Previous transaction that triggered this followup transaction. - - # Path of previous request - say $tx->previous->req->url->path; - =head2 remote_port my $port = $tx->remote_port; diff --git a/lib/Mojo/Transaction/HTTP.pm b/lib/Mojo/Transaction/HTTP.pm index 7c96568..9245d06 100644 --- a/lib/Mojo/Transaction/HTTP.pm +++ b/lib/Mojo/Transaction/HTTP.pm @@ -3,40 +3,52 @@ use Mojo::Base 'Mojo::Transaction'; use Mojo::Transaction::WebSocket; +has 'previous'; + sub client_read { my ($self, $chunk) = @_; # Skip body for HEAD request my $res = $self->res; - $res->content->skip_body(1) if $self->req->method eq 'HEAD'; + $res->content->skip_body(1) if uc $self->req->method eq 'HEAD'; return unless $res->parse($chunk)->is_finished; - # Unexpected 1xx reponse + # Unexpected 1xx response return $self->{state} = 'finished' if !$res->is_status_class(100) || $res->headers->upgrade; $self->res($res->new)->emit(unexpected => $res); - $self->client_read($res->leftovers) if $res->has_leftovers; + return unless length(my $leftovers = $res->content->leftovers); + $self->client_read($leftovers); } sub client_write { shift->_write(0) } +sub is_empty { !!(uc $_[0]->req->method eq 'HEAD' || $_[0]->res->is_empty) } + sub keep_alive { my $self = shift; # Close my $req = $self->req; my $res = $self->res; - my $req_conn = lc($req->headers->connection || ''); - my $res_conn = lc($res->headers->connection || ''); + my $req_conn = lc(defined $req->headers->connection ? $req->headers->connection : ''); + my $res_conn = lc(defined $res->headers->connection ? $res->headers->connection : ''); return undef if $req_conn eq 'close' || $res_conn eq 'close'; - # Keep alive + # Keep-alive return 1 if $req_conn eq 'keep-alive' || $res_conn eq 'keep-alive'; - # No keep alive for 1.0 + # No keep-alive for 1.0 return !($req->version eq '1.0' || $res->version eq '1.0'); } +sub redirects { + my $previous = shift; + my @redirects; + unshift @redirects, $previous while $previous = $previous->previous; + return \@redirects; +} + sub server_read { my ($self, $chunk) = @_; @@ -48,7 +60,7 @@ sub server_read { # Generate response return unless $req->is_finished && !$self->{handled}++; $self->emit(upgrade => Mojo::Transaction::WebSocket->new(handshake => $self)) - if lc($req->headers->upgrade || '') eq 'websocket'; + if lc(defined $req->headers->upgrade ? $req->headers->upgrade : '') eq 'websocket'; $self->emit('request'); } @@ -57,10 +69,10 @@ sub server_write { shift->_write(1) } sub _body { my ($self, $msg, $finish) = @_; - # Prepare chunk + # Prepare body chunk my $buffer = $msg->get_body_chunk($self->{offset}); my $written = defined $buffer ? length $buffer : 0; - $self->{write} = $msg->is_dynamic ? 1 : ($self->{write} - $written); + $self->{write} = $msg->content->is_dynamic ? 1 : ($self->{write} - $written); $self->{offset} = $self->{offset} + $written; if (defined $buffer) { delete $self->{delay} } @@ -71,7 +83,7 @@ sub _body { } # Finished - $self->{state} = $finish ? 'finished' : 'read_response' + $self->{state} = $finish ? 'finished' : 'read' if $self->{write} <= 0 || (defined $buffer && !length $buffer); return defined $buffer ? $buffer : ''; @@ -80,24 +92,23 @@ sub _body { sub _headers { my ($self, $msg, $head) = @_; - # Prepare chunk + # Prepare header chunk my $buffer = $msg->get_header_chunk($self->{offset}); my $written = defined $buffer ? length $buffer : 0; $self->{write} = $self->{write} - $written; $self->{offset} = $self->{offset} + $written; - # Write body + # Switch to body if ($self->{write} <= 0) { $self->{offset} = 0; # Response without body - $head = $head && ($self->req->method eq 'HEAD' || $msg->is_empty); - if ($head) { $self->{state} = 'finished' } + if ($head && $self->is_empty) { $self->{state} = 'finished' } # Body else { - $self->{state} = 'write_body'; - $self->{write} = $msg->is_dynamic ? 1 : $msg->body_size; + $self->{http_state} = 'body'; + $self->{write} = $msg->content->is_dynamic ? 1 : $msg->body_size; } } @@ -107,17 +118,17 @@ sub _headers { sub _start_line { my ($self, $msg) = @_; - # Prepare chunk + # Prepare start line chunk my $buffer = $msg->get_start_line_chunk($self->{offset}); my $written = defined $buffer ? length $buffer : 0; $self->{write} = $self->{write} - $written; $self->{offset} = $self->{offset} + $written; - # Write headers + # Switch to headers if ($self->{write} <= 0) { - $self->{state} = 'write_headers'; - $self->{write} = $msg->header_size; - $self->{offset} = 0; + $self->{http_state} = 'headers'; + $self->{write} = $msg->header_size; + $self->{offset} = 0; } return $buffer; @@ -126,37 +137,42 @@ sub _start_line { sub _write { my ($self, $server) = @_; - # Start writing + # Client starts writing right away + $self->{state} ||= 'write' unless $server; + return '' unless $self->{state} eq 'write'; + + # Nothing written yet $self->{$_} ||= 0 for qw(offset write); my $msg = $server ? $self->res : $self->req; - if ($server ? ($self->{state} eq 'write') : !$self->{state}) { + unless ($self->{http_state}) { # Connection header my $headers = $msg->headers; $headers->connection($self->keep_alive ? 'keep-alive' : 'close') unless $headers->connection; - # Write start line - $self->{state} = 'write_start_line'; - $self->{write} = $msg->start_line_size; + # Switch to start line + $self->{http_state} = 'start_line'; + $self->{write} = $msg->start_line_size; } # Start line my $chunk = ''; - $chunk .= $self->_start_line($msg) if $self->{state} eq 'write_start_line'; + $chunk .= $self->_start_line($msg) if $self->{http_state} eq 'start_line'; # Headers - $chunk .= $self->_headers($msg, $server) - if $self->{state} eq 'write_headers'; + $chunk .= $self->_headers($msg, $server) if $self->{http_state} eq 'headers'; # Body - $chunk .= $self->_body($msg, $server) if $self->{state} eq 'write_body'; + $chunk .= $self->_body($msg, $server) if $self->{http_state} eq 'body'; return $chunk; } 1; +=encoding utf8 + =head1 NAME Mojo::Transaction::HTTP - HTTP transaction @@ -168,7 +184,7 @@ Mojo::Transaction::HTTP - HTTP transaction # Client my $tx = Mojo::Transaction::HTTP->new; $tx->req->method('GET'); - $tx->req->url->parse('http://mojolicio.us'); + $tx->req->url->parse('http://example.com'); $tx->req->headers->accept('application/json'); say $tx->res->code; say $tx->res->headers->content_type; @@ -240,7 +256,18 @@ object. =head1 ATTRIBUTES -L inherits all attributes from L. +L inherits all attributes from L +and implements the following new ones. + +=head2 previous + + my $previous = $tx->previous; + $tx = $tx->previous(Mojo::Transaction->new); + +Previous transaction that triggered this followup transaction. + + # Path of previous request + say $tx->previous->req->url->path; =head1 METHODS @@ -259,12 +286,28 @@ Read data client-side, used to implement user agents. Write data client-side, used to implement user agents. +=head2 is_empty + + my $success = $tx->is_empty; + +Check transaction for C request and C<1xx>, C<204> or C<304> response. + =head2 keep_alive my $success = $tx->keep_alive; Check if connection can be kept alive. +=head2 redirects + + my $redirects = $tx->redirects; + +Return a list of all previous transactions that preceded this followup +transaction. + + # Paths of all previous requests + say $_->req->url->path for @{$tx->redirects}; + =head2 server_read $tx->server_read($bytes); diff --git a/lib/Mojo/Transaction/WebSocket.pm b/lib/Mojo/Transaction/WebSocket.pm index 446cb24..e04b6a7 100644 --- a/lib/Mojo/Transaction/WebSocket.pm +++ b/lib/Mojo/Transaction/WebSocket.pm @@ -2,15 +2,17 @@ package Mojo::Transaction::WebSocket; use Mojo::Base 'Mojo::Transaction'; use Config; +use Mojo::JSON; use Mojo::Transaction::HTTP; use Mojo::Util qw(b64_encode decode encode sha1_bytes xor_encode); use constant DEBUG => $ENV{MOJO_WEBSOCKET_DEBUG} || 0; -# 64bit Perl -use constant MODERN => $Config{ivsize} > 4; +# Perl with support for quads +use constant MODERN => + ((defined $Config{use64bitint} ? $Config{use64bitint} : '') eq 'define' || $Config{longsize} >= 8); -# Unique value from the spec +# Unique value from RFC 6455 use constant GUID => '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'; # Opcodes @@ -72,7 +74,7 @@ sub build_frame { # Mask payload if ($masked) { - my $mask = pack 'N', int(rand 9999999); + my $mask = pack 'N', int(rand 9 x 7); $payload = $mask . xor_encode($payload, $mask x 128); } @@ -89,15 +91,13 @@ sub client_handshake { my $self = shift; my $headers = $self->req->headers; - $headers->upgrade('websocket') unless $headers->upgrade; - $headers->connection('Upgrade') unless $headers->connection; - $headers->sec_websocket_protocol('mojo') - unless $headers->sec_websocket_protocol; + $headers->upgrade('websocket') unless $headers->upgrade; + $headers->connection('Upgrade') unless $headers->connection; $headers->sec_websocket_version(13) unless $headers->sec_websocket_version; - # Generate WebSocket challenge - $headers->sec_websocket_key(b64_encode(pack('N*', int(rand 9999999)), '')) - unless $headers->sec_websocket_key; + # Generate 16 byte WebSocket challenge + my $challenge = b64_encode sprintf('%16u', int(rand 9 x 16)), ''; + $headers->sec_websocket_key($challenge) unless $headers->sec_websocket_key; } sub client_read { shift->server_read(@_) } @@ -107,7 +107,13 @@ sub connection { shift->handshake->connection } sub finish { my $self = shift; - $self->send([1, 0, 0, 0, CLOSE, ''])->{finished} = 1; + + my $close = $self->{close} = [@_]; + my $payload = $close->[0] ? pack('n', $close->[0]) : ''; + $payload .= encode 'UTF-8', $close->[1] if defined $close->[1]; + $close->[0] = defined $close->[0] ? $close->[0] : 1005; + $self->send([1, 0, 0, 0, CLOSE, $payload])->{finished} = 1; + return $self; } @@ -159,7 +165,7 @@ sub parse_frame { } # Check message size - $self->finish and return undef if $len > $self->max_websocket_size; + $self->finish(1009) and return undef if $len > $self->max_websocket_size; # Check if whole packet has arrived my $masked = vec($head, 1, 8) & 0b10000000; @@ -193,8 +199,12 @@ sub resume { sub send { my ($self, $frame, $cb) = @_; - # Binary or raw text if (ref $frame eq 'HASH') { + + # JSON + $frame->{text} = Mojo::JSON->new->encode($frame->{json}) if $frame->{json}; + + # Binary or raw text $frame = exists $frame->{text} ? [1, 0, 0, 0, TEXT, $frame->{text}] @@ -202,7 +212,8 @@ sub send { } # Text - elsif (!ref $frame) { $frame = [1, 0, 0, 0, TEXT, encode('UTF-8', $frame)] } + $frame = [1, 0, 0, 0, TEXT, encode('UTF-8', $frame)] + if ref $frame ne 'ARRAY'; $self->once(drain => $cb) if $cb; $self->{write} .= $self->build_frame(@$frame); @@ -211,14 +222,19 @@ sub send { return $self->emit('resume'); } +sub server_close { + my $self = shift; + $self->{state} = 'finished'; + return $self->emit(finish => $self->{close} ? (@{$self->{close}}) : 1006); +} + sub server_handshake { my $self = shift; - # WebSocket handshake my $res_headers = $self->res->code(101)->headers; $res_headers->upgrade('websocket')->connection('Upgrade'); my $req_headers = $self->req->headers; - ($req_headers->sec_websocket_protocol || '') =~ /^\s*([^,]+)/ + (defined $req_headers->sec_websocket_protocol ? $req_headers->sec_websocket_protocol : '') =~ /^\s*([^,]+)/ and $res_headers->sec_websocket_protocol($1); $res_headers->sec_websocket_accept( _challenge($req_headers->sec_websocket_key)); @@ -259,29 +275,35 @@ sub _message { return if $op == PONG; # Close - return $self->finish if $op == CLOSE; + if ($op == CLOSE) { + return $self->finish unless length $frame->[5] >= 2; + return $self->finish(unpack('n', substr($frame->[5], 0, 2, '')), + decode('UTF-8', $frame->[5])); + } # Append chunk and check message size $self->{op} = $op unless exists $self->{op}; $self->{message} .= $frame->[5]; - $self->finish and last + return $self->finish(1009) if length $self->{message} > $self->max_websocket_size; # No FIN bit (Continuation) return unless $frame->[0]; - # Message + # Whole message my $msg = delete $self->{message}; - if (delete $self->{op} == TEXT) { - $self->emit(text => $msg); - $msg = decode 'UTF-8', $msg if $msg; - } - else { $self->emit(binary => $msg); } - $self->emit(message => $msg); + $self->emit(json => Mojo::JSON->new->decode($msg)) + if $self->has_subscribers('json'); + $op = delete $self->{op}; + $self->emit($op == TEXT ? 'text' : 'binary' => $msg); + $self->emit(message => $op == TEXT ? decode('UTF-8', $msg) : $msg) + if $self->has_subscribers('message'); } 1; +=encoding utf8 + =head1 NAME Mojo::Transaction::WebSocket - WebSocket transaction @@ -298,15 +320,15 @@ Mojo::Transaction::WebSocket - WebSocket transaction say "Message: $msg"; }); $ws->on(finish => sub { - my $ws = shift; - say 'WebSocket closed.'; + my ($ws, $code, $reason) = @_; + say "WebSocket closed with status $code."; }); =head1 DESCRIPTION L is a container for WebSocket transactions as -described in RFC 6455. Note that 64bit frames require a Perl with 64bit -integer support, or they are limited to 32bit. +described in RFC 6455. Note that 64bit frames require a Perl with support for +quads or they are limited to 32bit. =head1 EVENTS @@ -341,6 +363,15 @@ Emitted once all data has been sent. $ws->send(time); }); +=head2 finish + + $ws->on(finish => sub { + my ($ws, $code, $reason) = @_; + ... + }); + +Emitted when transaction is finished. + =head2 frame $ws->on(frame => sub { @@ -361,6 +392,22 @@ Emitted when a WebSocket frame has been received. say "Payload: $frame->[5]"; }); +=head2 json + + $ws->on(json => sub { + my ($ws, $json) = @_; + ... + }); + +Emitted when a complete WebSocket message has been received, all text and +binary messages will be automatically JSON decoded. Note that this event only +gets emitted when it has at least one subscriber. + + $ws->on(json => sub { + my ($ws, $hash) = @_; + say "Message: $hash->{msg}"; + }); + =head2 message $ws->on(message => sub { @@ -369,7 +416,8 @@ Emitted when a WebSocket frame has been received. }); Emitted when a complete WebSocket message has been received, text messages -will be automatically decoded. +will be automatically decoded. Note that this event only gets emitted when it +has at least one subscriber. $ws->on(message => sub { my ($ws, $msg) = @_; @@ -416,7 +464,7 @@ Mask outgoing frames with XOR cipher and a random 32bit key. $ws = $ws->max_websocket_size(1024); Maximum WebSocket message size in bytes, defaults to the value of the -C environment variable or C<262144>. +MOJO_MAX_WEBSOCKET_SIZE environment variable or C<262144>. =head1 METHODS @@ -425,7 +473,7 @@ L and implements the following new ones. =head2 new - my $multi = Mojo::Content::MultiPart->new; + my $ws = Mojo::Transaction::WebSocket->new; Construct a new L object and subscribe to C event with default message parser, which also handles C and @@ -437,15 +485,15 @@ C frames automatically. Build WebSocket frame. - # Continuation frame with FIN bit and payload - say $ws->build_frame(1, 0, 0, 0, 0, 'World!'); - - # Text frame with payload - say $ws->build_frame(0, 0, 0, 0, 1, 'Hello'); - # Binary frame with FIN bit and payload say $ws->build_frame(1, 0, 0, 0, 2, 'Hello World!'); + # Text frame with payload but without FIN bit + say $ws->build_frame(0, 0, 0, 0, 1, 'Hello '); + + # Continuation frame with FIN bit and payload + say $ws->build_frame(1, 0, 0, 0, 0, 'World!'); + # Close frame with FIN bit and without payload say $ws->build_frame(1, 0, 0, 0, 8, ''); @@ -489,8 +537,10 @@ Connection identifier or socket. =head2 finish $ws = $ws->finish; + $ws = $ws->finish(1000); + $ws = $ws->finish(1003 => 'Cannot accept data!'); -Finish the WebSocket connection gracefully. +Close WebSocket connection gracefully. =head2 is_websocket @@ -565,6 +615,7 @@ Resume C transaction. $ws = $ws->send({binary => $bytes}); $ws = $ws->send({text => $bytes}); + $ws = $ws->send({json => {test => [1, 2, 3]}}); $ws = $ws->send([$fin, $rsv1, $rsv2, $rsv3, $op, $bytes]); $ws = $ws->send($chars); $ws = $ws->send($chars => sub {...}); @@ -575,6 +626,12 @@ will be invoked once all data has been written. # Send "Ping" frame $ws->send([1, 0, 0, 0, 9, 'Hello World!']); +=head2 server_close + + $ws->server_close; + +Transaction closed server-side, used to implement web servers. + =head2 server_handshake $ws->server_handshake; @@ -595,8 +652,8 @@ Write data server-side, used to implement web servers. =head1 DEBUGGING -You can set the C environment variable to get some -advanced diagnostics information printed to C. +You can set the MOJO_WEBSOCKET_DEBUG environment variable to get some advanced +diagnostics information printed to C. MOJO_WEBSOCKET_DEBUG=1 diff --git a/lib/Mojo/URL.pm b/lib/Mojo/URL.pm index ba442e1..d1b2c5f 100644 --- a/lib/Mojo/URL.pm +++ b/lib/Mojo/URL.pm @@ -1,9 +1,6 @@ package Mojo::URL; use Mojo::Base -base; -use overload - 'bool' => sub {1}, - '""' => sub { shift->to_string }, - fallback => 1; +use overload bool => sub {1}, '""' => sub { shift->to_string }, fallback => 1; use Mojo::Parameters; use Mojo::Path; @@ -15,10 +12,11 @@ has [qw(fragment host port scheme userinfo)]; sub new { shift->SUPER::new->parse(@_) } sub authority { - my ($self, $authority) = @_; + my $self = shift; # New authority - if (defined $authority) { + if (@_) { + return $self unless defined(my $authority = shift); # Userinfo $authority =~ s/^([^\@]+)\@// and $self->userinfo(url_unescape $1); @@ -32,10 +30,11 @@ sub authority { } # Build authority - my $userinfo = $self->userinfo; - $authority .= url_escape($userinfo, '^A-Za-z0-9\-._~!$&\'()*+,;=:') . '@' - if $userinfo; - $authority .= defined $self->ihost ? $self->ihost : ''; + return undef unless defined(my $authority = $self->ihost); + if (my $userinfo = $self->userinfo) { + $userinfo = url_escape $userinfo, '^A-Za-z0-9\-._~!$&\'()*+,;=:'; + $authority = $userinfo . '@' . $authority; + } if (my $port = $self->port) { $authority .= ":$port" } return $authority; @@ -44,14 +43,10 @@ sub authority { sub clone { my $self = shift; - my $clone = Mojo::URL->new; - $clone->scheme($self->scheme); - $clone->userinfo($self->userinfo); - $clone->host($self->host); - $clone->port($self->port); + my $clone = $self->new; + $clone->$_($self->$_) for qw(scheme userinfo host port fragment); $clone->path($self->path->clone); $clone->query($self->query->clone); - $clone->fragment($self->fragment); $clone->base($self->base->clone) if $self->{base}; return $clone; @@ -66,11 +61,11 @@ sub ihost { if @_; # Check if host needs to be encoded - return undef unless my $host = $self->host; + return undef unless defined(my $host = $self->host); return lc $host unless $host =~ /[^\x00-\x7f]/; # Encode - return join '.', + return lc join '.', map { /[^\x00-\x7f]/ ? ('xn--' . punycode_encode $_) : $_ } split /\./, $host; } @@ -81,25 +76,20 @@ sub parse { my ($self, $url) = @_; return $self unless $url; - # Official regex - $url =~ m!(?:([^:/?#]+):)?(?://([^/?#]*))?([^?#]*)(?:\?([^#]*))?(?:#(.*))?!; - $self->scheme($1); - $self->authority($2); - $self->path->parse($3); - $self->query($4); - $self->fragment($5); - - return $self; + # Official regex from RFC 3986 + $url =~ m!^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?!; + return $self->scheme($2)->authority($4)->path($5)->query($7)->fragment($9); } sub path { - my ($self, $path) = @_; + my $self = shift; # Old path $self->{path} ||= Mojo::Path->new; - return $self->{path} unless $path; + return $self->{path} unless @_; # New path + my $path = shift; $self->{path} = ref $path ? $path : $self->{path}->merge($path); return $self; @@ -200,32 +190,24 @@ sub to_rel { sub to_string { my $self = shift; - # Protocol + # Scheme my $url = ''; - if (my $proto = $self->protocol) { $url .= "$proto://" } + if (my $proto = $self->protocol) { $url .= "$proto:" } # Authority my $authority = $self->authority; - $url .= $url ? $authority : $authority ? "//$authority" : ''; - - # Relative path - my $path = $self->path; - if (!$url) { $url .= "$path" } + $url .= "//$authority" if defined $authority; - # Absolute path - elsif ($path->leading_slash) { $url .= "$path" } - else { $url .= @{$path->parts} ? "/$path" : '' } + # Path + my $path = $self->path->to_string; + $url .= !$authority || $path eq '' || $path =~ m!^/! ? $path : "/$path"; # Query - my $query = join '', $self->query; - $url .= "?$query" if length $query; + if (length(my $query = $self->query->to_string)) { $url .= "?$query" } # Fragment - my $fragment = $self->fragment; - $url .= '#' . url_escape $fragment, '^A-Za-z0-9\-._~!$&\'()*+,;=%:@/?' - if $fragment; - - return $url; + return $url unless defined(my $fragment = $self->fragment); + return $url . '#' . url_escape $fragment, '^A-Za-z0-9\-._~!$&\'()*+,;=%:@/?'; } 1; @@ -242,7 +224,7 @@ Mojo::URL - Uniform Resource Locator # Parse my $url - = Mojo::URL->new('http://sri:foobar@kraih.com:3000/foo/bar?foo=bar#23'); + = Mojo::URL->new('http://sri:foobar@example.com:3000/foo/bar?foo=bar#23'); say $url->scheme; say $url->userinfo; say $url->host; @@ -255,10 +237,9 @@ Mojo::URL - Uniform Resource Locator my $url = Mojo::URL->new; $url->scheme('http'); $url->userinfo('sri:foobar'); - $url->host('kraih.com'); + $url->host('example.com'); $url->port(3000); $url->path('/foo/bar'); - $url->path('baz'); $url->query->param(foo => 'bar'); $url->fragment(23); say "$url"; @@ -324,7 +305,7 @@ following new ones. my $url = Mojo::URL->new; my $url = Mojo::URL->new('http://127.0.0.1:3000/foo?f=b&baz=2#foo'); -Construct a new L object. +Construct a new L object and C URL if necessary. =head2 authority @@ -359,7 +340,16 @@ Check if URL is absolute. $url = $url->parse('http://127.0.0.1:3000/foo/bar?fo=o&baz=23#foo'); -Parse URL. +Parse relative or absolute URL. + + # "/test/123" + $url->parse('/test/123?foo=bar')->path; + + # "example.com" + $url->parse('http://example.com/test/123?foo=bar')->host; + + # "sri@example.com" + $url->parse('mailto:sri@example.com')->path; =head2 path @@ -368,17 +358,17 @@ Parse URL. $url = $url->path('foo/bar'); $url = $url->path(Mojo::Path->new); -Path part of this URL, relative paths will be appended to the existing path, +Path part of this URL, relative paths will be merged with the existing path, defaults to a L object. - # "http://mojolicio.us/DOM/HTML" - Mojo::URL->new('http://mojolicio.us/perldoc/Mojo')->path('/DOM/HTML'); + # "http://example.com/DOM/HTML" + Mojo::URL->new('http://example.com/perldoc/Mojo')->path('/DOM/HTML'); - # "http://mojolicio.us/perldoc/DOM/HTML" - Mojo::URL->new('http://mojolicio.us/perldoc/Mojo')->path('DOM/HTML'); + # "http://example.com/perldoc/DOM/HTML" + Mojo::URL->new('http://example.com/perldoc/Mojo')->path('DOM/HTML'); - # "http://mojolicio.us/perldoc/Mojo/DOM/HTML" - Mojo::URL->new('http://mojolicio.us/perldoc/Mojo/')->path('DOM/HTML'); + # "http://example.com/perldoc/Mojo/DOM/HTML" + Mojo::URL->new('http://example.com/perldoc/Mojo/')->path('DOM/HTML'); =head2 protocol @@ -387,7 +377,7 @@ defaults to a L object. Normalized version of C. # "http" - Mojo::URL->new('HtTp://mojolicio.us')->protocol; + Mojo::URL->new('HtTp://example.com')->protocol; =head2 query @@ -397,44 +387,45 @@ Normalized version of C. $url = $url->query({append => 'to'}); $url = $url->query(Mojo::Parameters->new); -Query part of this URL, defaults to a L object. +Query part of this URL, pairs in an array will be merged and pairs in a hash +appended, defaults to a L object. # "2" - Mojo::URL->new('http://mojolicio.us?a=1&b=2')->query->param('b'); + Mojo::URL->new('http://example.com?a=1&b=2')->query->param('b'); - # "http://mojolicio.us?a=2&c=3" - Mojo::URL->new('http://mojolicio.us?a=1&b=2')->query(a => 2, c => 3); + # "http://example.com?a=2&c=3" + Mojo::URL->new('http://example.com?a=1&b=2')->query(a => 2, c => 3); - # "http://mojolicio.us?a=2&a=3" - Mojo::URL->new('http://mojolicio.us?a=1&b=2')->query(a => [2, 3]); + # "http://example.com?a=2&a=3" + Mojo::URL->new('http://example.com?a=1&b=2')->query(a => [2, 3]); - # "http://mojolicio.us?a=2&b=2&c=3" - Mojo::URL->new('http://mojolicio.us?a=1&b=2')->query([a => 2, c => 3]); + # "http://example.com?a=2&b=2&c=3" + Mojo::URL->new('http://example.com?a=1&b=2')->query([a => 2, c => 3]); - # "http://mojolicio.us?b=2" - Mojo::URL->new('http://mojolicio.us?a=1&b=2')->query([a => undef]); + # "http://example.com?b=2" + Mojo::URL->new('http://example.com?a=1&b=2')->query([a => undef]); - # "http://mojolicio.us?a=1&b=2&a=2&c=3" - Mojo::URL->new('http://mojolicio.us?a=1&b=2')->query({a => 2, c => 3}); + # "http://example.com?a=1&b=2&a=2&c=3" + Mojo::URL->new('http://example.com?a=1&b=2')->query({a => 2, c => 3}); =head2 to_abs my $abs = $url->to_abs; - my $abs = $url->to_abs(Mojo::URL->new('http://kraih.com/foo')); + my $abs = $url->to_abs(Mojo::URL->new('http://example.com/foo')); Clone relative URL and turn it into an absolute one. =head2 to_rel my $rel = $url->to_rel; - my $rel = $url->to_rel(Mojo::URL->new('http://kraih.com/foo')); + my $rel = $url->to_rel(Mojo::URL->new('http://example.com/foo')); Clone absolute URL and turn it into a relative one. =head2 to_string - my $string = $url->to_string; - my $string = "$url"; + my $str = $url->to_string; + my $str = "$url"; Turn URL into a string. diff --git a/lib/Mojo/Upload.pm b/lib/Mojo/Upload.pm index 1397054..9982e38 100644 --- a/lib/Mojo/Upload.pm +++ b/lib/Mojo/Upload.pm @@ -1,7 +1,6 @@ package Mojo::Upload; use Mojo::Base -base; -use Carp 'croak'; use Mojo::Asset::File; use Mojo::Headers; @@ -20,6 +19,8 @@ sub slurp { shift->asset->slurp } 1; +=encoding utf8 + =head1 NAME Mojo::Upload - Upload @@ -45,7 +46,8 @@ L implements the following attributes. my $asset = $upload->asset; $upload = $upload->asset(Mojo::Asset::File->new); -Asset containing the uploaded data, defaults to a L object. +Asset containing the uploaded data, usually a L or +L object. =head2 filename diff --git a/lib/Mojo/UserAgent.pm b/lib/Mojo/UserAgent.pm index 2a5c321..e8db8af 100644 --- a/lib/Mojo/UserAgent.pm +++ b/lib/Mojo/UserAgent.pm @@ -59,8 +59,6 @@ sub app_url { return Mojo::URL->new("$self->{proto}://localhost:$self->{port}/"); } -sub build_form_tx { shift->transactor->form(@_) } -sub build_json_tx { shift->transactor->json(@_) } sub build_tx { shift->transactor->tx(@_) } sub build_websocket_tx { shift->transactor->websocket(@_) } @@ -76,25 +74,17 @@ sub need_proxy { return !first { $host =~ /\Q$_\E$/ } @{$self->no_proxy || []}; } -sub post_form { - my $self = shift; - my $cb = ref $_[-1] eq 'CODE' ? pop : undef; - return $self->start($self->build_form_tx(@_), $cb); -} - -sub post_json { - my $self = shift; - my $cb = ref $_[-1] eq 'CODE' ? pop : undef; - return $self->start($self->build_json_tx(@_), $cb); -} - sub start { my ($self, $tx, $cb) = @_; + # Fork safety + unless (($self->{pid} = defined $self->{pid} ? $self->{pid} : $$) eq $$) { + $self->_cleanup; + delete $self->{$_} for qw(pid port); + } + # Non-blocking if ($cb) { - - # Start non-blocking warn "-- Non-blocking request (@{[$tx->req->url->to_abs]})\n" if DEBUG; unless ($self->{nb}) { croak 'Blocking request in progress' if keys %{$self->{connections}}; @@ -105,7 +95,7 @@ sub start { return $self->_start($tx, $cb); } - # Start blocking + # Blocking warn "-- Blocking request (@{[$tx->req->url->to_abs]})\n" if DEBUG; if ($self->{nb}) { croak 'Non-blocking requests in progress' if keys %{$self->{connections}}; @@ -113,9 +103,7 @@ sub start { $self->_cleanup; delete $self->{nb}; } - $self->_start($tx => sub { $tx = pop }); - - # Start event loop + $self->_start($tx => sub { shift->ioloop->stop; $tx = shift }); $self->ioloop->start; return $tx; @@ -146,10 +134,9 @@ sub _cache { for my $cached (@$old) { # Search for id/name and remove corrupted connections - if (!$found && ($cached->[1] eq $name || $cached->[0] eq $name)) { - my $stream = $loop->stream($cached->[1]); - if ($stream && !$stream->is_readable) { $found = $cached->[1] } - else { $loop->remove($cached->[1]) } + if (!$found && first { $_ eq $name } @$cached) { + next unless my $stream = $loop->stream($cached->[1]); + $stream->is_readable ? $stream->close : ($found = $cached->[1]); } # Requeue @@ -166,7 +153,7 @@ sub _cleanup { # Clean up active connections (by closing them) $self->_handle($_ => 1) for keys %{$self->{connections} || {}}; - # Clean up keep alive connections + # Clean up keep-alive connections $loop->remove($_->[1]) for @{delete $self->{cache} || []}; # Stop server @@ -214,19 +201,19 @@ sub _connect_proxy { $new => sub { my ($self, $tx) = @_; - # CONNECT failed - unless ((defined $tx->res->code ? $tx->res->code : '') eq '200') { + # CONNECT failed (connection needs to be kept alive) + unless ($tx->keep_alive && $tx->res->is_status_class(200)) { $old->req->error('Proxy connection failed'); return $self->_finish($old, $cb); } # Prevent proxy reassignment and start real transaction $old->req->proxy(0); - return $self->_start($old->connection($tx->connection), $cb) + my $id = $tx->connection; + return $self->_start($old->connection($id), $cb) unless $tx->req->url->protocol eq 'https'; # TLS upgrade - return unless my $id = $tx->connection; my $loop = $self->_loop; my $handle = $loop->stream($id)->steal_handle; my $c = delete $self->{connections}{$id}; @@ -300,28 +287,24 @@ sub _finish { my $res = $tx->res; if (my $err = $res->error) { $res->error($err) } - else { - - # Premature connection close - if ($close && !$res->code) { $res->error('Premature connection close') } + # Premature connection close + elsif ($close && !$res->code) { $res->error('Premature connection close') } - # 400/500 - elsif ($res->is_status_class(400) || $res->is_status_class(500)) { - $res->error($res->message, $res->code); - } + # 400/500 + elsif ($res->is_status_class(400) || $res->is_status_class(500)) { + $res->error($res->message, $res->code); } - # Stop event loop if necessary $self->$cb($tx); - $self->ioloop->stop unless $self->{nb}; } sub _handle { my ($self, $id, $close) = @_; # Remove request timeout + return unless my $loop = $self->_loop; my $c = $self->{connections}{$id}; - $self->_loop->remove($c->{timeout}) if $c->{timeout}; + $loop->remove($c->{timeout}) if $c->{timeout}; # Finish WebSocket my $old = $c->{tx}; @@ -336,7 +319,7 @@ sub _handle { if (my $jar = $self->cookie_jar) { $jar->extract($old) } $old->client_close; $self->_finish($new, $c->{cb}); - $new->client_read($old->res->leftovers); + $new->client_read($old->res->content->leftovers); } # Finish normal connection @@ -352,7 +335,7 @@ sub _handle { } } -sub _loop { $_[0]->{nb} ? Mojo::IOLoop->singleton : $_[0]->ioloop } +sub _loop { $_[0]{nb} ? Mojo::IOLoop->singleton : $_[0]->ioloop } sub _read { my ($self, $id, $chunk) = @_; @@ -373,26 +356,21 @@ sub _remove { # Close connection my $tx = (delete($self->{connections}{$id}) || {})->{tx}; - unless (!$close && $tx && $tx->keep_alive && !$tx->error) { + if ($close || !$tx || !$tx->keep_alive || $tx->error) { $self->_cache($id); return $self->_loop->remove($id); } - # Keep connection alive + # Keep connection alive (CONNECT requests get upgraded) $self->_cache(join(':', $self->transactor->endpoint($tx)), $id) - unless $tx->req->method eq 'CONNECT' && (defined $tx->res->code ? $tx->res->code : '') eq '200'; + unless uc $tx->req->method eq 'CONNECT'; } sub _redirect { my ($self, $c, $old) = @_; - - # Follow redirect unless the maximum has been reached already return undef unless my $new = $self->transactor->redirect($old); - my $redirects = delete $c->{redirects} || 0; - return undef unless $redirects < $self->max_redirects; - my $id = $self->_start($new, delete $c->{cb}); - - return $self->{connections}{$id}{redirects} = $redirects + 1; + return undef unless @{$old->redirects} < $self->max_redirects; + return $self->_start($new, delete $c->{cb}); } sub _server { @@ -421,7 +399,8 @@ sub _start { my $url = $req->url; if ($self->{port} || !$url->is_abs) { if (my $app = $self->app) { $self->_server->app($app) } - $url = $req->url($url->base($self->app_url)->to_abs)->url + my $base = $self->app_url; + $url->scheme($base->scheme)->authority($base->authority) unless $url->is_abs; } @@ -442,15 +421,15 @@ sub _start { # We identify ourselves and accept gzip compression my $headers = $req->headers; $headers->user_agent($self->name) unless $headers->user_agent; - $headers->accept_encoding('gzip') if (! $headers->accept_encoding && $Compress::Raw::Zlib::VERSION); + $headers->accept_encoding('gzip') unless $headers->accept_encoding; if (my $jar = $self->cookie_jar) { $jar->inject($tx) } # Connect and add request timeout if necessary my $id = $self->emit(start => $tx)->_connection($tx, $cb); - if (my $t = $self->request_timeout) { + if (my $timeout = $self->request_timeout) { weaken $self; $self->{connections}{$id}{timeout} = $self->_loop->timer( - $t => sub { $self->_error($id => 'Request timeout') }); + $timeout => sub { $self->_error($id => 'Request timeout') }); } return $id; @@ -504,7 +483,7 @@ Mojo::UserAgent - Non-blocking I/O HTTP and WebSocket user agent say $ua->get('www.☃.net?hello=there' => {DNT => 1})->res->body; # Form POST with exception handling - my $tx = $ua->post_form('search.cpan.org/search' => {q => 'mojo'}); + my $tx = $ua->post('https://metacpan.org/search' => form => {q => 'mojo'}); if (my $res = $tx->success) { say $res->body } else { my ($err, $code) = $tx->error; @@ -512,15 +491,14 @@ Mojo::UserAgent - Non-blocking I/O HTTP and WebSocket user agent } # Quick JSON API request with Basic authentication - say $ua->get('https://sri:s3cret@search.twitter.com/search.json?q=perl') - ->res->json('/results/0/text'); + say $ua->get('https://sri:s3cret@example.com/search.json?q=perl') + ->res->json('/results/0/title'); # Extract data from HTML and XML resources - say $ua->get('mojolicio.us')->res->dom->html->head->title->text; + say $ua->get('www.perl.org')->res->dom->html->head->title->text; # Scrape the latest headlines from a news site - say $ua->max_redirects(5)->get('www.reddit.com/r/perl/') - ->res->dom('p.title > a.title')->pluck('text')->shuffle; + say $ua->get('perlnews.org')->res->dom('h2 > a')->text->shuffle; # IPv6 PUT request with content my $tx @@ -532,20 +510,15 @@ Mojo::UserAgent - Non-blocking I/O HTTP and WebSocket user agent # TLS certificate authentication and JSON POST my $tx = $ua->cert('tls.crt')->key('tls.key') - ->post_json('https://mojolicio.us' => {top => 'secret'}); - - # Custom JSON PUT request - my $tx = $ua->build_json_tx('http://mojolicious/foo' => {hi => 'there'}); - $tx->req->method('PUT'); - say $ua->start($tx)->res->body; + ->post('https://example.com' => json => {top => 'secret'}); # Blocking parallel requests (does not work inside a running event loop) my $delay = Mojo::IOLoop->delay; for my $url ('mojolicio.us', 'cpan.org') { - $delay->begin; + my $end = $delay->begin(0); $ua->get($url => sub { my ($ua, $tx) = @_; - $delay->end($tx->res->dom->at('title')->text); + $end->($tx->res->dom->at('title')->text); }); } my @titles = $delay->wait; @@ -556,37 +529,43 @@ Mojo::UserAgent - Non-blocking I/O HTTP and WebSocket user agent ... }); for my $url ('mojolicio.us', 'cpan.org') { - $delay->begin; + my $end = $delay->begin(0); $ua->get($url => sub { my ($ua, $tx) = @_; - $delay->end($tx->res->dom->at('title')->text); + $end->($tx->res->dom->at('title')->text); }); } $delay->wait unless Mojo::IOLoop->is_running; - # Non-blocking WebSocket connection - $ua->websocket('ws://websockets.org:8787' => sub { + # Non-blocking WebSocket connection sending and receiving JSON messages + $ua->websocket('ws://example.com/echo.json' => sub { my ($ua, $tx) = @_; - $tx->on(finish => sub { say 'WebSocket closed.' }); - $tx->on(message => sub { - my ($tx, $msg) = @_; - say "WebSocket message: $msg"; + say 'WebSocket handshake failed!' and return unless $tx->is_websocket; + $tx->on(json => sub { + my ($tx, $hash) = @_; + say "WebSocket message via JSON: $hash->{msg}"; $tx->finish; }); - $tx->send('hi there!'); + $tx->send({json => {msg => 'Hello World!'}}); }); Mojo::IOLoop->start unless Mojo::IOLoop->is_running; =head1 DESCRIPTION L is a full featured non-blocking I/O HTTP and WebSocket user -agent, with C, C, C, C, C (long polling), C -compression and multiple event loop support. +agent, with IPv6, TLS, SNI, IDNA, Comet (long polling), keep-alive, connection +pooling, timeout, cookie, multipart, proxy, gzip compression and multiple +event loop support. + +All connections will be reset automatically if a new process has been forked, +this allows multiple processes to share the same L object +safely. -Optional modules L (4.0+), L (0.16+) and -L (1.75+) are supported transparently through -L, and used if installed. Individual features can also be -disabled with the C and C environment variables. +For better scalability (epoll, kqueue) and to provide IPv6 as well as TLS +support, the optional modules L (4.0+), L (0.16+) and +L (1.75+) will be used automatically by L if +they are installed. Individual features can also be disabled with the +MOJO_NO_IPV6 and MOJO_NO_TLS environment variables. See L for more. @@ -634,7 +613,7 @@ L implements the following attributes. $ua = $ua->ca('/etc/tls/ca.crt'); Path to TLS certificate authority file, defaults to the value of the -C environment variable. Also activates hostname verification. +MOJO_CA_FILE environment variable. Also activates hostname verification. # Show certificate authorities for debugging IO::Socket::SSL::set_defaults( @@ -645,7 +624,7 @@ C environment variable. Also activates hostname verification. my $cert = $ua->cert; $ua = $ua->cert('/etc/tls/client.crt'); -Path to TLS certificate file, defaults to the value of the C +Path to TLS certificate file, defaults to the value of the MOJO_CERT_FILE environment variable. =head2 connect_timeout @@ -654,7 +633,7 @@ environment variable. $ua = $ua->connect_timeout(5); Maximum amount of time in seconds establishing a connection may take before -getting canceled, defaults to the value of the C +getting canceled, defaults to the value of the MOJO_CONNECT_TIMEOUT environment variable or C<10>. =head2 cookie_jar @@ -688,7 +667,7 @@ Proxy server to use for HTTPS and WebSocket requests. $ua = $ua->inactivity_timeout(15); Maximum amount of time in seconds a connection can be inactive before getting -closed, defaults to the value of the C environment +closed, defaults to the value of the MOJO_INACTIVITY_TIMEOUT environment variable or C<20>. Setting the value to C<0> will allow connections to be inactive indefinitely. @@ -705,8 +684,8 @@ L object. my $key = $ua->key; $ua = $ua->key('/etc/tls/client.crt'); -Path to TLS key file, defaults to the value of the C -environment variable. +Path to TLS key file, defaults to the value of the MOJO_KEY_FILE environment +variable. =head2 local_address @@ -720,7 +699,7 @@ Local address to bind to. my $max = $ua->max_connections; $ua = $ua->max_connections(5); -Maximum number of keep alive connections that the user agent will retain +Maximum number of keep-alive connections that the user agent will retain before it starts closing the oldest cached ones, defaults to C<5>. =head2 max_redirects @@ -729,8 +708,7 @@ before it starts closing the oldest cached ones, defaults to C<5>. $ua = $ua->max_redirects(3); Maximum number of redirects the user agent will follow before it fails, -defaults to the value of the C environment variable or -C<0>. +defaults to the value of the MOJO_MAX_REDIRECTS environment variable or C<0>. =head2 name @@ -753,7 +731,7 @@ Domains that don't require a proxy server to be used. Maximum amount of time in seconds establishing a connection, sending the request and receiving a whole response may take before getting canceled, -defaults to the value of the C environment variable or +defaults to the value of the MOJO_REQUEST_TIMEOUT environment variable or C<0>. Setting the value to C<0> will allow the user agent to wait indefinitely. The timeout will reset for every followed redirect. @@ -802,54 +780,47 @@ Get absolute L object for C and switch protocol if necessary. # Port currently used for processing relative URLs say $ua->app_url->port; -=head2 build_form_tx - - my $tx = $ua->build_form_tx('http://kraih.com' => {a => 'b'}); - my $tx = $ua->build_form_tx('kraih.com', 'UTF-8', {a => 'b'}, {DNT => 1}); - -Generate L object with -L. - -=head2 build_json_tx - - my $tx = $ua->build_json_tx('http://kraih.com' => {a => 'b'}); - my $tx = $ua->build_json_tx('kraih.com' => {a => 'b'} => {DNT => 1}); - -Generate L object with -L. - =head2 build_tx - my $tx = $ua->build_tx(GET => 'kraih.com'); - my $tx = $ua->build_tx(PUT => 'http://kraih.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->build_tx(GET => 'example.com'); + my $tx = $ua->build_tx(PUT => 'http://example.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->build_tx( + PUT => 'http://example.com' => {DNT => 1} => form => {a => 'b'}); + my $tx = $ua->build_tx( + PUT => 'http://example.com' => {DNT => 1} => json => {a => 'b'}); Generate L object with L. # Request with cookie - my $tx = $ua->build_tx(GET => 'kraih.com'); + my $tx = $ua->build_tx(GET => 'example.com'); $tx->req->cookies({name => 'foo', value => 'bar'}); $ua->start($tx); =head2 build_websocket_tx - my $tx = $ua->build_websocket_tx('ws://localhost:3000'); - my $tx = $ua->build_websocket_tx('ws://localhost:3000' => {DNT => 1}); + my $tx = $ua->build_websocket_tx('ws://example.com'); + my $tx = + $ua->build_websocket_tx('ws://example.com' => {DNT => 1} => ['v1.proto']); Generate L object with L. =head2 delete - my $tx = $ua->delete('kraih.com'); - my $tx = $ua->delete('http://kraih.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->delete('example.com'); + my $tx = $ua->delete('http://example.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->delete( + 'http://example.com' => {DNT => 1} => form => {a => 'b'}); + my $tx = $ua->delete( + 'http://example.com' => {DNT => 1} => json => {a => 'b'}); -Perform blocking HTTP C request and return resulting +Perform blocking DELETE request and return resulting L object, takes the same arguments as L (except for the method). You can also append a callback to perform requests non-blocking. - $ua->delete('http://kraih.com' => sub { + $ua->delete('http://example.com' => sub { my ($ua, $tx) = @_; say $tx->res->body; }); @@ -859,21 +830,23 @@ append a callback to perform requests non-blocking. $ua = $ua->detect_proxy; -Check environment variables C, C, C, -C, C and C for proxy information. Automatic -proxy detection can be enabled with the C environment variable. +Check environment variables HTTP_PROXY, http_proxy, HTTPS_PROXY, https_proxy, +NO_PROXY and no_proxy for proxy information. Automatic proxy detection can be +enabled with the MOJO_PROXY environment variable. =head2 get - my $tx = $ua->get('kraih.com'); - my $tx = $ua->get('http://kraih.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->get('example.com'); + my $tx = $ua->get('http://example.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->get('http://example.com' => {DNT => 1} => form => {a => 'b'}); + my $tx = $ua->get('http://example.com' => {DNT => 1} => json => {a => 'b'}); -Perform blocking HTTP C request and return resulting -L object, takes the same arguments as -L (except for the method). You can also -append a callback to perform requests non-blocking. +Perform blocking GET request and return resulting L +object, takes the same arguments as L +(except for the method). You can also append a callback to perform requests +non-blocking. - $ua->get('http://kraih.com' => sub { + $ua->get('http://example.com' => sub { my ($ua, $tx) = @_; say $tx->res->body; }); @@ -881,15 +854,19 @@ append a callback to perform requests non-blocking. =head2 head - my $tx = $ua->head('kraih.com'); - my $tx = $ua->head('http://kraih.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->head('example.com'); + my $tx = $ua->head('http://example.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->head( + 'http://example.com' => {DNT => 1} => form => {a => 'b'}); + my $tx = $ua->head( + 'http://example.com' => {DNT => 1} => json => {a => 'b'}); -Perform blocking HTTP C request and return resulting -L object, takes the same arguments as -L (except for the method). You can also -append a callback to perform requests non-blocking. +Perform blocking HEAD request and return resulting L +object, takes the same arguments as L +(except for the method). You can also append a callback to perform requests +non-blocking. - $ua->head('http://kraih.com' => sub { + $ua->head('http://example.com' => sub { my ($ua, $tx) = @_; say $tx->res->body; }); @@ -897,21 +874,25 @@ append a callback to perform requests non-blocking. =head2 need_proxy - my $success = $ua->need_proxy('intranet.mojolicio.us'); + my $success = $ua->need_proxy('intranet.example.com'); Check if request for domain would use a proxy server. =head2 options - my $tx = $ua->options('kraih.com'); - my $tx = $ua->options('http://kraih.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->options('example.com'); + my $tx = $ua->options('http://example.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->options( + 'http://example.com' => {DNT => 1} => form => {a => 'b'}); + my $tx = $ua->options( + 'http://example.com' => {DNT => 1} => json => {a => 'b'}); -Perform blocking HTTP C request and return resulting +Perform blocking OPTIONS request and return resulting L object, takes the same arguments as L (except for the method). You can also append a callback to perform requests non-blocking. - $ua->options('http://kraih.com' => sub { + $ua->options('http://example.com' => sub { my ($ua, $tx) = @_; say $tx->res->body; }); @@ -919,15 +900,19 @@ append a callback to perform requests non-blocking. =head2 patch - my $tx = $ua->patch('kraih.com'); - my $tx = $ua->patch('http://kraih.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->patch('example.com'); + my $tx = $ua->patch('http://example.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->patch( + 'http://example.com' => {DNT => 1} => form => {a => 'b'}); + my $tx = $ua->patch( + 'http://example.com' => {DNT => 1} => json => {a => 'b'}); -Perform blocking HTTP C request and return resulting -L object, takes the same arguments as -L (except for the method). You can also -append a callback to perform requests non-blocking. +Perform blocking PATCH request and return resulting L +object, takes the same arguments as L +(except for the method). You can also append a callback to perform requests +non-blocking. - $ua->patch('http://kraih.com' => sub { + $ua->patch('http://example.com' => sub { my ($ua, $tx) = @_; say $tx->res->body; }); @@ -935,47 +920,19 @@ append a callback to perform requests non-blocking. =head2 post - my $tx = $ua->post('kraih.com'); - my $tx = $ua->post('http://kraih.com' => {DNT => 1} => 'Hi!'); - -Perform blocking HTTP C request and return resulting -L object, takes the same arguments as -L (except for the method). You can also -append a callback to perform requests non-blocking. - - $ua->post('http://kraih.com' => sub { - my ($ua, $tx) = @_; - say $tx->res->body; - }); - Mojo::IOLoop->start unless Mojo::IOLoop->is_running; - -=head2 post_form - - my $tx = $ua->post_form('http://kraih.com' => {a => 'b'}); - my $tx = $ua->post_form('kraih.com', 'UTF-8', {a => 'b'}, {DNT => 1}); + my $tx = $ua->post('example.com'); + my $tx = $ua->post('http://example.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->post( + 'http://example.com' => {DNT => 1} => form => {a => 'b'}); + my $tx = $ua->post( + 'http://example.com' => {DNT => 1} => json => {a => 'b'}); -Perform blocking HTTP C request with form data and return resulting -L object, takes the same arguments as -L. You can also append a callback to -perform requests non-blocking. - - $ua->post_form('http://kraih.com' => {q => 'test'} => sub { - my ($ua, $tx) = @_; - say $tx->res->body; - }); - Mojo::IOLoop->start unless Mojo::IOLoop->is_running; - -=head2 post_json - - my $tx = $ua->post_json('http://kraih.com' => {a => 'b'}); - my $tx = $ua->post_json('kraih.com' => {a => 'b'} => {DNT => 1}); - -Perform blocking HTTP C request with JSON data and return resulting -L object, takes the same arguments as -L. You can also append a callback to -perform requests non-blocking. +Perform blocking POST request and return resulting L +object, takes the same arguments as L +(except for the method). You can also append a callback to perform requests +non-blocking. - $ua->post_json('http://kraih.com' => {q => 'test'} => sub { + $ua->post('http://example.com' => sub { my ($ua, $tx) = @_; say $tx->res->body; }); @@ -983,15 +940,17 @@ perform requests non-blocking. =head2 put - my $tx = $ua->put('kraih.com'); - my $tx = $ua->put('http://kraih.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->put('example.com'); + my $tx = $ua->put('http://example.com' => {DNT => 1} => 'Hi!'); + my $tx = $ua->put('http://example.com' => {DNT => 1} => form => {a => 'b'}); + my $tx = $ua->put('http://example.com' => {DNT => 1} => json => {a => 'b'}); -Perform blocking HTTP C request and return resulting -L object, takes the same arguments as -L (except for the method). You can also -append a callback to perform requests non-blocking. +Perform blocking PUT request and return resulting L +object, takes the same arguments as L +(except for the method). You can also append a callback to perform requests +non-blocking. - $ua->put('http://kraih.com' => sub { + $ua->put('http://example.com' => sub { my ($ua, $tx) = @_; say $tx->res->body; }); @@ -1004,7 +963,7 @@ append a callback to perform requests non-blocking. Perform blocking request. You can also append a callback to perform requests non-blocking. - my $tx = $ua->build_tx(GET => 'http://kraih.com'); + my $tx = $ua->build_tx(GET => 'http://example.com'); $ua->start($tx => sub { my ($ua, $tx) = @_; say $tx->res->body; @@ -1013,17 +972,26 @@ non-blocking. =head2 websocket - $ua->websocket('ws://localhost:3000' => sub {...}); - $ua->websocket('ws://localhost:3000' => {DNT => 1} => sub {...}); + $ua->websocket('ws://example.com' => sub {...}); + $ua->websocket( + 'ws://example.com' => {DNT => 1} => ['v1.proto'] => sub {...}); Open a non-blocking WebSocket connection with transparent handshake, takes the -same arguments as L. +same arguments as L. The callback +will receive either a L or +L object. - $ua->websocket('ws://localhost:3000/echo' => sub { + $ua->websocket('ws://example.com/echo' => sub { my ($ua, $tx) = @_; + say 'WebSocket handshake failed!' and return unless $tx->is_websocket; + $tx->on(finish => sub { + my ($tx, $code, $reason) = @_; + say "WebSocket closed with status $code."; + }); $tx->on(message => sub { my ($tx, $msg) = @_; - say $msg; + say "WebSocket message: $msg"; + $tx->finish; }); $tx->send('Hi!'); }); @@ -1031,8 +999,8 @@ same arguments as L. =head1 DEBUGGING -You can set the C environment variable to get some -advanced diagnostics information printed to C. +You can set the MOJO_USERAGENT_DEBUG environment variable to get some advanced +diagnostics information printed to C. MOJO_USERAGENT_DEBUG=1 diff --git a/lib/Mojo/UserAgent/CookieJar.pm b/lib/Mojo/UserAgent/CookieJar.pm index 9420a7e..2701827 100644 --- a/lib/Mojo/UserAgent/CookieJar.pm +++ b/lib/Mojo/UserAgent/CookieJar.pm @@ -43,11 +43,11 @@ sub extract { for my $cookie (@{$tx->res->cookies}) { # Validate domain - my $host = lc $url->ihost; + my $host = $url->ihost; my $domain = lc(defined $cookie->domain ? $cookie->domain : $host); $domain =~ s/^\.//; - next unless $host eq $domain || $host =~ /\Q.$domain\E$/; - next if $host =~ /\.\d+$/; + next + if $host ne $domain && ($host !~ /\Q.$domain\E$/ || $host =~ /\.\d+$/); $cookie->domain($domain); # Validate path @@ -61,7 +61,7 @@ sub extract { sub find { my ($self, $url) = @_; - return unless my $domain = lc(defined $url->ihost ? $url->ihost : ''); + return unless my $domain = $url->ihost; my $path = $url->path->to_abs_string; my @found; while ($domain =~ /[^.]+\.[^.]+|localhost$/) { @@ -102,6 +102,8 @@ sub _path { $_[0] eq '/' || $_[0] eq $_[1] || $_[1] =~ m!^\Q$_[0]/! } 1; +=encoding utf8 + =head1 NAME Mojo::UserAgent::CookieJar - Cookie jar for HTTP user agents @@ -129,8 +131,8 @@ Mojo::UserAgent::CookieJar - Cookie jar for HTTP user agents =head1 DESCRIPTION -L is a minimalistic and relaxed cookie jar used by -L. +L is a minimalistic and relaxed cookie jar based +on RFC 6265 for L. =head1 ATTRIBUTES diff --git a/lib/Mojo/UserAgent/Transactor.pm b/lib/Mojo/UserAgent/Transactor.pm index d79f9be..271b118 100644 --- a/lib/Mojo/UserAgent/Transactor.pm +++ b/lib/Mojo/UserAgent/Transactor.pm @@ -13,6 +13,19 @@ use Mojo::Transaction::WebSocket; use Mojo::URL; use Mojo::Util 'encode'; +has generators => sub { {} }; + +sub new { + my $self = shift->SUPER::new(@_); + return $self->add_generator(form => \&_form)->add_generator(json => \&_json); +} + +sub add_generator { + my ($self, $name, $cb) = @_; + $self->generators->{$name} = $cb; + return $self; +} + sub endpoint { my ($self, $tx) = @_; @@ -25,54 +38,11 @@ sub endpoint { # Proxy for normal HTTP requests return $self->_proxy($tx, $proto, $host, $port) - if $proto eq 'http' && lc($req->headers->upgrade || '') ne 'websocket'; + if $proto eq 'http' && lc(defined $req->headers->upgrade ? $req->headers->upgrade : '') ne 'websocket'; return $proto, $host, $port; } -sub form { - my ($self, $url, $encoding) = (shift, shift, shift); - my $form = ref $encoding ? $encoding : shift; - $encoding = undef if ref $encoding; - - # Start with normal POST transaction - my $tx = $self->tx(POST => $url, @_); - - # Check for uploads and force multipart if necessary - my $multipart; - for my $value (map { ref $_ eq 'ARRAY' ? @$_ : $_ } values %$form) { - ++$multipart and last if ref $value eq 'HASH'; - } - my $req = $tx->req; - my $headers = $req->headers; - $headers->content_type('multipart/form-data') if $multipart; - - # Multipart - if ((defined $headers->content_type ? $headers->content_type : '') eq 'multipart/form-data') { - my $parts = $self->_multipart($encoding, $form); - $req->content( - Mojo::Content::MultiPart->new(headers => $headers, parts => $parts)); - } - - # Urlencoded - else { - $headers->content_type('application/x-www-form-urlencoded'); - my $p = Mojo::Parameters->new(map { $_ => $form->{$_} } sort keys %$form); - $p->charset($encoding) if defined $encoding; - $req->body($p->to_string); - } - - return $tx; -} - -sub json { - my ($self, $url, $data) = (shift, shift, shift); - my $tx = $self->tx(POST => $url, @_, Mojo::JSON->new->encode($data)); - my $headers = $tx->req->headers; - $headers->content_type('application/json') unless $headers->content_type; - return $tx; -} - sub peer { my ($self, $tx) = @_; return $self->_proxy($tx, $self->endpoint($tx)); @@ -83,14 +53,14 @@ sub proxy_connect { # Already a CONNECT request my $req = $old->req; - return undef if $req->method eq 'CONNECT'; + return undef if uc $req->method eq 'CONNECT'; # No proxy return undef unless my $proxy = $req->proxy; # WebSocket and/or HTTPS my $url = $req->url; - my $upgrade = lc($req->headers->upgrade || ''); + my $upgrade = lc(defined $req->headers->upgrade ? $req->headers->upgrade : ''); return undef unless $upgrade eq 'websocket' || $url->protocol eq 'https'; # CONNECT request @@ -116,8 +86,8 @@ sub redirect { # Clone request if necessary my $new = Mojo::Transaction::HTTP->new; my $req = $old->req; - my $method = $req->method; - if (grep { $_ eq $code } 301, 307, 308) { + my $method = uc $req->method; + if ($code eq 301 || $code eq 307 || $code eq 308) { return undef unless my $req = $req->clone; $new->req($req); $req->headers->remove('Host')->remove('Cookie')->remove('Referer'); @@ -140,8 +110,14 @@ sub tx { # Headers $req->headers->from_hash(shift) if ref $_[0] eq 'HASH'; + # Generator + if (@_ > 1) { + return $tx unless my $generator = $self->generators->{shift()}; + $self->$generator($tx, @_); + } + # Body - $req->body(shift) if @_; + elsif (@_) { $req->body(shift) } return $tx; } @@ -158,11 +134,13 @@ sub websocket { my $self = shift; # New WebSocket transaction - my $tx = $self->tx(GET => @_); - my $req = $tx->req; - my $abs = $req->url->to_abs; - my $proto = $abs->protocol; - $req->url($abs->scheme($proto eq 'wss' ? 'https' : 'http')) if $proto; + my $sub = ref $_[-1] eq 'ARRAY' ? pop : []; + my $tx = $self->tx(GET => @_); + my $req = $tx->req; + $req->headers->sec_websocket_protocol(join ', ', @$sub) if @$sub; + my $url = $req->url; + my $proto = $url->protocol; + $url->scheme($proto eq 'wss' ? 'https' : 'http') if $proto; # Handshake Mojo::Transaction::WebSocket->new(handshake => $tx)->client_handshake; @@ -170,8 +148,48 @@ sub websocket { return $tx; } +sub _form { + my ($self, $tx, $form, %options) = @_; + + # Check for uploads and force multipart if necessary + my $multipart; + for my $value (map { ref $_ eq 'ARRAY' ? @$_ : $_ } values %$form) { + ++$multipart and last if ref $value eq 'HASH'; + } + my $req = $tx->req; + my $headers = $req->headers; + $headers->content_type('multipart/form-data') if $multipart; + + # Multipart + if ((defined $headers->content_type ? $headers->content_type : '') eq 'multipart/form-data') { + my $parts = $self->_multipart($options{charset}, $form); + $req->content( + Mojo::Content::MultiPart->new(headers => $headers, parts => $parts)); + return $tx; + } + + # Query parameters or urlencoded + my $p = Mojo::Parameters->new(map { $_ => $form->{$_} } sort keys %$form); + $p->charset($options{charset}) if defined $options{charset}; + my $method = uc $req->method; + if ($method eq 'GET' || $method eq 'HEAD') { $req->url->query->merge($p) } + else { + $req->body($p->to_string); + $headers->content_type('application/x-www-form-urlencoded'); + } + return $tx; +} + +sub _json { + my ($self, $tx, $data) = @_; + $tx->req->body(Mojo::JSON->new->encode($data)); + my $headers = $tx->req->headers; + $headers->content_type('application/json') unless $headers->content_type; + return $tx; +} + sub _multipart { - my ($self, $encoding, $form) = @_; + my ($self, $charset, $form) = @_; my @parts; for my $name (sort keys %$form) { @@ -199,18 +217,18 @@ sub _multipart { # Filename and headers $filename = delete $value->{filename} || $name; - $filename = encode $encoding, $filename if $encoding; + $filename = encode $charset, $filename if $charset; $headers->from_hash($value); } # Field else { - $value = encode $encoding, $value if $encoding; + $value = encode $charset, $value if $charset; $part->asset(Mojo::Asset::Memory->new->add_chunk($value)); } # Content-Disposition - $name = encode $encoding, $name if $encoding; + $name = encode $charset, $name if $charset; my $disposition = qq{form-data; name="$name"}; $disposition .= qq{; filename="$filename"} if $filename; $headers->content_disposition($disposition); @@ -235,6 +253,8 @@ sub _proxy { 1; +=encoding utf8 + =head1 NAME Mojo::UserAgent::Transactor - User agent transactor @@ -245,87 +265,56 @@ Mojo::UserAgent::Transactor - User agent transactor # Simple GET request my $t = Mojo::UserAgent::Transactor->new; - say $t->tx(GET => 'http://mojolicio.us')->req->to_string; + say $t->tx(GET => 'http://example.com')->req->to_string; # PATCH request with "Do Not Track" header and content - say $t->tx(PATCH => 'mojolicio.us' => {DNT => 1} => 'Hi!')->req->to_string; + say $t->tx(PATCH => 'example.com' => {DNT => 1} => 'Hi!')->req->to_string; - # POST request with form data - say $t->form('http://kraih.com' => {a => [1, 2], b => 3})->req->to_string; + # POST request with form-data + say $t->tx(POST => 'example.com' => form => {a => 'b'})->req->to_string; - # POST request with JSON data - say $t->json('http://kraih.com' => {a => [1, 2], b => 3})->req->to_string; + # PUT request with JSON data + say $t->tx(PUT => 'example.com' => json => {a => 'b'})->req->to_string; =head1 DESCRIPTION L is the transaction building and manipulation framework used by L. -=head1 METHODS +=head1 ATTRIBUTES -L inherits all methods from L and -implements the following new ones. +L implements the following attributes. -=head2 endpoint +=head2 generators - my ($proto, $host, $port) = $t->endpoint(Mojo::Transaction::HTTP->new); + my $generators = $t->generators; + $t = $t->generators({foo => sub {...}}); -Actual endpoint for transaction. +Registered content generators. -=head2 form - - my $tx = $t->form('kraih.com' => {a => 'b'}); - my $tx = $t->form('http://kraih.com' => {a => 'b'}); - my $tx = $t->form('http://kraih.com' => {a => [qw(b c d)]}); - my $tx = $t->form('http://kraih.com' => {mytext => {file => '/foo.txt'}}); - my $tx = $t->form('http://kraih.com' => {mytext => {content => 'lalala'}}); - my $tx = $t->form('http://kraih.com' => - {mytexts => [{content => 'first'}, {content => 'second'}]}); - my $tx = $t->form('http://kraih.com' => { - myzip => { - file => Mojo::Asset::Memory->new->add_chunk('lalala'), - filename => 'foo.zip', - DNT => 1 - } - }); - my $tx = $t->form('http://kraih.com' => 'UTF-8' => {a => 'b'}); - my $tx = $t->form('http://kraih.com' => {a => 'b'} => {DNT => 1}); - my $tx = $t->form('http://kraih.com', 'UTF-8', {a => 'b'}, {DNT => 1}); +=head1 METHODS + +L inherits all methods from L and +implements the following new ones. -Versatile L transaction builder for C requests -with form data. +=head2 new - # Multipart upload with filename - my $tx = $t->form( - 'mojolicio.us' => {fun => {content => 'Hello!', filename => 'test.txt'}}); + my $t = Mojo::UserAgent::Transactor->new; - # Multipart upload streamed from file - my $tx = $t->form('mojolicio.us' => {fun => {file => '/etc/passwd'}}); +Construct a new transactor and register C
and C content +generators. -While the "multipart/form-data" content type will be automatically used -instead of "application/x-www-form-urlencoded" when necessary, you can also -enforce it by setting the header manually. +=head2 add_generator - # Force multipart - my $tx = $t->form( - 'http://kraih.com/foo', - {a => 'b'}, - {'Content-Type' => 'multipart/form-data'} - ); + $t = $t->add_generator(foo => sub {...}); -=head2 json +Register a new content generator. - my $tx = $t->json('kraih.com' => {a => 'b'}); - my $tx = $t->json('http://kraih.com' => [1, 2, 3]); - my $tx = $t->json('http://kraih.com' => {a => 'b'} => {DNT => 1}); - my $tx = $t->json('http://kraih.com' => [1, 2, 3] => {DNT => 1}); +=head2 endpoint -Versatile L transaction builder for C requests -with JSON data. + my ($proto, $host, $port) = $t->endpoint(Mojo::Transaction::HTTP->new); - # Change method - my $tx = $t->json('mojolicio.us/hello', {hello => 'world'}); - $tx->req->method('PATCH'); +Actual endpoint for transaction. =head2 peer @@ -349,26 +338,92 @@ C<307> or C<308> redirect response if possible. =head2 tx - my $tx = $t->tx(GET => 'kraih.com'); - my $tx = $t->tx(POST => 'http://kraih.com'); - my $tx = $t->tx(GET => 'http://kraih.com' => {DNT => 1}); - my $tx = $t->tx(PUT => 'http://kraih.com' => 'Hi!'); - my $tx = $t->tx(POST => 'http://kraih.com' => {DNT => 1} => 'Hi!'); + my $tx = $t->tx(GET => 'example.com'); + my $tx = $t->tx(POST => 'http://example.com'); + my $tx = $t->tx(GET => 'http://example.com' => {DNT => 1}); + my $tx = $t->tx(PUT => 'http://example.com' => 'Hi!'); + my $tx = $t->tx(PUT => 'http://example.com' => form => {a => 'b'}); + my $tx = $t->tx(PUT => 'http://example.com' => json => {a => 'b'}); + my $tx = $t->tx(POST => 'http://example.com' => {DNT => 1} => 'Hi!'); + my $tx = $t->tx( + PUT => 'http://example.com' => {DNT => 1} => form => {a => 'b'}); + my $tx = $t->tx( + PUT => 'http://example.com' => {DNT => 1} => json => {a => 'b'}); Versatile general purpose L transaction builder for -requests. - - # Inspect generated request - say $t->tx(GET => 'mojolicio.us' => {DNT => 1} => 'Bye!')->req->to_string; +requests, with support for content generators. - # Streaming response - my $tx = $t->tx(GET => 'http://mojolicio.us'); - $tx->res->body(sub { say $_[1] }); + # Generate and inspect custom GET request with DNT header and content + say $t->tx(GET => 'example.com' => {DNT => 1} => 'Bye!')->req->to_string; - # Custom socket - my $tx = $t->tx(GET => 'http://mojolicio.us'); + # Use a custom socket for processing this transaction + my $tx = $t->tx(GET => 'http://example.com'); $tx->connection($sock); + # Stream response content to STDOUT + my $tx = $t->tx(GET => 'http://example.com'); + $tx->res->content->unsubscribe('read')->on(read => sub { say $_[1] }); + + # PUT request with content streamed from file + my $tx = $t->tx(PUT => 'http://example.com'); + $tx->req->content->asset(Mojo::Asset::File->new(path => '/foo.txt')); + + # GET request with query parameters + my $tx = $t->tx(GET => 'http://example.com' => form => {a => 'b'}); + + # POST request with "application/json" content + my $tx = $t->tx( + POST => 'http://example.com' => json => {a => 'b', c => [1, 2, 3]}); + + # POST request with "application/x-www-form-urlencoded" content + my $tx = $t->tx( + POST => 'http://example.com' => form => {a => 'b', c => 'd'}); + + # PUT request with UTF-8 encoded form values + my $tx = $t->tx( + PUT => 'http://example.com' => form => {a => 'b'} => charset => 'UTF-8'); + + # POST request with form values sharing the same name + my $tx = $t->tx(POST => 'http://example.com' => form => {a => [qw(b c d)]}); + + # POST request with "multipart/form-data" content + my $tx = $t->tx( + POST => 'http://example.com' => form => {mytext => {content => 'lala'}}); + + # POST request with upload streamed from file + my $tx = $t->tx( + POST => 'http://example.com' => form => {mytext => {file => '/foo.txt'}}); + + # POST request with upload streamed from asset + my $asset = Mojo::Asset::Memory->new->add_chunk('lalala'); + my $tx = $t->tx( + POST => 'http://example.com' => form => {mytext => {file => $asset}}); + + # POST request with multiple files sharing the same name + my $tx = $t->tx(POST => 'http://example.com' => + form => {mytext => [{content => 'first'}, {content => 'second'}]}); + + # POST request with form values and customized upload (filename and header) + my $tx = $t->tx(POST => 'http://example.com' => form => { + a => 'b', + c => 'd', + mytext => { + content => 'lalala', + filename => 'foo.txt', + 'Content-Type' => 'text/plain' + } + }); + +The C content generator will automatically use query parameters for +GET/HEAD requests and the "application/x-www-form-urlencoded" content type for +everything else. Both get upgraded automatically to using the +"multipart/form-data" content type when necessary or when the header has been +set manually. + + # Force "multipart/form-data" + my $headers = {'Content-Type' => 'multipart/form-data'}; + my $tx = $t->tx(POST => 'example.com' => $headers => form => {a => 'b'}); + =head2 upgrade my $tx = $t->upgrade(Mojo::Transaction::HTTP->new); @@ -378,8 +433,8 @@ handshake if possible. =head2 websocket - my $tx = $t->websocket('ws://localhost:3000'); - my $tx = $t->websocket('ws://localhost:3000' => {DNT => 1}); + my $tx = $t->websocket('ws://example.com'); + my $tx = $t->websocket('ws://example.com' => {DNT => 1} => ['v1.proto']); Versatile L transaction builder for WebSocket handshake requests. diff --git a/lib/Mojo/Util.pm b/lib/Mojo/Util.pm index 0adb3fc..c424009 100644 --- a/lib/Mojo/Util.pm +++ b/lib/Mojo/Util.pm @@ -1,13 +1,18 @@ package Mojo::Util; use Mojo::Base 'Exporter'; -use Carp 'croak'; +use Carp qw(carp croak); use Digest::MD5 qw(md5 md5_hex); -BEGIN {eval {require Digest::SHA; import Digest::SHA qw(sha1 sha1_hex)}} +BEGIN {eval {require Digest::SHA; import Digest::SHA qw(hmac_sha1 sha1 sha1_hex)}} use Encode 'find_encoding'; use File::Basename 'dirname'; use File::Spec::Functions 'catfile'; use MIME::Base64 qw(decode_base64 encode_base64); +use Time::HiRes (); + +# Check for monotonic clock support +use constant MONOTONIC => eval + '!!Time::HiRes::clock_gettime(Time::HiRes::CLOCK_MONOTONIC())'; # Punycode bootstring parameters use constant { @@ -23,46 +28,33 @@ use constant { # To update HTML5 entities run this command # perl examples/entities.pl > lib/Mojo/entities.txt my %ENTITIES; -{ - open my $entities, '<', catfile(dirname(__FILE__), 'entities.txt'); - for my $entity (<$entities>) { - next unless $entity =~ /^(\S+)\s+U\+(\S+)(?:\s+U\+(\S+))?/; - $ENTITIES{$1} = defined $3 ? (chr(hex $2) . chr(hex $3)) : chr(hex $2); - } +for my $line (split "\x0a", slurp(catfile dirname(__FILE__), 'entities.txt')) { + next unless $line =~ /^(\S+)\s+U\+(\S+)(?:\s+U\+(\S+))?/; + $ENTITIES{$1} = defined $3 ? (chr(hex $2) . chr(hex $3)) : chr(hex $2); } -# DEPRECATED in Rainbow! -my %REVERSE = ("\x{0027}" => '#39;'); -$REVERSE{$ENTITIES{$_}} = defined $REVERSE{$ENTITIES{$_}} ? $REVERSE{$ENTITIES{$_}} : $_ - for sort { @{[$a =~ /[A-Z]/g]} <=> @{[$b =~ /[A-Z]/g]} } - sort grep {/;/} keys %ENTITIES; - # Encoding cache my %CACHE; our @EXPORT_OK = ( qw(b64_decode b64_encode camelize class_to_file class_to_path decamelize), - qw(decode encode get_line hmac_md5_sum hmac_sha1_sum html_unescape), - qw(md5_bytes md5_sum monkey_patch punycode_decode punycode_encode quote), - qw(secure_compare sha1_bytes sha1_sum slurp spurt squish trim unquote), - qw(url_escape url_unescape xml_escape xor_encode) + qw(decode deprecated encode get_line hmac_sha1_sum html_unescape md5_bytes), + qw(md5_sum monkey_patch punycode_decode punycode_encode quote), + qw(secure_compare sha1_bytes sha1_sum slurp split_header spurt squish), + qw(steady_time trim unquote url_escape url_unescape xml_escape xor_encode) ); -# DEPRECATED in Rainbow! -push @EXPORT_OK, 'html_escape'; - sub b64_decode { decode_base64($_[0]) } - sub b64_encode { encode_base64($_[0], $_[1]) } sub camelize { - my $string = shift; - return $string if $string =~ /^[A-Z]/; + my $str = shift; + return $str if $str =~ /^[A-Z]/; - # Camel case words + # CamelCase words return join '::', map { join '', map { ucfirst lc } split /_/, $_ - } split /-/, $string; + } split /-/, $str; } sub class_to_file { @@ -75,14 +67,14 @@ sub class_to_file { sub class_to_path { join '.', join('/', split /::|'/, shift), 'pm' } sub decamelize { - my $string = shift; - return $string if $string !~ /^[A-Z]/; + my $str = shift; + return $str if $str !~ /^[A-Z]/; # Module parts my @parts; - for my $part (split /::/, $string) { + for my $part (split /::/, $str) { - # Snake case words + # snake_case words my @words; push @words, lc $1 while $part =~ s/([A-Z]{1}[^A-Z]*)//; push @parts, join '_', @words; @@ -98,6 +90,11 @@ sub decode { return $bytes; } +sub deprecated { + local $Carp::CarpLevel = 1; + $ENV{MOJO_FATAL_DEPRECATIONS} ? croak(@_) : carp(@_); +} + sub encode { _encoding($_[0])->encode("$_[1]") } sub get_line { @@ -112,26 +109,13 @@ sub get_line { return $line; } -sub hmac_md5_sum { _hmac(\&md5, @_) } -sub hmac_sha1_sum { _hmac(\&sha1, @_) } - -# DEPRECATED in Rainbow! -sub html_escape { - warn <html_escape is DEPRECATED in favor of Mojo::Util->xml_escape!!! -EOF - my ($string, $pattern) = @_; - $pattern ||= '^\n\r\t !#$%(-;=?-~'; - return $string unless $string =~ /[^$pattern]/; - $string =~ s/([$pattern])/_encode($1)/ge; - return $string; -} +sub hmac_sha1_sum { unpack 'H*', hmac_sha1(@_) } sub html_unescape { - my $string = shift; - $string - =~ s/&(?:\#((?:\d{1,7}|x[[:xdigit:]]{1,6}));|(\w+;?))/_decode($1, $2)/ge; - return $string; + my $str = shift; + return $str if index($str, '&') == -1; + $str =~ s/&(?:\#((?:\d{1,7}|x[0-9a-fA-F]{1,6}));|(\w+;?))/_decode($1, $2)/ge; + return $str; } sub md5_bytes { md5(@_) } @@ -149,36 +133,32 @@ sub punycode_decode { my $input = shift; use integer; - # Delimiter - my @output; - push @output, split //, $1 if $input =~ s/(.*)\x2d//s; - my $n = PC_INITIAL_N; my $i = 0; my $bias = PC_INITIAL_BIAS; + my @output; + + # Consume all code points before the last delimiter + push @output, split //, $1 if $input =~ s/(.*)\x2d//s; + while (length $input) { my $oldi = $i; my $w = 1; # Base to infinity in steps of base for (my $k = PC_BASE; 1; $k += PC_BASE) { - - # Digit my $digit = ord substr $input, 0, 1, ''; $digit = $digit < 0x40 ? $digit + (26 - 0x30) : ($digit & 0x1f) - 1; $i += $digit * $w; my $t = $k - $bias; $t = $t < PC_TMIN ? PC_TMIN : $t > PC_TMAX ? PC_TMAX : $t; last if $digit < $t; - - $w *= (PC_BASE - $t); + $w *= PC_BASE - $t; } - # Bias $bias = _adapt($i - $oldi, @output + 1, $oldi == 0); $n += $i / (@output + 1); $i = $i % (@output + 1); - splice @output, $i++, 0, chr $n; } @@ -190,35 +170,28 @@ sub punycode_encode { my $output = shift; use integer; - # Split input + my $n = PC_INITIAL_N; + my $delta = 0; + my $bias = PC_INITIAL_BIAS; + + # Extract basic code points my $len = length $output; my @input = map {ord} split //, $output; my @chars = sort grep { $_ >= PC_INITIAL_N } @input; - - # Handle non basic characters $output =~ s/[^\x00-\x7f]+//gs; my $h = my $b = length $output; $output .= "\x2d" if $b > 0; - my $n = PC_INITIAL_N; - my $delta = 0; - my $bias = PC_INITIAL_BIAS; for my $m (@chars) { - - # Basic character next if $m < $n; - - # Walk all code points in order $delta += ($m - $n) * ($h + 1); $n = $m; + for (my $i = 0; $i < $len; $i++) { my $c = $input[$i]; - # Basic character - $delta++ if $c < $n; - - # Non basic character - if ($c == $n) { + if ($c < $n) { $delta++ } + elsif ($c == $n) { my $q = $delta; # Base to infinity in steps of base @@ -226,18 +199,12 @@ sub punycode_encode { my $t = $k - $bias; $t = $t < PC_TMIN ? PC_TMIN : $t > PC_TMAX ? PC_TMAX : $t; last if $q < $t; - - # Code point for digit "t" my $o = $t + (($q - $t) % (PC_BASE - $t)); $output .= chr $o + ($o < 26 ? 0x61 : 0x30 - 26); - $q = ($q - $t) / (PC_BASE - $t); } - # Code point for digit "q" $output .= chr $q + ($q < 26 ? 0x61 : 0x30 - 26); - - # Bias $bias = _adapt($delta, $h + 1, $h == $b); $delta = 0; $h++; @@ -252,9 +219,9 @@ sub punycode_encode { } sub quote { - my $string = shift; - $string =~ s/(["\\])/\\$1/g; - return qq{"$string"}; + my $str = shift; + $str =~ s/(["\\])/\\$1/g; + return qq{"$str"}; } sub secure_compare { @@ -276,6 +243,26 @@ sub slurp { return $content; } +sub split_header { + my $str = shift; + + my (@tree, @token); + while ($str =~ s/^[,;\s]*([^=;, ]+)\s*//) { + push @token, $1, undef; + $token[-1] = unquote($1) + if $str =~ s/^=\s*("(?:\\\\|\\"|[^"])*"|[^;, ]*)\s*//; + + # Separator + $str =~ s/^;\s*//; + next unless $str =~ s/^,\s*//; + push @tree, [@token]; + @token = (); + } + + # Take care of final token + return [@token ? (@tree, \@token) : @tree]; +} + sub spurt { my ($content, $path) = @_; croak qq{Can't open file "$path": $!} unless open my $file, '>', $path; @@ -285,49 +272,55 @@ sub spurt { } sub squish { - my $string = trim(@_); - $string =~ s/\s+/ /g; - return $string; + my $str = trim(@_); + $str =~ s/\s+/ /g; + return $str; +} + +sub steady_time () { + MONOTONIC + ? Time::HiRes::clock_gettime(Time::HiRes::CLOCK_MONOTONIC()) + : Time::HiRes::time; } sub trim { - my $string = shift; - $string =~ s/^\s+|\s+$//g; - return $string; + my $str = shift; + $str =~ s/^\s+|\s+$//g; + return $str; } sub unquote { - my $string = shift; - return $string unless $string =~ s/^"(.*)"$/$1/g; - $string =~ s/\\\\/\\/g; - $string =~ s/\\"/"/g; - return $string; + my $str = shift; + return $str unless $str =~ s/^"(.*)"$/$1/g; + $str =~ s/\\\\/\\/g; + $str =~ s/\\"/"/g; + return $str; } sub url_escape { - my ($string, $pattern) = @_; + my ($str, $pattern) = @_; $pattern ||= '^A-Za-z0-9\-._~'; - $string =~ s/([$pattern])/sprintf('%%%02X',ord($1))/ge; - return $string; + $str =~ s/([$pattern])/sprintf('%%%02X',ord($1))/ge; + return $str; } sub url_unescape { - my $string = shift; - return $string if index($string, '%') == -1; - $string =~ s/%([[:xdigit:]]{2})/chr(hex($1))/ge; - return $string; + my $str = shift; + return $str if index($str, '%') == -1; + $str =~ s/%([0-9a-fA-F]{2})/chr(hex($1))/ge; + return $str; } sub xml_escape { - my $string = shift; + my $str = shift; - $string =~ s/&/&/g; - $string =~ s//>/g; - $string =~ s/"/"/g; - $string =~ s/'/'/g; + $str =~ s/&/&/g; + $str =~ s//>/g; + $str =~ s/"/"/g; + $str =~ s/'/'/g; - return $string; + return $str; } sub xor_encode { @@ -343,8 +336,8 @@ sub xor_encode { sub _adapt { my ($delta, $numpoints, $firsttime) = @_; - use integer; + $delta = $firsttime ? $delta / PC_DAMP : $delta / 2; $delta += $delta / $numpoints; my $k = 0; @@ -357,44 +350,28 @@ sub _adapt { } sub _decode { + my ($point, $name) = @_; - # Numeric - return substr($_[0], 0, 1) eq 'x' ? chr(hex $_[0]) : chr($_[0]) unless $_[1]; + # Code point + return chr($point !~ /^x/ ? $point : hex $point) unless defined $name; # Find entity name - my $rest = ''; - my $entity = $_[1]; - while (length $entity) { - return "$ENTITIES{$entity}$rest" if exists $ENTITIES{$entity}; - $rest = chop($entity) . $rest; + my $rest = ''; + while (length $name) { + return "$ENTITIES{$name}$rest" if exists $ENTITIES{$name}; + $rest = chop($name) . $rest; } - return "&$_[1]"; -} - -# DEPRECATED in Rainbow! -sub _encode { - return exists $REVERSE{$_[0]} ? "&$REVERSE{$_[0]}" : "&#@{[ord($_[0])]};"; + return "&$rest"; } sub _encoding { $CACHE{$_[0]} = defined $CACHE{$_[0]} ? $CACHE{$_[0]} : defined find_encoding($_[0]) ? find_encoding($_[0]) : croak "Unknown encoding '$_[0]'"; } -sub _hmac { - my ($hash, $string, $secret) = @_; - - # Secret - $secret = $secret ? "$secret" : 'Very insecure!'; - $secret = $hash->($secret) if length $secret > 64; - - # HMAC - my $ipad = $secret ^ (chr(0x36) x 64); - my $opad = $secret ^ (chr(0x5c) x 64); - return unpack 'H*', $hash->($opad . $hash->($ipad . $string)); -} - 1; +=encoding utf8 + =head1 NAME Mojo::Util - Portable utility functions @@ -403,8 +380,8 @@ Mojo::Util - Portable utility functions use Mojo::Util qw(b64_encode url_escape url_unescape); - my $string = 'test=23'; - my $escaped = url_escape $string; + my $str = 'test=23'; + my $escaped = url_escape $str; say url_unescape $escaped; say b64_encode $escaped, ''; @@ -418,22 +395,22 @@ L implements the following functions. =head2 b64_decode - my $string = b64_decode $b64; + my $bytes = b64_decode $b64; -Base64 decode string. +Base64 decode bytes. =head2 b64_encode - my $b64 = b64_encode $string; - my $b64 = b64_encode $string, "\n"; + my $b64 = b64_encode $bytes; + my $b64 = b64_encode $bytes, "\n"; -Base64 encode string, the line ending defaults to a newline. +Base64 encode bytes, the line ending defaults to a newline. =head2 camelize my $camelcase = camelize $snakecase; -Convert snake case string to camel case and replace C<-> with C<::>. +Convert snake_case string to CamelCase and replace C<-> with C<::>. # "FooBar" camelize 'foo_bar'; @@ -450,10 +427,17 @@ Convert snake case string to camel case and replace C<-> with C<::>. Convert a class name to a file. - Foo::Bar -> foo_bar - FOO::Bar -> foobar - FooBar -> foo_bar - FOOBar -> foobar + # "foo_bar" + class_to_file 'Foo::Bar'; + + # "foobar" + class_to_file 'FOO::Bar'; + + # "foo_bar" + class_to_file 'FooBar'; + + # "foobar" + class_to_file 'FOOBar'; =head2 class_to_path @@ -461,14 +445,17 @@ Convert a class name to a file. Convert class name to path. - Foo::Bar -> Foo/Bar.pm - FooBar -> FooBar.pm + # "Foo/Bar.pm" + class_to_path 'Foo::Bar'; + + # "FooBar.pm" + class_to_path 'FooBar'; =head2 decamelize my $snakecase = decamelize $camelcase; -Convert camel case string to snake case and replace C<::> with C<->. +Convert CamelCase string to snake_case and replace C<::> with C<->. # "foo_bar" decamelize 'FooBar'; @@ -485,6 +472,13 @@ Convert camel case string to snake case and replace C<::> with C<->. Decode bytes to characters and return C if decoding failed. +=head2 deprecated + + deprecated 'foo is DEPRECATED in favor of bar'; + +Warn about deprecated feature from perspective of caller. You can also set the +MOJO_FATAL_DEPRECATIONS environment variable to make them die instead. + =head2 encode my $bytes = encode 'UTF-8', $chars; @@ -493,40 +487,34 @@ Encode characters to bytes. =head2 get_line - my $line = get_line \$string; + my $line = get_line \$str; Extract whole line from string or return C. Lines are expected to end with C<0x0d 0x0a> or C<0x0a>. -=head2 hmac_md5_sum - - my $checksum = hmac_md5_sum $string, 'passw0rd'; - -Generate HMAC-MD5 checksum for string. - =head2 hmac_sha1_sum - my $checksum = hmac_sha1_sum $string, 'passw0rd'; + my $checksum = hmac_sha1_sum $bytes, 'passw0rd'; -Generate HMAC-SHA1 checksum for string. +Generate HMAC-SHA1 checksum for bytes. =head2 html_unescape - my $string = html_unescape $escaped; + my $str = html_unescape $escaped; Unescape all HTML entities in string. =head2 md5_bytes - my $checksum = md5_bytes $string; + my $checksum = md5_bytes $bytes; -Generate binary MD5 checksum for string. +Generate binary MD5 checksum for bytes. =head2 md5_sum - my $checksum = md5_sum $string; + my $checksum = md5_sum $bytes; -Generate MD5 checksum for string. +Generate MD5 checksum for bytes. =head2 monkey_patch @@ -542,39 +530,39 @@ Monkey patch functions into package. =head2 punycode_decode - my $string = punycode_decode $punycode; + my $str = punycode_decode $punycode; Punycode decode string. =head2 punycode_encode - my $punycode = punycode_encode $string; + my $punycode = punycode_encode $str; Punycode encode string. =head2 quote - my $quoted = quote $string; + my $quoted = quote $str; Quote string. =head2 secure_compare - my $success = secure_compare $string1, $string2; + my $success = secure_compare $str1, $str2; Constant time comparison algorithm to prevent timing attacks. =head2 sha1_bytes - my $checksum = sha1_bytes $string; + my $checksum = sha1_bytes $bytes; -Generate binary SHA1 checksum for string. +Generate binary SHA1 checksum for bytes. =head2 sha1_sum - my $checksum = sha1_sum $string; + my $checksum = sha1_sum $bytes; -Generate SHA1 checksum for string. +Generate SHA1 checksum for bytes. =head2 slurp @@ -582,6 +570,21 @@ Generate SHA1 checksum for string. Read all data at once from file. +=head2 split_header + + my $tree = split_header 'foo="bar baz"; test=123, yada'; + +Split HTTP header value. + + # "one" + split_header('one; two="three four", five=six')->[0][0]; + + # "three four" + split_header('one; two="three four", five=six')->[0][3]; + + # "five" + split_header('one; two="three four", five=six')->[1][0]; + =head2 spurt $content = spurt $content, '/etc/passwd'; @@ -590,46 +593,53 @@ Write all data at once to file. =head2 squish - my $squished = squish $string; + my $squished = squish $str; Trim whitespace characters from both ends of string and then change all consecutive groups of whitespace into one space each. +=head2 steady_time + + my $time = steady_time; + +High resolution time, resilient to time jumps if a monotonic clock is +available through L. + =head2 trim - my $trimmed = trim $string; + my $trimmed = trim $str; Trim whitespace characters from both ends of string. =head2 unquote - my $string = unquote $quoted; + my $str = unquote $quoted; Unquote string. =head2 url_escape - my $escaped = url_escape $string; - my $escaped = url_escape $string, '^A-Za-z0-9\-._~'; + my $escaped = url_escape $str; + my $escaped = url_escape $str, '^A-Za-z0-9\-._~'; Percent encode unsafe characters in string, the pattern used defaults to C<^A-Za-z0-9\-._~>. =head2 url_unescape - my $string = url_unescape $escaped; + my $str = url_unescape $escaped; Decode percent encoded characters in string. =head2 xml_escape - my $escaped = xml_escape $string; + my $escaped = xml_escape $str; Escape unsafe characters C<&>, C>, C>, C<"> and C<'> in string. =head2 xor_encode - my $encoded = xor_encode $string, $key; + my $encoded = xor_encode $str, $key; XOR encode string with variable length key. diff --git a/lib/Mojolicious.pm b/lib/Mojolicious.pm index 4293946..e5334c4 100644 --- a/lib/Mojolicious.pm +++ b/lib/Mojolicious.pm @@ -14,6 +14,7 @@ use Mojolicious::Sessions; use Mojolicious::Static; use Mojolicious::Types; use Scalar::Util qw(blessed weaken); +use Time::HiRes 'gettimeofday'; has commands => sub { my $commands = Mojolicious::Commands->new(app => shift); @@ -21,7 +22,7 @@ has commands => sub { return $commands; }; has controller_class => 'Mojolicious::Controller'; -has mode => sub { $ENV{MOJO_MODE} || 'development' }; +has mode => sub { $ENV{MOJO_MODE} || $ENV{PLACK_ENV} || 'development' }; has moniker => sub { decamelize ref shift }; has plugins => sub { Mojolicious::Plugins->new }; has renderer => sub { Mojolicious::Renderer->new }; @@ -32,29 +33,26 @@ has secret => sub { # Warn developers about insecure default $self->log->debug('Your secret passphrase needs to be changed!!!'); - # Default to application name - return ref $self; + # Default to moniker + return $self->moniker; }; has sessions => sub { Mojolicious::Sessions->new }; has static => sub { Mojolicious::Static->new }; has types => sub { Mojolicious::Types->new }; -our $CODENAME = 'Rainbow'; -our $VERSION = '3.84'; +our $CODENAME = 'Top Hat'; +our $VERSION = '4.30'; sub AUTOLOAD { my $self = shift; - # Method my ($package, $method) = our $AUTOLOAD =~ /^([\w:]+)::(\w+)$/; croak "Undefined subroutine &${package}::$method called" unless blessed $self && $self->isa(__PACKAGE__); - # Check for helper + # Call helper with fresh controller croak qq{Can't locate object method "$method" via package "$package"} unless my $helper = $self->renderer->helpers->{$method}; - - # Call helper with fresh controller return $self->controller_class->new(app => $self)->$helper(@_); } @@ -71,19 +69,18 @@ sub new { my $r = $self->routes->namespaces([ref $self]); # Hide controller attributes/methods and "handler" - $r->hide(qw(AUTOLOAD DESTROY app cookie finish flash handler on param)); - $r->hide(qw(redirect_to render render_data render_exception render_json)); - $r->hide(qw(render_not_found render_partial render_static render_text)); - $r->hide(qw(rendered req res respond_to send session signed_cookie stash)); - $r->hide(qw(tx ua url_for write write_chunk)); + $r->hide(qw(app continue cookie finish flash handler match on param)); + $r->hide(qw(redirect_to render render_exception render_later render_maybe)); + $r->hide(qw(render_not_found render_static rendered req res respond_to)); + $r->hide(qw(send session signed_cookie stash tx url_for write write_chunk)); # Check if we have a log directory my $mode = $self->mode; $self->log->path($home->rel_file("log/$mode.log")) if -w $home->rel_file('log'); - $self->plugin($_) for qw(HeaderCondition DefaultHelpers TagHelpers); - $self->plugin($_) for qw(EPLRenderer EPRenderer RequestTimer PoweredBy); + $self->plugin($_) + for qw(HeaderCondition DefaultHelpers TagHelpers EPLRenderer EPRenderer); # Exception handling should be first in chain $self->hook(around_dispatch => \&_exception); @@ -112,7 +109,7 @@ sub dispatch { # Prepare transaction my $tx = $c->tx; - $c->res->code(undef) if $tx->is_websocket; + $tx->res->code(undef) if $tx->is_websocket; $self->sessions->load($c); my $plugins = $self->plugins->emit_hook(before_dispatch => $c); @@ -120,11 +117,14 @@ sub dispatch { $self->static->dispatch($c) and $plugins->emit_hook(after_static => $c) unless $tx->res->code; - # DEPRECATED in Rainbow! - if ($plugins->has_subscribers('after_static_dispatch')) { - warn <emit_hook_reverse(after_static_dispatch => $c); -after_static_dispatch hook is DEPRECATED in favor of before_routes!!! -EOF + # Start timer (ignore static files) + my $stash = $c->stash; + unless ($stash->{'mojo.static'} || $stash->{'mojo.started'}) { + my $req = $c->req; + my $method = $req->method; + my $path = $req->url->path->to_abs_string; + $self->log->debug(qq{$method "$path".}); + $stash->{'mojo.started'} = [gettimeofday]; } # Routes @@ -153,15 +153,12 @@ sub handler { # Dispatcher has to be last in the chain ++$self->{dispatch} + and $self->hook(around_action => sub { $_[2]->($_[1]) }) and $self->hook(around_dispatch => sub { $_[1]->app->dispatch($_[1]) }) unless $self->{dispatch}; # Process with chain - unless (eval { $self->plugins->emit_chain(around_dispatch => $c) }) { - $self->log->fatal("Processing request failed: $@"); - $tx->res->code(500); - $tx->resume; - } + $self->plugins->emit_chain(around_dispatch => $c); # Delayed response $self->log->debug('Nothing has been rendered, expecting delayed response.') @@ -196,6 +193,8 @@ sub _exception { 1; +=encoding utf8 + =head1 NAME Mojolicious - Real-time web framework @@ -255,10 +254,11 @@ L. my $mode = $app->mode; $app = $app->mode('production'); -The operating mode for your application, defaults to the value of the -C environment variable or C. You can also add per -mode logic to your application by defining methods named C<${mode}_mode> in -the application class, which will be called right before C. +The operating mode for your application, defaults to a value from the +MOJO_MODE and PLACK_ENV environment variables or C. You can also +add per mode logic to your application by defining methods named +C<${mode}_mode> in the application class, which will be called right before +C. sub development_mode { my $self = shift; @@ -332,9 +332,9 @@ startup method to define the url endpoints for your application. $app = $app->secret('passw0rd'); A secret passphrase used for signed cookies and the like, defaults to the -application name which is not very secure, so you should change it!!! As long -as you are using the insecure default there will be debug messages in the log -file reminding you to change your passphrase. +C of this application, which is not very secure, so you should change +it!!! As long as you are using the insecure default there will be debug +messages in the log file reminding you to change your passphrase. =head2 sessions @@ -346,6 +346,9 @@ object. You can usually leave this alone, see L for more information about working with session data. + # Change name of cookie used for all sessions + $app->sessions->cookie_name('mysession'); + =head2 static my $static = $app->static; @@ -368,6 +371,7 @@ L object. Responsible for connecting file extensions with MIME types, defaults to a L object. + # Add custom MIME type $app->types->type(twt => 'text/tweet'); =head1 METHODS @@ -382,7 +386,8 @@ new ones. Construct a new L application, calling C<${mode}_mode> and C in the process. Will automatically detect your home directory and set up logging based on your current operating mode. Also sets up the -renderer, static file server and a default set of plugins. +renderer, static file server, a default set of plugins and an +C hook with the default exception handling. =head2 build_tx @@ -393,18 +398,16 @@ object. =head2 defaults - my $defaults = $app->defaults; - my $foo = $app->defaults('foo'); - $app = $app->defaults({foo => 'bar'}); - $app = $app->defaults(foo => 'bar'); + my $hash = $app->defaults; + my $foo = $app->defaults('foo'); + $app = $app->defaults({foo => 'bar'}); + $app = $app->defaults(foo => 'bar'); Default values for L, assigned for every new request. - # Manipulate defaults - $app->defaults->{foo} = 'bar'; - my $foo = $app->defaults->{foo}; - delete $app->defaults->{foo}; + # Remove value + my $foo = delete $app->defaults->{foo}; =head2 dispatch @@ -449,7 +452,7 @@ requests indiscriminately. # Dispatchers will not run if there's already a response code defined $app->hook(before_dispatch => sub { my $c = shift; - $c->render(text => 'Skipped dispatchers!') + $c->render(text => 'Skipped static file server and router!') if $c->req->url->path->to_route =~ /do_not_dispatch/; }); @@ -486,7 +489,8 @@ Very useful for rewriting incoming requests and other preprocessing tasks. =item after_static -Emitted after the static file server decided to serve a static file. +Emitted after a static file response has been generated by the static file +server. $app->hook(after_static => sub { my $c = shift; @@ -498,8 +502,8 @@ controller object) =item before_routes -Emitted after the static file server decided if a static file should be served -and before the router starts its work. +Emitted after the static file server determined if a static file should be +served and before the router starts its work. $app->hook(before_routes => sub { my $c = shift; @@ -509,6 +513,25 @@ and before the router starts its work. Mostly used for custom dispatchers and collecting metrics. (Passed the default controller object) +=item around_action + +Emitted right before an action gets invoked and wraps around it, so you have +to manually forward to the next hook if you want to continue the chain. +Default action dispatching is the last hook in the chain, yours will run +before it. + + $app->hook(around_action => sub { + my ($next, $c, $action, $last) = @_; + ... + return $next->(); + }); + +This is a very powerful hook and should not be used lightly, it allows you for +example to pass additional arguments to actions or handle return values +differently. (Passed a callback leading to the next hook, the current +controller object, the action callback and a flag indicating if this action is +an endpoint) + =item after_render Emitted after content has been generated by the renderer that is not partial. @@ -553,8 +576,8 @@ and a call to C the last, yours will be in between. ... }); -This is a very powerful hook and should not be used lightly, it allows you to -customize application wide exception handling for example, consider it the +This is a very powerful hook and should not be used lightly, it allows you for +example to customize application wide exception handling, consider it the sledgehammer in your toolbox. (Passed a callback leading to the next hook and the default controller object) @@ -623,13 +646,13 @@ L. =head2 jQuery - Copyright (C) 2005, 2012 jQuery Foundation, Inc. + Copyright (C) 2005, 2013 jQuery Foundation, Inc. Licensed under the MIT License, L. =head2 prettify.js - Copyright (C) 2006, Google Inc. + Copyright (C) 2006, 2013 Google Inc. Licensed under the Apache License, Version 2.0 L. @@ -639,6 +662,8 @@ L. Every major release of L has a code name, these are the ones that have been used in the past. +4.0, C (u1F3A9) + 3.0, C (u1F308) 2.0, C (u1F343) @@ -657,6 +682,11 @@ have been used in the past. 0.999920, C (u2603) +=head1 SPONSORS + +Some of the work on this distribution has been sponsored by +L, thank you! + =head1 PROJECT FOUNDER Sebastian Riedel, C @@ -671,6 +701,8 @@ Abhijit Menon-Sen, C Glen Hinkle, C +Joel Berger, C + Marcus Ramberg, C =back @@ -753,6 +785,8 @@ Dmitriy Shalashov Dmitry Konstantinov +Dominik Jarmulowicz + Dominique Dumont Douglas Christopher Wilson @@ -779,14 +813,14 @@ Jaroslav Muhin Jesse Vincent -Joel Berger - Johannes Plunien John Kingsley Jonathan Yu +Josh Leder + Kazuhiro Shibuya Kevin Old @@ -835,6 +869,8 @@ Paul Evans Paul Tomlin +Pavel Shaydo + Pedro Melo Peter Edwards @@ -877,8 +913,6 @@ Tatsuhiko Miyagawa Terrence Brannon -The Perl Foundation - Tomas Znamenacek Ulrich Habel diff --git a/lib/Mojolicious/Command.pm b/lib/Mojolicious/Command.pm index 75f9c83..085bec3 100644 --- a/lib/Mojolicious/Command.pm +++ b/lib/Mojolicious/Command.pm @@ -88,13 +88,15 @@ sub write_rel_file { 1; +=encoding utf8 + =head1 NAME Mojolicious::Command - Command base class =head1 SYNOPSIS - # Lower case command name + # Lowercase command name package Mojolicious::Command::mycommand; use Mojo::Base 'Mojolicious::Command'; @@ -102,7 +104,7 @@ Mojolicious::Command - Command base class has description => "My first Mojo command.\n"; # Short usage message - has usage => <<"EOF"; + has usage => < "Start application with CGI.\n"; -has usage => <<"EOF"; +has usage => < "Upload distribution to CPAN.\n"; -has usage => <<"EOF"; +has usage => < \(my $user = ''); die $self->usage unless my $file = shift @args; - my $tx = Mojo::UserAgent->new->detect_proxy->post_form( - "https://$user:$password\@pause.perl.org/pause/authenquery" => { + my $tx = Mojo::UserAgent->new->detect_proxy->post( + "https://$user:$password\@pause.perl.org/pause/authenquery" => form => { HIDDENNAME => $user, CAN_MULTIPART => 1, pause99_add_uri_upload => basename($file), @@ -48,6 +48,8 @@ sub run { 1; +=encoding utf8 + =head1 NAME Mojolicious::Command::cpanify - Cpanify command diff --git a/lib/Mojolicious/Command/daemon.pm b/lib/Mojolicious/Command/daemon.pm index 1d9b717..1c708b9 100644 --- a/lib/Mojolicious/Command/daemon.pm +++ b/lib/Mojolicious/Command/daemon.pm @@ -5,7 +5,7 @@ use Getopt::Long qw(GetOptionsFromArray :config no_auto_abbrev no_ignore_case); use Mojo::Server::Daemon; has description => "Start application with HTTP and WebSocket server.\n"; -has usage => <<"EOF"; +has usage => < "Run code against application.\n"; -has usage => <<"EOF"; +has usage => <ua->get("/")->res->body' mojo eval -v 'app->home' + mojo eval -V 'app->renderer->paths' These options are available: -v, --verbose Print return value to STDOUT. + -V Print returned data structure to STDOUT. EOF sub run { my ($self, @args) = @_; - GetOptionsFromArray \@args, 'v|verbose' => \my $verbose; + GetOptionsFromArray \@args, 'v|verbose' => \my $v1, 'V' => \my $v2; my $code = shift @args || ''; # Run code against application my $app = $self->app; no warnings; - my $result = eval "package main; sub app { \$app }; $code"; - say $result if $verbose && defined $result; - return $@ ? die $@ : $result; + my $result = eval "package main; sub app; local *app = sub { \$app }; $code"; + return $@ ? die $@ : $result unless defined $result && ($v1 || $v2); + $v2 ? print($app->dumper($result)) : say $result; } 1; +=encoding utf8 + =head1 NAME Mojolicious::Command::eval - Eval command diff --git a/lib/Mojolicious/Command/generate.pm b/lib/Mojolicious/Command/generate.pm index 6c56490..7256d79 100644 --- a/lib/Mojolicious/Command/generate.pm +++ b/lib/Mojolicious/Command/generate.pm @@ -2,11 +2,11 @@ package Mojolicious::Command::generate; use Mojo::Base 'Mojolicious::Commands'; has description => "Generate files and directories from templates.\n"; -has hint => <<"EOF"; +has hint => < <<"EOF"; +has message => <run(@_) } 1; +=encoding utf8 + =head1 NAME Mojolicious::Command::generate - Generator command diff --git a/lib/Mojolicious/Command/generate/app.pm b/lib/Mojolicious/Command/generate/app.pm index 4c11e3b..1bf25ca 100644 --- a/lib/Mojolicious/Command/generate/app.pm +++ b/lib/Mojolicious/Command/generate/app.pm @@ -12,7 +12,7 @@ sub run { # Prevent bad applications die <here to move forward to a static page. __END__ + +=encoding utf8 + =head1 NAME Mojolicious::Command::generate::app - App generator command diff --git a/lib/Mojolicious/Command/generate/lite_app.pm b/lib/Mojolicious/Command/generate/lite_app.pm index 3574f87..ce03f3b 100644 --- a/lib/Mojolicious/Command/generate/lite_app.pm +++ b/lib/Mojolicious/Command/generate/lite_app.pm @@ -42,6 +42,9 @@ Welcome to the Mojolicious real-time web framework! __END__ + +=encoding utf8 + =head1 NAME Mojolicious::Command::generate::lite_app - Lite app generator command diff --git a/lib/Mojolicious/Command/generate/makefile.pm b/lib/Mojolicious/Command/generate/makefile.pm index fceaa77..8bba525 100644 --- a/lib/Mojolicious/Command/generate/makefile.pm +++ b/lib/Mojolicious/Command/generate/makefile.pm @@ -24,6 +24,9 @@ WriteMakefile( ); __END__ + +=encoding utf8 + =head1 NAME Mojolicious::Command::generate::makefile - Makefile generator command diff --git a/lib/Mojolicious/Command/generate/plugin.pm b/lib/Mojolicious/Command/generate/plugin.pm index 75c4224..453cf7f 100644 --- a/lib/Mojolicious/Command/generate/plugin.pm +++ b/lib/Mojolicious/Command/generate/plugin.pm @@ -41,6 +41,8 @@ sub register { 1; <% %>__END__ +<% %>=encoding utf8 + <% %>=head1 NAME <%= $class %> - Mojolicious Plugin @@ -86,7 +88,7 @@ plugin '<%= $name %>'; get '/' => sub { my $self = shift; - $self->render_text('Hello Mojo!'); + $self->render(text => 'Hello Mojo!'); }; my $t = Test::Mojo->new; @@ -110,6 +112,9 @@ WriteMakefile( ); __END__ + +=encoding utf8 + =head1 NAME Mojolicious::Command::generate::plugin - Plugin generator command diff --git a/lib/Mojolicious/Command/get.pm b/lib/Mojolicious/Command/get.pm index 4290354..ba002f9 100644 --- a/lib/Mojolicious/Command/get.pm +++ b/lib/Mojolicious/Command/get.pm @@ -8,9 +8,10 @@ use Mojo::JSON; use Mojo::JSON::Pointer; use Mojo::UserAgent; use Mojo::Util qw(decode encode); +use Scalar::Util 'weaken'; has description => "Perform HTTP request.\n"; -has usage => <<"EOF"; +has usage => < Charset of HTML/XML content, defaults to auto - detection or "UTF-8". + detection. -c, --content Content to send with request. -H, --header Additional HTTP header. -M, --method HTTP method to use, defaults to "GET". @@ -46,59 +47,38 @@ sub run { 'r|redirect' => \my $redirect, 'v|verbose' => \my $verbose; - die $self->usage unless my $url = decode 'UTF-8', do {my $tmp = shift @args; defined $tmp ? $tmp : ''}; + @args = map { decode 'UTF-8', $_ } @args; + die $self->usage unless my $url = shift @args; my $selector = shift @args; # Parse header pairs my %headers; /^\s*([^:]+)\s*:\s*(.+)$/ and $headers{$1} = $2 for @headers; - # Use global event loop singleton + # Detect proxy for absolute URLs my $ua = Mojo::UserAgent->new(ioloop => Mojo::IOLoop->singleton); + $url !~ m!^/! ? $ua->detect_proxy : $ua->app($self->app); $ua->max_redirects(10) if $redirect; - # Detect proxy for absolute URLs - if ($url !~ m!/!) { $ua->detect_proxy } - else { $ua->app($self->app) } - - # Do the real work with "start" event - my $v = my $buffer = ''; + my $buffer = ''; $ua->on( start => sub { - my $tx = pop; - - # Verbose callback - my $v = $verbose; - my $cb = sub { - my $res = shift; - - # Wait for headers - return unless $v && $res->headers->is_finished; - $v = undef; - - # Show request - my $req = $tx->req; - my $startline = $req->build_start_line; - my $req_headers = $req->build_headers; - warn "$startline$req_headers"; - - # Show response - my $version = $res->version; - my $code = $res->code; - my $msg = $res->message; - my $res_headers = $res->headers->to_string; - warn "HTTP/$version $code $msg\n$res_headers\n\n"; - }; - $tx->res->on(progress => $cb); - - # Stream content - $tx->res->body( - sub { - $cb->(my $res = shift); - - # Ignore intermediate content - return if $redirect && $res->is_status_class(300); - $selector ? ($buffer .= pop) : print(pop); + my ($ua, $tx) = @_; + + # Verbose + weaken $tx; + $tx->res->content->on( + body => sub { + warn $tx->req->$_ for qw(build_start_line build_headers); + warn $tx->res->$_ for qw(build_start_line build_headers); + } + ) if $verbose; + + # Stream content (ignore redirects) + $tx->res->content->unsubscribe('read')->on( + read => sub { + return if $redirect && $tx->res->is_status_class(300); + defined $selector ? ($buffer .= pop) : print pop; } ); } @@ -113,8 +93,8 @@ sub run { warn qq{Problem loading URL "$url". ($err)\n} if $err && !$code; # JSON Pointer - return unless $selector; - my $type = $tx->res->headers->content_type || ''; + return unless defined $selector; + my $type = defined $tx->res->headers->content_type ? $tx->res->headers->content_type : ''; return _json($buffer, $selector) if $type =~ /json/i; # Selector @@ -126,19 +106,16 @@ sub _json { return unless my $data = $json->decode(shift); return unless defined($data = Mojo::JSON::Pointer->new->get($data, shift)); return _say($data) unless ref $data eq 'HASH' || ref $data eq 'ARRAY'; - say($json->encode($data)); + say $json->encode($data); } -sub _say { - return unless length(my $value = shift); - say encode('UTF-8', $value); -} +sub _say { say encode('UTF-8', $_[0]) if length $_[0] } sub _select { my ($buffer, $selector, $charset, @args) = @_; - my $dom = Mojo::DOM->new->charset($charset)->parse($buffer); - my $results = $dom->find($selector); + $buffer = do { my $tmp = decode($charset, $buffer); defined $tmp ? $tmp : $buffer } if $charset; + my $results = Mojo::DOM->new($buffer)->find($selector); my $finished; while (defined(my $command = shift @args)) { @@ -158,7 +135,7 @@ sub _select { # Attribute elsif ($command eq 'attr') { next unless my $name = shift @args; - _say($_->attrs->{$name}) for @$results; + _say($_->attr->{$name}) for @$results; } # Unknown @@ -171,6 +148,8 @@ sub _select { 1; +=encoding utf8 + =head1 NAME Mojolicious::Command::get - Get command diff --git a/lib/Mojolicious/Command/inflate.pm b/lib/Mojolicious/Command/inflate.pm index 5cb4bf7..796ae7f 100644 --- a/lib/Mojolicious/Command/inflate.pm +++ b/lib/Mojolicious/Command/inflate.pm @@ -27,6 +27,8 @@ sub run { 1; +=encoding utf8 + =head1 NAME Mojolicious::Command::inflate - Inflate command diff --git a/lib/Mojolicious/Command/prefork.pm b/lib/Mojolicious/Command/prefork.pm index 421eada..70a03b0 100644 --- a/lib/Mojolicious/Command/prefork.pm +++ b/lib/Mojolicious/Command/prefork.pm @@ -6,7 +6,7 @@ use Mojo::Server::Prefork; has description => "Start application with preforking HTTP and WebSocket server.\n"; -has usage => <<"EOF"; +has usage => < Path to lock file, defaults to a random file. - -L, --lock-timeout Lock timeout, defaults to 0.5. + -L, --lock-timeout Lock timeout, defaults to 1. -l, --listen One or more locations you want to listen on, defaults to the value of MOJO_LISTEN or "http://*:3000". @@ -48,7 +48,7 @@ sub run { my $prefork = Mojo::Server::Prefork->new(app => $self->app); GetOptionsFromArray \@args, 'A|accepts=i' => sub { $prefork->accepts($_[1]) }, - 'a|accept-interval=i' => sub { $prefork->accept_interval($_[1]) }, + 'a|accept-interval=f' => sub { $prefork->accept_interval($_[1]) }, 'b|backlog=i' => sub { $prefork->backlog($_[1]) }, 'c|clients=i' => sub { $prefork->max_clients($_[1]) }, 'G|graceful-timeout=i' => sub { $prefork->graceful_timeout($_[1]) }, @@ -57,7 +57,7 @@ sub run { 'H|heartbeat-timeout=i' => sub { $prefork->heartbeat_timeout($_[1]) }, 'i|inactivity=i' => sub { $prefork->inactivity_timeout($_[1]) }, 'lock-file=s' => sub { $prefork->lock_file($_[1]) }, - 'L|lock-timeout=i' => sub { $prefork->lock_timeout($_[1]) }, + 'L|lock-timeout=f' => sub { $prefork->lock_timeout($_[1]) }, 'l|listen=s' => \my @listen, 'multi-accept=i' => sub { $prefork->multi_accept($_[1]) }, 'P|pid-file=s' => sub { $prefork->pid_file($_[1]) }, @@ -72,6 +72,8 @@ sub run { 1; +=encoding utf8 + =head1 NAME Mojolicious::Command::prefork - Prefork command diff --git a/lib/Mojolicious/Command/psgi.pm b/lib/Mojolicious/Command/psgi.pm index 3a62de7..d73c188 100644 --- a/lib/Mojolicious/Command/psgi.pm +++ b/lib/Mojolicious/Command/psgi.pm @@ -10,6 +10,8 @@ sub run { Mojo::Server::PSGI->new(app => shift->app)->to_psgi_app } 1; +=encoding utf8 + =head1 NAME Mojolicious::Command::psgi - PSGI command diff --git a/lib/Mojolicious/Command/routes.pm b/lib/Mojolicious/Command/routes.pm index 3b467d5..ba6c7a0 100644 --- a/lib/Mojolicious/Command/routes.pm +++ b/lib/Mojolicious/Command/routes.pm @@ -3,9 +3,10 @@ use Mojo::Base 'Mojolicious::Command'; use MojoLegacy::re 'regexp_pattern'; use Getopt::Long qw(GetOptionsFromArray :config no_auto_abbrev no_ignore_case); +use Mojo::Util 'encode'; has description => "Show available routes.\n"; -has usage => <<"EOF"; +has usage => <[0]; - $length[0] = $len if $len > $length[0]; - # Methods - unless (defined $node->[1]->via) { $len = length '*' } - else { $len = length(join ',', @{$node->[1]->via}) } - $length[1] = $len if $len > $length[1]; + my $via = $node->[0]->via; + $node->[2] = !$via ? '*' : uc join ',', @$via; # Name - $len = length $node->[1]->name; - $len += 2 if $node->[1]->has_custom_name; - $length[2] = $len if $len > $length[2]; + my $name = $node->[0]->name; + $node->[3] = $node->[0]->has_custom_name ? qq{"$name"} : $name; + + # Check column width + $table[$_] = _max($table[$_], length $node->[$_ + 1]) for 0 .. 2; } - # Draw all routes for my $node (@$routes) { - my @parts; - - # Pattern - push @parts, $node->[0]; - $parts[-1] .= ' ' x ($length[0] - length $parts[-1]); - - # Methods - my $methods; - unless (defined $node->[1]->via) { $methods = '*' } - else { $methods = uc join ',', @{$node->[1]->via} } - push @parts, $methods . ' ' x ($length[1] - length $methods); + my @parts = map { _padding($node->[$_ + 1], $table[$_]) } 0 .. 2; - # Name - my $name = $node->[1]->name; - $name = qq{"$name"} if $node->[1]->has_custom_name; - push @parts, $name . ' ' x ($length[2] - length $name); - - # Regex - my $pattern = $node->[1]->pattern; - $pattern->match('/', $node->[1]->is_endpoint); + # Regex (verbose) + my $pattern = $node->[0]->pattern; + $pattern->match('/', $node->[0]->is_endpoint); my $regex = (regexp_pattern $pattern->regex)[0]; - my $format = (regexp_pattern $pattern->format_regex || '')[0]; + my $format = (regexp_pattern($pattern->format_regex || ''))[0]; my $optional = !$pattern->constraints->{format} || $pattern->defaults->{format}; - $format .= '?' if $format && $optional; - push @parts, $format ? "$regex$format" : $regex if $verbose; + $regex .= $optional ? "(?:$format)?" : $format + if $format && !$node->[0]->partial; + push @parts, $regex if $verbose; - say join(' ', @parts); + say encode('UTF-8', join(' ', @parts)); } } +sub _max { $_[1] > $_[0] ? $_[1] : $_[0] } + +sub _padding { $_[0] . ' ' x ($_[1] - length $_[0]) } + sub _walk { - my ($self, $node, $depth, $routes) = @_; + my ($self, $route, $depth, $routes) = @_; my $prefix = ''; if (my $i = $depth * 2) { $prefix .= ' ' x $i . '+' } - push @$routes, [$prefix . ($node->pattern->pattern || '/'), $node]; + push @$routes, [$route, $prefix . ($route->pattern->pattern || '/')]; $depth++; - $self->_walk($_, $depth, $routes) for @{$node->children}; + $self->_walk($_, $depth, $routes) for @{$route->children}; $depth--; } 1; +=encoding utf8 + =head1 NAME Mojolicious::Command::routes - Routes command diff --git a/lib/Mojolicious/Command/test.pm b/lib/Mojolicious/Command/test.pm index 4c30236..0c22d6a 100644 --- a/lib/Mojolicious/Command/test.pm +++ b/lib/Mojolicious/Command/test.pm @@ -8,7 +8,7 @@ use Getopt::Long qw(GetOptionsFromArray :config no_auto_abbrev no_ignore_case); use Mojo::Home; has description => "Run unit tests.\n"; -has usage => <<"EOF"; +has usage => <new($path); - /\.t$/ and push(@args, $home->rel_file($_)) for @{$home->list_files}; + /\.t$/ and push @args, $home->rel_file($_) for @{$home->list_files}; say "Running tests from '", realpath($path), "'."; } @@ -42,6 +42,8 @@ sub run { 1; +=encoding utf8 + =head1 NAME Mojolicious::Command::test - Test command diff --git a/lib/Mojolicious/Command/version.pm b/lib/Mojolicious/Command/version.pm index 663e9f2..f6a96ea 100644 --- a/lib/Mojolicious/Command/version.pm +++ b/lib/Mojolicious/Command/version.pm @@ -17,15 +17,15 @@ sub run { my $tls = Mojo::IOLoop::Server::TLS ? $IO::Socket::SSL::VERSION : 'not installed'; - print <<"EOF"; + print < <<"EOF"; +has hint => < Path to your applications home directory, defaults to the value of MOJO_HOME or auto detection. - -m, --mode Run mode of your application, defaults to the value - of MOJO_MODE or "development". + -m, --mode Operating mode for your application, defaults to the + value of MOJO_MODE/PLACK_ENV or "development". See '$0 help COMMAND' for more information on a specific command. EOF -has message => <<"EOF"; +has message => <hint; } -# DEPRECATED in Rainbow! -sub start { - warn <start is DEPRECATED in favor of -Mojolicious::Commands->start_app!!! -EOF - my $self = shift; - return $self->start_app($ENV{MOJO_APP} => @_) if $ENV{MOJO_APP}; - return $self->new->app->start(@_); -} - sub start_app { my $self = shift; return Mojo::Server->new->build_app(shift)->start(@_); @@ -128,6 +117,8 @@ sub _command { 1; +=encoding utf8 + =head1 NAME Mojolicious::Commands - Command line interface @@ -179,7 +170,7 @@ Upload files to CPAN. $ ./myapp.pl daemon -Start application with standalone HTTP and WebSocket server server. +Start application with standalone HTTP and WebSocket server. =head2 eval @@ -320,7 +311,7 @@ Try to detect environment. $commands->run(@ARGV); Load and run commands. Automatic deployment environment detection can be -disabled with the C environment variable. +disabled with the MOJO_NO_DETECT environment variable. =head2 start_app diff --git a/lib/Mojolicious/Controller.pm b/lib/Mojolicious/Controller.pm index e66f731..42a671a 100644 --- a/lib/Mojolicious/Controller.pm +++ b/lib/Mojolicious/Controller.pm @@ -4,7 +4,6 @@ use Mojo::Base -base; # No imports, for security reasons! use Carp (); use Mojo::ByteStream; -use Mojo::Cookie::Response; use Mojo::Exception; use Mojo::Transaction::HTTP; use Mojo::URL; @@ -12,11 +11,11 @@ use Mojo::Util; use Mojolicious; use Mojolicious::Routes::Match; use Scalar::Util (); +use Time::HiRes (); has app => sub { Mojolicious->new }; -has match => sub { - Mojolicious::Routes::Match->new(GET => '/')->root(shift->app->routes); -}; +has match => + sub { Mojolicious::Routes::Match->new(root => shift->app->routes) }; has tx => sub { Mojo::Transaction::HTTP->new }; # Reserved stash values @@ -28,12 +27,11 @@ my %RESERVED = map { $_ => 1 } ( sub AUTOLOAD { my $self = shift; - # Method my ($package, $method) = our $AUTOLOAD =~ /^([\w:]+)::(\w+)$/; Carp::croak "Undefined subroutine &${package}::$method called" unless Scalar::Util::blessed $self && $self->isa(__PACKAGE__); - # Call helper + # Call helper with current controller Carp::croak qq{Can't locate object method "$method" via package "$package"} unless my $helper = $self->app->renderer->helpers->{$method}; return $self->$helper(@_); @@ -41,20 +39,20 @@ sub AUTOLOAD { sub DESTROY { } +sub continue { $_[0]->app->routes->continue($_[0]) } + sub cookie { - my ($self, $name, $value, $options) = @_; - $options ||= {}; + my ($self, $name) = (shift, shift); # Response cookie - if (defined $value) { + if (@_) { # Cookie too big + my $cookie = {name => $name, value => shift, %{shift || {}}}; $self->app->log->error(qq{Cookie "$name" is bigger than 4096 bytes.}) - if length $value > 4096; + if length $cookie->{value} > 4096; - # Create new cookie - $self->res->cookies( - Mojo::Cookie::Response->new(name => $name, value => $value, %$options)); + $self->res->cookies($cookie); return $self; } @@ -65,20 +63,20 @@ sub cookie { } sub finish { - my ($self, $chunk) = @_; + my $self = shift; # WebSocket my $tx = $self->tx; - $tx->finish and return $self if $tx->is_websocket; + $tx->finish(@_) and return $self if $tx->is_websocket; # Chunked stream - if ($tx->res->is_chunked) { - $self->write_chunk($chunk) if defined $chunk; + if ($tx->res->content->is_chunked) { + $self->write_chunk(@_) if @_; return $self->write_chunk(''); } # Normal stream - $self->write($chunk) if defined $chunk; + $self->write(@_) if @_; return $self->write(''); } @@ -152,44 +150,27 @@ sub render { my $self = shift; # Template may be first argument - my $template = @_ % 2 && !ref $_[0] ? shift : undef; - my $args = ref $_[0] ? $_[0] : {@_}; + my ($template, $args) = (@_ % 2 ? shift : undef, {@_}); $args->{template} = $template if $template; - - # Detect template name - my $stash = $self->stash; - unless ($args->{template} || $stash->{template}) { - - # Normal default template - my $controller = $args->{controller} || $stash->{controller}; - my $action = $args->{action} || $stash->{action}; - if ($controller && $action) { - $stash->{template} = join '/', - split(/-/, Mojo::Util::decamelize($controller)), $action; - } - - # Try the route name if we don't have controller and action - elsif (my $endpoint = $self->match->endpoint) { - $stash->{template} = $endpoint->name; - } - } + my $maybe = delete $args->{'mojo.maybe'}; # Render my $app = $self->app; my ($output, $format) = $app->renderer->render($self, $args); - return undef unless defined $output; - return Mojo::ByteStream->new($output) if $args->{partial}; + return defined $output ? Mojo::ByteStream->new($output) : undef + if $args->{partial}; + + # Maybe + return $maybe ? undef : !$self->render_not_found unless defined $output; # Prepare response $app->plugins->emit_hook(after_render => $self, \$output, $format); my $headers = $self->res->body($output)->headers; $headers->content_type($app->types->type($format) || 'text/plain') unless $headers->content_type; - return !!$self->rendered($stash->{status}); + return !!$self->rendered($self->stash->{status}); } -sub render_data { shift->render(data => @_) } - sub render_exception { my ($self, $e) = @_; @@ -214,14 +195,15 @@ sub render_exception { }; my $inline = $renderer->_bundled( $mode eq 'development' ? 'exception.development' : 'exception'); - return if $self->_fallbacks($options, 'exception', $inline); + return $self if $self->_fallbacks($options, 'exception', $inline); $self->_fallbacks({%$options, format => 'html'}, 'exception', $inline); + return $self; } -sub render_json { shift->render(json => @_) } - sub render_later { shift->stash('mojo.rendered' => 1) } +sub render_maybe { shift->render(@_, 'mojo.maybe' => 1) } + sub render_not_found { my $self = shift; @@ -234,15 +216,9 @@ sub render_not_found { = {template => "not_found.$mode", format => $format, status => 404}; my $inline = $renderer->_bundled( $mode eq 'development' ? 'not_found.development' : 'not_found'); - return if $self->_fallbacks($options, 'not_found', $inline); + return $self if $self->_fallbacks($options, 'not_found', $inline); $self->_fallbacks({%$options, format => 'html'}, 'not_found', $inline); -} - -sub render_partial { - my $self = shift; - my $template = @_ % 2 ? shift : undef; - return $self->render( - {@_, partial => 1, defined $template ? (template => $template) : ()}); + return $self; } sub render_static { @@ -250,11 +226,9 @@ sub render_static { my $app = $self->app; return !!$self->rendered if $app->static->serve($self, $file); $app->log->debug(qq{File "$file" not found, public directory missing?}); - return undef; + return !$self->render_not_found; } -sub render_text { shift->render(text => @_) } - sub rendered { my ($self, $status) = @_; @@ -263,8 +237,20 @@ sub rendered { $res->code($status || 200) if $status || !$res->code; # Finish transaction - unless ($self->stash->{'mojo.finished'}++) { + my $stash = $self->stash; + unless ($stash->{'mojo.finished'}++) { + + # Stop timer my $app = $self->app; + if (my $started = delete $stash->{'mojo.started'}) { + my $elapsed = sprintf '%f', + Time::HiRes::tv_interval($started, [Time::HiRes::gettimeofday()]); + my $rps = $elapsed == 0 ? '??' : sprintf '%.3f', 1 / $elapsed; + my $code = $res->code; + my $msg = $res->message || $res->default_message($code); + $app->log->debug("$code $msg (${elapsed}s, $rps/s)."); + } + $app->plugins->emit_hook_reverse(after_dispatch => $self); $app->sessions->store($self); } @@ -304,7 +290,9 @@ sub respond_to { } # Dispatch - ref $target eq 'CODE' ? $target->($self) : $self->render($target); + ref $target eq 'CODE' ? $target->($self) : $self->render(%$target); + + return $self; } sub send { @@ -388,15 +376,13 @@ sub stash { return $self; } -sub ua { shift->app->ua } - sub url_for { my $self = shift; - my $target = shift; $target = defined $target ? $target : ''; + my $target = do { my $tmp = shift; defined $tmp ? $tmp : '' }; # Absolute URL return $target if Scalar::Util::blessed $target && $target->isa('Mojo::URL'); - return Mojo::URL->new($target) if $target =~ m!^\w+://!; + return Mojo::URL->new($target) if $target =~ m!^(?:[^:/?#]+:|//)!; # Base my $url = Mojo::URL->new; @@ -418,12 +404,6 @@ sub url_for { else { my ($generated, $ws) = $self->match->path_for($target, @_); $path->parse($generated) if $generated; - - # Fix trailing slash - $path->trailing_slash(1) - if (!$target || $target eq 'current') && $req->url->path->trailing_slash; - - # Fix scheme for WebSockets $base->scheme($base->protocol eq 'https' ? 'wss' : 'ws') if $ws; } @@ -438,14 +418,16 @@ sub url_for { sub write { my ($self, $chunk, $cb) = @_; ($cb, $chunk) = ($chunk, undef) if ref $chunk eq 'CODE'; - $self->res->write($chunk => sub { shift and $self->$cb(@_) if $cb }); + my $content = $self->res->content; + $content->write($chunk => sub { shift and $self->$cb(@_) if $cb }); return $self->rendered; } sub write_chunk { my ($self, $chunk, $cb) = @_; ($cb, $chunk) = ($chunk, undef) if ref $chunk eq 'CODE'; - $self->res->write_chunk($chunk => sub { shift and $self->$cb(@_) if $cb }); + my $content = $self->res->content; + $content->write_chunk($chunk => sub { shift and $self->$cb(@_) if $cb }); return $self->rendered; } @@ -453,18 +435,16 @@ sub _fallbacks { my ($self, $options, $template, $inline) = @_; # Mode specific template - return 1 if $self->render($options); + return 1 if $self->render_maybe(%$options); # Normal template - $options->{template} = $template; - return 1 if $self->render($options); + return 1 if $self->render_maybe(%$options, template => $template); # Inline template my $stash = $self->stash; return undef unless $stash->{format} eq 'html'; delete $stash->{$_} for qw(extends layout); - delete $options->{template}; - return $self->render(%$options, inline => $inline, handler => 'ep'); + return $self->render_maybe(%$options, inline => $inline, handler => 'ep'); } 1; @@ -538,6 +518,12 @@ L or L object. L inherits all methods from L and implements the following new ones. +=head2 continue + + $c->continue; + +Continue dispatch chain. + =head2 cookie my $value = $c->cookie('foo'); @@ -548,14 +534,16 @@ implements the following new ones. Access request cookie values and create new response cookies. # Create response cookie with domain and expiration date - $c->cookie(user => 'sri', {domain => 'mojolicio.us', expires => time + 60}); + $c->cookie(user => 'sri', {domain => 'example.com', expires => time + 60}); =head2 finish $c = $c->finish; + $c = $c->finish(1000); + $c = $c->finish(1003 => 'Cannot accept data!'); $c = $c->finish('Bye!'); -Gracefully end WebSocket connection or long poll stream. +Close WebSocket connection or long poll stream gracefully. =head2 flash @@ -574,7 +562,9 @@ Data storage persistent only for the next request, stored in the C. my $cb = $c->on(finish => sub {...}); Subscribe to events of C, which is usually a L or -L object. +L object. Note that this method will +automatically respond to WebSocket handshake requests with a C<101> response +status. # Do something after the transaction has been finished $c->on(finish => sub { @@ -588,20 +578,17 @@ L object. $c->app->log->debug("Message: $msg"); }); - # Receive JSON object via WebSocket "Text" message - use Mojo::JSON 'j'; - $c->on(text => sub { - my ($c, $bytes) = @_; - my $test = j($bytes)->{test}; - $c->app->log->debug("Test: $test"); + # Receive JSON object via WebSocket message + $c->on(json => sub { + my ($c, $hash) = @_; + $c->app->log->debug("Test: $hash->{test}"); }); - # Receive JSON object via WebSocket "Binary" message - use Mojo::JSON 'j'; + # Receive WebSocket "Binary" message $c->on(binary => sub { my ($c, $bytes) = @_; - my $test = j($bytes)->{test}; - $c->app->log->debug("Test: $test"); + my $len = length $bytes; + $c->app->log->debug("Received $len bytes."); }); =head2 param @@ -615,8 +602,10 @@ L object. Access GET/POST parameters, file uploads and route placeholder values that are not reserved stash values. Note that this method is context sensitive in some -cases and therefore needs to be used with care, every GET/POST parameter can -have multiple values, which might have unexpected consequences. +cases and therefore needs to be used with care, there can always be multiple +values, which might have unexpected consequences. Parts of the request body +need to be loaded into memory to parse POST parameters, so you have to make +sure it is not excessively large. # List context is ambiguous and should be avoided my $hash = {foo => $self->param('foo')}; @@ -640,10 +629,10 @@ For more control you can also access request information directly. =head2 redirect_to - $c = $c->redirect_to('named'); $c = $c->redirect_to('named', foo => 'bar'); - $c = $c->redirect_to('/path'); - $c = $c->redirect_to('http://127.0.0.1/foo/bar'); + $c = $c->redirect_to('named', {foo => 'bar'}); + $c = $c->redirect_to('/perldoc'); + $c = $c->redirect_to('http://mojolicio.us/perldoc'); Prepare a C<302> redirect response, takes the same arguments as C. @@ -658,7 +647,6 @@ Prepare a C<302> redirect response, takes the same arguments as C. my $success = $c->render; my $success = $c->render(controller => 'foo', action => 'bar'); - my $success = $c->render({controller => 'foo', action => 'bar'}); my $success = $c->render(template => 'foo/index'); my $success = $c->render(template => 'index', format => 'html'); my $success = $c->render(data => $bytes); @@ -673,42 +661,22 @@ C hook unless the result is C. If no template is provided a default one based on controller and action or route name will be generated, all additional values get merged into the C. -=head2 render_data - - $c->render_data($bytes); - $c->render_data($bytes, format => 'png'); - -Render the given content as raw bytes, similar to C but data will -not be encoded. All additional values get merged into the C. - - # Longer version - $c->render(data => $bytes); - =head2 render_exception - $c->render_exception('Oops!'); - $c->render_exception(Mojo::Exception->new('Oops!')); + $c = $c->render_exception('Oops!'); + $c = $c->render_exception(Mojo::Exception->new('Oops!')); Render the exception template C or -C and set the response status code to C<500>. - -=head2 render_json - - $c->render_json({foo => 'bar'}); - $c->render_json([1, 2, -3], status => 201); - -Render a data structure as JSON. All additional values get merged into the -C. - - # Longer version - $c->render(json => {foo => 'bar'}); +C and set the response status code to C<500>. Also sets +the stash values C to a L object and C +to a copy of the C for use in the templates. =head2 render_later $c = $c->render_later; Disable automatic rendering to delay response generation, only necessary if -automatic rendring would result in a response. +automatic rendering would result in a response. # Delayed rendering $c->render_later; @@ -716,23 +684,24 @@ automatic rendring would result in a response. $c->render(text => 'Delayed by 2 seconds!'); }); -=head2 render_not_found +=head2 render_maybe - $c->render_not_found; + my $success = $c->render_maybe; + my $success = $c->render_maybe(controller => 'foo', action => 'bar'); + my $success = $c->render_maybe('foo/index', format => 'html'); -Render the not found template C or -C and set the response status code to C<404>. +Try to render content but do not call C if no response could +be generated, takes the same arguments as C. -=head2 render_partial + # Render template "index_local" only if it exists + $self->render_maybe('index_local') or $self->render('index'); - my $output = $c->render_partial('menubar'); - my $output = $c->render_partial('menubar', format => 'txt'); - my $output = $c->render_partial(template => 'menubar'); +=head2 render_not_found -Same as C but returns the rendered result. + $c = $c->render_not_found; - # Longer version - my $output = $c->render('menubar', partial => 1); +Render the not found template C or +C and set the response status code to C<404>. =head2 render_static @@ -743,22 +712,6 @@ Render a static file using L, usually from the C directories or C sections of your application. Note that this method does not protect from traversing to parent directories. -=head2 render_text - - $c->render_text('Hello World!'); - $c->render_text('Hello World!', layout => 'green'); - -Render the given content as Perl characters, which will be encoded to bytes. -All additional values get merged into the C. See C for an -alternative without encoding. Note that this does not change the content type -of the response, which is C by default. - - # Longer version - $c->render(text => 'Hello World!'); - - # Render "text/plain" response - $c->render_text('Hello World!', format => 'txt'); - =head2 rendered $c = $c->rendered; @@ -777,10 +730,14 @@ Get L object from L. my $req = $c->tx->req; # Extract request information - my $userinfo = $c->req->url->userinfo; + my $url = $c->req->url->to_abs; + my $userinfo = $c->req->url->to_abs->userinfo; + my $host = $c->req->url->to_abs->host; my $agent = $c->req->headers->user_agent; my $body = $c->req->body; + my $hash = $c->req->json; my $foo = $c->req->json('/23/foo'); + my $dom = $c->req->dom; my $bar = $c->req->dom('div.bar')->first->text; =head2 res @@ -797,7 +754,7 @@ Get L object from L. =head2 respond_to - $c->respond_to( + $c = $c->respond_to( json => {json => {message => 'Welcome!'}}, html => {template => 'welcome'}, any => sub {...} @@ -811,7 +768,7 @@ more than one MIME type will be ignored, unless the C header is set to the value C. $c->respond_to( - json => sub { $c->render_json({just => 'works'}) }, + json => sub { $c->render(json => {just => 'works'}) }, xml => {text => 'works'}, any => {data => '', status => 204} ); @@ -820,19 +777,21 @@ is set to the value C. $c = $c->send({binary => $bytes}); $c = $c->send({text => $bytes}); + $c = $c->send({json => {test => [1, 2, 3]}}); $c = $c->send([$fin, $rsv1, $rsv2, $rsv3, $op, $bytes]); $c = $c->send($chars); $c = $c->send($chars => sub {...}); Send message or frame non-blocking via WebSocket, the optional drain callback -will be invoked once all data has been written. +will be invoked once all data has been written. Note that this method will +automatically respond to WebSocket handshake requests with a C<101> response +status. # Send "Text" message $c->send('I ♥ Mojolicious!'); # Send JSON object as "Text" message - use Mojo::JSON 'j'; - $c->send({text => j({test => 'I ♥ Mojolicious!'})}); + $c->send({json => {test => 'I ♥ Mojolicious!'}}); # Send JSON object as "Binary" message use Mojo::JSON 'j'; @@ -841,6 +800,12 @@ will be invoked once all data has been written. # Send "Ping" frame $c->send([1, 0, 0, 0, 9, 'Hello World!']); + # Make sure previous message has been written before continuing + $c->send('First message!' => sub { + my $c = shift; + $c->send('Second message!'); + }); + For mostly idle WebSockets you might also want to increase the inactivity timeout, which usually defaults to C<15> seconds. @@ -885,10 +850,10 @@ discarded. =head2 stash - my $stash = $c->stash; - my $foo = $c->stash('foo'); - $c = $c->stash({foo => 'bar'}); - $c = $c->stash(foo => 'bar'); + my $hash = $c->stash; + my $foo = $c->stash('foo'); + $c = $c->stash({foo => 'bar'}); + $c = $c->stash(foo => 'bar'); Non persistent data storage and exchange, application wide default values can be set with L. Many stash values have a special @@ -897,50 +862,20 @@ C, C, C, C, C, C, C, C, C, C, C, C