This introduction is targeted at developers with little or outdated prior Horde knowledge. Having worked with any Backend Development Framework in any language can help. While a lot of these facts can be found in Wikis or previous article, others are fairly new or only relevant to the developments in the version of horde delivered via https://horde-satis.maintain.com – This article will be continuously updated as I have time or relevant questions pop up.
Setup your work environment
A ready-made docker-compose deployment can be downloaded here: https://github.com/maintaina/deployments/
A basic root project for installing horde via composer can be found here: https://github.com/maintaina-com/horde-deployment/
Either way, you should be able to connect into a running sample installation on your local desktop within less than 5 minutes. Visual Studio Code and other editors can directly connect into the container content, so there is no need for fancy mounts etc. Mind each repo’s readme files for instructions.
Horde as viewed by the composer installer
Composer is the installer used for Horde and also does most of the autoloading.
A root project should have at the very least the paths /web/ for all web visible assets, /vendor/ for dependencies and /var/ for both configuration, logs and variable data which should NOT be web-visible.
Besides the default library type, horde comes with a composer plugin to support the types horde-application, horde-theme and horde-library. The /web/ and /vendor/ dirs are under the control of composer, do NOT put any custom content there. It will be deleted with each subsequent update. This is different from the older PEAR installer which would only overwrite files with newer package content, but not remove any custom files not included in the package.
A ready-made docker-compose deployment with some default configuration can be found here:
All horde-application packages are installed to the /web/ dir.
All library, horde-library, horde-theme packages will be installed to the /vendor/ dir.
If a package is a horde-library or a horde-application and has a js/ dir, its contents are symlinked into a structure below web/js/. If a package is a horde-application or a horde-theme, specific links under the /web/themes/ dir are created on install or update. The installer plugin will search for application config files under the /var/config path and link them to /web/$app/config/. The installer will also autogenerate some files under /var/config if they are missing.
All Horde applications share the same filesystem layout.
- /app/controllers/ contains old-style Horde_Controller request handlers.
- /bin contains commandline scripts or cron jobs, usually prepended by the application name and without the .php suffix – these will automatically linked to the /vendor/bin path unless otherwise declared.
- /config contains actual defaults files from the package as well as symlinks to user-provided or autogenerated configuration items. The routes.php file goes here.
- /doc dir contains the license file, changelog.yml file, optionally a CHANGES file and any documentation in RST format. Some documentation can be autogenerated from the horde.org wiki.
- /lib dir contains PHP code which is usually unnamespaced and follows the PSR-0 autoloading standard.
- /locale contains machine-readable PO translation files and the sources from which they are generated.
- /migration contains code for automated buildup, upgrade and teardown of SQL database schema.
- /scripts contains upgrade scripts and non-php content like LDIF files or apache config snippets.
- /src dir contains PHP code following PSR-4 Namespaced Autoloading and PSR-12 Coding Standards.
- /templates contains PHP and HTML templates mostly for backend-rendered content
- /test contains unit tests and integration tests built with the phpunit framework.
- /themes contains css and image files. See separate section on themes.
- The top dir contains a composer.json manifest, a PEAR package.xml manifest, possibly some README.md and other control files. Traditionally, it housed all the entry points through which the browser would call into PHP code and get server-rendered pages as a result. This is no longer the recommended pattern.
Most of your newly developed code should live somewhere under /src. Libraries follow the same layout.
The general rule is you cannot hardcode any web-visible paths. All assumptions will eventually be wrong.
The webmail application could reside under https://imp.foo.org/ rather than https://foo.org/imp. Themes could be presented from a totally different subdomain. The horde backend knows how to autogenerate the appropriate URLs. Your frontend should not make any assumptions.
Use the horde-translation tool to maintain translations. It generates binary .po files from a human-readable format. PHP can use these files to translate English text to your chosen frontend language. You can expose your frontend language under language-independent keys filled with the appropriate translations. You should NOT maintain translations in your frontend code as translation files can be customized by the administrator.
Horde can apply themes at runtime. Theme CSS is applied in the following order:
- Horde global default CSS
- Horde global custom theme css
- App-specific default CSS
- App-specific custom theme css (if present)
This mechanism will break if you use a build process that prepends CSS definitions with build-specific prefixes.
User-provided themes have the composer type horde-theme. The installer will link them to the web/themes folder.
TODO code example
Caching and minifying themes
Horde has a runtime css minifier and bundler. All CSS will be bundled into a file and minified. This file will be cached in the web-visible static dir. If the user selects another theme, horde will minify and cache a different collection of files. For this to work, you should not hardcode paths to CSS file paths in your HTML templates or frontend code.
TODO code example
With this amount of configurability, you need a source of truth about what goes where. This is the Horde Registry.
Querying the Registry
Routes, Middlewares and Handlers
Each application may have a config/routes.php file which contains web-accessible routes relative to this application and.
use Horde\Core\Middleware\AuthHordeSession; use Horde\Core\Middleware\RedirectToLogin; use Horde\Passwd\Middleware\RenderReactApp; use Horde\Core\Middleware\ReturnSessionToken; use Horde\Core\Middleware\DemandAuthenticatedUser; use Horde\Core\Middleware\DemandSessionToken; use Horde\Passwd\Handler\ReactInit; use Horde\Passwd\Handler\Api\ChangePassword; $mapper->connect( 'Api', '/api/changepw', [ 'controller' => ChangePassword::class, 'stack' => [ AuthHordeSession::class, DemandAuthenticatedUser::class, // DemandSessionToken::class, ], ] ); $mapper->connect( 'ReactInit', '/react', [ 'controller' => ReactInit::class, 'stack' => [ AuthHordeSession::class, RedirectToLogin::class, ] ] );
This example from a development version of the passwd app shows two routes: /react would hand out a browser page with all the boilerplate to load a Single Page Application UI (SPA). /api/changepw would receive a JSON message and, if the request is valid, change the user’s password. Each route definition contains of a name, a URL pattern and a third parameter defining the controller, the middleware stack or constraints like only processing the route for POST request. A route can also contain placeholders that can expand into variables and defaults for optional parts. Routes are processed in a first-hit-wins strategy so place your specific route definitions before the general cases. All routes are interpreted as relative to the application’s webroot.
The controller can be any string representing a class which is either a traditional Horde_Controller, a PSR-15 Request Handler or a PSR-15 Middleware. The only requirement is that the Injector needs to know how to produce it, either via Autowiring or via some explicit Binder (Factory, Implementation, Annotation …).
The Stack is either an array of strings representing PSR-15 Middlewares or a string recognized by the injector which will produce an iterable list of Middlewares. If there is no stack parameter, a default stack will be applied: Only allow requests for authenticated users identified by session cookie, forward everybody else to the login page. If you do not want any middlewares before your controller, explicitly set stack to an empty array. The middleware stack will be executed top to bottom before the controller is called and the request can be modified in that phase. If any middleware decides to answer your request by itself, no further middlewares will be called and the controller will not be executed. Responses travel back upward through the middleware stack and can be modified during that phase.
Lifecycle of a browser session
A typical user session would go like this:
- A user navigates to webroot or any application route
- As his browser provides no valid session cookie, he is redirected to the login page
- The user enters credentials into the login form and posts a request. This will create a valid session cookie
- The user is forwarded to his desired route or a default route
- User interactions either trigger ajax requests to API routes or forward him to a location outside the Single Page Application (i.e. another horde app or someplace outside). For ajax requests which change backend content, another credential besides the cookie is needed, for example a custom header with the session key or a special write token.
- Eventually, the user logs out and his session will be invalidated