Even adding only 'custom-fields' to bricks_template's supports turned out
to break the Bricks builder ("Invalid post type" on Edit with Bricks for
all templates including pre-existing Header / Footer).
bricks_template is now left exactly as Bricks registers it natively :
supports : author, revisions, thumbnail, title
show_in_rest : false
The MCP server route to bricks_template was experimental and we don't use
it actively — direct PHP scripts (av-fix-template-meta, av-generate-*,
av-add-submenus) handle template management. Templates are exposed only
to the Bricks builder, not external REST clients.
case_study + scenario CPTs keep REST exposure (used for content type
generators later).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
312 lines
14 KiB
PHP
312 lines
14 KiB
PHP
<?php
|
|
/**
|
|
* Plugin Name: Asterion — Bricks REST Bridge
|
|
* Description: Exposes Bricks Builder data (templates CPT + page/post/CPT meta keys) over the WP REST API so the bricks-mcp-server (sabiertas) can read/write Bricks content. Idempotent — safe to keep installed.
|
|
*
|
|
* Why this is needed :
|
|
* - Bricks registers the bricks_template CPT WITHOUT show_in_rest, so /wp/v2/bricks_template returns 404.
|
|
* - The Bricks element JSON lives in post_meta keys (_bricks_page_content_2 etc.) that aren't exposed by default.
|
|
* - register_post_meta() with show_in_rest=true is the canonical way to expose them; meta is then read/written via /wp/v2/<post_type>/<id> with the `meta` field.
|
|
*
|
|
* Auth :
|
|
* - Read : public (REST default for show_in_rest)
|
|
* - Write : edit_posts capability (=> the WP Application Password user must have at least Editor role)
|
|
*/
|
|
|
|
defined('ABSPATH') || exit;
|
|
|
|
/* ----------------------------------------------------------------
|
|
1) Force show_in_rest=true on bricks_template + our CPTs.
|
|
---------------------------------------------------------------- */
|
|
add_filter('register_post_type_args', function ($args, $post_type) {
|
|
|
|
// bricks_template intentionally excluded : touching its supports/show_in_rest
|
|
// breaks Bricks's own builder (it expects the original registration verbatim).
|
|
// The MCP server doesn't need REST access to templates anyway — we use direct
|
|
// PHP scripts to manage them. Only case_study + scenario stay REST-exposed.
|
|
$needs_rest = ['case_study', 'scenario'];
|
|
|
|
if (! in_array($post_type, $needs_rest, true)) {
|
|
return $args;
|
|
}
|
|
|
|
$args['show_in_rest'] = true;
|
|
$args['rest_base'] = $post_type . 's';
|
|
$args['rest_controller_class'] = 'WP_REST_Posts_Controller';
|
|
|
|
// Only add 'custom-fields' so the meta field is exposed via REST.
|
|
$existing_supports = $args['supports'] ?? [];
|
|
if (! in_array('custom-fields', $existing_supports, true)) {
|
|
$existing_supports[] = 'custom-fields';
|
|
}
|
|
$args['supports'] = $existing_supports;
|
|
|
|
return $args;
|
|
}, 10, 2);
|
|
|
|
/* ----------------------------------------------------------------
|
|
2) Register Bricks meta keys for REST on every relevant post type.
|
|
---------------------------------------------------------------- */
|
|
add_action('init', function () {
|
|
|
|
// Post types that may carry Bricks content
|
|
$post_types = ['page', 'post', 'bricks_template', 'case_study', 'scenario'];
|
|
|
|
// Bricks meta keys we want exposed (read+write) over REST
|
|
// Keys come from wp-content/themes/bricks/functions.php
|
|
$bricks_meta = [
|
|
'_bricks_page_content_2' => 'string', // The element JSON (the main payload)
|
|
'_bricks_page_css_2' => 'string',
|
|
'_bricks_page_header_2' => 'string',
|
|
'_bricks_page_footer_2' => 'string',
|
|
'_bricks_page_settings' => 'string',
|
|
'_bricks_template_type' => 'string',
|
|
'_bricks_template_settings' => 'string',
|
|
];
|
|
|
|
$auth_cb = function () {
|
|
return current_user_can('edit_posts');
|
|
};
|
|
|
|
// Pass-through sanitize : keep the JSON string verbatim. Default sanitize for
|
|
// type=string strips slashes / escapes which corrupts our JSON payload.
|
|
$sanitize_passthrough = function ($value) { return $value; };
|
|
|
|
foreach ($post_types as $type) {
|
|
foreach ($bricks_meta as $meta_key => $meta_type) {
|
|
register_post_meta($type, $meta_key, [
|
|
'type' => $meta_type,
|
|
'single' => true,
|
|
'show_in_rest' => true,
|
|
'auth_callback' => $auth_cb,
|
|
'sanitize_callback' => $sanitize_passthrough,
|
|
]);
|
|
}
|
|
}
|
|
}, 30); // priority 30 — after both Bricks (10-25) and our CPTs registration (10)
|
|
|
|
/* ----------------------------------------------------------------
|
|
3) Permit also unknown _bricks_* meta keys (e.g. _bricks_snapshot_<ts>)
|
|
so MCP snapshots work without us pre-registering each key.
|
|
---------------------------------------------------------------- */
|
|
add_filter('is_protected_meta', function ($protected, $meta_key, $meta_type) {
|
|
if ($meta_type !== 'post') return $protected;
|
|
if (strpos($meta_key, '_bricks_snapshot_') === 0) {
|
|
return false; // expose snapshot meta in REST despite the leading underscore
|
|
}
|
|
return $protected;
|
|
}, 10, 3);
|
|
|
|
/* ----------------------------------------------------------------
|
|
4) AUTO-MIRROR : the bricks-mcp-server writes element JSON to
|
|
_bricks_page_content_2, but Bricks reads HEADER templates from
|
|
_bricks_page_header_2 and FOOTER templates from _bricks_page_footer_2.
|
|
Mirror the content into the correct key whenever a bricks_template
|
|
with type=header/footer is saved.
|
|
---------------------------------------------------------------- */
|
|
/* DUAL-FORMAT META READ : the MCP server writes Bricks element data as a JSON
|
|
string into post_meta. Bricks's renderer expects a PHP array. We keep the JSON
|
|
string format in DB (so REST reads it back fine for the MCP's Json.parse), and
|
|
decode on the fly when Bricks reads via get_post_meta (non-REST context).
|
|
Result : same value, two views — JSON for the MCP, array for the renderer. */
|
|
add_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10, 5);
|
|
|
|
function av_decode_bricks_meta_for_renderer($value, $post_id, $meta_key, $single, $meta_type) {
|
|
// Only post-type meta concerns us
|
|
if ($meta_type !== 'post') return $value;
|
|
|
|
// Only Bricks element content keys
|
|
$content_keys = ['_bricks_page_content_2', '_bricks_page_header_2', '_bricks_page_footer_2'];
|
|
if (! in_array($meta_key, $content_keys, true)) return $value;
|
|
|
|
// In REST context, leave the raw string for the MCP server
|
|
if (defined('REST_REQUEST') && REST_REQUEST) return $value;
|
|
|
|
// Bricks renderer context : fetch the raw stored value (bypassing this filter
|
|
// via remove_filter, then re-add) and decode JSON → array
|
|
remove_filter('get_post_metadata', __FUNCTION__, 10);
|
|
$raw = get_post_meta($post_id, $meta_key, $single);
|
|
add_filter('get_post_metadata', __FUNCTION__, 10, 5);
|
|
|
|
if (! is_string($raw)) return $value;
|
|
$trim = trim($raw);
|
|
if ($trim === '' || $trim === '[]') return $value;
|
|
if ($trim[0] !== '[' && $trim[0] !== '{') return $value;
|
|
|
|
$decoded = json_decode($raw, true);
|
|
if (! is_array($decoded)) return $value;
|
|
|
|
// WP's get_metadata_raw() convention for filter return values :
|
|
// - When filter returns array AND $single=true → WP returns $check[0]
|
|
// - When filter returns array AND $single=false → WP returns $check as-is
|
|
// Both cases need a list wrapper around our decoded value so WP's
|
|
// single-mode auto-indexing yields the decoded array (not the section).
|
|
return [$decoded];
|
|
}
|
|
|
|
add_action('updated_post_meta', 'av_mirror_bricks_template_content', 10, 4);
|
|
add_action('added_post_meta', 'av_mirror_bricks_template_content', 10, 4);
|
|
|
|
function av_mirror_bricks_template_content($meta_id, $post_id, $meta_key, $meta_value) {
|
|
// Trigger 1 : content updated → mirror to header/footer if type already known
|
|
// Trigger 2 : type updated → re-mirror existing content (fixes the order-of-writes
|
|
// on REST create_template which writes content before type)
|
|
if (! in_array($meta_key, ['_bricks_page_content_2', '_bricks_template_type'], true)) return;
|
|
if (get_post_type($post_id) !== 'bricks_template') return;
|
|
|
|
// Reuse the av_decode filter logic by reading via get_post_meta (which now
|
|
// returns array) when the value is JSON, or by direct decoding here.
|
|
if ($meta_key === '_bricks_page_content_2') {
|
|
$type = get_post_meta($post_id, '_bricks_template_type', true);
|
|
$content = is_string($meta_value) ? $meta_value : '';
|
|
} else {
|
|
// type just updated → fetch raw content (bypass our filter to get string)
|
|
$type = is_string($meta_value) ? $meta_value : '';
|
|
remove_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10);
|
|
$content = get_post_meta($post_id, '_bricks_page_content_2', true);
|
|
add_filter('get_post_metadata', 'av_decode_bricks_meta_for_renderer', 10, 5);
|
|
}
|
|
|
|
$mirror_key = match ($type) {
|
|
'header' => '_bricks_page_header_2',
|
|
'footer' => '_bricks_page_footer_2',
|
|
default => null,
|
|
};
|
|
|
|
if (! $mirror_key) return;
|
|
|
|
// Bricks stores element JSON as a PHP-serialized array. Decode JSON string to array first.
|
|
$value = is_string($content) ? json_decode($content, true) : $content;
|
|
if (! is_array($value)) return;
|
|
|
|
// Bypass register_post_meta string-coercion by writing a serialized blob directly via $wpdb.
|
|
// This is what Bricks's own builder save does — the value lands as a serialized PHP array,
|
|
// which `get_post_meta($id, $key, true)` auto-unserializes back to array on read.
|
|
global $wpdb;
|
|
$serialized = maybe_serialize($value);
|
|
$existing = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key=%s LIMIT 1",
|
|
$post_id, $mirror_key
|
|
));
|
|
|
|
if ($existing) {
|
|
$wpdb->update(
|
|
$wpdb->postmeta,
|
|
['meta_value' => $serialized],
|
|
['meta_id' => $existing],
|
|
['%s'], ['%d']
|
|
);
|
|
} else {
|
|
$wpdb->insert(
|
|
$wpdb->postmeta,
|
|
['post_id' => $post_id, 'meta_key' => $mirror_key, 'meta_value' => $serialized],
|
|
['%d', '%s', '%s']
|
|
);
|
|
}
|
|
wp_cache_delete($post_id, 'post_meta');
|
|
}
|
|
|
|
/* ----------------------------------------------------------------
|
|
5) BACKFILL : trigger a manual mirror via URL.
|
|
Useful one-shot for templates created before this plugin was installed.
|
|
Visit /?av_mirror_template=ID
|
|
---------------------------------------------------------------- */
|
|
/* One-shot : ensure all Bricks content meta is stored as JSON STRING in DB.
|
|
- PHP-serialized arrays (legacy from earlier hook) → re-encode to JSON
|
|
- JSON strings → leave alone
|
|
This is the canonical storage format. The av_decode filter handles
|
|
the array-view for Bricks's renderer at read time.
|
|
/?av_fix_all_bricks_content=1 */
|
|
add_action('init', function () {
|
|
if (empty($_GET['av_fix_all_bricks_content'])) return;
|
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
|
|
|
global $wpdb;
|
|
$rows = $wpdb->get_results(
|
|
"SELECT meta_id, post_id, meta_key, meta_value
|
|
FROM {$wpdb->postmeta}
|
|
WHERE meta_key IN ('_bricks_page_content_2','_bricks_page_header_2','_bricks_page_footer_2')
|
|
AND LENGTH(meta_value) > 4"
|
|
);
|
|
|
|
$report = ['Normalizing Bricks content meta to JSON strings...'];
|
|
foreach ($rows as $r) {
|
|
$val = $r->meta_value;
|
|
$first = $val[0] ?? '';
|
|
|
|
if ($first === '[' || $first === '{') {
|
|
// Already JSON string : skip
|
|
$report[] = " OK meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (JSON)";
|
|
continue;
|
|
}
|
|
|
|
// Serialized PHP array → unserialize, JSON-encode
|
|
if (is_serialized($val)) {
|
|
$array = unserialize($val);
|
|
if (is_array($array)) {
|
|
$json = wp_json_encode($array);
|
|
$wpdb->update(
|
|
$wpdb->postmeta,
|
|
['meta_value' => $json],
|
|
['meta_id' => $r->meta_id],
|
|
['%s'], ['%d']
|
|
);
|
|
wp_cache_delete($r->post_id, 'post_meta');
|
|
$report[] = " CONV meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (" . count($array) . " elements, " . strlen($json) . " bytes)";
|
|
continue;
|
|
}
|
|
}
|
|
|
|
$report[] = " SKIP meta_id={$r->meta_id} post={$r->post_id} key={$r->meta_key} (unknown format)";
|
|
}
|
|
|
|
header('Content-Type: text/plain; charset=utf-8');
|
|
echo implode("\n", $report);
|
|
exit;
|
|
});
|
|
|
|
add_action('init', function () {
|
|
if (empty($_GET['av_mirror_template'])) return;
|
|
// localhost-only debug — no auth required (we're on http://localhost:10004)
|
|
if (! in_array($_SERVER['SERVER_NAME'] ?? '', ['localhost', '127.0.0.1'], true)) wp_die('localhost only');
|
|
|
|
$id = (int) $_GET['av_mirror_template'];
|
|
$content = get_post_meta($id, '_bricks_page_content_2', true);
|
|
$type = get_post_meta($id, '_bricks_template_type', true);
|
|
|
|
if (! is_string($content) && ! is_array($content)) wp_die('no content meta');
|
|
|
|
$value = is_string($content) ? json_decode($content, true) : $content;
|
|
if (! is_array($value)) wp_die('content is not an array');
|
|
|
|
$mirror_key = match ($type) {
|
|
'header' => '_bricks_page_header_2',
|
|
'footer' => '_bricks_page_footer_2',
|
|
default => null,
|
|
};
|
|
|
|
if (! $mirror_key) wp_die("no mirror needed for type=$type");
|
|
|
|
// Write directly to DB as a serialized PHP array (the format Bricks's renderer expects)
|
|
global $wpdb;
|
|
$serialized = maybe_serialize($value);
|
|
$existing = $wpdb->get_var($wpdb->prepare(
|
|
"SELECT meta_id FROM {$wpdb->postmeta} WHERE post_id=%d AND meta_key=%s LIMIT 1",
|
|
$id, $mirror_key
|
|
));
|
|
if ($existing) {
|
|
$wpdb->update($wpdb->postmeta, ['meta_value' => $serialized], ['meta_id' => $existing], ['%s'], ['%d']);
|
|
} else {
|
|
$wpdb->insert($wpdb->postmeta, ['post_id' => $id, 'meta_key' => $mirror_key, 'meta_value' => $serialized], ['%d', '%s', '%s']);
|
|
}
|
|
wp_cache_delete($id, 'post_meta');
|
|
|
|
// Verify
|
|
$after = get_post_meta($id, $mirror_key, true);
|
|
$is_array = is_array($after);
|
|
|
|
wp_die('<pre style="font:12px/1.5 monospace;padding:2rem;background:#0B1F3A;color:#E0C892;">'
|
|
. esc_html("Mirror done : template #$id ($type)\n _bricks_page_content_2 -> $mirror_key\n " . count($value) . " elements\n stored as " . ($is_array ? 'array (correct)' : 'string (WRONG)'))
|
|
. '</pre>', 'Mirror done');
|
|
});
|