Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion lib/Horde/Core/Mime/Viewer/Vcard.php
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ protected function _renderInline()
$birthday = new Horde_Date($birthdays[0]);
$html .= $this->_row(
Horde_Core_Translation::t('Birthday'),
$birthday->strftime($prefs->getValue('date_format'))
$birthday->format($prefs->getValue('date_format'), new \Horde\Date\Formatter\IcuFormatter(), $GLOBALS['language'] ?? 'en_US')
);
} catch (Horde_Icalendar_Exception $e) {
}
Expand Down
49 changes: 49 additions & 0 deletions src/Factory/DateFormatPrefsFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

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

namespace Horde\Core\Factory;

use Horde\Core\Prefs\DateFormatPrefs;
use Horde_Injector;
use Psr\Log\LoggerInterface;
use Psr\Log\NullLogger;

/**
* Factory for DateFormatPrefs
*
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/
class DateFormatPrefsFactory
{
public function create(Horde_Injector $injector): DateFormatPrefs
{
$logger = null;
try {
$logger = $injector->getInstance(LoggerInterface::class);
} catch (\Throwable) {
// Logger unavailable — proceed without
}

return new DateFormatPrefs(
prefs: $injector->getInstance('Horde_Prefs'),
locale: $GLOBALS['language'] ?? 'en_US',
logger: $logger,
);
}
}
3 changes: 2 additions & 1 deletion src/Middleware/AuthHordeSession.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public function __construct(Horde_Registry $registry)

public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
{
if ($this->registry->isAuthenticated()) {
$isAuth = $this->registry->isAuthenticated();
if ($isAuth) {
$request = $request->withAttribute('HORDE_AUTHENTICATED_USER', $this->registry->getAuth());
$request = $request->withoutAttribute('HORDE_GUEST');
} else {
Expand Down
7 changes: 5 additions & 2 deletions src/Middleware/RedirectToLogin.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,15 +50,18 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
$user = $request->getAttribute('HORDE_AUTHENTICATED_USER');
$app = $request->getAttribute('app');

// Admins bypass all permission checks
if ($user && $this->registry->isAdmin(['user' => $user])) {
return $handler->handle($request);
}

// Check app-level read permission if PermissionService is available
if ($this->permissionService !== null && $app) {
if ($this->permissionService->exists($app)) {
// Pass empty string for guests — backend returns guest permissions
$checkUser = $user ?: '';
if (!$this->permissionService->hasPermission($app, $checkUser, ['read'])) {
return $this->redirectToLogin($request);
}
// Permission granted — allow through even if not authenticated (guest read)
return $handler->handle($request);
}
}
Expand Down
119 changes: 119 additions & 0 deletions src/Prefs/DateFormatPrefs.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

declare(strict_types=1);

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

namespace Horde\Core\Prefs;

use Horde\Date\Format;
use Horde_Prefs;
use Psr\Log\LoggerInterface;
use Throwable;

/**
* Date format preferences decorator
*
* Wraps date format preference access with automatic strftime-to-ICU
* conversion. On first access of a legacy strftime value:
* 1. Detects strftime format
* 2. Logs the conversion
* 3. Converts to ICU pattern
* 4. Writes back the converted value (failsafe, noop if impossible)
* 5. Returns the ICU pattern
*
* @category Horde
* @copyright 2026 The Horde Project
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
* @package Core
*/
class DateFormatPrefs
{
private const DATE_FORMAT_KEYS = [
'date_format',
'date_format_mini',
'time_format',
'time_format_mini',
];

public function __construct(
private Horde_Prefs $prefs,
private string $locale = 'en_US',
private ?LoggerInterface $logger = null,
) {}

/**
* Get a date format preference as an ICU pattern.
*
* If the stored value is strftime, converts it to ICU, writes back
* the converted value (failsafe), logs the conversion, and returns
* the ICU pattern. If already ICU, returns as-is.
*/
public function getDateFormat(string $key): string
{
$value = $this->prefs->getValue($key);
if ($value === null) {
return '';
}

if (!Format::isStrftimeFormat($value)) {
return $value;
}

$icu = Format::strftimeToIcu($value, $this->locale);

$this->logger?->notice(
"DateFormatPrefs: converting legacy strftime pref '{$key}': '{$value}' → '{$icu}'"
);

if (!$this->prefs->isLocked($key)) {
try {
$this->prefs->setValue($key, $icu);
} catch (Throwable) {
// Noop — read-only backend or other error
}
}

return $icu;
}

/**
* Get the short time format ICU pattern based on twentyFour pref.
*
* Replaces the common pattern:
* $prefs->getValue('twentyFour') ? '%R' : '%I:%M%p'
*/
public function getTimeFormatShort(): string
{
return $this->prefs->getValue('twentyFour') ? 'HH:mm' : 'h:mm a';
}

/**
* Get the time format with seconds based on twentyFour pref.
*
* Replaces the common pattern:
* $prefs->getValue('twentyFour') ? '%H:%M:%S' : '%I:%M:%S %p'
*/
public function getTimeFormatFull(): string
{
return $this->prefs->getValue('twentyFour') ? 'HH:mm:ss' : 'h:mm:ss a';
}

/**
* Check if a given pref key is a date format key handled by this class.
*/
public function isDateFormatKey(string $key): bool
{
return in_array($key, self::DATE_FORMAT_KEYS, true);
}
}
20 changes: 2 additions & 18 deletions src/Prefs/StrftimeFinding.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,37 +52,21 @@ public function __construct(
public readonly string $field,
public readonly string $location,
public readonly string $strftime,
public readonly string|array $icu,
public readonly string $icu,
public readonly string $confidence,
) {
if (!in_array($confidence, [self::CONFIDENCE_HIGH, self::CONFIDENCE_MEDIUM, self::CONFIDENCE_LOW], true)) {
throw new InvalidArgumentException("Invalid confidence level: $confidence");
}
}

/**
* Check if ICU pattern is locale-specific
*
* @return bool True if pattern depends on user locale
*/
public function isLocaleSpecific(): bool
{
return is_array($this->icu);
}

/**
* Get ICU pattern as string
*
* For locale-specific patterns, returns a description.
* For concrete patterns, returns the pattern itself.
*
* @return string ICU pattern or description
* @return string ICU pattern
*/
public function getIcuString(): string
{
if (is_array($this->icu)) {
return '[locale-specific: ' . implode(', ', array_keys($this->icu)) . ']';
}
return $this->icu;
}

Expand Down
Loading