';
// 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 = '';
$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 '
' . 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 '