<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">package FixMyStreet::Cobrand::UK;
use base 'FixMyStreet::Cobrand::Default';
use strict;

use Encode;
use JSON::MaybeXS;
use LWP::UserAgent;
use Path::Tiny;
use Time::Piece;
use mySociety::MaPit;
use mySociety::VotingArea;
use Utils;
use HighwaysEngland;

sub country             { return 'GB'; }
sub area_types          { [ 'DIS', 'LBO', 'MTD', 'UTA', 'CTY', 'COI', 'LGD' ] }
sub area_types_children { $mySociety::VotingArea::council_child_types }

sub csp_config {
    my $self = shift;
    return $self-&gt;feature('content_security_policy');
}

sub enter_postcode_text {
    my ( $self ) = @_;
    return _("Enter a nearby UK postcode, or street name and area");
}

sub example_places {
    return [ 'B2 4QA', 'Tib St, Manchester' ];
}

sub disambiguate_location {
    return {
        country =&gt; 'gb',
        google_country =&gt; 'uk',
        bing_culture =&gt; 'en-GB',
        bing_country =&gt; 'United Kingdom'
    };
}

sub map_type {
    my $self = shift;
    return 'OS::FMS' if $self-&gt;feature('os_maps_url') || $self-&gt;feature('os_maps_api_key');
    return $self-&gt;next::method();
}

sub process_open311_extras {
    my $self    = shift;
    my $ctx     = shift;
    my $body = shift;
    my $extra   = shift;
    my $fields  = shift || [];

    if ( $body &amp;&amp; $body-&gt;name =~ /Bromley/ ) {
        my @fields = ( 'fms_extra_title', @$fields );
        for my $field ( @fields ) {
            my $value = $ctx-&gt;get_param($field);

            if ( !$value ) {
                $ctx-&gt;stash-&gt;{field_errors}-&gt;{ $field } = _('This information is required');
            }
            push @$extra, {
                name =&gt; $field,
                description =&gt; uc( $field),
                value =&gt; $value || '',
            };
        }

        if ( $ctx-&gt;get_param('fms_extra_title') ) {
            $ctx-&gt;stash-&gt;{fms_extra_title} = $ctx-&gt;get_param('fms_extra_title');
            $ctx-&gt;stash-&gt;{extra_name_info} = 1;
        }
    }
}

sub geocode_postcode {
    my ( $self, $s ) = @_;

    if ($s =~ /^\d+$/) {
        return {
            error =&gt; 'FixMyStreet is a UK-based website. Please enter either a UK postcode, or street name and area.'
        };
    } elsif (mySociety::PostcodeUtil::is_valid_postcode($s)) {
        my $location = mySociety::MaPit::call('postcode', $s);
        if ($location-&gt;{error}) {
            return {
                error =&gt; $location-&gt;{code} =~ /^4/
                    ? _('That postcode was not recognised, sorry.')
                    : $location-&gt;{error}
            };
        }
        my $island = $location-&gt;{coordsyst};
        if (!$island) {
            return {
                error =&gt; _("Sorry, that appears to be a Crown dependency postcode, which we don't cover.")
            };
        }
        return {
            latitude  =&gt; $location-&gt;{wgs84_lat},
            longitude =&gt; $location-&gt;{wgs84_lon},
        };
    } elsif (my $junction_location = HighwaysEngland::junction_lookup($s)) {
        return $junction_location;
    }
    return {};
}

sub short_name {
    my $self = shift;
    my ($area) = @_;

    my $name = $area-&gt;{name} || $area-&gt;name;

    # Special case Durham as it's the only place with two councils of the same name
    return 'Durham+County' if $name eq 'Durham County Council';
    return 'Durham+City' if $name eq 'Durham City Council';

    $name =~ s/^(Royal|London) Borough of //;
    $name =~ s/ (Borough|City|District|County|Parish|Town) Council$//;
    $name =~ s/ Council$//;
    $name =~ s/ &amp; / and /;
    $name =~ tr{/}{_};
    $name = URI::Escape::uri_escape_utf8($name);
    $name =~ s/%20/+/g;
    return $name;
}

