My::Hash::* reimplementation for separate feature add-on packages (cleanup).
[MyWeb.git] / Web.pm
diff --git a/Web.pm b/Web.pm
index 599005f..c368a4b 100644 (file)
--- a/Web.pm
+++ b/Web.pm
@@ -40,7 +40,7 @@ our @EXPORT=qw(
                );
 our @ISA=qw(Tie::Handle Exporter);
 
-my %packages_used_hash;
+my %packages_used_hash;        # $packages_used_hash{$W->{"__PACKAGE__"}}{"_done"}=1;
 my %packages_used_array;
 
 BEGIN
@@ -114,6 +114,7 @@ use Apache2::Const qw(HTTP_MOVED_TEMPORARILY OK);
 use URI;
 use URI::QueryParam;
 use Cwd;
+require HTTP::Date;
 
 
 #our $W;
@@ -130,19 +131,21 @@ sub cleanup($)
 {
 my($apache_request)=@_;
 
+       $packages_used_hash{$W->{"__PACKAGE__"}}{"_done"}=1;
        # Sanity protection.
        $W=undef();
        return OK;
 }
 
-sub W_check(;$)
+sub request_check(;$)
 {
 my($self)=@_;
 
        # Use &eval to prevent: Global $r object is not available. Set:\n\tPerlOptions +GlobalRequest\nin ...
        # CGI requires valid "r": check it beforehand here.
        confess "Calling sensitive dynamic code from a static code" if !eval { Apache2::RequestUtil->request(); };
-       confess "Calling sensitive dynamic code without My::Web::init" if !$W->{"__PACKAGE__"};
+       # Do not: confess "Calling sensitive dynamic code without My::Web::init" if !$W->{"__PACKAGE__"};
+       # as it is valid at least while preparing arguments to call: &project::Lib::init
 }
 
 sub init ($%)
@@ -155,27 +158,33 @@ my($class,%args)=@_;
        # We cannot do it in BEGIN { } block
        # as it would not be tracked for each of the toplevel users later.
        Wuse 'WebConfig';
-       Wrequire 'My::Hash::Sub';
+       Wrequire 'My::Hash';
 
-       $W={};
-       tie %$W,"My::Hash::Sub";
-       %$W=(%WebConfig,%args); # override %WebConfig settings
-       $W->{"__PACKAGE__"}||=caller();
+       $W=My::Hash->new({
+               "__PACKAGE__"=>scalar(caller()),
+               %WebConfig,
+               %args,  # override %WebConfig settings
+               },"My::Hash::Sub","My::Hash::Push");
 
        # {"__PACKAGE__"} is mandatory for mod_perl-2.0;
        # $Apache2::Registry::curstash is no longer supported.
        do { cluck "No $_" if !$W->{$_}; } for "__PACKAGE__";
 
