File Coverage

blib/lib/Dancer/Plugin/DirectoryView.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             package Dancer::Plugin::DirectoryView;
2              
3             =head1 NAME
4              
5             Dancer::Plugin::DirectoryView - Browse directory contents in Dancer web apps
6              
7             =cut
8              
9 1     1   21544 use strict;
  1         4  
  1         46  
10              
11 1     1   6 use Cwd 'abs_path';
  1         2  
  1         61  
12 1     1   457 use Dancer ':syntax';
  0            
  0            
13             use Dancer::Engine;
14             use Dancer::MIME;
15             use Dancer::Plugin;
16             use DirHandle;
17             use File::ShareDir;
18             use File::Spec::Functions qw(catfile);
19             use HTTP::Date;
20             use URI::Escape;
21              
22             our $VERSION = '0.02';
23              
24             # Distribution-level shared data directory
25             my $dist_dir = File::ShareDir::dist_dir('Dancer-Plugin-DirectoryView');
26              
27             my $settings = plugin_setting;
28             my $path_prefix = $settings->{path_prefix} || '/dancer-directory-view';
29             # Need a leading slash
30             if ($path_prefix !~ m!^/!) {
31             $path_prefix = '/' . $path_prefix;
32             }
33              
34             my $mime = Dancer::MIME->instance();
35              
36             my $builtin_tpl = {};
37              
38             sub directory_view {
39             my $options;
40            
41             if (@_ == 1 || (@_ == 2 && UNIVERSAL::isa($_[1], 'HASH'))) {
42             #
43             # Called from the application
44             #
45             my ($root_url, $options) = @_;
46            
47             my $root_dir = $options->{root_dir};
48            
49             # Public directory
50             my $public_dir = abs_path(setting('public'));
51            
52             if (defined $root_dir) {
53             # Root directory is set explicitly -- is it an absolute path?
54             if (!File::Spec->file_name_is_absolute($root_dir)) {
55             # No -- we assume it's relative to the public directory
56             $root_dir = abs_path(catfile($public_dir, $root_dir));
57             }
58             }
59             else {
60             # Root directory not set -- assume it's the same as root URL,
61             # relative to the public directory
62             $root_dir = catfile($public_dir, split('/', $root_url));
63             }
64            
65             $options->{root_dir} = $root_dir;
66            
67             my $re_root = quotemeta($root_url);
68            
69             # Does the root URL have a trailing slash?
70             if ($root_url !~ m!/$!) {
71             # Add slash
72             $root_url =~ s!([^/])$!$1/!;
73            
74             # Add a redirection route
75             get qr{$re_root} => sub {
76             redirect $root_url;
77             };
78             }
79              
80             my $re_path = quotemeta($root_url) . '(.*)';
81            
82             get qr{$re_path} => sub {
83             my ($path) = splat;
84            
85             return directory_view(%$options, path => $path);
86             };
87             }
88             else {
89             #
90             # Called from a route handler
91             #
92             return _serve_files(@_);
93             }
94             }
95            
96             sub _serve_files {
97             my (%options) = @_;
98            
99             # Root directory
100             my $root_dir = $options{root_dir} || '.';
101             # Are system paths allowed?
102             my $system_path = $options{system_path} || 0;
103             # Template to use (if set to 0, a primitive built-in template is used)
104             my $template = $options{template} || 'basic';
105             # Should hidden files be included in the directory listing?
106             my $show_hidden_files = $options{show_hidden_files} || 0;
107            
108             # Current path
109             my $path = $options{path};
110            
111             # Views directory
112             my $views_dir = abs_path(setting('views'));
113            
114             # Strip off unwanted leading/trailing slashes
115             $root_dir =~ s!/$!!;
116             $path =~ s!^/!!;
117            
118             # If root_dir is not absolute, assume it is relative to public directory
119             if (!File::Spec->file_name_is_absolute($root_dir)) {
120             $root_dir = abs_path(catfile(abs_path(setting('public')), $root_dir));
121             }
122            
123             my $real_path = abs_path(catfile($root_dir, $path));
124             $real_path =~ s!/$!!;
125              
126             if (index($real_path, abs_path(setting('public'))) != 0 && !$system_path) {
127             # The requested file/directory lies outside of the public directory, but
128             # system paths are not allowed
129             return send_error("Not allowed", 403);
130             }
131            
132             # Make sure we're inside root_dir. This shouldn't actually be necessary, as
133             # Dancer takes care of potentially dangerous paths (e.g., containing "..")
134             # and we should be safe at this point, but let's do the check anyway in case
135             # the application is deployed in some weird insecure way or something.
136             if (index($real_path, $root_dir) != 0) {
137             return send_error("Not allowed", 403);
138             }
139            
140             if (-f $real_path) {
141             #
142             # Regular file
143             #
144             send_file($real_path, system_path => $system_path);
145             }
146             elsif (-d $real_path) {
147             #
148             # Directory -- show contents
149             #
150             my @files = ();
151            
152             if ($real_path ne $root_dir) {
153             push(@files, {
154             url => "../",
155             name => "Up to parent directory",
156             size => '',
157             mime_type => '',
158             mtime => '',
159             class => 'parent-directory'
160             });
161             }
162            
163             my $dh = DirHandle->new($real_path);
164             my @entries;
165             while (defined(my $entry = $dh->read)) {
166             next if $entry eq '.' || $entry eq '..';
167             next if $entry =~ /^\./ && !$show_hidden_files;
168             push @entries, $entry;
169             }
170            
171             # Mapping of MIME types to CSS class names
172             my %classes = (
173             'directory' => 'directory',
174             'application/javascript' => 'file-application-javascript',
175             'application/pdf' => 'file-application-pdf',
176             'application/vnd.ms-excel' => 'file-application-vnd-ms-excel',
177             'application/vnd.oasis.opendocument.spreadsheet' =>
178             'file-application-vnd-oasis-opendocument-spreadsheet',
179             'application/vnd.oasis.opendocument.text' =>
180             'file-application-vnd-oasis-opendocument-text',
181             'application/x-httpd-php' => 'file-application-x-php',
182             'application/x-msword' => 'file-application-msword',
183             'application/x-perl' => 'file-application-x-perl',
184             'application/xml' => 'file-application-xml',
185             'application/zip' => 'file-application-zip',
186             'image/jpeg' => 'file-image-x-generic',
187             'image/png' => 'file-image-x-generic',
188             'text/html' => 'file-text-html',
189             'text/plain' => 'file-text-plain',
190             'text/x-csrc' => 'file-text-x-csrc'
191             );
192            
193             for my $name (sort { $a cmp $b } @entries) {
194             my $file = catfile($real_path, $name);
195             my $url = $name;
196             $url = join '/', map { uri_escape($_) } split m!/!, $url;
197            
198             my $is_dir = -d $file;
199             my @stat = stat(_);
200            
201             if ($is_dir) {
202             $name .= '/';
203             $url .= '/';
204             }
205            
206             my $mime_type = $is_dir ? 'directory' : $mime->for_file($name)
207             || '';
208            
209             push(@files, {
210             url => $url,
211             name => $name,
212             size => $is_dir ? '' : _format_size($stat[7]),
213             mime_type => $mime_type,
214             mtime => HTTP::Date::time2str($stat[9]),
215             class => $classes{$mime_type} || 'file-unknown'
216             });
217             }
218              
219             if ($template) {
220             # Get a new instance of Dancer::Template::Simple
221             my $template_simple = Dancer::Engine->build(template => 'simple');
222             $template_simple->start_tag('<%');
223             $template_simple->stop_tag('%>');
224              
225             my $template_dir;
226              
227             # Look for the template files in the application's views directory
228             if (-d catfile($views_dir, $template)) {
229             $template_dir = catfile($views_dir, $template);
230             }
231             # Then, try the plugin's views directory
232             elsif (-d catfile($dist_dir, 'views', $template)) {
233             $template_dir = catfile($dist_dir, 'views', $template);
234             }
235             else {
236             # TODO: Template not found -- handle error
237             }
238            
239             my $file_tpl = catfile($template_dir, 'file.tt');
240             my $listing_tpl = catfile($template_dir, 'listing.tt');
241             my $layout_tpl = catfile($template_dir, 'layout.tt');
242            
243             # Render the list of files
244             my $files_html = '';
245             for my $file (@files) {
246             $files_html .= $template_simple->render($file_tpl,
247             { file => $file });
248             }
249            
250             # Insert the rendered list into the listing container
251             my $listing_html = $template_simple->render($listing_tpl,
252             { path => '/' . $path, files => $files_html });
253            
254             if ($options{layout}) {
255             # Is there a corresponding layout file in the views directory?
256             if (-f catfile($views_dir, 'layouts',
257             my $layout_file = $options{layout}))
258             {
259             # Display the directory listing using the specified layout
260             # file
261             return $template_simple->apply_layout($listing_html, {}, {
262             layout => $layout_file });
263             }
264             else {
265             # Use the application's default layout
266             return $template_simple->apply_layout($listing_html);
267             }
268             }
269             else {
270             # Display the listing in the template's layout
271             return $template_simple->render($layout_tpl,
272             { listing => $listing_html, path => '/' . $path,
273             path_prefix => $path_prefix, request => request,
274             template => 'default' });
275             }
276             }
277             else {
278             #
279             # Use a basic built-in template
280             #
281             my $files_html = '';
282             for my $file (@files) {
283             my $file_html = $builtin_tpl->{file};
284             $file_html =~ s/\[%\s*file.(\S*)\s*%\]/$file->{$1}/eg;
285             $files_html .= $file_html;
286             }
287             my $listing_html = $builtin_tpl->{listing};
288             $listing_html =~ s/\[%\s*path\s*%\]/"\/".$path/eg;
289             $listing_html =~ s/\[%\s*files\s*%\]/$files_html/eg;
290            
291             if ($options{layout}) {
292             # Get the application's template engine
293             my $template = engine 'template';
294             if (-f catfile($views_dir, 'layouts',
295             my $layout_file = $options{layout}))
296             {
297             # Display the directory listing using the specified layout
298             # file
299             return $template->apply_layout($listing_html, {},
300             { layout => $layout_file });
301             }
302             else {
303             # Use the default application layout
304             return $template->apply_layout($listing_html);
305             }
306             }
307             else {
308             # Use a primitive layout
309             (my $html = $builtin_tpl->{layout}) =~
310             s/\[%\s*content\s*%\]/$listing_html/eg;
311             $html =~ s/\[%\s*path\s*%\]/"\/".$path/eg;
312             return $html;
313             }
314             }
315             }
316             };
317              
318             my $path_prefix_re = quotemeta($path_prefix);
319              
320             get qr{^$path_prefix_re/.*} => sub {
321             (my $path = request->path_info) =~ s!^$path_prefix_re/!!;
322            
323             send_file(catfile($dist_dir, 'public', split('/', $path)),
324             system_path => 1);
325             };
326              
327             if (exists $settings->{url}) {
328             directory_view $settings->{url} => $settings;
329             }
330              
331             if (exists $settings->{directories}) {
332             for my $url (keys %{$settings->{directories}}) {
333             directory_view $url => $settings->{directories}->{$url} || {};
334             }
335             }
336              
337             register 'directory_view' => \&directory_view;
338              
339             register_plugin;
340              
341             sub _format_size {
342             my ($size) = @_;
343             $size ||= 0;
344            
345             if ($size > 1024**3) {
346             return sprintf("%.2f GB", $size / 1024**3);
347             }
348             elsif ($size > 1024**2) {
349             return sprintf("%.2f MB", $size / 1024**2);
350             }
351             elsif ($size > 1024) {
352             return sprintf("%.0f KB", $size / 1024);
353             }
354             else {
355             return sprintf("%d B", $size);
356             }
357             }
358              
359             # This piece of HTML is borrowed from Plack::App::Directory, which admits to
360             # have stolen it from rack/directory.rb. The world of open-source is full of
361             # thieves.
362             $builtin_tpl->{layout} = <
363            
364             [% path %]
365            
366            
373            
374             [% content %]
375            
376             END
377             $builtin_tpl->{listing} = <
378            

[% path %]

379            
380            
381            
382             Name
383             Size
384             Type
385             Last Modified
386            
387             [% files %]
388            
389            
390             END
391             $builtin_tpl->{file} = <
392            
393             [% file.name %]
394             [% file.size %]
395             [% file.mime_type %]
396             [% file.mtime %]
397            
398             END
399              
400             1; # End of Dancer::Plugin::DirectoryView
401             __END__