documentationify
Matt S Trout [Mon, 13 Aug 2012 20:21:18 +0000 (20:21 +0000)]
lib/HTML/String.pm
lib/HTML/String/Overload.pm
lib/HTML/String/TT.pm
lib/HTML/String/TT/Directive.pm
lib/HTML/String/Value.pm

index 033be4e..a320a69 100644 (file)
@@ -11,3 +11,145 @@ sub html {
 }
 
 1;
+
+__END__
+
+=head1 NAME
+
+HTML::String - mark strings as HTML to get auto-escaping
+
+=head1 SYNOPSIS
+
+  use HTML::String;
+  
+  my $not_html = 'Hello, Bob & Jake';
+  
+  my $html = html('<h1>').$not_html.html('</h1>');
+  
+  print html($html); # <h1>Hello, Bob &quot; Jake</h1>
+
+or, alternatively,
+
+  use HTML::String::Overload;
+  
+  my $not_html = 'Hello, Bob & Jake';
+  
+  my $html = do {
+    use HTML::String::Overload;
+    "<h1>${not_html}</h1>";
+  }
+  
+  print html($html); # <h1>Hello, Bob &quot; Jake</h1>
+
+(but see the L<HTML::String::Overload> documentation for details and caveats).
+
+See also L<HTML::String::TT> for L<Template Toolkit|Template> integration.
+
+=head1 DESCRIPTION
+
+Tired of trying to remember which strings in your program need HTML escaping?
+
+Working on something small enough to not need a templating engine - or code
+heavy enough to be better done with strings - but wanting to be careful about
+user supplied data?
+
+Yeah, me too, sometimes. So I wrote L<HTML::String>.
+
+The idea here is to have pervasive HTML escaping that fails closed - i.e.
+escapes everything that it isn't explicitly told not to. Since in the era
+of XSS (cross site scripting) attacks it's a matter of security as well as
+of not serving mangled markup, I've preferred to err on the side of
+inconvenience in places in order to make it as hard as possible to screw up.
+
+We export a single subroutine, L</html>, whose sole purpose in life
+is to construct an L<HTML::String::Value> object from a string, which then
+obsessively refuses to be concatenated to anything else without escaping it
+unless you asked for that not to happen by marking the other thing as HTML
+too.
+
+So
+
+  html($thing).$other_thing
+
+will return an object where C<$thing> won't be escaped, but C<$other_thing>
+will. Keeping concatenating stuff is fine; internally it's an array of parts.
+
+Because html() will happily take something that's already wrapped into a
+value object, when we print it out we can do:
+
+  print html($final_result);
+
+safe in the knowledge that if we got passed a value object that won't break
+anything, but if by some combination of alarums, excursions and murphy
+strikes we still have just a plain string by that point, the plain string
+will still get escaped on the way out.
+
+If you've got distinct blocks of code where you're assembling HTML, instead
+of using L</html> a lot you can say "all strings in this block are HTML
+so please mark them all to not be escaped" using L<HTML::String::Overload> -
+
+  my $string = 'This is a "normal" string';
+  
+  my $html;
+  
+  {
+    use HTML::String::Overload; # valid until the end of the block
+
+    $html = '<foo>'.$string.'</foo>'; # the two strings are html()ified
+  }
+
+  print $html; # prints <foo>This is a &quot;normal&quot; string</foo>
+
+Note however that due to a perl bug, you can't use backslash escapes in
+a string and have it still get marked as an HTML value, so instead of
+
+  "<h1>\n<div>"
+
+you need to write
+
+  "<h1>"."\n"."</div>"
+
+at least as far as 5.16.1, which is current as I write this. See
+L<HTML::String::Overload> for more details.
+
+For integration with L<Template Toolkit|Template>, see L<HTML::String::TT>.
+
+=head1 EXPORTS
+
+=head2 html
+
+  my $html = html($do_not_escape_this);
+
+Returns an L<HTML::String::Value> object containing a single string part
+marked not to be escaped.
+
+If you need to do something clever such as specifying packages for which
+to ignore escaping requests, see the L<HTML::String::Value> documentation
+and write your own subroutine - this one is as simple as
+
+  sub html {
+    return HTML::String::Value->new($_[0]);
+  }
+
+so providing configuration options would likely be more complicated and
+confusing than just writing the code.
+
+=head1 AUTHOR
+
+mst - Matt S. Trout (cpan:MSTROUT) <mst@shadowcat.co.uk>
+
+=head1 CONTRIBUTORS
+
+None yet - maybe this software is perfect! (ahahahahahahahahaha)
+
+=head1 COPYRIGHT
+
+Copyright (c) 2012 the HTML::String L</AUTHOR> and L</CONTRIBUTORS>
+as listed above.
+
+=head1 LICENSE
+
+This library is free software and may be distributed under the same terms
+as perl itself.
+
+=cut
index 3c72850..ece0d15 100644 (file)
@@ -18,3 +18,70 @@ sub unimport {
 }
 
 1;
