#!/usr/bin/perl # # DW::BML # # BML rendering for Plack via DW::Request. This module implements the BML # handler logic using the DW::Request abstraction layer instead of Apache APIs, # allowing BML pages to render under Plack. # # The existing Apache::BML module continues to work unchanged for mod_perl. # This module reuses the core BML engine (bml_decode, bml_block, config loading, # scheme/look system) and only replaces the handler and request adapter layers. # # Authors: # Mark Smith # # Copyright (c) 2025-2026 by Dreamwidth Studios, LLC. # # This program is free software; you may redistribute it and/or modify it under # the same terms as Perl itself. For a copy of the license, please reference # 'perldoc perlartistic' or 'perldoc perlgpl'. # package DW::BML; use strict; use v5.10; use Log::Log4perl; my $log = Log::Log4perl->get_logger(__PACKAGE__); use Cwd qw(abs_path); use Digest::MD5; use DW::Request; use DW::SiteScheme; use LJ::Directories; # Provide BML::* package functions for the Plack environment. Under mod_perl these # are defined by Apache::BML, but that module can't be loaded without Apache2::*. # Many non-BML callers (LJ::Lang::ml, LJ::Web, etc.) rely on these existing in any # web context, so we define them here at load time. # # If Apache::BML is already loaded (mod_perl), we skip all of this. unless ( defined &BML::ml ) { # Load Apache::BML for the core BML engine functions (bml_decode, # load_conffile, parsein, modified_time, etc.). Since Apache::BML # uses Apache2/APR modules at compile time, we install stubs for # those modules first so it can load without mod_perl present. # # This must happen BEFORE the BML::* overrides below so that our # Plack-safe versions replace the Apache-dependent originals. unless ( $INC{'Apache/BML.pm'} ) { for my $mod ( qw( Apache2::Const Apache2::Log Apache2::Request Apache2::RequestRec Apache2::RequestUtil Apache2::RequestIO APR::Table APR::Finfo ) ) { ( my $file = $mod ) =~ s!::!/!g; $file .= '.pm'; $INC{$file} //= __FILE__; } # Provide the Apache2::Const symbols that Apache::BML imports. # Apache2::Const uses an export-on-import model with tag groups; # we define the constants and an import() that pushes them into # the caller's namespace. { package Apache2::Const; my %const = ( OK => 0, NOT_FOUND => 404, REDIRECT => 302, SERVER_ERROR => 500, HTTP_NOT_MODIFIED => 304, FORBIDDEN => 403, DECLINED => -1, DONE => -2, ); my %tag = ( common => [qw(OK NOT_FOUND REDIRECT SERVER_ERROR FORBIDDEN DECLINED DONE)] ); sub import { my ( $class, @args ) = @_; my $caller = caller; my @names; for my $arg (@args) { if ( $arg =~ /^:(.+)/ && $tag{$1} ) { push @names, @{ $tag{$1} }; } elsif ( exists $const{$arg} ) { push @names, $arg; } } no strict 'refs'; for my $name (@names) { my $val = $const{$name}; *{"${caller}::${name}"} = sub () { $val }; } } } package DW::BML; require Apache::BML; } # BML::ml stub — gets redefined by BML::set_language() *BML::ml = sub { return "[ml_getter not defined]"; }; # BML::ML tied hash support — must be defined before tie calls *BML::ML::TIEHASH = sub { return bless {}, $_[0]; }; *BML::ML::FETCH = sub { return "[ml_getter not defined]"; }; *BML::ML::CLEAR = sub { }; # BML::Cookie tied hash support — must be defined before tie calls *BML::Cookie::TIEHASH = sub { return bless {}, $_[0]; }; *BML::Cookie::FETCH = sub { my ( $t, $key ) = @_; my $r = BML::get_request(); unless ($BML::COOKIES_PARSED) { my $cookie_header = eval { $r->headers_in->{"Cookie"} } // ''; foreach ( split( /;\s+/, $cookie_header ) ) { next unless /(.*)=(.*)/; my ( $name, $value ) = ( $1, $2 ); push @{ $BML::COOKIE_M{ BML::durl($name) } ||= [] }, BML::durl($value); } $BML::COOKIES_PARSED = 1; } return $BML::COOKIE_M{$key} || [] if $key =~ s/\[\]$//; return ( $BML::COOKIE_M{$key} || [] )->[-1]; }; *BML::Cookie::STORE = sub { my ( $t, $key, $val ) = @_; my $etime = 0; my $http_only = 0; ( $val, $etime, $http_only ) = @$val if ref $val eq "ARRAY"; $etime = undef unless $val ne ""; BML::set_cookie( $key, $val, $etime, undef, undef, $http_only ); }; *BML::Cookie::DELETE = sub { BML::Cookie::STORE( $_[0], $_[1], undef ); }; *BML::Cookie::CLEAR = sub { foreach ( keys %BML::COOKIE_M ) { BML::Cookie::STORE( $_[0], $_, undef ); } }; *BML::Cookie::EXISTS = sub { return defined $BML::COOKIE_M{ $_[1] }; }; *BML::Cookie::FIRSTKEY = sub { keys %BML::COOKIE_M; return each %BML::COOKIE_M; }; *BML::Cookie::NEXTKEY = sub { return each %BML::COOKIE_M; }; # Now tie the hashes tie %BML::ML, 'BML::ML' unless tied %BML::ML; tie %BML::COOKIE, 'BML::Cookie' unless tied %BML::COOKIE; # Language scope $BML::ML_SCOPE = '' unless defined $BML::ML_SCOPE; # The BML::set_language function — redefines BML::ml and BML::ML::FETCH *BML::set_language = sub { my ( $lang, $getter ) = @_; my $apache_r = BML::get_request(); if ($apache_r) { eval { $apache_r->notes->set( 'langpref', $lang ); }; } if ( Apache::BML::is_initialized() ) { my $req = $Apache::BML::cur_req; $req->{'lang'} = $lang; $getter ||= $req->{'env'}->{'HOOK-ml_getter'}; } no strict 'refs'; if ( $lang eq "debug" ) { no warnings 'redefine'; *{"BML::ml"} = sub { return $_[0]; }; *{"BML::ML::FETCH"} = sub { return $_[1]; }; } elsif ($getter) { no warnings 'redefine'; *{"BML::ml"} = sub { my ( $code, $vars ) = @_; if ( rindex( $code, '.', 0 ) == 0 ) { my $scope = $BML::ML_SCOPE; unless ($scope) { my $r = eval { DW::Request->get }; $scope = $r->note('ml_scope') if $r; } $code = $scope . $code if $scope; } return $getter->( $lang, $code, undef, $vars ); }; *{"BML::ML::FETCH"} = sub { my $code = $_[1]; if ( rindex( $code, '.', 0 ) == 0 ) { my $scope = $BML::ML_SCOPE; unless ($scope) { my $r = eval { DW::Request->get }; $scope = $r->note('ml_scope') if $r; } $code = $scope . $code if $scope; } return $getter->( $lang, $code ); }; } }; *BML::set_language_scope = sub { $BML::ML_SCOPE = $_[0]; }; *BML::get_language_scope = sub { return $BML::ML_SCOPE; }; *BML::get_language = sub { return undef unless Apache::BML::is_initialized(); return $Apache::BML::cur_req->{'lang'}; }; *BML::get_language_default = sub { return "en" unless Apache::BML::is_initialized(); return $Apache::BML::cur_req->{'env'}->{'DefaultLanguage'} || "en"; }; *BML::get_request = sub { return $Apache::BML::r if $Apache::BML::r; my $r = DW::Request->get; return unless $r; return DW::BML::RequestAdapter->new($r); }; *BML::get_uri = sub { my $r = BML::get_request() or return ''; my $uri = $r->uri; $uri =~ s/\.bml$//; return $uri; }; *BML::get_hostname = sub { my $r = BML::get_request() or return ''; return $r->hostname; }; *BML::get_method = sub { my $r = BML::get_request() or return ''; return $r->method; }; *BML::get_query_string = sub { my $r = BML::get_request() or return ''; return scalar( $r->args ); }; *BML::get_path_info = sub { my $r = BML::get_request() or return ''; return $r->path_info; }; *BML::get_remote_ip = sub { my $r = BML::get_request() or return ''; return $r->connection->client_ip; }; *BML::get_remote_host = sub { my $r = BML::get_request() or return ''; return $r->connection->remote_host; }; *BML::get_client_header = sub { my $hdr = shift; my $r = BML::get_request() or return ''; return $r->headers_in->{$hdr}; }; *BML::ehtml = sub { my $a = $_[0]; $a =~ s/\&/&/g; $a =~ s/\"/"/g; $a =~ s/\'/&\#39;/g; $a =~ s//>/g; return $a; }; *BML::eurl = sub { my $a = $_[0]; $a =~ s/([^a-zA-Z0-9_\-.\/\\\: ])/uc sprintf("%%%02x",ord($1))/eg; $a =~ tr/ /+/; return $a; }; *BML::durl = sub { my ($a) = @_; $a =~ tr/+/ /; $a =~ s/%([a-fA-F0-9][a-fA-F0-9])/pack("C", hex($1))/eg; return $a; }; *BML::ebml = sub { my $a = $_[0]; my $ra = ref $a ? $a : \$a; $$ra =~ s/\(=(\w)/\(= $1/g; $$ra =~ s/(\w)=\)/$1 =\)/g; $$ra =~ s/<\?/<?/g; $$ra =~ s/\?>/?>/g; return if ref $a; return $a; }; *BML::eall = sub { return BML::ebml( BML::ehtml( $_[0] ) ); }; *BML::noparse = sub { $Apache::BML::CodeBlockOpts{'raw'} = 1; return $_[0]; }; *BML::set_content_type = sub { return undef unless Apache::BML::is_initialized(); $Apache::BML::cur_req->{'content_type'} = $_[0] if $_[0]; }; *BML::set_status = sub { my $r = $Apache::BML::r or return; $r->status( $_[0] + 0 ) if $_[0]; }; *BML::redirect = sub { return undef unless Apache::BML::is_initialized(); $Apache::BML::cur_req->{'location'} = $_[0]; BML::finish_suppress_all(); }; *BML::finish = sub { return undef unless Apache::BML::is_initialized(); $Apache::BML::cur_req->{'stop_flag'} = 1; }; *BML::suppress_headers = sub { return undef unless Apache::BML::is_initialized(); BML::send_cookies(); $Apache::BML::cur_req->{'env'}->{'NoHeaders'} = 1; }; *BML::suppress_content = sub { return undef unless Apache::BML::is_initialized(); $Apache::BML::cur_req->{'env'}->{'NoContent'} = 1; }; *BML::finish_suppress_all = sub { BML::finish(); BML::suppress_headers(); BML::suppress_content(); }; *BML::http_response = sub { my ( $code, $msg ) = @_; my $r = $Apache::BML::r or return; $r->status($code); $r->content_type('text/html'); $r->print($msg); BML::finish_suppress_all(); }; *BML::http_only = sub { my $ua = BML::get_client_header("User-Agent") // ''; return 0 if $ua =~ /MSIE.+Mac_/; return 1; }; *BML::get_scheme = sub { return undef unless Apache::BML::is_initialized(); return $Apache::BML::cur_req->{'scheme'}; }; *BML::set_etag = sub { return undef unless Apache::BML::is_initialized(); $Apache::BML::cur_req->{'etag'} = $_[0]; }; *BML::want_last_modified = sub { return undef unless Apache::BML::is_initialized(); $Apache::BML::cur_req->{'want_last_modified'} = $_[0] if defined $_[0]; return $Apache::BML::cur_req->{'want_last_modified'}; }; *BML::note_mod_time = sub { Apache::BML::note_mod_time( $Apache::BML::cur_req, $_[0] ); }; *BML::self_link = sub { my $newvars = shift; my $r = $Apache::BML::r or return ''; my $link = $r->uri; my $form = \%BMLCodeBlock::FORM; $link .= "?"; foreach ( keys %$newvars ) { if ( !exists $form->{$_} ) { $form->{$_} = ""; } } foreach ( sort keys %$form ) { if ( defined $newvars->{$_} && !$newvars->{$_} ) { next; } my $val = $newvars->{$_} || $form->{$_}; next unless $val; $link .= BML::eurl($_) . "=" . BML::eurl($val) . "&"; } chop $link; return $link; }; *BML::page_newurl = sub { my $page = $_[0]; my @pair = (); foreach ( sort grep { $_ ne "page" } keys %BMLCodeBlock::FORM ) { push @pair, ( BML::eurl($_) . "=" . BML::eurl( $BMLCodeBlock::FORM{$_} ) ); } push @pair, "page=$page"; my $r = $Apache::BML::r or return ''; return $r->uri . "?" . join( "&", @pair ); }; *BML::reset_cookies = sub { %BML::COOKIE_M = (); $BML::COOKIES_PARSED = 0; }; *BML::send_cookies = sub { my $req = shift(); unless ($req) { return undef unless Apache::BML::is_initialized(); $req = $Apache::BML::cur_req; } foreach ( values %{ $req->{'cookies'} } ) { $req->{'r'}->err_headers_out->add( "Set-Cookie" => $_ ); } $req->{'cookies'} = {}; $req->{'env'}->{'SentCookies'} = 1; }; *BML::set_cookie = sub { return undef unless Apache::BML::is_initialized(); my ( $name, $value, $expires, $path, $domain, $http_only ) = @_; my $req = $Apache::BML::cur_req; my $e = $req->{'env'}; $path = $e->{'CookiePath'} unless defined $path; $domain = $e->{'CookieDomain'} unless defined $domain; if ( $domain && ref $domain eq "ARRAY" ) { foreach (@$domain) { BML::set_cookie( $name, $value, $expires, $path, $_, $http_only ); } return; } my ( $sec, $min, $hour, $mday, $mon, $year, $wday ) = gmtime($expires); $year += 1900; my @day = qw{Sunday Monday Tuesday Wednesday Thursday Friday Saturday}; my @month = qw{Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec}; my $cookie = BML::eurl($name) . "=" . BML::eurl($value); unless ( defined $expires && $expires == 0 ) { $cookie .= sprintf( "; expires=$day[$wday], %02d-$month[$mon]-%04d %02d:%02d:%02d GMT", $mday, $year, $hour, $min, $sec ); } $cookie .= "; path=$path" if $path; $cookie .= "; domain=$domain" if $domain; $cookie .= "; HttpOnly" if $http_only && BML::http_only(); if ( $e->{'SentCookies'} ) { $req->{'r'}->err_headers_out->add( "Set-Cookie" => $cookie ); } else { $req->{'cookies'}->{"$name:$domain"} = $cookie; } if ( defined $expires ) { $BML::COOKIE_M{$name} = [$value]; } else { delete $BML::COOKIE_M{$name}; } }; *BML::get_GET = sub { return \%BMLCodeBlock::GET; }; *BML::get_POST = sub { return \%BMLCodeBlock::POST; }; *BML::get_FORM = sub { return \%BMLCodeBlock::FORM; }; *BML::fill_template = sub { my ( $name, $vars ) = @_; die "Can't use BML::fill_template in non-BML context" unless $Apache::BML::cur_req; return Apache::BML::parsein( ${ $Apache::BML::cur_req->{'blockref'}->{ uc($name) } }, $vars ); }; *BML::do_later = sub { return 0; }; # no-op under Plack *BML::decide_language = sub { return undef unless Apache::BML::is_initialized(); my $req = $Apache::BML::cur_req; my $env = $req->{'env'}; my $uselang = $BMLCodeBlock::GET{'uselang'} // ''; if ( exists $env->{"Langs-$uselang"} || $uselang eq "debug" ) { return $uselang; } my $r = $req->{'r'}; my %lang_weight; my @langs = split( /\s*,\s*/, lc( eval { $r->headers_in->{"Accept-Language"} } // '' ) ); my $winner_weight = 0.0; my $winner; foreach (@langs) { s/-\w+//; if (/(.+);q=(.+)/) { $lang_weight{$1} = $2; } else { $lang_weight{$_} = 1.0; } if ( ( $lang_weight{$_} // 0 ) > $winner_weight && defined $env->{"ISOCode-$_"} ) { $winner_weight = $lang_weight{$_}; $winner = $env->{"ISOCode-$_"}; } } return $winner if $winner; return $LJ::LANGS[0] if @LJ::LANGS; return "en"; }; } # Cache for file lookups, mirrors Apache::LiveJournal's %FILE_LOOKUP_CACHE my %FILE_LOOKUP_CACHE; # resolve_path: given a URI, find the .bml file on disk # Returns ($redirect_url, $uri, $filepath) # - If redirect needed: ($url, undef, undef) # - If file found: (undef, $uri, $filepath) # - If nothing found: (undef, undef, undef) sub resolve_path { my ( $class, $uri ) = @_; return ( undef, undef, undef ) if $uri =~ m!(\.\.|\%|\.\/)!; if ( exists $FILE_LOOKUP_CACHE{$uri} ) { my $cached = $FILE_LOOKUP_CACHE{$uri}; return ( undef, $cached->[0], $cached->[1] ); } foreach my $dir ( LJ::get_all_directories('htdocs') ) { my $file = "$dir/$uri"; # main page: / => /index.bml my $resolved_uri = $uri; if ( -e "$file/index.bml" && $uri eq '/' ) { $file .= "index.bml"; $resolved_uri .= "index.bml"; } # /blah/file => /blah/file.bml if ( -e "$file.bml" ) { $file .= ".bml"; $resolved_uri .= ".bml"; } # /foo => /foo/ (redirect), /foo/ => /foo/index.bml if ( -d $file && -e "$file/index.bml" ) { unless ( $uri =~ m!/$! ) { my $redirect_url = LJ::create_url( $uri . "/" ); return ( $redirect_url, undef, undef ); } $file .= "index.bml"; $resolved_uri .= "index.bml"; } next unless -f $file; $file = abs_path($file); if ($file) { $resolved_uri =~ s!^/+!/!; $FILE_LOOKUP_CACHE{$uri} = [ $resolved_uri, $file ]; return ( undef, $resolved_uri, $file ); } } return ( undef, undef, undef ); } # render: render a BML file and send the response via DW::Request # Arguments: $file (absolute path), $uri (request URI) # Returns: 1 on success, 0 on failure (status already set on $r) sub render { my ( $class, $file, $uri ) = @_; my $r = DW::Request->get; # Stat the file unless ( -e $file ) { $log->warn("BML file does not exist: $file"); $r->status(404); $r->content_type('text/html'); $r->print('Not Found'); return 0; } unless ( -r $file ) { $log->warn("BML file not readable: $file"); $r->status(403); $r->content_type('text/html'); $r->print('Forbidden'); return 0; } my $modtime = ( stat($file) )[9]; # Never serve _config files if ( $file =~ /\b_config/ ) { $r->status(403); $r->content_type('text/html'); $r->print('Forbidden'); return 0; } # Install the request adapter so BML::get_request() etc. work my $adapter = DW::BML::RequestAdapter->new($r); local $Apache::BML::r = $adapter; # Create new BML request my $req = Apache::BML::initialize_cur_req( $adapter, $file ); # Setup env: walk up directories loading _config.bml files my $env = $req->{env}; my $dir = $file; my $docroot = $LJ::HTDOCS; $docroot =~ s!/$!!; my @dirconfs; my %confwant; while ($dir) { $dir =~ s!/[^/]*$!!; my $conffile = "$dir/_config.bml"; $confwant{$conffile} = 1; push @dirconfs, Apache::BML::load_conffile($conffile); last if $dir eq $docroot; } # Process config chain with SubConfig overrides my %eff_config; foreach my $cfile (@dirconfs) { my $conf = $Apache::BML::FileConfig{$cfile}; next unless $conf; $eff_config{$cfile} = $conf; if ( $conf->{'SubConfig'} ) { foreach my $sconf ( keys %confwant ) { my $sc = $conf->{'SubConfig'}{$sconf}; $eff_config{$cfile} = $sc if $sc; } } } foreach my $cfile (@dirconfs) { my $conf = $eff_config{$cfile}; next unless $conf; while ( my ( $k, $v ) = each %$conf ) { next if exists $env->{$k} || $k eq "SubConfig"; $env->{$k} = $v; } } # Token syntax my ( $TokenOpen, $TokenClose ); if ( $env->{'AllowOldSyntax'} ) { ( $TokenOpen, $TokenClose ) = ( '(?:<\?|\(=)', '(?:\?>|=\))' ); } else { ( $TokenOpen, $TokenClose ) = ( '<\?', '\?>' ); } # Force redirect hook if ( exists $env->{'HOOK-force_redirect'} ) { my $redirect_page = eval { $env->{'HOOK-force_redirect'}->($uri); }; if ( defined $redirect_page ) { $r->status(302); $r->header_out( 'Location' => $redirect_page ); $Apache::BML::r = undef; return 1; } } # Rewrite filename hook if ( exists $env->{'HOOK-rewrite_filename'} ) { eval { my $new_file = $env->{'HOOK-rewrite_filename'}->( req => $req, env => $env ); $file = $new_file if $new_file; }; } # Read the BML source unless ( open my $fh, '<', $file ) { $log->error("Couldn't open $file for reading: $!"); $r->status(500); $r->content_type('text/html'); $r->print('Internal Server Error'); $Apache::BML::r = undef; return 0; } else { my $bmlsource; { local $/ = undef; $bmlsource = <$fh>; } close $fh; # Track modification times Apache::BML::note_mod_time( $req, $modtime ); Apache::BML::note_mod_time( $req, $Apache::BML::base_recent_mod ); if ( !defined $Apache::BML::FileModTime{$file} || $modtime > $Apache::BML::FileModTime{$file} ) { $Apache::BML::FileModTime{$file} = $modtime; $req->{'filechanged'} = 1; } # Setup cookies and ML *BMLCodeBlock::COOKIE = *BML::COOKIE; BML::reset_cookies(); *BMLCodeBlock::ML = *BML::ML; # Parse form inputs from DW::Request _parse_inputs( $r, $req ); # XSS protection %BMLCodeBlock::GET_POTENTIAL_XSS = (); if ( $env->{MildXSSProtection} ) { foreach my $k ( keys %BMLCodeBlock::GET ) { next unless $BMLCodeBlock::GET{$k} =~ /\<|\%3C/i; $BMLCodeBlock::GET_POTENTIAL_XSS{$k} = $BMLCodeBlock::GET{$k}; delete $BMLCodeBlock::GET{$k}; delete $BMLCodeBlock::FORM{$k}; } } # Startup hook if ( $env->{'HOOK-startup'} ) { eval { $env->{'HOOK-startup'}->(); }; if ($@) { $r->status(500); $r->content_type('text/html'); $r->print("Error running startup hook:
\n$@"); return 1; } } # Code block init perl hook $BML::CODE_INIT_PERL = ""; if ( $env->{'HOOK-codeblock_init_perl'} ) { $BML::CODE_INIT_PERL = eval { $env->{'HOOK-codeblock_init_perl'}->(); }; if ($@) { $r->status(500); $r->content_type('text/html'); $r->print("Error running codeblock_init_perl hook:
\n$@"); return 1; } } # Determine scheme my $scheme = $r->note('bml_use_scheme') || $env->{'ForceScheme'} || $BMLCodeBlock::GET{skin} || $BMLCodeBlock::GET{'usescheme'} || $BML::COOKIE{'BMLschemepref'}; if ( exists $env->{'HOOK-alt_default_scheme'} ) { $scheme ||= eval { $env->{'HOOK-alt_default_scheme'}->($env); }; } my $default_scheme_override = undef; if ( $env->{'HOOK-default_scheme_override'} ) { $default_scheme_override = eval { $env->{'HOOK-default_scheme_override'}->( $scheme || DW::SiteScheme->default ); }; if ($@) { $r->status(500); $r->content_type('text/html'); $r->print("Error running scheme override hook:
\n$@"); return 1; } } $scheme ||= $default_scheme_override || DW::SiteScheme->default; # Scheme translation hook if ( $env->{'HOOK-scheme_translation'} ) { my $newscheme = eval { $env->{'HOOK-scheme_translation'}->($scheme); }; $scheme = $newscheme if $newscheme; } unless ( BML::set_scheme($scheme) ) { $scheme = $env->{'ForceScheme'} || DW::SiteScheme->default; BML::set_scheme($scheme); } # Language setup — keep .bml in scope so LJ::Lang::get_text can # match the scope to .bml.text files (regex: ^(/.+\.bml)(\..+)) my $lang_scope = $uri; BML::set_language_scope($lang_scope); my $lang = BML::decide_language(); BML::set_language($lang); # Run the BML decoder my $html = $env->{'_error'}; if ( $env->{'HOOK-before_decode'} ) { eval { $env->{'HOOK-before_decode'}->(); }; if ($@) { $r->status(500); $r->content_type('text/html'); $r->print("Error running before_decode hook:
\n$@"); return 1; } } Apache::BML::bml_decode( $req, \$bmlsource, \$html, { DO_CODE => $env->{'AllowCode'} } ) unless $html; # Send cookies BML::send_cookies($req); # Handle internal redirect if ( $r->note('internal_redir') ) { my $int_redir = DW::Routing->call( uri => $r->note('internal_redir') ); if ( defined $int_redir ) { $r->note( 'internal_redir', undef ); LJ::start_request(); return 1; } } # Handle redirect if ( $req->{'location'} ) { $r->status(302); $r->header_out( 'Location' => $req->{'location'} ); $Apache::BML::r = undef; return 1; } # ETag handling my $etag; if ( exists $req->{'etag'} ) { $etag = $req->{'etag'} if defined $req->{'etag'}; } else { $etag = Digest::MD5::md5_hex($html); } $etag = '"' . $etag . '"' if defined $etag; my $ifnonematch = $r->header_in("If-None-Match"); if ( defined $ifnonematch && defined $etag && $etag eq $ifnonematch ) { $r->status(304); $Apache::BML::r = undef; return 1; } my $content_type = $req->{'content_type'} || $env->{'DefaultContentType'} || "text/html"; unless ( $env->{'NoHeaders'} ) { my $ims = $r->header_in("If-Modified-Since"); my $modtime_http = Apache::BML::modified_time($req); if ( $ims && !$env->{'NoCache'} && $ims eq $modtime_http ) { $r->status(304); $Apache::BML::r = undef; return 1; } $r->content_type($content_type); if ( $env->{'NoCache'} ) { $r->header_out( "Cache-Control" => "no-cache" ); $r->no_cache; } $r->header_out( "Last-Modified" => $modtime_http ) if $env->{'Static'} || $req->{'want_last_modified'}; $r->header_out( "Cache-Control" => "private, proxy-revalidate" ); $r->header_out( "ETag" => $etag ) if defined $etag; my $length = length( $html // '' ); $r->header_out( 'Content-length' => $length ); } # Output the content unless ( $env->{'NoContent'} || $r->method eq 'HEAD' ) { $r->print( $html // '' ); } $r->status(200) unless $r->status; $Apache::BML::r = undef; return 1; } } # _parse_inputs: populate %BMLCodeBlock::GET, POST, FORM from DW::Request sub _parse_inputs { my ( $r, $req ) = @_; %BMLCodeBlock::GET = (); %BMLCodeBlock::POST = (); %BMLCodeBlock::FORM = (); # GET parameters — use preserve_case to match Apache::BML behavior # which doesn't lowercase GET args my $get_args = $r->get_args( preserve_case => 1 ); if ($get_args) { $get_args->each( sub { my ( $k, $v ) = @_; $BMLCodeBlock::GET{$k} .= "\0" if exists $BMLCodeBlock::GET{$k}; $BMLCodeBlock::GET{$k} .= $v; } ); } # POST parameters (only for url-encoded, not multipart) my $ct = $r->header_in('Content-Type') // ''; unless ( $ct =~ m!^multipart/form-data! ) { my $post_args = $r->post_args; if ($post_args) { $post_args->each( sub { my ( $k, $v ) = @_; $BMLCodeBlock::POST{$k} .= "\0" if exists $BMLCodeBlock::POST{$k}; $BMLCodeBlock::POST{$k} .= $v; } ); } } # FORM gets whichever method was used if ( $r->method eq 'POST' ) { %BMLCodeBlock::FORM = %BMLCodeBlock::POST; } else { %BMLCodeBlock::FORM = %BMLCodeBlock::GET; } } ########################################################################### # DW::BML::RequestAdapter # # Minimal adapter that makes DW::Request look enough like an Apache2 request # object for BML's public API functions (BML::get_request(), etc.) to work. ########################################################################### package DW::BML::RequestAdapter; sub new { my ( $class, $dw_request ) = @_; return bless { r => $dw_request }, $class; } sub uri { return $_[0]->{r}->uri; } sub method { return $_[0]->{r}->method; } sub args { return $_[0]->{r}->query_string; } sub path_info { return ''; # BML pages don't use path_info in Plack context } sub hostname { return $_[0]->{r}->host; } sub header_only { return $_[0]->{r}->method eq 'HEAD' ? 1 : 0; } sub status { my ( $self, $val ) = @_; if ( defined $val ) { return $self->{r}->status($val); } return $self->{r}->status; } sub content_type { my ( $self, $val ) = @_; if ( defined $val ) { return $self->{r}->content_type($val); } return $self->{r}->content_type; } sub print { my ( $self, @args ) = @_; $self->{r}->print($_) for @args; } sub no_cache { return $_[0]->{r}->no_cache; } # headers_in: returns a tied hash-like object for reading request headers sub headers_in { return DW::BML::RequestAdapter::HeadersIn->new( $_[0]->{r} ); } # headers_out / err_headers_out: returns an object for setting response headers sub headers_out { return DW::BML::RequestAdapter::HeadersOut->new( $_[0]->{r} ); } sub err_headers_out { return DW::BML::RequestAdapter::ErrHeadersOut->new( $_[0]->{r} ); } # notes: returns a tied hash-like object backed by DW::Request->note() sub notes { return DW::BML::RequestAdapter::Notes->new( $_[0]->{r} ); } # connection: returns an object with client_ip, remote_host, user sub connection { return DW::BML::RequestAdapter::Connection->new( $_[0]->{r} ); } # document_root: return $LJ::HTDOCS sub document_root { return $LJ::HTDOCS; } # pool: stub for cleanup_register (no-op under Plack) sub pool { return DW::BML::RequestAdapter::Pool->new; } # dir_config: stub, returns undef (no Apache dir config under Plack) sub dir_config { return undef; } # Apache constant stubs for code that calls $r->OK, $r->NOT_FOUND, etc. sub OK { return 0; } sub NOT_FOUND { return 404; } sub DECLINED { return -1; } sub status_line { my ( $self, $val ) = @_; if ( defined $val ) { $self->{_status_line} = $val; return; } return $self->{_status_line}; } # finfo: no-op sub finfo { } # filename sub filename { return $_[0]->{_filename}; } ########################################################################### # HeadersIn: read-only hash-like access to request headers ########################################################################### package DW::BML::RequestAdapter::HeadersIn; sub new { my ( $class, $r ) = @_; tie my %h, 'DW::BML::RequestAdapter::HeadersIn::Tie', $r; return bless [ $r, \%h ], $class; } use overload '%{}' => sub { return $_[0]->[1]; }, fallback => 1; package DW::BML::RequestAdapter::HeadersIn::Tie; sub TIEHASH { return bless { r => $_[1] }, $_[0] } sub FETCH { return $_[0]->{r}->header_in( $_[1] ) } sub EXISTS { return defined $_[0]->{r}->header_in( $_[1] ) } sub STORE { } # read-only ########################################################################### # HeadersOut: hash-like access to response headers ########################################################################### package DW::BML::RequestAdapter::HeadersOut; sub new { my ( $class, $r ) = @_; tie my %h, 'DW::BML::RequestAdapter::HeadersOut::Tie', $r; return bless [ $r, \%h ], $class; } use overload '%{}' => sub { return $_[0]->[1]; }, fallback => 1; package DW::BML::RequestAdapter::HeadersOut::Tie; sub TIEHASH { return bless { r => $_[1] }, $_[0] } sub FETCH { return $_[0]->{r}->header_out( $_[1] ) } sub STORE { $_[0]->{r}->header_out( $_[1], $_[2] ) } ########################################################################### # ErrHeadersOut: for Set-Cookie via ->add() ########################################################################### package DW::BML::RequestAdapter::ErrHeadersOut; sub new { my ( $class, $r ) = @_; return bless { r => $r }, $class; } sub add { my ( $self, $name, $value ) = @_; $self->{r}->err_header_out_add( $name, $value ); } ########################################################################### # Notes: hash-like access to per-request notes ########################################################################### package DW::BML::RequestAdapter::Notes; sub new { my ( $class, $r ) = @_; tie my %h, 'DW::BML::RequestAdapter::Notes::Tie', $r; # Use array-based object to avoid hash dereference triggering overload return bless [ $r, \%h ], $class; } sub set { my ( $self, $key, $value ) = @_; $self->[0]->note( $key, $value ); } use overload '%{}' => sub { return $_[0]->[1]; }, fallback => 1; package DW::BML::RequestAdapter::Notes::Tie; sub TIEHASH { return bless { r => $_[1] }, $_[0] } sub FETCH { return $_[0]->{r}->note( $_[1] ) } sub STORE { $_[0]->{r}->note( $_[1], $_[2] ) } ########################################################################### # Connection: client_ip, remote_host, user ########################################################################### package DW::BML::RequestAdapter::Connection; sub new { my ( $class, $r ) = @_; return bless { r => $r }, $class; } sub client_ip { return $_[0]->{r}->get_remote_ip; } sub remote_host { return $_[0]->{r}->get_remote_ip; } sub user { return undef; } ########################################################################### # Pool: stub for cleanup_register ########################################################################### package DW::BML::RequestAdapter::Pool; sub new { return bless {}, $_[0]; } sub cleanup_register { # No-op under Plack — cleanup happens at end of request naturally } 1;