gestione gare pubbliche
Non puoi selezionare più di 25 argomenti Gli argomenti devono iniziare con una lettera o un numero, possono includere trattini ('-') e possono essere lunghi fino a 35 caratteri.
 
 
 
 
 

3467 righe
113 KiB

<?php
ini_set('xdebug.max_nesting_level', 500);
include_once __DIR__ . '/XML_XPath20_Querier.class.php';
include_once __DIR__ . '/MyDOMDocument.class.php';
include_once __DIR__ . '/eFormsManager_xml_order.trait.php';
include_once __DIR__ . '/BS4FormGenerator.trait.php';
include_once __DIR__ . '/eFormsManager_render.trait.php';
include_once __DIR__ . '/eFormsManager_suggestions.trait.php';
include_once __DIR__ . '/TedEsenderApiManager.trait.php';
include_once __DIR__ . '/TedEsenderHelpersFuncions.trait.php';
/**
* Questa classe gestisce la compilazione. validazione e trasmissione dei form eNotice ( https://docs.ted.europa.eu/eforms/latest/index.html )
*/
class eFormsManager
{
use eFormsManager_suggestions, eFormsManager_render, eFormsManager_xml_order, TedEsenderHelpersFuncions, TedEsenderApiManager;
/**
* Riferimento a NuovaPiattaformaAppaltio
*
* @var NuovaPiattaformaAppalti|null
*/
public $npa = null;
/**
* La modalità attuale in cui eFormsManager sta girando
*
* @var string
*/
public $mode = "";
/**
* Stabilisce se eFormsManager è in modalità di debug
*
* @var boolean
*/
public $debug = false;
/**
* Stabilisce se è attivo il debug avanzato
*
* @var boolean
*/
public $advanced_debug = false;
/**
* Tutti i dati relativi al form correntemente caricato
*
* @var array
*/
public $info = [];
/**
* ID della notice correntemente caricata
*
* @var ?int
*/
public $notice = null;
/**
* Titolo della notice correntemente caricata
*
* @var string
*/
public $title = "";
/**
* Sezioni della notice caricata
*
* @var array
*/
public $sections = [];
/**
* Sezione correntemente caricata
*
* @var array|null
*/
public $current_section;
/**
* ID della sezione correntemente caricata
*
* @var string|null
*/
public $current_section_id;
/**
* Indice della sottosezione correntemente caricata
*
* @var int|null
*/
public $current_subsection;
/**
* Deserializzazione di notice-types.json
*
* @var array
*/
protected $notice_types;
/**
* Metadata dei campi fornito da fields.json
*
* @var array
*/
private $fields_metadata;
/**
* Metadata della strutture XML fornito da fields.json
*
* @var array
*/
private $xml_structure;
/**
* Dati temporanei per la gestione corretta dei campi unpublish
*
* @var array
*/
private $unpublish_temp_data = [];
/**
* Path dell'SDK di eForms2 caricato
*
* @var string
*/
private $current_sdk_path;
/**
* Deserializzazione delle codelist
*
* @var array
*/
private $codelist;
/**
* Lingua correntemente attiva.
*
* @var string
*/
private $language;
/**
* Traduzioni caricate di eForms
*
* @var array
*/
private $eforms_translations;
/**
* Campi attuali del form
*
* @var array
*/
private $fields;
/**
* Impostazioni attuali del form
*
* @var array
*/
private $settings;
/**
* La struttura XML attuale
*
* @var DOMDocument|string
*/
private $xml;
/**
* Fornisce esecuzione di query XPath 2.0 per la generazione
*
* @var XML_XPath20_Querier $gen_query_provider
*/
private $gen_query_provider;
/**
* Fornisce esecuzione di query XPath 2.0 per la validazione
*
* @var XML_XPath20_Querier $efx_query_provider
*/
private $efx_query_provider;
/**
* Counter interni per le ripetizioni dei campi
*
* @var array
*/
private $field_counters = [];
/**
* Archivio dei namespace XML conosciuti
*
* @var array
*/
private $namespaces = [];
/**
* Stabilisce se il render attuale di XML è finale
*
* @var bool
*/
private $final_xml_render = false;
/**
* Quando è falso, la valutazione di EFX viene disattivata
*
* @var boolean
*/
public $enable_efx_evaluation = true;
/**
* Tiene traccia delle sezioni correntemente complete.
* Viene popolato chiamando $this->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 .= "<a href='#' onclick=\"switchSection('{$schema_section}', 0)\">{$schema_label}</a>";
}
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" . " (<code class='text-dark'>{$schema_id}</code>-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 = <<<QUER
SELECT DATE_ADD(data_aggiornamento, INTERVAL 60 SECOND) - NOW() AS delta, utente_modifica FROM b_guue
WHERE
codice = :codice AND
utente_modifica > 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("<meta http-equiv='refresh' content='0;URL={$basepath}?codice={$codice}&form={$this->info['form']}'>");
}
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('/\{.+?(?<!\\\\)\} \$\{.+?(?<!\\\\)\}/', $query) === 1;
}
/**
* Wrapper per valutare una query efx che debba ritornare true o false
*
* @param String $query Query Xpath 2.0 da eseguire
* @param String $context Contesto Xpath 2.0 della query da eseguire
* @param integer $index Indice di riferimento per gestire ripetizioni
* @return boolean|null
*/
private function evalBoolean(String $query, String $context, int $index): ?bool
{
// Per qualche motivo a volte c'è EFX che è semplicemente true o false. Possiamo gestircelo noi.
if ($query === "true()") {
return true;
}
if ($query === "false()") {
return false;
}
// Se non valutiamo EFX ritorniamo null
if (!$this->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;
}
}