false, 'transparent' => false, 'layout' => 'full_width', 'preview_post' => 0]; public static function register(): void { $caps = []; foreach (['edit_post', 'read_post', 'delete_post', 'edit_posts', 'edit_others_posts', 'delete_posts', 'publish_posts', 'read_private_posts', 'create_posts', 'delete_private_posts', 'delete_published_posts', 'delete_others_posts', 'edit_private_posts', 'edit_published_posts'] as $cap) { $caps[$cap] = self::CAPABILITY; } register_post_type(self::POST_TYPE, [ 'labels' => [ 'name' => __('Theme templates', 'zadex'), 'singular_name' => __('Theme template', 'zadex'), ], 'public' => false, // Viewable so the editor URLs (?zadex=edit / ?zadex=preview) resolve like any post. Visitors get a 404 // (ThemeBuilder::guard_single); only editors can open a template on its own. 'publicly_queryable' => true, 'exclude_from_search' => true, 'show_ui' => false, 'show_in_nav_menus' => false, 'show_in_rest' => false, 'has_archive' => false, 'rewrite' => false, 'query_var' => false, 'hierarchical' => false, 'supports' => ['title', 'revisions'], 'capability_type' => 'post', 'capabilities' => $caps, 'map_meta_cap' => false, ]); foreach ([self::META_TYPE, self::META_CONDITIONS, self::META_SETTINGS] as $key) { register_post_meta(self::POST_TYPE, $key, [ 'single' => true, 'type' => 'string', 'show_in_rest' => false, 'auth_callback' => static fn(): bool => current_user_can(self::CAPABILITY), ]); } register_post_meta(self::POST_TYPE, self::META_PREVIEW, [ 'single' => true, 'type' => 'integer', 'show_in_rest' => false, 'auth_callback' => static fn(): bool => current_user_can(self::CAPABILITY), ]); } /** @return array type => label */ public static function type_labels(): array { return [ 'header' => __('Header', 'zadex'), 'footer' => __('Footer', 'zadex'), 'single' => __('Single', 'zadex'), 'archive' => __('Archive', 'zadex'), 'search' => __('Search results', 'zadex'), '404' => __('404 page', 'zadex'), 'loop-item' => __('Loop item', 'zadex'), 'popup' => __('Popup', 'zadex'), ] + (array) apply_filters('zadex/theme/template_labels', []); } public static function is_template(int $post_id): bool { return $post_id > 0 && get_post_type($post_id) === self::POST_TYPE; } public static function sanitize_type($type): string { $type = is_string($type) ? strtolower(trim($type)) : ''; return in_array($type, self::types(), true) ? $type : ''; } public static function type(int $post_id): string { return self::sanitize_type(get_post_meta($post_id, self::META_TYPE, true)); } public static function set_type(int $post_id, string $type): void { $type = self::sanitize_type($type); if ($type !== '') { update_post_meta($post_id, self::META_TYPE, $type); } } /** @return array> */ public static function conditions(int $post_id): array { $raw = get_post_meta($post_id, self::META_CONDITIONS, true); $decoded = is_string($raw) && $raw !== '' ? json_decode($raw, true) : (is_array($raw) ? $raw : null); return is_array($decoded) ? Conditions::sanitize($decoded) : []; } /** @return array> the sanitized rules that were stored */ public static function set_conditions(int $post_id, $rules): array { $clean = Conditions::sanitize(is_array($rules) ? $rules : []); update_post_meta($post_id, self::META_CONDITIONS, wp_slash((string) wp_json_encode($clean, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES))); return $clean; } /** @return array */ public static function settings(int $post_id): array { $raw = get_post_meta($post_id, self::META_SETTINGS, true); $decoded = is_string($raw) && $raw !== '' ? json_decode($raw, true) : (is_array($raw) ? $raw : null); $settings = self::sanitize_settings(is_array($decoded) ? $decoded : []); $preview = (int) get_post_meta($post_id, self::META_PREVIEW, true); if ($preview > 0) { $settings['preview_post'] = $preview; } return $settings; } /** Keeps the known keys only (plus defaults). */ public static function sanitize_settings(array $input): array { $out = self::SETTINGS_DEFAULTS; $out['sticky'] = !empty($input['sticky']); $out['transparent'] = !empty($input['transparent']); $out['layout'] = in_array($input['layout'] ?? '', ['full_width', 'canvas'], true) ? $input['layout'] : 'full_width'; $out['preview_post'] = isset($input['preview_post']) && is_numeric($input['preview_post']) ? max(0, (int) $input['preview_post']) : 0; return $out; } public static function set_settings(int $post_id, array $input): array { $clean = self::sanitize_settings($input); update_post_meta($post_id, self::META_SETTINGS, wp_slash((string) wp_json_encode($clean))); if ($clean['preview_post'] > 0) { update_post_meta($post_id, self::META_PREVIEW, $clean['preview_post']); } else { delete_post_meta($post_id, self::META_PREVIEW); } return $clean; } /** * Post shown as "the current post" when the editor previews a single template: the saved choice when it * still exists, else the latest published post. */ public static function preview_post(int $template_id): int { $saved = (int) get_post_meta($template_id, self::META_PREVIEW, true); if ($saved > 0 && get_post($saved)) { return $saved; } // Filter `zadex/theme/preview_post_type`: the post type a template type previews with (product templates: product). $post_type = (string) apply_filters('zadex/theme/preview_post_type', 'post', self::type($template_id)); $latest = get_posts(['post_type' => $post_type !== '' ? $post_type : 'post', 'post_status' => 'publish', 'numberposts' => 1, 'orderby' => 'date', 'order' => 'DESC', 'fields' => 'ids']); return $latest ? (int) $latest[0] : 0; } /** Creates a draft template and returns its id (0 on failure). */ public static function create(string $title, string $type): int { $type = self::sanitize_type($type); if ($type === '') { return 0; } $id = wp_insert_post([ 'post_type' => self::POST_TYPE, 'post_title' => $title !== '' ? $title : self::type_labels()[$type], 'post_status' => 'draft', ], true); if (is_wp_error($id) || !$id) { return 0; } update_post_meta((int) $id, self::META_TYPE, $type); return (int) $id; } }