Newer Older
326 lines | 7.317kb
add files
Yuki Kimoto authored on 2014-03-26
1
package Mojo::Path;
2
use Mojo::Base -base;
3
use overload
4
  '@{}'    => sub { shift->parts },
5
  bool     => sub {1},
6
  '""'     => sub { shift->to_string },
7
  fallback => 1;
8

            
9
use Mojo::Util qw(decode encode url_escape url_unescape);
10

            
11
has charset => 'UTF-8';
12

            
13
sub new { shift->SUPER::new->parse(@_) }
14

            
15
sub canonicalize {
16
  my $self = shift;
17

            
18
  my @parts;
19
  for my $part (@{$self->parts}) {
20

            
21
    # ".."
22
    if ($part eq '..') {
23
      (@parts && $parts[-1] ne '..') ? pop @parts : push @parts, '..';
24
    }
25

            
26
    # Something else than "."
27
    elsif ($part ne '.' && $part ne '') { push @parts, $part }
28
  }
29
  $self->trailing_slash(undef) unless @parts;
30

            
31
  return $self->parts(\@parts);
32
}
33

            
34
sub clone {
35
  my $self = shift;
36

            
37
  my $clone = $self->new->charset($self->charset);
38
  if (my $parts = $self->{parts}) {
39
    $clone->{$_} = $self->{$_} for qw(leading_slash trailing_slash);
40
    $clone->{parts} = [@$parts];
41
  }
42
  else { $clone->{path} = $self->{path} }
43

            
44
  return $clone;
45
}
46

            
47
sub contains {
48
  my ($self, $path) = @_;
49
  return $path eq '/' || $self->to_route =~ m!^\Q$path\E(?:/|$)!;
50
}
51

            
52
sub leading_slash { shift->_parse(leading_slash => @_) }
53

            
54
sub merge {
55
  my ($self, $path) = @_;
56

            
57
  # Replace
58
  return $self->parse($path) if $path =~ m!^/!;
59

            
60
  # Merge
61
  pop @{$self->parts} unless $self->trailing_slash;
62
  $path = $self->new($path);
63
  push @{$self->parts}, @{$path->parts};
64
  return $self->trailing_slash($path->trailing_slash);
65
}
66

            
67
sub parse {
68
  my $self = shift;
69
  $self->{path} = shift;
70
  delete @$self{qw(leading_slash parts trailing_slash)};
71
  return $self;
72
}
73

            
74
sub parts { shift->_parse(parts => @_) }
75

            
76
sub to_abs_string {
77
  my $path = shift->to_string;
78
  return $path =~ m!^/! ? $path : "/$path";
79
}
80

            
81
sub to_dir {
82
  my $clone = shift->clone;
83
  pop @{$clone->parts} unless $clone->trailing_slash;
84
  return $clone->trailing_slash(!!@{$clone->parts});
85
}
86

            
87
sub to_route {
88
  my $clone = shift->clone;
89
  my $route = join '/', @{$clone->parts};
90
  return "/$route" . ($clone->trailing_slash ? '/' : '');
91
}
92

            
93
sub to_string {
94
  my $self = shift;
95

            
96
  # Path
97
  my $charset = $self->charset;
98
  if (defined(my $path = $self->{path})) {
99
    $path = encode $charset, $path if $charset;
100
    return url_escape $path, '^A-Za-z0-9\-._~!$&\'()*+,;=%:@/';
101
  }
102

            
103
  # Build path
104
  my @parts = @{$self->parts};
105
  @parts = map { encode $charset, $_ } @parts if $charset;
106
  my $path = join '/',
107
    map { url_escape $_, '^A-Za-z0-9\-._~!$&\'()*+,;=:@' } @parts;
108
  $path = "/$path" if $self->leading_slash;
109
  $path = "$path/" if $self->trailing_slash;
110
  return $path;
111
}
112

            
113
sub trailing_slash { shift->_parse(trailing_slash => @_) }
114

            
115
sub _parse {
116
  my ($self, $name) = (shift, shift);
117

            
118
  unless ($self->{parts}) {
119
    my $path = url_unescape delete($self->{path}) // '';
120
    my $charset = $self->charset;
121
    $path = decode($charset, $path) // $path if $charset;
122
    $self->{leading_slash}  = $path =~ s!^/!!;
123
    $self->{trailing_slash} = $path =~ s!/$!!;
124
    $self->{parts}          = [split '/', $path, -1];
125
  }
126

            
127
  return $self->{$name} unless @_;
128
  $self->{$name} = shift;
129
  return $self;
130
}
131

            
132
1;
133

            
134
=encoding utf8
135

            
136
=head1 NAME
137

            
138
Mojo::Path - Path
139

            
140
=head1 SYNOPSIS
141

            
142
  use Mojo::Path;
143

            
144
  # Parse
145
  my $path = Mojo::Path->new('/foo%2Fbar%3B/baz.html');
146
  say $path->[0];
147

            
148
  # Build
149
  my $path = Mojo::Path->new('/i/♥');
150
  push @$path, 'mojolicious';
151
  say "$path";
152

            
153
=head1 DESCRIPTION
154

            
155
L<Mojo::Path> is a container for paths used by L<Mojo::URL>.
156

            
157
=head1 ATTRIBUTES
158

            
159
L<Mojo::Path> implements the following attributes.
160

            
161
=head2 charset
162

            
163
  my $charset = $path->charset;
164
  $path       = $path->charset('UTF-8');
165

            
166
Charset used for encoding and decoding, defaults to C<UTF-8>.
167

            
168
  # Disable encoding and decoding
