✨feature: Add split-direction (push/pull) process
This commit is contained in:
@@ -53,6 +53,8 @@ class Admin {
|
|||||||
add_action( 'admin_menu', array( $this, 'register_menu' ) );
|
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_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', 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_handshake', array( $this, 'handle_handshake' ) );
|
||||||
add_action( 'admin_post_site_sync_reset_state', array( $this, 'handle_reset_state' ) );
|
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' ) );
|
add_action( 'admin_post_site_sync_clear_logs', array( $this, 'handle_clear_logs' ) );
|
||||||
@@ -165,6 +167,23 @@ class Admin {
|
|||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php esc_html_e( 'Directions', 'site-sync' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<fieldset>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="enable_push" value="1" <?php checked( $settings['enable_push'] ); ?>>
|
||||||
|
<?php esc_html_e( 'Include outbound push (send this site\'s changes).', 'site-sync' ); ?>
|
||||||
|
</label>
|
||||||
|
<br>
|
||||||
|
<label>
|
||||||
|
<input type="checkbox" name="enable_pull" value="1" <?php checked( $settings['enable_pull'] ); ?>>
|
||||||
|
<?php esc_html_e( 'Include inbound pull (receive peer changes).', 'site-sync' ); ?>
|
||||||
|
</label>
|
||||||
|
<p class="description"><?php esc_html_e( 'Uncheck to disable that direction during scheduled and combined manual syncs.', 'site-sync' ); ?></p>
|
||||||
|
</fieldset>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
<?php submit_button( __( 'Save Settings', 'site-sync' ) ); ?>
|
<?php submit_button( __( 'Save Settings', 'site-sync' ) ); ?>
|
||||||
@@ -178,6 +197,17 @@ class Admin {
|
|||||||
<input type="hidden" name="action" value="site_sync_manual">
|
<input type="hidden" name="action" value="site_sync_manual">
|
||||||
<?php submit_button( __( 'Sync Now', 'site-sync' ), 'secondary' ); ?>
|
<?php submit_button( __( 'Sync Now', 'site-sync' ), 'secondary' ); ?>
|
||||||
</form>
|
</form>
|
||||||
|
<p><?php esc_html_e( 'Or run one direction at a time:', 'site-sync' ); ?></p>
|
||||||
|
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="margin-right:12px; display:inline-block;">
|
||||||
|
<?php wp_nonce_field( 'site_sync_manual_push', 'site_sync_manual_push_nonce' ); ?>
|
||||||
|
<input type="hidden" name="action" value="site_sync_manual_push">
|
||||||
|
<?php submit_button( __( 'Push Only', 'site-sync' ), 'secondary', 'submit', false ); ?>
|
||||||
|
</form>
|
||||||
|
<form method="post" action="<?php echo esc_url( admin_url( 'admin-post.php' ) ); ?>" style="display:inline-block;">
|
||||||
|
<?php wp_nonce_field( 'site_sync_manual_pull', 'site_sync_manual_pull_nonce' ); ?>
|
||||||
|
<input type="hidden" name="action" value="site_sync_manual_pull">
|
||||||
|
<?php submit_button( __( 'Pull Only', 'site-sync' ), 'secondary', 'submit', false ); ?>
|
||||||
|
</form>
|
||||||
|
|
||||||
<h2><?php esc_html_e( 'Maintenance', 'site-sync' ); ?></h2>
|
<h2><?php esc_html_e( 'Maintenance', 'site-sync' ); ?></h2>
|
||||||
<p><?php esc_html_e( 'Reset checkpoints to force a full resend on the next sync. Use if the destination was emptied or fell out of sync.', 'site-sync' ); ?></p>
|
<p><?php esc_html_e( 'Reset checkpoints to force a full resend on the next sync. Use if the destination was emptied or fell out of sync.', 'site-sync' ); ?></p>
|
||||||
@@ -302,6 +332,8 @@ class Admin {
|
|||||||
'post_meta_keys' => isset( $_POST['post_meta_keys'] ) ? wp_unslash( $_POST['post_meta_keys'] ) : '',
|
'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'] ) : '',
|
'sync_interval' => isset( $_POST['sync_interval'] ) ? wp_unslash( $_POST['sync_interval'] ) : '',
|
||||||
'enabled' => isset( $_POST['enabled'] ),
|
'enabled' => isset( $_POST['enabled'] ),
|
||||||
|
'enable_push' => isset( $_POST['enable_push'] ),
|
||||||
|
'enable_pull' => isset( $_POST['enable_pull'] ),
|
||||||
);
|
);
|
||||||
// phpcs:enable
|
// phpcs:enable
|
||||||
|
|
||||||
@@ -349,6 +381,60 @@ class Admin {
|
|||||||
exit;
|
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.
|
* 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(
|
||||||
|
'<div class="notice notice-info is-dismissible"><p>%s</p></div>',
|
||||||
|
esc_html__( 'Manual push triggered.', 'site-sync' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $_GET['site_sync_status'] ) && 'manual_pull_run' === $_GET['site_sync_status'] ) {
|
||||||
|
printf(
|
||||||
|
'<div class="notice notice-info is-dismissible"><p>%s</p></div>',
|
||||||
|
esc_html__( 'Manual pull triggered.', 'site-sync' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if ( isset( $_GET['site_sync_status'] ) && 'handshake_ok' === $_GET['site_sync_status'] ) {
|
if ( isset( $_GET['site_sync_status'] ) && 'handshake_ok' === $_GET['site_sync_status'] ) {
|
||||||
printf(
|
printf(
|
||||||
'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
|
'<div class="notice notice-success is-dismissible"><p>%s</p></div>',
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ class Settings {
|
|||||||
'peer_site_key' => '',
|
'peer_site_key' => '',
|
||||||
'peer_url' => '',
|
'peer_url' => '',
|
||||||
'enabled' => false,
|
'enabled' => false,
|
||||||
|
'enable_push' => true,
|
||||||
|
'enable_pull' => true,
|
||||||
'sync_interval' => 'site_sync_5min',
|
'sync_interval' => 'site_sync_5min',
|
||||||
'post_meta_keys' => self::DEFAULT_META_KEYS,
|
'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['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['site_uuid'] = isset( $data['site_uuid'] ) ? sanitize_text_field( $data['site_uuid'] ) : $settings['site_uuid'];
|
||||||
$settings['enabled'] = ! empty( $data['enabled'] );
|
$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'];
|
$metaKeys = $data['post_meta_keys'] ?? $settings['post_meta_keys'];
|
||||||
$settings['post_meta_keys'] = $this->normalize_meta_keys( $metaKeys );
|
$settings['post_meta_keys'] = $this->normalize_meta_keys( $metaKeys );
|
||||||
|
|
||||||
|
|||||||
@@ -56,6 +56,8 @@ class Sync_Engine {
|
|||||||
public function hooks(): void {
|
public function hooks(): void {
|
||||||
add_action( 'site_sync/run_cycle', array( $this, 'run_cycle' ), 10, 1 );
|
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_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( 'trashed_post', array( $this, 'mark_post_tombstone' ) );
|
||||||
add_action( 'before_delete_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 );
|
add_action( 'delete_term', array( $this, 'mark_term_tombstone' ), 10, 4 );
|
||||||
@@ -83,13 +85,36 @@ class Sync_Engine {
|
|||||||
$this->run_once( 'manual' );
|
$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.
|
* 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();
|
$settings = $this->settings->ensure_defaults();
|
||||||
|
$allowPush = $doPush;
|
||||||
|
$allowPull = $doPull;
|
||||||
$outCounts = array(
|
$outCounts = array(
|
||||||
'posts' => 0,
|
'posts' => 0,
|
||||||
'terms' => 0,
|
'terms' => 0,
|
||||||
@@ -102,6 +127,30 @@ class Sync_Engine {
|
|||||||
'errors' => 0,
|
'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'] ) ) {
|
if ( empty( $settings['peer_url'] ) ) {
|
||||||
Log::warning( 'Sync skipped; peer URL not configured.', array( 'source' => $source ) );
|
Log::warning( 'Sync skipped; peer URL not configured.', array( 'source' => $source ) );
|
||||||
$this->state->record_run( array( 'error' => 'Peer URL not configured' ) );
|
$this->state->record_run( array( 'error' => 'Peer URL not configured' ) );
|
||||||
@@ -123,54 +172,62 @@ class Sync_Engine {
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$outbound = $this->provide_outbox( true );
|
if ( $allowPush ) {
|
||||||
$outCounts = $outbound['meta']['counts'] ?? $outCounts;
|
$outbound = $this->provide_outbox( true );
|
||||||
$payload = $outbound;
|
$outCounts = $outbound['meta']['counts'] ?? $outCounts;
|
||||||
if ( isset( $payload['meta'] ) ) {
|
$payload = $outbound;
|
||||||
unset( $payload['meta'] );
|
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,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$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(
|
$this->state->record_run(
|
||||||
@@ -186,6 +243,7 @@ class Sync_Engine {
|
|||||||
'Sync cycle completed',
|
'Sync cycle completed',
|
||||||
array(
|
array(
|
||||||
'source' => $source,
|
'source' => $source,
|
||||||
|
'mode' => $mode,
|
||||||
'sent' => $outCounts,
|
'sent' => $outCounts,
|
||||||
'received' => $inCounts,
|
'received' => $inCounts,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class Transport {
|
|||||||
$url,
|
$url,
|
||||||
array(
|
array(
|
||||||
'headers' => $headers,
|
'headers' => $headers,
|
||||||
'timeout' => $this->timeout( 30 ),
|
'timeout' => $this->timeout( 180 ),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -94,7 +94,7 @@ class Transport {
|
|||||||
array(
|
array(
|
||||||
'headers' => $headers,
|
'headers' => $headers,
|
||||||
'body' => $payload,
|
'body' => $payload,
|
||||||
'timeout' => $this->timeout( 60 ),
|
'timeout' => $this->timeout( 180 ),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -115,7 +115,7 @@ class Transport {
|
|||||||
$url,
|
$url,
|
||||||
array(
|
array(
|
||||||
'headers' => $headers,
|
'headers' => $headers,
|
||||||
'timeout' => $this->timeout( 60 ),
|
'timeout' => $this->timeout( 180 ),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user