$log */ function extractTarGz(string $tmpFile, string $destDir, array &$log): bool { // ── Método 1: PharData ───────────────────────────────────── if (class_exists('PharData')) { $tempDir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'ep_' . uniqid(); @mkdir($tempDir, 0755, true); try { $phar = new PharData($tmpFile); $phar->extractTo($tempDir, null, true); // Identificar el directorio raíz dentro del tar (strip-components=1) $items = array_values(array_diff((array) scandir($tempDir), ['.', '..'])); $srcDir = (count($items) === 1 && is_dir($tempDir . DIRECTORY_SEPARATOR . $items[0])) ? $tempDir . DIRECTORY_SEPARATOR . $items[0] : $tempDir; copyDir($srcDir, $destDir); $log[] = ['ok' => true, 'msg' => 'Archivos extraídos con PharData.']; return true; } catch (Throwable $ex) { $log[] = ['ok' => false, 'msg' => 'PharData: ' . $ex->getMessage() . '. Probando con exec()…']; } finally { deleteRecursive($tempDir); } } // ── Método 2: exec(tar) como reserva ────────────────────── $disabled = array_map('trim', explode(',', (string)(ini_get('disable_functions') ?: ''))); $canExec = function_exists('exec') && function_exists('escapeshellarg') && !in_array('exec', $disabled, true) && !in_array('escapeshellarg', $disabled, true); if ($canExec) { $escapedTmp = escapeshellarg($tmpFile); $escapedDir = escapeshellarg($destDir); exec("tar -xzf {$escapedTmp} --strip-components=1 -C {$escapedDir} 2>&1", $out, $code); if ($code === 0) { $log[] = ['ok' => true, 'msg' => 'Archivos extraídos con tar (exec).']; return true; } $log[] = ['ok' => false, 'msg' => 'tar: ' . trim(implode(' ', $out))]; } return false; } /** * Crea la base de datos ejecutando schema.sql. * * Método primario: PDO (no requiere exec). * Método de reserva: exec(sqlite3 CLI). * * @param array $log */ function createDatabase(string $schema, string $db, array &$log): bool { // ── Método 1: PDO ────────────────────────────────────────── try { $pdo = new PDO('sqlite:' . $db); $pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $sql = (string) file_get_contents($schema); foreach (explode(';', $sql) as $stmt) { $stmt = trim($stmt); if ($stmt !== '') { try { $pdo->exec($stmt); } catch (Throwable $t) { /* ignora sentencias no críticas */ } } } $log[] = ['ok' => true, 'msg' => 'Base de datos creada con PDO.']; return true; } catch (Throwable $ex) { $log[] = ['ok' => false, 'msg' => 'PDO: ' . $ex->getMessage() . '. Probando sqlite3 CLI…']; } // ── Método 2: exec(sqlite3) como reserva ────────────────── $disabled = array_map('trim', explode(',', (string)(ini_get('disable_functions') ?: ''))); $canExec = function_exists('exec') && function_exists('escapeshellarg') && !in_array('exec', $disabled, true) && !in_array('escapeshellarg', $disabled, true); if ($canExec) { exec('sqlite3 ' . escapeshellarg($db) . ' < ' . escapeshellarg($schema) . ' 2>&1', $out, $code); if ($code === 0) { $log[] = ['ok' => true, 'msg' => 'Base de datos creada con sqlite3 CLI.']; return true; } $log[] = ['ok' => false, 'msg' => 'sqlite3 CLI: ' . trim(implode(' ', $out))]; } return false; } function extraFiles(): array { $skip = [INSTALLER, '.', '..']; $files = []; foreach ((array) scandir(INSTALL_DIR) as $f) { if (!in_array($f, $skip, true)) { $files[] = $f; } } return $files; } function fetchJson(string $url): ?array { if (function_exists('curl_init')) { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 15, CURLOPT_FOLLOWLOCATION => true, CURLOPT_USERAGENT => UA, CURLOPT_HTTPHEADER => ['Accept: application/vnd.github+json'], CURLOPT_SSL_VERIFYPEER => true, ]); $body = curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); if (!$body || $code !== 200) { return null; } } else { $ctx = stream_context_create(['http' => [ 'method' => 'GET', 'header' => 'User-Agent: ' . UA . "\r\nAccept: application/vnd.github+json\r\n", 'timeout' => 15, ]]); $body = @file_get_contents($url, false, $ctx); if (!$body) { return null; } } return json_decode((string) $body, true) ?: null; } function downloadFile(string $url, string $dest): bool { if (function_exists('curl_init')) { $fp = @fopen($dest, 'wb'); if (!$fp) { return false; } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_FILE => $fp, CURLOPT_TIMEOUT => 180, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 10, CURLOPT_USERAGENT => UA, CURLOPT_SSL_VERIFYPEER => true, ]); curl_exec($ch); $code = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); curl_close($ch); fclose($fp); return $code === 200 && filesize($dest) > 100; } $ctx = stream_context_create(['http' => [ 'method' => 'GET', 'header' => 'User-Agent: ' . UA . "\r\n", 'timeout' => 180, 'follow_location' => 1, ]]); $data = @file_get_contents($url, false, $ctx); if (!$data || strlen($data) < 100) { return false; } return file_put_contents($dest, $data) !== false; } // ── Comprobaciones del sistema ──────────────────────────────── function systemChecks(): array { $checks = []; // PHP version $checks[] = [ 'group' => 'PHP', 'label' => 'PHP ' . PHP_VERSION, 'req' => 'PHP 8+ recomendado', 'ok' => PHP_MAJOR_VERSION >= 8, 'required' => true, ]; // Extensiones obligatorias foreach (['pdo_sqlite', 'sqlite3', 'fileinfo', 'xmlwriter', 'zip', 'gd'] as $ext) { $checks[] = [ 'group' => 'Extensiones', 'label' => $ext, 'req' => 'extension_loaded', 'ok' => extension_loaded($ext), 'required' => true, ]; } // curl — opcional; necesaria solo para importar feeds RSS externos $checks[] = [ 'group' => 'Extensiones', 'label' => 'curl', 'req' => 'Importación de feeds RSS (opcional)', 'ok' => extension_loaded('curl'), 'required' => false, ]; // Apache mod_rewrite (heurística; null = no determinado) $mrOk = null; if (function_exists('apache_get_modules')) { $mrOk = in_array('mod_rewrite', apache_get_modules(), true); } elseif (!empty($_SERVER['HTTP_MOD_REWRITE'])) { $mrOk = strtolower($_SERVER['HTTP_MOD_REWRITE']) === 'on'; } $checks[] = [ 'group' => 'Servidor', 'label' => 'Apache mod_rewrite', 'req' => 'Rutas amigables de episodios', 'ok' => $mrOk, 'required' => false, ]; // PharData — extracción de .tar.gz sin exec() (recomendado) $pharOk = class_exists('PharData') && extension_loaded('phar'); $checks[] = [ 'group' => 'Servidor', 'label' => 'Extensión phar (PharData)', 'req' => 'Extracción del paquete sin exec()', 'ok' => $pharOk, 'required' => false, // exec() es alternativa válida ]; // exec() + escapeshellarg — alternativa de extracción si PharData no está disponible $disabled = array_map('trim', explode(',', (string)(ini_get('disable_functions') ?: ''))); $execOk = function_exists('exec') && function_exists('escapeshellarg') && !in_array('exec', $disabled, true) && !in_array('escapeshellarg', $disabled, true); $checks[] = [ 'group' => 'Servidor', 'label' => 'Función exec() — alternativa', 'req' => 'Solo necesaria si PharData no está disponible', 'ok' => $execOk, 'required' => false, ]; // Al menos uno de los dos métodos de extracción debe estar disponible $checks[] = [ 'group' => 'Servidor', 'label' => 'Método de extracción disponible', 'req' => 'PharData o exec(tar)', 'ok' => $pharOk || $execOk, 'required' => true, ]; // Directorio escribible $checks[] = [ 'group' => 'Permisos', 'label' => 'Directorio escribible', 'req' => e(INSTALL_DIR), 'ok' => is_writable(INSTALL_DIR), 'required' => true, ]; return $checks; } function canProceed(array $checks): bool { foreach ($checks as $c) { if ($c['required'] && $c['ok'] === false) { return false; } } return true; } // ── Lógica de instalación ───────────────────────────────────── function runInstall(bool $deleteExtra): array { $log = []; $error = ''; // 1. Borrar archivos previos if ($deleteExtra) { foreach (extraFiles() as $f) { deleteRecursive(INSTALL_DIR . DIRECTORY_SEPARATOR . $f); } $log[] = ['ok' => true, 'msg' => 'Archivos previos eliminados.']; } // 2. Obtener info de la última release $release = fetchJson(GH_API); if (!$release || !isset($release['tag_name'])) { return ['ok' => false, 'error' => 'No se pudo obtener la información de GitHub.', 'log' => $log]; } $version = ltrim((string) $release['tag_name'], 'v'); $tarUrl = ''; foreach ($release['assets'] ?? [] as $asset) { if (str_ends_with((string)($asset['name'] ?? ''), '.tar.gz')) { $tarUrl = (string)$asset['browser_download_url']; break; } } if (!$tarUrl) { $tarUrl = (string)($release['tarball_url'] ?? ''); } if (!$tarUrl) { return ['ok' => false, 'error' => 'No se encontró el archivo .tar.gz en la release.', 'log' => $log]; } $log[] = ['ok' => true, 'msg' => 'Release detectada: v' . $version]; // 3. Descargar $tmpFile = INSTALL_DIR . '/ep_install.tar.gz'; if (!downloadFile($tarUrl, $tmpFile)) { @unlink($tmpFile); return ['ok' => false, 'error' => 'No se pudo descargar el paquete de instalación.', 'log' => $log]; } $log[] = ['ok' => true, 'msg' => 'Paquete descargado (' . round(filesize($tmpFile) / 1024) . ' KB).']; // 4. Extraer (PharData preferido; exec como reserva) $extracted = extractTarGz($tmpFile, INSTALL_DIR, $log); // 5. Borrar el tar.gz siempre @unlink($tmpFile); if (!$extracted) { return ['ok' => false, 'error' => 'No se pudo extraer el paquete (PharData y exec fallaron).', 'log' => $log]; } // 6. Crear la base de datos (PDO preferido; sqlite3 CLI como reserva) $schema = INSTALL_DIR . '/schema.sql'; $db = INSTALL_DIR . '/podcast.sqlite'; if (!file_exists($schema)) { return ['ok' => false, 'error' => 'schema.sql no encontrado tras la extracción.', 'log' => $log]; } if (!createDatabase($schema, $db, $log)) { return ['ok' => false, 'error' => 'No se pudo crear la base de datos.', 'log' => $log]; } // 7. Crear directorios de medios @mkdir(INSTALL_DIR . '/audios', 0755, true); @mkdir(INSTALL_DIR . '/images', 0755, true); $log[] = ['ok' => true, 'msg' => 'Directorios audios/ e images/ creados.']; return ['ok' => true, 'error' => '', 'log' => $log]; } // ── Despacho de pasos ───────────────────────────────────────── $step = (string)($_GET['step'] ?? '1'); $done = isset($_GET['done']); $action = (string)($_POST['action'] ?? ''); $result = null; if ($step === '3' && $action === 'install') { $result = runInstall(!empty($_POST['confirm_delete'])); if ($result['ok']) { header('Location: ' . INSTALLER . '?done=1'); exit; } } $checks = systemChecks(); $canGo = canProceed($checks); $extras = ($step === '2') ? extraFiles() : []; // ── HTML ────────────────────────────────────────────────────── $currentStep = $done ? 4 : (int)$step; ?> EasyPodcast — Instalador
EasyPodcast Instalador automático · GitHub Releases
'; foreach ($steps as $i => $label) { $n = $i + 1; if ($n > 1) echo '
'; $cls = ''; if ($done || ($currentStep > $n)) { $cls = 'done'; $icon = '✓'; } elseif ($currentStep === $n) { $cls = 'active'; $icon = (string)$n; } else { $icon = (string)$n; } echo '
' . '' . $icon . '' . '' . e($label) . '' . '
'; } echo '
'; ?>
🎙️
¡Instalación completada!

EasyPodcast se ha instalado correctamente.

El archivo ha sido eliminado automáticamente.
No se pudo eliminar automáticamente. Bórralo manualmente del servidor por seguridad.
Instalación
Directorio de instalación
El directorio está vacío. Listo para instalar.
Se encontraron elemento(s) en el directorio. Deben eliminarse antes de continuar.
Cancelar
Compatibilidad del sistema
' . e($c['group']) . '

'; $lastGroup = $c['group']; } if ($c['ok'] === true) { $iconClass = 'ci-ok'; $icon = '✓'; } elseif ($c['ok'] === false) { $iconClass = 'ci-fail'; $icon = '✗'; } else { // null → no determinado (mod_rewrite) $iconClass = 'ci-warn'; $icon = '?'; } echo ''; } ?>
La extensión curl no está disponible. La importación de feeds RSS no estará disponible tras la instalación. Puedes instalar EasyPodcast sin curl; el resto de funciones funcionará con normalidad. Habilita curl en tu configuración de PHP si necesitas esta funcionalidad.
Todas las comprobaciones obligatorias han pasado.
Resuelve los errores marcados en rojo antes de continuar.

EasyPodcast — instalador automático