Skip to content
Draft
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
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
"horde/date": "^3 || dev-FRAMEWORK_6_0",
"horde/eventdispatcher": "^1 || dev-FRAMEWORK_6_0",
"horde/exception": "^3 || dev-FRAMEWORK_6_0",
"horde/form": "^3 || dev-FRAMEWORK_6_0",
"horde/group": "^3 || dev-FRAMEWORK_6_0",
"horde/hashtable": "^2 || dev-FRAMEWORK_6_0",
"horde/history": "^3 || dev-FRAMEWORK_6_0",
Expand Down
3 changes: 3 additions & 0 deletions src/DefaultInjectorBindings.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
use Horde\Core\Factory\PermissionServiceFactory;
use Horde\Core\Factory\PrefsServiceFactory;
use Horde\Core\Factory\RegistryConfigLoaderFactory;
use Horde\Core\Factory\RouteUrlWriterFactory;
use Horde\Core\Factory\SecretManagerFactory;
use Horde\Core\Factory\SessionHandlerFactory;
use Horde\Core\Factory\SimpleCacheFactory;
Expand All @@ -96,6 +97,7 @@
use Horde\Core\Service\VersionCheck\VersionService;
use Horde\Core\Uri\RegistryRouteMapperProvider;
use Horde\Core\Uri\RouteMapperProvider;
use Horde\Core\Uri\RouteUrlWriter;
use Horde\Db\Adapter as DbAdapter;
use Horde\Editor\Tinymce;
use Horde\HashTable\HashTable;
Expand Down Expand Up @@ -278,6 +280,7 @@ public function register(Injector $injector): void
ListenerProviderInterface::class => [EventDispatcherFactory::class, 'createListenerProvider'],
SimpleCacheInterface::class => SimpleCacheFactory::class,
PsrHttpClientInterface::class => HttpClientFactory::class,
RouteUrlWriter::class => RouteUrlWriterFactory::class,
];

$implementations = [
Expand Down
35 changes: 35 additions & 0 deletions src/Factory/RouteUrlWriterFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

/**
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/

namespace Horde\Core\Factory;

use Horde\Core\Config\RegistryState;
use Horde\Core\RuntimeRoutesProvider;
use Horde\Core\Uri\RouteUrlWriter;
use Horde\Injector\Injector;

class RouteUrlWriterFactory
{
public function create(Injector $injector): RouteUrlWriter
{
$provider = $injector->getInstance(RuntimeRoutesProvider::class);
$registryState = $injector->getInstance(RegistryState::class);
$hordeConfig = $registryState->getApplication('horde');
$webroot = $hordeConfig['webroot'] ?? '/horde';

return new RouteUrlWriter($provider, $provider->environ, $webroot);
}
}
6 changes: 3 additions & 3 deletions src/Middleware/AppRouter.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@
use Horde_String;
use Psr\Http\Message\ResponseFactoryInterface;
use Horde\Exception\HordeException;
use Horde\Core\RuntimeRoutesMapper;
use Horde\Core\RuntimeRoutesProvider;

/**
* AppRouter middleware
*
* Matches the request against the pre-loaded RuntimeRoutesMapper,
* Matches the request against the pre-loaded RuntimeRoutesProvider,
* resolves the per-route middleware stack, and dispatches the controller.
*
* Sets Attributes:
Expand All @@ -35,7 +35,7 @@
class AppRouter extends RampageRequestHandler implements MiddlewareInterface, RequestHandlerInterface
{
public function __construct(
private readonly RuntimeRoutesMapper $runtimeMapper,
private readonly RuntimeRoutesProvider $runtimeMapper,
private readonly Injector $injector,
) {}

Expand Down
4 changes: 2 additions & 2 deletions src/Middleware/HordeCore.php
Original file line number Diff line number Diff line change
Expand Up @@ -63,11 +63,11 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
$registry = $injector->getInstance('Horde_Registry');
$request = $request->withAttribute('registry', $registry);

// Bridge RuntimeRoutesMapper into legacy injector so controllers can use urlFor()
// Bridge RuntimeRoutesProvider into legacy injector so controllers can use urlFor()
$mapper = $request->getAttribute('mapper');
if ($mapper !== null) {
$injector->setInstance(\Horde\Routes\Mapper::class, $mapper);
$injector->setInstance(\Horde\Core\RuntimeRoutesMapper::class, $mapper);
$injector->setInstance(\Horde\Core\RuntimeRoutesProvider::class, $mapper);
}

// Push the identified app onto the legacy registry stack
Expand Down
20 changes: 20 additions & 0 deletions src/PageOutput/AssetCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,9 @@ class AssetCollector
/** @var array<string, array{content: string, httpEquiv: bool}> */
private array $metaTags = [];

/** @var string[] */
private array $inlineStyles = [];

/** @var string[] */
private array $linkTags = [];

Expand Down Expand Up @@ -134,6 +137,15 @@ public function addLinkTag(array $attrs = []): void
$this->linkTags[] = $out . ' />';
}

public function addInlineStyle(string $code): void
{
$code = trim($code);
if ($code === '') {
return;
}
$this->inlineStyles[] = $code;
}

/** @return string[] */
public function getScriptUrls(): array
{
Expand Down Expand Up @@ -187,6 +199,14 @@ public function renderStylesheetTags(): string
return $html;
}

