Skip to content

new API version to support CRUD operation on KB for admin users - #1948

Closed
denphi wants to merge 1 commit into
hubzero:2.4-mainfrom
denphi:feature/kb-api-v1.1-write
Closed

denphi wants to merge 1 commit into
hubzero:2.4-mainfrom
denphi:feature/kb-api-v1.1-write

Conversation

@denphi

@denphi denphi commented Oct 19, 2026

Copy link
Copy Markdown
Contributor

v1.0 of the knowledge base API is read-only. This adds a v1.1 controller that
keeps both of its tasks — with an ordinary caller's visibility unchanged: a
published article, in a published category, at an access level they hold — and
adds create, update and delete for articles and for the categories they sit in.

Writes are gated on core.admin, so a Super User only. That is stricter than
the admin UI, which needs core.manage on com_kb; the site has no authoring
interface for the knowledge base at all, so this holds the API to the stricter
of the two. A Super User also sees unpublished content, which they need in
order to manage it.

Routes

The component's existing router.php is unchanged. It hands a second path
segment to the task rather than to an id, so there is no /kb/categories/{id}
to route; the category is named by ?id= and the method picks the action.

Method URI Action
GET /v1.1/kb/list list articles
GET /v1.1/kb/{id} read an article
POST /v1.1/kb create an article
PUT /v1.1/kb/{id} update an article
DELETE /v1.1/kb/{id} delete an article
GET /v1.1/kb/categories list categories (?id= reads one)
POST /v1.1/kb/categories create a category
PUT /v1.1/kb/categories?id= update or move a category
DELETE /v1.1/kb/categories?id= delete a category

Bodies may be form-encoded or JSON. Updates apply only the fields the request
carries.

Notes for review

Category writes go through com_categories' own Nested model, since that is
what keeps the nested set, the materialised path the site routes on, and the
access asset in step. Three things fell out of that:

  • setNameSpace('com_kb') on every save. Without it the row's asset is named
    com_.category.N and never matches the com_kb.category.N the ACL checks
    ask for. The admin only does this when a Super User posts permission rules.
  • Nested::destroy() does not remove its descendants' assets — it builds each
    name from a namespace those blank models do not have — so deleteCategory()
    sweeps them by name afterwards.
  • An update only calls saveAsChildOf() when the parent actually changed.
    Calling it unconditionally, as the admin does, moves a category to the end of
    its siblings every time anyone edits it.

Alias uniqueness is enforced (within the category for articles, among siblings
for categories) because the site resolves both by alias, so a duplicate leaves
one row unreachable. COM_KB_ERROR_ALIAS_IN_USE already existed in the admin
strings but was never checked.

API-only strings live in a new api/language/en-GB/ rather than in the site's,
following com_blog and com_activity. That directory makes Lang::load('com_kb')
succeed in the API app, which short-circuits v1.0's fallback to the site file,
so it also carries COM_KB_ERROR_ARTICLE_NOT_FOUND and
COM_KB_ERROR_CATEGORY_NOT_FOUND. v1.0 is otherwise untouched and still
resolves them.

Testing

Exercised against a local hub: full CRUD for both types, nested creates, moves
with descendant path rebuilds, and the guards (409 on duplicate alias, on
deleting a category that still holds articles, and on reparenting a category
beneath itself; 422 on a missing title; 400 on a malformed JSON body; 404 on an
unknown category). The nested set stayed contiguous and other extensions'
categories were untouched; an article created through the API renders on the
site at its real URL and follows its category when moved. Guests and a
non-Super-User in the Administrator group get 403 on every write while still
listing what they may see. /api/v1.0/kb/list and unversioned /api/kb/list
are unaffected.

🤖 Generated with Claude Code

@nkissebe

Copy link
Copy Markdown
Contributor

Landed on 2.4-main via #1953, with the write operations folded into entriesv1_0.php rather than a v1.1 file (the loader would have made v1.1 the default for unversioned callers), plus the review fixes: nested-set-safe category delete, normalised alias on update, extension-wide alias check, route variables not taken from the JSON body, and field validation.

@nkissebe nkissebe closed this Oct 21, 2026
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.

2 participants