diff --git a/.env b/.env index 738f94d2..377beeb4 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 93a1416c..88a3626c 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/Plugins/DavisIMipPlugin.php b/src/Plugins/DavisIMipPlugin.php index 0b0be0ee..66280f70 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/src/Services/BirthdayService.php b/src/Services/BirthdayService.php index 108c3e3c..36530e51 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 48d6c280..f3a2e233 100644 --- a/templates/_partials/delegate_row.html.twig +++ b/templates/_partials/delegate_row.html.twig @@ -21,6 +21,6 @@
{{ "users.username"|trans }} : {{ delegate.username }}
{{ delegate.uri }}
diff --git a/templates/calendars/index.html.twig b/templates/calendars/index.html.twig
index 1de0a5c3..97cf6448 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 }}
diff --git a/tests/Functional/Plugins/ImipPluginTest.php b/tests/Functional/Plugins/ImipPluginTest.php
index 5811ec20..106372e7 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 = [];