Skip to content

Fix/rest api openwebui integration - #262

Open
amirdhs wants to merge 5 commits into
masterfrom
fix/rest-api-openwebui-integration
Open

Fix/rest api openwebui integration#262
amirdhs wants to merge 5 commits into
masterfrom
fix/rest-api-openwebui-integration

Conversation

@amirdhs

@amirdhs amirdhs commented Jul 26, 2026

Copy link
Copy Markdown
Contributor

Fix REST/JSON collection filters: nresults, date ranges, sort order and a schema disclosure

Found while connecting the groupdav.php/openapi.json REST API to an LLM tool-calling client
(Open WebUI). Four independent bugs, all reproducible with plain curl. Two return HTTP 500 on
documented parameters, one discloses table schemas, and one makes it impossible to bound a response.

Each commit stands alone and can be dropped independently.

1. nresults was silently ignored without a sync-token

CalDAV::jsonIndex() computed the limit but only forwarded it inside the
if (isset($_GET['sync-token'])) branch, and addressbook, calendar and timesheet
additionally required $options['root']['name'] === 'sync-collection'. A REST client therefore had
no way to bound a collection listing and always received the entire collection serialized as
JsContact / JsEvent / JsTask objects.

GET /groupdav.php/addressbook/?nresults=5      before: 10 entries    after: 5 entries
GET /groupdav.php/addressbook/?sync-token=&nresults=5     unchanged: 5 + more-results

Both gates are relaxed. sync-collection semantics (more-results plus a new sync-token) are
unchanged, and its forced ascending order still applies.

2. addressbook and timesheet defaulted to oldest-first

Defaults were egw_addressbook.contact_id and egw_timesheet.ts_id, both ascending. Combined with
(1), a limited request returns the N oldest entries — a plausible-looking wrong answer rather than
a visible failure. Now contact_modified DESC / ts_modified DESC. infolog already used
info_datemodified DESC; calendar has no ordering support.

Shipped in the same commit as (1) deliberately: a row cap without a sensible order is worse than no
cap at all.

3. filters[start] / filters[end] returned HTTP 500 on infolog and timesheet

CalDAV::jsonIndex() converts these into a synthetic
['name' => 'time-range', 'attrs' => [...]] element under an integer key. The JSON branch of
infolog_groupdav::_report_filters() merged $options['filters'] verbatim into the column filter,
so the array became a column:

GET /groupdav.php/<user>/infolog/?filters[start]=2026-07-01&filters[end]=2026-08-01
-> 500  Invalid SQL: ... AND Array ...  Unknown column 'Array' in 'WHERE'

It now goes through the existing _time_range_filter() helper, as the CalDAV/XML branch already
did. Verified against real rows: a June window matches the tasks due in June, a July window matches
none, and a narrow window discriminates correctly.

Timesheet genuinely has no time-range support, so there the same element now raises a
JsParseException (422) with an explanatory message instead of building a bogus ts_0 column.
doc/openapi/timesheet.json documents this.

4. Unknown filter names disclosed the full table schema

Db::column_data_implode() embedded print_r($column_definitions) and the supplied values in the
InvalidSql exception message, and CalDAV::exception_handler() echoes that message to the client:

GET /groupdav.php/<user>/timesheet/?filters[start]=2026-07-01
-> 500, 3832 bytes containing every column of egw_timesheet with types and precisions

The arrays are still written to error_log() for debugging; only the exception message is now a
single line. The same request returns 76 bytes.

This is the only change in core framework code. It alters the exception message only — no
control flow — and fixes the disclosure once for every app rather than per handler.

