bookmark_borderJWT Authentication: Building a Hybrid Model That Actually Works

Part 2 of 4: Architectural Evolution in Horde 6
In part 1 of the evolution series: The jQuery Problem I discussed frontend concerns – How to move off a dead mobile-only framework towards a mobile-first responsive design and not get caught in the next framework I don’t need. This time we move towards authentication concerns.


Several years ago I started exploring JWT authentication for Horde. The timing seemed wrong. Too many moving parts, too many unknowns about how it would integrate with our session-based architecture. I shelved the work and moved on to other priorities. Also life got a say in my time table more than once.

The trigger to revisit JWT wasn’t a sudden technical insight or some grand vision. It was user requests. Boring, persistent, increasingly urgent user requests. Enterprise administrators kept asking about OpenID Connect support, OAuth 2.0 integration, SAML single sign-on. These aren’t niche use cases anymore. They’re what enterprise deployments usually do in 2026. A web app with direct access to the company LDAP is becoming more rare, especially when talking about partial write access. “Can we authenticate with our Azure AD?” “Can users log in with their company Google account?” “Does Horde support SSO?” The questions kept coming and I kept saying, well, yes, partly, there are plans, … sometimes I knew what to do from custom developments not fit for public release.

Yet the answer was no. Not exactly like this. Not now. Not production ready. No. No. No. And the reason was architectural: Horde’s authentication stack was built entirely on PHP sessions. Twenty years of PHP sessions. Those enterprise authentication protocols expect token-based flows. Without JWT infrastructure, we couldn’t even start building those integrations without awkward compromises. And some things we did with sessions were starting to break because of tightened cookie and cross site security policies in browsers. You can’t really tell users to change their browser security and remove generally accepted and sane defaults because your app wants that.

So I revisited JWT. This time with production requirements pushing me forward and no excuses left to hide behind except, obviously, this is a free time volunteer work and I can do or not do what I want.

The Session-Based World

Horde has used PHP sessions for authentication since the beginning. The flow is straightforward:

  1. User submits username and password
  2. Authentication backend validates credentials (LDAP, SQL, IMAP, etc.)
  3. PHP session is created, credentials stored in $_SESSION
  4. Session ID cookie sent to browser
  5. Every subsequent request: PHP loads session from storage (files or database)
  6. Session contains actual credentials for IMAP/SMTP connections

This works. It’s worked for 20+ years. But it has limitations:

I/O overhead: Every HTTP request reads session data from filesystem or database. For high-traffic deployments this overhead is measurable.

No stateless operations: Everything requires a session internally. API clients get session cookies. They have no use for them but Horde needs to have a session so it issues one. Mobile apps need session persistence. Third-party integrations need session management. Long lived sessions keep the backend credentials between calls which only authenticate by token. Some recognition flows use sessions even in anonymous guest mode. That’s more expensive than it should be.

Limited scalability: Load balancing requires sticky sessions or shared session storage. Horizontal scaling is more complex than it should be.

No modern auth flows: OpenID Connect, OAuth 2.0, SAML all expect token-based authentication. Bridging sessions to tokens is awkward.

The federated authentication requests made it clear: we needed token infrastructure. And once we have it we can use it for a load of new features and integrations. But first things first.

The JWT Experiment

JWT (JSON Web Tokens) are a standard for representing claims securely. A JWT contains three parts:

  1. Header: Metadata (algorithm, token type)
  2. Payload: Claims (user identity, expiration, etc.)
  3. Signature: Cryptographic signature proving authenticity

Example JWT (decoded):

// Header
{
  "typ": "JWT",
  "alg": "HS256"
}
// Payload
{
  "sub": "aliced@spam.com",
  "iat": 1709856000,
  "exp": 1709859600,
  "jti": "a1b2c3d4"
}
// Signature (HMAC-SHA256 of header + payload + secret)

A token is base64-encoded and signed. Clients send it in HTTP headers: Authorization: Bearer eyJ0eXAiOiJKV1Qi...

The appeal: servers can validate tokens cryptographically without database lookups. Stateless authentication. Perfect for APIs and modern auth flows. The server gets to know

This is user ID aliced123 coupled to display name “Alice Dee <aliced@spam.com>” and I can trust she authenticated recently

The server does not need to look up the frequently required public name and it does not need to validate if alice is logged in – the token will expire without frequent renewal.

I built an initial JWT implementation for Horde. It worked. Technically. Somewhat. Users could authenticate, receive tokens, make some specific API calls which don’t need backend credentials. But production testing revealed problems.

Subtle problems. The kind that don’t show up in a proof of concept but bite you in production.

Security Edge Case #1: Information Disclosure

JWTs are, by themselves, not encrypted. They’re base64-encoded and signed, but the payload is readable by anyone.

This is documented. RFC 7519 is clear about it. I knew this. But knowing it intellectually and designing around it properly are not the same things.

My initial implementation put too much data in JWT claims:

{
  "sub": "aliced@spam.com",
  "apps": ["imp", "turba", "kronolith", "nag"],
  "lang": "de_AT",
  "timezone": "Europe/Vienna",
  "prefs": {
    "theme": "silver",
    "date_format": "Y-m-d"
  }
}

Why did I do this? Performance optimization. Showing off I didn’t need to hit much of the backend at all. The apps list and preferences are needed on every request. Loading them from the token avoids database queries.

Problem! Problem! This leaks information.

An attacker who intercepts the token (network sniffing, XSS, stolen logs) can see:

  • Which apps the user has access to (privilege enumeration)
  • User’s language and timezone (geographic tracking)
  • User preferences (fingerprinting)

This is information disclosure. Even if the attacker can’t forge tokens (signature verification prevents that), they learn things they shouldn’t.

The fix is minimal claims. Only put identity in the token:

{
  "sub": "aliced@spam.com",
  "iat": 1709856000,
  "exp": 1709859600,
  "jti": "a1b2c3d4"
}

That’s it. No apps list, no preferences, no metadata. Everything else comes from normal controller logic: $registry->listApps(), $prefs->getValue(), etc.

Does this hurt performance? Slightly. Much less than a full session. Those calls hit the database unless cached. But security beats optimization.

Security Edge Case #2: The Credentials Problem

Back to the fundamental tension with JWT in Horde: we need the user’s actual password for some backends.

Not for authentication. The token proves identity and recent credential check. But for many types of IMAP/SMTP connections, for some types of LDAP integration and some other old-enterprise technologies. When a user reads email on their primary account, Horde connects to their company’s IMAP server using their credentials just like Outlook and Thunderbird do.

When they send email Horde connects to their SMTP server with their credentials.

Those credentials must be stored somewhere accessible to the PHP process.

You absolutely should not put passwords in into the JWTs. Even if you encrypted the token (you can, using JWE), the token gets transmitted to the browser, logged, cached. The attack surface is enormous. Users don’t like to rotate their single sign on passwords more often than their organization demands and a memorable but strong password is a conundrum of itself.

The credentials must stay server-side. But storing unhashed password strings into a permanent database is … not best practice even if done right. Which means we still need ephemeral sessions best served from a self wiping store such as an in-memory filesystem or an in-memory database table. Shutdown the system for regular reboot and all is gone. Less ambitions installations just put the session in a regular filesystem or database table and run cleanup processes for anything PHP hasn’t cleaned up itself for whatever reasons. Avoid password leaking.

This realization was a bit frustrating. I thought JWT would let us eliminate sessions and now they won’t Instead, we need JWT and sessions.

The Hybrid Model:

  1. User authenticates with username/password (or some mechanism which transparently provides them)
  2. Credentials stored in PHP session (server-side only)
  3. JWT access token issued (short-lived, contains only identity)
  4. JWT refresh token issued (long-lived, bound to session)
  5. Browser stores both tokens
  6. API calls use access token (stateless, no session lookup)
  7. IMAP/SMTP operations load session for credentials

Not everything needs credentials. Most HTTP requests just need identity: “Is this user Alice?” Reading ticket lists, viewing calendars, browsing files—these operations check permissions but don’t need the user’s IMAP password.

For those operations, JWT provides stateless authentication. For operations that need credentials, we load the session.

Security Edge Case #3: Session File Pollution

PHP sessions are typically stored as files: /var/lib/php/sessions/sess_a1b2c3d4...

Our JWT refresh tokens have a 30-day lifetime. When they expire, the session should be destroyed. But my initial implementation had a bug:

// Refresh token expired
if ($refreshToken->isExpired()) {
    // Oops - session data cleared but file remains
    $_SESSION = [];
    return false;
}

This cleared session data but didn’t call session_destroy(). The session file remained on disk, empty but present. Over 30 days, thousands of empty session files accumulated.

File systems don’t love thousands of small files in a single directory. Even with tweaked settings using multiple sub directories to divide and conquer that indexing problem.Performance degrades. Disk space gets wasted. Cleanup scripts struggle. Databases also degrade performance rapidly once you are beyond in-memory reads.

The fix:

if ($refreshToken->isExpired()) {
    session_destroy();  // Actually delete the file
    return false;
}

Seems obvious in retrospect. But this is the kind of bug that only shows up in production after weeks of accumulated sessions.

Security Edge Case #4: Token Refresh Race Conditions

Users often have multiple tabs open. When the access token expires (1 hour), all tabs need to refresh it.

What happens when two tabs try to refresh simultaneously?

Initial implementation (broken):

function refreshAccessToken($refreshToken) {
    // Tab 1 and Tab 2 both enter here simultaneously
    if (!$refreshToken->isValid()) {
        return null;
    }
    // Generate new access token
    $newAccessToken = generateAccessToken($refreshToken->getUserId());
    // Tab 1 and Tab 2 both succeed, returning different tokens
    return $newAccessToken;
}

Both tabs get new access tokens. But now you have two valid tokens in circulation, potentially with different expiry times. Session state becomes confusing.

Worse scenario: if refresh token validation involves updating session state (tracking last refresh time, etc.), simultaneous updates can corrupt session data.

The fix: locking and validation:

function refreshAccessToken($refreshToken) {
    // Acquire session lock
    session_start();
    // Validate refresh token against session
    $sessionJti = $_SESSION['jwt_refresh_jti'] ?? null;
    if ($refreshToken->jti !== $sessionJti) {
        // Token doesn't match session, reject
        return null;
    }
    if ($refreshToken->isExpired()) {
        return null;
    }
    // Generate new access token
    $newAccessToken = generateAccessToken($refreshToken->getUserId());
    // Release lock
    session_write_close();
    return $newAccessToken;
}

