From a0442af44586115a2938590516b1af943928b2d4 Mon Sep 17 00:00:00 2001 From: Antoine B <56827368+4nt0ineB@users.noreply.github.com> Date: Wed, 7 Aug 2024 14:20:45 +0200 Subject: [PATCH] feat: added a geoip api endpoint (#10648) --- cgi/display.pl | 10 +- lib/ProductOpener/API.pm | 102 ++++++++---------- lib/ProductOpener/GeoIP.pm | 30 ++++++ lib/ProductOpener/Routing.pm | 25 ++--- .../patch-broken-json-body.json | 2 +- 5 files changed, 89 insertions(+), 80 deletions(-) diff --git a/cgi/display.pl b/cgi/display.pl index fffb4ea078fe7..549f99193abd4 100755 --- a/cgi/display.pl +++ b/cgi/display.pl @@ -52,7 +52,10 @@ # Special behaviors for API v3 requests -if ($env_query_string =~ /^\/?api\/v(3(\.\d+)?)\//) { +my $api_pattern = qr/^\/?api\/v(3(\.\d+)?)\//; +my $method_pattern = qr/^(POST|PUT|PATCH)$/; + +if ($env_query_string =~ $api_pattern) { # Record that we have an API v3 request, as errors (e.g. bad userid and password) will be handled differently # (through API::process_api_request instead of returning an error page in HTML) @@ -65,13 +68,14 @@ # if we have such a request, we need to read the body before CGI.pm tries to read it to get multipart/form-data parameters # We also need to do this before the call to init_request() which calls init_user() # so that authentification credentials user_id and password from the JSON body can be used to authenticate the user - if ($request_ref->{method} =~ /^(POST|PUT|PATCH)$/) { + if ($request_ref->{method} =~ $method_pattern) { read_request_body($request_ref); decode_json_request_body($request_ref); } + } -if (($env_query_string !~ /^\/?api\/v(3(\.\d+)?)\//) or ($request_ref->{method} !~ /^(POST|PUT|PATCH)$/)) { +if (($env_query_string !~ $api_pattern) or ($request_ref->{method} !~ $method_pattern)) { # Not an API v3 POST/PUT/PATCH request: we will use CGI.pm param() method to access query string or multipart/form-data parameters # The nginx reverse proxy turns /somepath?someparam=somevalue to /cgi/display.pl?/somepath?someparam=somevalue diff --git a/lib/ProductOpener/API.pm b/lib/ProductOpener/API.pm index 31ad07cd47ac6..a154d933f3c49 100644 --- a/lib/ProductOpener/API.pm +++ b/lib/ProductOpener/API.pm @@ -73,6 +73,7 @@ use ProductOpener::KnowledgePanels qw/create_knowledge_panels initialize_knowled use ProductOpener::Ecoscore qw/localize_ecoscore/; use ProductOpener::Packaging qw/%packaging_taxonomies/; use ProductOpener::Permissions qw/has_permission/; +use ProductOpener::GeoIP qw/get_country_for_ip_api/; use ProductOpener::APIProductRead qw/read_product_api/; use ProductOpener::APIProductWrite qw/write_product_api/; @@ -384,6 +385,43 @@ Reference to the request object. =cut +# Dipatch table for API actions +my $dispatch_table = { + # Product read or write + product => { + GET => \&read_product_api, + HEAD => \&read_product_api, + OPTIONS => sub {return;}, # Just return CORS headers + PATCH => \&write_product_api, + }, + # Product revert + 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) + POST => \&revert_product_api, + }, + # Product services + product_services => { + POST => \&product_services_api, + OPTIONS => sub {return;}, # Just return CORS headers + }, + # Taxonomy suggestions + taxonomy_suggestions => { + GET => \&taxonomy_suggestions_api, + HEAD => \&taxonomy_suggestions_api, + OPTIONS => sub {return;}, # Just return CORS headers + }, + # Tag read + tag => { + GET => \&read_tag_api, + HEAD => \&read_tag_api, + OPTIONS => sub {return;}, # Just return CORS headers + }, + geoip => { + GET => \&get_country_for_ip_api, + } + +}; + sub process_api_request ($request_ref) { $log->debug("process_api_request - start", {request => $request_ref}) if $log->is_debug(); @@ -396,70 +434,16 @@ sub process_api_request ($request_ref) { if $log->is_warn(); } else { - # Route the API request to the right processing function, based on API action (from path) and method - - # Product read or write - if ($request_ref->{api_action} eq "product") { - - if ($request_ref->{api_method} eq "OPTIONS") { - # Just return CORS headers - } - elsif ($request_ref->{api_method} eq "PATCH") { - write_product_api($request_ref); - } - elsif ($request_ref->{api_method} =~ /^(GET|HEAD)$/) { - read_product_api($request_ref); - } - else { - 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); + if (exists $dispatch_table->{$request_ref->{api_action}}) { + my $action_dispatch_ref = $dispatch_table->{$request_ref->{api_action}}; + if (exists $action_dispatch_ref->{$request_ref->{api_method}}) { + $action_dispatch_ref->{$request_ref->{api_method}}->($request_ref); } else { add_invalid_method_error($response_ref, $request_ref); } } - # Product services - elsif ($request_ref->{api_action} eq "product_services") { - - if ($request_ref->{api_method} eq "OPTIONS") { - # Just return CORS headers - } - elsif ($request_ref->{api_method} eq "POST") { - product_services_api($request_ref); - } - else { - add_invalid_method_error($response_ref, $request_ref); - } - } - # Taxonomy suggestions - elsif ($request_ref->{api_action} eq "taxonomy_suggestions") { - - if ($request_ref->{api_method} =~ /^(GET|HEAD|OPTIONS)$/) { - taxonomy_suggestions_api($request_ref); - } - else { - add_invalid_method_error($response_ref, $request_ref); - } - } - # Tag read - elsif ($request_ref->{api_action} eq "tag") { - - if ($request_ref->{api_method} =~ /^(GET|HEAD|OPTIONS)$/) { - read_tag_api($request_ref); - } - else { - add_invalid_method_error($response_ref, $request_ref); - } - } - # Unknown action else { $log->warn("process_api_request - unknown action", {request => $request_ref}) if $log->is_warn(); add_error( @@ -474,9 +458,7 @@ sub process_api_request ($request_ref) { } determine_response_result($response_ref); - add_localized_messages_to_api_response($request_ref->{lc}, $response_ref); - send_api_response($request_ref); $log->debug("process_api_request - stop", {request => $request_ref}) if $log->is_debug(); diff --git a/lib/ProductOpener/GeoIP.pm b/lib/ProductOpener/GeoIP.pm index d6a35a5a85340..14cb2dfebb3d7 100644 --- a/lib/ProductOpener/GeoIP.pm +++ b/lib/ProductOpener/GeoIP.pm @@ -50,6 +50,7 @@ BEGIN { @EXPORT_OK = qw( &get_country_for_ip &get_country_code_for_ip + &get_country_for_ip_api ); # symbols to export on request %EXPORT_TAGS = (all => [@EXPORT_OK]); @@ -138,4 +139,33 @@ sub get_country_code_for_ip ($ip) { return $country; } +sub get_country_for_ip_api ($request_ref) { + my $response_ref = $request_ref->{api_response}; + + my $error_id; + + if (not defined $request_ref->{ip}) { + $error_id = "missing_field"; + } + else { + $response_ref->{country} = get_country_for_ip($request_ref->{ip}); + $response_ref->{cc} = get_country_code_for_ip($request_ref->{ip}); + if (not defined $response_ref->{country}) { + $error_id = "invalid_field"; + } + } + + if (defined $error_id) { + push @{$response_ref->{errors}}, + { + message => {id => $error_id}, + field => {id => "ip"}, + impact => {id => "failure"}, + }; + $response_ref->{status_code} = $400; + } + + return; +} + 1; diff --git a/lib/ProductOpener/Routing.pm b/lib/ProductOpener/Routing.pm index aba5ae709b228..371367efc4fa6 100644 --- a/lib/ProductOpener/Routing.pm +++ b/lib/ProductOpener/Routing.pm @@ -185,10 +185,6 @@ Sometimes we modify request parameters (param) to correspond to request_ref: sub analyze_request($request_ref) { sanitize_request($request_ref); - return _analyze_request_impl($request_ref); -} - -sub _analyze_request_impl($request_ref) { $log->debug("analyze_request", {components => $request_ref->{components},}) if $log->is_debug(); @@ -289,7 +285,7 @@ sub org_route($request_ref) { $log->debug("org route", {orgid => $orgid, components => $request_ref->{components}}) if $log->is_debug(); # /search # /product/[code] - return _analyze_request_impl($request_ref); + return match_route($request_ref); } # api/v0/product(s)/[code] @@ -297,17 +293,9 @@ sub org_route($request_ref) { sub api_route($request_ref) { my @components = @{$request_ref->{components}}; my $api = $components[1]; # v0 + my $api_version = $api =~ /v(\d+)/ ? $1 : 0; my $api_action = $components[2]; # product - my $api_version = $api; - ($api_version) = $api =~ /v(\d+)/; - $api_version //= 0; - - # Also support "products" in order not to break apps that were using it - if ($api_action eq 'products') { - $api_action = 'product'; - } - # If the api_action is different than "search", check if it is the local path for "product" # so that urls like https://fr.openfoodfacts.org/api/v3/produit/4324232423 work (produit instead of product) # this is so that we can quickly add /api/v3/ to get the API @@ -319,7 +307,9 @@ sub api_route($request_ref) { } # some API actions have an associated object - if ($api_action eq "product") { # api/v3/product/[code] + + # Also support "products" in order not to break apps that were using it + if ($api_action =~ /^products?/) { # api/v3/product/[code] param("code", $components[3]); $request_ref->{code} = $components[3]; } @@ -327,7 +317,10 @@ sub api_route($request_ref) { param("tagtype", $components[3]); $request_ref->{tagtype} = $components[3]; param("tagid", $components[4]); - $request_ref->{tagid} = $components[5]; + $request_ref->{tagid} = $components[4]; + } + elsif ($api_action eq "geoip") { # api/v3/geoip/[ip] + $request_ref->{ip} = $components[3]; } # If return format is not xml or jqm or jsonp, default to json diff --git a/tests/integration/expected_test_results/api_v3_product_write/patch-broken-json-body.json b/tests/integration/expected_test_results/api_v3_product_write/patch-broken-json-body.json index 1feba8808200e..99a1c39ad62e0 100644 --- a/tests/integration/expected_test_results/api_v3_product_write/patch-broken-json-body.json +++ b/tests/integration/expected_test_results/api_v3_product_write/patch-broken-json-body.json @@ -2,7 +2,7 @@ "errors" : [ { "field" : { - "error" : "'null' expected, at character offset 0 (before \"not json\") at /opt/product-opener/lib/ProductOpener/API.pm line 224.\n", + "error" : "'null' expected, at character offset 0 (before \"not json\") at /opt/product-opener/lib/ProductOpener/API.pm line 225.\n", "id" : "body", "value" : "not json" },