Skip to content

Commit

Permalink
feat: Revert product to a previous revision (API + upcoming website i…
Browse files Browse the repository at this point in the history
…ntegration for moderators) (#9800)
  • Loading branch information
stephanegigandet authored Mar 8, 2024
1 parent cd5e555 commit 985e353
Show file tree
Hide file tree
Showing 34 changed files with 1,131 additions and 227 deletions.
2 changes: 1 addition & 1 deletion cgi/product_jqm_multilingual.pl
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ =head1 DESCRIPTION

$code = normalize_code($code);

if ($code !~ /^\d{4,24}$/) {
if (not is_valid_code($code)) {

$log->info("invalid code", {code => $code, original_code => $original_code}) if $log->is_info();
$response{status} = 0;
Expand Down
29 changes: 15 additions & 14 deletions cgi/product_multilingual.pl
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ ($product_ref)
# Response structure to keep track of warnings and errors
# Note: currently some warnings and errors are added,
# but we do not yet do anything with them
my $response_ref = get_initialized_response();
my $response_ref = ProductOpener::API::get_initialized_response();

my $type = single_param('type') || 'search_or_add';
my $action = single_param('action') || 'display';
Expand Down Expand Up @@ -220,7 +220,7 @@ ($product_ref)
if ((not defined $code) or ($code eq "")) {
$code = process_search_image_form(\$filename);
}
elsif ($code !~ /^\d{4,24}$/) {
elsif (not is_valid_code($code)) {
display_error_and_exit($Lang{invalid_barcode}{$lang}, 403);
}

Expand Down Expand Up @@ -315,7 +315,7 @@ ($product_ref)
if ((not defined $code) or ($code eq '')) {
display_error_and_exit($Lang{missing_barcode}{$lang}, 403);
}
elsif ($code !~ /^\d{4,24}$/) {
elsif (not is_valid_code($code)) {
display_error_and_exit($Lang{invalid_barcode}{$lang}, 403);
}
else {
Expand Down Expand Up @@ -776,19 +776,20 @@ ($product_ref, $field, $language)
;

$scripts .= <<HTML
<script type="text/javascript" src="/js/dist/webcomponentsjs/webcomponents-loader.js"></script>
<script type="text/javascript" src="/js/dist/cropper.js"></script>
<script type="text/javascript" src="/js/dist/jquery-cropper.js"></script>
<script type="text/javascript" src="/js/dist/jquery.form.js"></script>
<script type="text/javascript" src="/js/dist/tagify.min.js"></script>
<script type="text/javascript" src="/js/dist/jquery.iframe-transport.js"></script>
<script type="text/javascript" src="/js/dist/jquery.fileupload.js"></script>
<script type="text/javascript" src="/js/dist/load-image.all.min.js"></script>
<script type="text/javascript" src="/js/dist/canvas-to-blob.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/webcomponentsjs/webcomponents-loader.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/cropper.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/jquery-cropper.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/jquery.form.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/tagify.min.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/jquery.iframe-transport.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/jquery.fileupload.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/load-image.all.min.js"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/canvas-to-blob.js"></script>
<script type="text/javascript">
var admin = $moderator;
</script>
<script type="text/javascript" src="/js/dist/product-multilingual.js?v=$file_timestamps{'js/dist/product-multilingual.js'}"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/product-multilingual.js?v=$file_timestamps{'js/dist/product-multilingual.js'}"></script>
<script type="text/javascript" src="$static_subdomain/js/dist/product-history.js"></script>
HTML
;
Expand Down Expand Up @@ -1480,7 +1481,7 @@ ($product_ref, $field, $language)
$template_data_ref_display->{param_fields} = single_param("fields");
$template_data_ref_display->{type} = $type;
$template_data_ref_display->{code} = $code;
$template_data_ref_display->{display_product_history} = display_product_history($code, $product_ref);
$template_data_ref_display->{display_product_history} = display_product_history($request_ref, $code, $product_ref);
$template_data_ref_display->{product} = $product_ref;

process_template('web/pages/product_edit/product_edit_form_display.tt.html', $template_data_ref_display, \$html)
Expand Down
33 changes: 33 additions & 0 deletions docs/api/ref/api-v3.yml
Original file line number Diff line number Diff line change
Expand Up @@ -362,6 +362,39 @@ paths:
Categories:
- Packaging stats for a category
/api/v3/product_revert:
parameters: []
post:
summary: Revert a product to a previous revision
tags: []
responses:
'200':
description: OK
content:
application/json:
schema:
allOf:
- $ref: ./responses/response-status/response_status.yaml
operationId: post-api-v3-product_revert
description: |-
For moderators only, revert a product to a previous revision.
requestBody:
content:
application/json:
schema:
allOf:
- $ref: ./requestBodies/fields_tags_lc.yaml
- type: object
properties:
code:
type: string
description: Barcode of the product
rev:
type: integer
description: Revision number to revert to
description: |
The code and rev fields are mandatory.
parameters: []
components:
parameters:
cc:
Expand Down
2 changes: 1 addition & 1 deletion gulpfile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const sass = gulpSass(sassLib);

const jsSrc = [
"./html/js/display*.js",
"./html/js/product-multilingual.js",
"./html/js/product-*.js",
"./html/js/search.js",
"./html/js/hc-sticky.js",
"./html/js/stikelem.js",
Expand Down
60 changes: 60 additions & 0 deletions html/js/product-history.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
// This file is part of Product Opener.
//
// Product Opener
// Copyright (C) 2011-2024 Association Open Food Facts
// Contact: [email protected]
// Address: 21 rue des Iles, 94100 Saint-Maur des Fossés, France
//
// Product Opener is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as
// published by the Free Software Foundation, either version 3 of the
// License, or (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.

/*global revert_confirm_message*/
/*exported activate_product_revert_buttons_in_history*/

function activate_product_revert_buttons_in_history () {
$('#history_list a.product_revert_button').on('click', function() {
const code = $(this).data('code');
const rev = $(this).data('rev');
// using confirm, could be replaced with some JS dialog / modal
const confirm = window.confirm(revert_confirm_message); // eslint-disable-line no-alert
if (confirm) {
$.ajax({
url: '/api/v3/product_revert',
type: 'POST',
contentType: "application/json; charset=utf-8",
dataType: "json",
data: JSON.stringify({
code: code,
rev: rev,
fields: "rev"
// we don't pass cc and lc, as they will get the right default value from the subdomain
}),
success: function(data) {
let message = data.status;
if (data.status === 'success') {
message = message + ' - <a href="/product/' + code +'">' + data.result.lc_name + '</a>';
}
else {
message = message + ' - ' + data.result.lc_name;
}
$('#revert_result_' + rev).html(message);
}
});
}
});
}

$(function() {
activate_product_revert_buttons_in_history();
});

92 changes: 89 additions & 3 deletions lib/ProductOpener/API.pm
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ BEGIN {
&decode_json_request_body
&normalize_requested_code
&customize_response_for_product
&check_user_permission
); # symbols to export on request
%EXPORT_TAGS = (all => [@EXPORT_OK]);
}
Expand All @@ -71,9 +72,11 @@ use ProductOpener::Attributes qw/:all/;
use ProductOpener::KnowledgePanels qw/:all/;
use ProductOpener::Ecoscore qw/localize_ecoscore/;
use ProductOpener::Packaging qw/:all/;
use ProductOpener::Permissions qw/:all/;

use ProductOpener::APIProductRead qw/:all/;
use ProductOpener::APIProductWrite qw/:all/;
use ProductOpener::APIProductRevert qw/:all/;
use ProductOpener::APIProductServices qw/:all/;
use ProductOpener::APITagRead qw/:all/;
use ProductOpener::APITaxonomySuggestions qw/:all/;
Expand Down Expand Up @@ -106,8 +109,29 @@ sub add_warning ($response_ref, $warning_ref) {
return;
}

sub add_error ($response_ref, $error_ref) {
=head2 add_error ($response_ref, $error_ref, $status_code = 400)
Add an error to the response object.
=head3 Parameters
=head4 $response_ref (input)
Reference to the response object.
=head4 $error_ref (input)
Reference to the error object.
=head4 $status_code (input)
HTTP status code to return in the response, defaults to 400 bad request.
=cut

sub add_error ($response_ref, $error_ref, $status_code = 400) {
push @{$response_ref->{errors}}, $error_ref;
$response_ref->{status_code} = $status_code;
return;
}

Expand All @@ -124,7 +148,8 @@ sub add_invalid_method_error ($response_ref, $request_ref) {
api_action => $request_ref->{api_action},
},
impact => {id => "failure"},
}
},
405
);
return;
}
Expand Down Expand Up @@ -318,7 +343,8 @@ Reference to the customized product object.

sub send_api_response ($request_ref) {

my $status_code = $request_ref->{status_code} || "200";
my $status_code = $request_ref->{api_response}{status_code} || $request_ref->{status_code} || "200";
delete $request_ref->{api_response}{status_code};

my $json = JSON::PP->new->allow_nonref->canonical->utf8->encode($request_ref->{api_response});

Expand Down Expand Up @@ -389,6 +415,17 @@ sub process_api_request ($request_ref) {
add_invalid_method_error($response_ref, $request_ref);
}
}
# Product revert
elsif ($request_ref->{api_action} eq "product_revert") {

# Check that the method is POST (GET may be dangerous: it would allow to revert a product by just clicking or loading a link)
if ($request_ref->{api_method} eq "POST") {
revert_product_api($request_ref);
}
else {
add_invalid_method_error($response_ref, $request_ref);
}
}
# Product services
elsif ($request_ref->{api_action} eq "product_services") {

Expand Down Expand Up @@ -833,4 +870,53 @@ sub customize_response_for_product ($request_ref, $product_ref, $fields_comma_se
return $customized_product_ref;
}

=head2 check_user_permission ($request_ref, $response_ref, $permission)
Check the user has a specific permission, before processing an API request.
If the user does not have the permission, an error is added to the response.
=head3 Parameters
=head4 $request_ref (input)
Reference to the request object.
=head4 $response_ref (output)
Reference to the response object.
=head4 $permission (input)
Permission to check.
=head3 Return value
1 if the user does not have the permission, 0 otherwise.
=cut

sub check_user_permission ($request_ref, $response_ref, $permission) {

# We will return an error equal to 1 if the user does not have the permission
my $error = 0;

# Check if the user has permission
if (not has_permission($request_ref, $permission)) {
$error = 1;
$log->error("check_user_permission - user does not have permission", {permission => $permission})
if $log->is_error();
add_error(
$response_ref,
{
message => {id => "no_permission"},
field => {id => "permission", value => $permission},
impact => {id => "failure"},
},
403
);
}

return $error;
}

1;
7 changes: 2 additions & 5 deletions lib/ProductOpener/APIProductRead.pm
Original file line number Diff line number Diff line change
Expand Up @@ -124,17 +124,14 @@ sub read_product_api ($request_ref) {

# Return an error if we could not find a product

if ($request_ref->{api_version} >= 1) {
$request_ref->{status_code} = 404;
}

add_error(
$response_ref,
{
message => {id => "product_not_found"},
field => {id => "code", value => $code},
impact => {id => "failure"},
}
},
404
);
$response_ref->{result} = {id => "product_not_found"};
}
Expand Down
Loading

0 comments on commit 985e353

Please sign in to comment.