The invitee input had no combobox semantics: assistive
technology could not tell that the widget owns a listbox with
suggestions. Mark the input as role="combobox" with
aria-autocomplete="list" and an initial aria-expanded="false",
point aria-controls at the list and give the JS-built list a
matching id so the reference resolves.
The server splits the attendee input on whitespace, commas and
semicolons, but the search widget only treated commas and
semicolons as separators. Typing "a@x.com b" therefore sent the
whole string as the search query instead of "b", and picking a
contact dropped the whitespace-separated entry.
Split on /[\s;,]+/ in both fragment() and commit() so the
widget agrees with parseAttendees() on what an entry is.
hide() did not invalidate in-flight search requests. Clearing
the input or picking a contact while a request was pending let
the old response re-render and pop the suggestion list back
open. Bump requestSeq in hide() so every dismissal retires
pending responses.
Add a Vitest + jsdom harness with black-box tests for the
invitee widget, covering the stale-response race both on
field clear and on contact pick.
Since the event organizer is the calendar owner's email
address and the link parameter is display information,
the README explains both roles and the CalDAV
scheduling constraint, including the profile email
requirement for working invitations.
Security audit found two gaps. Sabre only quotes ICS
parameter values containing space, newline, quote, colon,
semicolon, caret, comma or plus, so a bare carriage
return in a contact name would be written into the
serialized event unquoted and break the line structure;
names with control characters are now dropped. The raw
attendee input and the search query had no length
bound, a huge POST field was fully split, iterated and
echoed back on re-render, a cheap CPU sink; both are
now capped, the query at the longest possible email
fragment.
The ICS organizer was taken from the external link's
organizer parameter. The CalDAV backend only schedules
invitations when the organizer matches the calendar
owner, so invites never went out under that value. The
event now names the authenticated user's email address;
the link organizer remains the fallback for users
without a profile email and stays visible on the
confirm page as meeting information.
Nextcloud checks the CSRF token on every controller method
without NoCSRFRequired, GET included. The invitee input's
bare fetch sends no request token, so the search answered
412 Precondition Failed. The endpoint is a read-only
address book query without side effects, so the attribute
is safe here.
The static datalist only carried the first 50 address book
contacts, so typed fragments could not match anything beyond
that. A new read-only GET /contacts/search endpoint queries
the full address book per keystroke, and a small vanilla JS
combobox shows the results and replaces only the fragment
after the last separator, so comma-separated lists keep
working. The server-rendered datalist remains as the no-JS
fallback; the JS removes the list attribute at runtime so
the native popup does not double the custom one.