WIP: Rewrite of transformer management
Some checks are pending
PHP_CodeSniffer / phpcs (push) Waiting to run
Unit Testing / phpunit (5.6, 6.2) (push) Waiting to run
Unit Testing / phpunit (7.0) (push) Waiting to run
Unit Testing / phpunit (7.2) (push) Waiting to run
Unit Testing / phpunit (7.3) (push) Waiting to run
Unit Testing / phpunit (7.4) (push) Waiting to run
Unit Testing / phpunit (8.0) (push) Waiting to run
Unit Testing / phpunit (8.1) (push) Waiting to run
Unit Testing / phpunit (8.2) (push) Waiting to run
Unit Testing / phpunit (latest) (push) Waiting to run

This commit is contained in:
André Menrath 2023-12-22 16:18:18 +01:00
parent b744dc551d
commit 6c6d9076a8
12 changed files with 743 additions and 12 deletions

View file

@ -26,6 +26,9 @@ use function Activitypub\snake_to_camel_case;
* @see https://www.w3.org/TR/activitystreams-core/#object
*/
class Base_Object {
const CONTEXT_ACTIVITYSTREAMS = 'https://www.w3.org/ns/activitystreams';
const CONTEXT_SECURITY = 'https://w3id.org/security/v1';
/**
* The object's unique global identifier
*
@ -457,7 +460,7 @@ class Base_Object {
}
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
$this->set( $var, $params[0] );
return $this->set( $var, $params[0] );
}
if ( \strncasecmp( $method, 'add', 3 ) === 0 ) {
@ -465,6 +468,13 @@ class Base_Object {
}
}
/**
* The ActivityPub object typicallly can define it's own JSON-LD context.
*/
public function __construct() {
$this->context = $this->get_context();
}
/**
* Magic function, to transform the object to string.
*
@ -506,6 +516,9 @@ class Base_Object {
* @return boolean True if the object has the key.
*/
public function has( $key ) {
if ( $key === 'actor ') {
$key = 'actor';
}
return property_exists( $this, $key );
}
@ -519,12 +532,16 @@ class Base_Object {
*/
public function set( $key, $value ) {
if ( ! $this->has( $key ) ) {
if ( $key === 'actor' ) {
$key = 'actor';
}
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
}
$this->$key = $value;
return $this->$key;
return $this;
}
/**
@ -692,4 +709,110 @@ class Base_Object {
public function get_object_var_keys() {
return \array_keys( \get_object_vars( $this ) );
}
/**
* Get the context information for a property.
*
* @param string $property
*
* @return array|null
*/
private function get_property_context( string $property ) {
$reflection_class = new \ReflectionClass( $this );
if ( $reflection_class->hasProperty( $property ) ) {
$reflection_property = $reflection_class->getProperty( $property );
$doc_omment = $reflection_property->getDocComment();
// Extract context information from the doc comment.
preg_match( '/@context\s+([^\s]+)/', $doc_omment, $matches );
if ( ! empty( $matches[1] ) ) {
return $matches[1];
} else {
return self::CONTEXT_ACTIVITYSTREAMS;
}
}
return null;
}
/**
* Compact the context via the JSON-LD syntax.
*
* @param array $key_context A certain part of the whole context.
* @param array $array The full URIs of the contexts.
* @param array $abbreviation The short name of the context.
*
* @return array $key_kontext The compacted context.
*/
private static function compact_context( $key_context, $namespace, $abbreviation ) {
$abbreviation_added = false;
foreach ( $key_context as $key => $value ) {
// Check if the key starts with the namespace
if ( strpos( $value, $namespace ) === 0 ) {
// Replace the key
$key_context[ $key ] = $abbreviation . ':' . substr( $value, strlen( $namespace ) );
// Add abbreviation element for the namespace only once
if ( ! $abbreviation_added ) {
$key_context = array( $abbreviation => $namespace ) + $key_context;
$abbreviation_added = true;
}
}
}
return $key_context;
}
/**
* Builds the context of the ActivityPub object via the PHP docs.
*
* @return array $context A shortened and clean JSON-LD context for the ActivityPub object.
*/
public static function get_context() {
$class = static::class;
// Try to lookup the context from a transient.
$transient = "activitypub_json_context_object_{$class}";
$context = get_transient( $transient );
if ( $context ) {
return $context;
}
$reflection_class = new \ReflectionClass( static::class );
$context = array(
self::CONTEXT_ACTIVITYSTREAMS,
self::CONTEXT_SECURITY,
);
$key_context = array();
foreach ( $reflection_class->getProperties() as $property ) {
$doc_omment = $property->getDocComment();
// Extract context information from the doc comment.
preg_match( '/@context\s+([^\s]+)/', $doc_omment, $matches );
if ( ! empty( $matches[1] ) ) {
$key_context[ snake_to_camel_case( $property->name ) ] = $matches[1];
}
}
$namespace_abbreviations = array(
'https://joinpeertube.org/ns#' => 'pt',
'https://joinmobilizon.org/ns#' => 'mz',
'https://schema.org/' => 'sc',
'https://www.w3.org/2002/12/cal/ical#' => 'ical',
);
foreach ( $namespace_abbreviations as $namespace => $abbreviation ) {
$key_context = self::compact_context( $key_context, $namespace, $abbreviation );
}
if ( ! empty( $key_context ) ) {
$context[] = $key_context;
}
set_transient( $transient, $context );
return $context;
}
}

View file

@ -0,0 +1,128 @@
<?php
/**
* ActivityPub Object of type Event.
*
* @package activity-event-transformers
* @license AGPL-3.0-or-later
*/
namespace Activitypub\Activity;
use Activitypub\Activity\Base_Object;
use function Activitypub\snake_to_camel_case;
/**
* Event is an implementation of one of the Activity Streams Event object type.
*
* This class contains extra keys as used by Mobilizon to ensure compatibility.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event
*/
class Event extends Base_Object {
// todo maybe rename to mobilizon event?
const REPLIES_MODERATION_OPTION_TYPES = array( 'allow_all', 'closed' );
const JOIN_MODE_TYPES = array( 'free', 'restricted', 'external' ); // amd 'invite', but not used by mobilizon atm
const ICAL_EVENT_STATUS_TYPES = array( 'TENTATIVE', 'CONFIRMED', 'CANCELLED' );
/**
* Event is an implementation of one of the
* Activity Streams
*
* @var string
*/
protected $type = 'Event';
protected $name;
protected $contacts;
/**
* Extension invented by PeerTube whether comments/replies are <enabled>
* Mobilizon also implemented this as a fallback to their own
* repliesModerationOption.
*
* @context https://joinpeertube.org/ns#commentsEnabled
* @see https://docs.joinpeertube.org/api/activitypub#video
* @see https://docs.joinmobilizon.org/contribute/activity_pub/
* @var bool|null
*/
protected $comments_enabled;
/**
* @context https://joinmobilizon.org/ns#timezone
* @var string
*/
protected $timezone;
/**
* @context https://joinmobilizon.org/ns#repliesModerationOption
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#repliesmoderation
* @var string
*/
protected $replies_moderation_option;
/**
* @context https://joinmobilizon.org/ns#anonymousParticipationEnabled
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#anonymousparticipationenabled
* @var bool
*/
protected $anonymous_participation_enabled;
/**
* @context https://schema.org/category
* @var enum
*/
protected $category;
/**
* @context https://schema.org/inLanguage
* @var
*/
protected $in_language;
/**
* @context https://joinmobilizon.org/ns#isOnline
* @var bool
*/
protected $is_online;
/**
* @context https://www.w3.org/2002/12/cal/ical#status
* @var enum
*/
protected $status;
/**
* @context https://joinmobilizon.org/ns#externalParticipationUrl
* @var string
*/
protected $external_participation_url;
/**
* @context https://joinmobilizon.org/ns#joinMode
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#joinmode
* @var
*/
protected $join_mode;
/**
* @context https://joinmobilizon.org/ns#participantCount
* @var int
*/
protected $participant_count;
/**
* @context https://schema.org/maximumAttendeeCapacity
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#maximumattendeecapacity
* @var int
*/
protected $maximum_attendee_capacity;
/**
* @context https://schema.org/remainingAttendeeCapacity
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#remainignattendeecapacity
* @var int
*/
protected $remaining_attendee_capacity;
}

View file

@ -0,0 +1,94 @@
<?php
/**
* Event is an implementation of one of the
* Activity Streams Event object type
*
* @package activity-event-transformers
* @license AGPL-3.0-or-later
*/
namespace Activitypub\Activity;
use Activitypub\Activity\Base_Object;
/**
* Event is an implementation of one of the
* Activity Streams Event object type
*
* The Object is the primary base type for the Activity Streams
* vocabulary.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event
*/
class Place extends Base_Object {
/**
* Place is an implementation of one of the
* Activity Streams
*
* @var string
*/
protected $type = 'Place';
/**
* Indicates the accuracy of position coordinates on a Place objects.
* Expressed in properties of percentage. e.g. "94.0" means "94.0% accurate".
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accuracy
* @var float xsd:float [>= 0.0f, <= 100.0f]
*/
protected $accuracy;
/**
* Indicates the altitude of a place. The measurement units is indicated using the units property.
* If units is not specified, the default is assumed to be "m" indicating meters.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-altitude
* @var float xsd:float
*/
protected $altitude;
/**
* The latitude of a place.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-latitude
* @var float xsd:float
*/
protected $latitude;
/**
* The longitude of a place.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-longitude
* @var float xsd:float
*/
protected $longitude;
/**
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-radius
* @var float
*/
protected $radius;
/**
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-units
* @var string
*/
protected $units;
/**
* Extension invented by PeerTube whether comments/replies are <enabled>
* Mobilizon also implemented this as a fallback to their own
* repliesModerationOption.
*
* @see https://docs.joinpeertube.org/api/activitypub#video
* @see https://docs.joinmobilizon.org/contribute/activity_pub/
*
* @var bool
*/
protected $comments_enabled;
/**
* @var Postal_Address|string
*/
protected $address;
}

View file

@ -0,0 +1,62 @@
<?php
/**
* PostalAddress is a custom ActivityPub object firstly used by Mobilizon
* derived from https://schema.org/PostalAddress.
*
* @package activity-event-transformers
* @license AGPL-3.0-or-later
*/
namespace Activitypub\Activity;
use Activitypub\Activity\Base_Object;
/**
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event
*/
class Postal_Address extends Base_Object {
/**
* Place is an implementation of one of the
* Activity Streams
*
* @var string
*/
protected $type = 'PostalAddress';
/**
* The country. For example, USA. You can also provide the two-letter ISO 3166-1 alpha-2 country code.
*
* @see http://en.wikipedia.org/wiki/ISO_3166-1
* @var string
*/
protected $address_country;
/**
* The locality in which the street address is, and which is in the region. For example, Mountain View.
*
* @var string
*/
protected $address_locality;
/**
* The region in which the locality is, and which is in the country.
* For example, California or another appropriate first-level Administrative division.
*
* @var string
*/
protected $address_region;
/**
* The postal code. For example, 94043.
*
* @var string
*/
protected $postal_code;
/**
* The street address. For example, 1600 Amphitheatre Pkwy.
*
* @var string
*/
protected $street_address;
}

View file

@ -62,7 +62,7 @@ class Activity_Dispatcher {
* @return void
*/
public static function send_activity( $wp_object, $type, $user_id = null ) {
$transformer = Factory::get_transformer( $wp_object );
$transformer = Factory::instance()->get_transformer( $wp_object );
if ( null !== $user_id ) {
$transformer->change_wp_user_id( $user_id );
@ -106,7 +106,7 @@ class Activity_Dispatcher {
return;
}
$transformer = Factory::get_transformer( $wp_object );
$transformer = Factory::instance()->get_transformer( $wp_object );
$transformer->change_wp_user_id( Users::BLOG_USER_ID );
$user_id = $transformer->get_wp_user_id();

View file

@ -197,4 +197,20 @@ class Migration {
wp_cache_delete( sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
}
}
/**
* Updates the supported post type settings to the mapped transformer setting.
* TODO: Test this
* @return void
*/
private static function migrate_from_version_number_transformer_management_placeholder() {
$supported_post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) );
Admin::register_settings();
$transformer_mapping = array();
foreach ( $supported_post_types as $supported_post_type ) {
$transformer_mapping[ $supported_post_type ] = 'activitypub/post';
}
update_option( 'activitypub_transformer_mapping', $transformer_mapping );
}
}

View file

@ -14,6 +14,24 @@ use Activitypub\Transformer\Post;
* - Activitypub\Activity\Base_Object
*/
class Attachment extends Post {
/**
* Getter function for the name of the transformer.
*
* @return string name
*/
public function get_name() {
return 'activitypub/attachment';
}
/**
* Getter function for the display name (label/title) of the transformer.
*
* @return string name
*/
public function get_label() {
return 'Built-In Transformer for WordPress Attachments';
}
/**
* Generates all Media Attachments for a Post.
*

View file

@ -18,6 +18,8 @@ abstract class Base {
*/
protected $object;
protected $activitypub_object_type = 'Base_Object';
/**
* Static function to Transform a WordPress Object.
*
@ -36,8 +38,10 @@ abstract class Base {
*
* @param stdClass $object
*/
public function __construct( $object ) {
$this->object = $object;
public function __construct( $object=null ) {
if ( $object ) {
$this->object = $object;
}
}
/**
@ -102,4 +106,30 @@ abstract class Base {
* @return int The User-ID of the WordPress Post
*/
abstract public function change_wp_user_id( $user_id );
/**
* Get the name used for registering the transformer with the ActivityPub plugin.
*
* @since version_number_transformer_management_placeholder
* @return string name
*/
abstract public function get_name();
/**
* Get the display name for the ActivityPub transformer.
*
* @since version_number_transformer_management_placeholder
* @return string display name
*/
abstract public function get_label();
/**
* Returns the ActivityStreams 2.0 Object-Type for a Post.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
*
* @return string The Object-Type.
*/
abstract protected function get_object_type();
}

View file

@ -9,7 +9,214 @@ use Activitypub\Transformer\Attachment;
* Transformer Factory
*/
class Factory {
public static function get_transformer( $object ) {
const DEFAULT_TRANSFORMER_MAPPING = array(
'post' => 'activitypub/post',
'page' => 'activitypub/post',
'attachment' => 'activitypub/attachment',
);
/**
* Transformers.
*
* Holds the list of all the ActivityPub transformers. Default is `null`.
*
* @since version_number_transformer_management_placeholder
* @access private
*
* @var \ActivityPub\Transformer\Base[]
*/
private $transformers = null;
/**
* F instance.
*
* Holds the transformer instance.
*
* @since version_number_transformer_management_placeholder
* @access protected
*/
protected static $_instances = [];
/**
* Instance.
*
* Ensures only one instance of the transformer manager class is loaded or can be loaded.
*
* @since version_number_transformer_management_placeholder
* @access public
* @static
*
* @return Transformer_Manager An instance of the class.
*/
public static function instance() {
$class_name = static::class_name();
if ( empty( static::$_instances[ $class_name ] ) ) {
static::$_instances[ $class_name ] = new static();
}
return static::$_instances[ $class_name ];
}
/**
* Class name.
*
* Retrieve the name of the class.
*
* @since version_number_transformer_management_placeholder
* @access public
* @static
*/
public static function class_name() {
return get_called_class();
}
/**
* Checks if a transformer is registered.
*
* @since version_number_transformer_management_placeholder
*
* @param string $name Transformer name including namespace.
* @return bool True if the block type is registered, false otherwise.
*/
public function is_registered( $name ) {
return isset( $this->transformers[ $name ] );
}
/**
* Register a transformer.
*
* @since version_number_transformer_management_placeholder
* @access public
*
* @param \ActivityPub\Transformer\Base $transformer_instance ActivityPub Transformer.
*
* @return bool True if the ActivityPub transformer was registered.
*/
public function register( \ActivityPub\Transformer\Base $transformer_instance ) {
if ( ! $transformer_instance instanceof \ActivityPub\Transformer\Base ) {
_doing_it_wrong(
__METHOD__,
\esc_html__( 'ActivityPub transformer instance must be a of \ActivityPub\Transformer_Base class.' ),
'version_number_transformer_management_placeholder'
);
return false;
}
$transformer_name = $transformer_instance->get_name();
if ( preg_match( '/[A-Z]+/', $transformer_name ) ) {
_doing_it_wrong(
__METHOD__,
\esc_html__( 'ActivityPub transformer names must not contain uppercase characters.' ),
'version_number_transformer_management_placeholder'
);
return false;
}
$name_matcher = '/^[a-z0-9-]+\/[a-z0-9-]+$/';
if ( ! preg_match( $name_matcher, $transformer_name ) ) {
_doing_it_wrong(
__METHOD__,
\esc_html__( 'ActivityPub transformer names must contain a namespace prefix. Example: my-plugin/my-custom-transformer' ),
'version_number_transformer_management_placeholder'
);
return false;
}
if ( $this->is_registered( $transformer_name ) ) {
_doing_it_wrong(
__METHOD__,
/* translators: %s: Block name. */
sprintf( 'ActivityPub transformer with name "%s" is already registered.', esc_html( $transformer_name ) ),
'version_number_transformer_management_placeholder'
);
return false;
}
/**
* Should the ActivityPub transformer be registered.
*
* @since version_number_transformer_management_placeholder
*
* @param bool $should_register Should the ActivityPub transformer be registered. Default is `true`.
* @param \ActivityPub\Transformer\Base $transformer_instance Widget instance.
*/
$should_register = apply_filters( 'activitypub_transformer_is_enabled', true, $transformer_instance );
if ( ! $should_register ) {
return false;
}
$this->transformers[ $transformer_name ] = $transformer_instance;
return true;
}
/**
* Init transformers.
*
* Initialize ActivityPub transformer manager.
* Include the builtin transformers by default and add third party ones.
*
* @since version_number_transformer_management_placeholder
* @access private
*/
private function init_transformers() {
$builtin_transformers = [
'post',
'attachment'
];
$this->transformers = [];
foreach ( $builtin_transformers as $transformer_name ) {
$class_name = ucfirst( $transformer_name );
$class_name = '\Activitypub\Transformer\\' . $class_name;
$this->register( new $class_name() );
}
/**
* Let other transformers register.
*
* Fires after the built-in Activitypub transformers are registered.
*
* @since version_number_transformer_management_placeholder
*
* @param Transformer_Factory $this The widgets manager.
*/
do_action( 'activitypub_transformers_register', $this );
}
/**
* Get available ActivityPub transformers.
*
* Retrieve the registered transformers list. If given a transformer name
* it returns the given transformer if it is registered.
*
* @since version_number_transformer_management_placeholder
* @access public
*
* @param string $transformer_name Optional. Transformer name. Default is null.
*
* @return Base|Base[]|null Registered transformers.
*/
public function get_transformer_class( $transformer_name = null ) {
if ( is_null( $this->transformers ) ) {
$this->init_transformers();
}
if ( null !== $transformer_name ) {
return isset( $this->transformers[ $transformer_name ] ) ? $this->transformers[ $transformer_name ] : null;
}
return $this->transformers;
}
public static function get_default_transformer( $object ) {
/**
* Filter the transformer for a given object.
*
@ -58,4 +265,40 @@ class Factory {
return null;
}
}
/**
* Get the mapped ActivityPub transformer.
*
* Returns a new instance of the needed WordPress to ActivityPub transformer.
*
* @since version_number_transformer_management_placeholder
* @access public
*
* @param WP_Post|WP_Comment $object The WordPress Post/Comment.
*
* @return \ActivityPub\Transformer\Base|null Registered transformers.
*/
public function get_transformer( $object ) {
switch ( get_class( $object ) ) {
case 'WP_Post':
$post_type = get_post_type( $object );
$transformer_mapping = \get_option( 'activitypub_transformer_mapping', self::DEFAULT_TRANSFORMER_MAPPING );
$transformer_name = $transformer_mapping[ $post_type ];
if ( $transformer_name ) {
$transformer_class = $this->get_transformer_class( $transformer_name );
} else {
return self::get_default_transformer( $object );
}
if ( $transformer_class ) {
return new $transformer_class( $object );
} else {
return self::get_default_transformer( $object );
}
case 'WP_Comment':
return new Comment( $object );
default:
self::get_default_transformer( $object );
}
}
}

View file

@ -24,6 +24,24 @@ use function Activitypub\site_supports_blocks;
* - Activitypub\Activity\Base_Object
*/
class Post extends Base {
/**
* Getter function for the name of the transformer.
*
* @return string name
*/
public function get_name() {
return 'activitypub/post';
}
/**
* Getter function for the display name (label/title) of the transformer.
*
* @return string name
*/
public function get_label() {
return 'Built-In Transformer for WordPress Posts';
}
/**
* Returns the ID of the WordPress Post.
*

View file

@ -1,7 +1,7 @@
<?php
$comment = \get_comment( \get_query_var( 'c', null ) ); // phpcs:ignore
$object = \Activitypub\Transformer\Factory::get_transformer( $comment );
$object = \Activitypub\Transformer\Factory::instance()->get_transformer( $comment );
$json = \array_merge( array( '@context' => \Activitypub\get_context() ), $object->to_object()->to_array() );
// filter output

View file

@ -2,8 +2,7 @@
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$post = \get_post();
$post_object = \Activitypub\Transformer\Factory::get_transformer( $post )->to_object();
$post_object->set_context( \Activitypub\get_context() );
$post_object = \Activitypub\Transformer\Factory::instance()->get_transformer( $post )->to_object();
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client