diff --git a/.env b/.env index 1e94eadad2725..1010665213d56 100644 --- a/.env +++ b/.env @@ -1,11 +1,23 @@ -DOCKER_CLIENT_TIMEOUT=120 -COMPOSE_HTTP_TIMEOUT=120 +# docker config +DOCKER_CLIENT_TIMEOUT=320 +COMPOSE_HTTP_TIMEOUT=320 COMPOSE_PROJECT_NAME=po COMPOSE_PATH_SEPARATOR=; COMPOSE_FILE=docker-compose.yml;docker/dev.yml +# exposition of product opener, leave blank to expose on all ports +# ends with ":" +PRODUCT_OPENER_EXPOSE=127.0.0.1: +# same but for all admin tools +ADMIN_EXPOSE=127.0.0.1: + +# version for backend and frontend images TAG=latest -WWW_DATA_HOST_USER=${UID} +# setting to align host user to internal user +USER_UID=1000 +USER_GID=1000 + +# env vars PRODUCERS_PLATFORM=0 PRODUCT_OPENER_DOMAIN=openfoodfacts.localhost PRODUCT_OPENER_PORT=80 @@ -17,7 +29,7 @@ MONGODB_HOST=mongodb MONGODB_CACHE_SIZE=8 # GB MONGO_INITDB_ROOT_USERNAME=root MONGO_INITDB_ROOT_PASSWORD=test -ROBOTOFF_URL=robotoff.openfoodfacts.localhost +ROBOTOFF_URL=http://host.docker.internal:5500 # connect to Robotoff running in separate docker-compose deployment GOOGLE_CLOUD_VISION_API_KEY= CROWDIN_PROJECT_IDENTIFIER= CROWDIN_PROJECT_KEY= @@ -25,3 +37,5 @@ GEOLITE2_PATH= GEOLITE2_LICENSE_KEY= GEOLITE2_ACCOUNT_ID= ELASTICSEARCH_HOSTS= +LOG_LEVEL_ROOT=TRACE +LOG_LEVEL_MONGODB=TRACE diff --git a/.github/workflows/container-deploy.yml b/.github/workflows/container-deploy.yml index 1d09829bea226..e515348a494c5 100644 --- a/.github/workflows/container-deploy.yml +++ b/.github/workflows/container-deploy.yml @@ -94,6 +94,9 @@ jobs: echo "COMPOSE_PATH_SEPARATOR=;" >> .env echo "COMPOSE_FILE=docker-compose.yml;docker/prod.yml;docker/geolite2.yml;docker/monitor.yml" >> .env + echo "PRODUCT_OPENER_EXPOSE=" >> .env + echo "ADMIN_EXPOSE=" >> .env + # Set App variables echo "TAG=sha-${{ github.sha }}" >> .env echo "PRODUCERS_PLATFORM=0" >> .env @@ -111,6 +114,8 @@ jobs: echo "GEOLITE2_PATH=${{ secrets.GEOLITE2_PATH }}" >> .env echo "GEOLITE2_LICENSE_KEY=${{ secrets.GEOLITE2_LICENSE_KEY }}" >> .env echo "GEOLITE2_ACCOUNT_ID=${{ secrets.GEOLITE2_ACCOUNT_ID }}" >> .env + echo "LOG_LEVEL_ROOT=ERROR" >> .env + echo "LOG_LEVEL_MONGODB=ERROR" >> .env # Override domain name in nginx.conf sed -i.bak "s/productopener.localhost/${{ secrets.PRODUCT_OPENER_DOMAIN }}/g" ./conf/nginx.conf @@ -129,6 +134,20 @@ jobs: cd ${{ matrix.env }} make create_external_volumes + - name: init backend + uses: appleboy/ssh-action@master + with: + host: ${{ secrets.HOST }} + username: ${{ secrets.USERNAME }} + key: ${{ secrets.SSH_PRIVATE_KEY }} + proxy_host: ${{ secrets.PROXY_HOST }} + proxy_username: ${{ secrets.USERNAME }} + proxy_key: ${{ secrets.SSH_PRIVATE_KEY }} + script_stop: false + script: | + cd ${{ matrix.env }} + make init_backend + - name: Start services uses: appleboy/ssh-action@master with: @@ -142,8 +161,8 @@ jobs: script: | cd ${{ matrix.env }} make hdown + make build_lang make up - make setup_incron - name: Check services are up uses: appleboy/ssh-action@master @@ -175,7 +194,7 @@ jobs: cd ${{ matrix.env }} make prune - - uses: frankie567/grafana-annotation-action@v1.0.2 + - uses: frankie567/grafana-annotation-action@v1.0.3 if: ${{ always() }} with: apiHost: https://grafana.openfoodfacts.org diff --git a/.github/workflows/crowdin-per-language.yml b/.github/workflows/crowdin-per-language.yml index 756f9fc0ce48a..45809ddd7a52f 100644 --- a/.github/workflows/crowdin-per-language.yml +++ b/.github/workflows/crowdin-per-language.yml @@ -22,7 +22,7 @@ jobs: - name: Checkout uses: actions/checkout@v2 - name: Matrix - uses: crowdin/github-action@1.4.0 + uses: crowdin/github-action@1.4.1 with: upload_translations: false # default is false download_translations: true diff --git a/.github/workflows/crowdin.yml b/.github/workflows/crowdin.yml index 0b7a7af8c3d41..e38b5b91c5bc0 100644 --- a/.github/workflows/crowdin.yml +++ b/.github/workflows/crowdin.yml @@ -17,7 +17,7 @@ jobs: - name: Checkout uses: actions/checkout@v2 - name: crowdin action - uses: crowdin/github-action@1.4.0 + uses: crowdin/github-action@1.4.1 with: upload_translations: false # default is false # Use this option to upload translations for a single specified language diff --git a/.gitignore b/.gitignore index d3832957b9d01..58d27086180b1 100644 --- a/.gitignore +++ b/.gitignore @@ -14,9 +14,10 @@ Thumbs.db .vstags # Logs +logs/ +debug/ Store.debug.txt *.log -logs/modperl_error_log nytprof*.out # Local databases @@ -36,6 +37,9 @@ html/js/dist html/images/icons/dist html/images/attributes/*.svg +# Images +html/images/products/* + # Don't store helm dependencies in our git repo docker/productopener/charts docker/logs diff --git a/Dockerfile b/Dockerfile index c23a0cb722e69..9b6ad815bb506 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,138 +1,152 @@ -FROM bitnami/minideb:buster AS modperl +# syntax=docker/dockerfile:1.2 +# Base user uid / gid keep 1000 on prod, align with your user on dev +ARG USER_UID=1000 +ARG USER_GID=1000 -# Install cpm to install cpanfile dependencies -RUN set -x \ - && install_packages \ - apache2 \ - apt-utils \ - cpanminus \ - g++ \ - gcc \ - less \ - libapache2-mod-perl2 \ - # libexpat1-dev \ - make \ - wget \ - imagemagick \ - graphviz \ - tesseract-ocr \ - # perlmagick \ - # - # Packages from ./cpanfile: - # If cpanfile specifies a newer version than apt has, cpanm will install the newer version. - # - libtie-ixhash-perl \ - libwww-perl \ - libimage-magick-perl \ - libxml-encoding-perl \ - libtext-unaccent-perl \ - libmime-lite-perl \ - libcache-memcached-fast-perl \ - libjson-pp-perl \ - libclone-perl \ - libcrypt-passwdmd5-perl \ - libencode-detect-perl \ - libgraphics-color-perl \ - libbarcode-zbar-perl \ - libxml-feedpp-perl \ - liburi-find-perl \ - libxml-simple-perl \ - libexperimental-perl \ - libapache2-request-perl \ - libdigest-md5-perl \ - libtime-local-perl \ - libdbd-pg-perl \ - libtemplate-perl \ - liburi-escape-xs-perl \ - # NB: not available in ubuntu 1804 LTS: - libmath-random-secure-perl \ - libfile-copy-recursive-perl \ - libemail-stuffer-perl \ - liblist-moreutils-perl \ - libexcel-writer-xlsx-perl \ - libpod-simple-perl \ - liblog-any-perl \ - liblog-log4perl-perl \ - liblog-any-adapter-log4perl-perl \ - # NB: not available in ubuntu 1804 LTS: - libgeoip2-perl \ - libemail-valid-perl \ - # - # cpan dependencies that can be satisfied by apt even if the package itself can't: - # - # Action::Retry - libmath-fibonacci-perl \ - # Algorithm::CheckDigits - libprobe-perl-perl \ - # CLDR::Number - libmath-round-perl \ - libsoftware-license-perl \ - libtest-differences-perl \ - libtest-exception-perl \ - # Data::Dumper::AutoEncode - # NB: not available in ubuntu 1804 LTS: - libmodule-build-pluggable-perl \ - libclass-accessor-lite-perl \ - # DateTime - libclass-singleton-perl \ - # DateTime::Locale - libfile-sharedir-install-perl \ - # Encode::Punycode - libnet-idn-encode-perl \ - libtest-nowarnings-perl \ - # File::chmod::Recursive - libfile-chmod-perl \ - # GeoIP2 - libdata-dumper-concise-perl \ - libdata-printer-perl \ - libdata-validate-ip-perl \ - libio-compress-perl \ - libjson-maybexs-perl \ - liblist-allutils-perl \ - liblist-someutils-perl \ - # GraphViz2 - libdata-section-simple-perl \ - libfile-which-perl \ - libipc-run3-perl \ - liblog-handler-perl \ - libtest-deep-perl \ - libwant-perl \ - # Image::OCR::Tesseract - libfile-find-rule-perl \ - liblinux-usermod-perl \ - # Locale::Maketext::Lexicon::Getcontext - liblocale-maketext-lexicon-perl \ - # Log::Any::Adapter::TAP - liblog-any-adapter-tap-perl \ - # Math::Random::Secure - libcrypt-random-source-perl \ - libmath-random-isaac-perl \ - libtest-sharedfork-perl \ - libtest-warn-perl \ - # Mojo::Pg - libsql-abstract-perl \ - # MongoDB - libauthen-sasl-saslprep-perl \ - libauthen-scram-perl \ - libbson-perl \ - libclass-xsaccessor-perl \ - libconfig-autoconf-perl \ - libdigest-hmac-perl \ - libpath-tiny-perl \ - libsafe-isa-perl \ - # Spreadsheet::CSV - libspreadsheet-parseexcel-perl \ - # Test::Number::Delta - libtest-number-delta-perl \ - libdevel-size-perl \ - gnumeric \ - incron -# Run www-data user as host user 'off' -ARG WWW_DATA_HOST_USER=1000 -RUN usermod -u $WWW_DATA_HOST_USER www-data +###################### +# Base modperl image stage +###################### +FROM bitnami/minideb:buster AS modperl +# Install cpm to install cpanfile dependencies +RUN --mount=type=cache,id=apt-cache,target=/var/cache/apt set -x && \ + install_packages \ + apache2 \ + apt-utils \ + cpanminus \ + g++ \ + gcc \ + less \ + libapache2-mod-perl2 \ + # libexpat1-dev \ + make \ + wget \ + imagemagick \ + graphviz \ + tesseract-ocr \ + # perlmagick \ + # + # Packages from ./cpanfile: + # If cpanfile specifies a newer version than apt has, cpanm will install the newer version. + # + libtie-ixhash-perl \ + libwww-perl \ + libimage-magick-perl \ + libxml-encoding-perl \ + libtext-unaccent-perl \ + libmime-lite-perl \ + libcache-memcached-fast-perl \ + libjson-pp-perl \ + libclone-perl \ + libcrypt-passwdmd5-perl \ + libencode-detect-perl \ + libgraphics-color-perl \ + libbarcode-zbar-perl \ + libxml-feedpp-perl \ + liburi-find-perl \ + libxml-simple-perl \ + libexperimental-perl \ + libapache2-request-perl \ + libdigest-md5-perl \ + libtime-local-perl \ + libdbd-pg-perl \ + libtemplate-perl \ + liburi-escape-xs-perl \ + # NB: not available in ubuntu 1804 LTS: + libmath-random-secure-perl \ + libfile-copy-recursive-perl \ + libemail-stuffer-perl \ + liblist-moreutils-perl \ + libexcel-writer-xlsx-perl \ + libpod-simple-perl \ + liblog-any-perl \ + liblog-log4perl-perl \ + liblog-any-adapter-log4perl-perl \ + # NB: not available in ubuntu 1804 LTS: + libgeoip2-perl \ + libemail-valid-perl \ + # + # cpan dependencies that can be satisfied by apt even if the package itself can't: + # + # Action::Retry + libmath-fibonacci-perl \ + # Algorithm::CheckDigits + libprobe-perl-perl \ + # CLDR::Number + libmath-round-perl \ + libsoftware-license-perl \ + libtest-differences-perl \ + libtest-exception-perl \ + # Data::Dumper::AutoEncode + # NB: not available in ubuntu 1804 LTS: + libmodule-build-pluggable-perl \ + libclass-accessor-lite-perl \ + # DateTime + libclass-singleton-perl \ + # DateTime::Locale + libfile-sharedir-install-perl \ + # Encode::Punycode + libnet-idn-encode-perl \ + libtest-nowarnings-perl \ + # File::chmod::Recursive + libfile-chmod-perl \ + # GeoIP2 + libdata-dumper-concise-perl \ + libdata-printer-perl \ + libdata-validate-ip-perl \ + libio-compress-perl \ + libjson-maybexs-perl \ + liblist-allutils-perl \ + liblist-someutils-perl \ + # GraphViz2 + libdata-section-simple-perl \ + libfile-which-perl \ + libipc-run3-perl \ + liblog-handler-perl \ + libtest-deep-perl \ + libwant-perl \ + # Image::OCR::Tesseract + libfile-find-rule-perl \ + liblinux-usermod-perl \ + # Locale::Maketext::Lexicon::Getcontext + liblocale-maketext-lexicon-perl \ + # Log::Any::Adapter::TAP + liblog-any-adapter-tap-perl \ + # Math::Random::Secure + libcrypt-random-source-perl \ + libmath-random-isaac-perl \ + libtest-sharedfork-perl \ + libtest-warn-perl \ + # Mojo::Pg + libsql-abstract-perl \ + # MongoDB + libauthen-sasl-saslprep-perl \ + libauthen-scram-perl \ + libbson-perl \ + libclass-xsaccessor-perl \ + libconfig-autoconf-perl \ + libdigest-hmac-perl \ + libpath-tiny-perl \ + libsafe-isa-perl \ + # Spreadsheet::CSV + libspreadsheet-parseexcel-perl \ + # Test::Number::Delta + libtest-number-delta-perl \ + libdevel-size-perl \ + gnumeric \ + incron + +# Run www-data user as host user 'off' or developper uid +ARG USER_UID +ARG USER_GID +RUN usermod --uid $USER_UID www-data && \ + groupmod --gid $USER_GID www-data + + +###################### # Stage for installing/compiling cpanfile dependencies +###################### FROM modperl AS builder WORKDIR /tmp @@ -141,14 +155,12 @@ WORKDIR /tmp COPY ./cpanfile /tmp/cpanfile # Add ProductOpener runtime dependencies from cpan -RUN cpanm --notest --quiet --skip-satisfied --local-lib /tmp/local/ --installdeps . - -# Stage for installing/compiling cpanfile dependencies with dev dependencies -FROM builder AS builder-vscode +RUN --mount=type=cache,id=cpanm-cache,target=/root/.cpanm cpanm --notest --quiet --skip-satisfied --local-lib /tmp/local/ --installdeps . -# Add ProductOpener runtime dependencies from cpan -RUN cpanm --with-develop --notest --quiet --skip-satisfied --local-lib /tmp/local/ --installdeps . +###################### +# backend production image stage +###################### FROM modperl AS runnable # Prepare Apache to include our custom config @@ -158,44 +170,41 @@ RUN rm /etc/apache2/sites-enabled/000-default.conf COPY --from=builder /tmp/local/ /opt/perl/local/ ENV PERL5LIB="/opt/product-opener/lib/:/opt/perl/local/lib/perl5/" ENV PATH="/opt/perl/local/bin:${PATH}" +# Create writable dirs and change ownership to www-data +RUN \ + mkdir -p var/run/apache2/ && \ + chown www-data:www-data var/run/apache2/ && \ + for path in data html_data users products product_images orgs new_images logs tmp; do \ + mkdir -p /mnt/podata/${path}; \ + done && \ + chown www-data:www-data -R /mnt/podata && \ + # Create symlinks of data files that are indeed conf data in /mnt/podata (because we currently mix data and conf data) + for path in ecoscore emb_codes forest-footprint ingredients lang packager-codes po taxonomies templates; do \ + ln -sf /opt/product-opener/${path} /mnt/podata/${path}; \ + done && \ + # Create some necessary files to ensure permissions in volumes + mkdir -p /opt/product-opener/html/data/ && \ + mkdir -p /opt/product-opener/html/images/ && \ + chown www-data:www-data -R /opt/product-opener/html/ && \ + # logs dir + mkdir -p /var/log/apache2/ && \ + chown www-data:www-data -R /var/log +# Install Product Opener from the workdir +COPY --chown=www-data:www-data . /opt/product-opener/ +RUN \ + # www-data user shall be able to use incron + echo www-data >> /etc/incron.allow && \ + incrontab -u www-data /opt/product-opener/conf/incron.conf EXPOSE 80 COPY ./docker/docker-entrypoint.sh / +WORKDIR /opt/product-opener/ +USER www-data ENTRYPOINT [ "/docker-entrypoint.sh" ] +# default command is apache2ctl start CMD ["apache2ctl", "-D", "FOREGROUND"] -FROM runnable AS runnable-vscode -COPY --from=builder-vscode /tmp/local/ /opt/perl/local/ - -FROM runnable-vscode AS vscode -# This Dockerfile adds a non-root 'vscode' user with sudo access. However, for Linux, -# this user's GID/UID must match your local user UID/GID to avoid permission issues -# with bind mounts. Update USER_UID / USER_GID if yours is not 1000. See -# https://aka.ms/vscode-remote/containers/non-root-user for details. -ARG USERNAME=vscode -ARG USER_UID=1000 -ARG USER_GID=$USER_UID - -# Configure apt and install packages -RUN install_packages apt-utils dialog 2>&1 \ - # - # Verify git, process tools, lsb-release (common in install instructions for CLIs) installed - && install_packages git iproute2 procps lsb-release \ - # Create a non-root user to use if preferred - see https://aka.ms/vscode-remote/containers/non-root-user. - && groupadd --gid $USER_GID $USERNAME \ - && useradd -s /bin/bash --uid $USER_UID --gid $USER_GID -m $USERNAME \ - # [Optional] Add sudo support for the non-root user - && install_packages sudo \ - && echo $USERNAME ALL=\(root\) NOPASSWD:ALL > /etc/sudoers.d/$USERNAME\ - && chmod 0440 /etc/sudoers.d/$USERNAME - -FROM runnable-vscode AS perldb - -# Readline support for the Perl debugger -RUN install_packages libterm-readline-gnu-perl libterm-readkey-perl - -FROM runnable AS withsrc - -# Install Product Opener from the workdir -COPY . /opt/product-opener/ -WORKDIR /opt/product-opener +###################### +# Prod image is default +###################### +FROM runnable as prod diff --git a/Dockerfile.frontend b/Dockerfile.frontend index 8180e6d142187..f101662c9aa68 100644 --- a/Dockerfile.frontend +++ b/Dockerfile.frontend @@ -1,21 +1,41 @@ -FROM node:lts as builder +# syntax = docker/dockerfile:1.2 +# Base user uid / gid keep 1000 on prod, align with your user on dev +ARG USER_UID=1000 +ARG USER_GID=1000 -RUN mkdir -p /opt/product-opener/node_modules -COPY package*.json /opt/product-opener/ -COPY .snyk /opt/product-opener/ +FROM node:lts as builder +ARG USER_UID +ARG USER_GID +RUN usermod --uid $USER_UID node && \ + groupmod --gid $USER_GID node && \ + mkdir -p /opt/product-opener/node_modules && \ + mkdir -p /opt/product-opener/html/data && \ + mkdir -p /opt/product-opener/html/css/dist && \ + mkdir -p /opt/product-opener/html/images/icons/dist && \ + mkdir -p /opt/product-opener/html/js/dist && \ + mkdir -p /opt/product-opener/html/images/products && \ + chown node:node -R /opt/product-opener/ +COPY --chown=node:node package*.json /opt/product-opener/ +COPY --chown=node:node .snyk /opt/product-opener/ WORKDIR /opt/product-opener -RUN npm install +USER node +RUN --mount=type=cache,id=npm-cache,target=/root/.npm npm install ENV PATH /opt/product-opener/node_modules/.bin:$PATH -COPY html /opt/product-opener/html -COPY icons /opt/product-opener/icons -COPY scss /opt/product-opener/scss -COPY gulpfile.js /opt/product-opener/ +COPY --chown=node:node html /opt/product-opener/html +COPY --chown=node:node icons /opt/product-opener/icons +COPY --chown=node:node scss /opt/product-opener/scss +COPY --chown=node:node gulpfile.js /opt/product-opener/ +COPY --chown=node:node .snyk /opt/product-opener/ # https://github.com/openfoodfacts/openfoodfacts-server/pull/5544#issuecomment-914221199 RUN find . -xtype l | xargs -I {} sh -c "origin=\$(readlink {} | tr '\\\\\\\\' '/') && ln -sf \$origin {}" RUN npm run build -FROM nginx:stable-alpine +FROM nginx:stable WORKDIR /opt/product-opener +ARG USER_UID +ARG USER_GID +RUN usermod -u $USER_UID www-data && \ + groupmod --gid $USER_GID www-data COPY --from=builder /opt/product-opener/html/ /opt/product-opener/html/ -COPY --from=builder /opt/product-opener/node_modules/ /opt/product-opener/node_modules/ +COPY --from=builder /opt/product-opener/node_modules/ /opt/product-opener/node_modules/ \ No newline at end of file diff --git a/Makefile b/Makefile index 6048b353ef435..cd29306fe58ed 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,13 @@ NAME = "ProductOpener" ENV_FILE ?= .env MOUNT_POINT ?= /mnt +DOCKER_LOCAL_DATA ?= /srv/off/docker_data HOSTS=127.0.0.1 world.productopener.localhost fr.productopener.localhost static.productopener.localhost ssl-api.productopener.localhost fr-en.productopener.localhost +export DOCKER_BUILDKIT=1 +export COMPOSE_DOCKER_CLI_BUILD=1 +UID ?= $(shell id -u) +export USER_UID:=${UID} + DOCKER_COMPOSE=docker-compose --env-file=${ENV_FILE} .DEFAULT_GOAL := dev @@ -27,7 +33,7 @@ goodbye: #-------# # Local # #-------# -dev: hello up setup_incron import_sample_data refresh_product_tags +dev: hello init_backend up import_sample_data refresh_product_tags @echo "🥫 You should be able to access your local install of Open Food Facts at http://productopener.localhost" @echo "🥫 You have around 100 test products. Please run 'make import_prod_data' if you want a full production dump (~2M products)." @@ -52,6 +58,7 @@ edit_etc_hosts: up: @echo "🥫 Building and starting containers …" ${DOCKER_COMPOSE} up -d --build 2>&1 + @echo "🥫 started service at http://openfoodfacts.localhost" down: @echo "🥫 Bringing down containers …" @@ -66,6 +73,7 @@ reset: hdown up restart: @echo "🥫 Restarting frontend & backend containers …" ${DOCKER_COMPOSE} restart backend frontend + @echo "🥫 started service at http://openfoodfacts.localhost" restart_db: @echo "🥫 Restarting MongoDB database …" @@ -87,12 +95,22 @@ tail: @echo "🥫 Reading logs (Apache2, Nginx) …" tail -f logs/**/* -setup_incron: - @echo "🥫 Setting up incron jobs defined in conf/incron.conf …" - ${DOCKER_COMPOSE} exec -T backend sh -c "\ - echo 'root' >> /etc/incron.allow && \ - incrontab -u root /opt/product-opener/conf/incron.conf && \ - incrond" + +#----------# +# Services # +#----------# +build_lang: + @echo "🥫 Rebuild language" + # Run build_lang.pl + ${DOCKER_COMPOSE} run --rm backend perl -I/opt/product-opener/lib -I/opt/perl/local/lib/perl5 /opt/product-opener/scripts/build_lang.pl + +# use this in dev if you messed up with permissions or user uid/gid +reset_owner: + @echo "🥫 reset owner" + ${DOCKER_COMPOSE} run --rm --no-deps --user root backend chown www-data:www-data -R /opt/product-opener/ /mnt/podata /var/log/apache2 /var/log/httpd || true + ${DOCKER_COMPOSE} run --rm --no-deps --user root frontend chown www-data:www-data -R /opt/product-opener/html/images/icons/dist /opt/product-opener/html/js/dist /opt/product-opener/html/css/dist + +init_backend: build_lang refresh_product_tags: @echo "🥫 Refreshing products tags (update MongoDB products_tags collection) …" @@ -101,7 +119,7 @@ refresh_product_tags: import_sample_data: @echo "🥫 Importing sample data (~100 products) into MongoDB …" - ${DOCKER_COMPOSE} exec --user=www-data backend bash /opt/product-opener/scripts/import_sample_data.sh + ${DOCKER_COMPOSE} run --rm backend bash /opt/product-opener/scripts/import_sample_data.sh import_prod_data: @echo "🥫 Importing production data (~2M products) into MongoDB …" @@ -123,16 +141,23 @@ front_lint: checks: front_lint +tests: + @echo "🥫 Runing tests …" + docker-compose run --rm backend prove -l + #------------# # Production # #------------# create_external_volumes: @echo "🥫 Creating external volumes (production only) …" + # zfs replications docker volume create --driver=local -o type=none -o o=bind -o device=${MOUNT_POINT}/data html_data docker volume create --driver=local -o type=none -o o=bind -o device=${MOUNT_POINT}/users users docker volume create --driver=local -o type=none -o o=bind -o device=${MOUNT_POINT}/products products docker volume create --driver=local -o type=none -o o=bind -o device=${MOUNT_POINT}/product_images product_images docker volume create --driver=local -o type=none -o o=bind -o device=${MOUNT_POINT}/orgs orgs + # local data + docker volume create --driver=local -o type=none -o o=bind -o device=${DOCKER_LOCAL_DATA}/podata podata #---------# # Cleanup # @@ -146,11 +171,11 @@ prune_cache: docker builder prune -f clean_folders: - rm html/images/products - rm -rf node_modules/ - rm -rf html/data/i18n/ - rm -rf html/{css,js}/dist/ - rm -rf tmp/ - rm -rf logs/ + ( rm html/images/products || true ) + ( rm -rf node_modules/ || true ) + ( rm -rf html/data/i18n/ || true ) + ( rm -rf html/{css,js}/dist/ || true ) + ( rm -rf tmp/ || true ) + ( rm logs/* logs/apache2/* logs/nginx/* || true ) clean: goodbye hdown prune prune_cache clean_folders diff --git a/cgi/css.pl b/cgi/css.pl new file mode 100644 index 0000000000000..a2233de922ac0 --- /dev/null +++ b/cgi/css.pl @@ -0,0 +1,50 @@ +#!/usr/bin/perl -w + +# This file is part of Product Opener. +# +# Product Opener +# Copyright (C) 2011-2020 Association Open Food Facts +# Contact: contact@openfoodfacts.org +# 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 . + +use Modern::Perl '2017'; +use utf8; + +use CGI::Carp qw(fatalsToBrowser); + +use ProductOpener::Config qw/:all/; +use ProductOpener::Store qw/:all/; +use ProductOpener::Display qw/:all/; +use ProductOpener::Lang qw/:all/; + +use Apache2::Const -compile => qw(OK); +use CGI qw/:cgi :form escapeHTML/; +use URI::Escape::XS; +use Encode; +use Log::Any qw($log); + +ProductOpener::Display::init(); + +# Redirect the left to right or right to left CSS based on the subdomain +# This is useful for static HTML files (e.g. donation page translated by CrowdIn) + +my $redirect = $static_subdomain . "/css/dist/app-" . lang('text_direction') . ".css?v=" . $file_timestamps{'css/dist/app-' . lang('text_direction') . '.css'}; + + +my $r = Apache2::RequestUtil->request(); +$r->headers_out->set(Location => $redirect); +$r->status(302); +return 302; \ No newline at end of file diff --git a/cgi/nutrients.pl b/cgi/nutrients.pl index 7495738b30bf2..8e3a9aa35b98f 100644 --- a/cgi/nutrients.pl +++ b/cgi/nutrients.pl @@ -3,7 +3,7 @@ # This file is part of Product Opener. # # Product Opener -# Copyright (C) 2011-2019 Association Open Food Facts +# Copyright (C) 2011-2021 Association Open Food Facts # Contact: contact@openfoodfacts.org # Address: 21 rue des Iles, 94100 Saint-Maur des Fossés, France # @@ -30,29 +30,22 @@ use ProductOpener::Display qw/:all/; use ProductOpener::Food qw/:all/; +use Log::Any qw($log); use CGI qw/:cgi :form escapeHTML/; use JSON::PP; ProductOpener::Display::init(); -# Recursively remove parent association to avoid redundant JSON data. -sub _remove_parent { - my $current_ref = shift; - - if (defined $current_ref->{nutrients}) { - foreach my $nutrient (@{$current_ref->{nutrients}}) { - _remove_parent($nutrient); - } - } - - delete $current_ref->{parent}; - - return; -} +# Turn the flat nutriments table array into a nested array of nutrients +# The level of each nutrient is indicated by leading dashes before its id: +# nutrient +# -sub-nutrient +# --sub-sub-nutrient my @table = (); -my $previous_ref; -my $previous_prefix_length = 0; +my $parent_level0; +my $parent_level1; + foreach (@{$nutriments_tables{$nutriment_table}}) { my $nid = $_; # Copy instead of alias @@ -62,33 +55,41 @@ sub _remove_parent { my $default_edit_form = $nid =~ /-$/ ? JSON::PP::false : JSON::PP::true; $nid =~ s/-$//g; - my $onid = $nid =~ s/^(-+)//gr; - my $prefix_length = defined $1 ? length($1) : 0; - my %current = ( id => $onid, important => $important, display_in_edit_form => $default_edit_form ); - my $current_ref = \%current; + my $onid = $nid =~ s/^(\-+)//gr; + + my $current_ref = { id => $onid, important => $important, display_in_edit_form => $default_edit_form }; my $name = get_nutrient_label($onid, $lc); if (defined $name) { $current_ref->{name} = $name; } - if (($prefix_length gt 0) or ($prefix_length gt $previous_prefix_length)) { - @{$previous_ref->{nutrients}} = () unless defined $previous_ref->{nutrients}; - push @{$previous_ref->{nutrients}}, $current_ref unless not defined $current_ref; - $current_ref->{parent} = $previous_ref; - } - else { - push @table, $current_ref unless not defined $current_ref; + my $prefix_length = 0; + if ($nid =~ s/^--//g) { + $prefix_length = 2; + } elsif($nid =~ s/^-//g) { + $prefix_length = 1; } - if (($prefix_length gt $previous_prefix_length) or ($prefix_length eq 0)) { - $previous_ref = $current_ref; - $previous_prefix_length = $prefix_length; + if ($prefix_length == 0) { + # I'm on level 0, I have no parent, and I'm the new level 0 parent + push @table, $current_ref; + $parent_level0 = $current_ref; + $parent_level1 = undef; + } + elsif (($prefix_length == 1) and (defined $parent_level0)) { + # I'm on level 1, my parent is the latest level 0 parent, and I'm the new level 1 parent + defined $parent_level0->{nutrients} or $parent_level0->{nutrients} = []; + push @{$parent_level0->{nutrients}}, $current_ref unless not defined $current_ref; + $parent_level1 = $current_ref; + } + elsif (($prefix_length == 2) and (defined $parent_level1)) { + # I'm on level 2, my parent is the latest level 1 parent + defined $parent_level1->{nutrients} or $parent_level1->{nutrients} = []; + push @{$parent_level1->{nutrients}}, $current_ref; + } + else { + $log->error("invalid nesting of nutrients", { nutriment_table => $nutriment_table, nid => $nid, prefix_length => $prefix_length, current_ref => $current_ref, parent_level0 => $parent_level0, parent_level1 => $parent_level1 }) if $log->is_error(); } -} - -# The parent attribute is only used to build up the structure. Just remove it here to avoid circular dependency in JSON. -foreach my $nutrient (@table) { - _remove_parent($nutrient); } my %result = ( nutrients => \@table ); diff --git a/conf/apache.conf b/conf/apache.conf index 5889cc2959262..097e061f26448 100644 --- a/conf/apache.conf +++ b/conf/apache.conf @@ -21,6 +21,9 @@ PerlPassEnv CROWDIN_PROJECT_KEY PerlPassEnv GEOLITE2_PATH PerlPassEnv POSTGRES_USER PerlPassEnv POSTGRES_PASSWORD +PerlPassEnv LOG_LEVEL_ROOT +PerlPassEnv LOG_LEVEL_MONGODB + @@ -38,7 +41,7 @@ PerlPassEnv POSTGRES_PASSWORD -PerlRequire /opt/product-opener/lib/startup_apache2.pl +PerlPostConfigRequire /opt/product-opener/lib/startup_apache2.pl # log the X-Forwarded-For IP address (the client ip) in access_log LogFormat "%{X-Forwarded-For}i %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" proxy diff --git a/conf/log.conf b/conf/log.conf index ec9d8016e23a7..0572b3a5f3b47 100644 --- a/conf/log.conf +++ b/conf/log.conf @@ -1,5 +1,5 @@ -log4perl.rootLogger=ERROR, LOGFILE -log4perl.logger.mongodb=INFO, MONGODB_LOGFILE +log4perl.rootLogger= sub {return ($ENV{LOG_LEVEL_ROOT} // "ERROR") . ", LOGFILE";} +log4perl.logger.mongodb= sub {return ($ENV{LOG_LEVEL_MONGODB} // "INFO") . ", MONGODB_LOGFILE";} log4perl.PatternLayout.cspec.S = sub { my $context = Log::Log4perl::MDC->get_context; use Data::Dumper (); local $Data::Dumper::Indent = 0; local $Data::Dumper::Terse = 1; local $Data::Dumper::Sortkeys = 1; local $Data::Dumper::Quotekeys = 0; local $Data::Dumper::Deparse= 1; my $str = Data::Dumper::Dumper($context); $str =~ s/[\n\r]/ /g; return $str; } log4perl.appender.LOGFILE=Log::Log4perl::Appender::File log4perl.appender.LOGFILE.filename=/mnt/podata/logs/log4perl.log diff --git a/conf/minion_log.conf b/conf/minion_log.conf index f281a156ec835..86b7a34029c3d 100644 --- a/conf/minion_log.conf +++ b/conf/minion_log.conf @@ -1,4 +1,4 @@ -log4perl.rootLogger=TRACE, LOGFILE +log4perl.rootLogger= sub {return ($ENV{LOG_LEVEL_ROOT} // "ERROR") . ", LOGFILE";} log4perl.PatternLayout.cspec.S = sub { my $context = Log::Log4perl::MDC->get_context; use Data::Dumper (); local $Data::Dumper::Indent = 0; local $Data::Dumper::Terse = 1; local $Data::Dumper::Sortkeys = 1; local $Data::Dumper::Quotekeys = 0; local $Data::Dumper::Deparse = 1; my $str = Data::Dumper::Dumper($context); $str =~ s/[\n\r]/ /g; return $str; } diff --git a/debug/.empty b/debug/.empty new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/docker-compose.yml b/docker-compose.yml index 5f7a8b31b35fe..abfa7410b8382 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,4 +1,54 @@ version: "3.7" + +x-backend-conf: &backend-conf + image: ghcr.io/openfoodfacts/openfoodfacts-server/backend:${TAG} + environment: + - PRODUCERS_PLATFORM + - PRODUCT_OPENER_DOMAIN + - PRODUCT_OPENER_PORT + - PRODUCT_OPENER_FLAVOR + - PRODUCT_OPENER_FLAVOR_SHORT + - MONGODB_HOST + - POSTGRES_USER + - POSTGRES_PASSWORD + - ROBOTOFF_URL + - GOOGLE_CLOUD_VISION_API_KEY + - CROWDIN_PROJECT_IDENTIFIER + - CROWDIN_PROJECT_KEY + - GEOLITE2_PATH + - LOG_LEVEL_ROOT + - LOG_LEVEL_MONGODB + depends_on: + - memcached + - postgres + volumes: + - html_data:/opt/product-opener/html/data # Static data (e.g dumps) + + # Binds from frontend container (read-only) + - icons_dist:/opt/product-opener/html/images/icons/dist:ro + - js_dist:/opt/product-opener/html/js/dist:ro + - css_dist:/opt/product-opener/html/css/dist:ro + - node_modules:/opt/product-opener/node_modules:ro + + # Users, products, product images and orgs files + - users:/mnt/podata/users # .sto + - products:/mnt/podata/products # .sto + - product_images:/opt/product-opener/html/images/products # .jpg + - orgs:/mnt/podata/orgs # .sto + # all the rest + - podata:/mnt/podata + + # Logs + - ./logs/apache2:/var/log/apache2 + - ./logs/apache2:/mnt/podata/logs + - ./logs/apache2:/var/log/httpd + + # Apache conf + - ./conf/apache.conf:/etc/apache2/sites-enabled/product-opener.conf + + networks: + - webnet + services: memcached: image: memcached:1.6-alpine @@ -14,53 +64,13 @@ services: - POSTGRES_DB=minion volumes: - pgdata:/var/lib/postgresql/data - backend: - image: ghcr.io/openfoodfacts/openfoodfacts-server/backend:${TAG} - environment: - - PRODUCERS_PLATFORM - - PRODUCT_OPENER_DOMAIN - - PRODUCT_OPENER_PORT - - PRODUCT_OPENER_FLAVOR - - PRODUCT_OPENER_FLAVOR_SHORT - - MONGODB_HOST - - POSTGRES_USER - - POSTGRES_PASSWORD - - ROBOTOFF_URL - - GOOGLE_CLOUD_VISION_API_KEY - - CROWDIN_PROJECT_IDENTIFIER - - CROWDIN_PROJECT_KEY - - GEOLITE2_PATH - depends_on: - - memcached - - postgres - volumes: - - html_data:/opt/product-opener/html/data # Static data (e.g dumps) - - # Binds from frontend container (read-only) - - icons_dist:/opt/product-opener/html/images/icons/dist:ro - - js_dist:/opt/product-opener/html/js/dist:ro - - css_dist:/opt/product-opener/html/css/dist:ro - - node_modules:/opt/product-opener/node_modules:ro - - # Users, products, product images and orgs files - - users:/mnt/podata/users # .sto - - products:/mnt/podata/products # .sto - - product_images:/mnt/podata/product_images # .jpg - - orgs:/mnt/podata/orgs # .sto - - # Logs - - ./logs/apache2:/var/log/apache2 - - ./logs/apache2:/mnt/podata/logs - - ./logs/apache2:/var/log/httpd - - # Apache conf - - ./conf/apache.conf:/etc/apache2/sites-enabled/product-opener.conf - - # Tmpfs - - type: tmpfs - target: /mnt/podata/mnt - networks: - - webnet + backend: *backend-conf + incron: + <<: *backend-conf + # This service run the incron jobs + # Only root can run incron + user: root + command: ["incrond", "-n"] frontend: image: ghcr.io/openfoodfacts/openfoodfacts-server/frontend:${TAG} depends_on: @@ -86,7 +96,7 @@ services: # Node modules - node_modules:/opt/product-opener/node_modules ports: - - ${PRODUCT_OPENER_PORT}:80 + - ${PRODUCT_OPENER_EXPOSE}${PRODUCT_OPENER_PORT}:80 networks: - webnet volumes: diff --git a/docker/admin-uis.yml b/docker/admin-uis.yml index b56d828214d77..9243b49cac705 100644 --- a/docker/admin-uis.yml +++ b/docker/admin-uis.yml @@ -5,7 +5,7 @@ services: depends_on: - mongodb ports: - - 8080:8080 + - ${ADMIN_EXPOSE}8080:8080 networks: - webnet environment: @@ -16,7 +16,7 @@ services: depends_on: - memcached ports: - - 8081:8080 + - ${ADMIN_EXPOSE}8081:8080 networks: - webnet adminer: @@ -24,7 +24,7 @@ services: depends_on: - postgres ports: - - 8082:8080 + - ${ADMIN_EXPOSE}8082:8080 networks: - webnet environment: diff --git a/docker/dev.yml b/docker/dev.yml index 265c50978bbf2..cac50a38bc17e 100644 --- a/docker/dev.yml +++ b/docker/dev.yml @@ -1,13 +1,27 @@ version: "3.7" + +x-backend-conf: &backend-conf + image: openfoodfacts-server/backend:dev + build: + context: . + dockerfile: Dockerfile + # align user id + args: + USER_UID: ${USER_UID:-1000} + USER_GID: ${USER_GID:-1000} + volumes: + # mount local folder for reload on dev changes. + # Note that this means /opt/product-opener/html/images/product wont be connected to product_images volume + # Sadly, there is no sane way to do this, while retaining compatibility with prod + # (which requires images/product to be empty in git) + - ./:/opt/product-opener + # we use this for debugging, from times to times + - ./debug:/mnt/podata/debug/ + + services: - backend: - image: openfoodfacts-server/backend:dev - build: - context: . - dockerfile: Dockerfile - target: runnable - volumes: - - ./:/opt/product-opener # mount local folder for reload on dev changes + backend: *backend-conf + incron: *backend-conf # in dev we want to use watch assets and recompile on the fly # also we want to build at start time in case some files changed, as we want to avoid recreating volumes dynamicfront: @@ -15,6 +29,9 @@ services: context: . target: builder dockerfile: Dockerfile.frontend + args: + USER_UID: ${USER_UID:-1000} + USER_GID: ${USER_GID:-1000} command: ["npm", "run", "build:dynamic"] volumes: # Static dist/ assets (JS, CSS, Icons, Image attributes) @@ -34,7 +51,10 @@ services: context: . dockerfile: Dockerfile.frontend args: - WWW_DATA_HOST_USER: ${WWW_DATA_HOST_USER} + USER_UID: ${USER_UID:-1000} + USER_GID: ${USER_GID:-1000} + volumes: + - ./html:/opt/product-opener/html/ mongodb: image: mongo:4.4 command: mongod --wiredTigerCacheSizeGB ${MONGODB_CACHE_SIZE} @@ -43,9 +63,12 @@ services: - MONGO_INITDB_ROOT_PASSWORD networks: - webnet + ports: + - 27017:27017 volumes: product_images: html_data: users: products: orgs: + podata: diff --git a/docker/docker-entrypoint.sh b/docker/docker-entrypoint.sh index bb1f0780d2474..b3e15574fea03 100755 --- a/docker/docker-entrypoint.sh +++ b/docker/docker-entrypoint.sh @@ -1,16 +1,5 @@ #!/bin/sh -# Create writable dirs and change ownership to www-data -for path in " " users products product_images orgs new_images logs; do - mkdir -p /mnt/podata/${path} - chown www-data:www-data /mnt/podata/${path} -done - -# Create symlinks of data files to /mnt/podata -for path in ecoscore emb_codes forest-footprint ingredients lang packager-codes po taxonomies templates; do - ln -sfT /opt/product-opener/${path} /mnt/podata/${path} -done - # Link site-specific translations ln -sfT /opt/product-opener/po/${PRODUCT_OPENER_FLAVOR} /mnt/podata/po/site-specific @@ -21,11 +10,9 @@ ln -sfT /opt/product-opener/lib/ProductOpener/SiteLang_${PRODUCT_OPENER_FLAVOR_S ln -sfT /opt/product-opener/lib/ProductOpener/Config_${PRODUCT_OPENER_FLAVOR_SHORT}.pm /opt/product-opener/lib/ProductOpener/Config.pm ln -sfT /opt/product-opener/lib/ProductOpener/Config2_docker.pm /opt/product-opener/lib/ProductOpener/Config2.pm -# Link product images -ln -sfT /mnt/podata/product_images /opt/product-opener/html/images/products - -# Run build_lang.pl -perl -I/opt/product-opener/lib -I/opt/perl/local/lib/perl5 /opt/product-opener/scripts/build_lang.pl +# this is not very elegant, but incron scripts won't have env variables so put them in a file +rm -f /tmp/env-export.sh && export > /tmp/env-export.sh +chown www-data:www-data /tmp/env-export.sh && chmod 0400 /tmp/env-export.sh # https://github.com/docker-library/httpd/blob/75e85910d1d9954ea0709960c61517376fc9b254/2.4/alpine/httpd-foreground set -e diff --git a/docker/prod.yml b/docker/prod.yml index a09e860596afe..4cbb324928c46 100644 --- a/docker/prod.yml +++ b/docker/prod.yml @@ -6,6 +6,8 @@ services: restart: always backend: restart: always + incron: + restart: always frontend: restart: always volumes: @@ -19,6 +21,8 @@ volumes: external: true product_images: external: true + podata: + external: true networks: webnet: diff --git a/html/donate/ak.html b/html/donate/ak.html index 5d767d77de935..9b5a1588362f8 100644 --- a/html/donate/ak.html +++ b/html/donate/ak.html @@ -113,7 +113,7 @@ />
-

