package Apache::Scriptor;
$VERSION="1.21";
use CGI::WebOut;
use Cwd;

# constructor new()
# Ñîçäàåò íîâûé Apache::Scriptor-îáúåêò.
sub new
{ my ($class)=@_;
  my $this = {
    Handlers        => {},
    HandDir         => ".",
    htaccess        => ".htaccess",
    # Çàïîìèíàåì, êàêîé çàïðîñ â äåéñòâèòåëüíîñòè áûë âûïîëíåí, ÷òîáû
    # ïîòîì èñêàòü åãî â htaccess-àõ.
    self_scriptname => $ENV{SCRIPT_NAME}
  };
  return bless($this,$class);
}


# void set_handlers_dir(string $dir)
# Óñòàíàâëèâàåò äèðåêòîðèþ äëÿ ïîèñêà îáðàáîò÷èêîâ.
sub set_handlers_dir
{ my ($this,$dir)=@_;
  $this->{HandDir}=$dir;
}

# void addhandler(ext1=>[h1, h2,...], ext2=>[...])
# Óñòàíàâëèâàåò îáðàáîò÷èê(è) äëÿ ðàñøèðåíèé ext1 è ext2.
# Çäåñü h1, h2 è ò.ä. ïðåäñòàâëÿþò ñîáîé ÑÑÛËÊÈ íà ôóíêöèè-îáðàáîò÷èêè.
# Åñëè æå îíè çàäàíû íå êàê ññûëêè, à êàê ÑÒÐÎÊÈ, òî â ìîìåíò îáðàùåíèÿ 
# ê î÷åðåäíîìó îáðàáîò÷èêó ïðîèçâîäèòñÿ ïîïûòêà åãî çàãðóçèòü èç ôàéëà,
# èìÿ êîòîðîãî ñîâïàäàåò ñ èìåíåì îáðàáîò÷èêà ñ ðàñøèðåíèåì ".pl" èç
# äèðåêòîðèè, êîòîðàÿ çàäàíà âûçîâîì set_handlers_dir().
sub addhandler
{ my ($this,%hands)=@_;
  %{$this->{Handlers}}=(%{$this->{Handlers}},%hands);
  return;
}

# void pushhandler(string ext, func &func)
# Äîáàâëÿåò îáðàáîò÷èê äëÿ ðàñøèðåíèÿ ext â êîíåö ñïèñêà îáðàáîò÷èêîâ.
sub pushhandler
{ my ($this,$ext,$func)=@_;
  $this->{Handlers}{$ext}||=[];
  push(@{$this->{Handlers}{$ext}},$func);
  return;
}

# void removehandler(ext1, ext2, ...)
# Óäàëÿåò îáðàáîò÷èê(è) äëÿ ðàñøèðåíèé ext1 è ext2.
sub removehandler
{ my ($this,@ext)=@_;
  foreach (@ext) { delete $this->{Handlers}{$_} }
  return;
}

# void set_404_url($url)
# Óñòàíàâëèâàåò àäðåñ ñòðàíèöû 404-é îøèáêè, íà êîòîðóþ áóäåò ïðîèçâåäåí 
# ðåäèðåêò, åñëè ôàéë íå íàéäåí.
sub set_404_url
{ my ($th,$url)=@_;
  $th->{404}=$url;
}

# void set_htaccess_name($name)
# Óñòàíàâëèâàåò èìÿ htaccess-ôàéëà. Ïî óìîë÷àíèþ ýòî .htaccess.
sub set_htaccess_name
{ my ($th,$htaccess)=@_;
  $th->{htaccess}=$htaccess;
}

