bookmark_borderA new phase in life

TL;DR – I changed job and this will not affect ongoing maint. of anything Horde

I spent almost my whole work life with a single employer. It was quite a trip. I was part of it as a company grew from a hand full of guys into fifty, then hundred and ever more. I saw how structures developed, how people grew with their tasks and how a brand recognition built. It was a great time and I took advantage of all the opportunities and challenges that came along with it. How many places I traveled. The excitement of speaking at conferences, being a trainer, leading teams, designing architectures.

But after almost 15 years I am at a point where things need to change. Family is a priority now in a different way. Home has a different meaning. I needed a clean break. A few days ago I started a new job with one of Europe’s most relevant software companies. So far everything is shiny and new – I like it.

A little change comes along with it, too. Work life will not involve anything PHP or Horde anymore. There is this new, clear distinction between these things I do for fun or out of private interest on the one hand and earning money on the other. You cannot hire me for freelance work.

Nobody needs to worry. The Maintaina Horde fork is not going away. Development work on PHP 8.1 compatibility and features has not stopped.

Commercial Horde support at B1 Systems will still be around. I had the pleasure to work with an excellent team and that team is well capable of keeping up the expected quality and response times.
If you need any work done for hire, contact that company.

It’s an exciting summer after a pandemic winter. I will possibly take some weeks outside of mailing lists and bug reports to concentrate on things I must do now and things I like to do now. This is going to be fun. Stay tuned for updates.

bookmark_borderNet_DNS2 PHP 8.x compat issue

TLDR: When using horde/mail_autoconfig or other features using pear/net_dns2 under PHP 8.x, use the “master” branch or wait for a release of version 1.5.3 or higher.

While upgrading the Maintaina Horde codebase for PHP 8.1, I stumbled upon a problem:

[Sun May 15 12:15:15.026499 2022] [php:error] [pid 156] [client 172.23.0.1:43894] PHP Fatal error:  Uncaught ValueError: fread(): Argument #2 ($length) must be greater than 0 in /srv/www/horde/vendor/pear/net_dns2/Net/DNS2/Cache/File.php:147\nStack trace:\n#0 /srv/www/horde/vendor/pear/net_dns2/Net/DNS2/Cache/File.php(147): fread()\n#1 [internal function]: Net_DNS2_Cache_File->__destruct()\n#2 {main}\n  thrown in /srv/www/horde/vendor/pear/net_dns2/Net/DNS2/Cache/File.php on line 147, referer: http://localhost/horde/admin/config/config.php

It turns out Net_DNS2 as of latest version 1.5.2 has a problem in its filesystem cache when run under PHP 8.x – it tries to read the cache file content even if the file size is 0 or the file does not yet exist. In PHP 8.x this yields a ValueError because reading with a length 0 should not be attempted.

Mike Pultz, the maintainer of Net_DNS2 already has a fix in his master branch. I have kindly asked him to release a stable version with this fix. However I understand it may take a while or may not be very high on the daily priority list of todos. I am not sure what other changes the master branch contains which might impair its usefulness to your use case. I am hesitant to put work into a problem somebody else already fixed. I will only provide a downstream-packaged version in case no update happens within reasonable time.

bookmark_borderSimplifying Routing / PSR-15 bootstrap in Horde

As you might remember from a previous post, Horde Core’s design is more complex than necessary or desirable for two main reasons:

  • Horde predates today’s standards like the Composer Autoloader and tries to solve problems on its own. Changing that will impair Horde’s ability to run without composer which we were hesitant to do, focusing on not breaking things previously possible.
  • Horde is highly flexible, extensible and configurable, which creates some chicken-egg problems to solve on each and every call to any endpoint inside a given app.

Today’s article concentrates on the latter problem. More precisely, we want to make routing more straight forward when a route is called.

A typical standalone or monolithic app usually is a composer root package. It knows its location relative to the autoloader, relative to the dependencies and relative to the fileroot of the composer installation. Moreover, the program usually knows about all its available routes. It will also have some builtin valid assumptions about how its different parts’ routes relate to the webroot.
None of this is true with a typical composer-based horde installation.

  • None of the apps is the root package
  • Apps are exposed to a web-readable subdir of the root package
  • While the relative filesystem location is known, each app can live in a separate subdomain, in the webroot or somewhere down the tree
  • Each app may reconfigure its template path, js path, themes path
  • The composer installer plugin makes a sane default. This default can be overridden
  • Each app can be served through multiple domains / vhosts with different registry and config settings in the same installation
  • Administrators can add local override routes
  • Parts of the code base rely on horde’s own runtime-configurable autoloader rather than composer.

