Compare commits

..

6 Commits

Author SHA1 Message Date
9fc17765f6 feat: adauga ROA AutoPass in lista de aplicatii online
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-09 17:06:27 +03:00
80d080e2d6 docs: documenteaza sub-proiectul oauth2/ in CLAUDE.md
Adauga sectiune despre index.php, pick.php, tokens/, php.ini si
tests/run_tests.php (ce face si cum se ruleaza).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 22:58:47 +03:00
b416bf5061 oauth2: teste automate pentru index.php + pick.php (php -S + HTTP)
Matricea ceruta: pick.php (state invalid/lipsa -> 400, pending, livrare
o singura data, TTL 600s, claim concurent) + index.php (refresh POST/GET
passthrough - regresia becbbe2, 302 authorize fara client_secret, state
invalid -> pagina eroare, callback ?error= cu/fara sesiune, flux complet
eroare pana la pick.php).

Rulare: php oauth2/tests/run_tests.php (34 asertiuni, exit 0/1).
Ruleaza pe copie in director temporar - nu atinge oauth2/tokens/ din repo.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 22:53:31 +03:00
becbbe2be4 oauth2: fix buton Actualizare - refresh_token acceptat si prin POST body
ROACONT refreshToken trimite refresh_token prin POST (anaf_efactura.prg:719),
iar originalul citea parametrii cu $_REQUEST (GET+POST). Citirea doar din
$_GET rupea butonul: cererea cadea pe redirectul authorize, logincert.anaf.ro
cerea certificat client si msxml6 dadea "Access is denied" (1429 nehandled).
Deploy-uit si verificat pe productie (POST si GET -> passthrough JSON ANAF).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-07 21:53:36 +03:00
1c80ab98fc curatare 2026-07-07 21:11:57 +03:00
1b94624a6d oauth2: flux automat token ANAF (FR-1..FR-11) + arhivare fisiere vechi
- index.php: schimb code->token cu tratare completa erori, sesiune
  PHP pentru legare state (CSRF), stocare atomica tokenuri in fisier
- pick.php: endpoint polling pentru ROACONT (preluare automata token)
- oauth2_tokens mutat in oauth2/tokens/ + .htaccess Require all denied
  (locatia initiala "extra-docroot" era de fapt in interiorul
  public_html, deci web-accesibila fara aceasta protectie)
- index1.php/index2.php/info.php (versiuni vechi) arhivate in
  _arhiva_oauth2/ cu .htaccess care blocheaza accesul HTTP

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-07 21:10:32 +03:00
11 changed files with 1067 additions and 471 deletions

View File

@@ -1,68 +0,0 @@
# language of the project (csharp, python, rust, java, typescript, go, cpp, or ruby)
# * For C, use cpp
# * For JavaScript, use typescript
# Special requirements:
# * csharp: Requires the presence of a .sln file in the project folder.
language: typescript
# whether to use the project's gitignore file to ignore files
# Added on 2025-04-07
ignore_all_files_in_gitignore: true
# list of additional paths to ignore
# same syntax as gitignore, so you can use * and **
# Was previously called `ignored_dirs`, please update your config if you are using that.
# Added (renamed) on 2025-04-07
ignored_paths: []
# whether the project is in read-only mode
# If set to true, all editing tools will be disabled and attempts to use them will result in an error
# Added on 2025-04-18
read_only: false
# list of tool names to exclude. We recommend not excluding any tools, see the readme for more details.
# Below is the complete list of tools for convenience.
# To make sure you have the latest list of tools, and to view their descriptions,
# execute `uv run scripts/print_tool_overview.py`.
#
# * `activate_project`: Activates a project by name.
# * `check_onboarding_performed`: Checks whether project onboarding was already performed.
# * `create_text_file`: Creates/overwrites a file in the project directory.
# * `delete_lines`: Deletes a range of lines within a file.
# * `delete_memory`: Deletes a memory from Serena's project-specific memory store.
# * `execute_shell_command`: Executes a shell command.
# * `find_referencing_code_snippets`: Finds code snippets in which the symbol at the given location is referenced.
# * `find_referencing_symbols`: Finds symbols that reference the symbol at the given location (optionally filtered by type).
# * `find_symbol`: Performs a global (or local) search for symbols with/containing a given name/substring (optionally filtered by type).
# * `get_current_config`: Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
# * `get_symbols_overview`: Gets an overview of the top-level symbols defined in a given file.
# * `initial_instructions`: Gets the initial instructions for the current project.
# Should only be used in settings where the system prompt cannot be set,
# e.g. in clients you have no control over, like Claude Desktop.
# * `insert_after_symbol`: Inserts content after the end of the definition of a given symbol.
# * `insert_at_line`: Inserts content at a given line in a file.
# * `insert_before_symbol`: Inserts content before the beginning of the definition of a given symbol.
# * `list_dir`: Lists files and directories in the given directory (optionally with recursion).
# * `list_memories`: Lists memories in Serena's project-specific memory store.
# * `onboarding`: Performs onboarding (identifying the project structure and essential tasks, e.g. for testing or building).
# * `prepare_for_new_conversation`: Provides instructions for preparing for a new conversation (in order to continue with the necessary context).
# * `read_file`: Reads a file within the project directory.
# * `read_memory`: Reads the memory with the given name from Serena's project-specific memory store.
# * `remove_project`: Removes a project from the Serena configuration.
# * `replace_lines`: Replaces a range of lines within a file with new content.
# * `replace_symbol_body`: Replaces the full definition of a symbol.
# * `restart_language_server`: Restarts the language server, may be necessary when edits not through Serena happen.
# * `search_for_pattern`: Performs a search for a pattern in the project.
# * `summarize_changes`: Provides instructions for summarizing the changes made to the codebase.
# * `switch_modes`: Activates modes by providing a list of their names
# * `think_about_collected_information`: Thinking tool for pondering the completeness of collected information.
# * `think_about_task_adherence`: Thinking tool for determining whether the agent is still on track with the current task.
# * `think_about_whether_you_are_done`: Thinking tool for determining whether the task is truly completed.
# * `write_memory`: Writes a named memory (for future reference) to Serena's project-specific memory store.
excluded_tools: []
# initial prompt for the project. It will always be given to the LLM upon activating the project
# (contrary to the memories, which are loaded on demand).
initial_prompt: ""
project_name: "SITE_ROMFAST_2025"

