############################################################ # # $Id: XSLT.pm 1077 2007-12-14 17:44:32Z nicolaw $ # Apache2::AutoIndex::XSLT - XSLT Based Directory Listings # # Copyright 2006, 2007 Nicola Worthington # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # ############################################################ package Apache2::AutoIndex::XSLT; # vim:ts=4:sw=4:tw=78 use 5.6.1; use strict; use warnings; #use warnings FATAL => 'all'; use File::Spec qw(); use Fcntl qw(); use XML::Quote qw(); use URI::Escape qw(); # Try to replace with Apache2::Util or Apache2::URI # This is libapreq2 - we're parsing the query string manually # to avoid loading another non-standard module # use Apache2::Request qw(); # These two are required in general use Apache2::ServerRec qw(); # $r->server use Apache2::RequestRec qw(); use Apache2::RequestUtil qw(); # $r->document_root # Used to return various Apache constant response codes use Apache2::Const -compile => qw(:common :options :config :cmd_how :override :types); # Used for writing to Apache logs use Apache2::Log qw(); # Used for parsing Apache configuration directives use Apache2::Module qw(); use Apache2::CmdParms qw(); # Needed for use with Apache2::Module callbacks # Used to get the main server Apache2::ServerRec (not the virtual ServerRec) use Apache2::ServerUtil qw(); # Used for Apache2::Util::ht_time time formatting use Apache2::Util qw(); use Apache2::URI qw(); # $r->construct_url use Apache2::Access qw(); # $r->allow_options #use Apache2::Directive qw(); # Possibly not needed use Apache2::SubRequest qw(); # Needed for subrequests :) use Apache2::RequestIO qw(); # Needed for $r->print # Start here ... # http://perl.apache.org/docs/2.0/user/config/custom.html # http://perl.apache.org/docs/2.0/api/Apache2/Module.html # http://perl.apache.org/docs/2.0/api/Apache2/Const.html # http://perl.apache.org/docs/2.0/user/porting/compat.html # http://httpd.apache.org/docs/2.2/mod/mod_autoindex.html # http://httpd.apache.org/docs/2.2/mod/mod_dir.html # http://www.modperl.com/book/chapters/ch8.html use vars qw($VERSION %DIRECTIVES %COUNTERS %FILETYPES); $VERSION = '0.04' || sprintf('%d.%02d', q$Revision: 531 $ =~ /(\d+)/g); %COUNTERS = (Listings => 0, Files => 0, Directories => 0, Errors => 0); # # Apache response handler # sub handler { my $r = shift; # Only handle directories return Apache2::Const::DECLINED unless $r->content_type && $r->content_type eq Apache2::Const::DIR_MAGIC_TYPE; # Parse query string and get config my ($qstring,$dir_cfg) = init_handler($r); # Read in the filetypes information if (!defined %FILETYPES && defined $dir_cfg->{FileTypesFilename}) { FileTypesFilename: for my $FileTypesFilename ( $dir_cfg->{FileTypesFilename}, File::Spec->catfile($r->document_root,$dir_cfg->{FileTypesFilename}), File::Spec->catfile(Apache2::ServerUtil->server_root,'conf',$dir_cfg->{FileTypesFilename}), File::Spec->catfile(Apache2::ServerUtil->server_root,$dir_cfg->{FileTypesFilename}) ) { my $ext = ''; if (open(FH,'<',$FileTypesFilename)) { while (local $_ = ) { if (my ($k,$v) = $_ =~ /^\s*(\S+)\s*:\s*(\S.*?)\s*$/) { if ($k =~ /ext(ension)?/i) { $v =~ s/^\.//; $ext = $v || ''; } elsif ($v) { $FILETYPES{lc($ext)}->{$k} = $v; } } } close(FH); last FileTypesFilename; } } } # Dump the configuration out to screen if (defined $qstring->{CONFIG}) { $r->content_type('text/plain'); $r->print(dump_apache_configuration($r)); return Apache2::Const::OK; } # Make sure we're at a URL with a trailing slash if ($dir_cfg->{DirectorySlash} && $r->uri !~ m,/$,) {# || $r->path_info){ $r->headers_out->add(Location => sprintf('%s/%s', $r->uri, ($r->args ? '?'.$r->args : '') )); return Apache2::Const::REDIRECT; } # Return a directory listing if we're allowed to if ($r->allow_options & Apache2::Const::OPT_INDEXES) { # Should we render the XSLT or not? my $render = 0; if ($dir_cfg->{RenderXSLT} || (!exists $dir_cfg->{RenderXSLT} && defined $dir_cfg->{RenderXSLTEnvVar} && defined $ENV{$dir_cfg->{RenderXSLTEnvVar}} && $ENV{$dir_cfg->{RenderXSLTEnvVar}} =~ /^\s*(On|1|Yes|True)\s*$/i) ) { eval { require XML::LibXSLT; require XML::LibXML; $render = 1; }; $r->log_error('Failed to load XML::LibXML or XML::LibXSLT modules: ', $@) if $@; } # Send the appropriate content type my $content_type = $render ? 'text/html' : 'text/xml; charset="utf-8"'; $r->content_type($content_type); return Apache2::Const::OK if $r->header_only; # The dir_xml subroutine will actually print and output # all the XML DTD and XML, returning an OK if everything # was successful. my $rtn = Apache2::Const::SERVER_ERROR; my $xml; eval { $xml = dir_xml($r,$dir_cfg,$qstring); unless ($render) { $r->print($xml); } else { my $parser = XML::LibXML->new(); my $source = $parser->parse_string($xml); my $subr = $r->lookup_uri($dir_cfg->{IndexStyleSheet}); my $xslt = XML::LibXSLT->new(); my $style_doc = $parser->parse_file($subr->filename); my $stylesheet = $xslt->parse_stylesheet($style_doc); my $results = $stylesheet->transform($source); $r->print($stylesheet->output_string($results)); } $rtn = Apache2::Const::OK; }; if (!defined $xml || $@) { $COUNTERS{Errors}++; warn $@, $r->print($@); }; return $rtn; # Otherwise he's not the messiah, he's a very naughty boy } else { $r->log_reason( sprintf('%s Directory index forbidden by rule', __PACKAGE__), sprintf('%s (%s)', $r->uri, $r->filename), ); return Apache2::Const::FORBIDDEN; } } sub transhandler { my $r = shift; # Only handle directories return Apache2::Const::DECLINED unless $r->uri =~ /\/$/; return Apache2::Const::DECLINED unless $r->content_type && $r->content_type eq Apache2::Const::DIR_MAGIC_TYPE; # Parse query string and get config my ($qstring,$dir_cfg) = init_handler($r); foreach (@{$dir_cfg->{DirectoryIndex}}){ my $subr = $r->lookup_uri($r->uri . $_); last if $subr->path_info; if (stat $subr->finfo){ $r->uri($subr->uri); last; } } return Apache2::Const::DECLINED; } # # Apache2::Status status page handler # # Let Apache2::Status know we're here if it's hanging around unless (exists $ENV{AUTOMATED_TESTING}) { eval { Apache2::Status->menu_item('AutoIndex' => sprintf('%s status',__PACKAGE__), \&status) if Apache2::Module::loaded('Apache2::Status'); }; } sub status { my $r = shift; my @status; push @status, sprintf('%s %s
', __PACKAGE__, $VERSION); push @status, sprintf('