This creates a lot of necessary complexity. The router needs to know the possible routes before it can map a request. To have the routes, the context described above must be established. Complexity cannot be removed without reducing flexibility. There is, however, a way out. The routing problem can be divided into three phases with different problems:

  • Development time – when routes are defined and changed frequently for a given app or service
  • Installation/Configuration time – when the administrator decides which apps’ routes will be available for your specific installation
  • Runtime – when a request comes in and the router must decide which route of which app needs to react – or none at all

Let’s ignore development time for now. It is just a complication of the other two cases. The design goal is to make runtime lean and simple. Runtime should initialize what the current route needs to work and as little as possible on top of that. Runtime needs to know all the routes and a minimal setup to make the router work. Complexity needs to be offloaded into installation time. Installation time needs to create a format of definite routes that runtime can process without a lot of setup and processing. As a side effect, we can gain speed for each individual call.

Modern Autoloaders are similar in concept: They have a setup stage where all known autoloader rules of the different packages are collected. In composer, the autoloader is re-collected in each installation or update process. The autoloader is exposed through a well-known location relative to the root package, vendor/autoload.php – it can be consumed by the application without further runtime setup. The autoloader can be optimized further by processing the autoloading rules into a fixed map of classes to filenames (Level 1), making these maps authoritative without an attempt to fail over on misses (Level 2a) and finally caching hits and misses into the in-memory APCu opcache. Each optimization process makes the lookup faster. This comes at the cost of flexibility. The mapping must be re-done whenever the installation changes. Otherwise things will fail. This is OK for production but it gets in the way of development. The same is true for the router.

The best optimization relies on the application code and configuration being static. The list of routes needs to be refreshed on change. Code updates are run through composer. The composer installer plugin can automatically refresh the router. Configuration updates can happen either through the horde web ui or through adding/editing files into the configuration area. Admins already know they need to run the composer horde-reconfigure command after they added new config files or removed files. Now they also need to run it when they changed file content. In development, routing information may change on the fly multiple times per hour. Offering a less optimized, more involved version of this route collection stage can help address the problem.

A new version of the RampageBootstrap codebase in horde/core is currently in development. It will offload more of horde’s early initialisation stages into a firmware stack and will reduce the early initialisation to the bare minimum. At the moment, I am still figuring out how we can do this in a backward compatible way.

bookmark_borderMaking horde/core more versatile

Hello, you may have seen my blog go quiet for a while. After two years of a global pandemic, I decided to take some more time with my family and also offload some knowledge and responsibilities around the Maintaina Horde codebase to my fellow team members at B1 Systems. I also took some time considering some very fundamental Horde design implications. One of these is horde/core.

You can use the Horde Framework and its libraries in two different styles. The first style I would call loosely coupled. Most libraries try to depend on as few as possible other horde libraries and concentrate on specific purposes. That makes them reusable outside of the wider context of the framework. For example, the backend for Michael Rubinsky’s blog uses the Horde Router, the Horde Controller library and the Horde Injector DIC as a kind of micro framework without much supporting code. A custom binder simulates setup of view paths normally performed by a horde environment.

The alternative is the “horde app” use case or tightly integrated use case. Your site will have a horde base app providing common capabilities and one or multiple modules or horde apps. They integrate with a framework-generated topbar, can use an inter-app API, react to a common styling choice etc. Developers benefit from being able to reuse solutions for preference storage and UI, configuration, permissions, user groups, predefined callbacks for webdav, rpc, language and presentation handling etc. This is all great if you just want to build some addon to your Horde Groupware installation. In other cases, Horde’s base does a lot of stuff you do not really need or want. I want to change that.

The horde/core library started out as a spinoff off the horde base app. It contains a lot of glue for putting together backends, drivers, config files, caches and it also provides base classes needed by the horde applications. The horde base app contains the endpoints for RPC, route-based UI, webdav, caldav. It also contains a micro bootstrapping file called horde/lib/core.php. This file installs an error handler, sets up some basic PHP sanity stuff, does some magic around Horde’s own autoloader, defines some constants and hooks into horde/config/horde.local.php, allowing the admin to inject some early-init custom magic. Our horde-installer-plugin for composer makes use of this hook to setup the composer autoloader (and some more constants) in a backward compatible way.

