aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet')
-rw-r--r--perllib/FixMyStreet/App/Controller/Contact.pm6
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm3
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/Update.pm3
-rw-r--r--perllib/FixMyStreet/App/View/Web.pm20
-rw-r--r--perllib/FixMyStreet/DB/Result/Comment.pm6
-rw-r--r--perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm6
-rw-r--r--perllib/FixMyStreet/DB/Result/Problem.pm17
-rw-r--r--perllib/FixMyStreet/DB/Result/User.pm4
-rw-r--r--perllib/FixMyStreet/Template.pm19
-rw-r--r--perllib/FixMyStreet/Template/Context.pm67
-rw-r--r--perllib/FixMyStreet/Template/SafeString.pm106
-rw-r--r--perllib/FixMyStreet/Template/Stash.pm75
-rw-r--r--perllib/FixMyStreet/Template/Variable.pm177
13 files changed, 485 insertions, 24 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Contact.pm b/perllib/FixMyStreet/App/Controller/Contact.pm
index 8477dd694..9ce89a9e2 100644
--- a/perllib/FixMyStreet/App/Controller/Contact.pm
+++ b/perllib/FixMyStreet/App/Controller/Contact.pm
@@ -7,6 +7,7 @@ BEGIN { extends 'Catalyst::Controller'; }
use MIME::Base64;
use mySociety::EmailUtil;
use FixMyStreet::Email;
+use FixMyStreet::Template::SafeString;
=head1 NAME
@@ -253,8 +254,9 @@ generally required to stash
sub setup_request : Private {
my ( $self, $c ) = @_;
- $c->stash->{contact_email} = $c->cobrand->contact_email;
- $c->stash->{contact_email} =~ s/\@/@/;
+ my $email = $c->cobrand->contact_email;
+ $email =~ s/\@/@/;
+ $c->stash->{contact_email} = FixMyStreet::Template::SafeString->new($email);
for my $param (qw/em subject message/) {
$c->stash->{$param} = $c->get_param($param);
diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm
index 270ad2ddb..899028ee9 100644
--- a/perllib/FixMyStreet/App/Controller/Report/New.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/New.pm
@@ -4,6 +4,7 @@ use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
+use utf8;
use Encode;
use List::MoreUtils qw(uniq);
use List::Util 'first';
@@ -895,7 +896,7 @@ sub process_user : Private {
oauth_report => { $report->get_inflated_columns }
};
unless ( $c->forward( '/auth/sign_in', [ $params{username} ] ) ) {
- $c->stash->{field_errors}->{password} = _('There was a problem with your login information. If you cannot remember your password, or do not have one, please fill in the ‘No’ section of the form.');
+ $c->stash->{field_errors}->{password} = _('There was a problem with your login information. If you cannot remember your password, or do not have one, please fill in the ‘No’ section of the form.');
return 1;
}
my $user = $c->user->obj;
diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm
index 1dc337c48..610f0f4eb 100644
--- a/perllib/FixMyStreet/App/Controller/Report/Update.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm
@@ -4,6 +4,7 @@ use Moose;
use namespace::autoclean;
BEGIN { extends 'Catalyst::Controller'; }
+use utf8;
use Path::Class;
use List::Util 'first';
use Utils;
@@ -143,7 +144,7 @@ sub process_user : Private {
oauth_update => { $update->get_inflated_columns }
};
unless ( $c->forward( '/auth/sign_in', [ $params{username} ] ) ) {
- $c->stash->{field_errors}->{password} = _('There was a problem with your login information. If you cannot remember your password, or do not have one, please fill in the ‘No’ section of the form.');
+ $c->stash->{field_errors}->{password} = _('There was a problem with your login information. If you cannot remember your password, or do not have one, please fill in the ‘No’ section of the form.');
return 1;
}
my $user = $c->user->obj;
diff --git a/perllib/FixMyStreet/App/View/Web.pm b/perllib/FixMyStreet/App/View/Web.pm
index 93aa0e2fb..1e1b50094 100644
--- a/perllib/FixMyStreet/App/View/Web.pm
+++ b/perllib/FixMyStreet/App/View/Web.pm
@@ -6,6 +6,7 @@ use warnings;
use FixMyStreet;
use FixMyStreet::Template;
+use FixMyStreet::Template::SafeString;
use Utils;
__PACKAGE__->config(
@@ -19,6 +20,7 @@ __PACKAGE__->config(
'tprintf', 'prettify_dt',
'version', 'decode',
'prettify_state',
+ 'mark_safe',
],
FILTERS => {
add_links => \&add_links,
@@ -59,7 +61,15 @@ sprintf (different name to avoid clash)
sub tprintf {
my ( $self, $c, $format, @args ) = @_;
@args = @{$args[0]} if ref $args[0] eq 'ARRAY';
- return sprintf $format, @args;
+ #$format = $format->plain if UNIVERSAL::isa($format, 'Template::HTML::Variable');
+ my $s = sprintf $format, @args;
+ return FixMyStreet::Template::SafeString->new($s);
+}
+
+sub mark_safe {
+ my ($self, $c, $s) = @_;
+ $s = $s->plain if UNIVERSAL::isa($s, 'FixMyStreet::Template::Variable');
+ return FixMyStreet::Template::SafeString->new($s);
}
=head2 Utils::prettify_dt
@@ -82,16 +92,16 @@ sub prettify_dt {
[% text | add_links | html_para %]
-Add some links to some text (and thus HTML-escapes the other text.
+Add some links to some text (and thus HTML-escapes the other text).
=cut
sub add_links {
my $text = shift;
+ $text = FixMyStreet::Template::conditional_escape($text);
$text =~ s/\r//g;
- $text = FixMyStreet::Template::html_filter($text);
$text =~ s{(https?://)([^\s]+)}{"<a href=\"$1$2\">$1" . _space_slash($2) . '</a>'}ge;
- return $text;
+ return FixMyStreet::Template::SafeString->new($text);
}
sub _space_slash {
@@ -113,7 +123,7 @@ sub markup_factory {
my $text = shift;
return $text unless $user && ($user->from_body || $user->is_superuser);
$text =~ s{\*(\S.*?\S)\*}{<i>$1</i>};
- $text;
+ FixMyStreet::Template::SafeString->new($text);
}
}
diff --git a/perllib/FixMyStreet/DB/Result/Comment.pm b/perllib/FixMyStreet/DB/Result/Comment.pm
index 1ffcc7b40..99c5b6dab 100644
--- a/perllib/FixMyStreet/DB/Result/Comment.pm
+++ b/perllib/FixMyStreet/DB/Result/Comment.pm
@@ -101,6 +101,7 @@ __PACKAGE__->load_components("+FixMyStreet::DB::RABXColumn");
__PACKAGE__->rabx_column('extra');
use Moo;
+use FixMyStreet::Template::SafeString;
use namespace::clean -except => [ 'meta' ];
use FixMyStreet::Template;
@@ -201,7 +202,7 @@ sub moderation_filter {
=head2 meta_line
Returns a string to be used on a report update, describing some of the metadata
-about an update
+about an update. Can include HTML.
=cut
@@ -225,6 +226,7 @@ sub meta_line {
} else {
$body = $self->user->body;
}
+ $body = FixMyStreet::Template::html_filter($body);
if ($body eq 'Bromley Council') {
$body = "$body <img src='/cobrands/bromley/favicon.png' alt=''>";
} elsif ($body eq 'Royal Borough of Greenwich') {
@@ -259,7 +261,7 @@ sub meta_line {
$meta .= ', ' . _( 'and a defect raised' );
}
- return $meta;
+ return FixMyStreet::Template::SafeString->new($meta);
};
sub problem_state_processed {
diff --git a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
index 18d2a7683..1805e1fd2 100644
--- a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
+++ b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
@@ -74,6 +74,7 @@ __PACKAGE__->belongs_to(
# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:FLKiZELcfBcc9VwHU2MZYQ
use Moo;
+use FixMyStreet::Template::SafeString;
use Text::Diff;
use Data::Dumper;
@@ -147,11 +148,12 @@ sub compare_photo {
push @deleted, $diff->Items(1);
push @added, $diff->Items(2);
}
- return (join ', ', map {
+ my $s = (join ', ', map {
"<del style='background-color:#fcc'>$_</del>";
} @deleted) . (join ', ', map {
"<ins style='background-color:#cfc'>$_</ins>";
} @added);
+ return FixMyStreet::Template::SafeString->new($s);
}
sub compare_extra {
@@ -212,7 +214,7 @@ sub string_diff {
$string .= $inserted;
}
}
- return $string;
+ return FixMyStreet::Template::SafeString->new($string);
}
1;
diff --git a/perllib/FixMyStreet/DB/Result/Problem.pm b/perllib/FixMyStreet/DB/Result/Problem.pm
index 97f0666e0..4b52cd11d 100644
--- a/perllib/FixMyStreet/DB/Result/Problem.pm
+++ b/perllib/FixMyStreet/DB/Result/Problem.pm
@@ -201,6 +201,8 @@ use Moo;
use namespace::clean -except => [ 'meta' ];
use Utils;
use FixMyStreet::Map::FMS;
+use FixMyStreet::Template;
+use FixMyStreet::Template::SafeString;
use LWP::Simple qw($ua);
use RABX;
use URI;
@@ -669,16 +671,16 @@ sub body {
my $cache = $problem->result_source->schema->cache;
return $cache->{bodies}{$problem->external_body} //= $c->model('DB::Body')->find({ id => $problem->external_body });
} else {
- $body = $problem->external_body;
+ $body = FixMyStreet::Template::html_filter($problem->external_body);
}
} else {
my $bodies = $problem->bodies;
my @body_names = sort map {
my $name = $_->name;
if ($c and FixMyStreet->config('AREA_LINKS_FROM_PROBLEMS')) {
- '<a href="' . $_->url . '">' . $name . '</a>';
+ '<a href="' . $_->url . '">' . FixMyStreet::Template::html_filter($name) . '</a>';
} else {
- $name;
+ FixMyStreet::Template::html_filter($name);
}
} values %$bodies;
if ( scalar @body_names > 2 ) {
@@ -688,7 +690,7 @@ sub body {
$body = join( _(' and '), @body_names);
}
}
- return $body;
+ return FixMyStreet::Template::SafeString->new($body);
}
@@ -778,17 +780,20 @@ sub can_display_external_id {
return 0;
}
+# This can return HTML and is safe, so returns a FixMyStreet::Template::SafeString
sub duration_string {
my ( $problem, $c ) = @_;
my $body = $c->cobrand->call_hook(link_to_council_cobrand => $problem) || $problem->body($c);
my $handler = $c->cobrand->call_hook(get_body_handler_for_problem => $problem);
if ( $handler && $handler->call_hook('is_council_with_case_management') ) {
- return sprintf(_('Received by %s moments later'), $body);
+ my $s = sprintf(_('Received by %s moments later'), $body);
+ return FixMyStreet::Template::SafeString->new($s);
}
return unless $problem->whensent;
- return sprintf(_('Sent to %s %s later'), $body,
+ my $s = sprintf(_('Sent to %s %s later'), $body,
Utils::prettify_duration($problem->whensent->epoch - $problem->confirmed->epoch, 'minute')
);
+ return FixMyStreet::Template::SafeString->new($s);
}
sub local_coords {
diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm
index 9554bbe7e..4f46fcfe2 100644
--- a/perllib/FixMyStreet/DB/Result/User.pm
+++ b/perllib/FixMyStreet/DB/Result/User.pm
@@ -449,8 +449,8 @@ sub has_permission_to {
return 0 unless $available{$permission_type};
return 1 if $self->is_superuser;
- return 0 if !$body_ids || (ref $body_ids && !@$body_ids);
- $body_ids = [ $body_ids ] unless ref $body_ids;
+ return 0 if !$body_ids || (ref $body_ids eq 'ARRAY' && !@$body_ids);
+ $body_ids = [ $body_ids ] unless ref $body_ids eq 'ARRAY';
my %body_ids = map { $_ => 1 } @$body_ids;
foreach (@{$self->body_permissions}) {
diff --git a/perllib/FixMyStreet/Template.pm b/perllib/FixMyStreet/Template.pm
index ba4376959..354b6c911 100644
--- a/perllib/FixMyStreet/Template.pm
+++ b/perllib/FixMyStreet/Template.pm
@@ -6,6 +6,9 @@ use warnings;
use FixMyStreet;
use mySociety::Locale;
use Attribute::Handlers;
+use FixMyStreet::Template::SafeString;
+use FixMyStreet::Template::Context;
+use FixMyStreet::Template::Stash;
my %FILTERS;
my %SUBS;
@@ -39,6 +42,8 @@ sub new {
my ($class, $config) = @_;
$config->{FILTERS}->{$_} = $FILTERS{$_} foreach keys %FILTERS;
$config->{ENCODING} = 'utf8';
+ $config->{STASH} = FixMyStreet::Template::Stash->new($config);
+ $config->{CONTEXT} = FixMyStreet::Template::Context->new($config);
$class->SUPER::new($config);
}
@@ -57,7 +62,8 @@ Passes the text to the localisation engine for translations.
=cut
sub loc : Fn {
- return _(@_);
+ my $s = _(@_);
+ return FixMyStreet::Template::SafeString->new($s);
}
=head2 nget
@@ -69,7 +75,7 @@ Use first or second string depending on the number.
=cut
sub nget : Fn {
- return mySociety::Locale::nget(@_);
+ return FixMyStreet::Template::SafeString->new(mySociety::Locale::nget(@_));
}
=head2 file_exists
@@ -104,6 +110,12 @@ sub html_filter : Filter('html') {
return $text;
}
+sub conditional_escape {
+ my $text = shift;
+ $text = html_filter($text) unless UNIVERSAL::isa($text, 'FixMyStreet::Template::SafeString');
+ return $text;
+}
+
=head2 html_paragraph
Same as Template Toolkit's html_paragraph, but converts single newlines
@@ -113,10 +125,11 @@ into <br>s too.
sub html_paragraph : Filter('html_para') {
my $text = shift;
+ $text = conditional_escape($text);
my @paras = grep { $_ } split(/(?:\r?\n){2,}/, $text);
s/\r?\n/<br>\n/g for @paras;
$text = "<p>\n" . join("\n</p>\n\n<p>\n", @paras) . "</p>\n";
- return $text;
+ return FixMyStreet::Template::SafeString->new($text);
}
1;
diff --git a/perllib/FixMyStreet/Template/Context.pm b/perllib/FixMyStreet/Template/Context.pm
new file mode 100644
index 000000000..de3212095
--- /dev/null
+++ b/perllib/FixMyStreet/Template/Context.pm
@@ -0,0 +1,67 @@
+package FixMyStreet::Template::Context;
+
+use strict;
+use warnings;
+use base qw(Template::Context);
+
+sub filter {
+ my $self = shift;
+ my ($name, $args, $alias) = @_;
+
+ # If we're passing through the safe filter, then unwrap
+ # from a Template::HTML::Variable if we are one.
+ if ( $name eq 'safe' ) {
+ return sub {
+ my $value = shift;
+ return $value->plain if UNIVERSAL::isa($value, 'FixMyStreet::Template::Variable');
+ return $value;
+ };
+ }
+
+ my $filter = $self->SUPER::filter(@_);
+
+ # If we are already going to auto-encode, we don't want to do it again.
+ # This makes the html filter a no-op on auto-encoded variables.
+ if ( $name eq 'html' ) {
+ return sub {
+ my $value = shift;
+ return $value if UNIVERSAL::isa($value, 'FixMyStreet::Template::Variable');
+ return $filter->($value);
+ };
+ }
+
+ return sub {
+ my $value = shift;
+
+ if ( UNIVERSAL::isa($value, 'FixMyStreet::Template::Variable') ) {
+ my $result = $filter->($value->plain);
+ return $result if UNIVERSAL::isa($result, 'FixMyStreet::Template::SafeString');
+ return ref($value)->new($result);
+ }
+
+ return $filter->($value);
+ };
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FixMyStreet::Template::Context - Similar to Template::HTML::Context but use
+'safe' rather than 'none' to be clear, also prevents html filter double-encoding,
+and doesn't rewrap a FixMyStreet::Template::SafeString.
+
+=head1 AUTHORS
+
+Martyn Smith, E<lt>msmith@cpan.orgE<gt>
+
+Matthew Somerville, E<lt>matthew@mysociety.orgE<gt>
+
+=head1 LICENSE
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.8.8 or,
+at your option, any later version of Perl 5 you may have available.
+
+=cut
diff --git a/perllib/FixMyStreet/Template/SafeString.pm b/perllib/FixMyStreet/Template/SafeString.pm
new file mode 100644
index 000000000..619bee048
--- /dev/null
+++ b/perllib/FixMyStreet/Template/SafeString.pm
@@ -0,0 +1,106 @@
+package FixMyStreet::Template::SafeString;
+
+use strict;
+use warnings;
+
+=head1 NAME
+
+FixMyStreet::Template::SafeString - a string that won't be escaped on output in a template
+
+=cut
+
+use overload
+ '""' => sub { ${$_[0]} },
+ '.' => \&concat,
+ '.=' => \&concatequals,
+ '=' => \&clone,
+ 'cmp' => \&cmp,
+;
+
+sub new {
+ my ($class, $value) = @_;
+
+ my $self = bless \$value, $class;
+
+ return $self;
+}
+
+sub cmp {
+ my ($self, $str) = @_;
+
+ if (ref $str eq __PACKAGE__) {
+ return $$self cmp $$str;
+ } else {
+ return $$self cmp $str;
+ }
+}
+
+sub concat {
+ my ($self, $str, $prefix) = @_;
+
+ return $self->clone() if not defined $str or $str eq '';
+
+ if ( $prefix ) {
+ return $str . $$self;
+ } else {
+ return $$self . $str;
+ }
+}
+
+sub concatequals {
+ my ($self, $str, $prefix) = @_;
+
+ if ( ref $str eq __PACKAGE__) {
+ $$self .= $$str;
+ return $self;
+ } else {
+ return $self->clone() if $str eq '';
+ $$self .= $str;
+ return $$self;
+ }
+}
+
+sub clone {
+ my $self = shift;
+
+ my $val = $$self;
+ my $clone = bless \$val, ref $self;
+
+ return $clone;
+}
+
+1;
+__END__
+
+=head1 SYNOPSIS
+
+ use FixMyStreet::Template;
+ use FixMyStreet::Template::SafeString;
+
+ my $s1 = "< test & stuff >";
+ my $s2 = FixMyStreet::Template::SafeString->new($s1);
+
+ my $tt = FixMyStreet::Template->new();
+ $tt->process(\"[% s1 %] * [% s2 %]\n", { s1 => $s1, s2 => $s2 });
+
+ # Produces output "&lt; test &amp; stuff &gt; * < test & stuff >"
+
+=head1 DESCRIPTION
+
+This object provides a safe string to use as part of the FixMyStreet::Template
+extension. It will not be automatically escaped when used, so can be used to
+pass HTML to a template by a function that is safely creating some.
+
+=head1 AUTHOR
+
+Matthew Somerville, E<lt>matthew@mysociety.orgE<gt>
+
+Martyn Smith, E<lt>msmith@cpan.orgE<gt>
+
+=head1 LICENSE
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.8.8 or,
+at your option, any later version of Perl 5 you may have available.
+
+=cut
diff --git a/perllib/FixMyStreet/Template/Stash.pm b/perllib/FixMyStreet/Template/Stash.pm
new file mode 100644
index 000000000..dd027400e
--- /dev/null
+++ b/perllib/FixMyStreet/Template/Stash.pm
@@ -0,0 +1,75 @@
+package FixMyStreet::Template::Stash;
+
+use strict;
+use warnings;
+use base qw(Template::Stash);
+use FixMyStreet::Template::Variable;
+use Scalar::Util qw(blessed);
+
+sub get {
+ my $self = shift;
+
+ my $value = $self->SUPER::get(@_);
+
+ $value = FixMyStreet::Template::Variable->new($value) unless ref $value;
+
+ return $value;
+}
+
+# To deal with being able to call var.upper or var.match
+sub _dotop {
+ my $self = shift;
+ my ($root, $item, $args, $lvalue) = @_;
+
+ $args ||= [ ];
+ $lvalue ||= 0;
+
+ return undef unless defined($root) and defined($item);
+ return undef if $item =~ /^[_.]/;
+
+ if (blessed($root) && $root->isa('FixMyStreet::Template::Variable')) {
+ if ((my $value = $Template::Stash::SCALAR_OPS->{ $item }) && ! $lvalue) {
+ my @result = &$value($root->{value}, @$args);
+ if (defined $result[0]) {
+ return scalar @result > 1 ? [ @result ] : $result[0];
+ }
+ return undef;
+ }
+ }
+
+ return $self->SUPER::_dotop(@_);
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FixMyStreet::Template::Stash - The same as Template::HTML::Stash, but
+additionally copes with scalar operations on stash items.
+
+=head1 FUNCTIONS
+
+=head2 get()
+
+An overridden function from Template::Stash that calls the parent class's get
+method, and returns a FixMyStreet::Template::Variable instead of a raw string.
+
+=head2 _dotop()
+
+An overridden function from Template::Stash so that scalar operations on
+wrapped FixMyStreet::Template::Variable strings still function correctly.
+
+=head1 AUTHOR
+
+Martyn Smith, E<lt>msmith@cpan.orgE<gt>
+
+Matthew Somerville, E<lt>matthew@mysociety.orgE<gt>
+
+=head1 LICENSE
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.8.8 or,
+at your option, any later version of Perl 5 you may have available.
+
+=cut
diff --git a/perllib/FixMyStreet/Template/Variable.pm b/perllib/FixMyStreet/Template/Variable.pm
new file mode 100644
index 000000000..9b5a0fcc4
--- /dev/null
+++ b/perllib/FixMyStreet/Template/Variable.pm
@@ -0,0 +1,177 @@
+package FixMyStreet::Template::Variable;
+
+use strict;
+use warnings;
+use FixMyStreet::Template;
+
+sub op_factory {
+ my ($op) = @_;
+
+ return eval q|sub {
+ my ($self, $str) = @_;
+
+ if ( ref $str eq __PACKAGE__) {
+ return $self->{value} | . $op . q| $str->{value};
+ }
+ else {
+ return $self->{value} | . $op . q| $str;
+ }
+ }|;
+}
+
+use overload
+ '""' => \&html_encoded,
+ '.' => \&concat,
+ '.=' => \&concatequals,
+ '=' => \&clone,
+
+ 'cmp' => op_factory('cmp'),
+ 'eq' => op_factory('eq'),
+ '<=>' => op_factory('<=>'),
+ '==' => op_factory('=='),
+ '%' => op_factory('%'),
+ '+' => op_factory('+'),
+ '-' => op_factory('-'),
+ '*' => op_factory('*'),
+ '/' => op_factory('/'),
+ '**' => op_factory('**'),
+ '>>' => op_factory('>>'),
+ '<<' => op_factory('<<'),
+;
+
+sub new {
+ my ($class, $value) = @_;
+
+ my $self = bless { value => $value }, $class;
+
+ return $self;
+}
+
+sub plain {
+ my $self = shift;
+
+ return $self->{value};
+}
+
+sub html_encoded {
+ my $self = shift;
+ return FixMyStreet::Template::html_filter($self->{value});
+}
+
+sub concat {
+ my ($self, $str, $prefix) = @_;
+
+ # Special case where we're _not_ going to html_encode now now
+ return $self->clone() if not defined $str or $str eq '';
+
+ if ( $prefix ) {
+ return $str . $self->html_encoded();
+ }
+ else {
+ return $self->html_encoded() . $str;
+ }
+}
+
+sub concatequals {
+ my ($self, $str, $prefix) = @_;
+
+ if ( ref $str eq __PACKAGE__) {
+ $self->{value} .= $str->{value};
+ return $self;
+ }
+ else {
+ # Special case where we're _not_ going to html_encode now now
+ return $self->clone() if $str eq '';
+
+ # Fix Template::HTML::Variable issue with double output
+ my $ret = $self->html_encoded . $str;
+ $self->{value} .= $str;
+ return $ret;
+ }
+}
+
+sub clone {
+ my $self = shift;
+
+ my $clone = bless { %$self }, ref $self;
+
+ return $clone;
+}
+
+1;
+__END__
+
+=head1 NAME
+
+FixMyStreet::Template::Variable - A "pretend" string that auto HTML encodes;
+a copy of Template::HTML::Variable with a bugfix.
+
+=head1 SYNOPSIS
+
+ use FixMyStreet::Template::Variable;
+
+ my $string = FixMyStreet::Template::Variable->new('< test & stuff >');
+
+ print $string, "\n";
+
+ # Produces output "&lt; test &amp; stuff &gt;"
+
+=head1 DESCRIPTION
+
+This object provides a "pretend" string to use as part of the
+FixMyStreet::Template extension.
+
+It automatically stringifies to an HTML encoded version of what it was created
+with, all the while trying to keep a sane state through string concatinations
+etc.
+
+=head1 FUNCTIONS
+
+=head2 new()
+
+Takes a single argument which is the string to set this variable to
+
+=head2 plain()
+
+Returns a non HTML-encoded version of the string (i.e. exactly what was passed
+to the new() function
+
+=head2 html_encoded()
+
+Returns an HTML encoded version of the string (used by the stringify
+overloads)
+
+=head2 concat()
+
+Implementation of overloaded . operator
+
+=head2 concatequals()
+
+Implementation of overloaded .= operator.
+
+The original Template::HTML::Variable has a bug here, whereby it adds the new
+string to its internal value, then returns the HTML encoded version of the
+whole string with the new string concatenated again (unescaped).
+
+=head2 clone()
+
+Returns a clone of this variable. (used for the implementation of the
+overloaded = operator).
+
+=head2 op_factory()
+
+Factory for generating operator overloading subs
+
+=head1 AUTHOR
+
+Martyn Smith, E<lt>msmith@cpan.orgE<gt>
+
+Matthew Somerville, E<lt>matthew@mysociety.orgE<gt>
+
+=head1 LICENSE
+
+This library is free software; you can redistribute it and/or modify
+it under the same terms as Perl itself, either Perl version 5.8.8 or,
+at your option, any later version of Perl 5 you may have available.
+
+=cut