[TikiWiki-commits] [Git][tikiwiki/tiki][master] [NEW] Wiki API: add PATCH endpoint for partial page updates (seo_title, ...
"Victor Emanouilov \(@kroky\) via TikiWiki-cvs" <[email protected]>
| Newsgroups | gmane.comp.cms.tiki.cvs |
|---|---|
| Message-ID | <69b2c705516f3_3b18a38070941@gitlab-sidekiq-low-urgency-cpu-bound-v2-6c8bd5d54b-tcjmr.mail> |
Victor Emanouilov pushed to branch master at Tiki Wiki CMS Groupware / Tiki
Commits:
6e5c1943 by Bernard Sfez at 2026-03-12T13:50:37+00:00
[NEW] Wiki API: add PATCH endpoint for partial page updates (seo_title,...
---
* [ENH] API: parse JSON request body in ApiBridge for all controllers
ApiBridge::handle() now detects application/json Content-Type and
merges the decoded JSON body into $request before creating the
JitFilter. This makes JSON fields available via $input in all API
controllers, removing the need for manual php://input parsing.
* [NEW] Wiki API: fix syntax error in tags array_map
* [NEW] Wiki API: fix phpcs spacing issues
* [NEW] Wiki API: fix phpcs spacing issues
* [NEW] Wiki API: add PATCH endpoint for partial page updates (seo_title, seo_description, categories, tags)
See merge request tikiwiki/tiki!9702
- - - - -
2 changed files:
- lib/core/Services/ApiBridge.php
- lib/core/Services/Wiki/Controller.php
Changes:
=====================================
lib/core/Services/ApiBridge.php
=====================================
@@ -38,6 +38,21 @@ class Services_ApiBridge
}
}
}
+ // Merge JSON request body when Content-Type is application/json.
+ // PHP only populates $_POST for form-urlencoded and multipart, not for JSON bodies.
+ // This makes JSON fields available via $input in all API controllers.
+ if (
+ isset($_SERVER['CONTENT_TYPE'])
+ && strpos($_SERVER['CONTENT_TYPE'], 'application/json') !== false
+ ) {
+ $rawBody = file_get_contents('php://input');
+ if (! empty($rawBody)) {
+ $jsonData = json_decode($rawBody, true);
+ if (is_array($jsonData)) {
+ $request = array_merge($request, $jsonData);
+ }
+ }
+ }
$this->jitRequest = new JitFilter($request);
if ($route['_route'] == 'home') {
$this->renderDocs();
@@ -175,6 +190,7 @@ class Services_ApiBridge
$routes->add('wiki-unlock', (new Route('wiki/unlock', ['controller' => 'wiki', 'action' => 'unlock_pages', 'confirmForm' => 'y']))->setMethods(['POST']));
$routes->add('wiki-zip', (new Route('wiki/zip', ['controller' => 'wiki', 'action' => 'zip', 'confirmForm' => 'y']))->setMethods(['POST']));
$routes->add('wiki-versions-delete', (new Route('wiki/page/{page}/delete', ['controller' => 'wiki', 'action' => 'remove_page_versions', 'confirmForm' => 'y']))->setMethods(['POST']));
+ $routes->add('wiki-patch', (new Route('wiki/page/{page}', ['controller' => 'wiki', 'action' => 'patch_page']))->setMethods(['PATCH']));
$routes->add('galleries', (new Route('galleries', ['controller' => 'file', 'action' => 'list_galleries', 'offset' => 0, 'maxRecords' => -1]))->setMethods(['GET']));
$routes->add('galleries-upload', (new Route('galleries/upload', ['controller' => 'file', 'action' => 'upload', 'upload' => 1]))->setMethods(['POST']));
$routes->add('galleries-download', (new Route('galleries/{fileId}/download', ['controller' => 'file', 'action' => 'download']))->setMethods(['GET']));
=====================================
lib/core/Services/Wiki/Controller.php
=====================================
@@ -962,6 +962,104 @@ class Services_Wiki_Controller
}
}
+ /**
+ * Partially updates a wiki page (SEO title, description, categories, tags)
+ * @param $input
+ * @return array
+ * @throws Services_Exception_NotFound
+ * @throws Services_Exception_Denied
+ */
+ public function action_patch_page($input)
+ {
+ global $user, $prefs;
+
+ $page = $input->page->pagename();
+ $tikilib = TikiLib::lib('tiki');
+ $info = $tikilib->get_page_info($page);
+
+ if (! $info) {
+ throw new Services_Exception_NotFound(tr('Page "%0" not found', $page));
+ }
+
+ $perms = Perms::get('wiki page', $page);
+ if (! $perms->edit) {
+ throw new Services_Exception_Denied();
+ }
+
+ $updated = [];
+
+ // Update SEO title (stored as a wiki page attribute)
+ // Uses JitFilter 'text' filter which applies StripTags — safe for unicode (ä, ö, ü, ß, etc.)
+ if (isset($input['seo_title'])) {
+ $seoTitle = $input->seo_title->text();
+ // Use mb_strlen for correct character count with multibyte characters (e.g. German, Japanese)
+ if (mb_strlen($seoTitle, 'UTF-8') > 160) {
+ throw new Services_Exception(tr('seo_title exceeds maximum length of 160 characters.'));
+ }
+ $attributelib = TikiLib::lib('attribute');
+ $attributelib->set_attribute('wiki page', $page, 'tiki.wiki.page_title', $seoTitle);
+ $updated[] = 'seo_title';
+ }
+
+ // Update SEO description (stored in tiki_pages.description)
+ // Uses JitFilter 'text' filter which applies StripTags — safe for unicode
+ if (isset($input['seo_description'])) {
+ $seoDesc = $input->seo_description->text();
+ // Use mb_strlen for correct character count with multibyte characters
+ if (mb_strlen($seoDesc, 'UTF-8') > 200) {
+ throw new Services_Exception(tr('seo_description exceeds maximum length of 200 characters.'));
+ }
+ $tikilib->update_page(
+ $page,
+ $info['data'],
+ 'API PATCH update',
+ $user,
+ $tikilib->get_ip_address(),
+ $seoDesc,
+ 1
+ );
+ $updated[] = 'seo_description';
+ }
+
+ // Update categories (full replace of assigned categories)
+ if (isset($input['categories'])) {
+ if ($prefs['feature_categories'] === 'y') {
+ $categlib = TikiLib::lib('categ');
+ $categlib->update_object_categories(
+ $input->asArray('categories'),
+ $page,
+ 'wiki page',
+ $info['description'],
+ $page,
+ 'tiki-index.php?page=' . urlencode($page)
+ );
+ $updated[] = 'categories';
+ }
+ }
+
+ // Update tags / freetags (full replace)
+ // Multi-word tags are auto-quoted so freetaglib handles them as single tags
+ if (isset($input['tags'])) {
+ global $tiki_p_freetags_tag;
+ if ($prefs['feature_freetags'] === 'y' && $tiki_p_freetags_tag === 'y') {
+ $freetaglib = TikiLib::lib('freetag');
+ $freetaglib->add_object('wiki page', $page, false, $info['description'], $page, 'tiki-index.php?page=' . urlencode($page));
+ $tagParts = array_map(function ($tag) {
+ return strpos($tag, ' ') !== false ? '"' . $tag . '"' : $tag;
+ }, $input->asArray('tags'));
+ $tagString = implode(' ', $tagParts);
+ $freetaglib->update_tags($user, $page, 'wiki page', $tagString, false, $info['lang']);
+ $updated[] = 'tags';
+ }
+ }
+
+ return [
+ 'status' => 'success',
+ 'page' => $page,
+ 'updated_fields' => $updated,
+ ];
+ }
+
/**
* Perform a plugin execution with specific input data (e.g. for ListExecute plugin)
*/
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/6e5c1943ddc9bd15304dec5403a0516da228f63d
--
View it on GitLab: https://gitlab.com/tikiwiki/tiki/-/commit/6e5c1943ddc9bd15304dec5403a0516da228f63d
You're receiving this email because of your account on gitlab.com. Manage all notifications: https://gitlab.com/-/profile/notifications | Help: https://gitlab.com/help
_______________________________________________
TikiWiki-cvs mailing list
[email protected]
https://lists.sourceforge.net/lists/listinfo/tikiwiki-cvs