+
+__END__
+
+=head1 NAME
+
+HTML::String::Overload - Use constant overloading with L<HTML::String>
+
+=head1 SYNOPSIS
+
+  use HTML::String::Overload;
+  
+  my $html = '<h1>'; # marked as HTML
+  
+  no HTML::String::Overload;
+
+  my $text = '<h1>'; # not touched
+  
+  my $html = do {
+    use HTML::String::Overload;
+  
+    '<h1>'; # marked as HTML
+  };
+  
+  my $text = '<h1>'; # not touched
+
+=head1 DESCRIPTION
+
+This module installs a constant overload for strings - see
+L<overload/Overloading constants> in overload.pm's docs for how that works.
+
+On import, we both set up the overload, and use L<B::Hooks::EndOfScope> to
+register a callback that will remove it again at the end of the block; you
+can remove it earlier by unimporting the module using C<no>.
+
+=head1 CAVEATS
+
+Due to a perl bug (L<https://rt.perl.org/rt3/Ticket/Display.html?id=49594>),
+you can't use backslash escapes in a string to be marked as HTML, so
+
+  use HTML::String::Overload;
+  
+  my $html = "<h1>\n<div>foo</div>\n</h1>";
+
+will not be marked as HTML - instead you need to write
+
+  my $html = '<h1>'."\n".'<div>foo</div>'."\n".'</h1>';
+
+which is annoying, so consider just using L<HTML::String> and doing
+
+  my $html = html("<h1>\n<div>foo</div>\n</h1>");
+
+in that case.
+
+The import method we provide does actually take extra options for constructing
+your L<HTML::String::Value> objects but I'm not yet convinced that's a correct
+public API, so use that feature at your own risk (the only example of this is
+in L<HTML::String::TT::Directive>, which is definitely not user serviceable).
+
+=head1 AUTHORS
+
+See L<HTML::String> for authors.
+
+=head1 COPYRIGHT AND LICENSE
+
+See L<HTML::String> for the copyright and license.
+
+=cut
index 360f056..9a6d1dd 100644 (file)
@@ -35,3 +35,97 @@ sub new {
 }
 
 1;
+
+__END__
+
+=head1 NAME
+
+HTML::String::TT - HTML string auto-escaping for L<Template Toolkit|Template> 
+
+=head1 SYNOPSIS
+
+  my $tt = HTML::String::TT->new(\%normal_tt_args);
+
+or, if you're using L<Catalyst::View::TT>:
+
+  use HTML::String::TT; # needs to be loaded before TT to work
+
+  __PACKAGE__->config(
+    CLASS => 'HTML::String::TT',
+  );
+
+Then, in your template -
+
+  <h1>
+    [% title %] <-- this will be automatically escaped
+  </h1>
+  <div id="main">
+    [% some_html | no_escape %] <-- this won't
+  </div>
+  [% html_var = '<foo>'; html_var %] <-- this won't anyway
+
+(but note that the C<content> key in wrappers shouldn't need this).
+
+=head1 DESCRIPTION
+
+L<HTML::String::TT> is a wrapper for L<Template Toolkit|Template> that
+installs the following overrides:
+
+=over 4
+
+=item * The directive generator is replaced with
+L<HTML::String::TT::Directive> which ensures L<HTML::String::Overload> is
+active for the template text.
+
+=item * The stash is forced to be L<Template::Stash> since
+L<Template::Stash::XS> gets utterly confused if you hand it an object.
+
+=item * A filter C<no_escape> is added to mark outside data that you don't
+want to be escaped.
+
+=back
+
+The override happens to B<all> of the plain strings in your template, so
+even things declared within directives such as
+
+  [% html_var = '<h1>' %]
+
+will not be escaped, but any string coming from anywhere else will be. This
+can be a little bit annoying when you then pass it to things that don't
+respond well to overloaded objects, but is essential to L<HTML::String>'s
+policy of "always fail closed" - I'd rather it throws an exception than
+lets a value through unescaped, and if you care about your HTML not having
+XSS (cross site scripting) vulnerabilities then I hope you'll agree.
+
+We mark a number of TT internals namespaces as "don't escape when called by
+these", since TT has a tendency to do things like
+
+  open FH, "< $name";
+
+which really don't work if it gets converted to C<&quot; $name> while you
+aren't looking.
+
+Additionally, since TT often calls C<ref> to decide e.g.
+if something is a string or a glob, it's important that L<UNIVERSAL::ref>
+is loaded before TT is. We check to see if the latter is loaded and the
+former not, and warn loudly that you're probably going to get weird errors.
+
+This warning is not joking. "Probably" is optimistic. Load this module first.
+
+=head1 FILTERS
+
+=head2 no_escape
+
+The C<no_escape> filter marks the filtered input to not be escaped,
+so that you can provide HTML chunks from externally and still render them
+within the TT code.
+
+=head1 AUTHORS
+
+See L<HTML::String> for authors.
+
+=head1 COPYRIGHT AND LICENSE
+
+See L<HTML::String> for the copyright and license.
+
+=cut
index 770f5fa..c28ae28 100644 (file)
@@ -30,3 +30,69 @@ sub text {
 }
 
 1;
+
+__END__
+
+=head1 NAME
+
+HTML::String::TT::Directive - L<Template::Directive> overrides to forcibly escape HTML strings
+
+=head1 SYNOPSIS
+
+This is not user serviceable, and is documented only for your edification.
+
+Please use L<HTML::String::TT> as this module could change, be renamed, or
+if I figure out a better way of implementing the functionality disappear
+entirely at any moment.
+
+=head1 METHODS
+
+=head2 template
+
+We override this top-level method in order to pretend two things to the
+perl subroutine definition that TT has generated - firstly,
+
+  package HTML::String::TT::_TMPL;
+
+to ensure that no packages marked to be ignored are the current one when
+the template code is executed. Secondly,
+
+  use HTML::String::Overload { ignore => { ... } };
+
+where the C<...> contains a list of TT internal packages to ignore so that
+things actually work. This list is not duplicated here since it may also
+change without warning.
+
+Additionally, the hashref option to L<HTML::String::Overload> is not
+documented there since I'm not yet convinced that's a public API either.
+
+=head2 text
+
+Due to a perl bug (L<https://rt.perl.org/rt3/Ticket/Display.html?id=49594>)
+we overload this method to change
+
+  "<foo>\n<bar>"
+
+into
+
+  "<foo>"."\n"."<bar>"
+
+since any string containing a backslash escape doesn't get marked as HTML.
+Since we don't want to escape things that backslash escapes are normally
+used for, this isn't really a problem for us.
+
+=head2 textblock
+
+For no reason I can comprehend at all, L<Template::Directive>'s C<textblock>
+method calls C<&text> instead of using a method call so we have to override
+this as well.
+
+=head1 AUTHORS
+
+See L<HTML::String> for authors.
+
+=head1 COPYRIGHT AND LICENSE
+
+See L<HTML::String> for the copyright and license.
+
+=cut
index e26692d..1e75271 100644 (file)
@@ -91,7 +91,23 @@ sub _hsv_dot {
 
 sub _hsv_is_true {
     my ($self) = @_;
-    return 1 if grep length($_), map $_->[0], @{$self->{parts}};
+    return 1 if grep $_, map $_->[0], @{$self->{parts}};
+}
+
+sub isa {
+    my $self = shift;
+    return (
+        eval { $self->_hsv_unescaped_string->isa(@_) }
+        or $self->SUPER::isa(@_)
+    );
+}
+
+sub can {
+    my $self = shift;
+    return (
+        eval { $self->_hsv_unescaped_string->can(@_) }
+        or $self->SUPER::can(@_)
+    );
 }
 
 sub ref { '' }
@@ -99,3 +115,173 @@ sub ref { '' }
 sub DESTROY { }
 
 1;
+
+__END__
+
+=head1 NAME
+
+HTML::String::Value - A scalar hiding as a string on behalf of L<HTML::String>
+
+=head1 SYNOPSIS
+
+Usually, you'd create this with L<HTML::String>'s L<HTML::String/html> export
+but:
+
+  my $html = HTML::String::Value->new($do_not_escape_this);
+
+  my $html = HTML::String::Value->new([ $do_not_escape_this, 0 ]);
+
+  my $html = HTML::String::Value->new([ $do_escape_this, 1 ]);
+
+  my $html = HTML::String::Value->new($already_an_html_string_value);
+
+  my $html = HTML::String::Value->new(@an_array_of_any_of_the_above);
+
+  my $html = HTML::String::Value->new(
+    @parts, { ignore => { package_name => 1 } }
+  );
+
+=head1 METHODS
+
+=head2 new
+
+  my $html = HTML::String::Value->new(@parts, \%options?);
+
+Each entry in @parts consists of one of:
+
+  'some text that will not be escaped'
+
+  [ 'some text that will not be escaped', 0 ]
+
+  [ 'text that you DO want to be scaped', 1 ]
+
+  $existing_html_string_value
+
+Currently, the %options hashref contains only:
+
+  (
+    ignore => { 'Package::One' => 1, 'Package::Two' => 1, ... }
+  )
+
+which tells this value object to ignore whether escaping has been requested
+for any particular chunk and instead to provide the unescaped version.
+
+When called on an existing object, does the equivalent of
+
+  $self->_hsv_unescaped_string->new(@args);
+
+to fit in with the "pretending to be a class name" behaviour provided by
+L</AUTOLOAD>.
+
+=head2 _hsv_escaped_string
+
+  $html->_hsv_escaped_string
+
+Returns a concatenation of all parts of this value with those marked for
+escaping escaped, unless the calling package has been specified in the
+C<ignore> option to L</new>.
+
+If the calling package has been marked as ignoring escaping, returns the
+result of L</_hsv_unescaped_string>.
+
+You probably shouldn't be calling this directly.
+
+=head2 _hsv_unescaped_string
+
+  $html->_hsv_unescaped_string
+
+Returns a concatenation of all parts of this value with no escaping performed.
+
+You probably shouldn't be calling this directly.
+
+=head2 _hsv_dot
+
+  $html->_hsv_dot($other_string, $reversed)
+
+Returns a new value object consisting of the two values' parts concatenated
+together (in reverse if C<$reversed> is true).
+
+Unlike L</new>, this method defaults to escaping a bare string provided.
+
+You probably shouldn't be calling this directly.
+
+=head2 _hsv_is_true
+
+  $html->_hsv_is_true
+
+Returns true if any of this value's parts are true.
+
+You probably shouldn't be calling this directly.
+
+=head2 AUTOLOAD
+
+  $html->method_name(@args)
+
+This calls the equivalent of
+
+  $html->_hsv_unescaped_string->method_name(@args)
+
+to allow for class method calls even when the class name has ended up being
+turned into a value object.
+
+=head2 isa
+
+  $html->isa($name)
+
+This method returns true if either the value or the unescaped string are
+isa the supplied C<$name> in order to allow for class method calls even when
+the class name has ended up being turned into a value object.
+
+=head2 can
+
+  $html->can($name)
+
+This method returns a coderef if either the value or the unescaped string
+provides this method; methods on the unescaped string are preferred to allow
+for class method calls even when the class name has ended up being
+turned into a value object.
+
+=head2 ref
+
+  $html->ref
+
+This method always returns C<''>. Since we register ourselves with
+L<UNIVERSAL::ref>, this means that
+
+  ref($html);
+
+will also return C<''>, which means that modules loaded after this one will
+see a value object as being a plain scalar unless they're explicitly checking
+the defined-ness of the return value of C<ref>, which probably means that they
+wanted to spot people cheating like we're trying to.
+
+If you have trouble with things trying to treat a value object as something
+other than a string, try loading L<UNIVERSAL::ref> earlier.
+
+=head2 DESTROY
+
+Overridden to do nothing so that L</AUTOLOAD> doesn't trap it.
+
+=head1 OPERATOR OVERLOADS
+
+=head2 stringification
+
+Stringification is overloaded to call L</_hsv_escaped_string>
+
+=head2 concatenation
+
+Concatentation is overloaded to call L</_hsv_dot>
+
+=head2 boolification
+
+Boolification is overloaded to call L</_hsv_is_true>
+
+=head1 AUTHORS
+
+See L<HTML::String> for authors.
+
+=head1 COPYRIGHT AND LICENSE
+
+See L<HTML::String> for the copyright and license.
+
+=cut