Also included

  • calendar silently ignored filters[search] and filters[linked].
    calendar_groupdav::_report_filters() had no Api\CalDAV::isJSON() branch, unlike the other three
    handlers, so REST filters (a plain name => value array, not CalDAV XML elements) all fell through
    to unknown filter --> ignored, while doc/openapi/calendar.json advertised both. Added
    jsonReportFilters().
    filters[linked] uses query['egw_cal.cal_id'] rather than sql_filter, because
    calendar_bo::search() deliberately overwrites $params['sql_filter'] from its own second
    argument. calendar_bo::search() accepts only one "query", so a free-text filters[search] cannot
    be combined with filters[linked]; that combination now reports a clear error rather than dropping
    one silently.

  • filters[order] was unreachable on timesheet. Its column mapper rewrote order to
    ts_order, producing bug (4). Now passed through, restricted to an optionally table-qualified
    column plus an optional direction. so_sql::search() already sanitises order_by, so this only
    converts a silently mangled sort into an explicit 422.

  • doc/openapi/*.json corrections. These descriptions are what an LLM client actually sees, so
    wrong ones directly produce wrong requests:

    • filters[start] / filters[end] declared "type": "datetime", which is not a valid JSON
      Schema type
      . Clients that map OpenAPI parameters onto function schemas pass it through
      verbatim, so the parameter arrives unusable and date filters are never sent. Now
      "type": "string" with "format": "date".
    • nresults was described as "Limit number of responses (only for sync-collection)", actively
      discouraging clients from sending it.
    • filters[order] is now documented for the four apps whose handlers read it (not calendar).
    • The filters[end] example read filters[start]=2026-02-01.
    • Accept is no longer required: true — it is an in: header parameter, and tool executors that
      forward only path and query parameters force a value that is then discarded. It remains in
      the spec as optional documentation.

@amirdhs
amirdhs requested a review from ralfbecker July 26, 2026 12:58
amirdhs added 5 commits July 26, 2026 16:06
Db::column_data_implode() embedded print_r($column_definitions) and the
supplied values into the InvalidSql exception message. CalDAV::exception_handler()
echoes that message to the client, so any REST request with an unknown filter
name returned the complete table definition of the queried table.

Reproduced with GET /groupdav.php/<user>/timesheet/?filters[nosuchthing]=1
which answered 500 with the full egw_timesheet schema (3832 bytes).

The arrays are still logged via error_log() for debugging; only the exception
message is now a single line. The same request now returns 76 bytes.
…lter()

CalDAV::jsonIndex() converts filters[start] / filters[end] into a synthetic
['name' => 'time-range', 'attrs' => [...]] element stored under an integer key.
The JSON/REST branch of infolog_groupdav::_report_filters() merged
$options['filters'] verbatim into the column filter, so that array ended up as
a column and produced "AND Array" in the SQL:

  GET /groupdav.php/<user>/infolog/?filters[start]=2026-07-01&filters[end]=2026-08-01
  -> 500 Invalid SQL: ... AND Array ... Unknown column 'Array' in 'WHERE'

Integer-keyed time-range elements are now converted with the existing
_time_range_filter() helper, as the CalDAV/XML branch already did, and appended
as a SQL fragment. Verified against real rows: a June window matches the tasks
due in June, a July window matches none.
…ests

calendar_groupdav::_report_filters() had no Api\CalDAV::isJSON() branch, unlike
the addressbook, infolog and timesheet handlers. REST filters arrive as a plain
name => value array rather than as CalDAV XML filter elements, so every one of
them fell through to the "unknown filter --> ignored" default arm. filters[search]
and filters[linked] were therefore documented in doc/REST-CalDAV-CardDAV/Calendar.md
and advertised in doc/openapi/calendar.json, but silently returned unfiltered
results.

Added jsonReportFilters(), which maps:
- search  -> query, either a free-text string matched against cal_title,
             cal_description and cal_location, or an array of <db-column> => <value>
- linked  -> query['egw_cal.cal_id'] via Api\Link::get_links(), table-qualified
             because cal_id exists in egw_cal and egw_cal_user. sql_filter is not
             usable here: calendar_bo::search() deliberately overwrites
             $params['sql_filter'] from its own second argument.
- filters[start] / filters[end] keep flowing through the existing time-range case.

calendar_bo::search() accepts only one "query", so a free-text filters[search]
cannot be combined with filters[linked]; that combination now reports a clear
error instead of silently dropping one of them.

CalDAV/CardDAV XML request handling is unchanged.
…d SQL

filter2col_filter()'s default arm prefixes any unknown filter name with "ts_",
which turned two valid-looking requests into 500s that echoed the whole
egw_timesheet schema back to the client:

- filters[start] / filters[end]: CalDAV::jsonIndex() appends these as a synthetic
  ['name' => 'time-range', ...] element under an integer key. Timesheet has no
  time-range support, so the array became a "ts_0" column. Array- and
  integer-keyed filters now raise a JsParseException (422) explaining that
  timesheet does not support date-range filtering.

- filters[order]: became "ts_order" instead of reaching propfind_generator(),
  so the documented ordering filter was unusable. It is now passed through,
  restricted to an optionally table-qualified column name plus an optional
  ASC/DESC. so_sql::search() already sanitises order_by via $sanitize_order_by,
  so this only turns a silently mangled sort into an explicit 422.
The descriptions in doc/openapi/*.json are what an OpenAPI tool-calling client
hands to an LLM, so incorrect ones directly cause wrong requests.

- filters[start] / filters[end] in calendar.json and infolog.json declared
  "type": "datetime", which is not a valid JSON Schema type. Clients that map
  OpenAPI parameters onto function-call schemas copy the type verbatim, so the
  parameter arrived unusable and date filters were never sent. Now
  "type": "string" with "format": "date".

- nresults claimed to "Limit number of responses (only for sync-collection)".
  It now spells out the actual mechanism: send sync-token= together with
  nresults=N to get a bounded chunk plus more-results and a follow-up sync-token.

- Documented filters[order], which the addressbook, infolog, timesheet and
  tracker handlers read but which appeared in no description, including each
  app's real default ordering. Not added to calendar.json, whose handler has no
  ordering support.

- The filters[end] example read "filters[start]=2026-02-01".

- calendar states its implicit -100/+365 day default window, and timesheet
  states that date-range filtering is unsupported and answers 422.

- Accept is no longer "required": true. It is an in: header parameter, and tool
  executors that forward only path and query parameters force the model to emit
  a value that is then discarded. It stays in the spec as optional documentation.
@amirdhs
amirdhs force-pushed the fix/rest-api-openwebui-integration branch from a8e8338 to d907679 Compare July 26, 2026 14:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant