#!/usr/bin/env bash set -Eeuo pipefail umask 022 # STEP023B - SERVER ATTESO: 215.6 / ax-accessi # # Applica il fix associazioni temporali ai tre controller: # - ax_dipendenti # - ax_dipendenti_1 # - ax_dipendentiold # # Target esclusivo: # /var/www/html/presenze.distribuite # # Silvelox viene verificato solo in lettura. # Gli altri portali custom non vengono toccati. START_DIR="$PWD" STAMP="$(date '+%Y%m%d_%H%M%S')" BASE="STEP023B_2156_DISTRIBUITE_TEMPORAL_ASSOCIATION_ALL_CONTROLLERS_PATCH_TEST_COMMIT_ZIP_ONLY_${STAMP}" WORKDIR="${START_DIR}/.${BASE}_work_$$" OUTZIP="${START_DIR}/${BASE}.zip" LOGFILE="${WORKDIR}/run.log" REPORT="${WORKDIR}/report.txt" BACKUP_DIR="${WORKDIR}/backup" PATCH_DIR="${WORKDIR}/patched" DIFF_DIR="${WORKDIR}/diff" PACKAGED=0 FAILURES=0 WARNINGS=0 COMMITTED=0 ROLLED_BACK=0 EXPECTED_HOST="${STEP_EXPECTED_HOST:-ax-accessi}" EXPECTED_IP="${STEP_EXPECTED_IP:-192.168.215.6}" SKIP_SERVER_CHECK="${STEP_SKIP_SERVER_CHECK:-0}" SOURCE_DIR="${STEP_SOURCE_DIR:-/var/www/html/presenze.silvelox}" TARGET_DIR="${STEP_TARGET_DIR:-/var/www/html/presenze.distribuite}" RELATIVE_FILES=( "components/templates/custom_templates/ax_attendance/ax_dipendenti/ctrl/controllerCtrl.php" "components/templates/custom_templates/ax_attendance/ax_dipendenti_1/ctrl/controllerCtrl.php" "components/templates/custom_templates/ax_attendance/ax_dipendentiold/ctrl/controllerCtrl.php" ) mkdir -p "$WORKDIR" "$BACKUP_DIR" "$PATCH_DIR" "$DIFF_DIR" : >"$LOGFILE" : >"$REPORT" log() { printf '%s\n' "$*" | tee -a "$LOGFILE"; } report() { printf '%s\n' "$*" >>"$REPORT"; } fail() { FAILURES=$((FAILURES + 1)); log "ERRORE: $*"; report "ERRORE: $*"; } warn() { WARNINGS=$((WARNINGS + 1)); log "ATTENZIONE: $*"; report "ATTENZIONE: $*"; } package_results() { local rc="${1:-0}" local zip_rc=0 [[ "$PACKAGED" -eq 1 ]] && return 0 PACKAGED=1 report "" report "Esito finale: errori=${FAILURES}, avvisi=${WARNINGS}, commit=${COMMITTED}, rollback=${ROLLED_BACK}, exit_code=${rc}" rm -f -- "$OUTZIP" if ! command -v zip >/dev/null 2>&1; then rm -rf -- "$WORKDIR" printf '%s\n' "ERRORE: comando zip non disponibile" >&2 return 127 fi (cd "$WORKDIR" && zip -qr "$OUTZIP" .) || zip_rc=$? if [[ "$zip_rc" -eq 0 ]]; then zip -T "$OUTZIP" >>"$LOGFILE" 2>&1 || zip_rc=$? fi rm -rf -- "$WORKDIR" if [[ "$zip_rc" -ne 0 ]]; then rm -f -- "$OUTZIP" printf '%s\n' "ERRORE: creazione/verifica ZIP fallita (rc=${zip_rc})" >&2 return "$zip_rc" fi printf '%s\n' "ZIP CREATO E VERIFICATO: $OUTZIP" } on_exit() { local rc=$? trap - EXIT INT TERM HUP package_results "$rc" || rc=$? exit "$rc" } trap on_exit EXIT trap 'exit 130' INT trap 'exit 143' TERM HUP log "$BASE" log "SERVER ATTESO: 215.6 / ax-accessi" log "SORGENTE SOLA LETTURA: $SOURCE_DIR" log "TARGET ESCLUSIVO: $TARGET_DIR" log "ALTRI PORTALI CUSTOM: NON TOCCATI" log "MODALITA': PATCH + TEST + COMMIT" log "Avvio: $(date --iso-8601=seconds)" ACTUAL_HOST="$(hostname -s 2>/dev/null || hostname)" ACTUAL_IPS="$(hostname -I 2>/dev/null | xargs || true)" { echo "STEP: $BASE" echo "Server atteso: 215.6 / ax-accessi" echo "Sorgente sola lettura: $SOURCE_DIR" echo "Target esclusivo: $TARGET_DIR" echo "Altri portali custom: NON TOCCATI" echo "Modalita': PATCH + TEST + COMMIT" echo "Data: $(date --iso-8601=seconds)" echo "Hostname: $ACTUAL_HOST" echo "IP: $ACTUAL_IPS" echo } >>"$REPORT" for cmd in grep awk wc sort zip hostname xargs tr sha256sum stat cp mv chmod chown diff dirname mkdir rm; do command -v "$cmd" >/dev/null 2>&1 || fail "comando richiesto non disponibile: $cmd" done [[ "$FAILURES" -eq 0 ]] || exit 20 if [[ "$SKIP_SERVER_CHECK" != "1" ]]; then [[ "$ACTUAL_HOST" == "$EXPECTED_HOST" ]] \ || fail "hostname ${ACTUAL_HOST}; previsto ${EXPECTED_HOST}" if [[ -n "$EXPECTED_IP" ]] && ! grep -qw -- "$EXPECTED_IP" <<<"$ACTUAL_IPS"; then fail "IP ${EXPECTED_IP} non presente tra: ${ACTUAL_IPS}" fi fi [[ "$FAILURES" -eq 0 ]] || exit 21 [[ "$SOURCE_DIR" == "/var/www/html/presenze.silvelox" ]] \ || fail "sorgente inattesa: $SOURCE_DIR" [[ "$TARGET_DIR" == "/var/www/html/presenze.distribuite" ]] \ || fail "target inatteso: $TARGET_DIR" [[ -d "$SOURCE_DIR" ]] || fail "sorgente assente: $SOURCE_DIR" [[ -d "$TARGET_DIR" ]] || fail "target assente: $TARGET_DIR" [[ "$FAILURES" -eq 0 ]] || exit 22 PHP_BIN="${STEP_PHP_BIN:-}" if [[ -z "$PHP_BIN" ]]; then if command -v php >/dev/null 2>&1; then PHP_BIN="$(command -v php)" else fail "php CLI non disponibile" exit 23 fi fi PATCHER="${WORKDIR}/patch_temporal_association.php" cat >"$PATCHER" <<'PHP' from), 0, 10) . \' 00:00:00\';'; $fixedTo = '$to = substr(trim((string) $data->to), 0, 10) . \' 23:59:59\';'; $oldFromPattern = <<<'REGEX' ~\$from\s*=\s*\$data->from\s*\.\s*(['"]) 00:00:00\1\s*;~ REGEX; $oldToPattern = <<<'REGEX' ~\$to\s*=\s*\$data->to\s*\.\s*(['"]) 23:59:59\1\s*;~ REGEX; $fixedFromPattern = <<<'REGEX' ~\$from\s*=\s*substr\s*\(\s*trim\s*\(\s*\(string\)\s*\$data->from\s*\)\s*,\s*0\s*,\s*10\s*\)\s*\.\s*(['"]) 00:00:00\1\s*;~ REGEX; $fixedToPattern = <<<'REGEX' ~\$to\s*=\s*substr\s*\(\s*trim\s*\(\s*\(string\)\s*\$data->to\s*\)\s*,\s*0\s*,\s*10\s*\)\s*\.\s*(['"]) 23:59:59\1\s*;~ REGEX; $oldFromCount = preg_match_all($oldFromPattern, $content); $oldToCount = preg_match_all($oldToPattern, $content); $fixedFromCount = preg_match_all($fixedFromPattern, $content); $fixedToCount = preg_match_all($fixedToPattern, $content); if ( $fixedFromCount === 1 && $fixedToCount === 1 && $oldFromCount === 0 && $oldToCount === 0 ) { echo "FIXED\n"; exit(0); } if ( $fixedFromCount === 0 && $fixedToCount === 0 && $oldFromCount === 1 && $oldToCount === 1 ) { if ($checkOnly) { echo "OLD\n"; exit(0); } $content = preg_replace($oldFromPattern, $fixedFrom, $content, 1, $replaceFrom); $content = preg_replace($oldToPattern, $fixedTo, $content, 1, $replaceTo); if ($content === null || $replaceFrom !== 1 || $replaceTo !== 1) { fwrite(STDERR, "Sostituzione fallita in {$file}\n"); exit(5); } if (file_put_contents($file, $content) === false) { fwrite(STDERR, "Scrittura fallita: {$file}\n"); exit(6); } echo "PATCHED\n"; exit(0); } fwrite( STDERR, "Stato ambiguo {$file}: oldFrom={$oldFromCount}, oldTo={$oldToCount}, fixedFrom={$fixedFromCount}, fixedTo={$fixedToCount}\n" ); exit(7); PHP "$PHP_BIN" -l "$PATCHER" >"${WORKDIR}/00_PATCHER_PHP_LINT.txt" 2>&1 \ || fail "php -l fallito sul patcher" [[ "$FAILURES" -eq 0 ]] || exit 24 log "[1/7] Precheck e preparazione dei tre controller" printf 'file_relativo\tstato_silvelox\tstato_target\tsource_hash\ttarget_hash\trisultato_patch\n' \ >"${WORKDIR}/01_PRECHECKS.tsv" declare -A SOURCE_HASHES declare -A TARGET_MODES declare -A TARGET_UIDS declare -A TARGET_GIDS declare -A PATCH_RESULTS for rel in "${RELATIVE_FILES[@]}"; do source_file="${SOURCE_DIR}/${rel}" target_file="${TARGET_DIR}/${rel}" [[ "$source_file" == "${SOURCE_DIR}/"* ]] || fail "sorgente fuori scope: $source_file" [[ "$target_file" == "${TARGET_DIR}/"* ]] || fail "target fuori scope: $target_file" [[ -f "$source_file" ]] || { fail "file sorgente assente: $source_file" continue } [[ -f "$target_file" ]] || { fail "file target assente: $target_file" continue } [[ ! -L "$source_file" ]] || fail "file sorgente symlink: $source_file" [[ ! -L "$target_file" ]] || fail "file target symlink: $target_file" "$PHP_BIN" -l "$source_file" >>"$LOGFILE" 2>&1 \ || fail "php -l sorgente fallito: $source_file" "$PHP_BIN" -l "$target_file" >>"$LOGFILE" 2>&1 \ || fail "php -l target fallito: $target_file" source_state="$("$PHP_BIN" "$PATCHER" --check "$source_file" 2>>"$LOGFILE")" \ || { fail "stato sorgente non riconosciuto: $source_file" continue } target_state="$("$PHP_BIN" "$PATCHER" --check "$target_file" 2>>"$LOGFILE")" \ || { fail "stato target non riconosciuto: $target_file" continue } source_hash="$(sha256sum "$source_file" | awk '{print $1}')" target_hash="$(sha256sum "$target_file" | awk '{print $1}')" SOURCE_HASHES["$rel"]="$source_hash" TARGET_MODES["$rel"]="$(stat -c '%a' "$target_file")" TARGET_UIDS["$rel"]="$(stat -c '%u' "$target_file")" TARGET_GIDS["$rel"]="$(stat -c '%g' "$target_file")" backup_file="${BACKUP_DIR}/${rel}" patch_file="${PATCH_DIR}/${rel}" mkdir -p "$(dirname "$backup_file")" "$(dirname "$patch_file")" cp -a -- "$target_file" "$backup_file" cp -a -- "$target_file" "$patch_file" patch_result="$("$PHP_BIN" "$PATCHER" "$patch_file" 2>>"$LOGFILE")" \ || { fail "patcher fallito: $rel" continue } PATCH_RESULTS["$rel"]="$patch_result" patch_state="$("$PHP_BIN" "$PATCHER" --check "$patch_file" 2>>"$LOGFILE")" \ || { fail "verifica file patchato fallita: $rel" continue } [[ "$patch_state" == "FIXED" ]] \ || fail "file patchato non FIXED: $rel" "$PHP_BIN" -l "$patch_file" >>"$LOGFILE" 2>&1 \ || fail "php -l file patchato fallito: $rel" diff_file="${DIFF_DIR}/$(printf '%s' "$rel" | tr '/' '_').diff" diff -u "$backup_file" "$patch_file" >"$diff_file" || true printf '%s\t%s\t%s\t%s\t%s\t%s\n' \ "$rel" "$source_state" "$target_state" "$source_hash" "$target_hash" "$patch_result" \ >>"${WORKDIR}/01_PRECHECKS.tsv" done [[ "$FAILURES" -eq 0 ]] || exit 25 log "[2/7] Test funzionale della normalizzazione" "$PHP_BIN" -r ' $tests = [ ["2026-07-01", "2026-07-01 00:00:00", "2026-07-01 23:59:59"], ["2026-07-01 08:39:00", "2026-07-01 00:00:00", "2026-07-01 23:59:59"], [" 2026-07-01 08:39:00 ", "2026-07-01 00:00:00", "2026-07-01 23:59:59"] ]; echo "input\tfrom\tto\tesito\n"; foreach ($tests as $test) { $input = $test[0]; $from = substr(trim((string) $input), 0, 10) . " 00:00:00"; $to = substr(trim((string) $input), 0, 10) . " 23:59:59"; $ok = ($from === $test[1] && $to === $test[2]); echo str_replace(["\t", "\r", "\n"], " ", $input), "\t", $from, "\t", $to, "\t", ($ok ? "OK" : "ERRORE"), "\n"; if (!$ok) { exit(10); } } ' >"${WORKDIR}/02_NORMALIZATION_TEST.tsv" 2>>"$LOGFILE" \ || fail "test funzionale normalizzazione fallito" [[ "$FAILURES" -eq 0 ]] || exit 26 rollback_all() { local rel local backup_file local target_file log "ROLLBACK AUTOMATICO DEI TRE CONTROLLER" for rel in "${RELATIVE_FILES[@]}"; do backup_file="${BACKUP_DIR}/${rel}" target_file="${TARGET_DIR}/${rel}" if [[ -f "$backup_file" ]]; then cp -a -- "$backup_file" "$target_file" fi done ROLLED_BACK=1 } log "[3/7] Commit atomico dei controller" for rel in "${RELATIVE_FILES[@]}"; do target_file="${TARGET_DIR}/${rel}" patch_file="${PATCH_DIR}/${rel}" tmp_file="${target_file}.step023b.$$" rm -f -- "$tmp_file" cp -a -- "$patch_file" "$tmp_file" chmod "${TARGET_MODES[$rel]}" "$tmp_file" chown "${TARGET_UIDS[$rel]}:${TARGET_GIDS[$rel]}" "$tmp_file" mv -f -- "$tmp_file" "$target_file" COMMITTED=$((COMMITTED + 1)) done log "[4/7] Postcheck completo" printf 'file_relativo\tstato_finale\tphp_lint\tsource_invariato\n' \ >"${WORKDIR}/03_POSTCHECKS.tsv" POST_OK=1 for rel in "${RELATIVE_FILES[@]}"; do source_file="${SOURCE_DIR}/${rel}" target_file="${TARGET_DIR}/${rel}" final_state="$("$PHP_BIN" "$PATCHER" --check "$target_file" 2>>"$LOGFILE")" \ || { final_state="ERRORE" POST_OK=0 } if "$PHP_BIN" -l "$target_file" >>"$LOGFILE" 2>&1; then lint="OK" else lint="ERRORE" POST_OK=0 fi source_hash_after="$(sha256sum "$source_file" | awk '{print $1}')" if [[ "$source_hash_after" == "${SOURCE_HASHES[$rel]}" ]]; then source_unchanged="SI" else source_unchanged="NO" POST_OK=0 fi [[ "$final_state" == "FIXED" ]] || POST_OK=0 printf '%s\t%s\t%s\t%s\n' \ "$rel" "$final_state" "$lint" "$source_unchanged" \ >>"${WORKDIR}/03_POSTCHECKS.tsv" done if [[ "$POST_OK" -ne 1 ]]; then fail "verifica finale fallita" rollback_all exit 27 fi log "[5/7] Verifica numero controller" [[ "$COMMITTED" -eq 3 ]] || { fail "controller committati=${COMMITTED}; attesi=3" rollback_all exit 28 } log "[6/7] Riepilogo file" printf 'file_relativo\trisultato_patch\n' >"${WORKDIR}/04_FILES_MODIFIED.tsv" for rel in "${RELATIVE_FILES[@]}"; do printf '%s\t%s\n' "$rel" "${PATCH_RESULTS[$rel]}" \ >>"${WORKDIR}/04_FILES_MODIFIED.tsv" done log "[7/7] Esito" { echo "PATCH COMPLETATA" echo "Server: 215.6 / ax-accessi" echo "Target esclusivo: $TARGET_DIR" echo "Controller verificati: ${#RELATIVE_FILES[@]}" echo "Controller committati: $COMMITTED" echo "Stato finale controller: FIXED" echo "Test data semplice: OK" echo "Test data con orario: OK" echo "PHP lint: OK" echo "Silvelox modificato: NO" echo "Altri portali custom toccati: NO" echo "Rollback automatico: non necessario" } >"${WORKDIR}/99_SUMMARY.txt" report "Applicato il fix associazioni temporali ai tre controller di presenze.distribuite." report "Silvelox verificato e non modificato." report "Altri portali custom non toccati." log "PATCH COMPLETATA E VERIFICATA"