first commit

This commit is contained in:
Roman Pyrih
2026-04-21 15:48:41 +02:00
commit 7483681901
10216 changed files with 3236626 additions and 0 deletions

View File

@@ -0,0 +1,221 @@
<?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 ical feed
*/
public static function add_rewrite_rules() {
add_rewrite_rule(
'^yacht-ical/([0-9]+)/([a-zA-Z0-9]+)\.ics$',
'index.php?yacht_ical_id=$matches[1]&yacht_ical_token=$matches[2]',
'top'
);
// Flush rewrite rules if our rule is not registered yet.
$rules = get_option( 'rewrite_rules' );
if ( is_array( $rules ) && ! isset( $rules['^yacht-ical/([0-9]+)/([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_id';
$vars[] = 'yacht_ical_token';
return $vars;
}
/**
* Handle feed request
*/
public static function handle_feed_request() {
$yacht_id = (int) get_query_var( 'yacht_ical_id', 0 );
$token = get_query_var( 'yacht_ical_token', '' );
if ( ! $yacht_id || ! $token ) {
return;
}
$yacht = get_post( $yacht_id );
if ( ! $yacht || 'yacht' !== $yacht->post_type ) {
status_header( 404 );
exit;
}
$stored_token = self::get_feed_token( $yacht_id );
if ( ! $stored_token || ! hash_equals( $stored_token, $token ) ) {
status_header( 403 );
exit;
}
self::output_ics( $yacht );
}
/**
* Get or create feed token for a yacht
*
* @param int $yacht_id Yacht ID.
* @return string
*/
public static function get_feed_token( $yacht_id ) {
$token = get_post_meta( $yacht_id, '_yacht_ical_token', true );
if ( empty( $token ) ) {
$token = wp_generate_password( 24, false );
update_post_meta( $yacht_id, '_yacht_ical_token', $token );
}
return $token;
}
/**
* Regenerate feed token
*
* @param int $yacht_id Yacht ID.
* @return string
*/
public static function regenerate_token( $yacht_id ) {
$token = wp_generate_password( 24, false );
update_post_meta( $yacht_id, '_yacht_ical_token', $token );
return $token;
}
/**
* Get feed URL for a yacht
*
* @param int $yacht_id Yacht ID.
* @return string
*/
public static function get_feed_url( $yacht_id ) {
$token = self::get_feed_token( $yacht_id );
return home_url( sprintf( '/yacht-ical/%d/%s.ics', $yacht_id, $token ) );
}
/**
* Output .ics file
*
* @param \WP_Post $yacht Yacht post.
*/
private static function output_ics( $yacht ) {
$bookings = get_posts(
array(
'post_type' => 'yacht_booking',
'posts_per_page' => -1,
'post_status' => 'publish',
'meta_query' => array(
'relation' => 'AND',
array(
'key' => '_booking_yacht_id',
'value' => $yacht->ID,
),
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="' . sanitize_file_name( $yacht->post_title ) . '.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( $yacht->post_title . ' - ' . $site_name );
$lines[] = 'X-WR-TIMEZONE:Europe/Warsaw';
foreach ( $bookings as $booking ) {
$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 );
$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;
}
}