The jti (JWT ID) claim is a unique identifier for each token. We store the current refresh token’s jti in the session. When refreshing, we verify the token’s jti matches the session. This prevents:

  • Using old/stolen refresh tokens
  • Race conditions from simultaneous refreshes
  • Token confusion from multiple clients

Only the token that matches the session’s jti can refresh. Other attempts are rejected.

Security Edge Case #5: Refresh Token Validation

The initial implementation had insufficient validation:

// BAD: Only checks signature and basic claims
if ($jwt->verify($secret)) {
    return $jwt;
}

This caught forged tokens (signature verification) but missed:

  • Expired refresh tokens: A refresh token with exp in the past should be rejected
  • Wrong token type: Using an access token as a refresh token (or vice versa)
  • Unbound tokens: Refresh tokens not associated with a session or associated with a closed session.
  • Revoked tokens: Tokens that should no longer be valid

Comprehensive validation:

function validateRefreshToken($token, $secret) {
    // Verify signature
    if (!$token->verify($secret)) {
        return null;
    }
    // Check expiration
    if ($token->exp < time()) {
        return null;
    }
    // Verify it's a refresh token
    if (($token->type ?? null) !== 'refresh') {
        return null;
    }
    // Verify jti matches session
    session_start();
    $sessionJti = $_SESSION['jwt_refresh_jti'] ?? null;
    if ($token->jti !== $sessionJti) {
        return null;
    }
    // Check session is still valid
    $sessionExp = $_SESSION['jwt_session_exp'] ?? 0;
    if ($sessionExp < time()) {
        session_destroy();
        return null;
    }
    return $token;
}

Each validation step catches a different attack or misconfiguration:

  • Signature verification: prevents forged tokens
  • Expiration check: prevents replay attacks with old tokens
  • Type check: prevents token confusion attacks
  • JTI check: prevents stolen token reuse
  • Session validation: ensures session hasn’t expired independently

This is defense in depth. If one check fails to catch an attack, others provide backup.

The Hybrid Architecture

After working through these edge cases, the architecture stabilized:

Access Tokens (stateless):

  • Lifetime: 1 hour initial default (configurable, 5 minutes to 2 hours). We are still figuring which value is best but I guess shorter is the general trend.
  • Contains: user identity, token ID, type, expiry
  • Used for: API authentication, stateless operations
  • Validation: signature + expiration check (no database)
  • Storage: client-side only (memory, localStorage, sessionStorage)

Refresh Tokens (session-bound):

  • Lifetime: 30 days (configurable, up to session max lifetime)
  • Contains: user identity, token ID, type, expiry
  • Used for: obtaining new access tokens
  • Validation: signature + expiration + jti check + session check
  • Storage: client-side (httpOnly cookie recommended) + session-side (jti)

Sessions (credential storage):

  • Lifetime: Traditional session max lifetime
  • Contains: credentials for IMAP/SMTP, refresh token jti, session metadata
  • Used for: operations requiring credentials
  • Validation: PHP session handling
  • Storage: server-side (files or database)

The flow:

1. Login (username + password)
   ↓
2. Create session, store credentials
   ↓
3. Generate refresh token (jti stored in session)
   ↓
4. Generate access token
   ↓
5. Return both tokens to client

// Later: API call
6. Client sends access token
   ↓
7. Server validates token (stateless, no DB)
   ↓
8. Request proceeds

// Later: Access token expires
9. Client sends refresh token
   ↓
10. Server validates refresh token (checks session)
    ↓
11. Generate new access token
    ↓
12. Return new access token

// Later: IMAP operation
13. Controller loads session (needs credentials)
    ↓
14. Connect to IMAP with stored credentials
    ↓
15. Fetch email

Most operations (reading tickets, viewing calendars, browsing files) could use access tokens. No session lookup, no database I/O for authentication.

Credential operations (email, calendar sync with auth, etc.) load sessions. Required for security. User-provided credentials must stay server-side and should best be forgotten once a session ends.

Configuration

JWT is optional and disabled by default. Enabling it:

// conf.php
$conf['auth']['jwt']['enabled'] = true;
$conf['auth']['jwt']['secret_file'] = '';  // Default: ${HORDE_CONFIG_BASE}/horde/jwt.secret
$conf['auth']['jwt']['issuer'] = 'mail.example.com';
$conf['auth']['jwt']['access_ttl'] = 3600;      // 1 hour
$conf['auth']['jwt']['refresh_ttl'] = 2592000;  // 30 days

The secret file is critical. Generate strong random data:

openssl rand -base64 32 > /var/horde/config/horde/jwt.secret
chmod 600 /var/horde/config/horde/jwt.secret
chown www-data:www-data /var/horde/config/horde/jwt.secret

Important: Token lifetimes must be ≤ session max lifetime. If your PHP session max lifetime is 1440 seconds (24 minutes), you can’t set refresh token TTL to 30 days. The session will expire first, breaking refresh token validation.

The test.php diagnostic screen has been amended to assist with this. It validates JWT configuration:

  • Secret file exists and is readable
  • Token lifetimes are reasonable
  • Issuer is configured
  • Permissions are correct

If the configuration is wrong, test.php shows red warnings with specific issues and suggestions.

API Authentication Flow

With JWT enabled, future API clients can authenticate without session cookies:

# Login
curl -X POST https://mail.example.com/api/v1/auth/login \
  -H "Content-Type: application/json" \
  -d '{
    "username": "aliced",
    "password": "secret123"
  }'
# Response
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
  "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Store both tokens. Use access token for API calls:

# Fetch contacts
curl https://mail.example.com/api/v1/contacts \
  -H "Authorization: Bearer eyJ0eXAiOiJKV1Qi..."
# Response
{
  "contacts": [
    {"id": 1, "name": "Bob Smith", "email": "bob@example.com"},
    {"id": 2, "name": "Carol Jones", "email": "carol@example.com"}
  ]
}

After 1 hour, access token expires. Use refresh token to get a new one:

# Refresh
curl -X POST https://mail.example.com/api/v1/auth/refresh \
  -H "Content-Type: application/json" \
  -d '{
    "refresh_token": "eyJ0eXAiOiJKV1QiLCJhbGc..."
  }'
# Response
{
  "access_token": "eyJ0eXAiOiJKV1QiLCJhbGc...",
  "token_type": "Bearer",
  "expires_in": 3600
}

After 30 days, refresh token expires. Full re-authentication required.

What This Enables

JWT authentication is infrastructure. By itself it doesn’t change user experience. Users logging in via web browser still get sessions as before.

But JWT opens doors:

Native Mobile Apps

Mobile apps can authenticate via API, store tokens, make authenticated requests. No session cookies, no web views, no embedded browsers. Native Swift/Kotlin apps will feel right at home.

This wasn’t possible before. Mobile apps had to embed web views and manage session cookies. Clunky and slow. A real turn off and guess what – I always wanted to do it but never finished my experiments.

Third-Party Integrations

External services will be able to authenticate as Horde users via API. Calendar sync tools, contact managers, email clients all can integrate via JWT tokens.

Previously, third-party integrations required storing user passwords or complex OAuth proxies. JWT tokens are revocable and scoped. It’s tempting to deliver permissions in scopes but I guess that’s just the data exposure problem in new clothes. If exposing permissions is needed (rather than checking permissions) I guess we better authenticate an API call rather than carry it around cookie-style.

OpenID Connect (Maybe, Finally)

OpenID Connect (OIDC) is a popular authentication scheme on top of OAuth 2.0. It’s how “Sign in with Google” works. With JWT infrastructure we can build OIDC integration for sites that want it:

  1. User clicks “Sign in with Google”
  2. Redirect to Google’s OIDC endpoint
  3. User authenticates with Google
  4. Google returns OIDC token
  5. Horde validates token, creates local session
  6. Issue Horde JWT tokens
  7. User is logged in

This also enables enterprise SSO scenarios: “Sign in with Azure AD”, “Sign in with Okta”, etc. Sign in with your mastodon account to some horde-backed services, why not?

The infrastructure (JWT handling, token validation, session binding) is ready. Building the OIDC integration is still a huge project. But now it’s straightforward.

OAuth 2.0 Providers (Will we?)

Horde could become an OAuth 2.0 provider. Other applications could delegate authentication to Horde: “Sign in with your Horde-Powered foo.org account.”

Use case: organization has Horde as central mail system. Internal wiki, ticket system, file server all delegate authentication to Horde. Single identity source, centralized access control. Also, inter-organization “welcome guest” or “granted trust” scenarios can avoid onboarding accounts. OAuth 2.0 is a more general approach to the OpenID Connect scenario which uses a specific case of OAuth 2.0 under the hood. Horde has had support for the original OAuth but back then it was not a major technology. Original OAuth and OAuth 2.0 are almost unrelated and ironically, OpenID and OpenID Connect aren’t really the same thing either.

SAML (If you can convince me)

SAML is another enterprise SSO standard. Like OIDC but older, more complex, more common in large organizations. OIDC is a lean and webby json solution, SAML is XML Enterprise Java era gore.

SAML integration requires token infrastructure. JWT provides a critical part but you have to deal with namespaces, complex xml messages, server-to-server out of band communication and it’s all heavyweight. Building SAML support now has a clear foundation but I am not motivated at all. Maybe somebody will sponsor it or has the compelling use case which thrills me and makes me long for SAML support without an external OIDC->SAML bridge. If such a thing exists. It’s like Skittles is for pubs and Bowling is that completely different activity but both roll a ball towards pins.

Performance Impact

Theory: stateless JWT authentication should dramatically reduce database I/O.

Practice: Modest improvement and little out of the box gains in typical Horde deployments. We will have to work to earn more benefits.

Why? Most Horde requests aren’t authentication-bound. They’re data-bound—loading emails, fetching calendar events, querying contacts. Authentication is 1-2% of total request time.

For API-heavy workloads (mobile apps polling for updates, sync services, webhooks), JWT provides measurable improvement. No session file reads, no database authentication queries.

For traditional web browser workflows, improvement is minimal. Sessions are already cached in memory (via opcache, redis or memcached), so session reads are fast.

