absolute path to the sections folder to read from * 'value_source' => 'meta' | 'draft_meta' * 'post_id' => resolved post ID for value reads * * @param int|null $post_id The page being rendered. Null = current queried post. * @return array */ public function resolve( $post_id = null ): array { if ( null === $post_id ) { $post_id = get_queried_object_id(); } // Editor preview (logged-in admin, ?slbuild_preview=1) ? serve from draft meta. if ( isset( $_GET['slbuild_preview'] ) && current_user_can( 'edit_pages' ) ) { // phpcs:ignore WordPress.Security.NonceVerification return array( 'sections_dir' => $this->get_live_sections_dir(), 'starter_sections_dir' => $this->get_starter_sections_dir(), 'value_source' => 'draft_meta', 'post_id' => (int) $post_id, ); } // Phase 1.5: valid preview token ? live sections + draft content data. // Primary use case: share a client-review link showing unsaved content changes // before clicking Publish. Section code stays live; only field values are from the draft. if ( $this->has_valid_preview_token( (int) $post_id ) ) { return array( 'sections_dir' => $this->get_live_sections_dir(), 'starter_sections_dir' => $this->get_starter_sections_dir(), 'value_source' => 'draft_meta', 'post_id' => (int) $post_id, ); } return array( 'sections_dir' => $this->get_live_sections_dir(), 'starter_sections_dir' => $this->get_starter_sections_dir(), 'value_source' => 'meta', 'post_id' => (int) $post_id, ); } /** * Returns true when the request carries a valid slbuild_preview_token for this post. * Works for logged-out users — token is checked against a stored transient only. * * @param int $post_id * @return bool */ private function has_valid_preview_token( int $post_id ): bool { if ( ! $post_id || ! isset( $_GET['slbuild_preview_token'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification return false; } $submitted = sanitize_text_field( wp_unslash( $_GET['slbuild_preview_token'] ) ); // phpcs:ignore WordPress.Security.NonceVerification if ( ! $submitted ) { return false; } $stored = get_transient( 'slbuild_preview_' . $post_id ); return $stored && hash_equals( $stored, $submitted ); } /** * Absolute path to the active theme's live /sections/ directory. * * @return string */ public function get_live_sections_dir(): string { return get_stylesheet_directory() . '/sections'; } /** * Absolute path to the active theme's draft /sections-draft/ directory. * Used in Phase 1.5 preview flow. * * @return string */ public function get_draft_sections_dir(): string { return get_stylesheet_directory() . '/sections-draft'; } /** * Absolute path to the plugin's bundled starter-sections directory. * Theme /sections/ always takes priority over these. * * @return string */ public function get_starter_sections_dir(): string { return rtrim( SLBUILD_PATH, '/\\' ) . '/starter-sections'; } /** * Read and return the active theme's slb-theme.json manifest. * Returns an empty array if the file is absent or malformed. * * @return array */ public function get_theme_manifest(): array { static $manifest = null; if ( null === $manifest ) { $path = get_stylesheet_directory() . '/slb-theme.json'; if ( ! file_exists( $path ) ) { $manifest = array(); } else { $raw = file_get_contents( $path ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents $decoded = json_decode( $raw, true ); $manifest = ( is_array( $decoded ) && ! empty( $decoded['slbuild_theme'] ) ) ? $decoded : array(); } } return $manifest; } }