Compare commits
8 Commits
3c8c38acde
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3954d58e8a | ||
|
|
cdf176e224 | ||
|
|
3b51382797 | ||
|
|
634e93236f | ||
|
|
6c5e503eb2 | ||
|
|
b01e7e0e88 | ||
|
|
158fb53d24 | ||
|
|
d56d46490a |
BIN
dist/oribi-tech-sync.zip
vendored
BIN
dist/oribi-tech-sync.zip
vendored
Binary file not shown.
@@ -8,6 +8,168 @@
|
|||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
if ( ! defined( 'ABSPATH' ) ) exit;
|
||||||
|
|
||||||
|
// ─── Admin bar pull buttons (front-end) ─────────────────────────────────────
|
||||||
|
add_action( 'admin_bar_menu', function ( WP_Admin_Bar $wp_admin_bar ) {
|
||||||
|
if ( is_admin() ) return;
|
||||||
|
if ( ! is_user_logged_in() ) return;
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) return;
|
||||||
|
|
||||||
|
// "Pull All" — visible everywhere on the front-end
|
||||||
|
$wp_admin_bar->add_node( [
|
||||||
|
'id' => 'oribi-sync-pull-all',
|
||||||
|
'title' => '<span class="ab-icon dashicons dashicons-update" aria-hidden="true"></span><span class="ab-label">Pull All</span>',
|
||||||
|
'href' => '#',
|
||||||
|
'meta' => [
|
||||||
|
'title' => 'Pull all pages and theme from Git',
|
||||||
|
],
|
||||||
|
] );
|
||||||
|
|
||||||
|
// "Pull Page" — only on singular pages/posts
|
||||||
|
if ( is_singular() ) {
|
||||||
|
$post = get_queried_object();
|
||||||
|
if ( $post instanceof WP_Post ) {
|
||||||
|
$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 );
|
||||||
|
|
||||||
|
// AJAX handler for the admin bar "Pull All" button
|
||||||
|
add_action( 'wp_ajax_oribi_sync_pull_all_pages', function () {
|
||||||
|
check_ajax_referer( 'oribi_sync_pull_all_pages' );
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Permission denied.', 403 );
|
||||||
|
|
||||||
|
$result = oribi_sync_run();
|
||||||
|
$result['ok'] ? wp_send_json_success( $result ) : wp_send_json_error( $result, 500 );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// AJAX handler for the admin bar pull button (no REST API exposure)
|
||||||
|
add_action( 'wp_ajax_oribi_sync_pull_page', function () {
|
||||||
|
check_ajax_referer( 'oribi_sync_pull_page' );
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) wp_send_json_error( 'Permission denied.', 403 );
|
||||||
|
|
||||||
|
$post_id = (int) ( $_POST['post_id'] ?? 0 );
|
||||||
|
if ( $post_id < 1 ) wp_send_json_error( 'Missing or invalid post_id.', 400 );
|
||||||
|
|
||||||
|
$result = oribi_sync_pull_page_from_repo( $post_id );
|
||||||
|
$result['ok'] ? wp_send_json_success( $result ) : wp_send_json_error( $result, 500 );
|
||||||
|
} );
|
||||||
|
|
||||||
|
// Front-end script for the "Pull All" admin bar button
|
||||||
|
add_action( 'wp_footer', function () {
|
||||||
|
if ( ! is_user_logged_in() ) return;
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) return;
|
||||||
|
if ( ! is_admin_bar_showing() ) return;
|
||||||
|
|
||||||
|
$ajax_url = admin_url( 'admin-ajax.php' );
|
||||||
|
$nonce_all = wp_create_nonce( 'oribi_sync_pull_all_pages' );
|
||||||
|
?>
|
||||||
|
<script>
|
||||||
|
(function () {
|
||||||
|
'use strict';
|
||||||
|
var btn = document.getElementById('wp-admin-bar-oribi-sync-pull-all');
|
||||||
|
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…'; }
|
||||||
|
|
||||||
|
var data = new URLSearchParams({
|
||||||
|
action: 'oribi_sync_pull_all_pages',
|
||||||
|
_ajax_nonce: <?php echo wp_json_encode( $nonce_all ); ?>
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(<?php echo wp_json_encode( $ajax_url ); ?>, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: data.toString()
|
||||||
|
})
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function () {
|
||||||
|
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 All'; }
|
||||||
|
if (link) { link.style.opacity = ''; link.style.pointerEvents = ''; }
|
||||||
|
alert('Oribi Sync pull failed: ' + err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
<?php
|
||||||
|
} );
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
$ajax_url = admin_url( 'admin-ajax.php' );
|
||||||
|
$nonce = wp_create_nonce( 'oribi_sync_pull_page' );
|
||||||
|
$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…'; }
|
||||||
|
|
||||||
|
var data = new URLSearchParams({
|
||||||
|
action: 'oribi_sync_pull_page',
|
||||||
|
_ajax_nonce: <?php echo wp_json_encode( $nonce ); ?>,
|
||||||
|
post_id: <?php echo $post_id; ?>
|
||||||
|
});
|
||||||
|
|
||||||
|
fetch(<?php echo wp_json_encode( $ajax_url ); ?>, {
|
||||||
|
method: 'POST',
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||||
|
body: data.toString()
|
||||||
|
})
|
||||||
|
.then(function (r) { return r.json(); })
|
||||||
|
.then(function () {
|
||||||
|
// Hard reload — cache-busting param forces 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 ──────────────────────────────────────────────────────
|
// ─── Register admin menu ──────────────────────────────────────────────────────
|
||||||
add_action( 'admin_menu', function () {
|
add_action( 'admin_menu', function () {
|
||||||
add_options_page(
|
add_options_page(
|
||||||
@@ -39,6 +201,13 @@ add_action( 'admin_post_oribi_sync_save_settings', function () {
|
|||||||
update_option( 'oribi_sync_branch', $branch, 'no' );
|
update_option( 'oribi_sync_branch', $branch, 'no' );
|
||||||
update_option( 'oribi_sync_provider', $provider, 'no' );
|
update_option( 'oribi_sync_provider', $provider, 'no' );
|
||||||
|
|
||||||
|
// Posts sync settings
|
||||||
|
$posts_enabled = isset( $_POST['oribi_sync_posts_enabled'] ) ? '1' : '0';
|
||||||
|
$posts_folder = sanitize_text_field( wp_unslash( $_POST['oribi_sync_posts_folder'] ?? 'posts' ) );
|
||||||
|
$posts_folder = trim( $posts_folder, '/' ) ?: 'posts';
|
||||||
|
update_option( 'oribi_sync_posts_enabled', $posts_enabled, 'no' );
|
||||||
|
update_option( 'oribi_sync_posts_folder', $posts_folder, 'no' );
|
||||||
|
|
||||||
// Only update PAT if a new one was provided (non-empty)
|
// Only update PAT if a new one was provided (non-empty)
|
||||||
if ( ! empty( $pat ) ) {
|
if ( ! empty( $pat ) ) {
|
||||||
oribi_sync_save_pat( $pat );
|
oribi_sync_save_pat( $pat );
|
||||||
@@ -88,6 +257,18 @@ add_action( 'admin_post_oribi_sync_pull', function () {
|
|||||||
exit;
|
exit;
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
add_action( 'admin_post_oribi_sync_force_pull', function () {
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Permission denied.' );
|
||||||
|
check_admin_referer( 'oribi_sync_force_pull' );
|
||||||
|
|
||||||
|
$result = oribi_sync_run( false, true );
|
||||||
|
|
||||||
|
set_transient( 'oribi_sync_result', $result, 60 );
|
||||||
|
|
||||||
|
wp_redirect( add_query_arg( 'oribi_sync_done', 'force_pull', admin_url( 'options-general.php?page=oribi-sync' ) ) );
|
||||||
|
exit;
|
||||||
|
} );
|
||||||
|
|
||||||
add_action( 'admin_post_oribi_sync_clear_pat', function () {
|
add_action( 'admin_post_oribi_sync_clear_pat', function () {
|
||||||
if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Permission denied.' );
|
if ( ! current_user_can( 'manage_options' ) ) wp_die( 'Permission denied.' );
|
||||||
check_admin_referer( 'oribi_sync_clear_pat' );
|
check_admin_referer( 'oribi_sync_clear_pat' );
|
||||||
@@ -106,7 +287,10 @@ add_action( 'admin_post_oribi_sync_push', function () {
|
|||||||
$post_id = (int) ( $_POST['oribi_sync_push_post_id'] ?? 0 );
|
$post_id = (int) ( $_POST['oribi_sync_push_post_id'] ?? 0 );
|
||||||
|
|
||||||
if ( $post_id > 0 ) {
|
if ( $post_id > 0 ) {
|
||||||
$result = oribi_sync_push_page( $post_id );
|
$post = get_post( $post_id );
|
||||||
|
$result = ( $post && $post->post_type === 'post' )
|
||||||
|
? oribi_sync_push_post( $post_id )
|
||||||
|
: oribi_sync_push_page( $post_id );
|
||||||
set_transient( 'oribi_sync_push_result', $result, 60 );
|
set_transient( 'oribi_sync_push_result', $result, 60 );
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,6 +344,7 @@ function oribi_sync_settings_page() {
|
|||||||
<p><strong><?php
|
<p><strong><?php
|
||||||
if ( $done === 'dry' ) echo 'Dry-run results';
|
if ( $done === 'dry' ) echo 'Dry-run results';
|
||||||
elseif ( $done === 'pull' ) echo 'Pull complete';
|
elseif ( $done === 'pull' ) echo 'Pull complete';
|
||||||
|
elseif ( $done === 'force_pull' ) echo 'Force Pull complete';
|
||||||
else echo 'Sync complete';
|
else echo 'Sync complete';
|
||||||
?></strong></p>
|
?></strong></p>
|
||||||
<?php oribi_sync_render_result_list( $sync_result ); ?>
|
<?php oribi_sync_render_result_list( $sync_result ); ?>
|
||||||
@@ -249,6 +434,36 @@ function oribi_sync_settings_page() {
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
<h2 style="margin-top:1.5em;">Posts Sync</h2>
|
||||||
|
<p class="description">Import WordPress posts from Markdown files with YAML front-matter stored in a repo subfolder.</p>
|
||||||
|
<table class="form-table" role="presentation">
|
||||||
|
<tr>
|
||||||
|
<th scope="row">Enable Posts Sync</th>
|
||||||
|
<td>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="oribi_sync_posts_enabled" value="1"
|
||||||
|
<?php checked( get_option( 'oribi_sync_posts_enabled', '' ), '1' ); ?> />
|
||||||
|
Sync posts from the repository
|
||||||
|
</label>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><label for="oribi_sync_posts_folder">Posts Folder</label></th>
|
||||||
|
<td>
|
||||||
|
<input type="text" name="oribi_sync_posts_folder" id="oribi_sync_posts_folder"
|
||||||
|
value="<?php echo esc_attr( get_option( 'oribi_sync_posts_folder', 'posts' ) ); ?>"
|
||||||
|
class="regular-text" placeholder="posts" />
|
||||||
|
<p class="description">
|
||||||
|
Repo folder containing <code>.md</code> files (e.g. <code>posts</code> or <code>content/posts</code>).
|
||||||
|
Files may use <code>YYYY-MM-DD-slug.md</code> naming. Supported front-matter keys:
|
||||||
|
<code>title</code>, <code>slug</code>, <code>status</code>, <code>date</code>,
|
||||||
|
<code>author</code>, <code>categories</code>, <code>tags</code>,
|
||||||
|
<code>excerpt</code>, <code>featured_image</code>.
|
||||||
|
</p>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
|
||||||
<?php submit_button( 'Save Settings' ); ?>
|
<?php submit_button( 'Save Settings' ); ?>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
@@ -267,6 +482,11 @@ function oribi_sync_settings_page() {
|
|||||||
onclick="return confirm('Pull content from the repo (no push). Continue?');">
|
onclick="return confirm('Pull content from the repo (no push). Continue?');">
|
||||||
Pull Only
|
Pull Only
|
||||||
</a>
|
</a>
|
||||||
|
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=oribi_sync_force_pull' ), 'oribi_sync_force_pull' ) ); ?>"
|
||||||
|
class="button"
|
||||||
|
onclick="return confirm('Force re-pull ALL content from the repo, bypassing change detection. Continue?');">
|
||||||
|
Force Pull
|
||||||
|
</a>
|
||||||
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=oribi_sync_push_all' ), 'oribi_sync_push_all' ) ); ?>"
|
<a href="<?php echo esc_url( wp_nonce_url( admin_url( 'admin-post.php?action=oribi_sync_push_all' ), 'oribi_sync_push_all' ) ); ?>"
|
||||||
class="button"
|
class="button"
|
||||||
onclick="return confirm('Push all synced pages to the repo (no pull). Continue?');">
|
onclick="return confirm('Push all synced pages to the repo (no pull). Continue?');">
|
||||||
@@ -333,6 +553,61 @@ function oribi_sync_settings_page() {
|
|||||||
</table>
|
</table>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<?php
|
||||||
|
// ── Synced posts ──────────────────────────────────────────────────
|
||||||
|
if ( get_option( 'oribi_sync_posts_enabled', '' ) ):
|
||||||
|
$synced_posts = new WP_Query( [
|
||||||
|
'post_type' => 'post',
|
||||||
|
'post_status' => [ 'publish', 'draft', 'pending', 'private' ],
|
||||||
|
'meta_key' => '_oribi_sync_checksum',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'orderby' => 'date',
|
||||||
|
'order' => 'DESC',
|
||||||
|
] );
|
||||||
|
|
||||||
|
if ( $synced_posts->have_posts() ):
|
||||||
|
?>
|
||||||
|
<h2>Synced Posts</h2>
|
||||||
|
<table class="widefat fixed striped oribi-sync-pages">
|
||||||
|
<thead><tr>
|
||||||
|
<th>Post</th>
|
||||||
|
<th style="width:80px;">Push</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<?php while ( $synced_posts->have_posts() ): $synced_posts->the_post();
|
||||||
|
$pid = get_the_ID();
|
||||||
|
$last_push = get_post_meta( $pid, '_oribi_sync_last_push', true );
|
||||||
|
$pr_url = get_post_meta( $pid, '_oribi_sync_pr_url', true );
|
||||||
|
?>
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<strong><?php the_title(); ?></strong>
|
||||||
|
<span class="oribi-sync-muted"><?php echo esc_html( get_post_field( 'post_name', $pid ) ); ?></span>
|
||||||
|
<span class="oribi-sync-muted"> — <?php echo esc_html( get_post_status( $pid ) ); ?></span>
|
||||||
|
<?php if ( $pr_url ): ?>
|
||||||
|
<a href="<?php echo esc_url( $pr_url ); ?>" target="_blank" rel="noopener" class="oribi-sync-pr-badge">PR</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ( $last_push ): ?>
|
||||||
|
<br /><span class="oribi-sync-muted">pushed <?php echo esc_html( $last_push ); ?></span>
|
||||||
|
<?php endif; ?>
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin:0;">
|
||||||
|
<input type="hidden" name="action" value="oribi_sync_push" />
|
||||||
|
<input type="hidden" name="oribi_sync_push_post_id" value="<?php echo esc_attr( $pid ); ?>" />
|
||||||
|
<?php wp_nonce_field( 'oribi_sync_push' ); ?>
|
||||||
|
<button type="submit" class="button button-small">Push</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
<?php endwhile; wp_reset_postdata(); ?>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<?php
|
||||||
|
endif; // have_posts
|
||||||
|
endif; // posts_enabled
|
||||||
|
?>
|
||||||
|
|
||||||
<?php // ── Theme preview (conditional) ────────────────────────────── ?>
|
<?php // ── Theme preview (conditional) ────────────────────────────── ?>
|
||||||
<?php if ( isset( $_GET['oribi_sync_tab'] ) && $_GET['oribi_sync_tab'] === 'theme' ): ?>
|
<?php if ( isset( $_GET['oribi_sync_tab'] ) && $_GET['oribi_sync_tab'] === 'theme' ): ?>
|
||||||
<hr />
|
<hr />
|
||||||
@@ -374,10 +649,13 @@ function oribi_sync_settings_page() {
|
|||||||
*/
|
*/
|
||||||
function oribi_sync_render_result_list( array $r ): void {
|
function oribi_sync_render_result_list( array $r ): void {
|
||||||
$items = [];
|
$items = [];
|
||||||
if ( ! empty( $r['created'] ) ) $items[] = 'Created: ' . implode( ', ', $r['created'] );
|
if ( ! empty( $r['created'] ) ) $items[] = 'Pages created: ' . implode( ', ', $r['created'] );
|
||||||
if ( ! empty( $r['updated'] ) ) $items[] = 'Updated: ' . implode( ', ', $r['updated'] );
|
if ( ! empty( $r['updated'] ) ) $items[] = 'Pages updated: ' . implode( ', ', $r['updated'] );
|
||||||
if ( ! empty( $r['theme_updated'] ) ) $items[] = 'Theme: ' . implode( ', ', $r['theme_updated'] );
|
if ( ! empty( $r['theme_updated'] ) ) $items[] = 'Theme: ' . implode( ', ', $r['theme_updated'] );
|
||||||
if ( ! empty( $r['trashed'] ) ) $items[] = 'Trashed: ' . implode( ', ', $r['trashed'] );
|
if ( ! empty( $r['trashed'] ) ) $items[] = 'Pages trashed: ' . implode( ', ', $r['trashed'] );
|
||||||
|
if ( ! empty( $r['posts_created'] ) ) $items[] = 'Posts created: ' . implode( ', ', $r['posts_created'] );
|
||||||
|
if ( ! empty( $r['posts_updated'] ) ) $items[] = 'Posts updated: ' . implode( ', ', $r['posts_updated'] );
|
||||||
|
if ( ! empty( $r['posts_trashed'] ) ) $items[] = 'Posts trashed: ' . implode( ', ', $r['posts_trashed'] );
|
||||||
if ( ! empty( $r['skipped'] ) ) $items[] = 'Skipped: ' . implode( ', ', $r['skipped'] );
|
if ( ! empty( $r['skipped'] ) ) $items[] = 'Skipped: ' . implode( ', ', $r['skipped'] );
|
||||||
if ( ! empty( $r['errors'] ) ) $items[] = 'Errors: ' . implode( '; ', $r['errors'] );
|
if ( ! empty( $r['errors'] ) ) $items[] = 'Errors: ' . implode( '; ', $r['errors'] );
|
||||||
|
|
||||||
|
|||||||
@@ -341,14 +341,15 @@ function oribi_sync_fetch_file( string $api_base, string $branch, string $file_p
|
|||||||
break;
|
break;
|
||||||
|
|
||||||
case 'gitea':
|
case 'gitea':
|
||||||
$url = $api_base . '/raw/' . $encoded_path . '?ref=' . rawurlencode( $branch );
|
// Use /contents/ endpoint which returns base64-encoded content (more reliable)
|
||||||
$accept = 'text/plain';
|
$url = $api_base . '/contents/' . $encoded_path . '?ref=' . rawurlencode( $branch );
|
||||||
|
$accept = 'application/json';
|
||||||
break;
|
break;
|
||||||
|
|
||||||
case 'github':
|
case 'github':
|
||||||
default:
|
default:
|
||||||
$url = $api_base . '/contents/' . $encoded_path . '?ref=' . rawurlencode( $branch );
|
$url = $api_base . '/contents/' . $encoded_path . '?ref=' . rawurlencode( $branch );
|
||||||
$accept = 'application/vnd.github.raw+json';
|
$accept = 'application/vnd.github.raw';
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -377,7 +378,30 @@ function oribi_sync_fetch_file( string $api_base, string $branch, string $file_p
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return wp_remote_retrieve_body( $response );
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
|
||||||
|
// For Gitea, the /contents/ endpoint returns base64-encoded content in JSON.
|
||||||
|
// Gitea (like GitHub) inserts \n every 60 chars in the base64 — strip them before decoding.
|
||||||
|
if ( $provider === 'gitea' ) {
|
||||||
|
$decoded = json_decode( $body, true );
|
||||||
|
if ( isset( $decoded['content'] ) && is_string( $decoded['content'] ) ) {
|
||||||
|
$clean = str_replace( [ "\r", "\n", " " ], '', $decoded['content'] );
|
||||||
|
$body = base64_decode( $clean, true );
|
||||||
|
if ( $body === false ) {
|
||||||
|
return new WP_Error( 'oribi_sync_decode_error', 'Failed to decode base64 content from Gitea.' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate and fix encoding if necessary (handles non-UTF-8 sources)
|
||||||
|
if ( ! empty( $body ) ) {
|
||||||
|
if ( ! mb_check_encoding( $body, 'UTF-8' ) ) {
|
||||||
|
// Try to convert from common encodings to UTF-8
|
||||||
|
$body = mb_convert_encoding( $body, 'UTF-8', 'UTF-8, ISO-8859-1, Windows-1252' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $body;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Tree filtering ───────────────────────────────────────────────────────────
|
// ─── Tree filtering ───────────────────────────────────────────────────────────
|
||||||
|
|||||||
1076
includes/post-sync.php
Normal file
1076
includes/post-sync.php
Normal file
File diff suppressed because it is too large
Load Diff
@@ -44,36 +44,6 @@ function oribi_sync_detect_pages_prefix(): string {
|
|||||||
return 'pages/';
|
return 'pages/';
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Auto-push on page save ──────────────────────────────────────────────────
|
|
||||||
add_action( 'save_post_page', 'oribi_sync_maybe_push_on_save', 20, 3 );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Push a synced page to the repo whenever it is saved.
|
|
||||||
*
|
|
||||||
* Only fires for pages that were previously pulled (have _oribi_sync_checksum
|
|
||||||
* meta), skips autosaves, revisions, and non-publish statuses.
|
|
||||||
* Uses a static guard to prevent re-entry when we update post meta after push.
|
|
||||||
*/
|
|
||||||
function oribi_sync_maybe_push_on_save( int $post_id, WP_Post $post, bool $update ): void {
|
|
||||||
// Guard: only on genuine content saves
|
|
||||||
if ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) return;
|
|
||||||
if ( wp_is_post_revision( $post_id ) ) return;
|
|
||||||
if ( $post->post_status !== 'publish' ) return;
|
|
||||||
|
|
||||||
// Guard: only pages that came from a sync (have checksum meta)
|
|
||||||
$checksum = get_post_meta( $post_id, '_oribi_sync_checksum', true );
|
|
||||||
if ( empty( $checksum ) ) return;
|
|
||||||
|
|
||||||
// Guard: prevent re-entry when push updates meta on the same post
|
|
||||||
static $pushing = [];
|
|
||||||
if ( isset( $pushing[ $post_id ] ) ) return;
|
|
||||||
$pushing[ $post_id ] = true;
|
|
||||||
|
|
||||||
oribi_sync_push_page( $post_id );
|
|
||||||
|
|
||||||
unset( $pushing[ $post_id ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
// ─── Generic authenticated request helpers ────────────────────────────────────
|
// ─── Generic authenticated request helpers ────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -117,17 +87,24 @@ function oribi_sync_api_request( string $method, string $url, array $body, strin
|
|||||||
$headers = array_merge(
|
$headers = array_merge(
|
||||||
oribi_sync_auth_headers( $provider, $pat ),
|
oribi_sync_auth_headers( $provider, $pat ),
|
||||||
[
|
[
|
||||||
'Content-Type' => 'application/json',
|
'Content-Type' => 'application/json; charset=utf-8',
|
||||||
'Accept' => 'application/json',
|
'Accept' => 'application/json',
|
||||||
'User-Agent' => 'Oribi-Sync-WP/' . ORIBI_SYNC_VERSION,
|
'User-Agent' => 'Oribi-Sync-WP/' . ORIBI_SYNC_VERSION,
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Ensure UTF-8 encoding of all body content
|
||||||
|
array_walk_recursive( $body, function ( &$item ) {
|
||||||
|
if ( is_string( $item ) && ! mb_check_encoding( $item, 'UTF-8' ) ) {
|
||||||
|
$item = mb_convert_encoding( $item, 'UTF-8' );
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
$args = [
|
$args = [
|
||||||
'method' => $method,
|
'method' => $method,
|
||||||
'timeout' => 30,
|
'timeout' => 30,
|
||||||
'headers' => $headers,
|
'headers' => $headers,
|
||||||
'body' => wp_json_encode( $body ),
|
'body' => wp_json_encode( $body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ),
|
||||||
];
|
];
|
||||||
|
|
||||||
$response = wp_remote_request( $url, $args );
|
$response = wp_remote_request( $url, $args );
|
||||||
@@ -175,9 +152,15 @@ function oribi_sync_gitea_get_file_meta( string $api_base, string $branch, strin
|
|||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Gitea inserts \n every 60 chars in base64 — strip before decoding.
|
||||||
|
$raw_b64 = $result['content'] ?? '';
|
||||||
|
$content = ! empty( $raw_b64 )
|
||||||
|
? base64_decode( str_replace( [ "\r", "\n", " " ], '', $raw_b64 ), true )
|
||||||
|
: '';
|
||||||
|
|
||||||
return [
|
return [
|
||||||
'sha' => $result['sha'] ?? '',
|
'sha' => $result['sha'] ?? '',
|
||||||
'content' => isset( $result['content'] ) ? base64_decode( $result['content'] ) : '',
|
'content' => ( $content !== false ) ? $content : '',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -205,6 +188,11 @@ function oribi_sync_gitea_put_file(
|
|||||||
?string $sha = null,
|
?string $sha = null,
|
||||||
string $message = ''
|
string $message = ''
|
||||||
) {
|
) {
|
||||||
|
// Validate and fix UTF-8 encoding before base64-encoding
|
||||||
|
if ( ! mb_check_encoding( $content, 'UTF-8' ) ) {
|
||||||
|
$content = mb_convert_encoding( $content, 'UTF-8', 'UTF-8, ISO-8859-1, Windows-1252' );
|
||||||
|
}
|
||||||
|
|
||||||
$encoded_path = implode( '/', array_map( 'rawurlencode', explode( '/', $filepath ) ) );
|
$encoded_path = implode( '/', array_map( 'rawurlencode', explode( '/', $filepath ) ) );
|
||||||
$url = $api_base . '/contents/' . $encoded_path;
|
$url = $api_base . '/contents/' . $encoded_path;
|
||||||
|
|
||||||
@@ -400,7 +388,16 @@ function oribi_sync_push_page( int $post_id, array $opts = [] ): array {
|
|||||||
// ── Generate content ──────────────────────────────────────────────────
|
// ── Generate content ──────────────────────────────────────────────────
|
||||||
$slug = $post->post_name;
|
$slug = $post->post_name;
|
||||||
$title = $post->post_title;
|
$title = $post->post_title;
|
||||||
$wp_content = $post->post_content;
|
|
||||||
|
// Read post_content directly from the DB — bypassing every get_post()
|
||||||
|
// filter — so we get exactly what oribi_sync_save_post() wrote.
|
||||||
|
global $wpdb;
|
||||||
|
$wp_content = (string) $wpdb->get_var(
|
||||||
|
$wpdb->prepare( 'SELECT post_content FROM ' . $wpdb->posts . ' WHERE ID = %d', $post_id )
|
||||||
|
);
|
||||||
|
|
||||||
|
// Clean any corruption baked in by previous syncs (e.g. \u0026amp; artefacts)
|
||||||
|
$wp_content = oribi_sync_clean_block_content( $wp_content );
|
||||||
|
|
||||||
$commit_msg = $opts['message'] ?? "Sync: update {$slug} from WordPress";
|
$commit_msg = $opts['message'] ?? "Sync: update {$slug} from WordPress";
|
||||||
|
|
||||||
|
|||||||
@@ -1,153 +0,0 @@
|
|||||||
<?php
|
|
||||||
/**
|
|
||||||
* Oribi Sync — REST API endpoints.
|
|
||||||
*
|
|
||||||
* POST /wp-json/oribi-sync/v1/sync — Trigger a sync
|
|
||||||
* POST /wp-json/oribi-sync/v1/sync — With ?dry_run=1 for preview
|
|
||||||
* GET /wp-json/oribi-sync/v1/status — Get last sync status
|
|
||||||
* POST /wp-json/oribi-sync/v1/webhook — Webhook trigger (secret-based auth)
|
|
||||||
*/
|
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) exit;
|
|
||||||
|
|
||||||
add_action( 'rest_api_init', function () {
|
|
||||||
|
|
||||||
// ── Trigger sync ──────────────────────────────────────────────────────
|
|
||||||
register_rest_route( 'oribi-sync/v1', '/sync', [
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => 'oribi_sync_rest_sync',
|
|
||||||
'permission_callback' => function () {
|
|
||||||
return current_user_can( 'manage_options' );
|
|
||||||
},
|
|
||||||
] );
|
|
||||||
|
|
||||||
// ── Sync status ───────────────────────────────────────────────────────
|
|
||||||
register_rest_route( 'oribi-sync/v1', '/status', [
|
|
||||||
'methods' => 'GET',
|
|
||||||
'callback' => 'oribi_sync_rest_status',
|
|
||||||
'permission_callback' => function () {
|
|
||||||
return current_user_can( 'manage_options' );
|
|
||||||
},
|
|
||||||
] );
|
|
||||||
|
|
||||||
// ── Push page to repo ──────────────────────────────────────────────────
|
|
||||||
register_rest_route( 'oribi-sync/v1', '/push', [
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => 'oribi_sync_rest_push',
|
|
||||||
'permission_callback' => function () {
|
|
||||||
return current_user_can( 'manage_options' );
|
|
||||||
},
|
|
||||||
] );
|
|
||||||
|
|
||||||
// ── Push all synced pages to repo ──────────────────────────────────────
|
|
||||||
register_rest_route( 'oribi-sync/v1', '/push-all', [
|
|
||||||
'methods' => 'POST',
|
|
||||||
'callback' => 'oribi_sync_rest_push_all',
|
|
||||||
'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',
|
|
||||||
'callback' => 'oribi_sync_rest_webhook',
|
|
||||||
'permission_callback' => '__return_true', // Auth handled in callback
|
|
||||||
] );
|
|
||||||
} );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST: Trigger sync.
|
|
||||||
*/
|
|
||||||
function oribi_sync_rest_sync( WP_REST_Request $request ): WP_REST_Response {
|
|
||||||
$dry_run = (bool) $request->get_param( 'dry_run' );
|
|
||||||
$result = oribi_sync_run( $dry_run );
|
|
||||||
|
|
||||||
// After pulling, push local changes back (skip during dry-run)
|
|
||||||
if ( ! $dry_run ) {
|
|
||||||
$push = oribi_sync_push_all();
|
|
||||||
$result['push'] = $push['results'];
|
|
||||||
}
|
|
||||||
|
|
||||||
return new WP_REST_Response( $result, $result['ok'] ? 200 : 500 );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST: Get last sync status.
|
|
||||||
*/
|
|
||||||
function oribi_sync_rest_status(): WP_REST_Response {
|
|
||||||
return new WP_REST_Response( [
|
|
||||||
'last_run' => get_option( 'oribi_sync_last_run', null ),
|
|
||||||
'log' => array_slice( get_option( 'oribi_sync_log', [] ), 0, 5 ),
|
|
||||||
'repo' => get_option( 'oribi_sync_repo', '' ),
|
|
||||||
'branch' => get_option( 'oribi_sync_branch', 'main' ),
|
|
||||||
'provider' => oribi_sync_get_provider(),
|
|
||||||
'has_pat' => ! empty( get_option( 'oribi_sync_pat', '' ) ),
|
|
||||||
] );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST: Webhook trigger.
|
|
||||||
*
|
|
||||||
* Validates using a shared secret stored in the WP option oribi_sync_webhook_secret
|
|
||||||
* or the constant ORIBI_SYNC_WEBHOOK_SECRET.
|
|
||||||
*
|
|
||||||
* Accepts GitHub-style X-Hub-Signature-256 header, or a simple
|
|
||||||
* Authorization: Bearer <secret> header.
|
|
||||||
*/
|
|
||||||
function oribi_sync_rest_webhook( WP_REST_Request $request ): WP_REST_Response {
|
|
||||||
$secret = defined( 'ORIBI_SYNC_WEBHOOK_SECRET' )
|
|
||||||
? ORIBI_SYNC_WEBHOOK_SECRET
|
|
||||||
: get_option( 'oribi_sync_webhook_secret', '' );
|
|
||||||
|
|
||||||
if ( empty( $secret ) ) {
|
|
||||||
return new WP_REST_Response( [ 'error' => 'Webhook secret not configured.' ], 403 );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check Authorization: Bearer <secret>
|
|
||||||
$auth = $request->get_header( 'Authorization' );
|
|
||||||
if ( $auth && preg_match( '/^Bearer\s+(.+)$/i', $auth, $m ) ) {
|
|
||||||
if ( ! hash_equals( $secret, $m[1] ) ) {
|
|
||||||
return new WP_REST_Response( [ 'error' => 'Invalid secret.' ], 403 );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Check GitHub X-Hub-Signature-256
|
|
||||||
elseif ( $sig = $request->get_header( 'X-Hub-Signature-256' ) ) {
|
|
||||||
$body = $request->get_body();
|
|
||||||
$expected = 'sha256=' . hash_hmac( 'sha256', $body, $secret );
|
|
||||||
if ( ! hash_equals( $expected, $sig ) ) {
|
|
||||||
return new WP_REST_Response( [ 'error' => 'Invalid signature.' ], 403 );
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return new WP_REST_Response( [ 'error' => 'Missing authentication.' ], 403 );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run sync
|
|
||||||
$result = oribi_sync_run();
|
|
||||||
|
|
||||||
return new WP_REST_Response( $result, $result['ok'] ? 200 : 500 );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST: Push a single page to the repo.
|
|
||||||
*/
|
|
||||||
function oribi_sync_rest_push( 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 );
|
|
||||||
}
|
|
||||||
|
|
||||||
$opts = [];
|
|
||||||
$message = $request->get_param( 'message' );
|
|
||||||
if ( ! empty( $message ) ) {
|
|
||||||
$opts['message'] = sanitize_text_field( $message );
|
|
||||||
}
|
|
||||||
|
|
||||||
$result = oribi_sync_push_page( $post_id, $opts );
|
|
||||||
|
|
||||||
return new WP_REST_Response( $result, $result['ok'] ? 200 : 500 );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* REST: Push all synced pages to the repo.
|
|
||||||
*/
|
|
||||||
@@ -11,6 +11,93 @@ if ( ! defined( 'ABSPATH' ) ) exit;
|
|||||||
|
|
||||||
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Insert or update a post while writing post_content DIRECTLY to the DB.
|
||||||
|
*
|
||||||
|
* Every code-path in wp_insert_post / wp_update_post runs the content through
|
||||||
|
* sanitize_post_field() → apply_filters('pre_post_content') and
|
||||||
|
* apply_filters('content_save_pre'), both of which have wp_kses_post
|
||||||
|
* callbacks that turn & (inside Gutenberg block JSON) into &.
|
||||||
|
* kses_remove_filters() only unhooks content_save_pre, NOT pre_post_content,
|
||||||
|
* so the ampersand corruption survived even with those wrappers.
|
||||||
|
*
|
||||||
|
* This helper lets WP create/update every other field normally (title, slug,
|
||||||
|
* status, dates, author …) with an empty content placeholder, then immediately
|
||||||
|
* overwrites post_content in the DB directly — no filters, no escaping beyond
|
||||||
|
* the $wpdb placeholder.
|
||||||
|
*
|
||||||
|
* @param array $post_arr Same shape as wp_insert_post / wp_update_post.
|
||||||
|
* @return int|WP_Error Post ID on success, WP_Error on failure.
|
||||||
|
*/
|
||||||
|
function oribi_sync_save_post( array $post_arr ) {
|
||||||
|
global $wpdb;
|
||||||
|
|
||||||
|
$content = $post_arr['post_content'] ?? '';
|
||||||
|
$post_arr['post_content'] = ''; // let WP handle everything else
|
||||||
|
|
||||||
|
if ( ! empty( $post_arr['ID'] ) ) {
|
||||||
|
$post_id = wp_update_post( $post_arr, true );
|
||||||
|
} else {
|
||||||
|
$post_id = wp_insert_post( $post_arr, true );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_wp_error( $post_id ) ) {
|
||||||
|
return $post_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
$wpdb->update(
|
||||||
|
$wpdb->posts,
|
||||||
|
[ 'post_content' => $content ],
|
||||||
|
[ 'ID' => (int) $post_id ],
|
||||||
|
[ '%s' ],
|
||||||
|
[ '%d' ]
|
||||||
|
);
|
||||||
|
|
||||||
|
clean_post_cache( (int) $post_id );
|
||||||
|
|
||||||
|
return $post_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean previously-corrupted Gutenberg block content.
|
||||||
|
*
|
||||||
|
* Old syncs ran content through wp_kses_post which HTML-entity-encoded `&`
|
||||||
|
* inside JSON attributes to `&`. php's json_encode then re-encoded that
|
||||||
|
* `&` to `\u0026`, producing `\u0026amp;` instead of just `\u0026`.
|
||||||
|
*
|
||||||
|
* This function corrects those artefacts so block JSON attributes contain
|
||||||
|
* the right unicode escape sequences.
|
||||||
|
*
|
||||||
|
* Also normalises plain `&` → `&` inside JSON block comments so the
|
||||||
|
* next round of json_encode produces a single clean `\u0026`.
|
||||||
|
*
|
||||||
|
* @param string $content Gutenberg block HTML.
|
||||||
|
* @return string Cleaned block HTML.
|
||||||
|
*/
|
||||||
|
function oribi_sync_clean_block_content( string $content ): string {
|
||||||
|
// json_encode always hex-escapes & as \u0026 (even with JSON_UNESCAPED_UNICODE,
|
||||||
|
// which only affects codepoints > U+007F). Previous syncs also ran content
|
||||||
|
// through wp_kses_post which turned & into &, so json_encode then produced
|
||||||
|
// \u0026amp; instead of just \u0026.
|
||||||
|
//
|
||||||
|
// Fix the double-encoded forms first, then unescape the remaining \u0026 back
|
||||||
|
// to literal & — Gutenberg's block JSON parser treats both identically.
|
||||||
|
// These sequences are unambiguous in Gutenberg block comment JSON.
|
||||||
|
$content = str_replace( '\u0026amp;', '&', $content );
|
||||||
|
$content = str_replace( '\u0026lt;', '<', $content );
|
||||||
|
$content = str_replace( '\u0026gt;', '>', $content );
|
||||||
|
$content = str_replace( '\u0026quot;', '"', $content );
|
||||||
|
$content = str_replace( '\u0026#039;', "'", $content );
|
||||||
|
// Clean any remaining plain hex-escapes of ASCII punctuation
|
||||||
|
$content = str_replace( '\u0026', '&', $content );
|
||||||
|
$content = str_replace( '\u003C', '<', $content );
|
||||||
|
$content = str_replace( '\u003E', '>', $content );
|
||||||
|
$content = str_replace( '\u0022', '"', $content );
|
||||||
|
$content = str_replace( '\u0027', "'", $content );
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Strip a case-insensitive directory prefix from a file path.
|
* Strip a case-insensitive directory prefix from a file path.
|
||||||
*
|
*
|
||||||
@@ -29,14 +116,26 @@ function oribi_sync_strip_prefix( string $path, string $prefix ): string {
|
|||||||
/** Generate a self-closing block comment (standalone or child blocks). */
|
/** Generate a self-closing block comment (standalone or child blocks). */
|
||||||
if ( ! function_exists( 'oribi_b' ) ) {
|
if ( ! function_exists( 'oribi_b' ) ) {
|
||||||
function oribi_b( $name, $attrs = [] ) {
|
function oribi_b( $name, $attrs = [] ) {
|
||||||
return '<!-- wp:oribi/' . $name . ' ' . wp_json_encode( $attrs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) . ' /-->';
|
$json = wp_json_encode( $attrs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
|
||||||
|
// json_encode always hex-escapes & < > ' for XSS safety, but these are
|
||||||
|
// inside HTML comments so they are safe as literals in Gutenberg block JSON.
|
||||||
|
$json = str_replace( [ '\u0026', '\u003C', '\u003E', '\u0022', '\u0027' ],
|
||||||
|
[ '&', '<', '>', '"', "'" ], $json );
|
||||||
|
return '<!-- wp:oribi/' . $name . ' ' . $json . ' /-->';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Generate an opening tag for a parent block comment. */
|
/** Generate an opening tag for a parent block comment. */
|
||||||
if ( ! function_exists( 'oribi_b_open' ) ) {
|
if ( ! function_exists( 'oribi_b_open' ) ) {
|
||||||
function oribi_b_open( $name, $attrs = [] ) {
|
function oribi_b_open( $name, $attrs = [] ) {
|
||||||
$json = ! empty( $attrs ) ? ' ' . wp_json_encode( $attrs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ) : '';
|
if ( ! empty( $attrs ) ) {
|
||||||
|
$json = wp_json_encode( $attrs, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES );
|
||||||
|
$json = str_replace( [ '\u0026', '\u003C', '\u003E', '\u0022', '\u0027' ],
|
||||||
|
[ '&', '<', '>', '"', "'" ], $json );
|
||||||
|
$json = ' ' . $json;
|
||||||
|
} else {
|
||||||
|
$json = '';
|
||||||
|
}
|
||||||
return '<!-- wp:oribi/' . $name . $json . ' -->';
|
return '<!-- wp:oribi/' . $name . $json . ' -->';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -100,10 +199,11 @@ function oribi_sync_execute_php( string $php_source, string $slug ) {
|
|||||||
* Run the full page sync.
|
* Run the full page sync.
|
||||||
*
|
*
|
||||||
* @param bool $dry_run If true, returns what would happen without making changes.
|
* @param bool $dry_run If true, returns what would happen without making changes.
|
||||||
|
* @param bool $force If true, bypasses SHA-based change detection and re-pulls all files.
|
||||||
*
|
*
|
||||||
* @return array{ok: bool, created: string[], updated: string[], trashed: string[], skipped: string[], errors: string[]}
|
* @return array{ok: bool, created: string[], updated: string[], trashed: string[], skipped: string[], errors: string[]}
|
||||||
*/
|
*/
|
||||||
function oribi_sync_run( bool $dry_run = false ): array {
|
function oribi_sync_run( bool $dry_run = false, bool $force = false ): array {
|
||||||
$result = [
|
$result = [
|
||||||
'ok' => true,
|
'ok' => true,
|
||||||
'created' => [],
|
'created' => [],
|
||||||
@@ -112,6 +212,9 @@ function oribi_sync_run( bool $dry_run = false ): array {
|
|||||||
'skipped' => [],
|
'skipped' => [],
|
||||||
'errors' => [],
|
'errors' => [],
|
||||||
'theme_updated' => [],
|
'theme_updated' => [],
|
||||||
|
'posts_created' => [],
|
||||||
|
'posts_updated' => [],
|
||||||
|
'posts_trashed' => [],
|
||||||
];
|
];
|
||||||
|
|
||||||
// ── Gather settings ────────────────────────────────────────────────────
|
// ── Gather settings ────────────────────────────────────────────────────
|
||||||
@@ -174,7 +277,7 @@ function oribi_sync_run( bool $dry_run = false ): array {
|
|||||||
$git_sha = $entry['sha'] ?? '';
|
$git_sha = $entry['sha'] ?? '';
|
||||||
$stored_git_sha = $existing ? get_post_meta( $existing->ID, '_oribi_sync_git_sha', true ) : '';
|
$stored_git_sha = $existing ? get_post_meta( $existing->ID, '_oribi_sync_git_sha', true ) : '';
|
||||||
|
|
||||||
if ( $existing && ! empty( $git_sha ) && $git_sha === $stored_git_sha ) {
|
if ( ! $force && $existing && ! empty( $git_sha ) && $git_sha === $stored_git_sha ) {
|
||||||
$result['skipped'][] = $slug . ' (unchanged)';
|
$result['skipped'][] = $slug . ' (unchanged)';
|
||||||
if ( ! $dry_run ) {
|
if ( ! $dry_run ) {
|
||||||
update_post_meta( $existing->ID, '_oribi_sync_last_run', current_time( 'mysql' ) );
|
update_post_meta( $existing->ID, '_oribi_sync_last_run', current_time( 'mysql' ) );
|
||||||
@@ -206,6 +309,9 @@ function oribi_sync_run( bool $dry_run = false ): array {
|
|||||||
$content = $raw_content;
|
$content = $raw_content;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clean any corruption from previous syncs (e.g. \u0026amp; artefacts)
|
||||||
|
$content = oribi_sync_clean_block_content( $content );
|
||||||
|
|
||||||
// Checksum based on raw source — used as fallback for providers without tree SHA
|
// Checksum based on raw source — used as fallback for providers without tree SHA
|
||||||
$checksum = hash( 'sha256', $raw_content );
|
$checksum = hash( 'sha256', $raw_content );
|
||||||
|
|
||||||
@@ -235,11 +341,11 @@ function oribi_sync_run( bool $dry_run = false ): array {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$update_result = wp_update_post( [
|
$update_result = oribi_sync_save_post( [
|
||||||
'ID' => $existing->ID,
|
'ID' => $existing->ID,
|
||||||
'post_content' => $content,
|
'post_content' => $content,
|
||||||
'post_status' => 'publish',
|
'post_status' => 'publish',
|
||||||
], true );
|
] );
|
||||||
|
|
||||||
if ( is_wp_error( $update_result ) ) {
|
if ( is_wp_error( $update_result ) ) {
|
||||||
$result['errors'][] = $slug . ': ' . $update_result->get_error_message();
|
$result['errors'][] = $slug . ': ' . $update_result->get_error_message();
|
||||||
@@ -258,13 +364,13 @@ function oribi_sync_run( bool $dry_run = false ): array {
|
|||||||
// Create new page
|
// Create new page
|
||||||
$title = oribi_sync_slug_to_title( $slug );
|
$title = oribi_sync_slug_to_title( $slug );
|
||||||
|
|
||||||
$post_id = wp_insert_post( [
|
$post_id = oribi_sync_save_post( [
|
||||||
'post_title' => $title,
|
'post_title' => $title,
|
||||||
'post_name' => $slug,
|
'post_name' => $slug,
|
||||||
'post_status' => 'publish',
|
'post_status' => 'publish',
|
||||||
'post_type' => 'page',
|
'post_type' => 'page',
|
||||||
'post_content' => $content,
|
'post_content' => $content,
|
||||||
], true );
|
] );
|
||||||
|
|
||||||
if ( is_wp_error( $post_id ) ) {
|
if ( is_wp_error( $post_id ) ) {
|
||||||
$result['errors'][] = $slug . ': ' . $post_id->get_error_message();
|
$result['errors'][] = $slug . ': ' . $post_id->get_error_message();
|
||||||
@@ -296,6 +402,18 @@ function oribi_sync_run( bool $dry_run = false ): array {
|
|||||||
$result['errors'][] = '[theme] ' . $err;
|
$result['errors'][] = '[theme] ' . $err;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ── Sync posts from repo posts folder ─────────────────────────────────
|
||||||
|
$posts_sync = oribi_sync_run_posts( $api_base, $branch, $provider, $pat, $tree, $dry_run );
|
||||||
|
$result['posts_created'] = $posts_sync['created'];
|
||||||
|
$result['posts_updated'] = $posts_sync['updated'];
|
||||||
|
$result['posts_trashed'] = $posts_sync['trashed'];
|
||||||
|
foreach ( $posts_sync['skipped'] as $sk ) {
|
||||||
|
$result['skipped'][] = '[post] ' . $sk;
|
||||||
|
}
|
||||||
|
foreach ( $posts_sync['errors'] as $err ) {
|
||||||
|
$result['errors'][] = '[post] ' . $err;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Record run ─────────────────────────────────────────────────────────
|
// ── Record run ─────────────────────────────────────────────────────────
|
||||||
if ( ! $dry_run ) {
|
if ( ! $dry_run ) {
|
||||||
oribi_sync_record_run( $result );
|
oribi_sync_record_run( $result );
|
||||||
@@ -387,6 +505,9 @@ function oribi_sync_record_run( array $result ): void {
|
|||||||
'skipped' => $result['skipped'],
|
'skipped' => $result['skipped'],
|
||||||
'errors' => $result['errors'],
|
'errors' => $result['errors'],
|
||||||
'theme_updated' => $result['theme_updated'] ?? [],
|
'theme_updated' => $result['theme_updated'] ?? [],
|
||||||
|
'posts_created' => $result['posts_created'] ?? [],
|
||||||
|
'posts_updated' => $result['posts_updated'] ?? [],
|
||||||
|
'posts_trashed' => $result['posts_trashed'] ?? [],
|
||||||
] );
|
] );
|
||||||
|
|
||||||
// Keep last 20 entries
|
// Keep last 20 entries
|
||||||
@@ -513,6 +634,135 @@ function oribi_sync_apply_theme_files( string $api_base, string $branch, string
|
|||||||
return $out;
|
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 ) {
|
||||||
|
// Clean any corruption from previous syncs
|
||||||
|
$content = oribi_sync_clean_block_content( $content );
|
||||||
|
$checksum = hash( 'sha256', $raw_content );
|
||||||
|
$git_sha = $target_entry['sha'] ?? '';
|
||||||
|
|
||||||
|
$update = oribi_sync_save_post( [
|
||||||
|
'ID' => $post->ID,
|
||||||
|
'post_content' => $content,
|
||||||
|
'post_status' => 'publish',
|
||||||
|
] );
|
||||||
|
|
||||||
|
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).
|
* Fetch theme files from the repo (for preview / apply).
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -20,8 +20,8 @@ require_once ORIBI_SYNC_DIR . 'includes/crypto.php';
|
|||||||
require_once ORIBI_SYNC_DIR . 'includes/api-client.php';
|
require_once ORIBI_SYNC_DIR . 'includes/api-client.php';
|
||||||
require_once ORIBI_SYNC_DIR . 'includes/sync-engine.php';
|
require_once ORIBI_SYNC_DIR . 'includes/sync-engine.php';
|
||||||
require_once ORIBI_SYNC_DIR . 'includes/push-client.php';
|
require_once ORIBI_SYNC_DIR . 'includes/push-client.php';
|
||||||
|
require_once ORIBI_SYNC_DIR . 'includes/post-sync.php';
|
||||||
require_once ORIBI_SYNC_DIR . 'includes/admin.php';
|
require_once ORIBI_SYNC_DIR . 'includes/admin.php';
|
||||||
require_once ORIBI_SYNC_DIR . 'includes/rest.php';
|
|
||||||
require_once ORIBI_SYNC_DIR . 'includes/theme-preview.php';
|
require_once ORIBI_SYNC_DIR . 'includes/theme-preview.php';
|
||||||
|
|
||||||
// ─── Activation / Deactivation ────────────────────────────────────────────────
|
// ─── Activation / Deactivation ────────────────────────────────────────────────
|
||||||
@@ -36,6 +36,8 @@ function oribi_sync_activate() {
|
|||||||
add_option( 'oribi_sync_pat', '', '', 'no' );
|
add_option( 'oribi_sync_pat', '', '', 'no' );
|
||||||
add_option( 'oribi_sync_last_run', '', '', 'no' );
|
add_option( 'oribi_sync_last_run', '', '', 'no' );
|
||||||
add_option( 'oribi_sync_log', [], '', 'no' );
|
add_option( 'oribi_sync_log', [], '', 'no' );
|
||||||
|
add_option( 'oribi_sync_posts_enabled', '', '', 'no' );
|
||||||
|
add_option( 'oribi_sync_posts_folder', 'posts', '', 'no' );
|
||||||
}
|
}
|
||||||
|
|
||||||
function oribi_sync_deactivate() {
|
function oribi_sync_deactivate() {
|
||||||
|
|||||||
@@ -17,10 +17,12 @@ delete_option( 'oribi_sync_log' );
|
|||||||
delete_option( 'oribi_sync_webhook_secret' );
|
delete_option( 'oribi_sync_webhook_secret' );
|
||||||
delete_option( 'oribi_sync_theme_applied' );
|
delete_option( 'oribi_sync_theme_applied' );
|
||||||
delete_option( 'oribi_sync_push_log' );
|
delete_option( 'oribi_sync_push_log' );
|
||||||
|
delete_option( 'oribi_sync_posts_enabled' );
|
||||||
|
delete_option( 'oribi_sync_posts_folder' );
|
||||||
|
|
||||||
// Remove sync metadata from posts
|
// Remove sync metadata from pages and posts
|
||||||
$posts = get_posts( [
|
$posts = get_posts( [
|
||||||
'post_type' => 'page',
|
'post_type' => [ 'page', 'post' ],
|
||||||
'post_status' => 'any',
|
'post_status' => 'any',
|
||||||
'posts_per_page' => -1,
|
'posts_per_page' => -1,
|
||||||
'meta_key' => '_oribi_sync_checksum',
|
'meta_key' => '_oribi_sync_checksum',
|
||||||
@@ -36,5 +38,17 @@ foreach ( $posts as $post_id ) {
|
|||||||
delete_post_meta( $post_id, '_oribi_sync_pr_url' );
|
delete_post_meta( $post_id, '_oribi_sync_pr_url' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Remove origin src meta from media attachments
|
||||||
|
$attachments = get_posts( [
|
||||||
|
'post_type' => 'attachment',
|
||||||
|
'post_status' => 'any',
|
||||||
|
'posts_per_page' => -1,
|
||||||
|
'meta_key' => '_oribi_sync_origin_src',
|
||||||
|
'fields' => 'ids',
|
||||||
|
] );
|
||||||
|
foreach ( $attachments as $att_id ) {
|
||||||
|
delete_post_meta( $att_id, '_oribi_sync_origin_src' );
|
||||||
|
}
|
||||||
|
|
||||||
// Clear any scheduled cron
|
// Clear any scheduled cron
|
||||||
wp_clear_scheduled_hook( 'oribi_sync_cron_run' );
|
wp_clear_scheduled_hook( 'oribi_sync_cron_run' );
|
||||||
|
|||||||
Reference in New Issue
Block a user