Mojolicious-Plugin-CanonicalURL
view release on metacpan or search on metacpan
lib/Mojolicious/Plugin/CanonicalURL.pm view on Meta::CPAN
package Mojolicious::Plugin::CanonicalURL;
use Mojo::Base 'Mojolicious::Plugin';
use Carp ();
use Exporter 'import';
use Mojo::Util ();
use Scalar::Util ();
use Sub::Quote ();
our $VERSION = '0.06';
our @EXPORT_OK = qw(remove_trailing_slashes);
sub register {
my (undef, $app, $config) = @_;
my (
$should_canonicalize_request_config,
$should_not_canonicalize_request_config,
$inline_code,
$end_with_slash,
$canonicalize_before_render,
%captures
) = _parse_and_validate_config($config);
my $sub_string = '';
my ($path_declared, $path_with_no_slashes_at_the_end_declared);
if (defined $should_canonicalize_request_config) {
($path_declared, $path_with_no_slashes_at_the_end_declared, $sub_string) = _create_should_canonicalize_request_sub_string(
config => $should_canonicalize_request_config,
captures => \%captures,
sub_string => $sub_string,
should_canonicalize_request => 1,
path_declared => $path_declared,
path_with_no_slashes_at_the_end_declared => $path_with_no_slashes_at_the_end_declared,
);
}
if (defined $should_not_canonicalize_request_config) {
($path_declared, $path_with_no_slashes_at_the_end_declared, $sub_string) = _create_should_canonicalize_request_sub_string(
config => $should_not_canonicalize_request_config,
captures => \%captures,
sub_string => $sub_string,
should_canonicalize_request => undef,
path_declared => $path_declared,
path_with_no_slashes_at_the_end_declared => $path_with_no_slashes_at_the_end_declared,
);
}
$sub_string .= $inline_code if $inline_code;
$sub_string .= 'my $_mpcu_path = $c->req->url->path->to_string;' unless $path_declared;
if ($end_with_slash) {
$sub_string .= q{
my $_mpcu_path_length = length($_mpcu_path);
return $next->() if $_mpcu_path_length != 0 and rindex($_mpcu_path, '/') == $_mpcu_path_length - 1 and ($_mpcu_path_length < 2 or rindex($_mpcu_path, '//') != $_mpcu_path_length - 2);
while (rindex($_mpcu_path, '/') == length($_mpcu_path) - 1) {
substr $_mpcu_path, -1, 1, '';
}
my $url = $c->req->url->clone;
$url->path($_mpcu_path)->path->trailing_slash(1);
$c->res->code(301);
$c->redirect_to($url);
};
} else {
$sub_string .= q{
return $next->() if $_mpcu_path eq '/' or rindex($_mpcu_path, '/') != length($_mpcu_path) - 1 or $_mpcu_path eq '';
while (rindex($_mpcu_path, '/') == length($_mpcu_path) - 1) {
substr $_mpcu_path, -1, 1, '';
}
$c->res->code(301);
$c->redirect_to($c->req->url->clone->path($_mpcu_path));
};
}
# Potentially flaky for a minor speed improvment. Could just assign $next and $c above to @_.
# Or could use Mojo::Template, but that would be awkward writing perl code.
$sub_string =~ s/\$next\b/\$_[0]/g;
$sub_string =~ s/\$c\b/\$_[1]/g;
$app->hook(around_action => _quote_sub($sub_string, \%captures));
if ($canonicalize_before_render) {
# replace return $next->() with return
$sub_string =~ s/return\s+\$_\[0\]->\(\)/return/g;
# replace $_[1] with $_[0] since $c is now the first argument
$sub_string =~ s/\$_\[1\]/\$_[0]/gm;
# we could set a stash variable if we failed to canonicalize in
# around_action, but the performance hit isn't big
$sub_string = "return if \$_[0]->res->is_redirect;$sub_string";
$app->hook(before_render => _quote_sub($sub_string, \%captures));
}
}
sub _quote_sub {
my ($sub_string, $captures) = @_;
return Sub::Quote::quote_sub $sub_string, $captures, {no_install => 1, no_defer => 1};
}
sub _parse_and_validate_config {
my ($config) = @_;
my (
$should_canonicalize_request,
$should_not_canonicalize_request,
$inline_code,
$end_with_slash,
$canonicalize_before_render
);
my %captures;
if (defined $config) {
Carp::confess 'config must be a hash reference, but was ' . Scalar::Util::reftype $config
if not defined Scalar::Util::reftype $config
or Scalar::Util::reftype $config ne 'HASH';
if (%$config) {
my $captures_allowed;
if (exists $config->{should_canonicalize_request}) {
($should_canonicalize_request, $captures_allowed) =
_validate_should_canonicalize_request_config(delete $config->{should_canonicalize_request}, 1);
}
if (exists $config->{should_not_canonicalize_request}) {
($should_not_canonicalize_request, $captures_allowed) = _validate_should_canonicalize_request_config(
delete $config->{should_not_canonicalize_request},
undef,
);
}
if (exists $config->{inline_code}) {
$inline_code = delete $config->{inline_code};
Carp::confess 'inline_code must be a true scalar value'
unless not defined Scalar::Util::reftype $inline_code and $inline_code;
$captures_allowed = 1;
}
if (exists $config->{canonicalize_before_render}) {
Carp::confess 'canonicalize_before_render must be a scalar value'
if defined Scalar::Util::reftype $config->{canonicalize_before_render};
$canonicalize_before_render = delete $config->{canonicalize_before_render};
}
if ($captures_allowed and exists $config->{captures}) {
%captures = %{delete $config->{captures}};
Carp::confess 'captures cannot be empty' unless %captures;
}
Carp::confess
'captures only applies when inline_code is set or a scalar reference is passed to should_canonicalize_request or should_not_canonicalize_request'
if exists $config->{captures};
if (exists $config->{end_with_slash}) {
$end_with_slash = delete $config->{end_with_slash};
Carp::confess 'end_with_slash must be a scalar value' if defined Scalar::Util::reftype $end_with_slash;
}
Carp::confess 'unknown keys passed in config: ' . Mojo::Util::dumper $config if keys %$config;
}
}
return (
$should_canonicalize_request,
$should_not_canonicalize_request,
$inline_code,
$end_with_slash,
$canonicalize_before_render,
%captures
);
}
sub _validate_should_canonicalize_request_config {
my ($config, $should_canonicalize_request) = @_;
my $captures_allowed;
my $config_name = _get_should_canonicalize_request_config_name($should_canonicalize_request);
my $config_reftype = Scalar::Util::reftype $config || '';
Carp::confess
"$config_name must be a scalar that evaluates to true and starts with a '/', a REGEXP, a SCALAR, a subroutine, an array reference, or a hash reference"
unless $config
and ((not $config_reftype and index($config, '/') == 0)
or grep { $config_reftype eq $_ } qw/ARRAY HASH REGEXP SCALAR CODE/);
if (defined $config_reftype and $config_reftype eq 'SCALAR') {
$captures_allowed = 1;
} elsif (defined $config_reftype and $config_reftype eq 'ARRAY') {
Carp::confess "array passed to $config_name must not be empty" unless @$config;
for (@$config) {
Carp::confess "elements of $config_name must be a true value" unless $_;
my $reftype = Scalar::Util::reftype $_;
Carp::confess
"elements of $config_name must have a reftype of undef (scalar), CODE, HASH, REGEXP, or SCALAR but was '$reftype'"
unless not defined $reftype
or $reftype eq 'CODE'
or $reftype eq 'HASH'
or $reftype eq 'REGEXP'
or $reftype eq 'SCALAR';
Carp::confess "elements of $config_name must begin with a '/' when they are scalar"
if not defined $reftype and index($_, '/') != 0;
if (defined $reftype and $reftype eq 'SCALAR') {
$captures_allowed = 1;
}
if (defined $reftype and $reftype eq 'HASH') {
_validate_starts_with_hash($config_name, $_);
}
}
} elsif (defined $config_reftype and $config_reftype eq 'HASH') {
_validate_starts_with_hash($config_name, $config);
}
return ($config, $captures_allowed);
}
sub _validate_starts_with_hash {
my ($config_name, $hash) = @_;
my %copy = %$hash;
Carp::confess "must provide key 'starts_with' to hash in $config_name" unless exists $copy{starts_with};
Carp::confess 'value for starts_with must not be undef' unless defined $copy{starts_with};
Carp::confess 'value for starts_with must be a scalar'
unless not defined Scalar::Util::reftype $copy{starts_with};
Carp::confess q{value for starts_with must begin with a '/'}
unless index(delete $copy{starts_with}, '/') == 0;
Carp::confess "unknown keys/values passed in hash inside of $config_name: " . Mojo::Util::dumper \%copy
if %copy
}
sub _create_should_canonicalize_request_sub_string {
my %args = @_;
my ($config, $captures, $sub_string, $should_canonicalize_request, $path_declared, $path_with_no_slashes_at_the_end_declared) =
@{{@_}}{qw/config captures sub_string should_canonicalize_request path_declared path_with_no_slashes_at_the_end_declared/};
my $path_with_no_slashes_at_the_end_declared_code = q{
my $_mpcu_path_with_no_slashes_at_the_end = $_mpcu_path;
while (rindex($_mpcu_path_with_no_slashes_at_the_end, '/') == length($_mpcu_path_with_no_slashes_at_the_end) - 1) {
substr $_mpcu_path_with_no_slashes_at_the_end, -1, 1, '';
}
};
my $config_name = _get_should_canonicalize_request_config_name($should_canonicalize_request);
my $config_variable_name = "\$$config_name";
my $if_or_unless = $should_canonicalize_request ? 'unless' : 'if';
my $reftype = Scalar::Util::reftype $config;
if (not defined $reftype) {
$config =~ s#/+\z##m;
$captures->{$config_variable_name} = \$config;
unless ($path_with_no_slashes_at_the_end_declared) {
unless ($path_declared) {
$sub_string .= 'my $_mpcu_path = $c->req->url->path->to_string;';
$path_declared = 1;
}
$sub_string .= $path_with_no_slashes_at_the_end_declared_code;
$path_with_no_slashes_at_the_end_declared = 1;
}
$sub_string .= "return \$next->() $if_or_unless \$_mpcu_path_with_no_slashes_at_the_end eq $config_variable_name;";
} elsif ($reftype eq 'REGEXP') {
unless ($path_declared) {
$sub_string .= 'my $_mpcu_path = $c->req->url->path->to_string;';
$path_declared = 1;
}
$captures->{$config_variable_name} = \$config;
$sub_string .= "return \$next->() $if_or_unless \$_mpcu_path =~ $config_variable_name;";
} elsif ($reftype eq 'SCALAR') {
my $code = $$config;
$code = "return \$next->() $if_or_unless $code" if $code !~ /\A\s*return/;
$code .= ';' unless $code =~ /;\s*\z/;
Carp::confess 'code must contain return $next->()' unless $code =~ /return\s+\$next->\(\)/;
$sub_string .= $code;
} elsif ($reftype eq 'CODE') {
$captures->{$config_variable_name} = \$config;
$sub_string .= qq{
( run in 3.758 seconds using v1.01-cache-2.11-cpan-39bf76dae61 )