field_manager = new MetaForge_FieldManager(); $this->init_hooks(); } private function init_hooks() { add_action('init', array($this, 'init')); add_action('admin_menu', array($this, 'admin_menu')); add_filter('parent_file', array($this, 'filter_parent_file')); add_action('add_meta_boxes', array($this, 'register_meta_box')); add_action('save_post', array($this, 'save_meta_box')); add_action('wp_ajax_metaforge_update_group_order', array($this, 'ajax_update_group_order')); add_action('wp_ajax_metaforge_delete_group', array($this, 'ajax_delete_group')); add_action('wp_ajax_metaforge_duplicate_group', array($this, 'ajax_duplicate_group')); add_filter('set-screen-option', array($this, 'set_screen_option'), 10, 3); add_filter('screen_options_show_screen', array($this, 'disable_screen_options'), 10, 2); // Taxonomy term hooks add_action('admin_init', array($this, 'register_taxonomy_hooks')); add_action('admin_init', array($this, 'handle_admin_actions')); add_action('admin_notices', array($this, 'render_repair_notice')); // User profile hooks add_action('show_user_profile', array($this, 'render_user_fields')); add_action('edit_user_profile', array($this, 'render_user_fields')); add_action('personal_options_update', array($this, 'save_user_fields')); add_action('edit_user_profile_update', array($this, 'save_user_fields')); // Comment hooks add_action('add_meta_boxes_comment', array($this, 'register_comment_meta_box')); add_action('edit_comment', array($this, 'save_comment_fields')); // Revision hooks add_action('wp_creating_post_revision', array($this, 'save_revision_meta'), 10, 2); add_action('wp_restore_post_revision', array($this, 'restore_revision_meta'), 10, 2); add_filter('_wp_post_revision_fields', array($this, 'add_revision_fields_to_diff')); // Cache invalidation hooks add_action('save_post_metaforge_group', array(__CLASS__, 'clear_cache')); add_action( 'deleted_post', function ($post_id) { if (get_post_type($post_id) === 'metaforge_group') { self::clear_cache(); } } ); add_action( 'trashed_post', function ($post_id) { if (get_post_type($post_id) === 'metaforge_group') { self::clear_cache(); } } ); add_action( 'untrashed_post', function ($post_id) { if (get_post_type($post_id) === 'metaforge_group') { self::clear_cache(); } } ); } public function init() { add_action('admin_enqueue_scripts', array($this, 'enqueue_admin_assets')); // Register the options store post type globally so options can be queried on the front-end register_post_type( 'metaforge_options', array( 'public' => false, 'show_ui' => false, 'capability_type' => 'post', 'capabilities' => array('create_posts' => 'do_not_allow'), 'map_meta_cap' => true, 'supports' => array('title'), ) ); $this->register_dynamic_post_meta(); } /** * Dynamically registers custom fields using register_post_meta to make them * available to Gutenberg Block Bindings API and REST API. */ private function register_dynamic_post_meta() { $all_groups = get_transient('metaforge_all_field_groups_cache'); if (false === $all_groups) { $posts = get_posts( array( 'post_type' => 'metaforge_group', 'posts_per_page' => -1, 'post_status' => 'publish', ) ); $all_groups = array(); foreach ($posts as $post) { $config = get_post_meta($post->ID, '_metaforge_group_config', true); if (is_array($config)) { $all_groups[] = $config; } } set_transient('metaforge_all_field_groups_cache', $all_groups, HOUR_IN_SECONDS * 24); } if (empty($all_groups)) { return; } foreach ($all_groups as $config) { if (!is_array($config) || empty($config['fields'])) { continue; } $target_post_types = array(); foreach ($config['location_rules'] ?? array() as $rule_group) { foreach ($rule_group as $rule) { $param = $rule['param'] ?? ''; $val = $rule['value'] ?? ''; if ($param === 'post_type') { if ($val === 'all') { $target_post_types = array_merge($target_post_types, get_post_types(array('public' => true))); } else { $target_post_types[] = $val; } } elseif (in_array($param, array('page_template', 'page_type', 'page_parent', 'page'), true)) { $target_post_types[] = 'page'; } elseif (in_array($param, array('post', 'post_format', 'post_category'), true)) { $target_post_types[] = 'post'; } } } $target_post_types = array_unique(array_filter($target_post_types)); if (empty($target_post_types)) { $target_post_types = array('post', 'page'); } foreach ($config['fields'] as $field) { $name = $field['name'] ?? ''; $type = $field['type'] ?? 'text'; if (!$name) { continue; } // Only register simple scalar fields to prevent WordPress from intercepting and breaking complex serialized arrays $allowed_types = array('text', 'textarea', 'number', 'color_picker', 'date_time', 'true_false', 'select', 'oembed'); $is_simple = in_array($type, $allowed_types, true); if ($type === 'select' && !empty($field['multiple'])) { $is_simple = false; } if ($is_simple) { $rest_type = 'string'; if ($type === 'true_false') { $rest_type = 'boolean'; } elseif ($type === 'number') { $rest_type = 'number'; } $args = array( 'show_in_rest' => true, 'single' => true, 'type' => $rest_type, ); foreach ($target_post_types as $post_type) { register_post_meta($post_type, $name, $args); } } add_filter( "_wp_post_revision_field_metaforge_field_{$name}", function ($value, $field_key, $post, $direction) use ($name) { $post_id = $post->ID; $val = wp_is_post_revision($post_id) ? get_post_meta($post_id, '_metaforge_rev_' . $name, true) : get_post_meta($post_id, $name, true); if (is_array($val)) { return wp_json_encode($val, JSON_HEX_TAG | JSON_HEX_AMP | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE); } return (string) $val; }, 10, 4 ); } } } public function enqueue_admin_assets($hook) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only URL query parameter for admin page asset routing. $page = isset($_GET['page']) ? sanitize_text_field(wp_unslash($_GET['page'])) : ''; $is_builder = (strpos($hook, 'metaforge') !== false) || (strpos($hook, 'metaforge_') !== false) || (strpos($hook, 'mfe-') !== false) || (strpos($page, 'metaforge') !== false) || (strpos($page, 'mfe') !== false) || in_array( $hook, array( 'toplevel_page_metaforge', 'metaforge_page_metaforge_post_types', 'metaforge_page_metaforge_taxonomies', 'metaforge_page_metaforge_tools', 'metaforge_page_metaforge_ai_settings', 'metaforge_page_metaforge_post_order', 'metaforge_page_metaforge_options_pages', 'metaforge_page_metaforge_term_order', 'metaforge_page_metaforge_pro_license', 'metaforge_page_metaforge_white_label', 'admin_page_metaforge_white_label', 'admin_page_metaforge_white_label', 'admin_page_metaforge_pro_license', ), true ); $is_options_page = (strpos($hook, 'metaforge_opts_') !== false) || (strpos($hook, '_page_metaforge_opts_') !== false) || (strpos($page, 'metaforge_opts_') !== false); $is_post = in_array($hook, array('post.php', 'post-new.php'), true); $is_term = in_array($hook, array('term.php', 'edit-tags.php'), true); $is_user = in_array($hook, array('profile.php', 'user-edit.php'), true); $is_comment = ($hook === 'comment.php'); $is_reorder = (strpos($hook, 'metaforge_reorder_') !== false) || (strpos($hook, 'metaforge_term_reorder_') !== false); if (!$is_builder && !$is_post && !$is_reorder && !$is_options_page && !$is_term && !$is_user && !$is_comment) { return; } $admin_js_ver = METAFORGE_VERSION . '.' . (file_exists(METAFORGE_PATH . 'assets/js/admin.js') ? filemtime(METAFORGE_PATH . 'assets/js/admin.js') : '1'); $field_js_ver = METAFORGE_VERSION . '.' . (file_exists(METAFORGE_PATH . 'assets/js/field.js') ? filemtime(METAFORGE_PATH . 'assets/js/field.js') : '1'); $vendor_ver = '1.15.2.14'; $admin_css_ver = METAFORGE_VERSION . '.' . (file_exists(METAFORGE_PATH . 'assets/css/admin.css') ? filemtime(METAFORGE_PATH . 'assets/css/admin.css') : '1'); $field_css_ver = METAFORGE_VERSION . '.' . (file_exists(METAFORGE_PATH . 'assets/css/field.css') ? filemtime(METAFORGE_PATH . 'assets/css/field.css') : '1'); wp_enqueue_media(); // Vendor scripts wp_register_script('metaforge-sortable-js', METAFORGE_URL . 'assets/js/vendor/sortable.min.js', array(), '1.15.7', true); wp_register_script('metaforge-select2-js', METAFORGE_URL . 'assets/js/vendor/select2.min.js', array('jquery'), '4.1.0', true); wp_register_style('metaforge-select2-css', METAFORGE_URL . 'assets/css/vendor/select2.min.css', array(), '4.1.0'); // WP color picker (needed on both screens for color fields) wp_enqueue_style('wp-color-picker'); wp_enqueue_script('wp-color-picker'); // Register Manager Hooks // Register separated scripts wp_register_script('metaforge-field-js', METAFORGE_URL . 'assets/js/field.js', array('jquery', 'metaforge-sortable-js', 'metaforge-select2-js', 'wp-color-picker'), $field_js_ver, true); wp_localize_script( 'metaforge-field-js', 'metaforgeFieldConfig', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('metaforge_dynamic_tax_nonce'), 'oembedNonce' => wp_create_nonce('metaforge_oembed_nonce'), ) ); wp_register_script('metaforge-admin-js', METAFORGE_URL . 'assets/js/admin.js', array('jquery', 'jquery-ui-sortable'), $admin_js_ver, true); // Enqueue correctly based on screen if ($is_builder) { wp_enqueue_script('metaforge-admin-js'); wp_localize_script( 'metaforge-admin-js', 'metaforgeAiConfig', array( 'ajaxUrl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('metaforge_ai_layout_nonce'), 'reorderNonce' => wp_create_nonce('metaforge_reorder_nonce'), 'deleteGroupNonce' => wp_create_nonce('metaforge_delete_group_nonce'), 'duplicateGroupNonce' => wp_create_nonce('metaforge_duplicate_group_nonce'), 'deleteCptNonce' => wp_create_nonce('metaforge_delete_cpt_nonce'), 'deleteTaxNonce' => wp_create_nonce('metaforge_delete_tax_nonce'), 'cptNonce' => wp_create_nonce('metaforge_cpt_generation_nonce'), 'taxNonce' => wp_create_nonce('metaforge_taxonomy_generation_nonce'), 'testConnectionNonce' => wp_create_nonce('metaforge_ai_test_connection_nonce'), 'isPro' => metaforge_is_pro_active(), ) ); } if ($is_post || $is_options_page || $is_term || $is_user || $is_comment) { wp_enqueue_editor(); wp_enqueue_script('metaforge-field-js'); wp_enqueue_style('metaforge-select2-css'); wp_enqueue_script('wplink'); wp_enqueue_style('editor-buttons'); add_action('admin_footer', array($this, 'output_link_dialog')); } // admin.css — builder UI only (field group list + builder form) and options pages if ($is_builder || $is_reorder || $is_options_page) { wp_enqueue_style('metaforge-admin-css', METAFORGE_URL . 'assets/css/admin.css', array(), $admin_css_ver); } // field.css — post/page edit meta box and options pages if ($is_post || $is_options_page || $is_term || $is_user || $is_comment) { wp_enqueue_style('metaforge-field-css', METAFORGE_URL . 'assets/css/field.css', array('metaforge-select2-css'), $field_css_ver); } } public function output_link_dialog() { if (!class_exists('_WP_Editors', false)) { require_once ABSPATH . 'wp-includes/class-wp-editor.php'; } _WP_Editors::wp_link_dialog(); } public function admin_menu() { global $admin_page_hooks; if (isset($admin_page_hooks['metaforge'])) { return; } $wl_settings = get_option('metaforge_pro_white_label_settings', array()); $brand_name = !empty($wl_settings['brand_name']) ? $wl_settings['brand_name'] : apply_filters('metaforge_admin_plugin_name', 'MetaForge'); $main_hook = add_menu_page( $brand_name, $brand_name, 'manage_metaforge', 'metaforge', array($this, 'admin_page'), 'dashicons-database' ); add_submenu_page( 'metaforge', 'Field Groups', 'Field Groups', 'manage_metaforge', 'metaforge', null ); add_action("load-{$main_hook}", array($this, 'load_field_groups_screen')); } public function set_screen_option($status, $option, $value) { if (in_array($option, array('metaforge_field_groups_per_page', 'metaforge_post_types_per_page', 'metaforge_taxonomies_per_page', 'metaforge_options_pages_per_page'), true)) { return (int) $value; } return $status; } public function disable_screen_options($show_screen, $screen) { if ( $screen && ( strpos($screen->id, 'metaforge') !== false || strpos($screen->id, 'mfe') !== false ) ) { return false; } // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; if ($page && (strpos($page, 'metaforge') !== false || strpos($page, 'mfe') !== false)) { return false; } return $show_screen; } public function load_field_groups_screen() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $action = isset($_GET['action']) ? sanitize_key(wp_unslash($_GET['action'])) : ''; if ($action !== 'add' && $action !== 'edit') { require_once METAFORGE_PATH . 'admin/FieldGroupListTable.php'; $this->field_group_list_table = new MetaForge_FieldGroupListTable(); $screen = get_current_screen(); if ($screen) { register_column_headers($screen, $this->field_group_list_table->get_columns()); } } } public function filter_parent_file($parent_file) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended $page = isset($_GET['page']) ? sanitize_key(wp_unslash($_GET['page'])) : ''; if (in_array($page, array('metaforge_post_order', 'metaforge_term_order'), true)) { return 'metaforge'; } return $parent_file; } public function admin_page() { // S-1: Gate the entire page — no action should be processed without capability. if (!current_user_can('manage_metaforge')) { wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'metaforge-engine')); } // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only UI action parameter. $action = isset($_GET['action']) ? sanitize_key(wp_unslash($_GET['action'])) : ''; if ($action === 'add' || $action === 'edit') { $this->render_group_form(); return; } $this->render_group_list(); } private function render_group_list() { // If viewing sync status and no sync items remain, redirect back to main list // phpcs:ignore WordPress.Security.NonceVerification.Recommended $status = isset($_GET['status']) ? sanitize_key(wp_unslash($_GET['status'])) : 'all'; $sync_count = MetaForge_JSONSyncManager::count_pending_sync(); if ($status === 'sync' && $sync_count === 0) { $redirect_args = array(); // phpcs:disable WordPress.Security.NonceVerification.Recommended if (isset($_GET['synced'])) { $redirect_args['synced'] = intval(wp_unslash($_GET['synced'])); } if (isset($_GET['sync_deleted'])) { $redirect_args['sync_deleted'] = 1; } // phpcs:enable WordPress.Security.NonceVerification.Recommended $target_url = !empty($redirect_args) ? add_query_arg($redirect_args, admin_url('admin.php?page=metaforge')) : admin_url('admin.php?page=metaforge'); wp_safe_redirect($target_url); exit; } if (isset($this->field_group_list_table)) { $list_table = $this->field_group_list_table; } else { require_once METAFORGE_PATH . 'admin/FieldGroupListTable.php'; $list_table = new MetaForge_FieldGroupListTable(); } $list_table->prepare_items(); // phpcs:ignore WordPress.Security.NonceVerification.Recommended $search_val = isset($_REQUEST['s']) ? sanitize_text_field(wp_unslash($_REQUEST['s'])) : ''; wp_cache_delete('metaforge_group', 'counts'); $total_groups = (int) (wp_count_posts('metaforge_group')->publish ?? 0); $is_empty = ($total_groups === 0 && $sync_count === 0 && empty($search_val) && ($status === 'all' || empty($status))); ?>

0): $synced_num = intval(wp_unslash($_GET['synced'])); ?>

