Base.pm 3.8 KB

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