The story isn’t about dramatic performance speedups for most installations. It’s about enabling scenarios that weren’t possible before (native mobile apps, third-party APIs) and providing foundation for future authentication features (OIDC, OAuth, SAML).

Backward Compatibility

JWT is additive. With JWT disabled (default), authentication works exactly as before.

With JWT enabled, both paths work:

  • API clients can use JWT tokens
  • Web browsers can use sessions
  • Old code using sessions continues working unchanged

No rewriting required. Controllers that check $registry->getAuth() work identically—the authentication layer abstracts whether auth came from session or JWT.

This was critical for acceptance. We couldn’t require rewriting thousands of lines of authentication code. JWT had to slot in without breaking existing functionality.

What I’d Do Differently

If I were starting over:

Start with minimal claims: I should have designed with minimal claims from day one instead of optimizing prematurely with apps list and preferences in tokens. Security beats performance, especially when the performance gain is not there out of the box.

Test session lifecycle earlier: The session file pollution bug took weeks to notice. I should have tested 30-day token lifecycles in development with accelerated time.

Document race conditions: I should have documented the refresh token race condition scenario explicitly in planning documents. I discovered it through alpha tester production logs. Should have anticipated it.

Lessons Learned

JWTs Are Not Magic

JWT authentication doesn’t eliminate sessions or magically make everything stateless. It provides selective statelessness. Operations that don’t need credentials can skip session lookups. Designing for this or reimplementing functionality is work and best integrated with other changes which touch particular sub systems.

The hybrid model is more complex than pure sessions or pure JWTs. But it’s the right architecture for Horde’s use case.

Security Edge Cases Are Subtle

Information disclosure (readable JWTs), session cleanup (filesystem pollution), race conditions (simultaneous refreshes) are all subtle. None of these are obvious without careful thought or production testing.

Reading JWT specs (RFC 7519) helps but doesn’t catch implementation-specific issues like session binding.

Validation Is Defense in Depth

Each validation check (signature, expiration, type, jti, session) catches different attacks. Comprehensive validation is tedious but necessary. In theory JWT backed services are less prone to CSRF. Also it’s nice NOT to automatically attach your credential to every call even for static or public assets.

Production Testing Finds Problems

Development testing caught signature verification issues. Production testing caught race conditions, session cleanup bugs, edge cases with token lifetimes.

No substitute for real traffic hitting the system.

Looking Forward

JWT authentication is shipping in Horde 6. The core infrastructure is stable. Edge cases resolved, production-tested, security-reviewed. What’s next?

Next steps:

Permission scopes: Current tokens grant full user access. While scoping tokens by default is not useful, issuing restricted tokens for a sub set of resources and permissions is useful. However we will need to re-validate if an opaque token and server side permission management isn’t more practical.

Token rotation: Automatic refresh token rotation on a regular schedule. Reduces window for stolen token attacks.

These enhancements build on the JWT foundation without changing the core architecture. That’s the value of getting infrastructure right. Future features become incremental additions rather than rewrites.


Coming up in Part 3: Vanilla web development and the economic sustainability argument for framework-free architecture in volunteer open source projects.

This is Part 2 of a 4-part series on Horde 6’s architectural evolution. Part 1 covered jQuery Mobile deprecation, Part 3 covers vanilla web sustainability, and Part 4 covers developer infrastructure improvements.

bookmark_borderThe jQuery Mobile Problem: Why Horde 6 Had to Move On

Part 1 of 4: Architectural Evolution in Horde 6

Turns out you can’t ship a stable release while depending on a framework that was deprecated five years ago. I sort of knew this deep down in my head. Yes, yes, unmaintained dependencies are a problem. But the full weight of it didn’t hit me until I started auditing Horde’s mobile interface code in preparation for Horde 6 stable. Our dear little project was dragging along not one dead javascript stack but two or three, depending on how you count. How to get out of this? What’s the deal? This is going to be part one of a four part series on how we are getting rid of jQuery Mobile and jQuery and how it is going to benefit desktop users mostly.

jQuery Mobile 1.3.2 was released in 2013. The jQuery Mobile project was officially deprecated in 2021. It receives no security updates, no bug fixes, no compatibility patches for modern browsers. Yet Horde’s entire mobile interface depends on it.

That’s a problem.

In today’s post I am going to tell you a story of how addressing that problem led me to rethink not just mobile UI but the entire approach to frontend development in Horde 6. It’s about technical debt, risk management and making architectural decisions that a volunteer project can actually sustain long-term.

The jQuery Mobile Era

Let me take you back to 2012-2013. The mobile web was exploding. Responsive design was new. Ethan Marcotte’s book came out in 2011. CSS3 was still bleeding edge and new features were adopted with some wariness. Flexbox existed but had terrible browser support. CSS Grid didn’t exist at all.

If you wanted a mobile-friendly web application, you had two realistic choices: build separate mobile templates from scratch, or use a framework. jQuery Mobile maybe wasn’t the leading framework for mobile web applications at the time but it had integration and brand recognition with its much more popular sister projects, jQuery and jQuery UI. It promised “write once, run anywhere” mobile interfaces in an era of browsers all doing their own thing in the most incompatible ways. With automatic styling, touch events, page transitions and an ecosystem of plugins it seemed like the right choice to get a mobile experience into Horde fast. Building a mobile interface from scratch would have taken years. jQuery Mobile gave us a prototype of a working mobile interface relatively quickly. Horde was a side project to some of the developers back then but a business to others. Adding mobile views promised to attract sponsoring from major sites.

And it worked. For a while.

When Frameworks Get Deprecated

The jQuery Mobile project effectively stopped development after 1.4.5 in 2014. A 1.5.0 release came in 2016 but was mostly maintenance. The project was officially deprecated in October 2021. Developers please migrate off the write once run anywhere framework. Horde didn’t. Mobile did not get the traction yet needed to compete with Horde’s other major concerns. Mobile was far from feature parity to desktop app, making it of limited value to end users. Limited value to end users meant little priority and the jQuery framework went out of the lime light but was still actively maintained and not going away quite yet.

But that’s only part of it. Here’s the thing about framework deprecation: it doesn’t just mean “no new features.” It means:

  • No security patches: Browser vendors change behavior and industry sensitivities develop. When security vulnerabilities get discovered who is going to fix them?
  • No compatibility updates: Modern browsers break old assumptions but the framework doesn’t adapt. Many reasons to choose a cross browser framework vanished over time.
  • No community support: Stack Overflow answers dry up. Documentation sites disappear or reference outdated examples.
  • Talent drain: Developers who knew the framework move on. Few really understand the code base.

Only a few tips of this iceberg were visible back then but we could have guessed maybe.

Audit Time!

I started with a simple question: how much jQuery Mobile code does Horde actually use?

This required reading through our mobile templates, tracking which jQuery Mobile widgets we depend on. I counted JavaScript method calls and CSS usage. The process took several days of systematic searching through the codebase even though I had some throwaway scripts to make it tenable. I’ve got a job to do after all and Horde isn’t an official part of it anymore.

The results were underwhelming.

Widgets in use: 10 out of 30+ available widgets

  • Listview (for navigation menus)
  • Button (styled buttons)
  • Toolbar (header/footer bars)
  • Navbar (tab navigation – and barely put to real use!)
  • Collapsible (expandable sections)
  • Popup (modal dialogs)
  • Panel (side menus)
  • Loader (loading spinners were popular back then)
  • Textinput (form fields)
  • Selectmenu (dropdowns)

JavaScript methods: 3 out of dozens available

  • .page() for page initialization
  • .popup() for modal dialogs
  • .listview('refresh') for updating lists

CSS classes: Heavy usage, but mostly for basic styling

  • ui-btn for button appearance
  • ui-bar for toolbars
  • ui-content for content padding
  • ui-listview for list styling
  • data-role attributes driving most behavior

To sum it up: About 90% of what I initially thought was “jQuery Mobile functionality” was just CSS styling. The framework was applying classes based on data-role attributes, but the actual behavior was minimal.

We were using approximately 5% of jQuery Mobile’s capabilities.

But for that 5% we were shipping:

  • jQuery: 96 KB
  • jQuery Mobile JavaScript: 125 KB
  • jQuery Mobile CSS: 115 KB
  • Icon images: ~5 MB (743 PNGs per theme)

Total payload: ~488 KB of code + 5 MB of images on first page load.

For 5% utilization. Now that’s not really mobile friendly. When you’re on a hiking trip and just want to check some item for peace of mind, this is a major frustration.

A Framework Lock-In Problem

Here’s where it gets uncomfortable. jQuery Mobile is deprecated. What do we migrate to?

I have been looking at the landscape of JS UI frameworks time and again since the 2020s:

React: Popular, well-maintained, massive ecosystem. But requires a complete architectural shift. Horde’s server-rendered PHP templates would need to become API endpoints. The entire application structure changes. I did some React-first inhouse projects in my former company. While I liked the design, major efforts to integrate it with Horde based custom apps written before turned out thorny business. It is really meant to be used as part of a Typescript or other compile-to-javascript pipeline with dependency management and many other aspects of the modern JS ecosystem which I don’t find appealing.

Vue: Similar story. Very popular framework. Quite some BC breaks between versions. But incompatible with where we are coming from.

Framework7: Specifically designed for mobile hybrid apps. Better fit, but smaller community. Is this going to be available five years from now?

Ionic: Also designed for hybrid apps. Heavy focus on Angular/React/Vue integration. Same architectural questions.

Bootstrap: General-purpose, but mobile-first. Could work, but still a large dependency for features we don’t need.

Some years passed and developers were busy keeping Horde’s core functionality compatible with a changing ecosystem. I knew it and thought about it with some dread: This problem won’t go away, it will only fester. It was not a main concern as the core Horde tools had good sync-to-mobile integrations: CalDAV, CardDAV, ActiveSync. I did some proof of concept for Thunderbird plugin integration but just as I started to going they broke/modernized their plugin ecosystem. Mozilla also was a bit discouraging with their policy towards Thunderbird. Sigh.

I was looking forward to having this exact conversation again five years down the line. “Framework X was great in 2026, but now it’s 2031 and everyone’s moved on to Framework Y. We need to rewrite.”

For a volunteer open source project with limited resources, this cycle is unsustainable.