views(); ?> display(); ?>
8) { // Return null (not false) so upstream array_filter can cleanly remove it // without corrupting the parent structure return null; } if (empty($raw_field['name'])) { return null; } // ── Phase 2: Schema-Level Type Whitelisting ─────────────────────────── $raw_type = sanitize_text_field($raw_field['type'] ?? 'text'); // Prevent accidental downgrade to 'text' if field contains sub-fields or layouts if ($raw_type === 'text') { if (!empty($raw_field['sub_fields'])) { $raw_type = 'repeater'; } elseif (!empty($raw_field['layouts'])) { $raw_type = 'flexible_content'; } } $allowed = apply_filters('metaforge/allowed_field_types', self::$allowed_field_types); if (!in_array($raw_type, $allowed, true)) { // Log the invalid attempt for forensic visibility MetaForge_Logger::log( 'invalid_field_type', array( 'rejected_type' => $raw_type, 'field_name' => $raw_field['name'] ?? 'unknown', ) ); $raw_type = 'text'; // Gracefully fall back — never crash on bad type } $options = array(); $raw_options_string = ''; if (!empty($raw_field['options'])) { if (is_array($raw_field['options'])) { $opt_lines = array(); foreach ($raw_field['options'] as $k => $v) { if (is_array($v)) { $key = isset($v['key']) ? trim((string) $v['key']) : (isset($v['value']) ? trim((string) $v['value']) : trim((string) $k)); $val = isset($v['value']) ? trim((string) $v['value']) : (isset($v['label']) ? trim((string) $v['label']) : (string) $key); } else { $key = is_int($k) ? strtolower(preg_replace('/[^a-zA-Z0-9]+/', '_', trim((string) $v))) : trim((string) $k); $key = trim($key, '_'); $val = trim((string) $v); } if ($key === $val || (strpos($val, '_') !== false && strpos($val, ' ') === false)) { $val = ucwords(str_replace('_', ' ', $val)); } $options[$key] = $val; $opt_lines[] = $key . ' : ' . $val; } $raw_options_string = implode("\n", $opt_lines); } else { $raw_options_string = is_string($raw_field['options']) ? sanitize_textarea_field(wp_unslash($raw_field['options'])) : ''; foreach (explode("\n", $raw_options_string) as $line) { $line = trim($line); if (empty($line)) { continue; } $parts = explode(':', $line, 2); if (count($parts) === 2) { $key = trim($parts[0]); $val = trim($parts[1]); if ($key === $val || (strpos($val, '_') !== false && strpos($val, ' ') === false)) { $val = ucwords(str_replace('_', ' ', $val)); } $options[$key] = $val; } else { $slug = strtolower(preg_replace('/[^a-zA-Z0-9]+/', '_', trim($line))); $slug = trim($slug, '_'); $label = trim($line); if ($label === $slug || (strpos($label, '_') !== false && strpos($label, ' ') === false)) { $label = ucwords(str_replace('_', ' ', $label)); } $options[$slug] = $label; } } } } $sub_fields = array(); if (!empty($raw_field['sub_fields']) && is_array($raw_field['sub_fields'])) { // Cap sub_fields per level to 200 — prevents horizontal memory bombs $raw_sub = array_slice($raw_field['sub_fields'], 0, 200); foreach ($raw_sub as $sub) { // Pass incremented depth to sub-fields $s = self::sanitize_builder_field($sub, $depth + 1); if ($s !== null) { // Use null-check instead of truthy $sub_fields[] = $s; } } } // ── Flexible Content Layouts ────────────────────────────────────────────── $layouts = array(); if (!empty($raw_field['layouts']) && is_array($raw_field['layouts'])) { $raw_layouts = array_slice($raw_field['layouts'], 0, 50); foreach ($raw_layouts as $li => $layout) { $raw_label = sanitize_text_field(wp_unslash($layout['label'] ?? '')); $raw_name = !empty($layout['name']) ? sanitize_key($layout['name']) : sanitize_key($raw_label); if (empty($raw_name)) { $raw_name = 'layout_' . ($li + 1); $raw_label = !empty($raw_label) ? $raw_label : 'Layout ' . ($li + 1); } elseif (empty($raw_label)) { $raw_label = ucwords(str_replace('_', ' ', $raw_name)); } $layout_sub_fields = array(); if (!empty($layout['sub_fields']) && is_array($layout['sub_fields'])) { $raw_layout_sub = array_slice($layout['sub_fields'], 0, 100); foreach ($raw_layout_sub as $lsf) { $ls = self::sanitize_builder_field($lsf, $depth + 1); if ($ls !== null) { $layout_sub_fields[] = $ls; } } } $layouts[] = array( 'name' => $raw_name, 'label' => $raw_label, 'sub_fields' => $layout_sub_fields, ); } } $conditional_logic = isset($raw_field['conditional_logic']) ? intval($raw_field['conditional_logic']) : 0; $conditions = array(); if ($conditional_logic && !empty($raw_field['conditions']) && is_array($raw_field['conditions'])) { foreach ($raw_field['conditions'] as $rule) { if (!empty($rule['field'])) { $conditions[] = array( 'field' => sanitize_key($rule['field']), 'operator' => sanitize_text_field($rule['operator']), 'value' => sanitize_text_field(wp_unslash($rule['value'] ?? '')), ); } } } return array( 'name' => sanitize_key($raw_field['name']), 'label' => sanitize_text_field(wp_unslash($raw_field['label'] ?? '')), 'type' => $raw_type, // already validated above 'options' => $options, 'options_raw' => sanitize_textarea_field($raw_options_string), 'sub_fields' => $sub_fields, 'layouts' => $layouts, 'conditional_logic' => $conditional_logic, 'conditions' => $conditions, 'layout' => sanitize_text_field($raw_field['layout'] ?? 'vertical'), 'width' => sanitize_text_field($raw_field['width'] ?? '100'), 'rows' => isset($raw_field['rows']) ? sanitize_text_field($raw_field['rows']) : '4', 'new_lines' => isset($raw_field['new_lines']) ? sanitize_text_field($raw_field['new_lines']) : 'br', 'required' => !empty($raw_field['required']) ? 1 : 0, 'sub_layout' => sanitize_text_field($raw_field['sub_layout'] ?? 'block'), 'image_return_format' => sanitize_text_field($raw_field['image_return_format'] ?? 'array'), 'dynamic_source' => sanitize_key($raw_field['dynamic_source'] ?? ''), 'taxonomy_slug' => sanitize_key($raw_field['taxonomy_slug'] ?? ''), 'appearance' => sanitize_key($raw_field['appearance'] ?? 'select'), 'allow_empty' => (!empty($raw_field['allow_empty']) || !empty($raw_field['sub_allow_empty'])) ? 1 : 0, 'endpoint' => !empty($raw_field['endpoint']) ? 1 : 0, 'unit' => isset($raw_field['unit']) ? sanitize_text_field($raw_field['unit']) : 'px', 'min' => isset($raw_field['min']) ? sanitize_text_field($raw_field['min']) : '', 'max' => isset($raw_field['max']) ? sanitize_text_field($raw_field['max']) : '', 'step' => isset($raw_field['step']) ? sanitize_text_field($raw_field['step']) : '', 'placeholder' => isset($raw_field['placeholder']) ? sanitize_text_field(wp_unslash($raw_field['placeholder'])) : '', 'default_value' => isset($raw_field['default_value']) ? sanitize_text_field(wp_unslash($raw_field['default_value'])) : '', ); } public static function sanitize_group_config($group) { if (!is_array($group)) { return null; } $sanitized_fields = array(); if (!empty($group['fields']) && is_array($group['fields'])) { foreach ($group['fields'] as $field) { $sf = self::sanitize_builder_field($field, 0); if ($sf !== null) { $sanitized_fields[] = $sf; } } } $sanitized_rules = array(); if (!empty($group['location_rules']) && is_array($group['location_rules'])) { foreach ($group['location_rules'] as $rule_group) { if (!is_array($rule_group)) { continue; } $sanitized_rule_group = array(); foreach ($rule_group as $rule) { if (!is_array($rule)) { continue; } $sanitized_rule_group[] = array( 'param' => sanitize_key($rule['param'] ?? ''), 'operator' => sanitize_text_field($rule['operator'] ?? '=='), 'value' => sanitize_text_field(wp_unslash($rule['value'] ?? '')), ); } if (!empty($sanitized_rule_group)) { $sanitized_rules[] = $sanitized_rule_group; } } } return array( 'group_name' => sanitize_text_field($group['group_name'] ?? ''), 'post_type' => sanitize_text_field($group['post_type'] ?? 'post'), 'location_rules' => $sanitized_rules, 'fields' => $sanitized_fields, 'style' => sanitize_text_field($group['style'] ?? 'default'), 'position' => sanitize_text_field($group['position'] ?? 'normal'), 'active' => !empty($group['active']) ? 1 : 0, 'schema_version' => isset($group['schema_version']) ? intval($group['schema_version']) : 1, 'register_as_block' => !empty($group['register_as_block']) ? 1 : 0, 'block_title' => !empty($group['block_title']) ? sanitize_text_field($group['block_title']) : sanitize_text_field($group['group_name'] ?? 'Block'), 'block_slug' => !empty($group['block_slug']) ? sanitize_title($group['block_slug']) : sanitize_title($group['group_name'] ?? 'block'), 'block_category' => sanitize_text_field($group['block_category'] ?? 'common'), 'block_icon' => sanitize_text_field($group['block_icon'] ?? 'admin-generic'), 'group_key' => sanitize_key($group['group_key'] ?? ''), ); } public static function sanitize_post_type_config($cpt) { if (!is_array($cpt)) { return null; } return array( 'plural_label' => sanitize_text_field($cpt['plural_label'] ?? ''), 'singular_label' => sanitize_text_field($cpt['singular_label'] ?? ''), 'slug' => sanitize_key($cpt['slug'] ?? ''), 'menu_icon' => sanitize_text_field($cpt['menu_icon'] ?? 'dashicons-admin-post'), 'public' => !empty($cpt['public']) ? 1 : 0, 'has_archive' => !empty($cpt['has_archive']) ? 1 : 0, 'show_in_rest' => !empty($cpt['show_in_rest']) ? 1 : 0, 'supports' => isset($cpt['supports']) && is_array($cpt['supports']) ? array_map('sanitize_text_field', $cpt['supports']) : array(), ); } public static function sanitize_taxonomy_config($tax) { if (!is_array($tax)) { return null; } return array( 'plural_label' => sanitize_text_field($tax['plural_label'] ?? ''), 'singular_label' => sanitize_text_field($tax['singular_label'] ?? ''), 'slug' => sanitize_key($tax['slug'] ?? ''), 'hierarchical' => !empty($tax['hierarchical']) ? 1 : 0, 'public' => !empty($tax['public']) ? 1 : 0, 'show_in_rest' => !empty($tax['show_in_rest']) ? 1 : 0, 'show_admin_column' => !empty($tax['show_admin_column']) ? 1 : 0, 'post_types' => isset($tax['post_types']) && is_array($tax['post_types']) ? array_map('sanitize_text_field', $tax['post_types']) : array(), ); } public static function sanitize_options_page_config($opt) { if (!is_array($opt)) { return null; } $parent = sanitize_text_field($opt['parent'] ?? 'metaforge_options_pages'); if ($parent === 'mfe_options_pages' || empty($parent)) { $parent = 'metaforge_options_pages'; } return array( 'title' => sanitize_text_field($opt['title'] ?? ''), 'slug' => sanitize_key($opt['slug'] ?? ''), 'parent' => $parent, ); } public static function clear_cache() { delete_transient('metaforge_all_field_groups_cache'); delete_transient('metaforge_field_flat_map'); wp_cache_delete('metaforge_group', 'counts'); do_action('metaforge/clear_cache'); } // ── Field group form (Add / Edit) ───────────────────────────────────────── public function handle_admin_actions() { // Only run for users with management capability if (!is_admin() || !current_user_can('manage_metaforge')) { return; } // Handle Repairing Broken Images if (isset($_GET['metaforge_repair_images']) && sanitize_text_field(wp_unslash($_GET['metaforge_repair_images'])) === '1') { check_admin_referer('metaforge_repair_images'); $repaired = $this->repair_broken_images(); wp_safe_redirect(admin_url('admin.php?page=metaforge&metaforge_repaired=' . $repaired)); exit; } // Handle Single Group Sync if (isset($_GET['page']) && sanitize_key(wp_unslash($_GET['page'])) === 'metaforge' && isset($_GET['action']) && sanitize_key(wp_unslash($_GET['action'])) === 'sync_one') { check_admin_referer('metaforge_sync_one', '_wpnonce'); $key = isset($_GET['key']) ? sanitize_key(wp_unslash($_GET['key'])) : ''; if ($key) { $count = MetaForge_JSONSyncManager::sync_groups(array($key)); $remaining = MetaForge_JSONSyncManager::count_pending_sync(); $redirect_url = ($remaining > 0) ? admin_url('admin.php?page=metaforge&status=sync&synced=' . $count) : admin_url('admin.php?page=metaforge&synced=' . $count); wp_safe_redirect($redirect_url); exit; } } // Handle Single Group Sync File Deletion if (isset($_GET['page']) && sanitize_key(wp_unslash($_GET['page'])) === 'metaforge' && isset($_GET['action']) && sanitize_key(wp_unslash($_GET['action'])) === 'delete_sync_file') { check_admin_referer('metaforge_delete_sync_file', '_wpnonce'); if (!current_user_can('manage_metaforge')) { wp_die(esc_html__('You do not have sufficient permissions to access this page.', 'metaforge-engine')); } $key = isset($_GET['key']) ? sanitize_text_field(wp_unslash($_GET['key'])) : ''; if ($key) { MetaForge_JSONSyncManager::delete_sync_file_by_key($key); } $remaining = MetaForge_JSONSyncManager::count_pending_sync(); $redirect_url = ($remaining > 0) ? admin_url('admin.php?page=metaforge&status=sync&sync_deleted=1') : admin_url('admin.php?page=metaforge&sync_deleted=1'); wp_safe_redirect($redirect_url); exit; } // 1. Handle Field Group Deletion if (isset($_GET['page']) && sanitize_key(wp_unslash($_GET['page'])) === 'metaforge' && isset($_GET['action']) && sanitize_key(wp_unslash($_GET['action'])) === 'delete') { check_admin_referer('metaforge_delete_group_get', '_wpnonce'); $index = isset($_GET['index']) ? absint(wp_unslash($_GET['index'])) : 0; if ($index > 0) { MetaForge_JSONSyncManager::delete_group($index); wp_delete_post($index, true); self::clear_cache(); } wp_safe_redirect(admin_url('admin.php?page=metaforge&deleted=true')); exit; } // 1b. Handle Field Group Duplication if (isset($_GET['page']) && sanitize_key(wp_unslash($_GET['page'])) === 'metaforge' && isset($_GET['action']) && sanitize_key(wp_unslash($_GET['action'])) === 'duplicate') { check_admin_referer('metaforge_duplicate_group', '_wpnonce'); $index = isset($_GET['index']) ? absint(wp_unslash($_GET['index'])) : 0; if ($index > 0) { $post = get_post($index); if ($post && $post->post_type === 'metaforge_group') { $config = get_post_meta($index, '_metaforge_group_config', true); if (is_array($config)) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verified above via check_admin_referer. $requested_name = isset($_GET['new_name']) ? sanitize_text_field(wp_unslash($_GET['new_name'])) : ''; $new_name = !empty($requested_name) ? $requested_name : (($config['group_name'] ?? $post->post_title) . ' (Copy)'); $config['group_name'] = $new_name; $config['group_key'] = 'group_' . substr(md5($new_name . time()), 0, 12); $new_post_id = wp_insert_post( array( 'post_title' => $new_name, 'post_type' => 'metaforge_group', 'post_status' => 'publish', 'menu_order' => $post->menu_order + 1, ) ); if ($new_post_id && !is_wp_error($new_post_id)) { MetaForge_JSONSyncManager::save_group($new_post_id, $config); self::clear_cache(); wp_safe_redirect(admin_url('admin.php?page=metaforge&duplicated=true')); exit; } } } } wp_safe_redirect(admin_url('admin.php?page=metaforge')); exit; } // 2. Handle Field Group Saving if (isset($_POST['metaforge_save_fields'])) { check_admin_referer('metaforge_save_group', 'metaforge_save_group_nonce'); $post_id = isset($_GET['index']) ? absint(wp_unslash($_GET['index'])) : null; // Check for JSON-based payload first (to bypass max_input_vars limits) if (!empty($_POST['metaforge_group_json'])) { $raw_json = sanitize_textarea_field(wp_unslash($_POST['metaforge_group_json'])); $json_data = json_decode($raw_json, true, 64); if (is_array($json_data)) { $_POST = array_merge($_POST, map_deep($json_data, 'sanitize_textarea_field')); } } $validation_error = null; $group_name = sanitize_text_field(wp_unslash($_POST['group_name'] ?? '')); if (empty($group_name)) { $validation_error = '⚠️ Field group name cannot be empty.'; } else { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Duplicate field group title lookup query. $duplicate_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = 'metaforge_group' AND post_title = %s AND post_status = 'publish' AND ID != %d", $group_name, $post_id ?: 0 ) ); if ($duplicate_id) { $validation_error = '⚠️ A field group with this name already exists. Please use a unique name to prevent conflicts.'; } } // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Field and location rule arrays unslashed and sanitized item-by-item below. $raw_fields_input = (!empty($_POST['fields']) && is_array($_POST['fields'])) ? wp_unslash($_POST['fields']) : array(); if (count($raw_fields_input) > 500) { $validation_error = '⚠️ Too many fields: maximum 500 top-level fields are allowed per group.'; } $fields = array(); if (!$validation_error && !empty($raw_fields_input)) { foreach ($raw_fields_input as $field) { $sf = $this->sanitize_builder_field($field); if ($sf !== null) { $fields[] = $sf; } } } $location_rules = array(); if (!empty($_POST['location_rules']) && is_array($_POST['location_rules'])) { foreach (wp_unslash($_POST['location_rules']) as $rule_group) { if (is_array($rule_group)) { $sanitized_group = array(); foreach ($rule_group as $rule) { $sanitized_group[] = array( 'param' => sanitize_text_field($rule['param'] ?? ''), 'operator' => sanitize_text_field($rule['operator'] ?? ''), 'value' => sanitize_text_field($rule['value'] ?? ''), ); } if (!empty($sanitized_group)) { $location_rules[] = $sanitized_group; } } } } // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $primary_post_type = 'post'; if (!empty($location_rules)) { foreach ($location_rules as $rg) { foreach ($rg as $r) { $param = $r['param'] ?? ''; $val = $r['value'] ?? ''; if ($param === 'post_type' && !empty($val)) { $primary_post_type = $val; break 2; } elseif (in_array($param, array('page_template', 'page_type', 'page_parent', 'page'), true)) { $primary_post_type = 'page'; } } } } elseif (!empty($_POST['post_type'])) { $primary_post_type = sanitize_text_field(wp_unslash($_POST['post_type'])); $location_rules = array( array( array( 'param' => 'post_type', 'operator' => '==', 'value' => $primary_post_type, ), ), ); } else { $location_rules = array( array( array( 'param' => 'post_type', 'operator' => '==', 'value' => 'post', ), ), ); } $new_group = array( 'group_name' => sanitize_text_field(wp_unslash($_POST['group_name'] ?? '')), 'post_type' => $primary_post_type, 'location_rules' => $location_rules, 'fields' => $fields, 'style' => sanitize_text_field(wp_unslash($_POST['style'] ?? 'default')), 'position' => sanitize_text_field(wp_unslash($_POST['position'] ?? 'normal')), 'active' => !empty($_POST['active']) ? 1 : 0, 'active_settings_tab' => !empty($_POST['active_settings_tab']) ? sanitize_key(wp_unslash($_POST['active_settings_tab'])) : 'location', 'schema_version' => 1, // Block Bridge settings 'register_as_block' => !empty($_POST['register_as_block']) ? 1 : 0, 'block_title' => !empty($_POST['block_title']) ? sanitize_text_field(wp_unslash($_POST['block_title'])) : $group_name, 'block_slug' => !empty($_POST['block_slug']) ? sanitize_title(wp_unslash($_POST['block_slug'])) : sanitize_title($group_name ?: 'block-' . ($post_id ?: time())), 'block_category' => sanitize_text_field(wp_unslash($_POST['block_category'] ?? 'common')), 'block_icon' => sanitize_text_field(wp_unslash($_POST['block_icon'] ?? 'admin-generic')), ); if ($validation_error) { set_transient('metaforge_validation_error_' . get_current_user_id(), $validation_error, 30); set_transient('metaforge_temp_group_config_' . get_current_user_id(), $new_group, 30); $redirect_url = admin_url('admin.php?page=metaforge&action=edit'); if ($post_id) { $redirect_url = add_query_arg('index', $post_id, $redirect_url); } wp_safe_redirect($redirect_url); exit; } $post_args = array( 'post_title' => $new_group['group_name'] ?: 'New Field Group', 'post_type' => 'metaforge_group', 'post_status' => 'publish', ); if ($post_id) { $post_args['ID'] = $post_id; wp_update_post($post_args); } else { $post_id = wp_insert_post($post_args); } if ($post_id && !is_wp_error($post_id)) { MetaForge_JSONSyncManager::save_group($post_id, $new_group); } self::clear_cache(); MetaForge_Logger::log( 'field_group_saved', array( 'group' => $new_group['group_name'], 'index' => $post_id, 'field_count' => count($fields), ) ); wp_safe_redirect(admin_url('admin.php?page=metaforge&action=edit&index=' . $post_id . '&saved=1')); exit; } } public function render_repair_notice() { if (!current_user_can('manage_metaforge')) { return; } // Only show notices on MetaForge settings/builder page to avoid clutter $screen = get_current_screen(); if (!$screen || strpos($screen->id, 'metaforge') === false) { return; } // phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only notice query parameter. if (isset($_GET['metaforge_repaired'])) { $count = intval(wp_unslash($_GET['metaforge_repaired'])); echo '
'; // translators: %d: Number of repaired images. echo '

' . esc_html( sprintf( // translators: %d: Number of repaired images. __('Successfully repaired %d AI placeholder image(s).', 'metaforge-engine'), $count ) ) . '

'; echo '
'; return; } // phpcs:enable WordPress.Security.NonceVerification.Recommended global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Attachment count query for AI placeholder repair notice. $count = $wpdb->get_var( "SELECT COUNT(ID) FROM {$wpdb->posts} WHERE post_type = 'attachment' AND (post_title LIKE '%AI Placeholder%' OR post_content LIKE '%AI Placeholder%')" ); if ($count > 0) { $repair_url = wp_nonce_url(admin_url('admin.php?page=metaforge&metaforge_repair_images=1'), 'metaforge_repair_images'); echo '
'; echo '

' . wp_kses_post( sprintf( // translators: %d: Number of placeholder images. esc_html__('MetaForge detected %d AI placeholder attachments. If some images are showing as broken, you can repair them automatically.', 'metaforge-engine'), intval($count) ) ) . ' ' . esc_html__('Repair Images Now', 'metaforge-engine') . '

'; echo '
'; } } private function repair_broken_images() { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Attachment list query for AI placeholder repair. $attachments = $wpdb->get_results( "SELECT ID, post_title, post_content FROM {$wpdb->posts} WHERE post_type = 'attachment' AND (post_title LIKE '%AI Placeholder%' OR post_content LIKE '%AI Placeholder%')" ); if (empty($attachments)) { return 0; } $repaired_count = 0; foreach ($attachments as $attachment) { $file_path = get_attached_file($attachment->ID); if (empty($file_path)) { continue; } $is_broken = false; if (!file_exists($file_path)) { $is_broken = true; } else { // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- Local file, already confirmed to exist above; only the first 100 bytes are needed to sniff for corruption, which WP_Filesystem::get_contents() cannot do without reading the whole file. $bytes = is_readable($file_path) ? file_get_contents($file_path, false, null, 0, 100) : false; if ($bytes !== false) { if (strpos($bytes, 'post_content, $matches)) { $keyword = sanitize_text_field(trim($matches[1])); } elseif (preg_match('/AI Placeholder:\s*(.+)/i', $attachment->post_title, $matches)) { $keyword = sanitize_text_field(trim($matches[1])); } $title = !empty($keyword) ? ucwords(str_replace(array('-', '_'), ' ', $keyword)) : 'Placeholder Image'; $svg_data = '' . '' . '' . '' . '' . esc_html($title) . '' . '1200 × 800 • MetaForge Placeholder' . ''; $dir = dirname($file_path); if (!file_exists($dir)) { wp_mkdir_p($dir); } global $wp_filesystem; if (empty($wp_filesystem)) { require_once ABSPATH . 'wp-admin/includes/file.php'; WP_Filesystem(); } if ($wp_filesystem) { $wp_filesystem->put_contents($file_path, $svg_data, FS_CHMOD_FILE); } ++$repaired_count; } } return $repaired_count; } private function render_group_form() { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only UI edit index parameter. $post_id = isset($_GET['index']) ? intval(wp_unslash($_GET['index'])) : null; $group = null; $validation_error = get_transient('metaforge_validation_error_' . get_current_user_id()); if ($validation_error !== false) { delete_transient('metaforge_validation_error_' . get_current_user_id()); $group = get_transient('metaforge_temp_group_config_' . get_current_user_id()); delete_transient('metaforge_temp_group_config_' . get_current_user_id()); } elseif ($post_id) { $post = get_post($post_id); if ($post && $post->post_type === 'metaforge_group') { $group = get_post_meta($post_id, '_metaforge_group_config', true); } } $index = $post_id; $group_name = $group['group_name'] ?? ''; $post_type = $group['post_type'] ?? 'post'; $fields = $group['fields'] ?? array(); $all_post_types = get_post_types(array('public' => true), 'objects'); if (isset($all_post_types['attachment'])) { unset($all_post_types['attachment']); } $is_edit = $index !== null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only notice query flag. $saved = isset($_GET['saved']); require METAFORGE_PATH . 'admin/views/builder.php'; } // ── Render one builder field row ───────────────────────────────────────── public function render_builder_field($field, $name_prefix, $index, $depth = 0, $is_template = false) { if (!is_array($field)) { return; } $type = $field['type'] ?? 'text'; $label = $field['label'] ?? ''; $name = $field['name'] ?? ''; $options = $field['options'] ?? array(); $sub_fields = $field['sub_fields'] ?? array(); // Auto-heal fields that were accidentally saved as 'text' when Pro was inactive if ($type === 'text') { if (!empty($sub_fields)) { $type = 'repeater'; } elseif (!empty($field['layouts'])) { $type = 'flexible_content'; } } $conditional_logic = $field['conditional_logic'] ?? 0; $conditions = $field['conditions'] ?? array(); $img_return = $field['image_return_format'] ?? 'array'; $is_sub = $depth > 0; $row_class = $is_sub ? 'mfe-sub-field-row mfe-field-wrap' : 'mfe-field-row mfe-field-wrap'; if ($is_template) { $row_class .= ' is-open'; } $header_class = $is_sub ? 'mfe-sf-row-header mfe-field-header mfe-toggle' : 'mfe-field-header mfe-toggle'; /* FIX: index is always shown as ($index+1) — for template we use placeholder */ $print_idx = $is_template ? 'MetaForge_INDEX' : ($index + 1); $type_icons = array( 'text' => 'dashicons-editor-textcolor', 'textarea' => 'dashicons-editor-paragraph', 'number' => 'dashicons-editor-ol', 'image' => 'dashicons-format-image', 'file' => 'dashicons-paperclip', 'wysiwyg' => 'dashicons-editor-kitchensink', 'select' => 'dashicons-list-view', 'checkbox' => 'dashicons-yes-alt', 'radio' => 'dashicons-marker', 'true_false' => 'dashicons-controls-repeat', 'link' => 'dashicons-admin-links', 'post_type_selector' => 'dashicons-admin-post', 'taxonomy' => 'dashicons-category', 'group' => 'dashicons-networking', 'repeater' => 'dashicons-controls-repeat', 'color_picker' => 'dashicons-art', 'gallery' => 'dashicons-format-gallery', 'user' => 'dashicons-admin-users', 'flexible_content' => 'dashicons-layout', 'date_time' => 'dashicons-calendar-alt', 'oembed' => 'dashicons-video-alt3', 'relationship' => 'dashicons-randomize', 'icon_picker' => 'dashicons-star-filled', 'code_editor' => 'dashicons-editor-code', 'range' => 'dashicons-leftright', 'button' => 'dashicons-button', ); $icon = $type_icons[$type] ?? 'dashicons-admin-generic'; $field_json = esc_attr(wp_json_encode($field)); require METAFORGE_PATH . 'admin/views/builder-field.php'; } // ── Meta box ────────────────────────────────────────────────────────────── public function register_meta_box() { // P-6: Only register the meta box on post types that actually have at least // one active MetaForge group assigned. Avoids a blank "No groups" box // on every unrelated post type. Uses the same transient as FieldManager. $all_groups = get_transient('metaforge_all_field_groups_cache'); $public_pts = array_values(get_post_types(array('public' => true), 'names')); if (false === $all_groups) { // Cache not yet built — register on all types as a safe fallback. // The box will self-filter inside render_meta_box() if no groups match. $post_types = $public_pts; } else { // Collect the set of post types that have at least one group. $post_types_with_groups = array(); foreach ($all_groups as $group) { // Groups registered as Gutenberg blocks are edited via blocks and excluded from bottom meta boxes if (!empty($group['register_as_block'])) { continue; } if (empty($group['location_rules']) || !is_array($group['location_rules'])) { // Legacy fallback: use the top-level post_type key. $pt = $group['post_type'] ?? ''; if ($pt) { $post_types_with_groups[$pt] = true; } continue; } foreach ($group['location_rules'] as $rule_group) { foreach ((array) $rule_group as $rule) { $param = $rule['param'] ?? ''; $val = $rule['value'] ?? ''; if ($param === 'post_type' && !empty($val)) { if ($val === 'all') { foreach ($public_pts as $pt) { $post_types_with_groups[$pt] = true; } } else { $post_types_with_groups[$val] = true; } } elseif (in_array($param, array('page_template', 'page_type', 'page_parent', 'page'), true)) { $post_types_with_groups['page'] = true; } elseif (in_array($param, array('post', 'post_format', 'post_category'), true)) { $post_types_with_groups['post'] = true; } elseif (in_array($param, array('post_status', 'taxonomy'), true)) { $post_types_with_groups['post'] = true; $post_types_with_groups['page'] = true; } } } } // Fallback: If groups exist but could not be mapped to specific post types, // register across public post types so no valid assignments are missed. $non_block_groups = array_filter($all_groups, fn($g) => empty($g['register_as_block'])); if (empty($post_types_with_groups) && !empty($non_block_groups)) { $post_types = $public_pts; } else { $post_types = array_keys($post_types_with_groups); } } if (empty($post_types)) { return; } add_meta_box( 'metaforge_meta_box', 'MetaForge Fields', array($this, 'render_meta_box'), $post_types, 'normal', 'default' ); } public function render_meta_box($post) { wp_nonce_field('metaforge_meta_box_nonce', 'metaforge_meta_box_nonce_field'); $groups = $this->field_manager->get_groups_by_post_type(get_post_type($post), $post->ID); // Exclude field groups that are registered as Gutenberg blocks $groups = array_filter( $groups, function ($g) { return empty($g['register_as_block']); } ); if (empty($groups)) { wp_add_inline_style('metaforge-field-css', '#metaforge_meta_box { display: none !important; }'); echo '