public function renderInlineStyleBlock(): string
{
if (empty($this->inlineStyles)) {
return '';
}
return "<style>\n" . implode("\n", $this->inlineStyles) . "\n</style>\n";
}

public function renderJsVarBlock(bool $topOnly = false): string
{
$lines = [];
Expand Down
1 change: 1 addition & 0 deletions src/PageOutput/PageComposer.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ public function renderHead(PageMeta $meta): string
$html .= " <head>\n";
$html .= ' ' . $this->assetCollector->renderMetaTags();
$html .= ' ' . $this->assetCollector->renderStylesheetTags();
$html .= ' ' . $this->assetCollector->renderInlineStyleBlock();

if ($meta->faviconUrl !== null) {
$html .= ' <link type="image/x-icon" href="'
Expand Down
77 changes: 77 additions & 0 deletions src/PageOutput/PageOutputAssetManager.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
<?php

declare(strict_types=1);

/**
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/

namespace Horde\Core\PageOutput;

use Horde\Form\V3\Renderer\AssetManager;

/**
* Adapter bridging Form V3's AssetManager to Core's page-level AssetCollector.
*
* When injected into HtmlRenderer, form-required assets (scripts,
* stylesheets, inline code) are collected by the page's AssetCollector
* instead of being rendered inline after the form. PageComposer then
* places them in the correct document location (head or deferred foot).
*
* Usage:
*
* $assetManager = new PageOutputAssetManager($assetCollector);
* $renderer = new HtmlRenderer(assetManager: $assetManager);
* $formHtml = $renderer->render($form, $url, 'post');
* // $formHtml contains no <script>/<link> tags
* // Assets appear when PageComposer renders head/foot
*
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/
#[Factory(factory: PageOutputAssetManagerFactory::class, method: 'create')]
class PageOutputAssetManager implements AssetManager
{
public function __construct(
private readonly AssetCollector $assetCollector,
) {}

public function addScript(string $file, array $attrs = []): void
{
$this->assetCollector->addScript($file);
}

public function addStylesheet(string $file, array $attrs = []): void
{
$this->assetCollector->addStylesheet($file);
}

public function addInlineScript(string $code): void
{
$this->assetCollector->addInlineScript($code);
}

public function addInlineStyle(string $code): void
{
$this->assetCollector->addInlineStyle($code);
}

public function render(): string
{
return '';
}

public function clear(): void
{
}
}
38 changes: 38 additions & 0 deletions src/PageOutput/PageOutputAssetManagerFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

/**
* Copyright 2026 The Horde Project (http://www.horde.org/)
*
* See the enclosed file LICENSE for license information (LGPL). If you
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
*
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/

namespace Horde\Core\PageOutput;

use Horde_Injector;
use Horde\Injector\Injector;

/**
* Factory for PageOutputAssetManager.
*
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/
class PageOutputAssetManagerFactory
{
public function create(Horde_Injector|Injector $injector): PageOutputAssetManager
{
$assetCollector = $injector->getInstance(AssetCollector::class);

return new PageOutputAssetManager($assetCollector);
}
}
6 changes: 3 additions & 3 deletions src/RampageBootstrap.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ public static function run(): void
}
$injector->setInstance(RegistryState::class, $registryState);

// 5. RuntimeRoutesMapper — pre-load ALL app routes
$runtimeMapper = new RuntimeRoutesMapper($registryState, $request);
// 5. RuntimeRoutesProvider — pre-load ALL app routes
$runtimeMapper = new RuntimeRoutesProvider($registryState, $request);
$runtimeMapper->loadAllApps();
$injector->setInstance(RuntimeRoutesMapper::class, $runtimeMapper);
$injector->setInstance(RuntimeRoutesProvider::class, $runtimeMapper);
$injector->setInstance(\Horde\Routes\Mapper::class, $runtimeMapper);

// 6. Match route
Expand Down
16 changes: 14 additions & 2 deletions src/RuntimeRoutesMapper.php → src/RuntimeRoutesProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,20 +18,24 @@

use Horde\Core\Config\RegistryState;
use Horde\Core\Middleware\DefaultStack;
use Horde\Core\Uri\RoutesProvider;
use Horde\Http\Uri;
use Horde\Routes\GroupMapper;
use Psr\Http\Message\ServerRequestInterface;

/**
* Runtime route mapper that loads routes from all registered apps via GroupMapper.
* Runtime routes provider — loads and serves routes from all registered apps.
*
* Used in developer/debug mode (when var/config/use_compiled_router is absent).
* In production, the compiled route cache is used instead via CompiledMatcher.
*
* Implements RoutesProvider so that RouteUrlWriter can generate URLs from
* named routes without coupling to the runtime-vs-compiled distinction.
*
* Group context (prefix, host, scheme, port, defaults) is applied to each app's
* routes at definition time. The resulting Route objects are fully self-contained.
*/
class RuntimeRoutesMapper extends GroupMapper
class RuntimeRoutesProvider extends GroupMapper implements RoutesProvider
{
public function __construct(
private readonly RegistryState $registryState,
Expand Down Expand Up @@ -117,4 +121,12 @@ public function loadAllApps(): void
$this->compile();
}

public function generateNamedPath(string $routeName, array $params = []): ?string
{
$route = $this->getRouteNames()[$routeName] ?? null;
if ($route === null) {
return null;
}
return $route->generate($params);
}
}
Loading
Loading