-       do { $W->{$_}=0  if !defined $W->{$_}; } for ("detect_ent");
-       do { $W->{$_}=0  if !defined $W->{$_}; } for ("detect_js");
-       do { $W->{$_}=1  if !defined $W->{$_}; } for ("have_css");      # AFAIK it does not hurt anyone.
-       do { $W->{$_}=1  if !defined $W->{$_}; } for ("footer");
-       do { $W->{$_}=1  if !defined $W->{$_}; } for ("footer_delimit");
-       do { $W->{$_}=1  if !defined $W->{$_}; } for ("footer_ids");
-       do { $W->{$_}=1  if !defined $W->{$_}; } for ("indexme");
-       do { $W->{$_}="" if !defined $W->{$_}; } for ("head");
-       do { $W->{$_}="" if !defined $W->{$_}; } for ("body_attr");
-       do { $W->{$_}="en-US" if !defined $W->{$_}; } for ("language");
+       # See: &escapeHTML
+       do { cluck "charset==$_, expecting ISO-8859-1" if $_ ne "ISO-8859-1"; } for CGI::charset();
+       CGI::charset("utf-8");
+
+       do { $W->{$_}=0  if !defined $W->{$_}; } for "detect_ent";
+       do { $W->{$_}=0  if !defined $W->{$_}; } for "detect_js";
+       do { $W->{$_}=1  if !defined $W->{$_}; } for "have_css";        # AFAIK it does not hurt anyone.
+       do { $W->{$_}=0  if !defined $W->{$_}; } for "css_inherit";
+       do { $W->{$_}=1  if !defined $W->{$_}; } for "footer";
+       do { $W->{$_}=1  if !defined $W->{$_}; } for "footer_delimit";
+       do { $W->{$_}=1  if !defined $W->{$_}; } for "footer_ids";
+       do { $W->{$_}=1  if !defined $W->{$_}; } for "indexme";
+       do { $W->{$_}="" if !defined $W->{$_}; } for "head";
+       do { $W->{$_}="" if !defined $W->{$_}; } for "body_attr";
+       do { $W->{$_}="en-US" if !defined $W->{$_}; } for "language";
 
        my $footer_any=0;
        for (qw(footer_ids)) {
@@ -234,7 +243,7 @@ my($class,%args)=@_;
 
        $W->{"have_js"}=($W->{"args"}{"have_js"} ? 1 : 0);
        if ($W->{"detect_js"} && !$W->{"have_js"}) {
-               $W->{"head"}.='<script type="text/javascript" src="'.path_web('/have_js.pm').'"></script>'."\n";
+               $W->{"head"}.='<script type="text/javascript" src="'.uri_escaped(path_web('/have_js.pm')).'"></script>'."\n";
                }
 
        do { _args_check(%$_) if $_; } for ($W->{"args_check"});
@@ -262,8 +271,16 @@ sub escapeHTML($)
 my($text)=@_;
 
        # Prevent &CGI::escapeHTML breaking utf-8 strings like: \xC4\x9B eq \x{11B}
-       do { cluck "charset==$_" if $_ && $_ ne "utf-8"; } for CGI::charset();
+       # Prevent case if we run under mod_perl but still just initializing:
+       request_check() if $ENV{"MOD_PERL"};
+       # Generally we are initialized from &init but we may be used without it without mod_perl
+       # and in such case check the change on all non-first invocations.
+       our $init;
+       if (!$ENV{"MOD_PERL"} && $init++) {
+               do { cluck "charset==$_" if $_ ne "utf-8"; } for CGI::charset();
+               }
        CGI::charset("utf-8");
+
        return CGI::escapeHTML($text);
 }
 