Help us to fund the Open Food Facts 2021 budget!

+

Help us to fund the Open Food Facts 2022 budget!

Open Food Facts is 100% free and independent of the food industry. We need your help to continue and to grow the project.

@@ -139,7 +139,7 @@

-

Help us to fund the Open Food Facts 2021 budget!

+

Help us to fund the Open Food Facts 2022 budget!

Open Food Facts is 100% free and independent of the food industry. We need your help to continue and to grow the project.

@@ -193,7 +193,7 @@

Help us to fund the Open Food Facts 2021 budget!

-

The donation form is being loaded.
You can also access it by clicking here.

+

The donation form is being loaded.
You can also access it by clicking here.

-

crwdns147866:0crwdne147866:0

+

crwdns172170:0crwdne172170:0

- crwdns147868:0crwdne147868:0 + crwdns172172:0crwdne172172:0

- crwdns147870:0crwdne147870:0 + crwdns172174:0crwdne172174:0

diff --git a/html/donate/lt.html b/html/donate/lt.html index 576cf8eb7e6c7..58c71a746513c 100644 --- a/html/donate/lt.html +++ b/html/donate/lt.html @@ -21,7 +21,7 @@ />
-

Help us to fund the Open Food Facts 2021 budget!

-

Open Food Facts is 100% free and independent of the food industry. We need your help to continue and to grow the project.

