' . $pending . '' : '';
add_submenu_page( self::PARENT, ethebo__( 'Bookings' ), ethebo__( 'Bookings' ) . $bubble, ETHEBO_CAP, 'ethebo-bookings', array( __CLASS__, 'bookings_page' ), 1 );
add_submenu_page( self::PARENT, ethebo__( 'Payments' ), ethebo__( 'Payments' ), 'manage_options', 'ethebo-payments', array( 'ETHEBO_Admin_Payments', 'page' ) );
add_submenu_page( self::PARENT, ethebo__( 'Settings' ), ethebo__( 'Settings' ), 'manage_options', 'ethebo-settings', array( 'ETHEBO_Admin_Settings', 'page' ) );
if ( ! defined( 'ETHEBO_PRO_VERSION' ) ) { // A short page about the separate Pro plugin (Pro has its own License page).
add_submenu_page( self::PARENT, ethebo__( 'Ethereal Booking Pro' ), ethebo__( 'Ethereal Booking Pro' ), 'manage_options', 'ethebo-pro', array( 'ETHEBO_Upgrade', 'page' ) );
}
}
/**
* Is this one of our admin screens? With Pro the top "Accommodation" menu opens the Calendar as
* admin.php?page=ethebo-calendar (without post_type), so the page slug is checked too.
*/
public static function is_our_screen() {
$screen = get_current_screen();
if ( $screen && ETHEBO_Post_Type::PT === $screen->post_type ) {
return true;
}
$page = isset( $_GET['page'] ) ? sanitize_key( $_GET['page'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only screen filter, nothing is saved.
return 0 === strpos( $page, 'ethebo-' );
}
public static function assets( $hook ) {
if ( ! self::is_our_screen() ) {
return;
}
$screen = get_current_screen();
if ( $screen && 'post' === $screen->base ) {
wp_enqueue_media(); // Media library for the photo gallery.
}
wp_enqueue_style( 'ethebo-admin', ETHEBO_URL . 'assets/css/admin.css', array(), ETHEBO_VERSION );
wp_enqueue_script( 'ethebo-admin', ETHEBO_URL . 'assets/js/admin.js', array( 'jquery', 'jquery-ui-sortable' ), ETHEBO_VERSION, true );
wp_localize_script( 'ethebo-admin', 'ETHEBO_ADMIN', array(
'ajax' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'ethebo_admin' ),
'syncing' => ethebo__( 'Syncing…' ),
'testing' => ethebo__( 'Checking the link…' ),
'checking' => ethebo__( 'Checking export links…' ),
'copied' => ethebo__( 'Copied ✓' ),
'sending' => ethebo__( 'Sending…' ),
'error' => ethebo__( 'Connection error. Please try again.' ),
) );
}
private static function redirect( $args ) {
// The error text stays on the server (a link can't inject its own text into the admin).
if ( isset( $args['ethebo_err'] ) ) {
set_transient( 'ethebo_err_' . get_current_user_id(), (string) $args['ethebo_err'], MINUTE_IN_SECONDS );
$args['ethebo_err'] = 1;
}
$back = wp_get_referer() ? wp_get_referer() : admin_url( self::PARENT . '&page=ethebo-bookings' );
wp_safe_redirect( add_query_arg( $args, remove_query_arg( array( 'ethebo_msg', 'ethebo_err' ), $back ) ) );
exit;
}
public static function notices() {
if ( ! self::is_our_screen() ) {
return;
}
$messages = array(
'synced' => array( 'success', ethebo__( 'Calendars synced.' ) ),
'sync_err' => array( 'warning', ethebo__( 'Sync finished with errors. Check the iCal links on the property.' ) ),
'confirmed' => array( 'success', ethebo__( 'Booking confirmed.' ) ),
'cancelled' => array( 'success', ethebo__( 'Booking cancelled.' ) ),
'pending' => array( 'success', ethebo__( 'Booking set back to pending.' ) ),
'deleted' => array( 'success', ethebo__( 'Booking deleted.' ) ),
'added' => array( 'success', ethebo__( 'Booking added.' ) ),
'paid' => array( 'success', ethebo__( 'Booking marked as paid.' ) ),
);
$msg = isset( $_GET['ethebo_msg'] ) ? sanitize_key( $_GET['ethebo_msg'] ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only screen filter, nothing is saved.
if ( isset( $messages[ $msg ] ) ) {
list( $type, $text ) = $messages[ $msg ];
printf( '
', esc_attr( $type ), esc_html( $text ) );
}
$err = isset( $_GET['ethebo_err'] ) ? get_transient( 'ethebo_err_' . get_current_user_id() ) : false; // phpcs:ignore WordPress.Security.NonceVerification
if ( $err ) {
delete_transient( 'ethebo_err_' . get_current_user_id() );
printf( '', esc_html( $err ) );
}
}
/* ---------------------------------------------------------------------
* Bookings page
* ------------------------------------------------------------------ */
public static function bookings_page() {
// phpcs:disable WordPress.Security.NonceVerification.Recommended -- Read-only list filters, nothing is saved.
$property = isset( $_GET['property'] ) ? absint( $_GET['property'] ) : 0;
$status = isset( $_GET['status'] ) ? sanitize_key( $_GET['status'] ) : '';
$show = isset( $_GET['show'] ) ? sanitize_key( $_GET['show'] ) : 'upcoming';
$paged = isset( $_GET['paged'] ) ? max( 1, absint( $_GET['paged'] ) ) : 1;
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$per_page = 30;
$args = array(
'property_id' => $property,
'status' => array_key_exists( $status, ethebo_statuses() ) ? $status : '',
'upcoming' => 'upcoming' === $show,
'limit' => $per_page,
'offset' => ( $paged - 1 ) * $per_page,
'order' => 'upcoming' === $show ? 'ASC' : 'DESC',
);
$rows = ETHEBO_Bookings::query( $args );
$total = ETHEBO_Bookings::count( $args );
$properties = get_posts( array(
'post_type' => ETHEBO_Post_Type::PT,
'post_status' => array( 'publish', 'private', 'draft', 'pending', 'future' ),
'numberposts' => -1,
'orderby' => 'title',
'order' => 'ASC',
) );
?>
| # |
|
|
|
|
|
|
|
|
| id ); ?> |
property_id ) ); ?> |
check_in ) ); ?> → check_out ) ); ?>
check_in, $r->check_out ) ) ); ?>
|
guest_name ? $r->guest_name : '—' ); ?>
guest_email ) : ?>
guest_email ); ?>
guest_phone ) : ?>
guest_phone ); ?>
source ) && $r->guests ) : ?>
guests ) ); ?>
notes ) : ?>
notes, 15 ) ); ?>
|
total ? esc_html( ethebo_price( $r->total ) ) : '—'; ?> |
source ) ); ?> |
status ] ?? $r->status ); ?>
payment_method ) : ?>
payment_method ) ); ?> ·
|
|
1 ) {
echo '';
echo wp_kses_post( paginate_links( array(
'base' => add_query_arg( 'paged', '%#%' ),
'format' => '',
'current' => $paged,
'total' => $pages,
) ) );
echo '
';
}
?>
source ) ) {
echo '' . esc_html( sprintf( ethebo__( 'Managed on %s' ), ethebo_source_label( $r->source ) ) ) . '';
return;
}
$links = array();
if ( 'pending' === $r->status ) {
$links[] = '' . esc_html( ethebo__( 'Confirm' ) ) . '';
}
if ( $r->payment_method && 'paid' !== $r->payment_status && 'cancelled' !== $r->status ) {
$links[] = '' . esc_html( ethebo__( 'Mark as paid' ) ) . '';
}
if ( 'cancelled' === $r->status ) {
$links[] = '' . esc_html( ethebo__( 'Restore' ) ) . '';
} else {
$links[] = '' . esc_html( ethebo__( 'Cancel' ) ) . '';
}
$links[] = '' . esc_html( ethebo__( 'Delete' ) ) . '';
echo wp_kses( implode( ' ', $links ), ethebo_allowed_html() );
}
private static function add_form( $properties, $selected ) {
// Prefilled when coming from a click on a day in the Calendar.
$in = isset( $_GET['check_in'] ) ? sanitize_text_field( wp_unslash( $_GET['check_in'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only screen filter, nothing is saved.
$in = ethebo_valid_date( $in ) ? $in : '';
$out = $in ? gmdate( 'Y-m-d', strtotime( $in . ' 00:00:00 UTC +1 day' ) ) : '';
?>
source ) ) {
self::redirect( array( 'ethebo_err' => ethebo__( 'This booking cannot be changed here.' ) ) );
}
switch ( $do ) {
case 'confirm':
case 'pending':
if ( 'cancelled' === $b->status && ! ETHEBO_Bookings::is_available( $b->property_id, $b->check_in, $b->check_out, $b->id ) ) {
self::redirect( array( 'ethebo_err' => ethebo__( 'These dates are already taken by another booking.' ) ) );
}
$new = 'confirm' === $do ? 'confirmed' : 'pending';
ETHEBO_Bookings::update( $id, array( 'status' => $new ) );
if ( 'confirmed' === $new && 'pending' === $b->status ) {
ethebo_notify_guest( $id, 'confirmed' );
}
self::redirect( array( 'ethebo_msg' => $new ) );
break;
case 'cancel':
ETHEBO_Bookings::update( $id, array( 'status' => 'cancelled' ) );
if ( 'website' === $b->source && in_array( $b->status, array( 'pending', 'confirmed' ), true ) ) {
ethebo_notify_guest( $id, 'cancelled' );
}
self::redirect( array( 'ethebo_msg' => 'cancelled' ) );
break;
case 'paid':
ETHEBO_Bookings::update( $id, array( 'payment_status' => 'paid', 'paid_amount' => $b->total ) );
if ( 'cancelled' !== $b->status ) {
ethebo_notify_guest( $id, 'paid' );
}
self::redirect( array( 'ethebo_msg' => 'paid' ) );
break;
case 'delete':
ETHEBO_Bookings::delete( $id );
self::redirect( array( 'ethebo_msg' => 'deleted' ) );
break;
}
self::redirect( array() );
}
public static function add_booking() {
if ( ! current_user_can( ETHEBO_CAP ) ) {
wp_die( esc_html( ethebo__( 'You do not have permission.' ) ) );
}
check_admin_referer( 'ethebo_add_booking' );
$p = wp_unslash( $_POST );
$post_id = absint( $p['property_id'] ?? 0 );
$in = sanitize_text_field( $p['check_in'] ?? '' );
$out = sanitize_text_field( $p['check_out'] ?? '' );
$status = sanitize_key( $p['status'] ?? 'confirmed' );
$status = in_array( $status, array( 'confirmed', 'pending', 'blocked' ), true ) ? $status : 'confirmed';
if ( ETHEBO_Post_Type::PT !== get_post_type( $post_id ) || ! ethebo_valid_date( $in ) || ! ethebo_valid_date( $out ) || $out <= $in ) {
self::redirect( array( 'ethebo_err' => ethebo__( 'Check the property and the dates (check-out must be after check-in).' ) ) );
}
if ( ! ETHEBO_Bookings::is_available( $post_id, $in, $out ) ) {
self::redirect( array( 'ethebo_err' => ethebo__( 'These dates overlap an existing booking.' ) ) );
}
$total = '' === ( $p['total'] ?? '' )
? ( 'blocked' === $status ? 0 : ethebo_nights( $in, $out ) * (float) ethebo_meta( $post_id, 'price', 0 ) )
: max( 0, (float) $p['total'] );
$id = ETHEBO_Bookings::create( array(
'property_id' => $post_id,
'check_in' => $in,
'check_out' => $out,
'status' => $status,
'source' => 'manual',
'guest_name' => substr( sanitize_text_field( $p['guest_name'] ?? '' ), 0, 190 ),
'guest_email' => substr( sanitize_email( $p['guest_email'] ?? '' ), 0, 190 ),
'guest_phone' => substr( sanitize_text_field( $p['guest_phone'] ?? '' ), 0, 50 ),
'guests' => max( 1, absint( $p['guests'] ?? 1 ) ),
'total' => $total,
'notes' => sanitize_textarea_field( $p['notes'] ?? '' ),
) );
if ( is_wp_error( $id ) ) {
self::redirect( array( 'ethebo_err' => $id->get_error_message() ) );
}
self::redirect( array( 'ethebo_msg' => 'added' ) );
}
/* ---------------------------------------------------------------------
* Settings
* ------------------------------------------------------------------ */
public static function register_settings() {
register_setting( 'ethebo_settings_group', 'ethebo_settings', array(
'type' => 'array',
'sanitize_callback' => array( __CLASS__, 'sanitize_settings' ),
'default' => ethebo_defaults(),
) );
}
/**
* Settings and Payments are separate forms on the same option: each form sends a "_section"
* and only the fields of that section change, everything else is kept.
*/
public static function sanitize_settings( $in ) {
$in = (array) $in;
$section = isset( $in['_section'] ) ? sanitize_key( $in['_section'] ) : 'all';
$out = wp_parse_args( (array) get_option( 'ethebo_settings', array() ), ethebo_defaults() );
if ( 'general' === $section || 'all' === $section || 0 === strpos( $section, 'gen_' ) ) {
$out = array_merge( $out, ETHEBO_Admin_Settings::sanitize( $in, $section ) );
}
if ( 'all' === $section || 0 === strpos( $section, 'pay_' ) ) {
$out = array_merge( $out, ETHEBO_Admin_Payments::sanitize( $in, $section, $out ) );
}
unset( $out['_section'], $out['enabled'] );
return $out;
}
public static function settings_updated( $old, $new ) {
if ( ( $old['sync_interval'] ?? '' ) !== ( $new['sync_interval'] ?? '' ) ) {
ETHEBO_Install::schedule( $new['sync_interval'] );
}
if ( ( $old['language'] ?? 'en' ) !== ( $new['language'] ?? 'en' ) ) {
update_option( 'ethebo_flush_rewrite', 1 ); // URL slugs depend on the language.
}
}
/** Settings → "Send a test email". */
public static function ajax_test_email() {
check_ajax_referer( 'ethebo_admin' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => ethebo__( 'You do not have permission.' ) ), 403 );
}
if ( ethebo_send_test_mail() ) {
wp_send_json_success( array( 'message' => sprintf( ethebo__( 'Sent to %s. Check the Inbox (and the Spam folder).' ), ethebo_admin_email() ) ) );
}
wp_send_json_error( array( 'message' => ethebo__( 'The server could not send the email. Install an SMTP plugin (e.g. WP Mail SMTP) and connect it to your domain email.' ) ) );
}
}