mourningdove/cgi-bin/DW/Request/Plack.pm

290 lines
7.7 KiB
Perl
Raw Normal View History

2026-05-24 01:03:05 +00:00
#!/usr/bin/perl
#
# DW::Request::Plack
#
# Abstraction layer for using Plack's $env model to power Dreamwidth based
# systems.
#
# Authors:
# Mark Smith <mark@dreamwidth.org>
#
# Copyright (c) 2021 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::Request::Plack;
use strict;
use v5.10;
use Log::Log4perl;
my $log = Log::Log4perl->get_logger(__PACKAGE__);
use DW::Request::Base;
use base 'DW::Request::Base';
use HTTP::Date ();
use Plack::Request;
use Plack::Response;
use URI;
use fields ( 'env', 'req', 'req_addr', 'res', 'res_body', 'res_length', 'notes', 'pnotes' );
$DW::Request::PLACK_AVAILABLE = 1;
BEGIN {
# Do initialization for pass-throughs that will go to the Plack::Request
# object inside
foreach my $method (qw/ method query_string /) {
no strict 'refs';
*{"DW::Request::Plack::$method"} = sub {
my DW::Request::Plack $self = shift;
return $self->{req}->$method(@_);
};
}
}
# uri: return just the path component, matching Apache's $r->uri behavior.
# Plack::Request->uri returns the full URL which breaks code expecting a path.
sub uri {
my DW::Request::Plack $self = $_[0];
return $self->{req}->path_info || '/';
}
# creates a new DW::Request object, based on what type of server environment we
# are running under
sub new {
my DW::Request::Plack $self = $_[0];
my $plack_env = $_[1];
# Create self if needed
$self = fields::new($self) unless ref $self;
$self->SUPER::new;
# Convert PSGI $env to Plack::Request and store for pass-thru usage
$self->{env} = $plack_env;
$self->{req} = Plack::Request->new($plack_env);
$self->{req_addr} = undef;
$self->{res} = Plack::Response->new;
$self->{res_body} = undef; # or scalar
$self->{res_length} = 0;
# now stick ourselves as the primary request ...
unless ($DW::Request::cur_req) {
$DW::Request::determined = 1;
$DW::Request::cur_req = $self;
}
return $self;
}
# response methods to update the response we're going to send
sub header_out {
my DW::Request::Plack $self = $_[0];
return $self->{res}->header( $_[1] ) if scalar @_ == 2;
return $self->{res}->header( $_[1] => $_[2] );
}
# In Plack there's no distinction between error and normal headers
*err_header_out = \&header_out;
# _add variants append instead of replacing (needed for Set-Cookie)
sub header_out_add {
my DW::Request::Plack $self = $_[0];
$self->{res}->headers->push_header( $_[1] => $_[2] );
}
*err_header_out_add = \&header_out_add;
sub set_last_modified {
my DW::Request::Plack $self = $_[0];
my $mtime = $_[1];
$self->{res}->headers->header( 'Last-Modified' => HTTP::Date::time2str($mtime) );
}
sub meets_conditions {
my DW::Request::Plack $self = $_[0];
my $ims = $self->{req}->header('If-Modified-Since');
return 0 unless $ims; # 0 = OK, proceed normally
my $lm = $self->{res}->headers->header('Last-Modified');
return 0 unless $lm;
my $ims_time = HTTP::Date::str2time($ims);
my $lm_time = HTTP::Date::str2time($lm);
return 0 unless defined $ims_time && defined $lm_time;
# If not modified, return 304
return 304 if $lm_time <= $ims_time;
return 0;
}
# incoming headers, from request
sub header_in {
my DW::Request::Plack $self = $_[0];
return $self->{req}->header( $_[1] ) if scalar @_ == 2;
return $self->{req}->header( $_[1] => $_[2] );
}
# return all request headers as a flat list of (key, value) pairs,
# matching DW::Request::Apache2::headers_in behavior
sub headers_in {
my DW::Request::Plack $self = $_[0];
my @headers;
$self->{req}->headers->scan( sub { push @headers, @_ } );
return @headers;
}
# get client address; allow overriding it because we need to set it in some
# cases when we're dealing with proxies
sub address {
my DW::Request::Plack $self = $_[0];
return $self->{req_addr} // $self->{req}->address if scalar @_ == 1;
return $self->{req_addr} = $_[1];
}
# return host
sub host {
my DW::Request::Plack $self = $_[0];
return $self->header_in('Host');
}
# set the status
sub status {
my DW::Request::Plack $self = $_[0];
$self->{res}->status( $_[1] ) if defined $_[1];
return $self->{res}->status;
}
# set or get the status line (e.g. "200 OK")
sub status_line {
my DW::Request::Plack $self = $_[0];
if ( scalar @_ == 2 ) {
my ($status) = $_[1] =~ m/^(\d+)/;
$self->{res}->status($status);
}
return $self->{res}->status;
}
# append to the body
sub print {
my DW::Request::Plack $self = $_[0];
push @{ $self->{res_body} ||= [] }, $_[1];
$self->{res_length} += length( $_[1] );
}
# flatten out the body and return the response
sub res {
my DW::Request::Plack $self = $_[0];
if ( defined $self->{res_body} ) {
$self->{res}->body( $self->{res_body} );
$self->{res}->content_length( $self->{res_length} );
}
return $self->{res}->finalize;
}
# return path
sub path {
my DW::Request::Plack $self = $_[0];
return $self->{req}->path;
}
# query parameters
sub query_parameters {
my DW::Request::Plack $self = $_[0];
return $self->{req}->query_parameters;
}
# return a new response that is a redirect
sub redirect {
my DW::Request::Plack $self = $_[0];
# Use a 303 because we want to be explicit that when we do a redirect we expect
# the user-agent to switch to a GET; this is an old assumption baked into the LJ/DW
# code now made explicit here.
#
# Set status and Location on the existing response object so that any cookies
# or headers already set (e.g. login session cookies) are preserved in the
# redirect response.
$self->{res}->status(303);
$self->{res}->header( 'Location' => $_[1] );
$self->{res_body} = undef;
$self->{res_length} = 0;
return $self->res;
}
# assemble a URL for something
sub uri_for {
my DW::Request::Plack $self = $_[0];
my ( $path, $args ) = ( $_[1], $_[2] );
my $uri = $self->{req}->base;
$uri->path( $uri->path . $path );
$uri->query_form(@$args) if %$args;
return $uri;
}
# content_type: getter reads from request, setter sets on response
sub content_type {
my DW::Request::Plack $self = $_[0];
if ( scalar @_ >= 2 ) {
return $self->{res}->content_type( $_[1] );
}
return $self->{req}->content_type;
}
# content: return raw request body
sub content {
my DW::Request::Plack $self = $_[0];
return $self->{req}->content;
}
# pnote: per-request notes hash (used by routing)
sub pnote {
my DW::Request::Plack $self = $_[0];
if ( scalar(@_) == 2 ) {
return $self->{pnotes}->{ $_[1] };
}
else {
return $self->{pnotes}->{ $_[1] } = $_[2];
}
}
# note: per-request notes hash (separate from pnotes)
sub note {
my DW::Request::Plack $self = $_[0];
if ( scalar(@_) == 2 ) {
return $self->{notes}->{ $_[1] };
}
else {
return $self->{notes}->{ $_[1] } = $_[2];
}
}
# no_cache: set cache-control headers to prevent caching
sub no_cache {
my DW::Request::Plack $self = $_[0];
$self->{res}->header( 'Cache-Control' => 'no-cache, no-store, must-revalidate' );
$self->{res}->header( 'Pragma' => 'no-cache' );
$self->{res}->header( 'Expires' => '0' );
}
# get_remote_ip: return the client IP address
sub get_remote_ip {
my DW::Request::Plack $self = $_[0];
return $self->address;
}
# Some things we need to pass to our base class
# *call_response_handler = \&DW::Request::call_response_handler;
1;