aboutsummaryrefslogtreecommitdiffstats
path: root/perllib/FixMyStreet
diff options
context:
space:
mode:
Diffstat (limited to 'perllib/FixMyStreet')
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth.pm46
-rw-r--r--perllib/FixMyStreet/App/Controller/Auth/Social.pm208
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/New.pm54
-rw-r--r--perllib/FixMyStreet/App/Controller/Report/Update.pm27
-rw-r--r--perllib/FixMyStreet/Cobrand/Default.pm21
-rw-r--r--perllib/FixMyStreet/Cobrand/UKCouncils.pm50
-rw-r--r--perllib/FixMyStreet/Cobrand/Westminster.pm148
-rw-r--r--perllib/FixMyStreet/DB/Result/User.pm52
8 files changed, 526 insertions, 80 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Auth.pm b/perllib/FixMyStreet/App/Controller/Auth.pm
index c194045b9..964d8f19a 100644
--- a/perllib/FixMyStreet/App/Controller/Auth.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth.pm
@@ -44,13 +44,12 @@ sub general : Path : Args(0) {
# decide which action to take
$c->detach('code_sign_in') if $clicked_sign_in_by_code || ($data_email && !$data_password);
- if (!$data_username && !$data_password && !$data_email) {
- $c->detach('social/facebook_sign_in') if $c->get_param('facebook_sign_in');
- $c->detach('social/twitter_sign_in') if $c->get_param('twitter_sign_in');
+ if (!$data_username && !$data_password && !$data_email && $c->get_param('social_sign_in')) {
+ $c->forward('social/handle_sign_in');
}
- $c->forward( 'sign_in', [ $data_username ] )
- && $c->detach( 'redirect_on_signin', [ $c->get_param('r') ] );
+ $c->forward( 'sign_in', [ $data_username ] )
+ && $c->detach( 'redirect_on_signin', [ $c->get_param('r') ] );
}
@@ -180,10 +179,13 @@ sub email_sign_in : Private {
name => $c->get_param('name'),
password => $user->password,
};
- $token_data->{facebook_id} = $c->session->{oauth}{facebook_id}
- if $c->get_param('oauth_need_email') && $c->session->{oauth}{facebook_id};
- $token_data->{twitter_id} = $c->session->{oauth}{twitter_id}
- if $c->get_param('oauth_need_email') && $c->session->{oauth}{twitter_id};
+
+ if ($c->get_param('oauth_need_email')) {
+ $token_data->{name} = $c->session->{oauth}{name}
+ if $c->session->{oauth}{name} && !$token_data->{name};
+ $c->forward('set_oauth_token_data', [ $token_data ]);
+ }
+
if ($c->stash->{current_user}) {
$token_data->{old_user_id} = $c->stash->{current_user}->id;
$token_data->{r} = 'auth/change_email/success';
@@ -214,6 +216,14 @@ sub get_token : Private {
return $data;
}
+sub set_oauth_token_data : Private {
+ my ( $self, $c, $token_data ) = @_;
+
+ foreach (qw/facebook_id twitter_id oidc_id extra logout_redirect_uri/) {
+ $token_data->{$_} = $c->session->{oauth}{$_} if $c->session->{oauth}{$_};
+ }
+}
+
=head2 token
Handle the 'email_sign_in' tokens. Find the account for the email address
@@ -272,9 +282,21 @@ sub process_login : Private {
$user->password( $data->{password}, 1 ) if $data->{password};
$user->facebook_id( $data->{facebook_id} ) if $data->{facebook_id};
$user->twitter_id( $data->{twitter_id} ) if $data->{twitter_id};
+ $user->add_oidc_id( $data->{oidc_id} ) if $data->{oidc_id};
+ $user->extra({
+ %{ $user->get_extra() },
+ %{ $data->{extra} }
+ }) if $data->{extra};
+
$user->update_or_insert;
$c->authenticate( { $type => $data->{$type}, $ver => 1 }, 'no_password' );
+ if ($data->{logout_redirect_uri}) {
+ $c->session->{oauth} ||= ();
+ $c->session->{oauth}{logout_redirect_uri} = $data->{logout_redirect_uri};
+ }
+
+
# send the user to their page
$c->detach( 'redirect_on_signin', [ $data->{r}, $data->{p} ] );
}
@@ -429,6 +451,12 @@ Log the user out. Tell them we've done so.
sub sign_out : Local {
my ( $self, $c ) = @_;
$c->logout();
+
+ if ( $c->sessionid && $c->session->{oauth} && $c->session->{oauth}{logout_redirect_uri} ) {
+ $c->response->redirect($c->session->{oauth}{logout_redirect_uri});
+ delete $c->session->{oauth}{logout_redirect_uri};
+ $c->detach;
+ }
}
sub ajax_sign_in : Path('ajax/sign_in') {
diff --git a/perllib/FixMyStreet/App/Controller/Auth/Social.pm b/perllib/FixMyStreet/App/Controller/Auth/Social.pm
index 097cac984..aa3177163 100644
--- a/perllib/FixMyStreet/App/Controller/Auth/Social.pm
+++ b/perllib/FixMyStreet/App/Controller/Auth/Social.pm
@@ -6,6 +6,10 @@ BEGIN { extends 'Catalyst::Controller'; }
use Net::Facebook::Oauth2;
use Net::Twitter::Lite::WithAPIv1_1;
+use OIDC::Lite::Client::WebServer::Azure;
+use URI::Escape;
+
+use mySociety::AuthToken;
=head1 NAME
@@ -13,10 +17,26 @@ FixMyStreet::App::Controller::Auth::Social - Catalyst Controller
=head1 DESCRIPTION
-Controller for the Facebook/Twitter authentication.
+Controller for the Facebook/Twitter/OpenID Connect authentication.
=head1 METHODS
+=head2 handle_sign_in
+
+Forwards to the appropriate (facebook|twitter|oidc)_sign_in method
+based on the social_sign_in parameter
+
+=cut
+
+sub handle_sign_in : Private {
+ my ($self, $c) = @_;
+
+ $c->detach('facebook_sign_in') if $c->get_param('social_sign_in') eq 'facebook';
+ $c->detach('twitter_sign_in') if $c->get_param('social_sign_in') eq 'twitter';
+ $c->detach('oidc_sign_in') if $c->get_param('social_sign_in') eq 'oidc';
+
+}
+
=head2 facebook_sign_in
Starts the Facebook authentication sequence.
@@ -142,6 +162,144 @@ sub twitter_callback: Path('/auth/Twitter') : Args(0) {
$c->forward('oauth_success', [ 'twitter', $info->{id}, $info->{name} ]);
}
+sub oidc : Private {
+ my ($self, $c) = @_;
+
+ my $config = $c->cobrand->feature('oidc_login');
+
+ OIDC::Lite::Client::WebServer::Azure->new(
+ id => $config->{client_id},
+ secret => $config->{secret},
+ authorize_uri => $config->{auth_uri},
+ access_token_uri => $config->{token_uri},
+ );
+}
+
+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 $oidc = $c->forward('oidc');
+ my $nonce = $self->generate_nonce();
+ my $url = $oidc->uri_to_redirect(
+ redirect_uri => $c->uri_for('/auth/OIDC'),
+ scope => 'openid',
+ state => 'login',
+ extra => {
+ response_mode => 'form_post',
+ nonce => $nonce,
+ },
+ );
+
+ my %oauth;
+ $oauth{return_url} = $c->get_param('r');
+ $oauth{detach_to} = $c->stash->{detach_to};
+ $oauth{detach_args} = $c->stash->{detach_args};
+ $oauth{nonce} = $nonce;
+
+ # 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} ) {
+ $redirect_uri .= "?post_logout_redirect_uri=";
+ $redirect_uri .= URI::Escape::uri_escape( $c->uri_for('/auth/sign_out') );
+ $oauth{logout_redirect_uri} = $redirect_uri;
+ }
+
+ $c->session->{oauth} = \%oauth;
+ $c->res->redirect($url);
+}
+
+sub oidc_callback: Path('/auth/OIDC') : Args(0) {
+ my ( $self, $c ) = @_;
+
+ my $oidc = $c->forward('oidc');
+
+ if ($c->get_param('error')) {
+ my $error_desc = $c->get_param('error_description');
+ my $password_reset_uri = $c->cobrand->feature('oidc_login')->{password_reset_uri};
+ if ($password_reset_uri && $error_desc =~ /^AADB2C90118:/) {
+ my $url = $oidc->uri_to_redirect(
+ uri => $password_reset_uri,
+ redirect_uri => $c->uri_for('/auth/OIDC'),
+ scope => 'openid',
+ state => 'password_reset',
+ extra => {
+ response_mode => 'form_post',
+ },
+ );
+ $c->res->redirect($url);
+ $c->detach;
+ } else {
+ $c->detach('oauth_failure');
+ }
+ }
+ $c->detach('/page_error_400_bad_request', []) unless $c->get_param('code') && $c->get_param('state');
+
+ # After a password reset on the OIDC endpoint the user isn't properly logged
+ # in, so redirect them to the usual OIDC login process.
+ if ( $c->get_param('state') eq 'password_reset' ) {
+ # The user may have reset their password as part of the sign-in-during-report
+ # process, so preserve their report and redirect them to the right place
+ # if that happened.
+ if ( $c->session->{oauth} ) {
+ $c->stash->{detach_to} = $c->session->{oauth}{detach_to};
+ $c->stash->{detach_args} = $c->session->{oauth}{detach_args};
+ }
+ $c->detach('oidc_sign_in', []);
+ }
+
+ # The only other valid state param is 'login' at this point.
+ $c->detach('/page_error_400_bad_request', []) unless $c->get_param('state') eq 'login';
+
+ my $id_token;
+ eval {
+ $id_token = $oidc->get_access_token(
+ code => $c->get_param('code'),
+ );
+ };
+ if ($@) {
+ (my $message = $@) =~ s/at [^ ]*Auth.pm.*//;
+ $c->detach('/page_error_500_internal_error', [ $message ]);
+ }
+
+ $c->detach('oauth_failure') unless $id_token;
+
+ # sanity check the token audience is us...
+ $c->detach('/page_error_500_internal_error', ['invalid id_token']) unless $id_token->payload->{aud} eq $c->cobrand->feature('oidc_login')->{client_id};
+
+ # 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};
+
+ # 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 $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) {
+ $email = $emails->[0];
+ }
+
+ # There's a chance that a user may have multiple OIDC logins, so build a namespaced uid to prevent collisions
+ my $uid = join(":", $c->cobrand->moniker, $c->cobrand->feature('oidc_login')->{client_id}, $id_token->payload->{sub});
+
+ # The cobrand may want to set values in the user extra field, e.g. a CRM ID
+ # which is passed to Open311 with reports made by this user.
+ my $extra = $c->cobrand->call_hook(oidc_user_extra => $id_token);
+
+ $c->forward('oauth_success', [ 'oidc', $uid, $name, $email, $extra ]);
+}
+
+# Just a wrapper around random_token to make mocking easier.
+sub generate_nonce : Private {
+ my ($self, $c) = @_;
+
+ return mySociety::AuthToken::random_token();
+}
+
+
sub oauth_failure : Private {
my ( $self, $c ) = @_;
@@ -155,30 +313,64 @@ sub oauth_failure : Private {
}
sub oauth_success : Private {
- my ($self, $c, $type, $uid, $name, $email) = @_;
+ my ($self, $c, $type, $uid, $name, $email, $extra) = @_;
my $user;
if ($email) {
- # Only Facebook gets here
+ # Only Facebook & OIDC get here
# We've got an ID and an email address
+
# Remove any existing mention of this ID
- my $existing = $c->model('DB::User')->find( { facebook_id => $uid } );
- $existing->update( { facebook_id => undef } ) if $existing;
- # Get or create a user, give it this Facebook ID
+ my $existing;
+ if ($type eq 'facebook') {
+ $existing = $c->model('DB::User')->find( { $type . '_id' => $uid } );
+ $existing->update( { $type . '_id' => undef } ) if $existing;
+ } elsif ( $type eq 'oidc' ) {
+ $existing = $c->model('DB::User')->find( { oidc_ids => \[
+ '&& ?', [ oidc_ids => [ $uid ] ]
+ ] } );
+ $existing->remove_oidc_id( $uid ) if $existing;
+ }
+
+ # Get or create a user, give it this Facebook/OIDC ID
$user = $c->model('DB::User')->find_or_new( { email => $email } );
- $user->facebook_id($uid);
+ if ( $type eq 'facebook' ) {
+ $user->facebook_id($uid);
+ } elsif ( $type eq 'oidc' ) {
+ $user->add_oidc_id($uid);
+ }
$user->name($name);
+ if ($extra) {
+ $user->extra({
+ %{ $user->get_extra() },
+ %$extra
+ });
+ }
$user->in_storage() ? $user->update : $user->insert;
} else {
# We've got an ID, but no email
- $user = $c->model('DB::User')->find( { $type . '_id' => $uid } );
+ if ($type eq 'oidc') {
+ $user = $c->model('DB::User')->find( { oidc_ids => \[
+ '&& ?', [ oidc_ids => [ $uid ] ]
+ ] } );
+ } else {
+ $user = $c->model('DB::User')->find( { $type . '_id' => $uid } );
+ }
if ($user) {
# Matching ID in our database
$user->name($name);
+ if ($extra) {
+ $user->extra({
+ %{ $user->get_extra() },
+ %$extra
+ });
+ }
$user->update;
} else {
# No matching ID, store ID for use later
$c->session->{oauth}{$type . '_id'} = $uid;
+ $c->session->{oauth}{name} = $name;
+ $c->session->{oauth}{extra} = $extra;
$c->stash->{oauth_need_email} = 1;
}
}
diff --git a/perllib/FixMyStreet/App/Controller/Report/New.pm b/perllib/FixMyStreet/App/Controller/Report/New.pm
index 120467905..a19c43af8 100644
--- a/perllib/FixMyStreet/App/Controller/Report/New.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/New.pm
@@ -803,10 +803,17 @@ sub process_user : Private {
# Report form includes two username fields: #form_username_register and #form_username_sign_in
$params{username} = (first { $_ } $c->get_param_list('username')) || '';
- if ( $c->cobrand->allow_anonymous_reports ) {
+ my $anon_button = $c->cobrand->allow_anonymous_reports eq 'button' && $c->get_param('report_anonymously');
+ my $anon_fallback = $c->cobrand->allow_anonymous_reports eq '1' && !$c->user_exists && !$params{username};
+ if ($anon_button || $anon_fallback) {
my $anon_details = $c->cobrand->anonymous_account;
- $params{username} ||= $anon_details->{email};
- $params{name} ||= $anon_details->{name};
+ my $user = $c->model('DB::User')->find_or_new({ email => $anon_details->{email} });
+ $user->name($anon_details->{name});
+ $report->user($user);
+ $report->name($user->name);
+ $c->stash->{no_reporter_alert} = 1;
+ $c->stash->{contributing_as_anonymous_user} = 1;
+ return 1;
}
# The user is already signed in. Extra bare block for 'last'.
@@ -938,6 +945,11 @@ sub process_report : Private {
$c->stash->{contributing_as_body} = $user->contributing_as('body', $c, $c->stash->{bodies});
$c->stash->{contributing_as_anonymous_user} = $user->contributing_as('anonymous_user', $c, $c->stash->{bodies});
}
+ # This is also done in process_user, but is needed here for anonymous() just below
+ my $anon_button = $c->cobrand->allow_anonymous_reports eq 'button' && $c->get_param('report_anonymously');
+ if ($anon_button) {
+ $c->stash->{contributing_as_anonymous_user} = 1;
+ }
# set some simple bool values (note they get inverted)
if ($c->stash->{contributing_as_body}) {
@@ -1129,12 +1141,13 @@ sub check_for_errors : Private {
$c->stash->{field_errors} ||= {};
my %field_errors = $c->cobrand->report_check_for_errors( $c );
+ my $report = $c->stash->{report};
+
# Zurich, we don't care about title or name
# There is no title, and name is optional
if ( $c->cobrand->moniker eq 'zurich' ) {
delete $field_errors{title};
delete $field_errors{name};
- my $report = $c->stash->{report};
$report->title( Utils::cleanup_text( substr($report->detail, 0, 25) ) );
# We only want to validate the phone number web requests (where the
@@ -1154,8 +1167,13 @@ sub check_for_errors : Private {
delete $field_errors{name};
}
+ # If we're making an anonymous report, we do not care about the name field
+ if ( $c->stash->{contributing_as_anonymous_user} ) {
+ delete $field_errors{name};
+ }
+
# if using social login then we don't care about other errors
- $c->stash->{is_social_user} = $c->get_param('facebook_sign_in') || $c->get_param('twitter_sign_in');
+ $c->stash->{is_social_user} = $c->get_param('social_sign_in') ? 1 : 0;
if ( $c->stash->{is_social_user} ) {
delete $field_errors{name};
delete $field_errors{username};
@@ -1179,7 +1197,6 @@ sub check_for_errors : Private {
if ( $c->cobrand->allow_anonymous_reports ) {
my $anon_details = $c->cobrand->anonymous_account;
- my $report = $c->stash->{report};
$report->user->email(undef) if $report->user->email eq $anon_details->{email};
$report->name(undef) if $report->name eq $anon_details->{name};
}
@@ -1197,10 +1214,8 @@ sub tokenize_user : Private {
password => $report->user->password,
title => $report->user->title,
};
- $c->stash->{token_data}{facebook_id} = $c->session->{oauth}{facebook_id}
- if $c->get_param('oauth_need_email') && $c->session->{oauth}{facebook_id};
- $c->stash->{token_data}{twitter_id} = $c->session->{oauth}{twitter_id}
- if $c->get_param('oauth_need_email') && $c->session->{oauth}{twitter_id};
+ $c->forward('/auth/set_oauth_token_data', [ $c->stash->{token_data} ])
+ if $c->get_param('oauth_need_email');
}
sub send_problem_confirm_email : Private {
@@ -1308,7 +1323,19 @@ sub process_confirmation : Private {
for (qw(name title facebook_id twitter_id)) {
$problem->user->$_( $data->{$_} ) if $data->{$_};
}
+ $problem->user->add_oidc_id($data->{oidc_id}) if $data->{oidc_id};
+ $problem->user->extra({
+ %{ $problem->user->get_extra() },
+ %{ $data->{extra} }
+ }) if $data->{extra};
+
$problem->user->update;
+
+ # Make sure OIDC logout redirection happens, if applicable
+ if ($data->{logout_redirect_uri}) {
+ $c->session->{oauth} ||= ();
+ $c->session->{oauth}{logout_redirect_uri} = $data->{logout_redirect_uri};
+ }
}
if ($problem->user->email_verified) {
$c->authenticate( { email => $problem->user->email, email_verified => 1 }, 'no_password' );
@@ -1368,11 +1395,7 @@ sub save_user_and_report : Private {
$c->stash->{detach_to} = '/report/new/oauth_callback';
$c->stash->{detach_args} = [$token->token];
- if ( $c->get_param('facebook_sign_in') ) {
- $c->detach('/auth/social/facebook_sign_in');
- } elsif ( $c->get_param('twitter_sign_in') ) {
- $c->detach('/auth/social/twitter_sign_in');
- }
+ $c->forward('/auth/social/handle_sign_in') if $c->get_param('social_sign_in');
}
# Save or update the user if appropriate
@@ -1531,6 +1554,7 @@ sub create_reporter_alert : Private {
my ( $self, $c ) = @_;
return if $c->stash->{no_reporter_alert};
+ return if $c->cobrand->call_hook('suppress_reporter_alerts');
my $problem = $c->stash->{report};
my $alert = $c->model('DB::Alert')->find_or_create( {
diff --git a/perllib/FixMyStreet/App/Controller/Report/Update.pm b/perllib/FixMyStreet/App/Controller/Report/Update.pm
index cbedf7a01..1825286ca 100644
--- a/perllib/FixMyStreet/App/Controller/Report/Update.pm
+++ b/perllib/FixMyStreet/App/Controller/Report/Update.pm
@@ -244,8 +244,7 @@ This makes sure we only proceed to processing if we've had the form submitted
sub check_form_submitted : Private {
my ( $self, $c ) = @_;
- return if $c->stash->{problem}->get_extra_metadata('closed_updates');
- return if $c->cobrand->call_hook(updates_disallowed => $c->stash->{problem});
+ return if $c->cobrand->updates_disallowed($c->stash->{problem});
return $c->get_param('submit_update') || '';
}
@@ -366,7 +365,7 @@ sub check_for_errors : Private {
);
# if using social login then we don't care about name and email errors
- $c->stash->{is_social_user} = $c->get_param('facebook_sign_in') || $c->get_param('twitter_sign_in');
+ $c->stash->{is_social_user} = $c->get_param('social_sign_in') ? 1 : 0;
if ( $c->stash->{is_social_user} ) {
delete $field_errors{name};
delete $field_errors{username};
@@ -404,10 +403,8 @@ sub tokenize_user : Private {
name => $update->user->name,
password => $update->user->password,
};
- $c->stash->{token_data}{facebook_id} = $c->session->{oauth}{facebook_id}
- if $c->get_param('oauth_need_email') && $c->session->{oauth}{facebook_id};
- $c->stash->{token_data}{twitter_id} = $c->session->{oauth}{twitter_id}
- if $c->get_param('oauth_need_email') && $c->session->{oauth}{twitter_id};
+ $c->forward('/auth/set_oauth_token_data', [ $c->stash->{token_data} ])
+ if $c->get_param('oauth_need_email');
}
=head2 save_update
@@ -440,11 +437,7 @@ sub save_update : Private {
$c->stash->{detach_to} = '/report/update/oauth_callback';
$c->stash->{detach_args} = [$token->token];
- if ( $c->get_param('facebook_sign_in') ) {
- $c->detach('/auth/social/facebook_sign_in');
- } elsif ( $c->get_param('twitter_sign_in') ) {
- $c->detach('/auth/social/twitter_sign_in');
- }
+ $c->forward('/auth/social/handle_sign_in') if $c->get_param('social_sign_in');
}
if ( $c->cobrand->never_confirm_updates ) {
@@ -585,8 +578,18 @@ sub process_confirmation : Private {
for (qw(name facebook_id twitter_id)) {
$comment->user->$_( $data->{$_} ) if $data->{$_};
}
+ $comment->user->add_oidc_id($data->{oidc_id}) if $data->{oidc_id};
+ $comment->user->extra({
+ %{ $comment->user->get_extra() },
+ %{ $data->{extra} }
+ }) if $data->{extra};
$comment->user->password( $data->{password}, 1 ) if $data->{password};
$comment->user->update;
+ # Make sure OIDC logout redirection happens, if applicable
+ if ($data->{logout_redirect_uri}) {
+ $c->session->{oauth} ||= ();
+ $c->session->{oauth}{logout_redirect_uri} = $data->{logout_redirect_uri};
+ }
}
if ($comment->user->email_verified) {
diff --git a/perllib/FixMyStreet/Cobrand/Default.pm b/perllib/FixMyStreet/Cobrand/Default.pm
index eaf27e3bc..a6c6f34c4 100644
--- a/perllib/FixMyStreet/Cobrand/Default.pm
+++ b/perllib/FixMyStreet/Cobrand/Default.pm
@@ -503,6 +503,19 @@ allowing them to report them as offensive.
sub allow_update_reporting { return 0; }
+=item updates_disallowed
+
+Returns a boolean indicating whether updates on a particular report are allowed
+or not. Default behaviour is disallowed if "closed_updates" metadata is set.
+
+=cut
+
+sub updates_disallowed {
+ my ($self, $problem) = @_;
+ return 1 if $problem->get_extra_metadata('closed_updates');
+ return 0;
+}
+
=item geocode_postcode
Given a QUERY, return LAT/LON and/or ERROR.
@@ -1058,8 +1071,10 @@ sub never_confirm_reports { 0; }
=item allow_anonymous_reports
-If true then can have reports that are truely anonymous - i.e with no email or name. You
-need to also put details in the anonymous_account function too.
+If true then a report submission with no user details will default to the user
+given via the anonymous_account function, and create it anonymously. If set to
+'button', then this will happen only when a report_anonymously button is
+pressed in the front end, rather than whenever a username is not provided.
=cut
@@ -1264,7 +1279,7 @@ sub allow_report_extra_fields { 0 }
sub social_auth_enabled {
my $self = shift;
- my $key_present = FixMyStreet->config('FACEBOOK_APP_ID') or FixMyStreet->config('TWITTER_KEY');
+ my $key_present = FixMyStreet->config('FACEBOOK_APP_ID') || FixMyStreet->config('TWITTER_KEY');
return $key_present && !$self->call_hook("social_auth_disabled");
}
diff --git a/perllib/FixMyStreet/Cobrand/UKCouncils.pm b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
index 794c3dec6..b59c8990b 100644
--- a/perllib/FixMyStreet/Cobrand/UKCouncils.pm
+++ b/perllib/FixMyStreet/Cobrand/UKCouncils.pm
@@ -273,10 +273,9 @@ see Buckinghamshire or Lincolnshire for an example.
sub lookup_site_code {
my $self = shift;
my $row = shift;
- my $buffer = shift;
+ my $field = shift;
- my $cfg = $self->lookup_site_code_config;
- $cfg->{buffer} = $buffer if $buffer;
+ my $cfg = $self->lookup_site_code_config($field);
my ($x, $y) = $row->local_coords;
my $features = $self->_fetch_features($cfg, $x, $y);
@@ -288,17 +287,7 @@ sub _fetch_features {
my $buffer = $cfg->{buffer};
my ($w, $s, $e, $n) = ($x-$buffer, $y-$buffer, $x+$buffer, $y+$buffer);
- my $uri = URI->new($cfg->{url});
- $uri->query_form(
- REQUEST => "GetFeature",
- SERVICE => "WFS",
- SRSNAME => $cfg->{srsname},
- TYPENAME => $cfg->{typename},
- VERSION => "1.1.0",
- outputformat => "geojson",
- BBOX => "$w,$s,$e,$n"
- );
-
+ my $uri = $self->_fetch_features_url($cfg, $w, $s, $e,$n);
my $response = get($uri) or return;
my $j = JSON->new->utf8->allow_nonref;
@@ -313,6 +302,24 @@ sub _fetch_features {
return $j->{features};
}
+sub _fetch_features_url {
+ my ($self, $cfg, $w, $s, $e, $n) = @_;
+
+ my $uri = URI->new($cfg->{url});
+ $uri->query_form(
+ REQUEST => "GetFeature",
+ SERVICE => "WFS",
+ SRSNAME => $cfg->{srsname},
+ TYPENAME => $cfg->{typename},
+ VERSION => "1.1.0",
+ outputformat => "geojson",
+ BBOX => "$w,$s,$e,$n"
+ );
+
+ return $uri;
+}
+
+
sub _nearest_feature {
my ($self, $cfg, $x, $y, $features) = @_;
@@ -321,16 +328,25 @@ sub _nearest_feature {
my $site_code = '';
my $nearest;
+ # We shouldn't receive anything aside from these geometry types, but belt and braces.
+ my $accept_types = $cfg->{accept_types} || {
+ LineString => 1,
+ MultiLineString => 1
+ };
+
for my $feature ( @{$features || []} ) {
next unless $cfg->{accept_feature}($feature);
-
- # We shouldn't receive anything aside from these two geometry types, but belt and braces.
- next unless $feature->{geometry}->{type} eq 'MultiLineString' || $feature->{geometry}->{type} eq 'LineString';
+ next unless $accept_types->{$feature->{geometry}->{type}};
my @linestrings = @{ $feature->{geometry}->{coordinates} };
if ( $feature->{geometry}->{type} eq 'LineString') {
@linestrings = ([ @linestrings ]);
}
+ # If it is a point, upgrade it to a one-segment zero-length
+ # MultiLineString so it can be compared by the distance function.
+ if ( $feature->{geometry}->{type} eq 'Point') {
+ @linestrings = ([ [ @linestrings ], [ @linestrings ] ]);
+ }
foreach my $coordinates (@linestrings) {
for (my $i=0; $i<@$coordinates-1; $i++) {
diff --git a/perllib/FixMyStreet/Cobrand/Westminster.pm b/perllib/FixMyStreet/Cobrand/Westminster.pm
new file mode 100644
index 000000000..3d99e59c4
--- /dev/null
+++ b/perllib/FixMyStreet/Cobrand/Westminster.pm
@@ -0,0 +1,148 @@
+package FixMyStreet::Cobrand::Westminster;
+use base 'FixMyStreet::Cobrand::Whitelabel';
+
+use strict;
+use warnings;
+
+use URI;
+
+sub council_area_id { return 2504; }
+sub council_area { return 'Westminster'; }
+sub council_name { return 'Westminster City Council'; }
+sub council_url { return 'Westminster'; }
+
+sub disambiguate_location {
+ my $self = shift;
+ return {
+ %{ $self->SUPER::disambiguate_location() },
+ town => 'Westminster',
+ centre => '51.513444,-0.160467',
+ bounds => [ 51.483816, -0.216088, 51.539793, -0.111101 ],
+ };
+}
+
+sub get_geocoder {
+ return 'OSM'; # default of Bing gives poor results, let's try overriding.
+}
+
+sub enter_postcode_text {
+ my ($self) = @_;
+ return 'Enter a ' . $self->council_area . ' postcode, or street name';
+}
+
+sub send_questionnaires { 0 }
+
+sub updates_disallowed {
+ my $self = shift;
+ my $c = $self->{c};
+
+ # Only WCC staff and superusers can leave updates
+ my $staff = $c->user_exists && $c->user->from_body && $c->user->from_body->name eq $self->council_name;
+ my $superuser = $c->user_exists && $c->user->is_superuser;
+
+ return ( $staff || $superuser ) ? 0 : 1;
+ }
+
+sub suppress_reporter_alerts { 1 }
+
+sub social_auth_enabled {
+ my $self = shift;
+
+ return $self->feature('oidc_login') ? 1 : 0;
+}
+
+sub allow_anonymous_reports { 'button' }
+
+sub admin_user_domain { 'westminster.gov.uk' }
+
+sub anonymous_account {
+ my $self = shift;
+ return {
+ email => $self->feature('anonymous_account') . '@' . $self->admin_user_domain,
+ name => 'Anonymous user',
+ };
+}
+
+sub oidc_user_extra {
+ my ($self, $id_token) = @_;
+
+ # Westminster want the CRM ID of the user to be passed in the
+ # account_id field of Open311 POST Service Requests, so
+ # extract it from the id token and store in user extra
+ # if it's available.
+ my $crm_id = $id_token->payload->{extension_CrmContactId};
+
+ return {
+ $crm_id ? (westminster_account_id => $crm_id) : (),
+ };
+}
+
+sub open311_config {
+ my ($self, $row, $h, $params) = @_;
+
+ my $id = $row->user->get_extra_metadata('westminster_account_id');
+ # Westminster require 0 as the account ID if there's no MyWestminster ID.
+ $h->{account_id} = $id || '0';
+
+ my $extra = $row->get_extra_fields;
+
+ # Reports made via the app probably won't have a USRN because we don't
+ # display the road layer. Instead we'll look up the closest asset from the
+ # asset service at the point we're sending the report over Open311.
+ if (!$row->get_extra_field_value('USRN')) {
+ if (my $ref = $self->lookup_site_code($row, 'USRN')) {
+ push @$extra, { name => 'USRN', value => $ref };
+ }
+ }
+
+ # Some categories require a UPRN to be set, so if the field is present
+ # but empty then look it up.
+ my $fields = $row->get_extra_fields;
+ my ($uprn_field) = grep { $_->{name} eq 'UPRN' } @$fields;
+ if ( $uprn_field && !$uprn_field->{value} ) {
+ if (my $ref = $self->lookup_site_code($row, 'UPRN')) {
+ push @$extra, { name => 'UPRN', value => $ref };
+ }
+ }
+
+ $row->set_extra_fields(@$extra);
+}
+
+sub lookup_site_code_config {
+ my ( $self, $field ) = @_;
+ # uncoverable subroutine
+ # uncoverable statement
+ my $layer = $field eq 'USRN' ? '40' : '25'; # 25 is UPRN
+
+ my %cfg = (
+ buffer => 1000, # metres
+ proxy_url => "https://tilma.staging.mysociety.org/resource-proxy/proxy.php",
+ url => "https://westminster.assets/$layer/query",
+ property => $field,
+ accept_feature => sub { 1 },
+
+ # UPRNs are Point geometries, so make sure they're allowed by
+ # _nearest_feature.
+ ( $field eq 'UPRN' ) ? (accept_types => { Point => 1 }) : (),
+ );
+ return \%cfg;
+}
+
+sub _fetch_features_url {
+ my ($self, $cfg, $w, $s, $e, $n) = @_;
+
+ # Westminster's asset proxy has a slightly different calling style to
+ # a standard WFS server.
+ my $uri = URI->new($cfg->{url});
+ $uri->query_form(
+ inSR => "27700",
+ outSR => "27700",
+ f => "geojson",
+ outFields => $cfg->{property},
+ geometry => "$w,$s,$e,$n",
+ );
+
+ return $cfg->{proxy_url} . "?" . $uri->as_string;
+}
+
+1;
diff --git a/perllib/FixMyStreet/DB/Result/User.pm b/perllib/FixMyStreet/DB/Result/User.pm
index fc651b4d1..4ea7524bb 100644
--- a/perllib/FixMyStreet/DB/Result/User.pm
+++ b/perllib/FixMyStreet/DB/Result/User.pm
@@ -24,22 +24,30 @@ __PACKAGE__->add_columns(
},
"email",
{ data_type => "text", is_nullable => 1 },
- "email_verified",
- { data_type => "boolean", default_value => \"false", is_nullable => 0 },
"name",
{ data_type => "text", is_nullable => 1 },
"phone",
{ data_type => "text", is_nullable => 1 },
- "phone_verified",
- { data_type => "boolean", default_value => \"false", is_nullable => 0 },
"password",
{ data_type => "text", default_value => "", is_nullable => 0 },
- "from_body",
- { data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
"flagged",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
+ "from_body",
+ { data_type => "integer", is_foreign_key => 1, is_nullable => 1 },
+ "title",
+ { data_type => "text", is_nullable => 1 },
+ "facebook_id",
+ { data_type => "bigint", is_nullable => 1 },
+ "twitter_id",
+ { data_type => "bigint", is_nullable => 1 },
"is_superuser",
{ data_type => "boolean", default_value => \"false", is_nullable => 0 },
+ "extra",
+ { data_type => "text", is_nullable => 1 },
+ "email_verified",
+ { data_type => "boolean", default_value => \"false", is_nullable => 0 },
+ "phone_verified",
+ { data_type => "boolean", default_value => \"false", is_nullable => 0 },
"created",
{
data_type => "timestamp",
@@ -54,16 +62,10 @@ __PACKAGE__->add_columns(
is_nullable => 0,
original => { default_value => \"now()" },
},
- "title",
- { data_type => "text", is_nullable => 1 },
- "twitter_id",
- { data_type => "bigint", is_nullable => 1 },
- "facebook_id",
- { data_type => "bigint", is_nullable => 1 },
- "extra",
- { data_type => "text", is_nullable => 1 },
"area_ids",
{ data_type => "integer[]", is_nullable => 1 },
+ "oidc_ids",
+ { data_type => "text[]", is_nullable => 1 },
);
__PACKAGE__->set_primary_key("id");
__PACKAGE__->add_unique_constraint("users_facebook_id_key", ["facebook_id"]);
@@ -129,8 +131,8 @@ __PACKAGE__->has_many(
);
-# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-05-23 18:03:28
-# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:qtmzA7ywVkyQpjLh1ienNg
+# Created by DBIx::Class::Schema::Loader v0.07035 @ 2019-06-20 16:31:44
+# DO NOT MODIFY THIS OR ANYTHING ABOVE! md5sum:Ryb6giJm/7N7svg/d+2GeA
# These are not fully unique constraints (they only are when the *_verified
# is true), but this is managed in ResultSet::User's find() wrapper.
@@ -142,6 +144,7 @@ __PACKAGE__->rabx_column('extra');
use Moo;
use Text::CSV;
+use List::MoreUtils 'uniq';
use FixMyStreet::SMS;
use mySociety::EmailUtil;
use namespace::clean -except => [ 'meta' ];
@@ -534,6 +537,7 @@ sub anonymize_account {
title => undef,
twitter_id => undef,
facebook_id => undef,
+ oidc_ids => undef,
});
}
@@ -654,4 +658,20 @@ sub in_role {
return $self->roles_hash->{$role};
}
+sub add_oidc_id {
+ my ($self, $oidc_id) = @_;
+
+ my $oidc_ids = $self->oidc_ids || [];
+ my @oidc_ids = uniq ( $oidc_id, @$oidc_ids );
+ $self->oidc_ids(\@oidc_ids);
+}
+
+sub remove_oidc_id {
+ my ($self, $oidc_id) = @_;
+
+ my $oidc_ids = $self->oidc_ids || [];
+ my @oidc_ids = grep { $_ ne $oidc_id } @$oidc_ids;
+ $self->oidc_ids(scalar @oidc_ids ? \@oidc_ids : undef);
+}
+
1;