File Coverage

lib/Plack/Middleware/DirListing.pm
Criterion Covered Total %
statement 102 105 97.1
branch 16 24 66.6
condition 8 9 88.8
subroutine 20 20 100.0
pod 2 9 22.2
total 148 167 88.6


line stmt bran cond sub pod time code
1             package Plack::Middleware::DirListing;
2             # ABSTRACT: Display a directory listing if no default index html page
3              
4 2     2   579361 use parent qw( Plack::Middleware );
  2         4  
  2         35  
5 2     2   1164 use DirHandle;
  2         1146  
  2         59  
6 2     2   1034 use HTML::Entities;
  2         12173  
  2         208  
7 2     2   1184 use Plack::Request;
  2         185389  
  2         105  
8 2     2   1160 use Plack::MIME;
  2         2210  
  2         128  
9 2     2   14 use Plack::Util::Accessor qw( root );
  2         3  
  2         22  
10 2     2   146 use URI::Escape;
  2         3  
  2         126  
11 2     2   1174 use Time::Piece;
  2         25994  
  2         9  
12              
13 2     2   182 use strict;
  2         3  
  2         51  
14 2     2   8 use warnings;
  2         3  
  2         112  
15 2     2   22 use v5.10;
  2         6  
16              
17             sub dir_html {
18              
19 5     5 0 6 state $html = do {
20 2         16 local $/ = undef;
21 2         155 my $data_string = ;
22 2         27 close DATA;
23              
24 2         193 $data_string =~ s/%(?!s)/%%/g;
25 2         93 $data_string;
26             };
27              
28 5         342 return $html;
29             }
30              
31             sub file_html {
32 26     26 0 38 return <
33            
34            
35             %s
36             %s
37             %s
38             %s
39            
40             FILE
41             }
42              
43             sub last_modified {
44 21     21 0 24 my ($self, $date) = @_;
45              
46 21         59 return Time::Piece->new( $date )->strftime( "%d-%b-%Y %H:%M" );
47             }
48              
49             sub sort_order {
50 5     5 0 14 my ($self, $env, $page) = @_;
51              
52 5 100       43 if (my ($field, $order) = $env->{ QUERY_STRING } =~ /C=(\w);O=(\w)/) {
53 3 100       11 my $invert = ($order eq 'A') ? 'D' : 'A';
54 3         101 $page =~ s/C=$field;O=$order/C=$field;O=$invert/;
55             }
56              
57 5         41 return $page;
58             }
59              
60             my %col_sort = (
61             'NA' => sub { $a->[2] cmp $b->[2] },
62             'ND' => sub { $b->[2] cmp $a->[2] },
63             'MA' => sub { $a->[6] <=> $b->[6] },
64             'MD' => sub { $b->[6] <=> $a->[6] },
65             'SA' => sub { $a->[4] <=> $b->[4] },
66             'SD' => sub { $b->[4] <=> $a->[4] },
67             'DA' => sub { $a->[5] cmp $b->[5] },
68             'DD' => sub { $b->[5] cmp $a->[5] },
69             );
70              
71             sub filetype_class {
72 21     21 0 26 my ($self, $filetype) = @_;
73              
74 21 50       31 return 'ft_directory' if ($filetype eq 'directory');
75 21 50       61 return 'ft_image' if ($filetype =~ /^image/);
76 21 50       27 return 'ft_pdf' if ($filetype =~ /pdf$/);
77 21 50       32 return 'ft_html' if ($filetype =~ /html$/);
78              
79 21         37 return '';
80             }
81              
82              
83             sub read_dir {
84 5     5 0 12 my ($self, $env, $dir) = @_;
85              
86 5         6 my @files;
87              
88 5         30 my $dh = DirHandle->new($dir);
89              
90 5         412 while (defined(my $ent = $dh->read)) {
91 31 100 100     1606 next if $ent eq '.' or $ent eq '..';
92              
93 21         29 my $file = "$dir/$ent";
94 21         34 my $url = $env->{PATH_INFO} . $ent;
95              
96 21         221 my $is_dir = -d $file;
97 21         45 my @stat = stat _;
98              
99 21         58 $url = join '/', map {uri_escape($_)} split m{/}, $url;
  43         398  
100              
101 21 50       209 if ($is_dir) {
102 0         0 $ent .= "/";
103 0         0 $url .= "/";
104             }
105              
106 21 50 50     75 my $mime_type = $is_dir ? 'directory' : ( Plack::MIME->mime_type($file) || 'text/plain' );
107 21         226 my $filetype_class = $self->filetype_class( $mime_type );
108 21         49 push @files, [ $filetype_class, $url, $ent, $self->last_modified( $stat[9] ), $stat[7], $mime_type, $stat[9] ];
109             }
110              
111 5         795 my ($field, $order) = $env->{ QUERY_STRING } =~ /C=(\w);O=(\w)/;
112 5   100     19 $field ||= 'N';
113 5   100     26 $order ||= 'A';
114              
115 5         35 @files = sort { &{ $col_sort{ "$field$order" } } } @files;
  27         24  
  27         29  
116              
117 5         31 return [ [ 'ft_parent', "../", "Parent Directory", '', '', '', 0], @files ];
118             }
119              
120             sub prepare_app {
121 2     2 1 713054 my ($self) = @_;
122              
123 2 50       13 $self->root('.') unless $self->root;
124             }
125              
126             # NOTE: Copied from Plack::App::Directory as that module makes it
127             # impossible to override the HTML.
128              
129             sub serve_path {
130 5     5 0 32 my $self = shift;
131 5         10 my ($env, $dir) = @_;
132              
133 5         14 my $files = $self->read_dir( $env, $dir );
134              
135 5         187 my $path = Plack::Util::encode_html("Index of $env->{PATH_INFO}");
136             my $files_html = join "\n", map {
137 26         952 my $f = $_;
138 26         67 sprintf $self->file_html, map Plack::Util::encode_html($_), @{ $f }[ 0..5 ];
  26         72  
139 5         60 } @{ $files };
  5         11  
140 5         237 my $page = sprintf $self->dir_html, $path, $path, $files_html, $env->{ HTTP_HOST };
141              
142 5         28 $page = $self->sort_order( $env, $page );
143              
144 5         106 return [ 200, ['Content-Type' => 'text/html; charset=utf-8'], [ $page ] ];
145             }
146              
147             sub call {
148 7     7 1 93022 my ( $self, $env ) = @_;
149 7         59 my $req = Plack::Request->new( $env );
150              
151 7         90 my $dir = $self->root . $req->path_info();
152 7 50       288 if (-d $dir) {
153 7 100       28 if (substr( $dir, -1 ) eq '/') {
154 5         16 return $self->serve_path( $env, $dir );
155             }
156             else {
157 2         34 my $uri = $req->uri();
158 2         796 $uri->path( $uri->path . '/' );
159 2         152 my $res = $req->new_response(301); # new Plack::Response
160 2         2617 $res->headers([
161             'Location' => $uri,
162             'Content-Type' => 'text/html; charset=UTF-8',
163             'Cache-Control' => 'must-revalidate, max-age=3600'
164             ]);
165              
166 2         318 my $uhe = encode_entities($uri);
167 2         87 $res->body( <
168             301 Moved Permanently

Moved Permanently

The document has moved here.

169             REDIRECT_BODY
170              
171 2         33 return $res->finalize;
172             }
173             }
174              
175 0           return $self->app->($env);
176             }
177              
178             =head1 NAME
179              
180             Plack::Middleware::DirListing - Display a listing of a directory in html
181              
182             =head1 SYNOPSIS
183              
184             use Plack::Builder;
185             use Plack::App::File;
186              
187             my $app = Plack::App::File->new({ root => '.' })->to_app;
188              
189             builder {
190             enable "DirListing", root => '.';
191             $app;
192             }
193              
194             =head1 DESCRIPTION
195              
196             This Plack middleware provides the same functionality as L, but *only* serves the directory listing if the URL points to a directory. It does not try to serve any files.
197              
198             It also strives to have a cleaner UI that more closely matches a prettified version of the Apache web server's output.
199              
200             This modules does not attempt to find a default html file for the directory. If desired, include L or L before this module.
201              
202             =head1 CONFIGURATION
203              
204             =over 4
205              
206             =item root
207              
208             Document root directory. Defaults to the current directory.
209              
210             =back
211              
212             =head1 AUTHOR
213              
214             Keith Carangelo
215              
216             =cut
217              
218             1;
219              
220             __DATA__