Every framework migration means:

  • Months of development time rewriting working code
  • Risk of regressions and bugs
  • Documentation updates
  • Trying to explain to fellow developers and incidental contributors why any of this is necessary right now
  • Testing across all applications

We can’t keep doing this every five years.

Vanilla Web Revelations

Modern CSS is shockingly capable. It took me a while to understand that. I’m not very good at CSS but my wife is a trained media designer and I also consulted some friends on my worries.

I started experimenting with what you could build using only modern web standards. No frameworks, no libraries, just CSS3 and modern JavaScript. I wasn’t tabooing third party libraries and utilities but I wanted to see how far it would get me.

CSS Grid (widely supported since 2017): Handles complex layouts that used to require framework grid systems.

CSS Flexbox (widely supported since 2015): Handles most layout needs—centering, alignment, distribution.

CSS Variables (widely supported since 2016): Runtime theming without build tools or preprocessors. One less step crying for an ever-changing tool chain just to test your latest three line change.

CSS Media Queries (ancient, universally supported): Responsive breakpoints.

CSS @layer (supported since 2022): Manage specificity and style precedence.

Native HTML5 elements<details> replaces collapsible widgets, <dialog> replaces modal popups, <progress> replaces custom loaders.

Here’s what shocked me: There’s nothing left of what jQuery Mobile and in general jQuery did for Horde’s limited needs. It’s all possible without a fat glue layer evening the field across browsers. Another way the world has changed: Latest versions of browsers are available almost universally. The share of users who use 12 to 36 month old versions of major browsers is almost zero. While fringe and mobile browsers still don’t support all features of chromium-based and mozilla-based desktop browsers in their latest versions, old browser versions are almost a non-issue.

Example: Theme Switching

jQuery Mobile themes required:

  • 5-10 MB of PNG icons per theme
  • Separate CSS files per theme (100+ KB each)
  • JavaScript theme switching that reloaded pages
  • Build process to generate theme variations

Vanilla CSS with CSS Variables:

/* themes/blue/tokens.css - just 2 KB */
:root {
  --color-primary: #0066cc;
  --color-primary-dark: #004c99;
  --color-bg: #ffffff;
  --color-text: #333333;
  --color-border: #dddddd;
  --space-sm: 0.5rem;
  --space-md: 1rem;
  --space-lg: 1.5rem;
  --border-radius: 4px;
  --shadow: 0 2px 4px rgba(0,0,0,0.1);
}

/* Automatic dark mode based on system preference */
@media (prefers-color-scheme: dark) {
  :root {
    --color-primary: #4488ff;
    --color-primary-dark: #6699ff;
    --color-bg: #1a1a1a;
    --color-text: #e0e0e0;
    --color-border: #333333;
    --shadow: 0 2px 4px rgba(0,0,0,0.3);
  }
}

/* Components use variables */
.button {
  background: var(--color-primary);
  color: var(--color-bg);
  padding: var(--space-md);
  border-radius: var(--border-radius);
  box-shadow: var(--shadow);
}

.button:hover {
  background: var(--color-primary-dark);
}

That’s it. No build tools. No webpack. No SASS compilation. Just load a different token file and the entire interface updates instantly.

Dark mode support could even respect the user’s system preference. Want to override it? Load a different theme file. Theme files are 2-5 KB each. I am not quite there yet. I have implemented dark mode as a separate theme and it’s a palette swapped knock-off default theme. Did I just tell you I’m not good at CSS and appreciate any help?

Example: Responsive Layouts

jQuery Mobile grid system:

<div class="ui-grid-a">
  <div class="ui-block-a">Column 1</div>
  <div class="ui-block-b">Column 2</div>
</div>

Required framework CSS to interpret ui-grid-a and ui-block-* classes.

CSS Grid (no framework):

.app-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
  gap: var(--space-md);
}
<div class="app-grid">
  <div>Column 1</div>
  <div>Column 2</div>
</div>

The layout is responsive automatically. Columns wrap to rows on narrow screens without media queries. The browser handles it. The opposite is not quite true, not yet. Our trivial mobile-first PoC doesn’t leverage additional screen estate in a good way. But we have all the tools to build that into it.

Example: Native HTML5 Widgets

jQuery Mobile collapsible:

<div data-role="collapsible">
  <h3>Advanced Options</h3>
  <div>Content here</div>
</div>

Required jQuery Mobile JavaScript (~125 KB) to handle click events, animations, state management.

Native HTML5 <details>:

<details>
  <summary>Advanced Options</summary>
  <div>Content here</div>
</details>

Zero JavaScript. Fully accessible. Works everywhere I tested. Browser handles keyboard navigation, ARIA attributes, animations.

Performance Math

Not to be confused with performative math. Let’s do the actual math on what users download.

jQuery Mobile stack (what we had):

  • jQuery: 96 KB
  • jQuery Mobile JS: 125 KB
  • jQuery Mobile CSS: 115 KB
  • Mobile app JavaScript (8 apps): 90 KB
  • jQuery Mobile theme: 115 KB
  • Icons (PNG, 743 files): ~5 MB

Total first load: ~488 KB JavaScript/CSS + 5 MB images

Subsequent page loads still hit ~350 KB because JavaScript/CSS isn’t fully cached across page navigations in jQuery Mobile’s architecture.

Vanilla responsive stack (what we built):

  • Core responsive JavaScript: 25 KB
  • Responsive CSS: 15 KB
  • Theme tokens: 2-5 KB
  • Icons (SVG sprite): 100 KB
  • Mobile app JavaScript – evolving but I am pretty sure to reach feature parity with previous mobile screens with less than 100 KB summed across apps.

Total first load: ~70 KB JavaScript/CSS + 100 KB images

That’s 85% smaller for code and 98% smaller for images.

For users on slow connections that’s a blessing and it doesn’t hurt others either. Mobile users in rural areas. Users with expensive or limited data plans. Users in developing countries or Germany.

The Decision: Mobile-First Migration Strategy

So vanilla CSS could replace jQuery Mobile. The question became: how do we migrate without destabilizing the entire platform?

Horde has two frontend codebases:

  1. Desktop interface: PrototypeJS-based, stable, works, users know it
  2. Mobile interface: jQuery Mobile-based, incomplete, deprecated dependencies

The obvious instinct: migrate the desktop interface first because it has more users and more functionality.

I made the opposite choice: Start with mobile.

Here’s why:

Risk Management

Desktop is stable. Users depend on it. It works. PrototypeJS is old (2005-2015 era) but it’s not deprecated—the code still runs fine in modern browsers. There’s no urgent security problem forcing our hand. We will however begin to evolve the delivery basis. The way ajax works in these apps. The way pages are delivered through exposed php pages rather than routes. Some ball of mud problems which need to be untangled for performance.

Mobile is incomplete. The mobile interface was always a subset of functionality. Users expect it to be limited. Mobile users probably aren’t thrilled by what we delivered for years and will accept anything more modern willingly.

If we break something in the mobile interface during migration, the blast radius is contained. Users can switch to desktop view as a workaround. If we break the desktop interface, we’ve disrupted the primary workflow for thousands of users with no fallback.

Incremental Progress

Starting with mobile means we can ship improvements incrementally:

  1. Phase 0: Login (✅ completed) – Responsive login page, all users
  2. Phase 1: Portal (✅ completed) – App launcher, mobile portal replacement
  3. Phase 2: Guest pages (in progress) – Public calendars, wikis, ticket forms
  4. Phase 3: Apps (planned) – Nag, Ansel, Turba, Gollem, etc.

Each phase delivers value without requiring complete migration. Users benefit immediately. Development can proceed at sustainable pace.

Contrast this with “desktop first” which would require:

  • Replacing PrototypeJS across all apps simultaneously (too risky)
  • Building feature parity before shipping anything (too slow)
  • Higher coordination overhead (more developers, more testing)

Validation Before Commitment

By starting with mobile, we validate the vanilla CSS approach on a smaller surface area before committing to desktop migration.

If the approach has problems, we can learn from it. Adopting a framework or discrete helper libraries into a vanilla code base is always possible. Performance issues? Browser incompatibilities? Maintenance difficulties? Learn to deal with them when they pop up.

If the approach works well then we have confidence to expand it to desktop in Horde 7.

Parallel Development

Mobile-first doesn’t block other work. Of course it costs time. Desktop PrototypeJS interface continues working unchanged. Team can work on responsive mobile views and desktop features in parallel without conflicts.

This matters for volunteer open source—contributors. We want to attract contributions from those who are not married to Horde but have an itch to scratch, a feature to improve, a real world use case to share. Our core team works on different features at different times. Parallel tracks let development continue without waiting for migration completion. The need to be complete is a real release killer.

What We Built So Far

The responsive login and mobile portal shipped with Horde 6.

Responsive Login

Single login page works on all devices:

  • Desktop: full-width layout, traditional appearance
  • Tablet: centered layout, touch-friendly controls
  • Phone: full-screen, large touch targets

Dark mode support: Respects prefers-color-scheme to some extent automatically. Users on macOS/iOS/Android/Windows with system dark mode enabled get dark login automatically. It doesn’t work all that well on the rest of the screens yet.

No framework dependencies: Pure HTML5, CSS3, vanilla JavaScript.

Backwards compatible: All authentication backends work unchanged (LDAP, SQL, IMAP, custom). We also ported the logic to add fields for 2FA if the respective API is present.

Size: ~5 KB total (HTML, CSS, minimal JS for form validation).

Mobile Portal

App launcher for mobile/tablet browsers:

Grid layout: Responsive grid using CSS Grid, wraps automatically based on screen width.
App icons: SVG sprite sheet, single 100 KB file contains all icons for all apps, theme-independent.
Touch-friendly: Large tap targets (48×48 pixels minimum), proper spacing, no accidental clicks.
Fast: Initial load ~70 KB, subsequent navigations use cached resources.
Theme support: Dark and red themes included, more coming, 2-5 KB per theme.

What Changed for Users

Mobile/tablet users: Immediately see the new responsive login. After login, they get the new mobile portal (detected by user agent). Significantly faster load times, modern appearance, better touch experience.
Desktop users: See the responsive login but after login, they get the same PrototypeJS portal they’ve always had. No disruption to workflow.
Administrators: No configuration changes required. The responsive views are automatic based on browser user agent. No feature flags to toggle.

