233 lines
6.1 KiB
PHP
233 lines
6.1 KiB
PHP
<?php
|
|
/**
|
|
* iCal Feed Generator (export)
|
|
*
|
|
* Generates .ics feed per yacht for subscription by Google Calendar or other apps.
|
|
*
|
|
* @package YachtBooking
|
|
*/
|
|
|
|
namespace YachtBooking\Integrations\ICal;
|
|
|
|
use YachtBooking\Booking;
|
|
use YachtBooking\Yacht;
|
|
|
|
// Exit if accessed directly.
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* ICal Feed class
|
|
*/
|
|
class ICal_Feed {
|
|
|
|
/**
|
|
* Register rewrite rules and query vars
|
|
*/
|
|
public static function register() {
|
|
add_action( 'init', array( __CLASS__, 'add_rewrite_rules' ) );
|
|
add_filter( 'query_vars', array( __CLASS__, 'add_query_vars' ) );
|
|
add_action( 'template_redirect', array( __CLASS__, 'handle_feed_request' ) );
|
|
}
|
|
|
|
/**
|
|
* Add rewrite rule for global ical feed
|
|
*/
|
|
public static function add_rewrite_rules() {
|
|
add_rewrite_rule(
|
|
'^yacht-ical-global/([a-zA-Z0-9]+)\.ics$',
|
|
'index.php?yacht_ical_global=1&yacht_ical_token=$matches[1]',
|
|
'top'
|
|
);
|
|
|
|
// Flush rewrite rules if our rule is not registered yet.
|
|
$rules = get_option( 'rewrite_rules' );
|
|
if ( is_array( $rules ) && ! isset( $rules['^yacht-ical-global/([a-zA-Z0-9]+)\.ics$'] ) ) {
|
|
flush_rewrite_rules( false );
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add query vars
|
|
*
|
|
* @param array $vars Query vars.
|
|
* @return array
|
|
*/
|
|
public static function add_query_vars( $vars ) {
|
|
$vars[] = 'yacht_ical_token';
|
|
$vars[] = 'yacht_ical_global';
|
|
return $vars;
|
|
}
|
|
|
|
/**
|
|
* Handle feed request — globalny feed iCal (jeden plik dla całej floty).
|
|
*/
|
|
public static function handle_feed_request() {
|
|
$is_global = (int) get_query_var( 'yacht_ical_global', 0 );
|
|
$token = get_query_var( 'yacht_ical_token', '' );
|
|
|
|
if ( ! $is_global || ! $token ) {
|
|
return;
|
|
}
|
|
|
|
$stored_token = self::get_global_feed_token();
|
|
if ( ! $stored_token || ! hash_equals( $stored_token, $token ) ) {
|
|
status_header( 403 );
|
|
exit;
|
|
}
|
|
|
|
self::output_global_ics();
|
|
}
|
|
|
|
/**
|
|
* Get or create global feed token (one shared token for all-yachts feed).
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function get_global_feed_token() {
|
|
$token = get_option( 'yacht_booking_global_ical_token', '' );
|
|
|
|
if ( empty( $token ) ) {
|
|
$token = wp_generate_password( 24, false );
|
|
update_option( 'yacht_booking_global_ical_token', $token );
|
|
}
|
|
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* Regenerate global feed token (invalidates the previous URL).
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function regenerate_global_token() {
|
|
$token = wp_generate_password( 24, false );
|
|
update_option( 'yacht_booking_global_ical_token', $token );
|
|
return $token;
|
|
}
|
|
|
|
/**
|
|
* Get global feed URL (all yachts in one .ics).
|
|
*
|
|
* @return string
|
|
*/
|
|
public static function get_global_feed_url() {
|
|
$token = self::get_global_feed_token();
|
|
return home_url( sprintf( '/yacht-ical-global/%s.ics', $token ) );
|
|
}
|
|
|
|
/**
|
|
* Output global .ics feed — wszystkie jachty w jednym pliku.
|
|
*
|
|
* Każdy event ma SUMMARY w formacie "{nazwa_jachtu} - {klient}".
|
|
* Eventy z _booking_source = 'ical_import_global' są pomijane (anti-loop —
|
|
* nie wysyłamy z powrotem do Google tego, co stamtąd przyszło).
|
|
*/
|
|
private static function output_global_ics() {
|
|
$bookings = get_posts(
|
|
array(
|
|
'post_type' => 'yacht_booking',
|
|
'posts_per_page' => -1,
|
|
'post_status' => 'publish',
|
|
'meta_query' => array(
|
|
array(
|
|
'key' => '_booking_status',
|
|
'value' => 'cancelled',
|
|
'compare' => '!=',
|
|
),
|
|
),
|
|
)
|
|
);
|
|
|
|
$site_name = get_bloginfo( 'name' );
|
|
$domain = wp_parse_url( home_url(), PHP_URL_HOST );
|
|
|
|
header( 'Content-Type: text/calendar; charset=utf-8' );
|
|
header( 'Content-Disposition: inline; filename="yachts-all.ics"' );
|
|
header( 'Cache-Control: no-cache, must-revalidate' );
|
|
|
|
$lines = array();
|
|
$lines[] = 'BEGIN:VCALENDAR';
|
|
$lines[] = 'VERSION:2.0';
|
|
$lines[] = 'PRODID:-//YachtBooking//NONSGML v1.0//PL';
|
|
$lines[] = 'CALSCALE:GREGORIAN';
|
|
$lines[] = 'METHOD:PUBLISH';
|
|
$lines[] = 'X-WR-CALNAME:' . self::escape_ical(
|
|
sprintf(
|
|
/* translators: %s: site name */
|
|
__( 'Wszystkie jachty - %s', 'yacht-booking' ),
|
|
$site_name
|
|
)
|
|
);
|
|
$lines[] = 'X-WR-TIMEZONE:Europe/Warsaw';
|
|
|
|
foreach ( $bookings as $booking ) {
|
|
// Anti-loop: pomiń eventy które zostały zaimportowane z globalnego GCal.
|
|
$source = get_post_meta( $booking->ID, '_booking_source', true );
|
|
if ( ICal_Import::GLOBAL_IMPORT_SOURCE === $source ) {
|
|
continue;
|
|
}
|
|
|
|
$yacht_id = (int) Booking::get_yacht_id( $booking->ID );
|
|
if ( ! $yacht_id ) {
|
|
continue;
|
|
}
|
|
|
|
$yacht = get_post( $yacht_id );
|
|
if ( ! $yacht ) {
|
|
continue;
|
|
}
|
|
|
|
$start = Booking::get_start_date( $booking->ID );
|
|
$end = Booking::get_end_date( $booking->ID );
|
|
$status = Booking::get_status( $booking->ID );
|
|
$name = Booking::get_customer_name( $booking->ID );
|
|
|
|
if ( ! $start || ! $end ) {
|
|
continue;
|
|
}
|
|
|
|
// iCal DTEND for all-day events is exclusive.
|
|
$end_exclusive = gmdate( 'Ymd', strtotime( $end . ' +1 day' ) );
|
|
$created = get_the_date( 'Ymd\THis\Z', $booking );
|
|
|
|
// Prefiks nazwy jachtu — kluczowy dla późniejszego importu po prefiksie.
|
|
$summary = sprintf( '%s - %s', $yacht->post_title, $name );
|
|
if ( 'pending' === $status ) {
|
|
$summary = '[' . __( 'Oczekująca', 'yacht-booking' ) . '] ' . $summary;
|
|
}
|
|
|
|
$lines[] = 'BEGIN:VEVENT';
|
|
$lines[] = 'UID:booking-' . $booking->ID . '@' . $domain;
|
|
$lines[] = 'DTSTART;VALUE=DATE:' . gmdate( 'Ymd', strtotime( $start ) );
|
|
$lines[] = 'DTEND;VALUE=DATE:' . $end_exclusive;
|
|
$lines[] = 'DTSTAMP:' . gmdate( 'Ymd\THis\Z' );
|
|
$lines[] = 'CREATED:' . $created;
|
|
$lines[] = 'SUMMARY:' . self::escape_ical( $summary );
|
|
$lines[] = 'STATUS:CONFIRMED';
|
|
$lines[] = 'TRANSP:OPAQUE';
|
|
$lines[] = 'END:VEVENT';
|
|
}
|
|
|
|
$lines[] = 'END:VCALENDAR';
|
|
|
|
echo implode( "\r\n", $lines ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- iCal format
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Escape iCal text value
|
|
*
|
|
* @param string $text Text.
|
|
* @return string
|
|
*/
|
|
private static function escape_ical( $text ) {
|
|
$text = str_replace( '\\', '\\\\', $text );
|
|
$text = str_replace( ',', '\\,', $text );
|
|
$text = str_replace( ';', '\\;', $text );
|
|
$text = str_replace( "\n", '\\n', $text );
|
|
return $text;
|
|
}
|
|
}
|