<?php
/*
 * StrategyRating - archivio delle misurazioni sul server.
 * Salva i dati in file dentro la cartella "sr-data" (creata da sola).
 * NON serve nessun database.
 *
 * I dati delle misurazioni arrivano gia' CIFRATI dal browser, quindi sul server
 * non sono leggibili neppure aprendo i file.
 */

/* ---- CONFIGURAZIONE ---------------------------------------------------- */
// sha256 della password di accesso alla misurazione (la stessa che si usa sul sito).
$SR_PASSWORD_SHA256 = '0602a7baead1ff4f4396c29a88606d001749530d291405702763755b29146739';
$SR_SESSION_HOURS   = 12;                 // dopo quante ore va rifatto l'accesso
$SR_MAX_BODY_BYTES  = 12 * 1024 * 1024;   // dimensione massima di una richiesta
$SR_MAX_FAILS       = 8;                  // tentativi sbagliati ...
$SR_FAIL_WINDOW_SEC = 600;                // ... ogni 10 minuti, poi blocco temporaneo
/* ------------------------------------------------------------------------ */

header('Content-Type: application/json; charset=utf-8');
header('Cache-Control: no-store');
header('X-Content-Type-Options: nosniff');

$base = __DIR__ . '/sr-data';

function out($code, $data) {
    http_response_code($code);
    echo json_encode($data);
    exit;
}

function prepare_base($base) {
    if (!is_dir($base)) { @mkdir($base, 0755, true); }
    if (!is_dir($base) || !is_writable($base)) { return false; }
    $ht = $base . '/.htaccess';
    if (!file_exists($ht)) {
        @file_put_contents($ht, "Require all denied\n<IfModule !mod_authz_core.c>\nOrder allow,deny\nDeny from all\n</IfModule>\n");
    }
    $ix = $base . '/index.html';
    if (!file_exists($ix)) { @file_put_contents($ix, ''); }
    return true;
}

function read_json_file($f, $default) {
    if (!file_exists($f)) { return $default; }
    $s = @file_get_contents($f);
    if ($s === false) { return $default; }
    $j = json_decode($s, true);
    return is_array($j) ? $j : $default;
}

function write_json_file($f, $data) {
    $tmp = $f . '.tmp' . getmypid();
    if (@file_put_contents($tmp, json_encode($data), LOCK_EX) === false) { return false; }
    return @rename($tmp, $f);
}

function valid_id($id) {
    return is_string($id) && preg_match('/^[A-Za-z0-9_-]{1,80}$/', $id) === 1;
}

function as_map($arr) {
    return (is_array($arr) && count($arr) > 0) ? $arr : new stdClass();
}

/* ---- richiesta ---------------------------------------------------------- */
$action = isset($_GET['action']) ? $_GET['action'] : '';

if ($action === 'ping') {
    $ok = prepare_base($base);
    out(200, array('ok' => true, 'php' => PHP_VERSION, 'writable' => $ok));
}

if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
    out(405, array('error' => 'Metodo non consentito'));
}
if (!prepare_base($base)) {
    out(500, array('error' => 'Cartella sr-data non scrivibile'));
}

$raw = file_get_contents('php://input');
if ($raw === false || strlen($raw) > $SR_MAX_BODY_BYTES) {
    out(413, array('error' => 'Richiesta troppo grande'));
}
$body = json_decode($raw, true);
if (!is_array($body)) {
    out(400, array('error' => 'Richiesta non valida'));
}

$lockFile = fopen($base . '/lock', 'c');
if (!$lockFile) { out(500, array('error' => 'Blocco non disponibile')); }
flock($lockFile, LOCK_EX);

$now = time();
$sessionsFile = $base . '/sessions.json';
$attemptsFile = $base . '/attempts.json';

/* ---- accesso ------------------------------------------------------------ */
if ($action === 'login') {
    $ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'x';
    $attempts = read_json_file($attemptsFile, array());
    $recent = array();
    if (isset($attempts[$ip]) && is_array($attempts[$ip])) {
        foreach ($attempts[$ip] as $t) {
            if ($t > $now - $SR_FAIL_WINDOW_SEC) { $recent[] = $t; }
        }
    }
    if (count($recent) >= $SR_MAX_FAILS) {
        write_json_file($attemptsFile, $attempts);
        out(429, array('error' => 'Troppi tentativi. Riprova tra qualche minuto.'));
    }
    $pw = isset($body['password']) ? (string)$body['password'] : '';
    if (!hash_equals($SR_PASSWORD_SHA256, hash('sha256', $pw))) {
        $recent[] = $now;
        $attempts[$ip] = $recent;
        write_json_file($attemptsFile, $attempts);
        usleep(400000);
        out(401, array('error' => 'Password non corretta'));
    }
    unset($attempts[$ip]);
    write_json_file($attemptsFile, $attempts);
    $token = bin2hex(random_bytes(32));
    $sessions = read_json_file($sessionsFile, array());
    foreach ($sessions as $h => $exp) {
        if ($exp < $now) { unset($sessions[$h]); }
    }
    $sessions[hash('sha256', $token)] = $now + $SR_SESSION_HOURS * 3600;
    write_json_file($sessionsFile, $sessions);
    out(200, array('token' => $token));
}

