HEX
Server: Apache
System: Linux info 3.0 #1337 SMP Tue Jan 01 00:00:00 CEST 2000 all GNU/Linux
User: u115237990 (7145895)
PHP: 8.3.32
Disabled: NONE
Upload Files
File: /homepages/35/d993672390/htdocs/dermiteseborrheique/api/scan-barcode.php
<?php
/**
 * api/scan-barcode.php — Endpoint AJAX pour lookup produit
 *
 * Appelé par le scanner caméra et la recherche textuelle
 *
 * GET  ?barcode=3600550361020         → lookup par code-barres
 * GET  ?search=ducray&limit=10        → recherche par nom
 * POST {barcode, name, brand, ...}    → ajout manuel d'un produit
 */

define('DS_APP', true);
define('DS_API', true); // Pas d'en-têtes HTML
require_once dirname(__DIR__) . '/includes/init.php';
require_once DS_INCLUDES . 'openbeautyfacts.php';

// ── Auth requise ──────────────────────────────────────────────────────────────
auth_require();

// ── CORS pour les requêtes AJAX internes ──────────────────────────────────────
header('Content-Type: application/json; charset=utf-8');
header('X-Content-Type-Options: nosniff');

// ── Rate limiting simple (anti-spam) ─────────────────────────────────────────
$ip         = get_client_ip();
$rateKey    = 'scan_' . md5($ip);
$cacheFile  = sys_get_temp_dir() . '/' . $rateKey . '.tmp';

if (file_exists($cacheFile)) {
    $data = json_decode(file_get_contents($cacheFile), true);
    if ($data && $data['count'] > 60 && (time() - $data['ts']) < 60) {
        json_response(['success' => false, 'error' => 'Trop de requêtes. Attendez 1 minute.'], 429);
    }
    if ((time() - $data['ts']) > 60) {
        $data = ['count' => 1, 'ts' => time()];
    } else {
        $data['count']++;
    }
} else {
    $data = ['count' => 1, 'ts' => time()];
}
file_put_contents($cacheFile, json_encode($data));

// ── Routage selon méthode HTTP ────────────────────────────────────────────────

$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';

match($method) {
    'GET'  => handleGet(),
    'POST' => handlePost(),
    default => json_response(['success' => false, 'error' => 'Méthode non autorisée.'], 405),
};

// ── GET — lookup barcode ou recherche textuelle ───────────────────────────────

function handleGet(): never
{
    // Lookup par code-barres
    if (!empty($_GET['barcode'])) {
        $barcode = trim($_GET['barcode']);

        // 1. Chercher dans Open Beauty Facts (cosmétiques)
        $product = OpenBeautyFacts::lookupBarcode($barcode);
        $source  = 'openbeautyfacts';

        // 2. Si pas trouvé → chercher dans Open Food Facts (aliments/boissons)
        if (!$product) {
            $product = lookupOpenFoodFacts($barcode);
            $source  = 'openfoodfacts';
        }

        if (!$product) {
            json_response([
                'success' => false,
                'error'   => "Produit introuvable. Essayez de l'ajouter manuellement.",
                'barcode' => $barcode,
            ], 404);
        }

        // Enregistrer dans user_items et incrémenter usage
        if (!empty($product['id'])) {
            db_execute("UPDATE items SET usage_count = usage_count + 1 WHERE id = ?", [$product['id']]);
            $u = auth_user();
            if ($u) {
                db_execute(
                    "INSERT INTO user_items (user_id, item_id) VALUES (?, ?)
                      ON DUPLICATE KEY UPDATE usage_count = usage_count + 1, scanned_at = NOW()",
                    [$u['id'], $product['id']]
                );
            }

            // Si produit sans ingrédients -> enrichir depuis OFF/OBF
            $ingCount = db_count(
                "SELECT COUNT(*) FROM item_ingredients WHERE item_id = ?",
                [$product['id']]
            );
            if ($ingCount === 0 && !empty($product['barcode'])) {
                $enriched = lookupOpenFoodFacts($product['barcode'])
                         ?? lookupFromOBF($product['barcode']);
                if ($enriched && !empty($enriched['ingredients'])) {
                    // Mettre à jour les ingrédients texte
                    db_execute(
                        "UPDATE items SET ingredients = ? WHERE id = ?",
                        [$enriched['ingredients'], $product['id']]
                    );
                    // Sauvegarder les ingrédients parsés si fournis via JS
                }
            }
        }

        json_response([
            'success' => true,
            'source'  => $source,
            'product' => formatProductResponse($product),
            'needs_ingredients' => ($ingCount ?? 0) === 0,
        ]);
    }

    // Recherche textuelle (autocomplete)
    if (!empty($_GET['search'])) {
        $query  = sanitize_string(trim($_GET['search']), 100);
        $limit  = min((int)($_GET['limit'] ?? 10), 20);

        if (mb_strlen($query) < 2) {
            json_response(['success' => false, 'error' => 'Requête trop courte (min. 2 caractères).'], 400);
        }

        $results = OpenBeautyFacts::searchByName($query, $limit);

        json_response([
            'success' => true,
            'query'   => $query,
            'count'   => count($results),
            'results' => array_map('formatProductResponse', $results),
        ]);
    }

    // Récupérer un item existant par ID
    if (!empty($_GET['item_id'])) {
        $itemId = (int)$_GET['item_id'];
        $item   = db_fetch("SELECT * FROM items WHERE id = ?", [$itemId]);

        if (!$item) {
            json_response(['success' => false, 'error' => 'Item introuvable.'], 404);
        }

        json_response([
            'success' => true,
            'product' => formatProductResponse($item),
        ]);
    }

    json_response(['success' => false, 'error' => 'Paramètre manquant (barcode, search ou item_id).'], 400);
}