sub process_htaccess
{ my ($th,$fname)=@_;
  open(local *F,$fname) or return;
  # Ñíà÷àëà ñîáèðàåì âñå äèðåêòèâû èç .htaccess
  my %Action=();
  my @AddHandler=();
  while(!eof(F)) {
    my $s=<F>; $s=~s/^\s+|#.*|\s+$//sg; next if $s eq "";
    # Äèðåêòèâà Action
    if($s=~m/Action\s+([\w\d-]+)\s*"?([^"]+)"?/si) {
      $Action{$1}=1 if $2 eq $th->{self_scriptname};
    }
    # Äèðåêòèâà AddHandler
    if($s=~m/AddHandler\s+([\w\d-]+)\s*(.+)/si) {
      push @AddHandler, [ $1, [ map { s/^\s*\.?|\s+$//sg; $_?($_):() } split /\s+/, $2 ] ];
    }
    # Äèðåêòèâà ErrorDocument 404
    if($s=~/ErrorDocument\s+404\s+"?([^"]+)"?/si) {
      $th->set_404_url($1);
    }
  }
  # Çàòåì äîáàâëÿåì öåïî÷êè îáðàáîò÷èêîâ
  my %ProcessedExt=();
  foreach my $info (@AddHandler) {
    my ($hand,$ext)=@$info;
    # Ñðàçó îòìåòàåì îáðàáîò÷èêè, êîòîðûå ÍÅ óêàçûâàþò íà Apache::Scriptor.
    # Ìû íå ìîãëè ýòîãî ñäåëàòü â âåðõíåì öèêëå, ïîòîïìó ÷òî äèðåêòèâû
    # Action è AddHandler ìîãóò èäòè íå ïî ïîðÿäêó.
    next if !$Action{$hand};
    # Äîáàâëÿåì äëÿ êàæäîãî ðàñøèðåíèÿ îáðàáîò÷èê â öåïî÷êó
    foreach my $ext (@$ext) {
      # Åñëè ýòî ðàñøèðåíèå âñòðå÷àåòñÿ â òåêóùåì htaccess-ôàéëå 
      # âïåðâûå, ýòî çíà÷èò, ÷òî íà÷àòà î÷åðåäíàÿ öåïî÷êà îáðàáîò÷èêîâ.
      #  ýòîì ñëó÷àå íóæíî óäàëèòü óæå èìåþùóþñÿ öåïî÷êó.
      if(!$ProcessedExt{$ext}) {
        $th->removehandler($ext);
        $ProcessedExt{$ext}=1;
      }
      # Çàòåì ñïîêîéíî âûçûâàåì pushhandler()
      $th->pushhandler($ext,$hand);
    }
  }
}

sub process_htaccesses
{ my ($th,$path)=@_;
  # Ñíà÷àëà îïðåäåëÿåì âñå ïîëíûå ïóòè ê htaccess-ôàéëàì
  my @Hts=();
  while($path=~m{[/\\]}) {
    if(-d $path) {
      my $ht="$path/$th->{htaccess}";
      unshift(@Hts,$ht) if -f $ht;
    }
    $path=~s{[/\\][^/\\]*$}{}s;
  }
  # Çàòåì îáðàáàòûâàåì ýòè ôàéëû, íà÷èíàÿ ñ ñàìîãî êîðíåâîãî
  map { $th->process_htaccess($_) } @Hts;
}

# void run_uri(string $uri [,string $path_translated])
# Çàïóñêàåò óêàçàííûé URI íà îáðàáîòêó. Åñëè óêàçàí ïàðàìåòð $path_translated,
# òî îí ñîäåðæèò ïîëíîå èìÿ ôàéëà ñ ñîäåðæèìûì äëÿ îáðàáîòêè. Â ïðîòèâíîì 
# ñëó÷àå èìÿ ôàéëà âû÷èñëÿåòñÿ àâòîìàòè÷åñêè íà îñíîâå $uri (ýòî íå âñåãäà
# ðàáîòàåò ïðàâèëüíî - íàïðèìåð, òàêàÿ øòóêà íå ïðîéäåò, åñëè äèðåêòîðèÿ áûëà
# çàâåäåíà êàê Alias Apache).
sub run_uri
{ my ($this,$uri,$path)=@_;
  Header("X-Powered-by: Apache::Scriptor v$VERSION. (C) Dmitry Koterov <koterov at cpan dot org>") if !$CopySend++;

  # Òåïåðü ðàáîòàåì ñ ÊÎÏÈÅÉ îáúåêòà. Òàêèì îáðàçîì, äàëüíåéøèå âûçîâû
  # process_htaccesses è ò.ä. íå îòðàçÿòñÿ íà îáùåì ñîñòîÿíèè îáúåêòà
  # ïîñëå îêîí÷àíèÿ çàïðîñà.
  local $this->{Handlers}={%{$this->{Handlers}}};
  local $this->{404}=$this->{404};

  # Ðàçäåëÿåì íà URL è QUERY_STRING
  local ($ENV{SCRIPT_NAME},$q) = split /\?/, $uri, 2;
  $ENV{QUERY_STRING}=defined $q? $q : "";

  # Âû÷èñëÿåì ïóòü ê ôàéëó ñêðèïòà ïî URI
  if(!$path) {
    $path="$ENV{DOCUMENT_ROOT}$ENV{SCRIPT_NAME}";
  }

  # Ãîòîâèì íîâûå ïåðåìåííûå îêðóæåíèÿ, ÷òîáû ñêðûòü Apache::Scriptor;
  local $ENV{REQUEST_URI}     = $uri;
  local $ENV{SCRIPT_FILENAME} = $path;
  local $ENV{REDIRECT_URL};     delete($ENV{REDIRECT_URL});
  local $ENV{REDIRECT_STATUS};  delete($ENV{REDIRECT_STATUS});
  # Ìåíÿåì òåêóùóþ äèðåêòîðèþ.
  my $MyDir=getcwd(); 
  ($MyDir) = $MyDir=~/(.*)/;
  my ($dir) = $path; $dir=~s{(.)[/\\][^/\\]*$}{$1}sg;
 
  chdir($dir); getcwd(); # getcwd: Ñáðàñûâàåò $ENV{PWD}. Íàì ýòî íàäî? Ôèã çíàåò...
  # Îáðàáàòûâàåì ôàéëû .htaccess.
  $this->process_htaccesses($path);

  # Âñå. Òåïåðü ñîñòîÿíèå ïåðåìåííûõ ñêðèïòà òàêîå æå, êàê ó ñòðàíèöû,
  # êîòîðàÿ â äàëüíåéøåì ïîëó÷èò óïðàâëåíèå. Çàïóñêàåì îáðàáîò÷èêè.
  $this->__run_handlers();
  
  # Âîññòàíàâëèâàåì òåêóùóþ äèðåêòîðèþ
  chdir($MyDir); getcwd(); 
}


