completed * * @var array */ public $completed_subsections = []; /** * Quando è true, vengono nascoste le label interne delle sezioni * * @var boolean */ public $hide_labels = true; /** * Rappresenta un override temporaneo del notice_id * * @var string|null */ private $override_notice_id = null; /** * Rappresenta la versione attualmente caricata del SDK eForms2 * * @var string */ private $loaded_version; /** * Rappresenta la versione di default dell'SDK da caricare */ const SDK_VERSION = "1.9.1"; /** * Costante usata per indicare al costruttore che va bene qualunque tipo di notice al caricamento */ const ANY_NOTICE_TYPE = "ANY"; /** * Rappresenta il path di default per l'SDK eForms */ const STATIC_PATH = __DIR__ . DIRECTORY_SEPARATOR . ".." . DIRECTORY_SEPARATOR . "eforms" . DIRECTORY_SEPARATOR . self::SDK_VERSION; /** * Viene aggregato ai valori di tempo o date per aggiungere il fuso orario TODO: In futuro andrebbe integrato meglio */ protected const TIMEZONE_SUFFIX = "+02:00"; /** * Questo array contrassegna le sezioni escluse dalla regola di auto not-mandatory quando una sezione è vuota */ const EXCLUDED_FROM_AUTO_NOT_MANDATORY_SECTIONS = [ "GR-Part" => true, "GR-Lot" => true, "GR-Organisations" => true, "GR-Procedure-TenderingTerms" => true, "GR-LotResult-Contract-Id-Ref" => true ]; /** * Questo array serve a riconosce i campi per la pubblicazione successiva */ const UNPUBLISH_FIELDS = [ "BT-195" => true, "BT-196" => true, "BT-197" => true, "BT-198" => true, ]; /** * Riferimenti degli schema per ricavare i nomi * * @var array */ private const SCHEMA_REFERENCES = [ "LOT" => "ND-Lot", "GLO" => "ND-LotsGroup", "ORG" => "ND-Organization", "TPO" => "ND-Touchpoint", "UBO" => "ND-UBO", "RES" => "ND-LotResult", "TEN" => "ND-LotTender", "CON" => "ND-SettledContract", "TPO" => "ND-Touchpoint", "PAR" => "ND-Part", ]; /** * Questo array collega uno schema ID ad una sezione, per poter reindirizzare correttamente l'utente */ const SCHEMA_ID_TO_SECTION = [ "ORG" => [ "label" => "Organizzazioni", "section" => "GR-Organisations-Section", ], "TPO" => [ "label" => "Organizzazioni", "section" => "GR-Organisations-Section", ], "UBO" => [ "label" => "Organizzazioni", "section" => "GR-Organisations-Section", ], "LOT" => [ "label" => "Lotti", "section" => "GR-Lot", ], "PAR" => [ "label" => "Parti", "section" => "GR-Part", ], "GLO" => [ "label" => "Gruppi di Lotti", "section" => "GR-LotsGroup", ], "RES" => [ "label" => "Risultati", "section" => "GR-Result", ], "TPA" => [ "label" => "Risultati", "section" => "GR-Result", ], "TEN" => [ "label" => "Risultati", "section" => "GR-Result", ], "CON" => [ "label" => "Risultati", "section" => "GR-Result", ], ]; // Dizionari di workaround /** * Quando EFX è off, mandatory viene sovrascritto con queste regole */ const EFX_OFF_DEFAULT_MANDATORY = [ "BT-536" => false, "BT-5141" => false, "BT-5421" => false, "OPP-050" => false, ]; /** * Quando un constraint fallisce, queste regole vengono valutate per FORBIDDEN */ const CONSTRAINT_FAILED_DEFAULT_FORBIDDEN = [ "BT-5421" => true, "BT-5422" => true, "BT-5423" => true, "BT-5141" => true, ]; /** * Quando un constraint fallisce, queste regole vengono valutate per MANDATORY */ const CONSTRAINT_FAILED_DEFAULT_MANDATORY = []; /** * Queste regole vengono sempre applicate per rosvrascrivere MANDATORY */ const MANDATORY_OVERRIDE = []; /** * Connessione al database * * @var \myPDO */ private $pdo; /** * Imposta la modalità di operazione attuale. Serve per definire alcuni comportamenti speciali * * @param string $mode * @return void */ public function setMode(string $mode) { $this->mode = $mode; } /** * Aggiorna il query provider per EFX quando viene aggiornata la strutture XML * * @return void */ protected function onUpdateXml(): void { $this->efx_query_provider = new XML_XPath20_Querier($this->xml, true, true); } /** * Popola il dizionario di eForms2 per le traduzioni * * @return Array|Null */ static private function getTranslator(): ?array { $path = static::STATIC_PATH; $language = strtolower($_SESSION["language"] ?? "it"); if (!file_exists("{$path}/translations/{$language}.json")) { $vocabulary = []; $files = glob("{$path}/translations/*_{$language}.xml"); foreach ($files as $file) { $xml = simplexml_load_file($file); foreach ($xml->entry as $translation) { $vocabulary[(string) $translation->attributes()["key"]] = (string) $translation; } $xml = null; gc_collect_cycles(); } file_put_contents("{$path}/translations/{$language}.json", json_encode($vocabulary, JSON_PRETTY_PRINT)); } return json_decode(file_get_contents("{$path}/translations/{$language}.json"), TRUE); } /** * Ritorna uno scheletro di select contenente la lista di notice types disponibili * * @return array|null */ static public function listAvailableNoticeForms(): ?array { $path = static::STATIC_PATH; if (file_exists("{$path}/notice-types/notice-types.json")) { $notices = file_get_contents("{$path}/notice-types/notice-types.json"); $settings = json_decode(file_get_contents("{$path}/../../config/eforms/settings.json"), true); if (is_json($notices)) { $select = [ 'name' => 'form', 'title' => "Tipologia di avviso", 'attrs' => [ 'rel' => 'S;12;12;A' ], 'options' => [] ]; $notices = json_decode($notices, TRUE); $vocabulary = self::getTranslator(); foreach ($notices["noticeSubTypes"] as $value) { if (DEVELOP_ENV || empty($settings["forms"]) || in_array($value["subTypeId"], $settings["forms"])) { $select['options'][$value["subTypeId"]] = "{$value["subTypeId"]}. {$vocabulary[$value["_label"]]}"; } } return $select; } } return null; } /** * Carica/salva le impostazioni dell'editor passate dal'utente * * @return void */ protected function handleUserSessionSettings() { // ENABLE EFX $enable_efx = $_SESSION["ted_enable_efx"] ?? true; if (isset($_GET["efx_mode"])) { if ($_GET["efx_mode"] !== "0") { $enable_efx = true; } else { $enable_efx = false; } $_SESSION["ted_enable_efx"] = $enable_efx; } $this->enable_efx_evaluation = $enable_efx; // HIDE LABELS $hide_labels = $_SESSION["ted_hide_labels"] ?? true; if (isset($_GET["hide_labels"])) { if ($_GET["hide_labels"] !== "0") { $hide_labels = true; } else { $hide_labels = false; } $_SESSION["ted_hide_labels"] = $hide_labels; } $this->hide_labels = $hide_labels; if(defined("__CMDS_JOB_EXEC")) { $canAccessDebug = false; } else { $canAccessDebug = ($_SESSION["utente"] ?? false) && $_SESSION["utente"]->isSupportoOrRoot(); } // ADVANCED DEBUG if ($canAccessDebug) { $advanced_debug = $_SESSION["ted_advanced_debug"] ?? false; if (isset($_GET["advanced_debug"])) { if ($_GET["advanced_debug"] !== "0") { $advanced_debug = true; } else { $advanced_debug = false; } $_SESSION["ted_advanced_debug"] = $advanced_debug; } $this->advanced_debug = $advanced_debug; $forced_suggestion = $_SESSION["ted_forced_suggestion"] ?? true; if (isset($_GET["forced_suggestion"])) { if ($_GET["forced_suggestion"] !== "1") { $forced_suggestion = false; } else { $forced_suggestion = true; } $_SESSION["ted_forced_suggestion"] = $forced_suggestion; } $this->enable_forced_suggestions = $forced_suggestion; // Nelle variazioni non possiamo avere suggerimenti forzati onde evitare conflitti if($this->rectification) { $this->enable_forced_suggestions = false; } $_SESSION["ted_forced_suggestion"] = $this->enable_forced_suggestions ? "1" : "0"; } } /** * Istanzia la classe partendo da un notice type e l'id della notice * * @param String $notice * @param integer $codice * @param NuovaPiattaformaAppalti|null $npa */ public function __construct(String $notice, Int $codice = 0, ?NuovaPiattaformaAppalti $npa = null) { // Prevent directory trasversal from notice $notice = str_replace(".", "", $notice); $notice = str_replace("/", "", $notice); global $root, $pdo; $this->notice = $notice; $this->language = strtolower($_SESSION["language"] ?? "it"); $this->version = $this->__get("version"); $this->pdo = $pdo; $this->init($codice); if($npa !== null) { $this->npa = $npa; } else { // Recuperiamo la scheda ANAC collegata $schedaAnacCollegata = NuovaPiattaformaAppalti::fetchSchede( ["s.codice", "s.codice_npa"], ["codice_scheda_guue" => $this->info["codice"]] ,true, false ); if(!empty($schedaAnacCollegata)) { $this->npa = NuovaPiattaformaAppalti::init($schedaAnacCollegata["codice_npa"], $schedaAnacCollegata["codice"], null, null, null, NuovaPiattaformaAppalti::AUTOMATICALLY_GET_SDK_VERSION, $this); } } } /** * Set version value * * @param mixed $name * @param mixed $value * @return void */ public function __set($name, $value) { // TODO: Picchierò chiunque decida di usare __get e __set in un futuro progetto. // Per delle proprietà dinamiche è meglio avere dei getter e setter espliciti. // - Luca Corigliano if ($name == "version") { $path = dirname(__DIR__) . DIRECTORY_SEPARATOR . "eforms" . DIRECTORY_SEPARATOR . $value; if (file_exists($path)) { $this->version = $value; } else { $this->version = self::SDK_VERSION; } $this->initializeSDK(); } } /** * Obtain version varible * * @return mixed */ public function __get(String $name) { if ($name == "version") { if (empty($this->version)) { $this->version = self::SDK_VERSION; } return $this->version; } } /** * Check if all sections have been completed * * @return bool */ public function ready(): bool { return $this->completed(); } /** * Check if once this section is completed, the form will be ready for submission. * * @param string $section * @param string $subsection * @return bool */ public function willFormBeReadyAfterSectionCompletion(String $section, String $subsection = null): Bool { // Se è già tutto completo direi proprio di sì if ($this->completed()) return true; foreach ($this->completed_subsections as $key => $value) { // Ripetibile if (is_array($value)) { foreach ($value as $iteration => $complete) { // Ci sono altre sezioni non complete if (!$value && $key !== $section) return false; } } // Non ripetibile else if (!$value && $key !== $section) return false; } return true; } // Dizionari alternativi // Nota: // Questi dizionari vanno mossi in dei JSON legati alla versione SDK // Questo sovrascrive a priori public const OVERRIDE_VOCABULARY = [ "rule|text|SA-OPP-030-T02-ALL-REV-TIC" => "Il campo non può riferirsi alla distribuzione di ricavi", ]; // Questo sovrascrive qualora non venisse trovata la voce public const FALLBACK_VOCABULARY = [ "group|name|ND-LotAwardWeightCriterionParameter" => "Parametro Pesato", "group|name|ND-LotAwardFixedCriterionParameter" => "Parametro Fisso", "group|name|ND-LotAwardThresholdCriterionParameter" => "Parametro di Soglia", "group|name|ND-LotsGroupAwardWeightCriterionParameter" => "Parametro Pesato", "group|name|ND-LotsGroupAwardFixedCriterionParameter" => "Parametro Fisso", "group|name|ND-LotsGroupAwardThresholdCriterionParameter" => "Parametro di Soglia", "group|name|ND-LotContractAdditionalNature" => "Natura Aggiuntiva", "group|name|ND-LotEnvironmentalImpactType" => "Riduzione impatto ambientale", "group|name|ND-LotSocialObjectiveType" => "Obiettivo sociale", "group|name|ND-LotInnovativeAcquisitionType" => "Obiettivo innovativo", "group|name|ND-SecondStageCriterionParameter" => "Criteri di secondo stadio", "group|name|ND-ChangedSection" => "Sezione cambiata" ]; /** * Traduce una voce relativa ad eForms 2 ed applica correzioni per l'usabilità * * @param String $key * @return String|null */ public function translate(String $key): ?String { $retval = $key; // Check override dictionary if (!empty(self::OVERRIDE_VOCABULARY[$key])) { $retval = self::OVERRIDE_VOCABULARY[$key]; } // Check eForms vocabulary elseif (!empty($this->eforms_translations[$key])) { $retval = $this->eforms_translations[$key]; } // Check fallback dictionary elseif (!empty(self::FALLBACK_VOCABULARY[$key])) { $retval = self::FALLBACK_VOCABULARY[$key]; } else { if ($this->debug) { return "Traduzione mancante: \"{$key}\""; } return null; } // Handle (d) (t) correctly if (str_contains($key, "(t)")) { $retval .= " (Orario)"; } // Handle (a) (b) (c) correctly elseif (str_contains($key, "BT-510(a)")) { $retval .= " (Linea 1)"; } elseif (str_contains($key, "BT-510(b)")) { $retval .= " (Linea 2)"; } elseif (str_contains($key, "BT-510(c)")) { $retval .= " (Linea 3)"; } return $retval; } /** * Genera la struttura intera del form, includendo tutto ciò che viene specificato in $all_sections. * * @param boolean $metadata Se false, non vengono ritornati i metadati per il rendering * @param array $all_sections Se specificato la creazione si limita alle sezioni specificate (incluse ripetizioni). Altrimenti genera tutto * @return array */ public function createFullFormStructure(Bool $metadata = true, array $all_sections = []): array { $this->generateXMLNotice('utf-8', false); if (empty($all_sections)) { $all_sections = $this->sections; } $data_source = $this->info["valori"]["eforms2"] ?? []; $retVal = []; foreach ($all_sections as $section) { $data_source_for_section = $data_source[$section["id"]]["values"] ?? []; $integration_values_for_section = $data_source[$section["id"]]["integration_values"] ?? []; $section_elements = $this->fields["content"][$section["index"]]; $this->current_section = $section_elements; $this->current_section_id = $section["id"]; if ($section["_repeatable"] ?? false) { foreach ($data_source_for_section as $iteration_index => $iteration_data_source) { $integration_values = $integration_values_for_section[$iteration_index] ?? []; $iteration_structure = $this->createSection($section_elements, $metadata, [], $iteration_data_source, $integration_values, (int) $iteration_index); foreach ($iteration_structure[$section["id"]] as $key => $value) { $retVal[$section["id"]][$iteration_index][$key] = $value; } } } else { $section_structure = $this->createSection($section_elements, $metadata, [], $data_source_for_section, $integration_values_for_section, 0); foreach ($section_structure[$section["id"]] as $key => $value) { $retVal[$section["id"]][$key] = $value; } } } return $retVal; } /** * createFormStructure Crea la struttura di un form partendo da una sezione e l'eventuale indice di sottosezione. * * @param array $section Sezione da generare * @param int $subsection_index Indice di sottosezione * @param bool $metadata Se false, non vengono ritornati i metadati per il rendering * @param ?array $full_structure Se passato, viene popolato con la struttura intera generata includendo eventuali elementi generati per coerenza EFX * @return array Struttura del form (modello concettuale) */ public function createFormStructure(array $section, Int $subsection_index, Bool $metadata = true, ?array &$full_structure = null): array { // Generiamo la struttura interna a partire dai dati // Ottenendo il modello concettuale // È importante sia intera per ottenere gli indici di ogni elemento ripetibile // Generazione intera if (false) { $full_structure = $this->createFullFormStructure($metadata); // Generazione di tutti i ripetibili di sezione (più veloce, forse meno precisa) } else { $full_structure = $this->createFullFormStructure($metadata, [$section]); } $this->current_subsection = $subsection_index; // Verifichiamo che sia stata creata la sezione, potrebbe non esserlo // qualora non sia ancora popolato il JSON sul Database $section_structure = $full_structure[$section["id"]] ?? []; if ($section["_repeatable"] ?? false) { $section_structure = $section_structure[$subsection_index] ?? []; } // Se non c'è una stuttura, ne creiamo uno scheletro basato puramente sui metadati if (empty($section_structure)) { $elements = $this->fields["content"][$section["index"]]; if ($elements["id"] == $section["id"]) { $this->current_section = $elements; $this->current_section_id = $section["id"]; $section_structure = $this->createSection($elements, $metadata, [], [], [], $subsection_index); } } return $section_structure; } /** * Crea il modello concettuale/struttura di una singola sezione * * @param Array $section La sezione da processare * @param boolean $metadata Se false, non vengono ritornati i metadati per il rendering * @param array $prefix Indica la gerarchia attuale della sezione. Viene popolato durante le iterazioni. * @param array $values Indica i valori attuali da usare per riempire i fields * @param array $integration_values Indica i valori forzati passati dalle integrazioni * @param integer $index L'indice attuale di ripetizione/iterazione * @param boolean $repetition Indica se il rendering attuale è una ripetizione/iterazione * @param boolean $section_forbidden Quando diventa true, l'itera sezione sarà forbidden * @param boolean $section_not_mandatory Se null, viene controllato che la sezione ripetibile abbia valori. Quando diventa true decide se tutta la sezione non è mandatory. Quando diventa falsa invece previene ulteriori check in profondità * @return Array Struttura della sezione (modello concettuale) */ public function createSection(array $section, Bool $metadata = true, array $prefix = [], array $values = [], array $integration_values = [], Int $index = 0, Bool $repetition = false, Bool $section_forbidden = false, $section_not_mandatory = null): array { // Qui vengono salvati i valori di di ritorno $retval = []; // Rintracciamo i valori relativi a questa sezione e teniamo conto della profondità $depth = 0; foreach ($prefix as $key) { $depth++; if (!empty($values[$key])) $values = $values[$key]; if (!empty($integration_values[$key])) $integration_values = $integration_values[$key]; } switch ($section["contentType"] ?? null) { // Gruppi di campi case 'group': if (!empty($section["content"])) { $repetitions = 1; // Regole specifiche per sezioni ripetibili if ($section["_repeatable"] ?? false) { // Se l'identifier di una ripetibile è forbidden, vuol dire che è ripetibile anche tutta la sua sezione if (isset($section["_identifierFieldId"])) { $identifierField = $this->fields_metadata[$section["_identifierFieldId"]] ?? null; if ($identifierField !== null) { $section_forbidden_property = $this->checkFieldProperty($identifierField, "forbidden", 0); if ($section_forbidden_property["value"] === true) { $section_forbidden = true; } } } // Teniamo conto della quantità di ripetizioni $groups = $this->get($values, $section["id"]); if (!empty($groups)) { $repetitions = count($groups); } } // Id della sezione ("GR-Ecc") $id = $section["id"]; // Per ogni ripetizione for ($i = 0; $i < $repetitions; $i++) { // Se una sezione è vuota, togliamo i vincoli di obbligatorietà dall'utente in modo da // non obbligarlo a compilare una sezione che non serve if(self::EXCLUDED_FROM_AUTO_NOT_MANDATORY_SECTIONS[$section["id"]] ?? false) { $section_not_mandatory = false; } if ($section_not_mandatory === null && ($section["_repeatable"] ?? false)) { $section_not_mandatory = $this->countValues($values, $section["id"], $i) <= 0; } // New section data è un puntatore a questa sezione all'interno del valore di ritorno. // Viene implementato in questo modo per poter gestire sveltamente le ripetizioni $new_section_data = &$retval[$id]; $new_prefix = $prefix; // Creazione del nuovo prefisso if (empty($prefix)) { // Root, non può essere ripetibile (e non deve.) $new_prefix[] = $id; } else { $new_prefix[] = $id; // Se è ripetibile, puntiamo new_section_data all'iterazione corrente if (($section["_repeatable"] ?? false)) { $new_section_data = &$new_section_data[$i]; // Eccezione per add2guue, forziamo la ripetizione attuale if ($repetition) { $i = $index; } // Aggiungiamo l'id della ripetizione al prefisso $new_prefix[] = $i; } } // Per tutti i contenuti di questa sezione, ci chiamiamo ricorsivamente foreach ($section["content"] as $content) { foreach ($this->createSection($content, $metadata, $new_prefix, $values, $integration_values, $i, false, $section_forbidden, $section_not_mandatory) as $key => $val) { $new_section_data[$key] = $val; } } // Ai fini del codice di rendering, ci salviamo anche dei metadati di sezione if ($metadata) { $new_section_data["__meta"]["display_type"] = $section["displayType"]; $new_section_data["__meta"]["repeatable"] = $section["_repeatable"] ?? false; $new_section_data["__meta"]["label"] = $section["_label"] ?? ""; $new_section_data["__meta"]["id"] = $section["id"] ?? ""; $new_section_data["__meta"]["unique_identifier"] = implode("-", $new_prefix); $new_section_data["__meta"]["iteration"] = $i; $new_section_data["__meta"]["total_iterations"] = $repetitions; $new_section_data["__meta"]["section_object"] = $section; $new_section_data["__meta"]["prefix"] = $prefix; $new_section_data["__meta"]["hasAnyValue"] = $this->hasAnyValue($values, $section["id"], $i); $new_section_data["__meta"]["preventDelete"] = $this->countForcedIntegrationValues($integration_values, $section["id"], $i) > 0; $identifierFieldID = $section["_identifierFieldId"] ?? null; $captionFieldID = $section["_captionFieldId"] ?? null; $tabnameProvider = []; if ($captionFieldID) { $tabnameProvider[] = "#" . $captionFieldID; } if ($identifierFieldID) { $tabnameProvider[] = "#" . $identifierFieldID; } if (!empty($tabnameProvider)) { $new_section_data["__meta"]["tabNameProvider"] = implode("|", $tabnameProvider); } } } } break; // Campi case 'field': // Prendiamo le info di questo field da fields.json $field = $this->fields_metadata[$section["id"]]; // Andiamo a salvare (ed ottenere) l'indice attuale del campo. // Questo è utile qualora lo stesso campo si ripeta più volte per poi associarlo ai suoi risultati EFX if (!isset($this->field_counters[$section['id']])) $this->field_counters[$section['id']] = -1; $current_index = ++$this->field_counters[$section['id']]; // Proprietà da verificare // TODO Nota: // Repeatable non è considerato in quanto viene usato solo da "BT-702(b)-notice", che è la lingua della notice, che è sempre una per noi. // In futuro sarà possile che sia necessario implementarlo. // Assert invece viene considerato in un altro punto del codice $properties = [ "forbidden", "mandatory", ]; if ($this->advanced_debug) { // Teniamo traccia di info utili per il debug $has_efx = []; $error_state = []; } // Per ogni proprietà da considerare foreach ($properties as $property) { $$property = ["value" => false, "severity" => ""]; // Questo assegnerà i valori di default alle variabili $forbidden, $mandatory // Se l'intera sezione è forbidden, siamo forbidden a priori if ($section_forbidden === true && $property === "forbidden") { $$property["value"] = true; continue; // Le altre proprietà sono irrilevanti se siamo forbidden } // Se l'intera sezione è non-obbligatoria, inutile controllare if ($section_not_mandatory === true && $property === "mandatory") { $$property["value"] = false; continue; } if (!empty($field[$property])) { // Controlliamo i campi, considerando anche i constraint $$property = $this->checkFieldProperty($field, $property, $current_index); // In debug lanciamo un eccezione se qualcosa va storto if ($this->debug) { if ($$property["value"] === null) { throw new Exception("Invalid value for {$property}"); } } // Salva valori di debug if ($this->advanced_debug) { if ($$property["has_efx"] === true) { $has_efx["any"] = true; $has_efx[$property] = true; } if ($$property["error_state"] === true) { $error_state["any"] = true; $error_state[$property] = true; } } } } // WORKAROUND: Se fallisce EFX per forbidden in mancanza di contesto, faccio override manuale su false e // rendo non obbligatorio if ($forbidden["error_state"] ?? false) { $forbidden["value"] = false; $mandatory["value"] = false; } // Tiriamo fuori il valore di questo field $retreived_value = $this->retrieveValue($values, $section["id"]); // Override per input, scaricati alla fine del metodo $input_overrides = []; if($this->info["stato"] === "BOZZA") { if (($integration_value = ($integration_values[$section["id"]] ?? false))) { if ($this->enable_forced_suggestions && $integration_value["forced"]) { $retreived_value = $integration_value["value"]; $input_overrides["attrs"]["readonly"] = 1; $input_overrides["title_before"] = "🪄 " ; } else { if (empty($retreived_value)) { $input_overrides["title_before"] = "💡 "; $retreived_value = $integration_value["value"]; } if(!is_array($integration_value["value"])) { $input_overrides["attrs"]["suggested_val"] = "{$integration_value["value"]}"; } } } } // Determiniamo che il valore sia esistente $has_retreived_value = !empty($retreived_value); if ($has_retreived_value && isset($retreived_value["value"])) { $has_retreived_value = !empty($retreived_value["value"]); } // Gestione di un campo che ha la possibilità di avere un Unpublished if (!empty($field["privacy"])) { // Otteniamo il nome del primo dei campi unpublished // es. BT-195(BT-156)-NoticeResult $privacyProviderFieldId = $field["privacy"]["unpublishedFieldId"]; // Otteniamo ora un ID generico che possiamo poi usare in futuro dagli unpublish stessi // per poter ricavare le info che stiamo salvando ora // es, (BT-156)-NoticeResult $genericPrivacyProviderId = substr($privacyProviderFieldId, strpos($privacyProviderFieldId, "(")); // Se c'è già (caso (a), (b)), aggiorniamo has_value nel caso fossimo popolati if ($has_retreived_value && isset($this->unpublish_temp_data[$genericPrivacyProviderId[$current_index]])) { $this->unpublish_temp_data[$genericPrivacyProviderId][$current_index]["has_value"] = true; } else { // Altrimenti creiamo i dati temporanei $this->unpublish_temp_data[$genericPrivacyProviderId][$current_index] = [ "targetId" => $field["id"], "has_value" => $has_retreived_value, "mandatory" => $mandatory["value"], "forbidden" => $forbidden["value"], "code" => $field["privacy"]["code"], ]; } } if (self::UNPUBLISH_FIELDS[$field["btId"] ?? ""] ?? false) { // Verifichiamo se è parzialmente o completamente compilato $privacy_field_fully_compiled = false; $privacy_field_partially_compiled = false; $privacy_field_compiled_count = 0; foreach (self::UNPUBLISH_FIELDS as $btId => $dummy) { if ($btId === "BT-195") continue; // Non ci interessa di BT-195 // Verifichiamo le key di values foreach ($values as $key => $value) { // Match di BT-196(BT-XYZ)-AAAAA a BT-196 ecc if (str_starts_with($key, $btId)) { if (!empty($value)) { $privacy_field_compiled_count++; } } } $privacy_field_fully_compiled = $privacy_field_compiled_count >= (count(self::UNPUBLISH_FIELDS) - 1); $privacy_field_partially_compiled = $privacy_field_compiled_count > 0; } // Otteniamo i dati temporanei salvati in precedenza $genericPrivacyProviderId = substr($field["id"], strpos($field["id"], "(")); $tempUnpublishData = $this->unpublish_temp_data[$genericPrivacyProviderId][$current_index]; // Se il target è popolato o forbidden, tutto diventa forbidden if ($tempUnpublishData["forbidden"]) { $forbidden["value"] = true; } else { // Per il campo che seleziona l'elemento da non pubblicare // ce lo andiamo a pescare automaticamente dalla sua codelist in base // al campo al quale si riferisce. if ($field["btId"] === "BT-195") { if (!$privacy_field_fully_compiled) { $forbidden["value"] = true; } $input_overrides["type"] = "hidden_but_better"; $input_overrides["attrs"]["readonly"] = "1"; $input_overrides["value"] = $input_overrides["val"] = $forbidden["value"] ? "" : $tempUnpublishData["code"]; } // Se il campo di sorgente è obbligatorio o questa parte è almeno parzialmente compilata, tutto diventa obbligatorio if ($privacy_field_partially_compiled) { $mandatory["value"] = true; } else { $mandatory["value"] = false; } } } if (array_key_exists($field["btId"], self::MANDATORY_OVERRIDE)) { $mandatory["value"] = self::MANDATORY_OVERRIDE[$field["btId"]]; } $rel = ["N", "0", "0", "A"]; if (!empty($field["maxLength"])) { $rel[2] = $field["maxLength"]; } if ($mandatory["value"] === true) { $rel[0] = "S"; $rel[1] = "1"; } // Valori di base dell'input $input = [ "title" => $this->translate($section["_label"]), "required" => $rel[0] == "S" ? true : false, "mandatory" => $mandatory["value"] === true, "forbidden" => $forbidden["value"] === true, "name" => "eforms2[{$section["id"]}]", "rel" => $rel, "type" => null, "help" => "", "val" => $retreived_value, "attrs" => [ "data-internalname" => $field["id"] ?? "", ] ]; if ($forbidden["value"] === false && $has_retreived_value) { $assert = $this->checkFieldProperty($field, "assert", $current_index); if ($assert["value"] === false && $assert["error_state"] === false) { $input["assert_fail"] = $this->translate($assert["message"]); } } // Forbidden if ($forbidden["value"] === true) { $input["attrs"]["disabled"] = 1; } else { unset($input["attrs"]["disabled"]); } // Full name if (!empty($prefix)) { array_shift($prefix); if (empty($prefix)) { $input["name"] = "eforms2[{$field["id"]}]"; } else { $input["name"] = "eforms2[" . implode("][", $prefix) . "][{$field["id"]}]"; } } // Debug data if ($this->advanced_debug) { $input["attrs"]["has_efx"] = str_replace("\"", "'", json_encode($has_efx)); $input["attrs"]["error_state"] = str_replace("\"", "'", json_encode($error_state)); $input["attrs"]["current_index"] = $current_index; $input["attrs"]["section"] = $section["id"]; } //$input["title"] .= " ({$field["id"]})"; // Gestiamo CPV e NUTS if (!empty($field["codeList"])) { $codelist_id = $field["codeList"]["value"]["id"] ?? ""; if (str_contains($codelist_id, "nuts")) { $input["type"] = "nuts"; } elseif ($codelist_id == "cpv") { $input["type"] = "cpv"; } } if ($input["type"] === null) { $input["type"] = $this->getDisplayTypeFor($section["displayType"], $field); } // Fix for input overrides if (!empty($input_overrides["type"])) { $input["type"] = $input_overrides["type"]; } if ($input["type"] === "text" && ($field["pattern"] ?? false)) { $input["attrs"]["data-regex"] = $field["pattern"]["value"]; } if ($input["type"] === "date") { if ($input["val"]) { $input["val"] = self::FixDate($input["val"], "d/m/Y"); } } else if ($input["type"] === "radio") { $input["options"] = ["true" => __guue("Si"), "false" => __guue("No")]; } else if ($input["type"] == "cpv") { // Forse è per questo che si rompono i CPV $input["attrs"]["class"] = "row"; } if (in_array($input["type"], ["select", "radio"]) && !empty($field["codeList"])) { if (in_array($input["type"], ["select", "radio"]) && !empty($field["codeList"])) { $input["options"] = $this->getAvailableCodeListFor($field["codeList"]["value"]["id"]); } if (!empty($this->codelist[str_replace("eforms-", "", $field["codeList"]["value"]["id"]) . "-" . $this->notice])) { $input["options"] = $this->getAvailableCodeListFor($this->codelist[str_replace("eforms-", "", $field["codeList"]["value"]["id"]) . "-" . $this->notice]["id"]); } else { $input["options"] = $this->getAvailableCodeListFor($field["codeList"]["value"]["id"]); } } // Workaround per durata if ($section["id"] == "BT-538-Lot") { $input["options"] = ["__SPECIFY" => __guue("Come specificato")] + $input["options"]; } if ($field["type"] == "id-ref" && !empty($field["idSchemes"])) { $input["attrs"]["readonly"] = 1; foreach ($field["idSchemes"] as $id) { if (!empty($this->info["valori"]["settings"]["scheme"][$id])) { $settings = $this->info["valori"]["settings"]["scheme"][$id]; $ids = self::get($this->info["valori"], "eforms2.{$settings["xpath"]}"); unset($input["attrs"]["readonly"]); $input["type"] = "select"; if (!empty($ids)) { if (!is_array($ids)) $ids = [$ids]; //$input["attrs"]["multiple"] = ""; if (is_array($ids[0])) { foreach ($ids as $list) { foreach ($list as $identifier) { $input["options"][$identifier] = $identifier; } } } else { foreach ($ids as $identifier) { $input["options"][$identifier] = $identifier; } } } else { } } } } // Campi di riferimento a ID di ripetibili if (in_array($field["type"], ["id", "id-ref"]) && !empty($field["idSchemes"])) { // Tentativo di popolazione automatica dei nomi dei ripetibili // Per ogni idSchema foreach ($field["idSchemes"] as $idSchema) { // Cerchiamo il nodo di riferimento $node = self::SCHEMA_REFERENCES[$idSchema] ?? null; if ($node) { $node_metadata = $this->xml_structure[$node] ?? null; if ($node_metadata) { // Cerchiamo l'id dei campi che lo conterranno $captionFieldId = $node_metadata["captionFieldId"]; $identifierFieldId = $node_metadata["identifierFieldId"]; // Peschiamo i campi in se $captionField = $this->fields_metadata[$captionFieldId] ?? null; $identifierField = $this->fields_metadata[$identifierFieldId] ?? null; if ($captionField && $identifierField) { // Cerchiamoli tramite XPath $captions = $this->gen_query_provider->runQuery($captionField["xpathAbsolute"]); $identifiers = $this->gen_query_provider->runQuery($identifierField["xpathAbsolute"]); // Verifichiamo che non siano spaiati if (count($captions) === count($identifiers)) { foreach ($captions as $index => $caption_element) { // Otteniamo i valori $caption = $caption_element->nodeValue; $identifier = $identifiers[$index]->nodeValue; // Buttiamoli nel select $input["options"][$identifier] = "$caption ($identifier)"; } } } } } } // Cerchiamo di creare un link di aiuto per l'utente $schema_ids = is_array($field["idSchemes"]) ? $field["idSchemes"] : explode(",", $field["idSchemes"]); $schema_id_link = ""; // Per ogni schema_id (ORG, TPO, LOT, GPO) $schema_id_link_data = []; foreach ($schema_ids as $schema_id) { // Vediamo se abbiamo un riferimento alla sezione if ($section_data = self::SCHEMA_ID_TO_SECTION[$schema_id] ?? false) { $schema_id_link_data[$section_data["section"]] = $section_data["label"]; } else { $schema_id_link_data[$schema_id] = ""; } } // Una volta raggruppati creiamo i link $i = 0; foreach ($schema_id_link_data as $schema_section => $schema_label) { if (empty($schema_label)) { $schema_id_link .= $schema_section; } else { $schema_id_link .= "{$schema_label}"; } if ($i++ < count($schema_id_link_data) - 1) { $schema_id_link .= ", "; } } $input["help"] = __guue("Per completare queste informazioni è necessario compilare la relativa sezione: ") . "$schema_id_link" . " ({$schema_id}-000X)"; } if ((in_array($field["type"], ["id"])) && !empty($field["idScheme"]) && is_string($field["idScheme"])) { $input["attrs"]["readonly"] = 1; $input["help"] = $input["help"] = __guue("Non è necessario compilare questo campo, sarà popolato automaticamente"); if ($this->current_section) { $dot_path_array = []; $dot_path_array[] = $this->current_section["id"]; $dot_path_array[] = "values"; if ($this->current_section["_repeatable"] ?? false) { $dot_path_array[] = "*"; } if (!empty($prefix)) { foreach ($prefix as $i => $p) { $dot_path_array[] = is_numeric($p) ? "*" : $p; } } $dot_path_array[] = $section["id"]; $dot_path = implode(".", $dot_path_array); $settings = []; $settings["xpath"] = $dot_path; $settings["section"] = $this->current_section["id"]; if (!empty($field["pattern"]["value"])) { $settings["regex"] = $field["pattern"]["value"]; $settings["schemeName"] = $field["schemeName"]; $settings["pad"]["length"] = 4; $settings["pad"]["string"] = "0"; $settings["pad"]["type"] = STR_PAD_LEFT; // Get prefix from supplied regex // eg: ^ORG-\d{4}$ -> ORG- $regex = $settings["regex"]; if (preg_match('/\^(\w+)\-.+/', $regex)) { $settings["pad"]["prefix"] = substr($regex, 1, strpos($regex, "-", 1)); } } $_SESSION["notice" . $this->notice]["settings"]["scheme"][$field["idScheme"]] = $this->info["valori"]["settings"]["scheme"][$field["idScheme"]] = $settings; if ($forbidden["value"] === true) { self::unset($this->info["valori"], $settings["xpath"]); } } } $value = $this->info["valori"][$section["id"]] ?? null; if (empty($value)) { $value = (string) $this->getSuggetion($section["id"]); } if (!empty($value)) { $field["val"] = $value; } if($input["type"] == "value") { $input["rel"][3] = "2D"; } if (!empty($input_overrides)) { if(isset($input_overrides["title_before"])) { $input["title"] = $input_overrides["title_before"] . $input["title"]; } $input = array_merge_recursive_distinct($input, $input_overrides); } $retval[$input["name"]] = $input; break; default: throw new Exception("Invalid type {$section['contentType']}"); break; } return $retval; } /** * Va a verificare che una sezione abbia qualunque valore * * @param array $values * @param string $section_id * @return boolean */ private function hasAnyValue(array $values, string $section_id, int $iteration = -1): bool { return $this->countValues($values, $section_id, $iteration) >= 1; } private function countForcedIntegrationValues(array $values, string $section_id, int $iteration = -1) { if(!$this->enable_forced_suggestions) return 0; $value = $values[$section_id] ?? null; if (empty($value)) { foreach ($values as $v) { if (!empty($v[$section_id])) { $value = $v[$section_id]; break; } } } if ($iteration > -1) { if (!is_array($value)) return 0; $value = $value[$iteration] ?? null; } if (!is_array($value)) { return 0; } $count = 0; array_walk_recursive($value, function ($item, $key) use (&$count, $section_id, $iteration) { // Non contiamo currency o tipi di durata if ($key === "forced" && $item === true) $count++; }); return $count; } /** * Va a contare il numero di valori di una sezione * * @param array $values * @param string $section_id * @param int $iteration * @return int */ private function countValues(array $values, string $section_id, int $iteration = -1): int { $value = $values[$section_id] ?? null; if (empty($value)) { foreach ($values as $v) { if (!empty($v[$section_id])) { $value = $v[$section_id]; break; } } } if ($iteration > -1) { if (!is_array($value)) return 0; $value = $value[$iteration] ?? null; } if (!is_array($value)) { return empty($value) ? 0 : 1; } $count = 0; array_walk_recursive($value, function ($item, $key) use (&$count, $section_id, $iteration) { // Non contiamo currency o tipi di durata if ($key === "type") return; // Non contiamo elementi vuoti if (empty($item)) return; // Non contiamo fieldIdentifier if (preg_match('/[A-Z]{3}-[0-9]{4}/', $item) === 1) return; $count++; }); return $count; } /** * Va a cercarsi i valori di una sezione ricorsivamente * * @param array $values * @param string $section_id * @return mixed */ private function retrieveValue(array $values, string $section_id) { $value = $values[$section_id] ?? null; if (empty($value)) { foreach ($values as $v) { if (!empty($v[$section_id])) { return $v[$section_id]; } } } return $value; } /** * Nasconde/mostra la sezione GR-Change quando necessario * * @return void */ public function manageChangeSection(): void { $info = json_decode($this->info["info"] ?? "{}"); if (!($info->rectification ?? false)) { $this->sections = array_values(array_filter($this->sections, function ($section) { return $section["id"] != "GR-Change"; })); } } /** * Verifica che la sezione attuale sia completa. * * @param string $section_name * @param integer|null $subsection_index * @return boolean */ public function isSectionComplete(string $section_name, ?int $subsection_index = null): bool { // Forziamo una chiamata a completed se non è popolata l'origine dati if (empty($this->completed_subsections)) $this->completed(); $completed = $this->completed_subsections[$section_name] ?? null; // Nessuna entry vuol dire non completo if (empty($completed)) { return false; } return $subsection_index === null ? ($this->completed_subsections[$section_name] ?? false) : ($this->completed_subsections[$section_name][$subsection_index] ?? false); } /** * Verify whether all sections have been completed. * * @return Bool */ public function completed(): Bool { $this->manageChangeSection(); $subsections = array_keys($this->getRepeatableSections()); $retval = true; foreach ($this->sections as $section) { if (in_array($section["id"], $subsections)) { $repetitions = isset($this->info["valori"]["settings"]["reps"][$section["id"]]) ? $this->info["valori"]["settings"]["reps"][$section["id"]] : 1; if ($repetitions > 0) { for ($i = 0; $i < $repetitions; $i++) { $draft = self::get($this->info, "valori.eforms2.{$section['id']}.info.{$i}.draft", true); $this->completed_subsections[$section["id"]][$i] = !$draft; if ($retval && $draft) { $retval = false; } } } } else { $draft = self::get($this->info, "valori.eforms2.{$section['id']}.info.draft", true); $this->completed_subsections[$section["id"]] = !$draft; if ($retval && $draft) { $retval = false; } } } return $retval; } /** * Get all repeteable sections * * @return Array */ public function getRepeatableSections(): ?array { $sections = $this->sections; foreach ($sections as $index => &$section) { if (empty($section["_repeatable"])) { unset($sections[$index]); continue; } $section["rel"] = "S;0;0;N"; $section["label"] = $this->translate($section["_label"]); $section["value"] = $this->info["valori"]["settings"]["reps"][$section["id"]] ?? 0; $section["reps_locked"] = $this->info["valori"]["settings"]["reps_locked"][$section["id"]] ?? false; if ($section["id"] == "GR-Lot") { $section["rel"] = "S;0;0;N"; $section["value"] = $this->info["valori"]["settings"]["reps"][$section["id"]] ?? 1; $section["label"] = __guue("Numero di lotti"); $section["reps_locked"] = $this->info["valori"]["settings"]["reps_locked"][$section["id"]] ?? false; } } return array_column($sections, null, "id"); } //TODO: Descrivere il metodo /** * getLastNumber * * @return int */ public function getLastNumber(): int { $number = 0; $latest = $this->pdo->go("SELECT MAX(numero) FROM b_guue WHERE anno = :anno", [":anno" => date('Y')])->fetch(PDO::FETCH_COLUMN, 0); if ($latest > 0) { $number = $latest; } return $number; } /** * Stores data for future use * * @return Array */ public function save(array $data = []): ?array { // Verifica blocco concorrenza $current_user = $_SESSION["utente"]->codice ?? null; if ($current_user !== null && ($this->info["codice"] ?? 0) > 0) { $bind = [ ':codice' => $this->info["codice"], ':codice_utente' => $current_user, ]; $sql = << 0 AND utente_modifica != :codice_utente HAVING delta > 0 LIMIT 1 QUER; $ris = $this->pdo->go($sql, $bind); if ($ris->rowCount() == 1) { $lock_data = $ris->fetch(PDO::FETCH_ASSOC); return ["concurrence_lock" => $lock_data]; } } $record = $this->info; if(empty($record["notice_id"])) { $record["notice_id"] = uuidgen_guue(); } if(empty($record["notice_version"])) { $record["notice_version"] = 1; } $data["eforms2"] = $this->organizeRepeteableGroups($data["eforms2"] ?? []); $sections = array_column($this->sections, null, "id"); // Salviamoci i vecchi dati di draft $info = $record["valori"]["eforms2"][$data["current_section"]]["info"] ?? []; if ($sections[$data["current_section"]]["_repeatable"]) { $record["valori"]["eforms2"][$data["current_section"]] = []; $values = self::get($this->info["valori"], "eforms2.{$data["current_section"]}.values"); $values[$data["section-repetition"]] = $data["eforms2"]; ksort($values); } else { $values = $data["eforms2"]; } //dd($draft); self::set($record["valori"], "eforms2.{$data["current_section"]}.values", $values); $record = $this->setSchemaIDForeachRepeteableItems($record); if (!empty($data["reps"])) { $record["valori"]["settings"]["reps"] = $data["reps"]; foreach ($data["reps"] as $repetition_id => $repetitions) { if ($repetitions < 1 || empty($record["valori"]["eforms2"][$repetition_id])) { $record["valori"]["eforms2"][$repetition_id]["values"] = []; } else if ((int) $repetitions < count($record["valori"]["eforms2"][$repetition_id]["values"])) { $record["valori"]["eforms2"][$repetition_id]["values"] = array_slice($record["valori"]["eforms2"][$repetition_id]["values"], 0, (int) $repetitions); } //$record["valori"]["eforms2"][$data["current_section"]]["info"][$repetition_id] = } } if ($sections[$data["current_section"]]["_repeatable"]) { // Ripetibile $info[$data["current_subsection_index"]] = ["draft" => $data["draft"] == "S"]; } else { // Non ripetibile $info = ["draft" => $data["draft"] == "S"]; } // Aggiorniamo info $record["valori"]["eforms2"][$data["current_section"]]["info"] = $info; $record["valori"]["eforms2"] = array_filter($record["valori"]["eforms2"], fn ($key) => !empty($key), ARRAY_FILTER_USE_KEY); $record["titolo"] = $data["titolo"]; $record["valori"] = json_encode($record["valori"], JSON_INVALID_UTF8_IGNORE); $record["data_aggiornamento"] = date('d/m/Y H:i:s'); $record["data_trasmissione"] = null; $record["modulo"] = $data["modulo"] ?? null; $record["codice_elemento"] = $data["codice_elemento"] ?? 0; $record["lotto_riferimento"] = $data["lotto_riferimento"] ?? null; $record["stato"] = TedEsender::getStati()[0]["title"]; if (!$_SESSION["utente"]->fromHere()) { $record["codice_ente"] = $_SESSION["utente"]->codice_ente; } if(empty($record["notice_id"])) { $record["notice_id"] = uuidgen(); $record["notice_version"] = 1; } $salva = new Salva(); $salva->debug = false; $salva->nome_tabella = "b_guue"; $salva->operazione = empty($record["codice"]) ? "INSERT" : "UPDATE"; $salva->ignore = ["soft_delete"]; if ($salva->operazione == "UPDATE") { $salva->ignore = array_merge($salva->ignore, ["data_creazione", "data_pubblicazione", "info"]); } $salva->oggetto = $record; $codice = $salva->save(); if (!empty($codice)) { $record["first_save"] = $salva->operazione === "INSERT"; $record["codice"] = $codice; $this->info = $salva->oggetto; $this->info["codice"] = $codice; $this->info["valori"] = json_decode($this->info["valori"], 1); if ($_SESSION["utente"]->gerarchia > 15 && $salva->operazione == "INSERT") { $permesso = [ "sezione" => "guue", "codice_elemento" => $this->info["codice"], "codice_gestore" => $_SESSION["ente"]->codice, "funzione" => "0", "codice_utente" => $_SESSION["utente"]->codice, ]; $salva_p = new salva(); $salva_p->debug = false; $salva_p->nome_tabella = "r_permessi"; $salva_p->operazione = "INSERT"; $salva_p->oggetto = $permesso; $salva_p->save(); } if ($data["draft"] == "N" && $this->completed()) { $this->generateXMLNotice(); $this->xml = $this->sortXMLElementsByXSD($this->xml); $record["xml"] = $this->xml->saveXML(); $salva->oggetto = $record; $salva->operazione = "UPDATE"; $salva->save(); } } return $record; } /** * fixArrayForXML * * @param mixed $data * @return void */ private function fixArrayForXML(array &$data): void { $keys = array_keys($data); foreach ($keys as $key) { if (is_array($data[$key])) { $this->fixArrayForXML($data[$key]); } else { if ($key == "BT-538-Lot") { if ($data[$key] === "__SPECIFY") { $data[$key] = null; } } if (strpos($key, "(t)-") !== false) { preg_match('/^\d{2}[:]\d{2}$/', $data[$key], $m); preg_match('/^\d{2}[:]\d{2}:\d{2}$/', $data[$key], $_m); if (!empty($m)) { $data[$key] .= ":00" . self::TIMEZONE_SUFFIX; } if (!empty($_m)) { $data[$key] .= self::TIMEZONE_SUFFIX; } } if (is_string($data[$key])) { preg_match('/^\d{2}[\/]\d{2}[\/]\d{4}$/', $data[$key], $matches, PREG_OFFSET_CAPTURE, 0); if (!empty($matches)) { $data[$key] = (DateTime::createFromFormat("d/m/Y", $data[$key]))->format('Y-m-d' . self::TIMEZONE_SUFFIX); } } } } } /** * toXML * * @return String */ public function toXML(): ?String { $this->generateXMLNotice(); $this->xml = $this->sortXMLElementsByXSD($this->xml); $xml = $this->xml->saveXML(); $this->xml = new DOMDocument(); $this->xml->loadXML($xml, LIBXML_NSCLEAN); return $this->xml->saveXML(); } /** * Effettua un override (che non viene salvato su DB) * del notice_id da inviare con la scheda * * @param string $notice_id * @return void */ public function overrideNoticeID(string $notice_id) : void { $this->override_notice_id = $notice_id; } /** * Ottiene il noticeID della scheda. */ public function getNoticeID(): ?string { if(!empty($this->override_notice_id)) return $this->override_notice_id; return $this->info["notice_id"] ?? ""; } /** * Ottiene la versione della scheda * * @return string */ public function getNoticeVersion(): string { return str_pad(intval($this->info["notice_version"] ?? 1), 2, "0", STR_PAD_LEFT); } /** * Convert the visual model into XML format * * @return String */ public function generateXMLNotice($encoding = '', bool $final_xml_render = true): void { $this->final_xml_render = $final_xml_render; $notice = array_column($this->notice_types["noticeSubTypes"], null, "subTypeId")[$this->notice]; $subtypes = array_column($this->notice_types["documentTypes"], null, "id"); $subtype = $subtypes[$notice["documentType"]]; $this->xml = new DOMDocument('1.0', $encoding); $this->xml->preserveWhiteSpace = true; $this->xml->formatOutput = true; $root = $this->xml->createElement($subtype["rootElement"]); if (!empty($subtype["namespace"])) { $root = $this->xml->createElementNS($subtype["namespace"], $subtype["rootElement"]); } if (!empty($subtype["additionalNamespaces"])) { foreach ($subtype["additionalNamespaces"] as $ns) { // Store namespace url $this->namespaces[$ns["prefix"]] = $ns["uri"]; // Add namespace data to dom $root->setAttributeNS('http://www.w3.org/2000/xmlns/', "xmlns:{$ns["prefix"]}", $ns["uri"]); } } $this->xml->appendChild($root); // Create XPath provider $this->gen_query_provider = new XML_XPath20_Querier($this->xml, false, true); $publicationDate = (new DateTime('now'))->getTimestamp(); if(false) { $publicationDate -= (23*60*60 + 30*60); } if (!empty($this->fields["metadata"])) { //dd($notice["legalBasis"]);32014L0024 foreach ($this->fields["metadata"] as $metadata) { $field = $this->fields_metadata[$metadata["id"]]; $value = ""; switch ($field["btId"]) { case "BT-01": $value = $notice["legalBasis"]; break; case "BT-02": $value = $notice["type"]; break; case "BT-03": $value = $notice["formType"]; break; case "BT-04": case "BT-701": $value = $this->getNoticeID(); break; case "BT-757": $value = $this->getNoticeVersion(); break; case "BT-702": $value = ""; if ($field["id"] == "BT-702(a)-notice") { $value = "ITA"; } break; case "BT-05": if ($field["id"] == "BT-05(a)-notice") { $value = date("Y-m-d", $publicationDate) . self::TIMEZONE_SUFFIX; } else if ($field["id"] == "BT-05(b)-notice") { $value = date("H:i:s", $publicationDate) . self::TIMEZONE_SUFFIX; } break; case "OPT-001": $value = $this->notice_types["ublVersion"]; break; case "OPT-002": $sdk_version = $this->getNoticeSDKVersion(); $value = "eforms-sdk-{$sdk_version}"; break; case "OPP-070": $value = $this->notice; break; case "OPT-999": $value = date('Y-m-d') . self::TIMEZONE_SUFFIX; break; default: break; } if (!$this->final_xml_render || !empty_but_not_zero($value)) { $this->addFieldToXML($field["xpathAbsolute"], $this->xml->childNodes->item(0), $value, [], $field["id"]); } } } if (!empty($this->info["valori"]["eforms2"])) { $this->fixArrayForXML($this->info["valori"]["eforms2"]); $this->info["valori"]["eforms2"] = array_filter($this->info["valori"]["eforms2"], fn ($key) => !empty($key), ARRAY_FILTER_USE_KEY); foreach ($this->sections as $section) { if (!empty($this->info["valori"]["eforms2"][$section["name"]]["values"])) { $values = $this->info["valori"]["eforms2"][$section["name"]]["values"]; $section = $this->fields["content"][$section["index"]]; $_tmp = []; $this->addGroupToXML($section, $values, $_tmp); } } } $this->onUpdateXml(); } /** * Flatten given data into values array * * @param $data * @param Array $values * * @return void */ private function extrapolateValues($data, array &$values): void { if (is_array($data)) { foreach ($data as $d) { $this->extrapolateValues($d, $values); } } else { $values[] = $data; } } /** * Add a group to the XML document * * @param Array $section * @param Array $values * @param Bool $repeatable * * @return void */ private function addGroupToXML(array $section, array $values, array &$current_repetitions = []): void { $contentType = $section["contentType"]; // Render field if ($contentType === "field") { // Fetch metadata $metadata = $this->fields_metadata[$section["id"]] ?? null; assert($metadata !== null, "Invalid section id {$section['id']}"); // Get value $is_duration_or_amount = false; if (($metadata["legalType"] ?? "") === "VALUE" || ($metadata["legalType"] ?? "") === "DURATION") { $is_duration_or_amount = true; $value = $values; } else { $value = $values[$section["id"]] ?? ""; } // Get xpath $xpath = $metadata["xpathAbsolute"]; if (!$this->final_xml_render || !empty_but_not_zero($value)) { if ($is_duration_or_amount) { if (isset($value["value"]) && !empty_but_not_zero($value["value"]) && !empty($value["type"])) { $actualVal = $value["value"]; if(($metadata["legalType"] ?? "") === "VALUE") { $actualVal = is_numeric($value["value"]) ? number_format($value["value"], 2, ".", "") : $value["value"]; } $element = $this->addFieldToXML($xpath, $this->xml->childNodes->item(0), $actualVal, $current_repetitions, $metadata["id"]); $element->setAttribute($metadata["legalType"] == "VALUE" ? "currencyID" : "unitCode", $value["type"]); } } else { // Fix CPV Format if (!empty($value) && in_array($metadata["btId"] ?? "", ["BT-262", "BT-263"])) { $value = str_pad($value, 8, "0"); } // Create element $element = $this->addFieldToXML($xpath, $this->xml->childNodes->item(0), $value, $current_repetitions, $metadata["id"]); // Set default listName where not specified if (!empty($metadata["codeList"]) && substr($element->nodeName, -4) == "Code") { if (empty($element->getAttribute("listName"))) { $element->setAttribute("listName", $metadata["codeList"]["value"]["id"]); } } // Fix text-multilingual if (($metadata["type"] ?? "") === "text-multilingual" || $element->nodeName === "cbc:Name") { $element->setAttribute("languageID", "ITA"); } // Add schemeName where applicable if ($element->nodeName === "cbc:ID") { // Prendiamo i primi tre caratteri per ottenere LOT, ORG, CON ecc $scheme = substr($element->nodeValue, 0, 3); // Verifichiamo che ci siano le impostazioni per lo scheme if (!empty($scheme) && !in_array($scheme, ["GLO"]) && isset($this->info["valori"]["settings"]["scheme"][$scheme])) { // Otteniamo schemeName $schemeName = $this->info["valori"]["settings"]["scheme"][$scheme]["schemeName"] ?? null; if ($schemeName !== null) { // Settiamo schemeName $element->setAttribute("schemeName", $schemeName); } } } } } return; } // Process group if ($contentType === "group") { // Check for content assert(!empty($section["content"]), "Empty content for {$section['id']}"); // Repeatable if ($section["_repeatable"] ?? false) { // Check for IDs if (array_has_numeric_keys($values)) { // Order and fill holes ksort($values); $values = array_values($values); $extrapolated_values = []; //verify given group contents before add to XML $this->extrapolateValues($values, $extrapolated_values); $extrapolated_values = array_filter($extrapolated_values, function ($value) { //filter empty values and ID from group values if ($value == "") { return false; } else { $matches = []; preg_match_all('/[A-Z]{3}[-]\d{4}/m', $value, $matches); return empty($matches[0]); } return true; }); if (empty($extrapolated_values) && !empty($section["_idScheme"])) { //codice magico non toccare } else { //if not all data was filtered store into XML // Get path of the group $group_path = $this->xml_structure[$section["nodeId"]]; // For each repetition foreach ($values as $rep_index => &$rep_values) { // Explode the XPath $xpathRelative = $this->explode_xpath($group_path["xpathRelative"]); // Remove attributes $xpathRelative = array_filter($xpathRelative, fn ($val) => strpos($val, "[") === false); // Rebuild the XPath $xpathRelative = implode("/", $xpathRelative); // Note the repetition $current_repetitions[$xpathRelative] = $rep_index; // Render subelements foreach ($section["content"] as $key => $sub_section) { $subsection_values = $rep_values[$sub_section["id"]] ?? []; // Wrap a possible field in a container so its rendering works correctly if (!is_array($subsection_values) && $sub_section["contentType"] === "field") { $subsection_values = [$sub_section["id"] => $subsection_values]; } // Recursive rendering $this->addGroupToXML($sub_section, $subsection_values, $current_repetitions); } } } } // Not repeatable } else { foreach ($section["content"] as $sub_section) { // Wrap a possible field in a container so its rendering works correctly $subsection_values = $values[$sub_section["id"]] ?? []; if (!is_array($subsection_values) && $sub_section["contentType"] === "field") { $subsection_values = [$sub_section["id"] => $subsection_values]; } // Recursive rendering $this->addGroupToXML($sub_section, $subsection_values, $current_repetitions); } } } else { throw new Exception("Invalid section type {$section['contentType']}"); } } /** * Converts an XPath expression into an array, filtering out unnecessary elements if clean == true * * @param String $xpath * @param Bool $strip_useless_parts * @return Array */ private static function explode_xpath(String $xpath, Bool $keep_attributes = false): array { preg_match_all("/\[(?:[^\[\]]|(?R))+\]|[^\[\]\/]+/", $xpath, $matches); $retval = $matches[0]; if ($keep_attributes) { $tmp = $retval; $retval = []; foreach ($tmp as $element) { if ($element[0] == "[") { $retval[count($retval) - 1] .= $element; } else { $retval[] = $element; } } } return $retval; } const FORCE_REPEATABLE_IN_FINAL_RENDER = [ "cac:ContractExecutionRequirement" => true, "cac:ContractingSystem" => true, "cac:ContractingPartyType" => true, "cac:ContractingActivity" => true, ]; const FORCE_EMPTY_ATTRIBUTE_IN_FINAL_RENDER = []; private static function setNodeText(DOMElement &$element, String $value) { /** @var DOMElement $child */ foreach ($element->childNodes as $child) { if ($child->nodeType === XML_TEXT_NODE) { $element->removeChild($child); } } $textNode = $element->ownerDocument->createTextNode($value); $element->appendChild($textNode); } public static function explode_attribute_selector($attribute_selector) { // Valore di ritorno $retval = []; // Buffer del segmento attuale $buffer = ""; // Livello di nest attuale $nest = 0; for ($i = 0; $i < strlen($attribute_selector); $i++) { $char = $attribute_selector[$i]; if ($char == "[") { // Se troviamo una [ nel corso del segmento attuale, aumentiamo il livello di nesting $nest++; } elseif ($char == "]") { // Riduciamo il livello di nesting $nest--; // Se il nesting è a 0 abbiamo finito il segmento attuale if ($nest === 0) { $buffer .= $char; $retval[] = $buffer; $buffer = ""; continue; } } // Appendiamo il carattere $buffer .= $char; } return $retval; } /** * Add a field to the XML document using its absolute path * * @param String $xpath * @param String $value * * @return DOMElement */ private function addFieldToXML(String $xpath, DOMElement $element, String $value, array $current_repetitions = [], string $field_name = null, DOMElement $parent = null): DOMElement { // Explode Xpath removing () calls $path = $this->explode_xpath($xpath, true); // Rimuoviamo il * iniziale che esiste solo per questioni di compatibilità if ($path[0] === "*") { array_shift($path); } // Se dobbiamo lavorare sul padre (e abbiamo un padre) if ($path[0] === "..") { if ($parent !== null) $element = $parent; array_shift($path); } /** * @var DOMElement $element */ // Foreach part of the path for ($index = 0; $index < count($path); $index++) { // Get current segment $tag = $path[$index]; // Check if it has a selector $has_attribute_selector = str_contains($tag, "["); // Keep the full segment $selector = $tag; // If we have an attribute selector we isolate it if ($has_attribute_selector) { $attribute_selector = substr($tag, strpos($tag, "[")); //$attribute_selector = rtrim($attribute_selector, "]"); $tag = substr($tag, 0, strpos($tag, "[")); } $_repeat = false; // Skip NOP since it's been already managed by the previous iteration if ($selector === "NOP") { continue; } else if (strpos($tag, "(") !== false) { if (strpos($tag, "text()=") === 0) { // Verifichiamo che non ci sia già il testo prima if (strlen($element->nodeValue) <= 0) { $text_attribute = trim(substr($tag, strpos($tag, "/text()=") + 8), "'[]"); self::setNodeText($element, $text_attribute); } } else { // (Al momento) non gestiamo altri tipi di attributi } } // If the part of the path is an attribute we set it else if (strpos($selector, "@") === 0) { if (str_contains($selector, "=") && (substr_count($selector, "'") == 2 || substr_count($selector, '"') == 2)) { // If it is we mark it for creation and make sure the next iteration doesn't have to $attribute = explode("=", $selector); $attribute_name = str_replace("@", "", $attribute[0]); $attribute_value = str_replace(["'", '"'], "", $attribute[1]); $element->setAttribute($attribute_name, $attribute_value); } else { $element->setAttribute(ltrim($selector, "@"), $value); } // Create / select an elemenenet } else { // Check if it has a prefix $has_prefix = str_contains($tag, ":"); $tag_prefix = ""; // Prefix if ($has_prefix) { $tag_split = explode(":", $tag, 2); $tag_prefix = $tag_split[0]; } // Check if it has an attribute we have to consider $has_attribute = $index < count($path) - 1 && strpos($path[$index + 1], "@") === 0; $create_attribute = false; $attribute_name = $attribute_value = ""; // If we do if ($has_attribute) { // We check if it's in the format @attribute='value' $next_selector = $path[$index + 1]; if (str_contains($next_selector, "=") && (substr_count($next_selector, "'") == 2 || substr_count($next_selector, '"') == 2)) { // If it is we mark it for creation and make sure the next iteration doesn't have to $create_attribute = true; $attribute = explode("=", $next_selector); $attribute_name = str_replace("@", "", $attribute[0]); $attribute_value = str_replace(["'", '"'], "", $attribute[1]); $path[$index + 1] = "NOP"; } } // Select target element $absolute_selector = $element->getNodePath() . "/" . $selector; // Verify that we have an attribute /*if ($has_attribute) { $absolute_selector = $absolute_selector . "[{$next_selector}]"; }*/ try { $nodes = $this->gen_query_provider->runQuery($absolute_selector); } catch (Throwable $e) { // Query fallita } // Check if we have to create a new one $repetitions_for_element = $current_repetitions[$tag] ?? false; $_repeat = !empty($current_repetitions) && $repetitions_for_element !== false && $repetitions_for_element > 0 && count($nodes) < ($repetitions_for_element + 1); if (empty($nodes) || $_repeat || ($this->final_xml_render && (self::FORCE_REPEATABLE_IN_FINAL_RENDER[$selector] ?? false))) { // Use namespace for prefix if ($has_prefix) { $child = $this->xml->createElementNS($this->namespaces[$tag_prefix], $tag); } else { $child = $this->xml->createElement($tag); } $force_add = self::FORCE_EMPTY_ATTRIBUTE_IN_FINAL_RENDER[$tag] ?? null; if ($this->final_xml_render && $force_add !== null) { foreach ($force_add as $fa_element) { $full_name = implode(":", $fa_element); $el = $this->xml->createElementNS($this->namespaces[$fa_element[0]], $full_name, ""); $child->appendChild($el); } } // Create attribute if we have to if ($create_attribute) { $attribute_name = str_replace(" ", "", $attribute_name); $attribute_value = str_replace(" ", "", $attribute_value); $child->setAttribute($attribute_name, $attribute_value); } // Se ha un selettore di attributo vuol dire che si aspetta qualcosa di specifico dai figli if ($has_attribute_selector) { if (!$this->final_xml_render || !empty_but_not_zero($value)) { $all_attributes = $this->explode_attribute_selector($attribute_selector); $attributes = []; foreach ($all_attributes as $attribute) { $attribute = ltrim($attribute, "["); if (!str_starts_with($attribute, "not(") && !empty($attribute)) { $attributes[] = $attribute; } } if (!empty($attributes)) { $attribute_selector = implode("/", $attributes); // Creiamo i figli richiesti $this->addFieldToXML("*/" . $attribute_selector, $child, "", $current_repetitions, "", $element); } } } // If it's the last part if ($selector == $path[array_key_last($path)]) { // We populate the value if (!empty_but_not_zero($value)) { if (!empty($field_name)) { $comment = $this->xml->createComment("$field_name" . ($repetitions_for_element ? "[$repetitions_for_element]" : "")); $element->appendChild($comment); } self::setNodeText($child, $value); $element->appendChild($child); } } else { $element->appendChild($child); } $element = $child; continue; } // We don't have to create so we select it $_index = 0; // Check what we have to select $target_index = $repetitions_for_element ?? 0; foreach ($nodes as $node) { if ($node->parentNode->nodeName == $element->nodeName) { if ($target_index == $_index) { $element = $node; break; } $_index++; } } } if ($selector == $path[array_key_last($path)] && $selector[0] !== "@" && !empty($value)) { if (!empty($field_name) && $element->parentNode) { $comment = $this->xml->createComment("$field_name" . ($repetitions_for_element ? "[$repetitions_for_element]" : "")); $element->parentNode->insertBefore($comment, $element); } self::setNodeText($element, $value); } } return $element; } /** * Set the ID of a repeteable item * * @param mixed $data * @return Array */ private function setSchemaIDForeachRepeteableItems(array $data = []): array { // TODO Commentare la funzione if (isset($_SESSION["notice" . $this->notice])) { $schemas = $_SESSION["notice" . $this->notice]["settings"]["scheme"]; } else { $schemas = $this->info["valori"]["settings"]["scheme"] ?? null; } if (!empty($schemas) && !empty($data["valori"])) { $sections = $this->getSections(); $sections = array_column($sections, null, "id"); foreach ($schemas as $scheme => $settings) { $section = $sections[$settings["section"]]; if ($section !== null) { $data["valori"]["settings"]["scheme"][$scheme] = $settings; $dot_path = "eforms2.{$settings["xpath"]}"; $fields = self::get($data["valori"], $dot_path, null, true); if (!empty($fields)) { $index = 0; foreach ($fields as $key => $value) { $index++; $val = $settings["pad"]["prefix"] . str_pad($index, $settings["pad"]["length"], $settings["pad"]["string"], $settings["pad"]["type"]); if(!str_contains($key, "BT-137-Lot")) self::set($data["valori"], $key, $val); } } } } } return $data; } /** * Ottiene il displaytype per il campo in questione * * @param String $displayType Il displayType attuale come specificato dal form type json * @param array $field Il campo, come specificato da fields.json * @return String Tipo compatibile con bs4formgenerator */ private function getDisplayTypeFor(String $displayType, array $field): String { if (in_array($field["type"], ["text-multilingual", "indicator", "date", "url", "email", "number", "integer", "text", "time", "amount", "phone", "measure"])) { $displayType = strtoupper($field["type"]); } if (!empty($field["legalType"]) && $field["legalType"] == "DURATION" && $field["type"] == "measure") { $displayType = "DURATION"; } if (!empty($field["legalType"]) && $field["legalType"] == "VALUE" && $field["type"] == "amount") { $displayType = "VALUE"; } switch ($displayType) { case "DURATION": return "duration"; case "AMOUNT": case "VALUE": return "value"; case "URL": return "url"; case "EMAIL": return "email"; case "MEASURE": case "NUMBER": return "number"; case "DATE": return "date"; case "TIME": return "time"; case "TEXTAREA": return "textarea"; case "COMBOBOX": if (!empty($field["codeList"]["value"]["id"])) { return "select"; } return "text"; case "INDICATOR": return "radio"; case "RADIO": if (!empty($field["codeList"]["value"]["id"])) { return "radio"; } return "text"; } return "text"; } /** * Riorganizza la struttura delle sezioni ripetibili. Utile per assicurarsi che non vi siano buchi nella struttura * * @param array $data * @return array */ private function organizeRepeteableGroups(array $data): array { $ret = []; foreach ($data as $key => $val) { if (is_numeric($key)) { $ret[] = $val; } else { $ret[$key] = $val; } } foreach ($ret as &$val) { if (is_array($val)) { $val = $this->organizeRepeteableGroups($val); } } return $ret; } /** * Initializza l'SDK di eForms2 caricando tutti i file relativi * * @return void */ private function initializeSDK(): void { // Preveniamo caricamenti superflui. if ($this->fields !== null && $this->version === $this->loaded_version) { return; } $this->loaded_version = $this->version; $this->current_sdk_path = dirname(__DIR__) . DIRECTORY_SEPARATOR . "eforms" . DIRECTORY_SEPARATOR . $this->version; $this->eforms_translations = $this->getTranslator(); $this->title = $this->translate("notice|name|{$this->notice}"); $this->settings = jsonToArray("{$this->current_sdk_path}/../../config/eforms/settings.json"); $this->fields = $this->getNoticeSchema($this->notice); $this->sections = $this->getSections(); $tmp_metadata = jsonToArray("{$this->current_sdk_path}/fields/fields.sa.json"); $this->fields_metadata = array_column($tmp_metadata["fields"], null, "id"); $this->xml_structure = array_column($tmp_metadata["xmlStructure"], null, "id"); // Personalizzazioni di fields.json $custom_fields_path = "{$this->current_sdk_path}/fields/fields.custom.json"; if (file_exists($custom_fields_path)) { $custom_fields = jsonToArray($custom_fields_path); $custom_fields = array_column($custom_fields["fields"], null, "id"); $this->fields_metadata = array_merge_recursive_distinct($this->fields_metadata, $custom_fields); } $this->codelist = jsonToArray("{$this->current_sdk_path}/codelists/codelists.json"); $this->codelist = array_column($this->codelist["codelists"], null, "id"); $this->notice_types = jsonToArray("{$this->current_sdk_path}/notice-types/notice-types.json"); if (!is_dir("{$this->current_sdk_path}/jsoncodelists") || count(glob("{$this->current_sdk_path}/jsoncodelists/*.gc")) == 0) { foreach ($this->codelist as $key => $attributes) { $this->getAvailableCodeListFor($key); } } } /** * Si occupa del caricamento (o della creazione) di un eForm * * @param integer $codice * @return void */ private function init($codice = 0): void { $this->info = get_campi("b_guue"); $this->info["codice"] = 0; $this->info["codice_ente"] = $_SESSION["ente"]->codice; $this->info["version"] = "eForms"; $this->info["sdk_version"] = $this->__get("version"); $this->info["form"] = $this->notice; $this->info["anno"] = date('Y'); $this->info["data_creazione"] = date('d/m/Y H:i:s'); $this->info["utente_modifica"] = $_SESSION["utente"]->codice ?? 0; $this->info["valori"] = []; if (!empty($codice)) { $bind = [':codice' => $codice]; $sql = "SELECT * FROM b_guue WHERE codice = :codice AND soft_delete !='S'"; $ris = $this->pdo->go($sql, $bind); if ($ris->rowCount() == 1) { $this->info = $ris->fetch(PDO::FETCH_ASSOC); $continue = true; if (!isset($_SESSION["utente"]->elaborazioneCoda) && (!empty($this->info["modulo"]) && !empty($this->info["codice_elemento"]))) { $continue = false; if ($_SESSION["utente"]->permission($this->info["modulo"],$this->info["codice_elemento"])) { $continue = true; } } if ($continue) { // Preveniamo il cambio di form type if($this->notice === self::ANY_NOTICE_TYPE) { $this->notice = $this->info["form"]; } elseif ($this->info["form"] !== $this->notice) { $basepath = explode('?', $_SERVER['REQUEST_URI'], 2)[0]; die(""); } if (is_json($this->info["valori"])) { $this->info["valori"] = json_decode($this->info["valori"], 1); } else { $this->info["valori"] = []; } $this->version = $this->info["sdk_version"]; $this->initializeSDK(); } else { throw new LogicException("Non è stato possibile accedere alla scheda GUUE #{$codice}"); } } else { throw new LogicException("Scheda #{$codice} non trovata."); } } else { // ANY_NOTICE_TYPE non è compatibile se non c'è un codice if($this->notice === self::ANY_NOTICE_TYPE) { throw new LogicException("Non puoi istanziare una nuova scheda di tipo ANY"); } } // Preveniamo la creazione di form invalidi if ($this->fields === null) { throw new LogicException("Non è stato possibile accedere alla scheda GUUE #{$codice}"); } if($this->info["modulo"] == "albo_fornitori") { if ($this->enable_forced_suggestions) { // Forziamo il numero di ripetizione dei lotti $this->info["valori"]["settings"]["reps"]["GR-Lot"] = 1; // Impediamo all'utente di modificarlo $this->info["valori"]["settings"]["reps_locked"]["GR-Lot"] = true; // Forziamo il numero di ripetizione dei gruppi di lotti a 0 $this->info["valori"]["settings"]["reps"]["GR-LotsGroup"] = 0; // Impediamo all'utente di modificarlo $this->info["valori"]["settings"]["reps_locked"]["GR-LotsGroup"] = true; } } // Handle user settings $this->handleUserSessionSettings(); } /** * Carica il formato del form attuale * * @param string $index * @return array|null */ private function getNoticeSchema(string $index): ?array { if (file_exists("{$this->current_sdk_path}/notice-types/{$index}.json")) { return jsonToArray("{$this->current_sdk_path}/notice-types/{$index}.json"); } return null; } /** * Ritorna le sezioni nell'ordine del render * * @return array */ public function getReorderedSections(): array { $sections = $this->sections; $found_index = null; foreach ($sections as $index => $section) { // Cerchiamo la sezione organizzazioni if ($section["id"] === "GR-Organisations-Section") { // Teniamo nota dell'indice $found_index = $index; break; } } // Se abbiamo organizzazioni if ($found_index !== null) { // Mettiamola da parte $organizations = $sections[$found_index]; // Cancelliamola dall'array unset($sections[$found_index]); // Rimettiamola ad index 0 array_unshift($sections, $organizations); } return $sections; } /** * Ottiene le sezioni del form attuale * * @return array|null */ private function getSections(): ?array { if (!empty($this->fields)) { foreach ($this->fields["content"] as $index => $content) { if ($content["displayType"] == "SECTION") { $sections[] = [ "index" => $index, "id" => $content["id"], "name" => $content["id"], "type" => $content["displayType"], "object" => $content["contentType"], "_label" => $content["_label"], "_repeatable" => $content["_repeatable"] ?? false, ]; } } return $sections; } return null; } /** * Ottiene le codelist per il campo specificato. Generalmente sono usate per popolare le opzioni di un select * Questo metodo di occupa anche di convertire il formato delle codelist in JSON per questioni di velocità * * @param String $id * @return array|null */ private function getAvailableCodeListFor(String $id): ?array { if (empty($this->codelist[$id])) { return []; } if (file_exists("{$this->current_sdk_path}/jsoncodelists/{$this->codelist[$id]["filename"]}")) { $options = file_get_contents("{$this->current_sdk_path}/jsoncodelists/{$this->codelist[$id]["filename"]}"); $options = json_decode($options, TRUE); } else if (file_exists("{$this->current_sdk_path}/codelists/{$this->codelist[$id]["filename"]}")) { $list = simplexml_load_file("{$this->current_sdk_path}/codelists/{$this->codelist[$id]["filename"]}"); if (!empty($list) && !empty($list->SimpleCodeList->Row)) { foreach ($list->SimpleCodeList->Row as $Row) { $code = $Row->xpath("Value[@ColumnRef='code']"); if (!empty($code) && count($code) == 1) { $values = $Row->xpath("Value[not(@ColumnRef='code') and not(@ColumnRef='Name')]"); if (count($values) > 0) { foreach ($values as $value) { $options[(string) $value["ColumnRef"]][(string) $code[0]->SimpleValue] = (string) $value->SimpleValue; } } } } $opts = json_encode($options, JSON_PRETTY_PRINT); if (!is_dir("{$this->current_sdk_path}/jsoncodelists")) { mkdir("{$this->current_sdk_path}/jsoncodelists", 0750); } file_put_contents("{$this->current_sdk_path}/jsoncodelists/{$this->codelist[$id]["filename"]}", $opts); $opts = null; gc_collect_cycles(); } } switch ($this->language) { case 'it': $language = "ita_label"; if (!empty($options[$language])) { break; } case 'es': $language = "spa_label"; if (!empty($options[$language])) { break; } case 'de': $language = "deu_label"; if (!empty($options[$language])) { break; } case 'en': default: $language = "eng_label"; break; } if (!empty($options) && !empty($options[$language])) { return $options[$language]; } return []; } /** * Verifica una proprietà di un campo, valutando EFX se necessario * * @param array $field Campo da valutare, preso da fields.sjon * @param String $property_name Nome della proprietà * @param int $index Indice del campo per gestire occorrenze multiple * @return array Ritorna un array contenente il risultato della verifica */ private function checkFieldProperty(array $field, String $property_name, int $index): array { // Valore di ritorno $return_value = [ "value" => null, // Effettivo valore della proprietà (null|true|false) "severity" => null, // Severità della proprietà "has_efx" => false, // Se true, questa proprietà è stata verificata usando EFX "error_state" => false, // Se true, questa proprietà non è stata valutata correttamente "message" => null, // Messaggio della proprietà, usato generalmente dalle assertion per mostrare un messaggio di errore ]; // Se non c'è EFX ci sono alcune regole speciali impostabili if (!$this->enable_efx_evaluation) { if ($property_name == "mandatory" && array_key_exists($field["btId"], self::EFX_OFF_DEFAULT_MANDATORY)) { $return_value["value"] = self::EFX_OFF_DEFAULT_MANDATORY[$field["btId"]]; return $return_value; } } if (!empty($field)) { if (!empty($field[$property_name])) { $property = $field[$property_name]; // Credo che severity sia per decidere quale prende priorità ? Per ora sembrano tutti ERROR $return_value["severity"] = $property["severity"]; // Valore di base della proprietà (che può poi essere sovrascritto dai constraint) if ($this->isEFX($property["value"])) { // Questo valore può essere EFX $return_value["has_efx"] = true; $efx_return = $this->evalBoolean($property["value_xpath"], $property["value_context"], $index); // Se EFX ha ritornato null siamo incappati in un errore if ($efx_return === null) { $return_value["error_state"] = true; } else { $return_value["value"] = $efx_return; } } else { $return_value["value"] = $property["value"] ?? false; } // Valutazione dei constraints if (!empty($property["constraints"])) { foreach ($property["constraints"] as $constraint) { $constraint_value = $this->checkConstraint($constraint, $property_name, $index); if ($constraint_value["has_efx"]) { $return_value["has_efx"] = true; } // WORKAROUND if ($constraint_value["error_state"]) { if ($property_name == "forbidden") { if (array_key_exists($field["btId"], self::CONSTRAINT_FAILED_DEFAULT_FORBIDDEN)) { $return_value["value"] = self::CONSTRAINT_FAILED_DEFAULT_FORBIDDEN[$field["btId"]]; return $return_value; } } elseif ($property_name == "mandatory") { if (array_key_exists($field["btId"], self::CONSTRAINT_FAILED_DEFAULT_MANDATORY)) { $return_value["value"] = self::CONSTRAINT_FAILED_DEFAULT_MANDATORY[$field["btId"]]; return $return_value; } } $return_value["error_state"] = true; } if ($constraint_value["value"] !== null) { $return_value["value"] = $constraint_value["value"]; $return_value["message"] = $constraint_value["message"]; $return_value["severity"] = $constraint_value["severity"]; break; } } } } } return $return_value; } /** * Metodo abbastanza crudo per verificare che l'espressione sia EFX * * @param mixed $query * @return boolean */ private static function isEFX($query): bool { if (gettype($query) != "string") return false; return preg_match('/\{.+?(?enable_efx_evaluation) { return null; } $error = null; // Valutiamo la query $result = $this->efx_query_provider->evalBoolean($query, $context, $index, $error); // Se fallisce con tipo sbagliato, wrappiamo tutto in boolean() if ($result === null && $error === XML_XPath20_Querier::XPATH_FAIL_WRONG_TYPE) { $result = $this->efx_query_provider->evalBoolean("boolean($query)", $context, $index, $error); } // Ritorniamo il risultato return $result; } /** * Esegue la valutazione di un constraint * * @param array $constraint Il constraint da valutare * @param string $property_name Il nome della proprietà * @param string $index L'indice dell'elemento per gestire ripetizioni * @return array */ private function checkConstraint(array $constraint, string $property_name, int $index): array { $return_value = [ "error_state" => false, "has_efx" => false, "value" => null, "severity" => null, "message" => null, ]; // Step 1. Capire se il constraint si applica a noi // Controlliamo di avere una notice type list if (!empty($constraint["noticeTypes"])) { // Se lo abbiamo, verifichiamo si riferisca a noi if (!in_array($this->notice, $constraint["noticeTypes"])) { // Non si riferisce a noi return $return_value; } } // Step 2. Valutare se c'è una condizione if (!empty($constraint["condition"])) { $condition = $constraint["condition"]; // Verifichiamo che la condizione sia EFX if (!$this->isEFX($condition)) { throw new Exception("Expected an EFX query, but got {$condition} instead."); } $return_value["has_efx"] = true; $condition_eval = $this->evalBoolean($constraint["condition_xpath"], $constraint["condition_context"], $index); // Se la condizione è falsa, il constraint non si applica e ritorniamo senza errore if ($condition_eval === false) { return $return_value; } // Se abbiamo ricevuto null da EFX qualcosa è andato storto e logghiamo l'errore if ($condition_eval === null) { $return_value["error_state"] = true; return $return_value; } } // Step 3. Valutare il valore della condizione assert(!empty($constraint["value"]), "Invalid constraint structure."); // Value può essere XPath if ($this->isEFX($constraint["value"])) { $return_value["has_efx"] = true; $value_eval = $this->evalBoolean($constraint["value_xpath"], $constraint["value_context"], $index); // Se ritorna null, siamo in stato di errore if ($value_eval === null) { $return_value["error_state"] = true; return $return_value; } // Altrimenti possiamo applicarlo al valore di ritorno $return_value["value"] = $value_eval; $return_value["message"] = $constraint["message"] ?? null; $return_value["severity"] = $constraint["severity"] ?? null; } else { $return_value["value"] = $constraint["value"] ?? false; } return $return_value; } /** * Fa il parsing di una data in maniera automatica a partire dai formati tipi di di Eforms2 e la ritorna nel formato richiesto * * @param String $date_str * @param String $targetFormat * @return string|null */ protected static function FixDate(String $date_str, String $targetFormat): ?string { $idx = strpos($date_str, self::TIMEZONE_SUFFIX); if ($idx !== false) { $date_str = substr($date_str, 0, $idx); } $sourceFormat = self::DetectDateFormat($date_str); if (!$sourceFormat) return null; $date = DateTime::createFromFormat($sourceFormat, $date_str); return $date !== false ? $date->format($targetFormat) : null; } /** * Rileva il formato data di una stringa * * @param String $date_str * @return String */ protected static function DetectDateFormat(String $date_str): ?String { if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $date_str)) return 'Y-m-d'; if (preg_match('/^\d{2}\/\d{2}\/\d{4}$/', $date_str)) return 'm/d/Y'; if (preg_match('/^\d{2}\.\d{2}\.\d{4}$/', $date_str)) return 'd.m.Y'; return null; } /** * Ottiene la versione SDK di questa notice * * @return string */ public function getNoticeSDKVersion(): string { return substr($this->notice_types["sdkVersion"], 0, strlen($this->notice_types["sdkVersion"]) - 2); } /** * Returns the modules that can provide data * * @param string $modulo * @param integer $elemento * @return array */ public static function moduliCollegati() { return [ "gare" => "/gare/guue/integration.php", "pcp" => "/pcp/guue/integration.php", "concorsi" => "/concorsi/guue/integration.php", "albo_fornitori" => "/albo_fornitori/guue/integration.php", ]; } /** * Ritorna le pubblicazioni collegate ad una gara o altro modulo * * @param string $modulo * @param integer $elemento * @return array */ public static function getPubblicazioniFromModulo(string $modulo, int $elemento) { global $pdo; if (!empty(self::moduliCollegati()[$modulo])) { return $pdo->go( "SELECT codice, titolo, numero, anno, numero_ojs, numero_tecnico, form, version, stato, data_trasmissione, data_pubblicazione FROM b_guue WHERE modulo = :modulo AND codice_elemento = :elemento AND soft_delete = 'N'", [":modulo" => $modulo, ":elemento" => $elemento] )->fetchAll(PDO::FETCH_ASSOC); } } /** * Determina che la scheda attuale sia pronta per essere bloccata per ANAC * * @return boolean */ public function canBeReadyForAnac() : bool { return ($this->info["stato"] ?? "") === "DA TRASMETTERE"; } /** * Determina che la scheda attuale sia stata bloccata per ANAC * * @return boolean */ public function isReadyForAnac() : bool { return ($this->info["stato"] ?? "") === "TRASMESSO"; } /** * Determina che la scheda attuale sia trasmissibile ad anac * * @return boolean */ public function isReadyForAnacTransmission() : bool { return ($this->info["stato"] ?? "") === "TRASMESSO" || ($this->info["stato"] ?? "") === "PUBBLICATO"; } /** * Determina che la scheda attuale sia idonea ad essere sbloccata per ANAC * * @return boolean */ public function canBeUnlockedForANAC() : bool { if(empty($this->info["codice"])) return false; if(!$this->isReadyForAnac()) return false; if(empty($this->npa)) return true; $scheda = $this->npa->scheda; // Se la scheda è trasmessa o più e non presenta errori, non si può sbloccare if($scheda["stato"] > 10 && $scheda["errore"] == "NO") { return false; } return true; } public static function nuovaSchedaFromAnac(string $notice, string $titolo, NuovaPiattaformaAppalti $npa) : ?int { $ted = new self($notice, 0, $npa); $ted->setMode("save"); $ted->info["modulo"] = $npa->fascicolo["modulo_riferimento"]; $ted->info["codice_elemento"] = $npa->fascicolo["id_riferimento"]; $ted->info["lotto_riferimento"] = null; if(!empty($npa->scheda["codice_lotto"])) { $codiceLottoNpa = $npa->scheda["codice_lotto"]; if(!empty($codiceLottoNpa)) { $lot = $npa->getLots($codiceLottoNpa) ?? null; if($lot) { $lot = reset($lot); $ted->info["lotto_riferimento"] = $lot["codice"]; } } } $ted->info["eforms2"]["GR-Organisations-Section"] = []; $ted->info["current_section"] = "GR-Organisations-Section"; $ted->info["titolo"] = $titolo; $ted->info["draft"] = "S"; $ted->manageChangeSection(); $ted->setSuggestions(); $ted->save($ted->info); return empty($ted->info["codice"]) ? null : $ted->info["codice"]; } /** * Duplica una scheda GUUE e ritorna il codice in caso di successo * * @param integer $codice Scheda GUUE da duplicare * @param boolean $rectify Se true la scheda viene marchiata come rettifica * @param boolean $copy_data Se true vengono copiati anche i dati * @return integer|null Codice della nuova scheda in caso di successo o NULL in caso di fallimento */ public static function duplicaScheda(int $codice, bool $rectify = false, bool $copy_data = true) : ?int { global $pdo; $record = $pdo->go("SELECT * FROM b_guue WHERE codice = :codice", [":codice" => $codice])->fetch(PDO::FETCH_OBJ); $record->numero_ojs = ""; $record->errori = ""; $record->risposta = ""; $record->stato = "BOZZA"; $record->data_trasmissione = NULL; $record->data_pubblicazione = NULL; $record->utente_modifica = isset($_SESSION["utente"]) ? $_SESSION["utente"]->codice : -1; if($rectify) { $record->valori = json_decode($record->valori, true); $record->valori["eforms2"]["GR-Change"] = [ "values" => [ "2" => [ "BT-758-notice" => $record->numero_tecnico ] ] ]; $record->valori = json_encode($record->valori); } $record->info = json_encode(["rectification" => $rectify]); $record->numero_tecnico = ""; $response = $pdo->go( "INSERT INTO b_guue (codice_ente, modulo, codice_elemento, lotto_riferimento, version, sdk_version, titolo, numero, anno, numero_ojs, numero_tecnico, form, campi, info, valori, xml, errori, risposta, stato, data_trasmissione, utente_modifica, notice_version, notice_id) VALUES (:codice_ente, :modulo, :codice_elemento, :lotto_riferimento, :version, :sdk_version, :titolo, :numero, :anno, :numero_ojs, :numero_tecnico, :form, :campi, :info, :valori, :xml, :errori, :risposta, :stato, :data_trasmissione, :utente_modifica, :notice_version, :notice_id)", [ ":codice_ente" => $record->codice_ente, ":modulo" => $record->modulo, ":codice_elemento" => $record->codice_elemento, ":lotto_riferimento" => $record->lotto_riferimento, ":version" => $record->version, ":sdk_version" => $record->sdk_version, ":titolo" => $record->titolo, ":numero" => $record->numero, ":anno" => $record->anno, ":numero_ojs" => $record->numero_ojs, ":numero_tecnico" => $record->numero_tecnico, ":form" => $record->form, ":campi" => $record->campi, ":info" => $record->info, ":valori" => $copy_data ? $record->valori : "", ":xml" => "", ":errori" => $record->errori, ":risposta" => $record->risposta, ":stato" => $record->stato, ":data_trasmissione" => $record->data_trasmissione, ":utente_modifica" => $record->utente_modifica, ":notice_id" => uuidgen_guue(), ":notice_version" => $record->notice_version + 1, ":utente_modifica" => $record->utente_modifica, ] ); if ($response->errorInfo()[0] == "00000") { $codice = $pdo->lastInsertId(); if ($_SESSION["utente"]->gerarchia > 15) { $permesso = [ "sezione" => "guue", "codice_elemento" => $codice, "codice_gestore" => $_SESSION["ente"]->codice, "funzione" => "0", "codice_utente" => $_SESSION["utente"]->codice, ]; $salva_p = new salva(); $salva_p->debug = false; $salva_p->nome_tabella = "r_permessi"; $salva_p->operazione = "INSERT"; $salva_p->oggetto = $permesso; $salva_p->save(); } return $codice; } return NULL; } /** * Trova le schede GUUE secondo i filtri richiesti * * @return array */ public static function fetchSchede(array $fields = ["*"], array $filters = [], bool $multiple = true): array { $fields = array_filter($fields, function($field) { if($field === "*") return true; if(!myPDO::validFieldName($field)) { throw new Exception("Invalid field name {$field}"); } return true; }, ARRAY_FILTER_USE_BOTH); global $pdo; $fields_str = implode(",", $fields); $bind = []; $filter_str = []; // Aggiungo soft delete $filters["soft_delete"] = ["!=", "S"]; // Per ogni filtro, lo aggiungiamo nei bind e nella query foreach ($filters as $field => $value) { if(!myPDO::validFieldName($field)) { throw new Exception("Invalid field name {$field}"); } $operand = "="; if (is_array($value)) { $operand = $value[0]; $value = $value[1]; } $bind_name = uniqid(); if($operand === "IN" && is_array($value)) { if(!empty($value)) { $in_str = "{$field} IN ("; foreach($value as $idx => $val) { $in_str .= ":{$bind_name}_{$idx},"; $bind[":{$bind_name}_{$idx}"] = $val; } $in_str = rtrim($in_str, ",") . ")"; $filter_str[] = $in_str; } } elseif($operand === "OR" && is_array($value)) { $in_str = "("; foreach($value as $idx => $val) { $subOperand = "="; if(is_array($val)) { $subOperand = $val[0]; $val = $val[1]; } elseif($val === null) { $subOperand = "IS"; } $in_str .= " {$field} {$subOperand} :{$bind_name}_{$idx} OR"; $bind[":{$bind_name}_{$idx}"] = $val; } $in_str = rtrim($in_str, " OR") . ")"; $filter_str[] = $in_str; } else { $bind[":{$bind_name}"] = $value; $filter_str[] = "{$field} $operand :{$bind_name}"; } } // Ricostruiamo la query where if (!empty($filter_str)) { $filter_str = "WHERE " . implode(" AND ", $filter_str); } else { $filter_str = ""; } $limit_str = ""; if (!$multiple) { $limit_str = "LIMIT 1"; } $sql = "SELECT {$fields_str} FROM b_guue {$filter_str} ORDER BY codice ASC {$limit_str}"; $result = $pdo->go($sql, $bind); $schede = $multiple ? $result->fetchAll(PDO::FETCH_ASSOC) : $result->fetch(PDO::FETCH_ASSOC); if (!is_array($schede)) { return []; } return $schede; } }