' . esc_html__('No field groups assigned to this post.', 'metaforge-engine') . '

'; return; } foreach ($groups as $group) { echo '
'; foreach ($group['fields'] as $field) { if (!is_array($field) || empty($field['name'])) { continue; } $value = get_post_meta($post->ID, $field['name'], true); if (in_array($field['type'], array('repeater', 'group', 'flexible_content'), true)) { if (!is_array($value)) { $value = array(); } else { // Strip ghost template rows (key=999) written by older // plugin versions, then re-index to 0-based sequential. $value = $this->strip_template_rows($value); if (in_array($field['type'], array('repeater', 'flexible_content'), true)) { $value = array_values($value); } } } MetaForge_FieldManager::render_field($field, $value); } echo '
'; } } public function save_meta_box($post_id) { if ( !isset($_POST['metaforge_meta_box_nonce_field']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['metaforge_meta_box_nonce_field'])), 'metaforge_meta_box_nonce') ) { return; } if (defined('DOING_AUTOSAVE') && DOING_AUTOSAVE) { return; } $post_type = get_post_type($post_id); if ('page' === $post_type) { if (!current_user_can('edit_page', $post_id)) { return; } } else { if (!current_user_can('edit_post', $post_id)) { return; } } $groups = $this->field_manager->get_groups_by_post_type(get_post_type($post_id), $post_id); $groups = array_filter( $groups, function ($g) { return empty($g['register_as_block']); } ); $this->save_fields($post_id, 'post', $groups); } public function save_fields($object_id, $object_type, $groups) { // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Nonce verified in caller save_post hook handler; raw JSON decoded and meta values sanitized per field type below. if (!empty($_POST['metaforge_post_json'])) { $raw_json = wp_unslash($_POST['metaforge_post_json']); if (is_string($raw_json) && $raw_json !== '' && $raw_json !== '{}' && $raw_json !== '[]') { $json_data = json_decode($raw_json, true); if (is_array($json_data) && !empty($json_data)) { $_POST['metaforge'] = $json_data; } } } // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $all_fields = array(); if (is_array($groups)) { foreach ($groups as $group) { if (!empty($group['fields']) && is_array($group['fields'])) { foreach ($group['fields'] as $field) { if (!empty($field['name'])) { $all_fields[] = $field; } } } } } // phpcs:disable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash -- Meta values sanitized per field type below. foreach ($all_fields as $field) { $field_key = $field['name']; $field_type = $field['type'] ?? 'text'; $value = isset(($_POST['metaforge'] ?? $_POST['mfe'])[$field_key]) ? wp_unslash(($_POST['metaforge'] ?? $_POST['mfe'])[$field_key]) : null; // phpcs:enable WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized, WordPress.Security.ValidatedSanitizedInput.MissingUnslash // Protect existing Pro field data in database if MetaForge Pro extension is inactive $is_pro_type = in_array($field_type, array('flexible_content', 'repeater', 'gallery', 'relationship', 'icon_picker', 'code_editor', 'accordion', 'oembed'), true); if ($is_pro_type && !MetaForge_Field_Registry::get_field_type($field_type) && !class_exists('MetaForge_' . str_replace(' ', '', ucwords(str_replace('_', ' ', $field_type))) . 'Field')) { continue; } if ($value === null && $field_type === 'checkbox') { $value = array(); } if ($value === null && $field_type === 'true_false') { $value = '0'; } if ($field_type === 'group' && !empty($field['sub_fields'])) { $raw_group = is_array($value) ? $value : array(); foreach ($field['sub_fields'] as $sub_field) { $sub_key = $sub_field['name'] ?? ''; $sub_type = $sub_field['type'] ?? 'text'; if (!$sub_key) { continue; } if ($sub_type === 'checkbox') { if (!array_key_exists($sub_key, $raw_group)) { $raw_group[$sub_key] = array(); } } elseif ($sub_type === 'true_false') { if (!array_key_exists($sub_key, $raw_group)) { $raw_group[$sub_key] = '0'; } } } $value = $raw_group; } if ($field_type === 'repeater' && !empty($field['sub_fields']) && is_array($value)) { foreach ($value as $row_index => $row_data) { if (!is_array($row_data)) { continue; } foreach ($field['sub_fields'] as $sub_field) { $sub_key = $sub_field['name'] ?? ''; $sub_type = $sub_field['type'] ?? 'text'; if (!$sub_key) { continue; } if ($sub_type === 'checkbox' && !array_key_exists($sub_key, $row_data)) { $value[$row_index][$sub_key] = array(); } elseif ($sub_type === 'true_false' && !array_key_exists($sub_key, $row_data)) { $value[$row_index][$sub_key] = '0'; } } } } if ($value === null) { if ($field_type === 'repeater' || $field_type === 'flexible_content') { $value = array(); } else { if ($object_type === 'post') { delete_post_meta($object_id, sanitize_key($field_key)); } elseif ($object_type === 'term') { delete_term_meta($object_id, sanitize_key($field_key)); } elseif ($object_type === 'user') { delete_user_meta($object_id, sanitize_key($field_key)); } elseif ($object_type === 'comment') { delete_comment_meta($object_id, sanitize_key($field_key)); } continue; } } $registry_instance = MetaForge_Field_Registry::get_field_type($field_type); if (!$registry_instance) { $class_name = 'MetaForge_' . str_replace(' ', '', ucwords(str_replace('_', ' ', $field_type))) . 'Field'; if (class_exists($class_name)) { $registry_instance = new $class_name(); } } if ($registry_instance) { $clean = $registry_instance->sanitize($value, $field); if (is_array($clean) && in_array($field_type, array('repeater', 'group', 'flexible_content'), true)) { $clean = $this->strip_template_rows($clean); if (in_array($field_type, array('repeater', 'flexible_content'), true)) { $clean = array_values($clean); } } } else { $clean = $this->sanitize_recursive($value); if (is_array($clean)) { $clean = $this->strip_template_rows($clean); if (in_array($field_type, array('repeater', 'flexible_content'), true)) { $clean = array_values($clean); } } } if ($object_type === 'post') { update_post_meta($object_id, sanitize_key($field_key), $clean); } elseif ($object_type === 'term') { update_term_meta($object_id, sanitize_key($field_key), $clean); } elseif ($object_type === 'user') { update_user_meta($object_id, sanitize_key($field_key), $clean); } elseif ($object_type === 'comment') { update_comment_meta($object_id, sanitize_key($field_key), $clean); } } } public function register_taxonomy_hooks() { $taxonomies = get_taxonomies(); foreach ($taxonomies as $taxonomy) { add_action("{$taxonomy}_add_form_fields", array($this, 'render_taxonomy_add_fields')); add_action("{$taxonomy}_edit_form_fields", array($this, 'render_taxonomy_edit_fields')); add_action("created_{$taxonomy}", array($this, 'save_taxonomy_fields')); add_action("edited_{$taxonomy}", array($this, 'save_taxonomy_fields')); } } public function render_taxonomy_add_fields($taxonomy) { $groups = $this->field_manager->get_groups_by_context( array( 'taxonomy' => $taxonomy, ) ); if (empty($groups)) { return; } wp_nonce_field('metaforge_meta_box_nonce', 'metaforge_meta_box_nonce_field'); foreach ($groups as $group) { echo '

