#!/usr/bin/env bash # STEP045 - Porting Presenze 215.6 # # Fix reale del salvataggio calendario Silvelox. # # Causa confermata dal messaggio STEP044: # Incorrect integer value: '' for column ax_calendar.cal_caus # # Il vecchio database non rigoroso convertiva implicitamente le stringhe # numeriche vuote in 0. MariaDB strict le rifiuta. # # Questa patch: # - NON cambia la struttura del database; # - NON modifica appCtrl.js o i form; # - NON forza causali obbligatorie; # - normalizza solo i campi numerici vuoti/non numerici a 0; # - mantiene invariati i valori numerici validi; # - conserva la diagnostica STEP044. # # Server atteso: 215.6 / ax-accessi set -Eeuo pipefail umask 077 STEP_NAME="STEP045_2156_SILVELOX_CALENDAR_EMPTY_NUMERIC_VALUES_COMPAT_FIX_PATCH_VERIFIED_20260731" EXPECTED_HOST="${EXPECTED_HOST:-ax-accessi}" EXPECTED_IP_FRAGMENT="${EXPECTED_IP_FRAGMENT:-192.168.215.6}" TARGET_ROOT="${TARGET_ROOT:-/var/www/html/presenze.silvelox}" TARGET_FILE="${TARGET_FILE:-$TARGET_ROOT/components/templates/custom_templates/ax_attendance/ax_calendar/ctrl/controllerCtrl.php}" TARGET_DB="${TARGET_DB:-presenze_silvelox}" TARGET_HOST="${TARGET_HOST:-presenze.silvelox.axuno.it}" ALLOW_TEST="${ALLOW_TEST:-0}" SKIP_RELOAD="${SKIP_RELOAD:-0}" SKIP_HTTP="${SKIP_HTTP:-0}" SKIP_DB_TEST="${SKIP_DB_TEST:-0}" FAIL_AFTER_PATCH="${FAIL_AFTER_PATCH:-0}" STAMP="$(date +%Y%m%d_%H%M%S)" OUTPUT_ZIP="$PWD/${STEP_NAME}_${STAMP}.zip" WORKDIR="$(mktemp -d "$PWD/.${STEP_NAME}.XXXXXX")" REPORT="$WORKDIR/${STEP_NAME}_REPORT.txt" BACKUP="$WORKDIR/controllerCtrl.php.before" AFTER="$WORKDIR/controllerCtrl.php.after" PATCHER="$WORKDIR/patch.php" UNIT_CLASS="$WORKDIR/controller_class.php" UNIT_TEST="$WORKDIR/unit_test.php" UNIT_OUT="$WORKDIR/unit_test.out" DB_SQL="$WORKDIR/db_clone_test.sql" DB_OUT="$WORKDIR/db_clone_test.out" CHANGED=0 cleanup() { rm -rf -- "$WORKDIR" } reload_runtime() { if [[ "$SKIP_RELOAD" == "1" ]]; then return 0 fi if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files 2>/dev/null | grep -q '^php5\.6-fpm\.service'; then systemctl reload php5.6-fpm elif command -v service >/dev/null 2>&1; then service php5.6-fpm reload fi apache2ctl configtest apache2ctl graceful } rollback() { if [[ "$CHANGED" == "1" && -f "$BACKUP" ]]; then echo "ROLLBACK: ripristino controller originale." >&2 cp -a -- "$BACKUP" "$TARGET_FILE" reload_runtime >/dev/null 2>&1 || true CHANGED=0 fi } on_error() { local rc=$? local line="${1:-?}" trap - ERR rollback printf 'ERRORE linea %s, codice %s\n' "$line" "$rc" >&2 exit "$rc" } on_interrupt() { trap - ERR rollback exit 130 } trap cleanup EXIT trap 'on_error "$LINENO"' ERR trap on_interrupt INT TERM section() { printf '\n\n================================================================\n%s\n================================================================\n' "$1" >> "$REPORT" } HOST_NOW="$(hostname 2>/dev/null || true)" IPS_NOW="$(hostname -I 2>/dev/null || true)" { echo "$STEP_NAME" echo "DATA: $(date --iso-8601=seconds 2>/dev/null || date)" echo "SERVER: $HOST_NOW" echo "IP: $IPS_NOW" echo "PORTALE: $TARGET_ROOT" echo "FILE: $TARGET_FILE" echo "DATABASE: $TARGET_DB" echo "SCOPO: compatibilita' legacy per campi numerici vuoti" echo "MODIFICA DB: NO" } > "$REPORT" if [[ "$ALLOW_TEST" != "1" ]]; then if [[ "$HOST_NOW" != "$EXPECTED_HOST" && "$IPS_NOW" != *"$EXPECTED_IP_FRAGMENT"* ]]; then echo "ERRORE: server errato; atteso 215.6 / ax-accessi." >&2 exit 10 fi fi [[ -f "$TARGET_FILE" ]] || { echo "ERRORE: controller non trovato: $TARGET_FILE" >&2 exit 11 } PHP_BIN="$(command -v php5.6 || command -v php || true)" [[ -n "$PHP_BIN" ]] || { echo "ERRORE: PHP CLI non disponibile." >&2 exit 12 } section "Pre-check controller STEP044" { "$PHP_BIN" -l "$TARGET_FILE" grep -Fq "private function diagnosticaErroreCalendario(" "$TARGET_FILE" [[ "$(grep -F -c "diagnosticaErroreCalendario(" "$TARGET_FILE")" -eq 4 ]] [[ "$(grep -F -c "private function inserisciEventoUtente(" "$TARGET_FILE")" -eq 1 ]] [[ "$(grep -F -c "private function inserisciEvento(" "$TARGET_FILE")" -eq 1 ]] [[ "$(grep -F -c "private function modificaEvento(" "$TARGET_FILE")" -eq 1 ]] sha256sum "$TARGET_FILE" } >> "$REPORT" 2>&1 cp -a -- "$TARGET_FILE" "$BACKUP" cat > "$PATCHER" <<'PHP' interoCalendario"; if (strpos($data, $already) !== false) { return array($data, false); } $replacement = $marker . convertEol($block, detectEol($data)); return array(str_replace($marker, $replacement, $data), true); } $eol = detectEol($data); $changed = false; if (strpos($data, "private function interoCalendario(") === false) { $marker = " private function ini() {"; if (substr_count($data, $marker) !== 1) { throw new Exception("Punto inserimento helper non trovato"); } $helper = <<<'PHPHELPER' private function interoCalendario($dati, $campo) { if ( !isset($dati->$campo) || $dati->$campo === '' || $dati->$campo === null || !is_numeric($dati->$campo) ) { return 0; } return (int) $dati->$campo; } PHPHELPER; $helper = convertEol($helper, $eol); $data = str_replace($marker, $helper . $marker, $data); $changed = true; } $normalizationBlock = <<<'PHPBLOCK' $causaleId = $this->interoCalendario($dati, 'causale_id'); $causaleLavId = $this->interoCalendario($dati, 'causale_lav_id'); $causaleType = $this->interoCalendario($dati, 'causale_type'); $causaleLavType = $this->interoCalendario( $dati, 'causale_lav_type' ); $interessatoId = $this->interoCalendario( $dati, 'interessato_id' ); $causale_obj = ''; $causale_lav_obj = ''; PHPBLOCK; foreach ( array( 'inserisciEventoUtente', 'inserisciEvento', 'modificaEvento' ) as $functionName ) { list($data, $functionChanged) = insertAtFunctionStart( $data, $functionName, $normalizationBlock ); $changed = $changed || $functionChanged; } $replacements = array( '$destination = $dati->interessato_id;' => '$destination = $interessatoId;', "'\$dati->causale_id'" => "'\$causaleId'", "'\$dati->causale_lav_id'" => "'\$causaleLavId'", "'\$dati->causale_lav_type'" => "'\$causaleLavType'", "'\$dati->causale_type'" => "'\$causaleType'" ); $expectedCounts = array( '$destination = $dati->interessato_id;' => 3, "'\$dati->causale_id'" => 3, "'\$dati->causale_lav_id'" => 3, "'\$dati->causale_lav_type'" => 3, "'\$dati->causale_type'" => 3 ); foreach ($replacements as $old => $new) { $oldCount = substr_count($data, $old); $newCount = substr_count($data, $new); $expected = $expectedCounts[$old]; if ($oldCount === 0 && $newCount === $expected) { continue; } if ($oldCount !== $expected || $newCount !== 0) { throw new Exception( "Struttura inattesa per [" . $old . "]: old=" . $oldCount . " new=" . $newCount ); } $data = str_replace($old, $new, $data, $count); if ($count !== $expected) { throw new Exception( "Sostituzioni errate per [" . $old . "]: " . $count ); } $changed = true; } if ($changed) { if (file_put_contents($file, $data) === false) { fwrite(STDERR, "Impossibile scrivere il controller\n"); exit(4); } echo "PATCH_APPLICATA\n"; } else { echo "PATCH_GIA_PRESENTE\n"; } PHP section "Applicazione patch" CHANGED=1 if PATCH_RESULT="$("$PHP_BIN" "$PATCHER" "$TARGET_FILE" 2>&1)"; then : else rc=$? echo "$PATCH_RESULT" >&2 exit "$rc" fi echo "$PATCH_RESULT" | tee -a "$REPORT" if [[ "$FAIL_AFTER_PATCH" == "1" ]]; then echo "ERRORE DI TEST DOPO PATCH" >&2 false fi section "Verifica statica completa" { "$PHP_BIN" -l "$TARGET_FILE" [[ "$(grep -F -c "private function interoCalendario(" "$TARGET_FILE")" -eq 1 ]] [[ "$(grep -F -c "\$causaleId = \$this->interoCalendario" "$TARGET_FILE")" -eq 3 ]] [[ "$(grep -F -c "\$causaleLavId = \$this->interoCalendario" "$TARGET_FILE")" -eq 3 ]] [[ "$(grep -F -c "\$causaleType = \$this->interoCalendario" "$TARGET_FILE")" -eq 3 ]] [[ "$(grep -F -c "\$causaleLavType = \$this->interoCalendario" "$TARGET_FILE")" -eq 3 ]] [[ "$(grep -F -c "\$interessatoId = \$this->interoCalendario" "$TARGET_FILE")" -eq 3 ]] [[ "$(grep -F -c "'\$causaleId'" "$TARGET_FILE")" -eq 3 ]] [[ "$(grep -F -c "'\$causaleLavId'" "$TARGET_FILE")" -eq 3 ]] [[ "$(grep -F -c "'\$causaleType'" "$TARGET_FILE")" -eq 3 ]] [[ "$(grep -F -c "'\$causaleLavType'" "$TARGET_FILE")" -eq 3 ]] [[ "$(grep -F -c "\$destination = \$interessatoId;" "$TARGET_FILE")" -eq 3 ]] [[ "$(grep -F -c "'\$dati->causale_id'" "$TARGET_FILE" || true)" -eq 0 ]] [[ "$(grep -F -c "'\$dati->causale_lav_id'" "$TARGET_FILE" || true)" -eq 0 ]] [[ "$(grep -F -c "'\$dati->causale_type'" "$TARGET_FILE" || true)" -eq 0 ]] [[ "$(grep -F -c "'\$dati->causale_lav_type'" "$TARGET_FILE" || true)" -eq 0 ]] echo "PHP lint: OK" echo "Tre flussi normalizzati: OK" echo "Diagnostica STEP044 conservata: OK" } >> "$REPORT" 2>&1 cp -a -- "$TARGET_FILE" "$AFTER" section "Unit test del controller reale con custom_engine simulato" head -n -4 "$TARGET_FILE" > "$UNIT_CLASS" cat > "$WORKDIR/a000_ax_include_controller.php" <<'PHP' lastSql = $sql; } public function GetLastInsertId() { return 1; } public function NewExecQueryToArray( $sql, &$array, $mode ) { if (strpos($sql, 'FROM ax_calendar') !== false) { $array = array( array( 'cal_id' => 1, 'cal_stato' => 'APPROVATO' ) ); return; } if (strpos($sql, 'FROM ax_user') !== false) { $array = array( array( 'usrId' => 1, 'usrFirst' => 'Test', 'usrLast' => 'Utente' ) ); return; } $array = array(); } public function NewExecQueryToArrayIndex( $sql, &$array, $mode, $index ) { $array = array(); } } PHP mkdir -p "$WORKDIR/docroot/ax_common" cp -a -- "$WORKDIR/a000_ax_include_controller.php" \ "$WORKDIR/docroot/ax_common/a000_ax_include_controller.php" cat > "$UNIT_TEST" <<'PHP' setAccessible(true); ob_start(); $reflection->invoke($object, $payload); $json = ob_get_clean(); $decoded = json_decode($json, true); if ( !is_array($decoded) || !isset($decoded['result']) || (string) $decoded['result'] !== '1' ) { fwrite(STDERR, "Risposta non valida da " . $method . "\n"); exit(20); } } function assertContains($needle, $haystack, $label) { if (strpos($haystack, $needle) === false) { fwrite( STDERR, $label . " non trovato:\n" . $needle . "\nSQL:\n" . $haystack . "\n" ); exit(21); } } $ctrl = new emptyController(); $ctrl->conn = new custom_engine(); $empty = (object) array( 'inizio' => '2026-07-31', 'fine' => '2026-07-31', 'tipo' => '0', 'feste' => false, 'allDay' => true, 'cal_stato' => 'APPROVATO', 'cal_color' => 'rgba(1,1,1,1)', 'titolo' => 'EVENTO SENZA CAUSALE', 'descrizione' => '', 'note' => '', 'causale_id' => null, 'causale_lav_id' => null, 'causale_type' => null, 'causale_lav_type' => null, 'interessato_id' => null ); invokePrivate($ctrl, 'inserisciEvento', $empty); $sql = $ctrl->conn->lastSql; assertContains( "VALUES ('0','0','0'", $sql, 'Valori destinazione/causali normalizzati' ); assertContains( ",'0','0','", $sql, 'Indici causale normalizzati' ); $selected = (object) array( 'inizio' => '2026-07-31', 'fine' => '2026-07-31', 'tipo' => '1', 'feste' => false, 'allDay' => true, 'cal_stato' => 'APPROVATO', 'cal_color' => 'rgba(1,1,1,1)', 'titolo' => 'FERIE', 'descrizione' => 'FERIE', 'note' => '', 'causale_id' => '11', 'causale_lav_id' => '11', 'causale_type' => '3', 'causale_lav_type' => '3', 'interessato_id' => '56' ); invokePrivate($ctrl, 'inserisciEvento', $selected); $sql = $ctrl->conn->lastSql; assertContains( "VALUES ('56','11','11'", $sql, 'Valori numerici validi preservati' ); assertContains( ",'3','3','", $sql, 'Tipi causale validi preservati' ); $edit = (object) array( 'id' => '1', 'start' => '2026-07-31 00:00:00', 'end' => '2026-07-31 23:59:59', 'tipo' => '0', 'festa' => false, 'allDay' => true, 'cal_stato' => 'APPROVATO', 'backgroundColor' => 'rgba(1,1,1,1)', 'title' => 'EVENTO', 'description' => '', 'note' => '', 'causale_id' => '', 'causale_lav_id' => '', 'causale_type' => '', 'causale_lav_type' => '', 'interessato_id' => '', 'user_attuale_id' => '1' ); invokePrivate($ctrl, 'modificaEvento', $edit); $sql = $ctrl->conn->lastSql; assertContains( "`cal_destination`='0'", $sql, 'Destinazione UPDATE normalizzata' ); assertContains( "`cal_caus`='0'", $sql, 'cal_caus UPDATE normalizzata' ); assertContains( "`cal_caus_work`='0'", $sql, 'cal_caus_work UPDATE normalizzata' ); assertContains( "`cal_time_index_work`='0'", $sql, 'cal_time_index_work UPDATE normalizzata' ); assertContains( "`cal_time_index`='0'", $sql, 'cal_time_index UPDATE normalizzata' ); echo "UNIT_TEST_OK\n"; PHP if "$PHP_BIN" "$UNIT_TEST" "$WORKDIR/docroot" "$UNIT_CLASS" \ > "$UNIT_OUT" 2>&1; then : else cat "$UNIT_OUT" >&2 exit 30 fi cat "$UNIT_OUT" >> "$REPORT" grep -Fxq "UNIT_TEST_OK" "$UNIT_OUT" section "Test MariaDB su tabella clonata" if [[ "$SKIP_DB_TEST" == "1" ]]; then echo "Test DB saltato in ambiente simulato." >> "$REPORT" else MYSQL_BIN="$(command -v mariadb || command -v mysql || true)" [[ -n "$MYSQL_BIN" ]] || { echo "ERRORE: client MariaDB/MySQL non disponibile." >&2 exit 31 } TEST_TABLE="_step045_ax_calendar_${STAMP}_$$" TEST_TABLE="${TEST_TABLE//[^A-Za-z0-9_]/_}" cat > "$DB_SQL" <> "$REPORT" [[ "$HTTP_CODE" =~ ^(200|302)$ ]] else echo "curl non disponibile." >> "$REPORT" fi section "Esito" { echo "PATCH: $PATCH_RESULT" echo "PHP LINT: OK" echo "UNIT TEST CONTROLLER REALE: OK" echo "VALORI VUOTI -> 0: OK" echo "VALORI 56/11/3 PRESERVATI: OK" echo "UPDATE NORMALIZZATO: OK" echo "TEST DB SU CLONE: OK" echo "TABELLA REALE MODIFICATA: NO" echo "DIAGNOSTICA STEP044: CONSERVATA" echo "ESITO: COMPLETATO" } >> "$REPORT" ( cd "$WORKDIR" zip -q -9 "$OUTPUT_ZIP" \ "$(basename "$REPORT")" \ "$(basename "$BACKUP")" \ "$(basename "$AFTER")" \ "$(basename "$UNIT_OUT")" \ "$(basename "$DB_SQL")" \ "$(basename "$DB_OUT")" ) zip -T "$OUTPUT_ZIP" >/dev/null CHANGED=0 echo "STEP045 COMPLETATO" echo "ZIP: $OUTPUT_ZIP"