@@ -309,6 +326,7 @@ sub dir_top_abs_disk()
 
 sub unparsed_uri()
 {
+       request_check();
        if (!$W->{"unparsed_uri"}) {
                # Do not: $W->{"r"}
                # as we may be called before &init from: &My::Project::init
@@ -360,6 +378,15 @@ my($in,%args)=@_;
        return $uri->rel(unparsed_uri());
 }
 
+my %path_abs_disk_for_package; # $path_abs_disk_for_package{$W->{"__PACKAGE__"}}{$path_abs_disk}=1;
+
+sub path_abs_disk_register($)
+{
+my($path_abs_disk)=@_;
+
+       $path_abs_disk_for_package{$W->{"__PACKAGE__"}}{$path_abs_disk}=1;
+}
+
 # $args{"uri_as_in"}=1 to permit passing URI objects as: $in
 sub path_abs_disk($%)
 {
@@ -370,7 +397,9 @@ my($in,%args)=@_;
        cluck if !uri_is_local($uri);
        my $path=$uri->path();
        cluck "URI compatibility: ->path() not w/leading slash of URI \"$uri\"; path: $path" if $path!~m{^/};
-       return dir_top_abs_disk().$path;
+       my $r=dir_top_abs_disk().$path;
+       path_abs_disk_register $r if !defined $args{"register"} || $args{"register"};
+       return $r;
 }
 
 sub fatal (;$);
@@ -534,9 +563,9 @@ sub uri_escaped($)
 my($uri)=@_;
 
        cluck if !ref $uri;
-       W_check();
        my $urient=escapeHTML($uri);
        return $uri    if $uri eq $urient;
+       request_check();
        return $urient if uri_is_local $uri;
        return $uri    if defined $W->{"have_ent"} && !$W->{"have_ent"};        # non-ent client
        return $urient if $W->{"have_ent"};     # ent client
@@ -549,7 +578,7 @@ sub a_href($;$%)
 {
 my($in,$contents,%args)=@_;
 
-       W_check();
+       request_check();
        do { $$_=1 if !defined $$_; } for (\$args{"size"});
        if (!defined $contents) {
                $contents=$in;
@@ -585,7 +614,7 @@ my($self,$sub,@sub_args)=@_;
 
 sub input_hidden_persistents()
 {
-       W_check();
+       request_check();
        return join("",map({
                my $key=$_;
                my $val=$W->{"args"}{$key};
@@ -709,7 +738,7 @@ my($in,%args)=@_;
 
        cluck if !uri_is_local $in;
        my $uri=in_to_uri_abs $in;
-       my $path_abs_disk=path_abs_disk $uri,%args,"uri_as_in"=>1;
+       my $path_abs_disk=path_abs_disk $uri,%args,"uri_as_in"=>1,"register"=>0;
 
        # Known image extension?
        return path_web($uri,%args,"uri_as_in"=>1),$path_abs_disk if $uri->path()=~m#$img_variants_re#o;
@@ -717,6 +746,7 @@ my($in,%args)=@_;
        my @nego_variants;
        for my $var (@img_variants) {
                my $path_abs_disk_variant=$path_abs_disk.".".$var->{"id"};
+               path_abs_disk_register($path_abs_disk_variant);
                __PACKAGE__->make_file($path_abs_disk_variant);
                push @nego_variants,negotiate_variant(
                                %$var,
@@ -734,7 +764,7 @@ sub img ($$%)
 {
 my($in,$alt,%args)=@_;
 
-       W_check();
+       request_check();
        my($path_web,$path_abs_disk)=_img_src($in,%args);
        my($width,$height)=Image::Size::imgsize($path_abs_disk);
        $alt=~s/<[^>]*>//g;
@@ -795,10 +825,45 @@ sub no_cache($)
 {
 my($self)=@_;
 
-       header("Expires"=>"Mon, 26 Jul 1997 05:00:00 GMT");     # date in the past
-       header("Last-Modified"=>strftime("%a, %d %b %Y %H:%M:%S GMT",gmtime()));        # always modified
-       header("Cache-Control"=>"no-cache, must-revalidate");   # HTTP/1.1
+       header("Expires"=>HTTP::Date::time2str(1000000000));    # date in the past
+       header("Last-Modified"=>HTTP::Date::time2str());        # always modified
+       header("Cache-Control"=>join(", ",
+                       "no-cache",
+                       "no-store",
+                       "must-revalidate",
+                       "max-age=0",
+                       "pre-check=0",  # MSIE
+                       "post-check=0", # MSIE
+                       ));     # HTTP/1.1
        header("Pragma"=>"no-cache");   # HTTP/1.0
+       header("Vary"=>"*");    # content may ba based on unpredictable sources
+}
+
+sub last_modified($)
+{
+my($self)=@_;
+
+       return if !$packages_used_hash{$W->{"__PACKAGE__"}}{"_done"};
+       our %path_abs_disk_registered;
+       if (!$path_abs_disk_registered{$W->{"__PACKAGE__"}}++) {
+               for my $package_orig (@{$packages_used_array{$W->{"__PACKAGE__"}}}) {
+                       local $_=$package_orig.".pm";
+                       s{::}{/}g;
+                       path_abs_disk "/$_","register"=>1;
+                       }
+               }
+       my $mtime_newest;
+       for my $path_abs_disk (keys(%{$path_abs_disk_for_package{$W->{"__PACKAGE__"}}})) {
+###print STDERR "CHECK:$path_abs_disk\n";
+               my $mtime=(stat $path_abs_disk)[9];
+               do { cluck "No mtime for: $path_abs_disk"; next; } if !$mtime;
+               $mtime_newest=$mtime if !$mtime_newest || $mtime_newest<$mtime;
+               }
+       cluck "No mtime_newest found for the current W __PACKAGE__: ".$W->{"__PACKAGE__"}
+                       if !$mtime_newest;
+       # "Vary" header is REQUIRED in this case:
+       header("Last-Modified"=>HTTP::Date::time2str($mtime_newest));
+       return 1;
 }
 
 sub heading()
@@ -810,6 +875,7 @@ my($class)=@_;
        header("Content-Style-Type"=>"text/css");
        header("Content-Script-Type"=>"text/javascript");
        do { header("Content-Language"=>$_) if $_; } for $W->{"language"};
+       $class->last_modified() if !$W->{"no_cache"};
        $class->no_cache() if $W->{"no_cache"};
 
        while (my($key,$val)=each(%{$W->{"headers"}})) {
@@ -871,6 +937,11 @@ my($class)=@_;
 <link rel="stylesheet" type="text/css" href="@{[ uri_escaped(path_web $css) ]}" />
 HERE
                        }
+               if ($W->{"css_inherit"}) {
+                       Wprint <<"HERE";
+<script type="text/javascript" src="@{[ uri_escaped(path_web('/My/css_inherit.js')) ]}" />
+HERE
+                       }
                }
        Wprint '<meta name="robots" content="'.($W->{"indexme"} ? "" : "no" ).'index,follow" />'."\n";
        Wprint $W->{"head"};