'handle_connect',
'anybook_save_code' => 'handle_save_code',
'anybook_save_settings' => 'handle_save_settings',
'anybook_disconnect' => 'handle_disconnect',
'anybook_refresh' => 'handle_refresh',
'anybook_publish_page' => 'handle_publish_page',
);
/** Hooks the page and its actions. */
public static function init() {
add_action( 'admin_menu', array( __CLASS__, 'menu' ) );
add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_page_assets' ) );
foreach ( self::ACTIONS as $action => $method ) {
add_action( 'admin_post_' . $action, array( __CLASS__, $method ) );
}
add_filter( 'plugin_action_links_' . plugin_basename( ANYBOOK_FILE ), array( __CLASS__, 'action_links' ) );
add_action( 'admin_notices', array( __CLASS__, 'connect_notice' ) );
add_action( 'admin_init', array( Plugin::class, 'privacy_policy_content' ) );
}
/** Adds the Anybook menu item. */
public static function menu() {
$hook = add_menu_page(
__( 'Anybook Online Booking', 'anybook' ),
__( 'Anybook', 'anybook' ),
self::CAPABILITY,
self::PAGE,
array( __CLASS__, 'render_page' ),
'dashicons-calendar-alt',
58
);
if ( $hook ) {
add_action( 'load-' . $hook, array( __CLASS__, 'maybe_handle_return' ) );
}
}
/**
* The settings page's small style and its "are you sure" for Disconnect, on this page only.
*
* @param string $hook The admin page being loaded.
*/
public static function enqueue_page_assets( $hook ) {
if ( 'toplevel_page_' . self::PAGE !== $hook ) {
return;
}
wp_register_style( self::ASSET_HANDLE, false, array(), ANYBOOK_VERSION );
wp_enqueue_style( self::ASSET_HANDLE );
wp_add_inline_style( self::ASSET_HANDLE, '.anybook-inline-form{display:inline}.anybook-status .anybook-ok{color:#00a32a}.anybook-status .anybook-warn{color:#d63638}' );
wp_register_script( self::ASSET_HANDLE, false, array(), ANYBOOK_VERSION, true );
wp_enqueue_script( self::ASSET_HANDLE );
wp_add_inline_script( self::ASSET_HANDLE, 'document.addEventListener("submit",function(e){var m=e.target.getAttribute("data-anybook-confirm");if(m&&!window.confirm(m)){e.preventDefault();}});' );
}
/**
* A "Settings" link beside the plugin on the Plugins screen.
*
* @param array $links Existing links.
*/
public static function action_links( $links ) {
$links = is_array( $links ) ? $links : array();
$links[] = '' . esc_html__( 'Settings', 'anybook' ) . '';
return $links;
}
/**
* After activation, on the Plugins screen only: a pointer to Connect, until the site is connected. The plugin's
* own page has Connect itself, and no other admin screen shows this.
*/
public static function connect_notice() {
$screen = function_exists( 'get_current_screen' ) ? get_current_screen() : null;
if ( ! $screen || 'plugins' !== $screen->id ) {
return;
}
if ( ! current_user_can( self::CAPABILITY ) || Settings::is_connected() ) {
return;
}
printf(
'
',
esc_html__( 'Anybook Online Booking is ready. Connect it to your Anybook business to show your booking widget on this website.', 'anybook' ),
esc_url( self::page_url() ),
esc_html__( 'Connect to Anybook', 'anybook' )
);
}
/**
* This page's address, with an optional notice code.
*
* @param string $notice Notice code, or ''.
*/
public static function page_url( $notice = '' ) {
$url = admin_url( 'admin.php?page=' . self::PAGE );
return '' === $notice ? $url : add_query_arg( 'anybook_notice', rawurlencode( $notice ), $url );
}
/**
* The browser coming back from Anybook: handle it once, then reload the page without the return values so a
* refresh cannot replay it.
*/
public static function maybe_handle_return() {
// The single-use state Anybook carries back does the nonce's job here; Connection checks it.
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['state'] ) || ! ( isset( $_GET['anybook_slug'] ) || isset( $_GET['anybook_error'] ) ) ) {
return;
}
if ( ! current_user_can( self::CAPABILITY ) ) {
wp_die( esc_html__( 'Sorry, you are not allowed to connect this site to Anybook.', 'anybook' ), '', array( 'response' => 403 ) );
}
$query = array();
foreach ( array( 'state', 'anybook_slug', 'anybook_name', 'anybook_error' ) as $key ) {
if ( isset( $_GET[ $key ] ) && is_string( $_GET[ $key ] ) ) {
$query[ $key ] = sanitize_text_field( wp_unslash( $_GET[ $key ] ) );
}
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$notice = Connection::handle_return( $query, get_current_user_id() );
self::redirect( self::page_url( $notice ) );
}
/**
* Connect to Anybook: draft the booking page if asked, then send the browser to Anybook. The page is published
* only when the Owner agrees there (review Q1).
*/
public static function handle_connect() {
self::check( 'anybook_connect' );
if ( Settings::admin_origin() !== Settings::site_origin() ) {
// Anybook would allow the admin's address, where the widget never shows (review S-I1).
self::redirect( self::page_url( 'origin-mismatch' ) );
}
$guest_page = '';
$page_made = false;
if ( ! empty( $_POST['anybook_make_page'] ) && Booking_Page::is_held_back() ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- checked in check().
// The admin made the page private, pending or scheduled on purpose: it is neither published nor offered.
$guest_page = '';
} elseif ( ! empty( $_POST['anybook_make_page'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing -- checked in check().
if ( Booking_Page::is_published() ) {
$guest_page = (string) get_permalink( Booking_Page::current_id() );
} else {
$page_id = Booking_Page::ensure_draft();
if ( is_wp_error( $page_id ) ) {
self::redirect( self::page_url( 'page-failed' ) );
}
$guest_page = Booking_Page::future_permalink( $page_id );
$page_made = true;
}
}
$url = Connection::start( get_current_user_id(), $guest_page, $page_made );
// wp_safe_redirect, with the booking site allowed for this one redirect only.
$host = wp_parse_url( Settings::site(), PHP_URL_HOST );
$allow = function ( $hosts ) use ( $host ) {
$hosts[] = $host;
return $hosts;
};
add_filter( 'allowed_redirect_hosts', $allow );
self::redirect( $url );
}
/** The business-code fallback. */
public static function handle_save_code() {
self::check( 'anybook_save_code' );
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- checked in check().
$code = isset( $_POST['anybook_code'] ) ? sanitize_text_field( wp_unslash( $_POST['anybook_code'] ) ) : '';
self::redirect( self::page_url( Connection::save_code( $code ) ) );
}
/** Default colors, and the booking site address under Advanced. */
public static function handle_save_settings() {
self::check( 'anybook_save_settings' );
$notice = 'saved';
$changes = array();
// phpcs:disable WordPress.Security.NonceVerification.Missing -- checked in check().
foreach ( array(
'accent' => 'anybook_accent',
'text' => 'anybook_text',
) as $key => $field ) {
if ( ! isset( $_POST[ $field ] ) ) {
continue;
}
$raw = sanitize_text_field( wp_unslash( $_POST[ $field ] ) );
$clean = Settings::clean_colour( $raw );
if ( '' !== $raw && '' === $clean ) {
$notice = 'bad-colour'; // The color saved before stays.
continue;
}
$changes[ $key ] = $clean;
}
$site = isset( $_POST['anybook_site'] ) ? sanitize_text_field( wp_unslash( $_POST['anybook_site'] ) ) : '';
// phpcs:enable WordPress.Security.NonceVerification.Missing
if ( '' !== $site && Settings::clean_site( $site ) !== Settings::site() ) {
$clean_site = Settings::form_site( $site );
if ( ! self::may_change_site() ) {
$notice = 'site-not-allowed';
} elseif ( '' === $clean_site ) {
$notice = 'bad-site';
} else {
$changes['site'] = $clean_site;
Settings::clear_cache();
}
}
if ( $changes ) {
Settings::update( $changes );
}
self::redirect( self::page_url( $notice ) );
}
/**
* Whether this user may change the booking site on the settings page: never when wp-config.php sets it, and
* only with unfiltered_html (review S-M1).
*/
public static function may_change_site() {
return '' === Settings::site_from_constant() && current_user_can( self::SITE_CAPABILITY );
}
/** Disconnect from the business. */
public static function handle_disconnect() {
self::check( 'anybook_disconnect' );
Connection::disconnect();
self::redirect( self::page_url( 'disconnected' ) );
}
/**
* Publishes the booking page the plugin made, for when Anybook already uses it as the guest page but the return
* from Connect never arrived (the tab was closed after Allow, or the 15 minutes ran out).
*/
public static function handle_publish_page() {
self::check( 'anybook_publish_page' );
Booking_Page::publish( Booking_Page::current_id() );
self::redirect( self::page_url( 'page-published' ) );
}
/** Ask Anybook again for the status, at most once every 30 seconds per person (reviews S-L2, Q15). */
public static function handle_refresh() {
self::check( 'anybook_refresh' );
if ( ! Client::may_refresh( get_current_user_id() ) ) {
self::redirect( self::page_url( 'checked-recently' ) );
}
Settings::clear_cache();
self::redirect( self::page_url() );
}
/**
* Stops unless the user may act and the form's nonce is good.
*
* @param string $action Nonce action.
*/
private static function check( $action ) {
if ( ! current_user_can( self::CAPABILITY ) ) {
wp_die( esc_html__( 'Sorry, you are not allowed to change the Anybook settings.', 'anybook' ), '', array( 'response' => 403 ) );
}
check_admin_referer( $action );
}
/**
* Redirects and stops.
*
* @param string $url Where to.
*/
private static function redirect( $url ) {
wp_safe_redirect( $url );
exit;
}
/**
* The notice for a code, as [type, message], or null.
*
* @param string $code Notice code from the address.
*/
public static function notice( $code ) {
$fallback = __( 'You can also enter your business code below: Anybook shows it to every staff member under Settings, Online booking.', 'anybook' );
$notices = array(
'connected' => array( 'success', __( 'Connected to Anybook. Next, add the Anybook booking block to a page, or use your Book online page if you asked for one: it is now published.', 'anybook' ) ),
'connected-pending' => array( 'warning', __( 'Connected. Anybook has not approved your business yet, so the booking widget will not show until it does.', 'anybook' ) ),
'disconnected' => array( 'success', __( 'Disconnected. The booking widget no longer shows on this website, and your Book online page shows visitors nothing until you connect again. To stop this website showing your widget for good, remove it from allowed websites in Anybook too.', 'anybook' ) ),
'saved' => array( 'success', __( 'Settings saved.', 'anybook' ) ),
'error-cancelled' => array( 'warning', __( 'Connecting was cancelled on Anybook. Nothing was changed there, and a booking page made for this connect stays a draft.', 'anybook' ) ),
'error-not-owner' => array( 'warning', __( 'Only your business\'s Owner can connect a website on Anybook. Ask the Owner to click Connect here, or enter your business code below.', 'anybook' ) ),
'error-not-approved' => array( 'warning', __( 'Your business is not approved on Anybook yet. Connect again once Anybook has approved it.', 'anybook' ) . ' ' . $fallback ),
'error-other' => array( 'error', __( 'Anybook could not connect this site.', 'anybook' ) . ' ' . $fallback ),
'bad-state' => array( 'error', __( 'That connect link has expired or was already used. Please click Connect to Anybook again.', 'anybook' ) ),
'bad-return' => array( 'error', __( 'Anybook sent back something the plugin could not read. Please click Connect to Anybook again.', 'anybook' ) ),
'bad-code' => array( 'error', __( 'That is not a business code. Codes are letters, numbers and dashes, like bribie.', 'anybook' ) ),
'unknown-business' => array( 'error', __( 'Anybook has no approved business with that code. Check the code, or try again once Anybook has approved your business.', 'anybook' ) ),
'unreachable' => array( 'error', __( 'Anybook could not be reached. Please try again in a minute.', 'anybook' ) ),
'bad-colour' => array( 'error', __( 'A color was not saved: colors are written like #0b6e4f. The color saved before is kept.', 'anybook' ) ),
'bad-site' => array( 'error', __( 'The booking site must be https://site.anybookapp.com. For any other address, set ANYBOOK_SITE in wp-config.php. Other settings were saved.', 'anybook' ) ),
'site-not-allowed' => array( 'error', __( 'Only an administrator who may add scripts to this site can change the booking site address. Other settings were saved.', 'anybook' ) ),
'origin-mismatch' => array( 'error', __( 'Your WordPress admin and your website have different addresses, so Connect would allow the wrong one. Enter your business code below instead.', 'anybook' ) ),
'checked-recently' => array( 'info', __( 'Anybook was asked less than 30 seconds ago. The status below is its answer.', 'anybook' ) ),
'page-published' => array( 'success', __( 'Your booking page is published.', 'anybook' ) ),
'page-failed' => array( 'error', __( 'The booking page could not be made. Untick "Make a booking page for me" to connect without it.', 'anybook' ) ),
);
return isset( $notices[ $code ] ) ? $notices[ $code ] : null;
}
/** Prints the settings page. */
public static function render_page() {
if ( ! current_user_can( self::CAPABILITY ) ) {
return;
}
$settings = Settings::all();
// Only a code chosen from the fixed list above is shown; nothing from the address is printed.
$code = isset( $_GET['anybook_notice'] ) ? sanitize_key( wp_unslash( $_GET['anybook_notice'] ) ) : ''; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$notice = self::notice( $code );
?>
get_error_message();
if ( 'anybook_unreachable' === $status->get_error_code() ) {
$why .= ' ' . __( 'Only this page is affected: your visitors\' booking widget loads straight from Anybook and does not depend on this check.', 'anybook' );
}
self::line( false, $why );
} else {
self::line(
$status['approved'],
$status['approved'] ? __( 'Your business is approved on Anybook.', 'anybook' ) : __( 'Your business is waiting for Anybook\'s approval.', 'anybook' )
);
self::line(
$status['onlineBooking'],
$status['onlineBooking'] ? __( 'Online booking is on.', 'anybook' ) : __( 'Online booking is off on Anybook (Settings, Online booking).', 'anybook' )
);
self::line(
$status['originAllowed'],
$status['originAllowed']
/* translators: %s: this website's address. */
? sprintf( __( 'This website (%s) is on your allowed websites, so it can show your booking widget.', 'anybook' ), $origin )
/* translators: %s: this website's address. */
: sprintf( __( 'This website (%s) is not on your allowed websites in Anybook, so the widget will not load here. Connect again as the Owner, or add it in Anybook (Settings, Online booking).', 'anybook' ), $origin )
);
$guest_messages = array(
'this-site' => __( 'Your guest page (where guests\' email links open) is on this website.', 'anybook' ),
'elsewhere' => __( 'Your guest page (where guests\' email links open) is on another website.', 'anybook' ),
'none' => __( 'You have no guest page yet. Guests\' email links (to see, change or pay for a booking) need one: see Set your guest page below.', 'anybook' ),
);
self::line( 'this-site' === $status['guestPage'], $guest_messages[ $status['guestPage'] ] );
}
$page_messages = array(
'missing' => __( 'The booking page the plugin made has been deleted.', 'anybook' ),
'unpublished' => __( 'The booking page is not published.', 'anybook' ),
'no-widget' => __( 'The booking page no longer holds the Anybook widget.', 'anybook' ),
);
$problem = Booking_Page::problem();
if ( '' !== $problem ) {
self::line( false, $page_messages[ $problem ] );
}
?>
·
%2$s%3$s',
esc_attr( $ok ? 'dashicons-yes-alt anybook-ok' : 'dashicons-warning anybook-warn' ),
esc_html( $ok ? __( 'OK:', 'anybook' ) : __( 'To fix:', 'anybook' ) ),
esc_html( $text )
);
}
}