<html><head><meta name="color-scheme" content="light dark"></head><body><pre style="word-wrap: break-word; white-space: pre-wrap;">package FixMyStreet::App::Controller::Alert;
use Moose;
use namespace::autoclean;

BEGIN { extends 'Catalyst::Controller'; }

use mySociety::EmailUtil qw(is_valid_email);
use FixMyStreet::Gaze;

=head1 NAME

FixMyStreet::App::Controller::Alert - Catalyst Controller

=head1 DESCRIPTION

Catalyst Controller.

=head1 METHODS

=cut

=head2 alert

Show the alerts page

=cut

sub index : Path('') : Args(0) {
    my ( $self, $c ) = @_;

    unless ( $c-&gt;req-&gt;referer &amp;&amp; $c-&gt;req-&gt;referer =~ /fixmystreet\.com/ ) {
        $c-&gt;forward( 'add_recent_photos', [10] );
    }
}

sub list : Path('list') : Args(0) {
    my ( $self, $c ) = @_;

    $c-&gt;forward('/auth/get_csrf_token');

    return
      unless $c-&gt;forward('setup_request')
          &amp;&amp; $c-&gt;forward('prettify_pc')
          &amp;&amp; $c-&gt;forward('determine_location')
          &amp;&amp; $c-&gt;forward( 'add_recent_photos', [5] )
          &amp;&amp; $c-&gt;forward('setup_council_rss_feeds')
          &amp;&amp; $c-&gt;forward('setup_coordinate_rss_feeds');
}

=head2 subscribe

Target for subscribe form

=cut

sub subscribe : Path('subscribe') : Args(0) {
    my ( $self, $c ) = @_;

    $c-&gt;detach('rss') if $c-&gt;get_param('rss');

    my $id = $c-&gt;get_param('id');
    $c-&gt;forward('/report/load_problem_or_display_error', [ $id ]) if $id;

    # if it exists then it's been submitted so we should
    # go to subscribe email and let it work out the next step
    $c-&gt;detach('subscribe_email')
        if $c-&gt;get_param('rznvy') || $c-&gt;get_param('alert');

    $c-&gt;go('updates') if $id;

    # shouldn't get to here but if we have then do something sensible
    $c-&gt;go('index');
}

=head2 rss

Redirects to relevant RSS feed

=cut

sub rss : Private {
    my ( $self, $c ) = @_;
    my $feed = $c-&gt;get_param('feed');

    unless ($feed) {
        $c-&gt;stash-&gt;{errors} = [ _('Please select the feed you want') ];
        $c-&gt;go('list');
    }

    my $url;
    if ( $feed =~ /^area:(?:\d+:)+(.*)$/ ) {
        ( my $id = $1 ) =~ tr{:_}{/+};
        $url = $c-&gt;cobrand-&gt;base_url() . '/rss/area/' . $id;
        $c-&gt;res-&gt;redirect($url);
    }
    elsif ( $feed =~ /^(?:council|ward):(?:\d+:)+(.*)$/ ) {
        ( my $id = $1 ) =~ tr{:_}{/+};
        $url = $c-&gt;cobrand-&gt;base_url() . '/rss/reports/' . $id;
        $c-&gt;res-&gt;redirect($url);
    }
    elsif ( $feed =~ /^local:([\d\.-]+):([\d\.-]+)$/ ) {
        my $distance = $c-&gt;forward('get_distance');
        $url = $c-&gt;cobrand-&gt;base_url() . '/rss/l/' . $1 . ',' . $2;
        $url .= "/$distance" if $distance;
        $c-&gt;res-&gt;redirect($url);
    }
    else {
        $c-&gt;stash-&gt;{errors} = [ _('Illegal feed selection') ];
        $c-&gt;go('list');
    }
}

=head2 subscribe_email

Sign up to email alerts

=cut

sub subscribe_email : Private {
    my ( $self, $c ) = @_;

    $c-&gt;forward('/auth/check_csrf_token');

    $c-&gt;stash-&gt;{errors} = [];
    $c-&gt;forward('process_user');

    my $type = $c-&gt;get_param('type') || "";
    if ( $type eq 'updates' ) {
        $c-&gt;forward('set_update_alert_options');
    } elsif ( $type eq 'local' ) {
        $c-&gt;forward('set_local_alert_options');
    } else {
        $c-&gt;detach( '/page_error_404_not_found', [ 'Invalid type' ] );
    }

    push @{ $c-&gt;stash-&gt;{errors} }, _('Please select the type of alert you want')
      if !$c-&gt;stash-&gt;{alert_options};

    $c-&gt;go($type eq 'updates' ? 'updates' : 'list') if @{ $c-&gt;stash-&gt;{errors} };

    $c-&gt;forward('create_alert');
    if ( $c-&gt;stash-&gt;{alert}-&gt;confirmed ) {
        $c-&gt;stash-&gt;{confirm_type} = 'created';
        $c-&gt;stash-&gt;{template} = 'tokens/confirm_alert.html';
    } else {
        $c-&gt;forward('send_confirmation_email');
    }
}

