diff --git a/.php-cs-fixer.dist.php b/.php-cs-fixer.dist.php index 2db37b8..b930fd7 100644 --- a/.php-cs-fixer.dist.php +++ b/.php-cs-fixer.dist.php @@ -1,8 +1,7 @@ exclude(['fixtures']); return (new PhpCsFixer\Config()) ->setRules([ '@PER-CS' => true, - '@PHP82Migration' => true, + '@PHP83Migration' => true, 'php_unit_test_class_requires_covers' => true, + 'nullable_type_declaration_for_default_null_value' => true, ]) ->setFinder($finder) ; diff --git a/src/ApplicationLinker.php b/src/ApplicationLinker.php index ebec087..86c1f28 100644 --- a/src/ApplicationLinker.php +++ b/src/ApplicationLinker.php @@ -52,7 +52,7 @@ public function run(): void $this->filesystem->ensureDirectoryExists($webDir); // Ensure we have a static dir for ephemeral, generated files ... $this->filesystem->ensureDirectoryExists($webDir . '/static'); - + // TODO: Move implementations to separate classes foreach ($this->appPackages as $app) { if ($app === 'horde/components') { continue; @@ -72,12 +72,15 @@ public function run(): void 'files' => [ 'LICENSE', 'composer.json', 'composer.lock', '.gitattributes', '.horde.yml', '.travis.yml', 'package.xml', 'phpunit.xml.dist', - '.gitignore', 'README.rst', + '.gitignore', 'README.rst', 'README.md', 'README', 'CHANGELOG.md', + '.php-cs-fixer.dist.php', '.php-cs-fixer.cache', 'phpunit.xml', ], 'dirs' => [ 'doc', 'test', 'bin', + 'lib', + 'src', 'script', 'scripts', 'static', // static should be ensured to exist in webdir. @@ -116,10 +119,93 @@ public function run(): void $appWebDir . '/' . $name ); } + } elseif ($this->mode == 'proxy') { + // This list is different from the one for the linker + $filterList = [ + + 'files' => [ + '.gitignore', + '.gitattributes', + 'README.rst', + 'README', + 'LICENSE', + 'phpunit.xml', + 'phpunit.xml.dist', + 'composer.json', + ], + 'dirs' => [ + 'bin', + 'lib', + 'src', + '.git', + '.github', + 'doc', + 'js', + 'script', + 'scripts', + 'static', + 'config', + 'locale', + 'themes', + 'templates', + 'vendor', + ], + ]; + $this->filesystem->emptyDirectory($appWebDir, true); + // We are already per-app + // appWebDir and appVendorDir are already set + $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()) { + continue; + } + $name = $entry->getFilename(); + $relativePathName = $r->getSubPathname(); + $relativePath = $r->getSubPath(); + $split = explode(DIRECTORY_SEPARATOR, $relativePathName, 3); + // Skip subpaths of the filtered dirs + if (in_array( + $split[0], + $filterList['dirs'] + )) { + continue; + } + if (in_array( + $name, + $filterList['files'] + )) { + continue; + } + $this->filesystem->ensureDirectoryExists($appWebDir . DIRECTORY_SEPARATOR . $relativePath); + $pathProxyToAutoloader = $this->filesystem->findShortestPath( + $appWebDir . DIRECTORY_SEPARATOR . $relativePathName, + $vendorDir . DIRECTORY_SEPARATOR . 'autoload.php', + preferRelative: true + ); + $pathProxyToFile = $this->filesystem->findShortestPath( + $appWebDir . DIRECTORY_SEPARATOR . $relativePathName, + $appVendorDir . DIRECTORY_SEPARATOR . $relativePathName, + preferRelative: true + ); + + $originalContent = file_get_contents($appVendorDir . DIRECTORY_SEPARATOR . $relativePathName); + if (str_contains((string) $originalContent, 'filesystem->filePutContentsIfModified( + $appWebDir . DIRECTORY_SEPARATOR . $relativePathName, + $content + ); + } // EndForEach File } else { $copy = new RecursiveCopy($appVendorDir, $appWebDir, array_merge($filterList['files'], $filterList['dirs'])); $copy->copy(); } - } + } // EndForEach App } } diff --git a/src/HordeLocalFileWriter.php b/src/HordeLocalFileWriter.php index 0e34c99..4f0560f 100644 --- a/src/HordeLocalFileWriter.php +++ b/src/HordeLocalFileWriter.php @@ -19,6 +19,7 @@ class HordeLocalFileWriter private string $webDir; private Filesystem $filesystem; + private string $vendorHordeDir; /** * Undocumented function @@ -27,11 +28,12 @@ class HordeLocalFileWriter * @param string $baseDir * @param string[] $apps */ - public function __construct(Filesystem $filesystem, string $baseDir, array $apps) + public function __construct(Filesystem $filesystem, string $baseDir, array $apps, private string $mode = 'symlink') { $this->filesystem = $filesystem; $this->configDir = $baseDir . '/var/config'; $this->vendorDir = $baseDir . '/vendor'; + $this->vendorHordeDir = $this->vendorDir . DIRECTORY_SEPARATOR . 'horde' . DIRECTORY_SEPARATOR . 'horde'; $this->webDir = $baseDir . '/web'; $this->apps = $apps; } @@ -49,9 +51,14 @@ private function processApp(string $app): void [$vendor, $name] = explode('/', $app, 2); $this->filesystem->ensureDirectoryExists($this->configDir . "/$name"); $path = $this->configDir . "/$name/horde.local.php"; + $hordeBaseDir = $hordeWebDir; + if ($this->mode === 'proxy') { + $hordeBaseDir = $this->vendorHordeDir; + } $hordeLocalFileContent = sprintf( - "configDir ); // special case horde/horde needs to require the composer autoloader if ($app == 'horde/horde') { diff --git a/src/HordeReconfigureCommand.php b/src/HordeReconfigureCommand.php index 1e08ec1..aee4497 100644 --- a/src/HordeReconfigureCommand.php +++ b/src/HordeReconfigureCommand.php @@ -7,6 +7,7 @@ use Composer\Command\BaseCommand; use Composer\Factory as ComposerFactory; use Composer\Plugin\Capability\CommandProvider as CommandProviderCapability; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -17,6 +18,7 @@ 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->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.'); + 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)); + $flow = HordeReconfigureFlow::fromPartialComposer($composer, new SymphonyOutputAdapter($output), $mode); } else { - $flow = HordeReconfigureFlow::fromComposer($composer, new SymphonyOutputAdapter($output)); + $flow = HordeReconfigureFlow::fromComposer($composer, new SymphonyOutputAdapter($output), $mode); } return $flow->run(); } diff --git a/src/HordeReconfigureFlow.php b/src/HordeReconfigureFlow.php index cc6b0fe..947e341 100644 --- a/src/HordeReconfigureFlow.php +++ b/src/HordeReconfigureFlow.php @@ -18,6 +18,7 @@ use Symfony\Component\Console\Output\OutputInterface; use Horde\Composer\IOAdapter\SymphonyOutputAdapter; use RuntimeException; +use strncasecmp; class HordeReconfigureFlow { @@ -42,14 +43,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): self + public static function fromComposer(Composer $composer, ?FlowIoInterface $output = null, string $mode = 'symlink'): self { - return self::fromAnyComposer($composer, $output); + return self::fromAnyComposer($composer, $output, $mode); } - public static function fromPartialComposer(PartialComposer $composer, ?FlowIoInterface $output = null): self + public static function fromPartialComposer(PartialComposer $composer, ?FlowIoInterface $output = null, string $mode = 'symlink'): self { - return self::fromAnyComposer($composer, $output); + return self::fromAnyComposer($composer, $output, $mode); } /** @@ -63,9 +64,12 @@ 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): self + private static function fromAnyComposer($composer, ?FlowIoInterface $output = null, string $mode = 'symlink'): self { - $mode = \strncasecmp(\PHP_OS, 'WIN', 3) === 0 ? 'copy' : 'symlink'; + // Symlink mode does not work on Windows + if ($mode == 'symlink') { + $mode = strncasecmp(\PHP_OS, 'WIN', 3) === 0 ? 'copy' : 'symlink'; + } $tree = DirectoryTree::fromComposerJsonPath(ComposerFactory::getComposerFile()); $vendorDir = $composer->getConfig()->get('vendor-dir'); if (!is_string($vendorDir)) { @@ -100,18 +104,20 @@ public function run(): int $filesystem ); $snippetHandler->handle(); - + $this->io->writeln('Configuration mode: ' . $this->mode); $this->io->writeln('Writing app configs to /var/config dir'); $registrySnippetFileWriter = new RegistrySnippetFileWriter( $filesystem, $rootPackageDir, - $hordeApps + $hordeApps, + $this->mode ); $registrySnippetFileWriter->run(); $hordeLocalWriter = new HordeLocalFileWriter( $filesystem, $rootPackageDir, $hordeApps, + $this->mode ); $hordeLocalWriter->run(); $this->io->writeln('Linking app configs to /web Dir'); diff --git a/src/JsTreeLinker.php b/src/JsTreeLinker.php index 0fabdc9..664058f 100644 --- a/src/JsTreeLinker.php +++ b/src/JsTreeLinker.php @@ -76,7 +76,7 @@ public function run(): void // app javascript dirs are exposed under js/$app foreach ($this->apps as $app) { [$vendor, $name] = explode('/', $app, 2); - $appPath = $this->webDir . '/' . $name; + $appPath = $this->vendorDir . '/' . $vendor . '/' . $name; $jsSourcePath = $appPath . '/js'; if (!$this->filesystem->isReadable($jsSourcePath)) { continue; @@ -115,7 +115,7 @@ public function linkDir(string $sourceDir, string $targetDir): void } $sourceFile = $sourceDir . '/' . $sourceItem; $targetFile = $targetDir . '/' . $sourceItem; - if ($this->mode === 'symlink') { + if (in_array($this->mode, ['symlink', 'proxy'])) { $this->filesystem->relativeSymlink($sourceFile, $targetFile); } else { if (is_file($sourceFile)) { diff --git a/src/RegistrySnippetFileWriter.php b/src/RegistrySnippetFileWriter.php index 5efa382..8e2f0c8 100644 --- a/src/RegistrySnippetFileWriter.php +++ b/src/RegistrySnippetFileWriter.php @@ -18,8 +18,6 @@ class RegistrySnippetFileWriter private string $configRegistryDir; private string $webDir; - private Filesystem $filesystem; - /** * Undocumented function * @@ -27,9 +25,18 @@ class RegistrySnippetFileWriter * @param string $baseDir * @param string[] $apps */ - public function __construct(Filesystem $filesystem, string $baseDir, array $apps) - { - $this->filesystem = $filesystem; + public function __construct( + private Filesystem $filesystem, + /** + * The config dir for the registry + */ + private string $baseDir, + array $apps, + private string $mode = 'symlink' + ) { + /** + * The config dir for the registry + */ $this->configDir = $baseDir . '/var/config'; /** * The config dir for the registry snippets @@ -48,7 +55,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`' . PHP_EOL . + ' * To redo, delete file and run `composer horde-reconfigure --mode=' . $this->mode . '`' . PHP_EOL . ' */' . PHP_EOL; $registry00FileContent .= sprintf( '$deployment_webroot = \'%s\'; @@ -91,10 +98,12 @@ public function run(): void } else { // A registry snippet should ensure the install dir is known $registryAppFilename = $this->configRegistryDir . '/02-location-' . $appName . '.php'; + $appInVendorDir = $this->baseDir . DIRECTORY_SEPARATOR . 'vendor' . DIRECTORY_SEPARATOR . $appVendor . DIRECTORY_SEPARATOR . $appName . DIRECTORY_SEPARATOR; $registryAppSnippet .= - '$this->applications[\'' . $appName . '\'][\'fileroot\'] = "$deployment_fileroot/' . $appName . '";' . PHP_EOL . + '$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->applications[\'horde\'][\'fileroot\'] . \'/../themes/' . $appName . '/\';' . PHP_EOL . + '$this->applications[\'' . $appName . '\'][\'themesfs\'] = \'' . $this->webDir . DIRECTORY_SEPARATOR . $app . DIRECTORY_SEPARATOR . '\';' . PHP_EOL . '$this->applications[\'' . $appName . '\'][\'themesuri\'] = $this->applications[\'horde\'][\'webroot\'] . \'/../themes/' . $appName . '/\';'; } $this->filesystem->filePutContentsIfModified($registryAppFilename, $registryAppSnippet); diff --git a/templates/00-horde.php b/templates/00-horde.php new file mode 100644 index 0000000..4c3ae5f --- /dev/null +++ b/templates/00-horde.php @@ -0,0 +1,59 @@ + +declare(strict_types=1); +/** + * AUTOGENERATED ONLY IF ABSENT. + * Edit this file to match your needs. If edited, this file is backup relevant. + * To redo, delete file and run `composer horde-reconfigure` + */ +// Default configuration relative to an autodetected webroot. This works in most cases. +$deployment_webroot = '/'; +$deployment_fileroot = 'webDir; ?>'; +$app_fileroot = '/var/www/horde-dev/web/horde'; +$app_webroot = webDir; ?>'/horde'; + +// Support separate webroots for some or all apps. +// As of horde-installer-plugin 2.7 this is an experimental feature and might be subject to breaking changes. +$canonical_webroots = []; +$useSubdomains = false; + +if (isset($useSubdomains) and $useSubdomains) { +// If you use a different webroot for at least one app, you should also set this. +// Otherwise navigating from one app to another yields unexpected results. +$canonical_webroots = [ + 'horde' => 'https://horde.mydomain.com/horde', + 'js' => 'https://horde.mydomain.com/js', + 'static' => 'https://horde.mydomain.com/static', + 'themes' => 'https://horde.mydomain.com/themes', +]; + +// Example for a custom webroot for an app +// $canonical_webroots['myapp'] = 'http://myapp.dev.localhost:5080/horde'; + 'tasks', + 'turba' => 'contacts', + 'kronolith' => 'calendar', + 'ansel' => 'photos', + 'wicked' => 'wiki', + 'passwd' => 'password', + 'chora' => 'code', + 'imp' => 'webmail', + 'whups' => 'tickets', +]; +$skipApps = [ + 'horde/components', + 'horde/horde', + 'horde/base', +]; +foreach ($this->apps as $app) { + // Skip these horde/components app + if (in_array($app, $skipApps)) { + continue; + } + [$vendor, $name] = explode('/', $app, 2); + $subdomain = $suggestedSubdomains[$name] ?? $name; + echo '$canonical_webroots[\'' . $name . "'] = \'https://$subdomain.mydomain.com/';\n"; +} +?> + +} \ No newline at end of file diff --git a/test/ConfigLinkerTest.php b/test/ConfigLinkerTest.php index e0ae8cc..6b6b6ea 100644 --- a/test/ConfigLinkerTest.php +++ b/test/ConfigLinkerTest.php @@ -11,6 +11,7 @@ * @category Horde * @package HordeInstallerPlugin * @subpackage UnitTests + * @coversNothing */ class ConfigLinkerTest extends TestCase { @@ -18,7 +19,7 @@ class ConfigLinkerTest extends TestCase private string $fixture; public function setUp(): void { - $this->fixture = __DIR__. '/fixture/ConfigLinker'; + $this->fixture = __DIR__ . '/fixture/ConfigLinker'; $this->linker = new ConfigLinker($this->fixture); } diff --git a/test/RecursiveCopyTest.php b/test/RecursiveCopyTest.php index 19e92c2..7dce5b6 100644 --- a/test/RecursiveCopyTest.php +++ b/test/RecursiveCopyTest.php @@ -11,6 +11,7 @@ * @category Horde * @package HordeInstallerPlugin * @subpackage UnitTests + * @coversNothing */ class RecursiveCopyTest extends TestCase { @@ -18,7 +19,7 @@ class RecursiveCopyTest extends TestCase private string $fixture; public function setUp(): void { - $this->fixture = __DIR__. '/fixture/RecursiveCopy'; + $this->fixture = __DIR__ . '/fixture/RecursiveCopy'; $this->copy = new RecursiveCopy($this->fixture . '/source', $this->fixture . '/dest'); } @@ -34,7 +35,5 @@ public function testCopyTree() $this->assertFileExists($this->fixture . '/dest/sub1/sub2/egal.txt'); } - public function tearDown(): void - { - } + public function tearDown(): void {} }