$booking Appointment and public display details. */ public static function render( array $booking ): string { $dates = array(); foreach ( array( 'start_utc', 'end_utc' ) as $key ) { $value = (string) ( $booking[ $key ] ?? '' ); $date = DateTimeImmutable::createFromFormat( '!Y-m-d H:i:s', $value, DateTimeHelper::utc_timezone() ); if ( ! $date || $date->format( 'Y-m-d H:i:s' ) !== $value ) { throw new InvalidArgumentException( 'Invalid calendar date.' ); } $dates[ $key ] = $date; } if ( $dates['end_utc'] <= $dates['start_utc'] ) { throw new InvalidArgumentException( 'Invalid calendar interval.' ); } $lines = array( 'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//Abientot//Booking//EN', 'CALSCALE:GREGORIAN', 'BEGIN:VEVENT', 'UID:' . hash( 'sha256', home_url( '/' ) . '|' . (int) $booking['id'] ) . '@abientot', 'DTSTAMP:' . gmdate( 'Ymd\THis\Z' ), 'DTSTART:' . $dates['start_utc']->format( 'Ymd\THis\Z' ), 'DTEND:' . $dates['end_utc']->format( 'Ymd\THis\Z' ), 'SUMMARY:' . self::escape( (string) ( $booking['service'] ?? '' ) ), 'LOCATION:' . self::escape( (string) ( $booking['location'] ?? '' ) ), 'STATUS:' . ( 'confirmed' === ( $booking['status'] ?? '' ) ? 'CONFIRMED' : 'TENTATIVE' ), 'END:VEVENT', 'END:VCALENDAR', ); return implode( "\r\n", array_map( array( self::class, 'fold' ), $lines ) ) . "\r\n"; } private static function escape( string $value ): string { $value = wp_strip_all_tags( $value ); $value = str_replace( array( "\r\n", "\r" ), "\n", $value ); $value = preg_replace( '/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/', '', $value ); return str_replace( array( '\\', ';', ',', "\n" ), array( '\\\\', '\\;', '\\,', '\\n' ), $value ); } /** Fold at 75 octets without splitting a UTF-8 character. */ private static function fold( string $line ): string { $output = ''; while ( strlen( $line ) > 75 ) { $length = 75; while ( ( ord( $line[ $length ] ) & 0xC0 ) === 0x80 ) { --$length; } $output .= substr( $line, 0, $length ) . "\r\n"; $line = ' ' . substr( $line, $length ); } return $output . $line; } }