I think, outside of support for old-style code bases, none of this should happen. But our current implementation of the controller framework still depends on that magic and uses code in horde/core which also depends on it. In pre-composer environments, horde needs some tricks to find out basic facts about where it is, where everything else is, how to setup autoloading etc. In modern environments, we should neither pollute the global namespace with constants and global variables nor should we have an involved, tightly coupled setup process way before we even look at the app and route called for. We need to reorder how and when things are done.

Both developers and runtime operators benefit from this redesign. Developers can reduce boilerplate when relying on much, but not all of the Horde Framework and not exactly building a module for a horde installation. They are less entangled in conventions which do not make sense anymore. Operators will notice a lower memory, i/o and computation footprint, resulting in higher speed. This is because we throw out a lot of overhead unrelated to the current call. In the old setup we even created an IMAP connection when the current user was forbidden to use the mail component and the screen shown was for changing passwords or handling preferences. Yes, even when downloading an addressbook you would have triggered and IMAP connection handshake. We do not need that and by now, we have to tools to avoid it.

Stay tuned for more details in upcoming articles.

bookmark_borderPHP 8 Horde (Maintaina)

Over the next few days, all Horde libraries and apps in the maintaina-com organization will be whitelisted for PHP 8x. in their FRAMEWORK_6_0 branch development versions. One next step will be a flavour of the OpenSUSE based containers and deployments which runs off PHP 8.0. While some few libraries have been enabled for PHP 8, it is almost certain that horde as a whole will not run correctly. Main culprits are the horde/rpc and horde/form packages and their user code, but there are some other ugly places that need attention.

Development Baseline at 7.4

Code in the maintaina-com repo will stay compatible with PHP 7.4 – at least for the time being. Decisions at Horde LLC may override that at some point or time may just march on. PHP 7.4 has been released two years ago, has ended active support 20 days ago and will be EOLed for upstream security support on November 28th 2022 – roughly 11 months to go. Linux distributions have a tradition to follow their own schedules and backport security fixes. OpenSUSE LEAP 15.3 ships with PHP 7.4 while openSUSE Tumbleweed has switched to PHP 8.0.13 – with PHP 8.1 versions becoming available from official repos soon.

This is a tough decision as PHP 8 and 8.1 have some really interesting features which would allow us to develop more elegant, more readable and more efficient code. For software that is not intended for this audience, I will immediately allow using 8.x-only features as soon as we are confident with Horde’s compatibility. This is going to be a major theme of January and possibly February.

No need to switch right now

If you are running Horde as of horde.org master branches or maintaina-com FRAMEWORK_6_0 branches off PHP 7.4, you should NOT switch right now. We will announce once we think any leftover issues are minor enough for an acceptable early adopter experience.

No particular love for 8.0.x

There is no guarantee our runtime will stay fixed at 8.0. PHP 8.1 offers a lot of new features and a considerable performance boost for some relevant scenarios. While making Maintaina Horde work with 8.x on a 7.4 feature baseline is the first step, the logical next step is upgrading feature baseline to 8.1 or higher. This will be much less of a problem if we get an official Horde 6 release in the meantime and users can choose between a properly conservative release version and a more adventurous Maintaina version. This is not something I have under control though. Horde LLC do as they find appropriate and sustainable and for many users, there is little reason to choose Maintaina over the official releases once we have a Horde 6 version that properly runs on recent PHP and supports Composer out of the box. I am perfectly fine with that and looking forward to it. I will always assist with a migration path as far as I can afford to.

Time is Money, Money buys Time

If you have an urgent commercial interest in a PHP 8-ready Horde version, you really do not want to rely on Maintaina’s timelines and priorities which may be subject to change. You will need to spend money. Approach somebody to do it for you, either Horde LLC or the company I work for, B1 Systems GmbH – both are formidable places to look for Horde-experienced development resources.

Update 2021-12-18 21:00 CET

