'
. $bodyHtml . '';
exit;
}
/** Vicolo cieco: dice cosa è successo e come rimediare a mano. */
function ins_stop($title, $html)
{
ins_page('
' . ins_h($title) . '
' . $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 '';
}
/* ---------------------------------------------------------------- *
* 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.
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('
Qui c\'è WordPress: non installo.
'
. '
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 .= '
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 .= '
Manca qualcosa di necessario: correggi e ricarica.
'
. '
La via manuale resta valida
'
. '
Scarica il pacchetto, copialo via FTP e apri setup.php.
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.
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 .= '
Tutto a posto.
'
. 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',
'
');
}
// 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.
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.
');
}
$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.
';
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 .= '
Tutti i file scaricati e verificati.
'
. 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 .= '
Spostati ' . (int) $aside['spostati']
. ' elementi in ' . ins_h($aside['dest']) . '/' . ins_h(INS_ASIDE_SUB)
. '/. Niente è stato cancellato.
'
. '
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 .= '
' . ins_h($aside['motivo']) . '
';
}
if ($aside['rimasti'] !== array()) {
$h .= '
Non sono riuscito a spostare '
. count($aside['rimasti']) . ' elementi: sono rimasti dov\'erano.
'
. '
' . 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.
File installati: versione ' . ins_h($state['version']) . '.
'
. '
'
. '
' . ($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.
'
: '
Non sono riuscito a cancellare install.php: '
. 'toglilo tu via FTP.
';
$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', '