sub updates : Path('updates') : Args(0) {
    my ( $self, $c ) = @_;

    $c-&gt;forward('/auth/get_csrf_token');

    $c-&gt;stash-&gt;{email} = $c-&gt;get_param('rznvy');
    $c-&gt;stash-&gt;{email} ||= $c-&gt;user-&gt;email if $c-&gt;user_exists;
}

=head2 confirm

Confirm signup to or unsubscription from an alert. Forwarded here from Tokens.

=cut

sub confirm : Private {
    my ( $self, $c ) = @_;

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

    if ( $c-&gt;stash-&gt;{confirm_type} eq 'subscribe' ) {
        $alert-&gt;confirm();
    }
    elsif ( $c-&gt;stash-&gt;{confirm_type} eq 'unsubscribe' ) {
        $alert-&gt;disable();
    }
}

=head2 create_alert

Take the alert options from the stash and use these to create a new
alert. If it finds an existing alert that's the same then use that

=cut 

sub create_alert : Private {
    my ( $self, $c ) = @_;

    my $options = $c-&gt;stash-&gt;{alert_options};

    my $alert = $c-&gt;model('DB::Alert')-&gt;find($options);

    unless ($alert) {
        $options-&gt;{cobrand}      = $c-&gt;cobrand-&gt;moniker();
        $options-&gt;{cobrand_data} = '';
        $options-&gt;{lang}         = $c-&gt;stash-&gt;{lang_code};

        $alert = $c-&gt;model('DB::Alert')-&gt;new($options);
        $alert-&gt;insert();
    }

    $c-&gt;stash-&gt;{alert} = $alert;

    if ( $c-&gt;user_exists &amp;&amp; ($c-&gt;user-&gt;id == $alert-&gt;user-&gt;id || $c-&gt;stash-&gt;{can_create_for_another})) {
        $alert-&gt;confirm();
        if ($c-&gt;stash-&gt;{can_create_for_another} &amp;&amp; $c-&gt;user-&gt;id != $alert-&gt;user-&gt;id) {
            # User has been subscribed to the new_updates alert email by a staff member. Send them an email to let them know.
            $c-&gt;forward('send_subscribed_email');
        }
    } else {
        $alert-&gt;confirmed(0);
    }

}

=head2 set_update_alert_options

Set up the options in the stash required to create a problem update alert

=cut

sub set_update_alert_options : Private {
    my ( $self, $c ) = @_;

    my $options = {
        user       =&gt; $c-&gt;stash-&gt;{alert_user},
        alert_type =&gt; 'new_updates',
        parameter  =&gt; $c-&gt;stash-&gt;{problem}-&gt;id,
    };

    $c-&gt;stash-&gt;{alert_options} = $options;
}

=head2 set_local_alert_options

Set up the options in the stash required to create a local problems alert

=cut

sub set_local_alert_options : Private {
    my ( $self, $c ) = @_;

    my $feed = $c-&gt;get_param('feed');

    my ( $type, @params, $alert );
    if ( $feed =~ /^area:(?:\d+:)?(\d+)/ ) {
        $type = 'area_problems';
        push @params, $1;
    }
    elsif ( $feed =~ /^council:(\d+)/ ) {
        $type = 'council_problems';
        push @params, $1, $1;
    }
    elsif ( $feed =~ /^ward:(\d+):(\d+)/ ) {
        $type = 'ward_problems';
        push @params, $1, $2;
    }
    elsif ( $feed =~
        m{ \A local: ( [\+\-]? \d+ \.? \d* ) : ( [\+\-]? \d+ \.? \d* ) }xms )
    {
        $type = 'local_problems';
        push @params, $2, $1; # Note alert parameters are lon,lat
        my $distance = $c-&gt;forward('get_distance');
        push @params, $distance if $distance;
    }
    return unless $type;

    my $options = {
        user       =&gt; $c-&gt;stash-&gt;{alert_user},
        alert_type =&gt; $type
    };

    if ( scalar @params == 1 ) {
        $options-&gt;{parameter} = $params[0];
    } elsif ( scalar @params == 2 ) {
        $options-&gt;{parameter}  = $params[0];
        $options-&gt;{parameter2} = $params[1];
    } elsif ( scalar @params == 3 ) {
        $options-&gt;{parameter}  = $params[0];
        $options-&gt;{parameter2} = $params[1];
        $options-&gt;{parameter3} = $params[2];
    }

    $c-&gt;stash-&gt;{alert_options} = $options;
}

=head2 send_confirmation_email