View File

@@ -34,6 +34,23 @@ Directorul `efactura-generator/` din acest repo este o **oglindă** a proiectulu
`config.json` de pe server (`~/public_html/efactura-generator/config.json`) conține `api_key` și nu e nici în repo, nici în sync — se gestionează manual pe a2hosting.
## Sub-proiectul `oauth2/`
Flux OAuth2 ANAF eFactura (obținere access/refresh token) folosit de aplicația desktop ROA/ROACONT. Deployat pe `https://romfast.ro/oauth2/`.
- **`index.php`** — endpoint principal, trei ramuri:
1. **Refresh token** (`refresh_token` primit prin `$_POST` sau `$_GET`) → passthrough curl către ANAF, întoarce JSON brut (folosit de butonul "Actualizare" din ROA).
2. **Intrare inițială** (fără `?code`) → redirect 302 către ANAF authorize. Dacă vine cu `?state=<64 hex>` salvează state-ul în sesiunea PHP (flux automat, legat de `pick.php`); fără `state` = flux manual/retro-compat (tokenurile apar direct în pagină).
3. **Callback ANAF** (`?code=...` sau `?error=...`) → schimb code→token; dacă există state valid în sesiune scrie fișier în `tokens/<sha256(state)>.json` (succes sau eroare) și afișează doar o pagină de confirmare; altfel (retro-compat) afișează access/refresh token direct în HTML.
- **`pick.php`** — endpoint de polling apelat de ROA cu `POST state=<64 hex>`. Răspunde `{"status":"pending"}` până apare fișierul de token, apoi îl livrează o singură dată (claim atomic prin `rename`) și îl șterge. TTL 600s pentru fișiere orfane/`.tmp`.
- **`tokens/`** — director de stocare temporară token/eroare, cheie = SHA-256(state). Protejat cu `.htaccess` (`Require all denied`); creat automat de `index.php`/`pick.php` dacă lipsește. Nu se commite conținutul (fișiere efemere, curățate de TTL).
- **`php.ini`** — config logging erori local pentru acest subdirector (`error_log = /oauth2/error_log.txt`).
- **`tests/run_tests.php`** — suită de teste self-contained (fără framework extern), acoperă `index.php` + `pick.php`. Rulare:
```bash
php oauth2/tests/run_tests.php
```
Ce face: copiază `index.php`/`pick.php` într-un director temporar (nu atinge `tokens/` din repo), pornește `php -S 127.0.0.1:8317` pe copie cu `session.save_path` propriu, rulează matricea de teste prin cereri HTTP reale (fără dependențe externe), oprește serverul și curăță totul la final (`register_shutdown_function`). Exit code 0 = toate testele au trecut; ieșirea arată `OK`/`FAIL` per test plus un rezumat `N trecute, M eșuate`. Notă: testele de refresh declanșează un apel curl real către `logincert.anaf.ro` cu un refresh_token fictiv (ANAF răspunde `invalid_grant`) — asercțiunile (nu e 302, `Content-Type` JSON) rămân valabile și offline.
## Development
No build process. Serve locally with:
@@ -85,6 +102,7 @@ The navbar HTML is **manually duplicated** across every page (no templating). Wh
- `/menu/` — company pages (about, contact, support, services, jobs, references)
- `/roa/` — ROA ERP product pages (one per module)
- `/efactura-generator/` — standalone Romanian e-invoice editor (loads XML, edits, prints, exports XML)
- `/oauth2/` — ANAF eFactura OAuth2 token flow for the ROA desktop app (see "Sub-proiectul `oauth2/`" above)
### efactura-generator