' . esc_html($group['group_name']) . '

'; foreach ($group['fields'] as $field) { if (!is_array($field) || empty($field['name'])) { continue; } echo '
'; echo ''; MetaForge_FieldManager::render_field($field, ''); echo '
'; } } } public function render_taxonomy_edit_fields($term) { $groups = $this->field_manager->get_groups_by_context( array( 'taxonomy' => $term->taxonomy, ) ); if (empty($groups)) { return; } wp_nonce_field('metaforge_meta_box_nonce', 'metaforge_meta_box_nonce_field'); foreach ($groups as $group) { echo ''; echo '

' . esc_html($group['group_name']) . '

'; echo ''; foreach ($group['fields'] as $field) { if (!is_array($field) || empty($field['name'])) { continue; } $value = get_term_meta($term->term_id, $field['name'], true); if (in_array($field['type'], array('repeater', 'group', 'flexible_content'), true)) { if (!is_array($value)) { $value = array(); } else { $value = $this->strip_template_rows($value); if (in_array($field['type'], array('repeater', 'flexible_content'), true)) { $value = array_values($value); } } } echo ''; echo ''; echo ''; MetaForge_FieldManager::render_field($field, $value); echo ''; echo ''; } } } public function save_taxonomy_fields($term_id) { if ( !isset($_POST['metaforge_meta_box_nonce_field']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['metaforge_meta_box_nonce_field'])), 'metaforge_meta_box_nonce') ) { return; } $term = get_term($term_id); if (!$term || is_wp_error($term)) { return; } $taxonomy_obj = get_taxonomy($term->taxonomy); if (!$taxonomy_obj || !current_user_can($taxonomy_obj->cap->edit_terms) || !current_user_can('edit_term', $term_id)) { return; } $groups = $this->field_manager->get_groups_by_context( array( 'taxonomy' => $term->taxonomy, ) ); $this->save_fields($term_id, 'term', $groups); } public function render_user_fields($user) { $user_role = !empty($user->roles) ? reset($user->roles) : ''; $groups = $this->field_manager->get_groups_by_context( array( 'user_role' => $user_role, ) ); if (empty($groups)) { return; } wp_nonce_field('metaforge_meta_box_nonce', 'metaforge_meta_box_nonce_field'); foreach ($groups as $group) { echo '

