Skip to content

Public API availability check compares weekday string against integer days, rejecting all bookings #32

Description

@Rikdekker

Environment:

  • RoomVox 1.4.0
  • PHP 8.2+ (any supported version)

Summary

PublicApiController compares the weekday as a string ("mon") against availabilityRules.rules[].days, which is stored as an array of integers (06, 0 = Sunday). The comparison can never match on PHP 8, so for any room with availability rules enabled the Public API treats every moment as outside the allowed hours.

The CalDAV/iTIP path is not affected — it does this correctly — so bookings made through a normal calendar client behave as expected. This is why the bug is easy to miss.

Steps to reproduce

  1. Create a room and enable Restrict booking hours, e.g. the built-in preset Weekdays 08–18 (stores days: [1,2,3,4,5])
  2. Create an API token with scope book
  3. On a Tuesday at 10:00, POST /api/v1/rooms/{id}/bookings with a valid title, start and end well inside the window
  4. Also call GET /api/v1/rooms/{id}/status

Expected: booking is created (201); status reports free/busy.

Actual: booking is rejected with 422 {"error":"Booking is outside available hours"}; status always reports unavailable.

Rooms without availability rules are unaffected, since the whole block is skipped.

Cause

The data model stores weekdays as integers. From src/views/RoomEditor.vue:513-521:

const weekDays = computed(() => [
    { value: 1, label: t('roomvox', 'Mon') },
    ...
    { value: 0, label: t('roomvox', 'Sun') },
])

and the presets at src/views/RoomEditor.vue:221 pass applyPreset([1,2,3,4,5], '08:00', '18:00').

SchedulingPlugin::bookingFitsRule() reads this correctly — lib/Dav/SchedulingPlugin.php:817:

// Get day of week (0=Sunday, 6=Saturday) matching our data model
$startDay = (int)$start->format('w');
...
return in_array($startDay, $allowedDays, true)

PublicApiController instead formats the day as a lowercase abbreviation and compares it loosely against those integers, in three places:

1. roomStatus()lib/Controller/PublicApiController.php:76

$dayOfWeek = strtolower($now->format('D')); // mon, tue, etc.
...
if (in_array($dayOfWeek, $rule['days'] ?? []) && ...

2. roomAvailability()lib/Controller/PublicApiController.php:190

$dayOfWeek = strtolower($rangeStart->format('D'));
foreach ($room['availabilityRules']['rules'] ?? [] as $rule) {
    if (in_array($dayOfWeek, $rule['days'] ?? [])) {

3. createBooking()lib/Controller/PublicApiController.php:397

$dayOfWeek = strtolower($startDt->format('D'));
...
if (in_array($dayOfWeek, $rule['days'] ?? []) && ...
        return new JSONResponse(['error' => 'Booking is outside available hours'], 422);

Since PHP 8 changed string↔int comparison, "mon" == 1 is false (under PHP 7 semantics "mon" == 0 would have been true, which would have made Sunday spuriously match). Confirmed on PHP 8.5.4:

date: 2026-09-07 (Monday), rule days: [1,2,3,4,5]
PublicApiController: in_array("mon", [1,2,3,4,5])      = false
SchedulingPlugin:    in_array(1, [1,2,3,4,5], true)    = true

Impact

For rooms with availability rules enabled:

Endpoint Effect
POST /api/v1/rooms/{id}/bookings Always 422 — booking via the API is impossible
GET /api/v1/rooms/{id}/status Always reports unavailable
GET /api/v1/rooms/{id}/availability Falls through to the 00:0023:59 default, so the room's real opening hours are never reflected

This makes the Public API unusable for its documented purpose (displays, kiosks, digital signage) on exactly those rooms that have opening hours configured.

Suggested fix

Use (int)$dt->format('w') and strict in_array(..., true) in all three places, matching SchedulingPlugin. Given the logic is now duplicated in four locations with two different data interpretations, extracting a single shared helper (e.g. on RoomService or a small AvailabilityRules value object) and having both SchedulingPlugin and PublicApiController call it would prevent this class of drift.

Worth adding a regression test that feeds the same room + timestamp through both paths and asserts they agree.

Related observations

While verifying the above, two adjacent inconsistencies surfaced. Happy to split these into separate issues if preferred:

  1. Availability rules are not applied per occurrence. docs/features/availability-rules.md:51 states "A weekly meeting fails if any week falls outside the availability rules", but isWithinAvailability() is called once, on the master event's DTSTART/DTEND (lib/Dav/SchedulingPlugin.php:208). The booking-horizon check does handle recurrence (:720-781), and hasConflict() expands occurrences (lib/Service/CalDAVService.php:1000-1021) — only the availability rules do not. Either the docs or the code should be corrected.

  2. BookingApiController enforces no availability rules or horizon at all — it calls hasConflict() only (:100, :194, :233). Bookings created through the admin UI therefore bypass configured opening hours. This may well be intentional (admins may override), but it is not documented anywhere.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions