array(
'name' => ethebo__( 'Properties' ),
'singular_name' => ethebo__( 'Property' ),
'menu_name' => ethebo__( 'Accommodation' ),
'all_items' => ethebo__( 'All properties' ),
'add_new' => ethebo__( 'Add property' ),
'add_new_item' => ethebo__( 'Add new property' ),
'edit_item' => ethebo__( 'Edit property' ),
'new_item' => ethebo__( 'New property' ),
'view_item' => ethebo__( 'View property' ),
'search_items' => ethebo__( 'Search properties' ),
'not_found' => ethebo__( 'No properties found' ),
'not_found_in_trash' => ethebo__( 'No properties in trash' ),
'featured_image' => ethebo__( 'Main photo' ),
'set_featured_image' => ethebo__( 'Set main photo' ),
),
'public' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-building',
'menu_position' => 25,
'supports' => array( 'title', 'editor', 'thumbnail', 'excerpt' ),
'rewrite' => array( 'slug' => $slug ),
'show_in_rest' => true,
) );
register_taxonomy( self::TAX, self::PT, array(
'labels' => array(
'name' => ethebo__( 'Types' ),
'singular_name' => ethebo__( 'Type' ),
'add_new_item' => ethebo__( 'Add new type' ),
'edit_item' => ethebo__( 'Edit type' ),
'all_items' => ethebo__( 'All types' ),
),
'hierarchical' => true,
'show_admin_column' => true,
'show_in_rest' => true,
'rewrite' => array( 'slug' => $tax_slug ),
) );
}
/**
* The URL slugs depend on the language. Whenever they differ from the ones WordPress
* last saved (language change, plugin update…), rebuild the rewrite rules once.
*/
public static function maybe_flush() {
$slugs = implode( '|', ethebo_slugs() );
if ( get_option( 'ethebo_flush_rewrite' ) || get_option( 'ethebo_rewrite_slugs' ) !== $slugs ) {
delete_option( 'ethebo_flush_rewrite' );
update_option( 'ethebo_rewrite_slugs', $slugs );
flush_rewrite_rules( false );
}
}
/** Old links (e.g. /akomodim/room-1/ after switching to English) redirect to the current slug. */
public static function redirect_old_slugs() {
if ( ! is_404() ) {
return;
}
$path = trim( (string) wp_parse_url( isset( $_SERVER['REQUEST_URI'] ) ? esc_url_raw( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '', PHP_URL_PATH ), '/' );
$home = trim( (string) wp_parse_url( home_url(), PHP_URL_PATH ), '/' );
$path = $home && 0 === strpos( $path, $home . '/' ) ? substr( $path, strlen( $home ) + 1 ) : $path;
$parts = explode( '/', $path, 2 );
list( $slug, $tax_slug ) = ethebo_slugs();
foreach ( ethebo_slugs( true ) as $old ) {
$map = array( $old[0] => $slug, $old[1] => $tax_slug );
if ( isset( $map[ $parts[0] ] ) && $map[ $parts[0] ] !== $parts[0] ) {
// "type"/"tipo" are common words, so only redirect taxonomy paths that really exist.
if ( $old[1] === $parts[0] && ( empty( $parts[1] ) || ! get_term_by( 'slug', strtok( $parts[1], '/' ), self::TAX ) ) ) {
continue;
}
wp_safe_redirect( user_trailingslashit( home_url( '/' . $map[ $parts[0] ] . ( isset( $parts[1] ) ? '/' . $parts[1] : '' ) ) ), 301 );
exit;
}
}
}
public static function meta_boxes() {
add_meta_box( 'ethebo_details', ethebo__( 'Details & price' ), array( __CLASS__, 'details_box' ), self::PT, 'normal', 'high' );
add_meta_box( 'ethebo_gallery', ethebo__( 'Photo gallery' ), array( __CLASS__, 'gallery_box' ), self::PT, 'normal', 'high' );
add_meta_box( 'ethebo_sync', ethebo__( 'Sync with Booking.com / Airbnb' ), array( __CLASS__, 'sync_box' ), self::PT, 'normal', 'default' );
add_meta_box( 'ethebo_upcoming', ethebo__( 'Upcoming bookings' ), array( __CLASS__, 'upcoming_box' ), self::PT, 'side', 'default' );
}
public static function details_box( $post ) {
wp_nonce_field( 'ethebo_save_property', 'ethebo_nonce' );
$fields = array(
// label, min, step, default
'price' => array( sprintf( ethebo__( 'Price per night (%s)' ), ethebo_opt( 'currency' ) ), 0, '0.01', '' ),
'max_guests' => array( ethebo__( 'Max guests' ), 1, '1', 2 ),
'min_nights' => array( ethebo__( 'Minimum nights' ), 1, '1', 1 ),
'bedrooms' => array( ethebo__( 'Bedrooms' ), 0, '1', '' ),
'bathrooms' => array( ethebo__( 'Bathrooms' ), 0, '1', '' ),
);
echo '
';
foreach ( $fields as $key => $f ) {
printf(
'
%2$s
',
esc_attr( $key ),
esc_html( $f[0] ),
esc_attr( ethebo_meta( $post->ID, $key, $f[3] ) ),
esc_attr( $f[1] ),
esc_attr( $f[2] )
);
}
echo '
';
echo '' . esc_html( ethebo__( 'For a hotel: create each room as a separate property (e.g. "Hotel X – Double Room 101"), so each one has its own calendar.' ) ) . '
';
}
/** Attachment IDs of the property gallery, in the chosen order. */
public static function gallery_ids( $post_id ) {
return array_values( array_filter( array_map( 'absint', explode( ',', (string) get_post_meta( $post_id, '_ethebo_gallery', true ) ) ) ) );
}
public static function gallery_box( $post ) {
$ids = self::gallery_ids( $post->ID );
?>
×
+
post_status;
$feeds = array(
'booking' => array(
'label' => 'Booking.com',
'placeholder' => 'https://ical.booking.com/v1/export?t=...',
'help' => ethebo__( 'Booking.com Extranet → Rates & Availability → Sync calendars → choose the room → Export calendar → copy the link.' ),
),
'airbnb' => array(
'label' => 'Airbnb',
'placeholder' => 'https://www.airbnb.com/calendar/ical/123456.ics?s=...',
'help' => ethebo__( 'Airbnb → Listings → choose the listing → Availability → Connect calendars → Export calendar → copy the link.' ),
),
);
?>
2
Import calendar (same place where you got the link in step 1).' ) ); ?>
$label ) : ?>
ID, $ch ); ?>
ID ); ?>
ID ); ?>
Publish / Save once and the links for Booking.com and Airbnb will appear here.' ) ); ?>
check_out > $today;
} );
?>
check_in ) . ' → ' . ethebo_format_date( $r->check_out ) ); ?>
· source ) . ' · ' . ( ethebo_statuses()[ $r->status ] ?? $r->status ) ); ?>
$post->ID, 'upcoming' => true, 'limit' => 6 ) );
$rows = array_filter( $rows, function ( $r ) {
return 'cancelled' !== $r->status;
} );
if ( ! $rows ) {
echo '' . esc_html( ethebo__( 'No upcoming bookings.' ) ) . '
';
} else {
echo '';
foreach ( $rows as $r ) {
printf(
'%s %s → %s%s ',
esc_attr( sanitize_html_class( $r->source ) ),
esc_html( ethebo_source_label( $r->source ) ),
esc_html( ethebo_format_date( $r->check_in ) ),
esc_html( ethebo_format_date( $r->check_out ) ),
esc_html( $r->guest_name )
);
}
echo ' ';
}
printf(
'%s →
',
esc_url( admin_url( 'edit.php?post_type=' . self::PT . '&page=ethebo-bookings&property=' . $post->ID ) ),
esc_html( ethebo__( 'All bookings' ) )
);
}
public static function save( $post_id, $post ) {
if ( ! isset( $_POST['ethebo_nonce'] ) || ! wp_verify_nonce( sanitize_key( $_POST['ethebo_nonce'] ), 'ethebo_save_property' ) ) {
return;
}
if ( ( defined( 'DOING_AUTOSAVE' ) && DOING_AUTOSAVE ) || ! current_user_can( 'edit_post', $post_id ) ) {
return;
}
$in = isset( $_POST['ethebo'] ) ? map_deep( wp_unslash( (array) $_POST['ethebo'] ), 'sanitize_textarea_field' ) : array();
update_post_meta( $post_id, '_ethebo_price', max( 0, round( (float) ( $in['price'] ?? 0 ), 2 ) ) );
update_post_meta( $post_id, '_ethebo_max_guests', max( 1, absint( $in['max_guests'] ?? 1 ) ) );
wp_cache_delete( 'ethebo_max_guests', 'ethebo' ); // The guests filter of the search reads the new maximum.
update_post_meta( $post_id, '_ethebo_min_nights', max( 1, absint( $in['min_nights'] ?? 1 ) ) );
update_post_meta( $post_id, '_ethebo_bedrooms', '' === ( $in['bedrooms'] ?? '' ) ? '' : absint( $in['bedrooms'] ) );
update_post_meta( $post_id, '_ethebo_bathrooms', '' === ( $in['bathrooms'] ?? '' ) ? '' : absint( $in['bathrooms'] ) );
if ( isset( $in['gallery'] ) ) {
$gallery = array_unique( array_filter( array_map( 'absint', explode( ',', (string) $in['gallery'] ) ), 'wp_attachment_is_image' ) );
update_post_meta( $post_id, '_ethebo_gallery', implode( ',', $gallery ) );
}
$old_feeds = ETHEBO_iCal::feeds( $post_id );
update_post_meta( $post_id, '_ethebo_ical_booking', self::clean_url( $in['ical_booking'] ?? '' ) );
update_post_meta( $post_id, '_ethebo_ical_airbnb', self::clean_url( $in['ical_airbnb'] ?? '' ) );
$other = array_filter( array_map( array( __CLASS__, 'clean_url' ), preg_split( '/\s+/', (string) ( $in['ical_other'] ?? '' ) ) ) );
update_post_meta( $post_id, '_ethebo_ical_other', implode( "\n", array_unique( $other ) ) );
ETHEBO_iCal::token( $post_id );
// Links changed (or never synced): sync right away.
$feeds = ETHEBO_iCal::feeds( $post_id );
if ( $feeds !== $old_feeds || ( $feeds && ! get_post_meta( $post_id, '_ethebo_last_sync', true ) ) ) {
delete_transient( 'ethebo_sync_lock_' . $post_id );
ETHEBO_iCal::sync_property( $post_id );
}
}
/** Classic editor for properties: simpler, and meta boxes save/refresh immediately. */
public static function classic_editor( $use, $post_type ) {
return self::PT === $post_type ? false : $use;
}
public static function clean_url( $url ) {
$url = trim( (string) $url );
$url = preg_replace( '#^webcals?://#i', 'https://', $url );
return esc_url_raw( $url, array( 'http', 'https' ) );
}
public static function on_delete( $post_id ) {
if ( self::PT === get_post_type( $post_id ) ) {
ETHEBO_Bookings::delete_for_property( $post_id );
}
}
public static function columns( $cols ) {
$new = array();
foreach ( $cols as $key => $label ) {
$new[ $key ] = $label;
if ( 'title' === $key ) {
$new['ethebo_price'] = ethebo__( 'Price / night' );
$new['ethebo_guests'] = ethebo__( 'Guests' );
$new['ethebo_sync'] = ethebo__( 'Sync' );
}
}
return $new;
}
public static function column( $col, $post_id ) {
switch ( $col ) {
case 'ethebo_price':
echo esc_html( ethebo_price( ethebo_meta( $post_id, 'price', 0 ) ) );
break;
case 'ethebo_guests':
echo esc_html( ethebo_meta( $post_id, 'max_guests', '–' ) );
break;
case 'ethebo_sync':
$log = (array) get_post_meta( $post_id, '_ethebo_sync_log', true );
$log = array_filter( $log );
if ( ! $log ) {
echo '— ';
break;
}
foreach ( $log as $source => $e ) {
printf(
' %s ',
$e['ok'] ? 'ok' : 'err',
esc_attr( $e['ok'] ? 'OK' : $e['msg'] ),
esc_html( ethebo_source_label( $source ) )
);
}
break;
}
}
}