=16px, contrast inalt, un singur job per pagina, fara linkuri/butoane) function showPage($page_title, $h1_text, $h1_color, $body_text) { header('Content-Type: text/html; charset=utf-8'); echo '' . "\n"; echo '' . "\n"; echo '' . "\n"; echo '' . "\n"; echo '' . htmlspecialchars($page_title, ENT_QUOTES, 'UTF-8') . '' . "\n"; echo '' . "\n"; echo '' . "\n"; echo '

ROA Romfast

' . "\n"; echo '

' . htmlspecialchars($h1_text, ENT_QUOTES, 'UTF-8') . '

' . "\n"; echo '

' . htmlspecialchars($body_text, ENT_QUOTES, 'UTF-8') . '

' . "\n"; echo '' . "\n"; } // =================================================================== // Citire explicita parametri GET (FR-10: elimina parse_str care injecta variabile) // =================================================================== $refresh_token = isset($_GET['refresh_token']) ? (string)$_GET['refresh_token'] : ''; $code = isset($_GET['code']) ? (string)$_GET['code'] : ''; $state_param = isset($_GET['state']) ? (string)$_GET['state'] : ''; $error_param = isset($_GET['error']) ? (string)$_GET['error'] : ''; $error_description = isset($_GET['error_description']) ? (string)$_GET['error_description'] : ''; // =================================================================== // Ramura REFRESH TOKEN — neatinsa functional (FR-4) // Apelata din ROA direct cu ?refresh_token=... pentru prelungirea tokenului // =================================================================== if (!empty($refresh_token)) { $url = 'https://logincert.anaf.ro/anaf-oauth2/v1/token'; $fields = [ 'client_id' => $client_id, 'client_secret' => $client_secret, 'refresh_token' => $refresh_token, 'redirect_uri' => $redirect_uri, 'grant_type' => 'refresh_token', ]; $fields_string = http_build_query($fields); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); curl_setopt($ch, CURLOPT_TIMEOUT, 30); $jsonobj = curl_exec($ch); curl_close($ch); header('Content-Type: application/json'); // La eroare curl: corp gol, identic cu comportamentul vechi (FR-4: ramura neatinsa) echo ($jsonobj !== false) ? $jsonobj : ''; exit(); } // =================================================================== // Configurare cookie de sesiune PHP (FR-2) // session_set_cookie_params TREBUIE sa preceada session_start pe AMBELE intrari: // - intrarea initiala (fara ?code): browser deschis de ROACONT // - callback-ul ANAF (cu ?code): navigare top-level cross-site // // SameSite=Lax este obligatoriu: callback-ul ANAF este o navigare top-level dintr-un // alt domeniu (logincert.anaf.ro -> romfast.ro) — cu SameSite=Strict cookie-ul nu ar // fi trimis in callback si legarea sesiune ar esua silentios. // =================================================================== if (PHP_VERSION_ID >= 70300) { // Sintaxa cu array: disponibila incepand cu PHP 7.3 session_set_cookie_params([ 'lifetime' => 0, 'path' => '/', 'secure' => true, 'httponly' => true, 'samesite' => 'Lax', ]); } else { // Fallback PHP < 7.3: SameSite injectat ca sufix al parametrului path session_set_cookie_params(0, '/; SameSite=Lax', '', true, true); } session_start(); // =================================================================== // Callback de eroare ANAF (?error=...) — FR-10 // Tratare explicita; nu se re-redirecteaza la authorize (ar produce bucla) // =================================================================== if (!empty($error_param)) { // Extrage state-ul din sesiune pentru a scrie fisierul de eroare $sess_state = null; if (!empty($_SESSION['roa_state']) && is_array($_SESSION['roa_state'])) { $sess = $_SESSION['roa_state']; if (!empty($sess['value']) && (time() - (int)$sess['ts']) < 900) { $sess_state = $sess['value']; } } unset($_SESSION['roa_state']); session_write_close(); // Scrie fisier de eroare pentru state-ul din sesiune (daca exista si valid) if ($sess_state !== null && validState($sess_state)) { $fname = hash('sha256', $sess_state); $token_file = $tokens_dir . '/' . $fname . '.json'; $tmp_file = $tokens_dir . '/' . $fname . '.tmp'; // Anti-suprascriere: nu suprascrie un fisier de succes existent (FR-10) if (!file_exists($token_file)) { $err_data = json_encode([ 'error' => $error_param, 'error_description' => $error_description, ]); if ($err_data !== false && file_put_contents($tmp_file, $err_data) !== false) { @rename($tmp_file, $token_file); } } } // Pagina de eroare (§3.3: un job, fara retry link, fara re-redirect) $cause = !empty($error_description) ? $error_description : $error_param; showPage( 'Autorizare nereușită', 'Autorizarea nu a reușit', '#c0392b', $cause . ' Reveniți în aplicația ROA — de acolo puteți relua operația sau folosi completarea manuală.' ); exit(); } // =================================================================== // Intrare INITIALA — nicio sesiune, fara ?code: redirect catre ANAF authorize // =================================================================== if (empty($code)) { if (!empty($state_param)) { // Validare format state inainte de orice operatie (protectie path traversal) if (!validState($state_param)) { session_write_close(); // L1: H1 conform §3.3 ("Autorizarea nu a reușit" pentru toate paginile de eroare) showPage( 'Autorizare nereușită', 'Autorizarea nu a reușit', '#c0392b', 'Parametru de sesiune invalid. Reluați operația din aplicația ROA.' ); exit(); } // Salveaza state in sesiune cu timestamp (mecanism PRIMAR de legare) $_SESSION['roa_state'] = ['value' => $state_param, 'ts' => time()]; session_write_close(); } else { // FR-2 igienă sesiune: fara state in URL = flux manual/retro-compat // Curata orice roa_state ramas dintr-o tentativa automata anterioara esuata // (altfel, la callback, am afisa pagina de succes fara tokenuri = fund de sac) unset($_SESSION['roa_state']); session_write_close(); } // URL authorize ANAF — client_secret NU se trimite la authorize (FR-10) // client_secret apartine doar schimbului code->token (POST, fara URL) $url_auth = 'https://logincert.anaf.ro/anaf-oauth2/v1/authorize'; $url_auth .= '?client_id=' . urlencode($client_id); $url_auth .= '&response_type=code'; $url_auth .= '&redirect_uri=' . urlencode($redirect_uri); $url_auth .= '&token_content_type=jwt'; // FR-1: propagare state la ANAF daca exista // Nota: doc ANAF (pag.23) spune "State se lasa necompletat" — propagarea e incerta // dar trimitem oricum; legarea primara ramane sesiunea PHP if (!empty($state_param)) { $url_auth .= '&state=' . urlencode($state_param); } header('Location: ' . $url_auth); exit(); } // =================================================================== // Callback cu cod de autorizare (?code=...) — schimb code -> token // =================================================================== // FR-2: identifica state-ul din sesiune (mecanismul PRIMAR de legare) // state-ul din sesiune = cel generat de ROACONT si salvat la intrarea initiala $sess_state = null; if (!empty($_SESSION['roa_state']) && is_array($_SESSION['roa_state'])) { $sess = $_SESSION['roa_state']; if (!empty($sess['value']) && (time() - (int)$sess['ts']) < 900) { // roa_state valid (sub 15 minute) $sess_state = $sess['value']; } // roa_state mai vechi de 15 minute se ignora (si se sterge mai jos) } // Consuma state-ul din sesiune (indiferent de validitate — evita re-utilizare) unset($_SESSION['roa_state']); // FR-2: validare CSRF oportunistica // Daca ANAF a propagat state in callback (comportament incert), trebuie sa coincida cu sesiunea // Nepotrivire = posibil CSRF sau sesiune gresita -> eroare, fara stocare tokenuri if (!empty($state_param) && $sess_state !== null) { if ($state_param !== $sess_state) { session_write_close(); showPage( 'Eroare de securitate', 'Autorizarea nu a reușit', '#c0392b', 'Identificatorul de sesiune nu corespunde. Operația a fost anulată din motive de securitate. ' . 'Reveniți în aplicația ROA și reluați de acolo.' ); exit(); } } // FR-11 / C10: elibereaza lock-ul de sesiune inainte de curl // ANAF poate raspunde lent; nu tinem lock-ul de sesiune blocat in acest timp session_write_close(); // Schimb cod de autorizare -> token (FR-10: cu timeouts + tratare erori) $url_token = 'https://logincert.anaf.ro/anaf-oauth2/v1/token'; $fields_token = [ 'client_id' => $client_id, 'client_secret' => $client_secret, 'code' => $code, 'redirect_uri' => $redirect_uri, 'grant_type' => 'authorization_code', 'token_content_type' => 'jwt', ]; $fields_string = http_build_query($fields_token); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, $url_token); curl_setopt($ch, CURLOPT_POST, true); curl_setopt($ch, CURLOPT_POSTFIELDS, $fields_string); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // FR-10: timeout conectare curl_setopt($ch, CURLOPT_TIMEOUT, 30); // FR-10: timeout total $jsonobj = curl_exec($ch); $curl_errno = curl_errno($ch); $http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); // FR-10: tratare explicita erori curl / HTTP != 200 / JSON de eroare // (inainte: un esec afisa tokenuri goale) $error_msg = null; // text uman (pagina browser + error_description) $err_code = null; // cod scurt ASCII citit de ROACONT (loJson.error -> M12) $token_data = null; if ($curl_errno !== 0) { $err_code = 'curl_error'; $error_msg = 'Eroare de rețea la contactarea ANAF (curl errno: ' . (int)$curl_errno . ').'; } elseif ($http_code !== 200) { $err_code = 'http_error'; $error_msg = 'ANAF a răspuns cu codul HTTP ' . (int)$http_code . '.'; } else { $token_data = json_decode((string)$jsonobj, true); if ($token_data === null) { $err_code = 'json_invalid'; $error_msg = 'Răspunsul ANAF nu a putut fi decodificat (JSON invalid).'; } elseif (!empty($token_data['error'])) { $err_code = $token_data['error']; // cod brut ANAF: invalid_grant, access_denied etc. $err_desc = !empty($token_data['error_description']) ? $token_data['error_description'] : ''; $error_msg = 'ANAF: ' . $err_code . ($err_desc ? ' — ' . $err_desc : '') . '.'; } } // =================================================================== // Scriere fisier tokenuri (FR-3, FR-11) // Se efectueaza DOAR daca sesiunea avea un state valid (flux automat) // =================================================================== if ($sess_state !== null && validState($sess_state)) { $fname = hash('sha256', $sess_state); $token_file = $tokens_dir . '/' . $fname . '.json'; $tmp_file = $tokens_dir . '/' . $fname . '.tmp'; if ($error_msg !== null) { // Eroare: scrie fisier de eroare // FR-10 anti-suprascriere: nu suprascrie un token de succes existent // (ex. refresh pe pagina callback dupa succes: code expirat -> eroare -> nu stergem tokenul bun) if (!file_exists($token_file)) { $err_data = json_encode([ 'error' => $err_code, // cod scurt ASCII citit de ROACONT (fara mojibake) 'error_description' => $error_msg, // text uman UTF-8 (diagnostic) ]); if ($err_data !== false && file_put_contents($tmp_file, $err_data) !== false) { @rename($tmp_file, $token_file); } } } else { // Succes: scriere atomica a tokenurilor $json_out = json_encode($token_data, JSON_UNESCAPED_SLASHES); $write_ok = false; if ($json_out !== false && file_put_contents($tmp_file, $json_out) !== false) { // @rename() pe acelasi filesystem = atomic (pick.php nu poate citi JSON partial) if (@rename($tmp_file, $token_file)) { $write_ok = true; } else { @unlink($tmp_file); // curata .tmp la esec rename; TTL il curata oricum } } if (!$write_ok) { // Scriere esuata — afisam eroare (nu succes fals); ROACONT va detecta timeout $error_msg = 'Tokenul ANAF a fost primit dar nu a putut fi salvat temporar pe server. Reveniți în aplicație și reluați operația.'; } } } // =================================================================== // Afisare pagina rezultat (§3.3) // =================================================================== if ($sess_state !== null) { // Flux automat (state in sesiune): afiseaza succes sau eroare // Tokenurile NU apar in pagina — sunt ridicate de pick.php if ($error_msg !== null) { showPage( 'Autorizare nereușită', 'Autorizarea nu a reușit', '#c0392b', $error_msg . ' Reveniți în aplicația ROA — de acolo puteți relua operația sau folosi completarea manuală.' ); } else { // §3.3 pagina succes: H1 verde, corp cu instructiunea de revenire in ROA showPage( 'Autorizare reușită', 'Autorizare reușită', '#27ae60', 'Reveniți în aplicația ROA — tokenul se preia automat. Puteți închide această pagină.' ); } } else { // FR-4 retro-compat: fara state in sesiune = versiune veche ROACONT sau sesiune expirata // Comportamentul ACTUAL ramas neschimbat: tokenurile apar in pagina pentru copiere manuala if ($error_msg !== null) { echo '

Eroare la obtinerea tokenului: ' . htmlspecialchars($error_msg, ENT_QUOTES, 'UTF-8') . '

'; } else { echo '

Copiati ANAF Access Token si Refresh Token de pe liniile urmatoare si completati-le in aplicatie

'; echo '

Access Token:

'; echo htmlspecialchars((string)(isset($token_data['access_token']) ? $token_data['access_token'] : ''), ENT_QUOTES, 'UTF-8'); echo '

'; echo '

Refresh Token:

'; echo htmlspecialchars((string)(isset($token_data['refresh_token']) ? $token_data['refresh_token'] : ''), ENT_QUOTES, 'UTF-8'); echo '

'; } }