2
_arhiva_oauth2/.htaccess Normal file
View File

@@ -0,0 +1,2 @@
# Interzice accesul HTTP la fisierele arhivate (belt-and-braces, langa extra-docroot)
Require all denied

View File

@@ -151,6 +151,13 @@
<span class="link-arrow mt-4 inline-block">Deschide aplicația →</span>
</a>
<a href="https://autopass.romfast.ro" target="_blank" rel="noopener" class="professional-card" style="padding: 26px 24px;">
<span class="feature-icon"><i data-lucide="car" class="w-5 h-5" style="color:var(--primary)"></i></span>
<span class="feature-title mt-4 inline-block">ROA AutoPass</span>
<p class="feature-desc mt-2">Declari prestațiile de service auto la RAR din câteva click-uri — încarci Excel/CSV, restul se face automat.</p>
<span class="link-arrow mt-4 inline-block">Deschide aplicația →</span>
</a>
</div>
<p class="hero-sub mx-auto text-center mt-10" style="font-size:15px;">
@@ -171,6 +178,7 @@
<a href="https://pdfqr.roa.romfast.ro" target="_blank" rel="noopener" title="Generează cod QR și-l adaugă la PDF" class="inline-flex items-center gap-1.5 text-sm"><i data-lucide="scan-line" class="w-4 h-4"></i>PDF QR</a>
<a href="https://city-discovery.roa.romfast.ro" target="_blank" rel="noopener" title="Joc City Adventure Game" class="inline-flex items-center gap-1.5 text-sm"><i data-lucide="gamepad-2" class="w-4 h-4"></i>City Discovery</a>
<a href="https://space.roa.romfast.ro" target="_blank" rel="noopener" title="Rezervări spații" class="inline-flex items-center gap-1.5 text-sm"><i data-lucide="calendar-check" class="w-4 h-4"></i>Space Booking</a>
<a href="https://autopass.romfast.ro" target="_blank" rel="noopener" title="Declarații RAR pentru service-uri auto" class="inline-flex items-center gap-1.5 text-sm"><i data-lucide="car" class="w-4 h-4"></i>ROA AutoPass</a>
</div>
</div>

View File

