diff --git a/.forgejo/workflows/phpunit.yml b/.forgejo/workflows/phpunit.yml index f8e08ac..c79e189 100644 --- a/.forgejo/workflows/phpunit.yml +++ b/.forgejo/workflows/phpunit.yml @@ -38,7 +38,7 @@ jobs: path: | ${{ env.WP_CORE_DIR }} ${{ env.WP_TESTS_DIR }} - key: cache-wordpress-67-2 + key: cache-wordpress-67-3 - name: Cache Composer id: cache-composer-phpunit @@ -107,5 +107,10 @@ jobs: - name: Run Integration tests for Modern Events Calendar Lite run: cd /workspace/Event-Federation/wordpress-activitypub-event-bridge/ && ./vendor/bin/phpunit --filter=modern_events_calendar_lite + env: + PHP_VERSION: ${{ matrix.php-version }} + + - name: Run Integration tests for EventPrime + run: cd /workspace/Event-Federation/wordpress-activitypub-event-bridge/ && ./vendor/bin/phpunit --filter=eventprime env: PHP_VERSION: ${{ matrix.php-version }} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a9a38f..ce5ffe0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +* Integration for EventPrime – Events Calendar, Bookings and Tickets + ### Fixed * Fixed that transformer hook function might also return `null`. diff --git a/README.md b/README.md index 2e12c7a..7e64f0f 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,7 @@ This plugin depends on the [ActivityPub plugin](https://wordpress.org/plugins/ac * [Eventin](https://de.wordpress.org/plugins/wp-event-solution/) * [Modern Events Calendar Lite](https://webnus.net/modern-events-calendar/) * [GatherPress](https://gatherpress.org/) +* [EventPrime – Events Calendar, Bookings and Tickets](https://wordpress.org/plugins/eventprime-event-calendar-management/) ## Configuration ## @@ -102,6 +103,10 @@ We're always interested in your feedback. Feel free to reach out to us via [E-Ma ## Unreleased +### Added + +* Integration for EventPrime – Events Calendar, Bookings and Tickets + ### Fixed * Fixed that transformer hook function might also return `null`. diff --git a/bin/install-wp-tests.sh b/bin/install-wp-tests.sh index 3cc440f..7cbf282 100755 --- a/bin/install-wp-tests.sh +++ b/bin/install-wp-tests.sh @@ -257,6 +257,7 @@ install_wp_plugins() { install_wp_plugin the-events-calendar "6.8.1" install_wp_plugin very-simple-event-list install_wp_plugin gatherpress + install_wp_plugin eventprime-event-calendar-management install_wp_plugin events-manager "6.6.3" install_wp_plugin wp-event-manager "3.1.45.1" install_wp_plugin wp-event-solution "4.0.14" diff --git a/composer.json b/composer.json index 4da8b48..d4c2b0b 100644 --- a/composer.json +++ b/composer.json @@ -54,11 +54,12 @@ "@test-events-manager", "@test-wp-event-manager", "@test-eventin", - "@test-modern-events-calendar-lite" + "@test-modern-events-calendar-lite", + "@test-eventprime" ], "test-debug": [ "@prepare-test", - "@test-gatherpress" + "@test-eventprime" ], "test-vs-event-list": "phpunit --filter=vs_event_list", "test-the-events-calendar": "phpunit --filter=the_events_calendar", @@ -67,6 +68,7 @@ "test-wp-event-manager": "phpunit --filter=wp_event_manager", "test-eventin": "phpunit --filter=eventin", "test-modern-events-calendar-lite": "phpunit --filter=modern_events_calendar_lite", + "test-eventprime": "phpunit --filter=eventprime", "test-all": "phpunit" } } diff --git a/includes/activitypub/transformer/class-eventprime.php b/includes/activitypub/transformer/class-eventprime.php new file mode 100644 index 0000000..c247ebe --- /dev/null +++ b/includes/activitypub/transformer/class-eventprime.php @@ -0,0 +1,78 @@ +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 { + $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 ''; + } + } + + /** + * Get location from the event object. + */ + protected function get_location(): ?Place { + $venue_term_id = get_post_meta( $this->wp_object->ID, 'em_venue', true ); + if ( ! $venue_term_id ) { + return null; + } + + $venue = wp_get_post_terms( $this->wp_object->ID, 'em_venue' ); + + if ( empty( $venue ) ) { + return null; + } else { + $venue = $venue[0]; + } + + $place = new Place(); + + $place->set_name( $venue->name ); + $place->set_content( $venue->description ); + + $address = get_term_meta( $venue->term_id, 'em_address', true ); + $display_address = get_term_meta( $venue->term_id, 'em_display_address_on_frontend', true ); + + if ( $address && $display_address ) { + $place->set_address( get_term_meta( $venue->term_id, 'em_address', true ) ); + } + + return $place; + } +} diff --git a/includes/activitypub/transformer/class-gatherpress.php b/includes/activitypub/transformer/class-gatherpress.php index 756524a..ea742b3 100644 --- a/includes/activitypub/transformer/class-gatherpress.php +++ b/includes/activitypub/transformer/class-gatherpress.php @@ -1,6 +1,6 @@ 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; + + $post = get_post( $post_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + + // Ensure WordPress functions use the new post data. + setup_postdata( $post ); + // Return the default ActivityPub template. + return $activitypub_template; + } + + return $template; + } +} diff --git a/readme.txt b/readme.txt index 66ef1b9..f200dd6 100644 --- a/readme.txt +++ b/readme.txt @@ -57,6 +57,7 @@ This plugin depends on the [ActivityPub plugin](https://wordpress.org/plugins/ac * [Eventin](https://de.wordpress.org/plugins/wp-event-solution/) * [Modern Events Calendar Lite](https://webnus.net/modern-events-calendar/) * [GatherPress](https://gatherpress.org/) +* [EventPrime – Events Calendar, Bookings and Tickets](https://wordpress.org/plugins/eventprime-event-calendar-management/) == Configuration == diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 4262f2b..fef1717 100755 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -71,6 +71,9 @@ function _manually_load_plugin() { case 'wp_event_manager': $plugin_file = 'wp-event-manager/wp-event-manager.php'; break; + case 'eventprime': + $plugin_file = 'eventprime-event-calendar-management/event-prime.php'; + break; } if ( $plugin_file ) { diff --git a/tests/test-class-plugin-eventprime.php b/tests/test-class-plugin-eventprime.php new file mode 100644 index 0000000..354f6df --- /dev/null +++ b/tests/test-class-plugin-eventprime.php @@ -0,0 +1,186 @@ +activate_activitypub_support_for_active_event_plugins(); + + // Delete all posts afterwards. + _delete_all_posts(); + + $this->setup_mockup_data(); + } + + /** + * Setup mockup events. + */ + private function setup_mockup_data() { + $this->mockup_events = array( + 'minimal_event' => array( + 'name' => 'EventPrime Event title', + 'description' => 'EventPrime event description', + 'status' => 'Publish', + 'em_event_type' => '', + 'em_venue' => '', + 'em_organizer' => '', + 'em_performer' => '', + 'em_start_date' => strtotime( '+10 days 15:00:00' ), + 'em_end_date' => strtotime( '+10 days 16:00:00' ), + 'em_enable_booking' => 'bookings_off', + 'em_ticket_price' => 0, + ), + ); + + $this->mockup_venue = array( + 'name' => 'Test Venue', + 'address' => 'Fediverse-street 1337, 1234 Fediverse-town', + ); + } + + /** + * 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( 'em_event', get_option( 'activitypub_support_post_types' ) ); + + // Create an EventPrime Event without content. + $ep_functions = new Eventprime_Basic_Functions(); + + $post_id = $ep_functions->insert_event_post_data( $this->mockup_events['minimal_event'] ); + + $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 ); + } + + /** + * Test transformation of minimal event. + */ + public function test_transformation_of_minimal_event() { + // Create an EventPrime Event without content. + $ep_functions = new Eventprime_Basic_Functions(); + + $post_id = $ep_functions->insert_event_post_data( $this->mockup_events['minimal_event'] ); + + $wp_object = get_post( $post_id ); + + // Call the transformer Factory. + $event_array = \Activitypub\Transformer\Factory::get_transformer( $wp_object )->to_object()->to_array(); + + // Check that the event ActivityStreams representation contains everything as expected. + $this->assertEquals( 'Event', $event_array['type'] ); + $this->assertEquals( 'EventPrime Event title', $event_array['name'] ); + $this->assertEquals( 'EventPrime event description', wp_strip_all_tags( $event_array['content'] ) ); + $this->assertEquals( gmdate( 'Y-m-d', strtotime( '+10 days 15:00:00' ) ) . 'T15:00:00Z', $event_array['startTime'] ); + $this->assertEquals( gmdate( 'Y-m-d', strtotime( '+10 days 16:00:00' ) ) . 'T16:00:00Z', $event_array['endTime'] ); + $this->assertTrue( $event_array['commentsEnabled'] ); + $this->assertEquals( 'allow_all', $event_array['repliesModerationOption'] ); + $this->assertEquals( 'external', $event_array['joinMode'] ); + $this->assertArrayNotHasKey( 'location', $event_array ); + $this->assertEquals( 'MEETING', $event_array['category'] ); + } + + /** + * Test transformation of minimal event. + */ + public function test_transformation_of_minimal_event_with_venue() { + // Create an EventPrime Event without content. + $ep_functions = new Eventprime_Basic_Functions(); + + $venue_term_id = wp_insert_term( $this->mockup_venue['name'], 'em_venue' )['term_id']; + add_term_meta( $venue_term_id, 'em_address', $this->mockup_venue['address'], true ); + add_term_meta( $venue_term_id, 'em_display_address_on_frontend', true, true ); + + $event_data = $this->mockup_events['minimal_event']; + $event_data['em_venue'] = $venue_term_id; + + $post_id = $ep_functions->insert_event_post_data( $event_data ); + + $wp_object = get_post( $post_id ); + + // Call the transformer Factory. + $event_array = \Activitypub\Transformer\Factory::get_transformer( $wp_object )->to_object()->to_array(); + + // Check that the event ActivityStreams representation contains everything as expected. + $this->assertEquals( 'Event', $event_array['type'] ); + $this->assertEquals( 'EventPrime Event title', $event_array['name'] ); + $this->assertEquals( 'EventPrime event description', wp_strip_all_tags( $event_array['content'] ) ); + $this->assertEquals( gmdate( 'Y-m-d', strtotime( '+10 days 15:00:00' ) ) . 'T15:00:00Z', $event_array['startTime'] ); + $this->assertEquals( gmdate( 'Y-m-d', strtotime( '+10 days 16:00:00' ) ) . 'T16:00:00Z', $event_array['endTime'] ); + $this->assertTrue( $event_array['commentsEnabled'] ); + $this->assertEquals( 'allow_all', $event_array['repliesModerationOption'] ); + $this->assertEquals( 'external', $event_array['joinMode'] ); + $this->assertEquals( $this->mockup_venue['name'], $event_array['location']['name'] ); + $this->assertEquals( $this->mockup_venue['address'], $event_array['location']['address'] ); + + $this->assertEquals( 'MEETING', $event_array['category'] ); + } + + /** + * Test transformation of minimal event with venue which has a hidden address. + */ + public function test_transformation_of_minimal_event_with_venue_with_hidden_address() { + // Create an EventPrime Event without content. + $ep_functions = new Eventprime_Basic_Functions(); + + $venue_term_id = wp_insert_term( $this->mockup_venue['name'], 'em_venue' )['term_id']; + add_term_meta( $venue_term_id, 'em_address', $this->mockup_venue['address'], true ); + add_term_meta( $venue_term_id, 'em_display_address_on_frontend', false, true ); + + $event_data = $this->mockup_events['minimal_event']; + $event_data['em_venue'] = $venue_term_id; + + $post_id = $ep_functions->insert_event_post_data( $event_data ); + + $wp_object = get_post( $post_id ); + + // Call the transformer Factory. + $event_array = \Activitypub\Transformer\Factory::get_transformer( $wp_object )->to_object()->to_array(); + + // Check that the event ActivityStreams representation contains everything as expected. + $this->assertArrayNotHasKey( 'address', $event_array['location'] ); + } +}