# Âíóòðåííÿÿ ôóíêöèÿ - çàïóñêàåò îáðàáîò÷èêè äëÿ ôàéëà, êîòîðûé çàäàí â %ENV.
# Âûçûâàåòñÿ Â ÊÎÍÒÅÊÑÒÅ ÝÒÎÃÎ ÔÀÉËÀ (òî åñòü, %ENV íàõîäèòñÿ â òàêîì æå ñîñòîÿíèè,
# êàê ïîñëå îáÿ÷íîãî çàïóñêà ñêðèïòà Àïà÷åì, è òåêóùàÿ äèðåêòîðèÿ ñîîòâåòñòâóåò
# äèðåêòîðèè ñî ñòðàíèöåé).
sub __run_handlers
{ my ($th)=@_;
  # ðàñøèðåíèå ôàéëà
  my ($ext)  = $ENV{SCRIPT_FILENAME}=~m|\.([^.]*)$|; if(!defined $ext) { $ext=""; }

  # âûáèðàåì ñïèñîê îáðàáîò÷èêîâ äëÿ ýòîãî ðàñøèðåíèÿ
  $th->{Handlers}{$ext} 
    or die "$ENV{SCRIPT_NAME}: could not find handlers chain for extension \"$ext\"\n";

  # âõîäíîé áóôåð (âíà÷àëå â íåì ñîäåðæèìîå ôàéëà, åñëè äîñòóïíî)
  my $input="";
  if(open(local *F, $ENV{SCRIPT_FILENAME})) { local ($/,$\); binmode(F); $input=<F>; }

  # ïðîõîäèìñÿ ïî âñåì îáðàáîò÷èêàì
  my $next=1; # íîìåð ñëåäóþùåãî îáðàáîò÷èêà
  my @hands=@{$th->{Handlers}{$ext}};
  NoAutoflush() if @hands>1;
  foreach my $hand (@hands)
  { # Îáúåêò ïåðåíàïðàâëåíèÿ âûâîäà. Åñëè ó íàñ âñåãî îäèí îáðàáîò÷èê, òî 
    # ïåðåíàïðàâëÿòü âûâîä íå ïîòðåáóåòñÿ. Èíà÷å - ïîòðåáóåòñÿ, ÷òî è äåëàåòñÿ
    my $OutObj=$hands[$next++]? CGI::WebOut->new : undef;
    my $func=$hand; # óêàçàòåëü íà ôóíêöèþ
    # Ïðîâåðÿåì - íóæíî ëè çàãðóçèòü îáðàáîò÷èê?
    if((ref($func)||"") ne "CODE") {
      # ïåðåêëþ÷àåì ïàêåò
      package Apache::Scriptor::Handlers; 
      # îáðàáîò÷èêà åùå íåò â ýòîì ïàêåòå?
      if(!*{$func}{CODE}) {
        my $hname="$th->{HandDir}/$func.pl";
        -f $hname or die "$ENV{SCRIPT_NAME}: could not load the file $hname for handler $hand\n";
        do "$hname";
        *{$func}{CODE} or die "$ENV{SCRIPT_NAME}: cannot find handler $hand in $hname after loading $hname\n";
      }
      # ïîëó÷àåì óêàçàòåëü íà ôóíêöèþ îáðàáîò÷èêà
      local $this=$th;
      $func=*{$func}{CODE};
    }
    # Ôóíêöèÿ îáðàáîò÷èêà ïðèíèìàåò ïàðàìåòð: âõîäíîé áóôåð.
    # Åå çàäà÷à - îáðàáîòàòü åãî è, èñïîëüçóÿ print, ïðîïå÷àòàòü ðåçóëüòàò.
    #  ñëó÷àå îøèáêè (ôàéë íå íàéäåí) ôóíêöèÿ äîëæíà âîçâðàòèòü -1!
    my $result=&$func($input);
    if($result eq "-1") {
      if($th->{404} && $th->{404} ne $th->{self_scriptname}) {
        Redirect($th->{404});
        exit;
      } else {
        die "$hand: could not find the file $ENV{SCRIPT_FILENAME}\n";
      }
    }

    # Òî, ÷òî ïîëó÷èëîñü, êëàäåì âî âõîäíîé áóôåð äëÿ ñëåäóþùåãî îáðàáîò÷èêà.
    # Åñëè âûâîä íå ïåðåíàïðàâëÿëñÿ, òî êëàäåì òóäà "".
    $input=$OutObj?$OutObj->buf:"";
  }
  # Îêîí÷àòåëüíûé ðåçóëüòàò îêàæåòñÿ âî âõîäíîì áóôåðå (êàê áóäòî ãîòîâûé äëÿ 
  # ñëåäóþùåãî îáðàáîò÷èêà, êîòîðîãî íåò). Åãî-òî ìû è âûâîäèì â áðàóçåð.
  print $input;
}