169
  $path->charset(undef);
170

            
171
=head1 METHODS
172

            
173
L<Mojo::Path> inherits all methods from L<Mojo::Base> and implements the
174
following new ones.
175

            
176
=head2 new
177

            
178
  my $path = Mojo::Path->new;
179
  my $path = Mojo::Path->new('/foo%2Fbar%3B/baz.html');
180

            
181
Construct a new L<Mojo::Path> object and L</"parse"> path if necessary.
182

            
183
=head2 canonicalize
184

            
185
  $path = $path->canonicalize;
186

            
187
Canonicalize path.
188

            
189
  # "/foo/baz"
190
  Mojo::Path->new('/foo/./bar/../baz')->canonicalize;
191

            
192
  # "/../baz"
193
  Mojo::Path->new('/foo/../bar/../../baz')->canonicalize;
194

            
195
=head2 clone
196

            
197
  my $clone = $path->clone;
198

            
199
Clone path.
200

            
201
=head2 contains
202

            
203
  my $bool = $path->contains('/i/♥/mojolicious');
204

            
205
Check if path contains given prefix.
206

            
207
  # True
208
  Mojo::Path->new('/foo/bar')->contains('/');
209
  Mojo::Path->new('/foo/bar')->contains('/foo');
210
  Mojo::Path->new('/foo/bar')->contains('/foo/bar');
211

            
212
  # False
213
  Mojo::Path->new('/foo/bar')->contains('/f');
214
  Mojo::Path->new('/foo/bar')->contains('/bar');
215
  Mojo::Path->new('/foo/bar')->contains('/whatever');
216

            
217
=head2 leading_slash
218

            
219
  my $bool = $path->leading_slash;
220
  $path    = $path->leading_slash($bool);
221

            
222
Path has a leading slash. Note that this method will normalize the path and
223
that C<%2F> will be treated as C</> for security reasons.
224

            
225
=head2 merge
226

            
227
  $path = $path->merge('/foo/bar');
228
  $path = $path->merge('foo/bar');
229
  $path = $path->merge(Mojo::Path->new('foo/bar'));
230

            
231
Merge paths. Note that this method will normalize both paths if necessary and
232
that C<%2F> will be treated as C</> for security reasons.
233

            
234
  # "/baz/yada"
235
  Mojo::Path->new('/foo/bar')->merge('/baz/yada');
236

            
237
  # "/foo/baz/yada"
238
  Mojo::Path->new('/foo/bar')->merge('baz/yada');
239

            
240
  # "/foo/bar/baz/yada"
241
  Mojo::Path->new('/foo/bar/')->merge('baz/yada');
242

            
243
=head2 parse
244

            
245
  $path = $path->parse('/foo%2Fbar%3B/baz.html');
246

            
247
Parse path.
248

            
249
=head2 to_abs_string
250

            
251
  my $str = $path->to_abs_string;
252

            
253
Turn path into an absolute string.
254

            
255
  # "/i/%E2%99%A5/mojolicious"
256
  Mojo::Path->new('/i/%E2%99%A5/mojolicious')->to_abs_string;
257
  Mojo::Path->new('i/%E2%99%A5/mojolicious')->to_abs_string;
258

            
259
=head2 parts
260

            
261
  my $parts = $path->parts;
262
  $path     = $path->parts([qw(foo bar baz)]);
263

            
264
The path parts. Note that this method will normalize the path and that C<%2F>
265
will be treated as C</> for security reasons.
266

            
267
  # Part with slash
268
  push @{$path->parts}, 'foo/bar';
269

            
270
=head2 to_dir
271

            
272
  my $dir = $route->to_dir;
273

            
274
Clone path and remove everything after the right-most slash.
275

            
276
  # "/i/%E2%99%A5/"
277
  Mojo::Path->new('/i/%E2%99%A5/mojolicious')->to_dir->to_abs_string;
278

            
279
  # "i/%E2%99%A5/"
280
  Mojo::Path->new('i/%E2%99%A5/mojolicious')->to_dir->to_abs_string;
281

            
282
=head2 to_route
283

            
284
  my $route = $path->to_route;
285

            
286
Turn path into a route.
287

            
288
  # "/i/♥/mojolicious"
289
  Mojo::Path->new('/i/%E2%99%A5/mojolicious')->to_route;
290
  Mojo::Path->new('i/%E2%99%A5/mojolicious')->to_route;
291

            
292
=head2 to_string
293

            
294
  my $str = $path->to_string;
295
  my $str = "$path";
296

            
297
Turn path into a string.
298

            
299
  # "/i/%E2%99%A5/mojolicious"
300
  Mojo::Path->new('/i/%E2%99%A5/mojolicious')->to_string;
301

            
302
  # "i/%E2%99%A5/mojolicious"
303
  Mojo::Path->new('i/%E2%99%A5/mojolicious')->to_string;
304

            
305
=head2 trailing_slash
306

            
307
  my $bool = $path->trailing_slash;
308
  $path    = $path->trailing_slash($bool);
309

            
310
Path has a trailing slash. Note that this method will normalize the path and
311
that C<%2F> will be treated as C</> for security reasons.
312

            
313
=head1 PATH PARTS
314

            
315
Direct array reference access to path parts is also possible. Note that this
316
will normalize the path and that C<%2F> will be treated as C</> for security
317
reasons.
318

            
319
  say $path->[0];
320
  say for @$path;
321

            
322
=head1 SEE ALSO
323

            
324
L<Mojolicious>, L<Mojolicious::Guides>, L<http://mojolicio.us>.
325

            
326
=cut