sub find_closest {
    my ($self, $data) = @_;

    $data = { problem =&gt; $data } if ref $data ne 'HASH';

    my $problem = $data-&gt;{problem};
    my $lat = $problem ? $problem-&gt;latitude : $data-&gt;{latitude};
    my $lon = $problem ? $problem-&gt;longitude : $data-&gt;{longitude};

    my $closest = $self-&gt;SUPER::find_closest($data);

    ($lat, $lon) = map { Utils::truncate_coordinate($_) } $lat, $lon;
    my $j = mySociety::MaPit::call('nearest', "4326/$lon,$lat");
    if ($j-&gt;{postcode}) {
        $closest-&gt;{postcode} = $j-&gt;{postcode};
    }

    return $closest;
}

sub reports_body_check {
    my ( $self, $c, $code ) = @_;

    # Deal with Bexley and Greenwich name not starting with short name
    if ($code =~ /bexley|greenwich/i) {
        my $body = $c-&gt;model('DB::Body')-&gt;search( { name =&gt; { -like =&gt; "%$code%" } } )-&gt;single;
        $c-&gt;stash-&gt;{body} = $body;
        return $body;
    }

    # Manual misspelling redirect
    if ($code =~ /^rhondda cynon taff$/i) {
        my $url = $c-&gt;uri_for( '/reports/Rhondda+Cynon+Taf' );
        $c-&gt;res-&gt;redirect( $url );
        $c-&gt;detach();
    }

    # Old ONS codes
    if ($code =~ /^(\d\d)([a-z]{2})?([a-z]{2})?$/i) {
        my $area = mySociety::MaPit::call( 'area', uc $code );
        $c-&gt;detach( 'redirect_index' ) if $area-&gt;{error}; # Given a bad/old ONS code
        if (length($code) == 6) {
            my $council = mySociety::MaPit::call( 'area', $area-&gt;{parent_area} );
            $c-&gt;stash-&gt;{ward} = $area;
            $c-&gt;stash-&gt;{body} = $council;
        } else {
            $c-&gt;stash-&gt;{body} = $area;
        }
        $c-&gt;detach( 'redirect_body' );
    }

    # New ONS codes
    if ($code =~ /^[ESWN]\d{8}$/i) {
        my $area = mySociety::MaPit::call( 'area', uc $code );
        $c-&gt;detach( 'redirect_index' ) if $area-&gt;{error}; # Given a bad/old ONS code
        if ($code =~ /^(E05|W05|S13)/) {
            my $council = mySociety::MaPit::call( 'area', $area-&gt;{parent_area} );
            $c-&gt;stash-&gt;{ward} = $area;
            $c-&gt;stash-&gt;{body} = $council;
            $c-&gt;detach( 'redirect_body' );
        } elsif ($code =~ /^(W06|S12|E0[6-9]|E10)/) {
            $c-&gt;stash-&gt;{body} = $area;
            $c-&gt;detach( 'redirect_body' );
        }
    }

    return;
}

sub munge_body_areas_practical {
    my ($self, $body, $area_ids) = @_;

    if (@$area_ids == 4 &amp;&amp; $area_ids-&gt;[1] == 2488) { # Brent
        @$area_ids = (2488);
    } elsif ($area_ids-&gt;[0] == 2487 &amp;&amp; $area_ids-&gt;[1] == 2488) { # Harrow
        @$area_ids = (2487);
    } elsif ($area_ids-&gt;[0] == 2488 &amp;&amp; $area_ids-&gt;[1] == 2489) { # Barnet
        @$area_ids = (2489);
    } elsif ($area_ids-&gt;[0] == 2488 &amp;&amp; $area_ids-&gt;[1] == 2505) { # Camden
        @$area_ids = (2505);
    } elsif (@$area_ids == 3 &amp;&amp; $area_ids-&gt;[0] == 2561) { # Bristol
        @$area_ids = (2561);
    }
}