package Apache::Scriptor::Handlers;
use CGI::WebOut;
#  ýòîì ïàêåòå ïåðå÷èñëÿþòñÿ ñòàíäàðòíûå îáðàáîò÷èêè, 
# êîòîðûå, ñêîðåå âñåãî, áóäóò èñïðîëüçîâàíû â ïåðâóþ î÷åðåäü.
# Èìåííî â ýòîò ïàêåò ïîïàäàþò îáðàáîò÷èêè, çàãðóæåííûå àâòîìàòè÷åñêè.

# Îáðàáîò÷èê ïî óìîë÷àíèþ - ïðîñòî âûâîäèò òåêñò
sub default
{ my ($input,$fname)=@_;
  -f $ENV{SCRIPT_FILENAME} or return -1;
  CGI::WebOut::Header("Content-type: text/html");
  print $input;
}

# Îáðàáîò÷èê perl-ñêðèïòîâ. Ïîäðàçóìåâàåòñÿ, ÷òî âûâîä ñêðèïòà èäåò ÷åðåç print.
sub perl
{ my ($input)=@_;
  -f $ENV{SCRIPT_FILENAME} or return -1;
  eval("\n#line 1 \"$ENV{SCRIPT_NAME}\"\npackage main; $input");
}

return 1;
__END__







=head1 NAME

Apache::Scriptor - Support for Apache handlers conveyor.

=head1 SYNOPSIS

Synopsis are not so easy as in other modules, that's why let's see example below.

=head1 FEATURES

=over 4

=item *

Uses ONLY perl binary.

=item *

Helps to organize the Apache handler conveyor. That means you can redirect the output from one handler to another handler.

=item *

Supports non-existance URL handling and 404 Error processing.

=item *

Uses C<.htaccess> files to configure.

=back


