From 81ea0be874dd6fb131918ab45d8478d9fc940c4f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Mon, 25 Nov 2024 18:39:41 +0100 Subject: [PATCH] add custom template filter for EventPrime --- .../transformer/class-eventprime.php | 37 ++--- includes/plugins/class-eventprime.php | 154 ++++++++++++++++++ tests/test-class-plugin-eventprime.php | 111 +++++++++++++ 3 files changed, 277 insertions(+), 25 deletions(-) create mode 100644 tests/test-class-plugin-eventprime.php diff --git a/includes/activitypub/transformer/class-eventprime.php b/includes/activitypub/transformer/class-eventprime.php index 05546ee..2551b17 100644 --- a/includes/activitypub/transformer/class-eventprime.php +++ b/includes/activitypub/transformer/class-eventprime.php @@ -22,40 +22,27 @@ use GatherPress\Core\Event as GatherPress_Event; * @since 1.0.0 */ final class EventPrime extends Event { - - /** - * The current GatherPress Event object. - * - * @var GatherPress_Event - */ - protected $gp_event; - - /** - * Extend the constructor, to also set the GatherPress objects. - * - * This is a special class object form The Events Calendar which - * has a lot of useful functions, we make use of our getter functions. - * - * @param WP_Post $wp_object The WordPress object. - * @param string $wp_taxonomy The taxonomy slug of the event post type. - */ - public function __construct( $wp_object, $wp_taxonomy ) { - parent::__construct( $wp_object, $wp_taxonomy ); - $this->gp_event = new GatherPress_Event( $this->wp_object->ID ); - $this->gp_venue = $this->gp_event->get_venue_information(); - } - /** * Get the end time from the event object. */ protected function get_end_time(): ?string { - return $this->gp_event->get_datetime_end( 'Y-m-d\TH:i:s\Z' ); + $timestamp = get_post_meta( $this->wp_object->ID, 'em_end_date', true ); + if ( $timestamp ) { + return \gmdate( 'Y-m-d\TH:i:s\Z', $timestamp ); + } else { + return null; + } } /** * Get the end time from the event object. */ protected function get_start_time(): string { - return $this->gp_event->get_datetime_start( 'Y-m-d\TH:i:s\Z' ); + $timestamp = get_post_meta( $this->wp_object->ID, 'em_start_date', true ); + if ( $timestamp ) { + return \gmdate( 'Y-m-d\TH:i:s\Z', $timestamp ); + } else { + return ''; + } } } diff --git a/includes/plugins/class-eventprime.php b/includes/plugins/class-eventprime.php index a725b37..17a944e 100644 --- a/includes/plugins/class-eventprime.php +++ b/includes/plugins/class-eventprime.php @@ -9,6 +9,9 @@ namespace ActivityPub_Event_Bridge\Plugins; +use Activitypub\Signature; +use Eventprime_Basic_Functions; + // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore @@ -18,6 +21,13 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore * @since 1.0.0 */ final class EventPrime extends Event_Plugin { + /** + * Add filter for the template inclusion. + */ + public function __construct() { + \add_filter( 'template_include', array( self::class, 'render_activitypub_template' ), 100 ); + } + /** * Returns the full plugin file. * @@ -62,4 +72,148 @@ final class EventPrime extends Event_Plugin { public static function get_event_category_taxonomy(): string { return 'em_event_type'; } + + /** + * Determine whether the current request is an EventPrime ActivityPub request. + */ + private static function is_eventprime_activitypub_request() { + global $wp_query; + + /* + * ActivityPub requests are currently only made for + * author archives, singular posts, and the homepage. + */ + if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) ) { + return false; + } + + // Check if the current post type supports ActivityPub. + if ( \is_singular() ) { + $queried_object = \get_queried_object(); + + if ( ! $queried_object instanceof \WP_Post ) { + return false; + } + + if ( '[em_event]' !== $queried_object->post_content && '[em_events]' !== $queried_object->post_content ) { + return false; + } + } + + // Check if header already sent. + if ( ! \headers_sent() && ACTIVITYPUB_SEND_VARY_HEADER ) { + // Send Vary header for Accept header. + \header( 'Vary: Accept' ); + } + + // One can trigger an ActivityPub request by adding ?activitypub to the URL. + if ( isset( $wp_query->query_vars['activitypub'] ) ) { + return true; + } + + /* + * The other (more common) option to make an ActivityPub request + * is to send an Accept header. + */ + if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) { + $accept = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ); + + /* + * $accept can be a single value, or a comma separated list of values. + * We want to support both scenarios, + * and return true when the header includes at least one of the following: + * - application/activity+json + * - application/ld+json + * - application/json + */ + if ( preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) { + return true; + } + } + + return false; + } + + /** + * Extract the post id of the event for an EventPrime event query. + * + * @return bool|int The post ID if an event could be identified, false otherwise. + */ + private static function get_eventprime_post_id() { + $event = get_query_var( 'event' ); + if ( ! $event ) { + if ( ! empty( filter_input( INPUT_GET, 'event', FILTER_SANITIZE_FULL_SPECIAL_CHARS ) ) ) { + $event = rtrim( filter_input( INPUT_GET, 'event', FILTER_SANITIZE_FULL_SPECIAL_CHARS ), '/\\' ); + } + } + + if ( $event ) { + $ep_basic_functions = new Eventprime_Basic_Functions(); + return $ep_basic_functions->ep_get_id_by_slug( $event, 'em_event' ); + } + + return false; + } + + /** + * Add the ActivityPub template for EventPrime. + * + * @param string $template The path to the template object. + * @return string The new path to the JSON template. + */ + public static function render_activitypub_template( $template ) { + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { + return $template; + } + + // Check if the request is a page with (solely) the eventprime shortcode in it. + if ( ! self::is_eventprime_activitypub_request() ) { + return $template; + } + + if ( ! \is_singular() ) { + return $template; + } + + $post_id = self::get_eventprime_post_id(); + + if ( $post_id ) { + $preview = \get_query_var( 'preview' ); + if ( $preview ) { + $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-preview.php'; + } else { + $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php'; + } + } + + /* + * Check if the request is authorized. + * + * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch + * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch + */ + if ( $activitypub_template && ACTIVITYPUB_AUTHORIZED_FETCH ) { + $verification = Signature::verify_http_signature( $_SERVER ); + if ( \is_wp_error( $verification ) ) { + header( 'HTTP/1.1 401 Unauthorized' ); + + // Fallback as template_loader can't return http headers. + return $template; + } + } + + if ( $activitypub_template ) { + global $post; + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.OverrideProhibited + $post = get_post( $post_id ); + + // Ensure WordPress functions use the new post data. + setup_postdata( $post ); + // Return the default ActivityPub template. + return $activitypub_template; + } + + return $template; + } } diff --git a/tests/test-class-plugin-eventprime.php b/tests/test-class-plugin-eventprime.php new file mode 100644 index 0000000..98217fb --- /dev/null +++ b/tests/test-class-plugin-eventprime.php @@ -0,0 +1,111 @@ + array( + 'venue' => 'Minimal Venue', + 'status' => 'publish', + ), + 'complex_venue' => array( + 'venue' => 'Complex Venue', + 'status' => 'publish', + 'show_map' => false, + 'show_map_link' => false, + 'address' => 'Venue address', + 'city' => 'Venue city', + 'country' => 'Venue country', + 'province' => 'Venue province', + 'state' => 'Venue state', + 'stateprovince' => 'Venue stateprovince', + 'zip' => 'Venue zip', + 'phone' => 'Venue phone', + 'website' => 'http://venue.com', + ), + ); + + public const MOCKUP_EVENTS = array( + 'minimal_event' => array( + 'title' => 'My Event', + 'content' => 'Come to my event!', + 'start_date' => '+10 days 15:00:00', + 'duration' => HOUR_IN_SECONDS, + 'status' => 'publish', + ), + 'complex_event' => array( + 'title' => 'My Event', + 'content' => 'Come to my event!', + 'start_date' => '+10 days 15:00:00', + 'duration' => HOUR_IN_SECONDS, + 'status' => 'publish', + ), + ); + + /** + * Override the setup function, so that tests don't run if the Events Calendar is not active. + */ + public function set_up() { + parent::set_up(); + + if ( ! class_exists( '\Eventprime_Basic_Functions' ) ) { + self::markTestSkipped( 'The EventPrime Calendar management plugin is not active.' ); + } + + // Make sure that ActivityPub support is enabled for The Events Calendar. + $aeb = \ActivityPub_Event_Bridge\Setup::get_instance(); + $aeb->activate_activitypub_support_for_active_event_plugins(); + + // Delete all posts afterwards. + _delete_all_posts(); + } + + /** + * Test that the right transformer gets applied. + */ + public function test_the_events_calendar_transformer_class() { + // We only test for one event plugin being active at the same time, + // even though we support multiple onces in theory. + // But testing all combinations is beyond scope. + $active_event_plugins = \ActivityPub_Event_Bridge\Setup::get_instance()->get_active_event_plugins(); + $this->assertEquals( 1, count( $active_event_plugins ) ); + + // Enable ActivityPub support for the event plugin. + $this->assertContains( 'tribe_events', get_option( 'activitypub_support_post_types' ) ); + + $event_data = array(); + $event_data['name'] = 'EventPrime Event title'; + $event_data['description'] = 'EventPrime event description'; + $event_data['status'] = 'Publish'; + $event_data['em_event_type'] = ''; + $event_data['em_venue'] = ''; + $event_data['em_organizer'] = ''; + $event_data['em_performer'] = ''; + $event_data['em_start_date'] = strtotime( '+10 days 15:00:00' ); + $event_data['em_end_date'] = strtotime( '+10 days 16:00:00' ); + $event_data['em_enable_booking'] = 'bookings_off'; + $event_data['em_ticket_price'] = 0; + + // Create an EventPrime Event without content. + $ep_functions = new Eventprime_Basic_Functions(); + + $post_id = $ep_functions->insert_event_post_data( $event_data ); + + $wp_object = get_post( $post_id ); + + // Call the transformer Factory. + $transformer = \Activitypub\Transformer\Factory::get_transformer( $wp_object ); + + // Check that we got the right transformer. + $this->assertInstanceOf( \ActivityPub_Event_Bridge\Activitypub\Transformer\EventPrime::class, $transformer ); + } +}