@@ -1,136 +1,387 @@
<?php
// oAuth2 JWT Token
// index.php — OAuth2 ANAF eFactura: obtinere token automat + compatibilitate retro
// FR-1 (propagare state), FR-2 (sesiune primara), FR-3 (stocare tokenuri),
// FR-4 (retro-compat fara state), FR-10 (securitate), FR-11 (protectie stocare)
//
// Modificat: 2026-07-07 — implementare flux automat (stocare fisier + pagina succes)
$get = $_REQUEST;
$query_string = '';
foreach ($get as $key => $value) {
$query_string .= $key . '=' . $value . '&';
}
// echo($query_string);
parse_str($query_string);
// === Credentiale ANAF (pastrate din versiunea anterioara) ===
$client_id = 'f4c59081e488ac326f32f8ab52620023e996dd82d7d01163';
$client_secret = 'e65b9127acac254c5b9bfbbe68b9a6ca07b0d6df6dbb0023e996dd82d7d01163';
$redirect_uri = 'https://romfast.ro/oauth2/';
$client_id = 'f4c59081e488ac326f32f8ab52620023e996dd82d7d01163';
$client_secret = 'e65b9127acac254c5b9bfbbe68b9a6ca07b0d6df6dbb0023e996dd82d7d01163';
$redirect_uri = 'https://romfast.ro/oauth2/';
// === Director tokenuri (FR-11) ===
// oauth2/tokens/ — protejat explicit cu .htaccess (Require all denied), deoarece
// se afla in interiorul docroot-ului (public_html/oauth2/tokens), nu deasupra lui.
// Codul creaza directorul si .htaccess-ul daca lipsesc (ex. prima instalare)
$tokens_dir = __DIR__ . '/tokens';
if (!is_dir($tokens_dir)) {
@mkdir($tokens_dir, 0700, true);
}
$tokens_htaccess = $tokens_dir . '/.htaccess';
if (!file_exists($tokens_htaccess)) {
@file_put_contents($tokens_htaccess, "Require all denied\n");
}
// REFRESH TOKEN se apeleaza din ROA cu parametrul ?refresh_token=REFRESH_TOKEN pentru prelungirea valabilitatii tokenului
if (!empty($refresh_token)){
// ===================================================================
// Functii helper
// ===================================================================
// Validare format state: lungime fixa 64, doar [A-Za-z0-9] (protectie path traversal - NFR-1)
function validState($state) {
return is_string($state)
&& strlen($state) === 64
&& (bool)preg_match('/^[A-Za-z0-9]+$/', $state);
}
// Afisare pagina HTML simpla (§3.3: UTF-8 cu diacritice, H1 mare scanabil,
// font >=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 '<!DOCTYPE html>' . "\n";
echo '<html lang="ro"><head>' . "\n";
echo '<meta charset="utf-8">' . "\n";
echo '<meta name="viewport" content="width=device-width,initial-scale=1">' . "\n";
echo '<title>' . htmlspecialchars($page_title, ENT_QUOTES, 'UTF-8') . '</title>' . "\n";
echo '<style>' . "\n";
echo 'body{font-family:Arial,Helvetica,sans-serif;font-size:18px;margin:3em auto;' . "\n";
echo ' max-width:640px;padding:0 1.5em;background:#f7f7f7;color:#222}' . "\n";
echo 'h1{font-size:2em;color:' . $h1_color . ';margin-bottom:0.5em}' . "\n";
echo 'p{font-size:1em;line-height:1.7;margin:0}' . "\n";
echo '.brand{font-size:0.8em;color:#999;margin-bottom:2.5em}' . "\n";
echo '</style>' . "\n";
echo '</head><body>' . "\n";
echo '<p class="brand">ROA Romfast</p>' . "\n";
echo '<h1>' . htmlspecialchars($h1_text, ENT_QUOTES, 'UTF-8') . '</h1>' . "\n";
echo '<p>' . htmlspecialchars($body_text, ENT_QUOTES, 'UTF-8') . '</p>' . "\n";
echo '</body></html>' . "\n";
}
// ===================================================================
// Citire explicita parametri GET (FR-10: elimina parse_str care injecta variabile)
// ===================================================================
// refresh_token vine din ROA prin POST body (refreshToken - anaf_efactura.prg) sau GET;
// originalul folosea $_REQUEST, deci accepta ambele - citirea doar din $_GET rupe butonul Actualizare
$refresh_token = isset($_POST['refresh_token']) ? (string)$_POST['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';
//The data you want to send via POST
$fields = [
'client_id' => $client_id,
'client_secret' => $client_secret,
'refresh_token' => $refresh_token,
'redirect_uri' => $redirect_uri,
'grant_type' => 'refresh_token'
'grant_type' => 'refresh_token',
];
//url-ify the data for the POST
$fields_string = http_build_query($fields);
//open connection
$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);
//set the url, number of POST vars, POST data
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_POST, true);
curl_setopt($ch,CURLOPT_POSTFIELDS, $fields_string);
//So that curl_exec returns the contents of the cURL; rather than echoing it
curl_setopt($ch,CURLOPT_RETURNTRANSFER, true);
//execute post
$jsonobj = curl_exec($ch);
curl_close($ch);
// $arr = json_decode($jsonobj, true);
header('Content-Type: application/json');
// La eroare curl: corp gol, identic cu comportamentul vechi (FR-4: ramura neatinsa)
echo ($jsonobj !== false) ? $jsonobj : '';
exit();
}
/* $json = json_encode($jsonobj, JSON_UNESCAPED_SLASHES);
if ($json === false) {
// Avoid echo of empty string (which is invalid JSON), and
// JSONify the error message instead:
$json = json_encode(["error" => json_last_error_msg()]);
if ($json === false) {
// This should not happen, but we go all the way now:
$json = '{"error":"unknown"}';
// ===================================================================
// 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'];
}
// Set HTTP response status code to: 500 - Internal Server Error
http_response_code(500);
}
*/
header("Content-Type: application/json");
echo $jsonobj;
unset($_SESSION['roa_state']);
session_write_close();
// Close handle
//curl_close($ch);
// 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();
}
// COD AUTORIZARE se apeleaza din ROA fara parametri pentru obtinerea codului de autorizare de la ANAF. ANAF apeleaza aceasta pagina (callback) cu codul de autorizare
if (empty($code)) {
// Obtinere cod autorizare
$url = 'https://logincert.anaf.ro/anaf-oauth2/v1/authorize';
$url .='?client_id='.$client_id;
$url .='&client_secret='.$client_secret;
$url .='&response_type=code';
$url .='&redirect_uri='.$redirect_uri;
$url .='&token_content_type=jwt';
// 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();
}
// Redirectionez la pagina de autorizare ANAF pentru obtinerea codului de autorizare
// echo $url;
header('Location: '.$url);
// 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();
}
else {
// NEW TOKEN
// callback ANAF: https://romfast.ro/oauth2/?code=COD_AUTORIZARE
}
// Obtinere token pe baza codului de autorizare
// print_r('Codul de autorizare este: '.$code);
// 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();
$url = 'https://logincert.anaf.ro/anaf-oauth2/v1/token';
//The data you want to send via POST
$fields = [
// 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'
];
'token_content_type' => 'jwt',
];
$fields_string = http_build_query($fields_token);
//url-ify the data for the POST
$fields_string = http_build_query($fields);
$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
//open connection
$ch = curl_init();
$jsonobj = curl_exec($ch);
$curl_errno = curl_errno($ch);
$http_code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
//set the url, number of POST vars, POST data
curl_setopt($ch,CURLOPT_URL, $url);
curl_setopt($ch,CURLOPT_POST, true);
curl_setopt($ch,CURLOPT_POSTFIELDS, $fields_string);
//So that curl_exec returns the contents of the cURL; rather than echoing it
curl_setopt($ch,CURLOPT_RETURNTRANSFER, true);
//execute post
$jsonobj = curl_exec($ch);
//echo $jsonobj;
$arr = json_decode($jsonobj, true);
echo '<p>Copiati ANAF Access Token si Refresh Token de pe liniile urmatoare si completati-le in aplicatie</p>';
echo '<p><b>Access Token:</b></p><p>'.$arr["access_token"].'</p>';
echo '<p><b>Refresh Token:</b></p><p>'.$arr["refresh_token"].'</p>';
// Close handle
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 '<p>Eroare la obtinerea tokenului: ' . htmlspecialchars($error_msg, ENT_QUOTES, 'UTF-8') . '</p>';
} else {
echo '<p>Copiati ANAF Access Token si Refresh Token de pe liniile urmatoare si completati-le in aplicatie</p>';
echo '<p><b>Access Token:</b></p><p>';
echo htmlspecialchars((string)(isset($token_data['access_token']) ? $token_data['access_token'] : ''), ENT_QUOTES, 'UTF-8');
echo '</p>';
echo '<p><b>Refresh Token:</b></p><p>';
echo htmlspecialchars((string)(isset($token_data['refresh_token']) ? $token_data['refresh_token'] : ''), ENT_QUOTES, 'UTF-8');
echo '</p>';
}
}