sub council_rss_alert_options {
    my $self = shift;
    my $all_areas = shift;
    my $c = shift;

    my %councils = map { $_ =&gt; 1 } @{$self-&gt;area_types};

    my $num_councils = scalar keys %$all_areas;

    my ( @options, @reported_to_options );
    if ( $num_councils == 1 or $num_councils == 2 ) {
        my ($council, $ward);
        my $body = FixMyStreet::DB-&gt;resultset('Body')-&gt;active-&gt;search(
            {
                name =&gt; { -not_in =&gt; ['TfL', 'National Highways'] }
            })-&gt;for_areas(keys %$all_areas)-&gt;first;
        foreach (values %$all_areas) {
            if ($councils{$_-&gt;{type}}) {
                $council = $_;
                $council-&gt;{id} = $body-&gt;id; # Want to use body ID, not MapIt area ID
                $council-&gt;{short_name} = $self-&gt;short_name( $council );
                ( $council-&gt;{id_name} = $council-&gt;{short_name} ) =~ tr/+/_/;
            } else {
                $ward = $_;
                $ward-&gt;{short_name} = $self-&gt;short_name( $ward );
                ( $ward-&gt;{id_name} = $ward-&gt;{short_name} ) =~ tr/+/_/;
            }
        }
        $council-&gt;{name} = 'London Borough of Bromley'
            if $council-&gt;{name} eq 'Bromley Council';

        my $council_text;
        if ( $c-&gt;cobrand-&gt;is_council ) {
            $council_text = 'All problems within the council';
        } else {
            $council_text = sprintf( _('Problems within %s'), $council-&gt;{name});
        }

        push @options, {
            type      =&gt; 'council',
            id        =&gt; sprintf( 'council:%s:%s', $council-&gt;{id}, $council-&gt;{id_name} ),
            text      =&gt; $council_text,
            rss_text  =&gt; sprintf( _('RSS feed of problems within %s'), $council-&gt;{name}),
            uri       =&gt; $c-&gt;uri_for( '/rss/reports/' . $council-&gt;{short_name} ),
        };
        push @options, {
            type     =&gt; 'ward',
            id       =&gt; sprintf( 'ward:%s:%s:%s:%s', $council-&gt;{id}, $ward-&gt;{id}, $council-&gt;{id_name}, $ward-&gt;{id_name} ),
            rss_text =&gt; sprintf( _('RSS feed of problems within %s ward'), $ward-&gt;{name}),
            text     =&gt; sprintf( _('Problems within %s ward'), $ward-&gt;{name}),
            uri      =&gt; $c-&gt;uri_for( '/rss/reports/' . $council-&gt;{short_name} . '/' . $ward-&gt;{short_name} ),
        } if $ward;

    } elsif ( $num_councils == 4 ) {
        # Two-tier council
        my ($county, $district, $c_ward, $d_ward);
        foreach (values %$all_areas) {
            $_-&gt;{short_name} = $self-&gt;short_name( $_ );
            ( $_-&gt;{id_name} = $_-&gt;{short_name} ) =~ tr/+/_/;
            if ($_-&gt;{type} eq 'CTY') {
                $county = $_;
            } elsif ($_-&gt;{type} eq 'DIS') {
                $district = $_;
            } elsif ($_-&gt;{type} eq 'CED') {
                $c_ward = $_;
            } elsif ($_-&gt;{type} eq 'DIW') {
                $d_ward = $_;
            }
        }
        my $district_name = $district-&gt;{name};
        my $d_ward_name = $d_ward-&gt;{name};
        my $county_name = $county-&gt;{name};
        my $c_ward_name = $c_ward-&gt;{name};

        my $body_dis = FixMyStreet::DB-&gt;resultset('Body')-&gt;active-&gt;for_areas($district-&gt;{id})-&gt;first;
        my $body_cty = FixMyStreet::DB-&gt;resultset('Body')-&gt;active-&gt;for_areas($county-&gt;{id})-&gt;first;

        push @options, {
            type  =&gt; 'area',
            id    =&gt; sprintf( 'area:%s:%s', $district-&gt;{id}, $district-&gt;{id_name} ),
            text  =&gt; sprintf( _('Problems within %s'), $district_name ),
            rss_text =&gt; sprintf( _('RSS feed for %s'), $district_name ),
            uri =&gt; $c-&gt;uri_for( '/rss/area/' . $district-&gt;{short_name}  )
        }, {
            type      =&gt; 'area',
            id        =&gt; sprintf( 'area:%s:%s:%s:%s', $district-&gt;{id}, $d_ward-&gt;{id}, $district-&gt;{id_name}, $d_ward-&gt;{id_name} ),
            text      =&gt; sprintf( _('Problems within %s ward, %s'), $d_ward_name, $district_name ),
            rss_text  =&gt; sprintf( _('RSS feed for %s ward, %s'), $d_ward_name, $district_name ),
            uri       =&gt; $c-&gt;uri_for( '/rss/area/' . $district-&gt;{short_name} . '/' . $d_ward-&gt;{short_name} )
        }, {
            type  =&gt; 'area',
            id    =&gt; sprintf( 'area:%s:%s', $county-&gt;{id}, $county-&gt;{id_name} ),
            text  =&gt; sprintf( _('Problems within %s'), $county_name ),
            rss_text =&gt; sprintf( _('RSS feed for %s'), $county_name ),
            uri =&gt; $c-&gt;uri_for( '/rss/area/' . $county-&gt;{short_name}  )
        }, {
            type      =&gt; 'area',
            id        =&gt; sprintf( 'area:%s:%s:%s:%s', $county-&gt;{id}, $c_ward-&gt;{id}, $county-&gt;{id_name}, $c_ward-&gt;{id_name} ),
            text      =&gt; sprintf( _('Problems within %s ward, %s'), $c_ward_name, $county_name ),
            rss_text  =&gt; sprintf( _('RSS feed for %s ward, %s'), $c_ward_name, $county_name ),
            uri       =&gt; $c-&gt;uri_for( '/rss/area/' . $county-&gt;{short_name} . '/' . $c_ward-&gt;{short_name} )
        };

        push @reported_to_options, {
            type      =&gt; 'council',
            id        =&gt; sprintf( 'council:%s:%s', $body_dis-&gt;id, $district-&gt;{id_name} ),
            text      =&gt; sprintf( _('Reports sent to %s'), $district-&gt;{name} ),
            rss_text  =&gt; sprintf( _('RSS feed of %s'), $district-&gt;{name}),
            uri       =&gt; $c-&gt;uri_for( '/rss/reports/' . $district-&gt;{short_name} ),
        }, {
            type     =&gt; 'ward',
            id       =&gt; sprintf( 'ward:%s:%s:%s:%s', $body_dis-&gt;id, $d_ward-&gt;{id}, $district-&gt;{id_name}, $d_ward-&gt;{id_name} ),
            rss_text =&gt; sprintf( _('RSS feed of %s, within %s ward'), $district-&gt;{name}, $d_ward-&gt;{name}),
            text     =&gt; sprintf( _('Reports sent to %s, within %s ward'), $district-&gt;{name}, $d_ward-&gt;{name}),
            uri      =&gt; $c-&gt;uri_for( '/rss/reports/' . $district-&gt;{short_name} . '/' . $d_ward-&gt;{short_name} ),
        }
            if $body_dis;
        push @reported_to_options, {
            type      =&gt; 'council',
            id        =&gt; sprintf( 'council:%s:%s', $body_cty-&gt;id, $county-&gt;{id_name} ),
            text      =&gt; sprintf( _('Reports sent to %s'), $county-&gt;{name} ),
            rss_text  =&gt; sprintf( _('RSS feed of %s'), $county-&gt;{name}),
            uri       =&gt; $c-&gt;uri_for( '/rss/reports/' . $county-&gt;{short_name} ),
        }, {
            type     =&gt; 'ward',
            id       =&gt; sprintf( 'ward:%s:%s:%s:%s', $body_cty-&gt;id, $c_ward-&gt;{id}, $county-&gt;{id_name}, $c_ward-&gt;{id_name} ),
            rss_text =&gt; sprintf( _('RSS feed of %s, within %s ward'), $county-&gt;{name}, $c_ward-&gt;{name}),
            text     =&gt; sprintf( _('Reports sent to %s, within %s ward'), $county-&gt;{name}, $c_ward-&gt;{name}),
            uri      =&gt; $c-&gt;uri_for( '/rss/reports/' . $county-&gt;{short_name} . '/' . $c_ward-&gt;{short_name} ),
        }
            if $body_cty;

    } else {
        throw Error::Simple('An area with three tiers of council? Impossible! '. join('|',keys %$all_areas));
    }

    return ( \@options, @reported_to_options ? \@reported_to_options : undef );
}

