# Copyright (c) 2023 Yuki Kimoto
# MIT License
class HTTP::Tiny::URL {
version_from HTTP::Tiny;
allow HTTP::Tiny;
allow HTTP::Tiny::Handle;
use StringBuffer;
use Re;
use HTTP::Tiny::URL;
use HTTP::Tiny::Parameters;
use HTTP::Tiny::Util;
# Fields
has fragment : rw string;
has scheme : rw string;
has userinfo : rw string;
has host : rw string;
has port : rw int;
has path : HTTP::Tiny::Path;
has auth : rw string;
has base : rw HTTP::Tiny::URL;
has query : HTTP::Tiny::Parameters;
static method new : HTTP::Tiny::URL ($url : string = undef) {
my $self = new HTTP::Tiny::URL;
if ($url) {
$self->parse($url);
}
return $self;
}
method clone : HTTP::Tiny::URL () {
my $clone = HTTP::Tiny::URL->new;
$clone->{fragment} = $self->{fragment};
$clone->{host} = $self->{host};
$clone->{port} = $self->{port};
$clone->{scheme} = $self->{scheme};
$clone->{userinfo} = $self->{userinfo};
if ($self->{base}) {
$clone->{base} = $self->{base}->clone;
}
if ($self->{path}) {
$clone->{path} = $self->{path}->clone;
}
if ($self->{query}) {
$clone->{query} = $self->{query}->clone;
}
return $clone;
}
method host_port : string () {
my $host = $self->ihost;
unless ($host) {
return undef ;
}
my $port = $self->port;
unless ($port) {
return $host;
}
return "$host:$port";
}
method set_host_port : void ($host_port : string) {
my $host_port_ref = [$host_port];
if (my $replace_info = Re->s($host_port_ref, ":(\d+)$", "")) {
my $match = $replace_info->match;
$self->set_port((int)$match->cap1);
}
$host_port = $host_port_ref->[0];
my $host = HTTP::Tiny::Util->url_unescape($host_port);
if (Re->m($host, "[^\x00-\x7f]")) {
$self->set_ihost($host);
}
else {
$self->set_host($host);
}
}
method ihost : string () {
my $host = $self->host;
unless ($host) {
return undef;
}
unless (Re->m($host, "[^\x00-\x7f]")) {
return $host;
}
my $parts = Fn->split(".", $host, -1);
my $parts_punycode_encoded = (string[])Fn->map(method : string ($part : string) {
if (my $match = Re->m($part, "[^\x00-\x7f]")) {
return "xn--" . HTTP::Tiny::Util->punycode_encode($match->cap1);
}
else {
return $part;
}
}, $parts);
my $ihost = Fn->join(".", $parts_punycode_encoded);
return $ihost;
}
method set_ihost : void ($ihost : string) {
my $parts = Fn->split(".", $ihost, -1);
my $parts_punycode_decoded = (string[])Fn->map(method : string ($part : string) {
if (my $match = Re->m($part, "^xn--(.+)$")) {
return HTTP::Tiny::Util->punycode_decode($match->cap1);
}
else {
return $part;
}
}, $parts);
my $host = Fn->join(".", $parts_punycode_decoded);
$self->set_host($host);
}
method is_abs : int () {
return !!$self->scheme;
}
method parse : void ($url : string) {
# Official regex from RFC 3986
if (my $match = Re->m($url, "^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?")) {
if ($match->cap2) {
$self->set_scheme($match->cap2);
}
if ($match->cap5) {
$self->set_path(HTTP::Tiny::Path->new($match->cap5));
}
if ($match->cap7) {
$self->set_query(HTTP::Tiny::Parameters->new($match->cap7));
}
if ($match->cap9) {
$self->set_fragment(HTTP::Tiny::Util->url_unescape($match->cap9));
}
if (my $auth = $match->cap4) {
my $auth_ref = [$auth];
if (my $replace_info = Re->s($auth_ref, "^([^\@]+)\@", "")) {
my $match = $replace_info->match;
$self->set_userinfo(HTTP::Tiny::Util->url_unescape($match->cap1)) ;
}
$auth = $auth_ref->[0];
$self->set_host_port($auth);
}
}
}
method password : string () {
my $userinfo = "";
if ($self->userinfo) {
$userinfo = $self->userinfo;
}
my $password = (string)undef;
if (my $match = Re->m($userinfo, ":(.*)$")) {
$password = $match->cap1;
}
return $password;
}
method path : HTTP::Tiny::Path () {
unless ($self->{path}) {
$self->{path} = HTTP::Tiny::Path->new;
}
my $path = $self->{path};
return $path;
}
method set_path : void ($path : object of string|HTTP::Tiny::Path) {
unless ($path) {
die "The path \$path must be defined.";
}
if ($path isa HTTP::Tiny::Path) {
$self->{path} = (HTTP::Tiny::Path)$path;
}
else {
unless ($self->{path}) {
$self->{path} = HTTP::Tiny::Path->new;
}
$self->{path}->merge((string)$path);
}
}
method path_query : string () {
my $query = $self->query->to_string;
my $path_query = $self->path->to_string;
if (length $query) {
$path_query .= "?$query";
}
return $path_query;
}
method set_path_query : void ($pq : string) {
if (my $match = Re->m($pq, "^([^?#]*)(?:\?([^#]*))?")) {
if ($match->cap2) {
$self->set_path(HTTP::Tiny::Path->new($match->cap1));
$self->set_query(HTTP::Tiny::Parameters->new($match->cap2));
}
else {
$self->set_path(HTTP::Tiny::Path->new($match->cap1));
}
}
}
method protocol : string () {
my $protocol = "";
if (my $scheme = $self->scheme) {
$protocol = Fn->lc($scheme);
}
return $protocol;
}
method query : HTTP::Tiny::Parameters () {
unless ($self->{query}) {
$self->{query} = HTTP::Tiny::Parameters->new;
}
return $self->{query};
}
method set_query : void ($query : object of string[]|Hash|HTTP::Tiny::Parameters) {
unless ($query) {
die "The query \$query must be defined.";
}
unless ($self->{query}) {
$self->{query} = HTTP::Tiny::Parameters->new;
}
# Merge with hash
if ($query isa Hash) {
$self->{query}->merge($query->(Hash)->to_array);
}
# Append array
elsif ($query isa string[]) {
$self->{query}->append((string[])$query);
}
# New parameters
elsif ($query isa HTTP::Tiny::Parameters) {
$self->{query} = (HTTP::Tiny::Parameters)$query;
}
}
method to_abs : HTTP::Tiny::URL ($base : HTTP::Tiny::URL = undef) {
my $abs = $self->clone;
if ($abs->is_abs) {
return $abs;
}
# Scheme
unless ($base) {
$base = $abs->base;
}
$abs->set_base($base);
$abs->set_scheme($base->scheme);
# Authority
if ($abs->host) {
return $abs;
}
$abs->set_userinfo($base->userinfo);
$abs->set_host($base->host);
$abs->set_port($base->port);
# Absolute path
my $path = $abs->path;
if ($path->leading_slash) {
return $abs;
}
# Inherit path
if (!@{$path->parts}) {
my $abs_path = $base->path->clone;
$abs_path->canonicalize;
$abs->set_path($abs_path);
# Query
unless (length $abs->query->to_string) {
$abs->set_query($base->query->clone);
}
}
# Merge paths
else {
my $abs_path = $base->path->clone;
$abs_path->merge($path);
$abs_path->canonicalize;
$abs->set_path($abs_path);
}
return $abs;
}
method to_string : string () {
return $self->_string(0);
}
method to_unsafe_string : string () {
return $self->_string(1);
}
method _string : string ($unsafe : int) {
# Scheme
my $url = "";
if (my $proto = $self->protocol) {
$url .= "$proto:";
}
# Authority
my $auth = $self->host_port;
if ($auth) {
$auth = HTTP::Tiny::Util->url_escape($auth, q'^A-Za-z0-9\\-._~!$&\'()*+,;=:\\[\\]');
}
my $info = $self->userinfo;
if ($unsafe && $info) {
$auth = HTTP::Tiny::Util->url_escape(q'^A-Za-z0-9\\-._~!$&\'()*+,;=:') . "@" . $auth;
}
if ($auth) {
$url .= "//$auth";
}
# Path and query
my $path = $self->path_query;
if (!$auth || !length $path || Re->m($path, "^[/?]")) {
$url .= $path;
}
else {
$url .= "/$path";
}
# Fragment
my $fragment = $self->fragment;
unless (length $fragment) {
return $url;
}
$url .= "#" . HTTP::Tiny::Util->url_escape($fragment, q'^A-Za-z0-9\\-._~!$&\'()*+,;=:@/?');
return $url;
}
method username : string () {
my $userinfo = "";
if ($self->userinfo) {
}
my $username = (string)undef;
if (my $match = Re->m($username, "^([^:]+)")) {
$username = $match->cap1;
}
return $username;
}
}