I just ran the update to the metadata as a mass operation for everything which contains a .horde.yml file – the rest will have to wait until I stumble across it. I leveraged an edited version of horde/git-tools, some bash magic, some mass editing in vscode using their regex tool and some manual fixing.

  • All packages now formally require “php”: “^7.4 || ^8”
  • If horde-installer-plugin is required, I now go for “^2 || dev-FRAMEWORK_6_0” – however in maintaina-com/Core, I have a job that rebuilds composer.json on commit and this job showed me that the components tool needs an update in this aspect.
  • SPDX license code warnings for LGPL and GPL versions have been remedied to LGPL-2.0-only, LGPL-3.0-only, GPL-3.0-only each
  • Added the CI workflow where missing. Mostly it will fail until further editing. This is intentional.
  • I did NOT unify all versions of CI workflow as some deviations are intentional. I did however unify PHP versions for the unit tests to “7.4”, “8.0” and “latest” and I did unify phpunit versions to “9.5” and “latest”.
  • Unified/added the phpdoc workflow and the update-satis workflow as we had multiple versions for no good reason. I have settled for a version of the phpdoc job that will scan lib/, src/ and app/ if they exist
  • Cleaned up a lot of metadata mess in the Kolab related packages.
  • Removed some version: tags from composer.json files
  • Removed the optional pear dependency of imp for the ASN1 implementation from phpseclib – need to look for a proper composer-ready and less outdated replacement.

While the mass changes themselves seem to have gone right, the resulting avalanche of CI jobs showed some issues:

  • phpdoc job and update-satis job fail if they run in parallel and the satis repo content has changed since checkout. Either give the push commands in the loop a minute to wait each time or make the job smarter about handling these clashes. Still, failing is better than silently overwriting content
  • Having so many versions of the CI job is not maintainable. Need to factor out the boilerplate into an action, make version requirements a config variable with a builtin default and have some mechanism for there rare cases where extra software is needed for meaningful QA, i.e. database and storage related items.
  • After getting this migration done, upgrading the git-tools utility may be an interesting exercise in PHP 8 and PHPStan.
  • I may have created unnecessary conflicts with some open pull requests. Sorry, contributors. I will improve.

bookmark_borderHorde/Log Rewrite goes PSR-3

I have rewritten Horde/Log based on the PSR-3 Logging standard published by PHP-FIG.

Why?

It had to been done at some point. The current wave of Corona pandemic has cancelled some joyful other activities planned for this weekend and I had been looking into PSR-3 loggers for quite some time. Most importantly, I wanted to do something else which needed a separeate logging facility and I was not ready to invest time into the various pitfalls of the old logger design. Just look at the Logger Factory – it is much too complex. Currently, there is no good balance between having too few logs in general or being flooded with mostly useless details of all the different aspects of horde. Filtering is essential, but the data cannot easily be divided into the contexts that are relevant to different problems and tasks.

Goals

My main goal was simple: Consuming code should be able to give the Logger more context about the messages it sends. This can be helpful to sort out what is interesting and what is just distracting noise. For example, a logger-aware library or application may send markers along with the actual message. All messages go to the same logger, but different log handlers may be set up to only care about certain aspects. Want to write all caldav sync errors for a specific user to a separate file? Want to keep a separate log of failed login attempts? Want to forward your time tracking application’s “project closed” log events to an external json-consuming system? Even though the old logger had all the necessary parts, it made these tasks too difficult.

As a product, the new logger is not very interesting outside the Horde context. While it can be used for logging in PSR-3 aware libraries, it still has too many dependencies on other parts of the Horde ecosystem. To reduce this, I may factor out the Constraints log filter into a separate library. The opposite case is more interesting: If PSR-3 replaces tight coupling to a custom logger, projects may use their existing logger. They have one less alien dependency to deal with. This might make some libraries more attractive, like Horde/Activesync or Horde/Imap.

From a code quality perspective, I also wanted to make the code more transparent to readers and tools. The old implementation relied on a __call magic method without really needing it, multiple parts relied on tersely documented array structures. The new implementation passes PHPStan Level 8. Coverage with parameter and return type hints is very high, with native property and return types following where possible. The current implementation is based on version 1.1.4 of the standard. When moving to a PHP 8 minimum requirement, this can be upgraded to the more strictly typed version 3.0 standard.

However, I am still missing unit tests against the new code. As it is substantially different from the H5 implementation, I could not easily adapt the existing test cases. This will require more work.

Also, integration of the new Logger into the core system is a separate task. Old and new logger infrastructure will have to coexist for some time. There is simply too much code that needs to be touched.

Architecture

The logger is architected as a modular system. The consuming code only has to deal with the Horde\Log\Logger. It implements the Psr\Log\LoggerInterface. The logger can support custom log levels not covered by RFC 5424. Log Levels are implemented as objects with a string name and a criticality number value.

The PSR-3 standard mandates log messages may be strings or any object that can be turned into a string. Internally, we convert them into LogMessage objects containing the string message, a reference to the LogLevel object and a hash of context attributes.

