aboutsummaryrefslogtreecommitdiffstats
path: root/perllib
diff options
context:
space:
mode:
Diffstat (limited to 'perllib')
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Social.pm34
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm3
-rw-r--r--perllib/FixMyStreet/Cobrand/Hackney.pm187
-rw-r--r--perllib/FixMyStreet/Cobrand/UKCouncils.pm2
-rw-r--r--perllib/FixMyStreet/Cobrand/Zurich.pm2
-rw-r--r--perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm2
-rw-r--r--perllib/FixMyStreet/Email.pm4
-rw-r--r--perllib/FixMyStreet/Geocode/OSM.pm1
-rw-r--r--perllib/FixMyStreet/Queue/Item/Report.pm2
-rw-r--r--perllib/FixMyStreet/Script/Alerts.pm2
-rw-r--r--perllib/FixMyStreet/SendReport/Email.pm5
-rw-r--r--perllib/OIDC/Lite/Client/WebServer/AuthCodeFlow.pm (renamed from perllib/OIDC/Lite/Client/WebServer/Azure.pm)13
-rw-r--r--perllib/Open311/PopulateServiceList.pm8
-rwxr-xr-xperllib/Open311/PostServiceRequestUpdates.pm2
14 files changed, 244 insertions, 23 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Auth/Social.pm b/perllib/FixMyStreet/App/Controller/Auth/Social.pm
index 06e67573f..ce94fe256 100644
--- a/perllib/FixMyStreet/App/Controller/Auth/Social.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth/Social.pm
@@ -6,7 +6,7 @@ BEGIN { extends 'Catalyst::Controller'; }
use Net::Facebook::Oauth2;
use Net::Twitter::Lite::WithAPIv1_1;
-use OIDC::Lite::Client::WebServer::Azure;
+use OIDC::Lite::Client::WebServer::AuthCodeFlow;
use URI::Escape;
use mySociety::AuthToken;
@@ -167,7 +167,7 @@ sub oidc : Private {
my $config = $c->cobrand->feature('oidc_login');
- OIDC::Lite::Client::WebServer::Azure->new(
+ OIDC::Lite::Client::WebServer::AuthCodeFlow->new(
id => $config->{client_id},
secret => $config->{secret},
authorize_uri => $config->{auth_uri},
@@ -179,7 +179,9 @@ sub oidc_sign_in : Private {
my ( $self, $c ) = @_;
$c->detach( '/page_error_403_access_denied', [] ) if FixMyStreet->config('SIGNUPS_DISABLED');
- $c->detach( '/page_error_400_bad_request', [] ) unless $c->cobrand->feature('oidc_login');
+
+ my $cfg = $c->cobrand->feature('oidc_login');
+ $c->detach( '/page_error_400_bad_request', [] ) unless $cfg;
my $oidc = $c->forward('oidc');
my $nonce = $self->generate_nonce();
@@ -190,6 +192,15 @@ sub oidc_sign_in : Private {
extra => {
response_mode => 'form_post',
nonce => $nonce,
+ # auth_extra_params provides a way to pass custom parameters
+ # to the OIDC endpoint for the intial authentication request.
+ # This allows, for example, a custom scope to be used,
+ # or the `hd` parameter which customises the appearance of
+ # the login form.
+ # This is primarily useful for Google G Suite authentication - see
+ # available parameters here:
+ # https://developers.google.com/identity/protocols/oauth2/openid-connect#authenticationuriparameters
+ %{ $cfg->{auth_extra_params} || {} } ,
},
);
@@ -201,14 +212,14 @@ sub oidc_sign_in : Private {
# The OIDC endpoint may require a specific URI to be called to log the user
# out when they log out of FMS.
- if ( my $redirect_uri = $c->cobrand->feature('oidc_login')->{logout_uri} ) {
+ if ( my $redirect_uri = $cfg->{logout_uri} ) {
$redirect_uri .= "?post_logout_redirect_uri=";
$redirect_uri .= URI::Escape::uri_escape( $c->uri_for('/auth/sign_out') );
$oauth{logout_redirect_uri} = $redirect_uri;
}
# The OIDC endpoint may provide a specific URI for changing the user's password.
- if ( my $password_change_uri = $c->cobrand->feature('oidc_login')->{password_change_uri} ) {
+ if ( my $password_change_uri = $cfg->{password_change_uri} ) {
$oauth{change_password_uri} = $oidc->uri_to_redirect(
uri => $password_change_uri,
redirect_uri => $c->uri_for('/auth/OIDC'),
@@ -279,6 +290,7 @@ sub oidc_callback: Path('/auth/OIDC') : Args(0) {
eval {
$id_token = $oidc->get_access_token(
code => $c->get_param('code'),
+ redirect_uri => $c->uri_for('/auth/OIDC')
);
};
if ($@) {
@@ -294,10 +306,18 @@ sub oidc_callback: Path('/auth/OIDC') : Args(0) {
# check that the nonce matches what we set in the user session
$c->detach('/page_error_500_internal_error', ['invalid id_token']) unless $id_token->payload->{nonce} eq $c->session->{oauth}{nonce};
+ if (my $domains = $c->cobrand->feature('oidc_login')->{allowed_domains}) {
+ # Check that the hd payload is present in the token and matches the
+ # list of allowed domains from the config
+ my $hd = $id_token->payload->{hd};
+ my %allowed_domains = map { $_ => 1} @$domains;
+ $c->detach('oauth_failure') unless $allowed_domains{$hd};
+ }
+
# Some claims need parsing into a friendlier format
- # XXX check how much of this is Westminster/Azure-specific
- my $name = join(" ", $id_token->payload->{given_name}, $id_token->payload->{family_name});
+ my $name = $id_token->payload->{name} || join(" ", $id_token->payload->{given_name}, $id_token->payload->{family_name});
my $email = $id_token->payload->{email};
+
# WCC Azure provides a single email address as an array for some reason
my $emails = $id_token->payload->{emails};
if ($emails && @$emails) {
diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm
index 73340338b..bd8b7e4b4 100644
--- a/perllib/FixMyStreet/Cobrand/Default.pm
+++ b/perllib/FixMyStreet/Cobrand/Default.pm
@@ -957,9 +957,10 @@ Get stats to display on the council reports page
sub get_report_stats { return 0; }
sub get_body_sender {
- my ( $self, $body, $category ) = @_;
+ my ( $self, $body, $problem ) = @_;
# look up via category
+ my $category = $problem->category;
my $contact = $body->contacts->search( { category => $category } )->first;
if ( $body->can_be_devolved && $contact && $contact->send_method ) {
return { method => $contact->send_method, config => $contact, contact => $contact };
diff --git a/perllib/FixMyStreet/Cobrand/Hackney.pm b/perllib/FixMyStreet/Cobrand/Hackney.pm
new file mode 100644
index 000000000..234573e3e
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/Hackney.pm
@@ -0,0 +1,187 @@
+package FixMyStreet::Cobrand::Hackney;
+use parent 'FixMyStreet::Cobrand::Whitelabel';
+
+use strict;
+use warnings;
+use mySociety::EmailUtil qw(is_valid_email);
+
+sub council_area_id { return 2508; }
+sub council_area { return 'Hackney'; }
+sub council_name { return 'Hackney Council'; }
+sub council_url { return 'hackney'; }
+sub send_questionnaires { 0 }
+
+sub disambiguate_location {
+ my $self = shift;
+ my $string = shift;
+
+ my $town = 'Hackney';
+
+ # Teale Street is on the boundary with Tower Hamlets and
+ # shows the 'please use fixmystreet.com' message, but Hackney
+ # do provide services on that road.
+ ($string, $town) = ('E2 9AA', '') if $string =~ /^teale\s+st/i;
+
+ return {
+ %{ $self->SUPER::disambiguate_location() },
+ string => $string,
+ town => $town,
+ centre => '51.552267,-0.063316',
+ bounds => [ 51.519814, -0.104511, 51.577784, -0.016527 ],
+ };
+}
+
+sub do_not_reply_email { shift->feature('do_not_reply_email') }
+
+sub verp_email_domain { shift->feature('verp_email_domain') }
+
+sub get_geocoder {
+ return 'OSM'; # default of Bing gives poor results, let's try overriding.
+}
+
+sub geocoder_munge_query_params {
+ my ($self, $params) = @_;
+
+ $params->{addressdetails} = 1;
+}
+
+sub geocoder_munge_results {
+ my ($self, $result) = @_;
+ if (my $a = $result->{address}) {
+ if ($a->{road} && $a->{suburb} && $a->{postcode}) {
+ $result->{display_name} = "$a->{road}, $a->{suburb}, $a->{postcode}";
+ return;
+ }
+ }
+ $result->{display_name} = '' unless $result->{display_name} =~ /Hackney/;
+ $result->{display_name} =~ s/, United Kingdom$//;
+ $result->{display_name} =~ s/, London, Greater London, England//;
+ $result->{display_name} =~ s/, London Borough of Hackney//;
+}
+
+
+sub open311_config {
+ my ($self, $row, $h, $params) = @_;
+
+ $params->{multi_photos} = 1;
+}
+
+sub open311_extra_data {
+ my ($self, $row, $h, $extra, $contact) = @_;
+
+ my $open311_only = [
+ { name => 'report_url',
+ value => $h->{url} },
+ { name => 'title',
+ value => $row->title },
+ { name => 'description',
+ value => $row->detail },
+ { name => 'category',
+ value => $row->category },
+ ];
+
+ # Make sure contact 'email' set correctly for Open311
+ if (my $sent_to = $row->get_extra_metadata('sent_to')) {
+ $row->unset_extra_metadata('sent_to');
+ my $code = $sent_to->{$contact->email};
+ $contact->email($code) if $code;
+ }
+
+ return $open311_only;
+}
+
+sub map_type { 'OSM' }
+
+sub default_map_zoom { 5 }
+
+sub admin_user_domain { 'hackney.gov.uk' }
+
+sub social_auth_enabled {
+ my $self = shift;
+
+ return $self->feature('oidc_login') ? 1 : 0;
+}
+
+sub anonymous_account {
+ my $self = shift;
+ return {
+ email => $self->feature('anonymous_account') . '@' . $self->admin_user_domain,
+ name => 'Anonymous user',
+ };
+}
+
+sub open311_skip_existing_contact {
+ my ($self, $contact) = @_;
+
+ # For Hackney we want the 'protected' flag to prevent any changes to this
+ # contact at all.
+ return $contact->get_extra_metadata("open311_protect") ? 1 : 0;
+}
+
+sub open311_filter_contacts_for_deletion {
+ my ($self, $contacts) = @_;
+
+ # Don't delete open311 protected contacts when importing
+ return $contacts->search({
+ extra => { -not_like => '%T15:open311_protect,I1:1%' },
+ });
+}
+
+sub problem_is_within_area_type {
+ my ($self, $problem, $type) = @_;
+ my $layer_map = {
+ park => "greenspaces:hackney_park",
+ estate => "housing:lbh_estate",
+ };
+ my $layer = $layer_map->{$type};
+ return unless $layer;
+
+ my ($x, $y) = $problem->local_coords;
+
+ my $cfg = {
+ url => "https://map.hackney.gov.uk/geoserver/wfs",
+ srsname => "urn:ogc:def:crs:EPSG::27700",
+ typename => $layer,
+ outputformat => "json",
+ filter => "<Filter xmlns:gml=\"http://www.opengis.net/gml\"><Intersects><PropertyName>geom</PropertyName><gml:Point srsName=\"27700\"><gml:coordinates>$x,$y</gml:coordinates></gml:Point></Intersects></Filter>",
+ };
+
+ my $features = $self->_fetch_features($cfg, $x, $y) || [];
+ return scalar @$features ? 1 : 0;
+}
+
+sub get_body_sender {
+ my ( $self, $body, $problem ) = @_;
+
+ my $contact = $body->contacts->search( { category => $problem->category } )->first;
+
+ my $parts = join '\s*', qw(^ park : (.*?) ; estate : (.*?) ; other : (.*?) $);
+ my $regex = qr/$parts/i;
+ if (my ($park, $estate, $other) = $contact->email =~ $regex) {
+ my $to = $other;
+ if ($self->problem_is_within_area_type($problem, 'park')) {
+ $to = $park;
+ } elsif ($self->problem_is_within_area_type($problem, 'estate')) {
+ $to = $estate;
+ }
+ $problem->set_extra_metadata(sent_to => { $contact->email => $to });
+ if (is_valid_email($to)) {
+ return { method => 'Email', contact => $contact };
+ }
+ }
+ return $self->SUPER::get_body_sender($body, $problem);
+}
+
+# Translate email address to actual delivery address
+sub munge_sendreport_params {
+ my ($self, $row, $h, $params) = @_;
+
+ my $sent_to = $row->get_extra_metadata('sent_to') or return;
+ $row->unset_extra_metadata('sent_to');
+ for my $recip (@{$params->{To}}) {
+ my ($email, $name) = @$recip;
+ $recip->[0] = $sent_to->{$email} if $sent_to->{$email};
+ }
+}
+
+1;
diff --git a/perllib/FixMyStreet/Cobrand/UKCouncils.pm b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
index 21dd2d455..7456d9ddf 100644
--- a/perllib/FixMyStreet/Cobrand/UKCouncils.pm
+++ b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
@@ -392,7 +392,7 @@ sub _fetch_features_url {
SRSNAME => $cfg->{srsname},
TYPENAME => $cfg->{typename},
VERSION => "1.1.0",
- outputformat => "geojson",
+ outputformat => $cfg->{outputformat} || "geojson",
$cfg->{filter} ? ( Filter => $cfg->{filter} ) : ( BBOX => $cfg->{bbox} ),
);
diff --git a/perllib/FixMyStreet/Cobrand/Zurich.pm b/perllib/FixMyStreet/Cobrand/Zurich.pm
index 3cf678f9c..42932ec41 100644
--- a/perllib/FixMyStreet/Cobrand/Zurich.pm
+++ b/perllib/FixMyStreet/Cobrand/Zurich.pm
@@ -217,7 +217,7 @@ sub allow_photo_display {
}
sub get_body_sender {
- my ( $self, $body, $category ) = @_;
+ my ( $self, $body, $problem ) = @_;
return { method => 'Zurich' };
}
diff --git a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
index 1805e1fd2..6d14e6a9f 100644
--- a/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
+++ b/perllib/FixMyStreet/DB/Result/ModerationOriginalData.pm
@@ -163,7 +163,7 @@ sub compare_extra {
my $new = $other->get_extra_metadata;
my $both = { %$old, %$new };
- my @all_keys = sort keys %$both;
+ my @all_keys = grep { $_ ne 'sent_to' } sort keys %$both;
my @s;
foreach (@all_keys) {
if ($old->{$_} && $new->{$_}) {
diff --git a/perllib/FixMyStreet/Email.pm b/perllib/FixMyStreet/Email.pm
index 3d7b48539..0cc6a880c 100644
--- a/perllib/FixMyStreet/Email.pm
+++ b/perllib/FixMyStreet/Email.pm
@@ -79,7 +79,9 @@ sub _render_template {
}
sub unique_verp_id {
- sprintf('fms-%s@%s', generate_verp_token(@_), FixMyStreet->config('EMAIL_DOMAIN'));
+ my $parts = shift;
+ my $domain = shift || FixMyStreet->config('EMAIL_DOMAIN');
+ sprintf('fms-%s@%s', generate_verp_token(@$parts), $domain);
}
sub _unique_id {
diff --git a/perllib/FixMyStreet/Geocode/OSM.pm b/perllib/FixMyStreet/Geocode/OSM.pm
index 20e653cf6..06162d74c 100644
--- a/perllib/FixMyStreet/Geocode/OSM.pm
+++ b/perllib/FixMyStreet/Geocode/OSM.pm
@@ -45,6 +45,7 @@ sub string {
if $params->{bounds};
$query_params{countrycodes} = $params->{country}
if $params->{country};
+ $c->cobrand->call_hook(geocoder_munge_query_params => \%query_params);
$url .= join('&', map { "$_=$query_params{$_}" } sort keys %query_params);
$c->stash->{geocoder_url} = $url;
diff --git a/perllib/FixMyStreet/Queue/Item/Report.pm b/perllib/FixMyStreet/Queue/Item/Report.pm
index e38987838..60e9ad3dc 100644
--- a/perllib/FixMyStreet/Queue/Item/Report.pm
+++ b/perllib/FixMyStreet/Queue/Item/Report.pm
@@ -172,7 +172,7 @@ sub _create_reporters {
my @dear;
my %reporters = ();
while (my $body = $bodies->next) {
- my $sender_info = $self->cobrand->get_body_sender( $body, $row->category );
+ my $sender_info = $self->cobrand_handler->get_body_sender( $body, $row );
my $sender = "FixMyStreet::SendReport::" . $sender_info->{method};
if ( ! exists $self->senders->{ $sender } ) {
diff --git a/perllib/FixMyStreet/Script/Alerts.pm b/perllib/FixMyStreet/Script/Alerts.pm
index d07728092..03373a8cc 100644
--- a/perllib/FixMyStreet/Script/Alerts.pm
+++ b/perllib/FixMyStreet/Script/Alerts.pm
@@ -327,7 +327,7 @@ sub _send_aggregated_alert_email(%) {
} );
$data{unsubscribe_url} = $cobrand->base_url( $data{cobrand_data} ) . '/A/' . $token->token;
- my $sender = FixMyStreet::Email::unique_verp_id('alert', $data{alert_id});
+ my $sender = FixMyStreet::Email::unique_verp_id([ 'alert', $data{alert_id} ], $cobrand->call_hook('verp_email_domain'));
my $result = FixMyStreet::Email::send_cron(
$data{schema},
"$data{template}.txt",
diff --git a/perllib/FixMyStreet/SendReport/Email.pm b/perllib/FixMyStreet/SendReport/Email.pm
index 81a25f896..2d5e85f3e 100644
--- a/perllib/FixMyStreet/SendReport/Email.pm
+++ b/perllib/FixMyStreet/SendReport/Email.pm
@@ -53,10 +53,11 @@ sub send_from {
sub envelope_sender {
my ($self, $row) = @_;
+ my $cobrand = $row->get_cobrand_logged;
if ($row->user->email && $row->user->email_verified) {
- return FixMyStreet::Email::unique_verp_id('report', $row->id);
+ return FixMyStreet::Email::unique_verp_id([ 'report', $row->id ], $cobrand->call_hook('verp_email_domain'));
}
- return $row->get_cobrand_logged->do_not_reply_email;
+ return $cobrand->do_not_reply_email;
}
sub send {
diff --git a/perllib/OIDC/Lite/Client/WebServer/Azure.pm b/perllib/OIDC/Lite/Client/WebServer/AuthCodeFlow.pm
index b19dce90e..33a9a788f 100644
--- a/perllib/OIDC/Lite/Client/WebServer/Azure.pm
+++ b/perllib/OIDC/Lite/Client/WebServer/AuthCodeFlow.pm
@@ -1,4 +1,4 @@
-package OIDC::Lite::Client::WebServer::Azure;
+package OIDC::Lite::Client::WebServer::AuthCodeFlow;
use strict;
use warnings;
@@ -8,12 +8,15 @@ use OIDC::Lite::Client::IDTokenResponseParser;
=head1 NAME
-OIDC::Lite::Client::WebServer::Azure - extension to auth against Azure AD B2C
+OIDC::Lite::Client::WebServer::AuthCodeFlow - extension to auth against an
+identity provider using the authorization code flow, such as Azure AD B2C or
+Google OAuth 2.0.
+More info: https://openid.net/specs/openid-connect-core-1_0.html#CodeFlowSteps
OIDC::Lite doesn't appear to support the authorisation code flow to get an
-ID token - only an access token. Azure returns all its claims in the id_token
-and doesn't support a UserInfo endpoint, so this extension adds support for
-parsing the id_token when calling get_access_token.
+ID token - only an access token. This flow returns all its claims in the id_token
+(and may not support a UserInfo endpoint e.g. Azure AD B2C), so this extension
+adds support for parsing the id_token when calling get_access_token.
=cut
diff --git a/perllib/Open311/PopulateServiceList.pm b/perllib/Open311/PopulateServiceList.pm
index 20fca90b3..a3672770c 100644
--- a/perllib/Open311/PopulateServiceList.pm
+++ b/perllib/Open311/PopulateServiceList.pm
@@ -145,6 +145,8 @@ sub _handle_existing_contact {
my $service_name = $self->_normalize_service_name;
my $protected = $contact->get_extra_metadata("open311_protect");
+ return if $self->_current_body_cobrand && $self->_current_body_cobrand->call_hook(open311_skip_existing_contact => $contact);
+
print $self->_current_body->id . " already has a contact for service code " . $self->_current_service->{service_code} . "\n" if $self->verbose >= 2;
if ( $contact->state eq 'deleted' || $service_name ne $contact->category || $self->_current_service->{service_code} ne $contact->email ) {
@@ -370,7 +372,11 @@ sub _delete_contacts_not_in_service_list {
sub _delete_contacts_not_in_service_list_cobrand_overrides {
my ( $self, $found_contacts ) = @_;
- return $found_contacts;
+ if ($self->_current_body_cobrand && $self->_current_body_cobrand->can('open311_filter_contacts_for_deletion')) {
+ return $self->_current_body_cobrand->open311_filter_contacts_for_deletion($found_contacts);
+ } else {
+ return $found_contacts;
+ }
}
1;
diff --git a/perllib/Open311/PostServiceRequestUpdates.pm b/perllib/Open311/PostServiceRequestUpdates.pm
index d7345ea4d..a31bca8f7 100755
--- a/perllib/Open311/PostServiceRequestUpdates.pm
+++ b/perllib/Open311/PostServiceRequestUpdates.pm
@@ -50,7 +50,7 @@ sub open311_params {
my $conf = $body;
if ($comment) {
my $cobrand_logged = $comment->get_cobrand_logged;
- my $sender = $cobrand_logged->get_body_sender($body, $comment->problem->category);
+ my $sender = $cobrand_logged->get_body_sender($body, $comment->problem);
$conf = $sender->{config};
}