' . $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( '

%s

', 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( '

%s

', 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.' ) ) ); } }