render_notices();
$this->cron_warning();
}
/**
* Close the wrap and print the tour.
*/
private function footer() {
require HOOKBIRD_DIR . 'admin/views/tour.php';
echo '
';
}
/**
* Warn when deliveries are piling up because WP-Cron is not running.
*/
private function cron_warning() {
$next = Deliveries::next_due_time();
if ( ! $next || $next > time() - 5 * MINUTE_IN_SECONDS ) {
return;
}
echo '
';
if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
esc_html_e( 'Some deliveries are overdue. WP-Cron is disabled on this site, so make sure a system cron job calls wp-cron.php every minute, or run "wp hookbird run" from cron.', 'hookbird' );
} else {
esc_html_e( 'Some deliveries are overdue. WP-Cron runs when your site gets visits, so on quiet sites consider a real cron job. You can also process the queue now from Settings.', 'hookbird' );
}
echo '
';
}
// Pages.
/**
* Endpoints list.
*/
public function page_endpoints() {
$this->guard();
$endpoints = Endpoints::all();
$stats = Deliveries::stats();
$presets = Formatter::presets();
$checklist = Onboarding::checklist();
$this->header( 'hookbird', __( 'Endpoints', 'hookbird' ), __( 'Where your site events go, and how each destination is doing.', 'hookbird' ) );
require HOOKBIRD_DIR . 'admin/views/endpoints.php';
$this->footer();
}
/**
* Add or edit an endpoint.
*/
public function page_edit() {
$this->guard();
$id = absint( self::query_var( 'id' ) );
$endpoint = $id ? Endpoints::get( $id ) : null;
if ( $id && ! $endpoint ) {
wp_die( esc_html__( 'Endpoint not found.', 'hookbird' ) );
}
$presets = Formatter::presets();
$preset = $endpoint ? self::preset_of( $endpoint ) : sanitize_key( self::query_var( 'preset' ) );
if ( ! isset( $presets[ $preset ] ) ) {
$preset = 'slack';
}
// Values from a failed save take priority so the form is not lost.
$draft = get_transient( 'hookbird_draft_' . get_current_user_id() );
if ( is_array( $draft ) && (int) $draft['id'] === $id ) {
delete_transient( 'hookbird_draft_' . get_current_user_id() );
$values = $draft['values'];
$preset = isset( $presets[ $values['preset'] ] ) ? $values['preset'] : $preset;
} elseif ( $endpoint ) {
$values = array(
'name' => $endpoint['name'],
'url' => $endpoint['url'],
'events' => $endpoint['events'],
'headers' => $endpoint['headers'],
'conditions' => $endpoint['conditions'],
'chat_id' => isset( $endpoint['options']['chat_id'] ) ? $endpoint['options']['chat_id'] : '',
'active' => $endpoint['active'],
);
} else {
$values = array(
'name' => '',
'url' => '',
'events' => array(),
'headers' => array(),
'conditions' => array(
'match' => 'all',
'rules' => array(),
),
'chat_id' => '',
'active' => true,
);
}
$grouped = Registry::grouped();
$known = array_keys( Registry::events() );
$custom = array_values(
array_filter(
(array) $values['events'],
static function ( $key ) use ( $known ) {
return '*' !== $key && ! in_array( $key, $known, true );
}
)
);
$all = in_array( '*', (array) $values['events'], true );
$operators = Conditions::operators();
$paths = Registry::all_paths();
$woo = class_exists( 'WooCommerce' );
$stats = $endpoint ? Deliveries::stats() : array();
$this->header( 'hookbird-edit', $endpoint ? $endpoint['name'] : __( 'Add endpoint', 'hookbird' ), $endpoint ? __( 'Change where events go, which events are sent and when.', 'hookbird' ) : __( 'Choose a destination, pick the events it should get, then send a test.', 'hookbird' ) );
require HOOKBIRD_DIR . 'admin/views/edit.php';
$this->footer();
}
/**
* Delivery log and delivery detail.
*/
public function page_log() {
$this->guard();
$delivery_id = absint( self::query_var( 'delivery' ) );
$endpoints = array();
foreach ( Endpoints::all() as $endpoint ) {
$endpoints[ $endpoint['id'] ] = $endpoint;
}
if ( $delivery_id ) {
$delivery = Deliveries::get( $delivery_id );
if ( ! $delivery ) {
wp_die( esc_html__( 'Delivery not found.', 'hookbird' ) );
}
$endpoint = isset( $endpoints[ $delivery['endpoint_id'] ] ) ? $endpoints[ $delivery['endpoint_id'] ] : null;
$this->header( 'hookbird-log', __( 'Delivery details', 'hookbird' ), __( 'The exact payload that was sent and what the destination answered.', 'hookbird' ) );
require HOOKBIRD_DIR . 'admin/views/delivery.php';
$this->footer();
return;
}
$filters = array(
'endpoint_id' => absint( self::query_var( 'endpoint_id' ) ),
'event' => Endpoints::sanitize_event_key( self::query_var( 'event' ) ),
'status' => in_array( self::query_var( 'status' ), Deliveries::STATUSES, true ) ? self::query_var( 'status' ) : '',
'search' => self::query_var( 's' ),
);
$page = max( 1, absint( self::query_var( 'paged' ) ) );
$result = Deliveries::query(
$filters + array(
'page' => $page,
'per_page' => 25,
)
);
$events = Registry::events();
$this->header( 'hookbird-log', __( 'Delivery log', 'hookbird' ), __( 'Every event Hookbird sent, with the response. Failed deliveries can be resent with one click.', 'hookbird' ) );
require HOOKBIRD_DIR . 'admin/views/log.php';
$this->footer();
}
/**
* Settings and tools.
*/
public function page_settings() {
$this->guard();
$settings = Settings::all();
$counts = array(
'pending' => Deliveries::count( 'pending' ),
'failed' => Deliveries::count( 'failed' ),
'success' => Deliveries::count( 'success' ),
);
$next_run = wp_next_scheduled( Installer::CRON_PROCESS );
$sweep = wp_next_scheduled( Installer::CRON_SWEEP );
$this->header( 'hookbird-settings', __( 'Settings', 'hookbird' ), __( 'Retries, timeouts, alerts and tools for moving your setup between sites.', 'hookbird' ) );
require HOOKBIRD_DIR . 'admin/views/settings.php';
$this->footer();
}
/**
* Help & Setup: guides, troubleshooting and system status.
*/
public function page_help() {
$this->guard();
$guides = Onboarding::guides();
$faq = Onboarding::troubleshooting();
$checks = Onboarding::status_checks();
$presets = Formatter::presets();
$links = Feedback::links();
$support = Feedback::support_email();
$report = Feedback::system_report();
$contact = $this->support_state();
$this->header( 'hookbird-help', __( 'Help & Setup', 'hookbird' ), __( 'Step-by-step guides for each destination, fixes for common problems, support and a health check of this site.', 'hookbird' ) );
require HOOKBIRD_DIR . 'admin/views/help.php';
$this->footer();
}
/**
* What the support form shows: the last send result and the values to fill in.
*
* @return array status (''|sent|error), message, name, email, text, report.
*/
private function support_state() {
$key = 'hookbird_support_state_' . get_current_user_id();
$state = get_transient( $key );
delete_transient( $key );
$user = wp_get_current_user();
$defaults = array(
'status' => '',
'message' => '',
'name' => $user->display_name,
'email' => $user->user_email,
'text' => '',
'report' => false,
);
$state = is_array( $state ) ? array_merge( $defaults, $state ) : $defaults;
if ( 'sent' === $state['status'] ) {
$state['text'] = '';
$state['report'] = false;
}
return $state;
}
// Handlers.
/**
* Send the Help & Setup support form to the developer by email. Only runs when the admin clicks Send.
*/
public function handle_support() {
$this->guard();
check_admin_referer( 'hookbird_support' );
$to = Feedback::support_email();
$name = isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '';
$name = mb_substr( trim( str_replace( array( '<', '>', '"' ), '', $name ) ), 0, 100 );
$email = isset( $_POST['email'] ) ? sanitize_email( wp_unslash( $_POST['email'] ) ) : '';
$text = isset( $_POST['text'] ) ? sanitize_textarea_field( wp_unslash( $_POST['text'] ) ) : '';
$text = mb_substr( trim( str_replace( array( "\r\n", "\r" ), "\n", $text ) ), 0, 5000 );
$report = ! empty( $_POST['report'] );
$state = array(
'name' => $name,
'email' => $email,
'text' => $text,
'report' => $report,
);
$user = get_current_user_id();
$error = '';
if ( '' === $to ) {
$error = __( 'Email support is not available in this version. Please ask on the support forum.', 'hookbird' );
} elseif ( '' === $name ) {
$error = __( 'Please enter your name.', 'hookbird' );
} elseif ( ! is_email( $email ) ) {
$error = __( 'Please enter a valid email address, so the reply can reach you.', 'hookbird' );
} elseif ( mb_strlen( $text ) < 10 ) {
$error = __( 'Please describe your question in a few words.', 'hookbird' );
} elseif ( get_transient( 'hookbird_support_sent_' . $user ) ) {
$error = __( 'Your last message was sent less than a minute ago. Please wait a moment before sending another.', 'hookbird' );
}
if ( '' === $error ) {
global $wp_version;
$lines = array(
'Name: ' . $name,
'Email: ' . $email,
'Site: ' . home_url( '/' ),
'Hookbird ' . HOOKBIRD_VERSION . ', WordPress ' . $wp_version . ', PHP ' . PHP_VERSION,
'',
$text,
);
if ( $report ) {
$lines[] = '';
$lines[] = '--- System report ---';
$lines[] = Feedback::system_report();
}
$first = strtok( $text, "\n" );
$subject = '[Hookbird support] ' . mb_substr( trim( false === $first ? $text : $first ), 0, 70 );
$sent = wp_mail( $to, $subject, implode( "\n", $lines ), array( 'Reply-To: ' . $name . ' <' . $email . '>' ) );
if ( $sent ) {
set_transient( 'hookbird_support_sent_' . $user, 1, MINUTE_IN_SECONDS );
$state['status'] = 'sent';
/* translators: %s: the email address the reply will go to. */
$state['message'] = sprintf( __( 'Message sent. You will get a reply at %s.', 'hookbird' ), $email );
} else {
$error = __( 'Your site could not send the email. Check that it can send mail (an SMTP plugin usually fixes this), or ask on the support forum.', 'hookbird' );
}
}
if ( '' !== $error ) {
$state['status'] = 'error';
$state['message'] = $error;
}
set_transient( 'hookbird_support_state_' . $user, $state, 10 * MINUTE_IN_SECONDS );
wp_safe_redirect( admin_url( 'admin.php?page=hookbird-help' ) . '#support' );
exit;
}
/**
* Save an endpoint from the editor form.
*/
public function handle_save_endpoint() {
$this->guard();
check_admin_referer( 'hookbird_save_endpoint' );
$id = isset( $_POST['id'] ) ? absint( $_POST['id'] ) : 0;
$presets = Formatter::presets();
$preset = isset( $_POST['preset'] ) ? sanitize_key( wp_unslash( $_POST['preset'] ) ) : 'custom';
$preset = isset( $presets[ $preset ] ) ? $preset : 'custom';
$events = isset( $_POST['events'] ) ? array_map( 'sanitize_text_field', wp_unslash( (array) $_POST['events'] ) ) : array();
if ( ! empty( $_POST['all_events'] ) ) {
$events = array( '*' );
}
if ( ! empty( $_POST['custom_events'] ) ) {
$events = array_merge( $events, explode( ',', sanitize_text_field( wp_unslash( $_POST['custom_events'] ) ) ) );
}
$conditions = array(
'match' => isset( $_POST['conditions_match'] ) && 'any' === $_POST['conditions_match'] ? 'any' : 'all',
'rules' => array(),
);
if ( isset( $_POST['conditions'] ) && is_array( $_POST['conditions'] ) ) {
// Each rule is sanitized field by field in Endpoints::sanitize_conditions().
$raw_rules = map_deep( wp_unslash( $_POST['conditions'] ), 'sanitize_text_field' ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitized by map_deep.
foreach ( (array) $raw_rules as $rule ) {
if ( is_array( $rule ) && isset( $rule['field'] ) && '' !== trim( (string) $rule['field'] ) ) {
$conditions['rules'][] = $rule;
}
}
}
$values = array(
'name' => isset( $_POST['name'] ) ? sanitize_text_field( wp_unslash( $_POST['name'] ) ) : '',
'url' => isset( $_POST['url'] ) ? esc_url_raw( wp_unslash( $_POST['url'] ) ) : '',
'events' => $events,
'format' => $presets[ $preset ]['format'],
'headers' => isset( $_POST['headers'] ) ? sanitize_textarea_field( wp_unslash( $_POST['headers'] ) ) : '',
'conditions' => $conditions,
'options' => array(
'preset' => $preset,
'chat_id' => isset( $_POST['chat_id'] ) ? sanitize_text_field( wp_unslash( $_POST['chat_id'] ) ) : '',
),
'active' => ! empty( $_POST['active'] ),
);
$result = $id ? Endpoints::update( $id, $values ) : Endpoints::create( $values );
if ( is_wp_error( $result ) ) {
set_transient(
'hookbird_draft_' . get_current_user_id(),
array(
'id' => $id,
'values' => array(
'preset' => $preset,
'name' => $values['name'],
'url' => $values['url'],
'events' => $events,
'headers' => Endpoints::sanitize_headers( $values['headers'] ),
'conditions' => Endpoints::sanitize_conditions( $conditions ),
'chat_id' => $values['options']['chat_id'],
'active' => $values['active'],
),
),
5 * MINUTE_IN_SECONDS
);
$this->notice( $result->get_error_message(), 'error' );
$this->go( 'hookbird-edit', $id ? array( 'id' => $id ) : array( 'preset' => $preset ) );
}
$new_id = $id ? $id : (int) $result;
$this->notice( $id ? __( 'Endpoint saved.', 'hookbird' ) : __( 'Endpoint created. Send a test event to check that it works.', 'hookbird' ) );
$this->go( 'hookbird-edit', array( 'id' => $new_id ) );
}
/**
* Delete, enable, disable, resume or rotate.
*/
public function handle_endpoint_action() {
$this->guard();
check_admin_referer( 'hookbird_endpoint_action' );
$id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
$do = isset( $_GET['do'] ) ? sanitize_key( wp_unslash( $_GET['do'] ) ) : '';
$back = isset( $_GET['back'] ) && 'edit' === $_GET['back'] ? 'hookbird-edit' : 'hookbird';
if ( ! Endpoints::get( $id ) ) {
$this->notice( __( 'Endpoint not found.', 'hookbird' ), 'error' );
$this->go( 'hookbird' );
}
switch ( $do ) {
case 'delete':
Endpoints::delete( $id );
$this->notice( __( 'Endpoint deleted.', 'hookbird' ) );
$this->go( 'hookbird' );
break;
case 'enable':
Endpoints::set_active( $id, true );
Dispatcher::instance()->schedule_run( time() );
$this->notice( __( 'Endpoint enabled. Queued events will be sent shortly.', 'hookbird' ) );
break;
case 'disable':
Endpoints::set_active( $id, false );
$this->notice( __( 'Endpoint disabled. New events will not be queued for it.', 'hookbird' ) );
break;
case 'resume':
Endpoints::resume( $id );
Dispatcher::instance()->schedule_run( time() );
$this->notice( __( 'Endpoint resumed. Queued events will be sent shortly.', 'hookbird' ) );
break;
case 'rotate':
Endpoints::rotate_secret( $id );
$this->notice( __( 'New signing secret generated. Update it in your receiver.', 'hookbird' ), 'warning' );
break;
}
$this->go( $back, 'hookbird-edit' === $back ? array( 'id' => $id ) : array() );
}
/**
* Send a test event.
*/
public function handle_test_endpoint() {
$this->guard();
check_admin_referer( 'hookbird_test_endpoint' );
$id = isset( $_REQUEST['id'] ) ? absint( $_REQUEST['id'] ) : 0;
$event = isset( $_REQUEST['event'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['event'] ) ) : '';
$back = isset( $_REQUEST['back'] ) && 'list' === $_REQUEST['back'] ? 'hookbird' : 'hookbird-edit';
$result = Dispatcher::instance()->send_test( $id, $event );
if ( is_wp_error( $result ) ) {
$this->notice( $result->get_error_message(), 'error' );
} elseif ( $result['success'] ) {
/* translators: 1: event key, 2: HTTP status code, 3: duration in milliseconds. */
$this->notice( sprintf( __( 'Test "%1$s" delivered: HTTP %2$d in %3$d ms.', 'hookbird' ), $result['event'], $result['code'], $result['duration_ms'] ) );
} else {
/* translators: %s: error message. */
$this->notice( sprintf( __( 'Test failed: %s See the delivery log for the full response.', 'hookbird' ), $result['error'] ), 'error' );
}
$this->go( $back, 'hookbird-edit' === $back ? array( 'id' => $id ) : array() );
}
/**
* Resend a delivery.
*/
public function handle_resend() {
$this->guard();
check_admin_referer( 'hookbird_resend' );
$id = isset( $_GET['id'] ) ? absint( $_GET['id'] ) : 0;
$result = Dispatcher::instance()->resend( $id );
if ( is_wp_error( $result ) ) {
$this->notice( $result->get_error_message(), 'error' );
$this->go( 'hookbird-log' );
}
if ( $result['success'] ) {
/* translators: %d: HTTP status code. */
$this->notice( sprintf( __( 'Delivered: HTTP %d.', 'hookbird' ), $result['code'] ) );
} else {
/* translators: %s: error message. */
$this->notice( sprintf( __( 'Delivery failed again: %s', 'hookbird' ), $result['error'] ), 'error' );
}
$this->go( 'hookbird-log', array( 'delivery' => $result['delivery_id'] ) );
}
/**
* Queue tools.
*/
public function handle_tools() {
$this->guard();
check_admin_referer( 'hookbird_tools' );
$do = isset( $_POST['do'] ) ? sanitize_key( wp_unslash( $_POST['do'] ) ) : '';
if ( 'run' === $do ) {
Deliveries::recover_stuck();
if ( Dispatcher::is_running() ) {
$this->notice( __( 'The queue is already being sent in the background. Check the delivery log again in a minute.', 'hookbird' ), 'info' );
$this->go( 'hookbird-settings' );
}
$sent = Dispatcher::instance()->process_queue();
/* translators: %d: number of deliveries. */
$this->notice( sprintf( _n( 'Processed %d delivery.', 'Processed %d deliveries.', $sent, 'hookbird' ), $sent ) );
} elseif ( 'purge' === $do ) {
$deleted = Deliveries::purge( 0 );
/* translators: %d: number of log entries. */
$this->notice( sprintf( _n( 'Deleted %d log entry.', 'Deleted %d log entries.', $deleted, 'hookbird' ), $deleted ) );
}
$this->go( 'hookbird-settings' );
}
/**
* Download an export file.
*/
public function handle_export() {
$this->guard();
check_admin_referer( 'hookbird_export' );
$secrets = ! empty( $_POST['include_secrets'] );
$filename = 'hookbird-' . sanitize_title( wp_parse_url( home_url(), PHP_URL_HOST ) ) . '-' . gmdate( 'Y-m-d' ) . '.json';
nocache_headers();
header( 'Content-Type: application/json; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $filename . '"' );
echo wp_json_encode( Transfer::export( $secrets ), JSON_PRETTY_PRINT );
exit;
}
/**
* Import an export file.
*/
public function handle_import() {
$this->guard();
check_admin_referer( 'hookbird_import' );
if ( empty( $_FILES['import_file']['tmp_name'] ) || ! is_uploaded_file( $_FILES['import_file']['tmp_name'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput -- Temporary upload path, validated by is_uploaded_file().
$this->notice( __( 'Please choose an export file.', 'hookbird' ), 'error' );
$this->go( 'hookbird-settings' );
}
$name = isset( $_FILES['import_file']['name'] ) ? sanitize_file_name( wp_unslash( $_FILES['import_file']['name'] ) ) : '';
if ( 'json' !== strtolower( pathinfo( $name, PATHINFO_EXTENSION ) ) ) {
$this->notice( __( 'Please choose a .json export file.', 'hookbird' ), 'error' );
$this->go( 'hookbird-settings' );
}
if ( ! empty( $_FILES['import_file']['size'] ) && (int) $_FILES['import_file']['size'] > MB_IN_BYTES ) {
$this->notice( __( 'The file is too large.', 'hookbird' ), 'error' );
$this->go( 'hookbird-settings' );
}
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents, WordPress.Security.ValidatedSanitizedInput -- Reading the uploaded temporary file; contents are validated and sanitized by Transfer::import().
$data = json_decode( (string) file_get_contents( $_FILES['import_file']['tmp_name'] ), true );
$result = Transfer::import( is_array( $data ) ? $data : array(), ! empty( $_POST['import_settings'] ) );
if ( is_wp_error( $result ) ) {
$this->notice( $result->get_error_message(), 'error' );
} else {
/* translators: %d: number of endpoints. */
$this->notice( sprintf( _n( 'Imported %d endpoint.', 'Imported %d endpoints.', $result['created'], 'hookbird' ), $result['created'] ) );
foreach ( $result['errors'] as $error ) {
$this->notice( $error, 'warning' );
}
}
$this->go( 'hookbird-settings' );
}
}