sub report_check_for_errors {
    my $self = shift;
    my $c = shift;

    my %errors = $self-&gt;next::method($c);

    my $report = $c-&gt;stash-&gt;{report};

    if (!$errors{name} &amp;&amp; (length($report-&gt;name) &lt; 5
        || $report-&gt;name !~ m/\s/
        || $report-&gt;name =~ m/\ba\s*n+on+((y|o)mo?u?s)?(ly)?\b/i))
    {
        $errors{name} = _(
'Please enter your full name, councils need this information â€“ if you do not wish your name to be shown on the site, untick the box below'
        );
    }

    my $cobrand = $self-&gt;get_body_handler_for_problem($report);
    if ( $cobrand-&gt;can('report_validation') ) {
        $cobrand-&gt;report_validation( $report, \%errors );
    }

    return %errors;
}

=head2 get_body_handler_for_problem

Returns a cobrand for the body that a problem was sent to.

    my $handler = $cobrand-&gt;get_body_handler_for_problem($row);
    my $handler = $cobrand_class-&gt;get_body_handler_for_problem($row);

If body in bodies_str has a cobrand set in its extra metadata then an instance of that
cobrand class is returned, otherwise the default FixMyStreet cobrand is used.

=cut

sub get_body_handler_for_problem {
    my ($self, $row) = @_;

    # Do not do anything for National Highways here, as we don't want it to
    # treat this as a cobrand for e.g. submit report emails made on .com
    my @bodies = grep { $_-&gt;name !~ /National Highways/ } values %{$row-&gt;bodies};

    for my $body ( @bodies ) {
        my $cobrand = $body-&gt;get_cobrand_handler;
        return $cobrand if $cobrand;
    }

    return ref $self ? $self : $self-&gt;new;
}

