Initial Commit

This commit is contained in:
Riley Schneider
2025-12-03 16:38:10 +01:00
parent c5e26bf594
commit b732d8d4b5
17680 changed files with 5977495 additions and 2 deletions

View File

@@ -0,0 +1,176 @@
package Mojolicious::Plugin::Config;
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::File qw(path);
use Mojo::Util qw(decode);
sub load { $_[0]->parse(decode('UTF-8', path($_[1])->slurp), @_[1, 2, 3]) }
sub parse {
my ($self, $content, $file, $conf, $app) = @_;
# Run Perl code in sandbox
my $config = eval 'package Mojolicious::Plugin::Config::Sandbox; no warnings;'
. "sub app; local *app = sub { \$app }; use Mojo::Base -strict; $content";
die qq{Can't load configuration from file "$file": $@} if $@;
die qq{Configuration file "$file" did not return a hash reference.\n} unless ref $config eq 'HASH';
return $config;
}
sub register {
my ($self, $app, $conf) = @_;
# DEPRECATED!
$app->defaults(config => $app->config);
# Override
return $app->config if $app->config->{config_override};
# Config file
my $file = $conf->{file} || $ENV{MOJO_CONFIG};
$file ||= $app->moniker . '.' . ($conf->{ext} || 'conf');
# Mode specific config file
my $mode = $file =~ /^(.*)\.([^.]+)$/ ? join('.', $1, $app->mode, $2) : '';
my $home = $app->home;
$file = $home->child($file) unless path($file)->is_abs;
$mode = $home->child($mode) if $mode && !path($mode)->is_abs;
$mode = undef unless $mode && -e $mode;
# Read config file
my $config = {};
if (-e $file) { $config = $self->load($file, $conf, $app) }
# Check for default and mode specific config file
elsif (!$conf->{default} && !$mode) { die qq{Configuration file "$file" missing, maybe you need to create it?\n} }
# Merge everything
$config = {%$config, %{$self->load($mode, $conf, $app)}} if $mode;
$config = {%{$conf->{default}}, %$config} if $conf->{default};
return $app->config($config)->config;
}
1;
=encoding utf8
=head1 NAME
Mojolicious::Plugin::Config - Perl-ish configuration plugin
=head1 SYNOPSIS
# myapp.conf (it's just Perl returning a hash)
{
# Just a value
foo => "bar",
# Nested data structures are fine too
baz => ['♥'],
# You have full access to the application
music_dir => app->home->child('music')
};
# Mojolicious
my $config = $app->plugin('Config');
say $config->{foo};
# Mojolicious::Lite
my $config = plugin 'Config';
say $config->{foo};
# foo.html.ep
%= config->{foo}
# The configuration is available application-wide
my $config = app->config;
say $config->{foo};
# Everything can be customized with options
my $config = plugin Config => {file => '/etc/myapp.stuff'};
=head1 DESCRIPTION
L<Mojolicious::Plugin::Config> is a Perl-ish configuration plugin.
The application object can be accessed via C<$app> or the C<app> function, L<strict>, L<warnings>, L<utf8> and Perl
5.16 L<features|feature> are automatically enabled. A default configuration filename in the application home directory
will be generated from the value of L<Mojolicious/"moniker"> (C<$moniker.conf>). You can extend the normal
configuration file C<$moniker.conf> with C<mode> specific ones like C<$moniker.$mode.conf>, which will be detected
automatically.
If the configuration value C<config_override> has been set in L<Mojolicious/"config"> when this plugin is loaded, it
will not do anything.
The code of this plugin is a good example for learning to build new plugins, you're welcome to fork it.
See L<Mojolicious::Plugins/"PLUGINS"> for a list of plugins that are available by default.
=head1 OPTIONS
L<Mojolicious::Plugin::Config> supports the following options.
=head2 default
# Mojolicious::Lite
plugin Config => {default => {foo => 'bar'}};
Default configuration, making configuration files optional.
=head2 ext
# Mojolicious::Lite
plugin Config => {ext => 'stuff'};
File extension for generated configuration filenames, defaults to C<conf>.
=head2 file
# Mojolicious::Lite
plugin Config => {file => 'myapp.conf'};
plugin Config => {file => '/etc/foo.stuff'};
Path to configuration file, absolute or relative to the application home directory, defaults to the value of the
C<MOJO_CONFIG> environment variable or C<$moniker.conf> in the application home directory.
=head1 METHODS
L<Mojolicious::Plugin::Config> inherits all methods from L<Mojolicious::Plugin> and implements the following new ones.
=head2 load
$plugin->load($file, $conf, $app);
Loads configuration file and passes the content to L</"parse">.
sub load ($self, $file, $conf, $app) {
...
return $self->parse($content, $file, $conf, $app);
}
=head2 parse
$plugin->parse($content, $file, $conf, $app);
Parse configuration file.
sub parse ($self, $content, $file, $conf, $app) {
...
return $hash;
}
=head2 register
my $config = $plugin->register(Mojolicious->new);
my $config = $plugin->register(Mojolicious->new, {file => '/etc/app.conf'});
Register plugin in L<Mojolicious> application and merge configuration.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut

View File

@@ -0,0 +1,805 @@
package Mojolicious::Plugin::DefaultHelpers;
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::Asset::File;
use Mojo::ByteStream;
use Mojo::Collection;
use Mojo::Exception;
use Mojo::IOLoop;
use Mojo::Promise;
use Mojo::Util qw(dumper hmac_sha1_sum steady_time);
use Time::HiRes qw(gettimeofday tv_interval);
use Scalar::Util qw(blessed weaken);
sub register {
my ($self, $app) = @_;
# Controller alias helpers
for my $name (qw(app param stash session url_for)) {
$app->helper($name => sub { shift->$name(@_) });
}
# Stash key shortcuts (should not generate log messages)
for my $name (qw(extends layout title)) {
$app->helper($name => sub { shift->stash($name, @_) });
}
$app->helper(accepts => sub { $_[0]->app->renderer->accepts(@_) });
$app->helper(b => sub { shift; Mojo::ByteStream->new(@_) });
$app->helper(c => sub { shift; Mojo::Collection->new(@_) });
$app->helper(config => sub { shift->app->config(@_) });
$app->helper(content => sub { _content(0, 0, @_) });
$app->helper(content_for => sub { _content(1, 0, @_) });
$app->helper(content_with => sub { _content(0, 1, @_) });
$app->helper($_ => $self->can("_$_"))
for qw(csrf_token current_route flash inactivity_timeout is_fresh), qw(redirect_to respond_to url_with validation);
$app->helper(dumper => sub { shift; dumper @_ });
$app->helper(include => sub { shift->render_to_string(@_) });
$app->helper(log => \&_log);
$app->helper('proxy.get_p' => sub { _proxy_method_p('GET', @_) });
$app->helper('proxy.post_p' => sub { _proxy_method_p('POST', @_) });
$app->helper('proxy.start_p' => \&_proxy_start_p);
$app->helper("reply.$_" => $self->can("_$_")) for qw(asset file static);
$app->helper('reply.exception' => sub { _development('exception', @_) });
$app->helper('reply.not_found' => sub { _development('not_found', @_) });
$app->helper('timing.begin' => \&_timing_begin);
$app->helper('timing.elapsed' => \&_timing_elapsed);
$app->helper('timing.rps' => \&_timing_rps);
$app->helper('timing.server_timing' => \&_timing_server_timing);
$app->helper(ua => sub { shift->app->ua });
}
sub _asset {
my $c = shift;
$c->app->static->serve_asset($c, @_);
$c->rendered;
}
sub _block { ref $_[0] eq 'CODE' ? $_[0]() : $_[0] }
sub _content {
my ($append, $replace, $c, $name, $content) = @_;
$name ||= 'content';
my $hash = $c->stash->{'mojo.content'} //= {};
if (defined $content) {
if ($append) { $hash->{$name} .= _block($content) }
if ($replace) { $hash->{$name} = _block($content) }
else { $hash->{$name} //= _block($content) }
}
return Mojo::ByteStream->new($hash->{$name} // '');
}
sub _csrf_token { $_[0]->session->{csrf_token} ||= hmac_sha1_sum($$ . steady_time . rand, $_[0]->app->secrets->[0]) }
sub _current_route {
return '' unless my $route = shift->match->endpoint;
return @_ ? $route->name eq shift : $route->name;
}
sub _development {
my ($page, $c, $e) = @_;
$c->helpers->log->error(($e = _is_e($e) ? $e : Mojo::Exception->new($e))->inspect) if $page eq 'exception';
# Filtered stash snapshot
my $stash = $c->stash;
%{$stash->{snapshot} = {}} = map { $_ => $stash->{$_} } grep { !/^mojo\./ and defined $stash->{$_} } keys %$stash;
$stash->{exception} = $page eq 'exception' ? $e : undef;
# Render with fallbacks
my $app = $c->app;
my $mode = $app->mode;
my $options = {
format => $stash->{format} || $app->renderer->default_format,
handler => undef,
status => $page eq 'exception' ? 500 : 404,
template => "$page.$mode"
};
my $bundled = 'mojo/' . ($mode eq 'development' ? 'debug' : $page);
return $c if _fallbacks($c, $options, $page, $bundled);
_fallbacks($c, {%$options, format => 'html'}, $page, $bundled);
return $c;
}
sub _fallbacks {
my ($c, $options, $template, $bundled) = @_;
# Mode specific template
return 1 if $c->render_maybe(%$options);
# Normal template
return 1 if $c->render_maybe(%$options, template => $template);
# Inline template
my $stash = $c->stash;
return undef unless $options->{format} eq 'html';
delete @$stash{qw(extends layout)};
return $c->render_maybe($bundled, %$options, handler => 'ep');
}
sub _file { _asset(shift, Mojo::Asset::File->new(path => shift)) }
sub _flash {
my $c = shift;
# Check old flash
my $session = $c->session;
return $session->{flash} ? $session->{flash}{$_[0]} : undef if @_ == 1 && !ref $_[0];
# Initialize new flash and merge values
my $values = ref $_[0] ? $_[0] : {@_};
@{$session->{new_flash} //= {}}{keys %$values} = values %$values;
return $c;
}
sub _inactivity_timeout {
my ($c, $timeout) = @_;
my $stream = Mojo::IOLoop->stream($c->tx->connection // '');
$stream->timeout($timeout) if $stream;
return $c;
}
sub _is_e { blessed $_[0] && $_[0]->isa('Mojo::Exception') }
sub _is_fresh {
my ($c, %options) = @_;
return $c->app->static->is_fresh($c, \%options);
}
sub _log { $_[0]->stash->{'mojo.log'} ||= $_[0]->app->log->context('[' . $_[0]->req->request_id . ']') }
sub _proxy_method_p {
my ($method, $c) = (shift, shift);
return _proxy_start_p($c, $c->ua->build_tx($method, @_));
}
sub _proxy_start_p {
my ($c, $source_tx) = @_;
my $tx = $c->render_later->tx;
my $promise = Mojo::Promise->new;
$source_tx->res->content->auto_upgrade(0)->auto_decompress(0)->once(
body => sub {
my $source_content = shift;
my $source_res = $source_tx->res;
my $res = $tx->res;
my $content = $res->content;
$res->code($source_res->code)->message($source_res->message);
my $headers = $source_res->headers->clone->dehop;
$content->headers($headers);
$promise->resolve;
my $source_stream = Mojo::IOLoop->stream($source_tx->connection);
return unless my $stream = Mojo::IOLoop->stream($tx->connection);
my $write = $source_content->is_chunked ? 'write_chunk' : 'write';
$source_content->unsubscribe('read')->on(
read => sub {
my $data = pop;
$content->$write(length $data ? $data : ()) and $tx->resume;
# Throttle transparently when backpressure rises
return if $stream->can_write;
$source_stream->stop;
$stream->once(drain => sub { $source_stream->start });
}
);
# Unknown length (fall back to connection close)
$source_res->once(finish => sub { $content->$write('') and $tx->resume })
unless length($headers->content_length // '');
}
);
weaken $source_tx;
$source_tx->once(finish => sub { $promise->reject(_tx_error(@_)) });
$c->ua->start_p($source_tx)->catch(sub { });
return $promise;
}
sub _redirect_to {
my $c = shift;
# Don't override 3xx status
my $res = $c->res;
$res->headers->location($c->url_for(@_));
return $c->rendered($res->is_redirect ? () : 302);
}
sub _respond_to {
my ($c, $args) = (shift, ref $_[0] ? $_[0] : {@_});
# Find target
my $target;
my $renderer = $c->app->renderer;
my @formats = @{$renderer->accepts($c)};
for my $format (@formats ? @formats : ($renderer->default_format)) {
next unless $target = $args->{$format};
$c->stash->{format} = $format;
last;
}
# Fallback
unless ($target) {
return $c->rendered(204) unless $target = $args->{any};
delete $c->stash->{format};
}
# Dispatch
ref $target eq 'CODE' ? $target->($c) : $c->render(%$target);
return $c;
}
sub _static {
my ($c, $file) = @_;
return !!$c->rendered if $c->app->static->serve($c, $file);
$c->helpers->log->debug(qq{Static file "$file" not found});
return !$c->helpers->reply->not_found;
}
sub _timing_begin { shift->stash->{'mojo.timing'}{shift()} = [gettimeofday] }
sub _timing_elapsed {
my ($c, $name) = @_;
return undef unless my $started = $c->stash->{'mojo.timing'}{$name};
return tv_interval($started, [gettimeofday()]);
}
sub _timing_rps { $_[1] == 0 ? undef : sprintf '%.3f', 1 / $_[1] }
sub _timing_server_timing {
my ($c, $metric, $desc, $dur) = @_;
my $value = $metric;
$value .= qq{;desc="$desc"} if defined $desc;
$value .= ";dur=$dur" if defined $dur;
$c->res->headers->append('Server-Timing' => $value);
}
sub _tx_error { (shift->error // {})->{message} // 'Unknown error' }
sub _url_with {
my $c = shift;
return $c->url_for(@_)->query($c->req->url->query->clone);
}
sub _validation {
my $c = shift;
my $stash = $c->stash;
return $stash->{'mojo.validation'} if $stash->{'mojo.validation'};
my $req = $c->req;
my $token = $c->session->{csrf_token};
my $header = $req->headers->header('X-CSRF-Token');
my $hash = $req->params->to_hash;
$hash->{csrf_token} //= $header if $token && $header;
$hash->{$_} = $req->every_upload($_) for map { $_->name } @{$req->uploads};
my $v = $c->app->validator->validation->input($hash);
return $stash->{'mojo.validation'} = $v->csrf_token($token);
}
1;
=encoding utf8
=head1 NAME
Mojolicious::Plugin::DefaultHelpers - Default helpers plugin
=head1 SYNOPSIS
# Mojolicious
$app->plugin('DefaultHelpers');
# Mojolicious::Lite
plugin 'DefaultHelpers';
=head1 DESCRIPTION
L<Mojolicious::Plugin::DefaultHelpers> is a collection of helpers for L<Mojolicious>.
This is a core plugin, that means it is always enabled and its code a good example for learning to build new plugins,
you're welcome to fork it.
See L<Mojolicious::Plugins/"PLUGINS"> for a list of plugins that are available by default.
=head1 HELPERS
L<Mojolicious::Plugin::DefaultHelpers> implements the following helpers.
=head2 accepts
my $formats = $c->accepts;
my $format = $c->accepts('html', 'json', 'txt');
Select best possible representation for resource from C<format> C<GET>/C<POST> parameter, C<format> stash value or
C<Accept> request header with L<Mojolicious::Renderer/"accepts">, defaults to returning the first extension if no
preference could be detected.
# Check if JSON is acceptable
$c->render(json => {hello => 'world'}) if $c->accepts('json');
# Check if JSON was specifically requested
$c->render(json => {hello => 'world'}) if $c->accepts('', 'json');
# Unsupported representation
$c->render(data => '', status => 204)
unless my $format = $c->accepts('html', 'json');
# Detected representations to select from
my @formats = @{$c->accepts};
=head2 app
%= app->secrets->[0]
Alias for L<Mojolicious::Controller/"app">.
=head2 b
%= b('Joel is a slug')->slugify
Turn string into a L<Mojo::ByteStream> object.
=head2 c
%= c('a', 'b', 'c')->shuffle->join
Turn list into a L<Mojo::Collection> object.
=head2 config
%= config 'something'
Alias for L<Mojolicious/"config">.
=head2 content
%= content foo => begin
test
% end
%= content bar => 'Hello World!'
%= content 'foo'
%= content 'bar'
%= content
Store partial rendered content in a named buffer and retrieve it later, defaults to retrieving the named buffer
C<content>, which is used by the renderer for the C<layout> and C<extends> features. New content will be ignored if the
named buffer is already in use.
=head2 content_for
% content_for foo => begin
test
% end
%= content_for 'foo'
Same as L</"content">, but appends content to named buffers if they are already in use.
% content_for message => begin
Hello
% end
% content_for message => begin
world!
% end
%= content 'message'
=head2 content_with
% content_with foo => begin
test
% end
%= content_with 'foo'
Same as L</"content">, but replaces content of named buffers if they are already in use.
% content message => begin
world!
% end
% content_with message => begin
Hello <%= content 'message' %>
% end
%= content 'message'
=head2 csrf_token
%= csrf_token
Get CSRF token from L</"session">, and generate one if none exists.
=head2 current_route
% if (current_route 'login') {
Welcome to Mojolicious!
% }
%= current_route
Check or get name of current route.
=head2 dumper
%= dumper {some => 'data'}
Dump a Perl data structure with L<Mojo::Util/"dumper">, very useful for debugging.
=head2 extends
% extends 'blue';
% extends 'blue', title => 'Blue!';
Set C<extends> stash value, all additional key/value pairs get merged into the L</"stash">.
=head2 flash
my $foo = $c->flash('foo');
$c = $c->flash({foo => 'bar'});
$c = $c->flash(foo => 'bar');
%= flash 'foo'
Data storage persistent only for the next request, stored in the L</"session">.
# Show message after redirect
$c->flash(message => 'User created successfully!');
$c->redirect_to('show_user', id => 23);
=head2 inactivity_timeout
$c = $c->inactivity_timeout(3600);
Use L<Mojo::IOLoop/"stream"> to find the current connection and increase timeout if possible.
# Longer version
Mojo::IOLoop->stream($c->tx->connection)->timeout(3600);
=head2 include
%= include 'menubar'
%= include 'menubar', format => 'txt'
Alias for L<Mojolicious::Controller/"render_to_string">.
=head2 is_fresh
my $bool = $c->is_fresh;
my $bool = $c->is_fresh(etag => 'abc');
my $bool = $c->is_fresh(etag => 'W/"def"');
my $bool = $c->is_fresh(last_modified => $epoch);
Check freshness of request by comparing the C<If-None-Match> and C<If-Modified-Since> request headers to the C<ETag>
and C<Last-Modified> response headers with L<Mojolicious::Static/"is_fresh">.
# Add ETag/Last-Modified headers and check freshness before rendering
$c->is_fresh(etag => 'abc', last_modified => 1424985708)
? $c->rendered(304)
: $c->render(text => 'I ♥ Mojolicious!');
=head2 layout
% layout 'green';
% layout 'green', title => 'Green!';
Set C<layout> stash value, all additional key/value pairs get merged into the L</"stash">.
=head2 log
my $log = $c->log;
Alternative to L<Mojolicious/"log"> that includes L<Mojo::Message::Request/"request_id"> with every log message.
# Log message with context
$c->log->debug('This is a log message with request id');
# Pass logger with context to model
my $log = $c->log;
$c->some_model->create({foo => $foo}, $log);
=head2 param
%= param 'foo'
Alias for L<Mojolicious::Controller/"param">.
=head2 proxy->get_p
my $promise = $c->proxy->get_p('http://example.com' => {Accept => '*/*'});
Perform non-blocking C<GET> request and forward response as efficiently as possible, takes the same arguments as
L<Mojo::UserAgent/"get"> and returns a L<Mojo::Promise> object.
# Forward with exception handling
$c->proxy->get_p('http://mojolicious.org')->catch(sub ($err) {
$c->log->debug("Proxy error: $err");
$c->render(text => 'Something went wrong!', status => 400);
});
=head2 proxy->post_p
my $promise = $c->proxy->post_p('http://example.com' => {Accept => '*/*'});
Perform non-blocking C<POST> request and forward response as efficiently as possible, takes the same arguments as
L<Mojo::UserAgent/"post"> and returns a L<Mojo::Promise> object.
# Forward with exception handling
$c->proxy->post_p('example.com' => form => {test => 'pass'})->catch(sub ($err) {
$c->log->debug("Proxy error: $err");
$c->render(text => 'Something went wrong!', status => 400);
});
=head2 proxy->start_p
my $promise = $c->proxy->start_p(Mojo::Transaction::HTTP->new);
Perform non-blocking request for a custom L<Mojo::Transaction::HTTP> object and forward response as efficiently as
possible, returns a L<Mojo::Promise> object.
# Forward with exception handling
my $tx = $c->ua->build_tx(GET => 'http://mojolicious.org');
$c->proxy->start_p($tx)->catch(sub ($err) {
$c->log->debug("Proxy error: $err");
$c->render(text => 'Something went wrong!', status => 400);
});
# Forward with custom request and response headers
my $headers = $c->req->headers->clone->dehop;
$headers->header('X-Proxy' => 'Mojo');
my $tx = $c->ua->build_tx(GET => 'http://example.com' => $headers->to_hash);
$c->proxy->start_p($tx);
$tx->res->content->once(body => sub ($content) { $c->res->headers->header('X-Proxy' => 'Mojo') });
=head2 redirect_to
$c = $c->redirect_to('named', foo => 'bar');
$c = $c->redirect_to('named', {foo => 'bar'});
$c = $c->redirect_to('/index.html');
$c = $c->redirect_to('http://example.com/index.html');
Prepare a C<302> (if the status code is not already C<3xx>) redirect response with C<Location> header, takes the same
arguments as L</"url_for">.
# Moved Permanently
$c->res->code(301);
$c->redirect_to('some_route');
# Temporary Redirect
$c->res->code(307);
$c->redirect_to('some_route');
=head2 reply->asset
$c->reply->asset(Mojo::Asset::File->new);
Reply with a L<Mojo::Asset::File> or L<Mojo::Asset::Memory> object using L<Mojolicious::Static/"serve_asset">, and
perform content negotiation with C<Range>, C<If-Modified-Since> and C<If-None-Match> headers.
# Serve asset with custom modification time
my $asset = Mojo::Asset::Memory->new;
$asset->add_chunk('Hello World!')->mtime(784111777);
$c->res->headers->content_type('text/plain');
$c->reply->asset($asset);
# Serve static file if it exists
if (my $asset = $c->app->static->file('images/logo.png')) {
$c->res->headers->content_type('image/png');
$c->reply->asset($asset);
}
=head2 reply->exception
$c = $c->reply->exception('Oops!');
$c = $c->reply->exception(Mojo::Exception->new);
Render the exception template C<exception.$mode.$format.*> or C<exception.$format.*> and set the response status code
to C<500>. Also sets the stash values C<exception> to a L<Mojo::Exception> object and C<snapshot> to a copy of the
L</"stash"> for use in the templates.
=head2 reply->file
$c->reply->file('/etc/passwd');
Reply with a static file from an absolute path anywhere on the file system using L<Mojolicious/"static">.
# Longer version
$c->reply->asset(Mojo::Asset::File->new(path => '/etc/passwd'));
# Serve file from an absolute path with a custom content type
$c->res->headers->content_type('application/myapp');
$c->reply->file('/home/sri/foo.txt');
# Serve file from a secret application directory
$c->reply->file($c->app->home->child('secret', 'file.txt'));
=head2 reply->not_found
$c = $c->reply->not_found;
Render the not found template C<not_found.$mode.$format.*> or C<not_found.$format.*> and set the response status code
to C<404>. Also sets the stash value C<snapshot> to a copy of the L</"stash"> for use in the templates.
=head2 reply->static
my $bool = $c->reply->static('images/logo.png');
my $bool = $c->reply->static('../lib/MyApp.pm');
Reply with a static file using L<Mojolicious/"static">, usually from the C<public> directories or C<DATA> sections of
your application. Note that this helper uses a relative path, but does not protect from traversing to parent
directories.
# Serve file from a relative path with a custom content type
$c->res->headers->content_type('application/myapp');
$c->reply->static('foo.txt');
=head2 respond_to
$c = $c->respond_to(
json => {json => {message => 'Welcome!'}},
html => {template => 'welcome'},
any => sub {...}
);
Automatically select best possible representation for resource from C<format> C<GET>/C<POST> parameter, C<format> stash
value or C<Accept> request header, defaults to L<Mojolicious::Renderer/"default_format"> or rendering an empty C<204>
response. Each representation can be handled with a callback or a hash reference containing arguments to be passed to
L<Mojolicious::Controller/"render">.
# Everything else than "json" and "xml" gets a 204 response
$c->respond_to(
json => sub { $c->render(json => {just => 'works'}) },
xml => {text => '<just>works</just>'},
any => {data => '', status => 204}
);
For more advanced negotiation logic you can also use L</"accepts">.
=head2 session
%= session 'foo'
Alias for L<Mojolicious::Controller/"session">.
=head2 stash
%= stash 'foo'
% stash foo => 'bar';
Alias for L<Mojolicious::Controller/"stash">.
%= stash('name') // 'Somebody'
=head2 timing->begin
$c->timing->begin('foo');
Create named timestamp for L<"timing-E<gt>elapsed">.
=head2 timing->elapsed
my $elapsed = $c->timing->elapsed('foo');
Return fractional amount of time in seconds since named timstamp has been created with L</"timing-E<gt>begin"> or
C<undef> if no such timestamp exists.
# Log timing information
$c->timing->begin('database_stuff');
...
my $elapsed = $c->timing->elapsed('database_stuff');
$c->app->log->debug("Database stuff took $elapsed seconds");
=head2 timing->rps
my $rps = $c->timing->rps('0.001');
Return fractional number of requests that could be performed in one second if every singe one took the given amount of
time in seconds or C<undef> if the number is too low.
# Log more timing information
$c->timing->begin('web_stuff');
...
my $elapsed = $c->timing->elapsed('web_stuff');
my $rps = $c->timing->rps($elapsed);
$c->app->log->debug("Web stuff took $elapsed seconds ($rps per second)");
=head2 timing->server_timing
$c->timing->server_timing('metric');
$c->timing->server_timing('metric', 'Some Description');
$c->timing->server_timing('metric', 'Some Description', '0.001');
Create C<Server-Timing> header with optional description and duration.
# "Server-Timing: miss"
$c->timing->server_timing('miss');
# "Server-Timing: dc;desc=atl"
$c->timing->server_timing('dc', 'atl');
# "Server-Timing: db;desc=Database;dur=0.0001"
$c->timing->begin('database_stuff');
...
my $elapsed = $c->timing->elapsed('database_stuff');
$c->timing->server_timing('db', 'Database', $elapsed);
# "Server-Timing: miss, dc;desc=atl"
$c->timing->server_timing('miss');
$c->timing->server_timing('dc', 'atl');
=head2 title
%= title
% title 'Welcome!';
% title 'Welcome!', foo => 'bar';
Get or set C<title> stash value, all additional key/value pairs get merged into the L</"stash">.
=head2 ua
%= ua->get('mojolicious.org')->result->dom->at('title')->text
Alias for L<Mojolicious/"ua">.
=head2 url_for
%= url_for 'named', controller => 'bar', action => 'baz'
Alias for L<Mojolicious::Controller/"url_for">.
%= url_for('/index.html')->query(foo => 'bar')
=head2 url_with
%= url_with 'named', controller => 'bar', action => 'baz'
Does the same as L</"url_for">, but inherits query parameters from the current request.
%= url_with->query({page => 2})
=head2 validation
my $v = $c->validation;
Get L<Mojolicious::Validator::Validation> object for current request to validate file uploads as well as C<GET> and
C<POST> parameters extracted from the query string and C<application/x-www-form-urlencoded> or C<multipart/form-data>
message body. Parts of the request body need to be loaded into memory to parse C<POST> parameters, so you have to make
sure it is not excessively large. There's a 16MiB limit for requests by default.
# Validate GET/POST parameter
my $v = $c->validation;
$v->required('title', 'trim')->size(3, 50);
my $title = $v->param('title');
# Validate file upload
my $v = $c->validation;
$v->required('tarball')->upload->size(1, 1048576);
my $tarball = $v->param('tarball');
=head1 METHODS
L<Mojolicious::Plugin::DefaultHelpers> inherits all methods from L<Mojolicious::Plugin> and implements the following
new ones.
=head2 register
$plugin->register(Mojolicious->new);
Register helpers in L<Mojolicious> application.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut

View File

@@ -0,0 +1,99 @@
package Mojolicious::Plugin::EPLRenderer;
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::Template;
use Mojo::Util qw(encode md5_sum);
sub register {
my ($self, $app) = @_;
$app->renderer->add_handler(epl => sub { _render(@_, Mojo::Template->new, $_[1]) });
}
sub _render {
my ($renderer, $c, $output, $options, $mt, @args) = @_;
# Cached
if ($mt->compiled) {
$c->helpers->log->debug("Rendering cached @{[$mt->name]}");
$$output = $mt->process(@args);
}
# Not cached
else {
my $inline = $options->{inline};
my $name = defined $inline ? md5_sum encode('UTF-8', $inline) : undef;
return unless defined($name //= $renderer->template_name($options));
# Inline
if (defined $inline) {
$c->helpers->log->debug(qq{Rendering inline template "$name"});
$$output = $mt->name(qq{inline template "$name"})->render($inline, @args);
}
# File
else {
if (my $encoding = $renderer->encoding) { $mt->encoding($encoding) }
# Try template
if (defined(my $path = $renderer->template_path($options))) {
$c->helpers->log->debug(qq{Rendering template "$name"});
$$output = $mt->name(qq{template "$name"})->render_file($path, @args);
}
# Try DATA section
elsif (defined(my $d = $renderer->get_data_template($options))) {
$c->helpers->log->debug(qq{Rendering template "$name" from DATA section});
$$output = $mt->name(qq{template "$name" from DATA section})->render($d, @args);
}
# No template
else { $c->helpers->log->debug(qq{Template "$name" not found}) }
}
}
# Exception
die $$output if ref $$output;
}
1;
=encoding utf8
=head1 NAME
Mojolicious::Plugin::EPLRenderer - Embedded Perl Lite renderer plugin
=head1 SYNOPSIS
# Mojolicious
$app->plugin('EPLRenderer');
# Mojolicious::Lite
plugin 'EPLRenderer';
=head1 DESCRIPTION
L<Mojolicious::Plugin::EPLRenderer> is a renderer for C<epl> templates, which are pretty much just raw
L<Mojo::Template>.
This is a core plugin, that means it is always enabled and its code a good example for learning to build new plugins,
you're welcome to fork it.
See L<Mojolicious::Plugins/"PLUGINS"> for a list of plugins that are available by default.
=head1 METHODS
L<Mojolicious::Plugin::EPLRenderer> inherits all methods from L<Mojolicious::Plugin> and implements the following new
ones.
=head2 register
$plugin->register(Mojolicious->new);
Register renderer in L<Mojolicious> application.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut

View File

@@ -0,0 +1,116 @@
package Mojolicious::Plugin::EPRenderer;
use Mojo::Base 'Mojolicious::Plugin::EPLRenderer';
use Mojo::Template;
use Mojo::Util qw(encode md5_sum monkey_patch);
sub DESTROY { Mojo::Util::_teardown(shift->{namespace}) }
sub register {
my ($self, $app, $conf) = @_;
# Auto escape by default to prevent XSS attacks
my $ep = {auto_escape => 1, %{$conf->{template} // {}}, vars => 1};
my $ns = $self->{namespace} = $ep->{namespace} //= 'Mojo::Template::Sandbox::' . md5_sum "$self";
# Make "$self" and "$c" available in templates
$ep->{prepend} = 'my $self = my $c = _C;' . ($ep->{prepend} // '');
# Add "ep" handler and make it the default
$app->renderer->default_handler('ep')->add_handler(
$conf->{name} || 'ep' => sub {
my ($renderer, $c, $output, $options) = @_;
my $name = $options->{inline} // $renderer->template_name($options);
return unless defined $name;
my $key = md5_sum encode 'UTF-8', $name;
my $cache = $renderer->cache;
my $mt = $cache->get($key);
$cache->set($key => $mt = Mojo::Template->new($ep)) unless $mt;
# Export helpers only once
++$self->{helpers} and _helpers($ns, $renderer->helpers) unless $self->{helpers};
# Make current controller available and render with "epl" handler
no strict 'refs';
no warnings 'redefine';
local *{"${ns}::_C"} = sub {$c};
Mojolicious::Plugin::EPLRenderer::_render($renderer, $c, $output, $options, $mt, $c->stash);
}
);
}
sub _helpers {
my ($class, $helpers) = @_;
for my $method (grep {/^\w+$/} keys %$helpers) {
my $sub = $helpers->{$method};
monkey_patch $class, $method, sub { $class->_C->$sub(@_) };
}
}
1;
=encoding utf8
=head1 NAME
Mojolicious::Plugin::EPRenderer - Embedded Perl renderer plugin
=head1 SYNOPSIS
# Mojolicious
$app->plugin('EPRenderer');
$app->plugin(EPRenderer => {name => 'foo'});
$app->plugin(EPRenderer => {name => 'bar', template => {line_start => '.'}});
# Mojolicious::Lite
plugin 'EPRenderer';
plugin EPRenderer => {name => 'foo'};
plugin EPRenderer => {name => 'bar', template => {line_start => '.'}};
=head1 DESCRIPTION
L<Mojolicious::Plugin::EPRenderer> is a renderer for Embedded Perl templates. For more information see
L<Mojolicious::Guides::Rendering/"Embedded Perl">.
This is a core plugin, that means it is always enabled and its code a good example for learning to build new plugins,
you're welcome to fork it.
See L<Mojolicious::Plugins/"PLUGINS"> for a list of plugins that are available by default.
=head1 OPTIONS
L<Mojolicious::Plugin::EPRenderer> supports the following options.
=head2 name
# Mojolicious::Lite
plugin EPRenderer => {name => 'foo'};
Handler name, defaults to C<ep>.
=head2 template
# Mojolicious::Lite
plugin EPRenderer => {template => {line_start => '.'}};
Attribute values passed to L<Mojo::Template> objects used to render templates.
=head1 METHODS
L<Mojolicious::Plugin::EPRenderer> inherits all methods from L<Mojolicious::Plugin::EPLRenderer> and implements the
following new ones.
=head2 register
$plugin->register(Mojolicious->new);
$plugin->register(Mojolicious->new, {name => 'foo'});
Register renderer in L<Mojolicious> application.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut

View File

@@ -0,0 +1,85 @@
package Mojolicious::Plugin::HeaderCondition;
use Mojo::Base 'Mojolicious::Plugin';
use re qw(is_regexp);
sub register {
my ($self, $app) = @_;
$app->routes->add_condition(headers => \&_headers);
$app->routes->add_condition(agent => sub { _headers(@_[0 .. 2], {'User-Agent' => $_[3]}) });
$app->routes->add_condition(host => sub { _check($_[1]->req->url->to_abs->host, $_[3]) });
}
sub _check {
my ($value, $pattern) = @_;
return 1 if $value && $pattern && is_regexp($pattern) && $value =~ $pattern;
return $value && defined $pattern && $pattern eq $value;
}
sub _headers {
my ($route, $c, $captures, $patterns) = @_;
return undef unless $patterns && ref $patterns eq 'HASH' && keys %$patterns;
# All headers need to match
my $headers = $c->req->headers;
_check($headers->header($_), $patterns->{$_}) || return undef for keys %$patterns;
return 1;
}
1;
=encoding utf8
=head1 NAME
Mojolicious::Plugin::HeaderCondition - Header condition plugin
=head1 SYNOPSIS
# Mojolicious
$app->plugin('HeaderCondition');
$app->routes->get('/:controller/:action')
->requires(headers => {Referer => qr/example\.com/});
# Mojolicious::Lite
plugin 'HeaderCondition';
get '/' => (headers => {Referer => qr/example\.com/}) => sub {...};
# All headers need to match
$app->routes->get('/:controller/:action')->requires(headers => {
'X-Secret-Header' => 'Foo',
Referer => qr/example\.com/
});
# The "agent" condition is a shortcut for the "User-Agent" header
get '/' => (agent => qr/Firefox/) => sub {...};
# The "host" condition is a shortcut for the detected host
get '/' => (host => qr/mojolicious\.org/) => sub {...};
=head1 DESCRIPTION
L<Mojolicious::Plugin::HeaderCondition> is a route condition for header-based routes.
This is a core plugin, that means it is always enabled and its code a good example for learning to build new plugins,
you're welcome to fork it.
See L<Mojolicious::Plugins/"PLUGINS"> for a list of plugins that are available by default.
=head1 METHODS
L<Mojolicious::Plugin::HeaderCondition> inherits all methods from L<Mojolicious::Plugin> and implements the following
new ones.
=head2 register
$plugin->register(Mojolicious->new);
Register conditions in L<Mojolicious> application.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut

View File

@@ -0,0 +1,140 @@
package Mojolicious::Plugin::JSONConfig;
use Mojo::Base 'Mojolicious::Plugin::Config';
use Mojo::JSON qw(from_json);
use Mojo::Template;
sub parse {
my ($self, $content, $file, $conf, $app) = @_;
my $config = eval { from_json $self->render($content, $file, $conf, $app) };
die qq{Can't parse config "$file": $@} if $@;
die qq{Invalid config "$file"} unless ref $config eq 'HASH';
return $config;
}
sub register { shift->SUPER::register(shift, {ext => 'json', %{shift()}}) }
sub render {
my ($self, $content, $file, $conf, $app) = @_;
# Application instance and helper
my $prepend = q[no strict 'refs'; no warnings 'redefine';];
$prepend .= q[my $app = shift; sub app; local *app = sub { $app };];
$prepend .= q[use Mojo::Base -strict; no warnings 'ambiguous';];
my $mt = Mojo::Template->new($conf->{template} // {})->name($file);
my $output = $mt->prepend($prepend . $mt->prepend)->render($content, $app);
return ref $output ? die $output : $output;
}
1;
=encoding utf8
=head1 NAME
Mojolicious::Plugin::JSONConfig - JSON configuration plugin
=head1 SYNOPSIS
# myapp.json (it's just JSON with embedded Perl)
{
%# Just a value
"foo": "bar",
%# Nested data structures are fine too
"baz": ["♥"],
%# You have full access to the application
"music_dir": "<%= app->home->child('music') %>"
}
# Mojolicious
my $config = $app->plugin('JSONConfig');
say $config->{foo};
# Mojolicious::Lite
my $config = plugin 'JSONConfig';
say $config->{foo};
# foo.html.ep
%= config->{foo}
# The configuration is available application-wide
my $config = app->config;
say $config->{foo};
# Everything can be customized with options
my $config = plugin JSONConfig => {file => '/etc/myapp.conf'};
=head1 DESCRIPTION
L<Mojolicious::Plugin::JSONConfig> is a JSON configuration plugin that preprocesses its input with L<Mojo::Template>.
The application object can be accessed via C<$app> or the C<app> function. A default configuration filename in the
application home directory will be generated from the value of L<Mojolicious/"moniker"> (C<$moniker.json>). You can
extend the normal configuration file C<$moniker.json> with C<mode> specific ones like C<$moniker.$mode.json>, which
will be detected automatically.
If the configuration value C<config_override> has been set in L<Mojolicious/"config"> when this plugin is loaded, it
will not do anything.
The code of this plugin is a good example for learning to build new plugins, you're welcome to fork it.
See L<Mojolicious::Plugins/"PLUGINS"> for a list of plugins that are available by default.
=head1 OPTIONS
L<Mojolicious::Plugin::JSONConfig> inherits all options from L<Mojolicious::Plugin::Config> and supports the following
new ones.
=head2 template
# Mojolicious::Lite
plugin JSONConfig => {template => {line_start => '.'}};
Attribute values passed to L<Mojo::Template> object used to preprocess configuration files.
=head1 METHODS
L<Mojolicious::Plugin::JSONConfig> inherits all methods from L<Mojolicious::Plugin::Config> and implements the
following new ones.
=head2 parse
$plugin->parse($content, $file, $conf, $app);
Process content with L</"render"> and parse it with L<Mojo::JSON>.
sub parse ($self, $content, $file, $conf, $app) {
...
$content = $self->render($content, $file, $conf, $app);
...
return $hash;
}
=head2 register
my $config = $plugin->register(Mojolicious->new);
my $config = $plugin->register(Mojolicious->new, {file => '/etc/foo.conf'});
Register plugin in L<Mojolicious> application and merge configuration.
=head2 render
$plugin->render($content, $file, $conf, $app);
Process configuration file with L<Mojo::Template>.
sub render ($self, $content, $file, $conf, $app) {
...
return $content;
}
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut

View File

@@ -0,0 +1,76 @@
package Mojolicious::Plugin::Mount;
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::Server;
sub register {
my ($self, $app, $conf) = @_;
my $path = (keys %$conf)[0];
my $embed = Mojo::Server->new->load_app($conf->{$path});
$embed->secrets($app->secrets);
# Extract host
my $host;
($host, $path) = ($1 ? qr/^(?:.*\.)?\Q$2\E$/i : qr/^\Q$2\E$/i, $3) if $path =~ m!^(\*\.)?([^/]+)(/.*)?$!;
my $route = $app->routes->any($path)->partial(1)->to(app => $embed);
return $host ? $route->requires(host => $host) : $route;
}
1;
=encoding utf8
=head1 NAME
Mojolicious::Plugin::Mount - Application mount plugin
=head1 SYNOPSIS
# Mojolicious
my $route = $app->plugin(Mount => {'/prefix' => '/home/sri/foo/script/foo'});
# Mojolicious::Lite
my $route = plugin Mount => {'/prefix' => '/home/sri/myapp.pl'};
# Adjust the generated route and mounted application
my $example = plugin Mount => {'/example' => '/home/sri/example.pl'};
$example->to(message => 'It works great!');
my $app = $example->pattern->defaults->{app};
$app->config(foo => 'bar');
$app->log(app->log);
# Mount application with host
plugin Mount => {'example.com' => '/home/sri/myapp.pl'};
# Host and path
plugin Mount => {'example.com/myapp' => '/home/sri/myapp.pl'};
# Or even hosts with wildcard subdomains
plugin Mount => {'*.example.com/myapp' => '/home/sri/myapp.pl'};
=head1 DESCRIPTION
L<Mojolicious::Plugin::Mount> is a plugin that allows you to mount whole L<Mojolicious> applications.
The code of this plugin is a good example for learning to build new plugins, you're welcome to fork it.
See L<Mojolicious::Plugins/"PLUGINS"> for a list of plugins that are available by default.
=head1 METHODS
L<Mojolicious::Plugin::Mount> inherits all methods from L<Mojolicious::Plugin> and implements the following new ones.
=head2 register
my $route = $plugin->register(Mojolicious->new, {'/foo' => '/some/app.pl'});
Mount L<Mojolicious> application and return the generated route, which is usually a L<Mojolicious::Routes::Route>
object.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut

View File

@@ -0,0 +1,122 @@
package Mojolicious::Plugin::NotYAMLConfig;
use Mojo::Base 'Mojolicious::Plugin::JSONConfig';
use CPAN::Meta::YAML;
use Mojo::Util qw(decode encode);
sub register {
my ($self, $app, $conf) = @_;
$conf->{ext} //= 'yml';
$self->{yaml} = sub { CPAN::Meta::YAML::Load(decode 'UTF-8', shift) };
if (my $mod = $conf->{module}) {
die qq{YAML module $mod has no Load function} unless $self->{yaml} = $mod->can('Load');
}
return $self->SUPER::register($app, $conf);
}
sub parse {
my ($self, $content, $file, $conf, $app) = @_;
my $config = eval { $self->{yaml}->(encode('UTF-8', $self->render($content, $file, $conf, $app))) };
die qq{Can't parse config "$file": $@} if $@;
die qq{Invalid config "$file"} unless ref $config eq 'HASH';
return $config;
}
1;
=encoding utf8
=head1 NAME
Mojolicious::Plugin::NotYAMLConfig - Not quite YAML configuration plugin
=head1 SYNOPSIS
# myapp.yml (it's just YAML with embedded Perl)
---
foo: bar
baz:
- ♥
music_dir: <%= app->home->child('music') %>
# Mojolicious
my $config = $app->plugin('NotYAMLConfig');
say $config->{foo};
# Mojolicious::Lite
my $config = plugin 'NotYAMLConfig';
say $config->{foo};
# foo.html.ep
%= config->{foo}
# The configuration is available application-wide
my $config = app->config;
say $config->{foo};
# Everything can be customized with options
my $config = plugin NotYAMLConfig => {file => '/etc/myapp.conf'};
=head1 DESCRIPTION
L<Mojolicious::Plugin::NotYAMLConfig> is a YAML configuration plugin that preprocesses its input with L<Mojo::Template>.
By default it uses L<CPAN::Meta::YAML> for parsing, which is not the best YAML module available, but good enough for
most config files. If you need something more correct you can use a different module like L<YAML::XS> with the
L</"module"> option.
The application object can be accessed via C<$app> or the C<app> function. A default configuration filename in the
application home directory will be generated from the value of L<Mojolicious/"moniker"> (C<$moniker.yml>). You can
extend the normal configuration file C<$moniker.yml> with C<mode> specific ones like C<$moniker.$mode.yml>, which will
be detected automatically.
If the configuration value C<config_override> has been set in L<Mojolicious/"config"> when this plugin is loaded, it
will not do anything.
The code of this plugin is a good example for learning to build new plugins, you're welcome to fork it.
See L<Mojolicious::Plugins/"PLUGINS"> for a list of plugins that are available by default.
=head1 OPTIONS
L<Mojolicious::Plugin::NotYAMLConfig> inherits all options from L<Mojolicious::Plugin::JSONConfig> and supports the
following new ones.
=head2 module
# Mojolicious::Lite
plugin NotYAMLConfig => {module => 'YAML::PP'};
Alternative YAML module to use for parsing.
=head1 METHODS
L<Mojolicious::Plugin::NotYAMLConfig> inherits all methods from L<Mojolicious::Plugin::JSONConfig> and implements the
following new ones.
=head2 parse
$plugin->parse($content, $file, $conf, $app);
Process content with L<Mojolicious::Plugin::JSONConfig/"render"> and parse it with L<CPAN::Meta::YAML>.
sub parse ($self, $content, $file, $conf, $app) {
...
$content = $self->render($content, $file, $conf, $app);
...
return $hash;
}
=head2 register
my $config = $plugin->register(Mojolicious->new);
my $config = $plugin->register(Mojolicious->new, {file => '/etc/foo.conf'});
Register plugin in L<Mojolicious> application and merge configuration.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut

View File

@@ -0,0 +1,772 @@
package Mojolicious::Plugin::TagHelpers;
use Mojo::Base 'Mojolicious::Plugin';
use Mojo::ByteStream;
use Mojo::DOM::HTML qw(tag_to_html);
use Scalar::Util qw(blessed);
sub register {
my ($self, $app) = @_;
# Text field variations
my @time = qw(date month time week);
for my $name (@time, qw(color email number range search tel text url)) {
$app->helper("${name}_field" => sub { _input(@_, type => $name) });
}
$app->helper(datetime_field => sub { _input(@_, type => 'datetime-local') });
my @helpers = (
qw(csrf_field form_for hidden_field javascript label_for link_to select_field stylesheet submit_button),
qw(tag_with_error text_area)
);
$app->helper($_ => __PACKAGE__->can("_$_")) for @helpers;
$app->helper(button_to => sub { _button_to(0, @_) });
$app->helper(check_box => sub { _input(@_, type => 'checkbox') });
$app->helper(csrf_button_to => sub { _button_to(1, @_) });
$app->helper(file_field => sub { _empty_field('file', @_) });
$app->helper(image => sub { _tag('img', src => shift->url_for(shift), @_) });
$app->helper(input_tag => sub { _input(@_) });
$app->helper(password_field => sub { _empty_field('password', @_) });
$app->helper(radio_button => sub { _input(@_, type => 'radio') });
# "t" is just a shortcut for the "tag" helper
$app->helper($_ => sub { shift; _tag(@_) }) for qw(t tag);
}
sub _button_to {
my ($csrf, $c, $text) = (shift, shift, shift);
my $prefix = $csrf ? _csrf_field($c) : '';
return _form_for($c, @_, sub { $prefix . _submit_button($c, $text) });
}
sub _csrf_field {
my $c = shift;
return _hidden_field($c, csrf_token => $c->helpers->csrf_token, @_);
}
sub _empty_field {
my ($type, $c, $name) = (shift, shift, shift);
return _validation($c, $name, 'input', name => $name, @_, type => $type);
}
sub _form_for {
my ($c, @url) = (shift, shift);
push @url, shift if ref $_[0] eq 'HASH';
# Method detection
my $r = $c->app->routes->lookup($url[0]);
my $method = $r ? $r->suggested_method : 'GET';
my @post = $method ne 'GET' ? (method => 'POST') : ();
my $url = $c->url_for(@url);
$url->query({_method => $method}) if @post && $method ne 'POST';
return _tag('form', action => $url, @post, @_);
}
sub _hidden_field {
my ($c, $name, $value) = (shift, shift, shift);
return _tag('input', name => $name, value => $value, @_, type => 'hidden');
}
sub _input {
my ($c, $name) = (shift, shift);
my %attrs = @_ % 2 ? (value => shift, @_) : @_;
if (my @values = @{$c->every_param($name)}) {
# Checkbox or radiobutton
my $type = $attrs{type} || '';
if ($type eq 'checkbox' || $type eq 'radio') {
my $value = $attrs{value} // 'on';
delete $attrs{checked};
$attrs{checked} = undef if grep { $_ eq $value } @values;
}
# Others
else { $attrs{value} = $values[-1] }
}
return _validation($c, $name, 'input', name => $name, %attrs);
}
sub _javascript {
my $c = shift;
my $content = ref $_[-1] eq 'CODE' ? "//<![CDATA[\n" . pop->() . "\n//]]>" : '';
my @src = @_ % 2 ? (src => $c->url_for(shift)) : ();
return _tag('script', @src, @_, sub {$content});
}
sub _label_for {
my ($c, $name) = (shift, shift);
my $content = ref $_[-1] eq 'CODE' ? pop : shift;
return _validation($c, $name, 'label', for => $name, @_, $content);
}
sub _link_to {
my ($c, $content) = (shift, shift);
my @url = ($content);
# Content
unless (ref $_[-1] eq 'CODE') {
@url = (shift);
push @_, $content;
}
# Captures
push @url, shift if ref $_[0] eq 'HASH';
return _tag('a', href => $c->url_for(@url), @_);
}
sub _option {
my ($values, $pair) = @_;
$pair = [$pair => $pair] unless ref $pair eq 'ARRAY';
my %attrs = (value => $pair->[1], @$pair[2 .. $#$pair]);
delete $attrs{selected} if keys %$values;
$attrs{selected} = undef if $values->{$pair->[1]};
return _tag('option', %attrs, $pair->[0]);
}
sub _select_field {
my ($c, $name, $options, %attrs) = (shift, shift, shift, @_);
my %values = map { $_ => 1 } grep {defined} @{$c->every_param($name)};
my $groups = '';
for my $group (@$options) {
# "optgroup" tag
if (blessed $group && $group->isa('Mojo::Collection')) {
my ($label, $values, %attrs) = @$group;
my $content = join '', map { _option(\%values, $_) } @$values;
$groups .= _tag('optgroup', label => $label, %attrs, sub {$content});
}
# "option" tag
else { $groups .= _option(\%values, $group) }
}
return _validation($c, $name, 'select', name => $name, %attrs, sub {$groups});
}
sub _stylesheet {
my $c = shift;
my $content = ref $_[-1] eq 'CODE' ? "/*<![CDATA[*/\n" . pop->() . "\n/*]]>*/" : '';
return _tag('style', @_, sub {$content}) unless @_ % 2;
return _tag('link', rel => 'stylesheet', href => $c->url_for(shift), @_);
}
sub _submit_button {
my ($c, $value) = (shift, shift // 'Ok');
return _tag('input', value => $value, @_, type => 'submit');
}
sub _tag { Mojo::ByteStream->new(tag_to_html(@_)) }
sub _tag_with_error {
my ($c, $tag) = (shift, shift);
my ($content, %attrs) = (@_ % 2 ? pop : undef, @_);
$attrs{class} .= $attrs{class} ? ' field-with-error' : 'field-with-error';
return _tag($tag, %attrs, defined $content ? $content : ());
}
sub _text_area {
my ($c, $name) = (shift, shift);
my $cb = ref $_[-1] eq 'CODE' ? pop : undef;
my $content = @_ % 2 ? shift : undef;
$content = $c->param($name) // $content // $cb // '';
return _validation($c, $name, 'textarea', name => $name, @_, $content);
}
sub _validation {
my ($c, $name) = (shift, shift);
return _tag(@_) unless $c->helpers->validation->has_error($name);
return $c->helpers->tag_with_error(@_);
}
1;
=encoding utf8
=head1 NAME
Mojolicious::Plugin::TagHelpers - Tag helpers plugin
=head1 SYNOPSIS
# Mojolicious
$app->plugin('TagHelpers');
# Mojolicious::Lite
plugin 'TagHelpers';
=head1 DESCRIPTION
L<Mojolicious::Plugin::TagHelpers> is a collection of HTML tag helpers for L<Mojolicious>, based on the L<HTML Living
Standard|https://html.spec.whatwg.org>.
Most form helpers can automatically pick up previous input values and will show them as default. You can also use
L<Mojolicious::Plugin::DefaultHelpers/"param"> to set them manually and let necessary attributes always be generated
automatically.
% param country => 'germany' unless param 'country';
<%= radio_button country => 'germany' %> Germany
<%= radio_button country => 'france' %> France
<%= radio_button country => 'uk' %> UK
For fields that failed validation with L<Mojolicious::Plugin::DefaultHelpers/"validation"> the C<field-with-error>
class will be automatically added through L</"tag_with_error">, to make styling with CSS easier.
<input class="field-with-error" name="age" type="text" value="250">
This is a core plugin, that means it is always enabled and its code a good example for learning how to build new
plugins, you're welcome to fork it.
See L<Mojolicious::Plugins/"PLUGINS"> for a list of plugins that are available by default.
=head1 HELPERS
L<Mojolicious::Plugin::TagHelpers> implements the following helpers.
=head2 button_to
%= button_to Test => 'some_get_route'
%= button_to Test => some_get_route => {id => 23} => (class => 'menu')
%= button_to Test => 'http://example.com/test' => (class => 'menu')
%= button_to Remove => 'some_delete_route'
Generate portable C<form> tag with L</"form_for">, containing a single button.
<form action="/path/to/get/route">
<input type="submit" value="Test">
</form>
<form action="/path/to/get/route/23" class="menu">
<input type="submit" value="Test">
</form>
<form action="http://example.com/test" class="menu">
<input type="submit" value="Test">
</form>
<form action="/path/to/delete/route?_method=DELETE" method="POST">
<input type="submit" value="Remove">
</form>
=head2 check_box
%= check_box 'employed'
%= check_box employed => 1
%= check_box employed => 1, checked => undef, id => 'foo'
Generate C<input> tag of type C<checkbox>. Previous input values will automatically get picked up and shown as default.
<input name="employed" type="checkbox">
<input name="employed" type="checkbox" value="1">
<input checked id="foo" name="employed" type="checkbox" value="1">
=head2 color_field
%= color_field 'background'
%= color_field background => '#ffffff'
%= color_field background => '#ffffff', id => 'foo'
Generate C<input> tag of type C<color>. Previous input values will automatically get picked up and shown as default.
<input name="background" type="color">
<input name="background" type="color" value="#ffffff">
<input id="foo" name="background" type="color" value="#ffffff">
=head2 csrf_button_to
%= csrf_button_to Remove => 'some_delete_route'
Same as L</"button_to">, but also includes a L</"csrf_field">.
<form action="/path/to/delete/route?_method=DELETE" method="POST">
<input name="csrf_token" type="hidden" value="fa6a08...">
<input type="submit" value="Remove">
</form>
=head2 csrf_field
%= csrf_field
Generate C<input> tag of type C<hidden> with L<Mojolicious::Plugin::DefaultHelpers/"csrf_token">.
<input name="csrf_token" type="hidden" value="fa6a08...">
=head2 date_field
%= date_field 'end'
%= date_field end => '2012-12-21'
%= date_field end => '2012-12-21', id => 'foo'
Generate C<input> tag of type C<date>. Previous input values will automatically get picked up and shown as default.
<input name="end" type="date">
<input name="end" type="date" value="2012-12-21">
<input id="foo" name="end" type="date" value="2012-12-21">
=head2 datetime_field
%= datetime_field 'end'
%= datetime_field end => '2012-12-21T23:59:59'
%= datetime_field end => '2012-12-21T23:59:59', id => 'foo'
Generate C<input> tag of type C<datetime-local>. Previous input values will automatically get picked up and shown as
default.
<input name="end" type="datetime-local">
<input name="end" type="datetime-local" value="2012-12-21T23:59:59">
<input id="foo" name="end" type="datetime-local" value="2012-12-21T23:59:59">
=head2 email_field
%= email_field 'notify'
%= email_field notify => 'nospam@example.com'
%= email_field notify => 'nospam@example.com', id => 'foo'
Generate C<input> tag of type C<email>. Previous input values will automatically get picked up and shown as default.
<input name="notify" type="email">
<input name="notify" type="email" value="nospam@example.com">
<input id="foo" name="notify" type="email" value="nospam@example.com">
=head2 file_field
%= file_field 'avatar'
%= file_field 'avatar', id => 'foo'
Generate C<input> tag of type C<file>.
<input name="avatar" type="file">
<input id="foo" name="avatar" type="file">
=head2 form_for
%= form_for login => begin
%= text_field 'first_name'
%= submit_button
% end
%= form_for login => {format => 'txt'} => (method => 'POST') => begin
%= text_field 'first_name'
%= submit_button
% end
%= form_for '/login' => (enctype => 'multipart/form-data') => begin
%= text_field 'first_name', disabled => 'disabled'
%= submit_button
% end
%= form_for 'http://example.com/login' => (method => 'POST') => begin
%= text_field 'first_name'
%= submit_button
% end
%= form_for some_delete_route => begin
%= submit_button 'Remove'
% end
Generate portable C<form> tag with L<Mojolicious::Controller/"url_for">. For routes that do not allow C<GET>, a
C<method> attribute with the value C<POST> will be automatically added. And for methods other than C<GET> or C<POST>,
an C<_method> query parameter will be added as well.
<form action="/path/to/login">
<input name="first_name" type="text">
<input type="submit" value="Ok">
</form>
<form action="/path/to/login.txt" method="POST">
<input name="first_name" type="text">
<input type="submit" value="Ok">
</form>
<form action="/path/to/login" enctype="multipart/form-data">
<input disabled="disabled" name="first_name" type="text">
<input type="submit" value="Ok">
</form>
<form action="http://example.com/login" method="POST">
<input name="first_name" type="text">
<input type="submit" value="Ok">
</form>
<form action="/path/to/delete/route?_method=DELETE" method="POST">
<input type="submit" value="Remove">
</form>
=head2 hidden_field
%= hidden_field foo => 'bar'
%= hidden_field foo => 'bar', id => 'bar'
Generate C<input> tag of type C<hidden>.
<input name="foo" type="hidden" value="bar">
<input id="bar" name="foo" type="hidden" value="bar">
=head2 image
%= image '/images/foo.png'
%= image '/images/foo.png', alt => 'Foo'
Generate portable C<img> tag.
<img src="/path/to/images/foo.png">
<img alt="Foo" src="/path/to/images/foo.png">
=head2 input_tag
%= input_tag 'first_name'
%= input_tag first_name => 'Default'
%= input_tag 'employed', type => 'checkbox'
Generate C<input> tag. Previous input values will automatically get picked up and shown as default.
<input name="first_name">
<input name="first_name" value="Default">
<input name="employed" type="checkbox">
=head2 javascript
%= javascript '/script.js'
%= javascript '/script.js', defer => undef
%= javascript begin
var a = 'b';
% end
Generate portable C<script> tag for JavaScript asset.
<script src="/path/to/script.js"></script>
<script defer src="/path/to/script.js"></script>
<script><![CDATA[
var a = 'b';
]]></script>
=head2 label_for
%= label_for first_name => 'First name'
%= label_for first_name => 'First name', class => 'user'
%= label_for first_name => begin
First name
% end
%= label_for first_name => (class => 'user') => begin
First name
% end
Generate C<label> tag.
<label for="first_name">First name</label>
<label class="user" for="first_name">First name</label>
<label for="first_name">
First name
</label>
<label class="user" for="first_name">
First name
</label>
=head2 link_to
%= link_to Home => 'index'
%= link_to Home => 'index' => {format => 'txt'} => (class => 'menu')
%= link_to index => {format => 'txt'} => (class => 'menu') => begin
Home
% end
%= link_to Contact => 'mailto:sri@example.com'
<%= link_to index => begin %>Home<% end %>
<%= link_to '/file.txt' => begin %>File<% end %>
<%= link_to 'https://mojolicious.org' => begin %>Mojolicious<% end %>
<%= link_to url_for->query(foo => 'bar')->to_abs => begin %>Retry<% end %>
Generate portable C<a> tag with L<Mojolicious::Controller/"url_for">, defaults to using the capitalized link target as
content.
<a href="/path/to/index">Home</a>
<a class="menu" href="/path/to/index.txt">Home</a>
<a class="menu" href="/path/to/index.txt">
Home
</a>
<a href="mailto:sri@example.com">Contact</a>
<a href="/path/to/index">Home</a>
<a href="/path/to/file.txt">File</a>
<a href="https://mojolicious.org">Mojolicious</a>
<a href="http://127.0.0.1:3000/current/path?foo=bar">Retry</a>
=head2 month_field
%= month_field 'vacation'
%= month_field vacation => '2012-12'
%= month_field vacation => '2012-12', id => 'foo'
Generate C<input> tag of type C<month>. Previous input values will automatically get picked up and shown as default.
<input name="vacation" type="month">
<input name="vacation" type="month" value="2012-12">
<input id="foo" name="vacation" type="month" value="2012-12">
=head2 number_field
%= number_field 'age'
%= number_field age => 25
%= number_field age => 25, id => 'foo', min => 0, max => 200
Generate C<input> tag of type C<number>. Previous input values will automatically get picked up and shown as default.
<input name="age" type="number">
<input name="age" type="number" value="25">
<input id="foo" max="200" min="0" name="age" type="number" value="25">
=head2 password_field
%= password_field 'pass'
%= password_field 'pass', id => 'foo'
Generate C<input> tag of type C<password>.
<input name="pass" type="password">
<input id="foo" name="pass" type="password">
=head2 radio_button
%= radio_button 'test'
%= radio_button country => 'germany'
%= radio_button country => 'germany', checked => undef, id => 'foo'
Generate C<input> tag of type C<radio>. Previous input values will automatically get picked up and shown as default.
<input name="test" type="radio">
<input name="country" type="radio" value="germany">
<input checked id="foo" name="country" type="radio" value="germany">
=head2 range_field
%= range_field 'age'
%= range_field age => 25
%= range_field age => 25, id => 'foo', min => 0, max => 200
Generate C<input> tag of type C<range>. Previous input values will automatically get picked up and shown as default.
<input name="age" type="range">
<input name="age" type="range" value="25">
<input id="foo" max="200" min="200" name="age" type="range" value="25">
=head2 search_field
%= search_field 'q'
%= search_field q => 'perl'
%= search_field q => 'perl', id => 'foo'
Generate C<input> tag of type C<search>. Previous input values will automatically get picked up and shown as default.
<input name="q" type="search">
<input name="q" type="search" value="perl">
<input id="foo" name="q" type="search" value="perl">
=head2 select_field
%= select_field country => ['de', 'en']
%= select_field country => [[Germany => 'de'], 'en'], id => 'eu'
%= select_field country => [[Germany => 'de', selected => 'selected'], 'en']
%= select_field country => [c(EU => [[Germany => 'de'], 'en'], id => 'eu')]
%= select_field country => [c(EU => ['de', 'en']), c(Asia => ['cn', 'jp'])]
Generate C<select> and C<option> tags from array references and C<optgroup> tags from L<Mojo::Collection> objects.
Previous input values will automatically get picked up and shown as default.
<select name="country">
<option value="de">de</option>
<option value="en">en</option>
</select>
<select id="eu" name="country">
<option value="de">Germany</option>
<option value="en">en</option>
</select>
<select name="country">
<option selected="selected" value="de">Germany</option>
<option value="en">en</option>
</select>
<select name="country">
<optgroup id="eu" label="EU">
<option value="de">Germany</option>
<option value="en">en</option>
</optgroup>
</select>
<select name="country">
<optgroup label="EU">
<option value="de">de</option>
<option value="en">en</option>
</optgroup>
<optgroup label="Asia">
<option value="cn">cn</option>
<option value="jp">jp</option>
</optgroup>
</select>
=head2 stylesheet
%= stylesheet '/foo.css'
%= stylesheet '/foo.css', title => 'Foo style'
%= stylesheet begin
body {color: #000}
% end
Generate portable C<style> or C<link> tag for CSS asset.
<link href="/path/to/foo.css" rel="stylesheet">
<link href="/path/to/foo.css" rel="stylesheet" title="Foo style">
<style><![CDATA[
body {color: #000}
]]></style>
=head2 submit_button
%= submit_button
%= submit_button 'Ok!', id => 'foo'
Generate C<input> tag of type C<submit>.
<input type="submit" value="Ok">
<input id="foo" type="submit" value="Ok!">
=head2 t
%= t div => 'test & 123'
Alias for L</"tag">.
<div>test &amp; 123</div>
=head2 tag
%= tag 'br'
%= tag 'div'
%= tag 'div', id => 'foo', hidden => undef
%= tag 'div', 'test & 123'
%= tag 'div', id => 'foo', 'test & 123'
%= tag 'div', data => {my_id => 1, Name => 'test'}, 'test & 123'
%= tag div => begin
test & 123
% end
<%= tag div => (id => 'foo') => begin %>test & 123<% end %>
Alias for L<Mojo::DOM/"new_tag">.
<br>
<div></div>
<div id="foo" hidden></div>
<div>test &amp; 123</div>
<div id="foo">test &amp; 123</div>
<div data-my-id="1" data-name="test">test &amp; 123</div>
<div>
test & 123
</div>
<div id="foo">test & 123</div>
Very useful for reuse in more specific tag helpers.
my $output = $c->tag('meta');
my $output = $c->tag('meta', charset => 'UTF-8');
my $output = $c->tag('div', '<p>This will be escaped</p>');
my $output = $c->tag('div', sub { '<p>This will not be escaped</p>' });
Results are automatically wrapped in L<Mojo::ByteStream> objects to prevent accidental double escaping in C<ep>
templates.
=head2 tag_with_error
%= tag_with_error 'input', class => 'foo'
Same as L</"tag">, but adds the class C<field-with-error>.
<input class="foo field-with-error">
=head2 tel_field
%= tel_field 'work'
%= tel_field work => '123456789'
%= tel_field work => '123456789', id => 'foo'
Generate C<input> tag of type C<tel>. Previous input values will automatically get picked up and shown as default.
<input name="work" type="tel">
<input name="work" type="tel" value="123456789">
<input id="foo" name="work" type="tel" value="123456789">
=head2 text_area
%= text_area 'story'
%= text_area 'story', cols => 40
%= text_area story => 'Default', cols => 40
%= text_area story => (cols => 40) => begin
Default
% end
Generate C<textarea> tag. Previous input values will automatically get picked up and shown as default.
<textarea name="story"></textarea>
<textarea cols="40" name="story"></textarea>
<textarea cols="40" name="story">Default</textarea>
<textarea cols="40" name="story">
Default
</textarea>
=head2 text_field
%= text_field 'first_name'
%= text_field first_name => 'Default'
%= text_field first_name => 'Default', class => 'user'
Generate C<input> tag of type C<text>. Previous input values will automatically get picked up and shown as default.
<input name="first_name" type="text">
<input name="first_name" type="text" value="Default">
<input class="user" name="first_name" type="text" value="Default">
=head2 time_field
%= time_field 'start'
%= time_field start => '23:59:59'
%= time_field start => '23:59:59', id => 'foo'
Generate C<input> tag of type C<time>. Previous input values will automatically get picked up and shown as default.
<input name="start" type="time">
<input name="start" type="time" value="23:59:59">
<input id="foo" name="start" type="time" value="23:59:59">
=head2 url_field
%= url_field 'address'
%= url_field address => 'https://mojolicious.org'
%= url_field address => 'https://mojolicious.org', id => 'foo'
Generate C<input> tag of type C<url>. Previous input values will automatically get picked up and shown as default.
<input name="address" type="url">
<input name="address" type="url" value="https://mojolicious.org">
<input id="foo" name="address" type="url" value="https://mojolicious.org">
=head2 week_field
%= week_field 'vacation'
%= week_field vacation => '2012-W17'
%= week_field vacation => '2012-W17', id => 'foo'
Generate C<input> tag of type C<week>. Previous input values will automatically get picked up and shown as default.
<input name="vacation" type="week">
<input name="vacation" type="week" value="2012-W17">
<input id="foo" name="vacation" type="week" value="2012-W17">
=head1 METHODS
L<Mojolicious::Plugin::TagHelpers> inherits all methods from L<Mojolicious::Plugin> and implements the following new
ones.
=head2 register
$plugin->register(Mojolicious->new);
Register helpers in L<Mojolicious> application.
=head1 SEE ALSO
L<Mojolicious>, L<Mojolicious::Guides>, L<https://mojolicious.org>.
=cut