// ── POST — ajout manuel d'un produit ─────────────────────────────────────────

function handlePost(): never
{
    csrf_verify();

    $user = auth_user();

    // Lire le body JSON ou les données POST classiques
    $body = [];
    $contentType = $_SERVER['CONTENT_TYPE'] ?? '';
    if (str_contains($contentType, 'application/json')) {
        $body = json_decode(file_get_contents('php://input'), true) ?? [];
    } else {
        $body = $_POST;
    }

    // Validation
    $name = sanitize_string(trim($body['name'] ?? ''), 200);
    if (!$name) {
        json_response(['success' => false, 'error' => 'Le nom du produit est requis.'], 400);
    }

    $category = $body['category'] ?? 'cosmetic';
    $allowed  = ['food','drink','alcohol','tobacco','cosmetic','haircare','medication','supplement','other'];
    if (!in_array($category, $allowed)) $category = 'other';

    $barcode     = OpenBeautyFacts::sanitizeBarcode($body['barcode'] ?? '');
    $brand       = sanitize_string($body['brand'] ?? '', 100);
    $ingredients = sanitize_string($body['ingredients'] ?? '', 65535);

    // Si ingrédients fournis → analyser les suspects
    $warningIngredients = null;
    if ($ingredients) {
        $warnings = OpenBeautyFacts::detectSuspectIngredients($ingredients);
        if (!empty($warnings)) {
            $warningIngredients = json_encode($warnings, JSON_UNESCAPED_UNICODE);
        }
    }

    // Vérifier doublons (même nom + marque pour cet utilisateur)
    $existing = db_fetch(
        "SELECT id FROM items
         WHERE (user_id = ? OR is_global = 1)
           AND name = ?
           AND (brand = ? OR brand IS NULL)
         LIMIT 1",
        [$user['id'], $name, $brand]
    );

    if ($existing) {
        $item = db_fetch("SELECT * FROM items WHERE id = ?", [$existing['id']]);
        json_response([
            'success'    => true,
            'duplicate'  => true,
            'message'    => 'Ce produit existe déjà dans notre base.',
            'product'    => formatProductResponse($item),
        ]);
    }

    // Créer le nouvel item (privé à l'utilisateur)
    $itemId = db_insert(
        "INSERT INTO items
            (user_id, category, name, brand, barcode, ingredients, warning_ingredients, is_global)
         VALUES (?, ?, ?, ?, ?, ?, ?, 0)",
        [
            $user['id'],
            $category,
            $name,
            $brand ?: null,
            $barcode ?: null,
            $ingredients ?: null,
            $warningIngredients,
        ]
    );

    $item = db_fetch("SELECT * FROM items WHERE id = ?", [$itemId]);

    // Lier à l'utilisateur via user_items
    db_execute(
        "INSERT INTO user_items (user_id, item_id) VALUES (?, ?)
         ON DUPLICATE KEY UPDATE usage_count = usage_count + 1",
        [$user['id'], $itemId]
    );

    // Sauvegarder les ingrédients individuels si fournis
    if (!empty($body['parsed_ingredients']) && is_array($body['parsed_ingredients'])) {
        db_execute("DELETE FROM item_ingredients WHERE item_id = ?", [$itemId]);
        foreach (array_slice($body['parsed_ingredients'], 0, 200) as $ing) {
            $inci = sanitize_string($ing['inci_name'] ?? '', 300);
            if (!$inci) continue;
            db_execute(
                "INSERT IGNORE INTO item_ingredients (item_id, inci_name, common_name, position, is_suspect)
                 VALUES (?, ?, ?, ?, ?)",
                [
                    $itemId,
                    $inci,
                    sanitize_string($ing['common_name'] ?? '', 300) ?: null,
                    (int)($ing['position'] ?? 0),
                    !empty($ing['is_suspect']) ? 1 : 0,
                ]
            );
        }
    }

    json_response([
        'success' => true,
        'created' => true,
        'message' => 'Produit ajouté avec succès.',
        'product' => formatProductResponse($item),
    ], 201);
}

// ── Lookup Open Beauty Facts ──────────────────────────────────────────────────