Framework Fatigue Problem

Here’s the philosophical point that matters most for Horde’s long-term sustainability.

Horde is a volunteer open source project. We don’t have dedicated frontend teams with designers, React specialists, build tool experts, accessibility consultants. We have PHP developers who contribute in their spare time.

Every framework we adopt has a learning curve:

  • How does the framework structure components?
  • What’s the data flow model?
  • How do we integrate with server-rendered PHP?
  • What’s the build toolchain?
  • How do we debug when things break?

And every framework eventually becomes legacy:

  • jQuery was cutting-edge in 2006, legacy by 2020
  • jQuery Mobile was cutting-edge in 2012, deprecated by 2021
  • Angular 1 was cutting-edge in 2010, replaced by Angular 2+ (complete rewrite)
  • React is dominant in 2024, but who knows about 2030?

We can’t do that with the capacity we have these days.

Web standards don’t deprecate so fast. CSS Grid from 2017 still works perfectly in 2026. It will work in 2030. The browser vendors maintain backwards compatibility because billions of websites depend on these standards.

By building on web standards instead of frameworks, we’re making a bet on stability. The code we write today will still work in five years without a migration treadmill.

This is the economic argument for vanilla web development in volunteer projects: maintenance burden matters more than developer convenience.

Lessons Learned

Technical Debt Compounds

jQuery Mobile was deprecated in 2021. I didn’t start seriously addressing it until 2024-2025. That three-year delay made the problem worse:

  • Browser vendors changed behavior
  • Security concerns grew
  • Alternative frameworks came and went
  • Technical debt piled up elsewhere

The lesson: address deprecated dependencies quickly, not eventually.

Audits Reveal Surprises

I thought Horde used jQuery Mobile extensively. The audit showed 5% utilization. Without the audit I might have migrated to another heavy framework instead of realizing vanilla CSS could handle it. In fact a few years back I tried exactly that.

The lesson: Measure before deciding. Assumptions about dependency usage are often wrong.

Risk Management Over Speed

Starting with mobile instead of desktop felt slower. I was not super sure this was the right way at first. The mobile interface has fewer users, less functionality. But the risk reduction was worth it. I validated the approach without betting the farm.

The lesson: In architectural changes, risk management is more valuable than speed.

Frameworks Are Liabilities

Every framework dependency is a future maintenance burden. Not “might be”. Will be. The question is just when.

For volunteer projects especially, minimizing framework dependencies reduces long-term maintenance burden at the cost of upfront development time. That tradeoff is usually worth it.

The lesson: Choose boring technology. Web standards are boring. They’re also stable. There’s lessons in this for Horde’s PHP parts and the way it embraces PHP-FIG standards.

What’s Next (maybe)

The responsive login and mobile portal are shipped. jQuery Mobile is not yet removed from core Horde. The apps still load it. That is a break in style and technology which I want to eliminate one app at a time.

Next phases:

Guest page templates: Public calendars (Kronolith), wiki pages (Wicked), ticket forms (Whups) will get responsive templates. These are high-value because they’re often the first thing non-users see. A modern responsive public calendar is good marketing.

App migration: Starting with simpler apps (Nag for tasks, Ansel for photos) and moving to more complex apps (Turba for contacts, Kronolith for calendars, IMP for email). Each app gets responsive templates incrementally.

Desktop migration: Eventually. Horde 7 timeframe. By then we’ll have validated the responsive approach thoroughly on mobile and can migrate desktop with confidence.

This will take years. That’s fine. Incremental progress beats stalled rewrites.

The Bigger Picture

This isn’t just about jQuery Mobile. It’s about making architectural decisions that a volunteer project can sustain long-term.

Horde has been around since 2000. We’ve survived shifting trends—Ajax, Web 2.0, mobile revolution, SPA frameworks, Node.js everything. We’ve survived by being conservative about dependencies and prioritizing stability. Ironically, sometimes we dodged the bullets because we weren’t fast enough.

The jQuery Mobile migration continues that philosophy. Use web standards, avoid framework churn, make incremental progress, don’t break production deployments.

Boring? Yes. Sustainable? Also yes.

For a volunteer project maintaining software that thousands of organizations depend on, sustainability beats excitement every time.


Coming up in Part 2: JWT authentication architecture and the hybrid model that provides the foundation for modern API access and enterprise SSO integration.

This is Part 1 of a 4-part series on Horde 6’s architectural evolution. Part 2 covers JWT authentication, Part 3 covers vanilla web sustainability, and Part 4 covers developer infrastructure improvements.

bookmark_borderHorde’s new Two-Factor API

New Horde 6 feature: The horde/horde base app’s next release supports two factor logins.
Dmitry Petrov is working to release a new One-Time Password module which integrates with this new API.

Seemless integration for One Time Passwords.

Several years ago I did some downstream development for a customer. They wanted to use One Time Passwords (OTP) in their custom horde application as a way to offer Two-Factor Authentication (2FA). It worked well for the specific use case but it required patching the base Horde system or substantial reconfiguration, basically delegating authentication to this app. Unfortunately, this had several downsides.

Recently I was approached by Dmitry Petrov. He has built his own OTP solution for horde and offered to upstream his module. Time was ripe to finally provide an interface for Two Factor Authentication.

When horde detects the secondfactor/isEnabled API, it adds an additional field to the default login screen.

This also works in smartmobile view. The second factor is not required when connecting to JSON-RPC or CalDAV endpoints. It is only checked for UI logins. Support is currently restricted to the bare minimum. OTP authentication can be opt-in or mandatory – The horde base app does not know this. A future version may force the user into an OTP setup screen after login if no OTP is configured yet.

bookmark_borderSunsetting the Maintaina Horde Fork

A few years back I started a downstream fork of Horde to develop features I needed for foss and customer deployments without upstream dependencies. It went successful, was a great learning opportunity and a good exercise in critiquing our old tool chain and approaches. We had some well-known downstream users and contributors but I’d say it has run its course. It’s time to sunset Maintaina in a controlled way that’s fair towards our user base. As we are nearing a beta and prod release of horde 6 proper mostly built from Maintaina, we want to provide a smooth transition.

Horde 6 (upstream) is focusing on supporting PHP 8.4 without spamming warning&notices while Maintaina was originally targeted at PHP 7.4 through 8.1 – Still supporting anything before 8.2 is not a priority with upstream anymore. I will have to discuss with other maintainers of the fork.

Problems to solve:

  • Archive libraries which haven’t been touched for long
  • Coordinate upstreaming libraries with recent changes and archive them
  • Provide a feasible approach to consume only select maintaina packages and mostly upstream packagist
  • Clarify the future of changes downstream users want to keep but which compete with Horde upstream solutions
  • Invite maintainers of downstream code to maintain some upstream libraries to prevent stalling their own needs

I’ll keep you posted.

bookmark_borderPHP: The case for standalone null parameters

PHP 8.0 introduced null and false as members of union types but disallowed them as standalone parameter types. PHP 8.2 changed that and allowed null as standalone parameter types. What is this good for? Should they extend this to “never” one day? Why do I call standalone null parameters poor man’s generics?

What’s a null parameter? What’s a nullable parameter?

A null parameter is a parameter of type null which can only ever have one value null. Its core meaning is “not a value” as opposed to “empty string” or “false” or “zero”.

A union type which has other values but can also contain null is called nullable. For example, boolean is not a nullable type. It has the possible values true and false. If we want to allow a third state, we can create a new nullable type bool|null which has the possible values true, false and null.
In modern php, bool is a union type of true, which can only have the value true, and false, which can only have the value false. So bool|null is equivalent to true|false|null. Union types including null can also be written with a preceding question mark: bool|null is equivalent to ?bool

Isn’t this a bit pointless?

A null parameter by itself is not very interesting. After all, we know its only possible value. It is valuable as a type which can be extended. According to Liskov substitution principle parameters of subtypes should be contravariant to parent types. If the parent type accepts null as a parameter, the child type must accept null as a parameter but may accept anything else. The opposite is true for return types. The child class may have a more specific return type than the parent class but must not return anything the parent would not allow. This is called covariance. In PHP, the top type is called mixed and allows basically everything, even null values. The null type is at the other end of the scale. If the parent returns null, the child must not return anything else. There is one more restricted return type, never. A function of type never must not return at all. It is either an infinite loop or may only exit the whole program. But never is not an allowed parameter type. There is also a return type void which is in between the two. Void may return from functions, but it returns semantically nothing. Not even null.

What is it good for?

Defining an interface parameter as null is allows to define an almost generic interface which might be substantiated in derived classes. Let’s look at an example.

<?php

interface A {

        public function get(null $value): mixed;
}

class B implements A
{
        public function get(null $value): ?object;
        {
                return $value
        }
}