' . esc_html($group['group_name']) . '

'; echo ''; foreach ($group['fields'] as $field) { if (!is_array($field) || empty($field['name'])) { continue; } $value = get_user_meta($user->ID, $field['name'], true); if (in_array($field['type'], array('repeater', 'group', 'flexible_content'), true)) { if (!is_array($value)) { $value = array(); } else { $value = $this->strip_template_rows($value); if (in_array($field['type'], array('repeater', 'flexible_content'), true)) { $value = array_values($value); } } } echo ''; echo ''; echo ''; echo ''; } echo '
'; MetaForge_FieldManager::render_field($field, $value); echo '
'; } } public function save_user_fields($user_id) { if ( !isset($_POST['metaforge_meta_box_nonce_field']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['metaforge_meta_box_nonce_field'])), 'metaforge_meta_box_nonce') ) { return; } if (!current_user_can('edit_user', $user_id)) { return; } $user = get_userdata($user_id); $user_role = !empty($user->roles) ? reset($user->roles) : ''; $groups = $this->field_manager->get_groups_by_context( array( 'user_role' => $user_role, ) ); $this->save_fields($user_id, 'user', $groups); } public function register_comment_meta_box() { $groups = $this->field_manager->get_groups_by_context( array( 'comment' => 'all', ) ); if (!empty($groups)) { add_meta_box( 'metaforge_comment_meta_box', 'MetaForge Fields', array($this, 'render_comment_meta_box'), 'comment', 'normal', 'default' ); } } public function render_comment_meta_box($comment) { wp_nonce_field('metaforge_meta_box_nonce', 'metaforge_meta_box_nonce_field'); $groups = $this->field_manager->get_groups_by_context( array( 'comment' => 'all', ) ); foreach ($groups as $group) { echo '
'; echo '