function lookupFromOBF(string $barcode): ?array
{
    $barcode = preg_replace('/[^0-9]/', '', $barcode);
    if (!$barcode) return null;

    if (!function_exists('curl_init')) return null;

    $url = 'https://world.openbeautyfacts.org/api/v2/product/' . $barcode . '.json?fields=code,product_name,brands,ingredients_text,image_front_url';
    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 8,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_USERAGENT      => 'DermiteSeborrheique/1.0',
    ]);
    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200 || !$response) return null;
    $data = json_decode($response, true);
    if (empty($data['product']) || ($data['status'] ?? 0) !== 1) return null;

    $p = $data['product'];
    return [
        'ingredients' => $p['ingredients_text'] ?? '',
        'image_url'   => $p['image_front_url'] ?? null,
    ];
}

// ── Lookup Open Food Facts ────────────────────────────────────────────────────

function lookupOpenFoodFacts(string $barcode): ?array
{
    $barcode = preg_replace('/[^0-9]/', '', $barcode);
    if (!$barcode) return null;

    // Vérifier cache local d'abord
    $cached = db_fetch(
        "SELECT * FROM items WHERE barcode = ? AND is_global = 1 LIMIT 1",
        [$barcode]
    );
    if ($cached) return $cached;

    // Appel API Open Food Facts
    if (!function_exists('curl_init')) return null;

    $url = 'https://world.openfoodfacts.org/api/v2/product/' . $barcode . '.json?fields=code,product_name,brands,ingredients_text,image_front_url,categories_tags,nutriments,nova_group';

    $ch = curl_init($url);
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 8,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_USERAGENT      => 'DermiteSeborrheique/1.0 (contact@dermiteseborrheique.fr)',
        CURLOPT_HTTPHEADER     => ['Accept: application/json'],
    ]);

    $response = curl_exec($ch);
    $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($httpCode !== 200 || !$response) return null;

    $data = json_decode($response, true);
    if (empty($data['product']) || ($data['status'] ?? 0) !== 1) return null;

    $p = $data['product'];
    $name = $p['product_name'] ?? '';
    if (empty($name)) return null;

    // Déterminer la catégorie
    $categories = $p['categories_tags'] ?? [];
    $category = 'food';
    foreach ($categories as $cat) {
        if (str_contains($cat, 'beverages') || str_contains($cat, 'drinks')) {
            $category = 'drink'; break;
        }
        if (str_contains($cat, 'alcoholic') || str_contains($cat, 'wine') || str_contains($cat, 'beer')) {
            $category = 'alcohol'; break;
        }
    }

    $ingredients = $p['ingredients_text'] ?? '';

    // Détecter ingrédients suspects (réutilise la logique OBF)
    $warnings = null;
    if ($ingredients) {
        $w = OpenBeautyFacts::detectSuspectIngredients($ingredients);
        if (!empty($w)) $warnings = json_encode($w, JSON_UNESCAPED_UNICODE);
    }

    // Sauvegarder comme produit global
    $itemId = db_insert(
        "INSERT IGNORE INTO items
            (user_id, category, name, brand, barcode, ingredients, warning_ingredients, image_url, is_global, source)
         VALUES (NULL, ?, ?, ?, ?, ?, ?, ?, 1, 'off')",
        [
            $category,
            $name,
            $p['brands'] ?? null,
            $barcode,
            $ingredients ?: null,
            $warnings,
            $p['image_front_url'] ?? null,
        ]
    );

    return db_fetch("SELECT * FROM items WHERE id = ?", [$itemId]) ?: [
        'id'                  => 0,
        'category'            => $category,
        'name'                => $name,
        'brand'               => $p['brands'] ?? '',
        'barcode'             => $barcode,
        'ingredients'         => $ingredients,
        'warning_ingredients' => $warnings,
        'image_url'           => $p['image_front_url'] ?? null,
    ];
}

// ── Formatage de la réponse produit ──────────────────────────────────────────

function formatProductResponse(array $item): array
{
    $warnings = [];
    if (!empty($item['warning_ingredients'])) {
        $raw = is_string($item['warning_ingredients'])
            ? json_decode($item['warning_ingredients'], true)
            : $item['warning_ingredients'];
        $warnings = $raw ?? [];
    }

    return [
        'id'            => (int)($item['id'] ?? 0),
        'barcode'       => $item['barcode'] ?? null,
        'name'          => $item['name'] ?? '',
        'brand'         => $item['brand'] ?? '',
        'category'      => $item['category'] ?? 'cosmetic',
        'ingredients'   => $item['ingredients'] ?? '',
        'image_url'     => $item['image_url'] ?? null,
        'has_warnings'  => !empty($warnings),
        'warning_count' => count($warnings),
        'warnings'      => $warnings,
        // Résumé rapide pour l'UI
        'warning_summary' => !empty($warnings)
            ? implode(', ', array_column(array_slice($warnings, 0, 3), 'label'))
            : null,
    ];
}