=head2 link_to_council_cobrand

If a problem was sent to a UK council who has a FMS cobrand and the report is
currently being viewed on a different cobrand, then link the council's name to
that problem on the council's cobrand.

=cut

sub link_to_council_cobrand {
    my ( $self, $problem ) = @_;
    # If the report was sent to a cobrand that we're not currently on,
    # include a link to view it on the responsible cobrand.
    # This only occurs if the report was sent to a single body and we're not already
    # using the body name as a link to all problem reports.
    my $handler = $self-&gt;get_body_handler_for_problem($problem);
    $self-&gt;{c}-&gt;log-&gt;debug( sprintf "bodies: %s areas: %s self: %s handler: %s", $problem-&gt;bodies_str, $problem-&gt;areas, $self-&gt;moniker, $handler-&gt;moniker );
    my $bodies_str_ids = $problem-&gt;bodies_str_ids;
    if ( !FixMyStreet-&gt;config('AREA_LINKS_FROM_PROBLEMS') &amp;&amp;
         scalar(@$bodies_str_ids) == 1 &amp;&amp; $handler-&gt;is_council &amp;&amp;
         $handler-&gt;moniker ne $self-&gt;{c}-&gt;cobrand-&gt;moniker
       ) {
        my $url = sprintf("%s%s", $handler-&gt;base_url, $problem-&gt;url);
        return sprintf("&lt;a href='%s'&gt;%s&lt;/a&gt;", $url, $problem-&gt;body);
    } else {
        return $problem-&gt;body(0);
    }
}

sub lookup_by_ref_regex {
    return qr/^\s*(\d+)\s*$/;
}