LogFilters are gatekeepers which look into a LogMessage and decide if it may be logged. They can be used as global LogFilters to suppress a log message altogether or as local filters which only affect a certain log handler. The Logger may include many different LogHandlers. These implement the actual processing of logs, writing them to a file, to a local syslog program or sending them over the network. Log messages may further be formatted for different needs. One handler may want to send XML documents to another server, another handler may store plaintext in a structured file. PSR-3 proposes a templating format where the logger can fill placeholders in the message with data from the context array. In Horde/Log, this job is done by a series of LogFormatters. Depending on configuration, a LogHandler can have zero, one or many such LogFormatters. Not all combinations make sense.

PHP 8 readiness

The new code is ready to run on PHP 7.4 and PHP 8. However, the horde/constraint and horde/thrift dependencies have not yet been upgraded for PHP 8, limiting usefulness. This will be done as time permits, with many other topics having higher priority.

References

bookmark_borderOctober Review: TOTP in Horde

I have been working on multiple things recently.

Kronolith Web UI: Appointment Cancellation Bug

Fix an annoying bug where internal user attendees get cancellation mails when an appointment is updated by the owner. This only seems to happen from the Web UI, not from CalDAV. I already analysed how this is happening. The fix is going to be a little bigger as I do not want to invest in the legacy infrastructure (socalled “Imples”) and use the opportunity to use a more modern approach. Work is in progress.

New Material UI based frontend for passwd.

I have worked with the team on a Material Design based UI. It uses ReactJs and Typescript and the new horde/http_server library and it is very different from existing Horde UIs. Do not expect it to blend well with the existing horde look&feel. The whole thing is a proof of concept and is an alien as the DIMP UI was back in Horde 3. This proof of concept still lives in a public feature branch and if you want to try it, you need to enable a new setting in the Preferences Screen.

Two-Factor support in Horde Base and a TOTP library

More and more online services start using two-factor authentication for improved security. Along with a password, users have to enter some passcode they read from a keychain fob device or from an app on their phones (like Google Authenticator).

I have started a new library horde/otp which implements TOTP and other styles of passcodes used as a secondary authentication factor. The library needs some additional glue code in horde/core and horde/base which still has to be built. I would have liked to finish this in October but there is only so much time.

Improved horde-installer-plugin

The composer plugin for Horde has received some refactoring and enhancements. The current feature branch offers a custom command in the composer CLI . This custom command rebuilds the relevant configuration files when you move your Horde installation after running the install/update commands. There are also some minor changes to the way configurations are written. End users should not notice.

DNS library

B1 Systems have finally opensourced a DNS library for the Horde ecosystem. It has been used internally for some years. The library can serve as the DNS building block of an IPAM system, but it also has an adapter to apply changes to the Amazon Route 53 service.

PHPStan support

Beginning this month, libraries and apps will gradually introduce the static analyzer tool phpstan. The tool will run as part of the CI pipeline and detect various types of code imperfections which potentially can mean hard-to-detect bugs. The findings will be addressed as time permits.

bookmark_borderDeveloper Introduction to Maintaina Horde

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.

Filesystem layout

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.
  • /js contains ready-to-run Javascript code. Minifying is not necessary as it can be done by horde.
  • /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.

Web Layout

The general rule is you cannot hardcode any web-visible paths. All assumptions will eventually be wrong.
In a default installation as generated by the installer, the horde base app will be under /horde/ and the login screen will be under /horde/login.php etc. Other applications will be on the same level besides the /horde base app – /turba for the addressbook, /passwd for the password utility etc. Javascript will be visible under /js and themes under /themes – this is, unless you have configured something else. The webroot could be /tools/foo/bar rather than /.
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.

Translation

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.

Themes

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.

Caching and minifying Javascript

Horde has a runtime javascript bundler and minifier which will bundle all javascript marked for delivery into one file and minify it. This file will be cached into the web-visible static dir. On version update, the file name will change. For this to work, you need to tell the framework which javascript files to include rather than just dish out a static html file with hardcoded paths to the individual JS files.

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

The Registry

With this amount of configurability, you need a source of truth about what goes where. This is the Horde Registry.
The registry has an index of all applications, their base web and filesystem location, which RPC and Inter-App APIs they expose, how they are represented in the topbar menu and where their Javascript and Themes resources are available in the local filesystem and as viewed from the web.

