Add "Pull Page" functionality to admin bar and REST API endpoint

This commit is contained in:
Matt Batchelder
2026-02-21 12:19:19 -05:00
parent d56d46490a
commit 158fb53d24
3 changed files with 226 additions and 0 deletions

View File

@@ -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' => '<span class="ab-icon dashicons dashicons-download" aria-hidden="true"></span><span class="ab-label">Pull Page</span>',
'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;
?>
<script>
(function () {
'use strict';
var btn = document.getElementById('wp-admin-bar-oribi-sync-pull');
if (!btn) return;
btn.addEventListener('click', function (e) {
e.preventDefault();
var link = btn.querySelector('a');
var label = btn.querySelector('.ab-label');
if (link) { link.style.opacity = '0.5'; link.style.pointerEvents = 'none'; }
if (label) { label.textContent = 'Pulling…'; }
fetch(<?php echo wp_json_encode( $api_url ); ?>, {
method: 'POST',
credentials: 'same-origin',
headers: {
'Content-Type': 'application/json',
'X-WP-Nonce': <?php echo wp_json_encode( $nonce ); ?>
},
body: JSON.stringify({ post_id: <?php echo $post_id; ?> })
})
.then(function (r) { return r.json(); })
.then(function () {
// Hard reload — append a cache-busting param to force a fresh response
var url = new URL(window.location.href);
url.searchParams.set('_nocache', Date.now());
window.location.replace(url.toString());
})
.catch(function (err) {
if (label) { label.textContent = 'Pull Page'; }
if (link) { link.style.opacity = ''; link.style.pointerEvents = ''; }
alert('Oribi Sync pull failed: ' + err);
});
});
})();
</script>
<?php
} );
// ─── Register admin menu ──────────────────────────────────────────────────────
add_action( 'admin_menu', function () {
add_options_page(

View File

@@ -48,6 +48,15 @@ add_action( 'rest_api_init', function () {
},
] );
// ── Pull single page + theme ───────────────────────────────────────────
register_rest_route( 'oribi-sync/v1', '/pull-page', [
'methods' => '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.
*

View File

@@ -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).
*