62
oauth2/pick.php Normal file
View File

@@ -0,0 +1,62 @@
<?php
// pick.php — Endpoint polling preluare automata token OAuth2 ANAF eFactura
// Apelat de ROACONT cu POST state=<64 hex>; raspunde JSON:
// {"status":"pending"} cat timp tokenul nu a sosit (intotdeauna HTTP 200, nu 404),
// continutul fisierului de token (succes sau eroare ANAF) o SINGURA data, apoi il sterge.
// Fara session_start() — evita serializarea pe lock-ul de sesiune PHP.
header('Content-Type: application/json; charset=utf-8');
// Director tokenuri (acelasi ca in index.php) — protejat cu .htaccess
$tokens_dir = __DIR__ . '/tokens';
if (!is_dir($tokens_dir)) {
@mkdir($tokens_dir, 0700, true);
}
$tokens_htaccess = $tokens_dir . '/.htaccess';
if (!file_exists($tokens_htaccess)) {
@file_put_contents($tokens_htaccess, "Require all denied\n");
}
// Validare stricta format state: 64 caractere [A-Za-z0-9] (protectie path traversal)
$state = isset($_POST['state']) ? (string)$_POST['state'] : '';
if (strlen($state) !== 64 || !preg_match('/^[A-Za-z0-9]+$/', $state)) {
http_response_code(400);
echo json_encode(['error' => 'invalid_state']);
exit();
}
// TTL: sterge fisierele de tokenuri (si .tmp orfane) mai vechi de 10 minute
// (aliniat cu timeout-ul de polling din ROACONT)
$now = time();
foreach (array_merge((array)glob($tokens_dir . '/*.json'), (array)glob($tokens_dir . '/*.tmp')) as $f) {
if (($now - filemtime($f)) > 600) {
@unlink($f);
}
}
// Numele fisierului = SHA-256(state): secretul nu apare in listinguri de directoare
$token_file = $tokens_dir . '/' . hash('sha256', $state) . '.json';
if (!file_exists($token_file)) {
echo json_encode(['status' => 'pending']);
exit();
}
// Claim atomic prin rename: din doua cereri concurente una singura primeste tokenul,
// cealalta "pending"; previne si citirea unui JSON partial (index.php scrie .tmp + rename)
$claim_file = $tokens_dir . '/' . hash('sha256', $state) . '_claim_' . getmypid() . '.json';
if (!@rename($token_file, $claim_file)) {
echo json_encode(['status' => 'pending']);
exit();
}
// Citire INAINTE de stergere; la esec nu stergem (TTL curata fisierul claim ramas)
$content = file_get_contents($claim_file);
if ($content === false || $content === '') {
echo json_encode(['error' => 'read_error']);
exit();
}
@unlink($claim_file);
// Continutul JSON exact cum a fost salvat de index.php (tokenuri sau eroare ANAF)
echo $content;

322
oauth2/tests/run_tests.php Normal file
View File