/* ---- da qui in poi serve il token -------------------------------------- */
$token = isset($body['token']) ? (string)$body['token'] : '';
$sessions = read_json_file($sessionsFile, array());
$th = hash('sha256', $token);
if ($token === '' || !isset($sessions[$th]) || $sessions[$th] < $now) {
    out(401, array('error' => 'Sessione scaduta'));
}

$ns = isset($body['ns']) ? (string)$body['ns'] : '';
if ($ns !== 'it' && $ns !== 'en') {
    out(400, array('error' => 'Versione non valida'));
}
$nsDir = $base . '/' . $ns;
$dirRec = $nsDir . '/records';
$dirDel = $nsDir . '/deleted';
foreach (array($nsDir, $dirRec, $dirDel) as $d) {
    if (!is_dir($d)) { @mkdir($d, 0755, true); }
}
$statusFile = $nsDir . '/status.json';
$purgedFile = $nsDir . '/purged.json';

function load_dir($dir) {
    $res = array();
    $files = @scandir($dir);
    if (!$files) { return $res; }
    foreach ($files as $f) {
        if (substr($f, -5) !== '.json') { continue; }
        $id = substr($f, 0, -5);
        if (!valid_id($id)) { continue; }
        $j = read_json_file($dir . '/' . $f, null);
        if (is_array($j)) { $res[$id] = $j; }
    }
    return $res;
}

function full_state($dirRec, $dirDel, $statusFile, $purgedFile) {
    return array(
        'records' => as_map(load_dir($dirRec)),
        'deleted' => as_map(load_dir($dirDel)),
        'status'  => as_map(read_json_file($statusFile, array())),
        'purged'  => array_keys(read_json_file($purgedFile, array()))
    );
}

if ($action === 'load') {
    out(200, full_state($dirRec, $dirDel, $statusFile, $purgedFile));
}

if ($action === 'sync') {
    $ops = isset($body['ops']) && is_array($body['ops']) ? $body['ops'] : array();

    // misurazioni
    if (isset($ops['records']) && is_array($ops['records'])) {
        $up = isset($ops['records']['upsert']) && is_array($ops['records']['upsert']) ? $ops['records']['upsert'] : array();
        foreach ($up as $id => $item) {
            $id = (string)$id;
            if (!valid_id($id) || !is_array($item) || !isset($item['box'])) { continue; }
            $f = $dirRec . '/' . $id . '.json';
            $old = read_json_file($f, null);
            $newAt = isset($item['updatedAt']) ? (string)$item['updatedAt'] : '';
            if (is_array($old) && isset($old['updatedAt']) && (string)$old['updatedAt'] > $newAt) { continue; }
            write_json_file($f, array('box' => $item['box'], 'updatedAt' => $newAt));
        }
        $rm = isset($ops['records']['remove']) && is_array($ops['records']['remove']) ? $ops['records']['remove'] : array();
        foreach ($rm as $id) {
            if (valid_id($id)) { @unlink($dirRec . '/' . $id . '.json'); }
        }
    }

    // cancellate
    if (isset($ops['deleted']) && is_array($ops['deleted'])) {
        $up = isset($ops['deleted']['upsert']) && is_array($ops['deleted']['upsert']) ? $ops['deleted']['upsert'] : array();
        foreach ($up as $id => $item) {
            $id = (string)$id;
            if (!valid_id($id) || !is_array($item) || !isset($item['box'])) { continue; }
            write_json_file($dirDel . '/' . $id . '.json', array(
                'box' => $item['box'],
                'deletedAt' => isset($item['deletedAt']) ? (string)$item['deletedAt'] : ''
            ));
        }
        $rm = isset($ops['deleted']['remove']) && is_array($ops['deleted']['remove']) ? $ops['deleted']['remove'] : array();
        foreach ($rm as $id) {
            if (valid_id($id)) { @unlink($dirDel . '/' . $id . '.json'); }
        }
    }

    // stati
    if (isset($ops['status']) && is_array($ops['status'])) {
        $st = read_json_file($statusFile, array());
        $up = isset($ops['status']['upsert']) && is_array($ops['status']['upsert']) ? $ops['status']['upsert'] : array();
        foreach ($up as $id => $val) {
            $id = (string)$id;
            if (valid_id($id) && is_string($val) && strlen($val) <= 40) { $st[$id] = $val; }
        }
        $rm = isset($ops['status']['remove']) && is_array($ops['status']['remove']) ? $ops['status']['remove'] : array();
        foreach ($rm as $id) {
            if (isset($st[$id])) { unset($st[$id]); }
        }
        write_json_file($statusFile, $st);
    }

    // eliminate definitivamente
    if (isset($ops['purge']) && is_array($ops['purge'])) {
        $pg = read_json_file($purgedFile, array());
        foreach ($ops['purge'] as $id) {
            if (valid_id($id)) {
                $pg[$id] = $now;
                @unlink($dirRec . '/' . $id . '.json');
                @unlink($dirDel . '/' . $id . '.json');
            }
        }
        write_json_file($purgedFile, $pg);
    }

    out(200, array('ok' => true));
}

out(400, array('error' => 'Azione sconosciuta'));
