# -*- mode: perl; coding: utf-8 -*- package YATT::Toplevel::CGI; use strict; use warnings FATAL => qw(all); BEGIN {require Exporter; *import = \&Exporter::import} use base qw(File::Spec); use File::Basename; use Carp; use UNIVERSAL; #---------------------------------------- use YATT; use YATT::Types -alias => [MY => __PACKAGE__ , Translator => 'YATT::Translator::Perl']; require YATT::Inc; use YATT::Util; use YATT::Util::Finalizer; use YATT::Util::Taint qw(untaint_any); use YATT::Util::Symbol; use YATT::Util::CmdLine; use YATT::Exception; #---------------------------------------- use base qw(YATT::Class::Configurable); use YATT::Types -base => __PACKAGE__ , [Config => [qw(^cf_registry cf_driver cf_docs cf_tmpl cf_charset cf_debug_allowed_ip cf_translator_param cf_user_config cf_no_header cf_allow_unknown_config cf_auto_reload cf_no_chdir cf_rlimit cf_use_session ) , ['^cf_app_prefix' => 'YATT'] , ['^cf_find_root_upward' => 2] ]] , qw(:export_alias); Config->define(create => \&create_toplevel); #---------------------------------------- use vars map {'$'.$_} our @env_vars = qw(DOCUMENT_ROOT PATH_INFO PATH_TRANSLATED REDIRECT_REDIRECT_STATUS REDIRECT_STATUS REDIRECT_URL REQUEST_URI SCRIPT_FILENAME ); push our @EXPORT, (qw(&use_env_vars &rootname &capture &new_config ), map {'*'.$_} our @env_vars); our Config $CONFIG; our ($CGI, $SESSION, %COOKIE, %HEADER, $RANDOM_LIST, $RANDOM_INDEX); sub rc_global () { qw(CONFIG CGI SESSION HEADER COOKIE RANDOM_LIST RANDOM_INDEX) } push our @EXPORT_OK, (@EXPORT, map {'*'.$_} rc_global); sub ROOT_CONFIG () {'.htyattroot'} #---------------------------------------- # run -> run_zzz -> dispatch(handler) -> dispatch_zzz(handler) -> handler # run は環境変数を整えるためのエントリー関数。 sub run { my ($pack, $method) = splice @_, 0, 2; use_env_vars(); my $sub = $pack->can("run_$method") or croak "Can't find handler for $method"; &YATT::break_run; $sub->($pack, @_); } sub run_cgi { my $pack = shift; my $cgi = $pack->new_cgi(shift); local $CONFIG = my Config $config = $pack->new_config(shift); my ($root, $file, $error, $param); if (catch { ($pack, $root, $cgi, $file, $param) = $pack->prepare_dispatch($cgi, $config); } \ $error) { $pack->dispatch_error($root, $error , {phase => 'prepare', target => $file}); } else { $pack->run_retry_max(3, $root, $file, $cgi, $param); } } sub run_retry_max { my ($pack, $max, $root_or_config, $file, $cgi, @param) = @_; my $root = do { if (UNIVERSAL::isa($root_or_config, Config)) { my Config $config = $root_or_config; $config->{cf_registry} } else { $root_or_config; } }; my $rc = catch { $pack->dispatch($root, $cgi, $file, @param); } \ my $error; if ($rc) { my ($i) = (0); while ($rc and ($file, $cgi) = can_retry($error)) { if ($i++ > $max) { $pack->dispatch_error($root, $error , {phase => 'retry', target => $file}); undef $error; last; } $rc = catch { $pack->dispatch($root, $cgi, $file); } \ $error; } } if ($rc and not is_normal_end($error)) { $pack->dispatch_error($root, $error , {phase => 'action', target => $file}); } } sub create_toplevel { my $pack = shift; my Config $config = $pack->new_config(shift); $config->configure(@_) if @_; my $dir = $config->{cf_docs} ||= '.'; $pack->can('try_load_config')->($config, $dir); my $instpkg = $pack->get_instpkg($config); my @loader = (DIR => $config->{cf_docs}); push @loader, LIB => $config->{cf_tmpl} if $config->{cf_tmpl}; my $trans = $config->{cf_registry} = $instpkg->new_translator (\@loader, $config->translator_param); ($instpkg, $trans, $config); } # # XXX: should be: create_toplevel_from_cgi($cgi, $config) # => ($instpkg, $trans, $config, $cgi, $file, $param); # since $config->{cf_registry} points $translator. # sub prepare_dispatch { (my ($pack, $cgi), my Config $config) = @_; my ($rootdir, $file, $loader, $param) = do { if (not $config->{cf_registry} and $config->{cf_docs}) { # $config->try_load_config($config->{cf_docs}); ($config->{cf_docs}, $cgi->path_info , [DIR => $config->{cf_docs}]); } elsif ($REDIRECT_STATUS) { # 404 Not found handling my $target = $PATH_TRANSLATED || $DOCUMENT_ROOT . $REDIRECT_URL; # This ensures .htyattroot is loaded. ($pack->param_for_redirect($target , $SCRIPT_FILENAME || $0, $config , $REDIRECT_STATUS == 404 )); } elsif ($PATH_INFO and $SCRIPT_FILENAME) { (untaint_any(dirname($SCRIPT_FILENAME)) , untaint_any($PATH_INFO) , $pack->loader_for_script($SCRIPT_FILENAME, $config)); } else { $pack->plain_error($cgi, <plain_error($cgi, <plain_error($cgi, "Can't chdir to $rootdir: $!"); } unless ($PATH_INFO) { if ($PATH_TRANSLATED) { # XXX: ミス時に効率悪い。substr して eq に書き直すべき。 if (index($PATH_TRANSLATED, $rootdir) == 0) { $PATH_INFO = substr($PATH_TRANSLATED, length($rootdir)); } } } $cgi->charset($config->{cf_charset} || 'utf-8'); my $instpkg = $pack->get_instpkg($config); my $root = $config->{cf_registry} ||= $instpkg->new_translator ($loader, $config->translator_param , debug_translator => $ENV{DEBUG}); $instpkg->set_random_list; $instpkg->force_parameter_convention($cgi); # XXX: unless $config->{...} ($instpkg, $root, $cgi, $file, $param); } our $PARAM_CONVENTION = qr{^[\w:\-]}; sub force_parameter_convention { my ($pack, $cgi) = @_; my @deleted; foreach my $name ($cgi->param) { next if $name =~ $PARAM_CONVENTION; push @deleted, [$name => $cgi->param($name)]; $cgi->delete($name); } @deleted; } *get_instpkg = \&prepare_export; sub prepare_export { my ($pack, $config, $instpkg) = @_; $instpkg ||= $config && $config->app_prefix || 'main'; $pack->add_isa($instpkg, $pack); foreach my $name ($pack->rc_global) { *{globref($instpkg, $name)} = *{globref(MY, $name)}; } $instpkg } sub run_template { my ($pack, $file, $cgi, $config) = @_; if (defined $file and -r $file) { ($PATH_INFO, $REDIRECT_STATUS, $PATH_TRANSLATED) = ('', 200, $file); die "really?" unless $ENV{REDIRECT_STATUS} == 200; die "really?" unless $ENV{PATH_TRANSLATED} eq $file; } $pack->run_cgi($cgi, $config); } #======================================== # *: dispatch_zzz が無事に最後まで処理を終えた場合は bye を呼ぶ。 # *: dispatch_zzz の中では catch はしない。dispatch の外側(run)で catch する。 sub bye { die shift->Exception->new(error => '', normal => shift || 1 , caller => [caller], @_); } sub raise_retry { my ($pack, $file, $cgi, @param) = @_; die $pack->Exception->new(error => '', retry => [$file, $cgi, @param] , caller => [caller]) } sub dispatch { my ($top, $root, $cgi, $file, @param) = @_; &YATT::break_dispatch; $root->mark_load_failure; local $CGI = $cgi; local ($SESSION, %COOKIE, %HEADER); if ($CONFIG->{cf_use_session}) { $SESSION = $top->new_session($cgi); } my @elpath = $root->parse_elempath($top->canonicalize_html_filename($file)); my ($found, $renderer, $pkg, $widget); if (catch { $found = ($renderer, $pkg, $widget) = $root->lookup_handler_to(render => @elpath); } \ my $error) { $top->dispatch_error($root, $error , {phase => 'get_handler', target => $file}); } elsif (not $found) { # XXX: これも。 $top->dispatch_not_found($root, $file, @param); } elsif (not defined $renderer) { $top->dispatch_error($root, "Can't compile: $file" , {phase => 'get_handler', target => $file}); } else { unless ($CONFIG->{cf_no_chdir}) { # XXX: これもエラー処理を my $dir = untaint_any(dirname($widget->filename)); chdir($dir); } if (not defined $param[0] and $widget->public) { $param[0] = $widget->reorder_cgi_params($cgi); } if (my $handler = $pkg->can('dispatch_action')) { $handler->($top, $root, $renderer, $pkg, @param); } else { $top->dispatch_action($root, $renderer, $pkg, @param); } } } sub dispatch_not_found { my ($top, $root, $file) = @_; my $ERR = \*STDOUT; print $ERR "\n\nNot found: $file"; } # XXX: もう少し改善を。 sub dispatch_error { my ($top, $root, $error, $info) = @_; my $ERR = \*STDOUT; my ($found, $renderer, $pkg, $html); unless ($root) { print $ERR "\n\nroot_load_error($error)"; } elsif (catch { $found = ($renderer, $pkg) = $root->lookup_handler_to(render => 'error') } \ my $load_error) { print $ERR "\n\nload_error($load_error), original_error=($error)"; } elsif (not $found) { print $ERR $CGI ? $CGI->header : "\n\n"; print $ERR $error; $top->printenv_html($info, id => 'error_info') if $info; $top->printenv_html; } elsif (catch { $html = capture {$renderer->($pkg, [$error, $info])}; } \ my Exception $error2) { unless (ref $error2) { print $ERR "\n\nerror in error page($error2), original_error=($error)"; } elsif (not UNIVERSAL::isa($error2, Exception)) { print $ERR "\n\nUnknown error in error page($error2), original_error=($error)"; } elsif ($error2->is_normal) { # should be ignored } else { print $ERR "\n\nerror in error page($error2->{cf_error}), original_error=($error)"; } } else { print $ERR $CGI ? $CGI->header : "Content-type: text/html\n\n"; print $ERR $html; } $top->bye; } sub dispatch_action { my ($top, $root, $action, $pkg, @param) = @_; &YATT::break_handler; if ($CONFIG && $CONFIG->{cf_no_header}) { $action->($pkg, @param); } else { my $html = capture { $action->($pkg, @param) }; # XXX: SESSION, COOKIE, HEADER... print $SESSION ? $SESSION->header : $CGI->header; print $html; } $top->bye; } sub plain_error { my ($pack, $cgi, $message) = @_; print $cgi->header if $cgi; print $message; $pack->printenv_html; $pack->plain_exit($cgi ? 0 : 1); } sub plain_exit { my ($pack, $exit_code) = @_; exit $exit_code; } sub printenv_html { my ($pack, $env, %opts) = @_; $opts{id} ||= 'printenv'; my $ERR = \*STDOUT; $env ||= \%ENV; print $ERR "\n"; foreach my $k (sort keys %$env) { print $ERR "\n"; } print $ERR "
", $k, "", $env->{$k}, "
\n"; } #======================================== sub loader_for_script { my ($pack, $script_filename) = @_; my $driver = untaint_any(rootname($script_filename)); my @loader = (DIR => untaint_any("$driver.docs") , $pack->tmpl_for_driver($driver)); \@loader; } sub tmpl_for_driver { my ($pack, $rootname) = @_; return unless -d (my $dir = "$rootname.tmpl"); (LIB => $dir); } sub upward_find_file { my ($pack, $file, $level) = @_; my @path = $pack->splitdir($pack->rel2abs($file)); my $limit = defined $level ? @path - $level : 0; my ($dir); for (my $i = $#path - 1; $i >= $limit; $i--) { $dir = join "/", @path[0..$i]; $file = "$dir/" . $pack->ROOT_CONFIG; next unless -r $file; return wantarray ? ($dir, $file) : $file; } return } sub try_load_config { (my Config $config, my ($file)) = @_; my $dir; unless (defined $file and -r $file) { die "No such file or directory! " . (defined $file ? $file : "(undef)") . "\n"; } elsif (-f $file) { # ok $file = $config->rel2abs($file); $dir = dirname($file); } elsif (! -d $file) { die "Unsupported file type! $file"; } elsif (-r (my $found = "$file/" . $config->ROOT_CONFIG)) { ($dir, $file) = ($file, $found); } elsif ($config->find_root_upward and my @found = $config->upward_find_file ($file, $config->find_root_upward)) { ($dir, $file) = @found; } else { $dir = $file; } $config->configure(docs => $dir); return unless -r $file; # XXX: configure_by_file my @param = do { require YATT::XHF; my $parser = new YATT::XHF(filename => $file); $parser->read_as('pairlist'); }; $config->heavy_configure(@param); } sub trim_trailing_pathinfo { my ($pack, $strref, @prefix) = @_; @prefix = ('') unless @prefix; my @dirs = $pack->splitdir($$strref); my @found; while (@dirs and -e join("/", @prefix, @found, $dirs[0])) { push @found, shift @dirs; } $$strref = join("/", @found); return unless @dirs; join("/", @dirs); } sub param_for_redirect { (my ($pack, $path_translated, $script_filename) , my Config $cfobj, my $not_found) = @_; my $driver = untaint_any(rootname($script_filename)); my @params; if (not $not_found and not -e $path_translated) { # not_found でもないのに、 path_translated が not exists であるケース # == trailing path_info が有るケース。 push @params, $pack->trim_trailing_pathinfo(\$path_translated); } # This should set $cfobj->{cf_docs} unless ($cfobj->{cf_registry}) { # .htyattroot の読み込みは、registry 作成前の一度で十分。 $cfobj->try_load_config(dirname(untaint_any($path_translated))); } my $target = substr($path_translated , length($cfobj->{cf_docs})); my @loader = (DIR => $cfobj->{cf_docs} , $pack->tmpl_for_driver($driver)); return ($cfobj->{cf_docs}, $target, \@loader, @params ? \@params : ()); } #======================================== sub cgi_classes () { qw(CGI::Simple CGI) } sub new_cgi { my ($pack, $oldcgi) = @_; my $class; foreach my $c ($pack->cgi_classes) { eval qq{require $c}; unless ($@) { $class = $c; last; } } unless ($class) { die "Can't load any of cgi classes"; } # 1. To make sure passing 'public' parameters only. # 2. To avoid CGI::Simple eval() if (UNIVERSAL::isa($oldcgi, $class)) { $class->new($pack->extract_cgi_params($oldcgi)); } else { $class->new(defined $oldcgi ? $oldcgi : ()); } } sub new_session { my ($toplevel, $cgi) = @_; require CGI::Session; my ($dsn, @opts) = do { if (ref $CONFIG->{cf_use_session}) { @{$CONFIG->{cf_use_session}} } else { $CONFIG->{cf_use_session} } }; CGI::Session->new($dsn, $cgi, @opts); } sub entity_session { my ($pack, $name) = @_; $SESSION->param($name); } sub entity_save_session { $SESSION->save_param; } sub new_config { my $pack = shift; my Config $config = @_ == 1 ? shift : \@_; return $config if defined $config and ref $config and UNIVERSAL::isa($config, Config); if (ref $pack or not UNIVERSAL::isa($pack, Config)) { $pack = $pack->Config; } $config = $pack->new(do { unless (defined $config) { () } elsif (not ref $config) { (docs => $config) } elsif (ref $config eq 'ARRAY') { @$config } elsif (ref $config eq 'HASH') { %$config } else { $pack->plain_error(undef, <{cf_driver} = $0; $config; } sub heavy_configure { my Config $config = shift; my $config_keys = $config->fields_hash; my $trans_keys = $config->load_type('Translator')->fields_hash_of_class; my (@mine, @trans, @unknown); while (my ($name, $value) = splice @_, 0, 2) { my $mine = $config_keys->{"cf_$name"}; if ($mine) { push @mine, $name, $value; } if ($trans_keys->{"cf_$name"}) { push @trans, [$name, $value]; } elsif (not $mine) { push @unknown, [$name, $value]; } } $config->configure(@mine) if @mine; foreach my $name ($config->configkeys) { if ($trans_keys->{"cf_$name"} and defined (my $value = $config->{"cf_$name"})) { push @trans, [$name, $value]; } } $config->{cf_translator_param}{$_->[0]} = $_->[1] for @trans; if (@unknown) { unless ($config->{cf_allow_unknown_config}) { croak "Unknown config opts: " . join(", ", map {join("=", @$_)} @unknown); } $config->{cf_user_config}{$_->[0]} = $_->[1] for @unknown; } $config; } sub configure_rlimit { (my Config $config, my $rlimit_hash) = @_; my $class = 'YATT::Util::RLimit'; eval qq{require $class} or die $@; while (my ($rsrc, $limit) = each %$rlimit_hash) { if (my $sub = $class->can("rlimit_" . $rsrc)) { $sub->($limit); } else { $class->can('rlimit')->("RLIMIT_" . uc($rsrc), $limit); } } } sub extract_cgi_params { my ($pack, $cgi) = @_; my %param; foreach my $name ($cgi->param) { my @value = $cgi->param($name); if (@value > 1) { $param{$name} = \@value; } else { $param{$name} = $value[0]; } } \%param } sub new_translator { my ($self, $loader) = splice @_, 0, 2; my $pack = ref $self || $self; $pack->call_type(Translator => new => app_prefix => $pack , default_base_class => $pack , rc_global => [$pack->rc_global] , loader => $loader, @_); } sub use_env_vars { foreach my $vn (our @env_vars) { *{globref(MY, $vn)} = do { $ENV{$vn} = '' unless defined $ENV{$vn}; \ $ENV{$vn}; }; } $SCRIPT_FILENAME ||= $0; } #======================================== sub set_random_list { my ($this, $random) = @_; if (defined $random) { $RANDOM_LIST = ref $random ? $random : [split " ", $random]; $RANDOM_INDEX = 0; } else { undef $RANDOM_LIST; undef $RANDOM_INDEX; } } sub entity_rand { my ($this, $scalar) = @_; $scalar ||= 1; if ($RANDOM_LIST) { my $val = $RANDOM_LIST->[$RANDOM_INDEX++ % @$RANDOM_LIST]; $val * $scalar; } else { rand $scalar; } } sub entity_randomize { my ($this) = shift; my $sub = $this->can('entity_rand'); my @result; push @result, splice @_, $sub->($this, scalar @_), 1 while @_; wantarray ? @result : \@result; } sub entity_breakpoint { &YATT::breakpoint(); } sub entity_concat { my $this = shift; join '', @_; } sub entity_join { my ($this, $sep) = splice @_, 0, 2; join $sep, grep {defined $_ && $_ ne ''} @_; } sub entity_format { my ($this, $format) = (shift, shift); sprintf $format, @_; } sub entity_is_debug_allowed { my ($this) = @_; unless (defined $CGI->{'.allow_debug'}) { $CGI->{'.allow_debug'} = $this->is_debug_allowed($CGI->remote_addr); } $CGI->{'.allow_debug'}; } sub is_debug_allowed { my ($this, $ip) = @_; my $pat = $$CONFIG{cf_debug_allowed_ip}; unless (defined $pat) { $pat = $$CONFIG{cf_debug_allowed_ip} = $this->load_htdebug; } elsif (ref $pat) { $pat = $$CONFIG{cf_debug_allowed_ip} = qr{@{[join "|", map {"^$_"} @$pat]}}; } elsif ($pat eq '') { return 0 } $ip =~ $pat; } sub load_htdebug { my ($this) = @_; my $dir = untaint_any(dirname($CONFIG->{cf_driver})); my $fn = "$dir/.htdebug"; return '' unless -r $fn; open my $fh, '<', $fn or die "Can't open $fn: $!"; local $_; my @pat; while (<$fh>) { chomp; s/\#.*//; next unless /\S/; push @pat, '^'.quotemeta($_); } qr{@{[join "|", @pat]}}; } sub entity_CGI { $CGI } sub entity_remote_addr { $CGI->remote_addr } #======================================== sub entity_param { my ($this) = shift; $CGI->param(@_); } # # For &HTML(); shortcut. # To use this, special_entities should have 'HTML'. # sub entity_HTML { my $this = shift; \ join "", grep {defined $_} @_; } sub entity_dump { shift; YATT::Util::terse_dump(@_); } #======================================== sub canonicalize_html_filename { my $pack = shift; $_[0] .= "index" if $_[0] =~ m{/$}; my $copy = shift; $copy =~ s{\.(y?html?|yatt?)$}{}; $copy; } sub widget_path_in { my ($pack, $rootdir, $file) = @_; unless (index($file, $rootdir) == 0) { $pack->plain_error (undef, "Requested file $file is not in rootdir $rootdir"); } my @elempath = split '/', $pack->canonicalize_html_filename (substr($file, length($rootdir))); shift @elempath if defined $elempath[0] and $elempath[0] eq ''; @elempath; } sub YATT::Toplevel::CGI::Config::translator_param { my Config $config = shift; # print "translator_param: ", terse_dump($config), "\n"; map($_ ? (ref $_ eq 'ARRAY' ? @$_ : %$_) : () , $config->{cf_translator_param}) } #======================================== package YATT::Toplevel::CGI::Batch; use YATT::Inc; use base qw(YATT::Toplevel::CGI); use YATT::Util qw(catch); sub run_files { my $pack = shift; my ($method, $flag, @opts) = $pack->parse_opts(\@_); my $config = $pack->new_config(\@opts); $pack->parse_params(\@_, \ my %param); foreach my $file (@_) { print "=== $file ===\n" if $ENV{VERBOSE}; if (catch { $pack->run_template($pack->rel2abs($file), \%param, $config); } \ my $error) { print STDERR $error; } print "\n" if $ENV{VERBOSE}; } } sub dispatch_action { my ($top, $root, $action, $pkg, @param) = @_; &YATT::break_handler; $action->($pkg, @param); $top->bye; } 1;