sub report_new_munge_before_insert {
    my ($self, $report) = @_;

    if ($report-&gt;to_body_named('TfL')) {
        my $tfl = FixMyStreet::Cobrand-&gt;get_class_for_moniker('tfl')-&gt;new();
        $tfl-&gt;report_new_munge_before_insert($report);
    }
}

sub report_new_munge_after_insert {
    my ($self, $report) = @_;

    if ($report-&gt;to_body_named('National Highways')) {
        FixMyStreet::Cobrand::HighwaysEngland::report_new_munge_after_insert($self, $report);
    }
}

# Allow cobrands to disallow updates on some things.
# Note this only ever locks down more than the default.
sub updates_disallowed {
    my $self = shift;
    my ($problem) = @_;
    my $c = $self-&gt;{c};

    # If closed due to problem/category closure, want that to take precedence
    my $parent = $self-&gt;next::method(@_);
    return $parent if $parent;

    my $cfg = $self-&gt;feature('updates_allowed') || '';

    my $body_user = $c-&gt;user_exists &amp;&amp; $c-&gt;user-&gt;from_body &amp;&amp; $c-&gt;user-&gt;from_body-&gt;name eq $self-&gt;council_name;
    return $self-&gt;_updates_disallowed_check($cfg, $problem, $body_user);
}

sub _updates_disallowed_check {
    my ($self, $cfg, $problem, $body_user) = @_;

    my $c = $self-&gt;{c};
    my $superuser = $c-&gt;user_exists &amp;&amp; $c-&gt;user-&gt;is_superuser;
    my $staff = $body_user || $superuser;
    my $reporter = $c-&gt;user_exists &amp;&amp; $c-&gt;user-&gt;id == $problem-&gt;user-&gt;id;
    my $open = !($problem-&gt;is_fixed || $problem-&gt;is_closed);
    my $body_comment_user = $self-&gt;body &amp;&amp; $self-&gt;body-&gt;comment_user_id &amp;&amp; $problem-&gt;user_id == $self-&gt;body-&gt;comment_user_id;

    if ($cfg eq 'none') {
        return $cfg;
    } elsif ($cfg eq 'staff') {
        # Only staff and superusers can leave updates
        return $cfg unless $staff;
    } elsif ($cfg eq 'open') {
        return $cfg unless $open;
    } elsif ($cfg eq 'reporter') {
        return $cfg unless $reporter;
    } elsif ($cfg eq 'reporter-open') {
        return $cfg unless $reporter &amp;&amp; $open;
    } elsif ($cfg eq 'reporter/staff') {
        return $cfg unless $reporter || $staff;
    } elsif ($cfg eq 'reporter/staff-open') {
        return $cfg unless ($reporter || $staff) &amp;&amp; $open;
    } elsif ($cfg eq 'notopen311') {
        return $cfg unless !$body_comment_user;
    } elsif ($cfg eq 'notopen311-open') {
        return $cfg unless !$body_comment_user &amp;&amp; $open;
    } elsif ($cfg eq 'reporter-not-open/staff-open') {
        return $cfg unless ( $reporter &amp;&amp; !$open ) || ( $staff &amp;&amp; $open );
    }
    return '';
}

# Report if cobrand denies updates by user
sub deny_updates_by_user {
    my ($self, $row) = @_;
    my $cfg = $self-&gt;feature('updates_allowed') || '';
    if ($cfg eq 'none' || $cfg eq 'staff') {
        return 1;
    } elsif ($cfg eq 'reporter-open' &amp;&amp; !$row-&gt;is_open) {
        return 1;
    } elsif ($cfg eq 'open' &amp;&amp; !$row-&gt;is_open) {
        return 1;
    } else {
        return;
    }
};

# To use recaptcha, add a RECAPTCHA key to your config, with subkeys secret and
# site_key, taken from the recaptcha site. This shows it to non-UK IP addresses
# on alert and report pages.

sub requires_recaptcha {
    my $self = shift;
    my $c = $self-&gt;{c};

    return 0 if $c-&gt;user_exists;
    return 0 if !FixMyStreet-&gt;config('RECAPTCHA');
    return 0 unless $c-&gt;action =~ /^(alert|report|around|contact)/;
    return 0 if $c-&gt;user_country eq 'GB';
    return 1;
}

