From 64ad7d61ef73fd3549a09d0cc25f5142e17f9f48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Mon, 18 Nov 2024 16:07:09 +0100 Subject: [PATCH 01/33] wip --- .../collection/class-event-sources.php | 164 ++++++++++++ .../activitypub/model/class-event-source.php | 59 +++++ includes/admin/class-event-sources.php | 85 +++++++ includes/admin/class-settings-page.php | 5 + includes/class-event-sources.php | 97 +++++++ includes/class-setup.php | 1 + includes/table/class-event-sources.php | 240 ++++++++++++++++++ templates/admin-header.php | 9 +- templates/event-sources.php | 51 ++++ 9 files changed, 709 insertions(+), 2 deletions(-) create mode 100644 includes/activitypub/collection/class-event-sources.php create mode 100644 includes/activitypub/model/class-event-source.php create mode 100644 includes/admin/class-event-sources.php create mode 100644 includes/class-event-sources.php create mode 100644 includes/table/class-event-sources.php create mode 100644 templates/event-sources.php diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php new file mode 100644 index 0000000..fed94ff --- /dev/null +++ b/includes/activitypub/collection/class-event-sources.php @@ -0,0 +1,164 @@ + array( + 'name' => _x( 'Event Sources', 'post_type plural name', 'activitypub' ), + 'singular_name' => _x( 'Event Source', 'post_type single name', 'activitypub' ), + ), + 'public' => false, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => true, + 'supports' => array(), + ) + ); + + \register_post_meta( + self::POST_TYPE, + 'activitypub_inbox', + array( + 'type' => 'string', + 'single' => true, + 'sanitize_callback' => 'sanitize_url', + ) + ); + + \register_post_meta( + self::POST_TYPE, + 'activitypub_errors', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => function ( $value ) { + if ( ! is_string( $value ) ) { + throw new Exception( 'Error message is no valid string' ); + } + + return esc_sql( $value ); + }, + ) + ); + + \register_post_meta( + self::POST_TYPE, + 'activitypub_user_id', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => function ( $value ) { + return esc_sql( $value ); + }, + ) + ); + + \register_post_meta( + self::POST_TYPE, + 'activitypub_actor_json', + array( + 'type' => 'string', + 'single' => true, + 'sanitize_callback' => function ( $value ) { + return sanitize_text_field( $value ); + }, + ) + ); + } + + /** + * Add new Event Source. + * + * @param string $actor The Actor ID. + * + * @return Event_Source|WP_Error The Followed (WP_Post array) or an WP_Error. + */ + public static function add_event_source( $actor ) { + $meta = get_remote_metadata_by_actor( $actor ); + + if ( is_tombstone( $meta ) ) { + return $meta; + } + + if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { + return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) ); + } + + $event_source = new Event_Source(); + $event_source->from_array( $meta ); + + $post_id = $event_source->save(); + + if ( is_wp_error( $post_id ) ) { + return $post_id; + } + + return $event_source; + } + + /** + * Remove an Event Source (=Followed ActivityPub actor). + * + * @param string $actor The Actor URL. + * + * @return bool True on success, false on failure. + */ + public static function remove_event_source( $actor ) { + $actor = true; + return $actor; + } + + /** + * Get all Followers. + * + * @return array The Term list of Followers. + */ + public static function get_all_followers() { + $args = array( + 'nopaging' => true, + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => 'activitypub_inbox', + 'compare' => 'EXISTS', + ), + array( + 'key' => 'activitypub_actor_json', + 'compare' => 'EXISTS', + ), + ), + ); + return self::get_followers( null, null, null, $args ); + } +} diff --git a/includes/activitypub/model/class-event-source.php b/includes/activitypub/model/class-event-source.php new file mode 100644 index 0000000..c59113e --- /dev/null +++ b/includes/activitypub/model/class-event-source.php @@ -0,0 +1,59 @@ +get_icon(); + + if ( ! $icon ) { + return ''; + } + + if ( is_array( $icon ) ) { + return $icon['url']; + } + + return $icon; + } + + /** + * Convert a Custom-Post-Type input to an \ActivityPub_Event_Bridge\ActivityPub\Model\Event_Source. + * + * @param \WP_Post $post The post object. + * @return \ActivityPub_Event_Bridge\ActivityPub\Event_Source|WP_Error + */ + public static function init_from_cpt( $post ) { + if ( Event_Sources::POST_TYPE !== $post->post_type ) { + return false; + } + $actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true ); + $object = self::init_from_json( $actor_json ); + $object->set__id( $post->ID ); + $object->set_id( $post->guid ); + $object->set_name( $post->post_title ); + $object->set_summary( $post->post_excerpt ); + $object->set_published( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_date ) ) ); + $object->set_updated( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) ) ); + + return $object; + } +} diff --git a/includes/admin/class-event-sources.php b/includes/admin/class-event-sources.php new file mode 100644 index 0000000..f979dd9 --- /dev/null +++ b/includes/admin/class-event-sources.php @@ -0,0 +1,85 @@ +retrieve(); + + wp_send_json_success(); + } +} diff --git a/includes/admin/class-settings-page.php b/includes/admin/class-settings-page.php index b2dbb3e..c8132b6 100644 --- a/includes/admin/class-settings-page.php +++ b/includes/admin/class-settings-page.php @@ -116,6 +116,11 @@ class Settings_Page { \load_template( ACTIVITYPUB_EVENT_BRIDGE_PLUGIN_DIR . 'templates/settings.php', true, $args ); break; + case 'event-sources': + wp_enqueue_script( 'thickbox' ); + wp_enqueue_style( 'thickbox' ); + \load_template( ACTIVITYPUB_EVENT_BRIDGE_PLUGIN_DIR . 'templates/event-sources.php', true ); + break; case 'welcome': default: wp_enqueue_script( 'plugin-install' ); diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php new file mode 100644 index 0000000..6081a2d --- /dev/null +++ b/includes/class-event-sources.php @@ -0,0 +1,97 @@ + array( + 'name' => _x( 'Event Sources', 'post_type plural name', 'activitypub' ), + 'singular_name' => _x( 'Event Source', 'post_type single name', 'activitypub' ), + ), + 'public' => false, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => true, + 'supports' => array(), + ) + ); + + \register_post_meta( + self::POST_TYPE, + 'activitypub_inbox', + array( + 'type' => 'string', + 'single' => true, + 'sanitize_callback' => 'sanitize_url', + ) + ); + + \register_post_meta( + self::POST_TYPE, + 'activitypub_errors', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => function ( $value ) { + if ( ! is_string( $value ) ) { + throw new Exception( 'Error message is no valid string' ); + } + + return esc_sql( $value ); + }, + ) + ); + + \register_post_meta( + self::POST_TYPE, + 'activitypub_user_id', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => function ( $value ) { + return esc_sql( $value ); + }, + ) + ); + + \register_post_meta( + self::POST_TYPE, + 'activitypub_actor_json', + array( + 'type' => 'string', + 'single' => true, + 'sanitize_callback' => function ( $value ) { + return sanitize_text_field( $value ); + }, + ) + ); + } +} diff --git a/includes/class-setup.php b/includes/class-setup.php index 2912002..e6217a6 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -181,6 +181,7 @@ class Setup { } add_action( 'init', array( Health_Check::class, 'init' ) ); + add_action( 'init', array( Event_Sources::class, 'register_taxonomy' ) ); // Check if the minimum required version of the ActivityPub plugin is installed. if ( ! version_compare( $this->activitypub_plugin_version, ACTIVITYPUB_EVENT_BRIDGE_ACTIVITYPUB_PLUGIN_MIN_VERSION ) ) { diff --git a/includes/table/class-event-sources.php b/includes/table/class-event-sources.php new file mode 100644 index 0000000..32e552a --- /dev/null +++ b/includes/table/class-event-sources.php @@ -0,0 +1,240 @@ + \__( 'Event Source', 'activitypub' ), + 'plural' => \__( 'Event Sources', 'activitypub' ), + 'ajax' => false, + ) + ); + } + + /** + * Get columns. + * + * @return array + */ + public function get_columns() { + return array( + 'cb' => '', + 'avatar' => \__( 'Avatar', 'activitypub' ), + 'post_title' => \__( 'Name', 'activitypub' ), + 'username' => \__( 'Username', 'activitypub' ), + 'url' => \__( 'URL', 'activitypub' ), + 'published' => \__( 'Followed', 'activitypub' ), + 'modified' => \__( 'Last updated', 'activitypub' ), + ); + } + + /** + * Returns sortable columns. + * + * @return array + */ + public function get_sortable_columns() { + return array( + 'post_title' => array( 'post_title', true ), + 'modified' => array( 'modified', false ), + 'published' => array( 'published', false ), + ); + } + + /** + * Prepare items. + */ + public function prepare_items() { + $columns = $this->get_columns(); + $hidden = array(); + + $this->process_action(); + $this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() ); + + $page_num = $this->get_pagenum(); + $per_page = 20; + + $args = array(); + + // phpcs:disable WordPress.Security.NonceVerification.Recommended + if ( isset( $_GET['orderby'] ) ) { + $args['orderby'] = sanitize_text_field( wp_unslash( $_GET['orderby'] ) ); + } + + if ( isset( $_GET['order'] ) ) { + $args['order'] = sanitize_text_field( wp_unslash( $_GET['order'] ) ); + } + + if ( isset( $_GET['s'] ) && isset( $_REQUEST['_wpnonce'] ) ) { + $nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ); + if ( wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) { + $args['s'] = sanitize_text_field( wp_unslash( $_GET['s'] ) ); + } + } + // phpcs:enable WordPress.Security.NonceVerification.Recommended + + $dummy_event_sources = array( + 'total' => 1, + 'actors' => array( + Event_Source::init_from_array( + array( + 'id' => 'https://graz.social/@linos', + 'url' => 'https://graz.social/@linos', + 'preferredUsername' => 'linos', + 'name' => 'André Menrath', + 'icon' => 'https://graz.social/system/accounts/avatars/000/000/001/original/fe1c795256720361.jpeg', + ) + ), + ), + ); + + $event_sources = $dummy_event_sources; + $actors = $event_sources['actors']; + $counter = $event_sources['total']; + + $this->items = array(); + $this->set_pagination_args( + array( + 'total_items' => $counter, + 'total_pages' => ceil( $counter / $per_page ), + 'per_page' => $per_page, + ) + ); + + foreach ( $actors as $actor ) { + $item = array( + 'icon' => esc_attr( $actor->get_icon_url() ), + 'post_title' => esc_attr( $actor->get_name() ), + 'username' => esc_attr( $actor->get_preferred_username() ), + 'url' => esc_attr( object_to_uri( $actor->get_url() ) ), + 'identifier' => esc_attr( $actor->get_id() ), + 'published' => esc_attr( $actor->get_published() ), + 'modified' => esc_attr( $actor->get_updated() ), + ); + + $this->items[] = $item; + } + } + + /** + * Returns bulk actions. + * + * @return array + */ + public function get_bulk_actions() { + return array( + 'delete' => __( 'Delete', 'activitypub' ), + ); + } + + /** + * Column default. + * + * @param array $item Item. + * @param string $column_name Column name. + * @return string + */ + public function column_default( $item, $column_name ) { + if ( ! array_key_exists( $column_name, $item ) ) { + return __( 'None', 'activitypub' ); + } + return $item[ $column_name ]; + } + + /** + * Column avatar. + * + * @param array $item Item. + * @return string + */ + public function column_avatar( $item ) { + return sprintf( + '', + $item['icon'] + ); + } + + /** + * Column url. + * + * @param array $item Item. + * @return string + */ + public function column_url( $item ) { + return sprintf( + '%s', + esc_url( $item['url'] ), + $item['url'] + ); + } + + /** + * Column cb. + * + * @param array $item Item. + * @return string + */ + public function column_cb( $item ) { + return sprintf( '', esc_attr( $item['identifier'] ) ); + } + + /** + * Process action. + */ + public function process_action() { + if ( ! isset( $_REQUEST['followers'] ) || ! isset( $_REQUEST['_wpnonce'] ) ) { + return; + } + $nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ); + if ( ! wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) { + return; + } + + if ( ! current_user_can( 'edit_user', $this->user_id ) ) { + return; + } + + $followers = $_REQUEST['followers']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput + + if ( $this->current_action() === 'delete' ) { + if ( ! is_array( $followers ) ) { + $followers = array( $followers ); + } + foreach ( $followers as $follower ) { + FollowerCollection::remove_follower( $this->user_id, $follower ); + } + } + } + + /** + * Returns user count. + * + * @return int + */ + public function get_user_count() { + return FollowerCollection::count_followers( $this->user_id ); + } +} diff --git a/templates/admin-header.php b/templates/admin-header.php index efc02bc..855f055 100644 --- a/templates/admin-header.php +++ b/templates/admin-header.php @@ -9,8 +9,9 @@ $args = wp_parse_args( $args, array( - 'welcome' => '', - 'settings' => '', + 'welcome' => '', + 'settings' => '', + 'event-sources' => '', ) ); ?> @@ -28,6 +29,10 @@ $args = wp_parse_args( + + + +
diff --git a/templates/event-sources.php b/templates/event-sources.php new file mode 100644 index 0000000..2e46916 --- /dev/null +++ b/templates/event-sources.php @@ -0,0 +1,51 @@ + 'active', + ) +); + + +$table = new \ActivityPub_Event_Bridge\Table\Event_Sources(); +?> + +
+ +
+

+

