package Apache2::ASP::ASPPage;
use strict;
use warnings 'all';
use Carp 'confess';
use base 'Apache2::ASP::HTTPHandler';
use vars __PACKAGE__->VARS;
use Apache2::ASP::ASPDOM::Node;
use Apache2::ASP::ASPDOM::Document;
use HTTP::Date 'time2iso';
use Scalar::Util 'weaken';
use Data::Dumper;
#==============================================================================
sub new
{
my ($class, %args) = @_;
$class->init_asp_objects( $class->context );
foreach(qw/ virtual_path /)
{
confess "Required param '$_' was not provided"
unless defined($args{$_});
}# end foreach()
# Just so we don't end up with an injection of some kind one day:
delete($args{file_contents});
my $s = bless \%args, $class;
$s->{physical_path} = $s->context->server->MapPath( $s->virtual_path );
confess "File not found: '@{[ $s->physical_path ]}'"
unless -f $s->physical_path;
my $pm_folder = $s->context->config->web->page_cache_root . '/' . $s->context->config->web->application_name;
# Build out the folder structure to the Page Cache:
my @parts = grep { length($_) } split /\//, $pm_folder;
my $dir = '';
foreach( @parts )
{
$dir .= "/$_";
mkdir($dir) unless -d $dir;
}# end foreach()
my $pkg = $s->virtual_path;
$pkg =~ s/^\///;
$pkg =~ s/[^a-z0-9_]/_/ig;
$s->{package_name} = $s->context->config->web->application_name . '::' . $pkg;
$s->{pm_path} = $pm_folder . '/' . $pkg . '.pm';
my $pm_inc = $s->context->config->web->application_name . '/' . $pkg . '.pm';
# Determine age of ASP and PM:
my $asp_age = (stat($s->context->server->MapPath($s->virtual_path)))[9];
no strict 'refs';
my $timestamp = ${$s->package_name . "::TIMESTAMP"} || 0;
my $pm_age = (stat($s->pm_path))[9] || 0;
if( ( ! $pm_age ) || ( $asp_age > $pm_age ) )
{
#warn "(Re)compiling $pkg";
delete( $INC{$pm_inc} );
$s->_init_source_code();
$s->parse;
require $pm_inc;
}
elsif( $asp_age > $timestamp )
{
#warn "(Re)loading $pkg";
delete( $INC{$pm_inc} );
require $pm_inc;
}# end if()
return $s;
}# end new()
#==============================================================================
# Public read-only properties:
sub physical_path { $_[0]->{physical_path} }
sub virtual_path { $_[0]->{virtual_path} }
sub context { $Apache2::ASP::HTTPContext::ClassName->current }
sub package_name { $_[0]->{package_name} }
sub pm_path { $_[0]->{pm_path} }
sub directives { my $s = shift; @_ ? $s->{directives} = shift : $s->{directives} || { } }
sub source_code { $_[0]->{source_code} }
sub file_contents { $_[0]->{file_contents} }
sub is_masterpage { $_[0]->{is_masterpage} || 0 > 0 }
sub masterpage { $_[0]->{masterpage} or return; $_[0]->{masterpage} }
sub placeholders { my $s = shift; @_ ? $s->{placeholders} = shift : $s->{placeholders} || { } }
sub placeholder_contents { $_[0]->{placeholder_contents} || { } }
#==============================================================================
# Public read-write properties:
sub childpage
{
my $s = shift;
@_ ? $s->{childpage} = shift : $s->{childpage};
}# end childpage()
#==============================================================================
sub _init_source_code
{
my ($s) = @_;
return $s->{file_contents} if defined($s->{file_contents});
open my $ifh, '<', $s->physical_path
or confess "Cannot open '@{[ $s->physical_path ]}' for reading: $!";
local $/;
my $data = <$ifh>;
$s->{source_code} = \"$data";
$s->{file_contents} = \$data;
}# end _init_source_code()
#==============================================================================
sub parse
{
my ($s) = @_;
$s->{directives} = $s->_get_directives( );
if( exists($s->directives->{Page}->{UseMasterPage}) )
{
my $master = Apache2::ASP::ASPPage->new(
virtual_path => $s->directives->{Page}->{UseMasterPage},
childpage => $s,
);
$s->{masterpage} = $master->package_name->new(
virtual_path => $s->directives->{Page}->{UseMasterPage},
childpage => $s,
);
$s->{masterpage} = $s->masterpage->_initialize_page;
}# end if()
if( exists($s->directives->{MasterPage}) )
{
$s->{is_masterpage} = 1;
}# end if()
# Do the <%# %> tags:
$s->_eval_compile_tags;
# Setup the scriptlet <% %> tags:
$s->_parse_scriptlet_tags;
# Do the tags:
$s->_parse_include_tags;
# XXX: Make DOM
$s->_build_dom;
# Write the code to disk:
$s->_assemble_code;
}# end parse()
#==============================================================================
sub _read_cache
{
my ($s) = @_;
return unless $s->context->application->can('db_Main');
if( my $cache_args = $s->{directives}->{OutputCache} )
{
use Digest::MD5 'md5_hex';
# Get the key:
my $cache_id = $s->_cache_key;
my $range_start = time2iso( time() - $cache_args->{Duration} );
my $sth = $s->context->application->db_Main->prepare(<<"SQL");
SELECT pagecache_data
FROM asp_pagecache
WHERE pagecache_id = ?
AND created_on BETWEEN ? AND ?
SQL
$sth->execute( $cache_id, $range_start, time2iso() );
if( my ($cache_data) = $sth->fetchrow )
{
# We got cache:
return $cache_data;
}# end if()
}# end if()
}# end _read_cache()
#==============================================================================
sub _cache_key
{
my $s = shift;
my $cache_args = $s->directives->{OutputCache};
# Make the key:
my %key = ( );
if( my $field = $cache_args->{VaryBySession} )
{
$key{"Session:$field"} = $s->context->session->{$field};
}# end if()
if( my $field = $cache_args->{VaryByParam} )
{
$key{"Param:$field"} = $s->context->request->Form->{$field};
}# end if()
no warnings 'uninitialized';
my $key = md5_hex( $s->virtual_path . ':' .
join ':', map { "$_:$key{$_}" } sort keys(%key)
);
return $key;
}# end _cache_key()
#==============================================================================
sub _write_cache
{
my ($s, $data) = @_;
return unless $s->context->application->can('db_Main');
my $key = $s->_cache_key;
#warn "Storing cache...";
my $sth = $s->context->application->db_Main->prepare(<<"SQL");
DELETE FROM asp_pagecache WHERE pagecache_id = ?;
SQL
$sth->execute( $key );
$sth->finish();
$sth = $s->context->application->db_Main->prepare(<<"SQL");
INSERT INTO asp_pagecache ( pagecache_id, created_on, pagecache_data )
VALUES ( ?, ?, ? )
SQL
$sth->execute(
$key, time2iso(), $$data
);
$sth->finish;
}# end _write_cache()
#==============================================================================
sub _build_dom
{
my ($s) = @_;
my $ref = $s->source_code;
my %ids = ( );
my $doc = Apache2::ASP::ASPDOM::Document->new();
# Do ... tags:
my @lines = split /\r?\n/, ${ $s->file_contents };
while(
my ( $chunk, $tagName, $prefix, $tag, $attrs, $contents ) =
$$ref =~ m{
(<(([a-z_]+)\:([a-z0-9_:]+))\s*(.*?)\>(.*?)\<\/\2\>)
}ixs
)
{
# Parse the attributes:
my $attrs = $s->_parse_tag_attrs( $attrs );
local $_ = $tagName;
if( m/^asp:ContentPlaceHolder$/i )
{
confess "Only MasterPages can contain $tagName elements"
unless $s->is_masterpage;
$s->{placeholders}->{ $attrs->{id} } = $contents;
# Remove the chunk of code:
my $subname = "\$__self->" . $attrs->{id} . "(\$__context, \$__args);";
$$ref =~ s/\Q$chunk\E/~); $subname \$Response->Write(q~/;
}
elsif( m/^asp:Content$/i )
{
confess "$tagName found but no UseMasterPage defined"
unless $s->masterpage;
confess $s->masterpage->virtual_path . " does not define an asp:ContentPlaceHolder '" . $attrs->{PlaceHolderID} . "'"
unless exists( $s->masterpage->{placeholders}->{ $attrs->{PlaceHolderID} } )
or $s->masterpage->can( $attrs->{PlaceHolderID} );
while( my ( $chunk, $tagName, $prefix, $tag, $attrs, $contents2 ) =
$contents =~ m{
(<((asp)\:(ContentPlaceHolder))\s*(.*?)\>(.*?)\<\/\2\>)
}ixs
)
{
# We have a nested master page:
# Parse the attributes:
my $attrs = $s->_parse_tag_attrs( $attrs );
$s->{placeholders}->{ $attrs->{id} } = $contents2;
# Remove the chunk of code:
my $subname = "\$__self->" . $attrs->{id} . "(\$__context, \$__args);";
$contents =~ s/\Q$chunk\E/~); $subname \$Response->Write(q~/;
}# end while()
# Find the line on which this tag occurs:
my $line = 0;
$line++ until $lines[$line] =~ m/\<$tagName\s+/s;
$lines[$line] = '';
# Remove the chunk of code:
my $fixed_contents = '$Response->Write(q~' . $contents . '~);';
my $code_chunk = <<"CODE";
sub @{[ $attrs->{PlaceHolderID} ]} {
my (\$__self, \$__context, \$__args) = \@_;
\$__self->init_asp_objects( \$__context )
unless \$__self->{__did_init}++;
#line @{[ $line + 3 ]}
$fixed_contents
}
CODE
$s->{placeholder_contents}->{ $attrs->{PlaceHolderID} } = $code_chunk;
$$ref =~ s/\Q$chunk\E//;
}
else
{
# Unhandled tag:
# Find the line on which this tag occurs:
my $line = 0;
$line++ until $lines[$line] =~ m/\<$tagName\s+/s;
confess "Unhandled tag '$tagName' in '@{[ $s->virtual_path ]}' line @{[ $line + 1 ]}\n";
}# end if()
}# end while()
# Do tags:
while(
my ( $chunk, $tagName, $prefix, $tag, $attrs ) =
$$ref =~ m{
(<(([a-z_]+)\:([a-z0-9_:]+))\s*(.*?)\/>)
}ix
)
{
# Parse the attributes:
my $attrs = $s->_parse_tag_attrs( $attrs );
local $_ = $tagName;
if( m/^some:tagName$/ )
{
# It's a some:tagName - handle it:
}
else
{
# Unhandled tag:
# Find the line on which this tag occurs:
my $line = 0;
$line++ until $lines[$line] =~ m/\<$tagName\s+/s;
confess "Unhandled tag '$tagName' in '@{[ $s->virtual_path ]}' line @{[ $line + 1 ]}\n";
}# end if()
# Remove the chunk of code
$$ref =~ s/\Q$chunk\E//;
}# end while()
}# end _build_dom()
#==============================================================================
sub _parse_include_tags
{
my ($s) = @_;
my $ref = $s->source_code;
$$ref =~ s{
\<\!\-\-\s*\#include\s+virtual\="(.*?)"\s*\-\-\>
}{~); \$Response->Include(\$Server->MapPath("$1")); \$Response->Write(q~}xsg;
}# end _parse_include_tags()
#==============================================================================
sub _parse_scriptlet_tags
{
my ($s) = @_;
my $ref = $s->source_code;
# Parse <% %> items:
$$ref =~ s{
<%\=(.*?)%>
}{
'~);$Response->Write(' . $1 . ');$Response->Write(q~'
}xgse;
$$ref =~ s{
<%\s*([^\@\#\=]?.*?)%>
}{
my $txt = $1; '~);' . $txt . ';$Response->Write(q~'
}gxse;
$$ref =~ s/(\$Response\->End)/return $1/gs;
$$ref = ';$Response->Write(q~' . $$ref . '~);';
# Now do the final ~ substitution:
$$ref =~ s{(\(q~)(.*?)(~\);)}{
my $pre = $1;
my $post = $3;
(my $txt = $2) =~ s/~/\\~/g;
"$pre$txt$post"
}xsge;
}# end _parse_scriptlet_tags()
#==============================================================================
sub _eval_compile_tags
{
my ($s) = @_;
my $ref = $s->source_code;
no warnings 'uninitialized';
while( $$ref =~ m/(<%\#\s*(.*?)\s*%>)/ )
{
my $tag = $1;
my $var = $2;
$$ref =~ s/\Q$tag\E/$var/ee;
}# end while()
}# end _eval_compile_tags()
#==============================================================================
sub _assemble_code
{
my ($s) = @_;
local $s->{childpage} = undef;
my $copy = bless {%$s}, ref($s);
unless( ref($copy) eq $copy->package_name )
{
$copy = bless { %$copy }, $copy->package_name;
}# end unless()
local $copy->{masterpage} = ref($copy->{masterpage});
local $copy->{source_code} = \'';
local $copy->{file_contents} = \'';
my $dump = Dumper( $copy );
$dump =~ s/^\$VAR1\s+\=//;
my $virtual_path = $s->masterpage ? $s->masterpage->virtual_path : '';
# Preamble:
my $code = <<"CODE";
package @{[ $s->package_name ]};
use strict;
use warnings 'all';
no warnings 'redefine';
our \$TIMESTAMP = @{[ time() ]};
sub _initialize_page {
eval { \$_[0]->SUPER::_initialize_page };
\$_[0]->init_asp_objects( \$_[0]->context );
\$_[0] = $dump;
\$_[0]->{masterpage} = \$_[0]->{masterpage}->new( virtual_path => '$virtual_path' ) if \$_[0]->{masterpage};
\$_[0];
}
CODE
# Things work differently if we have/don't have a master page:
if( $s->masterpage )
{
# Sub-class the master page:
$code .= <<"CODE";
BEGIN {
(my \$pkg = '@{[ ref($s->masterpage) ]}.pm') =~ s/::/\\\\/g;
use Apache2::ASP::ASPPage;
#eval { require \$pkg; 1 } or
Apache2::ASP::ASPPage->new(
virtual_path => '@{[ $s->masterpage->virtual_path ]}'
);
}
use base '@{[ ref($s->masterpage) ]}';
use vars ( '\$Master', __PACKAGE__->VARS );
@{[ join "\n\n", map { $s->placeholder_contents->{$_} } keys(%{ $s->placeholder_contents }) ]}
@{[ join "\n\n", map { "sub $_ {\$Response->Write(q~$s->{placeholders}->{$_}~);}" } keys(%{$s->placeholders}) ]}
CODE
}
else
{
# Just sub-class this class:
$code .= <<"CODE";
use base 'Apache2::ASP::ASPPage';
use vars __PACKAGE__->VARS;
sub run {
my (\$__self,\$__context, \$__args) = \@_;
\$__self->_initialize_page;
if( my \$cached = \$__self->_read_cache )
{
\$__self->{directives}->{OutputCache} = undef;
\$Response->Write( \$cached );
return;
}# end if()
\$__context->{page} = \$__self unless \$__self->is_masterpage;
#line 1
@{[ ${$s->source_code} ]}
@{[ join "\n\n", map { "sub $_ {\$Response->Write(q~$s->{placeholders}->{$_}~);}" } keys(%{$s->placeholders}) ]}
}
CODE
}# end if()
# Finally:
$code .= <<"CODE";
1;# return true:
CODE
unlink( $s->pm_path ) if -f $s->pm_path;
open my $ofh, '>', $s->pm_path
or die "Cannot open '" . $s->pm_path . "' for writing: $!";
print $ofh $code;
close($ofh);
chmod( 0666, $s->pm_path );
}# end _assemble_code()
#==============================================================================
sub _get_directives
{
my ($s) = @_;
my $ref = $s->source_code;
my %directives = ( );
while( my ($tag, $directive, $attr_str) = $$ref =~ m/(<%@\s*(.*?)\s+(.*?)\s*%>)/ )
{
my $attrs = $s->_parse_tag_attrs( $attr_str );
$$ref =~ s/\Q$tag\E//;
$directives{$directive} = $attrs;
}# end while()
return \%directives;
}# end _get_directives()
#==============================================================================
sub _parse_tag_attrs
{
my ($s, $str) = @_;
my $attr = { };
while( $str =~ m@([^\s\=\"\']+)(\s*=\s*(?:(")(.*?)"|(')(.*?)'|([^'"\s=]+)['"]*))?@sg ) #@
{
my $key = $1;
my $test = $2;
my $val = ( $3 ? $4 : ( $5 ? $6 : $7 ));
if( $test )
{
$attr->{$key} = $val;
}
else
{
$attr->{$key} = $key;
}# end if()
}# end while()
return $attr;
}# end _parse_tag_attrs()
1;# return true:
=pod
=head1 NAME
Apache2::ASP::ASPPage - base class for all *.asp scripts
=head1 SYNOPSIS
Internal use only.
=head1 DESCRIPTION
All C<*.asp> scripts are 'compiled' down to their respective C<*.pm> files. The
package definition within those C<*.pm> files all begin with this code:
use base 'Apache2::ASP::ASPPage';
After that, you will find your code, just a little uglier than you wrote it.
This package is only used internally.
=head1 BUGS
It's possible that some bugs have found their way into this release.
Use RT L to submit bug reports.
=head1 HOMEPAGE
Please visit the Apache2::ASP homepage at L to see examples
of Apache2::ASP in action.
=head1 AUTHOR
John Drago L
=head1 COPYRIGHT AND LICENSE
Copyright 2007 John Drago, All rights reserved.
This software is free software. It may be used and distributed under the
same terms as Perl itself.
=cut