Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/ApplicationLinker.php
Original file line number Diff line number Diff line change
Expand Up @@ -156,10 +156,10 @@
$r = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($appVendorDir));
foreach ($r as $f => $entry) {
// Ignore directories as such - they are created if they have relevant files
if ($entry->isDir()) {

Check failure on line 159 in src/ApplicationLinker.php

View workflow job for this annotation

GitHub Actions / CI

Cannot call method isDir() on mixed.
continue;
}
$name = $entry->getFilename();

Check failure on line 162 in src/ApplicationLinker.php

View workflow job for this annotation

GitHub Actions / CI

Cannot call method getFilename() on mixed.
$relativePathName = $r->getSubPathname();
$relativePath = $r->getSubPath();
$split = explode(DIRECTORY_SEPARATOR, $relativePathName, 3);
Expand Down Expand Up @@ -198,7 +198,7 @@
$pathProxyToFile = $this->filesystem->findShortestPath(
$appWebDir . DIRECTORY_SEPARATOR . $relativePathName,
$appVendorDir . DIRECTORY_SEPARATOR . $relativePathName,
);
);
}


Expand All @@ -212,7 +212,7 @@
}
$this->filesystem->filePutContentsIfModified(
$appWebDir . DIRECTORY_SEPARATOR . $relativePathName,
$content

Check failure on line 215 in src/ApplicationLinker.php

View workflow job for this annotation

GitHub Actions / CI

Parameter #2 $content of method Composer\Util\Filesystem::filePutContentsIfModified() expects string, string|false given.
);
} // EndForEach File
} else {
Expand Down
18 changes: 14 additions & 4 deletions src/HordeReconfigureCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,16 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Horde\Composer\IOAdapter\SymphonyOutputAdapter;
use InvalidArgumentException;

class HordeReconfigureCommand extends BaseCommand
{
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(
<<<EOT
Expand All @@ -40,16 +43,23 @@
die('Error: Command was run without a relation to composer itself');
}
$mode = $input->getOption('mode');
if (!in_array($mode, ['symlink', 'proxy', 'copy'])) {
$output->writeln('<error>Invalid mode. Must be "symlink", "proxy" or "copy". "symlink" is the current default.</error>');
$force = (bool) $input->getOption('force');
$webroot = $input->getOption('webroot') ?? '/';
if (!str_ends_with($webroot, '/')) {

Check failure on line 48 in src/HordeReconfigureCommand.php

View workflow job for this annotation

GitHub Actions / CI

Parameter #1 $haystack of function str_ends_with expects string, mixed given.
$webroot .= '/';

Check failure on line 49 in src/HordeReconfigureCommand.php

View workflow job for this annotation

GitHub Actions / CI

Binary operation ".=" between mixed and '/' results in an error.
}
try {
$reconfigureOptions = new ReconfigureOptions(mode: $mode, force: $force, webroot: $webroot);

Check failure on line 52 in src/HordeReconfigureCommand.php

View workflow job for this annotation

GitHub Actions / CI

Parameter $webroot of class Horde\Composer\ReconfigureOptions constructor expects string, mixed given.

Check failure on line 52 in src/HordeReconfigureCommand.php

View workflow job for this annotation

GitHub Actions / CI

Parameter $mode of class Horde\Composer\ReconfigureOptions constructor expects string, mixed given.
} catch (InvalidArgumentException $e) {
$output->writeln('<error>' . $e->getMessage() . '</error>');
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();
}
Expand Down
62 changes: 46 additions & 16 deletions src/HordeReconfigureFlow.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand All @@ -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);
}

/**
Expand All @@ -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';
Expand All @@ -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;
}
/**
Expand All @@ -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');
Expand All @@ -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();
Expand All @@ -104,40 +134,40 @@ 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(
$filesystem,
$this->tree,
$hordeApps,
$hordeLibraries,
$this->mode
$mode
);
$jsLinker->run();
$this->io->writeln('Linking themes tree to /web/themes');
$themesHandler = new ThemesHandler(
$filesystem,
$rootPackageDir,
$vendorDir,
$this->mode
$mode
);

foreach ($hordeThemes as $theme) {
Expand All @@ -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;
}
Expand Down
20 changes: 20 additions & 0 deletions src/ReconfigureOptions.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
<?php

declare(strict_types=1);

namespace Horde\Composer;

use InvalidArgumentException;

class ReconfigureOptions
{
public function __construct(
public readonly string $mode = 'proxy',
public readonly bool $force = false,
public readonly string $webroot = '/'
) {
if (!in_array($mode, ['symlink', 'proxy', 'copy'])) {
throw new InvalidArgumentException('Invalid mode. Must be "symlink", "proxy" or "copy". "symlink" is the current default.');
}
}
}
45 changes: 33 additions & 12 deletions src/RegistrySnippetFileWriter.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ public function __construct(
*/
private string $baseDir,
array $apps,
private string $mode = 'symlink'
private ReconfigureOptions $options = new ReconfigureOptions(),
) {
/**
* The config dir for the registry
Expand All @@ -41,39 +41,49 @@ public function __construct(
/**
* The config dir for the registry snippets
*/
$this->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)) {
$registry00FileContent = '<?php' . PHP_EOL .
'/**' . 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\';
$deployment_fileroot = \'%s\';
$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 = '<?php' . PHP_EOL .
'/**' . PHP_EOL .
' * AUTOGENERATED ONLY IF ABSENT' . PHP_EOL .
' * If you activate at least one of these lines, please also set a full https URL for the default webroot where the remaining apps and the assets (js, themes, static) are located' . PHP_EOL .
' * Run `composer horde:reconfigure --mode=' . $this->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 = '<?php' . PHP_EOL .
'/**' . PHP_EOL .
' * AUTOGENERATED FILE WILL BE OVERWRITTEN ON EACH' . PHP_EOL .
Expand All @@ -90,24 +100,35 @@ public function run(): void
$registryAppSnippet .=
'$this->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, '<?php' . PHP_EOL . '// Empty default routes file. Put custom routes into var/config/' . $appName . '/routes.local.php and run composer horde:reconfigure' . PHP_EOL);
}
$this->filesystem->filePutContentsIfModified($registryAppFilename, $registryAppSnippet);
}
if (!file_exists($webrootOverrideFilePath)) {
$this->filesystem->filePutContentsIfModified($webrootOverrideFilePath, $webrootOverrideFileSnippet);
}
}
}
Loading