+

Help us to fund the Open Food Facts 2022 budget!

+

Open Food Facts yra 100% nemokama ir nepriklausoma nuo maisto pramonės. Mums reikia jūsų pagalbos, kad tęstume ir plėtotume projektą.

@@ -139,8 +139,8 @@

-

Help us to fund the Open Food Facts 2021 budget!

-

Open Food Facts is 100% free and independent of the food industry. We need your help to continue and to grow the project.

+

Help us to fund the Open Food Facts 2022 budget!

+

Open Food Facts yra 100% nemokama ir nepriklausoma nuo maisto pramonės. Mums reikia jūsų pagalbos, kad tęstume ir plėtotume projektą.

Help us to fund the Open Food Facts 2021 budget!
-

The donation form is being loaded.
You can also access it by clicking here.

+

The donation form is being loaded.
You can also access it by clicking here.

-

Where your donation goes

+

Kam skiriama jūsų auka

- Technology: Development, maintenance, servers to keep the - database growing and to add cool new features to the Open Food - Facts mobile app and web site. + Technologijos: kūrimas, priežiūra, serveriai, siekiant išlaikyti + duomenų bazių augimą ir pridėti naujų šaunių funkcijų į Open Food + Facts mobiliąją programėlę ir svetainę.

- People and projects: To give our best support to the Open - Food Facts community, to work with researchers, and to make our - food products data have the biggest impact on our health, our - planet, and society. + Žmonės ir projektai: Siekiame kuo geriau paremti „Open + Food Facts“ bendruomenę, bendradarbiauti su tyrėjais ir padaryti, kad mūsų + maisto produktų duomenys turėtų didžiausią įtaką mūsų sveikatai, + mūsų planetai ir visuomenei.

diff --git a/html/donate/ml.html b/html/donate/ml.html index e5975ee3473dd..9f44e53d87bac 100644 --- a/html/donate/ml.html +++ b/html/donate/ml.html @@ -113,7 +113,7 @@ />
-

Help us to fund the Open Food Facts 2021 budget!

+

Help us to fund the Open Food Facts 2022 budget!

Open Food Facts is 100% free and independent of the food industry. We need your help to continue and to grow the project.

@@ -139,7 +139,7 @@

-

Help us to fund the Open Food Facts 2021 budget!

+

Help us to fund the Open Food Facts 2022 budget!

Open Food Facts is 100% free and independent of the food industry. We need your help to continue and to grow the project.

@@ -193,7 +193,7 @@

Help us to fund the Open Food Facts 2021 budget!

-

The donation form is being loaded.
You can also access it by clicking here.

+

The donation form is being loaded.
You can also access it by clicking here.