=head1 EXAMPLE

  ### Consider the server structure:
  ### /
  ###   _Kernel/
  ###      handlers/
  ###        s_copyright.pl
  ###        ...
  ###      .htaccess
  ###      Scriptor.pl
  ###   .htaccess
  ###   test.htm

  ### File /.htaccess:
    # Setting up the conveyor for .htm:
    # "input" => eperl => s_copyright => "output" 
    Action     perl "/_Kernel/Scriptor.pl"
    AddHandler perl .htm
    Action     s_copyright "/_Kernel/Scriptor.pl"
    AddHandler s_copyright .htm


  ### File /_Kernel/.htaccess:
    # Enables Scriptor.pl as perl executable
    Options ExecCGI
    AddHandler cgi-script .pl

  ### File /_Kernel/Scriptor.pl:
    #!/usr/local/bin/perl -w 
    use FindBin qw($Bin);          # òåêóùàÿ äèðåêòîðèÿ
    my $HandDir="$Bin/handlers";   # äèðåêòîðèÿ ñ îáðàáîò÷èêàìè
    # This is run not as CGI-script?
    if(!$ENV{DOCUMENT_ROOT} || !$ENV{SCRIPT_NAME} || !$ENV{SERVER_NAME}) {
      print "This script has to be used only as Apache handler!\n\n";
      exit;
    }
    # Non-Apache-handler run?
    if(!$ENV{REDIRECT_URL}) {
      print "Location: http"."://$ENV{SERVER_NAME}/\n\n";
      exit;
    }
    require Apache::Scriptor;
    my $Scr=Apache::Scriptor->new();
    # Setting up the handlers' directory.
    $Scr->set_handlers_dir($HandDir);
    # Go on!
    $Scr->run_uri($ENV{REQUEST_URI},$ENV{PATH_TRANSLATED});

  ### File /_Kernel/handlers/s_copyright.pl:
    sub s_copyright
    {  my ($input)=@_;
       -f $ENV{SCRIPT_FILENAME} or return -1; # Error indicator
       # Adds the comment string BEFORE all the output.
       print '<!-- Copyright (C) by Dmitry Koterov (koterov at cpan dot org) -->\n'.$input;
       return 0; # OK
    }

  ### File /test.htm:
    print "<html><body>Hello, world!</body></html>";

  ### Then, user enters the URL: http://ourhost.com/test.htm.
  ### The result will be:
    Content-type: text/html\n\n
    <!-- Copyright (C) by Dmitry Koterov (koterov at cpan dot org) -->\n
    Hello, world!

=head1 OVERVIEW

This module is used to handle all the requests through the Perl script 
(such as C</_Kernel/Scriptor.pl>, see above). This script is just calling
the handlers conveyor for the specified file types.

When you place directives like these in your C<.htaccess> file:

  Action     s_copyright "/_Kernel/Scriptor.pl"
  AddHandler s_copyright .htm

Apache sees that, to process C<.htm> document, C</_Kernel/Scriptor.pl> handler
should be used. Then, Apache::Scriptor starts, reads this C<.htaccess> and remembers
the handler name for C<.htm> document: it is C<s_copyright>. Apache::Scriptor searches 
for C</_Kernel/handlers/s_copyright.pl>, trying to find the subroutine with the same name:
C<s_copyright()>. Then it runs that and passes the document body, returned from the previous 
handler, as the first parameter. 

How to start the new conveyor for extension C<.html>, for example? It's easy: you
place some Action-AddHandler pairs into the C<.htaccess> file. You must choose
the name for these handlers corresponding to the Scriptor handler file names 
(placed in C</_Kernel/handlers>). Apache does NOT care about these names, but 
Apache::Scriptor does. See example above (it uses two handlers: built-in C<perl> and user-defined C<s_copyright>).

=head1 DESCRIPTION

=over 11

=item C<require Apache::Scriptor>

Loads the module core.

=item C<Apache::Scriptor'new>

Creates the new Apache::Scriptor object. Then you may set up its 
properties and run methods (see below).

=item C<$obj'set_handlers_dir($dir)>

Sets up the directory, which is used to search for handlers.

=item C<$obj'run_uri($uri [, $filename])>

Runs the specified URI through the handlers conveyer and prints out 
the result. If C<$filename> parameter is specified, module does not
try to convert URL to filename and uses it directly.

=item C<$obj'addhandler(ext1=>[h1, h2,...], ext2=>[...])>

Manually sets up the handlers' conveyor for document extensions. 
Values of C<h1>, C<h2> etc. could be code references or 
late-loadable function names (as while parsing the C<.htaccess> file).

=item C<$obj'pushhandler($ext, $handler)>

Adds the handler C<$handler> th the end of the conveyor for extension C<$ext>.

=item C<$obj'removehandler($ext)>

Removes all the handlers for extension C<$ext>.

=item C<$obj'set_404_url($url)>

Sets up the redirect address for 404 error. By default, this value is 
bringing up from C<.htaccess> files.

=item C<$obj'set_htaccess_name($name)>

Tells Apache::Scriptor object then Apache user configuration file is called C<$name>
(by default C<$name=".htaccess">).

=item C<$obj'process_htaccess($filename)>

Processes all the directives in the C<.htaccess> file C<$filename> and adds
all the found handlers th the object.

=item C<package Apache::Scriptor::Handlers>

This package holds ALL the handler subroutines. You can place 
some user-defined handlers into it before loading the module to 
avoid their late loading from handlers directory.

=back

=head1 AUTHOR

Dmitry Koterov <koterov at cpan dot org>, http://www.dklab.ru

=head1 SEE ALSO

C<CGI::WebOut>.

=cut