Configuration Directives: %s

', join(', ',keys %DIRECTIVES) ); push @status, "\n"; while (my ($k,$v) = each %COUNTERS) { push @status, "\n"; } push @status, "
$k:$v
\n"; push @status, "

Configuration:
\n"; push @status, dump_apache_configuration($r)."

\n"; return \@status; } # # Private helper subroutines # sub init_handler { my $r = shift; # Get query string values - use this manual code instead of # Apache2::Request because it uses less memory, and Apache2::Request # does not come as standard with mod_perl2 (it's libapreq2 on CPAN) my $qstring = {}; for (split(/[&;]/,($r->args||''))) { my ($k,$v) = split('=',$_,2); next unless defined $k; $v = '' unless defined $v; $qstring->{URI::Escape::uri_unescape($k)} = URI::Escape::uri_unescape($v); } # Get the configuration directives my $dir_cfg = get_config($r->server, $r->per_dir_config); return ($qstring,$dir_cfg); } sub dir_xml { my ($r,$dir_cfg,$qstring) = @_; my $xml = ''; # Increment listings counter $COUNTERS{Listings}++; # Get directory to work on my $directory = $r->filename; $r->filename("$directory/") unless $directory =~ m/\/$/; # Open the physical directory on disk to get a list of all items inside. # This won't pick up virtual directories aliased in Apache's configs. my $dh; unless (opendir($dh,$directory)) { $r->log_reason( sprintf("%s Unable to open directory handle for '%s': %s", __PACKAGE__, $directory, $!), sprintf('%s (%s)', $r->uri, $directory), ); return Apache2::Const::FORBIDDEN; } # Send the XML header and top of the index tree $xml .= xml_header($r,$dir_cfg); $xml .= sprintf("\n", $r->uri, $r->construct_url); $xml .= xml_options($r,$qstring,$dir_cfg); $xml .= "\t\n" unless $r->uri =~ m,^/?$,; # Build a list of attributes for each item in the directory and then # print it as an element in the index tree. while (my $id = readdir($dh)) { next if $id eq '..' || $id eq '.'; next if grep($id =~ /^$_$/, @{$dir_cfg->{IndexIgnoreRegex}}); #my $subr = $r->lookup_file($id); # Not used yet my $filename = File::Spec->catfile($directory,$id); my $type = file_type($r,$id,$filename); my $attr = build_attributes($r,$dir_cfg,$id,$filename,$type); $xml .= sprintf("\t<%s %s />\n", $type, join(' ', map { sprintf("\n\t\t%s=\"%s\"",$_,$attr->{$_}) if defined $_ && defined $attr->{$_} } keys(%{$attr}) )); $COUNTERS{Files}++ if $type eq 'file'; $COUNTERS{Directories}++ if $type eq 'dir'; } # Close the index tree, directory handle and return $xml .= "\n"; closedir($dh); return $xml; } sub xml_options { my ($r,$qstring,$dir_cfg) = @_; my $xml = ''; my $format = "\t\t