File "WPNavigationController.php"

Full Path: /home/buyiwexj/public_html/wp-content/plugins/extendify/app/QuickEdit/Controllers/WPNavigationController.php
File size: 6.42 KB
MIME-type: text/x-php
Charset: utf-8

<?php

namespace Extendify\QuickEdit\Controllers;

defined('ABSPATH') || die('No direct access.');

use Extendify\Config;
use Extendify\QuickEdit\Schemas\Registry;
use Extendify\QuickEdit\Services\BlockFingerprint;

// Ref-based navigations (core/navigation with `ref`) keep their items in
// a separate wp_navigation post that SaveController's findBlock walk
// can't reach. This endpoint takes a (navPostId, itemIndex) addressed
// by NavRefTagger and applies the schema patch directly.
class WPNavigationController
{
    public static function init()
    {
        add_action('rest_api_init', [self::class, 'registerRoutes']);
    }

    public static function registerRoutes()
    {
        register_rest_route('extendify/v1', '/quick-edit/wp-navigation', [
            'methods'             => 'POST',
            'permission_callback' => [self::class, 'permissionCallback'],
            'callback'            => [self::class, 'handle'],
        ]);
    }

    public static function permissionCallback(): bool
    {
        return current_user_can(Config::$requiredCapability);
    }

    public static function handle(\WP_REST_Request $req)
    {
        $body = $req->get_json_params() ?: [];
        $navPostId = (int) ($body['navPostId'] ?? 0);
        $itemIndex = (int) ($body['itemIndex'] ?? -1);
        $blockType = (string) ($body['blockType'] ?? '');
        $patches = is_array($body['patches'] ?? null) ? $body['patches'] : [];

        if ($navPostId <= 0 || $itemIndex < 0 || $blockType === '' || !$patches) {
            return new \WP_REST_Response(
                ['error' => 'navPostId, itemIndex, blockType, patches required'],
                400
            );
        }
        if (!in_array($blockType, ['core/navigation-link', 'core/navigation-submenu'], true)) {
            return new \WP_REST_Response(
                ['error' => 'unsupported blockType for wp-navigation save'],
                400
            );
        }

        $navPost = get_post($navPostId);
        if (!$navPost || $navPost->post_type !== 'wp_navigation') {
            return new \WP_REST_Response(['error' => 'wp_navigation post not found'], 404);
        }
        if (!current_user_can('edit_post', $navPostId)) {
            return new \WP_REST_Response(['error' => 'forbidden for this navigation'], 403);
        }

        $schema = Registry::get($blockType);
        if (!$schema) {
            return new \WP_REST_Response(
                ['error' => 'no schema for blockType', 'blockType' => $blockType],
                400
            );
        }

        $blocks = parse_blocks($navPost->post_content);

        $found = self::findNthNavItem($blocks, $itemIndex);
        if ($found === null) {
            return new \WP_REST_Response(
                ['error' => 'nav item not found at index', 'itemIndex' => $itemIndex],
                404
            );
        }

        $target = $found['block'];
        if (($target['blockName'] ?? '') !== $blockType) {
            return new \WP_REST_Response([
                'error' => 'block type mismatch',
                'expected' => $blockType,
                'actual' => $target['blockName'] ?? null,
            ], 409);
        }

        // Positional itemIndex can address a different same-type item when the
        // render-time and parse-time orderings of a nested menu diverge; refuse
        // when the resolved item doesn't carry the clicked item's fingerprint.
        $fingerprint = is_array($body['fingerprint'] ?? null) ? $body['fingerprint'] : [];
        if ($fingerprint && !BlockFingerprint::matches($target, $fingerprint)) {
            return new \WP_REST_Response([
                'error'     => 'block fingerprint mismatch',
                'itemIndex' => $itemIndex,
            ], 409);
        }

        foreach ($patches as $patch) {
            if (!is_array($patch)) {
                continue;
            }
            $fieldKey = (string) ($patch['fieldKey'] ?? '');
            if ($fieldKey === '') {
                continue;
            }
            $target = $schema->apply($target, $fieldKey, $patch['value'] ?? null);
        }

        $blocks = self::replaceAtPath($blocks, $found['path'], $target);
        $newContent = serialize_blocks($blocks);

        $update = wp_update_post([
            'ID'           => $navPostId,
            'post_content' => wp_slash($newContent),
        ], true);
        if (is_wp_error($update)) {
            return new \WP_REST_Response(['error' => $update->get_error_message()], 500);
        }

        return new \WP_REST_Response(['ok' => true]);
    }

    // Pre-order, counting only navigation-link/-submenu so the index
    // matches NavRefTagger's render-time counter.
    private static function findNthNavItem(array $blocks, int $targetIndex)
    {
        $counter = 0;
        $found = null;

        $walk = function (array $list, array $pathSoFar) use (&$walk, &$counter, &$found, $targetIndex) {
            foreach ($list as $i => $block) {
                $name = $block['blockName'] ?? '';
                $isItem = $name === 'core/navigation-link' || $name === 'core/navigation-submenu';
                if ($isItem) {
                    if ($counter === $targetIndex) {
                        $found = ['block' => $block, 'path' => array_merge($pathSoFar, [$i])];
                        return;
                    }
                    $counter++;
                }
                if (!empty($block['innerBlocks'])) {
                    $walk($block['innerBlocks'], array_merge($pathSoFar, [$i, 'innerBlocks']));
                    if ($found !== null) {
                        return;
                    }
                }
            }
        };
        $walk($blocks, []);

        return $found;
    }

    // Mirrors SaveController::replaceBlockAtPath.
    private static function replaceAtPath(array $blocks, array $path, array $newBlock): array
    {
        if (empty($path)) {
            return $blocks;
        }
        $head = $path[0];
        $rest = array_slice($path, 1);
        if (!is_int($head) || !isset($blocks[$head])) {
            return $blocks;
        }
        if (empty($rest)) {
            $blocks[$head] = $newBlock;
            return $blocks;
        }
        if ($rest[0] === 'innerBlocks') {
            $blocks[$head]['innerBlocks'] = self::replaceAtPath(
                $blocks[$head]['innerBlocks'] ?? [],
                array_slice($rest, 1),
                $newBlock
            );
        }
        return $blocks;
    }
}