From e81b69f63e8d2052b897066733fd13f552a6e57e Mon Sep 17 00:00:00 2001 From: tchapi Date: Tue, 29 Sep 2026 23:43:05 +0200 Subject: [PATCH 1/2] chore --- src/Plugins/DavisIMipPlugin.php | 20 +++++++++++- tests/Functional/Plugins/ImipPluginTest.php | 36 +++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/src/Plugins/DavisIMipPlugin.php b/src/Plugins/DavisIMipPlugin.php index 0b0be0e..66280f7 100644 --- a/src/Plugins/DavisIMipPlugin.php +++ b/src/Plugins/DavisIMipPlugin.php @@ -195,7 +195,9 @@ public function schedule(ITip\Message $itip) return $else; }; - $url = $notEmpty('URL', false); + // The invitation renders this as a link, and it is whatever the organiser's client put in + // VEVENT.URL. Anything but a web or mail address is dropped rather than made clickable. + $url = $this->linkableUrl($notEmpty('URL', false)); $description = $notEmpty('DESCRIPTION', false); $location = $notEmpty('LOCATION', false); $locationImageDataAsBase64 = false; @@ -325,6 +327,22 @@ public function schedule(ITip\Message $itip) } } + /** + * @param string|false $url + * + * @return string|false the url if it is one a mail client should offer to open, false otherwise + */ + private function linkableUrl($url) + { + if (!is_string($url) || '' === $url) { + return false; + } + + $scheme = parse_url($url, PHP_URL_SCHEME); + + return \in_array(strtolower((string) $scheme), ['http', 'https', 'mailto'], true) ? $url : false; + } + /** * Returns a bunch of meta-data about the plugin. * diff --git a/tests/Functional/Plugins/ImipPluginTest.php b/tests/Functional/Plugins/ImipPluginTest.php index 5811ec2..106372e 100644 --- a/tests/Functional/Plugins/ImipPluginTest.php +++ b/tests/Functional/Plugins/ImipPluginTest.php @@ -131,6 +131,42 @@ public function testANonMailtoRecipientIsLogged(): void $this->assertSame([], $mailer->sent); } + /** + * `VEVENT.URL` comes from whoever created the event and the invitation makes it clickable in + * the recipient's mail client, so only web and mail addresses get through. + * + * @dataProvider urls + */ + public function testOnlyLinkableUrlsReachTheInvitation(string $url, bool $expected): void + { + $logs = []; + $plugin = $this->plugin($mailer = $this->mailer(null), $logs); + + $message = $this->message(); + $message->message->VEVENT->add('URL', $url); + $plugin->schedule($message); + + $this->assertCount(1, $mailer->sent); + + // The templates render from this context, and both of them only emit a link when it is set + $this->assertSame( + $expected ? $url : false, + $mailer->sent[0]->getContext()['url'], + $url.($expected ? ' should be offered' : ' should not be offered') + ); + } + + public static function urls(): iterable + { + yield 'https' => ['https://example.org/meeting', true]; + yield 'http' => ['http://example.org/meeting', true]; + yield 'mailto' => ['mailto:someone@example.org', true]; + yield 'javascript' => ['javascript:alert(1)', false]; + yield 'data' => ['data:text/html,', false]; + yield 'file' => ['file:///etc/passwd', false]; + yield 'no scheme' => ['example.org/meeting', false]; + } + public function testASuccessfulSendIsLoggedAndReported(): void { $logs = []; From 65b62246ea360027a3e148dd06b09f32f3a1ee99 Mon Sep 17 00:00:00 2001 From: tchapi Date: Wed, 30 Sep 2026 00:03:30 +0200 Subject: [PATCH 2/2] misc fixes --- .env | 3 +++ README.md | 23 ++++++++++++++++++++++ src/Services/BirthdayService.php | 4 ---- templates/_partials/delegate_row.html.twig | 2 +- templates/calendars/index.html.twig | 4 ++-- 5 files changed, 29 insertions(+), 7 deletions(-) diff --git a/.env b/.env index 738f94d..377beeb 100644 --- a/.env +++ b/.env @@ -42,6 +42,9 @@ ADMIN_PASSWORD=test ADMIN_AUTH_BYPASS=false # Auth Realm for HTTP auth +# Do not change this on an existing install: passwords created before v1.10.0, and any imported +# from Baikal, are md5(username:AUTH_REALM:password), so changing the realm silently invalidates +# every one of them. See the README. AUTH_REALM=SabreDAV # Auth Method for the frontend diff --git a/README.md b/README.md index 93a1416..88a3626 100644 --- a/README.md +++ b/README.md @@ -137,6 +137,29 @@ AUTH_METHOD=Basic # can be "Basic", "IMAP" or "LDAP" ``` > See [the following paragraph](#specific-environment-variables-for-imap-and-ldap-authentication-methods) for more information if you choose either IMAP or LDAP. +> [!WARNING] +> +> **Do not change `AUTH_REALM` on an existing installation.** Davis stored passwords as +> `md5(username:AUTH_REALM:password)` until v1.10.0 (September 2021), and a database imported from +> Baïkal uses the same scheme. The realm is part of those hashes, so changing it makes every one of +> them stop working, with no error beyond a failed login — the accounts are still there, they simply +> cannot authenticate any more. +> +> Passwords set since v1.10.0 use bcrypt and are unaffected. You can still be carrying legacy ones +> without ever having used Baïkal, if the account predates that version. To find out: +> +> ```sql +> SELECT username FROM users WHERE digesta1 NOT LIKE '$2y$%'; +> ``` +> +> Any row returned is a legacy hash tied to the current realm. +> +> There is no password reset in Davis. If you must change the realm, the only way back is to open +> each of those accounts in the dashboard and type a new password: the field is blank on the edit +> page and leaving it blank keeps the current hash, so filling it in is what replaces it. The new +> one is stored with bcrypt and no longer depends on the realm. Your own admin login is unaffected — +> it comes from `ADMIN_LOGIN` / `ADMIN_PASSWORD`, not from the users table. + **d. The global flags to enable CalDAV, CardDAV and WebDAV**. You can also disable the option to have calendars public ```shell diff --git a/src/Services/BirthdayService.php b/src/Services/BirthdayService.php index 108c3e3..36530e5 100644 --- a/src/Services/BirthdayService.php +++ b/src/Services/BirthdayService.php @@ -232,10 +232,6 @@ public function buildDataFromContact(string $cardData): ?VCalendar $leapDay = (2 === (int) $dateParts['month'] && 29 === (int) $dateParts['date']); - if (null === $dateParts['year'] || $originalYear < 1970) { - $birthday = ($leapDay ? '1972-' : '1970-') - .$dateParts['month'].'-'.$dateParts['date']; - } if ($leapDay) { /* Sabre\VObject supports BYMONTHDAY only if BYMONTH diff --git a/templates/_partials/delegate_row.html.twig b/templates/_partials/delegate_row.html.twig index 48d6c28..f3a2e23 100644 --- a/templates/_partials/delegate_row.html.twig +++ b/templates/_partials/delegate_row.html.twig @@ -21,6 +21,6 @@

{{ "users.username"|trans }} : {{ delegate.username }}

{{ "users.uri"|trans }} : {{ delegate.uri }} diff --git a/templates/calendars/index.html.twig b/templates/calendars/index.html.twig index 1de0a5c..97cf644 100644 --- a/templates/calendars/index.html.twig +++ b/templates/calendars/index.html.twig @@ -41,7 +41,7 @@ data-bs-toggle="modal" data-bs-target="#deleteModal-calendars" data-href="{{ path('calendar_delete',{userId: userId, id: calendar.id})}}" data-flavour="calendars" - class="btn btn-sm btn-outline-danger ms-1 delete-modal" + class="btn btn-sm btn-outline-danger ms-1" >⚠ {{ "delete"|trans }} @@ -67,7 +67,7 @@ data-bs-toggle="modal" data-bs-target="#deleteModal-calendars" data-href="{{ path('calendar_delete',{userId: userId, id: calendar.id})}}" data-flavour="calendars" - class="btn btn-outline-danger delete-modal" + class="btn btn-outline-danger" >⚠ {{ "delete"|trans }}