Generate a token and send out an alert subscription confirmation email and
then display confirmation page.

=cut

sub send_confirmation_email : Private {
    my ( $self, $c ) = @_;

    my $user = $c-&gt;stash-&gt;{alert}-&gt;user;

    # People using 2FA can not log in by code
    $c-&gt;detach( '/page_error_403_access_denied', [] ) if $user-&gt;has_2fa;

    my $data = {
        id =&gt; $c-&gt;stash-&gt;{alert}-&gt;id,
        type =&gt; 'subscribe',
    };

    # Logged in as a phone user, need to verify email and update user account
    if ($c-&gt;user_exists &amp;&amp; !$c-&gt;user-&gt;email_verified) {
        $data-&gt;{old_user_id} = $c-&gt;user-&gt;id;
    }

    my $token = $c-&gt;model("DB::Token")-&gt;create({
        scope =&gt; 'alert',
        data  =&gt; $data,
    });

    $c-&gt;stash-&gt;{token_url} = $c-&gt;uri_for_email( '/A', $token-&gt;token );

    $c-&gt;send_email( 'alert-confirm.txt', { to =&gt; $user-&gt;email } );

    $c-&gt;stash-&gt;{email_type} = 'alert';
    $c-&gt;stash-&gt;{template} = 'email_sent.html';
}

=head2 send_subscribed_email

Send an email to an address that has been subscribed to alerts by a staff user.

=cut

sub send_subscribed_email : Private {
    my ( $self, $c ) = @_;

    my $user = $c-&gt;stash-&gt;{alert}-&gt;user;

    my $token = $c-&gt;model("DB::Token")-&gt;create( {
        scope =&gt; 'alert',
        data  =&gt; {
            id =&gt; $c-&gt;stash-&gt;{alert}-&gt;id,
            type =&gt; 'unsubscribe',
        }
    } );
    $c-&gt;stash-&gt;{unsubscribe_url} = $c-&gt;cobrand-&gt;base_url( $c-&gt;stash-&gt;{alert_options}-&gt;{cobrand_data} ) . '/A/' . $token-&gt;token;
    $c-&gt;stash-&gt;{problem_url} = $c-&gt;cobrand-&gt;base_url_for_report($c-&gt;stash-&gt;{problem}) . $c-&gt;stash-&gt;{problem}-&gt;url;

    $c-&gt;send_email( 'alert-subscribed.txt', { to =&gt; $user-&gt;email } );
}

=head2 prettify_pc

This will canonicalise and prettify the postcode and stick a pretty_pc and pretty_pc_text in the stash.

=cut

sub prettify_pc : Private {
    my ( $self, $c ) = @_;

    my $pretty_pc = $c-&gt;get_param('pc');

    if ( mySociety::PostcodeUtil::is_valid_postcode( $c-&gt;get_param('pc') ) )
    {
        $pretty_pc = mySociety::PostcodeUtil::canonicalise_postcode(
            $c-&gt;get_param('pc') );
        my $pretty_pc_text = $pretty_pc;
        $pretty_pc_text =~ s/ //g;
        $c-&gt;stash-&gt;{pretty_pc_text} = $pretty_pc_text;
    }

    $c-&gt;stash-&gt;{pretty_pc} = $pretty_pc;

    return 1;
}

=head2 process_user

Fetch/check email address

=cut

sub process_user : Private {
    my ( $self, $c ) = @_;

    # If we are logged in (and not creating for someone else) we'll want to use
    # the logged in user (but note, if they don't have a verified email and it
    # is a local alert, we still want to email them confirmation)
    my $type = $c-&gt;get_param('type') || "";
    if ( $c-&gt;user_exists ) {
        $c-&gt;stash-&gt;{can_create_for_another} = $c-&gt;stash-&gt;{problem}
            &amp;&amp; $c-&gt;user-&gt;has_permission_to(contribute_as_another_user =&gt; $c-&gt;stash-&gt;{problem}-&gt;bodies_str_ids);
        if (!$c-&gt;stash-&gt;{can_create_for_another} &amp;&amp; ($type eq 'updates' || $c-&gt;user-&gt;email_verified)) {
            $c-&gt;stash-&gt;{alert_user} = $c-&gt;user-&gt;obj;
            return;
        }
    }

    my $email = $c-&gt;get_param('rznvy');
    $email = $email ? lc $email : '';
    $email =~ s{\s+}{}g;

    push @{ $c-&gt;stash-&gt;{errors} }, _('Please enter a valid email address')
      unless is_valid_email( $email );

    my $alert_user = $c-&gt;model('DB::User')-&gt;find_or_new( { email =&gt; $email } );
    $c-&gt;stash-&gt;{alert_user} = $alert_user;
}

=head2 setup_coordinate_rss_feeds

Takes the latitide and longitude from the stash and uses them to generate uris
for the local rss feeds

