list_table = new MetaForge_OptionsPageListTable(); $screen = get_current_screen(); if ( $screen ) { register_column_headers( $screen, $this->list_table->get_columns() ); } } } /** * Dynamically register each user-configured options page in the WP sidebar. */ public function register_options_pages() { $pages = get_option( self::OPTION_KEY, array() ); if ( empty( $pages ) ) { return; } foreach ( $pages as $page ) { $slug = sanitize_key( $page['slug'] ?? '' ); $title = sanitize_text_field( $page['title'] ?? 'Options' ); $parent = $page['parent'] ?? 'metaforge_options_pages'; if ( ! $slug ) { continue; } $wp_parent = ( $parent === 'metaforge_options_pages' || $parent === 'mfe_options_pages' ) ? 'metaforge' : $parent; if ( $wp_parent === 'none' ) { // Register as a new top-level menu page add_menu_page( $title, $title, 'manage_metaforge', 'metaforge_opts_' . $slug, function () use ( $page ) { $this->render_options_page( $page ); }, 'dashicons-admin-generic' ); // Also add a submenu item pointing to itself so the parent name is inherited properly add_submenu_page( 'metaforge_opts_' . $slug, $title, $title, 'manage_metaforge', 'metaforge_opts_' . $slug, null ); } else { add_submenu_page( $wp_parent, $title, $title, 'manage_metaforge', 'metaforge_opts_' . $slug, function () use ( $page ) { $this->render_options_page( $page ); } ); } } } /** * Render the Options Pages management screen (where you add/edit option pages). */ public function render_manager_page() { // 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_form(); return; } $this->render_list(); } private function render_list() { if ( isset( $this->list_table ) ) { $list_table = $this->list_table; } else { require_once METAFORGE_PATH . 'admin/OptionsPageListTable.php'; $list_table = new MetaForge_OptionsPageListTable(); } $list_table->prepare_items(); $add_url = admin_url( 'admin.php?page=metaforge_options_pages&action=add' ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only search query string parameter. $s_val = isset( $_REQUEST['s'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['s'] ) ) : ''; $is_empty = empty( $list_table->items ) && empty( $s_val ); ?>

Error: An options page with that slug already exists.

views(); ?> display(); ?>
$p ) { if ( $is_edit && intval( $edit_index ) === $idx ) { continue; } if ( ! empty( $p['slug'] ) ) { $existing_slugs[] = $p['slug']; } } ?>

An options page with that slug already exists.
Options Page Settings
Page Title e.g. Theme Settings
Page Slug e.g. theme_settings

Lowercase, underscores only. Used as the menu slug.

Parent Menu Where this page appears in the WordPress sidebar
$existing ) { if ( $edit_index !== null && $edit_index === $idx ) { continue; } if ( ( $existing['slug'] ?? '' ) === $slug ) { wp_safe_redirect( admin_url( 'admin.php?page=metaforge_options_pages&metaforge_error=duplicate' ) ); exit; } } $page_data = array( 'title' => $title, 'slug' => $slug, 'parent' => $parent, ); if ( $edit_index !== null && isset( $pages[ $edit_index ] ) ) { $pages[ $edit_index ] = $page_data; } else { $pages[] = $page_data; } update_option( self::OPTION_KEY, $pages ); $this->get_or_create_store_post( $slug, $title ); if ( $old_slug && $old_slug !== $slug ) { $this->update_field_groups_options_page_slug( $old_slug, $slug ); } } wp_safe_redirect( admin_url( 'admin.php?page=metaforge_options_pages' ) ); exit; } if ( isset( $_GET['action'] ) && $_GET['action'] === 'delete' && isset( $_GET['index'] ) ) { check_admin_referer( 'metaforge_delete_opts_page' ); if ( ! current_user_can( 'manage_metaforge' ) ) { wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'metaforge-engine' ) ); } $pages = get_option( self::OPTION_KEY, array() ); $index = intval( $_GET['index'] ); if ( isset( $pages[ $index ] ) ) { unset( $pages[ $index ] ); update_option( self::OPTION_KEY, array_values( $pages ) ); } wp_safe_redirect( admin_url( 'admin.php?page=metaforge_options_pages' ) ); exit; } } private function update_field_groups_options_page_slug( $old_slug, $new_slug ) { $posts = get_posts( array( 'post_type' => 'metaforge_group', 'posts_per_page' => -1, 'post_status' => 'any', ) ); foreach ( $posts as $post ) { $config = get_post_meta( $post->ID, '_metaforge_group_config', true ); if ( ! is_array( $config ) || empty( $config['location_rules'] ) ) { continue; } $updated = false; foreach ( $config['location_rules'] as &$group_rules ) { if ( ! is_array( $group_rules ) ) { continue; } foreach ( $group_rules as &$rule ) { if ( ( $rule['param'] ?? '' ) === 'options_page' && ( $rule['value'] ?? '' ) === $old_slug ) { $rule['value'] = $new_slug; $updated = true; } } } if ( $updated ) { update_post_meta( $post->ID, '_metaforge_group_config', $config ); } } MetaForge_Loader::clear_cache(); } /** * Render a user-defined options page with MetaForge fields. */ public function render_options_page( array $page ) { $slug = sanitize_key( $page['slug'] ); $title = esc_html( $page['title'] ); $store_id = $this->get_or_create_store_post( $slug, $page['title'] ); // Get all field groups assigned to this options page $groups = $this->get_groups_for_options_page( $slug ); $saved = false; if ( isset( $_POST['metaforge_save_options'] ) ) { check_admin_referer( 'metaforge_options_save_' . $slug, 'metaforge_options_nonce' ); if ( current_user_can( 'manage_metaforge' ) ) { // phpcs:disable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Raw JSON payload validated via json_decode() and options input array sanitized per field schema in save_options_values(). if ( ! empty( $_POST['metaforge_opts_json'] ) ) { $raw_json = wp_unslash( $_POST['metaforge_opts_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_option'] = $json_data; } } } $options_input = isset( $_POST['metaforge_option'] ) && is_array( $_POST['metaforge_option'] ) ? wp_unslash( $_POST['metaforge_option'] ) : array(); // phpcs:enable WordPress.Security.ValidatedSanitizedInput.InputNotSanitized $this->save_options_values( $store_id, $groups, $options_input ); $saved = true; } } ?>

No field groups are assigned to this options page yet. Create a Field Group and set its Location to Options Page → .

' . esc_html__( 'Save Options', 'metaforge-engine' ) . ''; MetaForge_AdminHeader::render_sub_header( esc_html( $page['title'] ), '', $save_btn ); ?>

strip_template_rows_options( $value ); if ( ( $field['type'] ?? '' ) === 'repeater' ) { $value = array_values( $value ); } } } MetaForge_FieldManager::render_field( $field, $value, 'metaforge_option[' . esc_attr( $field_name ) . ']' ); ?>
sanitize( $raw, $field ); if ( is_array( $clean ) && in_array( $type, array( 'repeater', 'group', 'flexible_content' ), true ) ) { $clean = $this->strip_template_rows_options( $clean ); if ( in_array( $type, array( 'repeater', 'flexible_content' ), true ) ) { $clean = array_values( $clean ); } } } else { switch ( $type ) { case 'textarea': $clean = sanitize_textarea_field( $raw ); break; case 'wysiwyg': $clean = wp_kses_post( $raw ); break; case 'true_false': $clean = (bool) $raw ? 1 : 0; break; case 'checkbox': case 'taxonomy': // Multi-value: store as serialized array $clean = is_array( $raw ) ? array_map( 'sanitize_text_field', $raw ) : sanitize_text_field( $raw ); break; case 'image': case 'file': $clean = absint( $raw ); break; case 'repeater': case 'group': $clean = $this->sanitize_recursive_options( $raw ); if ( is_array( $clean ) ) { $clean = $this->strip_template_rows_options( $clean ); if ( $type === 'repeater' ) { $clean = array_values( $clean ); } } break; case 'link': $clean = is_array( $raw ) ? array( 'url' => esc_url_raw( $raw['url'] ?? '' ), 'text' => sanitize_text_field( $raw['text'] ?? ( $raw['title'] ?? '' ) ), 'target' => in_array( $raw['target'] ?? '', array( '', '_blank' ) ) ? ( $raw['target'] ?? '' ) : '', ) : sanitize_text_field( $raw ); break; default: $clean = sanitize_text_field( $raw ); } } update_post_meta( $store_id, $name, $clean ); } } } /** * Get all field groups whose location rule targets a specific options page slug. * * P-2: Reads from the shared metaforge_all_field_groups_cache transient (same one used * everywhere else) instead of issuing a raw get_posts() on every options page render. */ public function get_groups_for_options_page( string $slug ): array { // Re-use the same transient populated by MetaForge_FieldManager::get_groups_by_post_type(). $all_groups = get_transient( 'metaforge_all_field_groups_cache' ); if ( false === $all_groups ) { // Cache miss: hydrate it the same way FieldManager does. $posts = get_posts( array( 'post_type' => 'metaforge_group', 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'menu_order', 'order' => 'ASC', ) ); $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 ); } $result = array(); foreach ( $all_groups as $config ) { if ( ! is_array( $config ) ) { continue; } foreach ( $config['location_rules'] ?? array() as $rule_group ) { foreach ( $rule_group as $rule ) { if ( ( $rule['param'] ?? '' ) === 'options_page' && ( $rule['value'] ?? '' ) === $slug ) { $result[] = $config; break 2; } } } } return $result; } /** * Ensure a backing store post exists for the given options page slug. * Returns the post ID. */ private function get_or_create_store_post( string $slug, string $title ): int { global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching -- Storage post ID lookup via custom post type query. $post_id = $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_title = %s AND post_type = %s AND post_status = 'publish' LIMIT 1", $slug, self::STORAGE_POST_TYPE ) ); if ( $post_id ) { return intval( $post_id ); } return wp_insert_post( array( 'post_title' => $slug, 'post_type' => self::STORAGE_POST_TYPE, 'post_status' => 'publish', ) ); } private function sanitize_recursive_options( $value ) { if ( is_array( $value ) ) { $out = array(); foreach ( $value as $k => $v ) { $out[ $k ] = $this->sanitize_recursive_options( $v ); } return $out; } return wp_kses_post( wp_unslash( (string) $value ) ); } private function strip_template_rows_options( $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_options( $v ) : $v; } return $out; } }