__cronOverrideRole) ? DelegheRoles::RP : $this->__cronOverrideRole; $userCodiceFiscale = empty($this->__cronOverrideCF) ? $this->getUserFromRole($userRole, RoleSyncBehaviour::NEVER_SYNC) : $this->__cronOverrideCF; } elseif (!empty($_SESSION["utente"]) && method_exists($_SESSION["utente"], "getInfo")) { $userCodiceFiscale = $_SESSION["utente"]->getInfo()["cf"]; } } if(empty($userRole)) { $userRole = $this->getUserRole($userCodiceFiscale, null, RoleSyncBehaviour::NEVER_SYNC); } // Gather delle info sulla stazione appaltante if(empty($saCodiceFiscale) && empty($saCodiceAusa)) { if(isset($this->overrideStazioneAppaltante)) { $saCodiceFiscale = $this->overrideStazioneAppaltante["codiceFiscale"]; $saCodiceAusa = $this->overrideStazioneAppaltante["codiceAusa"]; } else { // Proviamo se la delega corrente prevede info della SA $infoSA = $this->getFullDelegaInfo($userCodiceFiscale); if(!empty($infoSA) && !empty($infoSA["sa_cf"]) && !empty($infoSA["sa_ausa"])) { $saCodiceFiscale = $infoSA["sa_cf"]; $saCodiceAusa = $infoSA["sa_ausa"]; } else { // Proviamo poi dal fascicolo if (!empty($this->fascicolo["data"])) { $data = unserialize($this->fascicolo["data"]); } $saCodiceFiscale = $data["benficiario"]["codice_fiscale"] ?? null; $saCodiceAusa = $data["benficiario"]["codice_ausa"] ?? null; // Poi dall'ente if (isset($_SESSION["utente"]) && isset($_SESSION["ente"]) && (!defined("__CMDS_JOB_EXEC") || empty(__CMDS_JOB_EXEC))) { $ente = (!empty($_SESSION["utente"]->codice_ente)) ? $_SESSION["utente"]->codice_ente : $_SESSION["ente"]->codice; if (!empty($ente)) { $ente = Ente::getInfoFromID($ente); if (!empty($ente)) { $ente = reset($ente); if (!empty($ente["cf"]) && !empty($ente["codice_ausa"]) && $ente["cf"] != $saCodiceFiscale) { $saCodiceFiscale = $ente["cf"]; $saCodiceAusa = $ente["codice_ausa"]; } } } } } } } else { if(empty($saCodiceFiscale) || empty($saCodiceAusa)) { throw new Exception("Se viene specificato il CF della Stazione Appaltante è necessario specificare anche il codice AUSA e viceversa"); } } // Gathering del codice fiscale e ruolo $userCodiceFiscale = trim($userCodiceFiscale); if (!in_array($userRole, DelegheRoles::VALID_ROLES)) { return []; } $userLoa = $_SESSION["utente"]->authenticationLevel + 1; return [ "userCodiceFiscale" => $userCodiceFiscale, // codice fiscale dell'utente connesso alla piattaforma e operante per conto della Stazione appaltante "userRole" => $userRole, // ruolo dell'utente. RP = Responsabile del Porgetto; DRP = Delegato del Responsabile del Porgetto [RP, DRP] "userLoa" => "{$userLoa}", // level of assurance adottato dalla piattaforma per l'identificazione dell'utente [3, 4] "userIdpType" => (!empty($_SESSION["utente"]->publicSSO)) ? $_SESSION["utente"]->publicSSO : "CUSTOM", // tipo di identity provider utilizzato per stabilire l'identità dell'utente. "SACodiceFiscale" => DEVELOP_ENV && isset($config["DEV"]["SACodiceFiscale"]) ? $config["DEV"]["SACodiceFiscale"] : ($saCodiceFiscale), // codice Fiscale della stazione appaltante. Può essere nullo in caso di soggetti non dotati di personalità giuridica "SAcodiceAUSA" => DEVELOP_ENV && isset($config["DEV"]["SACodiceAusa"]) ? $config["DEV"]["SACodiceAusa"] : ($saCodiceAusa), // codice ausa della stazione appaltante alla quale appartiene l'utente "regCodicePiattaforma" => $config["pdnd"]["platform"]["regCodicePiattaforma"], // codice identificativo della piattaforma "regCodiceComponente" => $config["pdnd"]["platform"]["regCodiceComponente"], // codice identificativo del componente client "businessFlowID" => $businessFlowID, // coincide con idAppalto. Assume valore "00000000-0000-0000-0000-000000000000" nella prima transazione (che è necessariamente comunicaAppalto.crea-appalto) In tutte le operazioni successive riconduce la transazione all’appalto "traceID" => \Ramsey\Uuid\Uuid::uuid6()->toString(), // identificativo univoco assegnato dalla piattaforma alla richiesta "spanID" => \Ramsey\Uuid\Uuid::uuid6()->toString(), // identificativo univoco assegnato dalla piattaforma (?) all'operazione iniziale richiesta dall'utente "aud" => $aud, // Audience ]; } /** * Effettua una chiamata API verso NPA * * @param String $apiEndpoint Endpoint da contattare * @param String $method Metodo da contattare * @param array $body Corpo della richiesta * @param array|null $claims Claim per la generazione dei voucher * @param array|null $header * @param string|null $message Viene popolato con u neventuale messaggio d'errore * @param int $retry401 Viene usato per tenere traccia se la richiesta corrente è un tentativo per autenticazione fallita * @return String */ public function apiCall(String $apiEndpoint, String $method, array $body, ?array $claims = null, ?array $header = [], ?string &$message = null, int $retry401 = Retries401::NORMAL): String { // Cerchiamo le config $endpointConfig = $this->api_config[$apiEndpoint] ?? null; $methodConfig = $endpointConfig["methods"][$method] ?? null; if ($endpointConfig == null || $methodConfig == null) { throw new Exception("Chiamata API {$apiEndpoint}::{$method} non trovata."); } // Verifichiamo che l'utente possa fare la chiamata $canDoRequest = $this->canDoRequest($reason, $apiEndpoint, $method); if (!$canDoRequest) { $message = $reason["msg"]; if ($methodConfig["log_auth_failures"] ?? true) { $this->appendLog( $apiEndpoint, $method, ["canDoRequest" => false], is_array($reason) ? json_encode($reason) : $reason, ); } return false; } $saveLog = $methodConfig["log"] ?? true; $url = $endpointConfig["endpoint"] . "/" . $methodConfig["path"]; $request_method = $methodConfig["type"] ?? "GET"; $ch = curl_init(); $url = ltrim($url, "/"); if ($request_method == "POST") { $send_header = array_merge(["Content-Type: application/json"], $header); $payload = json_encode($body); curl_setopt($ch, CURLOPT_URL, $url); curl_setopt($ch, CURLOPT_POSTFIELDS, $payload); } else { $send_header = $header; $query = ""; if (!empty($body)) { $query = "?" . http_build_query($body); } $url = "{$url}{$query}"; curl_setopt($ch, CURLOPT_URL, $url); } // Generazione di un voucher generico if (empty($claims)) { $claims = $this->getAnacJwsCustomClaims( $endpointConfig["endpoint"], $this->getUUID() ); } if (isset($claims["userCodiceFiscale"])) { // Se è già uppercase skippiamo il secondo tentativo // Altrimenti il primo tentativo lo manda così com'è if ($retry401 == Retries401::UPPERCASE ) { $claims["userCodiceFiscale"] = strtoupper($claims["userCodiceFiscale"]); // Se è il terzo tentativo proviamo tutto lowercase } elseif ($retry401 == Retries401::LOWERCASE) { $claims["userCodiceFiscale"] = strtolower($claims["userCodiceFiscale"]); } } try { $authorization = PDND::getVoucher( $endpointConfig["purpose"], $claims ); } catch (\Throwable $t) { $message = DEVELOP_ENV ? $t->getMessage() : "Impossibile contattare i server PDND. Riprova più tardi."; $this->appendLog( $apiEndpoint, $method, ["pdnd" => true], "PDND error " . $t->getMessage(), ); return false; } // Mancanza di voucher if ($authorization === null || empty($authorization->access_token)) { $message = "Impossibile creare Voucher PDND"; $this->appendLog( $apiEndpoint, $method, [ "pdnd" => true, ], "Can't create voucher." ); return false; } $send_header = array_merge([ "Authorization: Bearer {$authorization->access_token}", "Agid-JWT-TrackingEvidence: {$authorization->tracking_evidence_jwt}" ], $send_header); curl_setopt($ch, CURLOPT_HTTPHEADER, $send_header); curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $request_method); curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, FALSE); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, FALSE); curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); curl_setopt($ch, CURLOPT_FOLLOWLOCATION, TRUE); curl_setopt($ch, CURLOPT_TIMEOUT, 60); if ($method === "serviceStatus") { curl_setopt($ch, CURLOPT_TIMEOUT, 10); } curl_setopt($ch, CURLOPT_MAXREDIRS, 10); curl_setopt($ch, CURLOPT_ENCODING, 'UTF-8'); ob_start(); $verbose = fopen('php://temp', 'w+'); curl_setopt($ch, CURLOPT_VERBOSE, true); curl_setopt($ch, CURLOPT_STDERR, $verbose); try { $result = curl_exec($ch); $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); if (isset($claims["userCodiceFiscale"])) { if($httpCode < 400 && in_array($apiEndpoint, ["pianificazioneAppalto", "comunicaAppalto", "comunicaPostPubblicazione"], true)) { $delegaAttuale = $this->getFullDelegaInfo($claims["userCodiceFiscale"]); // Se non c'è delega, possiamo considerarlo il nuovo RP essendo il primo submit if(empty($delegaAttuale) || empty($delegaAttuale["role"])) { $role = DelegheRoles::RP; } else { $role = $delegaAttuale["role"]; } // Aggiorniamo la delega self::setUserRoleToDB($claims["userCodiceFiscale"], $role, $this->fascicolo["codice"], $claims["SACodiceFiscale"], $claims["SAcodiceAUSA"]); } } // Detect 401 $is401 = false; if ($retry401 < Retries401::MAX) { $is401 = empty($result) || ($httpCode == 401 && is_string($result) && str_contains($result, "SEC05")) || ($httpCode == 400 && is_string($result) && str_contains($result, "AUT01")); } rewind($verbose); $verbose = stream_get_contents($verbose); // Workaround per case del codice fiscale if ($is401) { // Riproviamo if ($saveLog) { $this->appendLog( $apiEndpoint, $method, [ "request" => $verbose, "payload" => $payload ?? null, "result" => $result ?? null, "attempt" => $retry401, ] ); } return $this->apiCall($apiEndpoint, $method, $body, $claims, $header, $message, $retry401 + 1); } // Estriamo il transaction ID if (!empty($this->scheda["codice"])) { $pattern = '/(GovWay-Transaction-ID:)\s(.+\n)/'; preg_match($pattern, $verbose, $matches); $transactionId = $matches[2] ?? ""; if (!empty($transactionId)) { $infoScheda = new stdClass(); if (!empty($this->scheda["info"])) $infoScheda = unserialize($this->scheda["info"]); $infoScheda->lastTransactionID = $transactionId; $this->updateScheda(["info" => serialize($infoScheda)]); } } if ($saveLog) { $this->appendLog( $apiEndpoint, $method, [ "request" => $verbose, "payload" => $payload ?? null, "result" => $result ?? null, ] ); } curl_close($ch); return $result; } catch (\Exception $e) { $this->appendLog( $apiEndpoint, $method, [ "request" => $verbose, "payload" => $payload ?? null, "result" => $result ?? null, ], $e->getMessage(), ); return $e->getMessage(); } } /** * Ottiene il permesso per una chiamata API * * @param string|null $apiEndpoint * @param string|null $method * @param boolean|null $dynamicPermission * @return array */ public function getPermissionFor(?string $apiEndpoint, ?string $method, ?bool &$dynamicPermission = null): array { $permission = null; if ($apiEndpoint !== null && $method !== null) { // Verifichiamo i permessi se necessario $endpointConfig = $this->api_config[$apiEndpoint] ?? null; $methodConfig = $endpointConfig["methods"][$method] ?? null; // Verifichiamo i permessi, prende priorità quello del metodo $permission = $methodConfig["permission"] ?? null; // Il permesso dinamico è in base alla tipologia della scheda $dynamicPermission = $methodConfig["dynamic_permission"] ?? false; // Proviamo altrimenti a prendere i permessi generici della categoria if ($permission === null) { $permission = $endpointConfig["permission"] ?? null; $dynamicPermission = $endpointConfig["dynamic_permission"] ?? false; } } // Permessi di default if ($permission === null) { $permission = DelegheRoles::ANY; $dynamicPermission = false; } return $permission; } /** * Verifica che l'utente sia abilitato ad effettuare una richiesta * * @param array|null $reason Viene popolato con la motivazione per un eventuale diniego * @param string|null $apiEndpoint API Endpoint da contattare (opzionale) * @param string|null $method Metodo da contattare (opzionale) * @return boolean */ public function canDoRequest(?array &$reason = null, ?string $apiEndpoint = null, ?string $method = null) { if ($apiEndpoint === "ausaController" && $method === "getBy") { return true; } // Siamo un cronjob o simile $canDoRequest = false; if (defined("__CMDS_JOB_EXEC") && !empty(__CMDS_JOB_EXEC)) { return true; } // Siamo un utente $utente = $_SESSION["utente"] ?? null; // Metodo per evitare nesting eccessivo do { // Verifichiamo il livello di autenticazione if (!$utente || $utente->authenticationLevel < 2) { $reason = ["code" => "AUTH"]; break; } // Sanity check sul RUP if ($method !== "presa-carico") { if (isset($this->oggetto) && method_exists($this->oggetto, "getRup")) { $rp_cf = trim(strtoupper($this->getUserFromRole(DelegheRoles::RP))); $rup_cf = trim(strtoupper($this->oggetto->getRup()["codice_fiscale"] ?? "")); if ($rp_cf !== $rup_cf) { $reason = ["code" => "RUP_RP_MISMATCH"]; break; } } } // Otteniamo le info utente $userInfo = method_exists($utente, "getInfo") ? $utente->getInfo() : []; // Prendiamo il CF dell'utente $cf = $userInfo["cf"] ?? null; if (!$cf) { $reason = ["code" => "USER"]; break; } $permission = $this->getPermissionFor($apiEndpoint, $method, $dynamicPermission); if ($apiEndpoint !== null && $method !== null) { // Verifichiamo i permessi se necessario $endpointConfig = $this->api_config[$apiEndpoint] ?? null; $methodConfig = $endpointConfig["methods"][$method] ?? null; } // NO_AUTH non fa verifiche if ($permission !== null && $permission !== DelegheRoles::NO_AUTH) { // Otteniamo il permesso dell'utente $userRole = $this->getUserRole($cf, $permission, RoleSyncBehaviour::NEVER_SYNC); // Permesso negato if ($userRole === null || !in_array($userRole, $permission)) { $permissionText = implode(", ", $permission); $txtRole = $userRole ?? "Nessuno"; if ($apiEndpoint !== null && $methodConfig !== null) { $methodFriendlyName = ucfirst(str_replace("-", " ", $methodConfig["path"] ?? "")); $apiFriendlyName = ucfirst(camelCaseToWords($apiEndpoint)); $msg_pre = "Per effettuare l'operazione {$methodFriendlyName} su {$apiFriendlyName}"; } else { $msg_pre = "Per usare questo modulo"; } $reason = [ "code" => "PERMISSION_DENIED", "msg" => "{$msg_pre} ti serve uno dei seguenti ruoli: {$permissionText}. Ruolo rilevato: {$txtRole}" ]; return false; // Il messaggio viene popolato qui } // Se il permesso è dinamico, dobbiamo verificare la tipologia scheda if ($dynamicPermission && in_array($userRole, DelegheRoles::DRPs)) { // TODO Implementare verifica tipo scheda } } // Tutti i controlli passati $canDoRequest = true; } while (false); if (!empty($reason)) { $reason["msg"] = $this->managePermissionMessage($reason["code"]); } return $canDoRequest; } /** * managePermissionMessage * Mostra un messaggio amichevole relativo all'eventuale errore di autorizzazione * * @param mixed $code * @return void */ public function managePermissionMessage(String $code) { $return = "Errore sconosciuto"; if ($code == "RUP_RP_MISMATCH") { $rp_cf = $this->getUserFromRole(DelegheRoles::RP); if (isset($this->oggetto) && method_exists($this->oggetto, "getRup")) { $rup_cf = $this->oggetto->getRup()["codice_fiscale"] ?? ""; } else { $rup_cf = ""; } $return = "Il codice fiscale del Responsabile Unico di Procedura (RUP) {$rup_cf} (impostato sul pannello Incarichi) non corrisponde al codice fiscale del Responsabile Procedura (RP) {$rp_cf} (impostato sui Permessi ANAC). {{STEP}}
- Se invece si tratta di un errore, assicurarsi di impostare il Codice Fiscale corretto sul pannello Incarichi.

