Skip to content

Commit

Permalink
feat: added a geoip api endpoint (openfoodfacts#10648)
Browse files Browse the repository at this point in the history
  • Loading branch information
4nt0ineB authored Aug 7, 2024
1 parent 5c111d6 commit a0442af
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 80 deletions.
10 changes: 7 additions & 3 deletions cgi/display.pl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
102 changes: 42 additions & 60 deletions lib/ProductOpener/API.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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/;
Expand Down Expand Up @@ -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();
Expand All @@ -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(
Expand All @@ -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();
Expand Down
30 changes: 30 additions & 0 deletions lib/ProductOpener/GeoIP.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down Expand Up @@ -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;
25 changes: 9 additions & 16 deletions lib/ProductOpener/Routing.pm
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -289,25 +285,17 @@ 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]
# api/v0/search
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
Expand All @@ -319,15 +307,20 @@ 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];
}
elsif ($api_action eq "tag") { # api/v3/tag/[type]/[tagid]
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down

0 comments on commit a0442af

Please sign in to comment.