diff --git a/activitypub.php b/activitypub.php index 6bfc32c..1cd2924 100644 --- a/activitypub.php +++ b/activitypub.php @@ -37,8 +37,14 @@ function activitypub_init() { add_action( 'rest_api_init', array( 'Rest_Activitypub_Followers', 'register_routes' ) ); require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-webfinger.php'; + add_action( 'rest_api_init', array( 'Rest_Activitypub_Webfinger', 'register_routes' ) ); add_action( 'webfinger_user_data', array( 'Rest_Activitypub_Webfinger', 'add_webfinger_discovery' ), 10, 3 ); + require_once dirname( __FILE__ ) . '/includes/class-rest-activitypub-nodeinfo.php'; + add_action( 'rest_api_init', array( 'Rest_Activitypub_Nodeinfo', 'register_routes' ) ); + add_filter( 'nodeinfo_data', array( 'Rest_Activitypub_Nodeinfo', 'add_nodeinfo_discovery' ), 10, 2 ); + add_filter( 'nodeinfo2_data', array( 'Rest_Activitypub_Nodeinfo', 'add_nodeinfo2_discovery' ), 10 ); + // Configure activities require_once dirname( __FILE__ ) . '/includes/class-activitypub-activities.php'; add_action( 'activitypub_inbox_follow', array( 'Activitypub_Activities', 'accept' ), 10, 2 ); @@ -46,3 +52,28 @@ function activitypub_init() { add_action( 'activitypub_inbox_unfollow', array( 'Activitypub_Activities', 'unfollow' ), 10, 2 ); } add_action( 'plugins_loaded', 'activitypub_init' ); + +/** + * Add rewrite rules + */ +function activitypub_add_rewrite_rules() { + if ( ! class_exists( 'Webfinger' ) ) { + add_rewrite_rule( '^.well-known/webfinger', 'index.php?rest_route=/activitypub/1.0/webfinger', 'top' ); + } + + if ( ! class_exists( 'Nodeinfo' ) ) { + add_rewrite_rule( '^.well-known/nodeinfo', 'index.php?rest_route=/activitypub/1.0/nodeinfo/discovery', 'top' ); + add_rewrite_rule( '^.well-known/x-nodeinfo2', 'index.php?rest_route=/activitypub/1.0/nodeinfo2', 'top' ); + } +} +add_action( 'init', 'activitypub_add_rewrite_rules', 1 ); + +/** + * Flush rewrite rules; + */ +function activitypub_flush_rewrite_rules() { + activitypub_add_rewrite_rules(); + flush_rewrite_rules(); +} +register_activation_hook( __FILE__, 'activitypub_flush_rewrite_rules' ); +register_deactivation_hook( __FILE__, 'flush_rewrite_rules' ); diff --git a/includes/class-rest-activitypub-nodeinfo.php b/includes/class-rest-activitypub-nodeinfo.php new file mode 100644 index 0000000..74ceb95 --- /dev/null +++ b/includes/class-rest-activitypub-nodeinfo.php @@ -0,0 +1,162 @@ + WP_REST_Server::READABLE, + 'callback' => array( 'Rest_Activitypub_Nodeinfo', 'discovery' ), + ), + ) + ); + + register_rest_route( + 'activitypub/1.0', '/nodeinfo', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( 'Rest_Activitypub_Nodeinfo', 'nodeinfo' ), + ), + ) + ); + + register_rest_route( + 'activitypub/1.0', '/nodeinfo2', array( + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( 'Rest_Activitypub_Nodeinfo', 'nodeinfo2' ), + ), + ) + ); + } + + /** + * Render NodeInfo file + * + * @param WP_REST_Request $request + * @return WP_REST_Response + */ + public static function nodeinfo( $request ) { + $nodeinfo = array(); + + $nodeinfo['version'] = '2.0'; + $nodeinfo['software'] = array( + 'name' => 'wordpress', + 'version' => get_bloginfo( 'version' ), + ); + + $users = count_users(); + $posts = wp_count_posts(); + $comments = wp_count_comments(); + + $nodeinfo['usage'] = array( + 'users' => array( + 'total' => (int) $users['total_users'], + ), + 'localPosts' => (int) $posts->publish, + 'localComments' => (int) $comments->approved, + ); + + $nodeinfo['openRegistrations'] = false; + $nodeinfo['protocols'] = array('activitypub'); + + $nodeinfo['services'] = array( + 'inbound' => array(), + 'outbound' => array(), + ); + + $nodeinfo['metadata'] = new stdClass; + + return new WP_REST_Response( $nodeinfo, 200 ); + } + + /** + * Render NodeInfo file + * + * @param WP_REST_Request $request + * @return WP_REST_Response + */ + public static function nodeinfo2( $request ) { + $nodeinfo = array(); + + $nodeinfo['version'] = '1.0'; + $nodeinfo['software'] = array( + 'baseUrl' => home_url( '/' ), + 'name' => get_bloginfo( 'name' ), + 'software' => 'wordpress', + 'version' => get_bloginfo( 'version' ), + ); + + $users = count_users(); + $posts = wp_count_posts(); + $comments = wp_count_comments(); + + $nodeinfo['usage'] = array( + 'users' => array( + 'total' => (int) $users['total_users'], + ), + 'localPosts' => (int) $posts->publish, + 'localComments' => (int) $comments->approved, + ); + + $nodeinfo['openRegistrations'] = false; + $nodeinfo['protocols'] = array('activitypub'); + + $nodeinfo['services'] = array( + 'inbound' => array(), + 'outbound' => array(), + ); + + $nodeinfo['metadata'] = new stdClass; + + return new WP_REST_Response( $nodeinfo, 200 ); + } + + /** + * Render NodeInfo discovery file + * + * @param WP_REST_Request $request + * @return WP_REST_Response + */ + public static function discovery( $request ) { + $discovery = array(); + $discovery['links'] = array( + array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', + 'href' => get_rest_url( null, 'activitypub/1.0/nodeinfo' ), + ), + ); + + return new WP_REST_Response( $discovery, 200 ); + } + + /** + * Extend NodeInfo data + * + * @param array $nodeinfo NodeInfo data + * @param array updated data + */ + public static function add_nodeinfo_discovery( $nodeinfo, $version ) { + if ( '2.0' == $version) { + $nodeinfo['protocols'][] = 'activitypub'; + } else { + $nodeinfo['protocols']['inbound'][] = 'activitypub'; + $nodeinfo['protocols']['outbound'][] = 'activitypub'; + } + return $nodeinfo; + } + + /** + * Extend NodeInfo2 data + * + * @param array $nodeinfo NodeInfo2 data + * @param array updated data + */ + public static function add_nodeinfo2_discovery( $nodeinfo ) { + $nodeinfo['protocols'][] = 'activitypub'; + return $nodeinfo; + } +} diff --git a/includes/class-rest-activitypub-webfinger.php b/includes/class-rest-activitypub-webfinger.php index d240b0e..eb194c5 100644 --- a/includes/class-rest-activitypub-webfinger.php +++ b/includes/class-rest-activitypub-webfinger.php @@ -1,6 +1,89 @@ WP_REST_Server::READABLE, + 'callback' => array( 'Rest_Activitypub_Webfinger', 'webfinger' ), + 'args' => self::request_parameters(), + ), + ) + ); + } + + /** + * Render JRD file + * + * @param WP_REST_Request $request + * @return WP_REST_Response + */ + public static function webfinger( $request ) { + $resource = $request->get_param( 'resource' ); + + $matches = array(); + $matched = preg_match( '/^acct:([^@]+)@(.+)$/', $resource, $matches ); + + if ( ! $matched ) { + return new WP_Error( 'activitypub_unsupported_resource', __( 'Resouce is invalid', 'activitypub' ), array( 'status' => 400 ) ); + } + + $resource_identifier = $matches[1]; + $resource_host = $matches[2]; + + if ( wp_parse_url( home_url( '/' ), PHP_URL_HOST ) !== $resource_host ) { + return new WP_Error( 'activitypub_wrong_host', __( 'Resouce host does not match blog host', 'activitypub' ), array( 'status' => 404 ) ); + } + + $user = get_user_by( 'login', esc_sql( $resource_identifier ) ); + + if ( ! $user ) { + return new WP_Error( 'activitypub_user_not_found', __( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); + } + + $json = array( + 'subject' => $resource, + 'aliases' => array( + get_author_posts_url( $user->ID ), + ), + 'links' => array( + array( + 'rel' => 'self', + 'type' => 'application/activity+json', + 'href' => get_author_posts_url( $user->ID ), + ), + array( + 'rel' => 'http://webfinger.net/rel/profile-page', + 'type' => 'text/html', + 'href' => get_author_posts_url( $user->ID ), + ), + ), + ); + + return new WP_REST_Response( $json, 200 ); + } + + /** + * The supported parameters + * + * @return array list of parameters + */ + public static function request_parameters() { + $params = array(); + + $params['resource'] = array( + 'required' => true, + 'type' => 'string', + 'pattern' => '^acct:([^@]+)@(.+)$', + ); + + return $params; + } + /** * Add WebFinger discovery links * diff --git a/includes/functions.php b/includes/functions.php index 6f1b3fa..3d2d4d7 100644 --- a/includes/functions.php +++ b/includes/functions.php @@ -48,7 +48,7 @@ if ( ! function_exists( 'base64_url_encode' ) ) { * @return string the encoded text */ function base64_url_encode( $input ) { - return strtr( base64_encode( $input ), '+/', '-_' ); + return strtr( base64_encode( $input ), '+/', '-_' ); // phpcs:ignore } } if ( ! function_exists( 'base64_url_decode' ) ) { @@ -60,7 +60,7 @@ if ( ! function_exists( 'base64_url_decode' ) ) { * @return string the decoded text */ function base64_url_decode( $input ) { - return base64_decode( strtr( $input, '-_', '+/' ) ); + return base64_decode( strtr( $input, '-_', '+/' ) ); // phpcs:ignore } }