class C extends B
{
        public function get(A|null $value): XmlElement;
        {
            if (is_null($value)
            {
                $value = new SimpleXmlElement("Not Null");
            }

                return $value;
        }
}

class D implements A
{
    public function get(B $value)
    {

    }
}

If you don’t see the point, let me explain. interface A defines that any implementing class must have a method get(null $value) which has only one mandatory parameter. This parameter must accept null as a value. Any additional parameters must be optional. Any derived class may make the first parameter accept more types and even make it optional. The only drawback: Class D cannot be implemented because function get does not accept null as its first parameter $value.

Generics: I wish I had a little more nothing

This is as close to actual generics as one gets with PHP. Generics are code templates which do not have specific parameter and return types until an actual class is created from them. Some other languages do have them but PHP doesn’t. Larry Garfield and others have made the case for them over and over again.

There are some challenges in the engine which make them hard to implement. It would matter less if we had some tbd or changeme type which can only exist in interfaces and can be overridden by any other type. But we don’t. At least we have standalone null.

bookmark_borderStolperfrei: Wir können noch so viel lernen

Ria Weyprecht befasst sich seit über zwei Jahrzehnten mit Web-Gestaltung und Technik – ob nun mit WordPress, anderen Plattformen, Inhouse-Frameworks oder ganz frei. Mindestens seit der Halbzeit spielt auch Barrierefreiheit zunehmend eine Rolle. Mit großem Interesse lese ich seit einiger Zeit auch ihren deutschsprachigen Blog “stolperfrei.digital”. Dort stellt sie immer wieder Techniken vor, mit denen man die Barrierefreiheit von Webseiten verbessern und unnötige Schwierigkeiten vermeiden kann. Ob Zugänglichkeit für Sehbehinderte und Benutzer von Screen Readern, ob Leichte Sprache oder die Vermeidung von Reizüberflutung, immer nah am Stand der aktuellen Forschung und mit Blick auf die Praxis.

Mein beruflicher Schwerpunkt hat sich von Frontend-Belangen immer weiter weg entwickelt. Arbeite ich dann in der Freizeit an Projekten wie Horde Groupware oder JMRI, dann bekommen Profis und Betroffene wahrscheinlich das Grauen. Die technische Basis ist trotz vieler Neuerungen nicht ganz taufrisch und auch das Wissen um den richtigen Einsatz von Gestaltungsmitteln braucht immer mal wieder eine kleine Auffrischung. Es fehlt mir im Frontend auch die handwerkliche Routine und Praxis, die ich tiefer in der Applikation mühelos vorweisen kann. Wie gut, dass im Stolperfrei-Blog immer wieder kleine und große Themen aufgerissen werden. Ich kann direkt überprüfen: Gibt es hier für mich etwas zu tun? Kann ich das Gezeigte anwenden? Oft finde ich auch Hinweise auf weiterführende Themen.

Ich kann nur empfehlen, auch gleich den Newsletter zu abonnieren.

Dies ist keine bezahlte Werbung. Ich bin einfach begeistert.

bookmark_borderA wicked problem from the past

In the last few evenings there was great despair. Trying to solve one problem would reveal yet another one. As a result, I hesitated to release some of the changes immediately. I don’t want to make people suffer, having to deal with new improved problems of the day whenever they run an update on their horde installations. I’m glad feedback on the mailing list has improved quite a lot, sometimes coming with patches and suggestions and even people telling me things start to just work. That’s great. But whenever you see the light, there’s a new challenge on the horizon. Or rather something very old lurking in the shadows.

Break it till you make it

So recently we felt confident enough to switch our local installation from a frozen state to the latest version of the wicked wiki and the whups bug tracker. We also updated the PHP container, the database version and the likes. This turned into a great opportunity to test the rollback plan. 🙄 Issues were cascading.

Bullets you might have dodged

Generally having both the composer autoloader and the horde autoloader work together creates some friction. This is complicated when the exact order in which they are initialized is not the same in all scenarios. As reported previously, Horde’s apps are not strictly conforming the PSR-0 autoloading standard while the libraries mostly do. Newer releases of the apps autoload using a class map instead of PSR-0 logic. But that class map was not restricted enough in the first iterations. Thus you might have the canonical locations of classes in the autoloader, but also the version in the /web/ dir or, in case of custom hook classes, the version from the /var/config/ directory. Torben Dannhauer suggested to restrict the rules and we will do that with the next iteration. The first attempt to fix this unfortunately broke overriding the mime.local.php config file. An update is in the git tree and will be released as a version later this week. But I’m glad we ran into this as it revealed another dark secret

We part, each to wander the abyss alone

Turned out the wicked wiki software carried a little library Text_Wiki in its belly. Hailing from PHP 4 days, it’s archaic in its structure and its way of implementing features. Parts of the test suite predate phpunit and the way it’s packaged is clearly not from our times. This library also exists as a separate package horde/text_wiki. Which also exists as pear/text_wiki both in the classic PEAR archive and the modern github repository. While the base package does not exist in packagist, the extension driver for mediawiki format does. Odd enough. It’s really a shame the software from the old PEAR ecosystem is slowly fading away because of its ossification. They all share ancestry and they were all largely written and maintained by the same set of people but they are all different in subtle ways.

Text_Wiki works great when it works. But it’s a real treasure trove of incompatibilities with modern PHP versions. Over the years, unicode support has evolved along with the strictness of PHP’s core. While I am very much willing to contribute back any changes to the official pear version, I have my doubts if they will be accepted or handled at all.

Rising again like the phoenix

I really, really did not want to deal with any of this. Creating a fourth version out of three splinters feels like madness. But what can you do?
The result is an upcoming version of horde/text_wiki and horde/wicked which is a radical break. The new text/wiki has no PSR-0 lib/ content. It’s all PSR-4, it’s namespaced, every single class name and file name has changed. A new test suite has been started but only a tiny fraction of unit tests have been ported so far. The different extension drivers for mediawiki and tiki dialects have been incorporated into the base package as I did not want to maintain a hand full of packages.

The whole thing couples as little to horde as possible. It has its own exceptions based on the PHP builtin exception tree, so no dependency on horde/exception. I even stripped the dependency on Pear_Exception as there is no value in keeping that nowadays. Anything which ties this revamped library into the horde ecosystem now lives in horde/wicked. Extra markup rules which render horde portal blocks into a wiki page. Adapters querying the database if a linked page already exists or must be created. Dynamic links created from the registry and application states. None of this glue belongs into the text_wiki library.

Many incompatibilities with modern PHP have been solved. Often this was a matter of mixing and matching bits from the three different splinter versions out in the wild. Some of it is just a chore to do. Of course, the pear XML files are gone and the composer file is fully compliant with packagist and most recent composer. At least this part has been contributed back already.

Dawn, finally

It will be another few evenings until the new horde/wicked and the new horde/text_wiki are ready for a tagged release, probably along with some of the changes to other libraries I explained above. There’s probably something that will break and need fixing. But that shouldn’t block progress for too long.

bookmark_borderProxy Mode in horde-installer-plugin

Horde 5 relied on the PEAR ecosystem ripping apart their packages and putting everything where it needs to be. But PEAR is a thing of the past. Horde 6 integrates with the composer installer using an additional piece called the horde/horde-installer-plugin. This composer plugin links together pieces from the installed composer packages into the right places and adds auto-generated configuration to make sure everything is found even though stuff is NOT mixed together the way PEAR did. This was a good starting point early in Horde 6 development when we took great pains not to break the previous distribution installs (which relied on PEAR or PEAR like behaviour) and installs based on horde-git-tools.

However, by now this is much less a priority and we are absolutely willing to move forward and make things less complicated, potentially breaking and changing code that relied on these older schemes. Much less code and assets are going to be exposed through the /web/ folder.

Proxy Mode vs Symlink Mode

Version 2.7.0 of horde/horde-installer-plugin introduces a new switch to the horde:reconfigure command:

composer horde:reconfigure --mode=proxy
  • In proxy mode composer will not symlink whole apps to the web dir but only create folders and files outside lib/, src/, test/ and a fairly large exclusion list. The endpoint php files will forward requests to the actual code in /vendor dir. This should reduce code duplication and makes it more obvious where related files need to be looked up
  • A new constant HORDE_CONFIG_BASE is written to the autogenerated registry snippets.
  • The composer autoloader is directly loaded by the proxy php files before delegating to the actual file in the vendor dir.
    If you see unexpected class not found messages when testing proxy mode, please upgrade your horde/core package.

Currently proxy mode is a one-off. However, in an upcoming version the last used mode will be persisted to the composer.json file and reused on subsequent commands without an explicit mode option. Proxy mode is supposed to become the default in horde-installer-plugin 3.0.

Benefits of proxy mode

Proxy mode is the first step towards clarifying the intended architecture. Until recently much of our detection logic needed to behave differently if called through a file in the webroot or the same file but in its original vendor/horde/$app location. The new proxy files always perform the same way:

  • First load the composer autoloader because the file always knows where the autoloader is relative to its own path.
  • Then load the actual file in the vendor/horde/$app location
  • Horde’s own internal PSR-0+ autoloader would only be loaded after composer’s autoloader. It hits action less and less often and will probably be removed from the default setup altogether.

This means the actual class files can rely on __FILE__ pointing to a predictable scheme and much less guess work and protection code is needed. On the other hand, it reveals some hidden problems and complications which developers can now fix before making this new mode the default. Unexpected lowercase class names (as found in whups and Horde Forms), complicated chicken/egg issues in the Registry, duplicate class names and parts of the error handling broken by subtle changes in newer PHP versions can now be seen and fixed.

As a consequence of spotting bugs and loading issues as I go along, starting with version 2.7.1 the composer plugin will check if a file var/config/autoload-extra.php exists. If it does, it will be included after the composer autoloader. This allows selectively hardcoding some class files until fixed versions of apps become available – or to actively undefine something for tests.

bookmark_borderUnit Testing Horde 6

Some years ago I reasoned about upgrading unit tests from ancient PHPUnit 4 to then-recent PHPUnit 9.
Back then Horde’s unit test suite would use a Horde_Test class inheriting from the actual phpunit test cases. Even then I was fairly certain that this approach was not very practical in the long run.
Why extending PHPUnit might be wrong – ralf-lang.de

Turns out this is right. With many new or refactored components I started to use plain PHPUnit 9 and only resorted to Horde_Test when I strictly needed it. This simplified things a lot. The Horde organisation houses a dozen or so apps, almost 200 library repositories and some auxillary stuff. Having Horde_Test in the inheritance hierarchy makes any upgrades awkward. Need to be more strict about signatures for a new PHP release? Fix it all at once. Want to adopt a newer phpunit framework? Do it wholesale. Hook into some internal PHPUnit mechanism? They told us not to and they use their freedom to break internal classes very liberally. Tough luck.

So instead I am mostly abandoning the Horde_Test_Case base class and everything that comes with it.

  • PHPUnit hooks right into the composer autoloader. No more AllTests.php, autoload.php or bootstrap.php
  • I use the PHPUnit version I see fit. If I rarely update a component and it’s supposed to run on old PHP 7.4 through PHP 8.4, then I am fine when the tests work with PHPUnit 10.
  • If I develop something bleeding edge which does not need to work on PHP 8.2 and older, I simply start out at PHPUnit 12.
  • If I want to abstract out a test helper which is useful beyond the limits of an individual repo, I try to avoid building new bases classes. Instead I build it as a utility

Many libraries which get a wholesale upgrade no longer depend on Horde/Test. This is good because Horde/Test pulls in too many implicit dependencies anyway.

This comes at a price though. Our custom test runner relied on a specific structure of horde tests. The internal quality check pipeline in the horde-components tool also relied on a specific phpunit version and library layout. These are now broken and I won’t try that approach again.

So the new layout looks fairly standard.

