diff options
author | Marius Halden <marius.h@lden.org> | 2019-10-30 19:28:55 +0100 |
---|---|---|
committer | Marius Halden <marius.h@lden.org> | 2019-10-30 19:28:55 +0100 |
commit | 377bd96aab7cad3434185c30eb908c9da447fe40 (patch) | |
tree | 7ec5527e205d5b62caaa862a7de8cd25199c8bf0 /perllib/FixMyStreet/App/Controller/Moderate.pm | |
parent | 56f61b1441070aa0b9ddcfc74aca46c20313609f (diff) | |
parent | 92b253904062edd533e55c22824de6fd01e2f7c1 (diff) |
Merge tag 'v2.6' into fiksgatami-dev
Diffstat (limited to 'perllib/FixMyStreet/App/Controller/Moderate.pm')
-rw-r--r-- | perllib/FixMyStreet/App/Controller/Moderate.pm | 406 |
1 files changed, 233 insertions, 173 deletions
diff --git a/perllib/FixMyStreet/App/Controller/Moderate.pm b/perllib/FixMyStreet/App/Controller/Moderate.pm index 86143b5ea..22869d531 100644 --- a/perllib/FixMyStreet/App/Controller/Moderate.pm +++ b/perllib/FixMyStreet/App/Controller/Moderate.pm @@ -23,9 +23,9 @@ data to change. - user to be from_body - user to have a "moderate" record in user_body_permissions -The original data of the report is stored in moderation_original_data, so -that it can be reverted/consulted if required. All moderation events are -stored in admin_log. +The original and previous data of the report is stored in +moderation_original_data, so that it can be reverted/consulted if required. +All moderation events are stored in admin_log. =head1 SEE ALSO @@ -37,71 +37,155 @@ DB tables: =cut +sub end : ActionClass('RenderView') { + my ($self, $c) = @_; + + if ($c->stash->{moderate_errors}) { + $c->stash->{show_moderation} = 'report'; + $c->stash->{template} = 'report/display.html'; + $c->forward('/report/display'); + } elsif ($c->res->redirect) { + # Do nothing if we're already going somewhere + } else { + $c->res->redirect($c->stash->{report_uri}); + } +} + sub moderate : Chained('/') : PathPart('moderate') : CaptureArgs(0) { } sub report : Chained('moderate') : PathPart('report') : CaptureArgs(1) { my ($self, $c, $id) = @_; my $problem = $c->model('DB::Problem')->find($id); + $c->detach unless $problem; my $cobrand_base = $c->cobrand->base_url_for_report( $problem ); my $report_uri = $cobrand_base . $problem->url; $c->stash->{cobrand_base} = $cobrand_base; $c->stash->{report_uri} = $report_uri; - $c->res->redirect( $report_uri ); # this will be the final endpoint after all processing... - # ... and immediately, if the user isn't authorized $c->detach unless $c->user_exists; - $c->detach unless $c->user->has_permission_to(moderate => $problem->bodies_str_ids); $c->forward('/auth/check_csrf_token'); - my $original = $problem->find_or_new_related( moderation_original_data => { + $c->stash->{history} = $problem->new_related( moderation_original_data => { title => $problem->title, detail => $problem->detail, photo => $problem->photo, anonymous => $problem->anonymous, + longitude => $problem->longitude, + latitude => $problem->latitude, + category => $problem->category, + $problem->extra ? (extra => $problem->extra) : (), }); + $c->stash->{original} = $problem->moderation_original_data || $c->stash->{history}; $c->stash->{problem} = $problem; - $c->stash->{problem_original} = $original; $c->stash->{moderation_reason} = $c->get_param('moderation_reason') // ''; } sub moderate_report : Chained('report') : PathPart('') : Args(0) { my ($self, $c) = @_; + my $problem = $c->stash->{problem}; + + # Make sure user can moderate this report + $c->detach unless $c->user->can_moderate($problem); + + $c->forward('check_edited_elsewhere'); $c->forward('report_moderate_hide'); my @types = grep $_, - $c->forward('report_moderate_title'), - $c->forward('report_moderate_detail'), - $c->forward('report_moderate_anon'), - $c->forward('report_moderate_photo'); + $c->forward('moderate_state'), + ($c->user->can_moderate_title($problem, 1) + ? $c->forward('moderate_text', [ 'title' ]) + : ()), + $c->forward('moderate_text', [ 'detail' ]), + $c->forward('moderate_boolean', [ 'anonymous', 'show_name' ]), + $c->forward('moderate_boolean', [ 'photo' ]), + $c->forward('moderate_location'), + $c->forward('moderate_category'), + $c->forward('moderate_extra'); + + # Deal with possible photo changes. If a moderate form uses a standard + # photo upload field (with upload_fileid, label and file upload handlers), + # this will allow photos to be changed, not just switched on/off. You will + # probably want a hidden field with problem_photo=1 to skip that check. + my $photo_edit_form = defined $c->get_param('photo1'); + if ($photo_edit_form) { + $c->forward('/photo/process_photo'); + if ( my $photo_error = delete $c->stash->{photo_error} ) { + $c->stash->{moderate_errors} ||= []; + push @{ $c->stash->{moderate_errors} }, $photo_error; + } else { + my $fileid = $c->stash->{upload_fileid}; + if ($fileid ne $problem->photo) { + $problem->get_photoset->delete_cached; + $problem->photo($fileid || undef); + push @types, 'photo'; + } + } + } - $c->detach( 'report_moderate_audit', \@types ) + $c->detach( 'report_moderate_audit', \@types ); } -sub moderating_user_name { - my $user = shift; - return $user->from_body ? $user->from_body->name : _('an administrator'); +sub check_edited_elsewhere : Private { + my ($self, $c) = @_; + + my $problem = $c->stash->{problem}; + my $last_moderation = $problem->latest_moderation; + return unless $last_moderation; + + my $form_started = $c->get_param('form_started') || 0; + if ($form_started && $form_started < $last_moderation->created->epoch) { + $c->stash->{moderate_errors} ||= []; + push @{$c->stash->{moderate_errors}}, + _('Someone has moderated this report since you started.') . ' ' . + sprintf(_('Please <a href="#%s">check their changes</a> and resolve any differences.'), + 'update_m' . $last_moderation->id); + $c->detach; + } } -sub report_moderate_audit : Private { - my ($self, $c, @types) = @_; +sub moderate_log_entry : Private { + my ($self, $c, $object_type, @types) = @_; my $user = $c->user->obj; my $reason = $c->stash->{'moderation_reason'}; - my $problem = $c->stash->{problem} or die; + my $object = $object_type eq 'update' ? $c->stash->{comment} : $c->stash->{problem}; my $types_csv = join ', ' => @types; + my $log_reason = "($types_csv)"; + $log_reason = "$reason $log_reason" if $reason; + + # We attach the log to the moderation entry if present, or the object if not (hiding) $c->model('DB::AdminLog')->create({ action => 'moderation', user => $user, - admin_user => moderating_user_name($user), - object_id => $problem->id, - object_type => 'problem', - reason => (sprintf '%s (%s)', $reason, $types_csv), + admin_user => $user->moderating_user_name, + object_id => $c->stash->{history}->id || $object->id, + object_type => $c->stash->{history}->id ? 'moderation' : $object_type, + reason => $log_reason, }); +} + +sub report_moderate_audit : Private { + my ($self, $c, @types) = @_; + + my $problem = $c->stash->{problem} or die; + + return unless @types; # If nothing moderated, nothing to do + return if $c->stash->{moderate_errors}; # Don't update anything if errors + + # Okay, now update the report + $problem->update; + + return if @types == 1 && $types[0] eq 'state'; # If only state changed, no log entry needed + + # We've done some non-state moderation, save the history + $c->stash->{history}->insert; + + $c->forward('moderate_log_entry', [ 'problem', @types ]); if ($problem->user->email_verified && $c->cobrand->send_moderation_notifications) { my $token = $c->model("DB::Token")->create({ @@ -109,6 +193,7 @@ sub report_moderate_audit : Private { data => { id => $problem->id } }); + my $types_csv = join ', ' => @types; $c->send_email( 'problem-moderated.txt', { to => [ [ $problem->user->email, $problem->name ] ], types => $types_csv, @@ -116,6 +201,7 @@ sub report_moderate_audit : Private { problem => $problem, report_uri => $c->stash->{report_uri}, report_complain_uri => $c->stash->{cobrand_base} . '/contact?m=' . $token->token, + moderated_data => $c->stash->{history}, }); } } @@ -135,97 +221,153 @@ sub report_moderate_hide : Private { } } -sub report_moderate_title : Private { - my ( $self, $c ) = @_; +sub moderate_text : Private { + my ($self, $c, $thing) = @_; - my $problem = $c->stash->{problem} or die; - my $original = $c->stash->{problem_original}; + my $object = $c->stash->{comment} || $c->stash->{problem}; + my $param = $c->stash->{comment} ? 'update_' : 'problem_'; - my $old_title = $problem->title; - my $original_title = $original->title; + my $thing_for_original_table = $thing; + # Update 'text' field is stored in original table's 'detail' field + $thing_for_original_table = 'detail' if $c->stash->{comment} && $thing eq 'text'; - my $title = $c->get_param('problem_revert_title') ? - $original_title - : $c->get_param('problem_title'); + my $old = $object->$thing; + my $original_thing = $c->stash->{original}->$thing_for_original_table; - if ($title ne $old_title) { - $original->insert unless $original->in_storage; - $problem->update({ title => $title }); - return 'title'; - } + my $new = $c->get_param($param . 'revert_' . $thing) ? + $original_thing + : $c->get_param($param . $thing); - return; + if ($new ne $old) { + $object->$thing($new); + return $thing_for_original_table; + } } -sub report_moderate_detail : Private { - my ( $self, $c ) = @_; +sub moderate_boolean : Private { + my ( $self, $c, $thing, $reverse ) = @_; - my $problem = $c->stash->{problem} or die; - my $original = $c->stash->{problem_original}; - - my $old_detail = $problem->detail; - my $original_detail = $original->detail; - my $detail = $c->get_param('problem_revert_detail') ? - $original_detail - : $c->get_param('problem_detail'); - - if ($detail ne $old_detail) { - $original->insert unless $original->in_storage; - $problem->update({ detail => $detail }); - return 'detail'; + my $object = $c->stash->{comment} || $c->stash->{problem}; + my $param = $c->stash->{comment} ? 'update_' : 'problem_'; + my $original = $c->stash->{original}->photo; + + return if $thing eq 'photo' && !$original; + + my $new; + if ($reverse) { + $new = $c->get_param($param . $reverse) ? 0 : 1; + } else { + $new = $c->get_param($param . $thing) ? 1 : 0; + } + my $old = $object->$thing ? 1 : 0; + + if ($new != $old) { + if ($thing eq 'photo') { + $object->$thing($new ? $original : undef); + $object->get_photoset->delete_cached; + } else { + $object->$thing($new); + } + return $thing; } - return; } -sub report_moderate_anon : Private { - my ( $self, $c ) = @_; +sub moderate_extra : Private { + my ($self, $c) = @_; - my $problem = $c->stash->{problem} or die; - my $original = $c->stash->{problem_original}; + my $object = $c->stash->{comment} || $c->stash->{problem}; + + my $changed; + my @extra = grep { /^extra\./ } keys %{$c->req->params}; + foreach (@extra) { + my ($field_name) = /extra\.(.*)/; + my $old = $object->get_extra_metadata($field_name) || ''; + my $new = $c->get_param($_); + if ($new ne $old) { + $object->set_extra_metadata($field_name, $new); + $changed = 1; + } + } + if ($changed) { + return 'extra'; + } +} - my $show_user = $c->get_param('problem_show_name') ? 1 : 0; - my $anonymous = $show_user ? 0 : 1; - my $old_anonymous = $problem->anonymous ? 1 : 0; +sub moderate_location : Private { + my ($self, $c) = @_; - if ($anonymous != $old_anonymous) { + my $problem = $c->stash->{problem}; - $original->insert unless $original->in_storage; - $problem->update({ anonymous => $anonymous }); - return 'anonymous'; + my $moved = $c->forward('/admin/report_edit_location', [ $problem ]); + if (!$moved) { + # New lat/lon isn't valid, show an error + $c->stash->{moderate_errors} ||= []; + push @{ $c->stash->{moderate_errors} }, _('Invalid location. New location must be covered by the same council.'); + } elsif ($moved == 2) { + return 'location'; } - return; } -sub report_moderate_photo : Private { - my ( $self, $c ) = @_; +# No update left at present +sub moderate_category : Private { + my ($self, $c) = @_; - my $problem = $c->stash->{problem} or die; - my $original = $c->stash->{problem_original}; + return unless $c->get_param('category'); - return unless $original->photo; + # The admin category editing needs to know all the categories etc + $c->forward('/admin/categories_for_point'); - my $show_photo = $c->get_param('problem_show_photo') ? 1 : 0; - my $old_show_photo = $problem->photo ? 1 : 0; + my $problem = $c->stash->{problem}; - if ($show_photo != $old_show_photo) { - $original->insert unless $original->in_storage; - $problem->update({ photo => $show_photo ? $original->photo : undef }); - return 'photo'; + my $changed = $c->forward( '/admin/report_edit_category', [ $problem, 1 ] ); + # It might need to set_report_extras in future + if ($changed) { + return 'category'; + } +} + +# Note that if a cobrand allows state moderation, then the moderation reason +# given will be added as an update and thus be publicly available (unlike with +# normal moderation). +sub moderate_state : Private { + my ($self, $c) = @_; + + my $new_state = $c->get_param('state'); + return unless $new_state; + + my $problem = $c->stash->{problem}; + if ($problem->state ne $new_state) { + $problem->state($new_state); + $problem->add_to_comments( { + text => $c->stash->{moderation_reason}, + created => \'current_timestamp', + confirmed => \'current_timestamp', + user_id => $c->user->id, + name => $c->user->from_body ? $c->user->from_body->name : $c->user->name, + state => 'confirmed', + mark_fixed => 0, + anonymous => $c->user->from_body ? 0 : 1, + problem_state => $new_state, + } ); + return 'state'; } - return; } sub update : Chained('report') : PathPart('update') : CaptureArgs(1) { my ($self, $c, $id) = @_; my $comment = $c->stash->{problem}->comments->find($id); - my $original = $comment->find_or_new_related( moderation_original_data => { + # Make sure user can moderate this update + $c->detach unless $comment && $c->user->can_moderate($comment); + + $c->stash->{history} = $comment->new_related( moderation_original_data => { detail => $comment->text, photo => $comment->photo, anonymous => $comment->anonymous, + $comment->extra ? (extra => $comment->extra) : (), }); $c->stash->{comment} = $comment; - $c->stash->{comment_original} = $original; + $c->stash->{original} = $comment->moderation_original_data || $c->stash->{history}; } sub moderate_update : Chained('update') : PathPart('') : Args(0) { @@ -234,31 +376,16 @@ sub moderate_update : Chained('update') : PathPart('') : Args(0) { $c->forward('update_moderate_hide'); my @types = grep $_, - $c->forward('update_moderate_detail'), - $c->forward('update_moderate_anon'), - $c->forward('update_moderate_photo'); - - $c->detach( 'update_moderate_audit', \@types ) -} - -sub update_moderate_audit : Private { - my ($self, $c, @types) = @_; - - my $user = $c->user->obj; - my $reason = $c->stash->{'moderation_reason'}; - my $problem = $c->stash->{problem} or die; - my $comment = $c->stash->{comment} or die; - - my $types_csv = join ', ' => @types; - - $c->model('DB::AdminLog')->create({ - action => 'moderation', - user => $user, - admin_user => moderating_user_name($user), - object_id => $comment->id, - object_type => 'update', - reason => (sprintf '%s (%s)', $reason, $types_csv), - }); + $c->forward('moderate_text', [ 'text' ]), + $c->forward('moderate_boolean', [ 'anonymous', 'show_name' ]), + $c->forward('moderate_extra'), + $c->forward('moderate_boolean', [ 'photo' ]); + + if (@types) { + $c->stash->{history}->insert; + $c->stash->{comment}->update; + $c->detach('moderate_log_entry', [ 'update', @types ]); + } } sub update_moderate_hide : Private { @@ -269,77 +396,10 @@ sub update_moderate_hide : Private { if ($c->get_param('update_hide')) { $comment->hide; - $c->detach( 'update_moderate_audit', ['hide'] ); # break chain here. - } - return; -} - -sub update_moderate_detail : Private { - my ( $self, $c ) = @_; - - my $problem = $c->stash->{problem} or die; - my $comment = $c->stash->{comment} or die; - my $original = $c->stash->{comment_original}; - - my $old_detail = $comment->text; - my $original_detail = $original->detail; - my $detail = $c->get_param('update_revert_detail') ? - $original_detail - : $c->get_param('update_detail'); - - if ($detail ne $old_detail) { - $original->insert unless $original->in_storage; - $comment->update({ text => $detail }); - return 'detail'; - } - return; -} - -sub update_moderate_anon : Private { - my ( $self, $c ) = @_; - - my $problem = $c->stash->{problem} or die; - my $comment = $c->stash->{comment} or die; - my $original = $c->stash->{comment_original}; - - my $show_user = $c->get_param('update_show_name') ? 1 : 0; - my $anonymous = $show_user ? 0 : 1; - my $old_anonymous = $comment->anonymous ? 1 : 0; - - if ($anonymous != $old_anonymous) { - $original->insert unless $original->in_storage; - $comment->update({ anonymous => $anonymous }); - return 'anonymous'; - } - return; -} - -sub update_moderate_photo : Private { - my ( $self, $c ) = @_; - - my $problem = $c->stash->{problem} or die; - my $comment = $c->stash->{comment} or die; - my $original = $c->stash->{comment_original}; - - return unless $original->photo; - - my $show_photo = $c->get_param('update_show_photo') ? 1 : 0; - my $old_show_photo = $comment->photo ? 1 : 0; - - if ($show_photo != $old_show_photo) { - $original->insert unless $original->in_storage; - $comment->update({ photo => $show_photo ? $original->photo : undef }); - return 'photo'; + $c->detach('moderate_log_entry', [ 'update', 'hide' ]); # break chain here. } } -sub return_text : Private { - my ($self, $c, $text) = @_; - - $c->res->content_type('text/plain; charset=utf-8'); - $c->res->body( $text // '' ); -} - __PACKAGE__->meta->make_immutable; 1; |