class-avcf-abilities-etch-pro.php
2 weeks ago
class-avcf-abilities-etch.php
2 weeks ago
class-avcf-etch-detector.php
2 weeks ago
class-avcf-etch-helpers.php
2 weeks ago
class-avcf-abilities-etch-pro.php
867 lines
| 1 | <?php |
| 2 | /** |
| 3 | * Etch — Advanced MCP abilities (the "pro" file). Pass 1 here is content modeling (post types, taxonomies, custom fields, loops, queries); the design pass (components, templates, styles) is appended to this same file. |
| 4 | * |
| 5 | * Post types (etch_cpts), taxonomies (etch_taxonomies), custom field groups + |
| 6 | * fields (etch_cfs), loops (etch_loops), and queries (etch_queries) — all plain |
| 7 | * WP option maps. Definition CRUD is reliable option read/modify/write; actual |
| 8 | * registration happens at Etch's runtime from these options. Site-wide writers |
| 9 | * are confirm-gated. Built from the Novamira reference; not runtime-tested. |
| 10 | * |
| 11 | * @package atarim-visual-collaboration |
| 12 | */ |
| 13 | |
| 14 | if ( ! defined('ABSPATH') ) { |
| 15 | exit; |
| 16 | } |
| 17 | |
| 18 | class AVCF_Abilities_Etch_Pro extends AVCF_Abilities_Base { |
| 19 | |
| 20 | const O_CPT = 'etch_cpts'; |
| 21 | const O_TAX = 'etch_taxonomies'; |
| 22 | const O_CF = 'etch_cfs'; |
| 23 | const O_LOOP = 'etch_loops'; |
| 24 | const O_QRY = 'etch_queries'; |
| 25 | const O_STYLES = 'etch_styles'; |
| 26 | const O_SHEETS = 'etch_global_stylesheets'; |
| 27 | const COMPONENT_CPT = 'wp_block'; |
| 28 | const TEMPLATE_TAX = 'wp_theme'; |
| 29 | |
| 30 | /** @var AVCF_Etch_Detector */ |
| 31 | private $detector; |
| 32 | |
| 33 | public function __construct() { |
| 34 | $this->detector = new AVCF_Etch_Detector(); |
| 35 | } |
| 36 | |
| 37 | public function register() { |
| 38 | if ( ! $this->detector->avcf_etch_is_available() ) { |
| 39 | return; |
| 40 | } |
| 41 | $this->register_post_types(); |
| 42 | $this->register_taxonomies(); |
| 43 | $this->register_custom_fields(); |
| 44 | $this->register_loops(); |
| 45 | $this->register_queries(); |
| 46 | $this->register_components(); |
| 47 | $this->register_templates(); |
| 48 | $this->register_styles(); |
| 49 | } |
| 50 | |
| 51 | /** Resolve the Etch template CPT (the WP block-template type). */ |
| 52 | public function template_cpt() { |
| 53 | return defined( 'ETCH_TEMPLATE_POST_TYPE' ) ? constant( 'ETCH_TEMPLATE_POST_TYPE' ) : 'wp_template'; |
| 54 | } |
| 55 | |
| 56 | /* ------------------------------ shared ----------------------------- */ |
| 57 | |
| 58 | private function ro_meta() { |
| 59 | return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => true, 'destructive' => false, 'idempotent' => true ] ]; |
| 60 | } |
| 61 | private function write_meta( $destructive = false ) { |
| 62 | return [ 'mcp' => [ 'public' => true, 'type' => 'tool' ], 'annotations' => [ 'readonly' => false, 'destructive' => (bool) $destructive, 'idempotent' => false ] ]; |
| 63 | } |
| 64 | private function std_out() { |
| 65 | return [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ]; |
| 66 | } |
| 67 | private function site_can() { return current_user_can( 'manage_options' ); } |
| 68 | private function confirm_gate( $input, $what ) { |
| 69 | if ( empty( $input['confirm'] ) ) { return [ 'success' => true, 'applied' => false, 'message' => sprintf( 'Dry run: %s is site-wide. Re-call with confirm:true.', $what ) ]; } |
| 70 | return null; |
| 71 | } |
| 72 | public function opt_get( $name ) { $v = get_option( $name, [] ); return is_array( $v ) ? $v : []; } |
| 73 | public function opt_set( $name, $map ) { return (bool) update_option( $name, $map ); } |
| 74 | public function gen_id() { return substr( md5( uniqid( '', true ) ), 0, 10 ); } |
| 75 | |
| 76 | /* ------------------------ post types / taxonomies ------------------ */ |
| 77 | |
| 78 | /** Shared slug=>args map CRUD registrar for post types & taxonomies. */ |
| 79 | private function register_slug_map( $opt, $singular, $extra_create = [] ) { |
| 80 | $self = $this; $std = $this->std_out(); |
| 81 | $kind = $singular; // 'post-type' | 'taxonomy' |
| 82 | $noun = str_replace( '-', ' ', $singular ); |
| 83 | |
| 84 | wp_register_ability( 'atarim/etch-list-' . $kind . 's', [ |
| 85 | 'label' => 'List Etch ' . ucwords( $noun ) . 's', 'category' => 'atarim', |
| 86 | 'description' => sprintf( 'List the %ss Etch registers (the %s option). Returns { slug, label } per entry; fetch full args with get-%s.', $noun, $opt, $kind ), |
| 87 | 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ], |
| 88 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'items' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 89 | 'execute_callback' => function( $input = [] ) use ( $self, $opt ) { |
| 90 | $map = $self->opt_get( $opt ); |
| 91 | $out = []; |
| 92 | foreach ( $map as $slug => $args ) { |
| 93 | $label = is_array( $args ) ? ( $args['label'] ?? ( isset( $args['labels']['name'] ) ? $args['labels']['name'] : (string) $slug ) ) : (string) $slug; |
| 94 | $out[] = [ 'slug' => (string) $slug, 'label' => $label ]; |
| 95 | } |
| 96 | return [ 'success' => true, 'items' => $out, 'message' => sprintf( '%d %s(s).', count( $out ), $noun ) ]; |
| 97 | }, |
| 98 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 99 | 'meta' => $this->ro_meta(), |
| 100 | ] ); |
| 101 | |
| 102 | wp_register_ability( 'atarim/etch-get-' . $kind, [ |
| 103 | 'label' => 'Get Etch ' . ucwords( $noun ), 'category' => 'atarim', |
| 104 | 'description' => sprintf( 'Get the full registration args for one %s by slug.', $noun ), |
| 105 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'slug' => [ 'type' => 'string' ] ], 'required' => [ 'slug' ], 'additionalProperties' => false ], |
| 106 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'slug' => [ 'type' => 'string' ], 'args' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 107 | 'execute_callback' => function( $input = [] ) use ( $self, $opt, $noun ) { |
| 108 | $map = $self->opt_get( $opt ); $slug = (string) $input['slug']; |
| 109 | if ( ! isset( $map[ $slug ] ) ) { return [ 'success' => false, 'message' => sprintf( '%s "%s" not found.', ucfirst( $noun ), $slug ) ]; } |
| 110 | return [ 'success' => true, 'slug' => $slug, 'args' => (array) $map[ $slug ], 'message' => 'OK.' ]; |
| 111 | }, |
| 112 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 113 | 'meta' => $this->ro_meta(), |
| 114 | ] ); |
| 115 | |
| 116 | $create_props = array_merge( [ |
| 117 | 'slug' => [ 'type' => 'string' ], |
| 118 | 'label' => [ 'type' => 'string' ], |
| 119 | 'args' => [ 'type' => 'object', 'description' => 'Full register_' . str_replace( '-', '_', $kind ) . ' args (merged over sensible defaults).' ], |
| 120 | 'confirm' => [ 'type' => 'boolean', 'default' => false ], |
| 121 | ], $extra_create ); |
| 122 | |
| 123 | wp_register_ability( 'atarim/etch-create-' . $kind, [ |
| 124 | 'label' => 'Create Etch ' . ucwords( $noun ), 'category' => 'atarim', |
| 125 | 'description' => sprintf( 'SITE-WIDE. Register a new %s. slug + label required. args is the full registration args object (merged over defaults). Dry run unless confirm:true.', $noun ) . ( $kind === 'taxonomy' ? ' object_type is the array of post-type slugs this taxonomy attaches to.' : '' ), |
| 126 | 'input_schema' => [ 'type' => 'object', 'properties' => $create_props, 'required' => [ 'slug', 'label' ], 'additionalProperties' => false ], |
| 127 | 'output_schema'=> $std, |
| 128 | 'execute_callback' => function( $input = [] ) use ( $self, $opt, $noun, $kind ) { |
| 129 | $gate = $self->confirm_gate( $input, 'Registering a ' . $noun ); if ( $gate ) { return $gate; } |
| 130 | $slug = sanitize_key( (string) $input['slug'] ); |
| 131 | if ( $slug === '' ) { return [ 'success' => false, 'message' => 'A valid slug is required.' ]; } |
| 132 | $map = $self->opt_get( $opt ); |
| 133 | if ( isset( $map[ $slug ] ) ) { return [ 'success' => false, 'message' => sprintf( 'A %s with slug "%s" already exists.', $noun, $slug ) ]; } |
| 134 | $args = isset( $input['args'] ) && is_array( $input['args'] ) ? $input['args'] : []; |
| 135 | $args['label'] = (string) $input['label']; |
| 136 | if ( $kind === 'taxonomy' && isset( $input['object_type'] ) && is_array( $input['object_type'] ) ) { $args['object_type'] = array_values( array_map( 'sanitize_key', $input['object_type'] ) ); } |
| 137 | if ( ! isset( $args['public'] ) ) { $args['public'] = true; } |
| 138 | if ( ! isset( $args['show_in_rest'] ) ) { $args['show_in_rest'] = true; } |
| 139 | if ( $kind === 'post-type' && ! isset( $args['supports'] ) ) { $args['supports'] = [ 'title', 'editor' ]; } |
| 140 | $map[ $slug ] = $args; |
| 141 | if ( ! $self->opt_set( $opt, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 142 | return [ 'success' => true, 'message' => sprintf( '%s "%s" created (takes effect after Etch re-registers / next load).', ucfirst( $noun ), $slug ) ]; |
| 143 | }, |
| 144 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 145 | 'meta' => $this->write_meta( false ), |
| 146 | ] ); |
| 147 | |
| 148 | wp_register_ability( 'atarim/etch-edit-' . $kind, [ |
| 149 | 'label' => 'Edit Etch ' . ucwords( $noun ), 'category' => 'atarim', |
| 150 | 'description' => sprintf( 'SITE-WIDE. Edit a %s by slug (args merged; label convenience). Dry run unless confirm:true.', $noun ), |
| 151 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'slug' => [ 'type' => 'string' ], 'label' => [ 'type' => 'string' ], 'args' => [ 'type' => 'object' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'slug' ], 'additionalProperties' => false ], |
| 152 | 'output_schema'=> $std, |
| 153 | 'execute_callback' => function( $input = [] ) use ( $self, $opt, $noun ) { |
| 154 | $gate = $self->confirm_gate( $input, 'Editing a ' . $noun ); if ( $gate ) { return $gate; } |
| 155 | $map = $self->opt_get( $opt ); $slug = (string) $input['slug']; |
| 156 | if ( ! isset( $map[ $slug ] ) ) { return [ 'success' => false, 'message' => sprintf( '%s "%s" not found.', ucfirst( $noun ), $slug ) ]; } |
| 157 | $args = is_array( $map[ $slug ] ) ? $map[ $slug ] : []; |
| 158 | if ( isset( $input['args'] ) && is_array( $input['args'] ) ) { $args = array_merge( $args, $input['args'] ); } |
| 159 | if ( isset( $input['label'] ) ) { $args['label'] = (string) $input['label']; } |
| 160 | $map[ $slug ] = $args; |
| 161 | if ( ! $self->opt_set( $opt, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 162 | return [ 'success' => true, 'message' => sprintf( '%s "%s" updated.', ucfirst( $noun ), $slug ) ]; |
| 163 | }, |
| 164 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 165 | 'meta' => $this->write_meta( false ), |
| 166 | ] ); |
| 167 | |
| 168 | wp_register_ability( 'atarim/etch-delete-' . $kind, [ |
| 169 | 'label' => 'Delete Etch ' . ucwords( $noun ), 'category' => 'atarim', |
| 170 | 'description' => sprintf( 'SITE-WIDE. Unregister a %s by slug (removes the definition; existing content is not deleted). Dry run unless confirm:true.', $noun ), |
| 171 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'slug' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'slug' ], 'additionalProperties' => false ], |
| 172 | 'output_schema'=> $std, |
| 173 | 'execute_callback' => function( $input = [] ) use ( $self, $opt, $noun ) { |
| 174 | $gate = $self->confirm_gate( $input, 'Deleting a ' . $noun ); if ( $gate ) { return $gate; } |
| 175 | $map = $self->opt_get( $opt ); $slug = (string) $input['slug']; |
| 176 | if ( ! isset( $map[ $slug ] ) ) { return [ 'success' => false, 'message' => sprintf( '%s "%s" not found.', ucfirst( $noun ), $slug ) ]; } |
| 177 | unset( $map[ $slug ] ); |
| 178 | if ( ! $self->opt_set( $opt, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 179 | return [ 'success' => true, 'message' => sprintf( '%s "%s" deleted.', ucfirst( $noun ), $slug ) ]; |
| 180 | }, |
| 181 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 182 | 'meta' => $this->write_meta( true ), |
| 183 | ] ); |
| 184 | } |
| 185 | |
| 186 | private function register_post_types() { |
| 187 | $this->register_slug_map( self::O_CPT, 'post-type' ); |
| 188 | } |
| 189 | private function register_taxonomies() { |
| 190 | $this->register_slug_map( self::O_TAX, 'taxonomy', [ 'object_type' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ], 'description' => 'Post-type slugs this taxonomy attaches to.' ] ] ); |
| 191 | } |
| 192 | |
| 193 | /* --------------------------- custom fields ------------------------- */ |
| 194 | |
| 195 | private function register_custom_fields() { |
| 196 | $self = $this; $std = $this->std_out(); |
| 197 | |
| 198 | wp_register_ability( 'atarim/etch-list-field-groups', [ |
| 199 | 'label' => 'List Etch Field Groups', 'category' => 'atarim', |
| 200 | 'description' => 'List Etch custom field groups (etch_cfs): { id, label, post_types, field_count }. Fetch full definitions with get-field-group.', |
| 201 | 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ], |
| 202 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'groups' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 203 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 204 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); |
| 205 | $out = []; |
| 206 | foreach ( $map as $id => $g ) { |
| 207 | if ( ! is_array( $g ) ) { continue; } |
| 208 | $fields = isset( $g['fields'] ) && is_array( $g['fields'] ) ? $g['fields'] : []; |
| 209 | $out[] = [ 'id' => (string) $id, 'label' => $g['label'] ?? '', 'post_types' => $g['post_types'] ?? ( $g['assigned_to'] ?? [] ), 'field_count' => count( $fields ) ]; |
| 210 | } |
| 211 | return [ 'success' => true, 'groups' => $out, 'message' => sprintf( '%d group(s).', count( $out ) ) ]; |
| 212 | }, |
| 213 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 214 | 'meta' => $this->ro_meta(), |
| 215 | ] ); |
| 216 | |
| 217 | wp_register_ability( 'atarim/etch-get-field-group', [ |
| 218 | 'label' => 'Get Etch Field Group', 'category' => 'atarim', |
| 219 | 'description' => 'Get a field group (label, post_types, and its fields) by id.', |
| 220 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 221 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'group' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 222 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 223 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $id = (string) $input['id']; |
| 224 | if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $id ) ]; } |
| 225 | return [ 'success' => true, 'group' => array_merge( [ 'id' => $id ], (array) $map[ $id ] ), 'message' => 'OK.' ]; |
| 226 | }, |
| 227 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 228 | 'meta' => $this->ro_meta(), |
| 229 | ] ); |
| 230 | |
| 231 | wp_register_ability( 'atarim/etch-create-field-group', [ |
| 232 | 'label' => 'Create Etch Field Group', 'category' => 'atarim', |
| 233 | 'description' => 'SITE-WIDE. Create a custom field group. label required; post_types is the array of post-type slugs to attach to; fields is an optional initial array of field defs ({key, label, type, ...}). Dry run unless confirm:true.', |
| 234 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'label' => [ 'type' => 'string' ], 'post_types' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], 'fields' => [ 'type' => 'array' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'label' ], 'additionalProperties' => false ], |
| 235 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 236 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 237 | $gate = $self->confirm_gate( $input, 'Creating a field group' ); if ( $gate ) { return $gate; } |
| 238 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); |
| 239 | $id = $self->gen_id(); |
| 240 | $map[ $id ] = [ |
| 241 | 'label' => (string) $input['label'], |
| 242 | 'post_types' => isset( $input['post_types'] ) && is_array( $input['post_types'] ) ? array_values( array_map( 'sanitize_key', $input['post_types'] ) ) : [], |
| 243 | 'fields' => isset( $input['fields'] ) && is_array( $input['fields'] ) ? array_values( $input['fields'] ) : [], |
| 244 | ]; |
| 245 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 246 | return [ 'success' => true, 'id' => $id, 'message' => 'Field group created.' ]; |
| 247 | }, |
| 248 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 249 | 'meta' => $this->write_meta( false ), |
| 250 | ] ); |
| 251 | |
| 252 | wp_register_ability( 'atarim/etch-edit-field-group', [ |
| 253 | 'label' => 'Edit Etch Field Group', 'category' => 'atarim', |
| 254 | 'description' => 'SITE-WIDE. Edit a field group by id (label and/or post_types). To change fields use add/edit/delete-field. Dry run unless confirm:true.', |
| 255 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'label' => [ 'type' => 'string' ], 'post_types' => [ 'type' => 'array', 'items' => [ 'type' => 'string' ] ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 256 | 'output_schema'=> $std, |
| 257 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 258 | $gate = $self->confirm_gate( $input, 'Editing a field group' ); if ( $gate ) { return $gate; } |
| 259 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $id = (string) $input['id']; |
| 260 | if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $id ) ]; } |
| 261 | if ( isset( $input['label'] ) ) { $map[ $id ]['label'] = (string) $input['label']; } |
| 262 | if ( isset( $input['post_types'] ) && is_array( $input['post_types'] ) ) { $map[ $id ]['post_types'] = array_values( array_map( 'sanitize_key', $input['post_types'] ) ); } |
| 263 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 264 | return [ 'success' => true, 'message' => 'Field group updated.' ]; |
| 265 | }, |
| 266 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 267 | 'meta' => $this->write_meta( false ), |
| 268 | ] ); |
| 269 | |
| 270 | wp_register_ability( 'atarim/etch-delete-field-group', [ |
| 271 | 'label' => 'Delete Etch Field Group', 'category' => 'atarim', |
| 272 | 'description' => 'SITE-WIDE. Delete a field group (and its fields) by id. Dry run unless confirm:true.', |
| 273 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 274 | 'output_schema'=> $std, |
| 275 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 276 | $gate = $self->confirm_gate( $input, 'Deleting a field group' ); if ( $gate ) { return $gate; } |
| 277 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $id = (string) $input['id']; |
| 278 | if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $id ) ]; } |
| 279 | unset( $map[ $id ] ); |
| 280 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 281 | return [ 'success' => true, 'message' => 'Field group deleted.' ]; |
| 282 | }, |
| 283 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 284 | 'meta' => $this->write_meta( true ), |
| 285 | ] ); |
| 286 | |
| 287 | wp_register_ability( 'atarim/etch-add-field', [ |
| 288 | 'label' => 'Add Etch Field', 'category' => 'atarim', |
| 289 | 'description' => 'SITE-WIDE. Add a field to a field group. field is the field def ({key, label, type, ...}); key must be unique within the group. Dry run unless confirm:true.', |
| 290 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'group_id' => [ 'type' => 'string' ], 'field' => [ 'type' => 'object' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'group_id', 'field' ], 'additionalProperties' => false ], |
| 291 | 'output_schema'=> $std, |
| 292 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 293 | $gate = $self->confirm_gate( $input, 'Adding a field' ); if ( $gate ) { return $gate; } |
| 294 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $gid = (string) $input['group_id']; |
| 295 | if ( ! isset( $map[ $gid ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $gid ) ]; } |
| 296 | $field = is_array( $input['field'] ) ? $input['field'] : []; |
| 297 | $key = isset( $field['key'] ) ? (string) $field['key'] : ''; |
| 298 | if ( $key === '' ) { return [ 'success' => false, 'message' => 'field.key is required.' ]; } |
| 299 | $fields = isset( $map[ $gid ]['fields'] ) && is_array( $map[ $gid ]['fields'] ) ? $map[ $gid ]['fields'] : []; |
| 300 | foreach ( $fields as $f ) { if ( is_array( $f ) && ( ( $f['key'] ?? null ) === $key ) ) { return [ 'success' => false, 'message' => sprintf( 'A field with key "%s" already exists in this group.', $key ) ]; } } |
| 301 | $fields[] = $field; |
| 302 | $map[ $gid ]['fields'] = array_values( $fields ); |
| 303 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 304 | return [ 'success' => true, 'message' => sprintf( 'Field "%s" added.', $key ) ]; |
| 305 | }, |
| 306 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 307 | 'meta' => $this->write_meta( false ), |
| 308 | ] ); |
| 309 | |
| 310 | wp_register_ability( 'atarim/etch-edit-field', [ |
| 311 | 'label' => 'Edit Etch Field', 'category' => 'atarim', |
| 312 | 'description' => 'SITE-WIDE. Edit a field within a group by key. patch is merged into the field def. Dry run unless confirm:true.', |
| 313 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'group_id' => [ 'type' => 'string' ], 'key' => [ 'type' => 'string' ], 'patch' => [ 'type' => 'object' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'group_id', 'key', 'patch' ], 'additionalProperties' => false ], |
| 314 | 'output_schema'=> $std, |
| 315 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 316 | $gate = $self->confirm_gate( $input, 'Editing a field' ); if ( $gate ) { return $gate; } |
| 317 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $gid = (string) $input['group_id']; $key = (string) $input['key']; |
| 318 | if ( ! isset( $map[ $gid ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $gid ) ]; } |
| 319 | $fields = isset( $map[ $gid ]['fields'] ) && is_array( $map[ $gid ]['fields'] ) ? $map[ $gid ]['fields'] : []; |
| 320 | $found = false; |
| 321 | foreach ( $fields as &$f ) { if ( is_array( $f ) && ( ( $f['key'] ?? null ) === $key ) ) { $f = array_merge( $f, is_array( $input['patch'] ) ? $input['patch'] : [] ); $f['key'] = $key; $found = true; break; } } |
| 322 | unset( $f ); |
| 323 | if ( ! $found ) { return [ 'success' => false, 'message' => sprintf( 'Field "%s" not found in group.', $key ) ]; } |
| 324 | $map[ $gid ]['fields'] = array_values( $fields ); |
| 325 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 326 | return [ 'success' => true, 'message' => sprintf( 'Field "%s" updated.', $key ) ]; |
| 327 | }, |
| 328 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 329 | 'meta' => $this->write_meta( false ), |
| 330 | ] ); |
| 331 | |
| 332 | wp_register_ability( 'atarim/etch-delete-field', [ |
| 333 | 'label' => 'Delete Etch Field', 'category' => 'atarim', |
| 334 | 'description' => 'SITE-WIDE. Remove a field from a group by key. Dry run unless confirm:true.', |
| 335 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'group_id' => [ 'type' => 'string' ], 'key' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'group_id', 'key' ], 'additionalProperties' => false ], |
| 336 | 'output_schema'=> $std, |
| 337 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 338 | $gate = $self->confirm_gate( $input, 'Deleting a field' ); if ( $gate ) { return $gate; } |
| 339 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); $gid = (string) $input['group_id']; $key = (string) $input['key']; |
| 340 | if ( ! isset( $map[ $gid ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Field group "%s" not found.', $gid ) ]; } |
| 341 | $fields = isset( $map[ $gid ]['fields'] ) && is_array( $map[ $gid ]['fields'] ) ? $map[ $gid ]['fields'] : []; |
| 342 | $new = array_values( array_filter( $fields, function( $f ) use ( $key ) { return ! ( is_array( $f ) && ( ( $f['key'] ?? null ) === $key ) ); } ) ); |
| 343 | if ( count( $new ) === count( $fields ) ) { return [ 'success' => false, 'message' => sprintf( 'Field "%s" not found in group.', $key ) ]; } |
| 344 | $map[ $gid ]['fields'] = $new; |
| 345 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_CF, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 346 | return [ 'success' => true, 'message' => sprintf( 'Field "%s" deleted.', $key ) ]; |
| 347 | }, |
| 348 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 349 | 'meta' => $this->write_meta( true ), |
| 350 | ] ); |
| 351 | |
| 352 | wp_register_ability( 'atarim/etch-list-dynamic-fields', [ |
| 353 | 'label' => 'List Etch Dynamic Fields', 'category' => 'atarim', |
| 354 | 'description' => 'List the dynamic field keys available to bind in the builder — the custom fields defined across all Etch field groups ({ key, label, type, group }). Standard post fields (title, content, excerpt, featured image, etc.) are also available in Etch independently of this list.', |
| 355 | 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ], |
| 356 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'fields' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 357 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 358 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_CF ); |
| 359 | $out = []; |
| 360 | foreach ( $map as $gid => $g ) { |
| 361 | if ( ! is_array( $g ) ) { continue; } |
| 362 | foreach ( ( isset( $g['fields'] ) && is_array( $g['fields'] ) ? $g['fields'] : [] ) as $f ) { |
| 363 | if ( ! is_array( $f ) ) { continue; } |
| 364 | $out[] = [ 'key' => $f['key'] ?? '', 'label' => $f['label'] ?? '', 'type' => $f['type'] ?? '', 'group' => (string) $gid ]; |
| 365 | } |
| 366 | } |
| 367 | return [ 'success' => true, 'fields' => $out, 'message' => sprintf( '%d dynamic field(s).', count( $out ) ) ]; |
| 368 | }, |
| 369 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 370 | 'meta' => $this->ro_meta(), |
| 371 | ] ); |
| 372 | } |
| 373 | |
| 374 | /* ------------------------------- loops ----------------------------- */ |
| 375 | |
| 376 | private function register_loops() { |
| 377 | $self = $this; $std = $this->std_out(); |
| 378 | |
| 379 | wp_register_ability( 'atarim/etch-list-loops', [ |
| 380 | 'label' => 'List Etch Loops', 'category' => 'atarim', |
| 381 | 'description' => 'List Etch loops (etch_loops): { id, name, key }.', |
| 382 | 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ], |
| 383 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'loops' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 384 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 385 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_LOOP ); |
| 386 | $out = []; |
| 387 | foreach ( $map as $id => $l ) { if ( is_array( $l ) ) { $out[] = [ 'id' => (string) $id, 'name' => $l['name'] ?? '', 'key' => $l['key'] ?? '' ]; } } |
| 388 | return [ 'success' => true, 'loops' => $out, 'message' => sprintf( '%d loop(s).', count( $out ) ) ]; |
| 389 | }, |
| 390 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 391 | 'meta' => $this->ro_meta(), |
| 392 | ] ); |
| 393 | |
| 394 | wp_register_ability( 'atarim/etch-get-loop', [ |
| 395 | 'label' => 'Get Etch Loop', 'category' => 'atarim', |
| 396 | 'description' => 'Get a loop (name, key, config) by id. config.type is e.g. "wp-query" with config.args being WP_Query args.', |
| 397 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 398 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'loop' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 399 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 400 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_LOOP ); $id = (string) $input['id']; |
| 401 | if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Loop "%s" not found.', $id ) ]; } |
| 402 | return [ 'success' => true, 'loop' => array_merge( [ 'id' => $id ], (array) $map[ $id ] ), 'message' => 'OK.' ]; |
| 403 | }, |
| 404 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 405 | 'meta' => $this->ro_meta(), |
| 406 | ] ); |
| 407 | |
| 408 | wp_register_ability( 'atarim/etch-create-loop', [ |
| 409 | 'label' => 'Create Etch Loop', 'category' => 'atarim', |
| 410 | 'description' => 'SITE-WIDE. Create a loop. name required; config is the loop config ({ type:"wp-query", args:{...WP_Query args} }) — model it on an existing loop (get-loop). Dry run unless confirm:true.', |
| 411 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'type' => 'string' ], 'config' => [ 'type' => 'object' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'name' ], 'additionalProperties' => false ], |
| 412 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 413 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 414 | $gate = $self->confirm_gate( $input, 'Creating a loop' ); if ( $gate ) { return $gate; } |
| 415 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_LOOP ); |
| 416 | $id = $self->gen_id(); |
| 417 | $map[ $id ] = [ 'name' => (string) $input['name'], 'key' => $id, 'config' => isset( $input['config'] ) && is_array( $input['config'] ) ? $input['config'] : [ 'type' => 'wp-query', 'args' => [] ], 'global' => false ]; |
| 418 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_LOOP, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 419 | return [ 'success' => true, 'id' => $id, 'message' => 'Loop created.' ]; |
| 420 | }, |
| 421 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 422 | 'meta' => $this->write_meta( false ), |
| 423 | ] ); |
| 424 | |
| 425 | wp_register_ability( 'atarim/etch-edit-loop', [ |
| 426 | 'label' => 'Edit Etch Loop', 'category' => 'atarim', |
| 427 | 'description' => 'SITE-WIDE. Edit a loop by id (name and/or config; config merged). Dry run unless confirm:true.', |
| 428 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'name' => [ 'type' => 'string' ], 'config' => [ 'type' => 'object' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 429 | 'output_schema'=> $std, |
| 430 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 431 | $gate = $self->confirm_gate( $input, 'Editing a loop' ); if ( $gate ) { return $gate; } |
| 432 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_LOOP ); $id = (string) $input['id']; |
| 433 | if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Loop "%s" not found.', $id ) ]; } |
| 434 | if ( isset( $input['name'] ) ) { $map[ $id ]['name'] = (string) $input['name']; } |
| 435 | if ( isset( $input['config'] ) && is_array( $input['config'] ) ) { |
| 436 | $cur = isset( $map[ $id ]['config'] ) && is_array( $map[ $id ]['config'] ) ? $map[ $id ]['config'] : []; |
| 437 | $map[ $id ]['config'] = array_merge( $cur, $input['config'] ); |
| 438 | } |
| 439 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_LOOP, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 440 | return [ 'success' => true, 'message' => 'Loop updated.' ]; |
| 441 | }, |
| 442 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 443 | 'meta' => $this->write_meta( false ), |
| 444 | ] ); |
| 445 | |
| 446 | wp_register_ability( 'atarim/etch-delete-loop', [ |
| 447 | 'label' => 'Delete Etch Loop', 'category' => 'atarim', |
| 448 | 'description' => 'SITE-WIDE. Delete a loop by id. Dry run unless confirm:true.', |
| 449 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 450 | 'output_schema'=> $std, |
| 451 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 452 | $gate = $self->confirm_gate( $input, 'Deleting a loop' ); if ( $gate ) { return $gate; } |
| 453 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_LOOP ); $id = (string) $input['id']; |
| 454 | if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Loop "%s" not found.', $id ) ]; } |
| 455 | unset( $map[ $id ] ); |
| 456 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_LOOP, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 457 | return [ 'success' => true, 'message' => 'Loop deleted.' ]; |
| 458 | }, |
| 459 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 460 | 'meta' => $this->write_meta( true ), |
| 461 | ] ); |
| 462 | } |
| 463 | |
| 464 | /* ------------------------------ queries ---------------------------- */ |
| 465 | |
| 466 | private function register_queries() { |
| 467 | $self = $this; |
| 468 | |
| 469 | wp_register_ability( 'atarim/etch-list-queries', [ |
| 470 | 'label' => 'List Etch Queries', 'category' => 'atarim', |
| 471 | 'description' => 'List saved Etch queries (etch_queries): { id, name }.', |
| 472 | 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ], |
| 473 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'queries' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 474 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 475 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_QRY ); |
| 476 | $out = []; |
| 477 | foreach ( $map as $id => $q ) { if ( is_array( $q ) ) { $out[] = [ 'id' => (string) $id, 'name' => $q['name'] ?? '' ]; } } |
| 478 | return [ 'success' => true, 'queries' => $out, 'message' => sprintf( '%d query(ies).', count( $out ) ) ]; |
| 479 | }, |
| 480 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 481 | 'meta' => $this->ro_meta(), |
| 482 | ] ); |
| 483 | |
| 484 | wp_register_ability( 'atarim/etch-get-query', [ |
| 485 | 'label' => 'Get Etch Query', 'category' => 'atarim', |
| 486 | 'description' => 'Get a saved query (name, args) by id.', |
| 487 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 488 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'query' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 489 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 490 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_QRY ); $id = (string) $input['id']; |
| 491 | if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Query "%s" not found.', $id ) ]; } |
| 492 | return [ 'success' => true, 'query' => array_merge( [ 'id' => $id ], (array) $map[ $id ] ), 'message' => 'OK.' ]; |
| 493 | }, |
| 494 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 495 | 'meta' => $this->ro_meta(), |
| 496 | ] ); |
| 497 | |
| 498 | wp_register_ability( 'atarim/etch-get-query-preview', [ |
| 499 | 'label' => 'Get Etch Query Preview', 'category' => 'atarim', |
| 500 | 'description' => 'Run a query and return a preview of matched posts ({ id, title, type }). Provide query_id (a saved query) OR args (WP_Query args directly). Capped at 20 results.', |
| 501 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'query_id' => [ 'type' => 'string' ], 'args' => [ 'type' => 'object' ] ], 'additionalProperties' => false ], |
| 502 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'found_posts' => [ 'type' => 'integer' ], 'posts' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 503 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 504 | $args = []; |
| 505 | if ( isset( $input['args'] ) && is_array( $input['args'] ) ) { $args = $input['args']; } |
| 506 | elseif ( isset( $input['query_id'] ) ) { |
| 507 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_QRY ); $id = (string) $input['query_id']; |
| 508 | if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Query "%s" not found.', $id ) ]; } |
| 509 | $args = isset( $map[ $id ]['args'] ) && is_array( $map[ $id ]['args'] ) ? $map[ $id ]['args'] : []; |
| 510 | } else { |
| 511 | return [ 'success' => false, 'message' => 'Provide query_id or args.' ]; |
| 512 | } |
| 513 | $args['posts_per_page'] = min( 20, isset( $args['posts_per_page'] ) ? (int) $args['posts_per_page'] : 20 ); |
| 514 | $args['fields'] = 'ids'; |
| 515 | $q = new WP_Query( $args ); |
| 516 | $posts = []; |
| 517 | foreach ( $q->posts as $pid ) { $posts[] = [ 'id' => (int) $pid, 'title' => get_the_title( $pid ), 'type' => get_post_type( $pid ) ]; } |
| 518 | return [ 'success' => true, 'found_posts' => (int) $q->found_posts, 'posts' => $posts, 'message' => sprintf( '%d found; showing %d.', (int) $q->found_posts, count( $posts ) ) ]; |
| 519 | }, |
| 520 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 521 | 'meta' => $this->ro_meta(), |
| 522 | ] ); |
| 523 | } |
| 524 | |
| 525 | /* ---------------------------- components --------------------------- */ |
| 526 | |
| 527 | private function register_components() { |
| 528 | $self = $this; $std = $this->std_out(); |
| 529 | |
| 530 | wp_register_ability( 'atarim/etch-list-components', [ |
| 531 | 'label' => 'List Etch Components', 'category' => 'atarim', |
| 532 | 'description' => 'List Etch components (reusable-block-backed): { id, name, key }. Components are wp_block posts carrying Etch component metadata.', |
| 533 | 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ], |
| 534 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'components' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 535 | 'execute_callback' => function( $input = [] ) { |
| 536 | $posts = get_posts( [ 'post_type' => AVCF_Abilities_Etch_Pro::COMPONENT_CPT, 'post_status' => 'any', 'numberposts' => -1, 'meta_key' => 'etch_component_html_key' ] ); |
| 537 | $out = []; |
| 538 | foreach ( $posts as $p ) { $out[] = [ 'id' => $p->ID, 'name' => $p->post_title, 'key' => (string) get_post_meta( $p->ID, 'etch_component_html_key', true ) ]; } |
| 539 | return [ 'success' => true, 'components' => $out, 'message' => sprintf( '%d component(s).', count( $out ) ) ]; |
| 540 | }, |
| 541 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 542 | 'meta' => $this->ro_meta(), |
| 543 | ] ); |
| 544 | |
| 545 | wp_register_ability( 'atarim/etch-get-component', [ |
| 546 | 'label' => 'Get Etch Component', 'category' => 'atarim', |
| 547 | 'description' => 'Get one component by id: name, key, description, properties, and its element tree (parsed from the block content). lossy:true means it holds freeform HTML the tree can\'t represent.', |
| 548 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 549 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'component' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 550 | 'execute_callback' => function( $input = [] ) { |
| 551 | $id = (int) $input['id']; $p = get_post( $id ); |
| 552 | if ( ! $p || $p->post_type !== AVCF_Abilities_Etch_Pro::COMPONENT_CPT ) { return [ 'success' => false, 'message' => 'Not an Etch component id.' ]; } |
| 553 | $props = get_post_meta( $id, 'etch_component_properties', true ); |
| 554 | $tree = AVCF_Etch_Helpers::parse_tree( $p->post_content ); |
| 555 | return [ 'success' => true, 'component' => [ |
| 556 | 'id' => $id, 'name' => $p->post_title, 'key' => (string) get_post_meta( $id, 'etch_component_html_key', true ), |
| 557 | 'description' => $p->post_excerpt, 'properties' => is_array( $props ) ? $props : [], |
| 558 | 'tree' => $tree, 'node_count' => AVCF_Etch_Helpers::tree_count( $tree ), 'lossy' => AVCF_Etch_Helpers::is_lossy( $p->post_content ), |
| 559 | ], 'message' => 'OK.' ]; |
| 560 | }, |
| 561 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 562 | 'meta' => $this->ro_meta(), |
| 563 | ] ); |
| 564 | |
| 565 | wp_register_ability( 'atarim/etch-create-component', [ |
| 566 | 'label' => 'Create Etch Component', 'category' => 'atarim', |
| 567 | 'description' => 'SITE-WIDE. Create a component. name required; description optional; tree is the element tree ([{block,attrs?,children?,html?}], serialized to block content); properties is the component\'s props object. Dry run unless confirm:true.', |
| 568 | 'input_schema' => [ 'type' => 'object', 'properties' => [ |
| 569 | 'name' => [ 'type' => 'string' ], 'description' => [ 'type' => 'string' ], |
| 570 | 'tree' => [ 'type' => 'array' ], 'properties' => [ 'type' => 'object' ], |
| 571 | 'confirm' => [ 'type' => 'boolean', 'default' => false ], |
| 572 | ], 'required' => [ 'name' ], 'additionalProperties' => false ], |
| 573 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'integer' ], 'key' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 574 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 575 | $gate = $self->confirm_gate( $input, 'Creating a component' ); if ( $gate ) { return $gate; } |
| 576 | $content = ''; |
| 577 | if ( isset( $input['tree'] ) && is_array( $input['tree'] ) ) { $content = AVCF_Etch_Helpers::serialize_tree( AVCF_Etch_Helpers::sanitize_tree( $input['tree'] ) ); } |
| 578 | $id = wp_insert_post( [ 'post_type' => AVCF_Abilities_Etch_Pro::COMPONENT_CPT, 'post_title' => (string) $input['name'], 'post_excerpt' => isset( $input['description'] ) ? (string) $input['description'] : '', 'post_content' => $content, 'post_status' => 'publish' ], true ); |
| 579 | if ( is_wp_error( $id ) ) { return [ 'success' => false, 'message' => $id->get_error_message() ]; } |
| 580 | $key = 'c' . substr( md5( uniqid( 'etch_component', true ) ), 0, 12 ); |
| 581 | update_post_meta( $id, 'etch_component_html_key', $key ); |
| 582 | update_post_meta( $id, 'etch_component_properties', isset( $input['properties'] ) && is_array( $input['properties'] ) ? $input['properties'] : [] ); |
| 583 | return [ 'success' => true, 'id' => (int) $id, 'key' => $key, 'message' => 'Component created.' ]; |
| 584 | }, |
| 585 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 586 | 'meta' => $this->write_meta( false ), |
| 587 | ] ); |
| 588 | |
| 589 | wp_register_ability( 'atarim/etch-edit-component', [ |
| 590 | 'label' => 'Edit Etch Component', 'category' => 'atarim', |
| 591 | 'description' => 'SITE-WIDE. Edit a component by id (name, description, tree, and/or properties). tree replaces the block content; properties replaces the props object. Dry run unless confirm:true.', |
| 592 | 'input_schema' => [ 'type' => 'object', 'properties' => [ |
| 593 | 'id' => [ 'type' => 'integer', 'minimum' => 1 ], 'name' => [ 'type' => 'string' ], 'description' => [ 'type' => 'string' ], |
| 594 | 'tree' => [ 'type' => 'array' ], 'properties' => [ 'type' => 'object' ], |
| 595 | 'confirm' => [ 'type' => 'boolean', 'default' => false ], |
| 596 | ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 597 | 'output_schema'=> $std, |
| 598 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 599 | $gate = $self->confirm_gate( $input, 'Editing a component' ); if ( $gate ) { return $gate; } |
| 600 | $id = (int) $input['id']; $p = get_post( $id ); |
| 601 | if ( ! $p || $p->post_type !== AVCF_Abilities_Etch_Pro::COMPONENT_CPT ) { return [ 'success' => false, 'message' => 'Not an Etch component id.' ]; } |
| 602 | $up = [ 'ID' => $id ]; |
| 603 | if ( isset( $input['name'] ) ) { $up['post_title'] = (string) $input['name']; } |
| 604 | if ( isset( $input['description'] ) ) { $up['post_excerpt'] = (string) $input['description']; } |
| 605 | if ( isset( $input['tree'] ) && is_array( $input['tree'] ) ) { $up['post_content'] = AVCF_Etch_Helpers::serialize_tree( AVCF_Etch_Helpers::sanitize_tree( $input['tree'] ) ); } |
| 606 | if ( count( $up ) > 1 ) { $r = wp_update_post( $up, true ); if ( is_wp_error( $r ) ) { return [ 'success' => false, 'message' => $r->get_error_message() ]; } } |
| 607 | if ( isset( $input['properties'] ) && is_array( $input['properties'] ) ) { update_post_meta( $id, 'etch_component_properties', $input['properties'] ); } |
| 608 | return [ 'success' => true, 'message' => sprintf( 'Component %d updated.', $id ) ]; |
| 609 | }, |
| 610 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 611 | 'meta' => $this->write_meta( false ), |
| 612 | ] ); |
| 613 | |
| 614 | wp_register_ability( 'atarim/etch-delete-component', [ |
| 615 | 'label' => 'Delete Etch Component', 'category' => 'atarim', |
| 616 | 'description' => 'SITE-WIDE. Delete a component by id. Dry run unless confirm:true. (Instances referencing it may break.)', |
| 617 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'minimum' => 1 ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 618 | 'output_schema'=> $std, |
| 619 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 620 | $id = (int) $input['id']; $p = get_post( $id ); |
| 621 | if ( ! $p || $p->post_type !== AVCF_Abilities_Etch_Pro::COMPONENT_CPT ) { return [ 'success' => false, 'message' => 'Not an Etch component id.' ]; } |
| 622 | if ( empty( $input['confirm'] ) ) { return [ 'success' => true, 'message' => sprintf( 'Dry run: would delete component %d. Re-call with confirm:true.', $id ) ]; } |
| 623 | $r = wp_delete_post( $id, true ); |
| 624 | return $r ? [ 'success' => true, 'message' => sprintf( 'Component %d deleted.', $id ) ] : [ 'success' => false, 'message' => 'Delete failed.' ]; |
| 625 | }, |
| 626 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 627 | 'meta' => $this->write_meta( true ), |
| 628 | ] ); |
| 629 | |
| 630 | wp_register_ability( 'atarim/etch-duplicate-component', [ |
| 631 | 'label' => 'Duplicate Etch Component', 'category' => 'atarim', |
| 632 | 'description' => 'SITE-WIDE. Duplicate a component by id (new post + new component key, title suffixed "(copy)"). Dry run unless confirm:true.', |
| 633 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'integer', 'minimum' => 1 ], 'name' => [ 'type' => 'string', 'description' => 'Optional name for the copy.' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 634 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'integer' ], 'key' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 635 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 636 | $gate = $self->confirm_gate( $input, 'Duplicating a component' ); if ( $gate ) { return $gate; } |
| 637 | $id = (int) $input['id']; $p = get_post( $id ); |
| 638 | if ( ! $p || $p->post_type !== AVCF_Abilities_Etch_Pro::COMPONENT_CPT ) { return [ 'success' => false, 'message' => 'Not an Etch component id.' ]; } |
| 639 | $title = isset( $input['name'] ) ? (string) $input['name'] : ( $p->post_title . ' (copy)' ); |
| 640 | $new = wp_insert_post( [ 'post_type' => AVCF_Abilities_Etch_Pro::COMPONENT_CPT, 'post_title' => $title, 'post_excerpt' => $p->post_excerpt, 'post_content' => $p->post_content, 'post_status' => 'publish' ], true ); |
| 641 | if ( is_wp_error( $new ) ) { return [ 'success' => false, 'message' => $new->get_error_message() ]; } |
| 642 | $key = 'c' . substr( md5( uniqid( 'etch_component', true ) ), 0, 12 ); |
| 643 | update_post_meta( $new, 'etch_component_html_key', $key ); |
| 644 | $props = get_post_meta( $id, 'etch_component_properties', true ); |
| 645 | update_post_meta( $new, 'etch_component_properties', is_array( $props ) ? $props : [] ); |
| 646 | return [ 'success' => true, 'id' => (int) $new, 'key' => $key, 'message' => 'Component duplicated.' ]; |
| 647 | }, |
| 648 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 649 | 'meta' => $this->write_meta( false ), |
| 650 | ] ); |
| 651 | } |
| 652 | |
| 653 | /* ----------------------------- templates --------------------------- */ |
| 654 | |
| 655 | private function register_templates() { |
| 656 | $self = $this; $std = $this->std_out(); |
| 657 | |
| 658 | wp_register_ability( 'atarim/etch-list-templates', [ |
| 659 | 'label' => 'List Etch Templates', 'category' => 'atarim', |
| 660 | 'description' => 'List Etch templates (WP block templates): { id, title, slug, theme }. The slug is the template-hierarchy slot (e.g. "single", "archive"); theme is the bound wp_theme term.', |
| 661 | 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ], |
| 662 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'templates' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 663 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 664 | $posts = get_posts( [ 'post_type' => $self->template_cpt(), 'post_status' => 'any', 'numberposts' => -1 ] ); |
| 665 | $out = []; |
| 666 | foreach ( $posts as $p ) { |
| 667 | $terms = wp_get_post_terms( $p->ID, AVCF_Abilities_Etch_Pro::TEMPLATE_TAX, [ 'fields' => 'slugs' ] ); |
| 668 | $out[] = [ 'id' => $p->ID, 'title' => $p->post_title, 'slug' => $p->post_name, 'theme' => ( ! is_wp_error( $terms ) && $terms ) ? $terms[0] : '' ]; |
| 669 | } |
| 670 | return [ 'success' => true, 'templates' => $out, 'message' => sprintf( '%d template(s).', count( $out ) ) ]; |
| 671 | }, |
| 672 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 673 | 'meta' => $this->ro_meta(), |
| 674 | ] ); |
| 675 | |
| 676 | wp_register_ability( 'atarim/etch-get-template', [ |
| 677 | 'label' => 'Get Etch Template', 'category' => 'atarim', |
| 678 | 'description' => 'Get one template by id: title, slug, theme. (Manage the template\'s element content with the core get-content/set-content abilities using its id.)', |
| 679 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'template_id' => [ 'type' => 'integer', 'minimum' => 1 ] ], 'required' => [ 'template_id' ], 'additionalProperties' => false ], |
| 680 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'template' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 681 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 682 | $id = (int) $input['template_id']; $p = get_post( $id ); |
| 683 | if ( ! $p || $p->post_type !== $self->template_cpt() ) { return [ 'success' => false, 'message' => 'Not an Etch template id.' ]; } |
| 684 | $terms = wp_get_post_terms( $id, AVCF_Abilities_Etch_Pro::TEMPLATE_TAX, [ 'fields' => 'slugs' ] ); |
| 685 | return [ 'success' => true, 'template' => [ 'id' => $id, 'title' => $p->post_title, 'slug' => $p->post_name, 'theme' => ( ! is_wp_error( $terms ) && $terms ) ? $terms[0] : '' ], 'message' => 'OK.' ]; |
| 686 | }, |
| 687 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 688 | 'meta' => $this->ro_meta(), |
| 689 | ] ); |
| 690 | |
| 691 | wp_register_ability( 'atarim/etch-create-template', [ |
| 692 | 'label' => 'Create Etch Template', 'category' => 'atarim', |
| 693 | 'description' => 'SITE-WIDE. Create a template. title + slug required (slug = the hierarchy slot, e.g. "single"); theme is the wp_theme term to bind to (defaults to the active theme). Element content is empty — author it via set-content with the new id. Dry run unless confirm:true.', |
| 694 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'title' => [ 'type' => 'string' ], 'slug' => [ 'type' => 'string' ], 'theme' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'title', 'slug' ], 'additionalProperties' => false ], |
| 695 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 696 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 697 | $gate = $self->confirm_gate( $input, 'Creating a template' ); if ( $gate ) { return $gate; } |
| 698 | $slug = sanitize_title( (string) $input['slug'] ); |
| 699 | if ( $slug === '' ) { return [ 'success' => false, 'message' => 'A valid slug is required.' ]; } |
| 700 | $id = wp_insert_post( [ 'post_type' => $self->template_cpt(), 'post_title' => (string) $input['title'], 'post_name' => $slug, 'post_status' => 'publish' ], true ); |
| 701 | if ( is_wp_error( $id ) ) { return [ 'success' => false, 'message' => $id->get_error_message() ]; } |
| 702 | $theme = isset( $input['theme'] ) && $input['theme'] !== '' ? (string) $input['theme'] : get_stylesheet(); |
| 703 | wp_set_post_terms( $id, [ $theme ], AVCF_Abilities_Etch_Pro::TEMPLATE_TAX ); |
| 704 | return [ 'success' => true, 'id' => (int) $id, 'message' => 'Template created.' ]; |
| 705 | }, |
| 706 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 707 | 'meta' => $this->write_meta( false ), |
| 708 | ] ); |
| 709 | |
| 710 | wp_register_ability( 'atarim/etch-edit-template', [ |
| 711 | 'label' => 'Edit Etch Template', 'category' => 'atarim', |
| 712 | 'description' => 'SITE-WIDE. Edit a template by id (title, slug, and/or theme binding). Dry run unless confirm:true.', |
| 713 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'template_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'title' => [ 'type' => 'string' ], 'slug' => [ 'type' => 'string' ], 'theme' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'template_id' ], 'additionalProperties' => false ], |
| 714 | 'output_schema'=> $std, |
| 715 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 716 | $gate = $self->confirm_gate( $input, 'Editing a template' ); if ( $gate ) { return $gate; } |
| 717 | $id = (int) $input['template_id']; $p = get_post( $id ); |
| 718 | if ( ! $p || $p->post_type !== $self->template_cpt() ) { return [ 'success' => false, 'message' => 'Not an Etch template id.' ]; } |
| 719 | $up = [ 'ID' => $id ]; |
| 720 | if ( isset( $input['title'] ) ) { $up['post_title'] = (string) $input['title']; } |
| 721 | if ( isset( $input['slug'] ) ) { $up['post_name'] = sanitize_title( (string) $input['slug'] ); } |
| 722 | if ( count( $up ) > 1 ) { $r = wp_update_post( $up, true ); if ( is_wp_error( $r ) ) { return [ 'success' => false, 'message' => $r->get_error_message() ]; } } |
| 723 | if ( isset( $input['theme'] ) ) { wp_set_post_terms( $id, [ (string) $input['theme'] ], AVCF_Abilities_Etch_Pro::TEMPLATE_TAX ); } |
| 724 | return [ 'success' => true, 'message' => sprintf( 'Template %d updated.', $id ) ]; |
| 725 | }, |
| 726 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 727 | 'meta' => $this->write_meta( false ), |
| 728 | ] ); |
| 729 | |
| 730 | wp_register_ability( 'atarim/etch-delete-template', [ |
| 731 | 'label' => 'Delete Etch Template', 'category' => 'atarim', |
| 732 | 'description' => 'SITE-WIDE. Delete a template by id. Dry run unless confirm:true.', |
| 733 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'template_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'template_id' ], 'additionalProperties' => false ], |
| 734 | 'output_schema'=> $std, |
| 735 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 736 | $id = (int) $input['template_id']; $p = get_post( $id ); |
| 737 | if ( ! $p || $p->post_type !== $self->template_cpt() ) { return [ 'success' => false, 'message' => 'Not an Etch template id.' ]; } |
| 738 | if ( empty( $input['confirm'] ) ) { return [ 'success' => true, 'message' => sprintf( 'Dry run: would delete template %d. Re-call with confirm:true.', $id ) ]; } |
| 739 | $r = wp_delete_post( $id, true ); |
| 740 | return $r ? [ 'success' => true, 'message' => sprintf( 'Template %d deleted.', $id ) ] : [ 'success' => false, 'message' => 'Delete failed.' ]; |
| 741 | }, |
| 742 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 743 | 'meta' => $this->write_meta( true ), |
| 744 | ] ); |
| 745 | |
| 746 | wp_register_ability( 'atarim/etch-duplicate-template', [ |
| 747 | 'label' => 'Duplicate Etch Template', 'category' => 'atarim', |
| 748 | 'description' => 'SITE-WIDE. Duplicate a template by id (copies content + theme binding; new slug required to avoid a hierarchy clash). Dry run unless confirm:true.', |
| 749 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'template_id' => [ 'type' => 'integer', 'minimum' => 1 ], 'slug' => [ 'type' => 'string' ], 'title' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'template_id', 'slug' ], 'additionalProperties' => false ], |
| 750 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'integer' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 751 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 752 | $gate = $self->confirm_gate( $input, 'Duplicating a template' ); if ( $gate ) { return $gate; } |
| 753 | $id = (int) $input['template_id']; $p = get_post( $id ); |
| 754 | if ( ! $p || $p->post_type !== $self->template_cpt() ) { return [ 'success' => false, 'message' => 'Not an Etch template id.' ]; } |
| 755 | $slug = sanitize_title( (string) $input['slug'] ); |
| 756 | if ( $slug === '' ) { return [ 'success' => false, 'message' => 'A valid slug is required.' ]; } |
| 757 | $new = wp_insert_post( [ 'post_type' => $self->template_cpt(), 'post_title' => isset( $input['title'] ) ? (string) $input['title'] : ( $p->post_title . ' (copy)' ), 'post_name' => $slug, 'post_content' => $p->post_content, 'post_status' => 'publish' ], true ); |
| 758 | if ( is_wp_error( $new ) ) { return [ 'success' => false, 'message' => $new->get_error_message() ]; } |
| 759 | $terms = wp_get_post_terms( $id, AVCF_Abilities_Etch_Pro::TEMPLATE_TAX, [ 'fields' => 'slugs' ] ); |
| 760 | if ( ! is_wp_error( $terms ) && $terms ) { wp_set_post_terms( $new, $terms, AVCF_Abilities_Etch_Pro::TEMPLATE_TAX ); } |
| 761 | return [ 'success' => true, 'id' => (int) $new, 'message' => 'Template duplicated.' ]; |
| 762 | }, |
| 763 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 764 | 'meta' => $this->write_meta( false ), |
| 765 | ] ); |
| 766 | } |
| 767 | |
| 768 | /* ------------------------- styles / stylesheets -------------------- */ |
| 769 | |
| 770 | private function register_styles() { |
| 771 | $self = $this; $std = $this->std_out(); |
| 772 | |
| 773 | wp_register_ability( 'atarim/etch-get-styles', [ |
| 774 | 'label' => 'Get Etch Styles', 'category' => 'atarim', |
| 775 | 'description' => 'Read the Etch styles layer (the etch_styles option) — the design tokens/variables layer. Returns the raw structure (empty is normal before any styles are saved).', |
| 776 | 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ], |
| 777 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'styles' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 778 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 779 | return [ 'success' => true, 'styles' => $self->opt_get( AVCF_Abilities_Etch_Pro::O_STYLES ), 'message' => 'OK.' ]; |
| 780 | }, |
| 781 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 782 | 'meta' => $this->ro_meta(), |
| 783 | ] ); |
| 784 | |
| 785 | wp_register_ability( 'atarim/etch-list-stylesheets', [ |
| 786 | 'label' => 'List Etch Stylesheets', 'category' => 'atarim', |
| 787 | 'description' => 'List Etch global stylesheets (etch_global_stylesheets): { id, name }. Fetch CSS with get-stylesheet.', |
| 788 | 'input_schema' => [ 'type' => 'object', 'properties' => [], 'additionalProperties' => false ], |
| 789 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'stylesheets' => [ 'type' => 'array' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 790 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 791 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_SHEETS ); |
| 792 | $out = []; |
| 793 | foreach ( $map as $id => $s ) { if ( is_array( $s ) ) { $out[] = [ 'id' => (string) $id, 'name' => $s['name'] ?? '' ]; } } |
| 794 | return [ 'success' => true, 'stylesheets' => $out, 'message' => sprintf( '%d stylesheet(s).', count( $out ) ) ]; |
| 795 | }, |
| 796 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 797 | 'meta' => $this->ro_meta(), |
| 798 | ] ); |
| 799 | |
| 800 | wp_register_ability( 'atarim/etch-get-stylesheet', [ |
| 801 | 'label' => 'Get Etch Stylesheet', 'category' => 'atarim', |
| 802 | 'description' => 'Get a global stylesheet by id: { id, name, css }.', |
| 803 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 804 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'stylesheet' => [ 'type' => 'object' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 805 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 806 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_SHEETS ); $id = (string) $input['id']; |
| 807 | if ( ! isset( $map[ $id ] ) || ! is_array( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Stylesheet "%s" not found.', $id ) ]; } |
| 808 | return [ 'success' => true, 'stylesheet' => [ 'id' => $id, 'name' => $map[ $id ]['name'] ?? '', 'css' => $map[ $id ]['css'] ?? '' ], 'message' => 'OK.' ]; |
| 809 | }, |
| 810 | 'permission_callback' => function() { return current_user_can( 'edit_posts' ); }, |
| 811 | 'meta' => $this->ro_meta(), |
| 812 | ] ); |
| 813 | |
| 814 | wp_register_ability( 'atarim/etch-create-stylesheet', [ |
| 815 | 'label' => 'Create Etch Stylesheet', 'category' => 'atarim', |
| 816 | 'description' => 'SITE-WIDE. Create a global stylesheet. name required; css is the stylesheet body. Dry run unless confirm:true.', |
| 817 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'name' => [ 'type' => 'string' ], 'css' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'name' ], 'additionalProperties' => false ], |
| 818 | 'output_schema'=> [ 'type' => 'object', 'properties' => [ 'success' => [ 'type' => 'boolean' ], 'id' => [ 'type' => 'string' ], 'message' => [ 'type' => 'string' ] ], 'required' => [ 'success', 'message' ] ], |
| 819 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 820 | $gate = $self->confirm_gate( $input, 'Creating a stylesheet' ); if ( $gate ) { return $gate; } |
| 821 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_SHEETS ); |
| 822 | $id = $self->gen_id(); |
| 823 | $map[ $id ] = [ 'name' => (string) $input['name'], 'css' => isset( $input['css'] ) ? (string) $input['css'] : '' ]; |
| 824 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_SHEETS, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 825 | return [ 'success' => true, 'id' => $id, 'message' => 'Stylesheet created.' ]; |
| 826 | }, |
| 827 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 828 | 'meta' => $this->write_meta( false ), |
| 829 | ] ); |
| 830 | |
| 831 | wp_register_ability( 'atarim/etch-edit-stylesheet', [ |
| 832 | 'label' => 'Edit Etch Stylesheet', 'category' => 'atarim', |
| 833 | 'description' => 'SITE-WIDE. Edit a stylesheet by id (name and/or css). Dry run unless confirm:true.', |
| 834 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'name' => [ 'type' => 'string' ], 'css' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 835 | 'output_schema'=> $std, |
| 836 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 837 | $gate = $self->confirm_gate( $input, 'Editing a stylesheet' ); if ( $gate ) { return $gate; } |
| 838 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_SHEETS ); $id = (string) $input['id']; |
| 839 | if ( ! isset( $map[ $id ] ) || ! is_array( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Stylesheet "%s" not found.', $id ) ]; } |
| 840 | if ( isset( $input['name'] ) ) { $map[ $id ]['name'] = (string) $input['name']; } |
| 841 | if ( isset( $input['css'] ) ) { $map[ $id ]['css'] = (string) $input['css']; } |
| 842 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_SHEETS, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 843 | return [ 'success' => true, 'message' => sprintf( 'Stylesheet "%s" updated.', $id ) ]; |
| 844 | }, |
| 845 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 846 | 'meta' => $this->write_meta( false ), |
| 847 | ] ); |
| 848 | |
| 849 | wp_register_ability( 'atarim/etch-delete-stylesheet', [ |
| 850 | 'label' => 'Delete Etch Stylesheet', 'category' => 'atarim', |
| 851 | 'description' => 'SITE-WIDE. Delete a stylesheet by id. Dry run unless confirm:true.', |
| 852 | 'input_schema' => [ 'type' => 'object', 'properties' => [ 'id' => [ 'type' => 'string' ], 'confirm' => [ 'type' => 'boolean', 'default' => false ] ], 'required' => [ 'id' ], 'additionalProperties' => false ], |
| 853 | 'output_schema'=> $std, |
| 854 | 'execute_callback' => function( $input = [] ) use ( $self ) { |
| 855 | $gate = $self->confirm_gate( $input, 'Deleting a stylesheet' ); if ( $gate ) { return $gate; } |
| 856 | $map = $self->opt_get( AVCF_Abilities_Etch_Pro::O_SHEETS ); $id = (string) $input['id']; |
| 857 | if ( ! isset( $map[ $id ] ) ) { return [ 'success' => false, 'message' => sprintf( 'Stylesheet "%s" not found.', $id ) ]; } |
| 858 | unset( $map[ $id ] ); |
| 859 | if ( ! $self->opt_set( AVCF_Abilities_Etch_Pro::O_SHEETS, $map ) ) { return [ 'success' => false, 'message' => 'Failed to save.' ]; } |
| 860 | return [ 'success' => true, 'message' => sprintf( 'Stylesheet "%s" deleted.', $id ) ]; |
| 861 | }, |
| 862 | 'permission_callback' => function() use ( $self ) { return $self->site_can(); }, |
| 863 | 'meta' => $this->write_meta( true ), |
| 864 | ] ); |
| 865 | } |
| 866 | } |
| 867 |