=cut 

sub setup_coordinate_rss_feeds : Private {
    my ( $self, $c ) = @_;

    $c-&gt;stash-&gt;{rss_feed_id} =
      sprintf( 'local:%s:%s', $c-&gt;stash-&gt;{latitude}, $c-&gt;stash-&gt;{longitude} );

    my $rss_feed;
    if ( $c-&gt;stash-&gt;{pretty_pc_text} ) {
        $rss_feed = $c-&gt;uri_for( "/rss/pc/" . $c-&gt;stash-&gt;{pretty_pc_text} );
    }
    else {
        $rss_feed = $c-&gt;uri_for(
            sprintf( "/rss/l/%s,%s",
                $c-&gt;stash-&gt;{latitude},
                $c-&gt;stash-&gt;{longitude} )
        );
    }

    $c-&gt;stash-&gt;{rss_feed_uri} = $rss_feed;

    return 1;
}

=head2 setup_council_rss_feeds

Generate the details required to display the council/ward/area RSS feeds

=cut

sub setup_council_rss_feeds : Private {
    my ( $self, $c ) = @_;

    $c-&gt;stash-&gt;{area_check_action} = 'alert';
    unless ( $c-&gt;forward('/council/load_and_check_areas_and_wards') ) {
        $c-&gt;go('index');
    }

    ( $c-&gt;stash-&gt;{options}, $c-&gt;stash-&gt;{reported_to_options} ) =
      $c-&gt;cobrand-&gt;council_rss_alert_options( $c-&gt;stash-&gt;{all_areas}, $c );

    return 1;
}

=head2 determine_location

Do all the things we need to do to work out where the alert is for
and to setup the location related things for later

=cut

sub determine_location : Private {
    my ( $self, $c ) = @_;

    # Try to create a location for whatever we have
    unless ( $c-&gt;forward('/location/determine_location_from_coords')
        || $c-&gt;forward('/location/determine_location_from_pc') )
    {

        if ( $c-&gt;stash-&gt;{possible_location_matches} ) {
            $c-&gt;stash-&gt;{choose_target_uri} = $c-&gt;uri_for('/alert/list');
            $c-&gt;detach('choose');
        }

        $c-&gt;go('index');
    }

    my $dist = FixMyStreet::Gaze::get_radius_containing_population($c-&gt;stash-&gt;{latitude}, $c-&gt;stash-&gt;{longitude});
    $c-&gt;stash-&gt;{population_radius} = $dist;

    return 1;
}

=head2 add_recent_photos

    $c-&gt;forward( 'add_recent_photos', [ $num_photos ] );

Adds the most recent $num_photos to the template. If there is coordinate 
and population radius information in the stash uses that to limit it.

=cut

sub add_recent_photos : Private {
    my ( $self, $c, $num_photos ) = @_;

    if (    $c-&gt;stash-&gt;{latitude}
        and $c-&gt;stash-&gt;{longitude}
        and $c-&gt;stash-&gt;{population_radius} )
    {

        $c-&gt;stash-&gt;{photos} = $c-&gt;cobrand-&gt;recent_photos(
            'alert',
            $num_photos,
            $c-&gt;stash-&gt;{latitude},
            $c-&gt;stash-&gt;{longitude},
            $c-&gt;stash-&gt;{population_radius}
        );
    }
    else {
        $c-&gt;stash-&gt;{photos} = $c-&gt;cobrand-&gt;recent_photos('alert', $num_photos);
    }

    return 1;
}

sub choose : Private {
    my ( $self, $c ) = @_;
    $c-&gt;stash-&gt;{template} = 'alert/choose.html';
}


=head2 setup_request

Setup the variables we need for the rest of the request

=cut

sub setup_request : Private {
    my ( $self, $c ) = @_;

    $c-&gt;stash-&gt;{rznvy} = $c-&gt;get_param('rznvy');
    $c-&gt;stash-&gt;{selected_feed} = $c-&gt;get_param('feed');

    if ( $c-&gt;user ) {
        $c-&gt;stash-&gt;{rznvy} ||= $c-&gt;user-&gt;email;
    }

    $c-&gt;stash-&gt;{template} = 'alert/list-ajax.html' if $c-&gt;get_param('ajax');

    return 1;
}

sub get_distance : Private {
    my ($self, $c) = @_;

    my $distance = $c-&gt;get_param('distance') || '';
    $distance = 0 unless $distance =~ /^\d+$/;
    $distance = 150 if $distance &gt; 150; # Match Gaze maximum
    return $distance;
}

=head1 AUTHOR

Struan Donald

=head1 LICENSE

This library is free software. You can redistribute it and/or modify
it under the same terms as Perl itself.

=cut

__PACKAGE__-&gt;meta-&gt;make_immutable;

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