[ 'name' => __('Forms', 'aumframe'), 'singular_name' => __('Form', 'aumframe'), 'add_new_item' => __('Add form', 'aumframe'), 'edit_item' => __('Edit form', 'aumframe'), ], 'public' => false, 'show_ui' => true, 'show_in_menu' => Feature::menu_parent(), 'exclude_from_search' => true, 'publicly_queryable' => false, 'capability_type' => 'page', 'supports' => ['title'], ]; } /** One form's field list. */ public static function fields(int $form_id): array { $raw = get_post_meta($form_id, self::META_FIELDS, true); $arr = is_string($raw) ? json_decode($raw, true) : $raw; return is_array($arr) ? array_values(array_filter($arr, 'is_array')) : []; } /** * One form's settings, with defaults. * * The recipient defaults to the administrator's address and can be overridden per form — the vast * majority of sites have one recipient, and nobody should have to type it again for every form. */ public static function settings(int $form_id): array { $raw = get_post_meta($form_id, self::META_SETTINGS, true); $s = is_string($raw) ? json_decode($raw, true) : $raw; $s = is_array($s) ? $s : []; return wp_parse_args($s, [ 'recipient' => '', // Empty means the administrator's address 'subject' => '', // Empty means generated automatically 'body_template' => '', // Empty means generated from the fields (recommended) 'success_message' => __('Thanks — we’ll be in touch shortly.', 'aumframe'), 'success_redirect' => '', 'submit_label' => __('Submit', 'aumframe'), 'autoreply' => 0, 'autoreply_field' => '', // Which field holds the submitter's address 'autoreply_subject'=> '', 'autoreply_body' => '', ]); } public static function recipient(int $form_id): string { $s = self::settings($form_id); $to = trim((string) $s['recipient']); return $to !== '' ? $to : (string) get_option('admin_email'); } /** * Storing JSON in post meta requires wp_slash first. * * `update_post_meta()` runs `wp_unslash()` on the value internally (to accommodate slashed $_POST data), so * `\u79f0` inside the JSON becomes `u79f0` and `\n` becomes `n` — non-ASCII labels are mangled wholesale, * multi-line options collapse onto one line, and nothing is reported while saving; it only surfaces * when the front end renders. Slashing once first cancels it out exactly. */ public static function save_fields(int $form_id, array $fields): void { $clean = []; $seen = []; foreach ($fields as $f) { if (!is_array($f)) { continue; } $type = isset($f['type']) && isset(Fields::types()[$f['type']]) ? $f['type'] : 'text'; /* * A field id is the input name at submission time and has to be unique and safe. * A duplicate silently overwrites the earlier one — two fields both called "phone" would receive one * value — so uniqueness is enforced here rather than trusted to the front end. */ $id = sanitize_key($f['id'] ?? ''); if ($id === '' || isset($seen[$id])) { $id = 'f' . (count($clean) + 1) . '_' . substr(md5($id . microtime()), 0, 4); } $seen[$id] = true; $label = (string) ($f['label'] ?? ''); $label = $type === 'consent' ? wp_kses($label, Fields::consent_label_html()) : sanitize_text_field($label); $clean[] = [ 'id' => $id, 'type' => $type, 'label' => $label, 'placeholder' => sanitize_text_field($f['placeholder'] ?? ''), 'help' => sanitize_text_field($f['help'] ?? ''), 'required' => !empty($f['required']) ? 1 : 0, 'width' => (($f['width'] ?? 'full') === 'half') ? 'half' : 'full', 'options' => sanitize_textarea_field($f['options'] ?? ''), 'source' => [ 'kind' => in_array($f['source']['kind'] ?? '', ['post_type', 'taxonomy'], true) ? $f['source']['kind'] : '', 'name' => sanitize_key($f['source']['name'] ?? ''), ], 'default' => sanitize_text_field($f['default'] ?? ''), ]; } update_post_meta($form_id, self::META_FIELDS, wp_slash(wp_json_encode($clean, JSON_UNESCAPED_UNICODE))); } public static function save_settings(int $form_id, array $s): void { $clean = [ 'recipient' => sanitize_text_field($s['recipient'] ?? ''), 'subject' => sanitize_text_field($s['subject'] ?? ''), 'body_template' => wp_kses_post($s['body_template'] ?? ''), 'success_message' => sanitize_text_field($s['success_message'] ?? ''), 'success_redirect' => esc_url_raw($s['success_redirect'] ?? ''), 'submit_label' => sanitize_text_field($s['submit_label'] ?? ''), 'autoreply' => !empty($s['autoreply']) ? 1 : 0, 'autoreply_field' => sanitize_key($s['autoreply_field'] ?? ''), 'autoreply_subject' => sanitize_text_field($s['autoreply_subject'] ?? ''), 'autoreply_body' => sanitize_textarea_field($s['autoreply_body'] ?? ''), ]; update_post_meta($form_id, self::META_SETTINGS, wp_slash(wp_json_encode($clean, JSON_UNESCAPED_UNICODE))); } /** For dropdowns: every form on the site. */ public static function all(): array { $out = []; foreach (get_posts(['post_type' => self::POST_TYPE, 'numberposts' => -1, 'post_status' => 'publish']) as $p) { $out[] = ['id' => $p->ID, 'title' => $p->post_title ?: ('#' . $p->ID)]; } return $out; } }