Base.pm 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
  1. package Trog::Renderer::Base;
  2. use strict;
  3. use warnings;
  4. no warnings 'experimental';
  5. use feature qw{signatures state};
  6. use Encode qw{encode_utf8};
  7. use IO::Compress::Gzip;
  8. use Text::Xslate;
  9. use Trog::Themes;
  10. use Trog::Config;
  11. use Time::HiRes qw{tv_interval};
  12. =head1 Trog::Renderer::Base
  13. Basic rendering structure, subclass me.
  14. Sets up the methods which must be present for all templates, e.g. render_it for rendering dynamic template strings coming from a post.
  15. =cut
  16. our %renderers;
  17. sub render (%options) {
  18. die "Templated renders require a template to be passed" unless $options{template};
  19. my $template_dir = Trog::Themes::template_dir( $options{template}, $options{contenttype}, $options{component} );
  20. my $t = "$template_dir/$options{template}";
  21. die "Templated renders require an existing template to be passed, got $template_dir/$options{template}" unless -f $t || -s $t;
  22. #TODO make this work with posts all the time
  23. $options{child_processor} //= Text::Xslate->new( path => $template_dir );
  24. my $child_processor = $options{child_processor};
  25. $options{child_renderer} //= sub {
  26. my ( $template_string, $options ) = @_;
  27. # If it fails to render, it must be something else
  28. my $out = eval { $child_processor->render_string( $template_string, $options ) };
  29. return $out ? $out : $template_string;
  30. };
  31. $renderers{$template_dir} //= Text::Xslate->new(
  32. path => $template_dir,
  33. function => {
  34. render_it => $options{child_renderer},
  35. },
  36. );
  37. my $code = $options{code};
  38. my $body = encode_utf8( $renderers{$template_dir}->render( $options{template}, $options{data} ) );
  39. # Users can supply a post_processor to futz with the output (such as with minifiers) if they wish.
  40. $body = $options{post_processor}->($body) if $options{post_processor} && ref $options{post_processor} eq 'CODE';
  41. # Users can supply custom headers as part of the data in options.
  42. my %headers = headers( \%options, $body );
  43. return $body if $options{component};
  44. return [ $code, [%headers], [$body] ] unless $options{deflate};
  45. $headers{"Content-Encoding"} = "gzip";
  46. my $dfh;
  47. IO::Compress::Gzip::gzip( \$body => \$dfh );
  48. print $IO::Compress::Gzip::GzipError if $IO::Compress::Gzip::GzipError;
  49. $headers{"Content-Length"} = length($dfh);
  50. return [ $code, [%headers], [$dfh] ];
  51. }
  52. sub headers ( $options, $body ) {
  53. my $query = $options->{data};
  54. my $uh = ref $options->{headers} eq 'HASH' ? $options->{headers} : {};
  55. my $ct = $options->{contenttype} eq 'text/html' ? "text/html; charset=UTF-8" : "$options->{contenttype};";
  56. my %headers = (
  57. 'Content-Type' => $ct,
  58. 'Content-Length' => length($body),
  59. 'Cache-Control' => $query->{cachecontrol} // $Trog::Vars::cache_control{revalidate},
  60. 'X-Content-Type-Options' => 'nosniff',
  61. 'Vary' => 'Accept-Encoding',
  62. 'Server-Timing' => "render;dur=" . ( tv_interval( $query->{start} ) * 1000 ),
  63. %$uh,
  64. );
  65. #Disallow framing UNLESS we are in embed mode
  66. my $ancestor = $query->{domain} || 'none';
  67. $headers{"Content-Security-Policy"} = qq{frame-ancestors '$ancestor'} unless $query->{embed};
  68. $headers{'X-Frame-Options'} = 'DENY' unless $query->{embed};
  69. $headers{'Referrer-Policy'} = 'no-referrer-when-downgrade';
  70. #CSP. Yet another layer of 'no mixed content' plus whitelisted execution of remote resources.
  71. my $scheme = $query->{scheme} ? "$query->{scheme}:" : '';
  72. my $conf = Trog::Config::get();
  73. my $sites = $conf->param('security.allow_embeds_from') // '';
  74. $headers{'Content-Security-Policy'} .= ";default-src $scheme 'self' 'unsafe-eval' 'unsafe-inline' $sites";
  75. $headers{'Content-Security-Policy'} .= ";object-src 'none'";
  76. # Force https if we are https
  77. $headers{'Strict-Transport-Security'} = 'max-age=63072000' if ( $query->{scheme} // '' ) eq 'https';
  78. # We only set etags when users are logged in, cause we don't use statics
  79. $headers{'ETag'} = $query->{etag} if $query->{etag} && $query->{user};
  80. return %headers;
  81. }
  82. 1;