Il modulo ANAC resterà disattivo fino alla risoluzione dell'incongruenza."; if ($this->canClearDossier()) { $step = <<- Se si intende cambiare RP, sarà necessario cliccare su Aggiorna RP per riallineare RUP e RP. ERR; } else { $step = <<- Se si intende cambiare RP, il nuovo incaricato deve cliccare su Permessi ANAC > Prendi carico per diventare il nuovo Responsabile Procedura. ERR; } $return = str_replace("{{STEP}}", $step, $return); } else if ($code == "USER") { $return = "Codice fiscale dell'utente autenticato mancante, aggiorna il tuo profilo"; } else if ($code == "AUTH") { $return = 'Livello di autenticazione non sufficiente'; if (!empty($_SESSION["utente"]->authenticationLevel) && $_SESSION["utente"]->authenticationLevel >= 1) { if ($_SESSION["ente"]->certificazioneManualeUtenti()) { if ($_SESSION["utente"]->getInfo()["certificato"] == "S") { $return .= " - Abilita l'autenticazione a due fattori per procedere"; } else { $return .= " - Rivolgiti ad un amministratore per certificare l'utenza o accedi tramite SPID o CIE per procedere"; } } else { $return .= " - Accedi tramite SPID o CIE per procedere"; } } else { $return .= " - Accedi tramite SPID o CIE per procedere"; } } return $return; } /** * Shorthand per getServiceStatus che ritorna una booleana * * @param boolean $forceRefresh * @return bool True se tutti i servizi sono funzionali */ public function getServiceStatusBool(bool $forceRefresh = false) { return $this->getServiceStatus($forceRefresh)["title"] === "OK"; } /** * Questo metodo verifica che i server anac siano live * * @param boolean $forceRefresh Se impostato viene forzatamente effettuata una nuova ricerca, altrimenti viene chiesto ogni 10 minuti * @return array Risposta del server o della cache */ public function getServiceStatus(bool $forceRefresh = false) { global $pdo; //return ["title" => "KO", "updated_at" => "1970-01-01", "created_at" => "1970-01-01"]; $tabella = "b_stato_servizio_npa"; // Peschiamo l'ultimo status $lastStatus = $pdo->go("SELECT * FROM {$tabella} ORDER BY codice DESC LIMIT 1")->fetch(PDO::FETCH_ASSOC); if (empty($lastStatus)) { // Fallback $lastStatus = [ "status" => 999, "title" => "KO", "detail" => "KO", "created_at" => "1970-01-01", "updated_at" => "1970-01-01" ]; } // Per il cron forniamo sempre l'ultima risposta $isCron = defined("__CMDS_JOB_EXEC") && !empty(__CMDS_JOB_EXEC); if ($isCron) { $lastStatus["internal"] = "CRON"; return $lastStatus; } // Verifichiamo se dobbiamo forzare un refresh da API $shouldRefresh = $forceRefresh; if (!$shouldRefresh) { // Calcoliamo il tempo passato $timeDiff = time() - strtotime($lastStatus["updated_at"]); // Ricarichiamo ogni 10 minuti se l'ultimo stato era OK oppure ogni minuto se non lo era $shouldRefresh = $lastStatus["title"] === "OK" ? $timeDiff > 600 : $timeDiff > 60; } // Se non dobbiamo ricaricare, ritorniamo l'ultimo status if (!$shouldRefresh) { unset($lastStatus["codice"]); return $lastStatus; } // Se l'utente non è autorizzato evitiamo di effettuare la richiesta if (!$this->canDoRequest($cantRequest)) { $lastStatus["internal"] = "NO_PERMISSION"; return $lastStatus; } // Adesso passiamo alla richiesta $serviziComuni = $this->api_config["serviziComuni"]; // Se non abbiamo le impostazioni ritorniamo l'ultimo status if (empty($serviziComuni)) { $lastStatus["internal"] = "NO_SETTINGS"; return $lastStatus; } // Se l'utente non è autorizzato evitiamo di effettuare la richiesta if (!$this->canDoRequest($cantRequest, "serviziComuni", "serviceStatus")) { $lastStatus["internal"] = "NO_PERMISSION"; return $lastStatus; } $claims = $this->getAnacJwsCustomClaims($this->api_config["serviziComuni"]["endpoint"]); $result_str = $this->apiCall( "serviziComuni", "serviceStatus", [], $claims ); // Verifichiamo la richiesta if (empty($result_str) || !valid_json($result_str, $newStatus, TRUE)) { // Questo workaround si assicura che una risposta di diniego per autorizzazione // mancante non venga considerata un outage. if (str_contains($result_str, "AuthorizationContentDeny")) { $lastStatus["internal"] = "UNAUTHORIZED"; return $lastStatus; } // Se è invalida, consideriamolo un outage $newStatus = [ "status" => "999", "title" => "KO", "detail" => "Il Server ANAC non ha risposto correttamente", ]; }; // Ora che abbiamo un nuovo status $save = new salva(); $save->nome_tabella = $tabella; $save->operazione = "INSERT"; $save->expect = ["status", "title", "detail", "created_at", "updated_at"]; $save->debug = FALSE; if ($newStatus["title"] === $lastStatus["title"]) { // Se il nuovo stato è analogo a quello precedente ci limitiamo ad aggiornare updated_at $save->operazione = "UPDATE"; $save->expect[] = "codice"; $lastStatus["updated_at"] = date("Y-m-d H:i:s"); $save->oggetto = $lastStatus; $newStatus = $lastStatus; } else { // Altrimenti inseriamo il nuovo status $newStatus["created_at"] = date("Y-m-d H:i:s"); $newStatus["updated_at"] = date("Y-m-d H:i:s"); $save->oggetto = $newStatus; } $save->save(); unset($newStatus["codice"]); return $newStatus; } public static function manageANACResponse(string $result_str, ?string &$message = null): ?object { if (empty($result_str)) { $message = <<I server ANAC hanno fornito una risposta vuota.

Possibili cause:

  • Il Codice Fiscale dell'utenza in uso non è associato ad un profilo AUSA abilitato per questa procedura.
  • Alternativamente potrebbe indicare un problema tecnico dei server ANAC.

ERR; return null; } if (!valid_json($result_str, $result)) { $message = __("I server ANAC hanno fornito una risposta in un formato JSON non valido. Riprova più tardi."); return null; } return $result; } } /** * Comportamenti di richiesta della delega */ class Retries401 { const NORMAL = 0; const UPPERCASE = 1; const LOWERCASE = 2; const MAX = self::LOWERCASE; }