' . esc_html($group['group_name']) . '

'; foreach ($group['fields'] as $field) { if (!is_array($field) || empty($field['name'])) { continue; } $value = get_comment_meta($comment->comment_ID, $field['name'], true); if (in_array($field['type'], array('repeater', 'group', 'flexible_content'), true)) { if (!is_array($value)) { $value = array(); } else { $value = $this->strip_template_rows($value); if (in_array($field['type'], array('repeater', 'flexible_content'), true)) { $value = array_values($value); } } } MetaForge_FieldManager::render_field($field, $value); } echo '
'; } } public function save_comment_fields($comment_id) { if ( !isset($_POST['metaforge_meta_box_nonce_field']) || !wp_verify_nonce(sanitize_text_field(wp_unslash($_POST['metaforge_meta_box_nonce_field'])), 'metaforge_meta_box_nonce') ) { return; } if (!current_user_can('edit_comment', $comment_id)) { return; } $groups = $this->field_manager->get_groups_by_context( array( 'comment' => 'all', ) ); $this->save_fields($comment_id, 'comment', $groups); } public function save_revision_meta($revision_id, $post_id) { if (!current_user_can('edit_post', $post_id)) { return; } $post_type = get_post_type($post_id); $groups = $this->field_manager->get_groups_by_context(array('post_type' => $post_type)); if (empty($groups)) { return; } foreach ($groups as $group) { foreach ($group['fields'] ?? array() as $field) { $name = $field['name'] ?? ''; if (!$name) { continue; } $value = get_post_meta($post_id, $name, true); if ($value !== '') { update_post_meta($revision_id, '_metaforge_rev_' . $name, $value); } } } } public function restore_revision_meta($post_id, $revision_id) { if (!current_user_can('edit_post', $post_id)) { return; } $post_type = get_post_type($post_id); $groups = $this->field_manager->get_groups_by_context(array('post_type' => $post_type)); if (empty($groups)) { return; } foreach ($groups as $group) { foreach ($group['fields'] ?? array() as $field) { $name = $field['name'] ?? ''; if (!$name) { continue; } $value = get_post_meta($revision_id, '_metaforge_rev_' . $name, true); if ($value !== '') { update_post_meta($post_id, $name, $value); } else { delete_post_meta($post_id, $name); } } } } public function add_revision_fields_to_diff($fields) { $all_groups = get_transient('metaforge_all_field_groups_cache'); if (is_array($all_groups)) { foreach ($all_groups as $group) { foreach ($group['fields'] ?? array() as $field) { $name = $field['name'] ?? ''; if ($name) { $fields['metaforge_field_' . $name] = $field['label'] ?? $name; } } } } return $fields; } private function strip_template_rows($data) { if (!is_array($data)) { return $data; } $out = array(); foreach ($data as $k => $v) { if ((string) $k === '999') { continue; } $out[$k] = is_array($v) ? $this->strip_template_rows($v) : $v; } return $out; } /** * Recursively sanitize field values using standard post HTML sanitization. * * @param mixed $value The raw value to sanitize. * @return mixed */ private function sanitize_recursive($value) { if (is_array($value)) { $out = array(); foreach ($value as $k => $v) { $out[$k] = $this->sanitize_recursive($v); } return $out; } return wp_kses_post(wp_unslash((string) $value)); } public function ajax_update_group_order() { check_ajax_referer('metaforge_reorder_nonce', 'nonce'); if (!current_user_can('manage_metaforge')) { wp_send_json_error('Forbidden'); } $order = isset($_POST['order']) ? array_map('absint', wp_unslash((array) $_POST['order'])) : array(); if (empty($order)) { wp_send_json_error('Empty order'); } // Update menu_order on each metaforge_group CPT post. // This matches the storage model used everywhere else in the plugin. foreach ($order as $position => $post_id) { if ($post_id <= 0) { continue; } wp_update_post( array( 'ID' => $post_id, 'menu_order' => $position, ) ); } self::clear_cache(); wp_send_json_success('Order updated'); } public function ajax_delete_group() { check_ajax_referer('metaforge_delete_group_nonce', 'nonce'); if (!current_user_can('manage_metaforge')) { wp_send_json_error(array('message' => 'Unauthorized')); } $index = isset($_POST['index']) ? absint(wp_unslash($_POST['index'])) : null; if ($index === null || $index <= 0) { wp_send_json_error(); } MetaForge_JSONSyncManager::delete_group($index); wp_delete_post($index, true); self::clear_cache(); wp_send_json_success(); } public function ajax_duplicate_group() { check_ajax_referer('metaforge_duplicate_group_nonce', 'nonce'); if (!current_user_can('manage_metaforge')) { wp_send_json_error(array('message' => 'Unauthorized')); } $index = isset($_POST['index']) ? absint(wp_unslash($_POST['index'])) : 0; $new_name = isset($_POST['new_name']) ? sanitize_text_field(wp_unslash($_POST['new_name'])) : ''; if ($index <= 0) { wp_send_json_error(array('message' => 'Invalid group specified.')); } if (empty(trim($new_name))) { wp_send_json_error(array('message' => 'Field group title cannot be empty.')); } $post = get_post($index); if (!$post || $post->post_type !== 'metaforge_group') { wp_send_json_error(array('message' => 'Field group not found.')); } $config = get_post_meta($index, '_metaforge_group_config', true); if (!is_array($config)) { wp_send_json_error(array('message' => 'Group configuration data is missing.')); } $display_title = (strpos($new_name, '_') !== false && strpos($new_name, ' ') === false) ? ucwords(str_replace('_', ' ', $new_name)) : $new_name; // Prevent creating field groups with duplicate names global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Title duplication lookup for group duplication. $existing_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE (post_title = %s OR post_title = %s) AND post_type = 'metaforge_group' AND post_status != 'trash' LIMIT 1", $new_name, $display_title ) ); if ($existing_id) { wp_send_json_error(array('message' => sprintf('A field group named "%s" already exists. Please choose a unique name.', $display_title))); } $unique_key = 'group_' . substr(md5($new_name . microtime() . wp_rand()), 0, 12); $config['group_name'] = $display_title; $config['group_key'] = $unique_key; $new_post_id = wp_insert_post( array( 'post_title' => $display_title, 'post_type' => 'metaforge_group', 'post_status' => 'publish', 'menu_order' => $post->menu_order + 1, ) ); if (!$new_post_id || is_wp_error($new_post_id)) { wp_send_json_error(array('message' => 'Failed to create duplicated field group.')); } MetaForge_JSONSyncManager::save_group($new_post_id, $config); self::clear_cache(); wp_send_json_success( array( 'message' => 'Group duplicated successfully.', 'new_id' => $new_post_id, ) ); } }