diff --git a/includes/class-admin.php b/includes/class-admin.php index 73b17a2..cb08536 100644 --- a/includes/class-admin.php +++ b/includes/class-admin.php @@ -53,6 +53,8 @@ class Admin { add_action( 'admin_menu', array( $this, 'register_menu' ) ); add_action( 'admin_post_site_sync_save_settings', array( $this, 'handle_save_settings' ) ); add_action( 'admin_post_site_sync_manual', array( $this, 'handle_manual_sync' ) ); + add_action( 'admin_post_site_sync_manual_push', array( $this, 'handle_manual_push' ) ); + add_action( 'admin_post_site_sync_manual_pull', array( $this, 'handle_manual_pull' ) ); add_action( 'admin_post_site_sync_handshake', array( $this, 'handle_handshake' ) ); add_action( 'admin_post_site_sync_reset_state', array( $this, 'handle_reset_state' ) ); add_action( 'admin_post_site_sync_clear_logs', array( $this, 'handle_clear_logs' ) ); @@ -165,6 +167,23 @@ class Admin { + + + +
+ +
+ +

+
+ + @@ -178,6 +197,17 @@ class Admin { +

+
+ + + +
+
+ + + +

@@ -302,6 +332,8 @@ class Admin { 'post_meta_keys' => isset( $_POST['post_meta_keys'] ) ? wp_unslash( $_POST['post_meta_keys'] ) : '', 'sync_interval' => isset( $_POST['sync_interval'] ) ? wp_unslash( $_POST['sync_interval'] ) : '', 'enabled' => isset( $_POST['enabled'] ), + 'enable_push' => isset( $_POST['enable_push'] ), + 'enable_pull' => isset( $_POST['enable_pull'] ), ); // phpcs:enable @@ -349,6 +381,60 @@ class Admin { exit; } + /** + * Handles the manual push-only action from the admin form submission. + * + * @return void + */ + public function handle_manual_push(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have permission to do this.', 'site-sync' ) ); + } + + check_admin_referer( 'site_sync_manual_push', 'site_sync_manual_push_nonce' ); + + $settings = $this->settings->get_settings(); + do_action( 'site_sync/manual_push', $settings ); + + $redirect = add_query_arg( + array( + 'page' => 'site-sync', + 'site_sync_status' => 'manual_push_run', + ), + admin_url( 'admin.php' ) + ); + + wp_safe_redirect( $redirect ); + exit; + } + + /** + * Handles the manual pull-only action from the admin form submission. + * + * @return void + */ + public function handle_manual_pull(): void { + if ( ! current_user_can( 'manage_options' ) ) { + wp_die( esc_html__( 'You do not have permission to do this.', 'site-sync' ) ); + } + + check_admin_referer( 'site_sync_manual_pull', 'site_sync_manual_pull_nonce' ); + + $settings = $this->settings->get_settings(); + do_action( 'site_sync/manual_pull', $settings ); + + $redirect = add_query_arg( + array( + 'page' => 'site-sync', + 'site_sync_status' => 'manual_pull_run', + ), + admin_url( 'admin.php' ) + ); + + wp_safe_redirect( $redirect ); + exit; + } + /** * Renders admin notices for Site Sync actions based on query parameters. * @@ -373,6 +459,20 @@ class Admin { ); } + if ( isset( $_GET['site_sync_status'] ) && 'manual_push_run' === $_GET['site_sync_status'] ) { + printf( + '

%s

', + esc_html__( 'Manual push triggered.', 'site-sync' ) + ); + } + + if ( isset( $_GET['site_sync_status'] ) && 'manual_pull_run' === $_GET['site_sync_status'] ) { + printf( + '

%s

', + esc_html__( 'Manual pull triggered.', 'site-sync' ) + ); + } + if ( isset( $_GET['site_sync_status'] ) && 'handshake_ok' === $_GET['site_sync_status'] ) { printf( '

%s

', diff --git a/includes/class-settings.php b/includes/class-settings.php index 08520da..c909daa 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -24,6 +24,8 @@ class Settings { 'peer_site_key' => '', 'peer_url' => '', 'enabled' => false, + 'enable_push' => true, + 'enable_pull' => true, 'sync_interval' => 'site_sync_5min', 'post_meta_keys' => self::DEFAULT_META_KEYS, ); @@ -70,6 +72,8 @@ class Settings { $settings['shared_key'] = isset( $data['shared_key'] ) ? sanitize_text_field( $data['shared_key'] ) : $settings['shared_key']; $settings['site_uuid'] = isset( $data['site_uuid'] ) ? sanitize_text_field( $data['site_uuid'] ) : $settings['site_uuid']; $settings['enabled'] = ! empty( $data['enabled'] ); + $settings['enable_push'] = array_key_exists( 'enable_push', $data ) ? ! empty( $data['enable_push'] ) : $settings['enable_push']; + $settings['enable_pull'] = array_key_exists( 'enable_pull', $data ) ? ! empty( $data['enable_pull'] ) : $settings['enable_pull']; $metaKeys = $data['post_meta_keys'] ?? $settings['post_meta_keys']; $settings['post_meta_keys'] = $this->normalize_meta_keys( $metaKeys ); diff --git a/includes/class-sync-engine.php b/includes/class-sync-engine.php index 266d759..1ef267c 100644 --- a/includes/class-sync-engine.php +++ b/includes/class-sync-engine.php @@ -56,6 +56,8 @@ class Sync_Engine { public function hooks(): void { add_action( 'site_sync/run_cycle', array( $this, 'run_cycle' ), 10, 1 ); add_action( 'site_sync/manual_trigger', array( $this, 'run_manual' ), 10, 1 ); + add_action( 'site_sync/manual_push', array( $this, 'run_manual_push' ), 10, 1 ); + add_action( 'site_sync/manual_pull', array( $this, 'run_manual_pull' ), 10, 1 ); add_action( 'trashed_post', array( $this, 'mark_post_tombstone' ) ); add_action( 'before_delete_post', array( $this, 'mark_post_tombstone' ) ); add_action( 'delete_term', array( $this, 'mark_term_tombstone' ), 10, 4 ); @@ -83,13 +85,36 @@ class Sync_Engine { $this->run_once( 'manual' ); } + /** + * Manual push-only trigger from admin. + * + * @param array $settings Settings array. + */ + public function run_manual_push( $settings = array() ): void { // phpcs:ignore + $this->run_once( 'manual_push', true, false, false ); + } + + /** + * Manual pull-only trigger from admin. + * + * @param array $settings Settings array. + */ + public function run_manual_pull( $settings = array() ): void { // phpcs:ignore + $this->run_once( 'manual_pull', false, true, false ); + } + /** * Main sync loop. * - * @param string $source The source of the sync trigger (e.g., 'cron' or 'manual'). + * @param string $source The source of the sync trigger (e.g., 'cron' or 'manual'). + * @param bool $doPush Whether to perform a push operation. + * @param bool $doPull Whether to perform a pull operation. + * @param bool $respectSettings Whether to respect the settings for push/pull enablement. */ - public function run_once( string $source ): void { + public function run_once( string $source, bool $doPush = true, bool $doPull = true, bool $respectSettings = true ): void { $settings = $this->settings->ensure_defaults(); + $allowPush = $doPush; + $allowPull = $doPull; $outCounts = array( 'posts' => 0, 'terms' => 0, @@ -102,6 +127,30 @@ class Sync_Engine { 'errors' => 0, ); + if ( $respectSettings ) { + $allowPush = $allowPush && ! empty( $settings['enable_push'] ); + $allowPull = $allowPull && ! empty( $settings['enable_pull'] ); + } + + $mode = array( + 'push' => $allowPush, + 'pull' => $allowPull, + ); + + if ( ! $allowPush && ! $allowPull ) { + Log::info( 'Sync skipped; both directions disabled.', array( 'source' => $source ) ); + $this->state->record_run( + array( + 'error' => 'Sync directions disabled', + 'counts' => array( + 'sent' => $outCounts, + 'received' => $inCounts, + ), + ) + ); + return; + } + if ( empty( $settings['peer_url'] ) ) { Log::warning( 'Sync skipped; peer URL not configured.', array( 'source' => $source ) ); $this->state->record_run( array( 'error' => 'Peer URL not configured' ) ); @@ -123,54 +172,62 @@ class Sync_Engine { ) ); - $outbound = $this->provide_outbox( true ); - $outCounts = $outbound['meta']['counts'] ?? $outCounts; - $payload = $outbound; - if ( isset( $payload['meta'] ) ) { - unset( $payload['meta'] ); - } - - $push = $this->transport->post( '/wp-json/site-sync/v1/inbox', $payload ); - if ( is_wp_error( $push ) ) { - $this->report_error( 'Push to peer failed', $push, $source ); - $this->state->record_run( - array( - 'error' => $push->get_error_message(), - 'counts' => array( - 'sent' => $outCounts, - 'received' => $inCounts, - ), - ) - ); - return; - } - - $this->commit_outbox( $outbound['cursor'], $outbound['meta']['counts'] ?? array() ); - - $pull = $this->transport->get( '/wp-json/site-sync/v1/outbox' ); - if ( is_wp_error( $pull ) ) { - $this->report_error( 'Pull from peer failed', $pull, $source ); - $this->state->record_run( - array( - 'error' => $pull->get_error_message(), - 'counts' => array( - 'sent' => $outCounts, - 'received' => $inCounts, - ), - ) - ); - return; - } - - if ( is_array( $pull ) && ! empty( $pull['items'] ) ) { - $result = $this->handle_inbox( $pull ); - if ( is_array( $result ) ) { - $inCounts = array( - 'applied' => (int) ( $result['applied'] ?? 0 ), - 'skipped' => (int) ( $result['skipped'] ?? 0 ), - 'errors' => is_array( $result['errors'] ?? null ) ? count( $result['errors'] ) : 0, - ); + if ( $allowPush ) { + $outbound = $this->provide_outbox( true ); + $outCounts = $outbound['meta']['counts'] ?? $outCounts; + $payload = $outbound; + if ( isset( $payload['meta'] ) ) { + unset( $payload['meta'] ); } + + $push = $this->transport->post( '/wp-json/site-sync/v1/inbox', $payload ); + if ( is_wp_error( $push ) ) { + $this->report_error( 'Push to peer failed', $push, $source ); + $this->state->record_run( + array( + 'error' => $push->get_error_message(), + 'counts' => array( + 'sent' => $outCounts, + 'received' => $inCounts, + ), + ) + ); + return; + } + + $this->commit_outbox( $outbound['cursor'], $outbound['meta']['counts'] ?? array() ); + } else { + Log::info( 'Push skipped for this run.', array( 'source' => $source ) ); + } + + if ( $allowPull ) { + $pull = $this->transport->get( '/wp-json/site-sync/v1/outbox' ); + if ( is_wp_error( $pull ) ) { + $this->report_error( 'Pull from peer failed', $pull, $source ); + $this->state->record_run( + array( + 'error' => $pull->get_error_message(), + 'counts' => array( + 'sent' => $outCounts, + 'received' => $inCounts, + ), + ) + ); + return; + } + + if ( is_array( $pull ) && ! empty( $pull['items'] ) ) { + $result = $this->handle_inbox( $pull ); + if ( is_array( $result ) ) { + $inCounts = array( + 'applied' => (int) ( $result['applied'] ?? 0 ), + 'skipped' => (int) ( $result['skipped'] ?? 0 ), + 'errors' => is_array( $result['errors'] ?? null ) ? count( $result['errors'] ) : 0, + ); + } + } + } else { + Log::info( 'Pull skipped for this run.', array( 'source' => $source ) ); } $this->state->record_run( @@ -186,6 +243,7 @@ class Sync_Engine { 'Sync cycle completed', array( 'source' => $source, + 'mode' => $mode, 'sent' => $outCounts, 'received' => $inCounts, ) diff --git a/includes/class-transport.php b/includes/class-transport.php index 388b264..144698d 100644 --- a/includes/class-transport.php +++ b/includes/class-transport.php @@ -41,7 +41,7 @@ class Transport { $url, array( 'headers' => $headers, - 'timeout' => $this->timeout( 30 ), + 'timeout' => $this->timeout( 180 ), ) ); @@ -94,7 +94,7 @@ class Transport { array( 'headers' => $headers, 'body' => $payload, - 'timeout' => $this->timeout( 60 ), + 'timeout' => $this->timeout( 180 ), ) ); @@ -115,7 +115,7 @@ class Transport { $url, array( 'headers' => $headers, - 'timeout' => $this->timeout( 60 ), + 'timeout' => $this->timeout( 180 ), ) );