i { display: block; height: 100%; background: #1a73e8; } '; } function ins_page($bodyHtml, $extraHead) { echo '' . '' . 'adminLLM — installazione' . $extraHead . '

adminLLM — installazione

' . $bodyHtml . ''; exit; } /** Vicolo cieco: dice cosa è successo e come rimediare a mano. */ function ins_stop($title, $html) { ins_page('' . $html . '

La via manuale resta valida

' . '

Scarica il pacchetto, copialo via FTP e apri setup.php. ' . 'Le istruzioni stanno in README-install.md dentro il pacchetto. ' . 'Attenzione ai due file che cominciano per punto — .htaccess nella radice e ' . 'adminllm/.htaccess — che molti client FTP non mostrano e non caricano: ' . 'è esattamente il problema che questa pagina esiste per evitare.

', ''); } /** Un bottone che porta al passo successivo. */ function ins_form($step, $label, $hidden) { $h = ''; foreach ($hidden as $k => $v) { $h .= ''; } return '
' . $h . '
'; } /* ---------------------------------------------------------------- * * Rete. * ---------------------------------------------------------------- */ /** * Una GET, con verifica del certificato ACCESA e nessun modo per spegnerla. * Ritorna array('ok'=>bool, 'code'=>int, 'body'=>string, 'error'=>string). */ function ins_get($url, $timeout) { $ch = curl_init(); curl_setopt_array($ch, array( CURLOPT_URL => $url, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $timeout, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_FOLLOWLOCATION => false, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, CURLOPT_USERAGENT => 'adminLLM-installer', )); $body = curl_exec($ch); $err = (string) curl_error($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); // Niente curl_close(): no-op dalla 8.0, deprecata dalla 8.5. if ($body === false) { return array('ok' => false, 'code' => $code, 'body' => '', 'error' => $err !== '' ? $err : 'richiesta non riuscita'); } if ($code !== 200) { return array('ok' => false, 'code' => $code, 'body' => '', 'error' => 'il server ha risposto ' . $code); } return array('ok' => true, 'code' => $code, 'body' => (string) $body, 'error' => ''); } /* ---------------------------------------------------------------- * * Stato fra una richiesta e l'altra. * ---------------------------------------------------------------- */ function ins_state_path() { return INS_STAGE . '/state.json'; } function ins_state_read() { if (!is_file(ins_state_path())) { return array(); } $d = json_decode((string) file_get_contents(ins_state_path()), true); return is_array($d) ? $d : array(); } function ins_state_write($state) { if (!is_dir(INS_STAGE) && !@mkdir(INS_STAGE, 0755, true) && !is_dir(INS_STAGE)) { ins_stop('Non riesco a creare la cartella di lavoro', '

PHP non può scrivere in ' . ins_h(basename(__DIR__)) . '. ' . 'Imposta i permessi della cartella a 0755 (o 0775) via FTP e ricarica.

'); } $tmp = ins_state_path() . '.tmp'; if (@file_put_contents($tmp, json_encode($state)) === false || !@rename($tmp, ins_state_path())) { ins_stop('Non riesco a salvare lo stato dell\'installazione', '

Scrittura fallita in ' . ins_h(INS_STAGE) . '.

'); } } function ins_rmtree($dir) { if (!is_dir($dir)) { return; } $entries = scandir($dir); if ($entries === false) { return; } foreach ($entries as $e) { if ($e === '.' || $e === '..') { continue; } $p = $dir . '/' . $e; if (is_dir($p)) { ins_rmtree($p); } else { @unlink($p); } } @rmdir($dir); } /* ---------------------------------------------------------------- * * Cosa c'è già in questa cartella. * * «Qui c'è già qualcosa, sposta via quello che c'è» è un rifiuto senza una * strada, e chi lo legge resta fermo. I casi sono cinque e vogliono cinque * risposte diverse; uno di loro non è un fastidio ma una perdita di dati. * * WORDPRESS. Le pagine stanno in MySQL, e l'unico che sa renderle in HTML è * WordPress. Il nostro esportatore è un plugin che rende le pagine in loopback: * vuole WP vivo e raggiungibile via HTTP. Appena il front controller prende la * radice del dominio, sul disco restano temi, upload e wp-config.php — il * contenitore senza il contenuto — e quel sito non è più esportabile. Perciò * qui non si prosegue: si spiega, e si danno le tre strade. * * UN SITO QUALUNQUE. Non viene sovrascritto: viene spostato da parte, tutto * intero, prima che i nostri file arrivino. Il buco che questo chiude è che * fino alla 0.15.0 un sito la cui home era index.html passava indisturbato e * finiva sepolto vivo: nessun file cancellato, tutti irraggiungibili, perché * il front controller prende ogni richiesta e serve dalla cartella-versione. * ---------------------------------------------------------------- */ /** La cartella dove finisce il sito che c'era. Non è una cartella temporanea: * quella parola invita qualcuno a fare pulizia. */ define('INS_ASIDE', 'sito-precedente'); /** Dentro quella cartella, dove finiscono i file: il divieto e il LEGGIMI * stanno un piano sopra, così non possono collidere con niente di tuo. */ define('INS_ASIDE_SUB', 'sito'); /** * I nomi che NON si spostano, con il motivo di ciascuno. Pura. * * Una web root non contiene solo il sito: contiene anche cose dell'hosting che * spostare significa rompere, e cose nostre che spostare significa installare * il vuoto. * * index.php e .htaccess NON sono in questa lista, ed è deliberato: sono i due * nomi che stiamo per posare noi, quindi lasciarli dov'erano non vuol dire * conservarli — vuol dire sovrascriverli (rename() su POSIX passa sopra a un * file che c'è, senza dire niente). Sono anche i due file più importanti del * sito di prima. Si spostano come tutto il resto. */ function ins_setaside_skip() { return array( // Ci vive la sfida ACME: spostarla rompe il rinnovo del certificato fra // tre mesi, e nessuno lo collegherà all'installazione di oggi. '.well-known', // Roba del pannello dell'hosting: a volte non spostabile, a volte // necessaria, mai parte del sito. 'cgi-bin', 'stats', 'logs', 'error_docs', // I nostri: questa pagina e quella che le succede. 'install.php', 'setup.php', // La cartella di lavoro col rilascio già scaricato e verificato dentro: // spostarla vorrebbe dire installare il niente. basename(INS_STAGE), ); } /** True se questo nome va lasciato dov'è. Pura. */ function ins_setaside_keeps($name) { if ($name === '.' || $name === '..' || $name === '') { return true; } $low = strtolower($name); foreach (ins_setaside_skip() as $s) { if ($low === strtolower($s)) { return true; } } // Quello che un giro precedente ha già messo al sicuro ci resta: non si // mette da parte una cartella-da-parte dentro un'altra. return strncmp($low, INS_ASIDE, strlen(INS_ASIDE)) === 0; } /** I nomi da spostare, in ordine. Vuoto = qui non c'è nessun sito. */ function ins_setaside_list($dir) { $entries = @scandir($dir); if ($entries === false) { return array(); } $out = array(); foreach ($entries as $e) { if (!ins_setaside_keeps($e)) { $out[] = $e; } } sort($out); return $out; } /** * Che cosa c'è in questa cartella: 'adminllm', 'rilasci', 'wordpress', 'sito', * 'vuota'. L'ordine dei controlli è l'ordine della gravità. */ function ins_folder_kind($dir) { if (is_dir($dir . '/adminllm') || is_file($dir . '/installed.php') || is_file($dir . '/adminllm/var/installed.php')) { return 'adminllm'; } if (is_file($dir . '/latest.json')) { return 'rilasci'; } // Basta UNO dei tre segni: un wp-content/ senza wp-config.php è comunque // un WordPress (spostato, rotto, a metà migrazione) e va trattato come tale. if (is_file($dir . '/wp-config.php') || is_dir($dir . '/wp-content') || is_dir($dir . '/wp-includes')) { return 'wordpress'; } return ins_setaside_list($dir) === array() ? 'vuota' : 'sito'; } /** Dove mettere da parte: sito-precedente, o -2, -3… se ce n'è già una. */ function ins_setaside_dest($dir) { $base = $dir . '/' . INS_ASIDE; if (!file_exists($base)) { return $base; } for ($i = 2; $i <= 99; $i++) { if (!file_exists($base . '-' . $i)) { return $base . '-' . $i; } } return ''; } /** * Il divieto che va accanto ai dati. Dentro la cartella messa da parte può * esserci wp-config.php con le credenziali del database: il front controller * la rende invisibile finché l'.htaccess di radice c'è, ma il modo tipico in * cui questo progetto si è fatto male è proprio l'.htaccess che NON c'è * (client FTP che saltano i dotfile). Un secondo divieto costa una riga. */ function ins_aside_htaccess() { return "# adminLLM — il sito che c'era prima dell'installazione.\n" . "# NIENTE accesso diretto via HTTP: qui dentro possono esserci\n" . "# credenziali di database (wp-config.php) e sorgenti PHP.\n" . "\n" . "# Apache 2.4+\n" . "\n" . " Require all denied\n" . "\n" . "\n" . "# Apache 2.2 (fallback)\n" . "\n" . " Order allow,deny\n" . " Deny from all\n" . "\n"; } /** La frase che rimette in piedi il sito di prima, accanto al sito di prima. */ function ins_aside_readme() { return "Questo era il tuo sito.\n" . "======================\n" . "\n" . "L'installazione di adminLLM ha spostato dentro " . INS_ASIDE_SUB . "/, senza\n" . "cancellare niente, tutto quello che stava nella cartella principale del\n" . "dominio.\n" . "\n" . "PER RIAVERE IL SITO DI PRIMA\n" . " 1. cancella index.php e .htaccess dalla cartella principale;\n" . " 2. riporta fuori il contenuto di " . INS_ASIDE_SUB . "/ (anche i file che\n" . " cominciano per punto: molti client FTP non li mostrano e non li\n" . " copiano, ed e' esattamente cosi' che ci si fa male);\n" . " 3. cancella questa cartella e adminllm/.\n" . "\n" . "PER PORTARE QUESTO SITO DENTRO adminLLM\n" . " dal pannello, Importa: c'e' un canale che legge proprio da qui.\n" . "\n" . "PERCHE' UNA SOTTOCARTELLA\n" . " perche' il tuo sito puo' avere un .htaccess e un LEGGIMI suoi, e\n" . " scriverli sopra sarebbe stato il contrario di quello che questa\n" . " cartella promette. Il divieto e il presente file stanno un piano\n" . " sopra, dove non toccano niente di tuo.\n" . "\n" . "Questa cartella non e' temporanea e nessuno la cancella al posto tuo.\n" . "Resta qui finche' non decidi tu.\n"; } /** * Sposta il sito che c'era in $dest. Non sovrascrive e non cancella: sposta. * Quello che non riesce a spostare lo segnala e lo lascia dov'è — non fa * fallire l'installazione, perché un'installazione a metà è peggio di una * cartella con dentro tre file rimasti. * * Ritorna array('spostati'=>n, 'rimasti'=>array(nomi), 'dest'=>nome, 'motivo'=>''). */ function ins_setaside($dir, $dest) { $out = array('spostati' => 0, 'rimasti' => array(), 'dest' => $dest === '' ? '' : basename($dest), 'motivo' => ''); // Un secondo giro dell'installatore non deve poter inghiottire // l'installazione del primo. if (is_dir($dir . '/adminllm')) { $out['motivo'] = 'c\'è già adminllm/: non sposto niente'; return $out; } if ($dest === '') { $out['motivo'] = 'non trovo un nome libero dove mettere da parte'; $out['rimasti'] = ins_setaside_list($dir); return $out; } $names = array(); foreach (ins_setaside_list($dir) as $e) { if ($e !== basename($dest)) { // la destinazione non si sposta dentro sé stessa $names[] = $e; } } if ($names === array()) { return $out; // niente da spostare: niente cartella vuota } // Il sito va in una SOTTOCARTELLA, e il divieto con il LEGGIMI stanno un // piano sopra. Non è pignoleria: il sito di prima può avere un .htaccess e // un LEGGIMI.txt suoi, e scriverci sopra sarebbe stato il modo più stupido // di perdere qualcosa dentro la cartella che esiste per non perdere niente. $sub = $dest . '/' . INS_ASIDE_SUB; if (!is_dir($sub) && !@mkdir($sub, 0755, true) && !is_dir($sub)) { $out['motivo'] = 'non riesco a creare la cartella dove mettere da parte'; $out['rimasti'] = $names; return $out; } foreach ($names as $e) { if (@rename($dir . '/' . $e, $sub . '/' . $e)) { $out['spostati']++; } else { $out['rimasti'][] = $e; } } if ($out['spostati'] === 0) { @rmdir($sub); // non è rimasto niente dentro: via @rmdir($dest); return $out; } @file_put_contents($dest . '/.htaccess', ins_aside_htaccess()); @file_put_contents($dest . '/LEGGIMI.txt', ins_aside_readme()); return $out; } /* ---------------------------------------------------------------- * * Passo 0 — controlli preliminari. * ---------------------------------------------------------------- */ function ins_checks() { $c = array(); $phpOk = version_compare(PHP_VERSION, '8.0', '>='); $c[] = array('Versione di PHP', $phpOk ? 'ok' : 'no', PHP_VERSION . ' (serve 8.0+)', 'adminLLM richiede PHP 8.0 o più recente. Se il pannello dell\'hosting permette di scegliere ' . 'la versione, alzala e ricarica questa pagina.'); $curl = function_exists('curl_init'); $c[] = array('Estensione cURL', $curl ? 'ok' : 'no', $curl ? 'presente' : 'assente', 'Senza cURL questa pagina non può scaricare niente: resta la via manuale.'); $ssl = extension_loaded('openssl'); $c[] = array('Estensione openssl', $ssl ? 'ok' : 'no', $ssl ? 'presente' : 'assente', 'Serve a verificare la firma del rilascio. Senza, non si può stabilire che i file siano ' . 'quelli giusti — e questa pagina non ha un modo per fare finta di sì.'); $wr = is_writable(__DIR__); $c[] = array('Scrittura nella cartella', $wr ? 'ok' : 'no', $wr ? 'consentita' : 'negata: PHP non può scrivere qui', 'Imposta i permessi di questa cartella a 0755 (o 0775) via FTP.'); // Niente confronto col segnaposto: la timbratura al momento della build lo // sostituirebbe anche qui, e questo controllo sarebbe sempre falso. $key = strpos(INS_PUBKEY, 'BEGIN PUBLIC KEY') !== false; $c[] = array('Chiave di verifica', $key ? 'ok' : 'no', $key ? 'presente in questo file' : 'ASSENTE: questo installatore non è stato preparato', 'Hai preso una copia non pubblicata di install.php. Scaricalo da ' . ins_h(INS_BASE) . '/install.txt (arriva già chiamato install.php).'); // Cosa c'è già qui. Un'installazione presente non si sovrascrive: sarebbe il // modo più rapido di perdere il sito di qualcuno. latest.json accanto vuol // dire che questa copia è finita DENTRO la cartella dei rilasci, e // installare lì seppellirebbe la distribuzione sotto un'installazione. Un // sito qualunque invece NON blocca: si mette da parte, e qui si dice quanto // e dove, prima di cominciare. (WordPress non arriva mai fin qui: ha una // schermata sua in ins_phase_preflight.) $kind = ins_folder_kind(__DIR__); if ($kind === 'adminllm') { $c[] = array('Cosa c\'è qui', 'no', 'c\'è già un\'installazione di adminLLM', 'Questa pagina installa da zero. Per aggiornare, usa il pannello. Se vuoi davvero ' . 'reinstallare, sposta via adminllm/.'); } elseif ($kind === 'rilasci') { $c[] = array('Cosa c\'è qui', 'no', 'questa è la cartella dei rilasci, non un sito da installare', 'C\'è un latest.json accanto: installare qui seppellirebbe la distribuzione sotto ' . 'un\'installazione.'); } elseif ($kind === 'sito') { $n = count(ins_setaside_list(__DIR__)); $c[] = array('Cosa c\'è qui', 'warn', 'c\'è già un sito: ' . $n . ($n === 1 ? ' elemento verrà spostato' : ' elementi verranno spostati') . ' in ' . INS_ASIDE . '/', ''); } else { $c[] = array('Cosa c\'è qui', 'ok', 'cartella vuota', ''); } return $c; } /** * Le tre strade per chi ha WORDPRESS su un dominio solo. Vanno scritte QUI, non * in un file di piano che nessuno legge mentre installa: è il momento in cui * servono. * * Solo per WordPress, e non è una svista. A un sito qualunque queste tre strade * non servono: si installa qui, il sito si mette da parte, e il canale «il sito * che c'era già» lo riporta dentro. Offrirgliele lo mandava a cercare un * sottodominio per un problema che non ha — segnalato dal campo, 18/08/2026, * su un sito di tre pagine importato senza intoppi mentre la pagina diceva che * ci volevano due strade alternative. */ function ins_roads_html() { return '

Le tre strade, in ordine di rischio

' . '

1. Un sottodominio — per esempio prova.tuodominio.it, ' . 'che quasi ogni hosting regala dal pannello. Installi adminLLM lì, WordPress continua a ' . 'servire il sito com\'è, e l\'import lo raggiunge via HTTP come farebbe un visitatore. ' . 'Quando il sito nuovo ti piace, sposti la radice del dominio. ' . 'È la strada consigliata: non rischi niente e non c\'è un momento in cui il sito è giù.

' . '

2. Una sottocartella — adminLLM in /nuovo/, WordPress ' . 'padrone della radice. Stesso vantaggio, ma lo scambio finale è più delicato.

' . '

3. Esportare prima su disco — con WordPress ancora acceso, installa ' . 'il plugin adminLLM export (il bottone qui sotto te lo consegna, preso dal ' . 'rilascio firmato) e usa «Esporta su disco» da Impostazioni → adminLLM export: scrive la ' . 'copia resa del sito in wp-content/adminllm-export/. Poi torni qui e ' . 'installi: quella cartella viene messa da parte con tutto il resto, e nel pannello il ' . 'canale «il sito che c\'era già» la ritrova sul posto.

' . ins_form('wpplugin', '⬇ Scarica il plugin WordPress (.zip)', array()) . '

Da caricare su WordPress così com\'è, da Plugin → Aggiungi plugin → ' . 'Carica plugin. Servono i permalink «belli» attivi. Questo bottone non installa niente ' . 'qui: scarica il plugin dal rilascio, ne verifica firma e impronta, e te lo dà.

'; } /** Vicolo cieco con una strada: qui c'è WordPress e non si prosegue. */ function ins_page_wordpress() { ins_page('' . '

Le tue pagine non stanno in questi file: stanno nel database, e ' . 'l\'unico che sa trasformarle in HTML è WordPress stesso. Se installo adesso, adminLLM ' . 'prende la radice del dominio, WordPress smette di rispondere e quelle pagine non ' . 'sono più esportabili: sul disco restano temi, upload e wp-config.php, ' . 'cioè il contenitore senza il contenuto.

' . '

Non è un file di troppo da spostare: è una porta che si chiude. Perciò mi fermo qui, ' . 'e il tuo sito è esattamente come lo hai lasciato.

' . '

Ho riconosciuto WordPress da wp-config.php, ' . 'wp-content/ o wp-includes/ in questa cartella.

' . ins_roads_html() . '

Quando avrai fatto una di queste tre cose, ricarica questa pagina.

', ''); } function ins_phase_preflight() { // WordPress non è una riga in una tabella di controlli: è un discorso, e // farlo prima di tutto il resto evita di far leggere otto righe verdi a chi // sta per perdere il proprio sito. if (ins_folder_kind(__DIR__) === 'wordpress') { ins_page_wordpress(); } $checks = ins_checks(); $blocked = false; $warned = false; $rows = ''; foreach ($checks as $ck) { if ($ck[1] === 'no') { $blocked = true; } elseif ($ck[1] === 'warn') { $warned = true; } $cls = $ck[1] === 'ok' ? 'pass' : ($ck[1] === 'warn' ? 'warn' : 'fail'); $lbl = $ck[1] === 'ok' ? 'OK' : ($ck[1] === 'warn' ? 'NOTA' : 'NO'); $rows .= '' . $lbl . '' . ins_h($ck[0]) . '' . ins_h($ck[2]) . ($ck[1] === 'no' && $ck[3] !== '' ? '
' . $ck[3] . '' : '') . ''; } $body = '

Questa pagina scarica adminLLM da ' . ins_h(INS_BASE) . ', ne verifica ' . 'la firma e lo installa qui. I file li scrive PHP: i .htaccess non passano ' . 'più per il client FTP.

' . '

Controlli sull\'host

' . $rows . '
'; if ($blocked) { $body .= '' . '

La via manuale resta valida

' . '

Scarica il pacchetto, copialo via FTP e apri setup.php.

'; ins_page($body, ''); } if ($warned) { $names = ins_setaside_list(__DIR__); $show = array_slice($names, 0, 12); $body .= '' . '

Cosa succede quando premi il bottone

' . '

Prima che i file nuovi tocchino terra, tutto quello che c\'è adesso in questa ' . 'cartella viene spostato — non cancellato, non sovrascritto — in ' . '' . ins_h(INS_ASIDE) . '/' . ins_h(INS_ASIDE_SUB) . '/, con accanto un ' . 'LEGGIMI.txt che spiega come rimettere le cose com\'erano. ' . 'Quella cartella resta lì finché non decidi tu.

' . '

Restano dove sono .well-known/ (ci vive il rinnovo del certificato) e ' . 'le cartelle dell\'hosting come cgi-bin/, logs/, ' . 'stats/.

' . '

Da quel momento il sito di adesso non risponde più. adminLLM ' . 'prende ogni richiesta del dominio e serve quello che pubblichi tu: non è un modo per ' . 'affiancare i due siti, è un cambio di inquilino.

' . '

Verrà spostato: ' . implode(', ', array_map('ins_h', $show)) . '' . (count($names) > count($show) ? ' e altri ' . (count($names) - count($show)) : '') . '.

' . '

E il sito che c\'è adesso?

' . '

Non lo perdi e non devi rifarlo a mano. Appena installato, nel pannello trovi il ' . 'canale d\'importazione «Il sito che c\'era già», che legge proprio ' . 'da ' . ins_h(INS_ASIDE) . '/: pagine, immagini e fogli di stile entrano ' . 'nella copia di lavoro, li guardi in anteprima e li pubblichi. La cartella messa da ' . 'parte non viene nemmeno svuotata — i file si copiano, non si spostano.

' . '

L\'unica cosa che non torna indietro è il tempo fra adesso e quando ' . 'pubblichi: in mezzo il dominio serve adminLLM, non il sito di prima. Se non te lo ' . 'puoi permettere, installa adminLLM su un sottodominio ' . '(prova.tuodominio.it, che quasi ogni hosting regala dal pannello), ' . 'importa il sito da lì con il crawler, e sposta la radice del dominio solo quando sei ' . 'pronto.

' . ins_form('manifest', 'Ho capito: metti da parte il sito e installa', array()); ins_page($body, ''); } $body .= '' . ins_form('manifest', 'Scarica e verifica il rilascio', array()); ins_page($body, ''); } /* ---------------------------------------------------------------- * * Passo 1 — manifesto e firma. * ---------------------------------------------------------------- */ /** * Scarica latest.json, il manifesto e la firma, e VERIFICA. Ritorna * array('version'=>string, 'man'=>array); se qualcosa non torna non ritorna * affatto: si ferma con la pagina che spiega. * * Sta qui e non dentro ins_phase_manifest() perché ha due chiamanti: * l'installazione, e il bottone che consegna il plugin WordPress a chi non può * installare. Un secondo pezzo di codice che verifica una firma è un secondo * posto dove sbagliarlo. */ function ins_manifest_verified() { $latest = ins_get(INS_BASE . '/latest.json', 20); if (!$latest['ok']) { ins_stop('Non riesco a raggiungere il server dei rilasci', '

' . ins_h($latest['error']) . '

' . '

Se l\'host blocca le connessioni in uscita, questa strada è chiusa: ' . 'nessuna pagina può scaricare quello che il server non lascia passare.

'); } $l = json_decode($latest['body'], true); $version = is_array($l) && isset($l['version']) ? (string) $l['version'] : ''; if (!preg_match('/^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$/', $version)) { ins_stop('Il puntatore dei rilasci non è leggibile', '

latest.json non contiene una versione valida.

'); } $mf = ins_get(INS_BASE . '/' . $version . '/manifest.json', 20); $sg = ins_get(INS_BASE . '/' . $version . '/manifest.sig', 20); if (!$mf['ok'] || !$sg['ok']) { ins_stop('Manifesto o firma non scaricabili', '

' . ins_h($mf['ok'] ? $sg['error'] : $mf['error']) . '

'); } // Si verifica sui BYTE ricevuti, mai su una riserializzazione. $sig = base64_decode(trim($sg['body']), true); $ver = $sig === false ? -1 : openssl_verify($mf['body'], $sig, INS_PUBKEY, INS_ALGO); if ($ver !== 1) { ins_stop('FIRMA NON VALIDA — installazione interrotta', '

Il manifesto di ' . ins_h($version) . ' non è firmato dalla chiave che ' . 'questo installatore riconosce. Non proseguo, e non c\'è un modo per farlo proseguire: ' . 'o i file sono stati manomessi, o questo install.php è vecchio e la chiave di firma è ' . 'cambiata. Riscarica l\'installatore da ' . ins_h(INS_BASE) . '/install.txt.

'); } $man = json_decode($mf['body'], true); if (!is_array($man) || !isset($man['files']) || !is_array($man['files']) || count($man['files']) === 0) { ins_stop('Manifesto illeggibile', '

La firma è valida ma il contenuto non si capisce.

'); } return array('version' => $version, 'man' => $man); } function ins_phase_manifest() { $v = ins_manifest_verified(); $version = $v['version']; $man = $v['man']; $bytes = 0; foreach ($man['files'] as $f) { $bytes += (int) $f['bytes']; } ins_state_write(array( 'version' => $version, 'generated_at' => isset($man['generated_at']) ? (string) $man['generated_at'] : '', 'files' => $man['files'], 'done' => 0, 'bytes' => $bytes, )); $body = '' . '' . '' . '' . '' . '
Versione' . ins_h($version) . '
File' . count($man['files']) . '
Dimensione' . ins_h(ins_human($bytes)) . '
Costruito il' . ins_h(isset($man['generated_at']) ? $man['generated_at'] : '?') . '
' . '

Ogni file verrà controllato con la sua impronta scritta dentro il manifesto appena ' . 'verificato.

' . ins_form('files', 'Scarica i file', array()); ins_page($body, ''); } /** * Uno zip con dentro un file solo, senza compressione (metodo 0) e senza * l'estensione zip: qui non c'è un motore da cui prenderla in prestito, e * WordPress accetta benissimo uno zip non compresso. Stessa costruzione di * adminllm_zip_single() nel motore. */ function ins_zip_single($name, $bytes, $mtime) { $t = $mtime > 0 ? $mtime : 315532800; // epoca DOS: 1980-01-01 $dosTime = ((int) gmdate('G', $t) << 11) | ((int) gmdate('i', $t) << 5) | ((int) gmdate('s', $t) >> 1); $dosDate = (((int) gmdate('Y', $t) - 1980) << 9) | ((int) gmdate('n', $t) << 5) | (int) gmdate('j', $t); $crc = crc32($bytes); $n = strlen($name); $sz = strlen($bytes); $local = pack('VvvvvvVVVvv', 0x04034b50, 20, 0, 0, $dosTime, $dosDate, $crc, $sz, $sz, $n, 0) . $name . $bytes; $cd = pack('VvvvvvvVVVvvvvvVV', 0x02014b50, 20, 20, 0, 0, $dosTime, $dosDate, $crc, $sz, $sz, $n, 0, 0, 0, 0, 0, 0) . $name; $eocd = pack('VvvvvVVv', 0x06054b50, 0, 0, 1, 1, strlen($cd), strlen($local), 0); return $local . $cd . $eocd; } /** * Consegna il plugin WordPress, preso dal rilascio firmato e verificato come * ogni altro file. * * Perché sta in un installatore che si rifiuta di installare. La terza strada * offerta a chi ha WordPress su un dominio solo è «esporta prima su disco», e * per esportare serve il plugin — che finora si scaricava dal pannello di * un'installazione di adminLLM. Ma chi arriva qui NON HA un'installazione: è * qui proprio per farne una. Senza questo bottone quella strada è un consiglio * che non si può seguire. * * Niente tocca il disco: il file passa dalla rete alla risposta. */ function ins_phase_wpplugin() { $v = ins_manifest_verified(); $want = 'adminllm/tools/adminllm-wp-export.php'; $entry = null; foreach ($v['man']['files'] as $f) { if ((string) $f['path'] === $want) { $entry = $f; break; } } if ($entry === null || !preg_match('/^[0-9a-f]{64}$/', (string) $entry['sha256'])) { ins_stop('Il plugin non è in questo rilascio', '

Il rilascio ' . ins_h($v['version']) . ' non contiene ' . '' . ins_h($want) . '. Segnalalo.

'); } $r = ins_get(INS_BASE . '/' . $v['version'] . '/files/' . $entry['sha256'], 60); if (!$r['ok']) { ins_stop('Non riesco a scaricare il plugin', '

' . ins_h($r['error']) . '

'); } if (hash('sha256', $r['body']) !== $entry['sha256']) { ins_stop('Il plugin non corrisponde alla sua impronta', '

È arrivato diverso da come il manifesto firmato lo descrive. Non te lo consegno.

'); } $zip = ins_zip_single('adminllm-wp-export/adminllm-wp-export.php', $r['body'], time()); header('Content-Type: application/zip'); header('Content-Disposition: attachment; filename="adminllm-wp-export.zip"'); header('Content-Length: ' . (string) strlen($zip)); echo $zip; exit; } function ins_human($b) { return $b >= 1048576 ? round($b / 1048576, 1) . ' MB' : round($b / 1024) . ' kB'; } /* ---------------------------------------------------------------- * * Passo 2 — scaricamento ripartibile. * ---------------------------------------------------------------- */ function ins_phase_files() { $state = ins_state_read(); if (!isset($state['files'])) { ins_stop('Stato dell\'installazione perduto', '

Ricomincia ricaricando questa pagina.

'); } $files = $state['files']; $total = count($files); $i = (int) $state['done']; $start = time(); // Il ciclo si ferma da solo prima che ci pensi max_execution_time, e la // pagina si richiama: è la stessa forma del ciclo a passi dell'import, già // provata su questi host. while ($i < $total && (time() - $start) < INS_BUDGET) { $f = $files[$i]; $path = (string) $f['path']; // Un percorso del manifesto non può uscire dalla cartella di lavoro, // per quanto il manifesto sia firmato: la firma dice chi l'ha scritto, // non che sia sensato. if (strpos($path, '..') !== false || strpos($path, "\0") !== false || substr($path, 0, 1) === '/' || !preg_match('/^[0-9a-f]{64}$/', (string) $f['sha256'])) { ins_stop('Percorso non ammesso nel manifesto', '

' . ins_h($path) . '

'); } // Si scarica il BLOB, che sul server si chiama come la propria impronta. // I file del rilascio non stanno lì col loro nome perché un .php nella // radice web di un server con PHP viene eseguito invece che servito. $r = ins_get(INS_BASE . '/' . $state['version'] . '/files/' . $f['sha256'], 60); if (!$r['ok']) { ins_stop('Scaricamento interrotto su ' . $path, '

' . ins_h($r['error']) . '

' . '

Ricarica la pagina: riparte da dove si era fermata.

'); } if (hash('sha256', $r['body']) !== $f['sha256']) { ins_stop('Un file non corrisponde alla sua impronta', '

' . ins_h($path) . ' è arrivato diverso da come il manifesto ' . 'firmato lo descrive. Non proseguo.

'); } $dest = INS_STAGE . '/files/' . $path; $dir = dirname($dest); if (!is_dir($dir) && !@mkdir($dir, 0755, true) && !is_dir($dir)) { ins_stop('Non riesco a creare ' . $dir, ''); } if (@file_put_contents($dest, $r['body']) === false) { ins_stop('Non riesco a scrivere ' . $path, ''); } $i++; } $state['done'] = $i; ins_state_write($state); $pct = $total > 0 ? (int) round($i * 100 / $total) : 100; $body = '

Scaricati ' . $i . ' file su ' . $total . '.

' . '
'; if ($i < $total) { // Continua da sola; il bottone è per chi ha il JavaScript spento. $body .= '
' . '
' . '

Va avanti da sola. Non chiudere la pagina.

'; ins_page($body, ''); } $body .= '' . ins_form('install', 'Metti a posto i file', array()); ins_page($body, ''); } /* ---------------------------------------------------------------- * * Passo 3 — dalla cartella di lavoro alla radice. * ---------------------------------------------------------------- */ function ins_move($from, $to) { $entries = scandir($from); if ($entries === false) { return false; } foreach ($entries as $e) { if ($e === '.' || $e === '..') { continue; } $src = $from . '/' . $e; $dst = $to . '/' . $e; if (is_dir($src)) { if (!is_dir($dst) && !@mkdir($dst, 0755, true) && !is_dir($dst)) { return false; } if (!ins_move($src, $dst)) { return false; } } elseif (!@rename($src, $dst)) { return false; } } return true; } /** * Lascia in var/ l'elenco di cosa è stato installato: versione e impronta di * ogni file. È quello che permetterà a un aggiornamento di sapere quali file * appartengono al motore — e quindi di accorgersi che un rilascio nuovo ne ha * TOLTO uno, che è l'unica cosa che non si può dedurre dal manifesto nuovo. * * Il file porta la stessa riga di guardia dello store del motore, scritta qui a * mano: questa pagina non carica il motore apposta, e non può chiamare * adminllm_store_write(). La riga deve restare identica a ADMINLLM_STORE_GUARD * in adminllm/lib/store.php. */ function ins_write_installed($state) { $files = array(); foreach ($state['files'] as $f) { $files[(string) $f['path']] = (string) $f['sha256']; } ksort($files); $rec = array( 'version' => (string) $state['version'], 'generated_at' => isset($state['generated_at']) ? (string) $state['generated_at'] : '', 'installed_at' => gmdate('c'), 'files' => $files, ); $guard = ''; $dir = __DIR__ . '/adminllm/var'; if (!is_dir($dir)) { @mkdir($dir, 0775, true); } $json = json_encode($rec, 128 | 256 | 64); // PRETTY_PRINT|UNESCAPED_UNICODE|UNESCAPED_SLASHES @file_put_contents($dir . '/installed.php', $guard . "\n" . $json . "\n"); } /** Cosa è successo al sito che c'era, detto in chiaro nella pagina finale. */ function ins_aside_report($aside) { if ($aside['spostati'] === 0 && $aside['rimasti'] === array()) { return ''; // non c'era niente: niente da raccontare } $h = '

Il sito che c\'era

'; if ($aside['spostati'] > 0) { $h .= '' . '

Un piano sopra, in ' . ins_h($aside['dest']) . '/, trovi ' . 'LEGGIMI.txt con le tre righe che rimettono tutto com\'era e un ' . '.htaccess che tiene la cartella fuori dal web. ' . 'Non è una cartella temporanea: resta lì finché non decidi tu.

' . '

Per riportare quel sito dentro adminLLM: dal pannello, ' . 'Importa → «Il sito che c\'era già». Legge proprio da lì, copia ' . 'senza spostare, e ti fa scegliere cosa tenere.

'; } if ($aside['motivo'] !== '') { $h .= ''; } if ($aside['rimasti'] !== array()) { $h .= '' . '

' . implode(', ', array_map('ins_h', $aside['rimasti'])) . '. Nessuno di loro verrà servito (ogni richiesta passa dal front controller), ma se ' . 'contengono password — un wp-config.php, per dire — spostali a mano via FTP.

'; } return $h; } function ins_phase_install() { $state = ins_state_read(); if (!isset($state['files']) || (int) $state['done'] !== count($state['files'])) { ins_stop('Lo scaricamento non è completo', '

Ricarica la pagina e lascia finire il passo precedente.

'); } // Il sito che c'era si mette da parte QUI, fra la verifica e la posa: i file // nuovi sono già scaricati e controllati uno per uno, quindi la finestra in // cui il dominio non serve niente dura i secondi di un rename e non i minuti // di uno scaricamento — e se lo scaricamento fosse fallito, il sito di prima // non sarebbe stato toccato affatto. // // Prima di ins_write_installed(), che crea adminllm/var/: dopo, la guardia // dentro ins_setaside() vedrebbe adminllm/ e si rifiuterebbe di spostare. $aside = ins_setaside(__DIR__, ins_setaside_dest(__DIR__)); // Il verbale di cosa si sta installando, scritto PRIMA dello spostamento: // dopo, i file sono al loro posto e questa pagina ha già finito il suo // lavoro; se muore fra le due cose, meglio un verbale in più che uno in meno // (un aggiornamento lo rifà, un aggiornamento senza non sa cosa togliere). ins_write_installed($state); if (!ins_move(INS_STAGE . '/files', __DIR__)) { ins_stop('Non riesco a spostare i file al loro posto', '

I file scaricati sono in ' . ins_h(basename(INS_STAGE)) . '/files/: ' . 'puoi spostarli a mano via FTP e poi aprire setup.php.

'); } ins_rmtree(INS_STAGE); $htRoot = is_file(__DIR__ . '/.htaccess'); $htEngine = is_file(__DIR__ . '/adminllm/.htaccess'); $setup = is_file(__DIR__ . '/setup.php'); $body = '' . '' . '' . '' . '' . '' . '
' . ($htRoot ? 'OK' : 'NO') . '.htaccess nella radice' . ($htRoot ? 'scritto' : 'MANCA') . '
' . ($htEngine ? 'OK' : 'NO') . 'adminllm/.htaccess' . ($htEngine ? 'scritto' : 'MANCA') . '
' . '

Sono i due file che il client FTP saltava: qui li ha scritti PHP.

' . ins_aside_report($aside); if (!$setup) { ins_stop('setup.php non è arrivato', '

Il rilascio non contiene setup.php: segnalalo.

'); } // Questa pagina ha finito. Si toglie di mezzo PRIMA di mandare a setup.php, // così non resta in giro una pagina che sa installare. $gone = @unlink(__FILE__) && !is_file(__FILE__); $body .= $gone ? '

Questa pagina si è cancellata da sola.

' : ''; $body .= '

Ultimo passo

' . '

Ora tocca a setup.php: crea le cartelle dati, la prima versione del sito ' . 'e il tuo account.

' . '

'; ins_page($body, ''); } /* ---------------------------------------------------------------- * * Instradamento. * ---------------------------------------------------------------- */ // Tutto quello che segue ESEGUE: da qui in giù la pagina lavora e termina il // processo. Chi vuole solo le funzioni — tools/test-installer.php — definisce // INS_NO_RUN prima di includere questo file e si prende le funzioni pure senza // far partire un'installazione. Nessun altro effetto: sul server la costante // non è definita e il dispatcher gira come sempre. if (!defined('INS_NO_RUN')) { // La versione di PHP si controlla PRIMA di qualunque altra cosa: su un host // troppo vecchio deve uscire una frase, non un errore. if (version_compare(PHP_VERSION, '8.0', '<')) { ins_stop('PHP troppo vecchio: ' . PHP_VERSION . ' (serve almeno 8.0)', '

Se il pannello di controllo dell\'hosting permette di scegliere la versione di PHP, ' . 'alzala e ricarica questa pagina.

'); } if (strpos(INS_BASE, 'https://') !== 0) { ins_stop('Origine non sicura', '

L\'installatore scarica solo via HTTPS.

'); } $step = isset($_POST['step']) ? (string) $_POST['step'] : ''; if ($step === 'manifest') { ins_phase_manifest(); } elseif ($step === 'files') { ins_phase_files(); } elseif ($step === 'install') { ins_phase_install(); } elseif ($step === 'wpplugin') { ins_phase_wpplugin(); } ins_phase_preflight(); }