  • Tests are in the /test/ subdirectory
  • Unit tests which are intended to run on most platforms are in /test/unit. Tests which require external resources, expensive calculations or rarely used PHP extensions get their own lowercase test suite directory
    /test/ldap/
    /test/pam/
    /test/kolab/
    /test/integration/
  • The horde-components tool autodetects these test suites and sets up the appropriate PSR-4 autoload-dev rules in the composer.json file
  • Just call “phpunit” to run the test suites.

bookmark_borderI want to run horde/components on new PHP

If you want to run horde-components on a version of PHP which is not yet reflected in packagist.org released versions:

me@mine:~/horde/components$ composer config minimum-stability dev
me@mine:~/horde/components$ composer install –ignore-platform-reqs

No composer.lock file present. Updating dependencies to latest instead of installing from lock file. See https://getcomposer.org/install for more information.
Loading composer repositories with package information
Updating dependencies
Lock file operations: 90 installs, 0 updates, 0 removals

  • Locking fig/event-dispatcher-util (1.3.1)
  • Locking horde/alarm (v3.0.0alpha4)
  • Locking horde/argv (dev-FRAMEWORK_6_0 a1362b5)
  • Locking horde/auth (v3.0.0alpha6)
  • Locking horde/autoloader (dev-FRAMEWORK_6_0 dfb56fa)
  • Locking horde/browser (v3.0.0alpha4)
  • Locking horde/cache (dev-FRAMEWORK_6_0 29bb731)
  • Locking horde/cli (v3.0.0alpha5)
  • Locking horde/cli_modular (v3.0.0alpha4)
  • Locking horde/compress (v3.0.0alpha4)
  • Locking horde/compress_fast (v2.0.0alpha4)
  • Locking horde/constraint (v3.0.0alpha4)
  • Locking horde/controller (dev-FRAMEWORK_6_0 35005ea)
  • Locking horde/core (v3.0.0alpha14)
  • Locking horde/crypt_blowfish (v2.0.0alpha3)
  • Locking horde/css_parser (v2.0.0alpha4)
  • Locking horde/cssminify (v2.0.0alpha4)
  • Locking horde/data (v3.0.0alpha4)
  • Locking horde/date (v3.0.0alpha4)
  • Locking horde/dav (v2.0.0alpha4)
  • Locking horde/db (v3.0.0alpha4)
  • Locking horde/eventdispatcher (dev-FRAMEWORK_6_0 8253aa9)
  • Locking horde/exception (v3.0.0alpha4)
  • Locking horde/githubapiclient (dev-FRAMEWORK_6_0 fe17552)
  • Locking horde/group (v3.0.0alpha4)
  • Locking horde/hashtable (v2.0.0alpha4)
  • Locking horde/history (v3.0.0alpha4)
  • Locking horde/horde-installer-plugin (v2.5.5)
  • Locking horde/http (dev-FRAMEWORK_6_0 a98cb86)
  • Locking horde/http_server (dev-FRAMEWORK_6_0 4a91669)
  • Locking horde/icalendar (v3.0.0alpha4)
  • Locking horde/idna (v2.0.0alpha4)
  • Locking horde/injector (dev-FRAMEWORK_6_0 6fbc75e)
  • Locking horde/javascriptminify (v2.0.0alpha4)
  • Locking horde/listheaders (v2.0.0alpha4)
  • Locking horde/lock (v3.0.0alpha4)
  • Locking horde/log (v3.0.0alpha8)
  • Locking horde/logintasks (v3.0.0alpha4)
  • Locking horde/mail (v3.0.0alpha4)
  • Locking horde/mime (v3.0.0alpha5)
  • Locking horde/mime_viewer (v3.0.0alpha4)
  • Locking horde/nls (v3.0.0alpha4)
  • Locking horde/notification (v3.0.0alpha4)
  • Locking horde/pack (v2.0.0alpha4)
  • Locking horde/pear (dev-FRAMEWORK_6_0 6e99004)
  • Locking horde/perms (v3.0.0alpha4)
  • Locking horde/prefs (v3.0.0alpha6)
  • Locking horde/release (v4.0.0alpha4)
  • Locking horde/role (dev-FRAMEWORK_6_0 cac03aa)
  • Locking horde/rpc (v3.0.0alpha5)
  • Locking horde/secret (v3.0.0alpha4)
  • Locking horde/serialize (v3.0.0alpha4)
  • Locking horde/sessionhandler (v3.0.0alpha3)
  • Locking horde/share (v3.0.0alpha4)
  • Locking horde/stream (v2.0.0alpha4)
  • Locking horde/stream_filter (v3.0.0alpha4)
  • Locking horde/stream_wrapper (v3.0.0alpha4)
  • Locking horde/support (v3.0.0.1alpha4)
  • Locking horde/template (v3.0.0alpha4)
  • Locking horde/test (dev-FRAMEWORK_6_0 c512302)
  • Locking horde/text_diff (dev-FRAMEWORK_6_0 f7cfbc2)
  • Locking horde/text_filter (v3.0.0alpha3)
  • Locking horde/text_flowed (v3.0.0alpha4)
  • Locking horde/token (v3.0.0alpha4)
  • Locking horde/translation (v3.0.0alpha2)
  • Locking horde/url (v3.0.0alpha5)
  • Locking horde/util (dev-FRAMEWORK_6_0 f62a395)
  • Locking horde/view (v3.0.0alpha4)
  • Locking horde/xml_element (v3.0.0alpha4)
  • Locking horde/yaml (dev-FRAMEWORK_6_0 c222d58)
  • Locking pear/archive_tar (1.5.0)
  • Locking pear/console_getopt (dev-master f0098a8)
  • Locking pear/pear (dev-master 9d3ac5e)
  • Locking pear/structures_graph (dev-trunk 66368ac)
  • Locking pear/xml_util (dev-master a1ce442)
  • Locking php-extended/polyfill-php80-stringable (1.2.12)
  • Locking psr/container (dev-master 7079847)
  • Locking psr/event-dispatcher (dev-master bbd9eac)
  • Locking psr/http-client (dev-master bb5906e)
  • Locking psr/http-factory (1.1.0)
  • Locking psr/http-message (dev-master 402d35b)
  • Locking psr/http-server-handler (dev-master 13403d4)
  • Locking psr/http-server-middleware (dev-master 459eeb7)
  • Locking psr/log (dev-master f16e1d5)
  • Locking sabre/dav (4.7.x-dev 7183a67)
  • Locking sabre/event (5.1.x-dev 1538b1b)
  • Locking sabre/http (5.1.x-dev 4c2a2c0)
  • Locking sabre/uri (v2.x-dev 2c21ebd)
  • Locking sabre/vobject (4.5.x-dev ff22611)
  • Locking sabre/xml (2.2.x-dev 01a7927)
    Writing lock file
    Installing dependencies from lock file (including require-dev)
    Package operations: 90 installs, 0 updates, 0 removals
  • Downloading horde/horde-installer-plugin (v2.5.5)
  • Downloading horde/translation (v3.0.0alpha2)
  • Downloading horde/exception (v3.0.0alpha4)
  • Downloading horde/util (dev-FRAMEWORK_6_0 f62a395)
  • Downloading horde/nls (v3.0.0alpha4)
  • Downloading horde/date (v3.0.0alpha4)
  • Downloading horde/alarm (v3.0.0alpha4)
  • Downloading horde/auth (v3.0.0alpha6)
  • Downloading horde/autoloader (dev-FRAMEWORK_6_0 dfb56fa)
  • Downloading horde/compress_fast (v2.0.0alpha4)
  • Downloading horde/cache (dev-FRAMEWORK_6_0 29bb731)
  • Downloading horde/stream_wrapper (v3.0.0alpha4)
  • Downloading horde/support (v3.0.0.1alpha4)
  • Downloading horde/cli (v3.0.0alpha5)
  • Downloading horde/argv (dev-FRAMEWORK_6_0 a1362b5)
  • Downloading horde/cli_modular (v3.0.0alpha4)
  • Downloading psr/log (dev-master f16e1d5)
  • Downloading php-extended/polyfill-php80-stringable (1.2.12)
  • Downloading horde/constraint (v3.0.0alpha4)
  • Downloading horde/log (v3.0.0alpha8)
  • Downloading psr/container (dev-master 7079847)
  • Downloading horde/injector (dev-FRAMEWORK_6_0 6fbc75e)
  • Downloading horde/controller (dev-FRAMEWORK_6_0 35005ea)
  • Downloading horde/crypt_blowfish (v2.0.0alpha3)
  • Downloading horde/url (v3.0.0alpha5)
  • Downloading horde/css_parser (v2.0.0alpha4)
  • Downloading horde/cssminify (v2.0.0alpha4)
  • Downloading horde/text_flowed (v3.0.0alpha4)
  • Downloading horde/secret (v3.0.0alpha4)
  • Downloading horde/idna (v2.0.0alpha4)
  • Downloading horde/text_filter (v3.0.0alpha3)
  • Downloading horde/stream_filter (v3.0.0alpha4)
  • Downloading horde/stream (v2.0.0alpha4)
  • Downloading horde/mime (v3.0.0alpha5)
  • Downloading horde/mail (v3.0.0alpha4)
  • Downloading horde/listheaders (v2.0.0alpha4)
  • Downloading horde/icalendar (v3.0.0alpha4)
  • Downloading horde/browser (v3.0.0alpha4)
  • Downloading horde/data (v3.0.0alpha4)
  • Downloading psr/event-dispatcher (dev-master bbd9eac)
  • Downloading fig/event-dispatcher-util (1.3.1)
  • Downloading horde/eventdispatcher (dev-FRAMEWORK_6_0 8253aa9)
  • Downloading psr/http-message (dev-master 402d35b)
  • Downloading psr/http-factory (1.1.0)
  • Downloading psr/http-client (dev-master bb5906e)
  • Downloading horde/http (dev-FRAMEWORK_6_0 a98cb86)
  • Downloading horde/githubapiclient (dev-FRAMEWORK_6_0 fe17552)
  • Downloading horde/hashtable (v2.0.0alpha4)
  • Downloading horde/db (v3.0.0alpha4)
  • Downloading horde/history (v3.0.0alpha4)
  • Downloading psr/http-server-handler (dev-master 13403d4)
  • Downloading psr/http-server-middleware (dev-master 459eeb7)
  • Downloading horde/http_server (dev-FRAMEWORK_6_0 4a91669)
  • Downloading horde/javascriptminify (v2.0.0alpha4)
  • Downloading horde/lock (v3.0.0alpha4)
  • Downloading horde/logintasks (v3.0.0alpha4)
  • Downloading horde/compress (v3.0.0alpha4)
  • Downloading horde/mime_viewer (v3.0.0alpha4)
  • Downloading horde/notification (v3.0.0alpha4)
  • Downloading horde/pack (v2.0.0alpha4)
  • Downloading horde/yaml (dev-FRAMEWORK_6_0 c222d58)
  • Downloading horde/xml_element (v3.0.0alpha4)
  • Downloading horde/pear (dev-FRAMEWORK_6_0 6e99004)
  • Downloading horde/prefs (v3.0.0alpha6)
  • Downloading horde/serialize (v3.0.0alpha4)
  • Downloading horde/group (v3.0.0alpha4)
  • Downloading horde/perms (v3.0.0alpha4)
  • Downloading sabre/uri (v2.x-dev 2c21ebd)
  • Downloading sabre/xml (2.2.x-dev 01a7927)
  • Downloading sabre/vobject (4.5.x-dev ff22611)
  • Downloading sabre/event (5.1.x-dev 1538b1b)
  • Downloading sabre/http (5.1.x-dev 4c2a2c0)
  • Downloading sabre/dav (4.7.x-dev 7183a67)
  • Downloading pear/pear (dev-master 9d3ac5e)
  • Downloading pear/xml_util (dev-master a1ce442)
  • Downloading pear/structures_graph (dev-trunk 66368ac)
  • Downloading pear/console_getopt (dev-master f0098a8)
  • Downloading pear/archive_tar (1.5.0)
  • Downloading horde/view (v3.0.0alpha4)
  • Downloading horde/token (v3.0.0alpha4)
  • Downloading horde/template (v3.0.0alpha4)
  • Downloading horde/share (v3.0.0alpha4)
  • Downloading horde/sessionhandler (v3.0.0alpha3)
  • Downloading horde/core (v3.0.0alpha14)
  • Downloading horde/dav (v2.0.0alpha4)
  • Downloading horde/rpc (v3.0.0alpha5)
  • Downloading horde/release (v4.0.0alpha4)
  • Downloading horde/role (dev-FRAMEWORK_6_0 cac03aa)
  • Downloading horde/test (dev-FRAMEWORK_6_0 c512302)
  • Downloading horde/text_diff (dev-FRAMEWORK_6_0 f7cfbc2)
  • Installing horde/horde-installer-plugin (v2.5.5): Extracting archive
  • Installing horde/translation (v3.0.0alpha2): Extracting archive
  • Installing horde/exception (v3.0.0alpha4): Extracting archive
  • Installing horde/util (dev-FRAMEWORK_6_0 f62a395): Extracting archive
  • Installing horde/nls (v3.0.0alpha4): Extracting archive
  • Installing horde/date (v3.0.0alpha4): Extracting archive
  • Installing horde/alarm (v3.0.0alpha4): Extracting archive
  • Installing horde/auth (v3.0.0alpha6): Extracting archive
  • Installing horde/autoloader (dev-FRAMEWORK_6_0 dfb56fa): Extracting archive
  • Installing horde/compress_fast (v2.0.0alpha4): Extracting archive
  • Installing horde/cache (dev-FRAMEWORK_6_0 29bb731): Extracting archive
  • Installing horde/stream_wrapper (v3.0.0alpha4): Extracting archive
  • Installing horde/support (v3.0.0.1alpha4): Extracting archive
  • Installing horde/cli (v3.0.0alpha5): Extracting archive
  • Installing horde/argv (dev-FRAMEWORK_6_0 a1362b5): Extracting archive
  • Installing horde/cli_modular (v3.0.0alpha4): Extracting archive
  • Installing psr/log (dev-master f16e1d5): Extracting archive
  • Installing php-extended/polyfill-php80-stringable (1.2.12): Extracting archive
  • Installing horde/constraint (v3.0.0alpha4): Extracting archive
  • Installing horde/log (v3.0.0alpha8): Extracting archive
  • Installing psr/container (dev-master 7079847): Extracting archive
  • Installing horde/injector (dev-FRAMEWORK_6_0 6fbc75e): Extracting archive
  • Installing horde/controller (dev-FRAMEWORK_6_0 35005ea): Extracting archive
  • Installing horde/crypt_blowfish (v2.0.0alpha3): Extracting archive
  • Installing horde/url (v3.0.0alpha5): Extracting archive
  • Installing horde/css_parser (v2.0.0alpha4): Extracting archive
  • Installing horde/cssminify (v2.0.0alpha4): Extracting archive
  • Installing horde/text_flowed (v3.0.0alpha4): Extracting archive
  • Installing horde/secret (v3.0.0alpha4): Extracting archive
  • Installing horde/idna (v2.0.0alpha4): Extracting archive
  • Installing horde/text_filter (v3.0.0alpha3): Extracting archive
  • Installing horde/stream_filter (v3.0.0alpha4): Extracting archive
  • Installing horde/stream (v2.0.0alpha4): Extracting archive
  • Installing horde/mime (v3.0.0alpha5): Extracting archive
  • Installing horde/mail (v3.0.0alpha4): Extracting archive
  • Installing horde/listheaders (v2.0.0alpha4): Extracting archive
  • Installing horde/icalendar (v3.0.0alpha4): Extracting archive
  • Installing horde/browser (v3.0.0alpha4): Extracting archive
  • Installing horde/data (v3.0.0alpha4): Extracting archive
  • Installing psr/event-dispatcher (dev-master bbd9eac): Extracting archive
  • Installing fig/event-dispatcher-util (1.3.1): Extracting archive
  • Installing horde/eventdispatcher (dev-FRAMEWORK_6_0 8253aa9): Extracting archive
  • Installing psr/http-message (dev-master 402d35b): Extracting archive
  • Installing psr/http-factory (1.1.0): Extracting archive
  • Installing psr/http-client (dev-master bb5906e): Extracting archive
  • Installing horde/http (dev-FRAMEWORK_6_0 a98cb86): Extracting archive
  • Installing horde/githubapiclient (dev-FRAMEWORK_6_0 fe17552): Extracting archive
  • Installing horde/hashtable (v2.0.0alpha4): Extracting archive
  • Installing horde/db (v3.0.0alpha4): Extracting archive
  • Installing horde/history (v3.0.0alpha4): Extracting archive
  • Installing psr/http-server-handler (dev-master 13403d4): Extracting archive
  • Installing psr/http-server-middleware (dev-master 459eeb7): Extracting archive
  • Installing horde/http_server (dev-FRAMEWORK_6_0 4a91669): Extracting archive
  • Installing horde/javascriptminify (v2.0.0alpha4): Extracting archive
  • Installing horde/lock (v3.0.0alpha4): Extracting archive
  • Installing horde/logintasks (v3.0.0alpha4): Extracting archive
  • Installing horde/compress (v3.0.0alpha4): Extracting archive
  • Installing horde/mime_viewer (v3.0.0alpha4): Extracting archive
  • Installing horde/notification (v3.0.0alpha4): Extracting archive
  • Installing horde/pack (v2.0.0alpha4): Extracting archive
  • Installing horde/yaml (dev-FRAMEWORK_6_0 c222d58): Extracting archive
  • Installing horde/xml_element (v3.0.0alpha4): Extracting archive
  • Installing horde/pear (dev-FRAMEWORK_6_0 6e99004): Extracting archive
  • Installing horde/prefs (v3.0.0alpha6): Extracting archive
  • Installing horde/serialize (v3.0.0alpha4): Extracting archive
  • Installing horde/group (v3.0.0alpha4): Extracting archive
  • Installing horde/perms (v3.0.0alpha4): Extracting archive
  • Installing sabre/uri (v2.x-dev 2c21ebd): Extracting archive
  • Installing sabre/xml (2.2.x-dev 01a7927): Extracting archive
  • Installing sabre/vobject (4.5.x-dev ff22611): Extracting archive
  • Installing sabre/event (5.1.x-dev 1538b1b): Extracting archive
  • Installing sabre/http (5.1.x-dev 4c2a2c0): Extracting archive
  • Installing sabre/dav (4.7.x-dev 7183a67): Extracting archive
  • Installing pear/pear (dev-master 9d3ac5e): Extracting archive
  • Installing pear/xml_util (dev-master a1ce442): Extracting archive
  • Installing pear/structures_graph (dev-trunk 66368ac): Extracting archive
  • Installing pear/console_getopt (dev-master f0098a8): Extracting archive
  • Installing pear/archive_tar (1.5.0): Extracting archive
  • Installing horde/view (v3.0.0alpha4): Extracting archive
  • Installing horde/token (v3.0.0alpha4): Extracting archive
  • Installing horde/template (v3.0.0alpha4): Extracting archive
  • Installing horde/share (v3.0.0alpha4): Extracting archive
  • Installing horde/sessionhandler (v3.0.0alpha3): Extracting archive
  • Installing horde/core (v3.0.0alpha14): Extracting archive
  • Installing horde/dav (v2.0.0alpha4): Extracting archive
  • Installing horde/rpc (v3.0.0alpha5): Extracting archive
  • Installing horde/release (v4.0.0alpha4): Extracting archive
  • Installing horde/role (dev-FRAMEWORK_6_0 cac03aa): Extracting archive
  • Installing horde/test (dev-FRAMEWORK_6_0 c512302): Extracting archive
  • Installing horde/text_diff (dev-FRAMEWORK_6_0 f7cfbc2): Extracting archive
    73 package suggestions were added by new dependencies, use composer suggest to see details.
    Package php-extended/polyfill-php80-stringable is abandoned, you should avoid using it. Use php >= 8.0 instead.
    Generating autoload files
    Applying /presets for absent files in /var/config
    Looking for registry snippets from apps
    Writing app configs to /var/config dir
    Linking app configs to /web Dir
    Linking javascript tree to /web/js
    Linking themes tree to /web/themes

Beware, these constraints are there for a reason. Expect things to break in unexpected ways if versions are actually incompatible.