File "ManualUpdate.php"
Full Path: /var/www/html/back/vendor/psy/psysh/src/ManualUpdater/ManualUpdate.php
File size: 11.2 KB
MIME-type: text/x-php
Charset: utf-8
<?php
/*
* This file is part of Psy Shell.
*
* (c) 2012-2025 Justin Hileman
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Psy\ManualUpdater;
use Psy\ConfigPaths;
use Psy\Configuration;
use Psy\Exception\ErrorException;
use Psy\Exception\InvalidManualException;
use Psy\VersionUpdater\Downloader;
use Symfony\Component\Console\Helper\QuestionHelper;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Question\ConfirmationQuestion;
/**
* Manual update command.
*
* If a new manual version is available, this command will download and install it.
*/
class ManualUpdate
{
const SUCCESS = 0;
const FAILURE = 1;
/** @var array{checker: Checker, installer: Installer}[] */
private array $updates;
private ?Downloader $downloader = null;
/**
* @param array{checker: Checker, installer: Installer} ...$updates Update configuration(s)
*/
public function __construct(array ...$updates)
{
$this->updates = $updates;
}
/**
* Create a ManualUpdate instance from Configuration and command-line input.
*
* @param Configuration $config Configuration instance
* @param InputInterface $input Input interface
* @param OutputInterface $output Output interface
*
* @return self
*/
public static function fromConfig(Configuration $config, InputInterface $input, OutputInterface $output): self
{
$lang = $input->getOption('update-manual') ?: null;
// Clear the manual update cache when explicitly running --update-manual
$cacheFile = $config->getManualUpdateCheckCacheFile();
if ($cacheFile && \file_exists($cacheFile)) {
@\unlink($cacheFile);
}
// Get current manual language before potentially deleting files
$currentLang = null;
$removedInvalidSqlite = false;
$manualFile = $config->getManualDbFile();
if ($manualFile && \file_exists($manualFile)) {
try {
$manual = $config->getManual();
if ($manual) {
$currentMeta = $manual->getMeta();
$currentLang = $currentMeta['lang'] ?? null;
}
} catch (InvalidManualException $e) {
$removedInvalidSqlite = \substr($e->getManualFile(), -7) === '.sqlite';
self::handleInvalidManual($e, $input, $output);
}
}
$dataDir = $config->getManualInstallDir();
if ($dataDir === false) {
throw new \RuntimeException('Unable to find a writable data directory for manual installation');
}
$phpManualPath = $dataDir.'/php_manual.php';
$sqliteManualPath = $dataDir.'/php_manual.sqlite';
$formats = self::getFormatsToUpdate(
$input,
$output,
\file_exists($phpManualPath),
\file_exists($sqliteManualPath),
$removedInvalidSqlite,
$sqliteManualPath
);
// Build update configurations for selected formats
$checkerLang = $lang ?: $currentLang ?: 'en';
$updates = [];
foreach ($formats as $format) {
$path = $format === 'php' ? $phpManualPath : $sqliteManualPath;
$meta = self::getManualMeta($path);
$updates[] = [
'checker' => new GitHubChecker($checkerLang, $format, $meta['version'] ?? null, $meta['lang'] ?? null),
'installer' => new Installer($dataDir, $format),
];
}
return new self(...$updates);
}
/**
* Allow the downloader to be injected for testing.
*
* @return void
*/
public function setDownloader(Downloader $downloader)
{
$this->downloader = $downloader;
}
/**
* Get the currently set Downloader or create one based on the capabilities of the php environment.
*
* @throws ErrorException if a downloader cannot be created for the php environment
*/
private function getDownloader(): Downloader
{
if (!isset($this->downloader)) {
return Downloader\Factory::getDownloader();
}
return $this->downloader;
}
/**
* Update the manual installation.
*/
public function run(InputInterface $input, OutputInterface $output): int
{
foreach ($this->updates as $update) {
if (!$update['installer']->isDataDirWritable()) {
$output->writeln('<error>Data directory is not writable.</error>');
return self::FAILURE;
}
}
$downloader = $this->getDownloader();
$downloader->setTempDir(\sys_get_temp_dir());
$installed = [];
// Download and install each format
foreach ($this->updates as $update) {
$checker = $update['checker'];
$installer = $update['installer'];
if ($checker->isLatest()) {
continue;
}
$latestVersion = $checker->getLatest();
$downloadUrl = $checker->getDownloadUrl();
$output->write("Downloading manual v{$latestVersion}...");
try {
$downloaded = $downloader->download($downloadUrl);
} catch (ErrorException $e) {
$output->write(' <error>Failed.</error>');
$output->writeln(\sprintf('<error>%s</error>', $e->getMessage()));
$downloader->cleanup();
return self::FAILURE;
}
if (!$downloaded) {
$output->writeln(' <error>Download failed.</error>');
$downloader->cleanup();
return self::FAILURE;
}
$output->write(' <info>OK</info>'.\PHP_EOL);
$downloadedFile = $downloader->getFilename();
if (!$installer->install($downloadedFile)) {
$downloader->cleanup();
$output->writeln('<error>Failed to install manual.</error>');
return self::FAILURE;
}
$installed[] = [$installer->getInstallPath(), $latestVersion];
$downloader->cleanup();
}
if (empty($installed)) {
$output->writeln('<info>Manual is up-to-date.</info>');
} else {
foreach ($installed as [$installPath, $version]) {
$prettyPath = ConfigPaths::prettyPath($installPath);
$output->writeln("Installed manual v{$version} to <info>{$prettyPath}</info>");
}
}
return self::SUCCESS;
}
/**
* Handle an invalid manual file by prompting the user to remove it.
*
* @param InvalidManualException $e The exception containing invalid manual details
* @param InputInterface $input Input interface
* @param OutputInterface $output Output interface
*
* @throws \RuntimeException if user declines to remove the file or removal fails
*/
private static function handleInvalidManual(InvalidManualException $e, InputInterface $input, OutputInterface $output): void
{
$prettyPath = ConfigPaths::prettyPath($e->getManualFile());
$output->writeln(\sprintf('<error>Invalid manual file detected:</error> <info>%s</info>', $prettyPath));
$output->writeln('');
$helper = new QuestionHelper();
$question = new ConfirmationQuestion('Remove this file and continue? [Y/n] ', true);
if (!$helper->ask($input, $output, $question)) {
throw new \RuntimeException('Manual update cancelled.');
}
if (!\unlink($e->getManualFile())) {
throw new \RuntimeException(\sprintf('Failed to remove file: %s', $prettyPath));
}
$output->writeln('<info>Invalid manual file removed.</info>');
$output->writeln('');
}
/**
* Prompt user to download PHP format manual when they have/had legacy SQLite.
*
* @param InputInterface $input Input interface
* @param OutputInterface $output Output interface
* @param string $manualFile Path to current/former SQLite manual file
* @param bool $wasRemoved Whether the file was already removed
*
* @return bool True if user wants to download PHP format
*/
private static function promptMigrateToV3(InputInterface $input, OutputInterface $output, string $manualFile, bool $wasRemoved): bool
{
$prettyPath = ConfigPaths::prettyPath($manualFile);
$verb = $wasRemoved ? 'had' : 'have';
$output->writeln(\sprintf('You %s a legacy SQLite manual: <info>%s</info>', $verb, $prettyPath));
$output->writeln('');
$helper = new QuestionHelper();
$question = new ConfirmationQuestion('Download the current manual format? [Y/n] ', true);
return $helper->ask($input, $output, $question);
}
/**
* Determine which manual formats should be updated.
*
* @param InputInterface $input Input interface
* @param OutputInterface $output Output interface
* @param bool $hasPhpManual Whether PHP manual exists
* @param bool $hasSqliteManual Whether SQLite manual exists
* @param bool $removedInvalidSqlite Whether we just removed an invalid SQLite manual
* @param string $sqliteManualPath Path to SQLite manual file
*
* @return string[] Array of format names to update ('php', 'sqlite')
*/
private static function getFormatsToUpdate(
InputInterface $input,
OutputInterface $output,
bool $hasPhpManual,
bool $hasSqliteManual,
bool $removedInvalidSqlite,
string $sqliteManualPath
): array {
// Only SQLite exists (or just removed invalid SQLite): offer to add PHP format
if (!$hasPhpManual && ($hasSqliteManual || $removedInvalidSqlite)) {
if (self::promptMigrateToV3($input, $output, $sqliteManualPath, $removedInvalidSqlite)) {
return ['php', 'sqlite'];
}
return ['sqlite'];
}
// PHP exists, or neither exist: default to PHP, and include SQLite if it exists
$formats = ['php'];
if ($hasSqliteManual) {
$formats[] = 'sqlite';
}
return $formats;
}
/**
* Get manual metadata from a file.
*
* @param string $path Path to manual file
*
* @return array|null Metadata array with 'version' and 'lang' keys, or null if unavailable
*/
private static function getManualMeta(string $path): ?array
{
if (!\file_exists($path)) {
return null;
}
try {
if (\substr($path, -4) === '.php') {
$manual = new \Psy\Manual\V3Manual($path);
return $manual->getMeta();
}
if (\substr($path, -7) === '.sqlite') {
$pdo = new \PDO('sqlite:'.$path);
$manual = new \Psy\Manual\V2Manual($pdo);
return $manual->getMeta();
}
} catch (\Exception $e) {
// Ignore errors reading manual metadata
}
return null;
}
}