Registry Configuration

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
  • The backend sends all the HTML, CSS, graphics and javascript required to show the requested page. Javascript and CSS are each minified and bundled into cache files. Also, the framework inserts a javascript variable with dynamic data like the location of the API endpoints, chosen UI language etc.
  • 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

Permissions system

Administrator users

Shares

Preferences

Configuration System

Virtual Host specific configuration

Inter-App API and RPC

Special classes

ORM Layer horde/rdo

bookmark_borderCustom iCalendar data in Kronolith & Nag

The iCalendar exchange format is everywhere in Horde’s calendar (kronolith) and tasks (nag) apps. It is offered for manual import/export. It is the centerpiece of the CalDAV synchronisation protocol and various APIs of these apps. The format also plays a role in email invitiations and updates sent via iTip. It is a very powerful and well-accepted format. This is, unfortunately, a little bit of a problem. Fortunately, we also have a solution

The iCalendar format was originally defined by the IETF. They released RFC 2445 back in 1998. While Horde still understands that format, a newer version 2.0 was defined in RFC 5545 in 2009. An extension to the 2.0 standard was released in 2016: RFC 7986 defines additional attributes needed to describe related conferencing software resources or other technical aspects.

Server and client software has always been free to add custom attributes to the different components of the standard. For example, Horde Kronolith stores and reads a special attribute X-HORDE-ATTENDEE to mark event attendees who have a horde user attached. Mozilla uses attributes like X-MOZ-LASTACK and MS Exchange comes with a long list of custom attributes, among them X-CALEND. Understanding these attributes when processing a icalendar file is optional. However, servers are expected not to silently drop attributes they do not understand.

Up until now, both Kronolith and Nag did support a wide range of attributes, but far from all. Events are reconstructed from calendar backend contents. Unsupported data simply gets lost. As it is technically impossible to know all attributes anybody out there may use, the opposite definition was needed: An attribute is an “other” attribute if it is not in the list of attributes the app already handles. Both apps got a catalog of attributes they understand and a property for storing everything else. Support for actually storing and retrieving this data is currently limited to the SQL backend. I do not currently invest time in Kolab support, maybe I will never do that.

Also, while I took care to ensure that normal editing via the UI will not break the “other” data, I did not cross check with scenarios where ActiveSync is involved. Please report bugs as you find them.

CalDAV event Storage

For kronolith, an optional storage of unmodified caldav events as received has been added during development. I am not sure if I will keep this or remove it again. It is useful for debugging various support scenarios but under normal operation, it is not required. It may be useful for creating a cache of external caldav calendars though.

Extend to Turba Addressbook

Turba Addressbook uses a similar data format vcard and implements the sister protocol CardDAV. Things are indeed a little more complicated. There are three relevant versions of vcard 2.1, 3.0 and 4.0 and multiple extension RFCs with additional contact attributes exist. Worse, there is a host of X- attributes from popular desktop addressbook vendors, Evolution, Kontact, Thunderbird, Android, MS Exchange/Outlook… There are at least two different ways to represent contact groups and the whole thing is a little messy. But the real problem lies with Turba’s highly configurable nature. At least two backends need to be upgraded: Both SQL storage and LDAP play a key role in typical Turba use cases. The exact layout of each addressbook can be different even inside the same installation. The list of vcard attributes considered as natively supported needs to be calculated for each addressbook and additional attributes maybe need to be stored outside of the specific backend. This will take some time and consideration.

Still, Turba’s tendency to forget what it does not care for needs to be addressed. Otherwise users risk losing attributes on sync.

bookmark_borderMaintaina Horde switches to openSUSE LEAP

Our Horde docker images have switched over from Tumbleweed to openSUSE LEAP once again.

Recently our container build CI job in github.com broke down unexpectedly. An investigation showed that Tumbleweed’s core libraries, especially libc, were too new for the CI’s build system, based on Ubuntu LTS.

This is the second time we abandoned the Tumbleweed basis for Horde docker containers. OpenSUSE Leap 15.3 uses a relatively old, but well-maintained, set of base libraries. Both Leap and Tumbleweed deliver PHP 7.4 as a basis for Horde. In both systems, we skip the packaged composer version for a static pick which we will update from time to time. We may switch over to packaged composer if we feel confident.

For users and administrators of the image, both Tumbleweed and Leap 15.3 should feel more or less the same. For end users of the delivered horde setup, there should not be any downsides. We will switch back to the Tumbleweed image in a while when we have picked a more recent version of Ubuntu.