package Apache::PageKit::View; # $Id: View.pm,v 1.110 2004/05/03 13:48:29 borisz Exp $ # we want to extend this module to use different templating packages - # Template::ToolKit and HTML::Template use integer; use strict; use File::Find (); use File::Path (); use HTML::Clean (); use HTML::Template::XPath (); use Storable (); # how loading, filter and caching works on the templates. # 1. templates are pre-filtered, to convert MODEL_*,VIEW_* and PKIT_* tags # corresponding TMPL_ tags, and to run HTML::Clean # 2. template objects are loaded and stored on disk or in memory, in # a hash containing fields from the following set: # * exclude_params - array ref of lists of params to be excluded from tags # * html_template - HTML::Template object # * template_toolkit - Template-Tookit object (NOT USED NOW) # * filename - filename of template source # * include_mtimes - a hash ref with file names as keys and mtimes as values # (contains all of the files included by the tags # * component_ids - an array ref containing an array ref of component_ids and a hash ref # with the parameters for the compoent, that may have # code associated with them # * has_form - 1 if contains
tag, 0 otherwise. used to # determine whether to apply HTML::FillInForm module # the objects themselves are keyed by page_id, pkit_view and lang # 3. methods that are called externally # * new($view_dir, $content_dir, $cache_dir, $default_lang, $reload, $html_clean_level, $can_edit, [ $associated_objects ], [ $fillinform_objects], $input_param_object, $output_param_object) (args passed as hash reference) # * fill_in_view($page_id, $pkit_view, $lang) # * open_view($page_id, $pkit_view, $lang) # * param # * preparse_templates($view_root,$html_clean_level,$view_cache_dir); # * template_file_exists($page_id, $pkit_view) # 4. methods that are called interally # * _fetch_from_file_cache($page_id, $pkit_view, $lang); # * _prepare_content($template_text_ref, $page_id) # * _fill_in_content($template_text_ref, $page_id) # * _fill_in_content_loop(...) # * _load_page($page_id, $pkit_view, [$template_file]) # these global vars are initialised and then they are readonly! # this is done here mainly for speed. #use vars qw /%replace_start_tags %replace_end_tags $key_value_pattern/; our (%replace_start_tags, %replace_end_tags, $key_value_pattern); %replace_start_tags = ( MESSAGES => '', IS_ERROR => '', NOT_ERROR => '', HAVE_MESSAGES => '', HAVE_NOT_MESSAGES => '', HOSTNAME => '', MESSAGE => '', ERRORSTR => '', REALURL => '', ); %replace_end_tags = ( VIEW => '', IS_ERROR => '', NOT_ERROR => '', HAVE_MESSAGES => '', HAVE_NOT_MESSAGES => '', MESSAGES => '' ); # --------------------- $1 -------------------------- # $2 $3 $4 $5 $key_value_pattern = qr!(\s+(\w+)(?:\s*=\s*(?:"([^"]*)"|\'([^\']*)\'|(\w+)))?)!; #" $Apache::PageKit::View::cache_component = {}; # precompiled re to parse PKIT_COMMENT tags in a ballanced way. my %re_helper; %re_helper = ( std_parser => { pkit_comment_re => qr% \ (?: (?>[^\<]+) | \<(?!PKIT_COMMENT\>)(?!\/PKIT_COMMENT\>) #/ | (??{$re_helper{std_parser}->{pkit_comment_re}}) )* \<\/PKIT_COMMENT\> #/ %isx }, relaxed_parser => { pkit_comment_re => qr% \<(!--)?\s*PKIT_COMMENT\s*(?(1)--)\> (?: (?>[^\<]+) | \< (?!(!--)?\s*PKIT_COMMENT\s*(?(2)--)\>) (?!(!--)?\s*\/PKIT_COMMENT\s*(?(3)--)\>) #/ | (??{$re_helper{relaxed_parser}->{pkit_comment_re}}) )* \<(!--)?\/PKIT_COMMENT\s*(?(4)--)\> #/ %isx } ); sub new { my $class = shift; my $self = { @_ }; bless $self, $class; # $self->_init(@_); return $self; } sub fill_in_view { my ($view) = @_; # load record containing HTML::Template object my $record = $view->{record}; my $tmpl = $record->{html_template}; # Fill in (compiled) tags my $exclude_params_set = $record->{exclude_params_set}; if($exclude_params_set && @$exclude_params_set){ my $input_param_object = $view->{input_param_object}; my $orig_uri = $input_param_object->notes('orig_uri'); foreach my $exclude_params (@$exclude_params_set){ my @exclude_params = split(" ",$exclude_params); my $query_string = Apache::PageKit::params_as_string($input_param_object, \@exclude_params); #remove empty parameters as arised from http://ka.zyx.de/galerie?show=abc& or $query_string =~ s![?&]$!!; if($query_string){ $tmpl->param("pkit_selfurl$exclude_params", ($orig_uri . '?' . $query_string) . '&'); } else { $tmpl->param("pkit_selfurl$exclude_params", $orig_uri . '?'); } } } # fill in data from associated objects (for example from the Apache request # object if $apr is set foreach my $object (@{$view->{associated_objects}}){ foreach my $key ($object->param){ # note that we only fill in MODEL_VARs, to avoid errors when setting # loops in HTML::Template my $type = $tmpl->query(name => $key); if ( $type && $type eq 'VAR' ) { $view->{pkit_pk}->{browser_cache} = 'no'; # we need a separate variable for value to force scalar context # for multivalued params http://www.xx.yy/a?foo=12&foo=13 my $value = $object->param($key); $tmpl->param($key, $value); } } } # finally, we use the $output_param_object object to fill in template # get params from $view object # note that in this case we allow for MODEL_LOOPs as well as MODEL_VARs my $output_param_object = $view->{output_param_object}; foreach my $key ($output_param_object->param){ my $value = $output_param_object->param($key); $view->{pkit_pk}->{browser_cache} = 'no'; $tmpl->param($key, $value); } my $output = $tmpl->output; if($record->{has_form}){ # if fillinform_objects is set, then we use that to fill in any HTML # forms in the template. my $fif; if(@{$view->{fillinform_objects}}){ $view->{pkit_pk}->{browser_cache} = 'no'; $fif = HTML::FillInForm->new(); $output = $fif->fill(scalarref => \$output, fobject => $view->{fillinform_objects}, ignore_fields => $view->{ignore_fillinform_fields} ); } } if($view->{can_edit} eq 'yes'){ $view->{pkit_pk}->{browser_cache} = 'no'; Apache::PageKit::Edit::add_edit_links($view, $record, \$output); } return \$output; } # gets static gzipped file, creating it if necessary sub get_static_gzip { my ($view, $filename) = @_; my ($gzip_mtime, $gzipped_content); (my $relative_filename = $filename) =~ s!^$view->{view_dir}/!!; my $gzipped_filename = "$view->{cache_dir}/$relative_filename.gz"; # is the cache entry valid or changed on disc? if(-f "$gzipped_filename"){ open FH, "<$gzipped_filename" or return undef; binmode FH; # read mtime from first line chomp($gzip_mtime = ); # read rest of gzipped content local $/ = undef; $gzipped_content = ; close FH; if($view->{reload} ne 'no'){ # is the cache entry valid or changed on disc? my $mtime = ( stat($filename) )[9]; if($mtime != $gzip_mtime){ $gzipped_content = $view->_create_static_zip($filename, $gzipped_filename); } } } else { $gzipped_content = $view->_create_static_zip($filename, $gzipped_filename); } return $gzipped_content; } # opens template, each sub open_view { my ($view, $page_id, $pkit_view, $lang) = @_; return if exists $view->{already_loaded}->{$page_id}; my $record = $view->_fetch_from_file_cache($page_id, $pkit_view, $lang); unless($record){ # template not cached, load now $view->_load_page($page_id, $pkit_view); $record = $view->_fetch_from_file_cache($page_id, $pkit_view, $lang); die "Error loading record for page_id $page_id and view $pkit_view" unless $record; } if($view->{reload} ne 'no'){ # check for updated files on disk unless($view->_is_record_uptodate($record, $pkit_view, $page_id)){ # one of the included files changed on disk, reload $view->_load_page($page_id, $pkit_view); $record = $view->_fetch_from_file_cache($page_id, $pkit_view, $lang); } } $view->{record} = $record; $view->{already_loaded}->{$page_id} = 1; } sub preparse_templates { my ($view) = @_; my $view_dir = $view->{view_dir}; my $load_template_sub = sub { return unless /\.tmpl$/; my $template_file = "$File::Find::dir/$_"; my ($pkit_view, $page_id) = ($template_file =~ m!$view_dir/([^/]*)/Page/(.*?)\.(tmpl|tt)$!); return unless $page_id; $view->open_view($page_id, $pkit_view); }; File::Find::find({wanted => $load_template_sub}, # follow => 1}, $view_dir); } sub template_file_exists { my ($view, $page_id, $pkit_view) = @_; return 1 if $view->_find_template($pkit_view,$page_id); } # private methods # creates gzipped file sub _create_static_zip { my ($view, $filename, $gzipped_filename) = @_; local $/ = undef; open FH, "<$filename" or return undef; binmode FH; my $content = ; close FH; $view->_html_clean(\$content); my $gzipped_content = Compress::Zlib::memGzip($content); (my $gzipped_dir = $gzipped_filename) =~ s!(/)?[^/]*?$!!; File::Path::mkpath("$gzipped_dir"); if ($gzipped_content) { my $mtime = (stat($filename))[9]; if ( open GZIP, ">$gzipped_filename" ) { binmode GZIP; print GZIP "$mtime\n"; print GZIP $gzipped_content; close GZIP; } else { warn "can not create gzip cache file $view->{cache_dir}/$gzipped_filename: $!"; } return $gzipped_content; } return undef; } sub _fetch_from_file_cache { my ($view, $page_id, $pkit_view, $lang) = @_; my ($extra_param, $param_hash) = ("", ""); # get a list of requested params in the *.xsl file if (my @xml_params = sort keys %{$Apache::PageKit::Content::PAGE_ID_XSL_PARAMS->{$page_id}}) { my $param_obj = $view->{input_param_object}; for my $xml_param (@xml_params){ my $value = $param_obj->param($xml_param) || ''; $extra_param .= "&$xml_param=" . $value; } $param_hash = Digest::MD5::md5_hex($extra_param); } my $cache_filename = "$view->{cache_dir}/$page_id.$pkit_view.$lang$param_hash"; if(-f "$cache_filename"){ # cache file exists for specified language return Storable::lock_retrieve($cache_filename); } else { $cache_filename = "$view->{cache_dir}/$page_id.$pkit_view.$view->{default_lang}$param_hash"; if(-f "$cache_filename"){ # cache file exists for default language return Storable::lock_retrieve($cache_filename); } else { return; } } } sub _find_template { my ($view, $pkit_view, $id) = @_; my $template_file = "$view->{view_dir}/$pkit_view/$id.tmpl"; if(-f "$template_file"){ return $template_file; } else { $template_file = "$view->{view_dir}/Default/$id.tmpl"; if(-f "$template_file"){ return $template_file; } else { return undef; } } } # clean up html, remove white spaces, etc sub _html_clean { my ($view, $html_code_ref) = @_; my $html_clean_level = $view->{html_clean_level}; return unless $html_clean_level > 0; my $h = new HTML::Clean($html_code_ref,$html_clean_level) || die("can not open HTML::Clean object: $!"); $h->strip; $$html_code_ref = ${$h->data()}; } sub _include_components { my ($view, $page_id, $html_code_ref, $pkit_view) = @_; if ( $view->{relaxed_parser} eq 'yes' ) { $$html_code_ref =~ s%<(!--)?\s*PKIT_COMPONENT($key_value_pattern+)\s*/?(?(1)--)?>(?:<(!--)?\s*/PKIT_COMPONENT\s*(?(1)--)>)?%get_component($page_id,$2,$view,$pkit_view)%eig; } else { $$html_code_ref =~ s%<\s*PKIT_COMPONENT($key_value_pattern+)\s*/?>(<\s*/PKIT_COMPONENT\s*>)?%&get_component($page_id,$1,$view,$pkit_view)%eig; } sub get_component { my ($page_id, $params, $view, $pkit_view) = @_; my %params = (); while($params =~ m!$key_value_pattern!ig) { $params{uc($2)} = $+; } my $component_id = delete $params{NAME} or die qq{component item "NAME=..." not found}; unless($component_id =~ s!^/!!){ # relative component, component relative to page_id (my $page_id_dir = $page_id) =~ s![^/]*$!!; $component_id = $page_id_dir . $component_id; while ($component_id =~ s![^/]*/\.\./!!) {}; } my $cid_key = join '', $component_id, sort %params; unless ( $view->{component_ids_hash}->{$cid_key}++ ) { push @{ $view->{component_ids} }, [ $component_id , \%params ]; } # check for recursive pkit_components if($view->{component_ids_hash}->{$cid_key} > 100){ die "Likely recursive PKIT_COMPONENTS for component_id $component_id and giving up."; } my $template_ref = $view->_load_component($page_id, $component_id, $pkit_view, \%params); return $$template_ref; } } sub _is_record_uptodate { my ($view, $record, $pkit_view, $page_id) = @_; # first check timestamps my $include_mtimes = $record->{include_mtimes}; while (my ($filename, $cache_mtime) = each %$include_mtimes){ # check if file still exists unless(-f "$filename"){ return 0; } # check if file is up to date my $file_mtime = (stat($filename))[9]; # print "hi $filename - $cache_mtime - $file_mtime
"; if($file_mtime != $cache_mtime){ return 0; } if($filename =~ m!^$view->{view_dir}/Default/! && $pkit_view ne 'Default'){ # check to see if any new files have been uploaded to the $pkit_view dir (my $check_filename = $filename) =~ s!^$view->{view_dir}/Default/!$view->{view_dir}/$pkit_view/!; if (-f "$check_filename"){ return 0; } } } # record up to date! return 1; } # here the usage of "component" also includes page sub _load_component { my ($view, $page_id, $component_id, $pkit_view, $component_params) = @_; my $template_file = $view->_find_template($pkit_view, $component_id); my $template_ref; unless($template_file){ # no template file exists, attempt to generate from XML and XSL files # currently only XML::LibXSLT is supported $template_ref = $view->{content}->generate_template($page_id, $component_id, $pkit_view, $view->{input_param_object}, $component_params); } else { open TEMPLATE, "<$template_file" or die "can not read $template_file"; binmode TEMPLATE; local($/) = undef; my $template =