sub check_recaptcha {
    my $self = shift;
    my $c = $self-&gt;{c};

    return unless $self-&gt;requires_recaptcha;

    my $url = 'https://www.google.com/recaptcha/api/siteverify';
    my $res = LWP::UserAgent-&gt;new-&gt;post($url, {
        secret =&gt; FixMyStreet-&gt;config('RECAPTCHA')-&gt;{secret},
        response =&gt; $c-&gt;get_param('g-recaptcha-response'),
        remoteip =&gt; $c-&gt;req-&gt;address,
    });
    $res = decode_json($res-&gt;content);
    $c-&gt;detach('/page_error_400_bad_request', ['Bad recaptcha'])
        unless $res-&gt;{success};
}

sub public_holidays {
    my $nation = shift || 'england-and-wales';
    my $json = _get_bank_holiday_json();
    return [ map { $_-&gt;{date} } @{$json-&gt;{$nation}{events}} ];
}

sub is_public_holiday {
    my %args = @_;
    $args{date} ||= localtime;
    $args{date} = $args{date}-&gt;date;
    $args{nation} ||= 'england-and-wales';
    my $json = _get_bank_holiday_json();
    for my $event (@{$json-&gt;{$args{nation}}{events}}) {
        if ($event-&gt;{date} eq $args{date}) {
            return 1;
        }
    }
}

sub _get_bank_holiday_json {
    my $file = 'bank-holidays.json';
    my $cache_file = path(FixMyStreet-&gt;path_to("../data/$file"));
    my $js;
    # uncoverable branch true
    if (-s $cache_file &amp;&amp; -M $cache_file &lt;= 7 &amp;&amp; !FixMyStreet-&gt;test_mode) {
        # uncoverable statement
        $js = $cache_file-&gt;slurp_utf8;
    } else {
        $js = _fetch_url("https://www.gov.uk/$file");
        # uncoverable branch false
        $js = decode_utf8($js) if !utf8::is_utf8($js);
        # uncoverable branch true
        if ($js &amp;&amp; !FixMyStreet-&gt;test_mode) {
            # uncoverable statement
            $cache_file-&gt;spew_utf8($js);
        }
    }
    $js = JSON-&gt;new-&gt;decode($js) if $js;
    return $js;
}

sub _fetch_url {
    my $url = shift;
    my $ua = LWP::UserAgent-&gt;new;
    $ua-&gt;timeout(5);
    return if FixMyStreet-&gt;test_mode;
    # uncoverable statement
    $ua-&gt;get($url)-&gt;content;
}

sub ooh_times {
    my ($self, $body) = @_;
    my $times = $body-&gt;get_extra_metadata("ooh_times");
    return FixMyStreet::OutOfHours-&gt;new(times =&gt; $times, holidays =&gt; public_holidays());
}

# UK council dashboard summary/heatmap access

sub dashboard_body {
    my $self = shift;
    my $c = $self-&gt;{c};
    my $body = $c-&gt;user-&gt;from_body || $self-&gt;_user_to_body;
    return $body;
}

sub _user_to_body {
    my $self = shift;
    my $c = $self-&gt;{c};
    my $email = lc $c-&gt;user-&gt;email;
    return $self-&gt;_email_to_body($email);
}

sub _email_to_body {
    my ($self, $email) = @_;
    my $c = $self-&gt;{c};
    my ($domain) = $email =~ m{ @ (.*) \z }x;

    my @data = eval { FixMyStreet-&gt;path_to('../data/fixmystreet-councils.csv')-&gt;slurp };
    my $body;
    foreach (@data) {
        chomp;
        my ($d, $b) = split /\|/;
        if ($d eq $domain || $d eq $email) {
            $body = $b;
            last;
        }
    }
    # If we didn't find a lookup entry, default to the first part of the domain
    unless ($body) {
        $domain =~ s/\.gov\.uk$//;
        $body = ucfirst $domain;
    }

    $body = $c-&gt;forward('/reports/body_find', [ $body ]);
    return $body;
}

1;
</pre></body></html>