From 076e2619f0bf5244edd1e57373a5ab3858aa0a53 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sat, 14 Dec 2024 14:46:00 +0100 Subject: [PATCH] wip --- .../event-bridge-for-activitypub-admin.css | 13 + event-bridge-for-activitypub.php | 2 +- includes/activitypub/class-handler.php | 2 - .../collection/class-event-sources.php | 115 +++++-- .../activitypub/handler/class-announce.php | 49 --- includes/activitypub/handler/class-create.php | 34 +- includes/activitypub/handler/class-delete.php | 32 +- .../activitypub/model/class-event-source.php | 8 +- .../transmogrifier/class-gatherpress.php | 317 +++++++++++++++++- includes/class-event-sources.php | 90 ++--- includes/class-setup.php | 5 +- templates/event-sources.php | 6 +- templates/settings.php | 2 +- 13 files changed, 497 insertions(+), 178 deletions(-) delete mode 100644 includes/activitypub/handler/class-announce.php diff --git a/assets/css/event-bridge-for-activitypub-admin.css b/assets/css/event-bridge-for-activitypub-admin.css index 3b22ddd..17f255f 100644 --- a/assets/css/event-bridge-for-activitypub-admin.css +++ b/assets/css/event-bridge-for-activitypub-admin.css @@ -194,3 +194,16 @@ code.event-bridge-for-activitypub-settings-example-url { .event_bridge_for_activitypub-admin-table-container { padding-left: 22px; } + +.event_bridge_for_activitypub-admin-table-top > h2 { + display: inline-block; + font-size: 23px; + font-weight: 400; + margin: 0 10px 0 2px; + padding: 9px 0 4px; + line-height: 1.3; +} + +.event_bridge_for_activitypub-admin-table-top > a { + display: inline-block; +} diff --git a/event-bridge-for-activitypub.php b/event-bridge-for-activitypub.php index ec9cfad..37a27ff 100644 --- a/event-bridge-for-activitypub.php +++ b/event-bridge-for-activitypub.php @@ -3,7 +3,7 @@ * Plugin Name: Event Bridge for ActivityPub * Description: Integrating popular event plugins with the ActivityPub plugin. * Plugin URI: https://event-federation.eu/ - * Version: 0.3.2 + * Version: 0.3.2.4 * Author: André Menrath * Author URI: https://graz.social/@linos * Text Domain: event-bridge-for-activitypub diff --git a/includes/activitypub/class-handler.php b/includes/activitypub/class-handler.php index 0199486..0fa1433 100644 --- a/includes/activitypub/class-handler.php +++ b/includes/activitypub/class-handler.php @@ -13,7 +13,6 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub; defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Accept; -use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Announce; use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Update; use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Create; use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Delete; @@ -27,7 +26,6 @@ class Handler { */ public static function register_handlers() { Accept::init(); - Announce::init(); Update::init(); Create::init(); Delete::init(); diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index feea7fb..21129ac 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -34,8 +34,6 @@ class Event_Sources { \add_action( 'event_bridge_for_activitypub_unfollow', array( self::class, 'activitypub_unfollow_actor' ), 10, 2 ); } - - /** * Register the post type used to store the external event sources (i.e., followed ActivityPub actors). */ @@ -159,9 +157,22 @@ class Event_Sources { self::queue_follow_actor( $actor ); + self::delete_event_source_transients(); + return $event_source; } + /** + * Delete all transients related to the event sources. + * + * @return void + */ + public static function delete_event_source_transients(): void { + delete_transient( 'event_bridge_for_activitypub_event_sources' ); + delete_transient( 'event_bridge_for_activitypub_event_sources_hosts' ); + delete_transient( 'event_bridge_for_activitypub_event_sources_ids' ); + } + /** * Remove an Event Source (=Followed ActivityPub actor). * @@ -171,39 +182,68 @@ class Event_Sources { */ public static function remove_event_source( $actor ) { $actor = true; + self::delete_event_source_transients(); return $actor; } + /** + * Get an array will all unique hosts of all Event-Sources. + * + * @return array The Term list of Event Sources. + */ + public static function get_event_sources_hosts() { + $hosts = get_transient( 'event_bridge_for_activitypub_event_sources_hosts' ); + + if ( $hosts ) { + return $hosts; + } + + $actors = self::get_event_sources_with_count()['actors']; + + $hosts = array(); + foreach ( $actors as $actor ) { + $url = wp_parse_url( $actor->get_id() ); + if ( isset( $url['host'] ) ) { + $hosts[] = $url['host']; + } + } + + set_transient( 'event_bridge_for_activitypub_event_sources_hosts', $hosts ); + + return array_unique( $hosts ); + } + + /** + * Get add Event Sources ActivityPub IDs. + * + * @return array The Term list of Event Sources. + */ + public static function get_event_sources_ids() { + $ids = get_transient( 'event_bridge_for_activitypub_event_sources_ids' ); + + if ( $ids ) { + return $ids; + } + + $actors = self::get_event_sources_with_count()['actors']; + + $ids = array(); + foreach ( $actors as $actor ) { + $ids[] = $actor->get_id(); + } + + set_transient( 'event_bridge_for_activitypub_event_sources_ids', $ids ); + + return $ids; + } + /** * Get all Event-Sources. * * @return array The Term list of Event Sources. */ public static function get_event_sources() { - $args = array( - 'post_type' => self::POST_TYPE, - 'posts_per_page' => -1, - 'orderby' => 'ID', - 'order' => 'DESC', - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - 'meta_query' => array( - array( - 'key' => 'activitypub_actor_id', - 'compare' => 'EXISTS', - ), - ), - ); - - $query = new WP_Query( $args ); - - $event_sources = array_map( - function ( $post ) { - return Event_Source::init_from_cpt( $post ); - }, - $query->get_posts() - ); - - return $event_sources; + return self::get_event_sources_with_count()['actors']; } /** @@ -221,6 +261,12 @@ class Event_Sources { * } */ public static function get_event_sources_with_count( $number = -1, $page = null, $args = array() ) { + $event_sources = get_transient( 'event_bridge_for_activitypub_event_sources' ); + + if ( $event_sources ) { + return $event_sources; + } + $defaults = array( 'post_type' => self::POST_TYPE, 'posts_per_page' => $number, @@ -239,7 +285,11 @@ class Event_Sources { $query->get_posts() ); - return compact( 'actors', 'total' ); + $event_sources = compact( 'actors', 'total' ); + + set_transient( 'event_bridge_for_activitypub_event_sources', $event_sources ); + + return $event_sources; } /** @@ -389,4 +439,15 @@ class Event_Sources { $activity = $activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, \Activitypub\Collection\Actors::APPLICATION_USER_ID ); } + + /** + * Add Event Sources hosts to allowed hosts used by safe redirect. + * + * @param array $hosts The hosts before the filter. + * @return array + */ + public static function add_event_sources_hosts_to_allowed_redirect_hosts( $hosts ) { + $event_sources_hosts = self::get_event_sources_hosts(); + return array_merge( $hosts, $event_sources_hosts ); + } } diff --git a/includes/activitypub/handler/class-announce.php b/includes/activitypub/handler/class-announce.php deleted file mode 100644 index f79b3e7..0000000 --- a/includes/activitypub/handler/class-announce.php +++ /dev/null @@ -1,49 +0,0 @@ -get__id() ) { - return; - } - } -} diff --git a/includes/activitypub/handler/class-create.php b/includes/activitypub/handler/class-create.php index 0366259..c8e1713 100644 --- a/includes/activitypub/handler/class-create.php +++ b/includes/activitypub/handler/class-create.php @@ -8,6 +8,7 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; use Activitypub\Collection\Actors; +use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources; use Event_Bridge_For_ActivityPub\Setup; use function Activitypub\is_activity_public; @@ -46,6 +47,10 @@ class Create { return; } + if ( ! self::actor_is_event_source( $activity['actor'] ) ) { + return; + } + // Check if Activity is public or not. if ( ! is_activity_public( $activity ) ) { return; @@ -88,10 +93,11 @@ class Create { return false; } - if ( - 'Create' !== $json_params['type'] || 'Update' !== $json_params['type'] || - is_wp_error( $request ) - ) { + if ( empty( $json_params['actor'] ) ) { + return false; + } + + if ( ! in_array( $json_params['type'], array( 'Create', 'Update', 'Delete', 'Announce' ), true ) || is_wp_error( $request ) ) { return $valid; } @@ -103,11 +109,10 @@ class Create { $required = array( 'id', + 'startTime', + 'name', ); - // Limit this as a safety measure. - add_filter( 'wp_revisions_to_keep', array( 'revisions_to_keep' ) ); - if ( array_intersect( $required, array_keys( $object ) ) !== $required ) { return false; } @@ -116,11 +121,18 @@ class Create { } /** - * Return the number of revisions to keep. + * Check if an ActivityPub actor is an event source. * - * @return int The number of revisions to keep. + * @param string $actor_id The actor ID. + * @return bool */ - public static function revisions_to_keep() { - return 3; + public static function actor_is_event_source( $actor_id ) { + $event_sources = Event_Sources::get_event_sources(); + foreach ( $event_sources as $event_source ) { + if ( $actor_id === $event_source->get_id() ) { + return true; + } + } + return false; } } diff --git a/includes/activitypub/handler/class-delete.php b/includes/activitypub/handler/class-delete.php index ff4cede..ec4cd17 100644 --- a/includes/activitypub/handler/class-delete.php +++ b/includes/activitypub/handler/class-delete.php @@ -7,8 +7,8 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; -use Activitypub\Notification; use Activitypub\Collection\Actors; +use Event_Bridge_For_ActivityPub\Setup; /** * Handle Delete requests. @@ -20,30 +20,40 @@ class Delete { public static function init() { \add_action( 'activitypub_inbox_delete', - array( self::class, 'handle_delete' ) + array( self::class, 'handle_delete' ), + 15, + 2 ); } /** * Handle "Follow" requests. * - * @param array $activity The activity object. + * @param array $activity The activity-object. + * @param int $user_id The id of the local blog-user. */ - public static function handle_delete( $activity ) { - if ( ! isset( $activity['object'] ) ) { + public static function handle_delete( $activity, $user_id ) { + // We only process activities that are target to the application user. + if ( Actors::APPLICATION_USER_ID !== $user_id ) { return; } - $object = Actors::get_by_resource( $activity['object'] ); - - if ( ! $object || is_wp_error( $object ) ) { - // If we can not find a actor, we handle the `Delete` activity. + if ( ! Create::actor_is_event_source( $activity['actor'] ) ) { return; } - // We only expect `Delete` activities being answers to follow requests by the application actor. - if ( Actors::APPLICATION_USER_ID !== $object->get__id() ) { + // Check if an object is set. + if ( ! isset( $activity['object']['type'] ) || 'Event' !== $activity['object']['type'] ) { return; } + + $transmogrifier_class = Setup::get_transmogrifier(); + + if ( ! $transmogrifier_class ) { + return; + } + + $transmogrifier = new $transmogrifier_class( $activity['object'] ); + $transmogrifier->delete(); } } diff --git a/includes/activitypub/model/class-event-source.php b/includes/activitypub/model/class-event-source.php index b64da62..4ed45b8 100644 --- a/includes/activitypub/model/class-event-source.php +++ b/includes/activitypub/model/class-event-source.php @@ -12,6 +12,8 @@ use Activitypub\Activity\Actor; use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources; use WP_Error; +use function Activitypub\sanitize_url; + /** * Event-Source (=ActivityPub Actor that is followed) model. */ @@ -122,7 +124,7 @@ class Event_Source extends Actor { */ protected function get_post_meta_input() { $meta_input = array(); - $meta_input['activitypub_inbox'] = $this->get_shared_inbox(); + $meta_input['activitypub_inbox'] = sanitize_url( $this->get_shared_inbox() ); $meta_input['activitypub_actor_json'] = $this->to_json(); return $meta_input; @@ -150,7 +152,7 @@ class Event_Source extends Actor { */ public function save() { if ( ! $this->is_valid() ) { - return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) ); + return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'event-bridge-for-activitypub' ), array( 'status' => 400 ) ); } if ( ! $this->get__id() ) { @@ -215,7 +217,7 @@ class Event_Source extends Actor { $icon = $this->get_icon(); if ( isset( $icon['url'] ) ) { - $image = media_sideload_image( $icon['url'], $post_id, null, 'id' ); + $image = media_sideload_image( sanitize_url( $icon['url'] ), $post_id, null, 'id' ); } if ( isset( $image ) && ! is_wp_error( $image ) ) { set_post_thumbnail( $post_id, $image ); diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index 1255acd..aadf2a7 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -13,6 +13,10 @@ namespace Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier; use Activitypub\Activity\Extended_Object\Event; use Activitypub\Activity\Extended_Object\Place; +use DateTime; + +use function Activitypub\object_to_uri; +use function Activitypub\sanitize_url; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore @@ -57,8 +61,222 @@ class GatherPress { */ private function get_post_id_from_activitypub_id() { global $wpdb; - $id = $this->activitypub_event->get_id(); - return $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE guid=%s AND post_type=%s", $id, 'gatherpress_event' ) ); + return $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s", + esc_sql( $this->activitypub_event->get_id() ) + ) + ); + } + + /** + * Get the image URL and alt-text of an ActivityPub object. + * + * @param array $data The ActivityPub object as ann associative array. + * @return ?array Array containing the images URL and alt-text. + */ + private static function extract_image_alt_and_url( $data ) { + $image = array( + 'url' => null, + 'alt' => null, + ); + + // Check whether it is already simple. + if ( ! $data || is_string( $data ) ) { + $image['url'] = $data; + return $image; + } + + if ( ! isset( $data['type'] ) ) { + return $image; + } + + if ( ! in_array( $data['type'], array( 'Document', 'Image' ), true ) ) { + return $image; + } + + if ( isset( $data['url'] ) ) { + $image['url'] = $data['url']; + } elseif ( isset( $data['id'] ) ) { + $image['id'] = $data['id']; + } + + if ( isset( $data['name'] ) ) { + $image['alt'] = $data['name']; + } + + return $image; + } + + /** + * Returns the URL of the featured image. + * + * @return array + */ + private function get_featured_image() { + $event = $this->activitypub_event; + $image = $event->get_image(); + if ( $image ) { + return self::extract_image_alt_and_url( $image ); + } + $attachment = $event->get_attachment(); + if ( is_array( $attachment ) && ! empty( $attachment ) ) { + $supported_types = array( 'Image', 'Document' ); + $match = null; + + foreach ( $attachment as $item ) { + if ( in_array( $item['type'], $supported_types, true ) ) { + $match = $item; + break; + } + } + $attachment = $match; + } + return self::extract_image_alt_and_url( $attachment ); + } + + /** + * Given an image URL return an attachment ID. Image will be side-loaded into the media library if it doesn't exist. + * + * Forked from https://gist.github.com/kingkool68/a66d2df7835a8869625282faa78b489a. + * + * @param int $post_id The post ID where the image will be set as featured image. + * @param string $url The image URL to maybe sideload. + * @uses media_sideload_image + * @return string|int|WP_Error + */ + public static function maybe_sideload_image( $post_id, $url = '' ) { + global $wpdb; + + // Include necessary WordPress file for media handling. + if ( ! function_exists( 'media_sideload_image' ) ) { + require_once ABSPATH . 'wp-admin/includes/media.php'; + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/image.php'; + } + + // Check to see if the URL has already been fetched, if so return the attachment ID. + $attachment_id = $wpdb->get_var( + $wpdb->prepare( "SELECT `post_id` FROM {$wpdb->postmeta} WHERE `meta_key` = '_source_url' AND `meta_value` = %s", sanitize_url( $url ) ) + ); + if ( ! empty( $attachment_id ) ) { + return $attachment_id; + } + + $attachment_id = $wpdb->get_var( + $wpdb->prepare( "SELECT `ID` FROM {$wpdb->posts} WHERE guid=%s", $url ) + ); + if ( ! empty( $attachment_id ) ) { + return $attachment_id; + } + + // If the URL doesn't exist, sideload it to the media library. + return media_sideload_image( sanitize_url( $url ), $post_id, sanitize_url( $url ), 'id' ); + } + + + /** + * Sideload an image_url set it as featured image and add the alt-text. + * + * @param int $post_id The post ID where the image will be set as featured image. + * @param string $image_url The image URL. + * @param string $alt_text The alt-text of the image. + * @return int The attachment ID + */ + private static function set_featured_image_with_alt( $post_id, $image_url, $alt_text = '' ) { + // Maybe sideload the image or get the Attachment ID of an existing one. + $image_id = self::maybe_sideload_image( $post_id, $image_url ); + + if ( is_wp_error( $image_id ) ) { + // Handle the error. + return $image_id; + } + + // Set the image as the featured image for the post. + set_post_thumbnail( $post_id, $image_id ); + + // Update the alt text. + if ( ! empty( $alt_text ) ) { + update_post_meta( $image_id, '_wp_attachment_image_alt', sanitize_text_field( $alt_text ) ); + } + + return $image_id; // Return the attachment ID for further use if needed. + } + + /** + * Add tags to post. + * + * @param int $post_id The post ID. + */ + private function add_tags_to_post( $post_id ) { + $tags_array = $this->activitypub_event->get_tag(); + + // Ensure the input is valid. + if ( empty( $tags_array ) || ! is_array( $tags_array ) || ! $post_id ) { + return false; + } + + // Extract and process tag names. + $tag_names = array(); + foreach ( $tags_array as $tag ) { + if ( isset( $tag['name'] ) && 'Hashtag' === $tag['type'] ) { + $tag_names[] = ltrim( $tag['name'], '#' ); // Remove the '#' from the name. + } + } + + // Add the tags as terms to the post. + if ( ! empty( $tag_names ) ) { + wp_set_object_terms( $post_id, $tag_names, 'gatherpress_topic', true ); // 'true' appends to existing terms. + } + + return true; + } + + /** + * Add venue. + * + * @param int $post_id The post ID. + */ + private function add_venue( $post_id ) { + $location = $this->activitypub_event->get_location(); + + if ( ! $location ) { + return; + } + + if ( ! isset( $location['name'] ) ) { + return; + } + + $venue_instance = \GatherPress\Core\Venue::get_instance(); + $venue_name = sanitize_title( $location['name'] ); + $venue_slug = $venue_instance->get_venue_term_slug( $venue_name ); + $venue_post = $venue_instance->get_venue_post_from_term_slug( $venue_slug ); + + if ( ! $venue_post ) { + $venue_id = wp_insert_post( + array( + 'post_title' => sanitize_text_field( $location['name'] ), + 'post_type' => 'gatherpress_venue', + 'post_status' => 'publish', + ) + ); + } else { + $venue_id = $venue_post->ID; + } + + $venue_information = array(); + + $venue_information['fullAddress'] = $location['address'] ?? ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $venue_information['phone_number'] = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $venue_information['website'] = ''; + $venue_information['permalink'] = ''; + + $venue_json = wp_json_encode( $venue_information ); + + update_post_meta( $venue_id, 'gatherpress_venue_information', $venue_json ); + + wp_set_object_terms( $post_id, $venue_slug, '_gatherpress_venue', true ); // 'true' appends to existing terms. } /** @@ -68,12 +286,12 @@ class GatherPress { // Insert new GatherPress Event post. $post_id = wp_insert_post( array( - 'post_title' => $this->activitypub_event->get_name(), + 'post_title' => sanitize_text_field( $this->activitypub_event->get_name() ), 'post_type' => 'gatherpress_event', - 'post_content' => $this->activitypub_event->get_content(), - 'post_excerpt' => $this->activitypub_event->get_summary(), + 'post_content' => wp_kses_post( $this->activitypub_event->get_content() ) . '', + 'post_excerpt' => wp_kses_post( $this->activitypub_event->get_summary() ), 'post_status' => 'publish', - 'guid' => $this->activitypub_event->get_id(), + 'guid' => sanitize_url( $this->activitypub_event->get_id() ), ) ); @@ -81,12 +299,31 @@ class GatherPress { return; } - $event = new \GatherPress\Core\Event( $post_id ); + // Insert the Dates. + $event = new GatherPress_Event( $post_id ); + $start_time = $this->activitypub_event->get_start_time(); + $end_time = $this->activitypub_event->get_end_time(); + if ( ! $end_time ) { + $end_time = new DateTime( $start_time ); + $end_time->modify( '+1 hour' ); + $end_time = $end_time->format( 'Y-m-d H:i:s' ); + } $params = array( - 'datetime_start' => $this->activitypub_event->get_start_time(), - 'datetime_end' => $this->activitypub_event->get_end_time(), + 'datetime_start' => $start_time, + 'datetime_end' => $end_time, 'timezone' => $this->activitypub_event->get_timezone(), ); + + // Insert featured image. + $image = $this->get_featured_image(); + self::set_featured_image_with_alt( $post_id, $image['url'], $image['alt'] ); + + // Add hashtags as terms. + $this->add_tags_to_post( $post_id ); + + $this->add_venue( $post_id ); + + // Sanitization of the params is done in the save_datetimes function just in time. $event->save_datetimes( $params ); } @@ -94,18 +331,21 @@ class GatherPress { * Save the ActivityPub event object as GatherPress Event. */ public function update() { + // Limit this as a safety measure. + add_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) ); + $post_id = $this->get_post_id_from_activitypub_id(); // Insert new GatherPress Event post. $post_id = wp_update_post( array( 'ID' => $post_id, - 'post_title' => $this->activitypub_event->get_name(), + 'post_title' => sanitize_text_field( $this->activitypub_event->get_name() ), 'post_type' => 'gatherpress_event', - 'post_content' => $this->activitypub_event->get_content(), - 'post_excerpt' => $this->activitypub_event->get_summary(), + 'post_content' => wp_kses_post( $this->activitypub_event->get_content() ) . '', + 'post_excerpt' => wp_kses_post( $this->activitypub_event->get_summary() ), 'post_status' => 'publish', - 'guid' => $this->activitypub_event->get_id(), + 'guid' => sanitize_url( $this->activitypub_event->get_id() ), ) ); @@ -113,12 +353,57 @@ class GatherPress { return; } - $event = new \GatherPress\Core\Event( $post_id ); + // Insert the dates. + $event = new GatherPress_Event( $post_id ); + $start_time = $this->activitypub_event->get_start_time(); + $end_time = $this->activitypub_event->get_end_time(); + if ( ! $end_time ) { + $end_time = new DateTime( $start_time ); + $end_time->modify( '+1 hour' ); + $end_time = $end_time->format( 'Y-m-d H:i:s' ); + } $params = array( - 'datetime_start' => $this->activitypub_event->get_start_time(), - 'datetime_end' => $this->activitypub_event->get_end_time(), + 'datetime_start' => $start_time, + 'datetime_end' => $end_time, 'timezone' => $this->activitypub_event->get_timezone(), ); + // Sanitization of the params is done in the save_datetimes function just in time. $event->save_datetimes( $params ); + + // Insert featured image. + $image = $this->get_featured_image(); + self::set_featured_image_with_alt( $post_id, $image['url'], $image['alt'] ); + + // Add hashtags. + $this->add_tags_to_post( $post_id ); + + $this->add_venue( $post_id ); + + // Limit this as a safety measure. + remove_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) ); + } + + /** + * Save the ActivityPub event object as GatherPress event. + */ + public function delete() { + $post_id = $this->get_post_id_from_activitypub_id(); + + $thumbnail_id = get_post_thumbnail_id( $post_id ); + + if ( $thumbnail_id ) { + wp_delete_attachment( $thumbnail_id, true ); + } + + wp_delete_post( $post_id, true ); + } + + /** + * Return the number of revisions to keep. + * + * @return int The number of revisions to keep. + */ + public static function revisions_to_keep() { + return 5; } } diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index fe57ad9..760c81a 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -11,6 +11,7 @@ namespace Event_Bridge_For_ActivityPub; use Activitypub\Activity\Extended_Object\Event; use Activitypub\Collection\Actors; +use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Sources_Collection; use function Activitypub\get_remote_metadata_by_actor; use function Activitypub\is_activitypub_request; @@ -21,38 +22,6 @@ use function Activitypub\is_activitypub_request; * @package Event_Bridge_For_ActivityPub */ class Event_Sources { - /** - * The custom post type. - */ - const POST_TYPE = 'event_bridge_follow'; - - /** - * Constructor. - */ - public function __construct() { - \add_action( 'activitypub_inbox', array( $this, 'handle_activitypub_inbox' ), 15, 3 ); - } - - /** - * Handle the ActivityPub Inbox. - * - * @param array $data The raw post data JSON object as an associative array. - * @param int $user_id The target user id. - * @param string $type The activity type. - */ - public static function handle_activitypub_inbox( $data, $user_id, $type ) { - if ( Actors::APPLICATION_USER_ID !== $user_id ) { - return; - } - - if ( ! in_array( $type, array( 'Create', 'Delete', 'Announce', 'Undo Announce' ), true ) ) { - return; - } - - $event = Event::init_from_array( $data ); - return $event; - } - /** * Get metadata of ActivityPub Actor by ID/URL. * @@ -71,26 +40,6 @@ class Event_Sources { return get_remote_metadata_by_actor( $url ); } - /** - * Respond to the Ajax request to fetch feeds - */ - public function ajax_fetch_events() { - if ( ! isset( $_POST['Event_Bridge_For_ActivityPub'] ) ) { - wp_send_json_error( 'missing-parameters' ); - } - - check_ajax_referer( 'fetch-events-' . sanitize_user( wp_unslash( $_POST['actor'] ) ) ); - - $actor = Actors::get_by_resource( sanitize_user( wp_unslash( $_POST['actor'] ) ) ); - if ( ! $actor ) { - wp_send_json_error( 'unknown-actor' ); - } - - $actor->retrieve(); - - wp_send_json_success(); - } - /** * Get the Application actor via FEP-2677. * @@ -125,6 +74,39 @@ class Event_Sources { return false; } + /** + * Filter that cached external posts are not scheduled via the ActivityPub plugin. + * + * Posts that are actually just external events are treated as cache. They are displayed in + * the frontend HTML view and redirected via ActivityPub request, but we do not own them. + * + * @param bool $disabled If it is disabled already by others (the upstream ActivityPub plugin). + * @param WP_Post $post The WordPress post object. + * @return bool True if the post can be federated via ActivityPub. + */ + public static function is_cached_external_post( $disabled, $post = null ): bool { + if ( $disabled || ! $post ) { + return $disabled; + } + if ( ! str_starts_with( \get_site_url(), $post->guid ) ) { + return true; + } + return false; + } + + /** + * Determine whether a WP post is a cached external event. + * + * @param WP_Post $post The WordPress post object. + * @return bool + */ + public static function is_cached_external_event( $post ): bool { + if ( ! str_starts_with( \get_site_url(), $post->guid ) ) { + return true; + } + return false; + } + /** * Add the ActivityPub template for EventPrime. * @@ -150,8 +132,8 @@ class Event_Sources { return $template; } - if ( ! str_starts_with( \get_site_url(), $post->guid ) ) { - \wp_redirect( $post->guid, 301 ); + if ( self::is_cached_external_event( $post ) ) { + \wp_safe_redirect( $post->guid, 301 ); exit; } diff --git a/includes/class-setup.php b/includes/class-setup.php index 1b5de54..58f5778 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -15,6 +15,7 @@ namespace Event_Bridge_For_ActivityPub; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore +use Activitypub\Activity\Extended_Object\Event; use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Sources_Collection; use Event_Bridge_For_ActivityPub\ActivityPub\Handler; use Event_Bridge_For_ActivityPub\Admin\Event_Plugin_Admin_Notices; @@ -241,7 +242,9 @@ class Setup { if ( get_option( 'event_bridge_for_activitypub_event_sources_active' ) ) { add_action( 'init', array( Event_Sources_Collection::class, 'init' ) ); add_action( 'activitypub_register_handlers', array( Handler::class, 'register_handlers' ) ); - add_action( 'admin_init', array( User_Interface::class, 'init' ) ); + // add_action( 'admin_init', array( User_Interface::class, 'init' ) ); + add_filter( 'allowed_redirect_hosts', array( Event_Sources_Collection::class, 'add_event_sources_hosts_to_allowed_redirect_hosts' ) ); + add_filter( 'activitypub_is_post_disabled', array( Event_Sources::class, 'is_cached_external_post' ), 10, 2 ); } \add_filter( 'template_include', array( \Event_Bridge_For_ActivityPub\Event_Sources::class, 'redirect_activitypub_requests_for_cached_external_events' ), 100 ); diff --git a/templates/event-sources.php b/templates/event-sources.php index 4f541a5..3da2441 100644 --- a/templates/event-sources.php +++ b/templates/event-sources.php @@ -20,12 +20,14 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore ?>
- -

+ +
+

+
-

+