[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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.