From 158fb53d24d335f9efd825a42fff831bd5dc949f Mon Sep 17 00:00:00 2001 From: Matt Batchelder Date: Sat, 21 Feb 2026 12:19:19 -0500 Subject: [PATCH] Add "Pull Page" functionality to admin bar and REST API endpoint --- includes/admin.php | 76 +++++++++++++++++++++++ includes/rest.php | 23 +++++++ includes/sync-engine.php | 127 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 226 insertions(+) diff --git a/includes/admin.php b/includes/admin.php index c9eee8e..fe3f63b 100644 --- a/includes/admin.php +++ b/includes/admin.php @@ -8,6 +8,82 @@ if ( ! defined( 'ABSPATH' ) ) exit; +// ─── Admin bar pull button (front-end) ────────────────────────────────────── +add_action( 'admin_bar_menu', function ( WP_Admin_Bar $wp_admin_bar ) { + // Front-end only, logged-in admins, singular pages/posts + if ( is_admin() ) return; + if ( ! is_user_logged_in() ) return; + if ( ! current_user_can( 'manage_options' ) ) return; + if ( ! is_singular() ) return; + + $post = get_queried_object(); + if ( ! $post instanceof WP_Post ) return; + + $wp_admin_bar->add_node( [ + 'id' => 'oribi-sync-pull', + 'title' => 'Pull Page', + 'href' => '#', + 'meta' => [ + 'title' => 'Pull this page and theme from Git', + ], + ] ); +}, 100 ); + +// Front-end script that wires up the admin bar pull button +add_action( 'wp_footer', function () { + if ( ! is_user_logged_in() ) return; + if ( ! current_user_can( 'manage_options' ) ) return; + if ( ! is_singular() ) return; + if ( ! is_admin_bar_showing() ) return; + + $post = get_queried_object(); + if ( ! $post instanceof WP_Post ) return; + + $api_url = rest_url( 'oribi-sync/v1/pull-page' ); + $nonce = wp_create_nonce( 'wp_rest' ); + $post_id = (int) $post->ID; + ?> + + 'POST', + 'callback' => 'oribi_sync_rest_pull_page', + 'permission_callback' => function () { + return current_user_can( 'manage_options' ); + }, + ] ); + // ── Webhook (secret-based auth, no WP login required) ───────────────── register_rest_route( 'oribi-sync/v1', '/webhook', [ 'methods' => 'POST', @@ -86,6 +95,20 @@ function oribi_sync_rest_status(): WP_REST_Response { ] ); } +/** + * REST: Pull a single page and theme from the repo. + */ +function oribi_sync_rest_pull_page( WP_REST_Request $request ): WP_REST_Response { + $post_id = (int) $request->get_param( 'post_id' ); + if ( $post_id < 1 ) { + return new WP_REST_Response( [ 'ok' => false, 'message' => 'Missing or invalid post_id.' ], 400 ); + } + + $result = oribi_sync_pull_page_from_repo( $post_id ); + + return new WP_REST_Response( $result, $result['ok'] ? 200 : 500 ); +} + /** * REST: Webhook trigger. * diff --git a/includes/sync-engine.php b/includes/sync-engine.php index 9d995c4..7ab39d4 100644 --- a/includes/sync-engine.php +++ b/includes/sync-engine.php @@ -531,6 +531,133 @@ function oribi_sync_apply_theme_files( string $api_base, string $branch, string return $out; } +/** + * Pull a single page (and theme files) from the repo. + * + * Used by the admin-bar "Pull Page" button to re-sync only the page currently + * being viewed plus all theme files, then returns a result array. + * + * @param int $post_id WordPress post ID. + * + * @return array{ok: bool, created: string[], updated: string[], skipped: string[], errors: string[], theme_updated: string[]} + */ +function oribi_sync_pull_page_from_repo( int $post_id ): array { + $result = [ + 'ok' => true, + 'created' => [], + 'updated' => [], + 'skipped' => [], + 'errors' => [], + 'theme_updated' => [], + ]; + + $post = get_post( $post_id ); + if ( ! $post ) { + $result['ok'] = false; + $result['errors'][] = 'Post not found.'; + return $result; + } + + $slug = $post->post_name; + + // ── Gather settings ──────────────────────────────────────────────────── + $repo_url = get_option( 'oribi_sync_repo', '' ); + $branch = get_option( 'oribi_sync_branch', 'main' ) ?: 'main'; + $pat = oribi_sync_get_pat(); + + if ( empty( $repo_url ) || empty( $pat ) ) { + $result['ok'] = false; + $result['errors'][] = 'Repository URL or PAT is not configured.'; + return $result; + } + + $parsed = oribi_sync_parse_repo_url( $repo_url ); + if ( is_wp_error( $parsed ) ) { + $result['ok'] = false; + $result['errors'][] = $parsed->get_error_message(); + return $result; + } + + $provider = oribi_sync_get_provider(); + $api_base = oribi_sync_api_base( $provider, $parsed ); + + // ── Fetch tree ───────────────────────────────────────────────────────── + $tree = oribi_sync_fetch_tree( $api_base, $branch, $provider, $pat ); + if ( is_wp_error( $tree ) ) { + $result['ok'] = false; + $result['errors'][] = 'Tree fetch failed: ' . $tree->get_error_message(); + return $result; + } + + // ── Find the matching page file ──────────────────────────────────────── + $page_files = oribi_sync_filter_tree( $tree, 'Pages' ); + $target_entry = null; + + foreach ( $page_files as $entry ) { + $file_slug = oribi_sync_filename_to_slug( basename( $entry['path'] ) ); + if ( $file_slug === $slug ) { + $target_entry = $entry; + break; + } + } + + if ( ! $target_entry ) { + $result['skipped'][] = $slug . ' (not found in Pages/ directory)'; + } else { + $raw_content = oribi_sync_fetch_file( $api_base, $branch, $target_entry['path'], $provider, $pat ); + if ( is_wp_error( $raw_content ) ) { + $result['errors'][] = $target_entry['path'] . ': ' . $raw_content->get_error_message(); + $result['ok'] = false; + } else { + $raw_content = trim( $raw_content ); + $ext = strtolower( pathinfo( basename( $target_entry['path'] ), PATHINFO_EXTENSION ) ); + + if ( $ext === 'php' ) { + $content = oribi_sync_execute_php( $raw_content, $slug ); + if ( is_wp_error( $content ) ) { + $result['errors'][] = $target_entry['path'] . ': ' . $content->get_error_message(); + $result['ok'] = false; + $content = null; + } + } else { + $content = $raw_content; + } + + if ( $content !== null ) { + $checksum = hash( 'sha256', $raw_content ); + $git_sha = $target_entry['sha'] ?? ''; + + $update = wp_update_post( [ + 'ID' => $post->ID, + 'post_content' => $content, + 'post_status' => 'publish', + ], true ); + + if ( is_wp_error( $update ) ) { + $result['errors'][] = $slug . ': ' . $update->get_error_message(); + $result['ok'] = false; + } else { + update_post_meta( $post->ID, '_oribi_sync_checksum', $checksum ); + update_post_meta( $post->ID, '_oribi_sync_git_sha', $git_sha ); + update_post_meta( $post->ID, '_oribi_sync_source', $repo_url . '@' . $branch . ':' . $target_entry['path'] ); + update_post_meta( $post->ID, '_oribi_sync_last_run', current_time( 'mysql' ) ); + update_post_meta( $post->ID, '_wp_page_template', 'default' ); + $result['updated'][] = $slug; + } + } + } + } + + // ── Sync theme files ─────────────────────────────────────────────────── + $theme_sync = oribi_sync_apply_theme_files( $api_base, $branch, $provider, $pat, false, $tree ); + $result['theme_updated'] = $theme_sync['updated']; + foreach ( $theme_sync['errors'] as $err ) { + $result['errors'][] = '[theme] ' . $err; + } + + return $result; +} + /** * Fetch theme files from the repo (for preview / apply). *