@@ -0,0 +1,322 @@
<?php
// run_tests.php — teste pentru oauth2/index.php + oauth2/pick.php (flux automat token ANAF)
//
// Rulare: php oauth2/tests/run_tests.php
//
// Nu exista framework de teste in proiect; scriptul este self-contained:
// - copiaza index.php si pick.php intr-un director temporar (nu atinge tokens/ din repo),
// - porneste "php -S 127.0.0.1:8317" pe copie, cu sesiuni intr-un save_path propriu,
// - ruleaza matricea de teste cu cereri HTTP (streams, fara dependinte externe),
// - opreste serverul si curata directorul temporar; exit code 0 = toate au trecut.
//
// NOTA retea: testele de refresh (passthrough) declanseaza in index.php un apel curl
// real catre logincert.anaf.ro cu un refresh_token fictiv (ANAF raspunde invalid_grant).
// Asertiunile (nu e 302, Content-Type JSON) raman valabile si offline (corp gol).
error_reporting(E_ALL);
const BASE = 'http://127.0.0.1:8317';
// ===================================================================
// Helper HTTP: cerere fara follow-redirect, cu cookie optional
// Returneaza ['status' => int, 'headers' => array, 'body' => string]
// ===================================================================
function http($method, $url, $post = null, $cookie = null, $timeout = 45) {
$header = "Connection: close\r\n";
if ($cookie !== null) {
$header .= 'Cookie: ' . $cookie . "\r\n";
}
$opts = ['http' => [
'method' => $method,
'ignore_errors' => true, // returneaza corpul si la 4xx/5xx
'follow_location' => 0, // vrem sa vedem 302-ul, nu sa-l urmam
'timeout' => $timeout,
]];
if ($post !== null) {
$header .= "Content-Type: application/x-www-form-urlencoded\r\n";
$opts['http']['content'] = http_build_query($post);
}
$opts['http']['header'] = $header;
$body = @file_get_contents($url, false, stream_context_create($opts));
$headers = isset($http_response_header) ? $http_response_header : [];
$status = 0;
if (!empty($headers) && preg_match('#^HTTP/\S+\s+(\d{3})#', $headers[0], $m)) {
$status = (int)$m[1];
}
return ['status' => $status, 'headers' => $headers, 'body' => (string)$body];
}
// Cauta un header dupa nume (case-insensitive); null daca lipseste
function hdr($resp, $name) {
foreach ($resp['headers'] as $h) {
if (stripos($h, $name . ':') === 0) {
return trim(substr($h, strlen($name) + 1));
}
}
return null;
}
// ===================================================================
// Mod copil pentru testul de claim concurent:
// php run_tests.php --pick-once <url> <state>
// Face un singur POST pe pick.php si scrie corpul raspunsului pe stdout.
// ===================================================================
if (isset($argv[1]) && $argv[1] === '--pick-once') {
$r = http('POST', $argv[2], ['state' => $argv[3]]);
echo $r['body'];
exit(0);
}
// ===================================================================
// Setup: director temporar + server php -S
// ===================================================================
$tmp = rtrim(sys_get_temp_dir(), '/\\') . DIRECTORY_SEPARATOR . 'roa_oauth2_tests_' . getmypid();
$sessions = $tmp . DIRECTORY_SEPARATOR . 'sessions';
$tokens = $tmp . DIRECTORY_SEPARATOR . 'tokens';
if (!mkdir($tmp, 0700, true) || !mkdir($sessions, 0700, true)) {
fwrite(STDERR, "Nu pot crea directorul temporar: $tmp\n");
exit(2);
}
copy(__DIR__ . '/../index.php', $tmp . DIRECTORY_SEPARATOR . 'index.php');
copy(__DIR__ . '/../pick.php', $tmp . DIRECTORY_SEPARATOR . 'pick.php');
$server = proc_open(
[PHP_BINARY, '-S', '127.0.0.1:8317', '-t', $tmp,
'-d', 'session.save_path=' . $sessions,
'-d', 'xdebug.mode=off'],
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes
);
if (!is_resource($server)) {
fwrite(STDERR, "Nu pot porni php -S\n");
exit(2);
}
register_shutdown_function(function () use ($server, $tmp) {
@proc_terminate($server);
@proc_close($server);
// curatare best-effort a directorului temporar
$it = new RecursiveIteratorIterator(
new RecursiveDirectoryIterator($tmp, FilesystemIterator::SKIP_DOTS),
RecursiveIteratorIterator::CHILD_FIRST
);
foreach ($it as $f) {
$f->isDir() ? @rmdir($f->getPathname()) : @unlink($f->getPathname());
}
@rmdir($tmp);
});
// Asteapta serverul (max 5 secunde)
$ready = false;
for ($i = 0; $i < 50; $i++) {
$s = @fsockopen('127.0.0.1', 8317, $en, $es, 0.1);
if ($s) { fclose($s); $ready = true; break; }
usleep(100000);
}
if (!$ready) {
fwrite(STDERR, "Serverul php -S nu a pornit pe 127.0.0.1:8317\n");
exit(2);
}
// ===================================================================
// Asertiuni
// ===================================================================
$pass = 0;
$fail = 0;
function check($name, $cond, $detail = '') {
global $pass, $fail;
if ($cond) {
$pass++;
echo " OK $name\n";
} else {
$fail++;
echo " FAIL $name" . ($detail !== '' ? " [$detail]" : '') . "\n";
}
}
function state_nou() {
return bin2hex(random_bytes(32)); // 64 caractere hex = format valid
}
function fisier_token($tokens, $state) {
return $tokens . DIRECTORY_SEPARATOR . hash('sha256', $state) . '.json';
}
// ===================================================================
// pick.php
// ===================================================================
echo "pick.php\n";
// P1: fara state -> 400 invalid_state
$r = http('POST', BASE . '/pick.php', []);
check('P1 fara state -> 400 invalid_state',
$r['status'] === 400 && strpos($r['body'], 'invalid_state') !== false,
"status={$r['status']} body={$r['body']}");
// P2: state prea scurt -> 400
$r = http('POST', BASE . '/pick.php', ['state' => 'abc123']);
check('P2 state prea scurt -> 400', $r['status'] === 400, "status={$r['status']}");
// P3: lungime 64 dar caractere invalide (path traversal) -> 400
$r = http('POST', BASE . '/pick.php', ['state' => str_repeat('a', 60) . '/../']);
check('P3 caractere invalide -> 400', $r['status'] === 400, "status={$r['status']}");
// P4: state valid, fisier inexistent -> 200 pending
$s = state_nou();
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
check('P4 fara fisier -> 200 {"status":"pending"}',
$r['status'] === 200 && $r['body'] === '{"status":"pending"}',
"status={$r['status']} body={$r['body']}");
check('P4 Content-Type JSON',
stripos((string)hdr($r, 'Content-Type'), 'application/json') !== false);
// FR-11: prima cerere a creat tokens/ + .htaccess "Require all denied"
check('FR-11 tokens/.htaccess creat automat',
is_file($tokens . '/.htaccess')
&& strpos((string)file_get_contents($tokens . '/.htaccess'), 'Require all denied') !== false);
// P5: fisier prezent -> continutul exact, O SINGURA data, apoi pending
$s = state_nou();
$continut = '{"access_token":"AT_test","refresh_token":"RT_test","expires_in":3600}';
file_put_contents(fisier_token($tokens, $s), $continut);
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
check('P5 fisier prezent -> continutul exact', $r['body'] === $continut,
"body={$r['body']}");
check('P5 fisierul e sters dupa livrare', !file_exists(fisier_token($tokens, $s)));
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
check('P5 a doua cerere -> pending', $r['body'] === '{"status":"pending"}',
"body={$r['body']}");
// P6: TTL — fisier mai vechi de 10 minute -> sters, raspuns pending
$s = state_nou();
file_put_contents(fisier_token($tokens, $s), $continut);
touch(fisier_token($tokens, $s), time() - 700);
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
check('P6 fisier expirat (TTL 600s) -> pending', $r['body'] === '{"status":"pending"}',
"body={$r['body']}");
check('P6 fisierul expirat e sters', !file_exists(fisier_token($tokens, $s)));
// P7: TTL curata si .tmp orfane vechi
$tmp_orfan = $tokens . DIRECTORY_SEPARATOR . 'orfan.tmp';
file_put_contents($tmp_orfan, 'x');
touch($tmp_orfan, time() - 700);
http('POST', BASE . '/pick.php', ['state' => state_nou()]);
check('P7 .tmp orfan vechi e sters de TTL', !file_exists($tmp_orfan));
// P8: claim concurent — doua cereri simultane, exact una primeste tokenul.
// Nota: php -S pe Windows serveste secvential; concurenta reala e pe partea de
// client, dar mecanismul testat (rename atomic in pick.php) acopera ambele cazuri.
$s = state_nou();
file_put_contents(fisier_token($tokens, $s), $continut);
$copii = [];
$pipe = [];
for ($i = 0; $i < 2; $i++) {
$copii[$i] = proc_open(
[PHP_BINARY, '-d', 'xdebug.mode=off', __FILE__, '--pick-once', BASE . '/pick.php', $s],
[1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipe[$i]
);
}
$rasp = [];
for ($i = 0; $i < 2; $i++) {
$rasp[$i] = stream_get_contents($pipe[$i][1]);
proc_close($copii[$i]);
}
$cu_token = 0;
$cu_pending = 0;
foreach ($rasp as $b) {
if ($b === $continut) $cu_token++;
if ($b === '{"status":"pending"}') $cu_pending++;
}
check('P8 claim concurent: exact una primeste tokenul, cealalta pending',
$cu_token === 1 && $cu_pending === 1,
'raspunsuri: [' . implode('] [', $rasp) . ']');
// ===================================================================
// index.php
// ===================================================================
echo "index.php\n";
// I1: fara parametri -> 302 catre authorize, FARA client_secret, fara state
$r = http('GET', BASE . '/index.php');
$loc = (string)hdr($r, 'Location');
check('I1 fara parametri -> 302', $r['status'] === 302, "status={$r['status']}");
check('I1 Location = ANAF authorize',
strpos($loc, 'https://logincert.anaf.ro/anaf-oauth2/v1/authorize?') === 0, "loc=$loc");
check('I1 authorize contine response_type=code + jwt',
strpos($loc, 'response_type=code') !== false
&& strpos($loc, 'token_content_type=jwt') !== false);
check('I1 client_secret NU apare in URL-ul authorize',
strpos($loc, 'client_secret') === false, "loc=$loc");
check('I1 fara state in URL cand nu s-a trimis state',
strpos($loc, 'state=') === false, "loc=$loc");
// I2: ?state=valid -> 302 cu state propagat + cookie de sesiune
$s = state_nou();
$r = http('GET', BASE . '/index.php?state=' . $s);
$loc = (string)hdr($r, 'Location');
check('I2 ?state=valid -> 302 spre authorize', $r['status'] === 302
&& strpos($loc, 'https://logincert.anaf.ro/anaf-oauth2/v1/authorize?') === 0,
"status={$r['status']}");
check('I2 state propagat in authorize', strpos($loc, 'state=' . $s) !== false, "loc=$loc");
check('I2 client_secret NU apare', strpos($loc, 'client_secret') === false);
$setc = (string)hdr($r, 'Set-Cookie');
check('I2 Set-Cookie sesiune PHP', strpos($setc, 'PHPSESSID=') !== false, "set-cookie=$setc");
// I3: ?state cu format invalid -> pagina de eroare, fara redirect
$r = http('GET', BASE . '/index.php?state=' . str_repeat('a', 60) . '..%2F.');
check('I3 state invalid -> 200 pagina eroare (nu redirect)',
$r['status'] === 200 && hdr($r, 'Location') === null, "status={$r['status']}");
check('I3 pagina contine "Autorizarea nu a reușit"',
strpos($r['body'], 'Autorizarea nu a reușit') !== false);
// I4: refresh prin POST body -> passthrough JSON (regresia fixata in becbbe2:
// inainte, POST-ul era ignorat si cererea cadea pe 302 authorize)
$r = http('POST', BASE . '/index.php', ['refresh_token' => 'dummy_refresh_token_test']);
check('I4 refresh POST -> nu e 302 (regresie buton Actualizare)',
$r['status'] !== 302, "status={$r['status']}");
check('I4 refresh POST -> Content-Type application/json',
stripos((string)hdr($r, 'Content-Type'), 'application/json') !== false,
'content-type=' . hdr($r, 'Content-Type'));
// I5: refresh prin GET (compatibilitate retro) -> acelasi passthrough
$r = http('GET', BASE . '/index.php?refresh_token=dummy_refresh_token_test');
check('I5 refresh GET -> nu e 302', $r['status'] !== 302, "status={$r['status']}");
check('I5 refresh GET -> Content-Type application/json',
stripos((string)hdr($r, 'Content-Type'), 'application/json') !== false,
'content-type=' . hdr($r, 'Content-Type'));
// I6: callback ?error= fara sesiune -> pagina de eroare, fara fisier scris
$inainte = count((array)glob($tokens . '/*.json'));
$r = http('GET', BASE . '/index.php?error=access_denied&error_description=Test');
check('I6 ?error= fara sesiune -> pagina eroare',
$r['status'] === 200 && strpos($r['body'], 'Autorizarea nu a reușit') !== false,
"status={$r['status']}");
check('I6 nu se scrie fisier de token fara sesiune',
count((array)glob($tokens . '/*.json')) === $inainte);
// I7: flux complet de eroare: intrare cu state (sesiune) -> callback ?error=
// -> fisier de eroare scris -> pick.php il livreaza o singura data
$s = state_nou();
$r = http('GET', BASE . '/index.php?state=' . $s);
$cookie = null;
if (preg_match('/PHPSESSID=([^;]+)/', (string)hdr($r, 'Set-Cookie'), $m)) {
$cookie = 'PHPSESSID=' . $m[1];
}
check('I7 intrare cu state -> cookie sesiune obtinut', $cookie !== null);
$r = http('GET', BASE . '/index.php?error=access_denied&error_description=Utilizatorul+a+refuzat',
null, $cookie);
check('I7 callback ?error= cu sesiune -> pagina eroare',
$r['status'] === 200 && strpos($r['body'], 'Autorizarea nu a reușit') !== false,
"status={$r['status']}");
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
$j = json_decode($r['body'], true);
check('I7 pick.php livreaza eroarea ANAF',
is_array($j) && isset($j['error']) && $j['error'] === 'access_denied',
"body={$r['body']}");
$r = http('POST', BASE . '/pick.php', ['state' => $s]);
check('I7 a doua cerere pick -> pending', $r['body'] === '{"status":"pending"}',
"body={$r['body']}");
// ===================================================================
// Rezumat
// ===================================================================
echo "\n$pass trecute, $fail esuate\n";
exit($fail === 0 ? 0 : 1);

1
oauth2/tokens/.htaccess Normal file
View File

@@ -0,0 +1 @@
Require all denied