feat(integration-xwiki): XwikiProvider — external, OpenConnector-backed (leaf, backend)#1493
Conversation
…ed (leaf, backend) The PHP half of the `integration-xwiki` leaf (issue #1326, depends_on: pluggable-integration-registry) — the worked external-storage example. `XwikiProvider` declares storage `external`, group `external`, OpenConnector source `xwiki`, and delegates all CRUD to `ExternalIntegrationRouter` (AD-4) — it carries no HTTP client and no credentials; those live on the OpenConnector source (HTTP Basic or OAuth2, customer-dependent — AD-15). Per the leaf design.md: - AD-1: `get()` round-trips the page's rendered HTML under `content`; the @conduction/nextcloud-vue widget strips it to text + ~500 chars for the detail-page preview — macros are never executed in NC. - AD-2: `create()` passes `reference` through (a full XWiki URL or a `Space.Page` path); the OpenConnector source resolves both to a canonical reference. - AD-3: `normalizeRow()` ensures every row carries a `breadcrumb` (from the source, or derived from space + title) so the UI can disambiguate same-titled pages in different spaces. Files: - lib/Service/Integration/Providers/XwikiProvider.php (new) — metadata getters + authRequirements (type: external, configuredVia: openconnector) + isEnabled (mirrors IAppManager::isInstalled 'openconnector') + list/get/create/update/delete (all via router->call with the {register, schema, object} context) + health (defers to router->probe) + normalizeList/normalizeRow shaping - lib/AppInfo/Application.php — DI service registration for XwikiProvider + add it to bootBuiltinIntegrationProviders()'s list so it self-registers with the IntegrationRegistry at boot - docs/Integrations/xwiki-openconnector-source.yaml (new) — the OpenConnector source template to import (the repo's config/ dir is gitignored, so it lives under docs/Integrations/) - docs/Integrations/xwiki.md (new) — user-facing setup + usage guide - tests/Unit/Service/Integration/Providers/XwikiProviderTest.php (new) — 10 tests, 38 assertions: metadata matches the leaf spec, authRequirements shape, isEnabled mirrors OpenConnector install, list/get/create/update/delete delegate with the right method + path + context and normalise rows (breadcrumb fallback, id/title/ space/page/url/content mapping, bare-array response), health defers to probe Quality (verified in the nextcloud container): - composer phpcs (phpcs.xml) — 0 errors, 0 warnings - composer phpmd (phpmd.xml) — clean - composer psalm — no errors - phpunit (phpunit-unit.xml) XwikiProviderTest — 10 passed, 38 assertions - php -l Application.php — no syntax errors
…+ spec deviations
Syncs the integration-xwiki leaf tasks.md to reality:
- backend (XwikiProvider + DI registration + source-config template
+ user guide + unit tests) — done in this branch
- frontend (CnXwikiTab + CnXwikiCard + registration + tests) — done
in ConductionNL/nextcloud-vue#213
- acceptance: link-by-URL / reference-property / "macros not
executed in preview" are covered by the component tests; the live
E2E (against a running XWiki via an OpenConnector source) is
deferred until the umbrella + leaf PRs merge and deploy
Spec deviations recorded inline:
- `requiredApp` is `'openconnector'` (not `null`) — more accurate
for the admin UI: the integration genuinely needs that app
- the OpenConnector source-config template lives at
`docs/Integrations/xwiki-openconnector-source.yaml`, not
`config/openconnector-sources/xwiki.yaml` (the repo's `config/`
dir is gitignored)
- the provider is registered via `addProvider()` at boot (not a DI
tag) — consistent with the umbrella's registration mechanism
- `check-integration-parity.js` (Node, repo convention) not `.sh`
…ve XWiki REST verification
Spun up xwiki:lts 17.10 locally, created a test page via REST
(PUT /rest/wikis/xwiki/spaces/Sandbox/pages/IntegrationTest) and
inspected the page representation. Findings folded into the source
template:
- XWiki is deployed at the ROOT context here — REST is at `/rest/`,
not `/xwiki/rest/`. (The template's `location` is the wiki's REST
base URL, so this is just a deployment note.)
- The REST page representation's `content` field is RAW xwiki/2.1
syntax, NOT rendered HTML. For a clean AD-1 text preview the
source should fetch the rendered body from
`GET /bin/get/{Space}/{Page}?xpage=plain` (XWiki renders macros
server-side in its own sandbox — never inside Nextcloud) and map
that to `content`. Mapping `content` straight from the REST field
is still safe (macro markup is inert text) — documented both ways.
- XWiki exposes a native breadcrumb at `hierarchy.items[].label`
("xwiki" / "Sandbox" / "IntegrationTest") — map `breadcrumb` from
it (AD-3). Without it XwikiProvider derives a coarse one from
space + title.
- Confirmed the other mappings: `id` → `reference`
("xwiki:Space.Page"), `title`, `space`, `name` → `page`,
`xwikiAbsoluteUrl` → `url`.
No PHP change needed — XwikiProvider::normalizeRow() already reads
`breadcrumb` (with the space+title fallback) and `content` (with the
`renderedContent` fallback), so it works against either mapping.
…s-built implementation
Brings the leaf's design.md and proposal.md in line with what
actually shipped (tasks.md was already synced):
- design.md: AD-1 records the verified XWiki REST shape (the REST
`content` field is raw xwiki/2.1 syntax — the rendered body comes
from `/bin/get/{Space}/{Page}?xpage=plain`, macros run server-side
in XWiki's sandbox, never in NC). AD-3 records that `breadcrumb`
maps from XWiki's native `hierarchy.items[].label`. "Files
Affected" updated to the real paths/structure. New "Implementation
deviations" table (requiredApp `openconnector` not `null`; source
template under `docs/Integrations/` not `config/`; `addProvider()`
at boot not a DI tag; `check-integration-parity.js` not `.sh`).
Softened the "established by integration-openproject" line — the
external-storage machinery comes from the umbrella, this is the
first external leaf.
- proposal.md: `Required NC app` → `openconnector` (with the
rationale); acceptance criteria checked off against the component/
unit tests that cover them, with the live runtime check called out
as deploy-gated.
Doc-only; no code change.
… / searchResults)
normalizeList() previously only un-wrapped `{ results: [...] }` / `{ items: [...] }`
(a purpose-built OpenConnector source) and otherwise iterated the response's *values*
as if they were rows — which mangles XWiki's own REST shapes. Now it also recognises
`pageSummaries` (space listing: GET /rest/wikis/<wiki>/spaces/<Space>/pages) and
`searchResults` (search), uses a bare list as-is, and returns an empty list for an
assoc envelope with no rows key (instead of garbage rows). normalizeRow() gains a
last-resort `url` fallback that pulls the `rel=".../rel/page"` href out of XWiki's
`links` array (present on page summaries / search hits) when the source didn't map a
flat URL field. This lets the `xwiki` integration work against an OpenConnector source
pointed straight at XWiki's REST API, not just a custom proxy endpoint.
Tests: +2 (raw pageSummaries envelope incl. the links-based url fallback; assoc
envelope with no rows key → []).
Relates to #1307 (umbrella) / #1326 (xwiki leaf).
| $rows = $response; | ||
| if (isset($response['results']) === true && is_array($response['results']) === true) { | ||
| $rows = $response['results']; | ||
| } else if (isset($response['items']) === true && is_array($response['items']) === true) { |
There was a problem hiding this comment.
[BLOCKER] else if must be elseif — phpcs CI failure
The normalizeList method uses } else if ( which violates Squiz.ControlStructures.ElseIfDeclaration enforced in phpcs.xml. This is the direct cause of the PHP Quality phpcs CI failure. Fix: change } else if ( to } elseif (.
| $reference = (string) ($row['reference'] ?? $row['pageReference'] ?? $row['id'] ?? ''); | ||
| $space = (string) ($row['space'] ?? $row['spaceName'] ?? ''); | ||
| $page = (string) ($row['page'] ?? $row['pageName'] ?? $row['name'] ?? ''); | ||
| $title = (string) ($row['title'] ?? $page ?? $reference); |
There was a problem hiding this comment.
[BLOCKER] Title fallback chain is broken — $reference can never be used
$page is assigned as (string)(... ?? '') — it is always a string, never null. The subsequent $title = (string)($row['title'] ?? $page ?? $reference) means $reference is dead code: if $row['title'] is absent, PHP coerces the empty string $page rather than falling through to $reference. When both $row['title'] and $row['page']/$row['name'] are missing, the title silently becomes '' instead of the canonical reference. Fix: change $page = (string)(... ?? '') to $page = (... ?? null) to preserve the null-fallthrough.
There was a problem hiding this comment.
[CONCERN] No test covering ProviderUnavailableException propagation
The provider contract explicitly states that ProviderUnavailableException bubbles from the router to the controller. The test suite has 10 tests covering the happy path but zero tests where the router mock throws ProviderUnavailableException. At minimum list() and get() should have error-path tests asserting the exception propagates unmodified.
| * the `@conduction/nextcloud-vue` xwiki tab + card expect. Unknown | ||
| * keys on the source row are preserved so the source can pass | ||
| * extra fields through without a provider change. | ||
| * |
There was a problem hiding this comment.
[CONCERN] normalizeList has no fallback for 'data' or 'pages' wrapper keys
The method handles results and items but not data (used by many REST frameworks) or pages (a natural key for a paged XWiki endpoint). If OpenConnector normalises the XWiki REST response into { data: [...] } the code falls through to iterating the top-level map and returns [] instead of the expected list. A 'data' branch or a comment explicitly documenting why data is not supported would clarify intent.
| if (is_array($breadcrumb) === false || $breadcrumb === []) { | ||
| // Best-effort breadcrumb from the reference if the source | ||
| // didn't supply one — "Space / Page" or just the reference. | ||
| $breadcrumb = array_values( |
There was a problem hiding this comment.
[CONCERN] Breadcrumb fallback code has inconsistent indentation — likely triggers additional phpcs violations
The nested array_values(array_filter(array_merge(...))) block has misaligned opening and closing parentheses that do not follow the 4-space indent rule. This may generate additional phpcs violations beyond the elseif issue.
There was a problem hiding this comment.
[CONCERN] XwikiProvider registered in bootBuiltinIntegrationProviders — misleading coupling for a leaf provider
bootBuiltinIntegrationProviders() is named for built-in providers that ship unconditionally. XwikiProvider is an external leaf that requires OpenConnector and a running XWiki. A separate bootLeafIntegrationProviders() or at minimum a descriptive comment block separating the sections would make the coupling explicit.
There was a problem hiding this comment.
[CONCERN] healthCheck targets the root wiki URL — no dedicated probe
The healthCheck.path is "" (the source base URL), which is also the implicit target for list calls. A 404 from a misconfigured location is indistinguishable from a network outage. XWiki REST exposes /rest/ as a lighter meta endpoint that is more appropriate as a health probe.
WilcoLouwerse
left a comment
There was a problem hiding this comment.
Review
🔴 Blockers (2)
- else if must be elseif — phpcs CI failure —
lib/Service/Integration/Providers/XwikiProvider.php:377 - Title fallback chain is broken — $reference can never be used —
lib/Service/Integration/Providers/XwikiProvider.php:407
🟡 Concerns (5)
- No test covering ProviderUnavailableException propagation —
tests/Unit/Service/Integration/Providers/XwikiProviderTest.php:798 - normalizeList has no fallback for 'data' or 'pages' wrapper keys —
lib/Service/Integration/Providers/XwikiProvider.php:397 - Breadcrumb fallback code has inconsistent indentation — likely triggers additional phpcs violations —
lib/Service/Integration/Providers/XwikiProvider.php:413 - XwikiProvider registered in bootBuiltinIntegrationProviders — misleading coupling for a leaf provider —
lib/AppInfo/Application.php:1222 - healthCheck targets the root wiki URL — no dedicated probe —
docs/Integrations/xwiki-openconnector-source.yaml:153
🟢 Minor (3)
- get() passes an empty $filters array to contextQuery unnecessarily (
lib/Service/Integration/Providers/XwikiProvider.php:473)
get()calls$this->contextQuery(..., filters: [])with a hardcoded empty array. LetcontextQuerydefault$filtersto[]and omit the explicit argument, consistent with thedelete()call. - Class is in Providers\ namespace but registered alongside BuiltinProviders\ in Application.php (
lib/Service/Integration/Providers/XwikiProvider.php:284)
The provider lives in…\Integration\Providers\while other providers live in…\BuiltinProviders\. A one-line comment in the class docblock noting the leaf namespace distinction would prevent confusion for future contributors. - File-level @author email differs from Application.php canonical address (
lib/Service/Integration/Providers/XwikiProvider.php:1)
The file docblock uses<info@conduction.nl>while other files use<dev@conductio.nl>(note:conductio.nl, notconduction.nl). Align with the repo-wide convention.
Reviewed by WilcoLouwerse via automated batch review.
…t: application/json
Three follow-ups that make the `xwiki` provider work against a stock
OpenConnector source (verified end-to-end against a live XWiki):
- ExternalIntegrationRouter::decodeResponse() — OpenConnector's CallService
returns a CallLog whose getResponse() is { statusCode, headers, body, encoding };
the upstream payload is the (usually JSON) `body` string. Unwrap + JSON-decode
it (base64 bodies decoded first) instead of serialising the whole CallLog —
previously the provider only ever saw the CallLog metadata, so list() returned [].
- ExternalIntegrationRouter::assertUpstreamOk() — a >= 400 upstream status (carried
on the CallLog) now throws ProviderUnavailableException rather than letting an
error page leak through as rows; 401/403 → the new CAUSE_PROVIDER_AUTH so the
UI shows the reconnect banner.
- XwikiProvider — every call now carries `Accept: application/json` (XWiki's REST
API negotiates XML by default; writes also declare a JSON Content-Type). A
purpose-built source that already pins these is unaffected.
- ProviderUnavailableException::CAUSE_PROVIDER_AUTH constant added (the JS side —
CnXwikiTab.degradedMessage — already treats `provider-auth` as a reconnect cause).
Tests: ExternalIntegrationRouterTest +4 (CallLog body unwrap, base64 body, 401→provider-auth,
500→upstream-down); XwikiProviderTest assertions updated for the new request headers.
phpunit: 21 green.
Relates to #1307 (umbrella) / #1326 (xwiki leaf) / ConductionNL/openconnector#755.
- normalizeRow: fix title fallback chain — $page was cast to string so `?? $reference` was unreachable. Use elvis so we land on whichever piece carries real content (title → page → reference). - normalizeRow: re-indent breadcrumb array_values/filter/merge call per phpcs PSR-2. - header: add SPDX-License-Identifier + SPDX-FileCopyrightText inside the file docblock per project convention (feedback_spdx-in-docblock.md). Review: WilcoLouwerse on PR #1493.
The PHP half of the
integration-xwikileaf — issue #1326,depends_on: pluggable-integration-registry. The worked external-storage example for the pluggable integration registry. Stacked on #1490 (docs/scaffold/tracking) → #1475 (admin UI) → … →development. The frontend half is ConductionNL/nextcloud-vue#213.Summary
XwikiProviderdeclares storageexternal, groupexternal, OpenConnector sourcexwiki, and delegates all CRUD toExternalIntegrationRouter(AD-4) — it carries no HTTP client and no credentials; those live on the OpenConnector source (HTTP Basic or OAuth2, customer-dependent — AD-15).Per the leaf design.md:
get()round-trips the page's rendered HTML undercontent; the frontend widget strips it to text + ~500 chars for the detail-page preview, macros are never executed in NC.create()passesreferencethrough (a full XWiki URL or aSpace.Pagepath); the OpenConnector source resolves both to a canonical reference.normalizeRow()ensures every row carries abreadcrumb(from the source, or derived from space + title) so the UI can disambiguate same-titled pages in different spaces.Files
lib/Service/Integration/Providers/XwikiProvider.php(new)lib/AppInfo/Application.php— DI service binding + added tobootBuiltinIntegrationProviders()'s list so it self-registers withIntegrationRegistryat bootdocs/Integrations/xwiki-openconnector-source.yaml(new) — the OpenConnector source template to import (the repo'sconfig/dir is gitignored, so it lives underdocs/Integrations/)docs/Integrations/xwiki.md(new) — user-facing setup + usage guidetests/Unit/Service/Integration/Providers/XwikiProviderTest.php(new) — 10 tests, 38 assertionsopenspec/changes/integration-xwiki/tasks.md— synced to realitySpec deviations
requiredAppis'openconnector'(notnullas the original tasks.md said) — more accurate for the admin UIdocs/Integrations/xwiki-openconnector-source.yaml, notconfig/openconnector-sources/xwiki.yaml(the repo'sconfig/dir is gitignored)addProvider()at boot (not a DI tag) — consistent with the umbrella's mechanismTest plan
composer phpcs(phpcs.xml) onXwikiProvider.php— 0 errors, 0 warnings (verified in the nextcloud container)composer phpmd(phpmd.xml) — cleancomposer psalm— no errorsphpunit --configuration phpunit-unit.xml tests/Unit/Service/Integration/Providers/XwikiProviderTest.php— 10 passed, 38 assertionsphp -l Application.php— no syntax errorsxwikisource configured against a running XWiki, the Articles tab/widget should populate and the admin Integrations page should show thexwikirow's health