- Implemented `GET /repo-folders` to list available sub-folders in the configured repository. - Added `POST /push` to push a single page to the repository. - Introduced `POST /push-all` to push all synced pages back to the repository. - Enhanced `oribi_sync_rest_sync` to push local changes after pulling, except during dry runs. - Created `oribi_sync_push_page` and `oribi_sync_push_all` functions to handle page pushing logic. - Updated post meta on successful pushes to track last push time and SHA. - Added logging for push actions and errors. Enhance sync engine to support theme file synchronization - Added functionality to auto-apply changed theme files from the repository's theme directory. - Created `oribi_sync_apply_theme_files` to handle theme file updates during sync. - Ensured the existence of a minimal theme structure in the `ots-theme` directory. Refactor uninstall process to clean up additional post meta - Updated `uninstall.php` to remove new post meta related to push operations. - Ensured comprehensive cleanup of options and metadata upon plugin uninstallation. Introduce push client for handling page pushes to Gitea - Created `push-client.php` to encapsulate logic for pushing pages back to the Git repository. - Implemented conflict resolution by creating branches and opening pull requests when necessary. - Added helper functions for authenticated API requests to Gitea.
213 lines
7.9 KiB
PHP
213 lines
7.9 KiB
PHP
<?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' );
|
|
},
|
|
] );
|
|
|
|
// ── List Pages sub-folders from the repo ───────────────────────────
|
|
register_rest_route( 'oribi-sync/v1', '/repo-folders', [
|
|
'methods' => 'GET',
|
|
'callback' => 'oribi_sync_rest_repo_folders',
|
|
'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.
|
|
*/
|
|
function oribi_sync_rest_push_all( WP_REST_Request $request ): WP_REST_Response {
|
|
$result = oribi_sync_push_all();
|
|
|
|
return new WP_REST_Response( $result, $result['ok'] ? 200 : 500 );
|
|
}
|
|
|
|
/**
|
|
* REST: List available sub-folders under Pages/ in the configured repository.
|
|
*
|
|
* Returns a JSON object: { folders: ["folder-a", "folder-b", …] }
|
|
*/
|
|
function oribi_sync_rest_repo_folders(): WP_REST_Response {
|
|
$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 ) ) {
|
|
return new WP_REST_Response( [ 'error' => 'Repository URL or PAT is not configured.' ], 400 );
|
|
}
|
|
|
|
$parsed = oribi_sync_parse_repo_url( $repo_url );
|
|
if ( is_wp_error( $parsed ) ) {
|
|
return new WP_REST_Response( [ 'error' => $parsed->get_error_message() ], 400 );
|
|
}
|
|
|
|
$provider = oribi_sync_get_provider();
|
|
$api_base = oribi_sync_api_base( $provider, $parsed );
|
|
|
|
$tree = oribi_sync_fetch_tree( $api_base, $branch, $provider, $pat );
|
|
if ( is_wp_error( $tree ) ) {
|
|
return new WP_REST_Response( [ 'error' => 'Tree fetch failed: ' . $tree->get_error_message() ], 500 );
|
|
}
|
|
|
|
// Find direct sub-directories of Pages/
|
|
$folders = [];
|
|
$prefix = 'Pages/';
|
|
foreach ( $tree as $entry ) {
|
|
if ( $entry['type'] !== 'tree' ) continue;
|
|
if ( strpos( $entry['path'], $prefix ) !== 0 ) continue;
|
|
$relative = substr( $entry['path'], strlen( $prefix ) );
|
|
// Only direct children (no nested slash)
|
|
if ( strpos( $relative, '/' ) !== false ) continue;
|
|
if ( $relative === '' ) continue;
|
|
$folders[] = $relative;
|
|
}
|
|
|
|
sort( $folders );
|
|
|
|
return new WP_REST_Response( [ 'folders' => $folders ], 200 );
|
|
}
|