diff --git a/src/ApplicationLinker.php b/src/ApplicationLinker.php index 1812196..6de2201 100644 --- a/src/ApplicationLinker.php +++ b/src/ApplicationLinker.php @@ -198,7 +198,7 @@ public function run(): void $pathProxyToFile = $this->filesystem->findShortestPath( $appWebDir . DIRECTORY_SEPARATOR . $relativePathName, $appVendorDir . DIRECTORY_SEPARATOR . $relativePathName, - ); + ); } diff --git a/src/HordeReconfigureCommand.php b/src/HordeReconfigureCommand.php index aee4497..97ed67d 100644 --- a/src/HordeReconfigureCommand.php +++ b/src/HordeReconfigureCommand.php @@ -12,6 +12,7 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Horde\Composer\IOAdapter\SymphonyOutputAdapter; +use InvalidArgumentException; class HordeReconfigureCommand extends BaseCommand { @@ -19,6 +20,8 @@ protected function configure(): void { $this->setName('horde:reconfigure')->setAliases(['horde-reconfigure']); $this->addOption('mode', 'm', InputOption::VALUE_OPTIONAL, 'Should we "symlink" or "proxy" to the webdir?', 'symlink'); + $this->addOption('webroot', null, InputOption::VALUE_REQUIRED, 'The default webroot for the /horde, /js, /themes, /static and /$app dirs if not specifically overridden. Use either a relative path or a full HTTPS url. Examples: "/" (default), "/groupware", "http://localhost/", "https://horde.example.com"', '/'); + $this->addOption('force', null, InputOption::VALUE_NONE, 'Delete and rewrite items normally left untouched', null); $this->setDescription('Rewrite autogenerated configuration'); $this->setHelp( <<getOption('mode'); - if (!in_array($mode, ['symlink', 'proxy', 'copy'])) { - $output->writeln('Invalid mode. Must be "symlink", "proxy" or "copy". "symlink" is the current default.'); + $force = (bool) $input->getOption('force'); + $webroot = $input->getOption('webroot') ?? '/'; + if (!str_ends_with($webroot, '/')) { + $webroot .= '/'; + } + try { + $reconfigureOptions = new ReconfigureOptions(mode: $mode, force: $force, webroot: $webroot); + } catch (InvalidArgumentException $e) { + $output->writeln('' . $e->getMessage() . ''); return 1; } // This is needed to support PHP 7.4 (no union types) for both Composer 2.2 / 2.3 // Cannot use instanceof here as the class will not exist in 2.2. if (get_class($composer) === 'Composer\PartialComposer') { - $flow = HordeReconfigureFlow::fromPartialComposer($composer, new SymphonyOutputAdapter($output), $mode); + $flow = HordeReconfigureFlow::fromPartialComposer($composer, new SymphonyOutputAdapter($output), $reconfigureOptions); } else { - $flow = HordeReconfigureFlow::fromComposer($composer, new SymphonyOutputAdapter($output), $mode); + $flow = HordeReconfigureFlow::fromComposer($composer, new SymphonyOutputAdapter($output), $reconfigureOptions); } return $flow->run(); } diff --git a/src/HordeReconfigureFlow.php b/src/HordeReconfigureFlow.php index 947e341..761f0c3 100644 --- a/src/HordeReconfigureFlow.php +++ b/src/HordeReconfigureFlow.php @@ -26,13 +26,11 @@ class HordeReconfigureFlow /** * Modes: symlink, copy */ - private string $mode = 'symlink'; private DirectoryTree $tree; - public function __construct(DirectoryTree $tree, FlowIoInterface $io, string $mode = 'symlink') + public function __construct(DirectoryTree $tree, FlowIoInterface $io, public readonly ReconfigureOptions $options = new ReconfigureOptions()) { $this->io = $io; - $this->mode = $mode; $this->tree = $tree; } @@ -43,14 +41,14 @@ public function __construct(DirectoryTree $tree, FlowIoInterface $io, string $mo * @param FlowIoInterface|null $output * @return self */ - public static function fromComposer(Composer $composer, ?FlowIoInterface $output = null, string $mode = 'symlink'): self + public static function fromComposer(Composer $composer, ?FlowIoInterface $output = null, ReconfigureOptions $options = new ReconfigureOptions()): self { - return self::fromAnyComposer($composer, $output, $mode); + return self::fromAnyComposer($composer, $output, $options); } - public static function fromPartialComposer(PartialComposer $composer, ?FlowIoInterface $output = null, string $mode = 'symlink'): self + public static function fromPartialComposer(PartialComposer $composer, ?FlowIoInterface $output = null, ReconfigureOptions $options = new ReconfigureOptions()): self { - return self::fromAnyComposer($composer, $output, $mode); + return self::fromAnyComposer($composer, $output, $options); } /** @@ -64,8 +62,9 @@ public static function fromPartialComposer(PartialComposer $composer, ?FlowIoInt * * @TODO Refactor this once we require PHP 8.0 or higher */ - private static function fromAnyComposer($composer, ?FlowIoInterface $output = null, string $mode = 'symlink'): self + private static function fromAnyComposer($composer, ?FlowIoInterface $output = null, ReconfigureOptions $options = new ReconfigureOptions()): self { + $mode = $options->mode; // Symlink mode does not work on Windows if ($mode == 'symlink') { $mode = strncasecmp(\PHP_OS, 'WIN', 3) === 0 ? 'copy' : 'symlink'; @@ -77,7 +76,7 @@ private static function fromAnyComposer($composer, ?FlowIoInterface $output = nu } $outputInterface = $output ?? new SymphonyOutputAdapter(ComposerFactory::createOutput()); $tree->withVendorDir($vendorDir); - $flow = new HordeReconfigureFlow($tree, $outputInterface, $mode); + $flow = new HordeReconfigureFlow($tree, $outputInterface, $options); return $flow; } /** @@ -86,6 +85,7 @@ private static function fromAnyComposer($composer, ?FlowIoInterface $output = nu public function run(): int { // Get installed packages of types handled by installer + $mode = $this->options->mode; $filesystem = new Filesystem(); // This is sufficient for now but we actually know better $hordeApps = InstalledVersions::getInstalledPackagesByType('horde-application'); @@ -95,6 +95,36 @@ public function run(): int // We could simply ask InstalledVersions here, too $rootPackageDir = $this->tree->getRootPackageDir(); $vendorDir = $this->tree->getVendorDir(); + if ($this->options->force) { + $this->io->writeln('Force mode enabled, removing existing files'); + // Todo: Delegate to a method or helper class + foreach ($hordeApps as $app) { + [$vendorName, $appName] = explode('/', $app); + // horde.local.php files + $filesystem->remove($this->tree->getVarConfigDir() . '/' . $appName . '/horde.local.php'); + $filesystem->remove($vendorDir . '/' . $vendorName . '/' . $appName . '/config/horde.local.php'); + if ($app == 'horde') { + // remove horde registry file + $filesystem->remove($this->tree->getVarConfigDir() . '/horde/registry.d/00-horde.php'); + $filesystem->remove($this->tree->getVarConfigDir() . '/horde/registry.d/01-location-' . $appName . '.php'); + } else { + // remove app registry file + $filesystem->remove($this->tree->getVarConfigDir() . '/horde/registry.d/02-location-' . $appName . '.php'); + } + // remove webdir items + $filesystem->remove($this->tree->getWebReadableRootDir() . '/' . $appName); + $filesystem->remove($this->tree->getWebReadableRootDir() . '/js/' . $appName); + $filesystem->remove($this->tree->getWebReadableRootDir() . '/themes/' . $appName); + // remove vendor dir items + $filesystem->remove($vendorDir . '/' . $vendorName . '/' . $appName . '/config/conf.php'); + $filesystem->remove($vendorDir . '/' . $vendorName . '/' . $appName . '/config/hooks.php'); + $filesystem->remove($vendorDir . '/' . $vendorName . '/' . $appName . '/config/backends.local.php'); + $filesystem->remove($vendorDir . '/' . $vendorName . '/' . $appName . '/config/prefs.local.php'); + $filesystem->remove($vendorDir . '/' . $vendorName . '/' . $appName . '/config/routes.local.php'); + } + } else { + $this->io->writeln('Force mode not enabled, skipping removal of existing files'); + } $this->io->writeln('Applying /presets for absent files in /var/config'); $presetHandler = new PresetHandler($rootPackageDir, $filesystem); $presetHandler->handle(); @@ -104,24 +134,24 @@ public function run(): int $filesystem ); $snippetHandler->handle(); - $this->io->writeln('Configuration mode: ' . $this->mode); + $this->io->writeln('Configuration mode: ' . $mode); $this->io->writeln('Writing app configs to /var/config dir'); $registrySnippetFileWriter = new RegistrySnippetFileWriter( $filesystem, $rootPackageDir, $hordeApps, - $this->mode + $this->options ); $registrySnippetFileWriter->run(); $hordeLocalWriter = new HordeLocalFileWriter( $filesystem, $rootPackageDir, $hordeApps, - $this->mode + $mode ); $hordeLocalWriter->run(); $this->io->writeln('Linking app configs to /web Dir'); - $configLinker = new ConfigLinker($rootPackageDir, $this->mode); + $configLinker = new ConfigLinker($rootPackageDir, $mode); $configLinker->run(); $this->io->writeln('Linking javascript tree to /web/js'); $jsLinker = new JsTreeLinker( @@ -129,7 +159,7 @@ public function run(): int $this->tree, $hordeApps, $hordeLibraries, - $this->mode + $mode ); $jsLinker->run(); $this->io->writeln('Linking themes tree to /web/themes'); @@ -137,7 +167,7 @@ public function run(): int $filesystem, $rootPackageDir, $vendorDir, - $this->mode + $mode ); foreach ($hordeThemes as $theme) { @@ -151,7 +181,7 @@ public function run(): int } $themesHandler->setupThemes(); // ApplicationLinker must run after all changes to /vendor - $appLinker = new ApplicationLinker($filesystem, $hordeApps, $rootPackageDir, $this->mode); + $appLinker = new ApplicationLinker($filesystem, $hordeApps, $rootPackageDir, $mode); $appLinker->run(); return 0; } diff --git a/src/ReconfigureOptions.php b/src/ReconfigureOptions.php new file mode 100644 index 0000000..8ad22d8 --- /dev/null +++ b/src/ReconfigureOptions.php @@ -0,0 +1,20 @@ +configRegistryDir = $this->configDir . '/horde/registry.d'; - $this->webDir = $baseDir . '/web'; + $this->configRegistryDir = $this->configDir . DIRECTORY_SEPARATOR . 'horde' . DIRECTORY_SEPARATOR . 'registry.d'; + $this->webDir = $baseDir . DIRECTORY_SEPARATOR . 'web'; $this->apps = $apps; } public function run(): void { + $webrootUri = $this->options->webroot; // Ensure we have the base locations right $registry00FilePath = $this->configRegistryDir . '/00-horde.php'; if (!file_exists($registry00FilePath)) { @@ -55,7 +56,7 @@ public function run(): void '/**' . PHP_EOL . ' * AUTOGENERATED ONLY IF ABSENT' . PHP_EOL . ' * Edit this file to match your needs' . PHP_EOL . - ' * To redo, delete file and run `composer horde-reconfigure --mode=' . $this->mode . '`' . PHP_EOL . + ' * To reset from defaults, run `composer horde:reconfigure --force --mode=' . $this->options->mode . '`' . PHP_EOL . ' */' . PHP_EOL; $registry00FileContent .= sprintf( '$deployment_webroot = \'%s\'; @@ -63,17 +64,26 @@ public function run(): void $app_fileroot = \'%s\'; $app_webroot = \'%s\'; ', - '/', + $webrootUri, $this->webDir, - $this->webDir . '/horde', + $this->webDir . DIRECTORY_SEPARATOR . 'horde', '/horde' ); $this->filesystem->filePutContentsIfModified($registry00FilePath, $registry00FileContent); } + $webrootOverrideFilePath = $this->configDir . '/horde/registry.d/03-override-webroots.php'; + $webrootOverrideFileSnippet = 'options->mode . ' --webroot="https://assets.example.com/"`' . PHP_EOL . + ' */' . PHP_EOL; // Ensure we have a base foreach ($this->apps as $app) { [$appVendor, $appName] = explode('/', $app); + $webrootOverrideFileSnippet .= '// Customize ' . $app . ' webroot' . PHP_EOL; + $webrootOverrideFileSnippet .= "// \$this->applications['$appName']['webroot'] = 'https://$appName.example.com/';" . PHP_EOL; $registryAppSnippet = 'applications[\'' . $appName . '\'][\'fileroot\'] = \'' . $appInVendorDir . '\';' . PHP_EOL . '$this->applications[\'' . $appName . '\'][\'templates\'] = \'' . $appInVendorDir . 'templates' . DIRECTORY_SEPARATOR . '\';' . PHP_EOL . - '$this->applications[\'horde\'][\'webroot\'] = $app_webroot;' . PHP_EOL . + "\$this->applications['horde']['webroot'] = '{$webrootUri}horde/';" . PHP_EOL . '$this->applications[\'horde\'][\'jsfs\'] = $deployment_fileroot . \'/js/horde/\';' . PHP_EOL . - '$this->applications[\'horde\'][\'jsuri\'] = $deployment_webroot . \'js/horde/\';' . PHP_EOL . + "\$this->applications['horde']['jsuri'] = '{$webrootUri}js/horde';" . PHP_EOL . '$this->applications[\'horde\'][\'staticfs\'] = $deployment_fileroot . \'/static\';' . PHP_EOL . - '$this->applications[\'horde\'][\'staticuri\'] = $deployment_webroot . \'static\';' . PHP_EOL . + "\$this->applications['horde']['staticuri'] = '{$webrootUri}static/';" . PHP_EOL . '$this->applications[\'horde\'][\'themesfs\'] = $deployment_fileroot . \'/themes/horde/\';' . PHP_EOL . - '$this->applications[\'horde\'][\'themesuri\'] = $deployment_webroot . \'themes/horde/\';'; + "\$this->applications['horde']['themesuri'] = '{$webrootUri}themes/horde';" . PHP_EOL; } else { // A registry snippet should ensure the install dir is known $registryAppFilename = $this->configRegistryDir . '/02-location-' . $appName . '.php'; $registryAppSnippet .= '$this->applications[\'' . $appName . '\'][\'fileroot\'] = \'' . $appInVendorDir . '\';' . PHP_EOL . '$this->applications[\'' . $appName . '\'][\'templates\'] = \'' . $appInVendorDir . 'templates' . DIRECTORY_SEPARATOR . '\';' . PHP_EOL . - '$this->applications[\'' . $appName . '\'][\'webroot\'] = $this->applications[\'horde\'][\'webroot\'] . \'/../' . $appName . "';" . PHP_EOL . '$this->applications[\'' . $appName . '\'][\'themesfs\'] = \'' . $this->webDir . DIRECTORY_SEPARATOR . 'themes' . DIRECTORY_SEPARATOR . $appName . DIRECTORY_SEPARATOR . '\';' . PHP_EOL . - '$this->applications[\'' . $appName . '\'][\'themesuri\'] = $this->applications[\'horde\'][\'webroot\'] . \'/../themes/' . $appName . '/\';'; + '$this->applications[\'' . $appName . '\'][\'jsfs\'] = \'' . $this->webDir . DIRECTORY_SEPARATOR . 'js' . DIRECTORY_SEPARATOR . $appName . DIRECTORY_SEPARATOR . '\';' . PHP_EOL . + "\$this->applications['$appName']['webroot'] = '{$webrootUri}$appName/';" . PHP_EOL . + "\$this->applications['$appName']['jsuri'] = '{$webrootUri}js/$appName';" . PHP_EOL . + "\$this->applications['$appName']['themesuri'] = '{$webrootUri}themes/$appName';" . PHP_EOL . + '// End of ' . $appName . ' registry snippet' . PHP_EOL; + } + // Some versions of the middleware router require the routes.php file to exist even if empty + $routesFilePath = $appInVendorDir . DIRECTORY_SEPARATOR . 'config' . DIRECTORY_SEPARATOR . 'routes.php'; + if (!file_exists($routesFilePath)) { + $this->filesystem->filePutContentsIfModified($routesFilePath, 'filesystem->filePutContentsIfModified($registryAppFilename, $registryAppSnippet); } + if (!file_exists($webrootOverrideFilePath)) { + $this->filesystem->filePutContentsIfModified($webrootOverrideFilePath, $webrootOverrideFileSnippet); + } } }