+ + + + + + + + +
+ +
+
+ + + prepare_items(); + $table->search_box( 'Search', 'search' ); + $table->display(); + ?> +
+
-- 2.39.5 From d575f6ef70dddff9402b90fe9e639377d1e27b62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Thu, 5 Dec 2024 17:50:17 +0100 Subject: [PATCH 02/33] wip --- ...=> event-bridge-for-activitypub-admin.css} | 0 ... => event-bridge-for-activitypub-admin.js} | 0 .../collection/class-event-sources.php | 8 ++++---- .../activitypub/model/class-event-source.php | 10 +++++----- includes/admin/class-event-sources.php | 6 +++--- includes/admin/class-health-check.php | 2 +- includes/admin/class-settings-page.php | 2 +- includes/class-event-sources.php | 8 ++++---- includes/class-setup.php | 2 +- includes/table/class-event-sources.php | 6 +++--- templates/admin-header.php | 4 ++-- templates/event-sources.php | 20 +++++++++---------- 12 files changed, 34 insertions(+), 34 deletions(-) rename assets/css/{activitypub-event-bridge-admin.css => event-bridge-for-activitypub-admin.css} (100%) rename assets/js/{activitypub-event-bridge-admin.js => event-bridge-for-activitypub-admin.js} (100%) diff --git a/assets/css/activitypub-event-bridge-admin.css b/assets/css/event-bridge-for-activitypub-admin.css similarity index 100% rename from assets/css/activitypub-event-bridge-admin.css rename to assets/css/event-bridge-for-activitypub-admin.css diff --git a/assets/js/activitypub-event-bridge-admin.js b/assets/js/event-bridge-for-activitypub-admin.js similarity index 100% rename from assets/js/activitypub-event-bridge-admin.js rename to assets/js/event-bridge-for-activitypub-admin.js diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index fed94ff..c72453d 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -2,15 +2,15 @@ /** * Event sources collection file. * - * @package ActivityPub_Event_Bridge + * @package Event_Bridge_For_ActivityPub * @license AGPL-3.0-or-later */ -namespace ActivityPub_Event_Bridge\ActivityPub\Collection; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Collection; use WP_Error; use WP_Query; -use ActivityPub_Event_Bridge\ActivityPub\Event_Source; +use Event_Bridge_For_ActivityPub\ActivityPub\Event_Source; use function Activitypub\is_tombstone; use function Activitypub\get_remote_metadata_by_actor; @@ -22,7 +22,7 @@ class Event_Sources { /** * The custom post type. */ - const POST_TYPE = 'activitypub_event_bridge_follow'; + const POST_TYPE = 'Event_Bridge_For_ActivityPub_follow'; /** * Register the post type used to store the external event sources (i.e., followed ActivityPub actors). diff --git a/includes/activitypub/model/class-event-source.php b/includes/activitypub/model/class-event-source.php index c59113e..641d171 100644 --- a/includes/activitypub/model/class-event-source.php +++ b/includes/activitypub/model/class-event-source.php @@ -2,14 +2,14 @@ /** * Event-Source (=ActivityPub Actor that is followed) model. * - * @package ActivityPub_Event_Bridge + * @package Event_Bridge_For_ActivityPub * @license AGPL-3.0-or-later */ -namespace ActivityPub_Event_Bridge\ActivityPub; +namespace Event_Bridge_For_ActivityPub\ActivityPub; use Activitypub\Activity\Actor; -use ActivityPub_Event_Bridge\ActivityPub\Collection\Event_Sources; +use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources; use WP_Error; /** @@ -36,10 +36,10 @@ class Event_Source extends Actor { } /** - * Convert a Custom-Post-Type input to an \ActivityPub_Event_Bridge\ActivityPub\Model\Event_Source. + * Convert a Custom-Post-Type input to an \Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source. * * @param \WP_Post $post The post object. - * @return \ActivityPub_Event_Bridge\ActivityPub\Event_Source|WP_Error + * @return \Event_Bridge_For_ActivityPub\ActivityPub\Event_Source|WP_Error */ public static function init_from_cpt( $post ) { if ( Event_Sources::POST_TYPE !== $post->post_type ) { diff --git a/includes/admin/class-event-sources.php b/includes/admin/class-event-sources.php index f979dd9..bf14c3a 100644 --- a/includes/admin/class-event-sources.php +++ b/includes/admin/class-event-sources.php @@ -2,10 +2,10 @@ /** * Event Sources. * - * @package Activitypub_Event_Bridge + * @package Event_Bridge_For_ActivityPub */ -namespace ActivityPub_Event_Bridge\Admin; +namespace Event_Bridge_For_ActivityPub\Admin; use Activitypub\Collection\Actors; use Activitypub\Activity\Extended_Object\Event; @@ -67,7 +67,7 @@ class Event_Sources { * Respond to the Ajax request to fetch feeds */ public function ajax_fetch_events() { - if ( ! isset( $_POST['activitypub_event_bridge'] ) ) { + if ( ! isset( $_POST['Event_Bridge_For_ActivityPub'] ) ) { wp_send_json_error( 'missing-parameters' ); } diff --git a/includes/admin/class-health-check.php b/includes/admin/class-health-check.php index 61faa18..bfdb340 100644 --- a/includes/admin/class-health-check.php +++ b/includes/admin/class-health-check.php @@ -2,7 +2,7 @@ /** * Health_Check class. * - * @package Activitypub_Event_Bridge + * @package Event_Bridge_For_ActivityPub */ namespace Event_Bridge_For_ActivityPub\Admin; diff --git a/includes/admin/class-settings-page.php b/includes/admin/class-settings-page.php index b4b21a5..f53d379 100644 --- a/includes/admin/class-settings-page.php +++ b/includes/admin/class-settings-page.php @@ -119,7 +119,7 @@ class Settings_Page { case 'event-sources': wp_enqueue_script( 'thickbox' ); wp_enqueue_style( 'thickbox' ); - \load_template( ACTIVITYPUB_EVENT_BRIDGE_PLUGIN_DIR . 'templates/event-sources.php', true ); + \load_template( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_DIR . 'templates/event-sources.php', true ); break; case 'welcome': default: diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index 6081a2d..6b5c3b6 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -2,10 +2,10 @@ /** * Class for handling and saving the ActivityPub event sources (i.e. follows). * - * @package ActivityPub_Event_Bridge + * @package Event_Bridge_For_ActivityPub */ -namespace ActivityPub_Event_Bridge; +namespace Event_Bridge_For_ActivityPub; use Activitypub\Http; use Exception; @@ -15,13 +15,13 @@ use function register_post_type; /** * Class for handling and saving the ActivityPub event sources (i.e. follows). * - * @package ActivityPub_Event_Bridge + * @package Event_Bridge_For_ActivityPub */ class Event_Sources { /** * The custom post type. */ - const POST_TYPE = 'activitypub_event_bridge_follow'; + const POST_TYPE = 'event_bridge_follow'; /** * Register the post type used to store the external event sources (i.e., followed ActivityPub actors). diff --git a/includes/class-setup.php b/includes/class-setup.php index 272212d..841a65c 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -183,7 +183,7 @@ class Setup { } add_action( 'init', array( Health_Check::class, 'init' ) ); - add_action( 'init', array( Event_Sources::class, 'register_taxonomy' ) ); + add_action( 'init', array( Event_Sources::class, 'register_post_type' ) ); // Check if the minimum required version of the ActivityPub plugin is installed. if ( ! version_compare( $this->activitypub_plugin_version, EVENT_BRIDGE_FOR_ACTIVITYPUB_ACTIVITYPUB_PLUGIN_MIN_VERSION ) ) { diff --git a/includes/table/class-event-sources.php b/includes/table/class-event-sources.php index 32e552a..32210c7 100644 --- a/includes/table/class-event-sources.php +++ b/includes/table/class-event-sources.php @@ -2,14 +2,14 @@ /** * Event Sources Table-Class file. * - * @package ActivityPub_Event_Bridge + * @package Event_Bridge_For_ActivityPub */ -namespace ActivityPub_Event_Bridge\Table; +namespace Event_Bridge_For_ActivityPub\Table; use WP_List_Table; use Activitypub\Collection\Followers as FollowerCollection; -use ActivityPub_Event_Bridge\ActivityPub\Event_Source; +use Event_Bridge_For_ActivityPub\ActivityPub\Event_Source; use function Activitypub\object_to_uri; diff --git a/templates/admin-header.php b/templates/admin-header.php index 41980b3..7456149 100644 --- a/templates/admin-header.php +++ b/templates/admin-header.php @@ -33,8 +33,8 @@ $args = wp_parse_args( - - + +
diff --git a/templates/event-sources.php b/templates/event-sources.php index 2e46916..72e2fdc 100644 --- a/templates/event-sources.php +++ b/templates/event-sources.php @@ -2,7 +2,7 @@ /** * Event Sources management page for the ActivityPub Event Bridge. * - * @package ActivityPub_Event_Bridge + * @package Event_Bridge_For_ActivityPub */ // Exit if accessed directly. @@ -17,24 +17,24 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore ); -$table = new \ActivityPub_Event_Bridge\Table\Event_Sources(); +$table = new \Event_Bridge_For_ActivityPub\Table\Event_Sources(); ?> -
+
-

-

+

+

- - + + - -- 2.39.5 From a5a062bb17f6ec98ea3beb82ec8c104777c3e0ca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sat, 7 Dec 2024 20:07:47 +0100 Subject: [PATCH 03/33] fix namespaces --- includes/activitypub/model/class-event-source.php | 2 +- includes/table/class-event-sources.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/includes/activitypub/model/class-event-source.php b/includes/activitypub/model/class-event-source.php index 641d171..c063090 100644 --- a/includes/activitypub/model/class-event-source.php +++ b/includes/activitypub/model/class-event-source.php @@ -6,7 +6,7 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\ActivityPub; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Model; use Activitypub\Activity\Actor; use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources; diff --git a/includes/table/class-event-sources.php b/includes/table/class-event-sources.php index 32210c7..7575d8a 100644 --- a/includes/table/class-event-sources.php +++ b/includes/table/class-event-sources.php @@ -9,7 +9,7 @@ namespace Event_Bridge_For_ActivityPub\Table; use WP_List_Table; use Activitypub\Collection\Followers as FollowerCollection; -use Event_Bridge_For_ActivityPub\ActivityPub\Event_Source; +use Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source; use function Activitypub\object_to_uri; -- 2.39.5 From e8b2b4c899d41ca4ac78365b01fd8aafd462ccb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sat, 7 Dec 2024 20:20:17 +0100 Subject: [PATCH 04/33] fix more namespaces --- .../collection/class-event-sources.php | 16 +++++++------- includes/table/class-event-sources.php | 22 +++++++++---------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index c72453d..2d29b03 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -10,7 +10,7 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Collection; use WP_Error; use WP_Query; -use Event_Bridge_For_ActivityPub\ActivityPub\Event_Source; +use Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source; use function Activitypub\is_tombstone; use function Activitypub\get_remote_metadata_by_actor; @@ -32,8 +32,8 @@ class Event_Sources { self::POST_TYPE, array( 'labels' => array( - 'name' => _x( 'Event Sources', 'post_type plural name', 'activitypub' ), - 'singular_name' => _x( 'Event Source', 'post_type single name', 'activitypub' ), + 'name' => _x( 'Event Sources', 'post_type plural name', 'event-bridge-for-activitypub' ), + 'singular_name' => _x( 'Event Source', 'post_type single name', 'event-bridge-for-activitypub' ), ), 'public' => false, 'hierarchical' => false, @@ -63,7 +63,7 @@ class Event_Sources { 'single' => false, 'sanitize_callback' => function ( $value ) { if ( ! is_string( $value ) ) { - throw new Exception( 'Error message is no valid string' ); + throw new \Exception( 'Error message is no valid string' ); } return esc_sql( $value ); @@ -111,7 +111,7 @@ class Event_Sources { } if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) { - return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) ); + return new WP_Error( 'activitypub_invalid_actor', __( 'Invalid ActivityPub Actor', 'event-bridge-for-activitypub' ), array( 'status' => 400 ) ); } $event_source = new Event_Source(); @@ -139,9 +139,9 @@ class Event_Sources { } /** - * Get all Followers. + * Get all Event-Sources. * - * @return array The Term list of Followers. + * @return array The Term list of Event Sources. */ public static function get_all_followers() { $args = array( @@ -159,6 +159,6 @@ class Event_Sources { ), ), ); - return self::get_followers( null, null, null, $args ); + return self::get_event_sources( null, null, null, $args ); } } diff --git a/includes/table/class-event-sources.php b/includes/table/class-event-sources.php index 7575d8a..200aebb 100644 --- a/includes/table/class-event-sources.php +++ b/includes/table/class-event-sources.php @@ -27,8 +27,8 @@ class Event_Sources extends WP_List_Table { public function __construct() { parent::__construct( array( - 'singular' => \__( 'Event Source', 'activitypub' ), - 'plural' => \__( 'Event Sources', 'activitypub' ), + 'singular' => \__( 'Event Source', 'event-bridge-for-activitypub' ), + 'plural' => \__( 'Event Sources', 'event-bridge-for-activitypub' ), 'ajax' => false, ) ); @@ -42,12 +42,12 @@ class Event_Sources extends WP_List_Table { public function get_columns() { return array( 'cb' => '', - 'avatar' => \__( 'Avatar', 'activitypub' ), - 'post_title' => \__( 'Name', 'activitypub' ), - 'username' => \__( 'Username', 'activitypub' ), - 'url' => \__( 'URL', 'activitypub' ), - 'published' => \__( 'Followed', 'activitypub' ), - 'modified' => \__( 'Last updated', 'activitypub' ), + 'avatar' => \__( 'Avatar', 'event-bridge-for-activitypub' ), + 'post_title' => \__( 'Name', 'event-bridge-for-activitypub' ), + 'username' => \__( 'Username', 'event-bridge-for-activitypub' ), + 'url' => \__( 'URL', 'event-bridge-for-activitypub' ), + 'published' => \__( 'Followed', 'event-bridge-for-activitypub' ), + 'modified' => \__( 'Last updated', 'event-bridge-for-activitypub' ), ); } @@ -146,7 +146,7 @@ class Event_Sources extends WP_List_Table { */ public function get_bulk_actions() { return array( - 'delete' => __( 'Delete', 'activitypub' ), + 'delete' => __( 'Delete', 'event-bridge-for-activitypub' ), ); } @@ -159,7 +159,7 @@ class Event_Sources extends WP_List_Table { */ public function column_default( $item, $column_name ) { if ( ! array_key_exists( $column_name, $item ) ) { - return __( 'None', 'activitypub' ); + return __( 'None', 'event-bridge-for-activitypub' ); } return $item[ $column_name ]; } @@ -224,7 +224,7 @@ class Event_Sources extends WP_List_Table { $followers = array( $followers ); } foreach ( $followers as $follower ) { - FollowerCollection::remove_follower( $this->user_id, $follower ); + Event_Source::remove( $this->user_id, $follower ); } } } -- 2.39.5 From 1eda885719fd4002bb3b23e275612d3573f53f47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 8 Dec 2024 17:38:05 +0100 Subject: [PATCH 05/33] Draft logic of storing Event Sources as custom post type --- .../collection/class-event-sources.php | 118 ++++++++++++--- .../activitypub/model/class-event-source.php | 142 ++++++++++++++++++ includes/admin/class-event-sources.php | 85 ----------- includes/admin/class-settings-page.php | 67 +++++++++ includes/class-event-sources.php | 104 +++++++++++++ includes/class-settings.php | 10 ++ includes/table/class-event-sources.php | 83 +++++----- templates/event-sources.php | 11 +- templates/settings.php | 2 +- 9 files changed, 463 insertions(+), 159 deletions(-) delete mode 100644 includes/admin/class-event-sources.php diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index 2d29b03..72d3c4c 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -22,7 +22,7 @@ class Event_Sources { /** * The custom post type. */ - const POST_TYPE = 'Event_Bridge_For_ActivityPub_follow'; + const POST_TYPE = 'ebap_event_source'; /** * Register the post type used to store the external event sources (i.e., followed ActivityPub actors). @@ -47,7 +47,7 @@ class Event_Sources { \register_post_meta( self::POST_TYPE, - 'activitypub_inbox', + 'activitypub_actor_id', array( 'type' => 'string', 'single' => true, @@ -71,18 +71,6 @@ class Event_Sources { ) ); - \register_post_meta( - self::POST_TYPE, - 'activitypub_user_id', - array( - 'type' => 'string', - 'single' => false, - 'sanitize_callback' => function ( $value ) { - return esc_sql( $value ); - }, - ) - ); - \register_post_meta( self::POST_TYPE, 'activitypub_actor_json', @@ -94,12 +82,36 @@ class Event_Sources { }, ) ); + + \register_post_meta( + self::POST_TYPE, + 'event_source_active', + array( + 'type' => 'bool', + 'single' => true, + ) + ); + + \register_post_meta( + self::POST_TYPE, + 'event_source_utilize_announces', + array( + 'type' => 'string', + 'single' => true, + 'sanitize_callback' => function ( $value ) { + if ( 'same_origin' === $value ) { + return 'same_origin'; + } + return ''; + }, + ) + ); } /** * Add new Event Source. * - * @param string $actor The Actor ID. + * @param string $actor The Actor URL/ID. * * @return Event_Source|WP_Error The Followed (WP_Post array) or an WP_Error. */ @@ -143,22 +155,78 @@ class Event_Sources { * * @return array The Term list of Event Sources. */ - public static function get_all_followers() { + public static function get_event_sources() { $args = array( - 'nopaging' => true, + '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( - 'relation' => 'AND', + 'meta_query' => array( array( - 'key' => 'activitypub_inbox', - 'compare' => 'EXISTS', - ), - array( - 'key' => 'activitypub_actor_json', + 'key' => 'activitypub_actor_id', 'compare' => 'EXISTS', ), ), ); - return self::get_event_sources( null, null, null, $args ); + + $query = new WP_Query( $args ); + + $event_sources = array_map( + function ( $post ) { + return Event_Source::init_from_cpt( $post ); + }, + $query->get_posts() + ); + + return $event_sources; + } + + /** + * Get the Event Sources along with a total count for pagination purposes. + * + * @param int $number Maximum number of results to return. + * @param int $page Page number. + * @param array $args The WP_Query arguments. + * + * @return array { + * Data about the followers. + * + * @type array $followers List of `Follower` objects. + * @type int $total Total number of followers. + * } + */ + public static function get_event_sources_with_count( $number = -1, $page = null, $args = array() ) { + $defaults = array( + 'post_type' => self::POST_TYPE, + 'posts_per_page' => $number, + 'paged' => $page, + 'orderby' => 'ID', + 'order' => 'DESC', + ); + + $args = wp_parse_args( $args, $defaults ); + $query = new WP_Query( $args ); + $total = $query->found_posts; + $actors = array_map( + function ( $post ) { + return Event_Source::init_from_cpt( $post ); + }, + $query->get_posts() + ); + + return compact( 'actors', 'total' ); + } + + /** + * Remove a Follower. + * + * @param string $event_source The Actor URL. + * + * @return mixed True on success, false on failure. + */ + public static function remove( $event_source ) { + $post_id = Event_Source::get_wp_post_from_activitypub_actor_id( $event_source ); + return wp_delete_post( $post_id, true ); } } diff --git a/includes/activitypub/model/class-event-source.php b/includes/activitypub/model/class-event-source.php index c063090..d73e2ba 100644 --- a/includes/activitypub/model/class-event-source.php +++ b/includes/activitypub/model/class-event-source.php @@ -9,6 +9,7 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Model; use Activitypub\Activity\Actor; +use Activitypub\Webfinger; use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources; use WP_Error; @@ -16,6 +17,15 @@ use WP_Error; * Event-Source (=ActivityPub Actor that is followed) model. */ class Event_Source extends Actor { + const ACTIVITYPUB_USER_HANDLE_REGEXP = '(?:([A-Za-z0-9_.-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))'; + + /** + * The complete remote ActivityPub profile of the Event Source. + * + * @var int + */ + protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore + /** * Get the Icon URL (Avatar). * @@ -35,6 +45,24 @@ class Event_Source extends Actor { return $icon; } + /** + * Get the WordPress post which stores the Event Source by the ActivityPub actor id of the event source. + * + * @param string $actor_id The ActivityPub actor ID. + * @return ?int The WordPress post ID if the actor is found, null if not. + */ + public static function get_wp_post_from_activitypub_actor_id( $actor_id ) { + global $wpdb; + $post_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s AND post_type=%s", + esc_sql( $actor_id ), + Event_Sources::POST_TYPE + ) + ); + return $post_id ? intval( $post_id ) : null; + } + /** * Convert a Custom-Post-Type input to an \Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source. * @@ -53,7 +81,121 @@ class Event_Source extends Actor { $object->set_summary( $post->post_excerpt ); $object->set_published( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_date ) ) ); $object->set_updated( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) ) ); + $thumbnail_id = get_post_thumbnail_id( $post ); + if ( $thumbnail_id ) { + $object->set_icon( + array( + 'type' => 'Image', + 'url' => wp_get_attachment_image_url( $thumbnail_id, 'thumbnail', true ), + ) + ); + } return $object; } + + /** + * Validate the current Event Source ActivityPub actor object. + * + * @return boolean True if the verification was successful. + */ + public function is_valid() { + // The minimum required attributes. + $required_attributes = array( + 'id', + 'preferredUsername', + 'inbox', + 'publicKey', + 'publicKeyPem', + ); + + foreach ( $required_attributes as $attribute ) { + if ( ! $this->get( $attribute ) ) { + return false; + } + } + + return true; + } + + /** + * Save the current Event Source object to Database within custom post type. + * + * @return int|WP_Error The post ID or an WP_Error. + */ + public function save() { + if ( ! $this->is_valid() ) { + return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) ); + } + + if ( ! $this->get__id() ) { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $post_id = $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s", + esc_sql( $this->get_id() ) + ) + ); + + if ( $post_id ) { + $post = get_post( $post_id ); + $this->set__id( $post->ID ); + } + } + + $post_id = $this->get__id(); + + $args = array( + 'ID' => $post_id, + 'guid' => esc_url_raw( $this->get_id() ), + 'post_title' => wp_strip_all_tags( sanitize_text_field( $this->get_name() ) ), + 'post_author' => 0, + 'post_type' => Event_Sources::POST_TYPE, + 'post_name' => esc_url_raw( $this->get_id() ), + 'post_excerpt' => sanitize_text_field( wp_kses( $this->get_summary(), 'user_description' ) ), + 'post_status' => 'publish', + 'meta_input' => $this->get_post_meta_input(), + ); + + if ( ! empty( $post_id ) ) { + // If this is an update, prevent the "added" date from being overwritten by the current date. + $post = get_post( $post_id ); + $args['post_date'] = $post->post_date; + $args['post_date_gmt'] = $post->post_date_gmt; + } + + $post_id = wp_insert_post( $args ); + $this->_id = $post_id; + + // Abort if inserting or updating the post didn't work. + if ( 0 === $post_id || is_wp_error( $post_id ) ) { + return $post_id; + } + + // Delete old icon. + // Check if the post has a thumbnail. + $thumbnail_id = get_post_thumbnail_id( $post_id ); + + if ( $thumbnail_id ) { + // Remove the thumbnail from the post. + delete_post_thumbnail( $post_id ); + + // Delete the attachment (and its files) from the media library. + wp_delete_attachment( $thumbnail_id, true ); + } + + // Set new icon. + $icon = $this->get_icon(); + + if ( isset( $icon['url'] ) ) { + $image = media_sideload_image( $icon['url'], $post_id, null, 'id' ); + } + if ( isset( $image ) && ! is_wp_error( $image ) ) { + set_post_thumbnail( $post_id, $image ); + } + + return $post_id; + } } diff --git a/includes/admin/class-event-sources.php b/includes/admin/class-event-sources.php deleted file mode 100644 index bf14c3a..0000000 --- a/includes/admin/class-event-sources.php +++ /dev/null @@ -1,85 +0,0 @@ -retrieve(); - - wp_send_json_success(); - } -} diff --git a/includes/admin/class-settings-page.php b/includes/admin/class-settings-page.php index f53d379..a81252f 100644 --- a/includes/admin/class-settings-page.php +++ b/includes/admin/class-settings-page.php @@ -14,6 +14,10 @@ namespace Event_Bridge_For_ActivityPub\Admin; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore +use Activitypub\Webfinger; +use Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source; +use Event_Bridge_For_ActivityPub\Event_Sources; +use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Source_Collection; use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin; use Event_Bridge_For_ActivityPub\Setup; @@ -35,6 +39,10 @@ class Settings_Page { * @return void */ public static function admin_menu(): void { + add_action( + 'admin_init', + array( self::STATIC, 'maybe_add_event_source' ), + ); \add_options_page( 'Event Bridge for ActivityPub', __( 'Event Bridge for ActivityPub', 'event-bridge-for-activitypub' ), @@ -44,6 +52,65 @@ class Settings_Page { ); } + /** + * Checks whether the current request wants to add an event source (ActivityPub follow) and passed on to actual handler. + */ + public static function maybe_add_event_source() { + if ( ! isset( $_POST['event_bridge_for_activitypub_event_source'] ) ) { + return; + } + + // Check and verify request and check capabilities. + if ( ! wp_verify_nonce( sanitize_key( $_REQUEST['_wpnonce'] ), 'event-bridge-for-activitypub-event-sources-options' ) ) { + return; + } + + if ( ! current_user_can( 'manage_options' ) ) { + return; + } + + $event_source = sanitize_text_field( $_POST['event_bridge_for_activitypub_event_source'] ); + + $actor_url = false; + + $url = wp_parse_url( $event_source ); + + if ( isset( $url['path'] ) && isset( $url['host'] ) && isset( $url['scheme'] ) ) { + $actor_url = $event_source; + } else { + if ( preg_match( '/^@?' . Event_Source::ACTIVITYPUB_USER_HANDLE_REGEXP . '$/i', $event_source ) ) { + $actor_url = Webfinger::resolve( $event_source ); + if ( is_wp_error( $actor_url ) ) { + return; + } + } else { + if ( ! isset( $url['path'] ) && isset( $url['host'] ) ) { + $actor_url = Event_Sources::get_application_actor( $url['host'] ); + } + if ( self::is_domain( $event_source ) ) { + $actor_url = Event_Sources::get_application_actor( $event_source ); + } + } + } + + if ( ! $actor_url ) { + return; + } + + Event_Source_Collection::add_event_source( $actor_url ); + } + + /** + * Check if a string is a valid domain name. + * + * @param string $domain The input string which might be a domain. + * @return bool + */ + private static function is_domain( $domain ): bool { + $pattern = '/^(?!\-)(?:(?:[a-zA-Z\d](?:[a-zA-Z\d\-]{0,61}[a-zA-Z\d])?)\.)+(?!\d+$)[a-zA-Z\d]{2,63}$/'; + return 1 === preg_match( $pattern, $domain ); + } + /** * Adds Link to the settings page in the plugin page. * It's called via apply_filter('plugin_action_links_' . PLUGIN_NAME). diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index 6b5c3b6..c5ea476 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -7,9 +7,12 @@ namespace Event_Bridge_For_ActivityPub; +use Activitypub\Activity\Extended_Object\Event; +use Activitypub\Collection\Actors; use Activitypub\Http; use Exception; +use function Activitypub\get_remote_metadata_by_actor; use function register_post_type; /** @@ -23,6 +26,15 @@ class Event_Sources { */ const POST_TYPE = 'event_bridge_follow'; + /** + * Constructor. + */ + public function __construct() { + \add_action( 'init', array( $this, 'register_post_meta' ) ); + + \add_action( 'activitypub_inbox', array( $this, 'handle_activitypub_inbox' ), 15, 3 ); + } + /** * Register the post type used to store the external event sources (i.e., followed ActivityPub actors). */ @@ -94,4 +106,96 @@ class Event_Sources { ) ); } + + /** + * 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. + * + * @param string $url The URL or ID of the ActivityPub actor. + */ + public static function get_metadata( $url ) { + if ( ! is_string( $url ) ) { + return array(); + } + + if ( false !== strpos( $url, '@' ) ) { + if ( false === strpos( $url, '/' ) && preg_match( '#^https?://#', $url, $m ) ) { + $url = substr( $url, strlen( $m[0] ) ); + } + } + 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. + * + * @param string $domain The domain without scheme. + * @return bool|string The URL/ID of the application actor, false if not found. + */ + public static function get_application_actor( $domain ) { + $result = wp_remote_get( 'https://' . $domain . '/.well-known/nodeinfo' ); + + if ( is_wp_error( $result ) ) { + return false; + } + + $body = wp_remote_retrieve_body( $result ); + + $nodeinfo = json_decode( $body, true ); + + // Check if 'links' exists and is an array. + if ( isset( $nodeinfo['links'] ) && is_array( $nodeinfo['links'] ) ) { + foreach ( $nodeinfo['links'] as $link ) { + // Check if this link matches the application actor rel. + if ( isset( $link['rel'] ) && 'https://www.w3.org/ns/activitystreams#Application' === $link['rel'] ) { + if ( is_string( $link['href'] ) ) { + return $link['href']; + } + break; + } + } + } + + // Return false if no application actor is found. + return false; + } } diff --git a/includes/class-settings.php b/includes/class-settings.php index 3b15a2c..b895e2a 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -93,6 +93,16 @@ class Settings { 'default' => EVENT_BRIDGE_FOR_ACTIVITYPUB_CUSTOM_SUMMARY, ) ); + + \register_setting( + 'event-bridge-for-activitypub-event-sources', + 'event_bridge_for_activitypub_event_sources_active', + array( + 'type' => 'boolean', + 'description' => \__( 'Whether the event sources feature is activated.', 'event-bridge-for-activitypub' ), + 'default' => 1, + ) + ); } /** diff --git a/includes/table/class-event-sources.php b/includes/table/class-event-sources.php index 200aebb..32e6794 100644 --- a/includes/table/class-event-sources.php +++ b/includes/table/class-event-sources.php @@ -8,8 +8,7 @@ namespace Event_Bridge_For_ActivityPub\Table; use WP_List_Table; -use Activitypub\Collection\Followers as FollowerCollection; -use Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source; +use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Sources_Collection; use function Activitypub\object_to_uri; @@ -18,7 +17,7 @@ if ( ! \class_exists( '\WP_List_Table' ) ) { } /** - * Followers Table-Class. + * Event Sources Table-Class. */ class Event_Sources extends WP_List_Table { /** @@ -42,9 +41,9 @@ class Event_Sources extends WP_List_Table { public function get_columns() { return array( 'cb' => '', - 'avatar' => \__( 'Avatar', 'event-bridge-for-activitypub' ), - 'post_title' => \__( 'Name', 'event-bridge-for-activitypub' ), - 'username' => \__( 'Username', 'event-bridge-for-activitypub' ), + 'icon' => \__( 'Icon', 'event-bridge-for-activitypub' ), + 'name' => \__( 'Name', 'event-bridge-for-activitypub' ), + 'active' => \__( 'Active', 'event-bridge-for-activitypub' ), 'url' => \__( 'URL', 'event-bridge-for-activitypub' ), 'published' => \__( 'Followed', 'event-bridge-for-activitypub' ), 'modified' => \__( 'Last updated', 'event-bridge-for-activitypub' ), @@ -58,7 +57,7 @@ class Event_Sources extends WP_List_Table { */ public function get_sortable_columns() { return array( - 'post_title' => array( 'post_title', true ), + 'name' => array( 'name', true ), 'modified' => array( 'modified', false ), 'published' => array( 'published', false ), ); @@ -96,22 +95,7 @@ class Event_Sources extends WP_List_Table { } // phpcs:enable WordPress.Security.NonceVerification.Recommended - $dummy_event_sources = array( - 'total' => 1, - 'actors' => array( - Event_Source::init_from_array( - array( - 'id' => 'https://graz.social/@linos', - 'url' => 'https://graz.social/@linos', - 'preferredUsername' => 'linos', - 'name' => 'André Menrath', - 'icon' => 'https://graz.social/system/accounts/avatars/000/000/001/original/fe1c795256720361.jpeg', - ) - ), - ), - ); - - $event_sources = $dummy_event_sources; + $event_sources = Event_Sources_Collection::get_event_sources_with_count($per_page, $page_num, $args ); $actors = $event_sources['actors']; $counter = $event_sources['total']; @@ -127,9 +111,9 @@ class Event_Sources extends WP_List_Table { foreach ( $actors as $actor ) { $item = array( 'icon' => esc_attr( $actor->get_icon_url() ), - 'post_title' => esc_attr( $actor->get_name() ), - 'username' => esc_attr( $actor->get_preferred_username() ), - 'url' => esc_attr( object_to_uri( $actor->get_url() ) ), + 'name' => esc_attr( $actor->get_name() ), + 'url' => esc_attr( object_to_uri( $actor->get_id() ) ), + 'active' => esc_attr( get_post_meta( $actor->get__id(), 'event_source_active', true) ), 'identifier' => esc_attr( $actor->get_id() ), 'published' => esc_attr( $actor->get_published() ), 'modified' => esc_attr( $actor->get_updated() ), @@ -170,7 +154,7 @@ class Event_Sources extends WP_List_Table { * @param array $item Item. * @return string */ - public function column_avatar( $item ) { + public function column_icon( $item ) { return sprintf( '', $item['icon'] @@ -198,14 +182,32 @@ class Event_Sources extends WP_List_Table { * @return string */ public function column_cb( $item ) { - return sprintf( '', esc_attr( $item['identifier'] ) ); + return sprintf( '', esc_attr( $item['identifier'] ) ); + } + + /** + * Column action. + * + * @param array $item Item. + * @return string + */ + public function column_active( $item ) { + if ( $item['active'] ) { + $action = 'true'; + } else { + $action = 'false'; + } + return sprintf( + '%s', + $action + ); } /** * Process action. */ public function process_action() { - if ( ! isset( $_REQUEST['followers'] ) || ! isset( $_REQUEST['_wpnonce'] ) ) { + if ( ! isset( $_REQUEST['event_sources'] ) || ! isset( $_REQUEST['_wpnonce'] ) ) { return; } $nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) ); @@ -213,28 +215,19 @@ class Event_Sources extends WP_List_Table { return; } - if ( ! current_user_can( 'edit_user', $this->user_id ) ) { + if ( ! current_user_can( 'manage_options' ) ) { return; } - $followers = $_REQUEST['followers']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput + $event_sources = $_REQUEST['event_sources']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput - if ( $this->current_action() === 'delete' ) { - if ( ! is_array( $followers ) ) { - $followers = array( $followers ); + if ( 'delete' === $this->current_action() ) { + if ( ! is_array( $event_sources ) ) { + $event_sources = array( $event_sources ); } - foreach ( $followers as $follower ) { - Event_Source::remove( $this->user_id, $follower ); + foreach ( $event_sources as $event_source ) { + Event_Sources_Collection::remove( $event_source ); } } } - - /** - * Returns user count. - * - * @return int - */ - public function get_user_count() { - return FollowerCollection::count_followers( $this->user_id ); - } } diff --git a/templates/event-sources.php b/templates/event-sources.php index 72e2fdc..c62912a 100644 --- a/templates/event-sources.php +++ b/templates/event-sources.php @@ -34,14 +34,19 @@ $table = new \Event_Bridge_For_ActivityPub\Table\Event_Sources();
- - + + prepare_items(); $table->search_box( 'Search', 'search' ); diff --git a/templates/settings.php b/templates/settings.php index 2c9c077..7ddfa95 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -144,7 +144,7 @@ $current_category_mapping = \get_option( 'event_bridge_for_activitypub_ev
-
-- 2.39.5 From 970f3e77548a2794f990670a9aa05e036c1dae91 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 8 Dec 2024 21:57:53 +0100 Subject: [PATCH 07/33] add dummy handlers for incoming event source related activities and phpcs --- includes/activitypub/class-handler.php | 36 +++++++++++++ .../collection/class-event-sources.php | 11 ++-- includes/activitypub/handler/class-accept.php | 50 +++++++++++++++++++ .../activitypub/handler/class-announce.php | 49 ++++++++++++++++++ includes/activitypub/handler/class-create.php | 49 ++++++++++++++++++ includes/activitypub/handler/class-delete.php | 49 ++++++++++++++++++ includes/activitypub/handler/class-update.php | 49 ++++++++++++++++++ includes/admin/class-settings-page.php | 22 ++++---- includes/class-event-sources.php | 2 + includes/class-setup.php | 5 +- includes/integrations/class-event-plugin.php | 1 + includes/table/class-event-sources.php | 24 +++++---- templates/event-sources.php | 2 + 13 files changed, 320 insertions(+), 29 deletions(-) create mode 100644 includes/activitypub/class-handler.php create mode 100644 includes/activitypub/handler/class-accept.php create mode 100644 includes/activitypub/handler/class-announce.php create mode 100644 includes/activitypub/handler/class-create.php create mode 100644 includes/activitypub/handler/class-delete.php create mode 100644 includes/activitypub/handler/class-update.php diff --git a/includes/activitypub/class-handler.php b/includes/activitypub/class-handler.php new file mode 100644 index 0000000..b9961d2 --- /dev/null +++ b/includes/activitypub/class-handler.php @@ -0,0 +1,36 @@ + 'bool', - 'single' => true, + 'type' => 'bool', + 'single' => true, ) ); @@ -294,7 +295,7 @@ class Event_Sources { $actor = Event_Source::init_from_cpt( get_post( $post_id ) ); - if ( is_wp_error( $actor ) ) { + if ( ! $actor instanceof Event_Source ) { return $actor; } @@ -309,7 +310,7 @@ class Event_Sources { $activity->set_cc( null ); $activity->set_actor( $application->get_id() ); $activity->set_object( $to ); - $activity->set_id( $actor . '#follow-' . \preg_replace( '~^https?://~', '', $to ) ); + $activity->set_id( $application->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); \Activitypub\safe_remote_post( $inbox, $activity, \Activitypub\Collection\Actors::APPLICATION_USER_ID ); } @@ -345,7 +346,7 @@ class Event_Sources { $actor = Event_Source::init_from_cpt( get_post( $post_id ) ); - if ( is_wp_error( $actor ) ) { + if ( ! $actor instanceof Event_Source ) { return $actor; } diff --git a/includes/activitypub/handler/class-accept.php b/includes/activitypub/handler/class-accept.php new file mode 100644 index 0000000..4a86ae5 --- /dev/null +++ b/includes/activitypub/handler/class-accept.php @@ -0,0 +1,50 @@ +get__id() ) { + return; + } + } +} diff --git a/includes/activitypub/handler/class-announce.php b/includes/activitypub/handler/class-announce.php new file mode 100644 index 0000000..f79b3e7 --- /dev/null +++ b/includes/activitypub/handler/class-announce.php @@ -0,0 +1,49 @@ +get__id() ) { + return; + } + } +} diff --git a/includes/activitypub/handler/class-create.php b/includes/activitypub/handler/class-create.php new file mode 100644 index 0000000..854a084 --- /dev/null +++ b/includes/activitypub/handler/class-create.php @@ -0,0 +1,49 @@ +get__id() ) { + return; + } + } +} diff --git a/includes/activitypub/handler/class-delete.php b/includes/activitypub/handler/class-delete.php new file mode 100644 index 0000000..ff4cede --- /dev/null +++ b/includes/activitypub/handler/class-delete.php @@ -0,0 +1,49 @@ +get__id() ) { + return; + } + } +} diff --git a/includes/activitypub/handler/class-update.php b/includes/activitypub/handler/class-update.php new file mode 100644 index 0000000..16b7559 --- /dev/null +++ b/includes/activitypub/handler/class-update.php @@ -0,0 +1,49 @@ +get__id() ) { + return; + } + } +} diff --git a/includes/admin/class-settings-page.php b/includes/admin/class-settings-page.php index a81252f..c49602b 100644 --- a/includes/admin/class-settings-page.php +++ b/includes/admin/class-settings-page.php @@ -77,19 +77,17 @@ class Settings_Page { if ( isset( $url['path'] ) && isset( $url['host'] ) && isset( $url['scheme'] ) ) { $actor_url = $event_source; + } elseif ( preg_match( '/^@?' . Event_Source::ACTIVITYPUB_USER_HANDLE_REGEXP . '$/i', $event_source ) ) { + $actor_url = Webfinger::resolve( $event_source ); + if ( is_wp_error( $actor_url ) ) { + return; + } } else { - if ( preg_match( '/^@?' . Event_Source::ACTIVITYPUB_USER_HANDLE_REGEXP . '$/i', $event_source ) ) { - $actor_url = Webfinger::resolve( $event_source ); - if ( is_wp_error( $actor_url ) ) { - return; - } - } else { - if ( ! isset( $url['path'] ) && isset( $url['host'] ) ) { - $actor_url = Event_Sources::get_application_actor( $url['host'] ); - } - if ( self::is_domain( $event_source ) ) { - $actor_url = Event_Sources::get_application_actor( $event_source ); - } + if ( ! isset( $url['path'] ) && isset( $url['host'] ) ) { + $actor_url = Event_Sources::get_application_actor( $url['host'] ); + } + if ( self::is_domain( $event_source ) ) { + $actor_url = Event_Sources::get_application_actor( $event_source ); } } diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index b636569..f2701fa 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -3,6 +3,8 @@ * Class for handling and saving the ActivityPub event sources (i.e. follows). * * @package Event_Bridge_For_ActivityPub + * @since 1.0.0 + * @license AGPL-3.0-or-later */ namespace Event_Bridge_For_ActivityPub; diff --git a/includes/class-setup.php b/includes/class-setup.php index df9970a..e18e468 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -15,12 +15,13 @@ namespace Event_Bridge_For_ActivityPub; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore +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; use Event_Bridge_For_ActivityPub\Admin\General_Admin_Notices; use Event_Bridge_For_ActivityPub\Admin\Health_Check; use Event_Bridge_For_ActivityPub\Admin\Settings_Page; use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin; -use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Sources_Collection; require_once ABSPATH . 'wp-admin/includes/plugin.php'; @@ -178,6 +179,8 @@ class Setup { array( Settings_Page::class, 'settings_link' ) ); + add_action( 'activitypub_register_handlers', array( Handler::class, 'register_activitypub_handlers' ) ) ; + // If we don't have any active event plugins, or the ActivityPub plugin is not enabled, abort here. if ( empty( $this->active_event_plugins ) || ! $this->activitypub_plugin_is_active ) { return; diff --git a/includes/integrations/class-event-plugin.php b/includes/integrations/class-event-plugin.php index 56d020a..aba3d22 100644 --- a/includes/integrations/class-event-plugin.php +++ b/includes/integrations/class-event-plugin.php @@ -6,6 +6,7 @@ * * @package Event_Bridge_For_ActivityPub * @since 1.0.0 + * @license AGPL-3.0-or-later */ namespace Event_Bridge_For_ActivityPub\Integrations; diff --git a/includes/table/class-event-sources.php b/includes/table/class-event-sources.php index 2969483..9dcbf03 100644 --- a/includes/table/class-event-sources.php +++ b/includes/table/class-event-sources.php @@ -3,6 +3,8 @@ * Event Sources Table-Class file. * * @package Event_Bridge_For_ActivityPub + * @since 1.0.0 + * @license AGPL-3.0-or-later */ namespace Event_Bridge_For_ActivityPub\Table; @@ -40,13 +42,13 @@ class Event_Sources extends WP_List_Table { */ public function get_columns() { return array( - 'cb' => '', - 'icon' => \__( 'Icon', 'event-bridge-for-activitypub' ), - 'name' => \__( 'Name', 'event-bridge-for-activitypub' ), - 'active' => \__( 'Active', 'event-bridge-for-activitypub' ), - 'url' => \__( 'URL', 'event-bridge-for-activitypub' ), - 'published' => \__( 'Followed', 'event-bridge-for-activitypub' ), - 'modified' => \__( 'Last updated', 'event-bridge-for-activitypub' ), + 'cb' => '', + 'icon' => \__( 'Icon', 'event-bridge-for-activitypub' ), + 'name' => \__( 'Name', 'event-bridge-for-activitypub' ), + 'active' => \__( 'Active', 'event-bridge-for-activitypub' ), + 'url' => \__( 'URL', 'event-bridge-for-activitypub' ), + 'published' => \__( 'Followed', 'event-bridge-for-activitypub' ), + 'modified' => \__( 'Last updated', 'event-bridge-for-activitypub' ), ); } @@ -57,9 +59,9 @@ class Event_Sources extends WP_List_Table { */ public function get_sortable_columns() { return array( - 'name' => array( 'name', true ), - 'modified' => array( 'modified', false ), - 'published' => array( 'published', false ), + 'name' => array( 'name', true ), + 'modified' => array( 'modified', false ), + 'published' => array( 'published', false ), ); } @@ -113,7 +115,7 @@ class Event_Sources extends WP_List_Table { 'icon' => esc_attr( $actor->get_icon_url() ), 'name' => esc_attr( $actor->get_name() ), 'url' => esc_attr( object_to_uri( $actor->get_id() ) ), - 'active' => esc_attr( get_post_meta( $actor->get__id(), 'event_source_active', true) ), + 'active' => esc_attr( get_post_meta( $actor->get__id(), 'event_source_active', true ) ), 'identifier' => esc_attr( $actor->get_id() ), 'published' => esc_attr( $actor->get_published() ), 'modified' => esc_attr( $actor->get_updated() ), diff --git a/templates/event-sources.php b/templates/event-sources.php index b2a4b20..47e2f22 100644 --- a/templates/event-sources.php +++ b/templates/event-sources.php @@ -3,6 +3,8 @@ * Event Sources management page for the ActivityPub Event Bridge. * * @package Event_Bridge_For_ActivityPub + * @since 1.0.0 + * @license AGPL-3.0-or-later */ // Exit if accessed directly. -- 2.39.5 From 189e7b5f9f4a36fdbd41b0b83884f2cd90acb97d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 8 Dec 2024 22:01:06 +0100 Subject: [PATCH 08/33] phpcs --- includes/class-setup.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/includes/class-setup.php b/includes/class-setup.php index e18e468..49ac81c 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -179,7 +179,7 @@ class Setup { array( Settings_Page::class, 'settings_link' ) ); - add_action( 'activitypub_register_handlers', array( Handler::class, 'register_activitypub_handlers' ) ) ; + add_action( 'activitypub_register_handlers', array( Handler::class, 'register_activitypub_handlers' ) ); // If we don't have any active event plugins, or the ActivityPub plugin is not enabled, abort here. if ( empty( $this->active_event_plugins ) || ! $this->activitypub_plugin_is_active ) { -- 2.39.5 From 210bf9cc96169bda727a29283c448377c0fa7754 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 8 Dec 2024 22:36:40 +0100 Subject: [PATCH 09/33] add draft for allowing settings for which plugin integration is used for events sources --- .../transmogrify/class-gatherpress.php | 61 +++++++++++++++++++ includes/admin/class-settings-page.php | 20 ++++-- includes/class-settings.php | 36 +++++++++++ includes/class-setup.php | 2 - includes/integrations/class-event-plugin.php | 9 +++ includes/integrations/class-gatherpress.php | 9 +++ templates/event-sources.php | 9 ++- 7 files changed, 138 insertions(+), 8 deletions(-) create mode 100644 includes/activitypub/transmogrify/class-gatherpress.php diff --git a/includes/activitypub/transmogrify/class-gatherpress.php b/includes/activitypub/transmogrify/class-gatherpress.php new file mode 100644 index 0000000..b94efb5 --- /dev/null +++ b/includes/activitypub/transmogrify/class-gatherpress.php @@ -0,0 +1,61 @@ +activitypub_event = $activitypub_event; + } + + /** + * Save the ActivityPub event object as GatherPress Event. + */ + public function save() { + // Insert GatherPress Event here. + } +} diff --git a/includes/admin/class-settings-page.php b/includes/admin/class-settings-page.php index c49602b..2ad8999 100644 --- a/includes/admin/class-settings-page.php +++ b/includes/admin/class-settings-page.php @@ -162,12 +162,11 @@ class Settings_Page { $tab = sanitize_key( $_GET['tab'] ); } + $plugin_setup = Setup::get_instance(); + $event_plugins = $plugin_setup->get_active_event_plugins(); + switch ( $tab ) { case 'settings': - $plugin_setup = Setup::get_instance(); - - $event_plugins = $plugin_setup->get_active_event_plugins(); - $event_terms = array(); foreach ( $event_plugins as $event_plugin ) { @@ -182,9 +181,20 @@ class Settings_Page { \load_template( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php', true, $args ); break; case 'event-sources': + $supports_event_sources = array(); + + foreach ( $event_plugins as $event_plugin ) { + if ( $event_plugin->supports_event_sources() ) { + $supports_event_sources[ $event_plugin::class ] = $event_plugin->get_plugin_name(); + } + } + $args = array( + 'supports_event_sources' => $supports_event_sources, + ); + wp_enqueue_script( 'thickbox' ); wp_enqueue_style( 'thickbox' ); - \load_template( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_DIR . 'templates/event-sources.php', true ); + \load_template( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_DIR . 'templates/event-sources.php', true, $args ); break; case 'welcome': default: diff --git a/includes/class-settings.php b/includes/class-settings.php index b895e2a..c74dddc 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -103,6 +103,42 @@ class Settings { 'default' => 1, ) ); + + \register_setting( + 'event-bridge-for-activitypub', + 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', + array( + 'type' => 'array', + 'description' => \__( 'Define which plugin/integration is used for the event sources feature', 'event-bridge-for-activitypub' ), + 'default' => array(), + 'sanitize_callback' => array( self::class, 'sanitize_plugin_used_for_event_sources' ), + ) + ); + } + + /** + * Sanitize the option which event plugin. + * + * @param string $plugin The setting. + * @return string + */ + public static function sanitize_plugin_used_for_event_sources( $plugin ) { + if ( ! is_string( $plugin ) ) { + return ''; + } + $setup = Setup::get_instance(); + $active_event_plugins = $setup->get_active_event_plugins(); + + $valid_options = array(); + foreach ( $active_event_plugins as $active_event_plugin ) { + if ( $active_event_plugin->supports_event_sources() ) { + $valid_options[] = $active_event_plugin::class; + } + } + if ( in_array( $plugin, $valid_options, true ) ) { + return $plugin; + } + return ''; } /** diff --git a/includes/class-setup.php b/includes/class-setup.php index 49ac81c..68f2f49 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -179,8 +179,6 @@ class Setup { array( Settings_Page::class, 'settings_link' ) ); - add_action( 'activitypub_register_handlers', array( Handler::class, 'register_activitypub_handlers' ) ); - // If we don't have any active event plugins, or the ActivityPub plugin is not enabled, abort here. if ( empty( $this->active_event_plugins ) || ! $this->activitypub_plugin_is_active ) { return; diff --git a/includes/integrations/class-event-plugin.php b/includes/integrations/class-event-plugin.php index aba3d22..756926f 100644 --- a/includes/integrations/class-event-plugin.php +++ b/includes/integrations/class-event-plugin.php @@ -54,6 +54,15 @@ abstract class Event_Plugin { return array(); } + /** + * By default event sources are not supported by an event plugin integration. + * + * @return bool True if event sources are supported. + */ + public static function supports_event_sources(): bool { + return false; + } + /** * Get the plugins name from the main plugin-file's top-level-file-comment. */ diff --git a/includes/integrations/class-gatherpress.php b/includes/integrations/class-gatherpress.php index 57cd222..0efb319 100644 --- a/includes/integrations/class-gatherpress.php +++ b/includes/integrations/class-gatherpress.php @@ -66,4 +66,13 @@ final class GatherPress extends Event_Plugin { public static function get_event_category_taxonomy(): string { return class_exists( '\GatherPress\Core\Topic' ) ? \GatherPress\Core\Topic::TAXONOMY : 'gatherpress_topic'; } + + /** + * GatherPress supports the Event Sources feature. + * + * @return bool True if event sources are supported. + */ + public static function supports_event_sources(): bool { + return true; + } } diff --git a/templates/event-sources.php b/templates/event-sources.php index 47e2f22..cdfe441 100644 --- a/templates/event-sources.php +++ b/templates/event-sources.php @@ -18,12 +18,19 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore ) ); +if ( ! isset( $args ) || ! array_key_exists( 'supports_event_sources', $args ) ) { + return; +} + +if ( ! current_user_can( 'manage_options' ) ) { + return; +} $table = new \Event_Bridge_For_ActivityPub\Table\Event_Sources(); ?>
- +

-- 2.39.5 From 17ca4ff800fee719e83121d62c6db505a9620783 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Mon, 9 Dec 2024 18:16:07 +0100 Subject: [PATCH 10/33] enhance settings page and use upstream test class as boilderplate --- .../event-bridge-for-activitypub-admin.css | 9 + composer.json | 6 +- event-bridge-for-activitypub.php | 2 +- includes/class-settings.php | 9 +- includes/class-setup.php | 19 + templates/admin-header.php | 2 + templates/event-sources.php | 125 +++++- templates/settings.php | 3 +- templates/welcome.php | 2 + tests/bootstrap.php | 4 +- ...t-bridge-for-activitypub-event-sources.php | 356 ++++++++++++++++++ 11 files changed, 508 insertions(+), 29 deletions(-) create mode 100644 tests/test-class-event-bridge-for-activitypub-event-sources.php diff --git a/assets/css/event-bridge-for-activitypub-admin.css b/assets/css/event-bridge-for-activitypub-admin.css index 6c62d22..3b22ddd 100644 --- a/assets/css/event-bridge-for-activitypub-admin.css +++ b/assets/css/event-bridge-for-activitypub-admin.css @@ -185,3 +185,12 @@ code.event-bridge-for-activitypub-settings-example-url { #event_bridge_for_activitypub_summary_type_custom-details > details { padding: 0.5em; } + +.event_bridge_for_activitypub-list { + list-style: disc; + padding-left: 22px; +} + +.event_bridge_for_activitypub-admin-table-container { + padding-left: 22px; +} diff --git a/composer.json b/composer.json index 01cb90e..4f88074 100644 --- a/composer.json +++ b/composer.json @@ -56,11 +56,12 @@ "@test-eventin", "@test-modern-events-calendar-lite", "@test-eventprime", - "@test-event-organiser" + "@test-event-organiser", + "@test-event-bridge-for-activitypub-event-sources" ], "test-debug": [ "@prepare-test", - "@test-event-bridge-for-activitypub-shortcodes" + "@test-event-bridge-for-activitypub-event-sources" ], "test-vs-event-list": "phpunit --filter=vs_event_list", "test-the-events-calendar": "phpunit --filter=the_events_calendar", @@ -72,6 +73,7 @@ "test-eventprime": "phpunit --filter=eventprime", "test-event-organiser": "phpunit --filter=event_organiser", "test-event-bridge-for-activitypub-shortcodes": "phpunit --filter=event_bridge_for_activitypub_shortcodes", + "test-event-bridge-for-activitypub-event-sources": "phpunit --filter=event_bridge_for_activitypub_event_sources", "test-all": "phpunit" } } diff --git a/event-bridge-for-activitypub.php b/event-bridge-for-activitypub.php index 4bb34cf..917124a 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.1.1 + * Version: 0.3.1.3 * Author: André Menrath * Author URI: https://graz.social/@linos * Text Domain: event-bridge-for-activitypub diff --git a/includes/class-settings.php b/includes/class-settings.php index c74dddc..30d3cf4 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -98,14 +98,15 @@ class Settings { 'event-bridge-for-activitypub-event-sources', 'event_bridge_for_activitypub_event_sources_active', array( - 'type' => 'boolean', - 'description' => \__( 'Whether the event sources feature is activated.', 'event-bridge-for-activitypub' ), - 'default' => 1, + 'type' => 'boolean', + 'show_in_rest' => true, + 'description' => \__( 'Whether the event sources feature is activated.', 'event-bridge-for-activitypub' ), + 'default' => 0, ) ); \register_setting( - 'event-bridge-for-activitypub', + 'event-bridge-for-activitypub-event-sources', 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', array( 'type' => 'array', diff --git a/includes/class-setup.php b/includes/class-setup.php index 68f2f49..b381905 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -158,6 +158,25 @@ class Setup { return $active_event_plugins; } + /** + * Function that checks which event plugins support the event sources feature. + * + * @return array List of supported event plugins as keys from the SUPPORTED_EVENT_PLUGINS const. + */ + public static function detect_event_plugins_supporting_event_sources(): array { + $plugins_supporting_event_sources = array(); + + foreach ( self::EVENT_PLUGIN_CLASSES as $event_plugin_class ) { + if ( ! class_exists( $event_plugin_class ) || ! method_exists( $event_plugin_class, 'get_plugin_file' ) ) { + continue; + } + if ( call_user_func( array( $event_plugin_class, 'supports_event_sources' ) ) ) { + $plugins_supporting_event_sources[] = new $event_plugin_class(); + } + } + return $plugins_supporting_event_sources; + } + /** * Set up hooks for various purposes. * diff --git a/templates/admin-header.php b/templates/admin-header.php index 7456149..54d7ca2 100644 --- a/templates/admin-header.php +++ b/templates/admin-header.php @@ -3,6 +3,8 @@ * Template for the header and navigation of the admin pages. * * @package Event_Bridge_For_ActivityPub + * @since 1.0.0 + * @license AGPL-3.0-or-later */ // Exit if accessed directly. diff --git a/templates/event-sources.php b/templates/event-sources.php index cdfe441..8a03ffb 100644 --- a/templates/event-sources.php +++ b/templates/event-sources.php @@ -3,7 +3,7 @@ * Event Sources management page for the ActivityPub Event Bridge. * * @package Event_Bridge_For_ActivityPub - * @since 1.0.0 + * @since 1.0.0 * @license AGPL-3.0-or-later */ @@ -26,19 +26,103 @@ if ( ! current_user_can( 'manage_options' ) ) { return; } -$table = new \Event_Bridge_For_ActivityPub\Table\Event_Sources(); +$event_plugins_supporting_event_sources = $args['supports_event_sources']; + +$selected_plugin = \get_option( 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', '' ); +$event_sources_active = \get_option( 'event_bridge_for_activitypub_event_sources_active', false ); ?>
-
-

-

+

+ +
+ + + + + + + + + + + + + + +
+ + + + > +

+
+ + + +

+
+ +
+ +

+

+ '; + foreach ( $plugins_supporting_event_sources as $event_plugin ) { + echo '
  • ' . esc_attr( $event_plugin->get_plugin_name() ) . '
  • '; + } + echo ''; + return; + } + ?> +
    + '; + return; + } + ?> +
    - - - - +
    + +

    + + + + +
    +
    + + + prepare_items(); + $table->search_box( 'Search', 'search' ); + $table->display(); + ?> +
    +
    -
    -
    - - - prepare_items(); - $table->search_box( 'Search', 'search' ); - $table->display(); - ?> -
    -
    diff --git a/templates/settings.php b/templates/settings.php index 7ddfa95..842401d 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -6,6 +6,7 @@ * * @package Event_Bridge_For_ActivityPub * @since 1.0.0 + * @license AGPL-3.0-or-later * * @param array $args An array of arguments for the settings page. */ @@ -41,7 +42,7 @@ $current_category_mapping = \get_option( 'event_bridge_for_activitypub_ev
    - +

    diff --git a/templates/welcome.php b/templates/welcome.php index 503df4f..2dc3977 100644 --- a/templates/welcome.php +++ b/templates/welcome.php @@ -3,6 +3,8 @@ * Status page for the Event Bridge for ActivityPub. * * @package Event_Bridge_For_ActivityPub + * @since 1.0.0 + * @license AGPL-3.0-or-later */ // Exit if accessed directly. diff --git a/tests/bootstrap.php b/tests/bootstrap.php index 643a8aa..e409173 100755 --- a/tests/bootstrap.php +++ b/tests/bootstrap.php @@ -96,11 +96,13 @@ function _manually_load_plugin() { if ( $plugin_file ) { _manually_load_event_plugin( $plugin_file ); + } elseif ( 'event_bridge_for_activitypub_event_sources' === $event_bridge_for_activitypub_integration_filter ) { + // For the Event Sources feature we currently only test with GatherPress. + _manually_load_event_plugin( 'gatherpress/gatherpress.php' ); } else { // For all other tests we mainly use the Events Calendar as a reference. _manually_load_event_plugin( 'the-events-calendar/the-events-calendar.php' ); _manually_load_event_plugin( 'very-simple-event-list/vsel.php' ); - } // Hot fix that allows using Events Manager within unit tests, because the em_init() is later not run as admin. diff --git a/tests/test-class-event-bridge-for-activitypub-event-sources.php b/tests/test-class-event-bridge-for-activitypub-event-sources.php new file mode 100644 index 0000000..212d1c4 --- /dev/null +++ b/tests/test-class-event-bridge-for-activitypub-event-sources.php @@ -0,0 +1,356 @@ + 'https://remote.example/@id', + 'type' => 'Follow', + 'actor' => 'https://remote.example/@test', + 'object' => 'https://local.example/@test', + ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/1/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + $response = \rest_do_request( $request ); + + $this->assertEquals( 401, $response->get_status() ); + $this->assertEquals( 'activitypub_signature_verification', $response->get_data()['code'] ); + } + + /** + * Test missing attribute. + */ + public function test_missing_attribute() { + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + + $json = array( + 'id' => 'https://remote.example/@id', + 'type' => 'Follow', + 'actor' => 'https://remote.example/@test', + ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/1/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + $response = \rest_do_request( $request ); + + $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( 'rest_missing_callback_param', $response->get_data()['code'] ); + $this->assertEquals( 'object', $response->get_data()['data']['params'][0] ); + } + + /** + * Test follow request. + */ + public function test_follow_request() { + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + + $json = array( + 'id' => 'https://remote.example/@id', + 'type' => 'Follow', + 'actor' => 'https://remote.example/@test', + 'object' => 'https://local.example/@test', + ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/1/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + // Dispatch the request. + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + } + + /** + * Test follow request global inbox. + */ + public function test_follow_request_global_inbox() { + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + + $json = array( + 'id' => 'https://remote.example/@id', + 'type' => 'Follow', + 'actor' => 'https://remote.example/@test', + 'object' => 'https://local.example/@test', + ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + // Dispatch the request. + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + } + + /** + * Test create request with a remote actor. + */ + public function test_create_request() { + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + + // Invalid request, because of an invalid object. + $json = array( + 'id' => 'https://remote.example/@id', + 'type' => 'Create', + 'actor' => 'https://remote.example/@test', + 'object' => 'https://local.example/@test', + ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/1/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + // Dispatch the request. + $response = \rest_do_request( $request ); + $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( 'rest_invalid_param', $response->get_data()['code'] ); + + // Valid request, because of a valid object. + $json['object'] = array( + 'id' => 'https://remote.example/post/test', + 'type' => 'Note', + 'content' => 'Hello, World!', + 'inReplyTo' => 'https://local.example/post/test', + 'published' => '2020-01-01T00:00:00Z', + ); + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/1/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + // Dispatch the request. + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + } + + /** + * Test create request global inbox. + */ + public function test_create_request_global_inbox() { + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + + // Invalid request, because of an invalid object. + $json = array( + 'id' => 'https://remote.example/@id', + 'type' => 'Create', + 'actor' => 'https://remote.example/@test', + 'object' => 'https://local.example/@test', + ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + // Dispatch the request. + $response = \rest_do_request( $request ); + $this->assertEquals( 400, $response->get_status() ); + $this->assertEquals( 'rest_invalid_param', $response->get_data()['code'] ); + + // Valid request, because of a valid object. + $json['object'] = array( + 'id' => 'https://remote.example/post/test', + 'type' => 'Note', + 'content' => 'Hello, World!', + 'inReplyTo' => 'https://local.example/post/test', + 'published' => '2020-01-01T00:00:00Z', + ); + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + // Dispatch the request. + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + } + + /** + * Test update request. + */ + public function test_update_request() { + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + + $json = array( + 'id' => 'https://remote.example/@id', + 'type' => 'Update', + 'actor' => 'https://remote.example/@test', + 'object' => array( + 'id' => 'https://remote.example/post/test', + 'type' => 'Note', + 'content' => 'Hello, World!', + 'inReplyTo' => 'https://local.example/post/test', + 'published' => '2020-01-01T00:00:00Z', + ), + ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/1/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + // Dispatch the request. + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + } + + /** + * Test like request. + */ + public function test_like_request() { + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + + $json = array( + 'id' => 'https://remote.example/@id', + 'type' => 'Like', + 'actor' => 'https://remote.example/@test', + 'object' => 'https://local.example/post/test', + ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/1/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + // Dispatch the request. + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + } + + /** + * Test announce request. + */ + public function test_announce_request() { + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + + $json = array( + 'id' => 'https://remote.example/@id', + 'type' => 'Announce', + 'actor' => 'https://remote.example/@test', + 'object' => 'https://local.example/post/test', + ); + + $request = new \WP_REST_Request( 'POST', '/activitypub/1.0/users/1/inbox' ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( \wp_json_encode( $json ) ); + + // Dispatch the request. + $response = \rest_do_request( $request ); + $this->assertEquals( 202, $response->get_status() ); + } + + /** + * Test whether an activity is public. + * + * @dataProvider the_data_provider + * + * @param array $data The data. + * @param bool $check The check. + */ + public function test_is_activity_public( $data, $check ) { + $this->assertEquals( $check, Activitypub\is_activity_public( $data ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function the_data_provider() { + return array( + array( + array( + 'cc' => array( + 'https://example.org/@test', + 'https://example.com/@test2', + ), + 'to' => 'https://www.w3.org/ns/activitystreams#Public', + 'object' => array(), + ), + true, + ), + array( + array( + 'cc' => array( + 'https://example.org/@test', + 'https://example.com/@test2', + ), + 'to' => array( + 'https://www.w3.org/ns/activitystreams#Public', + ), + 'object' => array(), + ), + true, + ), + array( + array( + 'cc' => array( + 'https://example.org/@test', + 'https://example.com/@test2', + ), + 'object' => array(), + ), + false, + ), + array( + array( + 'cc' => array( + 'https://example.org/@test', + 'https://example.com/@test2', + ), + 'object' => array( + 'to' => 'https://www.w3.org/ns/activitystreams#Public', + ), + ), + true, + ), + array( + array( + 'cc' => array( + 'https://example.org/@test', + 'https://example.com/@test2', + ), + 'object' => array( + 'to' => array( + 'https://www.w3.org/ns/activitystreams#Public', + ), + ), + ), + true, + ), + ); + } +} -- 2.39.5 From ef1248beed937d8a7d6aa84ee9894a096bd1111c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Tue, 10 Dec 2024 19:34:15 +0100 Subject: [PATCH 11/33] wip --- includes/activitypub/class-handler.php | 1 - .../collection/class-event-sources.php | 11 +++ includes/activitypub/handler/class-create.php | 31 ++++-- includes/activitypub/handler/class-update.php | 1 + .../class-gatherpress.php | 27 +++++- includes/admin/class-settings-page.php | 16 ++- includes/class-settings.php | 15 ++- includes/class-setup.php | 24 ++++- includes/integrations/class-event-plugin.php | 10 ++ templates/event-sources.php | 97 ------------------- templates/settings.php | 79 ++++++++++++++- 11 files changed, 188 insertions(+), 124 deletions(-) rename includes/activitypub/{transmogrify => transmogrifier}/class-gatherpress.php (61%) diff --git a/includes/activitypub/class-handler.php b/includes/activitypub/class-handler.php index b9961d2..0199486 100644 --- a/includes/activitypub/class-handler.php +++ b/includes/activitypub/class-handler.php @@ -26,7 +26,6 @@ class Handler { * Register all ActivityPub handlers. */ public static function register_handlers() { - Accept::init(); Announce::init(); Update::init(); diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index 314bb73..b641f53 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -25,6 +25,15 @@ class Event_Sources { */ const POST_TYPE = 'ebap_event_source'; + /** + * Init. + */ + public static function init() { + self::register_post_type(); + \add_action( 'event_bridge_for_activitypub_follow', array( self::class, 'activitypub_follow_actor' ), 10, 2 ); + \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). */ @@ -146,6 +155,8 @@ class Event_Sources { return $post_id; } + self::queue_follow_actor( $actor ); + return $event_source; } diff --git a/includes/activitypub/handler/class-create.php b/includes/activitypub/handler/class-create.php index 854a084..411bbb3 100644 --- a/includes/activitypub/handler/class-create.php +++ b/includes/activitypub/handler/class-create.php @@ -7,8 +7,10 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; -use Activitypub\Notification; use Activitypub\Collection\Actors; +use Event_Bridge_For_ActivityPub\Setup; + +use function Activitypub\is_activity_public; /** * Handle Create requests. @@ -27,23 +29,32 @@ class Create { /** * Handle "Create" 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_create( $activity ) { - if ( ! isset( $activity['object'] ) ) { + public static function handle_create( $activity, $user_id ) { + // We only process activities that are target 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 `create` activity. + // Check if Activity is public or not. + if ( ! is_activity_public( $activity ) ) { return; } - // We only expect `create` 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' !== isset( $activity['object']['type'] ) ) { return; } + + $transmogrifier_class = Setup::get_transmogrifier(); + + if ( ! $transmogrifier_class ) { + return; + } + + $transmogrifier = new $transmogrifier_class( $activity['object'] ); + $transmogrifier->create(); } } diff --git a/includes/activitypub/handler/class-update.php b/includes/activitypub/handler/class-update.php index 16b7559..12e3c96 100644 --- a/includes/activitypub/handler/class-update.php +++ b/includes/activitypub/handler/class-update.php @@ -9,6 +9,7 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; use Activitypub\Notification; use Activitypub\Collection\Actors; +use Activitypub\Http; /** * Handle Update requests. diff --git a/includes/activitypub/transmogrify/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php similarity index 61% rename from includes/activitypub/transmogrify/class-gatherpress.php rename to includes/activitypub/transmogrifier/class-gatherpress.php index b94efb5..7db80d8 100644 --- a/includes/activitypub/transmogrify/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -9,7 +9,7 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transmogrify; +namespace Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier; use Activitypub\Activity\Extended_Object\Event; use Activitypub\Activity\Extended_Object\Place; @@ -20,7 +20,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use GatherPress\Core\Event as GatherPress_Event; /** - * ActivityPub Transmogrify for the GatherPress event plugin. + * ActivityPub Transmogrifier for the GatherPress event plugin. * * Handles converting incoming external ActivityPub events to GatherPress Events. * @@ -56,6 +56,27 @@ class GatherPress { * Save the ActivityPub event object as GatherPress Event. */ public function save() { - // Insert GatherPress Event here. + // Insert new GatherPress Event post. + $post_id = wp_insert_post( + array( + 'post_title' => $this->activitypub_event->get_name(), + 'post_type' => 'gatherpress_event', + 'post_content' => $this->activitypub_event->get_content(), + 'post_excerpt' => $this->activitypub_event->get_summary(), + 'post_status' => 'publish', + ) + ); + + if ( ! $post_id || is_wp_error( $post_id ) ) { + return; + } + + $event = new \GatherPress\Core\Event( $post_id ); + $params = array( + 'datetime_start' => $this->activitypub_event->get_start_time(), + 'datetime_end' => $this->activitypub_event->get_end_time(), + 'timezone' => $this->activitypub_event->get_timezone(), + ); + $event->save_datetimes( $params ); } } diff --git a/includes/admin/class-settings-page.php b/includes/admin/class-settings-page.php index 2ad8999..ac68816 100644 --- a/includes/admin/class-settings-page.php +++ b/includes/admin/class-settings-page.php @@ -173,14 +173,6 @@ class Settings_Page { $event_terms = array_merge( $event_terms, self::get_event_terms( $event_plugin ) ); } - $args = array( - 'slug' => self::SETTINGS_SLUG, - 'event_terms' => $event_terms, - ); - - \load_template( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php', true, $args ); - break; - case 'event-sources': $supports_event_sources = array(); foreach ( $event_plugins as $event_plugin ) { @@ -188,13 +180,19 @@ class Settings_Page { $supports_event_sources[ $event_plugin::class ] = $event_plugin->get_plugin_name(); } } + $args = array( + 'slug' => self::SETTINGS_SLUG, + 'event_terms' => $event_terms, 'supports_event_sources' => $supports_event_sources, ); + \load_template( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php', true, $args ); + break; + case 'event-sources': wp_enqueue_script( 'thickbox' ); wp_enqueue_style( 'thickbox' ); - \load_template( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_DIR . 'templates/event-sources.php', true, $args ); + \load_template( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_DIR . 'templates/event-sources.php', true ); break; case 'welcome': default: diff --git a/includes/class-settings.php b/includes/class-settings.php index 30d3cf4..02769ae 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -95,7 +95,7 @@ class Settings { ); \register_setting( - 'event-bridge-for-activitypub-event-sources', + 'event-bridge-for-activitypub', 'event_bridge_for_activitypub_event_sources_active', array( 'type' => 'boolean', @@ -106,7 +106,7 @@ class Settings { ); \register_setting( - 'event-bridge-for-activitypub-event-sources', + 'event-bridge-for-activitypub', 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', array( 'type' => 'array', @@ -115,6 +115,17 @@ class Settings { 'sanitize_callback' => array( self::class, 'sanitize_plugin_used_for_event_sources' ), ) ); + + \register_setting( + 'event-bridge-for-activitypub-event-sources', + 'event_bridge_for_activitypub_event_sources', + array( + 'type' => 'array', + 'description' => \__( 'Dummy setting', 'event-bridge-for-activitypub' ), + 'default' => array(), + 'sanitize_callback' => 'is_array', + ) + ); } /** diff --git a/includes/class-setup.php b/includes/class-setup.php index b381905..87bdb59 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -204,13 +204,17 @@ class Setup { } add_action( 'init', array( Health_Check::class, 'init' ) ); - add_action( 'init', array( Event_Sources_Collection::class, 'register_post_type' ) ); // Check if the minimum required version of the ActivityPub plugin is installed. if ( ! version_compare( $this->activitypub_plugin_version, EVENT_BRIDGE_FOR_ACTIVITYPUB_ACTIVITYPUB_PLUGIN_MIN_VERSION ) ) { return; } + if ( get_option( 'event_bridge_for_activitypub_event_sources_active' ) ) { + add_action( 'init', array( Event_Sources_Collection::class, 'init' ) ); + add_action( 'init', array( Handler::class, 'register_handlers' ) ); + } + add_filter( 'activitypub_transformer', array( $this, 'register_activitypub_event_transformer' ), 10, 3 ); } @@ -348,4 +352,22 @@ class Setup { self::activate_activitypub_support_for_active_event_plugins(); } + + /** + * Get the transmogrifier. + */ + public static function get_transmogrifier() { + $setup = self::get_instance(); + + $event_sources_active = get_option( 'event_bridge_for_activitypub_event_sources_active', false ); + $event_plugin = get_option( 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', '' ); + + if ( ! $event_sources_active || ! $event_plugin ) { + return; + } + $active_event_plugins = $setup->get_active_event_plugins(); + if ( array_key_exists( $event_plugin, $active_event_plugins ) ) { + return $active_event_plugins[ $event_plugin ]->get_transmogrifier_class(); + } + } } diff --git a/includes/integrations/class-event-plugin.php b/includes/integrations/class-event-plugin.php index 756926f..6c25b1a 100644 --- a/includes/integrations/class-event-plugin.php +++ b/includes/integrations/class-event-plugin.php @@ -95,4 +95,14 @@ abstract class Event_Plugin { public static function get_activitypub_event_transformer_class(): string { return str_replace( 'Integrations', 'Activitypub\Transformer', static::class ); } + + /** + * Returns the class used for transmogrifying an Event (ActivityStreams to Event plugin transformation). + */ + public static function get_transmogrifier_class(): ?string { + if ( ! self::supports_event_sources() ) { + return null; + } + return str_replace( 'Integrations', 'Activitypub\Transmogrifier', static::class ); + } } diff --git a/templates/event-sources.php b/templates/event-sources.php index 8a03ffb..4f541a5 100644 --- a/templates/event-sources.php +++ b/templates/event-sources.php @@ -17,105 +17,8 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore 'event-sources' => 'active', ) ); - -if ( ! isset( $args ) || ! array_key_exists( 'supports_event_sources', $args ) ) { - return; -} - -if ( ! current_user_can( 'manage_options' ) ) { - return; -} - -$event_plugins_supporting_event_sources = $args['supports_event_sources']; - -$selected_plugin = \get_option( 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', '' ); -$event_sources_active = \get_option( 'event_bridge_for_activitypub_event_sources_active', false ); ?> -
    -
    -

    - - - - - - - - - - - - - - - - -
    - - - - > -

    -
    - - - -

    -
    - - - -

    -

    - '; - foreach ( $plugins_supporting_event_sources as $event_plugin ) { - echo '
  • ' . esc_attr( $event_plugin->get_plugin_name() ) . '
  • '; - } - echo ''; - return; - } - ?> -
    - '; - return; - } - ?> -
    -

    diff --git a/templates/settings.php b/templates/settings.php index 842401d..c4fcc41 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -32,6 +32,15 @@ if ( ! current_user_can( 'manage_options' ) ) { return; } +if ( ! isset( $args ) || ! array_key_exists( 'supports_event_sources', $args ) ) { + return; +} + +$event_plugins_supporting_event_sources = $args['supports_event_sources']; + +$selected_plugin = \get_option( 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', '' ); +$event_sources_active = \get_option( 'event_bridge_for_activitypub_event_sources_active', false ); + $event_terms = $args['event_terms']; require_once EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_DIR . '/includes/event-categories.php'; @@ -42,7 +51,7 @@ $current_category_mapping = \get_option( 'event_bridge_for_activitypub_ev
    - +

    @@ -90,6 +99,74 @@ $current_category_mapping = \get_option( 'event_bridge_for_activitypub_ev
    +
    +

    + + + + + + + + + + + + + + +
    + + + + > +

    +
    + + + +

    +
    + +

    +

    + '; + foreach ( $plugins_supporting_event_sources as $event_plugin ) { + echo '
  • ' . esc_attr( $event_plugin->get_plugin_name() ) . '
  • '; + } + echo ''; + return; + } + ?> +
    +

    -- 2.39.5 From 16762b2b3140759e963faab10db36e47b8023f7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Tue, 10 Dec 2024 22:43:13 +0100 Subject: [PATCH 12/33] wip --- includes/activitypub/handler/class-create.php | 56 ++++++++++++++++++- .../transmogrifier/class-gatherpress.php | 2 +- includes/class-settings.php | 3 +- includes/class-setup.php | 6 +- includes/integrations/class-event-plugin.php | 2 +- 5 files changed, 62 insertions(+), 7 deletions(-) diff --git a/includes/activitypub/handler/class-create.php b/includes/activitypub/handler/class-create.php index 411bbb3..d980551 100644 --- a/includes/activitypub/handler/class-create.php +++ b/includes/activitypub/handler/class-create.php @@ -22,7 +22,15 @@ class Create { public static function init() { \add_action( 'activitypub_inbox_create', - array( self::class, 'handle_create' ) + array( self::class, 'handle_create' ), + 15, + 2 + ); + \add_filter( + 'activitypub_validate_object', + array( self::class, 'validate_object' ), + 12, + 3 ); } @@ -44,7 +52,7 @@ class Create { } // Check if an object is set. - if ( ! isset( $activity['object']['type'] ) || 'Event' !== isset( $activity['object']['type'] ) ) { + if ( ! isset( $activity['object']['type'] ) || 'Event' !== $activity['object']['type'] ) { return; } @@ -57,4 +65,48 @@ class Create { $transmogrifier = new $transmogrifier_class( $activity['object'] ); $transmogrifier->create(); } + + /** + * Validate the object. + * + * @param bool $valid The validation state. + * @param string $param The object parameter. + * @param \WP_REST_Request $request The request object. + * + * @return bool The validation state: true if valid, false if not. + */ + public static function validate_object( $valid, $param, $request ) { + $json_params = $request->get_json_params(); + + if ( isset( $json_params['object']['type'] ) && 'Event' === $json_params['object']['type'] ) { + $valid = true; + } + + if ( empty( $json_params['type'] ) ) { + return false; + } + + if ( + 'Create' !== $json_params['type'] || + is_wp_error( $request ) + ) { + return $valid; + } + + $object = $json_params['object']; + + if ( ! is_array( $object ) ) { + return false; + } + + $required = array( + 'id', + ); + + if ( array_intersect( $required, array_keys( $object ) ) !== $required ) { + return false; + } + + return $valid; + } } diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index 7db80d8..06c196d 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -55,7 +55,7 @@ class GatherPress { /** * Save the ActivityPub event object as GatherPress Event. */ - public function save() { + public function create() { // Insert new GatherPress Event post. $post_id = wp_insert_post( array( diff --git a/includes/class-settings.php b/includes/class-settings.php index 02769ae..ab88721 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -144,7 +144,8 @@ class Settings { $valid_options = array(); foreach ( $active_event_plugins as $active_event_plugin ) { if ( $active_event_plugin->supports_event_sources() ) { - $valid_options[] = $active_event_plugin::class; + $full_class = $active_event_plugin::class; + $valid_options[] = substr( $full_class, strrpos( $full_class, '\\' ) + 1 ); } } if ( in_array( $plugin, $valid_options, true ) ) { diff --git a/includes/class-setup.php b/includes/class-setup.php index 87bdb59..b0fd90c 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -366,8 +366,10 @@ class Setup { return; } $active_event_plugins = $setup->get_active_event_plugins(); - if ( array_key_exists( $event_plugin, $active_event_plugins ) ) { - return $active_event_plugins[ $event_plugin ]->get_transmogrifier_class(); + foreach ( $active_event_plugins as $active_event_plugin ) { + if ( strrpos( $active_event_plugin::class, $event_plugin ) ) { + return $active_event_plugin::get_transmogrifier_class(); + } } } } diff --git a/includes/integrations/class-event-plugin.php b/includes/integrations/class-event-plugin.php index 6c25b1a..a76e485 100644 --- a/includes/integrations/class-event-plugin.php +++ b/includes/integrations/class-event-plugin.php @@ -100,7 +100,7 @@ abstract class Event_Plugin { * Returns the class used for transmogrifying an Event (ActivityStreams to Event plugin transformation). */ public static function get_transmogrifier_class(): ?string { - if ( ! self::supports_event_sources() ) { + if ( ! static::supports_event_sources() ) { return null; } return str_replace( 'Integrations', 'Activitypub\Transmogrifier', static::class ); -- 2.39.5 From 3f5c57134e488ec8b9b97eaebcc0316f7de45d20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Wed, 11 Dec 2024 23:09:12 +0100 Subject: [PATCH 13/33] wip --- .../collection/class-event-sources.php | 2 + includes/activitypub/handler/class-create.php | 18 ++- includes/activitypub/handler/class-update.php | 34 ++++-- .../transmogrifier/class-gatherpress.php | 42 +++++++ includes/admin/class-user-interface.php | 111 ++++++++++++++++++ includes/class-setup.php | 4 +- 6 files changed, 198 insertions(+), 13 deletions(-) create mode 100644 includes/admin/class-user-interface.php diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index b641f53..feea7fb 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -34,6 +34,8 @@ 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). */ diff --git a/includes/activitypub/handler/class-create.php b/includes/activitypub/handler/class-create.php index d980551..0366259 100644 --- a/includes/activitypub/handler/class-create.php +++ b/includes/activitypub/handler/class-create.php @@ -41,7 +41,7 @@ class Create { * @param int $user_id The id of the local blog-user. */ public static function handle_create( $activity, $user_id ) { - // We only process activities that are target the application user. + // We only process activities that are target to the application user. if ( Actors::APPLICATION_USER_ID !== $user_id ) { return; } @@ -80,6 +80,8 @@ class Create { if ( isset( $json_params['object']['type'] ) && 'Event' === $json_params['object']['type'] ) { $valid = true; + } else { + return $valid; } if ( empty( $json_params['type'] ) ) { @@ -87,7 +89,7 @@ class Create { } if ( - 'Create' !== $json_params['type'] || + 'Create' !== $json_params['type'] || 'Update' !== $json_params['type'] || is_wp_error( $request ) ) { return $valid; @@ -103,10 +105,22 @@ class Create { 'id', ); + // 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; } return $valid; } + + /** + * Return the number of revisions to keep. + * + * @return int The number of revisions to keep. + */ + public static function revisions_to_keep() { + return 3; + } } diff --git a/includes/activitypub/handler/class-update.php b/includes/activitypub/handler/class-update.php index 12e3c96..a4de6eb 100644 --- a/includes/activitypub/handler/class-update.php +++ b/includes/activitypub/handler/class-update.php @@ -10,6 +10,9 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; use Activitypub\Notification; use Activitypub\Collection\Actors; use Activitypub\Http; +use Event_Bridge_For_ActivityPub\Setup; + +use function Activitypub\is_activity_public; /** * Handle Update requests. @@ -21,30 +24,41 @@ class Update { public static function init() { \add_action( 'activitypub_inbox_update', - array( self::class, 'handle_update' ) + array( self::class, 'handle_update' ), + 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_update( $activity ) { - if ( ! isset( $activity['object'] ) ) { + public static function handle_update( $activity, $user_id ) { + // We only process activities that are target 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 `Update` activity. + // Check if Activity is public or not. + if ( ! is_activity_public( $activity ) ) { return; } - // We only expect `Update` 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->update(); } } diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index 06c196d..1255acd 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -52,6 +52,15 @@ class GatherPress { $this->activitypub_event = $activitypub_event; } + /** + * Get post. + */ + 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' ) ); + } + /** * Save the ActivityPub event object as GatherPress Event. */ @@ -64,6 +73,39 @@ class GatherPress { 'post_content' => $this->activitypub_event->get_content(), 'post_excerpt' => $this->activitypub_event->get_summary(), 'post_status' => 'publish', + 'guid' => $this->activitypub_event->get_id(), + ) + ); + + if ( ! $post_id || is_wp_error( $post_id ) ) { + return; + } + + $event = new \GatherPress\Core\Event( $post_id ); + $params = array( + 'datetime_start' => $this->activitypub_event->get_start_time(), + 'datetime_end' => $this->activitypub_event->get_end_time(), + 'timezone' => $this->activitypub_event->get_timezone(), + ); + $event->save_datetimes( $params ); + } + + /** + * Save the ActivityPub event object as GatherPress Event. + */ + public function update() { + $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_type' => 'gatherpress_event', + 'post_content' => $this->activitypub_event->get_content(), + 'post_excerpt' => $this->activitypub_event->get_summary(), + 'post_status' => 'publish', + 'guid' => $this->activitypub_event->get_id(), ) ); diff --git a/includes/admin/class-user-interface.php b/includes/admin/class-user-interface.php new file mode 100644 index 0000000..b6a9c3e --- /dev/null +++ b/includes/admin/class-user-interface.php @@ -0,0 +1,111 @@ +%s', + \esc_url( $post->guid ), + \esc_html__( 'Open original page', 'event-bridge-for-activitypub' ) + ); + + return $actions; + } + + /** + * Check if a post is both an event post and external (from ActivityPub federation). + * + * @param WP_Post $post The post. + * @return bool + */ + private static function post_is_external_event_post( $post ) { + if ( 'gatherpress_event' !== $post->post_type ) { + return false; + } + return str_starts_with( $post->guid, 'https://ga.lan' ) ? true : false; + } + + /** + * Modify the user capabilities so that nobody can edit external events. + * + * @param array $caps Concerned user's capabilities. + * @param array $cap Required primitive capabilities for the requested capability. + * @param array $user_id The WordPress user ID. + * @param array $args Additional args. + * + * @return array + */ + public static function disable_editing_for_external_events( $caps, $cap, $user_id, $args ) { + if ( 'edit_post' === $cap && isset( $args[0] ) ) { + $post_id = $args[0]; + $post = get_post( $post_id ); + if ( $post && self::post_is_external_event_post( $post ) ) { + // Deny editing by returning 'do_not_allow'. + return array( 'do_not_allow' ); + } + } + return $caps; + } +} \ No newline at end of file diff --git a/includes/class-setup.php b/includes/class-setup.php index b0fd90c..b204bd6 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -21,6 +21,7 @@ use Event_Bridge_For_ActivityPub\Admin\Event_Plugin_Admin_Notices; use Event_Bridge_For_ActivityPub\Admin\General_Admin_Notices; use Event_Bridge_For_ActivityPub\Admin\Health_Check; use Event_Bridge_For_ActivityPub\Admin\Settings_Page; +use Event_Bridge_For_ActivityPub\Admin\User_Interface; use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin; require_once ABSPATH . 'wp-admin/includes/plugin.php'; @@ -212,7 +213,8 @@ class Setup { if ( get_option( 'event_bridge_for_activitypub_event_sources_active' ) ) { add_action( 'init', array( Event_Sources_Collection::class, 'init' ) ); - add_action( 'init', array( Handler::class, 'register_handlers' ) ); + add_action( 'activitypub_register_handlers', array( Handler::class, 'register_handlers' ) ); + add_action( 'admin_init', array( User_Interface::class, 'init' ) ); } add_filter( 'activitypub_transformer', array( $this, 'register_activitypub_event_transformer' ), 10, 3 ); -- 2.39.5 From be52d2705ff4db29650892b42101dee6b3dd7d79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Wed, 11 Dec 2024 23:27:32 +0100 Subject: [PATCH 14/33] add template to redirect activitypub requests for cached events --- includes/class-event-sources.php | 28 ++++++++++++++++++++++++++++ includes/class-setup.php | 1 + 2 files changed, 29 insertions(+) diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index f2701fa..ee10388 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -123,4 +123,32 @@ class Event_Sources { // Return false if no application actor is found. 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 redirect_activitypub_requests_for_cached_external_events( $template ) { + if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { + return $template; + } + + if ( ! \is_singular() ) { + return $template; + } + + global $post; + + if ( 'gatherpress_event' !== $post->post_type ) { + return $template; + } + + if ( ! str_starts_with( \get_site_url(), $post->guid ) ) { + \wp_safe_redirect( $post->guid, 301 ); + } + + return $template; + } } diff --git a/includes/class-setup.php b/includes/class-setup.php index b204bd6..c6eac0b 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -216,6 +216,7 @@ class Setup { add_action( 'activitypub_register_handlers', array( Handler::class, 'register_handlers' ) ); add_action( 'admin_init', array( User_Interface::class, 'init' ) ); } + \add_filter( 'template_include', array( \Event_Bridge_For_ActivityPub\Event_Sources::class, 'redirect_activitypub_requests_for_cached_external_events' ), 100 ); add_filter( 'activitypub_transformer', array( $this, 'register_activitypub_event_transformer' ), 10, 3 ); } -- 2.39.5 From c44d692aa4f44c6ac824e85d07402a992fe138d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Wed, 11 Dec 2024 23:32:19 +0100 Subject: [PATCH 15/33] fix --- includes/class-event-sources.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index ee10388..fe57ad9 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -13,6 +13,7 @@ use Activitypub\Activity\Extended_Object\Event; use Activitypub\Collection\Actors; use function Activitypub\get_remote_metadata_by_actor; +use function Activitypub\is_activitypub_request; /** * Class for handling and saving the ActivityPub event sources (i.e. follows). @@ -135,6 +136,10 @@ class Event_Sources { return $template; } + if ( ! is_activitypub_request() ) { + return $template; + } + if ( ! \is_singular() ) { return $template; } @@ -146,7 +151,8 @@ class Event_Sources { } if ( ! str_starts_with( \get_site_url(), $post->guid ) ) { - \wp_safe_redirect( $post->guid, 301 ); + \wp_redirect( $post->guid, 301 ); + exit; } return $template; -- 2.39.5 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 16/33] 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 ?>
    - -

    + +
    +

    +
    -

    +

    -- 2.39.5 From 55c70ce83143fa4d957f9205b6b176e49263f513 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 15 Dec 2024 09:52:20 +0100 Subject: [PATCH 17/33] wip --- includes/activitypub/handler/class-create.php | 23 +++++ .../transmogrifier/class-gatherpress.php | 98 +++++++++++++++++-- includes/admin/class-user-interface.php | 4 +- includes/class-event-sources.php | 20 ++++ includes/class-settings.php | 12 +++ includes/class-setup.php | 28 +++++- templates/settings.php | 34 ++++++- 7 files changed, 203 insertions(+), 16 deletions(-) diff --git a/includes/activitypub/handler/class-create.php b/includes/activitypub/handler/class-create.php index c8e1713..ff75359 100644 --- a/includes/activitypub/handler/class-create.php +++ b/includes/activitypub/handler/class-create.php @@ -8,6 +8,8 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; use Activitypub\Collection\Actors; +use DateTime; +use DateTimeZone; use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources; use Event_Bridge_For_ActivityPub\Setup; @@ -61,6 +63,10 @@ class Create { return; } + if ( self::is_time_passed( $activity['object']['startTime'] ) ) { + return; + } + $transmogrifier_class = Setup::get_transmogrifier(); if ( ! $transmogrifier_class ) { @@ -120,6 +126,23 @@ class Create { return $valid; } + /** + * Check if a given DateTime is already passed. + * + * @param string $time_string The ActivityPub like time string. + * @return bool + */ + private static function is_time_passed( $time_string ) { + // Create a DateTime object from the ActivityPub time string. + $time = new DateTime( $time_string, new DateTimeZone( 'UTC' ) ); + + // Get the current time in UTC. + $current_time = new DateTime( 'now', new DateTimeZone( 'UTC' ) ); + + // Compare the event time with the current time. + return $time < $current_time; + } + /** * Check if an ActivityPub actor is an event source. * diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index aadf2a7..4a52f75 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -12,8 +12,8 @@ namespace Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier; use Activitypub\Activity\Extended_Object\Event; -use Activitypub\Activity\Extended_Object\Place; use DateTime; +use Exception; use function Activitypub\object_to_uri; use function Activitypub\sanitize_url; @@ -56,6 +56,51 @@ class GatherPress { $this->activitypub_event = $activitypub_event; } + /** + * Validate a time string if it is according to the ActivityPub specification. + * + * @param string $time_string The time string. + * @return bool + */ + public static function is_valid_activitypub_time_string( $time_string ) { + // Try to create a DateTime object from the input string. + try { + $date = new DateTime( $time_string ); + } catch ( Exception $e ) { + // If parsing fails, it's not valid. + return false; + } + + // Ensure the timezone is correctly formatted (e.g., 'Z' or a valid offset). + $timezone = $date->getTimezone(); + $formatted_timezone = $timezone->getName(); + + // Return true only if the time string includes 'Z' or a valid timezone offset. + $valid = 'Z' === $formatted_timezone || preg_match( '/^[+-]\d{2}:\d{2}$/ ', $formatted_timezone ); + return $valid; + } + + /** + * Get a list of Post IDs of events that have ended. + * + * @param int $cache_retention_period Additional time buffer in seconds. + * @return int[] + */ + public static function get_past_events( $cache_retention_period = 0 ) { + global $wpdb; + + $time_limit = gmdate( 'Y-m-d H:i:s', time() - $cache_retention_period ); + + $results = $wpdb->get_col( + $wpdb->prepare( + "SELECT post_id FROM {$wpdb->prefix}gatherpress_events WHERE datetime_end < %s", + $time_limit + ) + ); + + return $results; + } + /** * Get post. */ @@ -232,6 +277,25 @@ class GatherPress { return true; } + /** + * Returns the URL of the online event link. + * + * @return ?string + */ + protected function get_online_event_link_from_attachments() { + $attachments = $this->activitypub_event->get_attachment(); + + if ( ! is_array( $attachments ) || empty( $attachments ) ) { + return; + } + + foreach ( $attachments as $attachment ) { + if ( array_key_exists( 'type', $attachment ) && 'Link' === $attachment['type'] && isset( $attachment['href'] ) ) { + return $attachment['href']; + } + } + } + /** * Add venue. * @@ -248,6 +312,17 @@ class GatherPress { return; } + // Fallback for Gancio instances. + if ( 'online' === $location['name'] ) { + $online_event_link = $this->get_online_event_link_from_attachments(); + if ( ! $online_event_link ) { + return; + } + update_post_meta( $post_id, 'gatherpress_online_event_link', sanitize_url( $online_event_link ) ); + wp_set_object_terms( $post_id, 'online-event', '_gatherpress_venue', false ); + return; + } + $venue_instance = \GatherPress\Core\Venue::get_instance(); $venue_name = sanitize_title( $location['name'] ); $venue_slug = $venue_instance->get_venue_term_slug( $venue_name ); @@ -276,14 +351,14 @@ class GatherPress { 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. + wp_set_object_terms( $post_id, $venue_slug, '_gatherpress_venue', false ); // 'true' appends to existing terms. } /** * Save the ActivityPub event object as GatherPress Event. */ public function create() { - // Insert new GatherPress Event post. + // Insert new GatherPress event post. $post_id = wp_insert_post( array( 'post_title' => sanitize_text_field( $this->activitypub_event->get_name() ), @@ -292,6 +367,10 @@ class GatherPress { 'post_excerpt' => wp_kses_post( $this->activitypub_event->get_summary() ), 'post_status' => 'publish', 'guid' => sanitize_url( $this->activitypub_event->get_id() ), + 'meta_input' => array( + 'event_bridge_for_activitypub_is_cached' => 'GatherPress', + 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, + ), ) ); @@ -299,7 +378,7 @@ class GatherPress { return; } - // Insert the Dates. + // 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(); @@ -313,18 +392,17 @@ class GatherPress { '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 as terms. + // Add hashtags. $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 ); } /** @@ -346,6 +424,10 @@ class GatherPress { 'post_excerpt' => wp_kses_post( $this->activitypub_event->get_summary() ), 'post_status' => 'publish', 'guid' => sanitize_url( $this->activitypub_event->get_id() ), + 'meta_input' => array( + 'event_bridge_for_activitypub_is_cached' => 'GatherPress', + 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, + ), ) ); diff --git a/includes/admin/class-user-interface.php b/includes/admin/class-user-interface.php index b6a9c3e..9aa00da 100644 --- a/includes/admin/class-user-interface.php +++ b/includes/admin/class-user-interface.php @@ -12,8 +12,6 @@ namespace Event_Bridge_For_ActivityPub\Admin; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore -use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin; - /** * Class responsible for Event Plugin related admin notices. * @@ -108,4 +106,4 @@ class User_Interface { } return $caps; } -} \ No newline at end of file +} diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index 760c81a..91c9f65 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -12,6 +12,9 @@ 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 Event_Bridge_For_ActivityPub\Activitypub\Transformer\GatherPress as TransformerGatherPress; +use Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier\GatherPress; +use Event_Bridge_For_ActivityPub\Integrations\GatherPress as IntegrationsGatherPress; use function Activitypub\get_remote_metadata_by_actor; use function Activitypub\is_activitypub_request; @@ -139,4 +142,21 @@ class Event_Sources { return $template; } + + /** + * Delete old cached events that took place in the past. + */ + public static function clear_cache() { + $cache_retention_period = get_option( 'event_bridge_for_activitypub_event_source_cache_retention', WEEK_IN_SECONDS ); + + $past_event_ids = GatherPress::get_past_events( $cache_retention_period ); + + foreach ( $past_event_ids as $post_id ) { + if ( has_post_thumbnail( $post_id ) ) { + $attachment_id = get_post_thumbnail_id( $post_id ); + wp_delete_attachment( $attachment_id, true ); + } + wp_delete_post( $post_id, true ); + } + } } diff --git a/includes/class-settings.php b/includes/class-settings.php index ab88721..d2dfd3a 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -105,6 +105,18 @@ class Settings { ) ); + \register_setting( + 'event-bridge-for-activitypub', + 'event_bridge_for_activitypub_event_source_cache_retention', + array( + 'type' => 'int', + 'show_in_rest' => true, + 'description' => \__( 'The cache retention period for external event sources.', 'event-bridge-for-activitypub' ), + 'default' => WEEK_IN_SECONDS, + 'sanitize_callback' => 'absint', + ) + ); + \register_setting( 'event-bridge-for-activitypub', 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', diff --git a/includes/class-setup.php b/includes/class-setup.php index 58f5778..3b65430 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -242,12 +242,36 @@ 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 ); + if ( ! wp_next_scheduled( 'event_bridge_for_activitypub_event_sources_clear_cache' ) ) { + wp_schedule_event( time(), 'daily', 'event_bridge_for_activitypub_event_sources_clear_cache' ); + } + + add_action( 'event_bridge_for_activitypub_event_sources_clear_cache', array( Event_Sources::class, 'clear_cache' ) ); + add_filter( + 'gatherpress_force_online_event_link', + function ( $force_online_event_link ) { + // Get the current post object. + $post = get_post(); + + // Check if we are in a valid context and the post type is 'gatherpress'. + if ( $post && 'gatherpress_event' === $post->post_type ) { + // Add your custom logic here to decide whether to force the link. + // For example, force it only if a specific meta field exists. + if ( get_post_meta( $post->ID, 'event_bridge_for_activitypub_is_cached', true ) ) { + return true; // Force the online event link. + } + } + + return $force_online_event_link; // Default behavior. + }, + 10, + 1 + ); } \add_filter( 'template_include', array( \Event_Bridge_For_ActivityPub\Event_Sources::class, 'redirect_activitypub_requests_for_cached_external_events' ), 100 ); - add_filter( 'activitypub_transformer', array( $this, 'register_activitypub_event_transformer' ), 10, 3 ); } diff --git a/templates/settings.php b/templates/settings.php index b56e27c..174930d 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -31,15 +31,16 @@ if ( ! isset( $args ) || ! array_key_exists( 'event_terms', $args ) ) { if ( ! current_user_can( 'manage_options' ) ) { return; } - +\get_option( 'event_bridge_for_activitypub_event_sources_active', false ); if ( ! isset( $args ) || ! array_key_exists( 'supports_event_sources', $args ) ) { return; } $event_plugins_supporting_event_sources = $args['supports_event_sources']; -$selected_plugin = \get_option( 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', '' ); -$event_sources_active = \get_option( 'event_bridge_for_activitypub_event_sources_active', false ); +$selected_plugin = \get_option( 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', '' ); +$event_sources_active = \get_option( 'event_bridge_for_activitypub_event_sources_active', false ); +$cache_retention_period = \get_option( 'event_bridge_for_activitypub_event_source_cache_retention', DAY_IN_SECONDS ); $event_terms = $args['event_terms']; @@ -145,6 +146,33 @@ $current_category_mapping = \get_option( 'event_bridge_for_activitypub_ev

    + + + + + + +

    + + -- 2.39.5 From 178beb7dd5cefdd8461fa6a407f0bed992961a94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 15 Dec 2024 12:17:32 +0100 Subject: [PATCH 18/33] add support for PostalAddress input --- .../transmogrifier/class-gatherpress.php | 166 ++++++++++-------- includes/class-event-sources.php | 12 +- includes/class-setup.php | 1 - 3 files changed, 102 insertions(+), 77 deletions(-) diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index 4a52f75..9af4b19 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -14,6 +14,7 @@ namespace Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier; use Activitypub\Activity\Extended_Object\Event; use DateTime; use Exception; +use WP_Error; use function Activitypub\object_to_uri; use function Activitypub\sanitize_url; @@ -296,6 +297,69 @@ class GatherPress { } } + /** + * Convert a PostalAddress to a string. + * + * @link https://schema.org/PostalAddress + * + * @param array $postal_address The PostalAddress as an associative array. + * @return string + */ + protected static function postal_address_to_string( $postal_address ) { + if ( ! is_array( $postal_address ) || 'PostalAddress' !== $postal_address['type'] ) { + _doing_it_wrong( + __METHOD__, + 'The parameter postal_address must be an associate array like schema.org/PostalAddress.', + esc_html( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_VERSION ) + ); + } + + $address = array(); + + $known_attributes = array( + 'streetAddress', + 'postalCode', + 'addressLocality', + 'addressState', + 'addressCountry', + ); + + foreach ( $known_attributes as $attribute ) { + if ( isset( $postal_address[ $attribute ] ) && is_string( $postal_address[ $attribute ] ) ) { + $address[] = $postal_address[ $attribute ]; + } + } + + $address_string = implode( ' ,', $address ); + + return $address_string; + } + + /** + * Convert an address to a string. + * + * @param mixed $address The address as an object, string or associative array. + * @return string + */ + protected static function address_to_string( $address ) { + if ( is_string( $address ) ) { + return $address; + } + + if ( is_object( $address ) ) { + $address = (array) $address; + } + + if ( ! is_array( $address ) || ! isset( $address['type'] ) ){ + return ''; + } + + if ( 'PostalAddress' === $address['type'] ) { + return self::postal_address_to_string( $address ); + } + return ''; + } + /** * Add venue. * @@ -342,8 +406,10 @@ class GatherPress { $venue_information = array(); - $venue_information['fullAddress'] = $location['address'] ?? ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $venue_information['phone_number'] = ''; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase + $address = $this->address_to_string(); + + $venue_information['fullAddress'] = $address; + $venue_information['phone_number'] = ''; $venue_information['website'] = ''; $venue_information['permalink'] = ''; @@ -351,86 +417,38 @@ class GatherPress { update_post_meta( $venue_id, 'gatherpress_venue_information', $venue_json ); - wp_set_object_terms( $post_id, $venue_slug, '_gatherpress_venue', false ); // 'true' appends to existing terms. + wp_set_object_terms( $post_id, $venue_slug, '_gatherpress_venue', false ); } /** * Save the ActivityPub event object as GatherPress Event. */ - public function create() { - // Insert new GatherPress event post. - $post_id = wp_insert_post( - array( - 'post_title' => sanitize_text_field( $this->activitypub_event->get_name() ), - 'post_type' => 'gatherpress_event', - 'post_content' => wp_kses_post( $this->activitypub_event->get_content() ) . '', - 'post_excerpt' => wp_kses_post( $this->activitypub_event->get_summary() ), - 'post_status' => 'publish', - 'guid' => sanitize_url( $this->activitypub_event->get_id() ), - 'meta_input' => array( - 'event_bridge_for_activitypub_is_cached' => 'GatherPress', - 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, - ), - ) - ); - - if ( ! $post_id || is_wp_error( $post_id ) ) { - return; - } - - // 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' => $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 ); - } - - /** - * Save the ActivityPub event object as GatherPress Event. - */ - public function update() { + public function save() { // 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' => sanitize_text_field( $this->activitypub_event->get_name() ), - 'post_type' => 'gatherpress_event', - 'post_content' => wp_kses_post( $this->activitypub_event->get_content() ) . '', - 'post_excerpt' => wp_kses_post( $this->activitypub_event->get_summary() ), - 'post_status' => 'publish', - 'guid' => sanitize_url( $this->activitypub_event->get_id() ), - 'meta_input' => array( - 'event_bridge_for_activitypub_is_cached' => 'GatherPress', - 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, - ), - ) + $args = array( + 'post_title' => sanitize_text_field( $this->activitypub_event->get_name() ), + 'post_type' => 'gatherpress_event', + 'post_content' => wp_kses_post( $this->activitypub_event->get_content() ) . '', + 'post_excerpt' => wp_kses_post( $this->activitypub_event->get_summary() ), + 'post_status' => 'publish', + 'guid' => sanitize_url( $this->activitypub_event->get_id() ), + 'meta_input' => array( + 'event_bridge_for_activitypub_is_cached' => 'GatherPress', + 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, + ), ); + if ( $post_id ) { + $args['ID'] = $post_id; + } + + // Insert new GatherPress Event post. + $post_id = wp_update_post( $args ); + if ( ! $post_id || is_wp_error( $post_id ) ) { return; } @@ -471,6 +489,14 @@ class GatherPress { public function delete() { $post_id = $this->get_post_id_from_activitypub_id(); + if ( ! $post_id ) { + return new WP_Error( + 'event_bridge_for_activitypub_remote_event_not_found', + \__( 'Remote event not found in cache', 'event-bridge-for-activitypub' ), + array( 'status' => 404 ) + ); + } + $thumbnail_id = get_post_thumbnail_id( $post_id ); if ( $thumbnail_id ) { diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index 91c9f65..82591d0 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -103,7 +103,11 @@ class Event_Sources { * @param WP_Post $post The WordPress post object. * @return bool */ - public static function is_cached_external_event( $post ): bool { + public static function is_cached_external_event_post( $post ): bool { + if ( 'gatherpress_event' !== $post->post_type ) { + return false; + } + if ( ! str_starts_with( \get_site_url(), $post->guid ) ) { return true; } @@ -131,11 +135,7 @@ class Event_Sources { global $post; - if ( 'gatherpress_event' !== $post->post_type ) { - return $template; - } - - if ( self::is_cached_external_event( $post ) ) { + if ( self::is_cached_external_event_post( $post ) ) { \wp_safe_redirect( $post->guid, 301 ); exit; } diff --git a/includes/class-setup.php b/includes/class-setup.php index 3b65430..cbe568e 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -15,7 +15,6 @@ 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; -- 2.39.5 From 37043e7a7b1b7d699a1660f0e404729798a2a0f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 15 Dec 2024 13:08:26 +0100 Subject: [PATCH 19/33] fix removal of event sources --- .../collection/class-event-sources.php | 31 +++++++++---------- includes/table/class-event-sources.php | 2 +- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index 21129ac..6710fb0 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -178,12 +178,23 @@ class Event_Sources { * * @param string $actor The Actor URL. * - * @return bool True on success, false on failure. + * @return WP_Post|false|null Post data on success, false or null on failure. */ public static function remove_event_source( $actor ) { - $actor = true; - self::delete_event_source_transients(); - return $actor; + $post_id = Event_Source::get_wp_post_from_activitypub_actor_id( $actor ); + + if ( ! $post_id ) { + return; + } + + $result = wp_delete_post( $post_id, true ); + + // If the deletion was successful delete all transients regarding event sources. + if ( $result ) { + self::delete_event_source_transients(); + } + + return $result; } /** @@ -292,18 +303,6 @@ class Event_Sources { return $event_sources; } - /** - * Remove a Follower. - * - * @param string $event_source The Actor URL. - * - * @return mixed True on success, false on failure. - */ - public static function remove( $event_source ) { - $post_id = Event_Source::get_wp_post_from_activitypub_actor_id( $event_source ); - return wp_delete_post( $post_id, true ); - } - /** * Queue a hook to run async. * diff --git a/includes/table/class-event-sources.php b/includes/table/class-event-sources.php index 9dcbf03..e8ecef5 100644 --- a/includes/table/class-event-sources.php +++ b/includes/table/class-event-sources.php @@ -228,7 +228,7 @@ class Event_Sources extends WP_List_Table { $event_sources = array( $event_sources ); } foreach ( $event_sources as $event_source ) { - Event_Sources_Collection::remove( $event_source ); + Event_Sources_Collection::remove_event_source( $event_source ); } } } -- 2.39.5 From cd451131fa7f19cd693507917a762fa01d374ee0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 15 Dec 2024 13:42:57 +0100 Subject: [PATCH 20/33] use blog actor --- .../collection/class-event-sources.php | 4 +-- includes/activitypub/handler/class-accept.php | 3 +- includes/activitypub/handler/class-create.php | 2 +- includes/activitypub/handler/class-delete.php | 2 +- includes/activitypub/handler/class-update.php | 4 +-- .../transmogrifier/class-gatherpress.php | 5 ++-- includes/class-event-sources.php | 28 ++++++++++++++++--- includes/class-setup.php | 5 +++- 8 files changed, 36 insertions(+), 17 deletions(-) diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index 6710fb0..24a2a74 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -374,7 +374,7 @@ class Event_Sources { $activity->set_object( $to ); $activity->set_id( $application->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); - \Activitypub\safe_remote_post( $inbox, $activity, \Activitypub\Collection\Actors::APPLICATION_USER_ID ); + \Activitypub\safe_remote_post( $inbox, $activity, \Activitypub\Collection\Actors::BLOG_USER_ID ); } /** @@ -436,7 +436,7 @@ class Event_Sources { ); $activity->set_id( $actor . '#unfollow-' . \preg_replace( '~^https?://~', '', $to ) ); $activity = $activity->to_json(); - \Activitypub\safe_remote_post( $inbox, $activity, \Activitypub\Collection\Actors::APPLICATION_USER_ID ); + \Activitypub\safe_remote_post( $inbox, $activity, \Activitypub\Collection\Actors::BLOG_USER_ID ); } /** diff --git a/includes/activitypub/handler/class-accept.php b/includes/activitypub/handler/class-accept.php index 4a86ae5..c246df9 100644 --- a/includes/activitypub/handler/class-accept.php +++ b/includes/activitypub/handler/class-accept.php @@ -8,7 +8,6 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; -use Activitypub\Notification; use Activitypub\Collection\Actors; /** @@ -43,7 +42,7 @@ class Accept { } // We only expect `Accept` activities being answers to follow requests by the application actor. - if ( Actors::APPLICATION_USER_ID !== $object->get__id() ) { + if ( Actors::BLOG_USER_ID !== $object->get__id() ) { return; } } diff --git a/includes/activitypub/handler/class-create.php b/includes/activitypub/handler/class-create.php index ff75359..e1d48db 100644 --- a/includes/activitypub/handler/class-create.php +++ b/includes/activitypub/handler/class-create.php @@ -45,7 +45,7 @@ class Create { */ public static function handle_create( $activity, $user_id ) { // We only process activities that are target to the application user. - if ( Actors::APPLICATION_USER_ID !== $user_id ) { + if ( Actors::BLOG_USER_ID !== $user_id ) { return; } diff --git a/includes/activitypub/handler/class-delete.php b/includes/activitypub/handler/class-delete.php index ec4cd17..88558f2 100644 --- a/includes/activitypub/handler/class-delete.php +++ b/includes/activitypub/handler/class-delete.php @@ -34,7 +34,7 @@ class Delete { */ 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 ) { + if ( Actors::BLOG_USER_ID !== $user_id ) { return; } diff --git a/includes/activitypub/handler/class-update.php b/includes/activitypub/handler/class-update.php index a4de6eb..a08f3a9 100644 --- a/includes/activitypub/handler/class-update.php +++ b/includes/activitypub/handler/class-update.php @@ -7,9 +7,7 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; -use Activitypub\Notification; use Activitypub\Collection\Actors; -use Activitypub\Http; use Event_Bridge_For_ActivityPub\Setup; use function Activitypub\is_activity_public; @@ -38,7 +36,7 @@ class Update { */ public static function handle_update( $activity, $user_id ) { // We only process activities that are target the application user. - if ( Actors::APPLICATION_USER_ID !== $user_id ) { + if ( Actors::BLOG_USER_ID !== $user_id ) { return; } diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index 9af4b19..a0d0db4 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -16,7 +16,6 @@ use DateTime; use Exception; use WP_Error; -use function Activitypub\object_to_uri; use function Activitypub\sanitize_url; // Exit if accessed directly. @@ -406,9 +405,9 @@ class GatherPress { $venue_information = array(); - $address = $this->address_to_string(); + $address_string = isset( $location['address'] ) ? $this->address_to_string( $location['address'] ) : ''; - $venue_information['fullAddress'] = $address; + $venue_information['fullAddress'] = $address_string; $venue_information['phone_number'] = ''; $venue_information['website'] = ''; $venue_information['permalink'] = ''; diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index 82591d0..633e4ff 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -9,12 +9,9 @@ namespace Event_Bridge_For_ActivityPub; -use Activitypub\Activity\Extended_Object\Event; -use Activitypub\Collection\Actors; +use Activitypub\Model\Blog; use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Sources_Collection; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\GatherPress as TransformerGatherPress; use Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier\GatherPress; -use Event_Bridge_For_ActivityPub\Integrations\GatherPress as IntegrationsGatherPress; use function Activitypub\get_remote_metadata_by_actor; use function Activitypub\is_activitypub_request; @@ -159,4 +156,27 @@ class Event_Sources { wp_delete_post( $post_id, true ); } } + + /** + * Add the Blog Authors to the following list of the Blog Actor + * if Blog not in single mode. + * + * @param array $follow_list The array of following urls. + * @param \Activitypub\Model\User $user The user object. + * + * @return array The array of following urls. + */ + public static function add_event_sources_to_following_collection( $follow_list, $user ) { + if ( ! $user instanceof Blog ) { + return $follow_list; + } + + $event_sources = Event_Sources_Collection::get_event_sources_ids(); + + if ( ! is_array( $event_sources ) ) { + return $follow_list; + } + + return array_merge( $follow_list, $event_sources ); + } } diff --git a/includes/class-setup.php b/includes/class-setup.php index cbe568e..3a34351 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -24,6 +24,8 @@ use Event_Bridge_For_ActivityPub\Admin\Settings_Page; use Event_Bridge_For_ActivityPub\Admin\User_Interface; use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin; +use function Activitypub\is_user_type_disabled; + require_once ABSPATH . 'wp-admin/includes/plugin.php'; /** @@ -238,7 +240,7 @@ class Setup { return; } - if ( get_option( 'event_bridge_for_activitypub_event_sources_active' ) ) { + if ( ! is_user_type_disabled( 'blog' ) && 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' ) ); @@ -249,6 +251,7 @@ class Setup { } add_action( 'event_bridge_for_activitypub_event_sources_clear_cache', array( Event_Sources::class, 'clear_cache' ) ); + add_filter( 'activitypub_rest_following', array( Event_Sources::class, 'add_event_sources_to_following_collection' ), 10, 2 ); add_filter( 'gatherpress_force_online_event_link', function ( $force_online_event_link ) { -- 2.39.5 From 6e92f2a5b87993bad32cde1385538a78560ac094 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 15 Dec 2024 13:44:57 +0100 Subject: [PATCH 21/33] phpcs --- includes/activitypub/collection/class-event-sources.php | 2 +- includes/activitypub/transmogrifier/class-gatherpress.php | 2 +- includes/class-setup.php | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index 24a2a74..fec6daf 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -178,7 +178,7 @@ class Event_Sources { * * @param string $actor The Actor URL. * - * @return WP_Post|false|null Post data on success, false or null on failure. + * @return WP_Post|false|null Post data on success, false or null on failure. */ public static function remove_event_source( $actor ) { $post_id = Event_Source::get_wp_post_from_activitypub_actor_id( $actor ); diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index a0d0db4..c7af620 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -349,7 +349,7 @@ class GatherPress { $address = (array) $address; } - if ( ! is_array( $address ) || ! isset( $address['type'] ) ){ + if ( ! is_array( $address ) || ! isset( $address['type'] ) ) { return ''; } diff --git a/includes/class-setup.php b/includes/class-setup.php index 3a34351..84b69e9 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -30,7 +30,7 @@ require_once ABSPATH . 'wp-admin/includes/plugin.php'; /** * Class Setup. - * + * This class is responsible for initializing Event Bridge for ActivityPub. * * @since 1.0.0 -- 2.39.5 From 96e0d0937cd1d69bc1e1783f0a77b6ec9a136f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 15 Dec 2024 14:08:16 +0100 Subject: [PATCH 22/33] improve admin ui --- .../event-bridge-for-activitypub-admin.css | 10 ++++++++ event-bridge-for-activitypub.php | 2 +- templates/settings.php | 25 +++++++++++++++++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/assets/css/event-bridge-for-activitypub-admin.css b/assets/css/event-bridge-for-activitypub-admin.css index 17f255f..3240749 100644 --- a/assets/css/event-bridge-for-activitypub-admin.css +++ b/assets/css/event-bridge-for-activitypub-admin.css @@ -207,3 +207,13 @@ code.event-bridge-for-activitypub-settings-example-url { .event_bridge_for_activitypub-admin-table-top > a { display: inline-block; } + +.settings_page_event-bridge-for-activitypub .notice-warning { + background: #fff; + border: 1px solid #c3c4c7; + border-left-width: 4px; + box-shadow: 0 1px 1px rgba(0,0,0,.04); + margin: 5px 15px 2px; + padding: 1px 12px; + border-left-color: #dba617; +} diff --git a/event-bridge-for-activitypub.php b/event-bridge-for-activitypub.php index 37a27ff..e3f4219 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.4 + * Version: 0.3.2.6 * Author: André Menrath * Author URI: https://graz.social/@linos * Text Domain: event-bridge-for-activitypub diff --git a/templates/settings.php b/templates/settings.php index 174930d..41b2c83 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -103,7 +103,7 @@ $current_category_mapping = \get_option( 'event_bridge_for_activitypub_ev

    @@ -179,7 +179,7 @@ $current_category_mapping = \get_option( 'event_bridge_for_activitypub_ev

    @@ -191,6 +191,27 @@ $current_category_mapping = \get_option( 'event_bridge_for_activitypub_ev } echo ''; return; + } else { + $activitypub_plugin_data = get_plugin_data( ACTIVITYPUB_PLUGIN_FILE ); + + $notice = sprintf( + /* translators: 1: The name of the ActivityPub plugin. */ + _x( + 'In order to use this feature your have to enable the Blog-Actor in the the %2$s settings.', + 'admin notice', + 'event-bridge-for-activitypub' + ), + admin_url( 'options-general.php?page=activitypub&tab=settings' ), + esc_html( $activitypub_plugin_data['Name'] ) + ); + + $allowed_html = array( + 'a' => array( + 'href' => true, + 'title' => true, + ), + ); + echo '

    ' . \wp_kses( $notice, $allowed_html ) . '

    '; } ?>
    -- 2.39.5 From 69f0cd3ccbaa4b83688cd4ec891305c1f169355c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 15 Dec 2024 16:12:24 +0100 Subject: [PATCH 23/33] fix migration to blog user --- .../collection/class-event-sources.php | 53 ++++++++++--------- includes/activitypub/handler/class-create.php | 2 +- includes/activitypub/handler/class-update.php | 2 +- .../activitypub/model/class-event-source.php | 19 ++++++- 4 files changed, 46 insertions(+), 30 deletions(-) diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index fec6daf..6e9350b 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -9,9 +9,10 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Collection; +use Activitypub\Model\Blog; +use Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source; use WP_Error; use WP_Query; -use Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source; use function Activitypub\is_tombstone; use function Activitypub\get_remote_metadata_by_actor; @@ -30,8 +31,8 @@ class Event_Sources { */ public static function init() { self::register_post_type(); - \add_action( 'event_bridge_for_activitypub_follow', array( self::class, 'activitypub_follow_actor' ), 10, 2 ); - \add_action( 'event_bridge_for_activitypub_unfollow', array( self::class, 'activitypub_unfollow_actor' ), 10, 2 ); + \add_action( 'event_bridge_for_activitypub_follow', array( self::class, 'activitypub_follow_actor' ), 10, 1 ); + \add_action( 'event_bridge_for_activitypub_unfollow', array( self::class, 'activitypub_unfollow_actor' ), 10, 1 ); } /** @@ -155,7 +156,7 @@ class Event_Sources { return $post_id; } - self::queue_follow_actor( $actor ); + $success = self::queue_follow_actor( $actor ); self::delete_event_source_transients(); @@ -181,12 +182,24 @@ class Event_Sources { * @return WP_Post|false|null Post data on success, false or null on failure. */ public static function remove_event_source( $actor ) { - $post_id = Event_Source::get_wp_post_from_activitypub_actor_id( $actor ); + $actor = Event_Source::get_by_id( $actor ); + + if ( ! $actor ) { + return; + } + + $post_id = $actor->get__id(); if ( ! $post_id ) { return; } + $thumbnail_id = get_post_thumbnail_id( $post_id ); + + if ( $thumbnail_id ) { + wp_delete_attachment( $thumbnail_id, true ); + } + $result = wp_delete_post( $post_id, true ); // If the deletion was successful delete all transients regarding event sources. @@ -336,7 +349,7 @@ class Event_Sources { public static function queue_follow_actor( $actor ) { $queued = self::queue( 'event_bridge_for_activitypub_follow', - $actor, + array( $actor ), 'event_bridge_for_activitypub_unfollow' ); @@ -344,27 +357,21 @@ class Event_Sources { } /** - * Follow an ActivityPub actor via the Application user. + * Follow an ActivityPub actor via the Blog user. * * @param string $actor_id The ID/URL of the Actor. */ public static function activitypub_follow_actor( $actor_id ) { - $post_id = Event_Source::get_wp_post_from_activitypub_actor_id( $actor_id ); + $actor = Event_Source::get_by_id( $actor_id ); - if ( ! $post_id ) { - return; - } - - $actor = Event_Source::init_from_cpt( get_post( $post_id ) ); - - if ( ! $actor instanceof Event_Source ) { + if ( ! $actor ) { return $actor; } $inbox = $actor->get_shared_inbox(); $to = $actor->get_id(); - $application = new \Activitypub\Model\Application(); + $application = new Blog(); $activity = new \Activitypub\Activity\Activity(); $activity->set_type( 'Follow' ); @@ -387,7 +394,7 @@ class Event_Sources { public static function queue_unfollow_actor( $actor ) { $queued = self::queue( 'event_bridge_for_activitypub_unfollow', - $actor, + array( $actor ), 'event_bridge_for_activitypub_follow' ); @@ -400,22 +407,16 @@ class Event_Sources { * @param string $actor_id The ActivityPub actor ID. */ public static function activitypub_unfollow_actor( $actor_id ) { - $post_id = Event_Source::get_wp_post_from_activitypub_actor_id( $actor_id ); + $actor = Event_Source::get_by_id( $actor_id ); - if ( ! $post_id ) { - return; - } - - $actor = Event_Source::init_from_cpt( get_post( $post_id ) ); - - if ( ! $actor instanceof Event_Source ) { + if ( ! $actor ) { return $actor; } $inbox = $actor->get_shared_inbox(); $to = $actor->get_id(); - $application = new \Activitypub\Model\Application(); + $application = new Blog(); if ( is_wp_error( $inbox ) ) { return $inbox; diff --git a/includes/activitypub/handler/class-create.php b/includes/activitypub/handler/class-create.php index e1d48db..5dc3809 100644 --- a/includes/activitypub/handler/class-create.php +++ b/includes/activitypub/handler/class-create.php @@ -74,7 +74,7 @@ class Create { } $transmogrifier = new $transmogrifier_class( $activity['object'] ); - $transmogrifier->create(); + $transmogrifier->save(); } /** diff --git a/includes/activitypub/handler/class-update.php b/includes/activitypub/handler/class-update.php index a08f3a9..6d77fef 100644 --- a/includes/activitypub/handler/class-update.php +++ b/includes/activitypub/handler/class-update.php @@ -57,6 +57,6 @@ class Update { } $transmogrifier = new $transmogrifier_class( $activity['object'] ); - $transmogrifier->update(); + $transmogrifier->save(); } } diff --git a/includes/activitypub/model/class-event-source.php b/includes/activitypub/model/class-event-source.php index 4ed45b8..800a82b 100644 --- a/includes/activitypub/model/class-event-source.php +++ b/includes/activitypub/model/class-event-source.php @@ -52,7 +52,7 @@ class Event_Source extends Actor { * @param string $actor_id The ActivityPub actor ID. * @return ?int The WordPress post ID if the actor is found, null if not. */ - public static function get_wp_post_from_activitypub_actor_id( $actor_id ) { + private static function get_wp_post_by_activitypub_actor_id( $actor_id ) { global $wpdb; $post_id = $wpdb->get_var( $wpdb->prepare( @@ -61,7 +61,22 @@ class Event_Source extends Actor { Event_Sources::POST_TYPE ) ); - return $post_id ? intval( $post_id ) : null; + return $post_id ? get_post( $post_id ) : null; + } + + /** + * Get the WordPress post which stores the Event Source by the ActivityPub actor id of the event source. + * + * @param string $actor_id The ActivityPub actor ID. + * @return ?Event_Source The WordPress post ID if the actor is found, null if not. + */ + public static function get_by_id( $actor_id ) { + $post = self::get_wp_post_by_activitypub_actor_id( $actor_id ); + + if ( $post ) { + $actor = self::init_from_cpt( $post ); + } + return $actor ?? null; } /** -- 2.39.5 From db4c72db868a80f9c815c9186d55ad3fe1dec2f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 15 Dec 2024 16:22:16 +0100 Subject: [PATCH 24/33] fix GatherPress transmogrifier --- includes/activitypub/transmogrifier/class-gatherpress.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index c7af620..652e041 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -442,11 +442,15 @@ class GatherPress { ); if ( $post_id ) { + // Update existing GatherPress event post. $args['ID'] = $post_id; + wp_update_post( $args ); + } else { + // Insert new GatherPress event post. + $post_id = wp_insert_post( $args ); } - // Insert new GatherPress Event post. - $post_id = wp_update_post( $args ); + if ( ! $post_id || is_wp_error( $post_id ) ) { return; -- 2.39.5 From 23f415b401f666457317fe73471d71320985682c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 15 Dec 2024 16:23:37 +0100 Subject: [PATCH 25/33] phpcs --- includes/activitypub/transmogrifier/class-gatherpress.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index 652e041..d30eefb 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -450,8 +450,6 @@ class GatherPress { $post_id = wp_insert_post( $args ); } - - if ( ! $post_id || is_wp_error( $post_id ) ) { return; } -- 2.39.5 From 0c0bba5d159ea791f01a44bccc49ec16c75c8e88 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 15 Dec 2024 17:01:15 +0100 Subject: [PATCH 26/33] fix custom summary admin UI, faulty merge with main --- .../js/event-bridge-for-activitypub-admin.js | 20 ++++++++++++ event-bridge-for-activitypub.php | 2 +- includes/class-setup.php | 31 +++++++++++++++---- 3 files changed, 46 insertions(+), 7 deletions(-) diff --git a/assets/js/event-bridge-for-activitypub-admin.js b/assets/js/event-bridge-for-activitypub-admin.js index d2eb9ea..3715707 100644 --- a/assets/js/event-bridge-for-activitypub-admin.js +++ b/assets/js/event-bridge-for-activitypub-admin.js @@ -11,4 +11,24 @@ jQuery( function( $ ) { $( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', false ); } } ); + + // Function to toggle visibility of custom details based on selected radio button. + function toggleCustomDetailsForSummary() { + if ($("#event_bridge_for_activitypub_summary_type_custom").is(':checked')) { + $("#event_bridge_for_activitypub_summary_type_custom-details").show(); + } else { + $("#event_bridge_for_activitypub_summary_type_custom-details").hide(); + } + } + + // Run the toggle function on page load. + $(document).ready(function() { + toggleCustomDetailsForSummary(); // Set the correct state on load. + + // Listen for changes on the radio buttons + $("input[name=event_bridge_for_activitypub_summary_type]").change(function() { + toggleCustomDetailsForSummary(); // Update visibility on change. + }); + }); + } ); diff --git a/event-bridge-for-activitypub.php b/event-bridge-for-activitypub.php index e3f4219..ac44073 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.6 + * Version: 0.3.2.7 * Author: André Menrath * Author URI: https://graz.social/@linos * Text Domain: event-bridge-for-activitypub diff --git a/includes/class-setup.php b/includes/class-setup.php index 84b69e9..b2cd172 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -414,22 +414,41 @@ class Setup { } /** - * Get the transmogrifier. + * Get the transmogrifier class. + * + * Retrieves the appropriate transmogrifier class based on the active event plugin. + * + * @return string|null The transmogrifier class name or null if not available. */ public static function get_transmogrifier() { + // Retrieve singleton instance. $setup = self::get_instance(); - $event_sources_active = get_option( 'event_bridge_for_activitypub_event_sources_active', false ); + // Get plugin options. + $event_sources_active = (bool) get_option( 'event_bridge_for_activitypub_event_sources_active', false ); $event_plugin = get_option( 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', '' ); - if ( ! $event_sources_active || ! $event_plugin ) { - return; + // Bail out if event sources are not active or no plugin is specified. + if ( ! $event_sources_active || empty( $event_plugin ) ) { + return null; } + + // Get the list of active event plugins. $active_event_plugins = $setup->get_active_event_plugins(); + + // Loop through active plugins to find a match. foreach ( $active_event_plugins as $active_event_plugin ) { - if ( strrpos( $active_event_plugin::class, $event_plugin ) ) { - return $active_event_plugin::get_transmogrifier_class(); + // Retrieve the class name of the active plugin. + $active_plugin_class = get_class( $active_event_plugin ); + + // Check if the active plugin class name contains the specified event plugin name. + if ( false !== strpos( $active_plugin_class, $event_plugin ) ) { + // Return the transmogrifier class provided by the plugin. + return $active_event_plugin->get_transmogrifier_class(); } } + + // Return null if no matching plugin is found. + return null; } } -- 2.39.5 From 57127064577f746bda7f125097e5e9513eaf7bbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Sun, 15 Dec 2024 22:25:28 +0100 Subject: [PATCH 27/33] refactoring --- docs/add_your_event_plugin.md | 4 +- .../collection/class-event-sources.php | 1 + includes/activitypub/handler/class-update.php | 7 +- .../activitypub/transmogrifier/class-base.php | 339 +++++++++++++++++ .../transmogrifier/class-gatherpress.php | 342 +----------------- .../class-the-events-calendar.php | 59 +++ includes/admin/class-health-check.php | 2 +- includes/admin/class-settings-page.php | 12 +- includes/admin/class-user-interface.php | 19 +- includes/class-event-sources.php | 31 +- includes/class-settings.php | 3 +- includes/class-setup.php | 94 ++--- .../integrations/class-event-organiser.php | 23 +- ...php => class-event-plugin-integration.php} | 42 +-- includes/integrations/class-eventin.php | 14 +- includes/integrations/class-eventprime.php | 13 +- .../integrations/class-events-manager.php | 14 +- includes/integrations/class-gatherpress.php | 78 +++- .../class-modern-events-calendar-lite.php | 14 +- .../class-the-events-calendar.php | 43 ++- includes/integrations/class-vs-event-list.php | 23 +- .../integrations/class-wp-event-manager.php | 23 +- .../interface-feature-event-sources.php | 42 +++ 23 files changed, 716 insertions(+), 526 deletions(-) create mode 100644 includes/activitypub/transmogrifier/class-base.php create mode 100644 includes/activitypub/transmogrifier/class-the-events-calendar.php rename includes/integrations/{class-event-plugin.php => class-event-plugin-integration.php} (73%) create mode 100644 includes/integrations/interface-feature-event-sources.php diff --git a/docs/add_your_event_plugin.md b/docs/add_your_event_plugin.md index e104340..3b79f84 100644 --- a/docs/add_your_event_plugin.md +++ b/docs/add_your_event_plugin.md @@ -28,10 +28,10 @@ First you need to add some basic information about your event plugin. Just creat final class My_Event_Plugin extends Event_Plugin { ``` -Then you need to tell the Event Bridge for ActivityPub about that class by adding it to the `EVENT_PLUGIN_CLASSES` constant in the `includes/setup.php` file: +Then you need to tell the Event Bridge for ActivityPub about that class by adding it to the `EVENT_PLUGIN_INTEGRATIONS` constant in the `includes/setup.php` file: ```php - private const EVENT_PLUGIN_CLASSES = array( + private const EVENT_PLUGIN_INTEGRATIONS = array( ... '\Event_Bridge_For_ActivityPub\Integrations\My_Event_Plugin', ); diff --git a/includes/activitypub/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index 6e9350b..79a5a93 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -31,6 +31,7 @@ class Event_Sources { */ public static function init() { self::register_post_type(); + \add_filter( 'allowed_redirect_hosts', array( self::class, 'add_event_sources_hosts_to_allowed_redirect_hosts' ) ); \add_action( 'event_bridge_for_activitypub_follow', array( self::class, 'activitypub_follow_actor' ), 10, 1 ); \add_action( 'event_bridge_for_activitypub_unfollow', array( self::class, 'activitypub_unfollow_actor' ), 10, 1 ); } diff --git a/includes/activitypub/handler/class-update.php b/includes/activitypub/handler/class-update.php index 6d77fef..06d046d 100644 --- a/includes/activitypub/handler/class-update.php +++ b/includes/activitypub/handler/class-update.php @@ -50,13 +50,12 @@ class Update { return; } - $transmogrifier_class = Setup::get_transmogrifier(); + $transmogrifier = Setup::get_transmogrifier(); - if ( ! $transmogrifier_class ) { + if ( ! $transmogrifier ) { return; } - $transmogrifier = new $transmogrifier_class( $activity['object'] ); - $transmogrifier->save(); + $transmogrifier->save( $activity['object'] ); } } diff --git a/includes/activitypub/transmogrifier/class-base.php b/includes/activitypub/transmogrifier/class-base.php new file mode 100644 index 0000000..0aceb09 --- /dev/null +++ b/includes/activitypub/transmogrifier/class-base.php @@ -0,0 +1,339 @@ +activitypub_event = $activitypub_event; + $this->save_event(); + } + + /** + * Get post. + */ + protected function get_post_id_from_activitypub_id() { + global $wpdb; + return $wpdb->get_var( + $wpdb->prepare( + "SELECT ID FROM $wpdb->posts WHERE guid=%s", + esc_sql( $this->activitypub_event->get_id() ) + ) + ); + } + + /** + * Validate a time string if it is according to the ActivityPub specification. + * + * @param string $time_string The time string. + * @return bool + */ + public static function is_valid_activitypub_time_string( $time_string ) { + // Try to create a DateTime object from the input string. + try { + $date = new DateTime( $time_string ); + } catch ( Exception $e ) { + // If parsing fails, it's not valid. + return false; + } + + // Ensure the timezone is correctly formatted (e.g., 'Z' or a valid offset). + $timezone = $date->getTimezone(); + $formatted_timezone = $timezone->getName(); + + // Return true only if the time string includes 'Z' or a valid timezone offset. + $valid = 'Z' === $formatted_timezone || preg_match( '/^[+-]\d{2}:\d{2}$/ ', $formatted_timezone ); + return $valid; + } + + /** + * 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 + */ + protected 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 + */ + protected 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 + */ + protected 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. + } + + /** + * Convert a PostalAddress to a string. + * + * @link https://schema.org/PostalAddress + * + * @param array $postal_address The PostalAddress as an associative array. + * @return string + */ + private static function postal_address_to_string( $postal_address ) { + if ( ! is_array( $postal_address ) || 'PostalAddress' !== $postal_address['type'] ) { + _doing_it_wrong( + __METHOD__, + 'The parameter postal_address must be an associate array like schema.org/PostalAddress.', + esc_html( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_VERSION ) + ); + } + + $address = array(); + + $known_attributes = array( + 'streetAddress', + 'postalCode', + 'addressLocality', + 'addressState', + 'addressCountry', + ); + + foreach ( $known_attributes as $attribute ) { + if ( isset( $postal_address[ $attribute ] ) && is_string( $postal_address[ $attribute ] ) ) { + $address[] = $postal_address[ $attribute ]; + } + } + + $address_string = implode( ' ,', $address ); + + return $address_string; + } + + /** + * Convert an address to a string. + * + * @param mixed $address The address as an object, string or associative array. + * @return string + */ + protected static function address_to_string( $address ) { + if ( is_string( $address ) ) { + return $address; + } + + if ( is_object( $address ) ) { + $address = (array) $address; + } + + if ( ! is_array( $address ) || ! isset( $address['type'] ) ) { + return ''; + } + + if ( 'PostalAddress' === $address['type'] ) { + return self::postal_address_to_string( $address ); + } + return ''; + } + + /** + * Save the ActivityPub event object as GatherPress event. + */ + public function delete() { + $post_id = $this->get_post_id_from_activitypub_id(); + + if ( ! $post_id ) { + return new WP_Error( + 'event_bridge_for_activitypub_remote_event_not_found', + \__( 'Remote event not found in cache', 'event-bridge-for-activitypub' ), + array( 'status' => 404 ) + ); + } + + $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; + } + + /** + * Returns the URL of the online event link. + * + * @return ?string + */ + protected function get_online_event_link_from_attachments() { + $attachments = $this->activitypub_event->get_attachment(); + + if ( ! is_array( $attachments ) || empty( $attachments ) ) { + return; + } + + foreach ( $attachments as $attachment ) { + if ( array_key_exists( 'type', $attachment ) && 'Link' === $attachment['type'] && isset( $attachment['href'] ) ) { + return $attachment['href']; + } + } + } +} diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index d30eefb..bb0170d 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -11,10 +11,7 @@ namespace Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier; -use Activitypub\Activity\Extended_Object\Event; use DateTime; -use Exception; -use WP_Error; use function Activitypub\sanitize_url; @@ -30,224 +27,7 @@ use GatherPress\Core\Event as GatherPress_Event; * * @since 1.0.0 */ -class GatherPress { - /** - * The current GatherPress Event object. - * - * @var Event - */ - protected $activitypub_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 array $activitypub_event The ActivityPub Event as associative array. - */ - public function __construct( $activitypub_event ) { - $activitypub_event = Event::init_from_array( $activitypub_event ); - - if ( is_wp_error( $activitypub_event ) ) { - return; - } - - $this->activitypub_event = $activitypub_event; - } - - /** - * Validate a time string if it is according to the ActivityPub specification. - * - * @param string $time_string The time string. - * @return bool - */ - public static function is_valid_activitypub_time_string( $time_string ) { - // Try to create a DateTime object from the input string. - try { - $date = new DateTime( $time_string ); - } catch ( Exception $e ) { - // If parsing fails, it's not valid. - return false; - } - - // Ensure the timezone is correctly formatted (e.g., 'Z' or a valid offset). - $timezone = $date->getTimezone(); - $formatted_timezone = $timezone->getName(); - - // Return true only if the time string includes 'Z' or a valid timezone offset. - $valid = 'Z' === $formatted_timezone || preg_match( '/^[+-]\d{2}:\d{2}$/ ', $formatted_timezone ); - return $valid; - } - - /** - * Get a list of Post IDs of events that have ended. - * - * @param int $cache_retention_period Additional time buffer in seconds. - * @return int[] - */ - public static function get_past_events( $cache_retention_period = 0 ) { - global $wpdb; - - $time_limit = gmdate( 'Y-m-d H:i:s', time() - $cache_retention_period ); - - $results = $wpdb->get_col( - $wpdb->prepare( - "SELECT post_id FROM {$wpdb->prefix}gatherpress_events WHERE datetime_end < %s", - $time_limit - ) - ); - - return $results; - } - - /** - * Get post. - */ - private function get_post_id_from_activitypub_id() { - global $wpdb; - 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. - } - +class GatherPress extends Base { /** * Add tags to post. * @@ -271,94 +51,12 @@ class GatherPress { // 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. + wp_set_object_terms( $post_id, $tag_names, 'gatherpress_topic', true ); } return true; } - /** - * Returns the URL of the online event link. - * - * @return ?string - */ - protected function get_online_event_link_from_attachments() { - $attachments = $this->activitypub_event->get_attachment(); - - if ( ! is_array( $attachments ) || empty( $attachments ) ) { - return; - } - - foreach ( $attachments as $attachment ) { - if ( array_key_exists( 'type', $attachment ) && 'Link' === $attachment['type'] && isset( $attachment['href'] ) ) { - return $attachment['href']; - } - } - } - - /** - * Convert a PostalAddress to a string. - * - * @link https://schema.org/PostalAddress - * - * @param array $postal_address The PostalAddress as an associative array. - * @return string - */ - protected static function postal_address_to_string( $postal_address ) { - if ( ! is_array( $postal_address ) || 'PostalAddress' !== $postal_address['type'] ) { - _doing_it_wrong( - __METHOD__, - 'The parameter postal_address must be an associate array like schema.org/PostalAddress.', - esc_html( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_VERSION ) - ); - } - - $address = array(); - - $known_attributes = array( - 'streetAddress', - 'postalCode', - 'addressLocality', - 'addressState', - 'addressCountry', - ); - - foreach ( $known_attributes as $attribute ) { - if ( isset( $postal_address[ $attribute ] ) && is_string( $postal_address[ $attribute ] ) ) { - $address[] = $postal_address[ $attribute ]; - } - } - - $address_string = implode( ' ,', $address ); - - return $address_string; - } - - /** - * Convert an address to a string. - * - * @param mixed $address The address as an object, string or associative array. - * @return string - */ - protected static function address_to_string( $address ) { - if ( is_string( $address ) ) { - return $address; - } - - if ( is_object( $address ) ) { - $address = (array) $address; - } - - if ( ! is_array( $address ) || ! isset( $address['type'] ) ) { - return ''; - } - - if ( 'PostalAddress' === $address['type'] ) { - return self::postal_address_to_string( $address ); - } - return ''; - } - /** * Add venue. * @@ -421,8 +119,10 @@ class GatherPress { /** * Save the ActivityPub event object as GatherPress Event. + * + * @return void */ - public function save() { + protected function save_event(): void { // Limit this as a safety measure. add_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) ); @@ -483,36 +183,4 @@ class GatherPress { // 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(); - - if ( ! $post_id ) { - return new WP_Error( - 'event_bridge_for_activitypub_remote_event_not_found', - \__( 'Remote event not found in cache', 'event-bridge-for-activitypub' ), - array( 'status' => 404 ) - ); - } - - $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/activitypub/transmogrifier/class-the-events-calendar.php b/includes/activitypub/transmogrifier/class-the-events-calendar.php new file mode 100644 index 0000000..ff08f15 --- /dev/null +++ b/includes/activitypub/transmogrifier/class-the-events-calendar.php @@ -0,0 +1,59 @@ +get_post_id_from_activitypub_id(); + + // Limit this as a safety measure. + remove_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) ); + } +} diff --git a/includes/admin/class-health-check.php b/includes/admin/class-health-check.php index 4c81c84..8364cac 100644 --- a/includes/admin/class-health-check.php +++ b/includes/admin/class-health-check.php @@ -97,7 +97,7 @@ class Health_Check { // Call the transformer Factory. $transformer = Transformer_Factory::get_transformer( $event_posts[0] ); // Check that we got the right transformer. - $desired_transformer_class = $event_plugin::get_activitypub_event_transformer_class(); + $desired_transformer_class = $event_plugin::get_activitypub_event_transformer( $event_posts[0] ); if ( $transformer instanceof $desired_transformer_class ) { return true; } diff --git a/includes/admin/class-settings-page.php b/includes/admin/class-settings-page.php index 2986bb5..54f29ed 100644 --- a/includes/admin/class-settings-page.php +++ b/includes/admin/class-settings-page.php @@ -19,6 +19,8 @@ use Event_Bridge_For_ActivityPub\ActivityPub\Model\Event_Source; use Event_Bridge_For_ActivityPub\Event_Sources; use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Source_Collection; use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin; +use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin_Integration; +use Event_Bridge_For_ActivityPub\Integrations\Feature_Event_Sources; use Event_Bridge_For_ActivityPub\Setup; /** @@ -171,15 +173,15 @@ class Settings_Page { case 'settings': $event_terms = array(); - foreach ( $event_plugins as $event_plugin ) { - $event_terms = array_merge( $event_terms, self::get_event_terms( $event_plugin ) ); + foreach ( $event_plugins as $event_plugin_integration ) { + $event_terms = array_merge( $event_terms, self::get_event_terms( $event_plugin_integration ) ); } $supports_event_sources = array(); - foreach ( $event_plugins as $event_plugin ) { - if ( $event_plugin->supports_event_sources() ) { - $supports_event_sources[ $event_plugin::class ] = $event_plugin->get_plugin_name(); + foreach ( $event_plugins as $event_plugin_integration ) { + if ( $event_plugin_integration instanceof Feature_Event_Sources && $event_plugin_integration instanceof Event_Plugin_Integration ) { + $supports_event_sources[ $event_plugin_integration::class ] = $event_plugin_integration::get_plugin_name(); } } diff --git a/includes/admin/class-user-interface.php b/includes/admin/class-user-interface.php index 9aa00da..9a3046f 100644 --- a/includes/admin/class-user-interface.php +++ b/includes/admin/class-user-interface.php @@ -9,6 +9,8 @@ namespace Event_Bridge_For_ActivityPub\Admin; +use Event_Bridge_For_ActivityPub\Event_Sources; + // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore @@ -59,7 +61,7 @@ class User_Interface { */ public static function row_actions( $actions, $post ) { // check if the post is enabled for ActivityPub. - if ( ! self::post_is_external_event_post( $post ) ) { + if ( ! Event_Sources::is_cached_external_event_post( $post ) ) { return $actions; } @@ -72,19 +74,6 @@ class User_Interface { return $actions; } - /** - * Check if a post is both an event post and external (from ActivityPub federation). - * - * @param WP_Post $post The post. - * @return bool - */ - private static function post_is_external_event_post( $post ) { - if ( 'gatherpress_event' !== $post->post_type ) { - return false; - } - return str_starts_with( $post->guid, 'https://ga.lan' ) ? true : false; - } - /** * Modify the user capabilities so that nobody can edit external events. * @@ -99,7 +88,7 @@ class User_Interface { if ( 'edit_post' === $cap && isset( $args[0] ) ) { $post_id = $args[0]; $post = get_post( $post_id ); - if ( $post && self::post_is_external_event_post( $post ) ) { + if ( $post && Event_Sources::is_cached_external_event_post( $post ) ) { // Deny editing by returning 'do_not_allow'. return array( 'do_not_allow' ); } diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index 633e4ff..25520de 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -12,6 +12,9 @@ namespace Event_Bridge_For_ActivityPub; use Activitypub\Model\Blog; use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Sources_Collection; use Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier\GatherPress; +use Event_Bridge_For_ActivityPub\Activitypub\Handler; +use Event_Bridge_For_ActivityPub\Admin\User_Interface; + use function Activitypub\get_remote_metadata_by_actor; use function Activitypub\is_activitypub_request; @@ -22,6 +25,22 @@ use function Activitypub\is_activitypub_request; * @package Event_Bridge_For_ActivityPub */ class Event_Sources { + /** + * Init. + */ + public static function init() { + \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_filter( 'activitypub_is_post_disabled', array( self::class, 'is_cached_external_post' ), 10, 2 ); + if ( ! \wp_next_scheduled( 'event_bridge_for_activitypub_event_sources_clear_cache' ) ) { + \wp_schedule_event( time(), 'daily', 'event_bridge_for_activitypub_event_sources_clear_cache' ); + } + \add_action( 'event_bridge_for_activitypub_event_sources_clear_cache', array( self::class, 'clear_cache' ) ); + \add_filter( 'activitypub_rest_following', array( self::class, 'add_event_sources_to_following_collection' ), 10, 2 ); + \add_filter( 'template_include', array( self::class, 'redirect_activitypub_requests_for_cached_external_events' ), 100 ); + } + /** * Get metadata of ActivityPub Actor by ID/URL. * @@ -88,10 +107,7 @@ class Event_Sources { if ( $disabled || ! $post ) { return $disabled; } - if ( ! str_starts_with( \get_site_url(), $post->guid ) ) { - return true; - } - return false; + return ! self::is_cached_external_event_post( $post ); } /** @@ -101,13 +117,10 @@ class Event_Sources { * @return bool */ public static function is_cached_external_event_post( $post ): bool { - if ( 'gatherpress_event' !== $post->post_type ) { - return false; - } - - if ( ! str_starts_with( \get_site_url(), $post->guid ) ) { + if ( get_post_meta( $post->id, 'event_bridge_for_activitypub_is_cached', true ) ) { return true; } + return false; } diff --git a/includes/class-settings.php b/includes/class-settings.php index d2dfd3a..d1fa7b4 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -15,6 +15,7 @@ namespace Event_Bridge_For_ActivityPub; defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Activitypub\Activity\Extended_Object\Event; +use Event_Bridge_For_ActivityPub\Integrations\Feature_Event_Sources; /** * Class responsible for the ActivityPui Event Extension related Settings. @@ -155,7 +156,7 @@ class Settings { $valid_options = array(); foreach ( $active_event_plugins as $active_event_plugin ) { - if ( $active_event_plugin->supports_event_sources() ) { + if ( $active_event_plugin instanceof Feature_Event_Sources ) { $full_class = $active_event_plugin::class; $valid_options[] = substr( $full_class, strrpos( $full_class, '\\' ) + 1 ); } diff --git a/includes/class-setup.php b/includes/class-setup.php index b2cd172..e3d3ca3 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -15,14 +15,14 @@ namespace Event_Bridge_For_ActivityPub; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore -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\ActivityPub\Transmogrifier\Base as Transmogrifier_Base; use Event_Bridge_For_ActivityPub\Admin\Event_Plugin_Admin_Notices; use Event_Bridge_For_ActivityPub\Admin\General_Admin_Notices; use Event_Bridge_For_ActivityPub\Admin\Health_Check; use Event_Bridge_For_ActivityPub\Admin\Settings_Page; use Event_Bridge_For_ActivityPub\Admin\User_Interface; use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin; +use Event_Bridge_For_ActivityPub\Integrations\Feature_Event_Sources; use function Activitypub\is_user_type_disabled; @@ -127,16 +127,16 @@ class Setup { * * @var array */ - private const EVENT_PLUGIN_CLASSES = array( - '\Event_Bridge_For_ActivityPub\Integrations\Events_Manager', - '\Event_Bridge_For_ActivityPub\Integrations\GatherPress', - '\Event_Bridge_For_ActivityPub\Integrations\The_Events_Calendar', - '\Event_Bridge_For_ActivityPub\Integrations\VS_Event_List', - '\Event_Bridge_For_ActivityPub\Integrations\WP_Event_Manager', - '\Event_Bridge_For_ActivityPub\Integrations\Eventin', - '\Event_Bridge_For_ActivityPub\Integrations\Modern_Events_Calendar_Lite', - '\Event_Bridge_For_ActivityPub\Integrations\EventPrime', - '\Event_Bridge_For_ActivityPub\Integrations\Event_Organiser', + private const EVENT_PLUGIN_INTEGRATIONS = array( + \Event_Bridge_For_ActivityPub\Integrations\Events_Manager::class, + \Event_Bridge_For_ActivityPub\Integrations\GatherPress::class, + \Event_Bridge_For_ActivityPub\Integrations\The_Events_Calendar::class, + \Event_Bridge_For_ActivityPub\Integrations\VS_Event_List::class, + \Event_Bridge_For_ActivityPub\Integrations\WP_Event_Manager::class, + \Event_Bridge_For_ActivityPub\Integrations\Eventin::class, + \Event_Bridge_For_ActivityPub\Integrations\Modern_Events_Calendar_Lite::class, + \Event_Bridge_For_ActivityPub\Integrations\EventPrime::class, + \Event_Bridge_For_ActivityPub\Integrations\Event_Organiser::class, ); /** @@ -169,13 +169,18 @@ class Setup { $all_plugins = array_merge( get_plugins(), get_mu_plugins() ); $active_event_plugins = array(); - foreach ( self::EVENT_PLUGIN_CLASSES as $event_plugin_class ) { - $event_plugin_file = call_user_func( array( $event_plugin_class, 'get_relative_plugin_file' ) ); + foreach ( self::EVENT_PLUGIN_INTEGRATIONS as $event_plugin_integration ) { + // Get the filename of the main plugin file of the event plugin (relative to the plugin dir). + $event_plugin_file = $event_plugin_integration::get_relative_plugin_file(); + + // This check should not be needed, but does not hurt. if ( ! $event_plugin_file ) { continue; } + + // Check if plugin is present on disk and is activated. if ( array_key_exists( $event_plugin_file, $all_plugins ) && \is_plugin_active( $event_plugin_file ) ) { - $active_event_plugins[ $event_plugin_file ] = new $event_plugin_class(); + $active_event_plugins[ $event_plugin_file ] = new $event_plugin_integration(); } } set_transient( 'event_bridge_for_activitypub_active_event_plugins', $active_event_plugins ); @@ -191,12 +196,9 @@ class Setup { public static function detect_event_plugins_supporting_event_sources(): array { $plugins_supporting_event_sources = array(); - foreach ( self::EVENT_PLUGIN_CLASSES as $event_plugin_class ) { - if ( ! class_exists( $event_plugin_class ) || ! method_exists( $event_plugin_class, 'get_plugin_file' ) ) { - continue; - } - if ( call_user_func( array( $event_plugin_class, 'supports_event_sources' ) ) ) { - $plugins_supporting_event_sources[] = new $event_plugin_class(); + foreach ( self::EVENT_PLUGIN_INTEGRATIONS as $event_plugin_integration ) { + if ( $event_plugin_integration instanceof Feature_Event_Sources ) { + $plugins_supporting_event_sources[] = new $event_plugin_integration(); } } return $plugins_supporting_event_sources; @@ -241,39 +243,8 @@ class Setup { } if ( ! is_user_type_disabled( 'blog' ) && 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_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 ); - if ( ! wp_next_scheduled( 'event_bridge_for_activitypub_event_sources_clear_cache' ) ) { - wp_schedule_event( time(), 'daily', 'event_bridge_for_activitypub_event_sources_clear_cache' ); - } - - add_action( 'event_bridge_for_activitypub_event_sources_clear_cache', array( Event_Sources::class, 'clear_cache' ) ); - add_filter( 'activitypub_rest_following', array( Event_Sources::class, 'add_event_sources_to_following_collection' ), 10, 2 ); - add_filter( - 'gatherpress_force_online_event_link', - function ( $force_online_event_link ) { - // Get the current post object. - $post = get_post(); - - // Check if we are in a valid context and the post type is 'gatherpress'. - if ( $post && 'gatherpress_event' === $post->post_type ) { - // Add your custom logic here to decide whether to force the link. - // For example, force it only if a specific meta field exists. - if ( get_post_meta( $post->ID, 'event_bridge_for_activitypub_is_cached', true ) ) { - return true; // Force the online event link. - } - } - - return $force_online_event_link; // Default behavior. - }, - 10, - 1 - ); + Event_Sources::init(); } - \add_filter( 'template_include', array( \Event_Bridge_For_ActivityPub\Event_Sources::class, 'redirect_activitypub_requests_for_cached_external_events' ), 100 ); add_filter( 'activitypub_transformer', array( $this, 'register_activitypub_event_transformer' ), 10, 3 ); } @@ -348,10 +319,7 @@ class Setup { // Get the transformer for a specific event plugins event-post type. foreach ( $this->active_event_plugins as $event_plugin ) { if ( $wp_object->post_type === $event_plugin->get_post_type() ) { - $transformer_class = $event_plugin::get_activitypub_event_transformer_class(); - if ( class_exists( $transformer_class ) ) { - return new $transformer_class( $wp_object, $event_plugin::get_event_category_taxonomy() ); - } + return $event_plugin::get_activitypub_event_transformer( $wp_object, $event_plugin::get_event_category_taxonomy() ); } } @@ -416,9 +384,9 @@ class Setup { /** * Get the transmogrifier class. * - * Retrieves the appropriate transmogrifier class based on the active event plugin. + * Retrieves the appropriate transmogrifier class based on the active event plugins and settings. * - * @return string|null The transmogrifier class name or null if not available. + * @return Transmogrifier_Base|null The transmogrifier class name or null if not available. */ public static function get_transmogrifier() { // Retrieve singleton instance. @@ -428,7 +396,7 @@ class Setup { $event_sources_active = (bool) get_option( 'event_bridge_for_activitypub_event_sources_active', false ); $event_plugin = get_option( 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', '' ); - // Bail out if event sources are not active or no plugin is specified. + // Exit if event sources are not active or no plugin is specified. if ( ! $event_sources_active || empty( $event_plugin ) ) { return null; } @@ -439,12 +407,12 @@ class Setup { // Loop through active plugins to find a match. foreach ( $active_event_plugins as $active_event_plugin ) { // Retrieve the class name of the active plugin. - $active_plugin_class = get_class( $active_event_plugin ); + $active_plugin_class_name = get_class( $active_event_plugin ); // Check if the active plugin class name contains the specified event plugin name. - if ( false !== strpos( $active_plugin_class, $event_plugin ) ) { + if ( false !== strpos( $active_plugin_class_name, $event_plugin ) ) { // Return the transmogrifier class provided by the plugin. - return $active_event_plugin->get_transmogrifier_class(); + return $active_event_plugin->get_transmogrifier(); } } diff --git a/includes/integrations/class-event-organiser.php b/includes/integrations/class-event-organiser.php index 0fbd190..d6c0348 100644 --- a/includes/integrations/class-event-organiser.php +++ b/includes/integrations/class-event-organiser.php @@ -11,6 +11,8 @@ namespace Event_Bridge_For_ActivityPub\Integrations; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event_Organiser as Event_Organiser_Transformer; + // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore @@ -21,7 +23,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore * * @since 1.0.0 */ -final class Event_Organiser extends Event_Plugin { +final class Event_Organiser extends Event_Plugin_Integration { /** * Returns the full plugin file. * @@ -49,15 +51,6 @@ final class Event_Organiser extends Event_Plugin { return array( 'event-organiser' ); } - /** - * Returns the ActivityPub transformer class. - * - * @return string - */ - public static function get_activitypub_transformer_class_name(): string { - return 'Event_Organiser'; - } - /** * Returns the taxonomy used for the plugin's event categories. * @@ -66,4 +59,14 @@ final class Event_Organiser extends Event_Plugin { public static function get_event_category_taxonomy(): string { return 'event-category'; } + + /** + * Returns the ActivityPub transformer for a Event_Organiser event post. + * + * @param WP_Post $post The WordPress post object of the Event. + * @return Event_Organiser_Transformer + */ + public static function get_activitypub_event_transformer( $post ): Event_Organiser_Transformer { + return new Event_Organiser_Transformer( $post, self::get_event_category_taxonomy() ); + } } diff --git a/includes/integrations/class-event-plugin.php b/includes/integrations/class-event-plugin-integration.php similarity index 73% rename from includes/integrations/class-event-plugin.php rename to includes/integrations/class-event-plugin-integration.php index fc21b04..252bb64 100644 --- a/includes/integrations/class-event-plugin.php +++ b/includes/integrations/class-event-plugin-integration.php @@ -11,11 +11,13 @@ namespace Event_Bridge_For_ActivityPub\Integrations; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event as Event_Transformer; +use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event as ActivityPub_Event_Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore +require_once EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_DIR . 'includes/integrations/interface-feature-event-sources.php'; + /** * Interface for a supported event plugin. * @@ -23,7 +25,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore * * @since 1.0.0 */ -abstract class Event_Plugin { +abstract class Event_Plugin_Integration { /** * Returns the plugin file relative to the plugins dir. * @@ -45,6 +47,14 @@ abstract class Event_Plugin { */ abstract public static function get_event_category_taxonomy(): string; + /** + * Returns the Activitypub transformer for the event plugins event post type. + * + * @param WP_Post $post The WordPress post object of the Event. + * @return ActivityPub_Event_Transformer + */ + abstract public static function get_activitypub_event_transformer( $post ): ActivityPub_Event_Transformer; + /** * Returns the IDs of the admin pages of the plugin. * @@ -54,19 +64,10 @@ abstract class Event_Plugin { return array(); } - /** - * By default event sources are not supported by an event plugin integration. - * - * @return bool True if event sources are supported. - */ - public static function supports_event_sources(): bool { - return false; - } - /** * Get the plugins name from the main plugin-file's top-level-file-comment. */ - final public static function get_plugin_name(): string { + public static function get_plugin_name(): string { $all_plugins = array_merge( get_plugins(), get_mu_plugins() ); if ( isset( $all_plugins[ static::get_relative_plugin_file() ]['Name'] ) ) { return $all_plugins[ static::get_relative_plugin_file() ]['Name']; @@ -88,21 +89,4 @@ abstract class Event_Plugin { return $is_event_plugins_edit_page || $is_event_plugins_settings_page; } - - /** - * Returns the Activitypub transformer for the event plugins event post type. - */ - public static function get_activitypub_event_transformer_class(): string { - return str_replace( 'Integrations', 'Activitypub\Transformer', static::class ); - } - - /** - * Returns the class used for transmogrifying an Event (ActivityStreams to Event plugin transformation). - */ - public static function get_transmogrifier_class(): ?string { - if ( ! static::supports_event_sources() ) { - return null; - } - return str_replace( 'Integrations', 'Activitypub\Transmogrifier', static::class ); - } } diff --git a/includes/integrations/class-eventin.php b/includes/integrations/class-eventin.php index 364a2a8..9cdb90c 100644 --- a/includes/integrations/class-eventin.php +++ b/includes/integrations/class-eventin.php @@ -11,6 +11,8 @@ namespace Event_Bridge_For_ActivityPub\Integrations; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Eventin as Eventin_Transformer; + // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore @@ -21,7 +23,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore * * @since 1.0.0 */ -final class Eventin extends Event_plugin { +final class Eventin extends Event_Plugin_Integration { /** * Returns the full plugin file. * @@ -57,4 +59,14 @@ final class Eventin extends Event_plugin { public static function get_event_category_taxonomy(): string { return 'etn_category'; } + + /** + * Returns the ActivityPub transformer for a Eventin event post. + * + * @param WP_Post $post The WordPress post object of the Event. + * @return Eventin_Transformer + */ + public static function get_activitypub_event_transformer( $post ): Eventin_Transformer { + return new Eventin_Transformer( $post, self::get_event_category_taxonomy() ); + } } diff --git a/includes/integrations/class-eventprime.php b/includes/integrations/class-eventprime.php index a50f7ff..abab86d 100644 --- a/includes/integrations/class-eventprime.php +++ b/includes/integrations/class-eventprime.php @@ -9,6 +9,8 @@ namespace Event_Bridge_For_ActivityPub\Integrations; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\EventPrime as EventPrime_Transformer; + use Activitypub\Signature; use Eventprime_Basic_Functions; @@ -20,7 +22,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore * * @since 1.0.0 */ -final class EventPrime extends Event_Plugin { +final class EventPrime extends Event_Plugin_Integration { /** * Add filter for the template inclusion. */ @@ -56,12 +58,13 @@ final class EventPrime extends Event_Plugin { } /** - * Returns the ActivityPub transformer class. + * Returns the ActivityPub transformer for a EventPrime event post. * - * @return string + * @param WP_Post $post The WordPress post object of the Event. + * @return EventPrime_Transformer */ - public static function get_activitypub_transformer_class_name(): string { - return 'EventPrime'; + public static function get_activitypub_event_transformer( $post ): EventPrime_Transformer { + return new EventPrime_Transformer( $post, self::get_event_category_taxonomy() ); } /** diff --git a/includes/integrations/class-events-manager.php b/includes/integrations/class-events-manager.php index 9208126..099a4f7 100644 --- a/includes/integrations/class-events-manager.php +++ b/includes/integrations/class-events-manager.php @@ -11,6 +11,8 @@ namespace Event_Bridge_For_ActivityPub\Integrations; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Events_Manager as Events_Manager_Transformer; + // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore @@ -21,7 +23,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore * * @since 1.0.0 */ -final class Events_Manager extends Event_Plugin { +final class Events_Manager extends Event_Plugin_Integration { /** * Returns the full plugin file. * @@ -57,4 +59,14 @@ final class Events_Manager extends Event_Plugin { public static function get_event_category_taxonomy(): string { return defined( 'EM_TAXONOMY_CATEGORY' ) ? constant( 'EM_TAXONOMY_CATEGORY' ) : 'event-categories'; } + + /** + * Returns the ActivityPub transformer for a Events_Manager event post. + * + * @param WP_Post $post The WordPress post object of the Event. + * @return Events_Manager_Transformer + */ + public static function get_activitypub_event_transformer( $post ): Events_Manager_Transformer { + return new Events_Manager_Transformer( $post, self::get_event_category_taxonomy() ); + } } diff --git a/includes/integrations/class-gatherpress.php b/includes/integrations/class-gatherpress.php index 8cc4393..1f446cc 100644 --- a/includes/integrations/class-gatherpress.php +++ b/includes/integrations/class-gatherpress.php @@ -11,6 +11,9 @@ namespace Event_Bridge_For_ActivityPub\Integrations; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\GatherPress as GatherPress_Transformer; +use Event_Bridge_For_ActivityPub\ActivityPub\Transmogrifier\GatherPress as GatherPress_Transmogrifier; + // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore @@ -21,7 +24,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore * * @since 1.0.0 */ -final class GatherPress extends Event_Plugin { +final class GatherPress extends Event_Plugin_Integration implements Feature_Event_Sources { /** * Returns the full plugin file. * @@ -49,15 +52,6 @@ final class GatherPress extends Event_Plugin { return array( class_exists( '\GatherPress\Core\Utility' ) ? \GatherPress\Core\Utility::prefix_key( 'general' ) : 'gatherpress_general' ); } - /** - * Returns the ActivityPub transformer class. - * - * @return string - */ - public static function get_activitypub_transformer_class_name(): string { - return 'GatherPress'; - } - /** * Returns the taxonomy used for the plugin's event categories. * @@ -68,11 +62,67 @@ final class GatherPress extends Event_Plugin { } /** - * GatherPress supports the Event Sources feature. + * Returns the ActivityPub transformer for a GatherPress event post. * - * @return bool True if event sources are supported. + * @param WP_Post $post The WordPress post object of the Event. + * @return GatherPress_Transformer */ - public static function supports_event_sources(): bool { - return true; + public static function get_activitypub_event_transformer( $post ): GatherPress_Transformer { + return new GatherPress_Transformer( $post, self::get_event_category_taxonomy() ); + } + + /** + * Returns the Transmogrifier for GatherPress. + */ + public static function get_transmogrifier(): GatherPress_Transmogrifier { + return new GatherPress_Transmogrifier(); + } + + /** + * Get a list of Post IDs of events that have ended. + * + * @param int $ended_before_time Filter: only get events that ended before that datetime as unix-time. + * + * @return array + */ + public static function get_cached_remote_events( $ended_before_time ): array { + global $wpdb; + + $ended_before_time_string = gmdate( 'Y-m-d H:i:s', $ended_before_time ); + + $results = $wpdb->get_col( + $wpdb->prepare( + "SELECT post_id FROM {$wpdb->prefix}gatherpress_events WHERE datetime_end < %s", + $ended_before_time_string + ) + ); + + return $results; + } + + /** + * Init function. + */ + public static function init() { + \add_filter( + 'gatherpress_force_online_event_link', + function ( $force_online_event_link ) { + // Get the current post object. + $post = get_post(); + + // Check if we are in a valid context and the post type is 'gatherpress'. + if ( $post && 'gatherpress_event' === $post->post_type ) { + // Add your custom logic here to decide whether to force the link. + // For example, force it only if a specific meta field exists. + if ( get_post_meta( $post->ID, 'event_bridge_for_activitypub_is_cached', true ) ) { + return true; // Force the online event link. + } + } + + return $force_online_event_link; // Default behavior. + }, + 10, + 1 + ); } } diff --git a/includes/integrations/class-modern-events-calendar-lite.php b/includes/integrations/class-modern-events-calendar-lite.php index 95bfef7..6abbd75 100644 --- a/includes/integrations/class-modern-events-calendar-lite.php +++ b/includes/integrations/class-modern-events-calendar-lite.php @@ -11,6 +11,8 @@ namespace Event_Bridge_For_ActivityPub\Integrations; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Modern_Events_Calendar_Lite as Modern_Events_Calendar_Lite_Transformer; + // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore @@ -21,7 +23,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore * * @since 1.0.0 */ -final class Modern_Events_Calendar_Lite extends Event_plugin { +final class Modern_Events_Calendar_Lite extends Event_Plugin_Integration { /** * Returns the full plugin file. * @@ -58,4 +60,14 @@ final class Modern_Events_Calendar_Lite extends Event_plugin { public static function get_event_category_taxonomy(): string { return 'mec_category'; } + + /** + * Returns the ActivityPub transformer for a Modern_Events_Calendar_Lite event post. + * + * @param WP_Post $post The WordPress post object of the Event. + * @return Modern_Events_Calendar_Lite_Transformer + */ + public static function get_activitypub_event_transformer( $post ): Modern_Events_Calendar_Lite_Transformer { + return new Modern_Events_Calendar_Lite_Transformer( $post, self::get_event_category_taxonomy() ); + } } diff --git a/includes/integrations/class-the-events-calendar.php b/includes/integrations/class-the-events-calendar.php index 6f6e17b..141d3fa 100644 --- a/includes/integrations/class-the-events-calendar.php +++ b/includes/integrations/class-the-events-calendar.php @@ -11,6 +11,9 @@ namespace Event_Bridge_For_ActivityPub\Integrations; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\The_Events_Calendar as The_Events_Calendar_Transformer; +use Event_Bridge_For_ActivityPub\ActivityPub\Transmogrifier\The_Events_Calendar as The_Events_Calendar_Transmogrifier; + // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore @@ -21,7 +24,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore * * @since 1.0.0 */ -final class The_Events_Calendar extends Event_plugin { +final class The_Events_Calendar extends Event_plugin_Integration implements Feature_Event_Sources { /** * Returns the full plugin file. * @@ -40,6 +43,25 @@ final class The_Events_Calendar extends Event_plugin { return class_exists( '\Tribe__Events__Main' ) ? \Tribe__Events__Main::POSTTYPE : 'tribe_event'; } + /** + * Returns the taxonomy used for the plugin's event categories. + * + * @return string + */ + public static function get_event_category_taxonomy(): string { + return class_exists( '\Tribe__Events__Main' ) ? \Tribe__Events__Main::TAXONOMY : 'tribe_events_cat'; + } + + /** + * Returns the ActivityPub transformer for a The_Events_Calendar event post. + * + * @param WP_Post $post The WordPress post object of the Event. + * @return The_Events_Calendar_Transformer + */ + public static function get_activitypub_event_transformer( $post ): The_Events_Calendar_Transformer { + return new The_Events_Calendar_Transformer( $post, self::get_event_category_taxonomy() ); + } + /** * Returns the IDs of the admin pages of the plugin. * @@ -55,11 +77,20 @@ final class The_Events_Calendar extends Event_plugin { } /** - * Returns the taxonomy used for the plugin's event categories. - * - * @return string + * Returns the Transmogrifier for The_Events_Calendar. */ - public static function get_event_category_taxonomy(): string { - return class_exists( '\Tribe__Events__Main' ) ? \Tribe__Events__Main::TAXONOMY : 'tribe_events_cat'; + public static function get_transmogrifier(): The_Events_Calendar_Transmogrifier { + return new The_Events_Calendar_Transmogrifier(); + } + + /** + * Get a list of Post IDs of events that have ended. + * + * @param int $ended_before_time Filter: only get events that ended before that datetime as unix-time. + * + * @return array + */ + public static function get_cached_remote_events( $ended_before_time ): array { + return array(); } } diff --git a/includes/integrations/class-vs-event-list.php b/includes/integrations/class-vs-event-list.php index dc2747d..17715d8 100644 --- a/includes/integrations/class-vs-event-list.php +++ b/includes/integrations/class-vs-event-list.php @@ -12,7 +12,7 @@ namespace Event_Bridge_For_ActivityPub\Integrations; -use Event_Bridge_For_ActivityPub\Event_Plugins; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\VS_Event_List as VS_Event_List_Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore @@ -24,7 +24,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore * * @since 1.0.0 */ -final class VS_Event_List extends Event_Plugin { +final class VS_Event_List extends Event_Plugin_Integration { /** * Returns the full plugin file. * @@ -52,15 +52,6 @@ final class VS_Event_List extends Event_Plugin { return array( 'settings_page_vsel' ); } - /** - * Returns the ActivityPub transformer class. - * - * @return string - */ - public static function get_activitypub_transformer_class_name(): string { - return 'VS_Event'; - } - /** * Returns the taxonomy used for the plugin's event categories. * @@ -69,4 +60,14 @@ final class VS_Event_List extends Event_Plugin { public static function get_event_category_taxonomy(): string { return 'event_cat'; } + + /** + * Returns the ActivityPub transformer for a VS_Event_List event post. + * + * @param WP_Post $post The WordPress post object of the Event. + * @return VS_Event_List_Transformer + */ + public static function get_activitypub_event_transformer( $post ): VS_Event_List_Transformer { + return new VS_Event_List_Transformer( $post, self::get_event_category_taxonomy() ); + } } diff --git a/includes/integrations/class-wp-event-manager.php b/includes/integrations/class-wp-event-manager.php index 2adc473..7e7c3e3 100644 --- a/includes/integrations/class-wp-event-manager.php +++ b/includes/integrations/class-wp-event-manager.php @@ -12,7 +12,7 @@ namespace Event_Bridge_For_ActivityPub\Integrations; -use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\WP_Event_Manager as WP_Event_Manager_Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore @@ -24,7 +24,7 @@ defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore * * @since 1.0.0 */ -final class WP_Event_Manager extends Event_Plugin { +final class WP_Event_Manager extends Event_Plugin_Integration { /** * Returns the full plugin file. * @@ -52,15 +52,6 @@ final class WP_Event_Manager extends Event_Plugin { return array( 'event-manager-settings' ); } - /** - * Returns the ActivityPub transformer class. - * - * @return string - */ - public static function get_activitypub_transformer_class_name(): string { - return 'WP_Event_Manager'; - } - /** * Returns the taxonomy used for the plugin's event categories. * @@ -69,4 +60,14 @@ final class WP_Event_Manager extends Event_Plugin { public static function get_event_category_taxonomy(): string { return 'event_listing_category'; } + + /** + * Returns the ActivityPub transformer for a WP_Event_Manager event post. + * + * @param WP_Post $post The WordPress post object of the Event. + * @return WP_Event_Manager_Transformer + */ + public static function get_activitypub_event_transformer( $post ): WP_Event_Manager_Transformer { + return new WP_Event_Manager_Transformer( $post, self::get_event_category_taxonomy() ); + } } diff --git a/includes/integrations/interface-feature-event-sources.php b/includes/integrations/interface-feature-event-sources.php new file mode 100644 index 0000000..b80f97d --- /dev/null +++ b/includes/integrations/interface-feature-event-sources.php @@ -0,0 +1,42 @@ + Date: Sun, 15 Dec 2024 23:55:13 +0100 Subject: [PATCH 28/33] Fix post_meta not being registered && some refactoring --- includes/activitypub/handler/class-create.php | 7 +- includes/activitypub/handler/class-delete.php | 7 +- .../activitypub/transmogrifier/class-base.php | 24 +++++- .../transmogrifier/class-gatherpress.php | 12 ++- includes/admin/class-user-interface.php | 10 +-- includes/class-event-sources.php | 44 ++++++++++- includes/class-settings.php | 19 +++-- includes/class-setup.php | 77 ++++++++++++------- templates/settings.php | 12 +-- 9 files changed, 138 insertions(+), 74 deletions(-) diff --git a/includes/activitypub/handler/class-create.php b/includes/activitypub/handler/class-create.php index 5dc3809..f83e42a 100644 --- a/includes/activitypub/handler/class-create.php +++ b/includes/activitypub/handler/class-create.php @@ -67,14 +67,13 @@ class Create { return; } - $transmogrifier_class = Setup::get_transmogrifier(); + $transmogrifier = Setup::get_transmogrifier(); - if ( ! $transmogrifier_class ) { + if ( ! $transmogrifier ) { return; } - $transmogrifier = new $transmogrifier_class( $activity['object'] ); - $transmogrifier->save(); + $transmogrifier->save( $activity['object'] ); } /** diff --git a/includes/activitypub/handler/class-delete.php b/includes/activitypub/handler/class-delete.php index 88558f2..9fa2224 100644 --- a/includes/activitypub/handler/class-delete.php +++ b/includes/activitypub/handler/class-delete.php @@ -47,13 +47,12 @@ class Delete { return; } - $transmogrifier_class = Setup::get_transmogrifier(); + $transmogrifier = Setup::get_transmogrifier(); - if ( ! $transmogrifier_class ) { + if ( ! $transmogrifier ) { return; } - $transmogrifier = new $transmogrifier_class( $activity['object'] ); - $transmogrifier->delete(); + $transmogrifier->delete( $activity['object'] ); } } diff --git a/includes/activitypub/transmogrifier/class-base.php b/includes/activitypub/transmogrifier/class-base.php index 0aceb09..b32d264 100644 --- a/includes/activitypub/transmogrifier/class-base.php +++ b/includes/activitypub/transmogrifier/class-base.php @@ -34,6 +34,8 @@ abstract class Base { /** * Internal function to actually save the event. + * + * @return false|int Post-ID on success, false on failure. */ abstract protected function save_event(); @@ -50,7 +52,13 @@ abstract class Base { } $this->activitypub_event = $activitypub_event; - $this->save_event(); + + $post_id = $this->save_event(); + + if ( $post_id ) { + update_post_meta( $post_id, 'event_bridge_for_activitypub_is_cached', 'yes' ); + update_post_meta( $post_id, 'activitypub_content_visibility', constant( 'ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL' ) ?? '' ); + } } /** @@ -287,9 +295,19 @@ abstract class Base { } /** - * Save the ActivityPub event object as GatherPress event. + * Delete a local event in WordPress that is a cached remote one. + * + * @param array $activitypub_event The ActivityPub event as associative array. */ - public function delete() { + public function delete( $activitypub_event ) { + $activitypub_event = Event::init_from_array( $activitypub_event ); + + if ( is_wp_error( $activitypub_event ) ) { + return; + } + + $this->activitypub_event = $activitypub_event; + $post_id = $this->get_post_id_from_activitypub_id(); if ( ! $post_id ) { diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index bb0170d..78128c6 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -120,9 +120,9 @@ class GatherPress extends Base { /** * Save the ActivityPub event object as GatherPress Event. * - * @return void + * @return false|int */ - protected function save_event(): void { + protected function save_event() { // Limit this as a safety measure. add_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) ); @@ -135,10 +135,6 @@ class GatherPress extends Base { 'post_excerpt' => wp_kses_post( $this->activitypub_event->get_summary() ), 'post_status' => 'publish', 'guid' => sanitize_url( $this->activitypub_event->get_id() ), - 'meta_input' => array( - 'event_bridge_for_activitypub_is_cached' => 'GatherPress', - 'activitypub_content_visibility' => ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, - ), ); if ( $post_id ) { @@ -151,7 +147,7 @@ class GatherPress extends Base { } if ( ! $post_id || is_wp_error( $post_id ) ) { - return; + return false; } // Insert the dates. @@ -182,5 +178,7 @@ class GatherPress extends Base { // Limit this as a safety measure. remove_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) ); + + return $post_id; } } diff --git a/includes/admin/class-user-interface.php b/includes/admin/class-user-interface.php index 9a3046f..52381e3 100644 --- a/includes/admin/class-user-interface.php +++ b/includes/admin/class-user-interface.php @@ -28,15 +28,7 @@ class User_Interface { public static function init() { \add_filter( 'page_row_actions', array( self::class, 'row_actions' ), 10, 2 ); \add_filter( 'post_row_actions', array( self::class, 'row_actions' ), 10, 2 ); - \add_action( - 'admin_init', - \add_filter( - 'map_meta_cap', - array( self::class, 'disable_editing_for_external_events' ), - 10, - 4 - ) - ); + \add_filter( 'map_meta_cap', array( self::class, 'disable_editing_for_external_events' ), 10, 4 ); } /** diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index 25520de..8b8f629 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -14,7 +14,8 @@ use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_S use Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier\GatherPress; use Event_Bridge_For_ActivityPub\Activitypub\Handler; use Event_Bridge_For_ActivityPub\Admin\User_Interface; - +use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin_Integration; +use Event_Bridge_For_ActivityPub\Integrations\Feature_Event_Sources; use function Activitypub\get_remote_metadata_by_actor; use function Activitypub\is_activitypub_request; @@ -31,7 +32,8 @@ class Event_Sources { public static function init() { \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( 'init', array( User_Interface::class, 'init' ) ); + \add_action( 'init', array( self::class, 'register_post_meta' ) ); \add_filter( 'activitypub_is_post_disabled', array( self::class, 'is_cached_external_post' ), 10, 2 ); if ( ! \wp_next_scheduled( 'event_bridge_for_activitypub_event_sources_clear_cache' ) ) { \wp_schedule_event( time(), 'daily', 'event_bridge_for_activitypub_event_sources_clear_cache' ); @@ -41,6 +43,31 @@ class Event_Sources { \add_filter( 'template_include', array( self::class, 'redirect_activitypub_requests_for_cached_external_events' ), 100 ); } + + /** + * Register post meta. + */ + public static function register_post_meta() { + $setup = Setup::get_instance(); + + foreach ( $setup->get_active_event_plugins() as $event_plugin_integration ) { + if ( ! $event_plugin_integration instanceof Feature_Event_Sources && $event_plugin_integration instanceof Event_Plugin_Integration ) { + continue; + } + \register_post_meta( + $event_plugin_integration::get_post_type(), + 'event_bridge_for_activitypub_is_cached', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => function ( $value ) { + return esc_sql( $value ); + }, + ) + ); + } + } + /** * Get metadata of ActivityPub Actor by ID/URL. * @@ -117,7 +144,7 @@ class Event_Sources { * @return bool */ public static function is_cached_external_event_post( $post ): bool { - if ( get_post_meta( $post->id, 'event_bridge_for_activitypub_is_cached', true ) ) { + if ( get_post_meta( $post->ID, 'event_bridge_for_activitypub_is_cached', true ) ) { return true; } @@ -157,9 +184,18 @@ class Event_Sources { * Delete old cached events that took place in the past. */ public static function clear_cache() { + // Get the event plugin integration that is used. + $event_plugin_integration = Setup::get_event_plugin_integration_used_for_event_sources_feature(); + + if ( ! $event_plugin_integration ) { + return; + } + $cache_retention_period = get_option( 'event_bridge_for_activitypub_event_source_cache_retention', WEEK_IN_SECONDS ); - $past_event_ids = GatherPress::get_past_events( $cache_retention_period ); + $ended_before_time = gmdate( 'Y-m-d H:i:s', time() - $cache_retention_period ); + + $past_event_ids = $event_plugin_integration::get_cached_remote_events( $ended_before_time ); foreach ( $past_event_ids as $post_id ) { if ( has_post_thumbnail( $post_id ) ) { diff --git a/includes/class-settings.php b/includes/class-settings.php index d1fa7b4..a9a91f8 100644 --- a/includes/class-settings.php +++ b/includes/class-settings.php @@ -120,12 +120,12 @@ class Settings { \register_setting( 'event-bridge-for-activitypub', - 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', + 'event_bridge_for_activitypub_integration_used_for_event_sources_feature', array( 'type' => 'array', 'description' => \__( 'Define which plugin/integration is used for the event sources feature', 'event-bridge-for-activitypub' ), 'default' => array(), - 'sanitize_callback' => array( self::class, 'sanitize_plugin_used_for_event_sources' ), + 'sanitize_callback' => array( self::class, 'sanitize_event_plugin_integration_used_for_event_sources' ), ) ); @@ -144,11 +144,11 @@ class Settings { /** * Sanitize the option which event plugin. * - * @param string $plugin The setting. + * @param string $event_plugin_integration The setting. * @return string */ - public static function sanitize_plugin_used_for_event_sources( $plugin ) { - if ( ! is_string( $plugin ) ) { + public static function sanitize_event_plugin_integration_used_for_event_sources( $event_plugin_integration ) { + if ( ! is_string( $event_plugin_integration ) ) { return ''; } $setup = Setup::get_instance(); @@ -157,14 +157,13 @@ class Settings { $valid_options = array(); foreach ( $active_event_plugins as $active_event_plugin ) { if ( $active_event_plugin instanceof Feature_Event_Sources ) { - $full_class = $active_event_plugin::class; - $valid_options[] = substr( $full_class, strrpos( $full_class, '\\' ) + 1 ); + $valid_options[] = $active_event_plugin::class; } } - if ( in_array( $plugin, $valid_options, true ) ) { - return $plugin; + if ( in_array( $event_plugin_integration, $valid_options, true ) ) { + return $event_plugin_integration; } - return ''; + return Setup::get_default_integration_class_name_used_for_event_sources_feature(); } /** diff --git a/includes/class-setup.php b/includes/class-setup.php index e3d3ca3..1c42639 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -15,12 +15,11 @@ namespace Event_Bridge_For_ActivityPub; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore -use Event_Bridge_For_ActivityPub\ActivityPub\Transmogrifier\Base as Transmogrifier_Base; +use Event_Bridge_For_ActivityPub\ActivityPub\Transmogrifier\Base as Transmogrifier; use Event_Bridge_For_ActivityPub\Admin\Event_Plugin_Admin_Notices; use Event_Bridge_For_ActivityPub\Admin\General_Admin_Notices; use Event_Bridge_For_ActivityPub\Admin\Health_Check; use Event_Bridge_For_ActivityPub\Admin\Settings_Page; -use Event_Bridge_For_ActivityPub\Admin\User_Interface; use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin; use Event_Bridge_For_ActivityPub\Integrations\Feature_Event_Sources; @@ -381,42 +380,66 @@ class Setup { self::activate_activitypub_support_for_active_event_plugins(); } + /** + * Get the event plugin integration class name used for the event sources feature. + * + * @return string The class name of the event plugin integration class. + */ + public static function get_event_plugin_integration_used_for_event_sources_feature() { + // Get plugin option. + $event_plugin_integration = get_option( 'event_bridge_for_activitypub_integration_used_for_event_sources_feature', '' ); + + // Exit if event sources are not active or no plugin is specified. + if ( empty( $event_plugin_integration ) ) { + return null; + } + + // Validate if setting is actual existing class. + if ( ! class_exists( $event_plugin_integration ) ) { + return null; + } + + return $event_plugin_integration; + } + /** * Get the transmogrifier class. * * Retrieves the appropriate transmogrifier class based on the active event plugins and settings. * - * @return Transmogrifier_Base|null The transmogrifier class name or null if not available. + * @return ?Transmogrifier The transmogrifier class name or null if not available. */ - public static function get_transmogrifier() { - // Retrieve singleton instance. - $setup = self::get_instance(); + public static function get_transmogrifier(): ?Transmogrifier { + $event_plugin_integration = self::get_event_plugin_integration_used_for_event_sources_feature(); - // Get plugin options. - $event_sources_active = (bool) get_option( 'event_bridge_for_activitypub_event_sources_active', false ); - $event_plugin = get_option( 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', '' ); - - // Exit if event sources are not active or no plugin is specified. - if ( ! $event_sources_active || empty( $event_plugin ) ) { + if ( ! $event_plugin_integration ) { return null; } - // Get the list of active event plugins. - $active_event_plugins = $setup->get_active_event_plugins(); - - // Loop through active plugins to find a match. - foreach ( $active_event_plugins as $active_event_plugin ) { - // Retrieve the class name of the active plugin. - $active_plugin_class_name = get_class( $active_event_plugin ); - - // Check if the active plugin class name contains the specified event plugin name. - if ( false !== strpos( $active_plugin_class_name, $event_plugin ) ) { - // Return the transmogrifier class provided by the plugin. - return $active_event_plugin->get_transmogrifier(); - } + // Validate if get_transformer method exists in event plugin integration. + if ( ! method_exists( $event_plugin_integration, 'get_transmogrifier' ) ) { + return null; } - // Return null if no matching plugin is found. - return null; + $transmogrifier = $event_plugin_integration::get_transmogrifier(); + + return $transmogrifier; + } + + /** + * Get the full class name of the first event plugin integration that is active and supports the event source feature. + * + * @return string The full class name of the event plugin integration. + */ + public static function get_default_integration_class_name_used_for_event_sources_feature(): string { + $setup = self::get_instance(); + + $event_plugin_integrations = $setup->get_active_event_plugins(); + foreach ( $event_plugin_integrations as $event_plugin_integration ) { + if ( $event_plugin_integration instanceof Feature_Event_Sources ) { + return $event_plugin_integration::class; + } + } + return ''; } } diff --git a/templates/settings.php b/templates/settings.php index 41b2c83..860fdaa 100644 --- a/templates/settings.php +++ b/templates/settings.php @@ -38,7 +38,7 @@ if ( ! isset( $args ) || ! array_key_exists( 'supports_event_sources', $args ) ) $event_plugins_supporting_event_sources = $args['supports_event_sources']; -$selected_plugin = \get_option( 'event_bridge_for_activitypub_plugin_used_for_event_source_feature', '' ); +$selected_plugin = \get_option( 'event_bridge_for_activitypub_integration_used_for_event_sources_feature', '' ); $event_sources_active = \get_option( 'event_bridge_for_activitypub_event_sources_active', false ); $cache_retention_period = \get_option( 'event_bridge_for_activitypub_event_source_cache_retention', DAY_IN_SECONDS ); @@ -128,18 +128,18 @@ $current_category_mapping = \get_option( 'event_bridge_for_activitypub_ev ?> - + -- 2.39.5 From e12a0734a55aeadf239ccbdfbde4db9366de6cdf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Mon, 16 Dec 2024 00:58:47 +0100 Subject: [PATCH 29/33] some progress on The Events Calendar Transmogrifier --- ...s-the-events-calendar-event-repository.php | 47 ++++++++++++++++++ .../class-the-events-calendar.php | 49 +++++++++++++++++-- 2 files changed, 91 insertions(+), 5 deletions(-) create mode 100644 includes/activitypub/transmogrifier/class-the-events-calendar-event-repository.php diff --git a/includes/activitypub/transmogrifier/class-the-events-calendar-event-repository.php b/includes/activitypub/transmogrifier/class-the-events-calendar-event-repository.php new file mode 100644 index 0000000..6952087 --- /dev/null +++ b/includes/activitypub/transmogrifier/class-the-events-calendar-event-repository.php @@ -0,0 +1,47 @@ +get_post_id_from_activitypub_id(); + $post_id = $this->get_post_id_from_activitypub_id(); + + $duration = $this->get_duration(); + + $args = array( + 'title' => sanitize_text_field( $this->activitypub_event->get_name() ), + 'content' => wp_kses_post( $this->activitypub_event->get_content() ), + 'start_date' => gmdate( 'Y-m-d H:i:s', strtotime( $this->activitypub_event->get_start_time() ) ), + 'duration' => $duration, + 'status' => 'publish', + 'guid' => sanitize_url( $this->activitypub_event->get_id() ), + ); + + $tribe_event = new The_Events_Calendar_Event_Repository(); + + if ( $post_id ) { + $args['post_title'] = $args['title']; + $args['post_content'] = $args['content']; + // Update existing GatherPress event post. + $post = \Tribe__Events__API::updateEvent( $post_id, $args ); + } else { + $post = $tribe_event->set_args( $args )->create(); + } + + if ( ! $post ) { + return false; + } // Limit this as a safety measure. remove_filter( 'wp_revisions_to_keep', array( self::class, 'revisions_to_keep' ) ); + + return $post->ID; + } + + /** + * Get the events duration in seconds. + * + * @return int + */ + private function get_duration() { + $end_time = $this->activitypub_event->get_end_time(); + if ( ! $end_time ) { + return 2 * HOUR_IN_SECONDS; + } + return abs( strtotime( $end_time ) - strtotime( $this->activitypub_event->get_start_time() ) ); } } -- 2.39.5 From c2856a12bd3cddf4fd0d5c5f505583de470e01b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Mon, 16 Dec 2024 01:15:01 +0100 Subject: [PATCH 30/33] phpcs --- includes/class-setup.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/class-setup.php b/includes/class-setup.php index 1c42639..d74aab5 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -245,6 +245,7 @@ class Setup { Event_Sources::init(); } add_filter( 'activitypub_transformer', array( $this, 'register_activitypub_event_transformer' ), 10, 3 ); + self::get_default_integration_class_name_used_for_event_sources_feature(); } /** @@ -437,7 +438,7 @@ class Setup { $event_plugin_integrations = $setup->get_active_event_plugins(); foreach ( $event_plugin_integrations as $event_plugin_integration ) { if ( $event_plugin_integration instanceof Feature_Event_Sources ) { - return $event_plugin_integration::class; + get_class( $event_plugin_integration ); } } return ''; -- 2.39.5 From b5a199fe9cc10ed6a2d19b1b5c8104c9acce44ef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Mon, 16 Dec 2024 17:36:23 +0100 Subject: [PATCH 31/33] cleanup and enhancement of documentation --- .gitignore | 1 + composer.json | 2 +- docs/README.md | 71 +++++++++++++ docs/event_plugin_integrations.md | 50 +++++++++ ...dd_your_event_plugin.md => transformer.md} | 37 +------ includes/activitypub/class-handler.php | 92 ++++++++++++++++ .../collection/class-event-sources.php | 24 ++++- includes/activitypub/handler/class-accept.php | 4 +- includes/activitypub/handler/class-create.php | 100 +----------------- includes/activitypub/handler/class-delete.php | 9 +- includes/activitypub/handler/class-update.php | 15 ++- .../activitypub/model/class-event-source.php | 8 ++ .../transformer/class-event-organiser.php | 4 +- .../activitypub/transformer/class-event.php | 2 +- .../activitypub/transformer/class-eventin.php | 4 +- .../transformer/class-eventprime.php | 4 +- .../transformer/class-events-manager.php | 4 +- .../transformer/class-gatherpress.php | 4 +- .../class-modern-events-calendar-lite.php | 4 +- .../transformer/class-the-events-calendar.php | 4 +- .../transformer/class-vs-event-list.php | 4 +- .../transformer/class-wp-event-manager.php | 4 +- .../activitypub/transmogrifier/class-base.php | 2 +- .../transmogrifier/class-gatherpress.php | 2 +- ...s-the-events-calendar-event-repository.php | 2 +- .../class-the-events-calendar.php | 2 +- includes/class-event-sources.php | 18 +++- includes/class-setup.php | 31 ++++-- .../integrations/class-event-organiser.php | 8 +- .../class-event-plugin-integration.php | 10 +- includes/integrations/class-eventin.php | 12 ++- includes/integrations/class-eventprime.php | 10 +- .../integrations/class-events-manager.php | 8 +- includes/integrations/class-gatherpress.php | 8 +- .../class-modern-events-calendar-lite.php | 8 +- .../class-the-events-calendar.php | 8 +- includes/integrations/class-vs-event-list.php | 7 +- .../integrations/class-wp-event-manager.php | 4 +- .../interface-feature-event-sources.php | 6 +- includes/table/class-event-sources.php | 3 + templates/welcome.php | 2 +- ...ss-activitypub-event-bridge-shortcodes.php | 10 +- tests/test-class-plugin-event-organiser.php | 2 +- tests/test-class-plugin-eventin.php | 2 +- tests/test-class-plugin-eventprime.php | 2 +- tests/test-class-plugin-events-manger.php | 2 +- tests/test-class-plugin-gatherpress.php | 2 +- ...ass-plugin-modern-events-calendar-lite.php | 2 +- .../test-class-plugin-the-events-calendar.php | 2 +- tests/test-class-plugin-vs-event-list.php | 2 +- tests/test-class-plugin-wp-event-manager.php | 2 +- 51 files changed, 406 insertions(+), 224 deletions(-) create mode 100644 docs/README.md create mode 100644 docs/event_plugin_integrations.md rename docs/{add_your_event_plugin.md => transformer.md} (88%) diff --git a/.gitignore b/.gitignore index e9adf39..fd51142 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ composer.lock .phpunit.result.cache node_modules/ package-lock.json +.phpdoc diff --git a/composer.json b/composer.json index 4f88074..1b77eba 100644 --- a/composer.json +++ b/composer.json @@ -61,7 +61,7 @@ ], "test-debug": [ "@prepare-test", - "@test-event-bridge-for-activitypub-event-sources" + "@test-the-events-calendar" ], "test-vs-event-list": "phpunit --filter=vs_event_list", "test-the-events-calendar": "phpunit --filter=the_events_calendar", diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..9ac1310 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,71 @@ +## Developer documentation + +### Overview + +The entry point of the plugin is the initialization of the singleton class `\Event_Bridge_For_ActivityPub\Setup` in the main plugin file `event-bridge-for-activitypub.php`. +The constructor of that class calls its `setup_hooks()` function. This function provides hooks that initialize all parts of the _Event Bridge For ActivityPub_ whenever needed. + +### File structure + +Note that almost all files and folder within the `activitypub` folders are structured the same way as in the WordPress ActivityPub plugin. + +### Event Plugin Integrations + +This plugin supports multiple event plugins, even at the same time. To add a new one you first need to add some basic information about your event plugin. Just create a new file in `./includes/integrations/my-event-plugin.php`. Implement at least all abstract functions of the `Event_Plugin_Integration` class. + +#### Basic Event Plugin Integration + +```php + namespace Event_Bridge_For_ActivityPub\Integrations; + + // Exit if accessed directly. + defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore + + /** + * Integration information for My Event Plugin + * + * This class defines necessary meta information is for the integration of My Event Plugin with the ActivityPub plugin. + */ + final class My_Event_Plugin extends Event_Plugin_Integration { +``` + +#### Registering an Event Plugin Integration + +Then you need to tell the Event Bridge for ActivityPub about that class by adding it to the `EVENT_PLUGIN_INTEGRATIONS` constant in the `includes/setup.php` file: + +```php + private const EVENT_PLUGIN_INTEGRATIONS = array( + ... + \Event_Bridge_For_ActivityPub\Integrations\My_Event_Plugin::class, + ); +``` + +#### Additional Feature: Event Sources + +Not all _Event Plugin Integrations_ support the event-sources feature. To add support for it an integration must implement the `Feature_Event_Sources` interface. + +```php +namespace Event_Bridge_For_ActivityPub\Integrations; + +// Exit if accessed directly. +defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore + + /** + * Integration information for My Event Plugin. + * + * This class defines necessary meta information is for the integration of My Event Plugin with the ActivityPub plugin. + * This integration supports the Event Sources Feature. + */ +final class GatherPress extends Event_Plugin_Integration implements Feature_Event_Sources { +``` + +### Transformer + +Transformers are the classes that convert an WordPress post type (e.g. one used for events) to _ActivityStreams_. +The Event Bridge for ActivityPub then takes care of applying the transformer, so you can jump right into [implementing it](./implement_an_activitypub_event_transformer.md). + +### Event Sources Feature – Transmogrifier + +The event sources feature allows to aggregate events from external ActivityPub actors. As the initialization of the Event-Sources feature is quite complex all of that initialization is done in the `init` function of the file `includes/class-event-sources.php` which is called by the `\Event_Bridge_For_ActivityPub\Setup` class (`includes/class-setup.php`). + +In this plugin we call a **_Transmogrifier_** the **opposite** of a **_Transformer_**. It takes care of converting an ActivityPub (`Event`) object in _ActivityStreams_ to the WordPress representation of an event plugin. The transmogrifier classes are only used when the `Event Sources` feature is activated. The _Event Bridge For ActivityPub_ can register transformers for multiple event plugins at the same time, however only one transmogrifier, that is one target even plugin can be used as a target for incoming external ActivityPub `Event` objects. diff --git a/docs/event_plugin_integrations.md b/docs/event_plugin_integrations.md new file mode 100644 index 0000000..e654184 --- /dev/null +++ b/docs/event_plugin_integrations.md @@ -0,0 +1,50 @@ + +## Event Plugin Integrations + +First you need to add some basic information about your event plugin. Just create a new file in `./includes/integrations/my-event-plugin.php`. Implement at least all abstract functions of the `Event_Plugin_Integration` class. + +### Basic Event Plugin Integration + +```php + namespace Event_Bridge_For_ActivityPub\Integrations; + + // Exit if accessed directly. + defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore + + /** + * Integration information for My Event Plugin + * + * This class defines necessary meta information is for the integration of My Event Plugin with the ActivityPub plugin. + */ + final class My_Event_Plugin extends Event_Plugin_Integration { +``` + +### Registering an Event Plugin Integration + +Then you need to tell the Event Bridge for ActivityPub about that class by adding it to the `EVENT_PLUGIN_INTEGRATIONS` constant in the `includes/setup.php` file: + +```php + private const EVENT_PLUGIN_INTEGRATIONS = array( + ... + \Event_Bridge_For_ActivityPub\Integrations\My_Event_Plugin::class, + ); +``` + +### Additional Feature: Event Sources + +Not all _Event Plugin Integrations_ support the event-sources feature. To add support for it an integration must implement the `Feature_Event_Sources` interface. + +```php +namespace Event_Bridge_For_ActivityPub\Integrations; + +// Exit if accessed directly. +defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore + + /** + * Integration information for My Event Plugin. + * + * This class defines necessary meta information is for the integration of My Event Plugin with the ActivityPub plugin. + * This integration supports the Event Sources Feature. + */ +final class GatherPress extends Event_Plugin_Integration implements Feature_Event_Sources { +``` diff --git a/docs/add_your_event_plugin.md b/docs/transformer.md similarity index 88% rename from docs/add_your_event_plugin.md rename to docs/transformer.md index 3b79f84..333e17e 100644 --- a/docs/add_your_event_plugin.md +++ b/docs/transformer.md @@ -2,43 +2,12 @@ > **_NOTE:_** This documentation is also likely to be useful for content types other than events. -The ActivityPub plugin offers a basic support for all post types out of the box, but it also allows the registration of external transformers. A transformer is a class that implements the [abstract transformer class](https://github.com/Automattic/wordpress-activitypub/blob/fb0e23e8854d149fdedaca7a9ea856f5fd965ec9/includes/transformer/class-base.php) and is responsible for generating the ActivityPub JSON representation of an WordPress post or comment object. +The ActivityPub plugin offers a basic support for all post types out of the box, but it also allows the registration of external transformers. A transformer is a class that implements the [abstract transformer class](https://github.com/Automattic/wordpress-activitypub/blob/fb0e23e8854d149fdedaca7a9ea856f5fd965ec9/includes/transformer/class-base.php) and is responsible for generating the ActivityPub JSON representation of an WordPress post or comment object. ## How it works To make the WordPress ActivityPub plugin use a custom transformer simply add a filter to the `activitypub_transformer` hook which provides access to the transformer factory. The [transformer factory](https://github.com/Automattic/wordpress-activitypub/blob/master/includes/transformer/class-factory.php#L12) determines which transformer is used to transform a WordPress object to ActivityPub. We provide a parent event transformer, that comes with common tasks needed for events. Furthermore, we provide admin notices, to prevent users from misconfiguration issues. -## Add your event plugin - -First you need to add some basic information about your event plugin. Just create a new file in `./includes/plugins/my-event-plugin.php`. Implement at least all abstract functions of the `Event_Plugin` class. - -```php - namespace Event_Bridge_For_ActivityPub\Integrations; - - // Exit if accessed directly. - defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore - - /** - * Integration information for My Event Plugin - * - * This class defines necessary meta information is for the integration of My Event Plugin with the ActivityPub plugin. - * - * @since 1.0.0 - */ - final class My_Event_Plugin extends Event_Plugin { -``` - -Then you need to tell the Event Bridge for ActivityPub about that class by adding it to the `EVENT_PLUGIN_INTEGRATIONS` constant in the `includes/setup.php` file: - -```php - private const EVENT_PLUGIN_INTEGRATIONS = array( - ... - '\Event_Bridge_For_ActivityPub\Integrations\My_Event_Plugin', - ); -``` - -The Event Bridge for ActivityPub then takes care of applying the transformer, so you can jump right into implementing it. - ## Writing an event transformer class Within WordPress most content types are stored as a custom post type in the posts table. The ActivityPub plugin offers a basic support for all post types out of the box. So-called transformers take care of converting WordPress WP_Post objects to ActivityStreams JSON. The ActivityPub plugin offers a generic transformer for all post types. Additionally, custom transformers can be implemented to better fit a custom post type, and they can be easily registered with the ActivityPub plugin. @@ -48,9 +17,9 @@ If you are writing a transformer for your event post type we recommend to start So create a new file at `./includes/activitypub/transformer/my-event-plugin.php`. ```php -namespace Event_Bridge_For_ActivityPub\Activitypub\Transformer; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event as Event_Transformer; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event as Event_Transformer; /** * ActivityPub Transformer for My Event Plugin' event post type. diff --git a/includes/activitypub/class-handler.php b/includes/activitypub/class-handler.php index 0fa1433..47a6f7c 100644 --- a/includes/activitypub/class-handler.php +++ b/includes/activitypub/class-handler.php @@ -12,6 +12,9 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore +use DateTime; +use DateTimeZone; +use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources; use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Accept; use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Update; use Event_Bridge_For_ActivityPub\ActivityPub\Handler\Create; @@ -29,5 +32,94 @@ class Handler { Update::init(); Create::init(); Delete::init(); + \add_filter( + 'activitypub_validate_object', + array( self::class, 'validate_object' ), + 12, + 3 + ); + } + + + /** + * Validate the object. + * + * @param bool $valid The validation state. + * @param string $param The object parameter. + * @param \WP_REST_Request $request The request object. + * + * @return bool The validation state: true if valid, false if not. + */ + public static function validate_object( $valid, $param, $request ) { + $json_params = $request->get_json_params(); + + if ( isset( $json_params['object']['type'] ) && 'Event' === $json_params['object']['type'] ) { + $valid = true; + } else { + return $valid; + } + + if ( empty( $json_params['type'] ) ) { + return false; + } + + 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; + } + + $object = $json_params['object']; + + if ( ! is_array( $object ) ) { + return false; + } + + $required = array( + 'id', + 'startTime', + 'name', + ); + + if ( array_intersect( $required, array_keys( $object ) ) !== $required ) { + return false; + } + + return $valid; + } + + /** + * Check if a given DateTime is already passed. + * + * @param string $time_string The ActivityPub like time string. + * @return bool + */ + public static function is_time_passed( $time_string ) { + // Create a DateTime object from the ActivityPub time string. + $time = new DateTime( $time_string, new DateTimeZone( 'UTC' ) ); + + // Get the current time in UTC. + $current_time = new DateTime( 'now', new DateTimeZone( 'UTC' ) ); + + // Compare the event time with the current time. + return $time < $current_time; + } + + /** + * Check that an ActivityPub actor is an event source (i.e. it is followed by the ActivityPub blog actor). + * + * @param string $actor_id The actor ID. + * @return bool True if the ActivityPub actor ID is followed, false otherwise. + */ + 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/collection/class-event-sources.php b/includes/activitypub/collection/class-event-sources.php index 79a5a93..b3fb37e 100644 --- a/includes/activitypub/collection/class-event-sources.php +++ b/includes/activitypub/collection/class-event-sources.php @@ -1,6 +1,17 @@ save( $activity['object'] ); } - - /** - * Validate the object. - * - * @param bool $valid The validation state. - * @param string $param The object parameter. - * @param \WP_REST_Request $request The request object. - * - * @return bool The validation state: true if valid, false if not. - */ - public static function validate_object( $valid, $param, $request ) { - $json_params = $request->get_json_params(); - - if ( isset( $json_params['object']['type'] ) && 'Event' === $json_params['object']['type'] ) { - $valid = true; - } else { - return $valid; - } - - if ( empty( $json_params['type'] ) ) { - return false; - } - - 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; - } - - $object = $json_params['object']; - - if ( ! is_array( $object ) ) { - return false; - } - - $required = array( - 'id', - 'startTime', - 'name', - ); - - if ( array_intersect( $required, array_keys( $object ) ) !== $required ) { - return false; - } - - return $valid; - } - - /** - * Check if a given DateTime is already passed. - * - * @param string $time_string The ActivityPub like time string. - * @return bool - */ - private static function is_time_passed( $time_string ) { - // Create a DateTime object from the ActivityPub time string. - $time = new DateTime( $time_string, new DateTimeZone( 'UTC' ) ); - - // Get the current time in UTC. - $current_time = new DateTime( 'now', new DateTimeZone( 'UTC' ) ); - - // Compare the event time with the current time. - return $time < $current_time; - } - - /** - * Check if an ActivityPub actor is an event source. - * - * @param string $actor_id The actor ID. - * @return bool - */ - 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 9fa2224..ae8eb8f 100644 --- a/includes/activitypub/handler/class-delete.php +++ b/includes/activitypub/handler/class-delete.php @@ -9,13 +9,14 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; use Activitypub\Collection\Actors; use Event_Bridge_For_ActivityPub\Setup; +use Event_Bridge_For_ActivityPub\ActivityPub\Handler; /** * Handle Delete requests. */ class Delete { /** - * Initialize the class, registering WordPress hooks. + * Initialize the class, registering the handler for incoming `Delete` activities to the ActivityPub plugin. */ public static function init() { \add_action( @@ -38,7 +39,7 @@ class Delete { return; } - if ( ! Create::actor_is_event_source( $activity['actor'] ) ) { + if ( ! Handler::actor_is_event_source( $activity['actor'] ) ) { return; } @@ -47,6 +48,10 @@ class Delete { return; } + if ( Handler::is_time_passed( $activity['object']['startTime'] ) ) { + return; + } + $transmogrifier = Setup::get_transmogrifier(); if ( ! $transmogrifier ) { diff --git a/includes/activitypub/handler/class-update.php b/includes/activitypub/handler/class-update.php index 06d046d..bbc918f 100644 --- a/includes/activitypub/handler/class-update.php +++ b/includes/activitypub/handler/class-update.php @@ -9,6 +9,7 @@ namespace Event_Bridge_For_ActivityPub\ActivityPub\Handler; use Activitypub\Collection\Actors; use Event_Bridge_For_ActivityPub\Setup; +use Event_Bridge_For_ActivityPub\ActivityPub\Handler; use function Activitypub\is_activity_public; @@ -17,7 +18,7 @@ use function Activitypub\is_activity_public; */ class Update { /** - * Initialize the class, registering WordPress hooks. + * Initialize the class, registering the handler for incoming `Update` activities to the ActivityPub plugin. */ public static function init() { \add_action( @@ -29,17 +30,21 @@ class Update { } /** - * Handle "Follow" requests. + * Handle incoming "Update" activities.. * * @param array $activity The activity-object. * @param int $user_id The id of the local blog-user. */ public static function handle_update( $activity, $user_id ) { - // We only process activities that are target the application user. + // We only process activities that are target to the application user. if ( Actors::BLOG_USER_ID !== $user_id ) { return; } + if ( ! Handler::actor_is_event_source( $activity['actor'] ) ) { + return; + } + // Check if Activity is public or not. if ( ! is_activity_public( $activity ) ) { return; @@ -50,6 +55,10 @@ class Update { return; } + if ( Handler::is_time_passed( $activity['object']['startTime'] ) ) { + return; + } + $transmogrifier = Setup::get_transmogrifier(); if ( ! $transmogrifier ) { diff --git a/includes/activitypub/model/class-event-source.php b/includes/activitypub/model/class-event-source.php index 800a82b..df9d882 100644 --- a/includes/activitypub/model/class-event-source.php +++ b/includes/activitypub/model/class-event-source.php @@ -2,6 +2,10 @@ /** * Event-Source (=ActivityPub Actor that is followed) model. * + * This class holds methods needed for relating an ActivityPub actor + * that is followed with the custom post type structure how it is + * stored within WordPress. + * * @package Event_Bridge_For_ActivityPub * @license AGPL-3.0-or-later */ @@ -16,6 +20,10 @@ use function Activitypub\sanitize_url; /** * Event-Source (=ActivityPub Actor that is followed) model. + * + * This class holds methods needed for relating an ActivityPub actor + * that is followed with the custom post type structure how it is + * stored within WordPress. */ class Event_Source extends Actor { const ACTIVITYPUB_USER_HANDLE_REGEXP = '(?:([A-Za-z0-9_.-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))'; diff --git a/includes/activitypub/transformer/class-event-organiser.php b/includes/activitypub/transformer/class-event-organiser.php index 6574f54..7a34b12 100644 --- a/includes/activitypub/transformer/class-event-organiser.php +++ b/includes/activitypub/transformer/class-event-organiser.php @@ -6,13 +6,13 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transformer; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Activitypub\Activity\Extended_Object\Place; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event; /** * ActivityPub Transformer for Event Organiser. diff --git a/includes/activitypub/transformer/class-event.php b/includes/activitypub/transformer/class-event.php index cb388c2..88c78b3 100644 --- a/includes/activitypub/transformer/class-event.php +++ b/includes/activitypub/transformer/class-event.php @@ -6,7 +6,7 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transformer; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore diff --git a/includes/activitypub/transformer/class-eventin.php b/includes/activitypub/transformer/class-eventin.php index 7b4131d..f1417e0 100644 --- a/includes/activitypub/transformer/class-eventin.php +++ b/includes/activitypub/transformer/class-eventin.php @@ -8,13 +8,13 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transformer; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Activitypub\Activity\Extended_Object\Place; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event; use DateTime; use DateTimeZone; use Etn\Core\Event\Event_Model; diff --git a/includes/activitypub/transformer/class-eventprime.php b/includes/activitypub/transformer/class-eventprime.php index c85548e..68a7f0d 100644 --- a/includes/activitypub/transformer/class-eventprime.php +++ b/includes/activitypub/transformer/class-eventprime.php @@ -6,13 +6,13 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transformer; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Activitypub\Activity\Extended_Object\Place; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event; /** * ActivityPub Transformer for VS Event diff --git a/includes/activitypub/transformer/class-events-manager.php b/includes/activitypub/transformer/class-events-manager.php index 92fbad2..bc84e2e 100644 --- a/includes/activitypub/transformer/class-events-manager.php +++ b/includes/activitypub/transformer/class-events-manager.php @@ -6,13 +6,13 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transformer; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Activitypub\Activity\Extended_Object\Place; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event as Event_Transformer; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event as Event_Transformer; use DateTime; use DateTimeZone; use EM_Event; diff --git a/includes/activitypub/transformer/class-gatherpress.php b/includes/activitypub/transformer/class-gatherpress.php index 79eb601..d58dc2b 100644 --- a/includes/activitypub/transformer/class-gatherpress.php +++ b/includes/activitypub/transformer/class-gatherpress.php @@ -6,14 +6,14 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transformer; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Activitypub\Activity\Extended_Object\Event as Event_Object; use Activitypub\Activity\Extended_Object\Place; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event; use GatherPress\Core\Event as GatherPress_Event; /** diff --git a/includes/activitypub/transformer/class-modern-events-calendar-lite.php b/includes/activitypub/transformer/class-modern-events-calendar-lite.php index edff084..3dfbd10 100644 --- a/includes/activitypub/transformer/class-modern-events-calendar-lite.php +++ b/includes/activitypub/transformer/class-modern-events-calendar-lite.php @@ -6,13 +6,13 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transformer; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Activitypub\Activity\Extended_Object\Place; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event; use MEC; use MEC\Events\Event as MEC_Event; diff --git a/includes/activitypub/transformer/class-the-events-calendar.php b/includes/activitypub/transformer/class-the-events-calendar.php index 880fee3..08aab3f 100644 --- a/includes/activitypub/transformer/class-the-events-calendar.php +++ b/includes/activitypub/transformer/class-the-events-calendar.php @@ -6,13 +6,13 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transformer; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Activitypub\Activity\Extended_Object\Place; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event; use WP_Post; use function Activitypub\esc_hashtag; diff --git a/includes/activitypub/transformer/class-vs-event-list.php b/includes/activitypub/transformer/class-vs-event-list.php index 42fdb14..329880b 100644 --- a/includes/activitypub/transformer/class-vs-event-list.php +++ b/includes/activitypub/transformer/class-vs-event-list.php @@ -6,13 +6,13 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transformer; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Activitypub\Activity\Extended_Object\Place; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event as Event_Transformer; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event as Event_Transformer; /** * ActivityPub Transformer for VS Event. diff --git a/includes/activitypub/transformer/class-wp-event-manager.php b/includes/activitypub/transformer/class-wp-event-manager.php index 7985291..aec0d58 100644 --- a/includes/activitypub/transformer/class-wp-event-manager.php +++ b/includes/activitypub/transformer/class-wp-event-manager.php @@ -6,13 +6,13 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transformer; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; // Exit if accessed directly. defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore use Activitypub\Activity\Extended_Object\Place; -use Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event as Event_Transformer; +use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event as Event_Transformer; use DateTime; /** diff --git a/includes/activitypub/transmogrifier/class-base.php b/includes/activitypub/transmogrifier/class-base.php index b32d264..6f399ae 100644 --- a/includes/activitypub/transmogrifier/class-base.php +++ b/includes/activitypub/transmogrifier/class-base.php @@ -7,7 +7,7 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transmogrifier; use Activitypub\Activity\Extended_Object\Event; use DateTime; diff --git a/includes/activitypub/transmogrifier/class-gatherpress.php b/includes/activitypub/transmogrifier/class-gatherpress.php index 78128c6..9420eb5 100644 --- a/includes/activitypub/transmogrifier/class-gatherpress.php +++ b/includes/activitypub/transmogrifier/class-gatherpress.php @@ -9,7 +9,7 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transmogrifier; use DateTime; diff --git a/includes/activitypub/transmogrifier/class-the-events-calendar-event-repository.php b/includes/activitypub/transmogrifier/class-the-events-calendar-event-repository.php index 6952087..32c940c 100644 --- a/includes/activitypub/transmogrifier/class-the-events-calendar-event-repository.php +++ b/includes/activitypub/transmogrifier/class-the-events-calendar-event-repository.php @@ -7,7 +7,7 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transmogrifier; use DateTime; diff --git a/includes/activitypub/transmogrifier/class-the-events-calendar.php b/includes/activitypub/transmogrifier/class-the-events-calendar.php index 4b52457..2106b20 100644 --- a/includes/activitypub/transmogrifier/class-the-events-calendar.php +++ b/includes/activitypub/transmogrifier/class-the-events-calendar.php @@ -9,7 +9,7 @@ * @license AGPL-3.0-or-later */ -namespace Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier; +namespace Event_Bridge_For_ActivityPub\ActivityPub\Transmogrifier; use DateTime; diff --git a/includes/class-event-sources.php b/includes/class-event-sources.php index 8b8f629..7b6d3ff 100644 --- a/includes/class-event-sources.php +++ b/includes/class-event-sources.php @@ -11,8 +11,7 @@ namespace Event_Bridge_For_ActivityPub; use Activitypub\Model\Blog; use Event_Bridge_For_ActivityPub\ActivityPub\Collection\Event_Sources as Event_Sources_Collection; -use Event_Bridge_For_ActivityPub\Activitypub\Transmogrifier\GatherPress; -use Event_Bridge_For_ActivityPub\Activitypub\Handler; +use Event_Bridge_For_ActivityPub\ActivityPub\Handler; use Event_Bridge_For_ActivityPub\Admin\User_Interface; use Event_Bridge_For_ActivityPub\Integrations\Event_Plugin_Integration; use Event_Bridge_For_ActivityPub\Integrations\Feature_Event_Sources; @@ -30,17 +29,30 @@ class Event_Sources { * Init. */ public static function init() { + // Register the Event Sources Collection which takes care of managing the event sources. \add_action( 'init', array( Event_Sources_Collection::class, 'init' ) ); + + // Register handlers for incoming activities to the ActivityPub plugin, e.g. incoming `Event` objects. \add_action( 'activitypub_register_handlers', array( Handler::class, 'register_handlers' ) ); + + // Apply modifications to the UI, e.g. disable editing of remote event posts. \add_action( 'init', array( User_Interface::class, 'init' ) ); + + // Register post meta to the event plugins post types needed for easier handling of this feature. \add_action( 'init', array( self::class, 'register_post_meta' ) ); + + // Register filters that prevent cached remote events from being federated again. \add_filter( 'activitypub_is_post_disabled', array( self::class, 'is_cached_external_post' ), 10, 2 ); + \add_filter( 'template_include', array( self::class, 'redirect_activitypub_requests_for_cached_external_events' ), 100 ); + + // Register daily schedule to cleanup cached remote events that have ended. if ( ! \wp_next_scheduled( 'event_bridge_for_activitypub_event_sources_clear_cache' ) ) { \wp_schedule_event( time(), 'daily', 'event_bridge_for_activitypub_event_sources_clear_cache' ); } \add_action( 'event_bridge_for_activitypub_event_sources_clear_cache', array( self::class, 'clear_cache' ) ); + + // Add the actors followed by the event sources feature to the `follow` collection of the used ActivityPub actor. \add_filter( 'activitypub_rest_following', array( self::class, 'add_event_sources_to_following_collection' ), 10, 2 ); - \add_filter( 'template_include', array( self::class, 'redirect_activitypub_requests_for_cached_external_events' ), 100 ); } diff --git a/includes/class-setup.php b/includes/class-setup.php index d74aab5..797d505 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -64,14 +64,6 @@ class Setup { * @since 1.0.0 */ protected function __construct() { - $this->activitypub_plugin_is_active = defined( 'ACTIVITYPUB_PLUGIN_VERSION' ) || - is_plugin_active( 'activitypub/activitypub.php' ); - // BeforeFirstRelease: decide whether we want to do anything at all when ActivityPub plugin is note active. - // if ( ! $this->activitypub_plugin_is_active ) { - // deactivate_plugins( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_FILE ); - // return; - // }. - $this->activitypub_plugin_version = self::get_activitypub_plugin_version(); add_action( 'plugins_loaded', array( $this, 'setup_hooks' ) ); } @@ -204,7 +196,7 @@ class Setup { } /** - * Set up hooks for various purposes. + * Main setup function of the plugin "Event Bridge For ActivityPub". * * This method adds hooks for different purposes as needed. * @@ -213,16 +205,30 @@ class Setup { * @return void */ public function setup_hooks(): void { + // Detect the presence of the ActivityPub plugin. + $this->activitypub_plugin_is_active = defined( 'ACTIVITYPUB_PLUGIN_VERSION' ) || \is_plugin_active( 'activitypub/activitypub.php' ); + $this->activitypub_plugin_version = self::get_activitypub_plugin_version(); + + // Detect active supported event plugins. $this->detect_active_event_plugins(); + // Register self-activation hook. register_activation_hook( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_FILE, array( $this, 'activate' ) ); + // Register listeners whenever any plugin gets activated or deactivated. add_action( 'activated_plugin', array( $this, 'redetect_active_event_plugins' ) ); add_action( 'deactivated_plugin', array( $this, 'redetect_active_event_plugins' ) ); + // Add hook that takes care of all notices in the Admin UI. add_action( 'admin_init', array( $this, 'do_admin_notices' ) ); + + // Add hook that registers all settings to WordPress. add_action( 'admin_init', array( Settings::class, 'register_settings' ) ); + + // Add hook that loads CSS and JavaScript files fr the Admin UI. add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_styles' ) ); + + // Register the settings page(s) of this plugin to WordPress. add_action( 'admin_menu', array( Settings_Page::class, 'admin_menu' ) ); add_filter( 'plugin_action_links_' . EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_BASENAME, @@ -234,18 +240,21 @@ class Setup { return; } + // Register health checks and status reports to the WordPress status report site. add_action( 'init', array( Health_Check::class, 'init' ) ); - // Check if the minimum required version of the ActivityPub plugin is installed. + // Check if the minimum required version of the ActivityPub plugin is installed, if not abort. if ( ! version_compare( $this->activitypub_plugin_version, EVENT_BRIDGE_FOR_ACTIVITYPUB_ACTIVITYPUB_PLUGIN_MIN_VERSION ) ) { return; } + // If the Event-Sources feature is enabled and all requirements are met, initialize it. if ( ! is_user_type_disabled( 'blog' ) && get_option( 'event_bridge_for_activitypub_event_sources_active' ) ) { Event_Sources::init(); } + + // Lastly but most importantly: register the ActivityPub transformers for events to the ActivityPub plugin. add_filter( 'activitypub_transformer', array( $this, 'register_activitypub_event_transformer' ), 10, 3 ); - self::get_default_integration_class_name_used_for_event_sources_feature(); } /** diff --git a/includes/integrations/class-event-organiser.php b/includes/integrations/class-event-organiser.php index d6c0348..55c03d4 100644 --- a/includes/integrations/class-event-organiser.php +++ b/includes/integrations/class-event-organiser.php @@ -2,7 +2,8 @@ /** * Event Organiser. * - * Defines all the necessary meta information for the Event Organiser plugin. + * Defines all the necessary meta information and methods for the integration + * of the WordPress "Event Organiser" plugin. * * @link https://wordpress.org/plugins/event-organiser/ * @package Event_Bridge_For_ActivityPub @@ -17,9 +18,10 @@ use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event_Organiser as Even defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore /** - * Interface for a supported event plugin. + * Event Organiser. * - * This interface defines which information is necessary for a supported event plugin. + * Defines all the necessary meta information and methods for the integration + * of the WordPress "Event Organiser" plugin. * * @since 1.0.0 */ diff --git a/includes/integrations/class-event-plugin-integration.php b/includes/integrations/class-event-plugin-integration.php index 252bb64..d18d2ee 100644 --- a/includes/integrations/class-event-plugin-integration.php +++ b/includes/integrations/class-event-plugin-integration.php @@ -1,8 +1,8 @@ assertInstanceOf( \Event_Bridge_For_ActivityPub\Activitypub\Transformer\Event_Organiser::class, $transformer ); + $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event_Organiser::class, $transformer ); } /** diff --git a/tests/test-class-plugin-eventin.php b/tests/test-class-plugin-eventin.php index 51ec62a..d316f2b 100644 --- a/tests/test-class-plugin-eventin.php +++ b/tests/test-class-plugin-eventin.php @@ -64,7 +64,7 @@ class Test_Eventin extends WP_UnitTestCase { $transformer = \Activitypub\Transformer\Factory::get_transformer( get_post( $event->id ) ); // Check that we got the right transformer. - $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\Activitypub\Transformer\Eventin::class, $transformer ); + $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Eventin::class, $transformer ); } /** diff --git a/tests/test-class-plugin-eventprime.php b/tests/test-class-plugin-eventprime.php index e8c8ce3..1e750ff 100644 --- a/tests/test-class-plugin-eventprime.php +++ b/tests/test-class-plugin-eventprime.php @@ -93,7 +93,7 @@ class Test_EventPrime extends WP_UnitTestCase { $transformer = \Activitypub\Transformer\Factory::get_transformer( $wp_object ); // Check that we got the right transformer. - $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\Activitypub\Transformer\EventPrime::class, $transformer ); + $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\ActivityPub\Transformer\EventPrime::class, $transformer ); } /** diff --git a/tests/test-class-plugin-events-manger.php b/tests/test-class-plugin-events-manger.php index ae87919..956a332 100644 --- a/tests/test-class-plugin-events-manger.php +++ b/tests/test-class-plugin-events-manger.php @@ -61,7 +61,7 @@ class Test_Events_Manager extends WP_UnitTestCase { $transformer = \Activitypub\Transformer\Factory::get_transformer( $wp_object ); // Check that we got the right transformer. - $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\Activitypub\Transformer\Events_Manager::class, $transformer ); + $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Events_Manager::class, $transformer ); } /** diff --git a/tests/test-class-plugin-gatherpress.php b/tests/test-class-plugin-gatherpress.php index 9a7a076..2fae248 100644 --- a/tests/test-class-plugin-gatherpress.php +++ b/tests/test-class-plugin-gatherpress.php @@ -65,7 +65,7 @@ class Test_GatherPress extends WP_UnitTestCase { $transformer = \Activitypub\Transformer\Factory::get_transformer( $event->event ); // Check that we got the right transformer. - $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\Activitypub\Transformer\GatherPress::class, $transformer ); + $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\ActivityPub\Transformer\GatherPress::class, $transformer ); } /** diff --git a/tests/test-class-plugin-modern-events-calendar-lite.php b/tests/test-class-plugin-modern-events-calendar-lite.php index 16db755..3e84a81 100644 --- a/tests/test-class-plugin-modern-events-calendar-lite.php +++ b/tests/test-class-plugin-modern-events-calendar-lite.php @@ -74,7 +74,7 @@ class Test_Modern_Events_Calendar_Lite extends WP_UnitTestCase { $transformer = \Activitypub\Transformer\Factory::get_transformer( $wp_object ); // Check that we got the right transformer. - $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\Activitypub\Transformer\Modern_Events_Calendar_Lite::class, $transformer ); + $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Modern_Events_Calendar_Lite::class, $transformer ); } /** diff --git a/tests/test-class-plugin-the-events-calendar.php b/tests/test-class-plugin-the-events-calendar.php index b2ef602..c88bef7 100644 --- a/tests/test-class-plugin-the-events-calendar.php +++ b/tests/test-class-plugin-the-events-calendar.php @@ -91,7 +91,7 @@ class Test_The_Events_Calendar extends WP_UnitTestCase { $transformer = \Activitypub\Transformer\Factory::get_transformer( $wp_object ); // Check that we got the right transformer. - $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\Activitypub\Transformer\The_Events_Calendar::class, $transformer ); + $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\ActivityPub\Transformer\The_Events_Calendar::class, $transformer ); } /** diff --git a/tests/test-class-plugin-vs-event-list.php b/tests/test-class-plugin-vs-event-list.php index 96ce9e0..a99d43f 100644 --- a/tests/test-class-plugin-vs-event-list.php +++ b/tests/test-class-plugin-vs-event-list.php @@ -58,7 +58,7 @@ class Test_VS_Event_List extends WP_UnitTestCase { $transformer = \Activitypub\Transformer\Factory::get_transformer( $wp_object ); // Check that we got the right transformer. - $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\Activitypub\Transformer\VS_Event_List::class, $transformer ); + $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\ActivityPub\Transformer\VS_Event_List::class, $transformer ); } /** diff --git a/tests/test-class-plugin-wp-event-manager.php b/tests/test-class-plugin-wp-event-manager.php index cfc5191..af68b20 100644 --- a/tests/test-class-plugin-wp-event-manager.php +++ b/tests/test-class-plugin-wp-event-manager.php @@ -58,7 +58,7 @@ class Test_WP_Event_Manager extends WP_UnitTestCase { $transformer = \Activitypub\Transformer\Factory::get_transformer( $wp_object ); // Check that we got the right transformer. - $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\Activitypub\Transformer\WP_Event_Manager::class, $transformer ); + $this->assertInstanceOf( \Event_Bridge_For_ActivityPub\ActivityPub\Transformer\WP_Event_Manager::class, $transformer ); } /** -- 2.39.5 From 423781ff23c4883988f84ac2f1a4e9dfb81e8d14 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Tue, 17 Dec 2024 18:42:43 +0100 Subject: [PATCH 32/33] Move docs to wiki --- docs/README.md | 71 ----------- docs/event_plugin_integrations.md | 50 -------- docs/transformer.md | 191 ------------------------------ 3 files changed, 312 deletions(-) delete mode 100644 docs/README.md delete mode 100644 docs/event_plugin_integrations.md delete mode 100644 docs/transformer.md diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 9ac1310..0000000 --- a/docs/README.md +++ /dev/null @@ -1,71 +0,0 @@ -## Developer documentation - -### Overview - -The entry point of the plugin is the initialization of the singleton class `\Event_Bridge_For_ActivityPub\Setup` in the main plugin file `event-bridge-for-activitypub.php`. -The constructor of that class calls its `setup_hooks()` function. This function provides hooks that initialize all parts of the _Event Bridge For ActivityPub_ whenever needed. - -### File structure - -Note that almost all files and folder within the `activitypub` folders are structured the same way as in the WordPress ActivityPub plugin. - -### Event Plugin Integrations - -This plugin supports multiple event plugins, even at the same time. To add a new one you first need to add some basic information about your event plugin. Just create a new file in `./includes/integrations/my-event-plugin.php`. Implement at least all abstract functions of the `Event_Plugin_Integration` class. - -#### Basic Event Plugin Integration - -```php - namespace Event_Bridge_For_ActivityPub\Integrations; - - // Exit if accessed directly. - defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore - - /** - * Integration information for My Event Plugin - * - * This class defines necessary meta information is for the integration of My Event Plugin with the ActivityPub plugin. - */ - final class My_Event_Plugin extends Event_Plugin_Integration { -``` - -#### Registering an Event Plugin Integration - -Then you need to tell the Event Bridge for ActivityPub about that class by adding it to the `EVENT_PLUGIN_INTEGRATIONS` constant in the `includes/setup.php` file: - -```php - private const EVENT_PLUGIN_INTEGRATIONS = array( - ... - \Event_Bridge_For_ActivityPub\Integrations\My_Event_Plugin::class, - ); -``` - -#### Additional Feature: Event Sources - -Not all _Event Plugin Integrations_ support the event-sources feature. To add support for it an integration must implement the `Feature_Event_Sources` interface. - -```php -namespace Event_Bridge_For_ActivityPub\Integrations; - -// Exit if accessed directly. -defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore - - /** - * Integration information for My Event Plugin. - * - * This class defines necessary meta information is for the integration of My Event Plugin with the ActivityPub plugin. - * This integration supports the Event Sources Feature. - */ -final class GatherPress extends Event_Plugin_Integration implements Feature_Event_Sources { -``` - -### Transformer - -Transformers are the classes that convert an WordPress post type (e.g. one used for events) to _ActivityStreams_. -The Event Bridge for ActivityPub then takes care of applying the transformer, so you can jump right into [implementing it](./implement_an_activitypub_event_transformer.md). - -### Event Sources Feature – Transmogrifier - -The event sources feature allows to aggregate events from external ActivityPub actors. As the initialization of the Event-Sources feature is quite complex all of that initialization is done in the `init` function of the file `includes/class-event-sources.php` which is called by the `\Event_Bridge_For_ActivityPub\Setup` class (`includes/class-setup.php`). - -In this plugin we call a **_Transmogrifier_** the **opposite** of a **_Transformer_**. It takes care of converting an ActivityPub (`Event`) object in _ActivityStreams_ to the WordPress representation of an event plugin. The transmogrifier classes are only used when the `Event Sources` feature is activated. The _Event Bridge For ActivityPub_ can register transformers for multiple event plugins at the same time, however only one transmogrifier, that is one target even plugin can be used as a target for incoming external ActivityPub `Event` objects. diff --git a/docs/event_plugin_integrations.md b/docs/event_plugin_integrations.md deleted file mode 100644 index e654184..0000000 --- a/docs/event_plugin_integrations.md +++ /dev/null @@ -1,50 +0,0 @@ - -## Event Plugin Integrations - -First you need to add some basic information about your event plugin. Just create a new file in `./includes/integrations/my-event-plugin.php`. Implement at least all abstract functions of the `Event_Plugin_Integration` class. - -### Basic Event Plugin Integration - -```php - namespace Event_Bridge_For_ActivityPub\Integrations; - - // Exit if accessed directly. - defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore - - /** - * Integration information for My Event Plugin - * - * This class defines necessary meta information is for the integration of My Event Plugin with the ActivityPub plugin. - */ - final class My_Event_Plugin extends Event_Plugin_Integration { -``` - -### Registering an Event Plugin Integration - -Then you need to tell the Event Bridge for ActivityPub about that class by adding it to the `EVENT_PLUGIN_INTEGRATIONS` constant in the `includes/setup.php` file: - -```php - private const EVENT_PLUGIN_INTEGRATIONS = array( - ... - \Event_Bridge_For_ActivityPub\Integrations\My_Event_Plugin::class, - ); -``` - -### Additional Feature: Event Sources - -Not all _Event Plugin Integrations_ support the event-sources feature. To add support for it an integration must implement the `Feature_Event_Sources` interface. - -```php -namespace Event_Bridge_For_ActivityPub\Integrations; - -// Exit if accessed directly. -defined( 'ABSPATH' ) || exit; // @codeCoverageIgnore - - /** - * Integration information for My Event Plugin. - * - * This class defines necessary meta information is for the integration of My Event Plugin with the ActivityPub plugin. - * This integration supports the Event Sources Feature. - */ -final class GatherPress extends Event_Plugin_Integration implements Feature_Event_Sources { -``` diff --git a/docs/transformer.md b/docs/transformer.md deleted file mode 100644 index 333e17e..0000000 --- a/docs/transformer.md +++ /dev/null @@ -1,191 +0,0 @@ -# Write a specialized ActivityPub transformer for an Event-Custom-Post-Type - -> **_NOTE:_** This documentation is also likely to be useful for content types other than events. - -The ActivityPub plugin offers a basic support for all post types out of the box, but it also allows the registration of external transformers. A transformer is a class that implements the [abstract transformer class](https://github.com/Automattic/wordpress-activitypub/blob/fb0e23e8854d149fdedaca7a9ea856f5fd965ec9/includes/transformer/class-base.php) and is responsible for generating the ActivityPub JSON representation of an WordPress post or comment object. - -## How it works - -To make the WordPress ActivityPub plugin use a custom transformer simply add a filter to the `activitypub_transformer` hook which provides access to the transformer factory. The [transformer factory](https://github.com/Automattic/wordpress-activitypub/blob/master/includes/transformer/class-factory.php#L12) determines which transformer is used to transform a WordPress object to ActivityPub. We provide a parent event transformer, that comes with common tasks needed for events. Furthermore, we provide admin notices, to prevent users from misconfiguration issues. - -## Writing an event transformer class - -Within WordPress most content types are stored as a custom post type in the posts table. The ActivityPub plugin offers a basic support for all post types out of the box. So-called transformers take care of converting WordPress WP_Post objects to ActivityStreams JSON. The ActivityPub plugin offers a generic transformer for all post types. Additionally, custom transformers can be implemented to better fit a custom post type, and they can be easily registered with the ActivityPub plugin. - -If you are writing a transformer for your event post type we recommend to start by extending the provided [event transformer](./includes/activitypub/transformer/class-event.php). It is an extension of the default generic post transformer and inherits useful default implementations for generating the [attachments](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attachment), rendering a proper [content](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-content) in HTML from either blocks or the classic editor, extracting [tags](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag) and more. Furthermore, it offers functions which are likely to be shared by multiple event plugins, so you do not need to reimplement those, or you can fork and extend them to your needs. - -So create a new file at `./includes/activitypub/transformer/my-event-plugin.php`. - -```php -namespace Event_Bridge_For_ActivityPub\ActivityPub\Transformer; - -use Event_Bridge_For_ActivityPub\ActivityPub\Transformer\Event as Event_Transformer; - -/** - * ActivityPub Transformer for My Event Plugin' event post type. - */ -class My_Event_Plugin extends Event_Transformer; { -``` - -The main function which controls the transformation is `to_object`. This one is called by the ActivityPub plugin to get the resulting ActivityStreams represented by a PHP-object (`\Activitypub\Activity\Object\Extended_Object\Event`). The conversion to the actual JSON-LD takes place later, and you don't need to cover that (> `to_array` > associative array > `to_json` > JSON). -The chances are good that you will not need to override that function. - - -```php -/** - * Transform the WordPress Object into an ActivityPub Event Object. - * - * @return Activitypub\Activity\Extended_Object\Event - */ -public function to_object() { - $activitypub_object = parent::to_object(); - // ... your additions. - return $activitypub_object; -} -``` - -We also recommend extending the constructor of the transformer class and set a specialized API object of the event, if it is available. For instance: - -```php - public function __construct( $wp_object, $wp_taxonomy ) { - parent::__construct( $wp_object, $wp_taxonomy ); - $this->event_api = new My_Event_Object_API( $wp_object ); - } -``` - -The ActivityPub object classes contain dynamic getter and setter functions: `set_()` and `get_()`. The function `transform_object_properties()` usually called by `to_object()` tries to set all properties known to the target ActivityPub object where a function called `get_` exists in the current transformer class. - -### How to add new properties - -Adding new properties is not encouraged to do at the transformer level. It's recommended to create a proper target ActivityPub object first. The target ActivityPub object also controls the JSON-LD context via the constant `JSON_LD_CONTEXT`. [Example](https://github.com/Automattic/wordpress-activitypub/blob/fb0e23e8854d149fdedaca7a9ea856f5fd965ec9/includes/activity/extended-object/class-event.php#L21). - - -### Properties - -> **_NOTE:_** Within PHP all properties are snake_case, they will be transformed to the according CamelCase by the ActivityPub plugin. So if to you set `start_time` by using the ActivityPub objects class function `set_start_time` or implementing a getter function in the transformer class called `get_start_time` the property `startTime` will be set accordingly in the JSON representation of the resulting ActivityPub object. - -You can find all available event related properties in the [event class](https://github.com/Automattic/wordpress-activitypub/blob/master/includes/activity/extended-object/class-event.php) along documentation and with links to the specifications. - -#### Mandatory fields - -In order to ensure your events are compatible with other ActivityPub Event implementations there are several required properties that must be set by your transformer. - -* **[`type`](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-type)**: if using the `Activitypub\Activity\Extended_Object\Event` class the type will default to `Event` without doing anything. - -* **[`startTime`](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-startTime)**: the events start time - -* **[`name`](https://www.w3.org/TR/activitystreams-vocabulary/#dfn-name)**: the title of the event - -#### Checklist for properties you SHOULD at least consider writing a getter functions for - -* **`endTime`** -* **`location`** – Note: the `address` within can be both a `string` or a `PostalAddress`. -* **`isOnline`** -* **`status`** -* **`get_tag`** -* **`timezone`** -* **`commentsEnabled`** - -## Writing integration tests - -Create a new tests class in `tests/test-class-plugin-my-event-plugin.php`. - -``` -/** - * Sample test case. - */ -class Test_My_Event_Plugin extends WP_UnitTestCase { -``` - -Implement a check whether your event plugin is active in the `set_up` function. It may be the presence of a class, function or constant. - -```php - /** - * 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 ( ! ) { - self::markTestSkipped( 'The Events Calendar plugin is not active.' ); - } - - // Make sure that ActivityPub support is enabled for The Events Calendar. - $aec = \Event_Bridge_For_ActivityPub\Setup::get_instance(); - $aec->activate_activitypub_support_for_active_event_plugins(); - - // Delete all posts afterwards. - _delete_all_posts(); - } -``` - -## Running the tests for your plugin/ add the tests to the CI pipeline - -### Install the plugin in the CI - -The tests are set up by the bash script in `bin/install-wp-tests.sh`. Make sure your WordPress Event plugin is installed within the function `install_wp_plugins`. - -### Add a composer script for your plugin - -In the pipeline we want to run each event plugins integration tests in a single command, to achieve that, we use phpunit's filters. - -```json -{ - "scripts": { - ... - "test": [ - ... - "@test-my-event-plugin" - ], - ... - "@test-my-event-plugin": "phpunit --filter=my_event_plugin", - ] - } -} -``` - -### Load your plugin during the tests - -To activate/load your plugin add it to the switch statement within the function `_manually_load_plugin()` within `tests/bootstrap.php`. - -```php - switch ( $event_bridge_for_activitypub_integration_filter ) { - ... - case 'my_event_plugin': - $plugin_file = 'my-event-plugin/my-event-plugin.php'; - break; -``` - -If you want to run your tests locally just change the `test-debug` script in the `composer.json` file: - -```json - "test-debug": [ - "@prepare-test", - "@test-my-event-plugin" - ], -``` - -Now you just can execute `docker compose up` to run the tests (make sure you have the latest docker and docker-compose installed). - -### Debugging the tests - -If you are using Visual Studio Code or VSCodium you can step-debug within the tests by adding this configuration to your `.vscode/launch.json`: - -```json -{ - "version": "0.2.0", - "configurations": [ - ..., - { - "name": "Listen for PHPUnit", - "type": "php", - "request": "launch", - "port": 9003, - "pathMappings": { - "/app/": "${workspaceRoot}/wp-content/plugins/event-bridge-for-activitypub/", - "/tmp/wordpress/": "${workspaceRoot}/" - }, - } - ] -} -``` -- 2.39.5 From 984973e18d07fc70ff47aff4a1a90becc97755e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Menrath?= Date: Tue, 17 Dec 2024 18:42:54 +0100 Subject: [PATCH 33/33] Improve PHPdocs --- includes/class-setup.php | 17 ++++++----------- 1 file changed, 6 insertions(+), 11 deletions(-) diff --git a/includes/class-setup.php b/includes/class-setup.php index 797d505..974ef68 100644 --- a/includes/class-setup.php +++ b/includes/class-setup.php @@ -107,7 +107,7 @@ class Setup { /** * Getter function for the active event plugins. * - * @return Event_Plugin[] + * @return \Event_Bridge_For_ActivityPub\Integrations\Event_Plugin_Integration[] */ public function get_active_event_plugins() { return $this->active_event_plugins; @@ -116,7 +116,7 @@ class Setup { /** * Holds all the classes for the supported event plugins. * - * @var array + * @var \Event_Bridge_For_ActivityPub\Integrations\Event_Plugin_Integration[] */ private const EVENT_PLUGIN_INTEGRATIONS = array( \Event_Bridge_For_ActivityPub\Integrations\Events_Manager::class, @@ -164,11 +164,6 @@ class Setup { // Get the filename of the main plugin file of the event plugin (relative to the plugin dir). $event_plugin_file = $event_plugin_integration::get_relative_plugin_file(); - // This check should not be needed, but does not hurt. - if ( ! $event_plugin_file ) { - continue; - } - // Check if plugin is present on disk and is activated. if ( array_key_exists( $event_plugin_file, $all_plugins ) && \is_plugin_active( $event_plugin_file ) ) { $active_event_plugins[ $event_plugin_file ] = new $event_plugin_integration(); @@ -212,7 +207,7 @@ class Setup { // Detect active supported event plugins. $this->detect_active_event_plugins(); - // Register self-activation hook. + // Register hook that runs when this plugin gets activated. register_activation_hook( EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_FILE, array( $this, 'activate' ) ); // Register listeners whenever any plugin gets activated or deactivated. @@ -222,13 +217,13 @@ class Setup { // Add hook that takes care of all notices in the Admin UI. add_action( 'admin_init', array( $this, 'do_admin_notices' ) ); - // Add hook that registers all settings to WordPress. + // Add hook that registers all settings of this plugin to WordPress. add_action( 'admin_init', array( Settings::class, 'register_settings' ) ); - // Add hook that loads CSS and JavaScript files fr the Admin UI. + // Add hook that loads CSS and JavaScript files for the Admin UI. add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_styles' ) ); - // Register the settings page(s) of this plugin to WordPress. + // Register the settings page(s) of this plugin to the WordPress admin menu. add_action( 'admin_menu', array( Settings_Page::class, 'admin_menu' ) ); add_filter( 'plugin_action_links_' . EVENT_BRIDGE_FOR_ACTIVITYPUB_PLUGIN_BASENAME, -- 2.39.5