Skip to content

Commit 8a0b169

Browse files
authored
Merge pull request #105 from horde/fix/permission-aware-redirect
Related to horde/turba#47 and horde/turba#50. See also | Repository | PR | |---|---| | Date | horde/Date#11 | | Form | horde/Form#33 | | Core | #105 | | Model | horde/Model#1 | | Mime_Viewer | horde/Mime_Viewer#2 | | Kolab_Resource | horde/Kolab_Resource#1 | | Rampage | horde/Rampage#2 | | Rdo | horde/Rdo#5 | | agora | horde/agora#3 | | chora | horde/chora#2 | | components | horde/components#21 | | dev.horde.org | horde/dev.horde.org#1 | | folks | horde/folks#3 | | gollem | horde/gollem#5 | | hermes | horde/hermes#6 | | hylax | horde/hylax#3 | | ingo | horde/ingo#25 | | jonah | horde/jonah#8 | | klutz | horde/klutz#1 | | kronolith | horde/kronolith#39 | | mnemo | horde/mnemo#18 | | operator | horde/operator#1 | | trean | horde/trean#5 | | wicked | horde/wicked#24 |
2 parents 6d838cb + 42be4f6 commit 8a0b169

6 files changed

Lines changed: 178 additions & 22 deletions

File tree

lib/Horde/Core/Mime/Viewer/Vcard.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ protected function _renderInline()
166166
$birthday = new Horde_Date($birthdays[0]);
167167
$html .= $this->_row(
168168
Horde_Core_Translation::t('Birthday'),
169-
$birthday->strftime($prefs->getValue('date_format'))
169+
$birthday->format($prefs->getValue('date_format'), new \Horde\Date\Formatter\IcuFormatter(), $GLOBALS['language'] ?? 'en_US')
170170
);
171171
} catch (Horde_Icalendar_Exception $e) {
172172
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright 2026 The Horde Project (http://www.horde.org/)
7+
*
8+
* See the enclosed file LICENSE for license information (LGPL). If you
9+
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
10+
*
11+
* @category Horde
12+
* @copyright 2026 The Horde Project
13+
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
14+
* @package Core
15+
*/
16+
17+
namespace Horde\Core\Factory;
18+
19+
use Horde\Core\Prefs\DateFormatPrefs;
20+
use Horde_Injector;
21+
use Psr\Log\LoggerInterface;
22+
use Psr\Log\NullLogger;
23+
24+
/**
25+
* Factory for DateFormatPrefs
26+
*
27+
* @category Horde
28+
* @copyright 2026 The Horde Project
29+
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
30+
* @package Core
31+
*/
32+
class DateFormatPrefsFactory
33+
{
34+
public function create(Horde_Injector $injector): DateFormatPrefs
35+
{
36+
$logger = null;
37+
try {
38+
$logger = $injector->getInstance(LoggerInterface::class);
39+
} catch (\Throwable) {
40+
// Logger unavailable — proceed without
41+
}
42+
43+
return new DateFormatPrefs(
44+
prefs: $injector->getInstance('Horde_Prefs'),
45+
locale: $GLOBALS['language'] ?? 'en_US',
46+
logger: $logger,
47+
);
48+
}
49+
}

src/Middleware/AuthHordeSession.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public function __construct(Horde_Registry $registry)
3737

3838
public function process(ServerRequestInterface $request, RequestHandlerInterface $handler): ResponseInterface
3939
{
40-
if ($this->registry->isAuthenticated()) {
40+
$isAuth = $this->registry->isAuthenticated();
41+
if ($isAuth) {
4142
$request = $request->withAttribute('HORDE_AUTHENTICATED_USER', $this->registry->getAuth());
4243
$request = $request->withoutAttribute('HORDE_GUEST');
4344
} else {

src/Middleware/RedirectToLogin.php

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -50,15 +50,18 @@ public function process(ServerRequestInterface $request, RequestHandlerInterface
5050
$user = $request->getAttribute('HORDE_AUTHENTICATED_USER');
5151
$app = $request->getAttribute('app');
5252

53+
// Admins bypass all permission checks
54+
if ($user && $this->registry->isAdmin(['user' => $user])) {
55+
return $handler->handle($request);
56+
}
57+
5358
// Check app-level read permission if PermissionService is available
5459
if ($this->permissionService !== null && $app) {
5560
if ($this->permissionService->exists($app)) {
56-
// Pass empty string for guests — backend returns guest permissions
5761
$checkUser = $user ?: '';
5862
if (!$this->permissionService->hasPermission($app, $checkUser, ['read'])) {
5963
return $this->redirectToLogin($request);
6064
}
61-
// Permission granted — allow through even if not authenticated (guest read)
6265
return $handler->handle($request);
6366
}
6467
}

src/Prefs/DateFormatPrefs.php

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* Copyright 2026 The Horde Project (http://www.horde.org/)
7+
*
8+
* See the enclosed file LICENSE for license information (LGPL). If you
9+
* did not receive this file, see http://www.horde.org/licenses/lgpl21.
10+
*
11+
* @category Horde
12+
* @copyright 2026 The Horde Project
13+
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
14+
* @package Core
15+
*/
16+
17+
namespace Horde\Core\Prefs;
18+
19+
use Horde\Date\Format;
20+
use Horde_Prefs;
21+
use Psr\Log\LoggerInterface;
22+
use Throwable;
23+
24+
/**
25+
* Date format preferences decorator
26+
*
27+
* Wraps date format preference access with automatic strftime-to-ICU
28+
* conversion. On first access of a legacy strftime value:
29+
* 1. Detects strftime format
30+
* 2. Logs the conversion
31+
* 3. Converts to ICU pattern
32+
* 4. Writes back the converted value (failsafe, noop if impossible)
33+
* 5. Returns the ICU pattern
34+
*
35+
* @category Horde
36+
* @copyright 2026 The Horde Project
37+
* @license http://www.horde.org/licenses/lgpl21 LGPL 2.1
38+
* @package Core
39+
*/
40+
class DateFormatPrefs
41+
{
42+
private const DATE_FORMAT_KEYS = [
43+
'date_format',
44+
'date_format_mini',
45+
'time_format',
46+
'time_format_mini',
47+
];
48+
49+
public function __construct(
50+
private Horde_Prefs $prefs,
51+
private string $locale = 'en_US',
52+
private ?LoggerInterface $logger = null,
53+
) {}
54+
55+
/**
56+
* Get a date format preference as an ICU pattern.
57+
*
58+
* If the stored value is strftime, converts it to ICU, writes back
59+
* the converted value (failsafe), logs the conversion, and returns
60+
* the ICU pattern. If already ICU, returns as-is.
61+
*/
62+
public function getDateFormat(string $key): string
63+
{
64+
$value = $this->prefs->getValue($key);
65+
if ($value === null) {
66+
return '';
67+
}
68+
69+
if (!Format::isStrftimeFormat($value)) {
70+
return $value;
71+
}
72+
73+
$icu = Format::strftimeToIcu($value, $this->locale);
74+
75+
$this->logger?->notice(
76+
"DateFormatPrefs: converting legacy strftime pref '{$key}': '{$value}' → '{$icu}'"
77+
);
78+
79+
if (!$this->prefs->isLocked($key)) {
80+
try {
81+
$this->prefs->setValue($key, $icu);
82+
} catch (Throwable) {
83+
// Noop — read-only backend or other error
84+
}
85+
}
86+
87+
return $icu;
88+
}
89+
90+
/**
91+
* Get the short time format ICU pattern based on twentyFour pref.
92+
*
93+
* Replaces the common pattern:
94+
* $prefs->getValue('twentyFour') ? '%R' : '%I:%M%p'
95+
*/
96+
public function getTimeFormatShort(): string
97+
{
98+
return $this->prefs->getValue('twentyFour') ? 'HH:mm' : 'h:mm a';
99+
}
100+
101+
/**
102+
* Get the time format with seconds based on twentyFour pref.
103+
*
104+
* Replaces the common pattern:
105+
* $prefs->getValue('twentyFour') ? '%H:%M:%S' : '%I:%M:%S %p'
106+
*/
107+
public function getTimeFormatFull(): string
108+
{
109+
return $this->prefs->getValue('twentyFour') ? 'HH:mm:ss' : 'h:mm:ss a';
110+
}
111+
112+
/**
113+
* Check if a given pref key is a date format key handled by this class.
114+
*/
115+
public function isDateFormatKey(string $key): bool
116+
{
117+
return in_array($key, self::DATE_FORMAT_KEYS, true);
118+
}
119+
}

src/Prefs/StrftimeFinding.php

Lines changed: 2 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -52,37 +52,21 @@ public function __construct(
5252
public readonly string $field,
5353
public readonly string $location,
5454
public readonly string $strftime,
55-
public readonly string|array $icu,
55+
public readonly string $icu,
5656
public readonly string $confidence,
5757
) {
5858
if (!in_array($confidence, [self::CONFIDENCE_HIGH, self::CONFIDENCE_MEDIUM, self::CONFIDENCE_LOW], true)) {
5959
throw new InvalidArgumentException("Invalid confidence level: $confidence");
6060
}
6161
}
6262

63-
/**
64-
* Check if ICU pattern is locale-specific
65-
*
66-
* @return bool True if pattern depends on user locale
67-
*/
68-
public function isLocaleSpecific(): bool
69-
{
70-
return is_array($this->icu);
71-
}
72-
7363
/**
7464
* Get ICU pattern as string
7565
*
76-
* For locale-specific patterns, returns a description.
77-
* For concrete patterns, returns the pattern itself.
78-
*
79-
* @return string ICU pattern or description
66+
* @return string ICU pattern
8067
*/
8168
public function getIcuString(): string
8269
{
83-
if (is_array($this->icu)) {
84-
return '[locale-specific: ' . implode(', ', array_keys($this->icu)) . ']';
85-
}
8670
return $this->icu;
8771
}
8872

0 commit comments

Comments
 (0)