updated plugin ActivityPub
version 5.8.0
This commit is contained in:
parent
19dfd317cc
commit
fdfbf76539
@ -1,14 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: ActivityPub
|
||||
* Plugin URI: https://github.com/pfefferle/wordpress-activitypub/
|
||||
* Plugin URI: https://github.com/Automattic/wordpress-activitypub
|
||||
* Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.
|
||||
* Version: 3.3.3
|
||||
* Version: 5.8.0
|
||||
* Author: Matthias Pfefferle & Automattic
|
||||
* Author URI: https://automattic.com/
|
||||
* License: MIT
|
||||
* License URI: http://opensource.org/licenses/MIT
|
||||
* Requires PHP: 7.0
|
||||
* Requires PHP: 7.2
|
||||
* Text Domain: activitypub
|
||||
* Domain Path: /languages
|
||||
*
|
||||
@ -19,55 +19,46 @@ namespace Activitypub;
|
||||
|
||||
use WP_CLI;
|
||||
|
||||
require_once __DIR__ . '/includes/compat.php';
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
\define( 'ACTIVITYPUB_PLUGIN_VERSION', '3.3.3' );
|
||||
|
||||
/**
|
||||
* Initialize the plugin constants.
|
||||
*/
|
||||
\defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || \define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' );
|
||||
\defined( 'ACTIVITYPUB_EXCERPT_LENGTH' ) || \define( 'ACTIVITYPUB_EXCERPT_LENGTH', 400 );
|
||||
\defined( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS' ) || \define( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS', true );
|
||||
\defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || \define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 3 );
|
||||
\defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=<p>)|(?<=<br>)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' );
|
||||
\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' );
|
||||
\defined( 'ACTIVITYPUB_URL_REGEXP' ) || \define( 'ACTIVITYPUB_URL_REGEXP', '(https?:|www\.)\S+[\w\/]' );
|
||||
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "<h2>[ap_title]</h2>\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" );
|
||||
\defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS', false );
|
||||
// Disable reactions like `Like` and `Announce` by default.
|
||||
\defined( 'ACTIVITYPUB_DISABLE_REACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_REACTIONS', true );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false );
|
||||
\defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) || \define( 'ACTIVITYPUB_SHARED_INBOX_FEATURE', false );
|
||||
\defined( 'ACTIVITYPUB_SEND_VARY_HEADER' ) || \define( 'ACTIVITYPUB_SEND_VARY_HEADER', false );
|
||||
\defined( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE' ) || \define( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE', 'note' );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_VERSION', '5.8.0' );
|
||||
|
||||
// Plugin related constants.
|
||||
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_FILE', ACTIVITYPUB_PLUGIN_DIR . basename( __FILE__ ) );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
require_once __DIR__ . '/includes/class-autoloader.php';
|
||||
require_once __DIR__ . '/includes/compat.php';
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
require_once __DIR__ . '/includes/constants.php';
|
||||
require_once __DIR__ . '/integration/load.php';
|
||||
|
||||
Autoloader::register_path( __NAMESPACE__, __DIR__ . '/includes' );
|
||||
|
||||
/**
|
||||
* Initialize REST routes.
|
||||
*/
|
||||
function rest_init() {
|
||||
Rest\Actors::init();
|
||||
Rest\Outbox::init();
|
||||
Rest\Inbox::init();
|
||||
Rest\Followers::init();
|
||||
Rest\Following::init();
|
||||
Rest\Webfinger::init();
|
||||
Rest\Comment::init();
|
||||
Rest\Server::init();
|
||||
Rest\Collection::init();
|
||||
Rest\Interaction::init();
|
||||
Rest\Post::init();
|
||||
( new Rest\Actors_Controller() )->register_routes();
|
||||
( new Rest\Actors_Inbox_Controller() )->register_routes();
|
||||
( new Rest\Application_Controller() )->register_routes();
|
||||
( new Rest\Collections_Controller() )->register_routes();
|
||||
( new Rest\Comments_Controller() )->register_routes();
|
||||
( new Rest\Followers_Controller() )->register_routes();
|
||||
( new Rest\Following_Controller() )->register_routes();
|
||||
( new Rest\Inbox_Controller() )->register_routes();
|
||||
( new Rest\Interaction_Controller() )->register_routes();
|
||||
( new Rest\Moderators_Controller() )->register_routes();
|
||||
( new Rest\Outbox_Controller() )->register_routes();
|
||||
( new Rest\Replies_Controller() )->register_routes();
|
||||
( new Rest\URL_Validator_Controller() )->register_routes();
|
||||
( new Rest\Webfinger_Controller() )->register_routes();
|
||||
|
||||
// Load NodeInfo endpoints only if blog is public.
|
||||
if ( is_blog_public() ) {
|
||||
Rest\NodeInfo::init();
|
||||
( new Rest\Nodeinfo_Controller() )->register_routes();
|
||||
}
|
||||
}
|
||||
\add_action( 'rest_api_init', __NAMESPACE__ . '\rest_init' );
|
||||
@ -76,17 +67,18 @@ function rest_init() {
|
||||
* Initialize plugin.
|
||||
*/
|
||||
function plugin_init() {
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Health_Check', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Comment', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Dispatcher', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Link', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Mailer', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ), 1 );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Move', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Options', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ) );
|
||||
|
||||
if ( site_supports_blocks() ) {
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Blocks', 'init' ) );
|
||||
@ -100,61 +92,27 @@ function plugin_init() {
|
||||
}
|
||||
\add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' );
|
||||
|
||||
|
||||
/**
|
||||
* Class Autoloader.
|
||||
* Initialize plugin admin.
|
||||
*/
|
||||
\spl_autoload_register(
|
||||
function ( $full_class ) {
|
||||
$base_dir = __DIR__ . '/includes/';
|
||||
$base = 'Activitypub\\';
|
||||
function plugin_admin_init() {
|
||||
// Menus are registered before `admin_init`, because of course they are.
|
||||
\add_action( 'admin_menu', array( __NAMESPACE__ . '\WP_Admin\Menu', 'admin_menu' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Admin', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Health_Check', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Settings', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Settings_Fields', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Welcome_Fields', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Advanced_Settings_Fields', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Blog_Settings_Fields', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\User_Settings_Fields', 'init' ) );
|
||||
|
||||
if ( strncmp( $full_class, $base, strlen( $base ) ) === 0 ) {
|
||||
$maybe_uppercase = str_replace( $base, '', $full_class );
|
||||
$class = strtolower( $maybe_uppercase );
|
||||
// All classes should be capitalized. If this is instead looking for a lowercase method, we ignore that.
|
||||
if ( $maybe_uppercase === $class ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( false !== strpos( $class, '\\' ) ) {
|
||||
$parts = explode( '\\', $class );
|
||||
$class = array_pop( $parts );
|
||||
$sub_dir = strtr( implode( '/', $parts ), '_', '-' );
|
||||
$base_dir = $base_dir . $sub_dir . '/';
|
||||
}
|
||||
|
||||
$filename = 'class-' . strtr( $class, '_', '-' );
|
||||
$file = $base_dir . $filename . '.php';
|
||||
|
||||
if ( file_exists( $file ) && is_readable( $file ) ) {
|
||||
require_once $file;
|
||||
} else {
|
||||
// translators: %s is the class name.
|
||||
$message = sprintf( esc_html__( 'Required class not found or not readable: %s', 'activitypub' ), esc_html( $full_class ) );
|
||||
Debug::write_log( $message );
|
||||
\wp_die( $message ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
if ( defined( 'WP_LOAD_IMPORTERS' ) && WP_LOAD_IMPORTERS ) {
|
||||
require_once __DIR__ . '/includes/wp-admin/import/load.php';
|
||||
\add_action( 'admin_init', __NAMESPACE__ . '\WP_Admin\Import\load' );
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Add plugin settings link.
|
||||
*
|
||||
* @param array $actions The current actions.
|
||||
*/
|
||||
function plugin_settings_link( $actions ) {
|
||||
$settings_link = array();
|
||||
$settings_link[] = \sprintf(
|
||||
'<a href="%1s">%2s</a>',
|
||||
\menu_page_url( 'activitypub', false ),
|
||||
\__( 'Settings', 'activitypub' )
|
||||
);
|
||||
|
||||
return \array_merge( $settings_link, $actions );
|
||||
}
|
||||
\add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), __NAMESPACE__ . '\plugin_settings_link' );
|
||||
\add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_admin_init' );
|
||||
|
||||
\register_activation_hook(
|
||||
__FILE__,
|
||||
@ -164,6 +122,19 @@ function plugin_settings_link( $actions ) {
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Redirect to the welcome page after plugin activation.
|
||||
*
|
||||
* @param string $plugin The plugin basename.
|
||||
*/
|
||||
function activation_redirect( $plugin ) {
|
||||
if ( ACTIVITYPUB_PLUGIN_BASENAME === $plugin ) {
|
||||
\wp_safe_redirect( \admin_url( 'options-general.php?page=activitypub' ) );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
\add_action( 'activated_plugin', __NAMESPACE__ . '\activation_redirect' );
|
||||
|
||||
\register_deactivation_hook(
|
||||
__FILE__,
|
||||
array(
|
||||
@ -180,16 +151,18 @@ function plugin_settings_link( $actions ) {
|
||||
)
|
||||
);
|
||||
|
||||
// Load integrations.
|
||||
require_once __DIR__ . '/integration/load.php';
|
||||
|
||||
/**
|
||||
* `get_plugin_data` wrapper.
|
||||
*
|
||||
* @deprecated 4.2.0 Use `get_plugin_data` instead.
|
||||
*
|
||||
* @param array $default_headers Optional. The default plugin headers. Default empty array.
|
||||
* @return array The plugin metadata array.
|
||||
*/
|
||||
function get_plugin_meta( $default_headers = array() ) {
|
||||
_deprecated_function( __FUNCTION__, '4.2.0', 'get_plugin_data' );
|
||||
|
||||
if ( ! $default_headers ) {
|
||||
$default_headers = array(
|
||||
'Name' => 'Plugin Name',
|
||||
@ -212,24 +185,22 @@ function get_plugin_meta( $default_headers = array() ) {
|
||||
|
||||
/**
|
||||
* Plugin Version Number used for caching.
|
||||
*
|
||||
* @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly.
|
||||
*/
|
||||
function get_plugin_version() {
|
||||
if ( \defined( 'ACTIVITYPUB_PLUGIN_VERSION' ) ) {
|
||||
_deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' );
|
||||
|
||||
return ACTIVITYPUB_PLUGIN_VERSION;
|
||||
}
|
||||
|
||||
$meta = get_plugin_meta( array( 'Version' => 'Version' ) );
|
||||
|
||||
return $meta['Version'];
|
||||
}
|
||||
|
||||
// Check for CLI env, to add the CLI commands.
|
||||
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
WP_CLI::add_command(
|
||||
'activitypub',
|
||||
'\Activitypub\Cli',
|
||||
array(
|
||||
'shortdesc' => __( 'ActivityPub related commands: Meta-Infos, Delete and soon Self-Destruct.', 'activitypub' ),
|
||||
'shortdesc' => 'ActivityPub related commands to manage plugin functionality and the federation of posts and comments.',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
.activitypub-settings {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .notice {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
margin: 0px auto 30px;
|
||||
margin: 0 auto 30px;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .update-nag {
|
||||
margin: 25px 20px 15px 22px;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .wrap {
|
||||
@ -20,6 +24,15 @@
|
||||
border-bottom: 1px solid #dcdcde;
|
||||
}
|
||||
|
||||
.activitypub-settings-header h1 {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
margin: 0 0.8rem 1rem;
|
||||
font-size: 23px;
|
||||
padding: 9px 0 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.activitypub-settings-title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -33,11 +46,10 @@
|
||||
}
|
||||
|
||||
.activitypub-settings-tabs-wrapper {
|
||||
display: -ms-inline-grid;
|
||||
-ms-grid-columns: auto auto auto auto;
|
||||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto auto auto auto;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.activitypub-settings-tab.active {
|
||||
@ -54,6 +66,20 @@
|
||||
transition: box-shadow .5s ease-in-out;
|
||||
}
|
||||
|
||||
.activitypub-settings .row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.activitypub-settings .row > div {
|
||||
max-width: calc(100% - 24px);
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.activitypub-settings .row .description {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.wp-header-end {
|
||||
visibility: hidden;
|
||||
margin: -2px 0 0;
|
||||
@ -168,8 +194,7 @@ input.blog-user-identifier {
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.activitypub-settings
|
||||
.logo {
|
||||
.activitypub-settings .logo {
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
position: relative;
|
||||
@ -177,22 +202,6 @@ input.blog-user-identifier {
|
||||
left: 40px;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .box {
|
||||
border: 1px solid #c3c4c7;
|
||||
background-color: #fff;
|
||||
padding: 1em 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .activitypub-welcome-page .box label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .activitypub-welcome-page input {
|
||||
font-size: 20px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .plugin-recommendations {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
@ -212,3 +221,52 @@ input.blog-user-identifier {
|
||||
.like .dashboard-comment-wrap .comment-author {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.activitypub-settings .welcome-tab-close {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
font-size: 13px;
|
||||
padding: 0 5px 0 20px;
|
||||
text-decoration: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.activitypub-settings .welcome-tab-close::before {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0;
|
||||
transition: all .1s ease-in-out;
|
||||
font: normal 16px/20px dashicons;
|
||||
content: '\f335';
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.activitypub-notice .count {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
margin: 1px 0 -1px 2px;
|
||||
padding: 0 5px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background-color: #dba617;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
z-index: 26;
|
||||
}
|
||||
|
||||
.activitypub-notice .dashicons-warning {
|
||||
color: #dba617;
|
||||
}
|
||||
|
||||
.extra-fields-nav a + a {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.rtl .extra-fields-nav a + a {
|
||||
margin-left: auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
115
wp-content/plugins/activitypub/assets/css/activitypub-embed.css
Normal file
115
wp-content/plugins/activitypub/assets/css/activitypub-embed.css
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* ActivityPub embed styles.
|
||||
*/
|
||||
|
||||
.activitypub-embed {
|
||||
background: #fff;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
max-width: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.activitypub-reply-block .activitypub-embed {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.activitypub-embed-header {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.activitypub-embed-header img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.activitypub-embed-header-text {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.activitypub-embed-header-text h2 {
|
||||
color: #000;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.activitypub-embed-header-text .ap-account {
|
||||
color: #687684;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.activitypub-embed-content {
|
||||
padding: 0 15px 15px;
|
||||
}
|
||||
|
||||
.activitypub-embed-content .ap-title {
|
||||
font-size: 23px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.activitypub-embed-content .ap-subtitle {
|
||||
font-size: 15px;
|
||||
color: #000;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
|
||||
.activitypub-embed-content .ap-preview {
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.activitypub-embed-content .ap-preview img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.activitypub-embed-content .ap-preview-text {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.activitypub-embed-meta {
|
||||
padding: 15px;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
color: #687684;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.activitypub-embed-meta .ap-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
@media only screen and (max-width: 399px) {
|
||||
.activitypub-embed-meta span.ap-stat {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.activitypub-embed-meta a.ap-stat {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.activitypub-embed-meta strong {
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.activitypub-embed-meta .ap-stat-label {
|
||||
color: #687684;
|
||||
}
|
@ -128,14 +128,15 @@
|
||||
title: $el.data( 'choose-text' ),
|
||||
library: wp.media.query( mediaQuery ),
|
||||
date: false,
|
||||
suggestedWidth: $el.data( 'size' ),
|
||||
suggestedHeight: $el.data( 'size' ),
|
||||
suggestedWidth: $el.data( 'width' ),
|
||||
suggestedHeight: $el.data( 'height' ),
|
||||
} ),
|
||||
new ImageCropperNoCustomizer( {
|
||||
control: {
|
||||
id: 'activitypub-header-image',
|
||||
params: {
|
||||
width: $el.data( 'size' ),
|
||||
height: $el.data( 'size' ),
|
||||
width: $el.data( 'width' ),
|
||||
height: $el.data( 'height' ),
|
||||
},
|
||||
},
|
||||
imgSelectOptions: calculateImageSelectOptions,
|
||||
@ -155,17 +156,26 @@
|
||||
// When an image is selected, run a callback.
|
||||
frame.on( 'select', function () {
|
||||
// Grab the selected attachment.
|
||||
var attachment = frame.state().get( 'selection' ).first();
|
||||
var attachment = frame.state().get( 'selection' ).first(),
|
||||
targetRatio = $el.data( 'width' ) / $el.data( 'height' ),
|
||||
currentRatio = attachment.attributes.width / attachment.attributes.height,
|
||||
alreadyCropped = false;
|
||||
|
||||
if (
|
||||
attachment.attributes.height === $el.data( 'size' ) &&
|
||||
$el.data( 'size' ) === attachment.attributes.width
|
||||
) {
|
||||
// Check if the image already has the correct aspect ratio (with a small tolerance).
|
||||
if ( Math.abs( currentRatio - targetRatio ) < 0.01 ) {
|
||||
// Check if this is the same image that was already selected.
|
||||
if ( attachment.id !== parseInt( $hiddenDataField.val(), 10 ) ) {
|
||||
// This is a new image with the correct aspect ratio.
|
||||
$hiddenDataField.val( attachment.id );
|
||||
}
|
||||
|
||||
alreadyCropped = true;
|
||||
}
|
||||
|
||||
if ( alreadyCropped ) {
|
||||
// Skip cropping for already cropped images.
|
||||
switchToUpdate( attachment.attributes );
|
||||
frame.close();
|
||||
|
||||
// Set the value of the hidden input to the attachment id.
|
||||
$hiddenDataField.val( attachment.id );
|
||||
} else {
|
||||
frame.setState( 'cropper' );
|
||||
}
|
||||
|
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-components', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-i18n', 'wp-plugins'), 'version' => '88603987940fec29730d');
|
||||
<?php return array('dependencies' => array('react', 'wp-components', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => '293b8e75ac7a589c5096');
|
||||
|
File diff suppressed because one or more lines are too long
@ -36,6 +36,23 @@
|
||||
"selectedUser": {
|
||||
"type": "string",
|
||||
"default": "site"
|
||||
},
|
||||
"buttonOnly": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"buttonText": {
|
||||
"type": "string",
|
||||
"default": "Follow"
|
||||
},
|
||||
"buttonSize": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"enum": [
|
||||
"small",
|
||||
"default",
|
||||
"compact"
|
||||
]
|
||||
}
|
||||
},
|
||||
"usesContext": [
|
||||
|
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '1ec66c1edf3d9b0b6678');
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '8f1a6f7e5f76d58a3204');
|
||||
|
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-right:1rem;padding-left:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-left:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white);margin-right:1rem}
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-right:1rem;padding-left:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-left:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white)}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow:not(:only-child){margin-right:1rem}
|
||||
|
@ -1 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-left:1rem;padding-right:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white);margin-left:1rem}
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-left:1rem;padding-right:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white)}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow:not(:only-child){margin-left:1rem}
|
||||
|
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'bc272e3d4aaa7992f4c7');
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '635ed3e6db3230ae865f');
|
||||
|
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '28a5bef9295566598f5c');
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => 'e98a40c18060cbb88187');
|
||||
|
@ -1,4 +1,4 @@
|
||||
(()=>{var e={20:(e,t,a)=>{"use strict";var r=a(609),n=Symbol.for("react.element"),l=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),o=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,a){var r,s={},c=null,p=null;for(r in void 0!==a&&(c=""+a),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(p=t.ref),t)l.call(t,r)&&!i.hasOwnProperty(r)&&(s[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===s[r]&&(s[r]=t[r]);return{$$typeof:n,type:e,key:c,ref:p,props:s,_owner:o.current}}},848:(e,t,a)=>{"use strict";e.exports=a(20)},609:e=>{"use strict";e.exports=window.React},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t<arguments.length;t++){var a=arguments[t];a&&(e=o(e,l(a)))}return e}function l(e){if("string"==typeof e||"number"==typeof e)return e;if("object"!=typeof e)return"";if(Array.isArray(e))return n.apply(null,e);if(e.toString!==Object.prototype.toString&&!e.toString.toString().includes("[native code]"))return e.toString();var t="";for(var a in e)r.call(e,a)&&e[a]&&(t=o(t,a));return t}function o(e,t){return t?e?e+" "+t:e+t:e}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},t={};function a(r){var n=t[r];if(void 0!==n)return n.exports;var l=t[r]={exports:{}};return e[r](l,l.exports,a),l.exports}a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.primitives;var r=a(848);const n=(0,r.jsx)(t.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,r.jsx)(t.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})});var l=a(609);const o=window.wp.components,i=window.wp.element,s=window.wp.blockEditor,c=window.wp.data,p=window.wp.coreData,u=window.wp.i18n,m=window.wp.apiFetch;var v=a.n(m);const d=window.wp.url;var w=a(942),f=a.n(w);function b({active:e,children:t,page:a,pageClick:r,className:n}){const o=f()("wp-block activitypub-pager",n,{current:e});return(0,l.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&r(a)}},t)}const y={outlined:"outlined",minimal:"minimal"};function g({compact:e,nextLabel:t,page:a,pageClick:r,perPage:n,prevLabel:o,total:i,variant:s=y.outlined}){const c=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(i/n)),p=f()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":e});return(0,l.createElement)("nav",{className:p},o&&(0,l.createElement)(b,{key:"prev",page:a-1,pageClick:r,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,l.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},c.map((e=>(0,l.createElement)(b,{key:e,page:e,pageClick:r,active:e===a,className:"page-numbers"},e)))),t&&(0,l.createElement)(b,{key:"next",page:a+1,pageClick:r,active:a===Math.ceil(i/n),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}const{namespace:h}=window._activityPubOptions;function _({selectedUser:e,per_page:t,order:a,title:r,page:n,setPage:o,className:s="",followLinks:c=!0,followerData:p=!1}){const m="site"===e?0:e,[w,f]=(0,l.useState)([]),[b,y]=(0,l.useState)(0),[_,E]=(0,l.useState)(0),[x,S]=function(){const[e,t]=(0,l.useState)(1);return[e,t]}(),N=n||x,C=o||S,O=(0,i.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */
|
||||
(()=>{var e={20:(e,t,a)=>{"use strict";var r=a(609),n=Symbol.for("react.element"),l=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),o=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,a){var r,c={},s=null,p=null;for(r in void 0!==a&&(s=""+a),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(p=t.ref),t)l.call(t,r)&&!i.hasOwnProperty(r)&&(c[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===c[r]&&(c[r]=t[r]);return{$$typeof:n,type:e,key:s,ref:p,props:c,_owner:o.current}}},848:(e,t,a)=>{"use strict";e.exports=a(20)},609:e=>{"use strict";e.exports=window.React},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t<arguments.length;t++){var a=arguments[t];a&&(e=o(e,l(a)))}return e}function l(e){if("string"==typeof e||"number"==typeof e)return e;if("object"!=typeof e)return"";if(Array.isArray(e))return n.apply(null,e);if(e.toString!==Object.prototype.toString&&!e.toString.toString().includes("[native code]"))return e.toString();var t="";for(var a in e)r.call(e,a)&&e[a]&&(t=o(t,a));return t}function o(e,t){return t?e?e+" "+t:e+t:e}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},t={};function a(r){var n=t[r];if(void 0!==n)return n.exports;var l=t[r]={exports:{}};return e[r](l,l.exports,a),l.exports}a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.primitives;var r=a(848);const n=(0,r.jsx)(t.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,r.jsx)(t.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})});var l=a(609);const o=window.wp.components,i=window.wp.element,c=window.wp.blockEditor,s=window.wp.data,p=window.wp.coreData,u=window.wp.i18n,m=window.wp.apiFetch;var v=a.n(m);const d=window.wp.url;var w=a(942),b=a.n(w);function f({active:e,children:t,page:a,pageClick:r,className:n}){const o=b()("wp-block activitypub-pager",n,{current:e});return(0,l.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&r(a)}},t)}function y({compact:e,nextLabel:t,page:a,pageClick:r,perPage:n,prevLabel:o,total:i,variant:c="outlined"}){const s=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(i/n)),p=b()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${c}`,{"is-compact":e});return(0,l.createElement)("nav",{className:p},o&&(0,l.createElement)(f,{key:"prev",page:a-1,pageClick:r,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,l.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},s.map((e=>(0,l.createElement)(f,{key:e,page:e,pageClick:r,active:e===a,className:"page-numbers"},e)))),t&&(0,l.createElement)(f,{key:"next",page:a+1,pageClick:r,active:a===Math.ceil(i/n),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}function g(){return window._activityPubOptions||{}}function h({selectedUser:e,per_page:t,order:a,title:r,page:n,setPage:o,className:c="",followLinks:s=!0,followerData:p=!1}){const m="site"===e?0:e,[w,b]=(0,l.useState)([]),[f,h]=(0,l.useState)(0),[k,E]=(0,l.useState)(0),[x,S]=function(){const[e,t]=(0,l.useState)(1);return[e,t]}(),N=n||x,C=o||S,O=(0,i.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */
|
||||
(0,u.__)("<span>←</span> Less","activitypub"),{span:(0,l.createElement)("span",{className:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),P=(0,i.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */
|
||||
(0,u.__)("More <span>→</span>","activitypub"),{span:(0,l.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),I=(e,a)=>{f(e),E(a),y(Math.ceil(a/t))};return(0,l.useEffect)((()=>{if(p&&1===N)return I(p.followers,p.total);const e=function(e,t,a,r){const n=`/${h}/actors/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,d.addQueryArgs)(n,l)}(m,t,a,N);v()({path:e}).then((e=>I(e.orderedItems,e.totalItems))).catch((()=>{}))}),[m,t,a,N,p]),(0,l.createElement)("div",{className:"activitypub-follower-block "+s},(0,l.createElement)("h3",null,r),(0,l.createElement)("ul",null,w&&w.map((e=>(0,l.createElement)("li",{key:e.url},(0,l.createElement)(k,{...e,followLinks:c}))))),b>1&&(0,l.createElement)(g,{page:N,perPage:t,total:_,pageClick:C,nextLabel:P,prevLabel:O,compact:"is-style-compact"===s}))}function k({name:e,icon:t,url:a,preferredUsername:r,followLinks:n=!0}){const i=`@${r}`,s={};return n||(s.onClick=e=>e.preventDefault()),(0,l.createElement)(o.ExternalLink,{className:"activitypub-link",href:a,title:i,...s},(0,l.createElement)("img",{width:"40",height:"40",src:t.url,className:"avatar activitypub-avatar",alt:e}),(0,l.createElement)("span",{className:"activitypub-actor"},(0,l.createElement)("strong",{className:"activitypub-name"},e),(0,l.createElement)("span",{className:"sep"},"/"),(0,l.createElement)("span",{className:"activitypub-handle"},i)))}const E=window._activityPubOptions?.enabled;function x({name:e}){const t=(0,u.sprintf)(/* translators: %s: block name */
|
||||
"This <strong>%s</strong> block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. It will be <strong>empty</strong> in other non-author contexts.",e);return(0,l.createElement)(o.Card,null,(0,l.createElement)(o.CardBody,null,(0,i.createInterpolateElement)(t,{strong:(0,l.createElement)("strong",null)})))}(0,e.registerBlockType)("activitypub/followers",{edit:function({attributes:e,setAttributes:t,context:{postType:a,postId:r}}){const{order:n,per_page:m,selectedUser:v,title:d}=e,w=(0,s.useBlockProps)(),[f,b]=(0,i.useState)(1),y=[{label:(0,u.__)("New to old","activitypub"),value:"desc"},{label:(0,u.__)("Old to new","activitypub"),value:"asc"}],g=function({withInherit:e=!1}){const t=E?.users?(0,c.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,i.useMemo)((()=>{if(!t)return[];const a=[];return E?.site&&a.push({label:(0,u.__)("Site","activitypub"),value:"site"}),e&&E?.users&&a.push({label:(0,u.__)("Dynamic User","activitypub"),value:"inherit"}),t.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),a)}),[t])}({withInherit:!0}),h=e=>a=>{b(1),t({[e]:a})},k=(0,c.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),n=t("postType",a,r)?.author;return null!=n?n:null}),[a,r]);return(0,i.useEffect)((()=>{g.length&&(g.find((({value:e})=>e===v))||t({selectedUser:g[0].value}))}),[v,g]),(0,l.createElement)("div",{...w},(0,l.createElement)(s.InspectorControls,{key:"setting"},(0,l.createElement)(o.PanelBody,{title:(0,u.__)("Followers Options","activitypub")},(0,l.createElement)(o.TextControl,{label:(0,u.__)("Title","activitypub"),help:(0,u.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:d,onChange:e=>t({title:e})}),g.length>1&&(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:v,options:g,onChange:h("selectedUser")}),(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Sort","activitypub"),value:n,options:y,onChange:h("order")}),(0,l.createElement)(o.RangeControl,{label:(0,u.__)("Number of Followers","activitypub"),value:m,onChange:h("per_page"),min:1,max:10}))),"inherit"===v?k?(0,l.createElement)(_,{...e,page:f,setPage:b,followLinks:!1,selectedUser:k}):(0,l.createElement)(x,{name:(0,u.__)("Followers","activitypub")}):(0,l.createElement)(_,{...e,page:f,setPage:b,followLinks:!1}))},save:()=>null,icon:n})})()})();
|
||||
(0,u.__)("More <span>→</span>","activitypub"),{span:(0,l.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),I=(e,a)=>{b(e),E(a),h(Math.ceil(a/t))};return(0,l.useEffect)((()=>{if(p&&1===N)return I(p.followers,p.total);const e=function(e,t,a,r){const{namespace:n}=g(),l=`/${n}/actors/${e}/followers`,o={per_page:t,order:a,page:r,context:"full"};return(0,d.addQueryArgs)(l,o)}(m,t,a,N);v()({path:e}).then((e=>I(e.orderedItems,e.totalItems))).catch((()=>{}))}),[m,t,a,N,p]),(0,l.createElement)("div",{className:"activitypub-follower-block "+c},(0,l.createElement)("h3",null,r),(0,l.createElement)("ul",null,w&&w.map((e=>(0,l.createElement)("li",{key:e.url},(0,l.createElement)(_,{...e,followLinks:s}))))),f>1&&(0,l.createElement)(y,{page:N,perPage:t,total:k,pageClick:C,nextLabel:P,prevLabel:O,compact:"is-style-compact"===c}))}function _({name:e,icon:t,url:a,preferredUsername:r,followLinks:n=!0}){const i=`@${r}`,c={};return n||(c.onClick=e=>e.preventDefault()),(0,l.createElement)(o.ExternalLink,{className:"activitypub-link",href:a,title:i,...c},(0,l.createElement)("img",{width:"40",height:"40",src:t.url,className:"avatar activitypub-avatar",alt:e}),(0,l.createElement)("span",{className:"activitypub-actor"},(0,l.createElement)("strong",{className:"activitypub-name"},e),(0,l.createElement)("span",{className:"sep"},"/"),(0,l.createElement)("span",{className:"activitypub-handle"},i)))}function k({name:e}){const{enabled:t}=g(),a=t?.site?"":(0,u.__)("It will be empty in other non-author contexts.","activitypub"),r=(0,u.sprintf)(/* translators: %1$s: block name, %2$s: extra information for non-author context */ /* translators: %1$s: block name, %2$s: extra information for non-author context */
|
||||
(0,u.__)("This <strong>%1$s</strong> block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,a).trim();return(0,l.createElement)(o.Card,null,(0,l.createElement)(o.CardBody,null,(0,i.createInterpolateElement)(r,{strong:(0,l.createElement)("strong",null)})))}(0,e.registerBlockType)("activitypub/followers",{edit:function({attributes:e,setAttributes:t,context:{postType:a,postId:r}}){const{order:n,per_page:m,selectedUser:v,title:d}=e,w=(0,c.useBlockProps)(),[b,f]=(0,i.useState)(1),y=[{label:(0,u.__)("New to old","activitypub"),value:"desc"},{label:(0,u.__)("Old to new","activitypub"),value:"asc"}],_=function({withInherit:e=!1}){const{enabled:t}=g(),a=t?.users?(0,s.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,i.useMemo)((()=>{if(!a)return[];const r=[];return t?.site&&r.push({label:(0,u.__)("Site","activitypub"),value:"site"}),e&&t?.users&&r.push({label:(0,u.__)("Dynamic User","activitypub"),value:"inherit"}),a.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),r)}),[a])}({withInherit:!0}),E=e=>a=>{f(1),t({[e]:a})},x=(0,s.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),n=t("postType",a,r)?.author;return null!=n?n:null}),[a,r]);return(0,i.useEffect)((()=>{_.length&&(_.find((({value:e})=>e===v))||t({selectedUser:_[0].value}))}),[v,_]),(0,l.createElement)("div",{...w},(0,l.createElement)(c.InspectorControls,{key:"setting"},(0,l.createElement)(o.PanelBody,{title:(0,u.__)("Followers Options","activitypub")},(0,l.createElement)(o.TextControl,{label:(0,u.__)("Title","activitypub"),help:(0,u.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:d,onChange:e=>t({title:e})}),_.length>1&&(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:v,options:_,onChange:E("selectedUser")}),(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Sort","activitypub"),value:n,options:y,onChange:E("order")}),(0,l.createElement)(o.RangeControl,{label:(0,u.__)("Number of Followers","activitypub"),value:m,onChange:E("per_page"),min:1,max:10}))),"inherit"===v?x?(0,l.createElement)(h,{...e,page:b,setPage:f,followLinks:!1,selectedUser:x}):(0,l.createElement)(k,{name:(0,u.__)("Followers","activitypub")}):(0,l.createElement)(h,{...e,page:b,setPage:f,followLinks:!1}))},save:()=>null,icon:n})})()})();
|
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '111b88843c05346aadbf');
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '34299fc181d49292ada0');
|
||||
|
@ -1,3 +1,3 @@
|
||||
(()=>{var e,t={250:(e,t,a)=>{"use strict";const r=window.React,n=window.wp.apiFetch;var l=a.n(n);const o=window.wp.url,i=window.wp.element,c=window.wp.i18n;var s=a(942),p=a.n(s);function u({active:e,children:t,page:a,pageClick:n,className:l}){const o=p()("wp-block activitypub-pager",l,{current:e});return(0,r.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&n(a)}},t)}const m={outlined:"outlined",minimal:"minimal"};function f({compact:e,nextLabel:t,page:a,pageClick:n,perPage:l,prevLabel:o,total:i,variant:c=m.outlined}){const s=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(i/l)),f=p()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${c}`,{"is-compact":e});return(0,r.createElement)("nav",{className:f},o&&(0,r.createElement)(u,{key:"prev",page:a-1,pageClick:n,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},s.map((e=>(0,r.createElement)(u,{key:e,page:e,pageClick:n,active:e===a,className:"page-numbers"},e)))),t&&(0,r.createElement)(u,{key:"next",page:a+1,pageClick:n,active:a===Math.ceil(i/l),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}const v=window.wp.components,{namespace:b}=window._activityPubOptions;function d({selectedUser:e,per_page:t,order:a,title:n,page:s,setPage:p,className:u="",followLinks:m=!0,followerData:v=!1}){const d="site"===e?0:e,[g,y]=(0,r.useState)([]),[k,h]=(0,r.useState)(0),[E,N]=(0,r.useState)(0),[x,_]=function(){const[e,t]=(0,r.useState)(1);return[e,t]}(),O=s||x,S=p||_,C=(0,i.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */
|
||||
(0,c.__)("<span>←</span> Less","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),L=(0,i.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */
|
||||
(0,c.__)("More <span>→</span>","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),q=(e,a)=>{y(e),N(a),h(Math.ceil(a/t))};return(0,r.useEffect)((()=>{if(v&&1===O)return q(v.followers,v.total);const e=function(e,t,a,r){const n=`/${b}/actors/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,o.addQueryArgs)(n,l)}(d,t,a,O);l()({path:e}).then((e=>q(e.orderedItems,e.totalItems))).catch((()=>{}))}),[d,t,a,O,v]),(0,r.createElement)("div",{className:"activitypub-follower-block "+u},(0,r.createElement)("h3",null,n),(0,r.createElement)("ul",null,g&&g.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(w,{...e,followLinks:m}))))),k>1&&(0,r.createElement)(f,{page:O,perPage:t,total:E,pageClick:S,nextLabel:L,prevLabel:C,compact:"is-style-compact"===u}))}function w({name:e,icon:t,url:a,preferredUsername:n,followLinks:l=!0}){const o=`@${n}`,i={};return l||(i.onClick=e=>e.preventDefault()),(0,r.createElement)(v.ExternalLink,{className:"activitypub-link",href:a,title:o,...i},(0,r.createElement)("img",{width:"40",height:"40",src:t.url,className:"avatar activitypub-avatar",alt:e}),(0,r.createElement)("span",{className:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},e),(0,r.createElement)("span",{className:"sep"},"/"),(0,r.createElement)("span",{className:"activitypub-handle"},o)))}const g=window.wp.domReady;a.n(g)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,i.createRoot)(e).render((0,r.createElement)(d,{...t}))}))}))},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t<arguments.length;t++){var a=arguments[t];a&&(e=o(e,l(a)))}return e}function l(e){if("string"==typeof e||"number"==typeof e)return e;if("object"!=typeof e)return"";if(Array.isArray(e))return n.apply(null,e);if(e.toString!==Object.prototype.toString&&!e.toString.toString().includes("[native code]"))return e.toString();var t="";for(var a in e)r.call(e,a)&&e[a]&&(t=o(t,a));return t}function o(e,t){return t?e?e+" "+t:e+t:e}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},a={};function r(e){var n=a[e];if(void 0!==n)return n.exports;var l=a[e]={exports:{}};return t[e](l,l.exports,r),l.exports}r.m=t,e=[],r.O=(t,a,n,l)=>{if(!a){var o=1/0;for(p=0;p<e.length;p++){for(var[a,n,l]=e[p],i=!0,c=0;c<a.length;c++)(!1&l||o>=l)&&Object.keys(r.O).every((e=>r.O[e](a[c])))?a.splice(c--,1):(i=!1,l<o&&(o=l));if(i){e.splice(p--,1);var s=n();void 0!==s&&(t=s)}}return t}l=l||0;for(var p=e.length;p>0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[a,n,l]},r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var a in t)r.o(t,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={996:0,528:0};r.O.j=t=>0===e[t];var t=(t,a)=>{var n,l,[o,i,c]=a,s=0;if(o.some((t=>0!==e[t]))){for(n in i)r.o(i,n)&&(r.m[n]=i[n]);if(c)var p=c(r)}for(t&&t(a);s<o.length;s++)l=o[s],r.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return r.O(p)},a=globalThis.webpackChunkwordpress_activitypub=globalThis.webpackChunkwordpress_activitypub||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var n=r.O(void 0,[528],(()=>r(250)));n=r.O(n)})();
|
||||
(()=>{var e,t={73:(e,t,a)=>{"use strict";const r=window.React,n=window.wp.apiFetch;var l=a.n(n);const o=window.wp.url,c=window.wp.element,i=window.wp.i18n;var s=a(942),p=a.n(s);function u({active:e,children:t,page:a,pageClick:n,className:l}){const o=p()("wp-block activitypub-pager",l,{current:e});return(0,r.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&n(a)}},t)}function m({compact:e,nextLabel:t,page:a,pageClick:n,perPage:l,prevLabel:o,total:c,variant:i="outlined"}){const s=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(c/l)),m=p()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${i}`,{"is-compact":e});return(0,r.createElement)("nav",{className:m},o&&(0,r.createElement)(u,{key:"prev",page:a-1,pageClick:n,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},s.map((e=>(0,r.createElement)(u,{key:e,page:e,pageClick:n,active:e===a,className:"page-numbers"},e)))),t&&(0,r.createElement)(u,{key:"next",page:a+1,pageClick:n,active:a===Math.ceil(c/l),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}const f=window.wp.components;function v({selectedUser:e,per_page:t,order:a,title:n,page:s,setPage:p,className:u="",followLinks:f=!0,followerData:v=!1}){const w="site"===e?0:e,[d,g]=(0,r.useState)([]),[y,k]=(0,r.useState)(0),[h,E]=(0,r.useState)(0),[N,x]=function(){const[e,t]=(0,r.useState)(1);return[e,t]}(),_=s||N,O=p||x,S=(0,c.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */
|
||||
(0,i.__)("<span>←</span> Less","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),C=(0,c.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */
|
||||
(0,i.__)("More <span>→</span>","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),L=(e,a)=>{g(e),E(a),k(Math.ceil(a/t))};return(0,r.useEffect)((()=>{if(v&&1===_)return L(v.followers,v.total);const e=function(e,t,a,r){const{namespace:n}=window._activityPubOptions||{},l=`/${n}/actors/${e}/followers`,c={per_page:t,order:a,page:r,context:"full"};return(0,o.addQueryArgs)(l,c)}(w,t,a,_);l()({path:e}).then((e=>L(e.orderedItems,e.totalItems))).catch((()=>{}))}),[w,t,a,_,v]),(0,r.createElement)("div",{className:"activitypub-follower-block "+u},(0,r.createElement)("h3",null,n),(0,r.createElement)("ul",null,d&&d.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(b,{...e,followLinks:f}))))),y>1&&(0,r.createElement)(m,{page:_,perPage:t,total:h,pageClick:O,nextLabel:C,prevLabel:S,compact:"is-style-compact"===u}))}function b({name:e,icon:t,url:a,preferredUsername:n,followLinks:l=!0}){const o=`@${n}`,c={};return l||(c.onClick=e=>e.preventDefault()),(0,r.createElement)(f.ExternalLink,{className:"activitypub-link",href:a,title:o,...c},(0,r.createElement)("img",{width:"40",height:"40",src:t.url,className:"avatar activitypub-avatar",alt:e}),(0,r.createElement)("span",{className:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},e),(0,r.createElement)("span",{className:"sep"},"/"),(0,r.createElement)("span",{className:"activitypub-handle"},o)))}const w=window.wp.domReady;a.n(w)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,c.createRoot)(e).render((0,r.createElement)(v,{...t}))}))}))},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t<arguments.length;t++){var a=arguments[t];a&&(e=o(e,l(a)))}return e}function l(e){if("string"==typeof e||"number"==typeof e)return e;if("object"!=typeof e)return"";if(Array.isArray(e))return n.apply(null,e);if(e.toString!==Object.prototype.toString&&!e.toString.toString().includes("[native code]"))return e.toString();var t="";for(var a in e)r.call(e,a)&&e[a]&&(t=o(t,a));return t}function o(e,t){return t?e?e+" "+t:e+t:e}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},a={};function r(e){var n=a[e];if(void 0!==n)return n.exports;var l=a[e]={exports:{}};return t[e](l,l.exports,r),l.exports}r.m=t,e=[],r.O=(t,a,n,l)=>{if(!a){var o=1/0;for(p=0;p<e.length;p++){a=e[p][0],n=e[p][1],l=e[p][2];for(var c=!0,i=0;i<a.length;i++)(!1&l||o>=l)&&Object.keys(r.O).every((e=>r.O[e](a[i])))?a.splice(i--,1):(c=!1,l<o&&(o=l));if(c){e.splice(p--,1);var s=n();void 0!==s&&(t=s)}}return t}l=l||0;for(var p=e.length;p>0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[a,n,l]},r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var a in t)r.o(t,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={996:0,528:0};r.O.j=t=>0===e[t];var t=(t,a)=>{var n,l,o=a[0],c=a[1],i=a[2],s=0;if(o.some((t=>0!==e[t]))){for(n in c)r.o(c,n)&&(r.m[n]=c[n]);if(i)var p=i(r)}for(t&&t(a);s<o.length;s++)l=o[s],r.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return r.O(p)},a=self.webpackChunkwordpress_activitypub=self.webpackChunkwordpress_activitypub||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var n=r.O(void 0,[528],(()=>r(73)));n=r.O(n)})();
|
37
wp-content/plugins/activitypub/build/reactions/block.json
Normal file
37
wp-content/plugins/activitypub/build/reactions/block.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"name": "activitypub/reactions",
|
||||
"apiVersion": 2,
|
||||
"version": "1.0.0",
|
||||
"title": "Fediverse Reactions",
|
||||
"category": "widgets",
|
||||
"icon": "heart",
|
||||
"description": "Display Fediverse likes and reposts",
|
||||
"supports": {
|
||||
"html": false,
|
||||
"align": true,
|
||||
"layout": {
|
||||
"default": {
|
||||
"type": "constrained",
|
||||
"orientation": "vertical",
|
||||
"justifyContent": "center"
|
||||
}
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"default": "Fediverse reactions"
|
||||
}
|
||||
},
|
||||
"blockHooks": {
|
||||
"core/post-content": "after"
|
||||
},
|
||||
"textdomain": "activitypub",
|
||||
"editorScript": "file:./index.js",
|
||||
"style": [
|
||||
"file:./style-index.css",
|
||||
"wp-components"
|
||||
],
|
||||
"viewScript": "file:./view.js"
|
||||
}
|
@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '32631215c76c36b38e5e');
|
3
wp-content/plugins/activitypub/build/reactions/index.js
Normal file
3
wp-content/plugins/activitypub/build/reactions/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
(()=>{"use strict";var e,t={373:(e,t,a)=>{const n=window.wp.blocks,r=window.React,l=window.wp.blockEditor,o=window.wp.element,s=window.wp.i18n,i=window.wp.components,c=window.wp.apiFetch;var u=a.n(c);function m(){return window._activityPubOptions||{}}const p=({reactions:e})=>{const{defaultAvatarUrl:t}=m(),[a,n]=(0,o.useState)(new Set),[l,s]=(0,o.useState)(new Map),i=(0,o.useRef)([]),c=()=>{i.current.forEach((e=>clearTimeout(e))),i.current=[]},u=(t,a)=>{c();const r=100,l=e.length;a&&s((e=>{const a=new Map(e);return a.set(t,"clockwise"),a}));const o=e=>{const o="right"===e,c=o?l-1:0,u=o?1:-1;for(let e=o?t:t-1;o?e<=c:e>=c;e+=u){const l=Math.abs(e-t),o=setTimeout((()=>{n((t=>{const n=new Set(t);return a?n.add(e):n.delete(e),n})),a&&e!==t&&s((t=>{const a=new Map(t),n=e-u,r=a.get(n);return a.set(e,"clockwise"===r?"counter":"clockwise"),a}))}),l*r);i.current.push(o)}};if(o("right"),o("left"),!a){const e=Math.max((l-t)*r,t*r),a=setTimeout((()=>{s(new Map)}),e+r);i.current.push(a)}};return(0,o.useEffect)((()=>()=>c()),[]),(0,r.createElement)("ul",{className:"reaction-avatars"},e.map(((e,n)=>{const o=l.get(n),s=["reaction-avatar",a.has(n)?"wave-active":"",o?`rotate-${o}`:""].filter(Boolean).join(" "),i=e.avatar||t;return(0,r.createElement)("li",{key:n},(0,r.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer",onMouseEnter:()=>u(n,!0),onMouseLeave:()=>u(n,!1)},(0,r.createElement)("img",{src:i,alt:e.name,className:s,width:"32",height:"32"})))})))},f=({reactions:e,type:t})=>(0,r.createElement)("ul",{className:"activitypub-reaction-list"},e.map(((e,t)=>(0,r.createElement)("li",{key:t},(0,r.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,r.createElement)("img",{src:e.avatar,alt:e.name,width:"32",height:"32"}),(0,r.createElement)("span",null,e.name)))))),h=({items:e,label:t})=>{const[a,n]=(0,o.useState)(!1),[l,s]=(0,o.useState)(null),[c,u]=(0,o.useState)(e.length),m=(0,o.useRef)(null);(0,o.useEffect)((()=>{if(!m.current)return;const t=()=>{const t=m.current;if(!t)return;const a=t.offsetWidth-(l?.offsetWidth||0)-12,n=Math.max(1,Math.floor((a-32)/22));u(Math.min(n,e.length))};t();const a=new ResizeObserver(t);return a.observe(m.current),()=>{a.disconnect()}}),[l,e.length]);const h=e.slice(0,c);return(0,r.createElement)("div",{className:"reaction-group",ref:m},(0,r.createElement)(p,{reactions:h}),(0,r.createElement)(i.Button,{ref:s,className:"reaction-label is-link",onClick:()=>n(!a),"aria-expanded":a},t),a&&l&&(0,r.createElement)(i.Popover,{anchor:l,onClose:()=>n(!1)},(0,r.createElement)(f,{reactions:e})))};function d({title:e="",postId:t=null,reactions:a=null,titleComponent:n=null}){const{namespace:l}=m(),[s,i]=(0,o.useState)(a),[c,p]=(0,o.useState)(!a);return(0,o.useEffect)((()=>{if(a)return i(a),void p(!1);t?(p(!0),u()({path:`/${l}/posts/${t}/reactions`}).then((e=>{i(e),p(!1)})).catch((()=>p(!1)))):p(!1)}),[t,a]),c?null:s&&Object.values(s).some((e=>e.items?.length>0))?(0,r.createElement)("div",{className:"activitypub-reactions"},n||e&&(0,r.createElement)("h6",null,e),Object.entries(s).map((([e,t])=>t.items?.length?(0,r.createElement)(h,{key:e,items:t.items,label:t.label}):null))):null}const v=e=>{const t=["#FF6B6B","#4ECDC4","#45B7D1","#96CEB4","#FFEEAD","#D4A5A5","#9B59B6","#3498DB","#E67E22"],a=(()=>{const e=["Bouncy","Cosmic","Dancing","Fluffy","Giggly","Hoppy","Jazzy","Magical","Nifty","Perky","Quirky","Sparkly","Twirly","Wiggly","Zippy"],t=["Badger","Capybara","Dolphin","Echidna","Flamingo","Giraffe","Hedgehog","Iguana","Jellyfish","Koala","Lemur","Manatee","Narwhal","Octopus","Penguin"];return`${e[Math.floor(Math.random()*e.length)]} ${t[Math.floor(Math.random()*t.length)]}`})(),n=t[Math.floor(Math.random()*t.length)],r=a.charAt(0),l=document.createElement("canvas");l.width=64,l.height=64;const o=l.getContext("2d");return o.fillStyle=n,o.beginPath(),o.arc(32,32,32,0,2*Math.PI),o.fill(),o.fillStyle="#FFFFFF",o.font="32px sans-serif",o.textAlign="center",o.textBaseline="middle",o.fillText(r,32,32),{name:a,url:"#",avatar:l.toDataURL()}},g=JSON.parse('{"UU":"activitypub/reactions"}');(0,n.registerBlockType)(g.UU,{edit:function({attributes:e,setAttributes:t,__unstableLayoutClassNames:a}){const n=(0,l.useBlockProps)({className:a}),[i]=(0,o.useState)({likes:{label:(0,s.sprintf)(/* translators: %d: Number of likes */ /* translators: %d: Number of likes */
|
||||
(0,s._x)("%d likes","number of likes","activitypub"),9),items:Array.from({length:9},((e,t)=>v()))},reposts:{label:(0,s.sprintf)(/* translators: %d: Number of reposts */ /* translators: %d: Number of reposts */
|
||||
(0,s._x)("%d reposts","number of reposts","activitypub"),6),items:Array.from({length:6},((e,t)=>v()))}}),c=(0,r.createElement)(l.RichText,{tagName:"h6",value:e.title,onChange:e=>t({title:e}),placeholder:(0,s.__)("Fediverse Reactions","activitypub"),disableLineBreaks:!0,allowedFormats:[]});return(0,r.createElement)("div",{...n},(0,r.createElement)(d,{titleComponent:c,reactions:i}))}})}},a={};function n(e){var r=a[e];if(void 0!==r)return r.exports;var l=a[e]={exports:{}};return t[e](l,l.exports,n),l.exports}n.m=t,e=[],n.O=(t,a,r,l)=>{if(!a){var o=1/0;for(u=0;u<e.length;u++){a=e[u][0],r=e[u][1],l=e[u][2];for(var s=!0,i=0;i<a.length;i++)(!1&l||o>=l)&&Object.keys(n.O).every((e=>n.O[e](a[i])))?a.splice(i--,1):(s=!1,l<o&&(o=l));if(s){e.splice(u--,1);var c=r();void 0!==c&&(t=c)}}return t}l=l||0;for(var u=e.length;u>0&&e[u-1][2]>l;u--)e[u]=e[u-1];e[u]=[a,r,l]},n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var a in t)n.o(t,a)&&!n.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={608:0,104:0};n.O.j=t=>0===e[t];var t=(t,a)=>{var r,l,o=a[0],s=a[1],i=a[2],c=0;if(o.some((t=>0!==e[t]))){for(r in s)n.o(s,r)&&(n.m[r]=s[r]);if(i)var u=i(n)}for(t&&t(a);c<o.length;c++)l=o[c],n.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return n.O(u)},a=self.webpackChunkwordpress_activitypub=self.webpackChunkwordpress_activitypub||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var r=n.O(void 0,[104],(()=>n(373)));r=n.O(r)})();
|
@ -0,0 +1 @@
|
||||
.activitypub-reactions h6{border-top:1px solid;border-top-color:var(--wp--preset--color--contrast-2);display:inline-block;padding-top:.5em}.activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75em;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0;padding:0}.activitypub-reactions .reaction-avatars li{margin:0 0 0 -10px;padding:0}.activitypub-reactions .reaction-avatars li:last-child{margin-left:0}.activitypub-reactions .reaction-avatars li a{display:block;text-decoration:none}.activitypub-reactions .reaction-avatars .reaction-avatar{border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);height:32px;transition:transform .6s cubic-bezier(.34,1.56,.64,1);width:32px;will-change:transform}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active{transform:translateY(-5px)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-clockwise{transform:translateY(-5px) rotate(-30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-counter{transform:translateY(-5px) rotate(30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar:hover{position:relative;z-index:1}.activitypub-reactions .reaction-label.components-button{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#2271b1);flex:0 0 auto;height:auto;padding:0;text-decoration:none;white-space:nowrap}.activitypub-reactions .reaction-label.components-button:hover{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#135e96);text-decoration:underline}.activitypub-reactions .reaction-label.components-button:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-reaction-list{background-color:var(--wp--preset--color--background,var(--wp--preset--color--custom-background,var(--wp--preset--color--base)));list-style:none;margin:0;max-width:300px;padding:.25em .7em .25em 1.3em;width:-moz-max-content;width:max-content}.activitypub-reaction-list ul{margin:0;padding:0}.activitypub-reaction-list li{font-size:var(--wp--preset--font-size--small);margin:0;padding:0}.activitypub-reaction-list a{align-items:center;color:var(--wp--preset--color--contrast,var(--wp--preset--color--secondary));display:flex;font-size:var(--wp--preset--font-size--small,.75rem);gap:.5em;justify-content:flex-start;padding:.5em;text-decoration:none}.activitypub-reaction-list a:hover{text-decoration:underline}.activitypub-reaction-list a img{border-radius:50%;flex:none;height:24px;width:24px}
|
@ -0,0 +1 @@
|
||||
.activitypub-reactions h6{border-top:1px solid;border-top-color:var(--wp--preset--color--contrast-2);display:inline-block;padding-top:.5em}.activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75em;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0;padding:0}.activitypub-reactions .reaction-avatars li{margin:0 -10px 0 0;padding:0}.activitypub-reactions .reaction-avatars li:last-child{margin-right:0}.activitypub-reactions .reaction-avatars li a{display:block;text-decoration:none}.activitypub-reactions .reaction-avatars .reaction-avatar{border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);height:32px;transition:transform .6s cubic-bezier(.34,1.56,.64,1);width:32px;will-change:transform}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active{transform:translateY(-5px)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-clockwise{transform:translateY(-5px) rotate(30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-counter{transform:translateY(-5px) rotate(-30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar:hover{position:relative;z-index:1}.activitypub-reactions .reaction-label.components-button{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#2271b1);flex:0 0 auto;height:auto;padding:0;text-decoration:none;white-space:nowrap}.activitypub-reactions .reaction-label.components-button:hover{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#135e96);text-decoration:underline}.activitypub-reactions .reaction-label.components-button:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-reaction-list{background-color:var(--wp--preset--color--background,var(--wp--preset--color--custom-background,var(--wp--preset--color--base)));list-style:none;margin:0;max-width:300px;padding:.25em 1.3em .25em .7em;width:-moz-max-content;width:max-content}.activitypub-reaction-list ul{margin:0;padding:0}.activitypub-reaction-list li{font-size:var(--wp--preset--font-size--small);margin:0;padding:0}.activitypub-reaction-list a{align-items:center;color:var(--wp--preset--color--contrast,var(--wp--preset--color--secondary));display:flex;font-size:var(--wp--preset--font-size--small,.75rem);gap:.5em;justify-content:flex-start;padding:.5em;text-decoration:none}.activitypub-reaction-list a:hover{text-decoration:underline}.activitypub-reaction-list a img{border-radius:50%;flex:none;height:24px;width:24px}
|
@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => 'd5cb95d9bd6062974b3c');
|
1
wp-content/plugins/activitypub/build/reactions/view.js
Normal file
1
wp-content/plugins/activitypub/build/reactions/view.js
Normal file
@ -0,0 +1 @@
|
||||
(()=>{"use strict";var e={n:t=>{var n=t&&t.__esModule?()=>t.default:()=>t;return e.d(n,{a:n}),n},d:(t,n)=>{for(var a in n)e.o(n,a)&&!e.o(t,a)&&Object.defineProperty(t,a,{enumerable:!0,get:n[a]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.React,n=window.wp.element,a=window.wp.domReady;var r=e.n(a);const c=window.wp.components,o=window.wp.apiFetch;var l=e.n(o);function s(){return window._activityPubOptions||{}}window.wp.i18n;const i=({reactions:e})=>{const{defaultAvatarUrl:a}=s(),[r,c]=(0,n.useState)(new Set),[o,l]=(0,n.useState)(new Map),i=(0,n.useRef)([]),u=()=>{i.current.forEach((e=>clearTimeout(e))),i.current=[]},m=(t,n)=>{u();const a=100,r=e.length;n&&l((e=>{const n=new Map(e);return n.set(t,"clockwise"),n}));const o=e=>{const o="right"===e,s=o?r-1:0,u=o?1:-1;for(let e=o?t:t-1;o?e<=s:e>=s;e+=u){const r=Math.abs(e-t),o=setTimeout((()=>{c((t=>{const a=new Set(t);return n?a.add(e):a.delete(e),a})),n&&e!==t&&l((t=>{const n=new Map(t),a=e-u,r=n.get(a);return n.set(e,"clockwise"===r?"counter":"clockwise"),n}))}),r*a);i.current.push(o)}};if(o("right"),o("left"),!n){const e=Math.max((r-t)*a,t*a),n=setTimeout((()=>{l(new Map)}),e+a);i.current.push(n)}};return(0,n.useEffect)((()=>()=>u()),[]),(0,t.createElement)("ul",{className:"reaction-avatars"},e.map(((e,n)=>{const c=o.get(n),l=["reaction-avatar",r.has(n)?"wave-active":"",c?`rotate-${c}`:""].filter(Boolean).join(" "),s=e.avatar||a;return(0,t.createElement)("li",{key:n},(0,t.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer",onMouseEnter:()=>m(n,!0),onMouseLeave:()=>m(n,!1)},(0,t.createElement)("img",{src:s,alt:e.name,className:l,width:"32",height:"32"})))})))},u=({reactions:e,type:n})=>(0,t.createElement)("ul",{className:"activitypub-reaction-list"},e.map(((e,n)=>(0,t.createElement)("li",{key:n},(0,t.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,t.createElement)("img",{src:e.avatar,alt:e.name,width:"32",height:"32"}),(0,t.createElement)("span",null,e.name)))))),m=({items:e,label:a})=>{const[r,o]=(0,n.useState)(!1),[l,s]=(0,n.useState)(null),[m,p]=(0,n.useState)(e.length),h=(0,n.useRef)(null);(0,n.useEffect)((()=>{if(!h.current)return;const t=()=>{const t=h.current;if(!t)return;const n=t.offsetWidth-(l?.offsetWidth||0)-12,a=Math.max(1,Math.floor((n-32)/22));p(Math.min(a,e.length))};t();const n=new ResizeObserver(t);return n.observe(h.current),()=>{n.disconnect()}}),[l,e.length]);const f=e.slice(0,m);return(0,t.createElement)("div",{className:"reaction-group",ref:h},(0,t.createElement)(i,{reactions:f}),(0,t.createElement)(c.Button,{ref:s,className:"reaction-label is-link",onClick:()=>o(!r),"aria-expanded":r},a),r&&l&&(0,t.createElement)(c.Popover,{anchor:l,onClose:()=>o(!1)},(0,t.createElement)(u,{reactions:e})))};function p({title:e="",postId:a=null,reactions:r=null,titleComponent:c=null}){const{namespace:o}=s(),[i,u]=(0,n.useState)(r),[p,h]=(0,n.useState)(!r);return(0,n.useEffect)((()=>{if(r)return u(r),void h(!1);a?(h(!0),l()({path:`/${o}/posts/${a}/reactions`}).then((e=>{u(e),h(!1)})).catch((()=>h(!1)))):h(!1)}),[a,r]),p?null:i&&Object.values(i).some((e=>e.items?.length>0))?(0,t.createElement)("div",{className:"activitypub-reactions"},c||e&&(0,t.createElement)("h6",null,e),Object.entries(i).map((([e,n])=>n.items?.length?(0,t.createElement)(m,{key:e,items:n.items,label:n.label}):null))):null}r()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-reactions-block"),(e=>{const a=JSON.parse(e.dataset.attrs);(0,n.createRoot)(e).render((0,t.createElement)(p,{...a}))}))}))})();
|
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '45f08e094782c24c4c34');
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '7160b6399cd924e1c7be');
|
||||
|
File diff suppressed because one or more lines are too long
@ -1 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex}
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex}
|
||||
|
@ -1 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex}
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex}
|
||||
|
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('wp-block-editor', 'wp-blocks', 'wp-data', 'wp-element', 'wp-plugins'), 'version' => '488f0199fb69ddcf6c53');
|
||||
<?php return array('dependencies' => array('wp-block-editor', 'wp-blocks', 'wp-data', 'wp-element', 'wp-plugins'), 'version' => 'f65a7269b5abb57d3e73');
|
||||
|
@ -1 +1 @@
|
||||
(()=>{"use strict";const t=window.wp.plugins,e=window.wp.blocks,n=window.wp.data,i=window.wp.blockEditor,r=window.wp.element;(0,t.registerPlugin)("activitypub-reply-intent",{render:()=>{const[t,o]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{if(t)return;const r=new URLSearchParams(window.location.search).get("in_reply_to");r&&setTimeout((()=>{const t=(0,e.createBlock)("activitypub/reply",{url:r}),o=(0,n.dispatch)(i.store);o.insertBlock(t),o.insertAfterBlock(t.clientId)}),200),o(!0)}),[t]),null}})})();
|
||||
(()=>{"use strict";const e=window.wp.plugins,t=window.wp.blocks,i=window.wp.data,n=window.wp.blockEditor,o=window.wp.element;let r=!1;(0,e.registerPlugin)("activitypub-reply-intent",{render:()=>((0,o.useEffect)((()=>{if(r)return;const e=new URLSearchParams(window.location.search).get("in_reply_to");e&&!r&&setTimeout((()=>{const o=(0,t.createBlock)("activitypub/reply",{url:e,embedPost:!0}),r=(0,i.dispatch)(n.store);r.insertBlock(o),r.insertAfterBlock(o.clientId)}),200),r=!0})),null)})})();
|
@ -8,14 +8,22 @@
|
||||
"icon": "commentReplyLink",
|
||||
"description": "Respond to posts, notes, videos, and other content on the fediverse. Ensure the URL originates from a federated social network like Mastodon, as other URLs might not function as expected.",
|
||||
"supports": {
|
||||
"html": false
|
||||
"html": false,
|
||||
"inserter": true,
|
||||
"reusable": false,
|
||||
"lock": false
|
||||
},
|
||||
"textdomain": "activitypub",
|
||||
"editorScript": "file:./index.js",
|
||||
"editorStyle": "file:./edit.css",
|
||||
"editorStyle": "file:./style-index.css",
|
||||
"style": "file:./index.css",
|
||||
"attributes": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"embedPost": {
|
||||
"type": "boolean",
|
||||
"default": null
|
||||
}
|
||||
}
|
||||
}
|
1
wp-content/plugins/activitypub/build/reply/index-rtl.css
Normal file
1
wp-content/plugins/activitypub/build/reply/index-rtl.css
Normal file
@ -0,0 +1 @@
|
||||
.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6;border-radius:8px;overflow:hidden}.activitypub-embed-content .ap-preview img{display:block;height:auto;width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta .ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684}.wp-block-activitypub-reply .components-spinner{height:12px;margin-bottom:0;margin-top:0;width:12px}
|
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '808c98599517db815fc5');
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => 'fcd855ff6f64b21029be');
|
||||
|
1
wp-content/plugins/activitypub/build/reply/index.css
Normal file
1
wp-content/plugins/activitypub/build/reply/index.css
Normal file
@ -0,0 +1 @@
|
||||
.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6;border-radius:8px;overflow:hidden}.activitypub-embed-content .ap-preview img{display:block;height:auto;width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta .ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684}.wp-block-activitypub-reply .components-spinner{height:12px;margin-bottom:0;margin-top:0;width:12px}
|
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
.activitypub-embed-container{margin-top:1em;min-height:100px;pointer-events:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.activitypub-embed-loading{align-items:center;display:flex;justify-content:center}.activitypub-embed-container .wp-block-embed{pointer-events:none!important}.activitypub-embed-preview,.activitypub-embed-preview iframe{pointer-events:none}.activitypub-reply-display{margin:1em 0}.activitypub-reply-display p{margin:0}.activitypub-reply-display a{color:#2271b1;text-decoration:none}.activitypub-reply-display a:hover{color:#135e96;text-decoration:underline}
|
@ -0,0 +1 @@
|
||||
.activitypub-embed-container{margin-top:1em;min-height:100px;pointer-events:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.activitypub-embed-loading{align-items:center;display:flex;justify-content:center}.activitypub-embed-container .wp-block-embed{pointer-events:none!important}.activitypub-embed-preview,.activitypub-embed-preview iframe{pointer-events:none}.activitypub-reply-display{margin:1em 0}.activitypub-reply-display p{margin:0}.activitypub-reply-display a{color:#2271b1;text-decoration:none}.activitypub-reply-display a:hover{color:#135e96;text-decoration:underline}
|
@ -9,7 +9,8 @@
|
||||
|
||||
namespace Activitypub\Activity;
|
||||
|
||||
use Activitypub\Link;
|
||||
use Activitypub\Activity\Extended_Object\Event;
|
||||
use Activitypub\Activity\Extended_Object\Place;
|
||||
|
||||
/**
|
||||
* \Activitypub\Activity\Activity implements the common
|
||||
@ -23,6 +24,43 @@ class Activity extends Base_Object {
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
);
|
||||
|
||||
/**
|
||||
* The default types for Activities.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const TYPES = array(
|
||||
'Accept',
|
||||
'Add',
|
||||
'Announce',
|
||||
'Arrive',
|
||||
'Block',
|
||||
'Create',
|
||||
'Delete',
|
||||
'Dislike',
|
||||
'Follow',
|
||||
'Flag',
|
||||
'Ignore',
|
||||
'Invite',
|
||||
'Join',
|
||||
'Leave',
|
||||
'Like',
|
||||
'Listen',
|
||||
'Move',
|
||||
'Offer',
|
||||
'Read',
|
||||
'Reject',
|
||||
'Remove',
|
||||
'TentativeAccept',
|
||||
'TentativeReject',
|
||||
'Travel',
|
||||
'Undo',
|
||||
'Update',
|
||||
'View',
|
||||
);
|
||||
|
||||
/**
|
||||
* The type of the object.
|
||||
*
|
||||
@ -37,10 +75,7 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object-term
|
||||
*
|
||||
* @var string
|
||||
* | Base_Object
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|Base_Object|null
|
||||
*/
|
||||
protected $object;
|
||||
|
||||
@ -52,11 +87,7 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-actor
|
||||
*
|
||||
* @var string
|
||||
* | \ActivityPhp\Type\Extended\AbstractActor
|
||||
* | array<Actor>
|
||||
* | array<Link>
|
||||
* | Link
|
||||
* @var string|array
|
||||
*/
|
||||
protected $actor;
|
||||
|
||||
@ -71,11 +102,7 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-target
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | array<ObjectType>
|
||||
* | Link
|
||||
* | array<Link>
|
||||
* @var string|array
|
||||
*/
|
||||
protected $target;
|
||||
|
||||
@ -87,10 +114,7 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-result
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|Base_Object
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
@ -103,9 +127,6 @@ class Activity extends Base_Object {
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies
|
||||
*
|
||||
* @var array
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
*/
|
||||
protected $replies;
|
||||
|
||||
@ -119,10 +140,7 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-origin
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|array
|
||||
*/
|
||||
protected $origin;
|
||||
|
||||
@ -132,10 +150,7 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-instrument
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|array
|
||||
*/
|
||||
protected $instrument;
|
||||
|
||||
@ -143,53 +158,93 @@ class Activity extends Base_Object {
|
||||
* Set the object and copy Object properties to the Activity.
|
||||
*
|
||||
* Any to, bto, cc, bcc, and audience properties specified on the object
|
||||
* MUST be copied over to the new Create activity by the server.
|
||||
* MUST be copied over to the new "Create" activity by the server.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#object-without-create
|
||||
*
|
||||
* @param array|string|Base_Object|Link|null $data Activity object.
|
||||
*
|
||||
* @return void
|
||||
* @param array|string|Base_Object|Activity|Actor|null $data Activity object.
|
||||
*/
|
||||
public function set_object( $data ) {
|
||||
// Convert array to object.
|
||||
$object = $data;
|
||||
|
||||
// Convert array to appropriate object type.
|
||||
if ( is_array( $data ) ) {
|
||||
$data = self::init_from_array( $data );
|
||||
$type = $data['type'] ?? null;
|
||||
|
||||
if ( in_array( $type, self::TYPES, true ) ) {
|
||||
$object = self::init_from_array( $data );
|
||||
} elseif ( in_array( $type, Actor::TYPES, true ) ) {
|
||||
$object = Actor::init_from_array( $data );
|
||||
} elseif ( in_array( $type, Base_Object::TYPES, true ) ) {
|
||||
switch ( $type ) {
|
||||
case 'Event':
|
||||
$object = Event::init_from_array( $data );
|
||||
break;
|
||||
case 'Place':
|
||||
$object = Place::init_from_array( $data );
|
||||
break;
|
||||
default:
|
||||
$object = Base_Object::init_from_array( $data );
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$object = Generic_Object::init_from_array( $data );
|
||||
}
|
||||
}
|
||||
|
||||
// Set object.
|
||||
$this->set( 'object', $data );
|
||||
$this->set( 'object', $object );
|
||||
$this->pre_fill_activity_from_object();
|
||||
}
|
||||
|
||||
if ( ! is_object( $data ) ) {
|
||||
/**
|
||||
* Fills the Activity with the specified activity object.
|
||||
*/
|
||||
public function pre_fill_activity_from_object() {
|
||||
$object = $this->get_object();
|
||||
|
||||
// Check if `$data` is a URL and use it to generate an ID then.
|
||||
if ( is_string( $object ) && filter_var( $object, FILTER_VALIDATE_URL ) && ! $this->get_id() ) {
|
||||
$this->set( 'id', $object . '#activity-' . strtolower( $this->get_type() ) . '-' . time() );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if `$data` is an object and copy some properties otherwise do nothing.
|
||||
if ( ! is_object( $object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
|
||||
$this->set( $i, $data->get( $i ) );
|
||||
$value = $object->get( $i );
|
||||
if ( $value && ! $this->get( $i ) ) {
|
||||
$this->set( $i, $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $data->get_published() && ! $this->get_published() ) {
|
||||
$this->set( 'published', $data->get_published() );
|
||||
if ( $object->get_published() && ! $this->get_published() ) {
|
||||
$this->set( 'published', $object->get_published() );
|
||||
}
|
||||
|
||||
if ( $data->get_updated() && ! $this->get_updated() ) {
|
||||
$this->set( 'updated', $data->get_updated() );
|
||||
if ( $object->get_updated() && ! $this->get_updated() ) {
|
||||
$this->set( 'updated', $object->get_updated() );
|
||||
}
|
||||
|
||||
if ( $data->get_attributed_to() && ! $this->get_actor() ) {
|
||||
$this->set( 'actor', $data->get_attributed_to() );
|
||||
if ( $object->get_attributed_to() && ! $this->get_actor() ) {
|
||||
$this->set( 'actor', $object->get_attributed_to() );
|
||||
}
|
||||
|
||||
if ( $data->get_in_reply_to() ) {
|
||||
$this->set( 'in_reply_to', $data->get_in_reply_to() );
|
||||
if ( $this->get_type() !== 'Announce' && $object->get_in_reply_to() && ! $this->get_in_reply_to() ) {
|
||||
$this->set( 'in_reply_to', $object->get_in_reply_to() );
|
||||
}
|
||||
|
||||
if ( $data->get_id() && ! $this->get_id() ) {
|
||||
$id = strtok( $data->get_id(), '#' );
|
||||
if ( $data->get_updated() ) {
|
||||
$updated = $data->get_updated();
|
||||
if ( $object->get_id() && ! $this->get_id() ) {
|
||||
$id = strtok( $object->get_id(), '#' );
|
||||
if ( $object->get_updated() ) {
|
||||
$updated = $object->get_updated();
|
||||
} elseif ( $object->get_published() ) {
|
||||
$updated = $object->get_published();
|
||||
} else {
|
||||
$updated = $data->get_published();
|
||||
$updated = time();
|
||||
}
|
||||
$this->set( 'id', $id . '#activity-' . strtolower( $this->get_type() ) . '-' . $updated );
|
||||
}
|
||||
|
@ -43,12 +43,39 @@ class Actor extends Base_Object {
|
||||
'@id' => 'lemmy:moderators',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'alsoKnownAs' => array(
|
||||
'@id' => 'as:alsoKnownAs',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'movedTo' => array(
|
||||
'@id' => 'as:movedTo',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'attributionDomains' => array(
|
||||
'@id' => 'toot:attributionDomains',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods',
|
||||
'discoverable' => 'toot:discoverable',
|
||||
'indexable' => 'toot:indexable',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* The default types for Actors.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const TYPES = array(
|
||||
'Application',
|
||||
'Group',
|
||||
'Organization',
|
||||
'Person',
|
||||
'Service',
|
||||
);
|
||||
|
||||
/**
|
||||
* The type of the object.
|
||||
*
|
||||
@ -62,8 +89,7 @@ class Actor extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#inbox
|
||||
*
|
||||
* @var string
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $inbox;
|
||||
|
||||
@ -73,8 +99,7 @@ class Actor extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#outbox
|
||||
*
|
||||
* @var string
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $outbox;
|
||||
|
||||
@ -175,13 +200,26 @@ class Actor extends Base_Object {
|
||||
protected $manually_approves_followers = false;
|
||||
|
||||
/**
|
||||
* Used to mark an object as containing sensitive content.
|
||||
* Mastodon displays a content warning, requiring users to click
|
||||
* through to view the content.
|
||||
* Domains allowed to use `fediverse:creator` for this actor in
|
||||
* published articles.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#sensitive
|
||||
* @see https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/
|
||||
*
|
||||
* @var boolean
|
||||
* @var array
|
||||
*/
|
||||
protected $sensitive = null;
|
||||
protected $attribution_domains = null;
|
||||
|
||||
/**
|
||||
* The target of the actor.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $moved_to;
|
||||
|
||||
/**
|
||||
* The alsoKnownAs of the actor.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $also_known_as;
|
||||
}
|
||||
|
@ -9,13 +9,6 @@
|
||||
|
||||
namespace Activitypub\Activity;
|
||||
|
||||
use WP_Error;
|
||||
use ReflectionClass;
|
||||
use DateTime;
|
||||
|
||||
use function Activitypub\camel_to_snake_case;
|
||||
use function Activitypub\snake_to_camel_case;
|
||||
|
||||
/**
|
||||
* Base_Object is an implementation of one of the
|
||||
* Activity Streams Core Types.
|
||||
@ -27,8 +20,56 @@ use function Activitypub\snake_to_camel_case;
|
||||
* 'Base_' for this reason.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-core/#object
|
||||
*
|
||||
* @method string|null get_actor() Gets one or more entities that performed or are expected to perform the activity.
|
||||
* @method string|null get_attributed_to() Gets the entity attributed as the original author.
|
||||
* @method array|null get_attachment() Gets the attachment property of the object.
|
||||
* @method array|null get_cc() Gets the secondary recipients of the object.
|
||||
* @method string|null get_content() Gets the content property of the object.
|
||||
* @method array|null get_icon() Gets the icon property of the object.
|
||||
* @method string|null get_id() Gets the object's unique global identifier.
|
||||
* @method array|null get_image() Gets the image property of the object.
|
||||
* @method array|string|null get_in_reply_to() Gets the objects this object is in reply to.
|
||||
* @method string|null get_name() Gets the natural language name of the object.
|
||||
* @method Base_Object|string|null get_object() Gets the direct object of the activity.
|
||||
* @method string|null get_published() Gets the date and time the object was published in ISO 8601 format.
|
||||
* @method string|null get_summary() Gets the natural language summary of the object.
|
||||
* @method array|null get_tag() Gets the tag property of the object.
|
||||
* @method array|string|null get_to() Gets the primary recipients of the object.
|
||||
* @method string get_type() Gets the type of the object.
|
||||
* @method string|null get_updated() Gets the date and time the object was updated in ISO 8601 format.
|
||||
* @method string|null get_url() Gets the URL of the object.
|
||||
*
|
||||
* @method string|array add_cc( string|array $cc ) Adds one or more entities to the secondary audience of the object.
|
||||
* @method string|array add_to( string|array $to ) Adds one or more entities to the primary audience of the object.
|
||||
*
|
||||
* @method Base_Object set_actor( string|array $actor ) Sets one or more entities that performed the activity.
|
||||
* @method Base_Object set_attachment( array $attachment ) Sets the attachment property of the object.
|
||||
* @method Base_Object set_attributed_to( string $attributed_to ) Sets the entity attributed as the original author.
|
||||
* @method Base_Object set_cc( array|string $cc ) Sets the secondary recipients of the object.
|
||||
* @method Base_Object set_content( string $content ) Sets the content property of the object.
|
||||
* @method Base_Object set_content_map( array $content_map ) Sets the content property of the object.
|
||||
* @method Base_Object set_icon( array $icon ) Sets the icon property of the object.
|
||||
* @method Base_Object set_id( string $id ) Sets the object's unique global identifier.
|
||||
* @method Base_Object set_image( array $image ) Sets the image property of the object.
|
||||
* @method Base_Object set_name( string $name ) Sets the natural language name of the object.
|
||||
* @method Base_Object set_origin( string $origin ) Sets the origin property of the object.
|
||||
* @method Base_Object set_published( string $published ) Sets the date and time the object was published in ISO 8601 format.
|
||||
* @method Base_Object set_sensitive( bool $sensitive ) Sets the sensitive property of the object.
|
||||
* @method Base_Object set_summary( string $summary ) Sets the natural language summary of the object.
|
||||
* @method Base_Object set_summary_map( array|null $summary_map ) Sets the summary property of the object.
|
||||
* @method Base_Object set_target( string $target ) Sets the target property of the object.
|
||||
* @method Base_Object set_to( array|string $to ) Sets the primary recipients of the object.
|
||||
* @method Base_Object set_type( string $type ) Sets the type of the object.
|
||||
* @method Base_Object set_updated( string $updated ) Sets the date and time the object was updated in ISO 8601 format.
|
||||
* @method Base_Object set_url( string $url ) Sets the URL of the object.
|
||||
*/
|
||||
class Base_Object extends Generic_Object {
|
||||
/**
|
||||
* The JSON-LD context for the object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
class Base_Object {
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
array(
|
||||
@ -38,13 +79,26 @@ class Base_Object {
|
||||
);
|
||||
|
||||
/**
|
||||
* The object's unique global identifier
|
||||
* The default types for Objects.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#obj-id
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||
*
|
||||
* @var string
|
||||
* @var array
|
||||
*/
|
||||
protected $id;
|
||||
const TYPES = array(
|
||||
'Article',
|
||||
'Audio',
|
||||
'Document',
|
||||
'Event',
|
||||
'Image',
|
||||
'Note',
|
||||
'Page',
|
||||
'Place',
|
||||
'Profile',
|
||||
'Relationship',
|
||||
'Tombstone',
|
||||
'Video',
|
||||
);
|
||||
|
||||
/**
|
||||
* The type of the object.
|
||||
@ -61,12 +115,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attachment
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $attachment;
|
||||
|
||||
@ -77,12 +126,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attributedto
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $attributed_to;
|
||||
|
||||
@ -92,12 +136,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audience
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $audience;
|
||||
|
||||
@ -127,10 +166,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
@ -191,12 +227,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
|
||||
*
|
||||
* @var string
|
||||
* | Image
|
||||
* | Link
|
||||
* | array<Image>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $icon;
|
||||
|
||||
@ -207,12 +238,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image-term
|
||||
*
|
||||
* @var string
|
||||
* | Image
|
||||
* | Link
|
||||
* | array<Image>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
@ -222,12 +248,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-inreplyto
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $in_reply_to;
|
||||
|
||||
@ -237,12 +258,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-location
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $location;
|
||||
|
||||
@ -251,10 +267,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-preview
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $preview;
|
||||
|
||||
@ -286,10 +299,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $summary;
|
||||
|
||||
@ -299,7 +309,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary
|
||||
*
|
||||
* @var array<string>|null
|
||||
* @var string[]|null
|
||||
*/
|
||||
protected $summary_map;
|
||||
|
||||
@ -312,12 +322,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $tag;
|
||||
|
||||
@ -333,11 +338,7 @@ class Base_Object {
|
||||
/**
|
||||
* One or more links to representations of the object.
|
||||
*
|
||||
* @var string
|
||||
* | array<string>
|
||||
* | Link
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
@ -347,12 +348,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-to
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $to;
|
||||
|
||||
@ -362,12 +358,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bto
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $bto;
|
||||
|
||||
@ -377,12 +368,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-cc
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $cc;
|
||||
|
||||
@ -392,12 +378,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bcc
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $bcc;
|
||||
|
||||
@ -443,13 +424,30 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies
|
||||
*
|
||||
* @var string
|
||||
* | Collection
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $replies;
|
||||
|
||||
/**
|
||||
* A Collection containing objects considered to be likes for
|
||||
* this object.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#likes
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $likes;
|
||||
|
||||
/**
|
||||
* A Collection containing objects considered to be shares for
|
||||
* this object.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#shares
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $shares;
|
||||
|
||||
/**
|
||||
* Used to mark an object as containing sensitive content.
|
||||
* Mastodon displays a content warning, requiring users to click
|
||||
@ -459,51 +457,7 @@ class Base_Object {
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $sensitive = false;
|
||||
|
||||
/**
|
||||
* Magic function to implement getter and setter.
|
||||
*
|
||||
* @param string $method The method name.
|
||||
* @param string $params The method params.
|
||||
*/
|
||||
public function __call( $method, $params ) {
|
||||
$var = \strtolower( \substr( $method, 4 ) );
|
||||
|
||||
if ( \strncasecmp( $method, 'get', 3 ) === 0 ) {
|
||||
if ( ! $this->has( $var ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
return $this->$var;
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
|
||||
return $this->set( $var, $params[0] );
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'add', 3 ) === 0 ) {
|
||||
$this->add( $var, $params[0] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic function, to transform the object to string.
|
||||
*
|
||||
* @return string The object id.
|
||||
*/
|
||||
public function __toString() {
|
||||
return $this->to_string();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to transform the object to string.
|
||||
*
|
||||
* @return string The object id.
|
||||
*/
|
||||
public function to_string() {
|
||||
return $this->get_id();
|
||||
}
|
||||
protected $sensitive;
|
||||
|
||||
/**
|
||||
* Generic getter.
|
||||
@ -514,21 +468,10 @@ class Base_Object {
|
||||
*/
|
||||
public function get( $key ) {
|
||||
if ( ! $this->has( $key ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
return new \WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
return call_user_func( array( $this, 'get_' . $key ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the object has a key
|
||||
*
|
||||
* @param string $key The key to check.
|
||||
*
|
||||
* @return boolean True if the object has the key.
|
||||
*/
|
||||
public function has( $key ) {
|
||||
return property_exists( $this, $key );
|
||||
return parent::get( $key );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -541,12 +484,10 @@ class Base_Object {
|
||||
*/
|
||||
public function set( $key, $value ) {
|
||||
if ( ! $this->has( $key ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
return new \WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$this->$key = $value;
|
||||
|
||||
return $this;
|
||||
return parent::set( $key, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -559,188 +500,9 @@ class Base_Object {
|
||||
*/
|
||||
public function add( $key, $value ) {
|
||||
if ( ! $this->has( $key ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
return new \WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
if ( ! isset( $this->$key ) ) {
|
||||
$this->$key = array();
|
||||
}
|
||||
|
||||
$attributes = $this->$key;
|
||||
$attributes[] = $value;
|
||||
|
||||
$this->$key = $attributes;
|
||||
|
||||
return $this->$key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array.
|
||||
*
|
||||
* @param string $json The JSON string.
|
||||
*
|
||||
* @return Base_Object An Object built from the JSON string.
|
||||
*/
|
||||
public static function init_from_json( $json ) {
|
||||
$array = \json_decode( $json, true );
|
||||
|
||||
if ( ! is_array( $array ) ) {
|
||||
$array = array();
|
||||
}
|
||||
|
||||
return self::init_from_array( $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert input array to a Base_Object.
|
||||
*
|
||||
* @param array $data The object array.
|
||||
*
|
||||
* @return Base_Object|WP_Error An Object built from the input array or WP_Error when it's not an array.
|
||||
*/
|
||||
public static function init_from_array( $data ) {
|
||||
if ( ! is_array( $data ) ) {
|
||||
return new WP_Error( 'invalid_array', __( 'Invalid array', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$object = new static();
|
||||
|
||||
foreach ( $data as $key => $value ) {
|
||||
$key = camel_to_snake_case( $key );
|
||||
call_user_func( array( $object, 'set_' . $key ), $value );
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array and pre-fill the object.
|
||||
*
|
||||
* @param string $json The JSON string.
|
||||
*/
|
||||
public function from_json( $json ) {
|
||||
$array = \json_decode( $json, true );
|
||||
|
||||
$this->from_array( $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array and pre-fill the object.
|
||||
*
|
||||
* @param array $data The array.
|
||||
*/
|
||||
public function from_array( $data ) {
|
||||
foreach ( $data as $key => $value ) {
|
||||
if ( $value ) {
|
||||
$key = camel_to_snake_case( $key );
|
||||
call_user_func( array( $this, 'set_' . $key ), $value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Object to an array.
|
||||
*
|
||||
* It tries to get the object attributes if they exist
|
||||
* and falls back to the getters. Empty values are ignored.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return array An array built from the Object.
|
||||
*/
|
||||
public function to_array( $include_json_ld_context = true ) {
|
||||
$array = array();
|
||||
$vars = get_object_vars( $this );
|
||||
|
||||
foreach ( $vars as $key => $value ) {
|
||||
// Ignore all _prefixed keys.
|
||||
if ( '_' === substr( $key, 0, 1 ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If value is empty, try to get it from a getter.
|
||||
if ( ! $value ) {
|
||||
$value = call_user_func( array( $this, 'get_' . $key ) );
|
||||
}
|
||||
|
||||
if ( is_object( $value ) ) {
|
||||
$value = $value->to_array( false );
|
||||
}
|
||||
|
||||
// If value is still empty, ignore it for the array and continue.
|
||||
if ( isset( $value ) ) {
|
||||
$array[ snake_to_camel_case( $key ) ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $include_json_ld_context ) {
|
||||
// Get JsonLD context and move it to '@context' at the top.
|
||||
$array = array_merge( array( '@context' => $this->get_json_ld_context() ), $array );
|
||||
}
|
||||
|
||||
$class = new ReflectionClass( $this );
|
||||
$class = strtolower( $class->getShortName() );
|
||||
|
||||
/**
|
||||
* Filter the array of the ActivityPub object.
|
||||
*
|
||||
* @param array $array The array of the ActivityPub object.
|
||||
* @param string $class The class of the ActivityPub object.
|
||||
* @param int $id The ID of the ActivityPub object.
|
||||
* @param Base_Object $object The ActivityPub object.
|
||||
*
|
||||
* @return array The filtered array of the ActivityPub object.
|
||||
*/
|
||||
$array = \apply_filters( 'activitypub_activity_object_array', $array, $class, $this->id, $this );
|
||||
|
||||
/**
|
||||
* Filter the array of the ActivityPub object by class.
|
||||
*
|
||||
* @param array $array The array of the ActivityPub object.
|
||||
* @param int $id The ID of the ActivityPub object.
|
||||
* @param Base_Object $object The ActivityPub object.
|
||||
*
|
||||
* @return array The filtered array of the ActivityPub object.
|
||||
*/
|
||||
return \apply_filters( "activitypub_activity_{$class}_object_array", $array, $this->id, $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Object to JSON.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return string The JSON string.
|
||||
*/
|
||||
public function to_json( $include_json_ld_context = true ) {
|
||||
$array = $this->to_array( $include_json_ld_context );
|
||||
$options = \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT;
|
||||
|
||||
/**
|
||||
* Options to be passed to json_encode()
|
||||
*
|
||||
* @param int $options The current options flags.
|
||||
*/
|
||||
$options = \apply_filters( 'activitypub_json_encode_options', $options );
|
||||
|
||||
return \wp_json_encode( $array, $options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the keys of the object vars.
|
||||
*
|
||||
* @return array The keys of the object vars.
|
||||
*/
|
||||
public function get_object_var_keys() {
|
||||
return \array_keys( \get_object_vars( $this ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON-LD context of this object.
|
||||
*
|
||||
* @return array $context A compacted JSON-LD context for the ActivityPub object.
|
||||
*/
|
||||
public function get_json_ld_context() {
|
||||
return static::JSON_LD_CONTEXT;
|
||||
return parent::add( $key, $value );
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,325 @@
|
||||
<?php
|
||||
/**
|
||||
* Generic Object.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity;
|
||||
|
||||
use function Activitypub\camel_to_snake_case;
|
||||
use function Activitypub\snake_to_camel_case;
|
||||
|
||||
/**
|
||||
* Generic Object.
|
||||
*
|
||||
* This class is used to create Generic Objects.
|
||||
* It is used to create objects that might be unknown by the plugin but
|
||||
* conform to the ActivityStreams vocabulary.
|
||||
*
|
||||
* @since 5.3.0
|
||||
*/
|
||||
#[\AllowDynamicProperties]
|
||||
class Generic_Object {
|
||||
/**
|
||||
* The JSON-LD context for the object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
);
|
||||
|
||||
/**
|
||||
* The object's unique global identifier
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#obj-id
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* Magic function, to transform the object to string.
|
||||
*
|
||||
* @return string The object id.
|
||||
*/
|
||||
public function __toString() {
|
||||
return $this->to_string();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to transform the object to string.
|
||||
*
|
||||
* @return string The object id.
|
||||
*/
|
||||
public function to_string() {
|
||||
return $this->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic function to implement getter and setter.
|
||||
*
|
||||
* @param string $method The method name.
|
||||
* @param string $params The method params.
|
||||
*/
|
||||
public function __call( $method, $params ) {
|
||||
$var = \strtolower( \substr( $method, 4 ) );
|
||||
|
||||
if ( \strncasecmp( $method, 'get', 3 ) === 0 ) {
|
||||
if ( ! $this->has( $var ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->$var;
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
|
||||
return $this->set( $var, $params[0] );
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'add', 3 ) === 0 ) {
|
||||
return $this->add( $var, $params[0] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic getter.
|
||||
*
|
||||
* @param string $key The key to get.
|
||||
*
|
||||
* @return mixed The value.
|
||||
*/
|
||||
public function get( $key ) {
|
||||
return call_user_func( array( $this, 'get_' . $key ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic setter.
|
||||
*
|
||||
* @param string $key The key to set.
|
||||
* @param string $value The value to set.
|
||||
*
|
||||
* @return mixed The value.
|
||||
*/
|
||||
public function set( $key, $value ) {
|
||||
$this->$key = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic adder.
|
||||
*
|
||||
* @param string $key The key to set.
|
||||
* @param mixed $value The value to add.
|
||||
*
|
||||
* @return mixed The value.
|
||||
*/
|
||||
public function add( $key, $value ) {
|
||||
if ( empty( $value ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $this->$key ) ) {
|
||||
$this->$key = array();
|
||||
}
|
||||
|
||||
if ( is_string( $this->$key ) ) {
|
||||
$this->$key = array( $this->$key );
|
||||
}
|
||||
|
||||
$attributes = $this->$key;
|
||||
|
||||
if ( is_array( $value ) ) {
|
||||
$attributes = array_merge( $attributes, $value );
|
||||
} else {
|
||||
$attributes[] = $value;
|
||||
}
|
||||
|
||||
$this->$key = array_unique( $attributes );
|
||||
|
||||
return $this->$key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the object has a key
|
||||
*
|
||||
* @param string $key The key to check.
|
||||
*
|
||||
* @return boolean True if the object has the key.
|
||||
*/
|
||||
public function has( $key ) {
|
||||
return property_exists( $this, $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array.
|
||||
*
|
||||
* @param string $json The JSON string.
|
||||
*
|
||||
* @return Generic_Object|\WP_Error An Object built from the JSON string or WP_Error when it's not a JSON string.
|
||||
*/
|
||||
public static function init_from_json( $json ) {
|
||||
$array = \json_decode( $json, true );
|
||||
|
||||
if ( ! is_array( $array ) ) {
|
||||
return new \WP_Error( 'invalid_json', __( 'Invalid JSON', 'activitypub' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
return self::init_from_array( $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert input array to a Base_Object.
|
||||
*
|
||||
* @param array $data The object array.
|
||||
*
|
||||
* @return Generic_Object|\WP_Error An Object built from the input array or WP_Error when it's not an array.
|
||||
*/
|
||||
public static function init_from_array( $data ) {
|
||||
if ( ! is_array( $data ) ) {
|
||||
return new \WP_Error( 'invalid_array', __( 'Invalid array', 'activitypub' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$object = new static();
|
||||
$object->from_array( $data );
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array and pre-fill the object.
|
||||
*
|
||||
* @param array $data The array.
|
||||
*/
|
||||
public function from_array( $data ) {
|
||||
foreach ( $data as $key => $value ) {
|
||||
if ( null !== $value ) {
|
||||
$key = camel_to_snake_case( $key );
|
||||
call_user_func( array( $this, 'set_' . $key ), $value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array and pre-fill the object.
|
||||
*
|
||||
* @param string $json The JSON string.
|
||||
*/
|
||||
public function from_json( $json ) {
|
||||
$array = \json_decode( $json, true );
|
||||
|
||||
$this->from_array( $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Object to an array.
|
||||
*
|
||||
* It tries to get the object attributes if they exist
|
||||
* and falls back to the getters. Empty values are ignored.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return array An array built from the Object.
|
||||
*/
|
||||
public function to_array( $include_json_ld_context = true ) {
|
||||
$array = array();
|
||||
$vars = get_object_vars( $this );
|
||||
|
||||
foreach ( $vars as $key => $value ) {
|
||||
if ( \is_wp_error( $value ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore all _prefixed keys.
|
||||
if ( '_' === substr( $key, 0, 1 ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If value is empty, try to get it from a getter.
|
||||
if ( ! $value ) {
|
||||
$value = call_user_func( array( $this, 'get_' . $key ) );
|
||||
}
|
||||
|
||||
if ( is_object( $value ) ) {
|
||||
$value = $value->to_array( false );
|
||||
}
|
||||
|
||||
// If value is still empty, ignore it for the array and continue.
|
||||
if ( isset( $value ) ) {
|
||||
$array[ snake_to_camel_case( $key ) ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $include_json_ld_context ) {
|
||||
// Get JsonLD context and move it to '@context' at the top.
|
||||
$array = array_merge( array( '@context' => $this->get_json_ld_context() ), $array );
|
||||
}
|
||||
|
||||
$class = new \ReflectionClass( $this );
|
||||
$class = strtolower( $class->getShortName() );
|
||||
|
||||
/**
|
||||
* Filter the array of the ActivityPub object.
|
||||
*
|
||||
* @param array $array The array of the ActivityPub object.
|
||||
* @param string $class The class of the ActivityPub object.
|
||||
* @param string $id The ID of the ActivityPub object.
|
||||
* @param Generic_Object $object The ActivityPub object.
|
||||
*
|
||||
* @return array The filtered array of the ActivityPub object.
|
||||
*/
|
||||
$array = \apply_filters( 'activitypub_activity_object_array', $array, $class, $this->id, $this );
|
||||
|
||||
/**
|
||||
* Filter the array of the ActivityPub object by class.
|
||||
*
|
||||
* @param array $array The array of the ActivityPub object.
|
||||
* @param string $id The ID of the ActivityPub object.
|
||||
* @param Generic_Object $object The ActivityPub object.
|
||||
*
|
||||
* @return array The filtered array of the ActivityPub object.
|
||||
*/
|
||||
return \apply_filters( "activitypub_activity_{$class}_object_array", $array, $this->id, $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Object to JSON.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return string The JSON string.
|
||||
*/
|
||||
public function to_json( $include_json_ld_context = true ) {
|
||||
$array = $this->to_array( $include_json_ld_context );
|
||||
$options = \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT | \JSON_UNESCAPED_SLASHES;
|
||||
|
||||
/**
|
||||
* Options to be passed to json_encode().
|
||||
*
|
||||
* @param int $options The current options flags.
|
||||
*/
|
||||
$options = \apply_filters( 'activitypub_json_encode_options', $options );
|
||||
|
||||
return \wp_json_encode( $array, $options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the keys of the object vars.
|
||||
*
|
||||
* @return array The keys of the object vars.
|
||||
*/
|
||||
public function get_object_var_keys() {
|
||||
return \array_keys( \get_object_vars( $this ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON-LD context of this object.
|
||||
*
|
||||
* @return array $context A compacted JSON-LD context for the ActivityPub object.
|
||||
*/
|
||||
public function get_json_ld_context() {
|
||||
return static::JSON_LD_CONTEXT;
|
||||
}
|
||||
}
|
@ -50,7 +50,7 @@ class Event extends Base_Object {
|
||||
);
|
||||
|
||||
/**
|
||||
* Mobilizon compatible values for repliesModertaionOption.
|
||||
* Mobilizon compatible values for repliesModerationOption.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
@ -342,15 +342,15 @@ class Event extends Base_Object {
|
||||
/**
|
||||
* Custom setter for the event category.
|
||||
*
|
||||
* Falls back to Mobilizons default category.
|
||||
* Falls back to Mobilizon's default category.
|
||||
*
|
||||
* @param string $category The category of the event.
|
||||
* @param bool $mobilizon_compatibilty Optional. Whether the category must be compatibly with Mobilizon. Default true.
|
||||
* @param bool $mobilizon_compatibility Optional. Whether the category must be compatibly with Mobilizon. Default true.
|
||||
*
|
||||
* @return Event
|
||||
*/
|
||||
public function set_category( $category, $mobilizon_compatibilty = true ) {
|
||||
if ( $mobilizon_compatibilty ) {
|
||||
public function set_category( $category, $mobilizon_compatibility = true ) {
|
||||
if ( $mobilizon_compatibility ) {
|
||||
$this->category = in_array( $category, self::DEFAULT_EVENT_CATEGORIES, true ) ? $category : 'MEETING';
|
||||
} else {
|
||||
$this->category = $category;
|
||||
|
@ -38,8 +38,8 @@ class Place extends Base_Object {
|
||||
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.
|
||||
* Indicates the altitude of a place. The measurement unit is indicated using the unit's property.
|
||||
* If unit 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
|
||||
|
@ -1,328 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub Activity_Dispatcher Class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Post;
|
||||
use WP_Comment;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Transformer\Factory;
|
||||
|
||||
/**
|
||||
* ActivityPub Activity_Dispatcher Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/
|
||||
*/
|
||||
class Activity_Dispatcher {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_send_post', array( self::class, 'send_post' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_comment', array( self::class, 'send_comment' ), 10, 2 );
|
||||
|
||||
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_update_profile_activity', array( self::class, 'send_profile_update' ) );
|
||||
|
||||
// Default filters to add Inboxes to sent to.
|
||||
\add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_follower' ), 10, 2 );
|
||||
\add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 );
|
||||
\add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Activities to followers and mentioned users or `Announce` (boost) a blog post.
|
||||
*
|
||||
* @param mixed $wp_object The ActivityPub Post.
|
||||
* @param string $type The Activity-Type.
|
||||
*/
|
||||
public static function send_activity_or_announce( $wp_object, $type ) {
|
||||
if ( is_user_type_disabled( 'blog' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_single_user() ) {
|
||||
self::send_activity( $wp_object, $type, Users::BLOG_USER_ID );
|
||||
} else {
|
||||
self::send_announce( $wp_object, $type );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Activities to followers and mentioned users.
|
||||
*
|
||||
* @param mixed $wp_object The ActivityPub Post.
|
||||
* @param string $type The Activity-Type.
|
||||
* @param int $user_id Optional. The WordPress User-ID.
|
||||
*/
|
||||
public static function send_activity( $wp_object, $type, $user_id = null ) {
|
||||
$transformer = Factory::get_transformer( $wp_object ); // Could potentially return a `\WP_Error` instance.
|
||||
|
||||
if ( \is_wp_error( $transformer ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( null !== $user_id ) {
|
||||
$transformer->change_wp_user_id( $user_id );
|
||||
}
|
||||
|
||||
$user_id = $transformer->get_wp_user_id();
|
||||
|
||||
if ( is_user_disabled( $user_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$activity = $transformer->to_activity( $type );
|
||||
|
||||
self::send_activity_to_followers( $activity, $user_id, $wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Announces to followers and mentioned users.
|
||||
*
|
||||
* @param mixed $wp_object The ActivityPub Post.
|
||||
* @param string $type The Activity-Type.
|
||||
*/
|
||||
public static function send_announce( $wp_object, $type ) {
|
||||
if ( ! in_array( $type, array( 'Create', 'Update', 'Delete' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transformer = Factory::get_transformer( $wp_object );
|
||||
|
||||
if ( \is_wp_error( $transformer ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = Users::BLOG_USER_ID;
|
||||
$activity = $transformer->to_activity( $type );
|
||||
$user = Users::get_by_id( Users::BLOG_USER_ID );
|
||||
|
||||
$announce = new Activity();
|
||||
$announce->set_type( 'Announce' );
|
||||
$announce->set_object( $activity );
|
||||
$announce->set_actor( $user->get_id() );
|
||||
|
||||
self::send_activity_to_followers( $announce, $user_id, $wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "Update" Activity when a user updates their profile.
|
||||
*
|
||||
* @param int $user_id The user ID to send an update for.
|
||||
*/
|
||||
public static function send_profile_update( $user_id ) {
|
||||
$user = Users::get_by_various( $user_id );
|
||||
|
||||
// Bail if that's not a good user.
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Build the update.
|
||||
$activity = new Activity();
|
||||
$activity->set_type( 'Update' );
|
||||
$activity->set_actor( $user->get_url() );
|
||||
$activity->set_object( $user->get_url() );
|
||||
$activity->set_to( 'https://www.w3.org/ns/activitystreams#Public' );
|
||||
|
||||
// Send the update.
|
||||
self::send_activity_to_followers( $activity, $user_id, $user );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an Activity to all followers and mentioned users.
|
||||
*
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
* @param int $user_id The user ID.
|
||||
* @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object.
|
||||
*/
|
||||
private static function send_activity_to_followers( $activity, $user_id, $wp_object ) {
|
||||
/**
|
||||
* Filter to prevent sending an Activity to followers.
|
||||
*
|
||||
* @param bool $send_activity_to_followers Whether to send the Activity to followers.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
* @param int $user_id The user ID.
|
||||
* @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object.
|
||||
*/
|
||||
if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $user_id, $wp_object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to modify the Activity before sending it to followers.
|
||||
*
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
* @param int $user_id The user ID.
|
||||
* @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object.
|
||||
*/
|
||||
$inboxes = apply_filters( 'activitypub_send_to_inboxes', array(), $user_id, $activity );
|
||||
$inboxes = array_unique( $inboxes );
|
||||
|
||||
if ( empty( $inboxes ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$json = $activity->to_json();
|
||||
|
||||
foreach ( $inboxes as $inbox ) {
|
||||
safe_remote_post( $inbox, $json, $user_id );
|
||||
}
|
||||
|
||||
set_wp_object_state( $wp_object, 'federated' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "Create" or "Update" Activity for a WordPress Post.
|
||||
*
|
||||
* @param int $id The WordPress Post ID.
|
||||
* @param string $type The Activity-Type.
|
||||
*/
|
||||
public static function send_post( $id, $type ) {
|
||||
$post = get_post( $id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to send an Activity for a Post.
|
||||
*
|
||||
* @param WP_Post $post The WordPress Post.
|
||||
* @param string $type The Activity-Type.
|
||||
*/
|
||||
do_action( 'activitypub_send_activity', $post, $type );
|
||||
|
||||
/**
|
||||
* Action to send a specific Activity for a Post.
|
||||
*
|
||||
* @param WP_Post $post The WordPress Post.
|
||||
*/
|
||||
do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "Create" or "Update" Activity for a WordPress Comment.
|
||||
*
|
||||
* @param int $id The WordPress Comment ID.
|
||||
* @param string $type The Activity-Type.
|
||||
*/
|
||||
public static function send_comment( $id, $type ) {
|
||||
$comment = get_comment( $id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to send an Activity for a Comment.
|
||||
*
|
||||
* @param WP_Comment $comment The WordPress Comment.
|
||||
* @param string $type The Activity-Type.
|
||||
*/
|
||||
do_action( 'activitypub_send_activity', $comment, $type );
|
||||
|
||||
/**
|
||||
* Action to send a specific Activity for a Comment.
|
||||
*
|
||||
* @param WP_Comment $comment The WordPress Comment.
|
||||
*/
|
||||
do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $comment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Default filter to add Inboxes of Followers.
|
||||
*
|
||||
* @param array $inboxes The list of Inboxes.
|
||||
* @param int $user_id The WordPress User-ID.
|
||||
*
|
||||
* @return array The filtered Inboxes
|
||||
*/
|
||||
public static function add_inboxes_of_follower( $inboxes, $user_id ) {
|
||||
$follower_inboxes = Followers::get_inboxes( $user_id );
|
||||
|
||||
return array_merge( $inboxes, $follower_inboxes );
|
||||
}
|
||||
|
||||
/**
|
||||
* Default filter to add Inboxes of Mentioned Actors
|
||||
*
|
||||
* @param array $inboxes The list of Inboxes.
|
||||
* @param int $user_id The WordPress User-ID.
|
||||
* @param array $activity The ActivityPub Activity.
|
||||
*
|
||||
* @return array The filtered Inboxes.
|
||||
*/
|
||||
public static function add_inboxes_by_mentioned_actors( $inboxes, $user_id, $activity ) {
|
||||
$cc = $activity->get_cc();
|
||||
if ( $cc ) {
|
||||
$mentioned_inboxes = Mention::get_inboxes( $cc );
|
||||
|
||||
return array_merge( $inboxes, $mentioned_inboxes );
|
||||
}
|
||||
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default filter to add Inboxes of Posts that are set as `in-reply-to`
|
||||
*
|
||||
* @param array $inboxes The list of Inboxes.
|
||||
* @param int $user_id The WordPress User-ID.
|
||||
* @param array $activity The ActivityPub Activity.
|
||||
*
|
||||
* @return array The filtered Inboxes
|
||||
*/
|
||||
public static function add_inboxes_of_replied_urls( $inboxes, $user_id, $activity ) {
|
||||
$in_reply_to = $activity->get_in_reply_to();
|
||||
|
||||
if ( ! $in_reply_to ) {
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
if ( ! is_array( $in_reply_to ) ) {
|
||||
$in_reply_to = array( $in_reply_to );
|
||||
}
|
||||
|
||||
foreach ( $in_reply_to as $url ) {
|
||||
$object = Http::get_remote_object( $url );
|
||||
|
||||
if (
|
||||
! $object ||
|
||||
\is_wp_error( $object ) ||
|
||||
empty( $object['attributedTo'] )
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$actor = object_to_uri( $object['attributedTo'] );
|
||||
$actor = Http::get_remote_object( $actor );
|
||||
|
||||
if ( ! $actor || \is_wp_error( $actor ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! empty( $actor['endpoints']['sharedInbox'] ) ) {
|
||||
$inboxes[] = $actor['endpoints']['sharedInbox'];
|
||||
} elseif ( ! empty( $actor['inbox'] ) ) {
|
||||
$inboxes[] = $actor['inbox'];
|
||||
}
|
||||
}
|
||||
|
||||
return $inboxes;
|
||||
}
|
||||
}
|
@ -8,6 +8,8 @@
|
||||
namespace Activitypub;
|
||||
|
||||
use Exception;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Outbox;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Extra_Fields;
|
||||
|
||||
@ -21,13 +23,15 @@ class Activitypub {
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'template_include', array( self::class, 'render_json_template' ), 99 );
|
||||
\add_filter( 'template_include', array( self::class, 'render_activitypub_template' ), 99 );
|
||||
\add_action( 'template_redirect', array( self::class, 'template_redirect' ) );
|
||||
\add_filter( 'redirect_canonical', array( self::class, 'redirect_canonical' ), 10, 2 );
|
||||
\add_filter( 'redirect_canonical', array( self::class, 'no_trailing_redirect' ), 10, 2 );
|
||||
\add_filter( 'query_vars', array( self::class, 'add_query_vars' ) );
|
||||
\add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
|
||||
|
||||
// Add support for ActivityPub to custom post types.
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post' ) ) : array();
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
\add_post_type_support( $post_type, 'activitypub' );
|
||||
@ -41,16 +45,18 @@ class Activitypub {
|
||||
|
||||
\add_action( 'user_register', array( self::class, 'user_register' ) );
|
||||
|
||||
\add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) );
|
||||
|
||||
if ( site_supports_blocks() ) {
|
||||
\add_action( 'tool_box', array( self::class, 'tool_box' ) );
|
||||
}
|
||||
|
||||
\add_filter( 'activitypub_get_actor_extra_fields', array( Extra_Fields::class, 'default_actor_extra_fields' ), 10, 2 );
|
||||
|
||||
\add_action( 'updated_postmeta', array( self::class, 'updated_postmeta' ), 10, 4 );
|
||||
\add_action( 'added_post_meta', array( self::class, 'updated_postmeta' ), 10, 4 );
|
||||
|
||||
\add_action( 'init', array( self::class, 'register_user_meta' ), 11 );
|
||||
|
||||
// Register several post_types.
|
||||
self::register_post_types();
|
||||
|
||||
self::register_oembed_providers();
|
||||
Embed::init();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -59,6 +65,9 @@ class Activitypub {
|
||||
public static function activate() {
|
||||
self::flush_rewrite_rules();
|
||||
Scheduler::register_schedules();
|
||||
|
||||
\add_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 10, 3 );
|
||||
Migration::update_comment_counts();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -67,6 +76,9 @@ class Activitypub {
|
||||
public static function deactivate() {
|
||||
self::flush_rewrite_rules();
|
||||
Scheduler::deregister_schedules();
|
||||
|
||||
\remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) );
|
||||
Migration::update_comment_counts( 2000 );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -74,6 +86,43 @@ class Activitypub {
|
||||
*/
|
||||
public static function uninstall() {
|
||||
Scheduler::deregister_schedules();
|
||||
|
||||
\remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) );
|
||||
Migration::update_comment_counts( 2000 );
|
||||
|
||||
\delete_option( 'activitypub_actor_mode' );
|
||||
\delete_option( 'activitypub_allow_likes' );
|
||||
\delete_option( 'activitypub_allow_replies' );
|
||||
\delete_option( 'activitypub_attribution_domains' );
|
||||
\delete_option( 'activitypub_authorized_fetch' );
|
||||
\delete_option( 'activitypub_application_user_private_key' );
|
||||
\delete_option( 'activitypub_application_user_public_key' );
|
||||
\delete_option( 'activitypub_blog_user_also_known_as' );
|
||||
\delete_option( 'activitypub_blog_user_mailer_new_dm' );
|
||||
\delete_option( 'activitypub_blog_user_mailer_new_follower' );
|
||||
\delete_option( 'activitypub_blog_user_mailer_new_mention' );
|
||||
\delete_option( 'activitypub_blog_user_moved_to' );
|
||||
\delete_option( 'activitypub_blog_user_private_key' );
|
||||
\delete_option( 'activitypub_blog_user_public_key' );
|
||||
\delete_option( 'activitypub_blog_description' );
|
||||
\delete_option( 'activitypub_blog_identifier' );
|
||||
\delete_option( 'activitypub_custom_post_content' );
|
||||
\delete_option( 'activitypub_db_version' );
|
||||
\delete_option( 'activitypub_default_extra_fields' );
|
||||
\delete_option( 'activitypub_enable_blog_user' );
|
||||
\delete_option( 'activitypub_enable_users' );
|
||||
\delete_option( 'activitypub_header_image' );
|
||||
\delete_option( 'activitypub_last_post_with_permalink_as_id' );
|
||||
\delete_option( 'activitypub_max_image_attachments' );
|
||||
\delete_option( 'activitypub_migration_lock' );
|
||||
\delete_option( 'activitypub_object_type' );
|
||||
\delete_option( 'activitypub_outbox_purge_days' );
|
||||
\delete_option( 'activitypub_shared_inbox' );
|
||||
\delete_option( 'activitypub_support_post_types' );
|
||||
\delete_option( 'activitypub_use_hashtags' );
|
||||
\delete_option( 'activitypub_use_opengraph' );
|
||||
\delete_option( 'activitypub_use_permalink_as_id_for_blog' );
|
||||
\delete_option( 'activitypub_vary_header' );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -83,25 +132,33 @@ class Activitypub {
|
||||
*
|
||||
* @return string The new path to the JSON template.
|
||||
*/
|
||||
public static function render_json_template( $template ) {
|
||||
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
|
||||
public static function render_activitypub_template( $template ) {
|
||||
if ( \wp_is_serving_rest_request() || \wp_doing_ajax() ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
self::add_headers();
|
||||
|
||||
if ( ! is_activitypub_request() ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
$json_template = false;
|
||||
$activitypub_template = false;
|
||||
$activitypub_object = Query::get_instance()->get_activitypub_object();
|
||||
|
||||
if ( \is_author() && ! is_user_disabled( \get_the_author_meta( 'ID' ) ) ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/user-json.php';
|
||||
} elseif ( is_comment() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/comment-json.php';
|
||||
} elseif ( \is_singular() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
|
||||
} elseif ( \is_home() && ! is_user_type_disabled( 'blog' ) ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php';
|
||||
if ( $activitypub_object ) {
|
||||
if ( \get_query_var( 'preview' ) ) {
|
||||
\define( 'ACTIVITYPUB_PREVIEW', true );
|
||||
|
||||
/**
|
||||
* Filter the template used for the ActivityPub preview.
|
||||
*
|
||||
* @param string $activitypub_template Absolute path to the template file.
|
||||
*/
|
||||
$activitypub_template = apply_filters( 'activitypub_preview_template', ACTIVITYPUB_PLUGIN_DIR . '/templates/post-preview.php' );
|
||||
} else {
|
||||
$activitypub_template = ACTIVITYPUB_PLUGIN_DIR . 'templates/activitypub-json.php';
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -110,7 +167,7 @@ class Activitypub {
|
||||
* @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
|
||||
* @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
|
||||
*/
|
||||
if ( $json_template && ACTIVITYPUB_AUTHORIZED_FETCH ) {
|
||||
if ( $activitypub_template && use_authorized_fetch() ) {
|
||||
$verification = Signature::verify_http_signature( $_SERVER );
|
||||
if ( \is_wp_error( $verification ) ) {
|
||||
header( 'HTTP/1.1 401 Unauthorized' );
|
||||
@ -120,8 +177,16 @@ class Activitypub {
|
||||
}
|
||||
}
|
||||
|
||||
if ( $json_template ) {
|
||||
return $json_template;
|
||||
if ( $activitypub_template ) {
|
||||
\set_query_var( 'is_404', false );
|
||||
|
||||
// Check if header already sent.
|
||||
if ( ! \headers_sent() ) {
|
||||
// Send 200 status header.
|
||||
\status_header( 200 );
|
||||
}
|
||||
|
||||
return $activitypub_template;
|
||||
}
|
||||
|
||||
return $template;
|
||||
@ -131,69 +196,98 @@ class Activitypub {
|
||||
* Add the 'self' link to the header.
|
||||
*/
|
||||
public static function add_headers() {
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
|
||||
$request_uri = $_SERVER['REQUEST_URI'];
|
||||
$id = Query::get_instance()->get_activitypub_object_id();
|
||||
|
||||
if ( ! $request_uri ) {
|
||||
if ( ! $id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only add self link to author pages...
|
||||
if ( is_author() ) {
|
||||
if ( is_user_disabled( get_queried_object_id() ) ) {
|
||||
return;
|
||||
}
|
||||
} elseif ( is_singular() ) { // or posts/pages/custom-post-types...
|
||||
if ( ! \post_type_supports( \get_post_type(), 'activitypub' ) ) {
|
||||
return;
|
||||
}
|
||||
} else { // otherwise return.
|
||||
return;
|
||||
}
|
||||
|
||||
// Add self link to html and http header.
|
||||
$host = wp_parse_url( home_url() );
|
||||
|
||||
/**
|
||||
* Filters the self link.
|
||||
*
|
||||
* @param string $self_link The self link.
|
||||
*/
|
||||
$self_link = apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . wp_unslash( $request_uri ) ) );
|
||||
$self_link = esc_url( $self_link );
|
||||
|
||||
if ( ! headers_sent() ) {
|
||||
header( 'Link: <' . $self_link . '>; rel="alternate"; type="application/activity+json"' );
|
||||
\header( 'Link: <' . esc_url( $id ) . '>; title="ActivityPub (JSON)"; rel="alternate"; type="application/activity+json"', false );
|
||||
|
||||
if ( \get_option( 'activitypub_vary_header' ) ) {
|
||||
// Send Vary header for Accept header.
|
||||
\header( 'Vary: Accept', false );
|
||||
}
|
||||
}
|
||||
|
||||
add_action(
|
||||
'wp_head',
|
||||
function () use ( $self_link ) {
|
||||
echo PHP_EOL . '<link rel="alternate" type="application/activity+json" href="' . esc_url( $self_link ) . '" />' . PHP_EOL;
|
||||
function () use ( $id ) {
|
||||
echo PHP_EOL . '<link rel="alternate" title="ActivityPub (JSON)" type="application/activity+json" href="' . esc_url( $id ) . '" />' . PHP_EOL;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove trailing slash from ActivityPub @username requests.
|
||||
*
|
||||
* @param string $redirect_url The URL to redirect to.
|
||||
* @param string $requested_url The requested URL.
|
||||
*
|
||||
* @return string $redirect_url The possibly-unslashed redirect URL.
|
||||
*/
|
||||
public static function no_trailing_redirect( $redirect_url, $requested_url ) {
|
||||
if ( get_query_var( 'actor' ) ) {
|
||||
return $requested_url;
|
||||
}
|
||||
|
||||
return $redirect_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add support for `p` and `author` query vars.
|
||||
*
|
||||
* @param string $redirect_url The URL to redirect to.
|
||||
* @param string $requested_url The requested URL.
|
||||
*
|
||||
* @return string $redirect_url
|
||||
*/
|
||||
public static function redirect_canonical( $redirect_url, $requested_url ) {
|
||||
if ( ! is_activitypub_request() ) {
|
||||
return $redirect_url;
|
||||
}
|
||||
|
||||
$query = \wp_parse_url( $requested_url, PHP_URL_QUERY );
|
||||
|
||||
if ( ! $query ) {
|
||||
return $redirect_url;
|
||||
}
|
||||
|
||||
$query_params = \wp_parse_args( $query );
|
||||
unset( $query_params['activitypub'] );
|
||||
|
||||
if ( 1 !== count( $query_params ) ) {
|
||||
return $redirect_url;
|
||||
}
|
||||
|
||||
if ( isset( $query_params['p'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( isset( $query_params['author'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $requested_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom redirects for ActivityPub requests.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function template_redirect() {
|
||||
self::add_headers();
|
||||
global $wp_query;
|
||||
|
||||
$comment_id = get_query_var( 'c', null );
|
||||
|
||||
// Check if it seems to be a comment.
|
||||
if ( ! $comment_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $comment_id ) {
|
||||
$comment = get_comment( $comment_id );
|
||||
|
||||
// Load a 404 page if `c` is set but not valid.
|
||||
// Load a 404-page if `c` is set but not valid.
|
||||
if ( ! $comment ) {
|
||||
global $wp_query;
|
||||
$wp_query->set_404();
|
||||
return;
|
||||
}
|
||||
@ -207,6 +301,29 @@ class Activitypub {
|
||||
exit;
|
||||
}
|
||||
|
||||
$actor = get_query_var( 'actor', null );
|
||||
if ( $actor ) {
|
||||
$actor = Actors::get_by_username( $actor );
|
||||
if ( ! $actor || \is_wp_error( $actor ) ) {
|
||||
$wp_query->set_404();
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_activitypub_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $actor->get__id() > 0 ) {
|
||||
$redirect_url = $actor->get_url();
|
||||
} else {
|
||||
$redirect_url = get_bloginfo( 'url' );
|
||||
}
|
||||
|
||||
wp_safe_redirect( $redirect_url, 301 );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the 'activitypub' query variable so WordPress won't mangle it.
|
||||
*
|
||||
@ -216,6 +333,9 @@ class Activitypub {
|
||||
*/
|
||||
public static function add_query_vars( $vars ) {
|
||||
$vars[] = 'activitypub';
|
||||
$vars[] = 'preview';
|
||||
$vars[] = 'author';
|
||||
$vars[] = 'actor';
|
||||
$vars[] = 'c';
|
||||
$vars[] = 'p';
|
||||
|
||||
@ -254,7 +374,7 @@ class Activitypub {
|
||||
}
|
||||
|
||||
// Check if comment has an avatar.
|
||||
$avatar = self::get_avatar_url( $id_or_email->comment_ID );
|
||||
$avatar = \get_comment_meta( $id_or_email->comment_ID, 'avatar_url', true );
|
||||
|
||||
if ( $avatar ) {
|
||||
if ( empty( $args['class'] ) ) {
|
||||
@ -272,20 +392,6 @@ class Activitypub {
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to retrieve Avatar URL if stored in meta.
|
||||
*
|
||||
* @param int|\WP_Comment $comment The comment ID or object.
|
||||
*
|
||||
* @return string The Avatar URL.
|
||||
*/
|
||||
public static function get_avatar_url( $comment ) {
|
||||
if ( \is_numeric( $comment ) ) {
|
||||
$comment = \get_comment( $comment );
|
||||
}
|
||||
return \get_comment_meta( $comment->comment_ID, 'avatar_url', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Store permalink in meta, to send delete Activity.
|
||||
*
|
||||
@ -294,7 +400,7 @@ class Activitypub {
|
||||
public static function trash_post( $post_id ) {
|
||||
\add_post_meta(
|
||||
$post_id,
|
||||
'activitypub_canonical_url',
|
||||
'_activitypub_canonical_url',
|
||||
\get_permalink( $post_id ),
|
||||
true
|
||||
);
|
||||
@ -306,7 +412,7 @@ class Activitypub {
|
||||
* @param string $post_id The Post ID.
|
||||
*/
|
||||
public static function untrash_post( $post_id ) {
|
||||
\delete_post_meta( $post_id, 'activitypub_canonical_url' );
|
||||
\delete_post_meta( $post_id, '_activitypub_canonical_url' );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -332,22 +438,12 @@ class Activitypub {
|
||||
if ( ! \class_exists( 'Nodeinfo_Endpoint' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
|
||||
\add_rewrite_rule(
|
||||
'^.well-known/nodeinfo',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo/discovery',
|
||||
'top'
|
||||
);
|
||||
\add_rewrite_rule(
|
||||
'^.well-known/x-nodeinfo2',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo',
|
||||
'top'
|
||||
);
|
||||
}
|
||||
|
||||
\add_rewrite_rule(
|
||||
'^@([\w\-\.]+)',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/$matches[1]',
|
||||
'top'
|
||||
);
|
||||
|
||||
\add_rewrite_rule( '^@([\w\-\.]+)\/?$', 'index.php?actor=$matches[1]', 'top' );
|
||||
\add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
|
||||
}
|
||||
|
||||
@ -359,15 +455,6 @@ class Activitypub {
|
||||
\flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds metabox on wp-admin/tools.php.
|
||||
*/
|
||||
public static function tool_box() {
|
||||
if ( \current_user_can( 'edit_posts' ) ) {
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/toolbox.php' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme compatibility stuff.
|
||||
*/
|
||||
@ -391,31 +478,7 @@ class Activitypub {
|
||||
}
|
||||
|
||||
/**
|
||||
* Display plugin upgrade notice to users.
|
||||
*
|
||||
* @param array $data The plugin data.
|
||||
*/
|
||||
public static function plugin_update_message( $data ) {
|
||||
if ( ! isset( $data['upgrade_notice'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf(
|
||||
'<div class="update-message">%s</div>',
|
||||
wp_kses(
|
||||
wpautop( $data['upgrade_notice '] ),
|
||||
array(
|
||||
'p' => array(),
|
||||
'a' => array( 'href', 'title' ),
|
||||
'strong' => array(),
|
||||
'em' => array(),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the "Followers" Taxonomy.
|
||||
* Register Custom Post Types.
|
||||
*/
|
||||
private static function register_post_types() {
|
||||
\register_post_type(
|
||||
@ -437,7 +500,7 @@ class Activitypub {
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_inbox',
|
||||
'_activitypub_inbox',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
@ -447,7 +510,7 @@ class Activitypub {
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_errors',
|
||||
'_activitypub_errors',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => false,
|
||||
@ -463,7 +526,7 @@ class Activitypub {
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_user_id',
|
||||
'_activitypub_user_id',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => false,
|
||||
@ -475,7 +538,7 @@ class Activitypub {
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_actor_json',
|
||||
'_activitypub_actor_json',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
@ -485,6 +548,128 @@ class Activitypub {
|
||||
)
|
||||
);
|
||||
|
||||
// Register Outbox Post-Type.
|
||||
register_post_type(
|
||||
Outbox::POST_TYPE,
|
||||
array(
|
||||
'labels' => array(
|
||||
'name' => _x( 'Outbox', 'post_type plural name', 'activitypub' ),
|
||||
'singular_name' => _x( 'Outbox Item', 'post_type single name', 'activitypub' ),
|
||||
),
|
||||
'capabilities' => array(
|
||||
'create_posts' => false,
|
||||
),
|
||||
'map_meta_cap' => true,
|
||||
'public' => false,
|
||||
'show_in_rest' => true,
|
||||
'rewrite' => false,
|
||||
'query_var' => false,
|
||||
'supports' => array( 'title', 'editor', 'author', 'custom-fields' ),
|
||||
'delete_with_user' => true,
|
||||
'can_export' => true,
|
||||
'exclude_from_search' => true,
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Register Activity Type meta for Outbox items.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
|
||||
*/
|
||||
\register_post_meta(
|
||||
Outbox::POST_TYPE,
|
||||
'_activitypub_activity_type',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => 'The type of the activity',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
$value = ucfirst( strtolower( $value ) );
|
||||
$schema = array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'Accept', 'Add', 'Announce', 'Arrive', 'Block', 'Create', 'Delete', 'Dislike', 'Flag', 'Follow', 'Ignore', 'Invite', 'Join', 'Leave', 'Like', 'Listen', 'Move', 'Offer', 'Question', 'Reject', 'Read', 'Remove', 'TentativeReject', 'TentativeAccept', 'Travel', 'Undo', 'Update', 'View' ),
|
||||
'default' => 'Announce',
|
||||
);
|
||||
|
||||
if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
|
||||
return $schema['default'];
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Outbox::POST_TYPE,
|
||||
'_activitypub_activity_actor',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
$schema = array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'application', 'blog', 'user' ),
|
||||
'default' => 'user',
|
||||
);
|
||||
|
||||
if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
|
||||
return $schema['default'];
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Outbox::POST_TYPE,
|
||||
'_activitypub_outbox_offset',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'single' => true,
|
||||
'description' => 'Keeps track of the followers offset when processing outbox items.',
|
||||
'sanitize_callback' => 'absint',
|
||||
'default' => 0,
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Outbox::POST_TYPE,
|
||||
'_activitypub_object_id',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
'description' => 'The ID (ActivityPub URI) of the object that the outbox item is about.',
|
||||
'sanitize_callback' => 'sanitize_url',
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Outbox::POST_TYPE,
|
||||
'activitypub_content_visibility',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
$schema = array(
|
||||
'type' => 'string',
|
||||
'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
|
||||
'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
|
||||
);
|
||||
|
||||
if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
|
||||
return $schema['default'];
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
// Both User and Blog Extra Fields types have the same args.
|
||||
$args = array(
|
||||
'labels' => array(
|
||||
@ -515,6 +700,9 @@ class Activitypub {
|
||||
\register_post_type( Extra_Fields::USER_POST_TYPE, $args );
|
||||
\register_post_type( Extra_Fields::BLOG_POST_TYPE, $args );
|
||||
|
||||
/**
|
||||
* Fires after ActivityPub custom post types have been registered.
|
||||
*/
|
||||
\do_action( 'activitypub_after_register_post_type' );
|
||||
}
|
||||
|
||||
@ -529,4 +717,182 @@ class Activitypub {
|
||||
$user->add_cap( 'activitypub' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete `activitypub_content_visibility` when updated to an empty value.
|
||||
*
|
||||
* @param int $meta_id ID of updated metadata entry.
|
||||
* @param int $object_id Post ID.
|
||||
* @param string $meta_key Metadata key.
|
||||
* @param mixed $meta_value Metadata value. This will be a PHP-serialized string representation of the value
|
||||
* if the value is an array, an object, or itself a PHP-serialized string.
|
||||
*/
|
||||
public static function updated_postmeta( $meta_id, $object_id, $meta_key, $meta_value ) {
|
||||
if ( 'activitypub_content_visibility' === $meta_key && empty( $meta_value ) ) {
|
||||
\delete_post_meta( $object_id, 'activitypub_content_visibility' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register some Mastodon oEmbed providers.
|
||||
*/
|
||||
public static function register_oembed_providers() {
|
||||
\wp_oembed_add_provider( '#https?://mastodon\.social/(@.+)/([0-9]+)#i', 'https://mastodon.social/api/oembed', true );
|
||||
\wp_oembed_add_provider( '#https?://mastodon\.online/(@.+)/([0-9]+)#i', 'https://mastodon.online/api/oembed', true );
|
||||
\wp_oembed_add_provider( '#https?://mastodon\.cloud/(@.+)/([0-9]+)#i', 'https://mastodon.cloud/api/oembed', true );
|
||||
\wp_oembed_add_provider( '#https?://mstdn\.social/(@.+)/([0-9]+)#i', 'https://mstdn.social/api/oembed', true );
|
||||
\wp_oembed_add_provider( '#https?://mastodon\.world/(@.+)/([0-9]+)#i', 'https://mastodon.world/api/oembed', true );
|
||||
\wp_oembed_add_provider( '#https?://mas\.to/(@.+)/([0-9]+)#i', 'https://mas.to/api/oembed', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register user meta.
|
||||
*/
|
||||
public static function register_user_meta() {
|
||||
$blog_prefix = $GLOBALS['wpdb']->get_blog_prefix();
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_also_known_as',
|
||||
array(
|
||||
'type' => 'array',
|
||||
'description' => 'An array of URLs that the user is known by.',
|
||||
'single' => true,
|
||||
'default' => array(),
|
||||
'sanitize_callback' => array( Sanitize::class, 'url_list' ),
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_old_host_data',
|
||||
array(
|
||||
'description' => 'Actor object for the user on the old host.',
|
||||
'single' => true,
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_moved_to',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => 'The new URL of the user.',
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'sanitize_url',
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_description',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => 'The user’s description.',
|
||||
'single' => true,
|
||||
'default' => '',
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
return wp_kses( $value, 'user_description' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_icon',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'The attachment ID for user’s profile image.',
|
||||
'single' => true,
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_header_image',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'The attachment ID for the user’s header image.',
|
||||
'single' => true,
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_mailer_new_dm',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Send a notification when someone sends this user a direct message.',
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
\add_filter( 'get_user_option_activitypub_mailer_new_dm', array( self::class, 'user_options_default' ) );
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_mailer_new_follower',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Send a notification when someone starts to follow this user.',
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
\add_filter( 'get_user_option_activitypub_mailer_new_follower', array( self::class, 'user_options_default' ) );
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_mailer_new_mention',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Send a notification when someone mentions this user.',
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
\add_filter( 'get_user_option_activitypub_mailer_new_mention', array( self::class, 'user_options_default' ) );
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
'activitypub_show_welcome_tab',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Whether to show the welcome tab.',
|
||||
'single' => true,
|
||||
'default' => 1,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
'activitypub_show_advanced_tab',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Whether to show the advanced tab.',
|
||||
'single' => true,
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default values for user options.
|
||||
*
|
||||
* @param bool|string $value Option value.
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function user_options_default( $value ) {
|
||||
if ( false === $value ) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
@ -1,784 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin Class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_User_Query;
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Extra_Fields;
|
||||
|
||||
/**
|
||||
* ActivityPub Admin Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Admin {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks,
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'admin_menu', array( self::class, 'admin_menu' ) );
|
||||
\add_action( 'admin_init', array( self::class, 'register_settings' ) );
|
||||
\add_action( 'load-comment.php', array( self::class, 'edit_comment' ) );
|
||||
\add_action( 'load-post.php', array( self::class, 'edit_post' ) );
|
||||
\add_action( 'load-edit.php', array( self::class, 'list_posts' ) );
|
||||
\add_action( 'personal_options_update', array( self::class, 'save_user_settings' ) );
|
||||
\add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
|
||||
\add_action( 'admin_notices', array( self::class, 'admin_notices' ) );
|
||||
|
||||
\add_filter( 'comment_row_actions', array( self::class, 'comment_row_actions' ), 10, 2 );
|
||||
\add_filter( 'manage_edit-comments_columns', array( static::class, 'manage_comment_columns' ) );
|
||||
\add_action( 'manage_comments_custom_column', array( static::class, 'manage_comments_custom_column' ), 9, 2 );
|
||||
|
||||
\add_filter( 'manage_posts_columns', array( static::class, 'manage_post_columns' ), 10, 2 );
|
||||
\add_action( 'manage_posts_custom_column', array( self::class, 'manage_posts_custom_column' ), 10, 2 );
|
||||
|
||||
\add_filter( 'manage_users_columns', array( self::class, 'manage_users_columns' ) );
|
||||
\add_action( 'manage_users_custom_column', array( self::class, 'manage_users_custom_column' ), 10, 3 );
|
||||
\add_filter( 'bulk_actions-users', array( self::class, 'user_bulk_options' ) );
|
||||
\add_filter( 'handle_bulk_actions-users', array( self::class, 'handle_bulk_request' ), 10, 3 );
|
||||
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
\add_action( 'show_user_profile', array( self::class, 'add_profile' ) );
|
||||
}
|
||||
|
||||
\add_filter( 'dashboard_glance_items', array( self::class, 'dashboard_glance_items' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add admin menu entry.
|
||||
*/
|
||||
public static function admin_menu() {
|
||||
$settings_page = \add_options_page(
|
||||
'Welcome',
|
||||
'ActivityPub',
|
||||
'manage_options',
|
||||
'activitypub',
|
||||
array( self::class, 'settings_page' )
|
||||
);
|
||||
|
||||
\add_action(
|
||||
'load-' . $settings_page,
|
||||
array( self::class, 'add_settings_help_tab' )
|
||||
);
|
||||
|
||||
// User has to be able to publish posts.
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
$followers_list_page = \add_users_page(
|
||||
\__( '⁂ Followers', 'activitypub' ),
|
||||
\__( '⁂ Followers', 'activitypub' ),
|
||||
'read',
|
||||
'activitypub-followers-list',
|
||||
array(
|
||||
self::class,
|
||||
'followers_list_page',
|
||||
)
|
||||
);
|
||||
|
||||
\add_action(
|
||||
'load-' . $followers_list_page,
|
||||
array( self::class, 'add_followers_list_help_tab' )
|
||||
);
|
||||
|
||||
\add_users_page(
|
||||
\__( '⁂ Extra Fields', 'activitypub' ),
|
||||
\__( '⁂ Extra Fields', 'activitypub' ),
|
||||
'read',
|
||||
\esc_url( \admin_url( '/edit.php?post_type=ap_extrafield' ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display admin menu notices about configuration problems or conflicts.
|
||||
*/
|
||||
public static function admin_notices() {
|
||||
$permalink_structure = \get_option( 'permalink_structure' );
|
||||
if ( empty( $permalink_structure ) ) {
|
||||
$admin_notice = \__( 'You are using the ActivityPub plugin with a permalink structure of "plain". This will prevent ActivityPub from working. Please go to "Settings" / "Permalinks" and choose a permalink structure other than "plain".', 'activitypub' );
|
||||
self::show_admin_notice( $admin_notice, 'error' );
|
||||
}
|
||||
|
||||
$current_screen = get_current_screen();
|
||||
if ( ! $current_screen ) {
|
||||
return;
|
||||
}
|
||||
if ( 'edit' === $current_screen->base && Extra_Fields::is_extra_fields_post_type( $current_screen->post_type ) ) {
|
||||
?>
|
||||
<div class="notice" style="margin: 0; background: none; border: none; box-shadow: none; padding: 15px 0 0 0; font-size: 14px;">
|
||||
<?php
|
||||
esc_html_e( 'These are extra fields that are used for your ActivityPub profile. You can use your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' );
|
||||
?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display one admin menu notice about configuration problems or conflicts.
|
||||
*
|
||||
* @param string $admin_notice The notice to display.
|
||||
* @param string $level The level of the notice (error, warning, success, info).
|
||||
*/
|
||||
private static function show_admin_notice( $admin_notice, $level ) {
|
||||
?>
|
||||
|
||||
<div class="notice notice-<?php echo esc_attr( $level ); ?>">
|
||||
<p><?php echo wp_kses( $admin_notice, 'data' ); ?></p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings page.
|
||||
*/
|
||||
public static function settings_page() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( empty( $_GET['tab'] ) ) {
|
||||
$tab = 'welcome';
|
||||
} else {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$tab = sanitize_key( $_GET['tab'] );
|
||||
}
|
||||
|
||||
switch ( $tab ) {
|
||||
case 'settings':
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' );
|
||||
break;
|
||||
case 'blog-profile':
|
||||
wp_enqueue_media();
|
||||
wp_enqueue_script( 'activitypub-header-image' );
|
||||
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/blog-settings.php' );
|
||||
break;
|
||||
case 'followers':
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/blog-followers-list.php' );
|
||||
break;
|
||||
case 'welcome':
|
||||
default:
|
||||
wp_enqueue_script( 'plugin-install' );
|
||||
add_thickbox();
|
||||
wp_enqueue_script( 'updates' );
|
||||
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/welcome.php' );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user settings page
|
||||
*/
|
||||
public static function followers_list_page() {
|
||||
// User has to be able to publish posts.
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/user-followers-list.php' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register ActivityPub settings
|
||||
*/
|
||||
public static function register_settings() {
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_post_content_type',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'Use title and link, summary, full or custom content', 'activitypub' ),
|
||||
'show_in_rest' => array(
|
||||
'schema' => array(
|
||||
'enum' => array(
|
||||
'title',
|
||||
'excerpt',
|
||||
'content',
|
||||
),
|
||||
),
|
||||
),
|
||||
'default' => 'content',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_custom_post_content',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'Define your own custom post template', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => ACTIVITYPUB_CUSTOM_POST_CONTENT,
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_max_image_attachments',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => \__( 'Number of images to attach to posts.', 'activitypub' ),
|
||||
'default' => ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS,
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_object_type',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'The Activity-Object-Type', 'activitypub' ),
|
||||
'show_in_rest' => array(
|
||||
'schema' => array(
|
||||
'enum' => array(
|
||||
'note',
|
||||
'wordpress-post-format',
|
||||
),
|
||||
),
|
||||
),
|
||||
'default' => 'note',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_use_hashtags',
|
||||
array(
|
||||
'type' => 'boolean',
|
||||
'description' => \__( 'Add hashtags in the content as native tags and replace the #tag with the tag-link', 'activitypub' ),
|
||||
'default' => '0',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_use_opengraph',
|
||||
array(
|
||||
'type' => 'boolean',
|
||||
'description' => \__( 'Automatically add "fediverse:creator" OpenGraph tags for Authors and the Blog-User.', 'activitypub' ),
|
||||
'default' => '1',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_support_post_types',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \esc_html__( 'Enable ActivityPub support for post types', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => array( 'post' ),
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_enable_users',
|
||||
array(
|
||||
'type' => 'boolean',
|
||||
'description' => \__( 'Every Author on this Blog (with the publish_posts capability) gets his own ActivityPub enabled Profile.', 'activitypub' ),
|
||||
'default' => '1',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_enable_blog_user',
|
||||
array(
|
||||
'type' => 'boolean',
|
||||
'description' => \__( 'Your Blog becomes an ActivityPub compatible Profile.', 'activitypub' ),
|
||||
'default' => '0',
|
||||
)
|
||||
);
|
||||
|
||||
// Blog-User Settings.
|
||||
\register_setting(
|
||||
'activitypub_blog',
|
||||
'activitypub_blog_description',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \esc_html__( 'The Description of the Blog-User', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => '',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub_blog',
|
||||
'activitypub_blog_identifier',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \esc_html__( 'The Identifier of the Blog-User', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => Blog::get_default_username(),
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
// Hack to allow dots in the username.
|
||||
$parts = explode( '.', $value );
|
||||
$sanitized = array();
|
||||
|
||||
foreach ( $parts as $part ) {
|
||||
$sanitized[] = \sanitize_title( $part );
|
||||
}
|
||||
|
||||
$sanitized = implode( '.', $sanitized );
|
||||
|
||||
// Check for login or nicename.
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'search' => $sanitized,
|
||||
'search_columns' => array( 'user_login', 'user_nicename' ),
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
add_settings_error(
|
||||
'activitypub_blog_identifier',
|
||||
'activitypub_blog_identifier',
|
||||
\esc_html__( 'You cannot use an existing author\'s name for the blog profile ID.', 'activitypub' ),
|
||||
'error'
|
||||
);
|
||||
|
||||
return Blog::get_default_username();
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
},
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub_blog',
|
||||
'activitypub_header_image',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => \__( 'The Attachment-ID of the Sites Header-Image', 'activitypub' ),
|
||||
'default' => null,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the ActivityPub settings to the Help tab.
|
||||
*/
|
||||
public static function add_settings_help_tab() {
|
||||
require_once ACTIVITYPUB_PLUGIN_DIR . 'includes/help.php';
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the follower list to the Help tab.
|
||||
*/
|
||||
public static function add_followers_list_help_tab() {
|
||||
// todo.
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the profile.
|
||||
*
|
||||
* @param \WP_User $user The user object.
|
||||
*/
|
||||
public static function add_profile( $user ) {
|
||||
$description = \get_user_option( 'activitypub_description', $user->ID );
|
||||
|
||||
wp_enqueue_media();
|
||||
wp_enqueue_script( 'activitypub-header-image' );
|
||||
|
||||
\load_template(
|
||||
ACTIVITYPUB_PLUGIN_DIR . 'templates/user-settings.php',
|
||||
true,
|
||||
array(
|
||||
'description' => $description,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the user settings.
|
||||
*
|
||||
* Handles the saving of the ActivityPub settings.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*/
|
||||
public static function save_user_settings( $user_id ) {
|
||||
if ( ! isset( $_REQUEST['_apnonce'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_apnonce'] ) );
|
||||
if (
|
||||
! wp_verify_nonce( $nonce, 'activitypub-user-settings' ) ||
|
||||
! current_user_can( 'edit_user', $user_id )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
$description = ! empty( $_POST['activitypub_description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['activitypub_description'] ) ) : false;
|
||||
if ( $description ) {
|
||||
\update_user_option( $user_id, 'activitypub_description', $description );
|
||||
} else {
|
||||
\delete_user_option( $user_id, 'activitypub_description' );
|
||||
}
|
||||
|
||||
$header_image = ! empty( $_POST['activitypub_header_image'] ) ? sanitize_text_field( wp_unslash( $_POST['activitypub_header_image'] ) ) : false;
|
||||
if ( $header_image && \wp_attachment_is_image( $header_image ) ) {
|
||||
\update_user_option( $user_id, 'activitypub_header_image', $header_image );
|
||||
} else {
|
||||
\delete_user_option( $user_id, 'activitypub_header_image' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the admin scripts and styles.
|
||||
*
|
||||
* @param string $hook_suffix The current page.
|
||||
*/
|
||||
public static function enqueue_scripts( $hook_suffix ) {
|
||||
wp_register_script(
|
||||
'activitypub-header-image',
|
||||
plugins_url(
|
||||
'assets/js/activitypub-header-image.js',
|
||||
ACTIVITYPUB_PLUGIN_FILE
|
||||
),
|
||||
array( 'jquery' ),
|
||||
get_plugin_version(),
|
||||
false
|
||||
);
|
||||
|
||||
if ( false !== strpos( $hook_suffix, 'activitypub' ) ) {
|
||||
wp_enqueue_style(
|
||||
'activitypub-admin-styles',
|
||||
plugins_url(
|
||||
'assets/css/activitypub-admin.css',
|
||||
ACTIVITYPUB_PLUGIN_FILE
|
||||
),
|
||||
array(),
|
||||
get_plugin_version()
|
||||
);
|
||||
wp_enqueue_script(
|
||||
'activitypub-admin-script',
|
||||
plugins_url(
|
||||
'assets/js/activitypub-admin.js',
|
||||
ACTIVITYPUB_PLUGIN_FILE
|
||||
),
|
||||
array( 'jquery' ),
|
||||
get_plugin_version(),
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
if ( 'index.php' === $hook_suffix ) {
|
||||
wp_enqueue_style(
|
||||
'activitypub-admin-styles',
|
||||
plugins_url(
|
||||
'assets/css/activitypub-admin.css',
|
||||
ACTIVITYPUB_PLUGIN_FILE
|
||||
),
|
||||
array(),
|
||||
get_plugin_version()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into the edit_comment functionality.
|
||||
*
|
||||
* Disables the edit_comment capability for federated comments.
|
||||
*/
|
||||
public static function edit_comment() {
|
||||
// Disable the edit_comment capability for federated comments.
|
||||
\add_filter(
|
||||
'user_has_cap',
|
||||
function ( $allcaps, $caps, $arg ) {
|
||||
if ( 'edit_comment' !== $arg[0] ) {
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
if ( was_comment_received( $arg[2] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $allcaps;
|
||||
},
|
||||
1,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into the edit_post functionality.
|
||||
*
|
||||
* Disables the edit_post capability for federated posts.
|
||||
*/
|
||||
public static function edit_post() {
|
||||
// Disable the edit_post capability for federated posts.
|
||||
\add_filter(
|
||||
'user_has_cap',
|
||||
function ( $allcaps, $caps, $arg ) {
|
||||
if ( 'edit_post' !== $arg[0] ) {
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
$post = get_post( $arg[2] );
|
||||
|
||||
if ( ! Extra_Fields::is_extra_field_post_type( $post->post_type ) ) {
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
if ( (int) get_current_user_id() !== (int) $post->post_author ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $allcaps;
|
||||
},
|
||||
1,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ActivityPub specific actions/filters to the post list view.
|
||||
*/
|
||||
public static function list_posts() {
|
||||
// Show only the user's extra fields.
|
||||
\add_action(
|
||||
'pre_get_posts',
|
||||
function ( $query ) {
|
||||
if ( $query->get( 'post_type' ) === 'ap_extrafield' ) {
|
||||
$query->set( 'author', get_current_user_id() );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Remove all views for the extra fields.
|
||||
$screen_id = get_current_screen()->id;
|
||||
|
||||
add_filter(
|
||||
"views_{$screen_id}",
|
||||
function ( $views ) {
|
||||
if ( Extra_Fields::is_extra_fields_post_type( get_current_screen()->post_type ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
return $views;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Comment row actions.
|
||||
*
|
||||
* @param array $actions The existing actions.
|
||||
* @param int|\WP_Comment $comment The comment object or ID.
|
||||
*
|
||||
* @return array The modified actions.
|
||||
*/
|
||||
public static function comment_row_actions( $actions, $comment ) {
|
||||
if ( was_comment_received( $comment ) ) {
|
||||
unset( $actions['edit'] );
|
||||
unset( $actions['quickedit'] );
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a column "activitypub".
|
||||
*
|
||||
* This column shows if the user has the capability to use ActivityPub.
|
||||
*
|
||||
* @param array $columns The columns.
|
||||
*
|
||||
* @return array The columns extended by the activitypub.
|
||||
*/
|
||||
public static function manage_users_columns( $columns ) {
|
||||
$columns['activitypub'] = __( 'ActivityPub', 'activitypub' );
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "comment-type" and "protocol" as column in WP-Admin.
|
||||
*
|
||||
* @param array $columns The list of column names.
|
||||
*
|
||||
* @return array The extended list of column names.
|
||||
*/
|
||||
public static function manage_comment_columns( $columns ) {
|
||||
$columns['comment_type'] = esc_attr__( 'Comment-Type', 'activitypub' );
|
||||
$columns['comment_protocol'] = esc_attr__( 'Protocol', 'activitypub' );
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "post_content" as column for Extra-Fields in WP-Admin.
|
||||
*
|
||||
* @param array $columns The list of column names.
|
||||
* @param string $post_type The post type.
|
||||
*
|
||||
* @return array The extended list of column names.
|
||||
*/
|
||||
public static function manage_post_columns( $columns, $post_type ) {
|
||||
if ( Extra_Fields::is_extra_fields_post_type( $post_type ) ) {
|
||||
$after_key = 'title';
|
||||
$index = array_search( $after_key, array_keys( $columns ), true );
|
||||
$columns = array_slice( $columns, 0, $index + 1 ) + array( 'extra_field_content' => esc_attr__( 'Content', 'activitypub' ) ) + $columns;
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "comment-type" and "protocol" as column in WP-Admin.
|
||||
*
|
||||
* @param array $column The column to implement.
|
||||
* @param int $comment_id The comment id.
|
||||
*/
|
||||
public static function manage_comments_custom_column( $column, $comment_id ) {
|
||||
if ( 'comment_type' === $column && ! defined( 'WEBMENTION_PLUGIN_DIR' ) ) {
|
||||
echo esc_attr( ucfirst( get_comment_type( $comment_id ) ) );
|
||||
} elseif ( 'comment_protocol' === $column ) {
|
||||
$protocol = get_comment_meta( $comment_id, 'protocol', true );
|
||||
|
||||
if ( $protocol ) {
|
||||
echo esc_attr( ucfirst( str_replace( 'activitypub', 'ActivityPub', $protocol ) ) );
|
||||
} else {
|
||||
esc_attr_e( 'Local', 'activitypub' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the results for the activitypub column.
|
||||
*
|
||||
* @param string $output Custom column output. Default empty.
|
||||
* @param string $column_name Column name.
|
||||
* @param int $user_id ID of the currently-listed user.
|
||||
*
|
||||
* @return string The column contents.
|
||||
*/
|
||||
public static function manage_users_custom_column( $output, $column_name, $user_id ) {
|
||||
if ( 'activitypub' !== $column_name ) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
if ( \user_can( $user_id, 'activitypub' ) ) {
|
||||
return '<span aria-hidden="true">✓</span><span class="screen-reader-text">' . esc_html__( 'ActivityPub enabled for this author', 'activitypub' ) . '</span>';
|
||||
} else {
|
||||
return '<span aria-hidden="true">✗</span><span class="screen-reader-text">' . esc_html__( 'ActivityPub disabled for this author', 'activitypub' ) . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a column "extra_field_content" to the post list view.
|
||||
*
|
||||
* @param string $column_name The column name.
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function manage_posts_custom_column( $column_name, $post_id ) {
|
||||
if ( 'extra_field_content' === $column_name ) {
|
||||
$post = get_post( $post_id );
|
||||
if ( Extra_Fields::is_extra_fields_post_type( $post->post_type ) ) {
|
||||
echo esc_attr( wp_strip_all_tags( $post->post_content ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add options to the Bulk dropdown on the users page.
|
||||
*
|
||||
* @param array $actions The existing bulk options.
|
||||
*
|
||||
* @return array The extended bulk options.
|
||||
*/
|
||||
public static function user_bulk_options( $actions ) {
|
||||
$actions['add_activitypub_cap'] = __( 'Enable for ActivityPub', 'activitypub' );
|
||||
$actions['remove_activitypub_cap'] = __( 'Disable for ActivityPub', 'activitypub' );
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bulk activitypub requests.
|
||||
*
|
||||
* * `add_activitypub_cap` - Add the activitypub capability to the selected users.
|
||||
* * `remove_activitypub_cap` - Remove the activitypub capability from the selected users.
|
||||
*
|
||||
* @param string $sendback The URL to send the user back to.
|
||||
* @param string $action The requested action.
|
||||
* @param array $users The selected users.
|
||||
*
|
||||
* @return string The URL to send the user back to.
|
||||
*/
|
||||
public static function handle_bulk_request( $sendback, $action, $users ) {
|
||||
if (
|
||||
'remove_activitypub_cap' !== $action &&
|
||||
'add_activitypub_cap' !== $action
|
||||
) {
|
||||
return $sendback;
|
||||
}
|
||||
|
||||
foreach ( $users as $user_id ) {
|
||||
$user = new \WP_User( $user_id );
|
||||
if (
|
||||
'add_activitypub_cap' === $action &&
|
||||
user_can( $user_id, 'publish_posts' )
|
||||
) {
|
||||
$user->add_cap( 'activitypub' );
|
||||
} elseif ( 'remove_activitypub_cap' === $action ) {
|
||||
$user->remove_cap( 'activitypub' );
|
||||
}
|
||||
}
|
||||
|
||||
return $sendback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ActivityPub infos to the dashboard glance items.
|
||||
*
|
||||
* @param array $items The existing glance items.
|
||||
*
|
||||
* @return array The extended glance items.
|
||||
*/
|
||||
public static function dashboard_glance_items( $items ) {
|
||||
\add_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers', 10, 3 );
|
||||
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
$follower_count = sprintf(
|
||||
// translators: %s: number of followers.
|
||||
_n(
|
||||
'%s Follower',
|
||||
'%s Followers',
|
||||
count_followers( \get_current_user_id() ),
|
||||
'activitypub'
|
||||
),
|
||||
\number_format_i18n( count_followers( \get_current_user_id() ) )
|
||||
);
|
||||
$items['activitypub-followers-user'] = sprintf(
|
||||
'<a class="activitypub-followers" href="%1$s" title="%2$s">%3$s</a>',
|
||||
\esc_url( \admin_url( 'users.php?page=activitypub-followers-list' ) ),
|
||||
\esc_attr__( 'Your followers', 'activitypub' ),
|
||||
\esc_html( $follower_count )
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! is_user_type_disabled( 'blog' ) && current_user_can( 'manage_options' ) ) {
|
||||
$follower_count = sprintf(
|
||||
// translators: %s: number of followers.
|
||||
_n(
|
||||
'%s Follower (Blog)',
|
||||
'%s Followers (Blog)',
|
||||
count_followers( Users::BLOG_USER_ID ),
|
||||
'activitypub'
|
||||
),
|
||||
\number_format_i18n( count_followers( Users::BLOG_USER_ID ) )
|
||||
);
|
||||
$items['activitypub-followers-blog'] = sprintf(
|
||||
'<a class="activitypub-followers" href="%1$s" title="%2$s">%3$s</a>',
|
||||
\esc_url( \admin_url( 'options-general.php?page=activitypub&tab=followers' ) ),
|
||||
\esc_attr__( 'The Blog\'s followers', 'activitypub' ),
|
||||
\esc_html( $follower_count )
|
||||
);
|
||||
}
|
||||
|
||||
\remove_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers' );
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
106
wp-content/plugins/activitypub/includes/class-autoloader.php
Normal file
106
wp-content/plugins/activitypub/includes/class-autoloader.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/**
|
||||
* Autoloader for Activitypub.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* An Autoloader that respects WordPress's filename standards.
|
||||
*/
|
||||
class Autoloader {
|
||||
|
||||
/**
|
||||
* Namespace separator.
|
||||
*/
|
||||
const NS_SEPARATOR = '\\';
|
||||
|
||||
/**
|
||||
* The prefix to compare classes against.
|
||||
*
|
||||
* @var string
|
||||
* @access protected
|
||||
*/
|
||||
protected $prefix;
|
||||
|
||||
/**
|
||||
* Length of the prefix string.
|
||||
*
|
||||
* @var int
|
||||
* @access protected
|
||||
*/
|
||||
protected $prefix_length;
|
||||
|
||||
/**
|
||||
* Path to the file to be loaded.
|
||||
*
|
||||
* @var string
|
||||
* @access protected
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $prefix Namespace prefix all classes have in common.
|
||||
* @param string $path Path to the files to be loaded.
|
||||
*/
|
||||
public function __construct( $prefix, $path ) {
|
||||
$this->prefix = $prefix;
|
||||
$this->prefix_length = \strlen( $prefix );
|
||||
$this->path = \rtrim( $path . '/' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers Autoloader's autoload function.
|
||||
*
|
||||
* @throws \Exception When autoload_function cannot be registered.
|
||||
*
|
||||
* @param string $prefix Namespace prefix all classes have in common.
|
||||
* @param string $path Path to the files to be loaded.
|
||||
*/
|
||||
public static function register_path( $prefix, $path ) {
|
||||
$loader = new self( $prefix, $path );
|
||||
\spl_autoload_register( array( $loader, 'load' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a class if its namespace starts with `$this->prefix`.
|
||||
*
|
||||
* @param string $class_name The class to be loaded.
|
||||
*/
|
||||
public function load( $class_name ) {
|
||||
if ( \strpos( $class_name, $this->prefix . self::NS_SEPARATOR ) !== 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip prefix from the start (ala PSR-4).
|
||||
$class_name = \substr( $class_name, $this->prefix_length + 1 );
|
||||
$class_name = \strtolower( $class_name );
|
||||
$dir = '';
|
||||
|
||||
$last_ns_pos = \strripos( $class_name, self::NS_SEPARATOR );
|
||||
if ( false !== $last_ns_pos ) {
|
||||
$namespace = \substr( $class_name, 0, $last_ns_pos );
|
||||
$namespace = \str_replace( '_', '-', $namespace );
|
||||
$class_name = \substr( $class_name, $last_ns_pos + 1 );
|
||||
$dir = \str_replace( self::NS_SEPARATOR, DIRECTORY_SEPARATOR, $namespace ) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
$path = $this->path . $dir . 'class-' . \str_replace( '_', '-', $class_name ) . '.php';
|
||||
|
||||
if ( ! \file_exists( $path ) ) {
|
||||
$path = $this->path . $dir . 'interface-' . \str_replace( '_', '-', $class_name ) . '.php';
|
||||
}
|
||||
|
||||
if ( ! \file_exists( $path ) ) {
|
||||
$path = $this->path . $dir . 'trait-' . \str_replace( '_', '-', $class_name ) . '.php';
|
||||
}
|
||||
|
||||
if ( \file_exists( $path ) ) {
|
||||
require_once $path;
|
||||
}
|
||||
}
|
||||
}
|
@ -7,8 +7,8 @@
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
|
||||
/**
|
||||
* Block class.
|
||||
@ -21,12 +21,14 @@ class Blocks {
|
||||
// This is already being called on the init hook, so just add it.
|
||||
self::register_blocks();
|
||||
|
||||
\add_action( 'wp_enqueue_scripts', array( self::class, 'add_data' ) );
|
||||
\add_action( 'enqueue_block_editor_assets', array( self::class, 'add_data' ) );
|
||||
\add_action( 'wp_head', array( self::class, 'inject_activitypub_options' ), 11 );
|
||||
\add_action( 'admin_print_scripts', array( self::class, 'inject_activitypub_options' ) );
|
||||
\add_action( 'load-post-new.php', array( self::class, 'handle_in_reply_to_get_param' ) );
|
||||
// Add editor plugin.
|
||||
\add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_editor_assets' ) );
|
||||
\add_action( 'init', array( self::class, 'register_postmeta' ), 11 );
|
||||
|
||||
\add_filter( 'activitypub_import_mastodon_post_data', array( self::class, 'filter_import_mastodon_post_data' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -42,7 +44,36 @@ class Blocks {
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
'sanitize_callback' => function ( $warning ) {
|
||||
if ( $warning ) {
|
||||
return \sanitize_text_field( $warning );
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
$post_type,
|
||||
'activitypub_content_visibility',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
$schema = array(
|
||||
'type' => 'string',
|
||||
'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
|
||||
'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
|
||||
);
|
||||
|
||||
if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
|
||||
return $schema['default'];
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -79,22 +110,22 @@ class Blocks {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add data to the block editor.
|
||||
* Output ActivityPub options as a script tag.
|
||||
*/
|
||||
public static function add_data() {
|
||||
$context = is_admin() ? 'editor' : 'view';
|
||||
$followers_handle = 'activitypub-followers-' . $context . '-script';
|
||||
$follow_me_handle = 'activitypub-follow-me-' . $context . '-script';
|
||||
public static function inject_activitypub_options() {
|
||||
$data = array(
|
||||
'namespace' => ACTIVITYPUB_REST_NAMESPACE,
|
||||
'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
|
||||
'enabled' => array(
|
||||
'site' => ! is_user_type_disabled( 'blog' ),
|
||||
'users' => ! is_user_type_disabled( 'user' ),
|
||||
),
|
||||
);
|
||||
$js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
|
||||
\wp_add_inline_script( $followers_handle, $js, 'before' );
|
||||
\wp_add_inline_script( $follow_me_handle, $js, 'before' );
|
||||
|
||||
printf(
|
||||
"\n<script>var _activityPubOptions = %s;</script>",
|
||||
wp_json_encode( $data )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -119,6 +150,38 @@ class Blocks {
|
||||
'render_callback' => array( self::class, 'render_reply_block' ),
|
||||
)
|
||||
);
|
||||
|
||||
\register_block_type_from_metadata(
|
||||
ACTIVITYPUB_PLUGIN_DIR . '/build/reactions',
|
||||
array(
|
||||
'render_callback' => array( self::class, 'render_post_reactions_block' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the post reactions block.
|
||||
*
|
||||
* @param array $attrs The block attributes.
|
||||
*
|
||||
* @return string The HTML to render.
|
||||
*/
|
||||
public static function render_post_reactions_block( $attrs ) {
|
||||
if ( ! isset( $attrs['postId'] ) ) {
|
||||
$attrs['postId'] = get_the_ID();
|
||||
}
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'class' => 'activitypub-reactions-block',
|
||||
'data-attrs' => wp_json_encode( $attrs ),
|
||||
)
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div %s></div>',
|
||||
$wrapper_attributes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -134,7 +197,7 @@ class Blocks {
|
||||
|
||||
// If the user string is 'site', return the Blog User ID.
|
||||
if ( 'site' === $user_string ) {
|
||||
return User_Collection::BLOG_USER_ID;
|
||||
return Actors::BLOG_USER_ID;
|
||||
}
|
||||
|
||||
// The only other value should be 'inherit', which means to use the query context to determine the User.
|
||||
@ -144,7 +207,7 @@ class Blocks {
|
||||
|
||||
// For a homepage/front page, if the Blog User is active, use it.
|
||||
if ( ( is_front_page() || is_home() ) && ! is_user_type_disabled( 'blog' ) ) {
|
||||
return User_Collection::BLOG_USER_ID;
|
||||
return Actors::BLOG_USER_ID;
|
||||
}
|
||||
|
||||
// If we're in a loop, use the post author.
|
||||
@ -192,7 +255,7 @@ class Blocks {
|
||||
*/
|
||||
public static function render_follow_me_block( $attrs ) {
|
||||
$user_id = self::get_user_id( $attrs['selectedUser'] );
|
||||
$user = User_Collection::get_by_id( $user_id );
|
||||
$user = Actors::get_by_id( $user_id );
|
||||
if ( is_wp_error( $user ) ) {
|
||||
if ( 'inherit' === $attrs['selectedUser'] ) {
|
||||
// If the user is 'inherit' and we couldn't determine the user, don't render anything.
|
||||
@ -210,7 +273,6 @@ class Blocks {
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'aria-label' => __( 'Follow me on the Fediverse', 'activitypub' ),
|
||||
'class' => 'activitypub-follow-me-block-wrapper',
|
||||
'data-attrs' => wp_json_encode( $attrs ),
|
||||
)
|
||||
@ -232,7 +294,7 @@ class Blocks {
|
||||
return '<!-- Followers block: `inherit` mode does not display on this type of page -->';
|
||||
}
|
||||
|
||||
$user = User_Collection::get_by_id( $followee_user_id );
|
||||
$user = Actors::get_by_id( $followee_user_id );
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return '<!-- Followers block: `' . $followee_user_id . '` not an active ActivityPub user -->';
|
||||
}
|
||||
@ -279,25 +341,47 @@ class Blocks {
|
||||
* @return string The HTML to render.
|
||||
*/
|
||||
public static function render_reply_block( $attrs ) {
|
||||
/**
|
||||
* Filter the reply block.
|
||||
*
|
||||
* @param string $html The HTML to render.
|
||||
* @param array $attrs The block attributes.
|
||||
*/
|
||||
return apply_filters(
|
||||
'activitypub_reply_block',
|
||||
sprintf(
|
||||
// Return early if no URL is provided.
|
||||
if ( empty( $attrs['url'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$show_embed = isset( $attrs['embedPost'] ) && $attrs['embedPost'];
|
||||
|
||||
$wrapper_attrs = get_block_wrapper_attributes(
|
||||
array(
|
||||
'aria-label' => __( 'Reply', 'activitypub' ),
|
||||
'class' => 'activitypub-reply-block',
|
||||
'data-in-reply-to' => $attrs['url'],
|
||||
)
|
||||
);
|
||||
|
||||
$html = '<div ' . $wrapper_attrs . '>';
|
||||
|
||||
// Try to get and append the embed if requested.
|
||||
if ( $show_embed ) {
|
||||
$embed = wp_oembed_get( $attrs['url'] );
|
||||
if ( $embed ) {
|
||||
$html .= $embed;
|
||||
}
|
||||
}
|
||||
|
||||
// Only show the link if we're not showing the embed.
|
||||
if ( ! $show_embed ) {
|
||||
$html .= sprintf(
|
||||
'<p><a title="%2$s" aria-label="%2$s" href="%1$s" class="u-in-reply-to" target="_blank">%3$s</a></p>',
|
||||
esc_url( $attrs['url'] ),
|
||||
esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ),
|
||||
// translators: %s is the URL of the post being replied to.
|
||||
sprintf( __( '↬%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', $attrs['url'] ) )
|
||||
),
|
||||
$attrs
|
||||
sprintf( __( '↬%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', esc_url( $attrs['url'] ) ) )
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a follower.
|
||||
*
|
||||
@ -330,4 +414,33 @@ class Blocks {
|
||||
$external_svg
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts content to blocks before saving to the database.
|
||||
*
|
||||
* @param array $data The post data to be inserted.
|
||||
* @param object $post The Mastodon Create activity.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function filter_import_mastodon_post_data( $data, $post ) {
|
||||
// Convert paragraphs to blocks.
|
||||
\preg_match_all( '#<p>.*?</p>#is', $data['post_content'], $matches );
|
||||
$blocks = \array_map(
|
||||
function ( $paragraph ) {
|
||||
return '<!-- wp:paragraph -->' . PHP_EOL . $paragraph . PHP_EOL . '<!-- /wp:paragraph -->' . PHP_EOL;
|
||||
},
|
||||
$matches[0] ?? array()
|
||||
);
|
||||
|
||||
$data['post_content'] = \rtrim( \implode( PHP_EOL, $blocks ), PHP_EOL );
|
||||
|
||||
// Add reply block if it's a reply.
|
||||
if ( null !== $post->object->inReplyTo ) {
|
||||
$reply_block = \sprintf( '<!-- wp:activitypub/reply {"url":"%1$s","embedPost":true} /-->' . PHP_EOL, \esc_url( $post->object->inReplyTo ) );
|
||||
$data['post_content'] = $reply_block . $data['post_content'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
@ -7,83 +7,14 @@
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_CLI;
|
||||
use WP_CLI_Command;
|
||||
use Activitypub\Collection\Outbox;
|
||||
|
||||
/**
|
||||
* WP-CLI commands.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Cli extends WP_CLI_Command {
|
||||
/**
|
||||
* Check the Plugins Meta-Information.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* [--Name]
|
||||
* The Plugin Name.
|
||||
*
|
||||
* [--PluginURI]
|
||||
* The Plugin URI.
|
||||
*
|
||||
* [--Version]
|
||||
* The Plugin Version.
|
||||
*
|
||||
* [--Description]
|
||||
* The Plugin Description.
|
||||
*
|
||||
* [--Author]
|
||||
* The Plugin Author.
|
||||
*
|
||||
* [--AuthorURI]
|
||||
* The Plugin Author URI.
|
||||
*
|
||||
* [--TextDomain]
|
||||
* The Plugin Text Domain.
|
||||
*
|
||||
* [--DomainPath]
|
||||
* The Plugin Domain Path.
|
||||
*
|
||||
* [--Network]
|
||||
* The Plugin Network.
|
||||
*
|
||||
* [--RequiresWP]
|
||||
* The Plugin Requires at least.
|
||||
*
|
||||
* [--RequiresPHP]
|
||||
* The Plugin Requires PHP.
|
||||
*
|
||||
* [--UpdateURI]
|
||||
* The Plugin Update URI.
|
||||
*
|
||||
* See: https://developer.wordpress.org/reference/functions/get_plugin_data/#return
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* $ wp webmention meta
|
||||
*
|
||||
* $ wp webmention meta --Version
|
||||
* Version: 1.0.0
|
||||
*
|
||||
* @param array|null $args The arguments.
|
||||
* @param array|null $assoc_args The associative arguments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function meta( $args, $assoc_args ) {
|
||||
$plugin_data = get_plugin_meta();
|
||||
|
||||
if ( $assoc_args ) {
|
||||
$plugin_data = array_intersect_key( $plugin_data, $assoc_args );
|
||||
} else {
|
||||
WP_CLI::line( __( "ActivityPub Plugin Meta:\n", 'activitypub' ) );
|
||||
}
|
||||
|
||||
foreach ( $plugin_data as $key => $value ) {
|
||||
WP_CLI::line( $key . ': ' . $value );
|
||||
}
|
||||
}
|
||||
class Cli extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Remove the entire blog from the Fediverse.
|
||||
@ -98,7 +29,7 @@ class Cli extends WP_CLI_Command {
|
||||
* @return void
|
||||
*/
|
||||
public function self_destruct( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
WP_CLI::warning( __( 'Self-Destructing is not implemented yet.', 'activitypub' ) );
|
||||
\WP_CLI::warning( 'Self-Destructing is not implemented yet.' );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -123,28 +54,27 @@ class Cli extends WP_CLI_Command {
|
||||
*
|
||||
* @synopsis <action> <id>
|
||||
*
|
||||
* @param array|null $args The arguments.
|
||||
* @param array $args The arguments.
|
||||
*/
|
||||
public function post( $args ) {
|
||||
$post = get_post( $args[1] );
|
||||
|
||||
if ( ! $post ) {
|
||||
WP_CLI::error( __( 'Post not found.', 'activitypub' ) );
|
||||
\WP_CLI::error( 'Post not found.' );
|
||||
}
|
||||
|
||||
switch ( $args[0] ) {
|
||||
case 'delete':
|
||||
// translators: %s is the ID of the post.
|
||||
WP_CLI::confirm( sprintf( __( 'Do you really want to delete the (Custom) Post with the ID: %s', 'activitypub' ), $args[1] ) );
|
||||
Scheduler::schedule_post_activity( 'trash', 'publish', $args[1] );
|
||||
WP_CLI::success( __( '"Delete"-Activity is queued.', 'activitypub' ) );
|
||||
\WP_CLI::confirm( 'Do you really want to delete the (Custom) Post with the ID: ' . $args[1] );
|
||||
add_to_outbox( $post, 'Delete', $post->post_author );
|
||||
\WP_CLI::success( '"Delete" activity is queued.' );
|
||||
break;
|
||||
case 'update':
|
||||
Scheduler::schedule_post_activity( 'publish', 'publish', $args[1] );
|
||||
WP_CLI::success( __( '"Update"-Activity is queued.', 'activitypub' ) );
|
||||
add_to_outbox( $post, 'Update', $post->post_author );
|
||||
\WP_CLI::success( '"Update" activity is queued.' );
|
||||
break;
|
||||
default:
|
||||
WP_CLI::error( __( 'Unknown action.', 'activitypub' ) );
|
||||
\WP_CLI::error( 'Unknown action.' );
|
||||
}
|
||||
}
|
||||
|
||||
@ -170,32 +100,131 @@ class Cli extends WP_CLI_Command {
|
||||
*
|
||||
* @synopsis <action> <id>
|
||||
*
|
||||
* @param array|null $args The arguments.
|
||||
* @param array $args The arguments.
|
||||
*/
|
||||
public function comment( $args ) {
|
||||
$comment = get_comment( $args[1] );
|
||||
|
||||
if ( ! $comment ) {
|
||||
WP_CLI::error( __( 'Comment not found.', 'activitypub' ) );
|
||||
\WP_CLI::error( 'Comment not found.' );
|
||||
}
|
||||
|
||||
if ( was_comment_received( $comment ) ) {
|
||||
WP_CLI::error( __( 'This comment was received via ActivityPub and cannot be deleted or updated.', 'activitypub' ) );
|
||||
\WP_CLI::error( 'This comment was received via ActivityPub and cannot be deleted or updated.' );
|
||||
}
|
||||
|
||||
switch ( $args[0] ) {
|
||||
case 'delete':
|
||||
// translators: %s is the ID of the comment.
|
||||
WP_CLI::confirm( sprintf( __( 'Do you really want to delete the Comment with the ID: %s', 'activitypub' ), $args[1] ) );
|
||||
Scheduler::schedule_comment_activity( 'trash', 'approved', $args[1] );
|
||||
WP_CLI::success( __( '"Delete"-Activity is queued.', 'activitypub' ) );
|
||||
\WP_CLI::confirm( 'Do you really want to delete the Comment with the ID: ' . $args[1] );
|
||||
add_to_outbox( $comment, 'Delete', $comment->user_id );
|
||||
\WP_CLI::success( '"Delete" activity is queued.' );
|
||||
break;
|
||||
case 'update':
|
||||
Scheduler::schedule_comment_activity( 'approved', 'approved', $args[1] );
|
||||
WP_CLI::success( __( '"Update"-Activity is queued.', 'activitypub' ) );
|
||||
add_to_outbox( $comment, 'Update', $comment->user_id );
|
||||
\WP_CLI::success( '"Update" activity is queued.' );
|
||||
break;
|
||||
default:
|
||||
WP_CLI::error( __( 'Unknown action.', 'activitypub' ) );
|
||||
\WP_CLI::error( 'Unknown action.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo an activity that was sent to the Fediverse.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <outbox_item_id>
|
||||
* The ID or URL of the outbox item to undo.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* $ wp activitypub undo 123
|
||||
* $ wp activitypub undo "https://example.com/?post_type=ap_outbox&p=123"
|
||||
*
|
||||
* @synopsis <outbox_item_id>
|
||||
*
|
||||
* @param array $args The arguments.
|
||||
*/
|
||||
public function undo( $args ) {
|
||||
$outbox_item_id = $args[0];
|
||||
if ( ! is_numeric( $outbox_item_id ) ) {
|
||||
$outbox_item_id = url_to_postid( $outbox_item_id );
|
||||
}
|
||||
|
||||
$outbox_item_id = get_post( $outbox_item_id );
|
||||
if ( ! $outbox_item_id ) {
|
||||
\WP_CLI::error( 'Activity not found.' );
|
||||
}
|
||||
|
||||
$undo_id = Outbox::undo( $outbox_item_id );
|
||||
if ( ! $undo_id ) {
|
||||
\WP_CLI::error( 'Failed to undo activity.' );
|
||||
}
|
||||
\WP_CLI::success( 'Undo activity scheduled.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-Schedule an activity that was sent to the Fediverse before.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <outbox_item_id>
|
||||
* The ID or URL of the outbox item to reschedule.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* $ wp activitypub reschedule 123
|
||||
* $ wp activitypub reschedule "https://example.com/?post_type=ap_outbox&p=123"
|
||||
*
|
||||
* @synopsis <outbox_item_id>
|
||||
*
|
||||
* @param array $args The arguments.
|
||||
*/
|
||||
public function reschedule( $args ) {
|
||||
$outbox_item_id = $args[0];
|
||||
if ( ! is_numeric( $outbox_item_id ) ) {
|
||||
$outbox_item_id = url_to_postid( $outbox_item_id );
|
||||
}
|
||||
|
||||
$outbox_item_id = get_post( $outbox_item_id );
|
||||
if ( ! $outbox_item_id ) {
|
||||
\WP_CLI::error( 'Activity not found.' );
|
||||
}
|
||||
|
||||
Outbox::reschedule( $outbox_item_id );
|
||||
|
||||
\WP_CLI::success( 'Rescheduled activity.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the blog to a new URL.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <from>
|
||||
* The current URL of the blog.
|
||||
*
|
||||
* <to>
|
||||
* The new URL of the blog.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* $ wp activitypub move https://example.com/ https://newsite.com/
|
||||
*
|
||||
* @synopsis <from> <to>
|
||||
*
|
||||
* @param array $args The arguments.
|
||||
*/
|
||||
public function move( $args ) {
|
||||
$from = $args[0];
|
||||
$to = $args[1];
|
||||
|
||||
$outbox_item_id = Move::account( $from, $to );
|
||||
|
||||
if ( is_wp_error( $outbox_item_id ) ) {
|
||||
\WP_CLI::error( $outbox_item_id->get_error_message() );
|
||||
} else {
|
||||
\WP_CLI::success( 'Move Scheduled.' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
use WP_Comment_Query;
|
||||
|
||||
/**
|
||||
@ -25,11 +25,14 @@ class Comment {
|
||||
|
||||
\add_filter( 'comment_reply_link', array( self::class, 'comment_reply_link' ), 10, 3 );
|
||||
\add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 );
|
||||
\add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 3 );
|
||||
\add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 2 );
|
||||
\add_action( 'wp_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
|
||||
\add_action( 'pre_get_comments', array( static::class, 'comment_query' ) );
|
||||
|
||||
\add_filter( 'pre_comment_approved', array( static::class, 'pre_comment_approved' ), 10, 2 );
|
||||
\add_filter( 'get_avatar_comment_types', array( static::class, 'get_avatar_comment_types' ), 99 );
|
||||
\add_action( 'update_option_activitypub_allow_likes', array( self::class, 'maybe_update_comment_counts' ), 10, 2 );
|
||||
\add_action( 'update_option_activitypub_allow_reposts', array( self::class, 'maybe_update_comment_counts' ), 10, 2 );
|
||||
\add_filter( 'pre_wp_update_comment_count_now', array( static::class, 'pre_wp_update_comment_count_now' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -46,8 +49,7 @@ class Comment {
|
||||
*/
|
||||
public static function comment_reply_link( $link, $args, $comment ) {
|
||||
if ( self::are_comments_allowed( $comment ) ) {
|
||||
$user_id = get_current_user_id();
|
||||
if ( $user_id && self::was_received( $comment ) && \user_can( $user_id, 'activitypub' ) ) {
|
||||
if ( \current_user_can( 'activitypub' ) && self::was_received( $comment ) ) {
|
||||
return self::create_fediverse_reply_link( $link, $args );
|
||||
}
|
||||
|
||||
@ -60,10 +62,17 @@ class Comment {
|
||||
);
|
||||
|
||||
$div = sprintf(
|
||||
'<div class="activitypub-remote-reply" data-attrs="%s"></div>',
|
||||
'<div class="reply activitypub-remote-reply" data-attrs="%s"></div>',
|
||||
esc_attr( wp_json_encode( $attrs ) )
|
||||
);
|
||||
|
||||
/**
|
||||
* Filters the HTML markup for the ActivityPub remote comment reply container.
|
||||
*
|
||||
* @param string $div The HTML markup for the remote reply container. Default is a div
|
||||
* with class 'activitypub-remote-reply' and data attributes for
|
||||
* the selected comment ID and internal comment ID.
|
||||
*/
|
||||
return apply_filters( 'activitypub_comment_reply_link', $div );
|
||||
}
|
||||
|
||||
@ -113,16 +122,10 @@ class Comment {
|
||||
|
||||
if ( is_single_user() && \user_can( $current_user, 'publish_posts' ) ) {
|
||||
// On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user.
|
||||
$current_user = Users::BLOG_USER_ID;
|
||||
$current_user = Actors::BLOG_USER_ID;
|
||||
}
|
||||
|
||||
$is_user_disabled = is_user_disabled( $current_user );
|
||||
|
||||
if ( $is_user_disabled ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return user_can_activitypub( $current_user );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -221,15 +224,13 @@ class Comment {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( is_single_user() && \user_can( $user_id, 'publish_posts' ) ) {
|
||||
if ( is_single_user() && \user_can( $user_id, 'activitypub' ) ) {
|
||||
// On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user.
|
||||
$user_id = Users::BLOG_USER_ID;
|
||||
$user_id = Actors::BLOG_USER_ID;
|
||||
}
|
||||
|
||||
$is_user_disabled = is_user_disabled( $user_id );
|
||||
|
||||
// User is disabled for federation.
|
||||
if ( $is_user_disabled ) {
|
||||
// User is not allowed to federate comments.
|
||||
if ( ! user_can_activitypub( $user_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -256,6 +257,8 @@ class Comment {
|
||||
array(
|
||||
'meta_key' => 'source_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
'orderby' => 'comment_date',
|
||||
'order' => 'DESC',
|
||||
)
|
||||
);
|
||||
|
||||
@ -263,10 +266,6 @@ class Comment {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( count( $comment_query->comments ) > 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $comment_query->comments[0];
|
||||
}
|
||||
|
||||
@ -480,6 +479,7 @@ class Comment {
|
||||
$handle = 'activitypub-remote-reply';
|
||||
$data = array(
|
||||
'namespace' => ACTIVITYPUB_REST_NAMESPACE,
|
||||
'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
|
||||
);
|
||||
$js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
|
||||
$asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/remote-reply/index.asset.php';
|
||||
@ -495,6 +495,7 @@ class Comment {
|
||||
true
|
||||
);
|
||||
\wp_add_inline_script( $handle, $js, 'before' );
|
||||
\wp_set_script_translations( $handle, 'activitypub' );
|
||||
|
||||
\wp_enqueue_style(
|
||||
$handle,
|
||||
@ -505,6 +506,27 @@ class Comment {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comment type by activity type.
|
||||
*
|
||||
* @param string $activity_type The activity type.
|
||||
*
|
||||
* @return array|null The comment type.
|
||||
*/
|
||||
public static function get_comment_type_by_activity_type( $activity_type ) {
|
||||
$activity_type = \strtolower( $activity_type );
|
||||
$activity_type = \sanitize_key( $activity_type );
|
||||
$comment_types = self::get_comment_types();
|
||||
|
||||
foreach ( $comment_types as $comment_type ) {
|
||||
if ( in_array( $activity_type, $comment_type['activity_types'], true ) ) {
|
||||
return $comment_type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the registered custom comment types.
|
||||
*
|
||||
@ -519,27 +541,48 @@ class Comment {
|
||||
/**
|
||||
* Is this a registered comment type.
|
||||
*
|
||||
* @param string $slug The name of the type.
|
||||
* @param string $slug The slug of the type.
|
||||
*
|
||||
* @return boolean True if registered.
|
||||
*/
|
||||
public static function is_registered_comment_type( $slug ) {
|
||||
$slug = strtolower( $slug );
|
||||
$slug = sanitize_key( $slug );
|
||||
$slug = \strtolower( $slug );
|
||||
$slug = \sanitize_key( $slug );
|
||||
|
||||
return in_array( $slug, array_keys( self::get_comment_types() ), true );
|
||||
$comment_types = self::get_comment_types();
|
||||
|
||||
return isset( $comment_types[ $slug ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the registered custom comment types names.
|
||||
* Return the registered custom comment type slugs.
|
||||
*
|
||||
* @return array The registered custom comment type names.
|
||||
* @return array The registered custom comment type slugs.
|
||||
*/
|
||||
public static function get_comment_type_slugs() {
|
||||
return array_keys( self::get_comment_types() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the registered custom comment type slugs.
|
||||
*
|
||||
* @deprecated 4.5.0 Use get_comment_type_slugs instead.
|
||||
*
|
||||
* @return array The registered custom comment type slugs.
|
||||
*/
|
||||
public static function get_comment_type_names() {
|
||||
return array_values( wp_list_pluck( self::get_comment_types(), 'type' ) );
|
||||
_deprecated_function( __METHOD__, '4.5.0', 'get_comment_type_slugs' );
|
||||
|
||||
return self::get_comment_type_slugs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a comment type.
|
||||
* Get the custom comment type.
|
||||
*
|
||||
* Check if the type is registered, if not, check if it is a custom type.
|
||||
*
|
||||
* It looks for the array key in the registered types and returns the array.
|
||||
* If it is not found, it looks for the type in the custom types and returns the array.
|
||||
*
|
||||
* @param string $type The comment type.
|
||||
*
|
||||
@ -548,12 +591,13 @@ class Comment {
|
||||
public static function get_comment_type( $type ) {
|
||||
$type = strtolower( $type );
|
||||
$type = sanitize_key( $type );
|
||||
$types = self::get_comment_types();
|
||||
|
||||
if ( in_array( $type, array_keys( $types ), true ) ) {
|
||||
$type_array = $types[ $type ];
|
||||
} else {
|
||||
$comment_types = self::get_comment_types();
|
||||
$type_array = array();
|
||||
|
||||
// Check array keys.
|
||||
if ( in_array( $type, array_keys( $comment_types ), true ) ) {
|
||||
$type_array = $comment_types[ $type ];
|
||||
}
|
||||
|
||||
/**
|
||||
@ -595,7 +639,7 @@ class Comment {
|
||||
*/
|
||||
public static function register_comment_types() {
|
||||
register_comment_type(
|
||||
'announce',
|
||||
'repost',
|
||||
array(
|
||||
'label' => __( 'Reposts', 'activitypub' ),
|
||||
'singular' => __( 'Repost', 'activitypub' ),
|
||||
@ -603,8 +647,13 @@ class Comment {
|
||||
'icon' => '♻️',
|
||||
'class' => 'p-repost',
|
||||
'type' => 'repost',
|
||||
// translators: %1$s username, %2$s object format (post, audio, ...), %3$s URL, %4$s domain.
|
||||
'excerpt' => __( '… reposted this!', 'activitypub' ),
|
||||
'collection' => 'reposts',
|
||||
'activity_types' => array( 'announce' ),
|
||||
'excerpt' => html_entity_decode( \__( '… reposted this!', 'activitypub' ) ),
|
||||
/* translators: %d: Number of reposts */
|
||||
'count_single' => _x( '%d repost', 'number of reposts', 'activitypub' ),
|
||||
/* translators: %d: Number of reposts */
|
||||
'count_plural' => _x( '%d reposts', 'number of reposts', 'activitypub' ),
|
||||
)
|
||||
);
|
||||
|
||||
@ -617,8 +666,13 @@ class Comment {
|
||||
'icon' => '👍',
|
||||
'class' => 'p-like',
|
||||
'type' => 'like',
|
||||
// translators: %1$s username, %2$s object format (post, audio, ...), %3$s URL, %4$s domain.
|
||||
'excerpt' => __( '… liked this!', 'activitypub' ),
|
||||
'collection' => 'likes',
|
||||
'activity_types' => array( 'like' ),
|
||||
'excerpt' => html_entity_decode( \__( '… liked this!', 'activitypub' ) ),
|
||||
/* translators: %d: Number of likes */
|
||||
'count_single' => _x( '%d like', 'number of likes', 'activitypub' ),
|
||||
/* translators: %d: Number of likes */
|
||||
'count_plural' => _x( '%d likes', 'number of likes', 'activitypub' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -631,7 +685,7 @@ class Comment {
|
||||
* @return array show avatars on Activities
|
||||
*/
|
||||
public static function get_avatar_comment_types( $types ) {
|
||||
$comment_types = self::get_comment_type_names();
|
||||
$comment_types = self::get_comment_type_slugs();
|
||||
$types = array_merge( $types, $comment_types );
|
||||
|
||||
return array_unique( $types );
|
||||
@ -651,19 +705,113 @@ class Comment {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not exclude likes and reposts on ActivityPub requests.
|
||||
if ( defined( 'ACTIVITYPUB_REQUEST' ) && ACTIVITYPUB_REQUEST ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not exclude likes and reposts on REST requests.
|
||||
if ( \wp_is_serving_rest_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not exclude likes and reposts on admin pages or on non-singular pages.
|
||||
if ( is_admin() || ! is_singular() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! empty( $query->query_vars['type__in'] ) ) {
|
||||
// Do not exclude likes and reposts if the query is for comments.
|
||||
if ( ! empty( $query->query_vars['type__in'] ) || ! empty( $query->query_vars['type'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( isset( $query->query_vars['count'] ) && true === $query->query_vars['count'] ) {
|
||||
return;
|
||||
// Exclude likes and reposts by the ActivityPub plugin.
|
||||
$query->query_vars['type__not_in'] = self::get_comment_type_slugs();
|
||||
}
|
||||
|
||||
// Exclude likes and reposts by the Webmention plugin.
|
||||
$query->query_vars['type__not_in'] = self::get_comment_type_names();
|
||||
/**
|
||||
* Filter the comment status before it is set.
|
||||
*
|
||||
* @param string $approved The approved comment status.
|
||||
* @param array $commentdata The comment data.
|
||||
*
|
||||
* @return boolean `true` if the comment is approved, `false` otherwise.
|
||||
*/
|
||||
public static function pre_comment_approved( $approved, $commentdata ) {
|
||||
if ( $approved || \is_wp_error( $approved ) ) {
|
||||
return $approved;
|
||||
}
|
||||
|
||||
if ( '1' !== \get_option( 'comment_previously_approved' ) ) {
|
||||
return $approved;
|
||||
}
|
||||
|
||||
if (
|
||||
empty( $commentdata['comment_meta']['protocol'] ) ||
|
||||
'activitypub' !== $commentdata['comment_meta']['protocol']
|
||||
) {
|
||||
return $approved;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$author = $commentdata['comment_author'];
|
||||
$author_url = $commentdata['comment_author_url'];
|
||||
// phpcs:ignore
|
||||
$ok_to_comment = $wpdb->get_var( $wpdb->prepare( "SELECT comment_approved FROM $wpdb->comments WHERE comment_author = %s AND comment_author_url = %s and comment_approved = '1' LIMIT 1", $author, $author_url ) );
|
||||
|
||||
if ( 1 === (int) $ok_to_comment ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return $approved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update comment counts when interaction settings are disabled.
|
||||
*
|
||||
* Triggers a recount when likes or reposts are disabled to ensure accurate comment counts.
|
||||
*
|
||||
* @param mixed $old_value The old option value.
|
||||
* @param mixed $value The new option value.
|
||||
*/
|
||||
public static function maybe_update_comment_counts( $old_value, $value ) {
|
||||
if ( '1' === $old_value && '1' !== $value ) {
|
||||
Migration::update_comment_counts();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the comment count to exclude ActivityPub comment types.
|
||||
*
|
||||
* @param int|null $new_count The new comment count. Default null.
|
||||
* @param int $old_count The old comment count.
|
||||
* @param int $post_id Post ID.
|
||||
*
|
||||
* @return int|null The updated comment count, or null to use the default query.
|
||||
*/
|
||||
public static function pre_wp_update_comment_count_now( $new_count, $old_count, $post_id ) {
|
||||
if ( null === $new_count ) {
|
||||
$excluded_types = array_filter( self::get_comment_type_slugs(), array( self::class, 'is_comment_type_enabled' ) );
|
||||
|
||||
if ( ! empty( $excluded_types ) ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB
|
||||
$new_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type NOT IN ('" . implode( "','", $excluded_types ) . "')", $post_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $new_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment type is enabled.
|
||||
*
|
||||
* @param string $comment_type The comment type.
|
||||
* @return bool True if the comment type is enabled.
|
||||
*/
|
||||
public static function is_comment_type_enabled( $comment_type ) {
|
||||
return '1' === get_option( "activitypub_allow_{$comment_type}s", '1' );
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,6 @@
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_DEBUG;
|
||||
use WP_DEBUG_LOG;
|
||||
|
||||
/**
|
||||
* ActivityPub Debug Class.
|
||||
*
|
||||
@ -20,9 +17,11 @@ class Debug {
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( WP_DEBUG_LOG ) {
|
||||
if ( \WP_DEBUG && \WP_DEBUG_LOG ) {
|
||||
\add_action( 'activitypub_safe_remote_post_response', array( self::class, 'log_remote_post_responses' ), 10, 2 );
|
||||
\add_action( 'activitypub_inbox', array( self::class, 'log_inbox' ), 10, 3 );
|
||||
|
||||
\add_action( 'activitypub_sent_to_inbox', array( self::class, 'log_sent_to_inbox' ), 10, 2 );
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,12 +47,26 @@ class Debug {
|
||||
$type = strtolower( $type );
|
||||
|
||||
if ( 'delete' !== $type ) {
|
||||
$url = object_to_uri( $data['actor'] );
|
||||
$actor = $data['actor'] ?? '';
|
||||
$url = object_to_uri( $actor );
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
|
||||
\error_log( "[INBOX] Request From: {$url} with Activity: " . \print_r( $data, true ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the sent to follower action.
|
||||
*
|
||||
* @param array $result The result of the remote post request.
|
||||
* @param string $inbox The inbox URL.
|
||||
*/
|
||||
public static function log_sent_to_inbox( $result, $inbox ) {
|
||||
if ( \is_wp_error( $result ) ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
|
||||
\error_log( "[DISPATCHER] Failed Request to: {$inbox} with Result: " . \print_r( $result, true ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a log entry.
|
||||
*
|
||||
|
466
wp-content/plugins/activitypub/includes/class-dispatcher.php
Normal file
466
wp-content/plugins/activitypub/includes/class-dispatcher.php
Normal file
@ -0,0 +1,466 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub Dispatcher Class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Outbox;
|
||||
|
||||
/**
|
||||
* ActivityPub Dispatcher Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/
|
||||
*/
|
||||
class Dispatcher {
|
||||
|
||||
/**
|
||||
* Batch size.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static $batch_size = ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE;
|
||||
|
||||
/**
|
||||
* Callback for the async batch processing.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $callback = array( self::class, 'send_to_followers' );
|
||||
|
||||
/**
|
||||
* Error codes that qualify for a retry.
|
||||
*
|
||||
* @see https://github.com/tfredrich/RestApiTutorial.com/blob/fd08b0f67f07450521d143b123cd6e1846cb2e3b/content/advanced/responses/retries.md
|
||||
* @var int[]
|
||||
*/
|
||||
public static $retry_error_codes = array( 408, 429, 500, 502, 503, 504 );
|
||||
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_process_outbox', array( self::class, 'process_outbox' ) );
|
||||
|
||||
// Default filters to add Inboxes to sent to.
|
||||
\add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 );
|
||||
\add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 );
|
||||
\add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_relays' ), 10, 3 );
|
||||
|
||||
// Fallback for `activitypub_send_to_inboxes` filter.
|
||||
\add_filter(
|
||||
'activitypub_additional_inboxes',
|
||||
function ( $inboxes, $actor_id, $activity ) {
|
||||
/**
|
||||
* Filters the list of interactees inboxes to send the Activity to.
|
||||
*
|
||||
* @param array $inboxes The list of inboxes to send to.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*
|
||||
* @deprecated 5.2.0 Use `activitypub_additional_inboxes` instead.
|
||||
* @deprecated 5.4.0 Use `activitypub_additional_inboxes` instead.
|
||||
*/
|
||||
$inboxes = \apply_filters_deprecated( 'activitypub_send_to_inboxes', array( $inboxes, $actor_id, $activity ), '5.2.0', 'activitypub_additional_inboxes' );
|
||||
$inboxes = \apply_filters_deprecated( 'activitypub_interactees_inboxes', array( $inboxes, $actor_id, $activity ), '5.4.0', 'activitypub_additional_inboxes' );
|
||||
|
||||
return $inboxes;
|
||||
},
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the outbox.
|
||||
*
|
||||
* @param int $id The outbox ID.
|
||||
*/
|
||||
public static function process_outbox( $id ) {
|
||||
$outbox_item = \get_post( $id );
|
||||
|
||||
// If the activity is not a post, return.
|
||||
if ( ! $outbox_item ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = Outbox::get_actor( $outbox_item );
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
// If the actor is not found, publish the post and don't try again.
|
||||
\wp_publish_post( $outbox_item );
|
||||
return;
|
||||
}
|
||||
|
||||
$activity = Outbox::get_activity( $outbox_item );
|
||||
|
||||
// Send to mentioned and replied-to users. Everyone other than followers.
|
||||
self::send_to_additional_inboxes( $activity, $actor->get__id(), $outbox_item );
|
||||
|
||||
if ( self::should_send_to_followers( $activity, $actor, $outbox_item ) ) {
|
||||
Scheduler::async_batch(
|
||||
self::$callback,
|
||||
$outbox_item->ID,
|
||||
self::$batch_size,
|
||||
\get_post_meta( $outbox_item->ID, '_activitypub_outbox_offset', true ) ?: 0 // phpcs:ignore
|
||||
);
|
||||
} else {
|
||||
// No followers to process for this update. We're done.
|
||||
\wp_publish_post( $outbox_item );
|
||||
\delete_post_meta( $outbox_item->ID, '_activitypub_outbox_offset' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously runs batch processing routines.
|
||||
*
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $batch_size Optional. The batch size. Default ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE.
|
||||
* @param int $offset Optional. The offset. Default 0.
|
||||
*
|
||||
* @return array|void The next batch of followers to process, or void if done.
|
||||
*/
|
||||
public static function send_to_followers( $outbox_item_id, $batch_size = ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE, $offset = 0 ) {
|
||||
$json = Outbox::get_activity( $outbox_item_id )->to_json();
|
||||
$actor = Outbox::get_actor( \get_post( $outbox_item_id ) );
|
||||
$inboxes = Followers::get_inboxes_for_activity( $json, $actor->get__id(), $batch_size, $offset );
|
||||
|
||||
$retries = self::send_to_inboxes( $inboxes, $outbox_item_id );
|
||||
|
||||
// Retry failed inboxes.
|
||||
if ( ! empty( $retries ) ) {
|
||||
self::schedule_retry( $retries, $outbox_item_id );
|
||||
}
|
||||
|
||||
if ( is_countable( $inboxes ) && count( $inboxes ) < $batch_size ) {
|
||||
\delete_post_meta( $outbox_item_id, '_activitypub_outbox_offset' );
|
||||
|
||||
/**
|
||||
* Fires when the followers are complete.
|
||||
*
|
||||
* @param array $inboxes The inboxes.
|
||||
* @param string $json The ActivityPub Activity JSON
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $batch_size The batch size.
|
||||
* @param int $offset The offset.
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_processing_complete', $inboxes, $json, $actor->get__id(), $outbox_item_id, $batch_size, $offset );
|
||||
|
||||
// No more followers to process for this update.
|
||||
\wp_publish_post( $outbox_item_id );
|
||||
} else {
|
||||
\update_post_meta( $outbox_item_id, '_activitypub_outbox_offset', $offset + $batch_size );
|
||||
|
||||
/**
|
||||
* Fires when the batch of followers is complete.
|
||||
*
|
||||
* @param array $inboxes The inboxes.
|
||||
* @param string $json The ActivityPub Activity JSON
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $batch_size The batch size.
|
||||
* @param int $offset The offset.
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_processing_batch_complete', $inboxes, $json, $actor->get__id(), $outbox_item_id, $batch_size, $offset );
|
||||
|
||||
return array( $outbox_item_id, $batch_size, $offset + $batch_size );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry sending to followers.
|
||||
*
|
||||
* @param string $transient_key The key to retrieve retry inboxes.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $attempt The attempt number.
|
||||
*/
|
||||
public static function retry_send_to_followers( $transient_key, $outbox_item_id, $attempt = 1 ) {
|
||||
$inboxes = \get_transient( $transient_key );
|
||||
if ( false === $inboxes ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the transient as we no longer need it.
|
||||
\delete_transient( $transient_key );
|
||||
|
||||
$retries = self::send_to_inboxes( $inboxes, $outbox_item_id );
|
||||
|
||||
// Retry failed inboxes.
|
||||
if ( ++$attempt < 3 && ! empty( $retries ) ) {
|
||||
self::schedule_retry( $retries, $outbox_item_id, $attempt );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send to inboxes.
|
||||
*
|
||||
* @param array $inboxes The inboxes to notify.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @return array The failed inboxes.
|
||||
*/
|
||||
private static function send_to_inboxes( $inboxes, $outbox_item_id ) {
|
||||
$json = Outbox::get_activity( $outbox_item_id )->to_json();
|
||||
$actor = Outbox::get_actor( \get_post( $outbox_item_id ) );
|
||||
$retries = array();
|
||||
|
||||
/**
|
||||
* Fires before sending an Activity to inboxes.
|
||||
*
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param array $inboxes The inboxes to send to.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
*/
|
||||
\do_action( 'activitypub_pre_send_to_inboxes', $json, $inboxes, $outbox_item_id );
|
||||
|
||||
foreach ( $inboxes as $inbox ) {
|
||||
$result = safe_remote_post( $inbox, $json, $actor->get__id() );
|
||||
|
||||
if ( is_wp_error( $result ) && in_array( $result->get_error_code(), self::$retry_error_codes, true ) ) {
|
||||
$retries[] = $inbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after an Activity has been sent to an inbox.
|
||||
*
|
||||
* @param array $result The result of the remote post request.
|
||||
* @param string $inbox The inbox URL.
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
*/
|
||||
\do_action( 'activitypub_sent_to_inbox', $result, $inbox, $json, $actor->get__id(), $outbox_item_id );
|
||||
}
|
||||
|
||||
return $retries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a retry.
|
||||
*
|
||||
* @param array $retries The inboxes to retry.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $attempt Optional. The attempt number. Default 1.
|
||||
*/
|
||||
private static function schedule_retry( $retries, $outbox_item_id, $attempt = 1 ) {
|
||||
$transient_key = 'activitypub_retry_' . \wp_generate_password( 12, false );
|
||||
\set_transient( $transient_key, $retries, WEEK_IN_SECONDS );
|
||||
|
||||
\wp_schedule_single_event(
|
||||
\time() + ( $attempt * $attempt * HOUR_IN_SECONDS ),
|
||||
'activitypub_async_batch',
|
||||
array(
|
||||
array( self::class, 'retry_send_to_followers' ),
|
||||
$transient_key,
|
||||
$outbox_item_id,
|
||||
$attempt,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an Activity to a custom list of inboxes, like mentioned users or replied-to posts.
|
||||
*
|
||||
* For all custom implementations, please use the `activitypub_additional_inboxes` filter.
|
||||
*
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param \WP_Post $outbox_item The WordPress object.
|
||||
*/
|
||||
private static function send_to_additional_inboxes( $activity, $actor_id, $outbox_item = null ) {
|
||||
/**
|
||||
* Filters the list of inboxes to send the Activity to.
|
||||
*
|
||||
* @param array $inboxes The list of inboxes to send to.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*/
|
||||
$inboxes = apply_filters( 'activitypub_additional_inboxes', array(), $actor_id, $activity );
|
||||
$inboxes = array_unique( $inboxes );
|
||||
|
||||
$retries = self::send_to_inboxes( $inboxes, $outbox_item->ID );
|
||||
|
||||
// Retry failed inboxes.
|
||||
if ( ! empty( $retries ) ) {
|
||||
self::schedule_retry( $retries, $outbox_item->ID );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default filter to add Inboxes of Mentioned Actors
|
||||
*
|
||||
* @param array $inboxes The list of Inboxes.
|
||||
* @param int $actor_id The WordPress Actor-ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*
|
||||
* @return array The filtered Inboxes.
|
||||
*/
|
||||
public static function add_inboxes_by_mentioned_actors( $inboxes, $actor_id, $activity ) {
|
||||
$cc = $activity->get_cc() ?? array();
|
||||
$to = $activity->get_to() ?? array();
|
||||
|
||||
$audience = array_merge( $cc, $to );
|
||||
|
||||
// Remove "public placeholder" and "same domain" from the audience.
|
||||
$audience = array_filter(
|
||||
$audience,
|
||||
function ( $actor ) {
|
||||
return 'https://www.w3.org/ns/activitystreams#Public' !== $actor && ! is_same_domain( $actor );
|
||||
}
|
||||
);
|
||||
|
||||
if ( $audience ) {
|
||||
$mentioned_inboxes = Mention::get_inboxes( $audience );
|
||||
|
||||
return array_merge( $inboxes, $mentioned_inboxes );
|
||||
}
|
||||
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default filter to add Inboxes of Posts that are set as `in-reply-to`
|
||||
*
|
||||
* @param array $inboxes The list of Inboxes.
|
||||
* @param int $actor_id The WordPress Actor-ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*
|
||||
* @return array The filtered Inboxes
|
||||
*/
|
||||
public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activity ) {
|
||||
$in_reply_to = $activity->get_in_reply_to();
|
||||
|
||||
if ( ! $in_reply_to ) {
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
if ( ! is_array( $in_reply_to ) ) {
|
||||
$in_reply_to = array( $in_reply_to );
|
||||
}
|
||||
|
||||
foreach ( $in_reply_to as $url ) {
|
||||
// No need to self-notify.
|
||||
if ( is_same_domain( $url ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$object = Http::get_remote_object( $url );
|
||||
|
||||
if (
|
||||
! $object ||
|
||||
\is_wp_error( $object ) ||
|
||||
empty( $object['attributedTo'] )
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$actor = object_to_uri( $object['attributedTo'] );
|
||||
$actor = Http::get_remote_object( $actor );
|
||||
|
||||
if ( ! $actor || \is_wp_error( $actor ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! empty( $actor['endpoints']['sharedInbox'] ) ) {
|
||||
$inboxes[] = $actor['endpoints']['sharedInbox'];
|
||||
} elseif ( ! empty( $actor['inbox'] ) ) {
|
||||
$inboxes[] = $actor['inbox'];
|
||||
}
|
||||
}
|
||||
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Blog Actor inboxes to Updates so the Blog User's followers are notified of edits.
|
||||
*
|
||||
* @deprecated 5.2.0 Use {@see Followers::maybe_add_inboxes_of_blog_user} instead.
|
||||
*
|
||||
* @param array $inboxes The list of Inboxes.
|
||||
* @param int $actor_id The WordPress Actor-ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*
|
||||
* @return array The filtered Inboxes.
|
||||
*/
|
||||
public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $activity ) { // phpcs:ignore
|
||||
_deprecated_function( __METHOD__, '5.2.0', 'Followers::maybe_add_inboxes_of_blog_user' );
|
||||
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if passed Activity is public.
|
||||
*
|
||||
* @param Activity $activity The Activity object.
|
||||
* @param \Activitypub\Model\User|\Activitypub\Model\Blog $actor The Actor object.
|
||||
* @param \WP_Post $outbox_item The Outbox item.
|
||||
*
|
||||
* @return boolean True if public, false if not.
|
||||
*/
|
||||
protected static function should_send_to_followers( $activity, $actor, $outbox_item ) {
|
||||
// Check if follower endpoint is set.
|
||||
$cc = $activity->get_cc() ?? array();
|
||||
$to = $activity->get_to() ?? array();
|
||||
|
||||
$audience = array_merge( $cc, $to );
|
||||
|
||||
$send = (
|
||||
// Check if activity is public.
|
||||
in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) ||
|
||||
// ...or check if follower endpoint is set.
|
||||
in_array( $actor->get_followers(), $audience, true )
|
||||
);
|
||||
|
||||
if ( $send ) {
|
||||
$followers = Followers::get_inboxes_for_activity( $activity->to_json(), $actor->get__id() );
|
||||
|
||||
// Only send if there are followers to send to.
|
||||
$send = ! is_countable( $followers ) || 0 < count( $followers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters whether to send an Activity to followers.
|
||||
*
|
||||
* @param bool $send_activity_to_followers Whether to send the Activity to followers.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param \WP_Post $outbox_item The WordPress object.
|
||||
*/
|
||||
return apply_filters( 'activitypub_send_activity_to_followers', $send, $activity, $actor->get__id(), $outbox_item );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Inboxes of Relays.
|
||||
*
|
||||
* @param array $inboxes The list of Inboxes.
|
||||
* @param int $actor_id The Actor-ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*
|
||||
* @return array The filtered Inboxes.
|
||||
*/
|
||||
public static function add_inboxes_of_relays( $inboxes, $actor_id, $activity ) {
|
||||
// Check if follower endpoint is set.
|
||||
$cc = $activity->get_cc() ?? array();
|
||||
$to = $activity->get_to() ?? array();
|
||||
|
||||
$audience = array_merge( $cc, $to );
|
||||
|
||||
// Check if activity is public.
|
||||
if ( ! in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) ) {
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
$relays = \get_option( 'activitypub_relays', array() );
|
||||
|
||||
if ( empty( $relays ) ) {
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
return array_merge( $inboxes, $relays );
|
||||
}
|
||||
}
|
263
wp-content/plugins/activitypub/includes/class-embed.php
Normal file
263
wp-content/plugins/activitypub/includes/class-embed.php
Normal file
@ -0,0 +1,263 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub Embed Handler.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* Class to handle embedding ActivityPub content
|
||||
*/
|
||||
class Embed {
|
||||
|
||||
/**
|
||||
* Initialize the embed handler
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'pre_oembed_result', array( self::class, 'maybe_use_activitypub_embed' ), 10, 3 );
|
||||
\add_filter( 'oembed_dataparse', array( self::class, 'handle_filtered_oembed_result' ), 11, 3 );
|
||||
\add_filter( 'oembed_request_post_id', array( self::class, 'register_fallback_hook' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ActivityPub embed HTML for a URL.
|
||||
*
|
||||
* @param string $url The URL to get the embed for.
|
||||
* @param boolean $inline_css Whether to inline CSS. Default true.
|
||||
*
|
||||
* @return string|false The embed HTML or false if not found.
|
||||
*/
|
||||
public static function get_html( $url, $inline_css = true ) {
|
||||
// Try to get ActivityPub representation.
|
||||
$object = Http::get_remote_object( $url );
|
||||
if ( is_wp_error( $object ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::get_html_for_object( $object, $inline_css );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ActivityPub embed HTML for an ActivityPub object.
|
||||
*
|
||||
* @param array $activity_object The ActivityPub object to build the embed for.
|
||||
* @param boolean $inline_css Whether to inline CSS. Default true.
|
||||
*
|
||||
* @return string The embed HTML.
|
||||
*/
|
||||
public static function get_html_for_object( $activity_object, $inline_css = true ) {
|
||||
$author_name = $activity_object['attributedTo'] ?? '';
|
||||
$avatar_url = $activity_object['icon']['url'] ?? '';
|
||||
$author_url = $author_name;
|
||||
|
||||
// If we don't have an avatar URL, but we have an author URL, try to fetch it.
|
||||
if ( ! $avatar_url && $author_url ) {
|
||||
$author = Http::get_remote_object( $author_url );
|
||||
if ( ! is_wp_error( $author ) ) {
|
||||
$avatar_url = $author['icon']['url'] ?? '';
|
||||
$author_name = $author['name'] ?? $author_name;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Webfinger where not found.
|
||||
if ( empty( $author['webfinger'] ) ) {
|
||||
if ( ! empty( $author['preferredUsername'] ) && ! empty( $author['url'] ) ) {
|
||||
// Construct webfinger-style identifier from username and domain.
|
||||
$domain = wp_parse_url( $author['url'], PHP_URL_HOST );
|
||||
$author['webfinger'] = '@' . $author['preferredUsername'] . '@' . $domain;
|
||||
} else {
|
||||
// Fallback to URL.
|
||||
$author['webfinger'] = $author_url;
|
||||
}
|
||||
}
|
||||
|
||||
$title = $activity_object['name'] ?? '';
|
||||
$content = $activity_object['content'] ?? '';
|
||||
$published = isset( $activity_object['published'] ) ? gmdate( get_option( 'date_format' ) . ', ' . get_option( 'time_format' ), strtotime( $activity_object['published'] ) ) : '';
|
||||
$boosts = isset( $activity_object['shares']['totalItems'] ) ? (int) $activity_object['shares']['totalItems'] : null;
|
||||
$favorites = isset( $activity_object['likes']['totalItems'] ) ? (int) $activity_object['likes']['totalItems'] : null;
|
||||
|
||||
$image = '';
|
||||
if ( isset( $activity_object['image']['url'] ) ) {
|
||||
$image = $activity_object['image']['url'];
|
||||
} elseif ( isset( $activity_object['attachment'] ) ) {
|
||||
foreach ( $activity_object['attachment'] as $attachment ) {
|
||||
if ( isset( $attachment['type'] ) && in_array( $attachment['type'], array( 'Image', 'Document' ), true ) ) {
|
||||
$image = $attachment['url'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ob_start();
|
||||
load_template(
|
||||
ACTIVITYPUB_PLUGIN_DIR . 'templates/reply-embed.php',
|
||||
false,
|
||||
array(
|
||||
'author_name' => $author_name,
|
||||
'author_url' => $author_url,
|
||||
'avatar_url' => $avatar_url,
|
||||
'published' => $published,
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'image' => $image,
|
||||
'boosts' => $boosts,
|
||||
'favorites' => $favorites,
|
||||
'url' => $activity_object['id'],
|
||||
'webfinger' => $author['webfinger'],
|
||||
)
|
||||
);
|
||||
|
||||
if ( $inline_css ) {
|
||||
// Grab the CSS.
|
||||
$css = \file_get_contents( ACTIVITYPUB_PLUGIN_DIR . 'assets/css/activitypub-embed.css' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
// We embed CSS directly because this may be in an iframe.
|
||||
printf( '<style>%s</style>', $css ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
// A little light whitespace cleanup.
|
||||
return preg_replace( '/\s+/', ' ', ob_get_clean() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a real oEmbed result exists for the given URL.
|
||||
*
|
||||
* @param string $url The URL to check.
|
||||
* @param array $args Additional arguments passed to wp_oembed_get().
|
||||
* @return bool True if a real oEmbed result exists, false otherwise.
|
||||
*/
|
||||
public static function has_real_oembed( $url, $args = array() ) {
|
||||
// Temporarily remove our filter to avoid infinite loops.
|
||||
\remove_filter( 'pre_oembed_result', array( self::class, 'maybe_use_activitypub_embed' ), 10, 3 );
|
||||
|
||||
// Try to get a "real" oEmbed result. If found, it'll be cached to avoid unnecessary HTTP requests in `wp_oembed_get`.
|
||||
$oembed_result = \wp_oembed_get( $url, $args );
|
||||
|
||||
// Add our filter back.
|
||||
\add_filter( 'pre_oembed_result', array( self::class, 'maybe_use_activitypub_embed' ), 10, 3 );
|
||||
|
||||
return false !== $oembed_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the oembed result to handle ActivityPub content when no oEmbed is found.
|
||||
* Implementation is a bit weird because there's no way to filter on a false result, we have to use `pre_oembed_result`.
|
||||
*
|
||||
* @param null|string $result The UNSANITIZED (and potentially unsafe) HTML that should be used to embed.
|
||||
* @param string $url The URL to the content that should be attempted to be embedded.
|
||||
* @param array $args Additional arguments passed to wp_oembed_get().
|
||||
* @return null|string Return null to allow normal oEmbed processing, or string for ActivityPub embed.
|
||||
*/
|
||||
public static function maybe_use_activitypub_embed( $result, $url, $args ) {
|
||||
// If we already have a result, return it.
|
||||
if ( null !== $result ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// If we found a real oEmbed, return null to allow normal processing.
|
||||
if ( self::has_real_oembed( $url, $args ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// No oEmbed found, try to get ActivityPub representation.
|
||||
$html = get_embed_html( $url );
|
||||
|
||||
// If we couldn't get an ActivityPub embed either, return null to allow normal processing.
|
||||
if ( ! $html ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return the ActivityPub embed HTML.
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cases where WordPress has filtered out the oEmbed result for security reasons,
|
||||
* but we can provide a safe ActivityPub-specific markup.
|
||||
*
|
||||
* This runs after wp_filter_oembed_result has potentially nullified the result.
|
||||
*
|
||||
* @param string|false $html The returned oEmbed HTML.
|
||||
* @param object $data A data object result from an oEmbed provider.
|
||||
* @param string $url The URL of the content to be embedded.
|
||||
* @return string|false The filtered oEmbed HTML or our ActivityPub embed.
|
||||
*/
|
||||
public static function handle_filtered_oembed_result( $html, $data, $url ) {
|
||||
// If we already have valid HTML, return it.
|
||||
if ( $html ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// If this isn't a rich or video type, we can't help.
|
||||
if ( ! isset( $data->type ) || ! \in_array( $data->type, array( 'rich', 'video' ), true ) ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// If there's no HTML in the data, we can't help.
|
||||
if ( empty( $data->html ) || ! \is_string( $data->html ) ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Try to get ActivityPub representation.
|
||||
$activitypub_html = get_embed_html( $url );
|
||||
if ( ! $activitypub_html ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Return our safer ActivityPub embed HTML.
|
||||
return $activitypub_html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the fallback hook for oEmbed requests.
|
||||
*
|
||||
* Avoids filtering every single API request.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @return int The post ID.
|
||||
*/
|
||||
public static function register_fallback_hook( $post_id ) {
|
||||
\add_filter( 'rest_request_after_callbacks', array( self::class, 'oembed_fediverse_fallback' ), 10, 3 );
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback for oEmbed requests to the Fediverse.
|
||||
*
|
||||
* @param \WP_REST_Response|\WP_Error $response Result to send to the client.
|
||||
* @param array $handler Route handler used for the request.
|
||||
* @param \WP_REST_Request $request Request used to generate the response.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error The response to send to the client.
|
||||
*/
|
||||
public static function oembed_fediverse_fallback( $response, $handler, $request ) {
|
||||
if ( is_wp_error( $response ) && 'oembed_invalid_url' === $response->get_error_code() ) {
|
||||
$url = $request->get_param( 'url' );
|
||||
$html = get_embed_html( $url );
|
||||
|
||||
if ( $html ) {
|
||||
$args = $request->get_params();
|
||||
$data = (object) array(
|
||||
'provider_name' => 'Embed Handler',
|
||||
'html' => $html,
|
||||
'scripts' => array(),
|
||||
);
|
||||
|
||||
/** This filter is documented in wp-includes/class-wp-oembed.php */
|
||||
$data->html = apply_filters( 'oembed_result', $data->html, $url, $args );
|
||||
|
||||
/** This filter is documented in wp-includes/class-wp-oembed-controller.php */
|
||||
$ttl = apply_filters( 'rest_oembed_ttl', DAY_IN_SECONDS, $url, $args );
|
||||
|
||||
set_transient( 'oembed_' . md5( serialize( $args ) ), $data, $ttl ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
|
||||
|
||||
$response = new \WP_REST_Response( $data );
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ use Activitypub\Handler\Create;
|
||||
use Activitypub\Handler\Delete;
|
||||
use Activitypub\Handler\Follow;
|
||||
use Activitypub\Handler\Like;
|
||||
use Activitypub\Handler\Move;
|
||||
use Activitypub\Handler\Undo;
|
||||
use Activitypub\Handler\Update;
|
||||
|
||||
@ -36,10 +37,8 @@ class Handler {
|
||||
Follow::init();
|
||||
Undo::init();
|
||||
Update::init();
|
||||
|
||||
if ( ! ACTIVITYPUB_DISABLE_REACTIONS ) {
|
||||
Like::init();
|
||||
}
|
||||
Move::init();
|
||||
|
||||
/**
|
||||
* Register additional handlers.
|
||||
|
@ -33,11 +33,11 @@ class Hashtag {
|
||||
*/
|
||||
public static function filter_activity_object( $activity ) {
|
||||
/* phpcs:ignore Squiz.PHP.CommentedOutCode.Found
|
||||
Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629
|
||||
if ( ! empty( $activity['summary'] ) ) {
|
||||
Only changed it for Person and Group as long is not merged: https://github.com/mastodon/mastodon/pull/28629
|
||||
*/
|
||||
if ( ! empty( $activity['summary'] ) && in_array( $activity['type'], array( 'Person', 'Group' ), true ) ) {
|
||||
$activity['summary'] = self::the_content( $activity['summary'] );
|
||||
}
|
||||
*/
|
||||
|
||||
if ( ! empty( $activity['content'] ) ) {
|
||||
$activity['content'] = self::the_content( $activity['content'] );
|
||||
@ -53,19 +53,27 @@ class Hashtag {
|
||||
* @param \WP_Post $post Post object.
|
||||
*/
|
||||
public static function insert_post( $post_id, $post ) {
|
||||
// Check if the post supports ActivityPub.
|
||||
if ( ! \post_type_supports( \get_post_type( $post ), 'activitypub' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the (custom) post supports tags.
|
||||
$taxonomies = \get_object_taxonomies( $post );
|
||||
if ( ! in_array( 'post_tag', $taxonomies, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tags = array();
|
||||
|
||||
if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $post->post_content, $match ) ) {
|
||||
$tags = array_merge( $tags, $match[1] );
|
||||
// Skip hashtags in HTML attributes, like hex colors.
|
||||
$content = wp_strip_all_tags( $post->post_content . "\n" . $post->post_excerpt );
|
||||
|
||||
if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $content, $match ) ) {
|
||||
$tags = array_unique( $match[1] );
|
||||
}
|
||||
|
||||
if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $post->post_excerpt, $match ) ) {
|
||||
$tags = array_merge( $tags, $match[1] );
|
||||
}
|
||||
|
||||
$tags = \implode( ', ', $tags );
|
||||
|
||||
\wp_add_post_tags( $post->ID, $tags );
|
||||
\wp_add_post_tags( $post->ID, \implode( ', ', $tags ) );
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,374 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Health_Check class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
/**
|
||||
* ActivityPub Health_Check Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Health_Check {
|
||||
|
||||
/**
|
||||
* Initialize health checks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'site_status_tests', array( self::class, 'add_tests' ) );
|
||||
\add_filter( 'debug_information', array( self::class, 'debug_information' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add tests to the Site Health Check.
|
||||
*
|
||||
* @param array $tests The test array.
|
||||
*
|
||||
* @return array The filtered test array.
|
||||
*/
|
||||
public static function add_tests( $tests ) {
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
$tests['direct']['activitypub_test_author_url'] = array(
|
||||
'label' => \__( 'Author URL test', 'activitypub' ),
|
||||
'test' => array( self::class, 'test_author_url' ),
|
||||
);
|
||||
}
|
||||
|
||||
$tests['direct']['activitypub_test_webfinger'] = array(
|
||||
'label' => __( 'WebFinger Test', 'activitypub' ),
|
||||
'test' => array( self::class, 'test_webfinger' ),
|
||||
);
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Author URL tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function test_author_url() {
|
||||
$result = array(
|
||||
'label' => \__( 'Author URL accessible', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Your author URL is accessible and supports the required "Accept" header.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_author_url',
|
||||
);
|
||||
|
||||
$check = self::is_author_url_accessible();
|
||||
|
||||
if ( true === $check ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['status'] = 'critical';
|
||||
$result['label'] = \__( 'Author URL is not accessible', 'activitypub' );
|
||||
$result['badge']['color'] = 'red';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
$check->get_error_message()
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* System Cron tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function test_system_cron() {
|
||||
$result = array(
|
||||
'label' => \__( 'System Task Scheduler configured', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\esc_html__( 'You seem to use the System Task Scheduler to process WP_Cron tasks.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_system_cron',
|
||||
);
|
||||
|
||||
if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['status'] = 'recommended';
|
||||
$result['label'] = \__( 'System Task Scheduler not configured', 'activitypub' );
|
||||
$result['badge']['color'] = 'orange';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Enhance your WordPress site’s performance and mitigate potential heavy loads caused by plugins like ActivityPub by setting up a system cron job to run WP Cron. This ensures scheduled tasks are executed consistently and reduces the reliance on website traffic for trigger events.', 'activitypub' )
|
||||
);
|
||||
$result['actions'] .= sprintf(
|
||||
'<p><a href="%s" target="_blank" rel="noopener">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
|
||||
esc_url( __( 'https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/', 'activitypub' ) ),
|
||||
__( 'Learn how to hook the WP-Cron into the System Task Scheduler.', 'activitypub' ),
|
||||
/* translators: Hidden accessibility text. */
|
||||
__( '(opens in a new tab)', 'activitypub' )
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebFinger tests.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function test_webfinger() {
|
||||
$result = array(
|
||||
'label' => \__( 'WebFinger endpoint', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Your WebFinger endpoint is accessible and returns the correct information.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_webfinger',
|
||||
);
|
||||
|
||||
$check = self::is_webfinger_endpoint_accessible();
|
||||
|
||||
if ( true === $check ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['status'] = 'critical';
|
||||
$result['label'] = \__( 'WebFinger endpoint is not accessible', 'activitypub' );
|
||||
$result['badge']['color'] = 'red';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
$check->get_error_message()
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `author_posts_url` is accessible and that request returns correct JSON.
|
||||
*
|
||||
* @return bool|WP_Error True if the author URL is accessible, WP_Error otherwise.
|
||||
*/
|
||||
public static function is_author_url_accessible() {
|
||||
$user = \wp_get_current_user();
|
||||
$author_url = \get_author_posts_url( $user->ID );
|
||||
$reference_author_url = self::get_author_posts_url( $user->ID, $user->user_nicename );
|
||||
|
||||
// Check for "author" in URL.
|
||||
if ( $author_url !== $reference_author_url ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL.
|
||||
\__(
|
||||
'Your author URL <code>%s</code> was replaced, this is often done by plugins.',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Try to access author URL.
|
||||
$response = \wp_remote_get(
|
||||
$author_url,
|
||||
array(
|
||||
'headers' => array( 'Accept' => 'application/activity+json' ),
|
||||
'redirection' => 0,
|
||||
)
|
||||
);
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL.
|
||||
\__(
|
||||
'Your author URL <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure. If the setup seems fine, maybe check if a plugin might restrict the access.',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$response_code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
// Check for redirects.
|
||||
if ( \in_array( $response_code, array( 301, 302, 307, 308 ), true ) ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL.
|
||||
\__(
|
||||
'Your author URL <code>%s</code> is redirecting to another page, this is often done by SEO plugins like "Yoast SEO".',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Check if response is JSON.
|
||||
$body = \wp_remote_retrieve_body( $response );
|
||||
|
||||
if ( ! \is_string( $body ) || ! \is_array( \json_decode( $body, true ) ) ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL.
|
||||
\__(
|
||||
'Your author URL <code>%s</code> does not return valid JSON for <code>application/activity+json</code>. Please check if your hosting supports alternate <code>Accept</code> headers.',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WebFinger endpoint is accessible and profile request returns correct JSON
|
||||
*
|
||||
* @return boolean|WP_Error
|
||||
*/
|
||||
public static function is_webfinger_endpoint_accessible() {
|
||||
$user = Users::get_by_id( Users::APPLICATION_USER_ID );
|
||||
$resource = $user->get_webfinger();
|
||||
|
||||
$url = Webfinger::resolve( $resource );
|
||||
if ( \is_wp_error( $url ) ) {
|
||||
$allowed = array( 'code' => array() );
|
||||
|
||||
$not_accessible = wp_kses(
|
||||
// translators: %s: Author URL.
|
||||
\__(
|
||||
'Your WebFinger endpoint <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure.',
|
||||
'activitypub'
|
||||
),
|
||||
$allowed
|
||||
);
|
||||
$invalid_response = wp_kses(
|
||||
// translators: %s: Author URL.
|
||||
\__(
|
||||
'Your WebFinger endpoint <code>%s</code> does not return valid JSON for <code>application/jrd+json</code>.',
|
||||
'activitypub'
|
||||
),
|
||||
$allowed
|
||||
);
|
||||
|
||||
$health_messages = array(
|
||||
'webfinger_url_not_accessible' => \sprintf(
|
||||
$not_accessible,
|
||||
$url->get_error_data()['data']
|
||||
),
|
||||
'webfinger_url_invalid_response' => \sprintf(
|
||||
// translators: %s: Author URL.
|
||||
$invalid_response,
|
||||
$url->get_error_data()['data']
|
||||
),
|
||||
);
|
||||
$message = null;
|
||||
if ( isset( $health_messages[ $url->get_error_code() ] ) ) {
|
||||
$message = $health_messages[ $url->get_error_code() ];
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
$url->get_error_code(),
|
||||
$message,
|
||||
$url->get_error_data()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the URL to the author page for the user with the ID provided.
|
||||
*
|
||||
* @global \WP_Rewrite $wp_rewrite WordPress rewrite component.
|
||||
*
|
||||
* @param int $author_id Author ID.
|
||||
* @param string $author_nicename Optional. The author's nicename (slug). Default empty.
|
||||
*
|
||||
* @return string The URL to the author's page.
|
||||
*/
|
||||
public static function get_author_posts_url( $author_id, $author_nicename = '' ) {
|
||||
global $wp_rewrite;
|
||||
|
||||
$auth_id = (int) $author_id;
|
||||
$link = $wp_rewrite->get_author_permastruct();
|
||||
|
||||
if ( empty( $link ) ) {
|
||||
$file = home_url( '/' );
|
||||
$link = $file . '?author=' . $auth_id;
|
||||
} else {
|
||||
if ( '' === $author_nicename ) {
|
||||
$user = get_userdata( $author_id );
|
||||
if ( ! empty( $user->user_nicename ) ) {
|
||||
$author_nicename = $user->user_nicename;
|
||||
}
|
||||
}
|
||||
$link = str_replace( '%author%', $author_nicename, $link );
|
||||
$link = home_url( user_trailingslashit( $link ) );
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function for generating site debug data when required.
|
||||
*
|
||||
* @param array $info The debug information to be added to the core information page.
|
||||
* @return array The filtered information
|
||||
*/
|
||||
public static function debug_information( $info ) {
|
||||
$info['activitypub'] = array(
|
||||
'label' => __( 'ActivityPub', 'activitypub' ),
|
||||
'fields' => array(
|
||||
'webfinger' => array(
|
||||
'label' => __( 'WebFinger Resource', 'activitypub' ),
|
||||
'value' => Webfinger::get_user_resource( wp_get_current_user()->ID ),
|
||||
'private' => true,
|
||||
),
|
||||
'author_url' => array(
|
||||
'label' => __( 'Author URL', 'activitypub' ),
|
||||
'value' => get_author_posts_url( wp_get_current_user()->ID ),
|
||||
'private' => true,
|
||||
),
|
||||
'plugin_version' => array(
|
||||
'label' => __( 'Plugin Version', 'activitypub' ),
|
||||
'value' => get_plugin_version(),
|
||||
'private' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $info;
|
||||
}
|
||||
}
|
@ -8,7 +8,7 @@
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
/**
|
||||
* ActivityPub HTTP Class
|
||||
@ -26,6 +26,13 @@ class Http {
|
||||
* @return array|WP_Error The POST Response or an WP_Error.
|
||||
*/
|
||||
public static function post( $url, $body, $user_id ) {
|
||||
/**
|
||||
* Fires before an HTTP POST request is made.
|
||||
*
|
||||
* @param string $url The URL endpoint.
|
||||
* @param string $body The POST body.
|
||||
* @param int $user_id The WordPress User ID.
|
||||
*/
|
||||
\do_action( 'activitypub_pre_http_post', $url, $body, $user_id );
|
||||
|
||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||
@ -35,7 +42,7 @@ class Http {
|
||||
$wp_version = get_masked_wp_version();
|
||||
|
||||
/**
|
||||
* Filter the HTTP headers user agent.
|
||||
* Filters the HTTP headers user agent string.
|
||||
*
|
||||
* @param string $user_agent The user agent string.
|
||||
*/
|
||||
@ -59,7 +66,14 @@ class Http {
|
||||
$code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( $code >= 400 ) {
|
||||
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
|
||||
$response = new WP_Error(
|
||||
$code,
|
||||
__( 'Failed HTTP Request', 'activitypub' ),
|
||||
array(
|
||||
'status' => $code,
|
||||
'response' => $response,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,6 +98,11 @@ class Http {
|
||||
* @return array|WP_Error The GET Response or a WP_Error.
|
||||
*/
|
||||
public static function get( $url, $cached = false ) {
|
||||
/**
|
||||
* Fires before an HTTP GET request is made.
|
||||
*
|
||||
* @param string $url The URL endpoint.
|
||||
*/
|
||||
\do_action( 'activitypub_pre_http_get', $url );
|
||||
|
||||
if ( $cached ) {
|
||||
@ -105,19 +124,29 @@ class Http {
|
||||
}
|
||||
|
||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||
$signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date );
|
||||
$signature = Signature::generate_signature( Actors::APPLICATION_USER_ID, 'get', $url, $date );
|
||||
|
||||
$wp_version = get_masked_wp_version();
|
||||
|
||||
/**
|
||||
* Filter the HTTP headers user agent.
|
||||
* Filters the HTTP headers user agent string.
|
||||
*
|
||||
* This filter allows developers to modify the user agent string that is
|
||||
* sent with HTTP requests.
|
||||
*
|
||||
* @param string $user_agent The user agent string.
|
||||
*/
|
||||
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
|
||||
|
||||
/**
|
||||
* Filters the timeout duration for remote GET requests in ActivityPub.
|
||||
*
|
||||
* @param int $timeout The timeout value in seconds. Default 100 seconds.
|
||||
*/
|
||||
$timeout = \apply_filters( 'activitypub_remote_get_timeout', 100 );
|
||||
|
||||
$args = array(
|
||||
'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ),
|
||||
'timeout' => $timeout,
|
||||
'limit_response_size' => 1048576,
|
||||
'redirection' => 3,
|
||||
'user-agent' => "$user_agent; ActivityPub",
|
||||
@ -164,19 +193,25 @@ class Http {
|
||||
*/
|
||||
public static function is_tombstone( $url ) {
|
||||
/**
|
||||
* Action before checking if the URL is a tombstone.
|
||||
* Fires before checking if the URL is a tombstone.
|
||||
*
|
||||
* @param string $url The URL to check.
|
||||
*/
|
||||
\do_action( 'activitypub_pre_http_is_tombstone', $url );
|
||||
|
||||
$response = \wp_safe_remote_get( $url );
|
||||
$response = \wp_safe_remote_get( $url, array( 'headers' => array( 'Accept' => 'application/activity+json' ) ) );
|
||||
$code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( in_array( (int) $code, array( 404, 410 ), true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$data = \wp_remote_retrieve_body( $response );
|
||||
$data = \json_decode( $data, true );
|
||||
if ( $data && isset( $data['type'] ) && 'Tombstone' === $data['type'] ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -200,24 +235,7 @@ class Http {
|
||||
* @return array|WP_Error The Object data as array or WP_Error on failure.
|
||||
*/
|
||||
public static function get_remote_object( $url_or_object, $cached = true ) {
|
||||
if ( is_array( $url_or_object ) ) {
|
||||
if ( array_key_exists( 'id', $url_or_object ) ) {
|
||||
$url = $url_or_object['id'];
|
||||
} elseif ( array_key_exists( 'url', $url_or_object ) ) {
|
||||
$url = $url_or_object['url'];
|
||||
} else {
|
||||
return new WP_Error(
|
||||
'activitypub_no_valid_actor_identifier',
|
||||
\__( 'The "actor" identifier is not valid', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
'object' => $url_or_object,
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$url = $url_or_object;
|
||||
}
|
||||
$url = object_to_uri( $url_or_object );
|
||||
|
||||
if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $url ) ) {
|
||||
$url = Webfinger::resolve( $url );
|
||||
|
@ -29,11 +29,11 @@ class Link {
|
||||
*/
|
||||
public static function filter_activity_object( $activity ) {
|
||||
/* phpcs:ignore Squiz.PHP.CommentedOutCode.Found
|
||||
Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629
|
||||
if ( ! empty( $activity['summary'] ) ) {
|
||||
Only changed it for Person and Group as long is not merged: https://github.com/mastodon/mastodon/pull/28629
|
||||
*/
|
||||
if ( ! empty( $activity['summary'] ) && in_array( $activity['type'], array( 'Person', 'Group' ), true ) ) {
|
||||
$activity['summary'] = self::the_content( $activity['summary'] );
|
||||
}
|
||||
*/
|
||||
|
||||
if ( ! empty( $activity['content'] ) ) {
|
||||
$activity['content'] = self::the_content( $activity['content'] );
|
||||
@ -112,6 +112,11 @@ class Link {
|
||||
$display_class .= 'ellipsis';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the rel attribute for ActivityPub links.
|
||||
*
|
||||
* @param string $rel The rel attribute string. Default 'nofollow noopener noreferrer'.
|
||||
*/
|
||||
$rel = apply_filters( 'activitypub_link_rel', 'nofollow noopener noreferrer' );
|
||||
|
||||
return \sprintf(
|
||||
|
337
wp-content/plugins/activitypub/includes/class-mailer.php
Normal file
337
wp-content/plugins/activitypub/includes/class-mailer.php
Normal file
@ -0,0 +1,337 @@
|
||||
<?php
|
||||
/**
|
||||
* Mailer Class.
|
||||
*
|
||||
* @package ActivityPub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
/**
|
||||
* Mailer Class.
|
||||
*/
|
||||
class Mailer {
|
||||
/**
|
||||
* Initialize the Mailer.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'comment_notification_subject', array( self::class, 'comment_notification_subject' ), 10, 2 );
|
||||
\add_filter( 'comment_notification_text', array( self::class, 'comment_notification_text' ), 10, 2 );
|
||||
|
||||
\add_action( 'activitypub_inbox_follow', array( self::class, 'new_follower' ), 10, 2 );
|
||||
\add_action( 'activitypub_inbox_create', array( self::class, 'direct_message' ), 10, 2 );
|
||||
\add_action( 'activitypub_inbox_create', array( self::class, 'mention' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the subject line for Like and Announce notifications.
|
||||
*
|
||||
* @param string $subject The default subject line.
|
||||
* @param int|string $comment_id The comment ID.
|
||||
*
|
||||
* @return string The filtered subject line.
|
||||
*/
|
||||
public static function comment_notification_subject( $subject, $comment_id ) {
|
||||
$comment = \get_comment( $comment_id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return $subject;
|
||||
}
|
||||
|
||||
$type = \get_comment_meta( $comment->comment_ID, 'protocol', true );
|
||||
|
||||
if ( 'activitypub' !== $type ) {
|
||||
return $subject;
|
||||
}
|
||||
|
||||
$singular = Comment::get_comment_type_attr( $comment->comment_type, 'singular' );
|
||||
|
||||
if ( ! $singular ) {
|
||||
return $subject;
|
||||
}
|
||||
|
||||
$post = \get_post( $comment->comment_post_ID );
|
||||
|
||||
/* translators: 1: Blog name, 2: Like or Repost, 3: Post title */
|
||||
return \sprintf( \esc_html__( '[%1$s] %2$s: %3$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $singular ), \esc_html( $post->post_title ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the notification text for Like and Announce notifications.
|
||||
*
|
||||
* @param string $message The default notification text.
|
||||
* @param int|string $comment_id The comment ID.
|
||||
*
|
||||
* @return string The filtered notification text.
|
||||
*/
|
||||
public static function comment_notification_text( $message, $comment_id ) {
|
||||
$comment = \get_comment( $comment_id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
$type = \get_comment_meta( $comment->comment_ID, 'protocol', true );
|
||||
|
||||
if ( 'activitypub' !== $type ) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
$comment_type = Comment::get_comment_type( $comment->comment_type );
|
||||
|
||||
if ( ! $comment_type ) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
$post = \get_post( $comment->comment_post_ID );
|
||||
$comment_author_domain = \gethostbyaddr( $comment->comment_author_IP );
|
||||
|
||||
/* translators: 1: Comment type, 2: Post title */
|
||||
$notify_message = \sprintf( html_entity_decode( esc_html__( 'New %1$s on your post “%2$s”.', 'activitypub' ) ), \esc_html( $comment_type['singular'] ), \esc_html( $post->post_title ) ) . "\r\n\r\n";
|
||||
/* translators: 1: Website name, 2: Website IP address, 3: Website hostname. */
|
||||
$notify_message .= \sprintf( \esc_html__( 'From: %1$s (IP address: %2$s, %3$s)', 'activitypub' ), \esc_html( $comment->comment_author ), \esc_html( $comment->comment_author_IP ), \esc_html( $comment_author_domain ) ) . "\r\n";
|
||||
/* translators: Reaction author URL. */
|
||||
$notify_message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $comment->comment_author_url ) ) . "\r\n\r\n";
|
||||
/* translators: Comment type label */
|
||||
$notify_message .= \sprintf( \esc_html__( 'You can see all %s on this post here:', 'activitypub' ), \esc_html( $comment_type['label'] ) ) . "\r\n";
|
||||
$notify_message .= \get_permalink( $comment->comment_post_ID ) . '#' . \esc_attr( $comment_type['type'] ) . "\r\n\r\n";
|
||||
|
||||
return $notify_message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification email for every new follower.
|
||||
*
|
||||
* @param array $activity The activity object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
*/
|
||||
public static function new_follower( $activity, $user_id ) {
|
||||
if ( $user_id > Actors::BLOG_USER_ID ) {
|
||||
if ( ! \get_user_option( 'activitypub_mailer_new_follower', $user_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_userdata( $user_id )->user_email;
|
||||
$admin_url = '/users.php?page=activitypub-followers-list';
|
||||
} else {
|
||||
if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_follower', '1' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_option( 'admin_email' );
|
||||
$admin_url = '/options-general.php?page=activitypub&tab=followers';
|
||||
}
|
||||
|
||||
$actor = get_remote_metadata_by_actor( $activity['actor'] );
|
||||
if ( ! $actor || \is_wp_error( $actor ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( empty( $actor['webfinger'] ) ) {
|
||||
$actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST );
|
||||
}
|
||||
|
||||
$template_args = array_merge(
|
||||
$actor,
|
||||
array(
|
||||
'admin_url' => $admin_url,
|
||||
'user_id' => $user_id,
|
||||
'stats' => array(
|
||||
'outbox' => null,
|
||||
'followers' => null,
|
||||
'following' => null,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $template_args['stats'] as $field => $value ) {
|
||||
if ( empty( $actor[ $field ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = Http::get( $actor[ $field ], true );
|
||||
if ( 200 === \wp_remote_retrieve_response_code( $result ) ) {
|
||||
$body = \json_decode( \wp_remote_retrieve_body( $result ), true );
|
||||
if ( isset( $body['totalItems'] ) ) {
|
||||
$template_args['stats'][ $field ] = $body['totalItems'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* translators: 1: Blog name, 2: Follower name */
|
||||
$subject = \sprintf( \__( '[%1$s] New Follower: %2$s', 'activitypub' ), \get_option( 'blogname' ), $actor['name'] );
|
||||
|
||||
\ob_start();
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-follower.php', false, $template_args );
|
||||
$html_message = \ob_get_clean();
|
||||
|
||||
$alt_function = function ( $mailer ) use ( $actor, $admin_url ) {
|
||||
/* translators: 1: Follower name */
|
||||
$message = \sprintf( \__( 'New Follower: %1$s.', 'activitypub' ), $actor['name'] ) . "\r\n\r\n";
|
||||
/* translators: Follower URL */
|
||||
$message .= \sprintf( \__( 'URL: %s', 'activitypub' ), \esc_url( $actor['url'] ) ) . "\r\n\r\n";
|
||||
$message .= \__( 'You can see all followers here:', 'activitypub' ) . "\r\n";
|
||||
$message .= \esc_url( \admin_url( $admin_url ) ) . "\r\n\r\n";
|
||||
$mailer->{'AltBody'} = $message;
|
||||
};
|
||||
\add_action( 'phpmailer_init', $alt_function );
|
||||
|
||||
\wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
|
||||
|
||||
\remove_action( 'phpmailer_init', $alt_function );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a direct message.
|
||||
*
|
||||
* @param array $activity The activity object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
*/
|
||||
public static function direct_message( $activity, $user_id ) {
|
||||
if (
|
||||
is_activity_public( $activity ) ||
|
||||
// Only accept messages that have the user in the "to" field.
|
||||
empty( $activity['to'] ) ||
|
||||
! in_array( Actors::get_by_id( $user_id )->get_id(), (array) $activity['to'], true )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $user_id > Actors::BLOG_USER_ID ) {
|
||||
if ( ! \get_user_option( 'activitypub_mailer_new_dm', $user_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_userdata( $user_id )->user_email;
|
||||
} else {
|
||||
if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_dm', '1' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_option( 'admin_email' );
|
||||
}
|
||||
|
||||
$actor = get_remote_metadata_by_actor( $activity['actor'] );
|
||||
|
||||
if ( ! $actor || \is_wp_error( $actor ) || empty( $activity['object']['content'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( empty( $actor['webfinger'] ) ) {
|
||||
$actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST );
|
||||
}
|
||||
|
||||
$template_args = array(
|
||||
'activity' => $activity,
|
||||
'actor' => $actor,
|
||||
'user_id' => $user_id,
|
||||
);
|
||||
|
||||
/* translators: 1: Blog name, 2 Actor name */
|
||||
$subject = \sprintf( \esc_html__( '[%1$s] Direct Message from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
|
||||
|
||||
\ob_start();
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-dm.php', false, $template_args );
|
||||
$html_message = \ob_get_clean();
|
||||
|
||||
$alt_function = function ( $mailer ) use ( $actor, $activity ) {
|
||||
$content = \html_entity_decode(
|
||||
\wp_strip_all_tags(
|
||||
str_replace( '</p>', PHP_EOL . PHP_EOL, $activity['object']['content'] )
|
||||
),
|
||||
ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
|
||||
);
|
||||
|
||||
/* translators: Actor name */
|
||||
$message = \sprintf( \esc_html__( 'New Direct Message: %s', 'activitypub' ), $content ) . "\r\n\r\n";
|
||||
/* translators: Actor name */
|
||||
$message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
|
||||
/* translators: Message URL */
|
||||
$message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $activity['object']['id'] ) ) . "\r\n\r\n";
|
||||
|
||||
$mailer->{'AltBody'} = $message;
|
||||
};
|
||||
\add_action( 'phpmailer_init', $alt_function );
|
||||
|
||||
\wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
|
||||
|
||||
\remove_action( 'phpmailer_init', $alt_function );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a mention notification.
|
||||
*
|
||||
* @param array $activity The activity object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
*/
|
||||
public static function mention( $activity, $user_id ) {
|
||||
if (
|
||||
// Only accept messages that have the user in the "cc" field.
|
||||
empty( $activity['cc'] ) ||
|
||||
! in_array( Actors::get_by_id( $user_id )->get_id(), (array) $activity['cc'], true )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $user_id > Actors::BLOG_USER_ID ) {
|
||||
if ( ! \get_user_option( 'activitypub_mailer_new_mention', $user_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_userdata( $user_id )->user_email;
|
||||
} else {
|
||||
if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_mention', '1' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_option( 'admin_email' );
|
||||
}
|
||||
|
||||
$actor = get_remote_metadata_by_actor( $activity['actor'] );
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( empty( $actor['webfinger'] ) ) {
|
||||
$actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST );
|
||||
}
|
||||
|
||||
$template_args = array(
|
||||
'activity' => $activity,
|
||||
'actor' => $actor,
|
||||
'user_id' => $user_id,
|
||||
);
|
||||
|
||||
/* translators: 1: Blog name, 2 Actor name */
|
||||
$subject = \sprintf( \esc_html__( '[%1$s] Mention from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
|
||||
|
||||
\ob_start();
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-mention.php', false, $template_args );
|
||||
$html_message = \ob_get_clean();
|
||||
|
||||
$alt_function = function ( $mailer ) use ( $actor, $activity ) {
|
||||
$content = \html_entity_decode(
|
||||
\wp_strip_all_tags(
|
||||
str_replace( '</p>', PHP_EOL . PHP_EOL, $activity['object']['content'] )
|
||||
),
|
||||
ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
|
||||
);
|
||||
|
||||
/* translators: Message content */
|
||||
$message = \sprintf( \esc_html__( 'New Mention: %s', 'activitypub' ), $content ) . "\r\n\r\n";
|
||||
/* translators: Actor name */
|
||||
$message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
|
||||
/* translators: Message URL */
|
||||
$message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $activity['object']['id'] ) ) . "\r\n\r\n";
|
||||
|
||||
$mailer->{'AltBody'} = $message;
|
||||
};
|
||||
\add_action( 'phpmailer_init', $alt_function );
|
||||
|
||||
\wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
|
||||
|
||||
\remove_action( 'phpmailer_init', $alt_function );
|
||||
}
|
||||
}
|
@ -82,7 +82,7 @@ class Mention {
|
||||
|
||||
$url = isset( $metadata['url'] ) ? object_to_uri( $metadata['url'] ) : object_to_uri( $metadata['id'] );
|
||||
|
||||
return \sprintf( '<a rel="mention" class="u-url mention" href="%s">@<span>%s</span></a>', esc_url( $url ), esc_html( $username ) );
|
||||
return \sprintf( '<a rel="mention" class="u-url mention" href="%1$s">@%2$s</a>', esc_url( $url ), esc_html( $username ) );
|
||||
}
|
||||
|
||||
return $result[0];
|
||||
|
@ -7,7 +7,11 @@
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Extra_Fields;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Outbox;
|
||||
use Activitypub\Transformer\Factory;
|
||||
|
||||
/**
|
||||
* ActivityPub Migration Class
|
||||
@ -20,6 +24,8 @@ class Migration {
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) );
|
||||
\add_action( 'activitypub_upgrade', array( self::class, 'async_upgrade' ), 10, 99 );
|
||||
\add_action( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ), 10, 2 );
|
||||
|
||||
self::maybe_migrate();
|
||||
}
|
||||
@ -30,10 +36,14 @@ class Migration {
|
||||
* This is the version that the database structure will be updated to.
|
||||
* It is the same as the plugin version.
|
||||
*
|
||||
* @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly.
|
||||
*
|
||||
* @return string The target version.
|
||||
*/
|
||||
public static function get_target_version() {
|
||||
return get_plugin_version();
|
||||
_deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' );
|
||||
|
||||
return ACTIVITYPUB_PLUGIN_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -47,9 +57,20 @@ class Migration {
|
||||
|
||||
/**
|
||||
* Locks the database migration process to prevent simultaneous migrations.
|
||||
*
|
||||
* @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
|
||||
*/
|
||||
public static function lock() {
|
||||
\update_option( 'activitypub_migration_lock', \time() );
|
||||
global $wpdb;
|
||||
|
||||
// Try to lock.
|
||||
$lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_migration_lock', \time() ) ); // phpcs:ignore WordPress.DB
|
||||
|
||||
if ( ! $lock_result ) {
|
||||
$lock_result = \get_option( 'activitypub_migration_lock' );
|
||||
}
|
||||
|
||||
return $lock_result;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -87,9 +108,9 @@ class Migration {
|
||||
* @return bool True if the database structure is up to date, false otherwise.
|
||||
*/
|
||||
public static function is_latest_version() {
|
||||
return (bool) version_compare(
|
||||
return (bool) \version_compare(
|
||||
self::get_version(),
|
||||
self::get_target_version(),
|
||||
ACTIVITYPUB_PLUGIN_VERSION,
|
||||
'=='
|
||||
);
|
||||
}
|
||||
@ -110,33 +131,91 @@ class Migration {
|
||||
|
||||
$version_from_db = self::get_version();
|
||||
|
||||
// Check for inital migration.
|
||||
// Check for initial migration.
|
||||
if ( ! $version_from_db ) {
|
||||
self::add_default_settings();
|
||||
$version_from_db = self::get_target_version();
|
||||
$version_from_db = ACTIVITYPUB_PLUGIN_VERSION;
|
||||
}
|
||||
|
||||
// Schedule the async migration.
|
||||
if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) {
|
||||
\wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) );
|
||||
}
|
||||
if ( version_compare( $version_from_db, '0.17.0', '<' ) ) {
|
||||
if ( \version_compare( $version_from_db, '0.17.0', '<' ) ) {
|
||||
self::migrate_from_0_16();
|
||||
}
|
||||
if ( version_compare( $version_from_db, '1.3.0', '<' ) ) {
|
||||
if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) {
|
||||
self::migrate_from_1_2_0();
|
||||
}
|
||||
if ( version_compare( $version_from_db, '2.1.0', '<' ) ) {
|
||||
if ( \version_compare( $version_from_db, '2.1.0', '<' ) ) {
|
||||
self::migrate_from_2_0_0();
|
||||
}
|
||||
if ( version_compare( $version_from_db, '2.3.0', '<' ) ) {
|
||||
if ( \version_compare( $version_from_db, '2.3.0', '<' ) ) {
|
||||
self::migrate_from_2_2_0();
|
||||
}
|
||||
if ( version_compare( $version_from_db, '3.0.0', '<' ) ) {
|
||||
if ( \version_compare( $version_from_db, '3.0.0', '<' ) ) {
|
||||
self::migrate_from_2_6_0();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.0.0', '<' ) ) {
|
||||
self::migrate_to_4_0_0();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.1.0', '<' ) ) {
|
||||
self::migrate_to_4_1_0();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.5.0', '<' ) ) {
|
||||
\wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_update_comment_counts' );
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.7.1', '<' ) ) {
|
||||
self::migrate_to_4_7_1();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.7.2', '<' ) ) {
|
||||
self::migrate_to_4_7_2();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.7.3', '<' ) ) {
|
||||
add_action( 'init', 'flush_rewrite_rules', 20 );
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) {
|
||||
Scheduler::register_schedules();
|
||||
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'create_post_outbox_items' ) );
|
||||
\wp_schedule_single_event( \time() + 15, 'activitypub_upgrade', array( 'create_comment_outbox_items' ) );
|
||||
add_action( 'init', 'flush_rewrite_rules', 20 );
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) {
|
||||
Scheduler::register_schedules();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '5.4.0', '<' ) ) {
|
||||
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) );
|
||||
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) );
|
||||
\add_action( 'init', 'flush_rewrite_rules', 20 );
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '5.7.0', '<' ) ) {
|
||||
self::delete_mastodon_api_orphaned_extra_fields();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '5.8.0', '<' ) ) {
|
||||
self::update_notification_options();
|
||||
}
|
||||
|
||||
update_option( 'activitypub_db_version', self::get_target_version() );
|
||||
/*
|
||||
* Add new update routines above this comment. ^
|
||||
*
|
||||
* Use 'unreleased' as the version number for new migrations and add tests for the callback directly.
|
||||
* The release script will automatically replace it with the actual version number.
|
||||
* Example:
|
||||
*
|
||||
* if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
|
||||
* // Update routine.
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when the system has to be migrated.
|
||||
*
|
||||
* @param string $version_from_db The version from which to migrate.
|
||||
* @param string $target_version The target version to migrate to.
|
||||
*/
|
||||
\do_action( 'activitypub_migrate', $version_from_db, ACTIVITYPUB_PLUGIN_VERSION );
|
||||
|
||||
\update_option( 'activitypub_db_version', ACTIVITYPUB_PLUGIN_VERSION );
|
||||
|
||||
self::unlock();
|
||||
}
|
||||
@ -147,11 +226,43 @@ class Migration {
|
||||
* @param string $version_from_db The version from which to migrate.
|
||||
*/
|
||||
public static function async_migration( $version_from_db ) {
|
||||
if ( version_compare( $version_from_db, '1.0.0', '<' ) ) {
|
||||
if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) {
|
||||
self::migrate_from_0_17();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously runs upgrade routines.
|
||||
*
|
||||
* @param callable $callback Callable upgrade routine. Must be a method of this class.
|
||||
* @params mixed ...$args Optional. Parameters that get passed to the callback.
|
||||
*/
|
||||
public static function async_upgrade( $callback ) {
|
||||
$args = \func_get_args();
|
||||
|
||||
// Bail if the existing lock is still valid.
|
||||
if ( self::is_locked() ) {
|
||||
\wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_upgrade', $args );
|
||||
return;
|
||||
}
|
||||
|
||||
self::lock();
|
||||
|
||||
$callback = array_shift( $args ); // Remove $callback from arguments.
|
||||
$next = \call_user_func_array( array( self::class, $callback ), $args );
|
||||
|
||||
self::unlock();
|
||||
|
||||
if ( ! empty( $next ) ) {
|
||||
// Schedule the next run, adding the result to the arguments.
|
||||
\wp_schedule_single_event(
|
||||
\time() + 30,
|
||||
'activitypub_upgrade',
|
||||
\array_merge( array( $callback ), \array_values( $next ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the custom template to use shortcodes instead of the deprecated templates.
|
||||
*/
|
||||
@ -262,6 +373,369 @@ class Migration {
|
||||
self::update_options_key( 'activitypub_blog_user_identifier', 'activitypub_blog_identifier' );
|
||||
}
|
||||
|
||||
/**
|
||||
* * Update actor-mode settings.
|
||||
* * Get the ID of the latest blog post and save it to the options table.
|
||||
*/
|
||||
private static function migrate_to_4_0_0() {
|
||||
$latest_post_id = 0;
|
||||
|
||||
// Get the ID of the latest blog post and save it to the options table.
|
||||
$latest_post = get_posts(
|
||||
array(
|
||||
'numberposts' => 1,
|
||||
'orderby' => 'ID',
|
||||
'order' => 'DESC',
|
||||
'post_type' => 'any',
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
|
||||
if ( $latest_post ) {
|
||||
$latest_post_id = $latest_post[0]->ID;
|
||||
}
|
||||
|
||||
\update_option( 'activitypub_last_post_with_permalink_as_id', $latest_post_id );
|
||||
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$followers = Followers::get_followers( $user->ID );
|
||||
|
||||
if ( $followers ) {
|
||||
\update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' );
|
||||
}
|
||||
}
|
||||
|
||||
$followers = Followers::get_followers( Actors::BLOG_USER_ID );
|
||||
|
||||
if ( $followers ) {
|
||||
\update_option( 'activitypub_use_permalink_as_id_for_blog', '1' );
|
||||
}
|
||||
|
||||
self::migrate_actor_mode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Upate to 4.1.0
|
||||
*
|
||||
* * Migrate the `activitypub_post_content_type` to only use `activitypub_custom_post_content`.
|
||||
*/
|
||||
public static function migrate_to_4_1_0() {
|
||||
$content_type = \get_option( 'activitypub_post_content_type' );
|
||||
|
||||
switch ( $content_type ) {
|
||||
case 'excerpt':
|
||||
$template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
|
||||
break;
|
||||
case 'title':
|
||||
$template = "[ap_title type=\"html\"]\n\n[ap_permalink type=\"html\"]";
|
||||
break;
|
||||
case 'content':
|
||||
$template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
|
||||
break;
|
||||
case 'custom':
|
||||
$template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
|
||||
break;
|
||||
default:
|
||||
$template = ACTIVITYPUB_CUSTOM_POST_CONTENT;
|
||||
break;
|
||||
}
|
||||
|
||||
\update_option( 'activitypub_custom_post_content', $template );
|
||||
|
||||
\delete_option( 'activitypub_post_content_type' );
|
||||
|
||||
$object_type = \get_option( 'activitypub_object_type', false );
|
||||
if ( ! $object_type ) {
|
||||
\update_option( 'activitypub_object_type', 'note' );
|
||||
}
|
||||
|
||||
// Clean up empty visibility meta.
|
||||
global $wpdb;
|
||||
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
"DELETE FROM $wpdb->postmeta
|
||||
WHERE meta_key = 'activitypub_content_visibility'
|
||||
AND (meta_value IS NULL OR meta_value = '')"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates post meta keys to be prefixed with an underscore.
|
||||
*/
|
||||
public static function migrate_to_4_7_1() {
|
||||
global $wpdb;
|
||||
|
||||
$meta_keys = array(
|
||||
'activitypub_actor_json',
|
||||
'activitypub_canonical_url',
|
||||
'activitypub_errors',
|
||||
'activitypub_inbox',
|
||||
'activitypub_user_id',
|
||||
);
|
||||
|
||||
foreach ( $meta_keys as $meta_key ) {
|
||||
// phpcs:ignore WordPress.DB
|
||||
$wpdb->update( $wpdb->postmeta, array( 'meta_key' => '_' . $meta_key ), array( 'meta_key' => $meta_key ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the post cache for Followers, we should have done this in 4.7.1 when we renamed those keys.
|
||||
*/
|
||||
public static function migrate_to_4_7_2() {
|
||||
global $wpdb;
|
||||
// phpcs:ignore WordPress.DB
|
||||
$followers = $wpdb->get_col(
|
||||
$wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Followers::POST_TYPE )
|
||||
);
|
||||
foreach ( $followers as $id ) {
|
||||
clean_post_cache( $id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update comment counts for posts in batches.
|
||||
*
|
||||
* @see Comment::pre_wp_update_comment_count_now()
|
||||
* @param int $batch_size Optional. Number of posts to process per batch. Default 100.
|
||||
* @param int $offset Optional. Number of posts to skip. Default 0.
|
||||
*/
|
||||
public static function update_comment_counts( $batch_size = 100, $offset = 0 ) {
|
||||
global $wpdb;
|
||||
|
||||
// Bail if the existing lock is still valid.
|
||||
if ( self::is_locked() ) {
|
||||
\wp_schedule_single_event(
|
||||
time() + ( 5 * MINUTE_IN_SECONDS ),
|
||||
'activitypub_update_comment_counts',
|
||||
array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset,
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
self::lock();
|
||||
|
||||
Comment::register_comment_types();
|
||||
$comment_types = Comment::get_comment_type_slugs();
|
||||
$type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')";
|
||||
|
||||
// Get and process this batch.
|
||||
$post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB
|
||||
$wpdb->prepare(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
"SELECT DISTINCT comment_post_ID FROM {$wpdb->comments} WHERE comment_approved = '1' {$type_inclusion} ORDER BY comment_post_ID LIMIT %d OFFSET %d",
|
||||
$batch_size,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
\wp_update_comment_count_now( $post_id );
|
||||
}
|
||||
|
||||
if ( count( $post_ids ) === $batch_size ) {
|
||||
// Schedule next batch.
|
||||
\wp_schedule_single_event(
|
||||
time() + MINUTE_IN_SECONDS,
|
||||
'activitypub_update_comment_counts',
|
||||
array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset + $batch_size,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
self::unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create outbox items for posts in batches.
|
||||
*
|
||||
* @param int $batch_size Optional. Number of posts to process per batch. Default 50.
|
||||
* @param int $offset Optional. Number of posts to skip. Default 0.
|
||||
* @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
|
||||
*/
|
||||
public static function create_post_outbox_items( $batch_size = 50, $offset = 0 ) {
|
||||
$posts = \get_posts(
|
||||
array(
|
||||
// our own `ap_outbox` will be excluded from `any` by virtue of its `exclude_from_search` arg.
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => $batch_size,
|
||||
'offset' => $offset,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'activitypub_status',
|
||||
'value' => 'federated',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Avoid multiple queries for post meta.
|
||||
\update_postmeta_cache( \wp_list_pluck( $posts, 'ID' ) );
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
|
||||
|
||||
self::add_to_outbox( $post, 'Create', $post->post_author, $visibility );
|
||||
|
||||
// Add Update activity when the post has been modified.
|
||||
if ( $post->post_modified !== $post->post_date ) {
|
||||
self::add_to_outbox( $post, 'Update', $post->post_author, $visibility );
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $posts ) === $batch_size ) {
|
||||
return array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset + $batch_size,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create outbox items for comments in batches.
|
||||
*
|
||||
* @param int $batch_size Optional. Number of posts to process per batch. Default 50.
|
||||
* @param int $offset Optional. Number of posts to skip. Default 0.
|
||||
* @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
|
||||
*/
|
||||
public static function create_comment_outbox_items( $batch_size = 50, $offset = 0 ) {
|
||||
$comments = \get_comments(
|
||||
array(
|
||||
'author__not_in' => array( 0 ), // Limit to comments by registered users.
|
||||
'number' => $batch_size,
|
||||
'offset' => $offset,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'activitypub_status',
|
||||
'value' => 'federated',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $comments as $comment ) {
|
||||
self::add_to_outbox( $comment, 'Create', $comment->user_id );
|
||||
}
|
||||
|
||||
if ( count( $comments ) === $batch_size ) {
|
||||
return array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset + $batch_size,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update _activitypub_actor_json meta values to ensure they are properly slashed.
|
||||
*
|
||||
* @param int $batch_size Optional. Number of meta values to process per batch. Default 100.
|
||||
* @param int $offset Optional. Number of meta values to skip. Default 0.
|
||||
* @return array|null Array with batch size and offset if there are more meta values to process, null otherwise.
|
||||
*/
|
||||
public static function update_actor_json_slashing( $batch_size = 100, $offset = 0 ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$meta_values = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d OFFSET %d",
|
||||
$batch_size,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $meta_values as $meta ) {
|
||||
$json = \json_decode( $meta->meta_value, true );
|
||||
|
||||
// If json_decode fails, try adding slashes.
|
||||
if ( null === $json && \json_last_error() !== JSON_ERROR_NONE ) {
|
||||
$escaped_value = \preg_replace( '#\\\\(?!["\\\\/bfnrtu])#', '\\\\\\\\', $meta->meta_value );
|
||||
$json = \json_decode( $escaped_value, true );
|
||||
|
||||
// Update the meta if json_decode succeeds with slashes.
|
||||
if ( null !== $json && \json_last_error() === JSON_ERROR_NONE ) {
|
||||
\update_post_meta( $meta->post_id, '_activitypub_actor_json', \wp_slash( $escaped_value ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( \count( $meta_values ) === $batch_size ) {
|
||||
return array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset + $batch_size,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update comment author emails with webfinger addresses for ActivityPub comments.
|
||||
*
|
||||
* @param int $batch_size Optional. Number of comments to process per batch. Default 50.
|
||||
* @param int $offset Optional. Number of comments to skip. Default 0.
|
||||
* @return array|null Array with batch size and offset if there are more comments to process, null otherwise.
|
||||
*/
|
||||
public static function update_comment_author_emails( $batch_size = 50, $offset = 0 ) {
|
||||
$comments = \get_comments(
|
||||
array(
|
||||
'number' => $batch_size,
|
||||
'offset' => $offset,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'protocol',
|
||||
'value' => 'activitypub',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $comments as $comment ) {
|
||||
$comment_author_url = $comment->comment_author_url;
|
||||
if ( empty( $comment_author_url ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$webfinger = Webfinger::uri_to_acct( $comment_author_url );
|
||||
if ( \is_wp_error( $webfinger ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
\wp_update_comment(
|
||||
array(
|
||||
'comment_ID' => $comment->comment_ID,
|
||||
'comment_author_email' => \str_replace( 'acct:', '', $webfinger ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( count( $comments ) === $batch_size ) {
|
||||
return array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset + $batch_size,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the defaults needed for the plugin to work.
|
||||
*
|
||||
@ -269,6 +743,41 @@ class Migration {
|
||||
*/
|
||||
public static function add_default_settings() {
|
||||
self::add_activitypub_capability();
|
||||
self::add_default_extra_field();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an activity to the outbox without federating it.
|
||||
*
|
||||
* @param \WP_Post|\WP_Comment $comment The comment or post object.
|
||||
* @param string $activity_type The type of activity.
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $visibility Optional. The visibility of the content. Default 'public'.
|
||||
*/
|
||||
private static function add_to_outbox( $comment, $activity_type, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) {
|
||||
$transformer = Factory::get_transformer( $comment );
|
||||
if ( ! $transformer || \is_wp_error( $transformer ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$activity = $transformer->to_activity( $activity_type );
|
||||
if ( ! $activity || \is_wp_error( $activity ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user is disabled, fall back to the blog user when available.
|
||||
if ( ! user_can_activitypub( $user_id ) ) {
|
||||
if ( user_can_activitypub( Actors::BLOG_USER_ID ) ) {
|
||||
$user_id = Actors::BLOG_USER_ID;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$post_id = Outbox::add( $activity, $user_id, $visibility );
|
||||
|
||||
// Immediately set to publish, no federation needed.
|
||||
\wp_publish_post( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -288,6 +797,43 @@ class Migration {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a default extra field for the user.
|
||||
*/
|
||||
private static function add_default_extra_field() {
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
|
||||
$title = \__( 'Powered by', 'activitypub' );
|
||||
$content = 'WordPress';
|
||||
|
||||
// Add a default extra field for each user.
|
||||
foreach ( $users as $user ) {
|
||||
\wp_insert_post(
|
||||
array(
|
||||
'post_type' => Extra_Fields::USER_POST_TYPE,
|
||||
'post_author' => $user->ID,
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $title,
|
||||
'post_content' => $content,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
\wp_insert_post(
|
||||
array(
|
||||
'post_type' => Extra_Fields::BLOG_POST_TYPE,
|
||||
'post_author' => 0,
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $title,
|
||||
'post_content' => $content,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename meta keys.
|
||||
*
|
||||
@ -323,4 +869,76 @@ class Migration {
|
||||
array( '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the actor mode settings.
|
||||
*/
|
||||
public static function migrate_actor_mode() {
|
||||
$blog_profile = \get_option( 'activitypub_enable_blog_user', '0' );
|
||||
$author_profiles = \get_option( 'activitypub_enable_users', '1' );
|
||||
|
||||
if (
|
||||
'1' === $blog_profile &&
|
||||
'1' === $author_profiles
|
||||
) {
|
||||
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE );
|
||||
} elseif (
|
||||
'1' === $blog_profile &&
|
||||
'1' !== $author_profiles
|
||||
) {
|
||||
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE );
|
||||
} elseif (
|
||||
'1' !== $blog_profile &&
|
||||
'1' === $author_profiles
|
||||
) {
|
||||
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes user extra fields where the author is the blog user.
|
||||
*
|
||||
* These extra fields were created when the Enable Mastodon Apps integration passed
|
||||
* an author_url instead of a user_id to the mastodon_api_account filter. This caused
|
||||
* Extra_Fields::default_actor_extra_fields() to run but fail to cache the fact it ran
|
||||
* for non-existent users. The result is a number of user extra fields with no author.
|
||||
*
|
||||
* @ticket https://github.com/Automattic/wordpress-activitypub/pull/1554
|
||||
*/
|
||||
public static function delete_mastodon_api_orphaned_extra_fields() {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$wpdb->delete(
|
||||
$wpdb->posts,
|
||||
array(
|
||||
'post_type' => Extra_Fields::USER_POST_TYPE,
|
||||
'post_author' => Actors::BLOG_USER_ID,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notification options.
|
||||
*/
|
||||
public static function update_notification_options() {
|
||||
$new_dm = \get_option( 'activitypub_mailer_new_dm', '1' );
|
||||
$new_follower = \get_option( 'activitypub_mailer_new_follower', '1' );
|
||||
|
||||
// Add the blog user notification options.
|
||||
\add_option( 'activitypub_blog_user_mailer_new_dm', $new_dm );
|
||||
\add_option( 'activitypub_blog_user_mailer_new_follower', $new_follower );
|
||||
\add_option( 'activitypub_blog_user_mailer_new_mention', '1' );
|
||||
|
||||
// Add the actor notification options.
|
||||
foreach ( Actors::get_collection() as $actor ) {
|
||||
\update_user_option( $actor->get__id(), 'activitypub_mailer_new_dm', $new_dm );
|
||||
\update_user_option( $actor->get__id(), 'activitypub_mailer_new_follower', $new_follower );
|
||||
\update_user_option( $actor->get__id(), 'activitypub_mailer_new_mention', '1' );
|
||||
}
|
||||
|
||||
// Delete the old notification options.
|
||||
\delete_option( 'activitypub_mailer_new_dm' );
|
||||
\delete_option( 'activitypub_mailer_new_follower' );
|
||||
}
|
||||
}
|
||||
|
313
wp-content/plugins/activitypub/includes/class-move.php
Normal file
313
wp-content/plugins/activitypub/includes/class-move.php
Normal file
@ -0,0 +1,313 @@
|
||||
<?php
|
||||
/**
|
||||
* Move class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Model\User;
|
||||
|
||||
/**
|
||||
* ActivityPub (Account) Move Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Move {
|
||||
|
||||
/**
|
||||
* Initialize the Move class.
|
||||
*/
|
||||
public static function init() {
|
||||
/**
|
||||
* Filter to enable automatically moving Fediverse accounts when the domain changes.
|
||||
*
|
||||
* @param bool $domain_moves_enabled Whether domain moves are enabled.
|
||||
*/
|
||||
$domain_moves_enabled = apply_filters( 'activitypub_enable_primary_domain_moves', false );
|
||||
|
||||
if ( $domain_moves_enabled ) {
|
||||
// Add the filter to change the domain.
|
||||
\add_filter( 'update_option_home', array( self::class, 'change_domain' ), 10, 2 );
|
||||
|
||||
if ( get_option( 'activitypub_old_host' ) ) {
|
||||
\add_action( 'activitypub_construct_model_actor', array( self::class, 'maybe_initiate_old_user' ) );
|
||||
\add_action( 'activitypub_pre_send_to_inboxes', array( self::class, 'pre_send_to_inboxes' ) );
|
||||
|
||||
if ( ! is_user_type_disabled( 'blog' ) ) {
|
||||
\add_filter( 'activitypub_pre_get_by_username', array( self::class, 'old_blog_username' ), 10, 2 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an ActivityPub account from one location to another.
|
||||
*
|
||||
* @param string $from The current account URL.
|
||||
* @param string $to The new account URL.
|
||||
*
|
||||
* @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
|
||||
*/
|
||||
public static function account( $from, $to ) {
|
||||
if ( is_same_domain( $from ) && is_same_domain( $to ) ) {
|
||||
return self::internally( $from, $to );
|
||||
}
|
||||
|
||||
return self::externally( $from, $to );
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an ActivityPub Actor from one location (internal) to another (external).
|
||||
*
|
||||
* This helps migrating local profiles to a new external profile:
|
||||
*
|
||||
* `Move::externally( 'https://example.com/?author=123', 'https://mastodon.example/users/foo' );`
|
||||
*
|
||||
* @param string $from The current account URL.
|
||||
* @param string $to The new account URL.
|
||||
*
|
||||
* @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
|
||||
*/
|
||||
public static function externally( $from, $to ) {
|
||||
$user = Actors::get_by_various( $from );
|
||||
|
||||
if ( \is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Update the movedTo property.
|
||||
if ( $user->get__id() > 0 ) {
|
||||
\update_user_option( $user->get__id(), 'activitypub_moved_to', $to );
|
||||
} else {
|
||||
\update_option( 'activitypub_blog_user_moved_to', $to );
|
||||
}
|
||||
|
||||
$response = Http::get_remote_object( $to );
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$target_actor = new Actor();
|
||||
$target_actor->from_array( $response );
|
||||
|
||||
// Check if the `Move` Activity is valid.
|
||||
$also_known_as = $target_actor->get_also_known_as() ?? array();
|
||||
if ( ! in_array( $from, $also_known_as, true ) ) {
|
||||
return new \WP_Error( 'invalid_target', __( 'Invalid target', 'activitypub' ) );
|
||||
}
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->set_type( 'Move' );
|
||||
$activity->set_actor( $user->get_id() );
|
||||
$activity->set_origin( $user->get_id() );
|
||||
$activity->set_object( $user->get_id() );
|
||||
$activity->set_target( $target_actor->get_id() );
|
||||
|
||||
// Add to outbox.
|
||||
return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC );
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal Move.
|
||||
*
|
||||
* Move an ActivityPub Actor from one location (internal) to another (internal).
|
||||
*
|
||||
* This helps migrating abandoned profiles to `Move` to other profiles:
|
||||
*
|
||||
* `Move::internally( 'https://example.com/?author=123', 'https://example.com/?author=321' );`
|
||||
*
|
||||
* ... or to change Actor-IDs like:
|
||||
*
|
||||
* `Move::internally( 'https://example.com/author/foo', 'https://example.com/?author=123' );`
|
||||
*
|
||||
* @param string $from The current account URL.
|
||||
* @param string $to The new account URL.
|
||||
*
|
||||
* @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
|
||||
*/
|
||||
public static function internally( $from, $to ) {
|
||||
$user = Actors::get_by_various( $from );
|
||||
|
||||
if ( \is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Add the old account URL to alsoKnownAs.
|
||||
if ( $user->get__id() > 0 ) {
|
||||
self::update_user_also_known_as( $user->get__id(), $from );
|
||||
\update_user_option( $user->get__id(), 'activitypub_moved_to', $to );
|
||||
} else {
|
||||
self::update_blog_also_known_as( $from );
|
||||
\update_option( 'activitypub_blog_user_moved_to', $to );
|
||||
}
|
||||
|
||||
// check if `$from` is a URL or an ID.
|
||||
if ( \filter_var( $from, FILTER_VALIDATE_URL ) ) {
|
||||
$actor = $from;
|
||||
} else {
|
||||
$actor = $user->get_id();
|
||||
}
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->set_type( 'Move' );
|
||||
$activity->set_actor( $actor );
|
||||
$activity->set_origin( $actor );
|
||||
$activity->set_object( $actor );
|
||||
$activity->set_target( $to );
|
||||
|
||||
return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the alsoKnownAs property of a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $from The current account URL.
|
||||
*/
|
||||
private static function update_user_also_known_as( $user_id, $from ) {
|
||||
// phpcs:ignore Universal.Operators.DisallowShortTernary.Found
|
||||
$also_known_as = \get_user_option( 'activitypub_also_known_as', $user_id ) ?: array();
|
||||
$also_known_as[] = $from;
|
||||
|
||||
\update_user_option( $user_id, 'activitypub_also_known_as', $also_known_as );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the alsoKnownAs property of the blog.
|
||||
*
|
||||
* @param string $from The current account URL.
|
||||
*/
|
||||
private static function update_blog_also_known_as( $from ) {
|
||||
$also_known_as = \get_option( 'activitypub_blog_user_also_known_as', array() );
|
||||
$also_known_as[] = $from;
|
||||
|
||||
\update_option( 'activitypub_blog_user_also_known_as', $also_known_as );
|
||||
}
|
||||
|
||||
/**
|
||||
* Change domain for all ActivityPub Actors.
|
||||
*
|
||||
* This method handles domain migration according to the ActivityPub Data Portability spec.
|
||||
* It stores the old host and calls Move::internally for each available profile.
|
||||
* It also caches the JSON representation of the old Actor for future lookups.
|
||||
*
|
||||
* @param string $from The old domain.
|
||||
* @param string $to The new domain.
|
||||
*
|
||||
* @return array Array of results from Move::internally calls.
|
||||
*/
|
||||
public static function change_domain( $from, $to ) {
|
||||
// Get all actors that need to be migrated.
|
||||
$actors = Actors::get_all();
|
||||
|
||||
$results = array();
|
||||
$to_host = \wp_parse_url( $to, \PHP_URL_HOST );
|
||||
$from_host = \wp_parse_url( $from, \PHP_URL_HOST );
|
||||
|
||||
// Store the old host for future reference.
|
||||
\update_option( 'activitypub_old_host', $from_host );
|
||||
|
||||
// Process each actor.
|
||||
foreach ( $actors as $actor ) {
|
||||
$actor_id = $actor->get_id();
|
||||
|
||||
// Replace the new host with the old host in the actor ID.
|
||||
$old_actor_id = str_replace( $to_host, $from_host, $actor_id );
|
||||
|
||||
// Call Move::internally for this actor.
|
||||
$result = self::internally( $old_actor_id, $actor_id );
|
||||
|
||||
if ( \is_wp_error( $result ) ) {
|
||||
// Log the error and continue with the next actor.
|
||||
Debug::write_log( 'Error moving actor: ' . $actor_id . ' - ' . $result->get_error_message() );
|
||||
continue;
|
||||
}
|
||||
|
||||
$json = str_replace( $to_host, $from_host, $actor->to_json() );
|
||||
|
||||
// Save the current actor data after migration.
|
||||
if ( $actor instanceof Blog ) {
|
||||
\update_option( 'activitypub_blog_user_old_host_data', $json, false );
|
||||
} else {
|
||||
\update_user_option( $actor->get__id(), 'activitypub_old_host_data', $json, false );
|
||||
}
|
||||
|
||||
$results[] = array(
|
||||
'actor' => $actor_id,
|
||||
'result' => $result,
|
||||
);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe initiate old user.
|
||||
*
|
||||
* This method checks if the current request domain matches the old host.
|
||||
* If it does, it retrieves the cached data for the user and populates the instance.
|
||||
*
|
||||
* @param Blog|User $instance The Blog or User instance to populate.
|
||||
*/
|
||||
public static function maybe_initiate_old_user( $instance ) {
|
||||
if ( ! Query::get_instance()->is_old_host_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $instance instanceof Blog ) {
|
||||
$cached_data = \get_option( 'activitypub_blog_user_old_host_data' );
|
||||
} elseif ( $instance instanceof User ) {
|
||||
$cached_data = \get_user_option( 'activitypub_old_host_data', $instance->get__id() );
|
||||
}
|
||||
|
||||
if ( ! empty( $cached_data ) ) {
|
||||
$instance->from_json( $cached_data );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-send to inboxes.
|
||||
*
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
*/
|
||||
public static function pre_send_to_inboxes( $json ) {
|
||||
$json = json_decode( $json, true );
|
||||
|
||||
if ( 'Move' !== $json['type'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_same_domain( $json['object'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Query::get_instance()->set_old_host_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to return the old blog username.
|
||||
*
|
||||
* @param null $pre The pre-existing value.
|
||||
* @param string $username The username to check.
|
||||
*
|
||||
* @return Blog|null The old blog instance or null.
|
||||
*/
|
||||
public static function old_blog_username( $pre, $username ) {
|
||||
$old_host = \get_option( 'activitypub_old_host' );
|
||||
|
||||
// Special case for Blog Actor on old host.
|
||||
if ( $old_host === $username && Query::get_instance()->is_old_host_request() ) {
|
||||
// Return a new Blog instance which will load the cached data in its constructor.
|
||||
$pre = new Blog();
|
||||
}
|
||||
|
||||
return $pre;
|
||||
}
|
||||
}
|
@ -60,7 +60,18 @@ class Notification {
|
||||
public function send() {
|
||||
$type = \strtolower( $this->type );
|
||||
|
||||
/**
|
||||
* Action to send ActivityPub notifications.
|
||||
*
|
||||
* @param Notification $instance The notification object.
|
||||
*/
|
||||
do_action( 'activitypub_notification', $this );
|
||||
|
||||
/**
|
||||
* Type-specific action to send ActivityPub notifications.
|
||||
*
|
||||
* @param Notification $instance The notification object.
|
||||
*/
|
||||
do_action( "activitypub_notification_{$type}", $this );
|
||||
}
|
||||
}
|
||||
|
124
wp-content/plugins/activitypub/includes/class-options.php
Normal file
124
wp-content/plugins/activitypub/includes/class-options.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* Options file.
|
||||
*
|
||||
* @package ActivityPub
|
||||
*/
|
||||
|
||||
namespace ActivityPub;
|
||||
|
||||
/**
|
||||
* Options class.
|
||||
*
|
||||
* @package ActivityPub
|
||||
*/
|
||||
class Options {
|
||||
|
||||
/**
|
||||
* Initialize the options.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'pre_option_activitypub_actor_mode', array( self::class, 'pre_option_activitypub_actor_mode' ) );
|
||||
\add_filter( 'pre_option_activitypub_authorized_fetch', array( self::class, 'pre_option_activitypub_authorized_fetch' ) );
|
||||
\add_filter( 'pre_option_activitypub_shared_inbox', array( self::class, 'pre_option_activitypub_shared_inbox' ) );
|
||||
\add_filter( 'pre_option_activitypub_vary_header', array( self::class, 'pre_option_activitypub_vary_header' ) );
|
||||
|
||||
\add_filter( 'pre_option_activitypub_allow_likes', array( self::class, 'maybe_disable_interactions' ) );
|
||||
\add_filter( 'pre_option_activitypub_allow_replies', array( self::class, 'maybe_disable_interactions' ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pre-get option filter for the Actor-Mode.
|
||||
*
|
||||
* @param string|false $pre The pre-get option value.
|
||||
*
|
||||
* @return string|false The actor mode or false if it should not be filtered.
|
||||
*/
|
||||
public static function pre_option_activitypub_actor_mode( $pre ) {
|
||||
if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) && ACTIVITYPUB_SINGLE_USER_MODE ) {
|
||||
return ACTIVITYPUB_BLOG_MODE;
|
||||
}
|
||||
|
||||
if ( \defined( 'ACTIVITYPUB_DISABLE_USER' ) && ACTIVITYPUB_DISABLE_USER ) {
|
||||
return ACTIVITYPUB_BLOG_MODE;
|
||||
}
|
||||
|
||||
if ( \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) && ACTIVITYPUB_DISABLE_BLOG_USER ) {
|
||||
return ACTIVITYPUB_ACTOR_MODE;
|
||||
}
|
||||
|
||||
return $pre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-get option filter for the Authorized Fetch.
|
||||
*
|
||||
* @param string $pre The pre-get option value.
|
||||
*
|
||||
* @return string If the constant is defined, return the value, otherwise return the pre-get option value.
|
||||
*/
|
||||
public static function pre_option_activitypub_authorized_fetch( $pre ) {
|
||||
if ( ! \defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) ) {
|
||||
return $pre;
|
||||
}
|
||||
|
||||
if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-get option filter for the Shared Inbox.
|
||||
*
|
||||
* @param string $pre The pre-get option value.
|
||||
*
|
||||
* @return string If the constant is defined, return the value, otherwise return the pre-get option value.
|
||||
*/
|
||||
public static function pre_option_activitypub_shared_inbox( $pre ) {
|
||||
if ( ! \defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) ) {
|
||||
return $pre;
|
||||
}
|
||||
|
||||
if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-get option filter for the Vary Header.
|
||||
*
|
||||
* @param string $pre The pre-get option value.
|
||||
*
|
||||
* @return string If the constant is defined, return the value, otherwise return the pre-get option value.
|
||||
*/
|
||||
public static function pre_option_activitypub_vary_header( $pre ) {
|
||||
if ( ! \defined( 'ACTIVITYPUB_SEND_VARY_HEADER' ) ) {
|
||||
return $pre;
|
||||
}
|
||||
|
||||
if ( ACTIVITYPUB_SEND_VARY_HEADER ) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow interactions if the constant is set.
|
||||
*
|
||||
* @param bool $pre_option The value of the option.
|
||||
* @return bool|string The value of the option.
|
||||
*/
|
||||
public static function maybe_disable_interactions( $pre_option ) {
|
||||
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return $pre_option;
|
||||
}
|
||||
}
|
351
wp-content/plugins/activitypub/includes/class-query.php
Normal file
351
wp-content/plugins/activitypub/includes/class-query.php
Normal file
@ -0,0 +1,351 @@
|
||||
<?php
|
||||
/**
|
||||
* Query class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Outbox;
|
||||
use Activitypub\Transformer\Factory;
|
||||
|
||||
/**
|
||||
* Singleton class to handle and store the ActivityPub query.
|
||||
*/
|
||||
class Query {
|
||||
|
||||
/**
|
||||
* The singleton instance.
|
||||
*
|
||||
* @var Query
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* The ActivityPub object.
|
||||
*
|
||||
* @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
private $activitypub_object;
|
||||
|
||||
/**
|
||||
* The ActivityPub object ID.
|
||||
*
|
||||
* @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-id
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $activitypub_object_id;
|
||||
|
||||
/**
|
||||
* Whether the current request is an ActivityPub request.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_activitypub_request;
|
||||
|
||||
/**
|
||||
* Whether the current request is from the old host.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_old_host_request;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*/
|
||||
private function __construct() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* The destructor.
|
||||
*/
|
||||
public function __destruct() {
|
||||
self::$instance = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance.
|
||||
*
|
||||
* @return Query The singleton instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! isset( self::$instance ) ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub object.
|
||||
*
|
||||
* @return object The ActivityPub object.
|
||||
*/
|
||||
public function get_activitypub_object() {
|
||||
if ( $this->activitypub_object ) {
|
||||
return $this->activitypub_object;
|
||||
}
|
||||
|
||||
if ( $this->prepare_activitypub_data() ) {
|
||||
return $this->activitypub_object;
|
||||
}
|
||||
|
||||
$queried_object = $this->get_queried_object();
|
||||
$transformer = Factory::get_transformer( $queried_object );
|
||||
|
||||
if ( $transformer && ! \is_wp_error( $transformer ) ) {
|
||||
$this->activitypub_object = $transformer->to_object();
|
||||
}
|
||||
|
||||
return $this->activitypub_object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub object ID.
|
||||
*
|
||||
* @return string The ActivityPub object ID.
|
||||
*/
|
||||
public function get_activitypub_object_id() {
|
||||
if ( $this->activitypub_object_id ) {
|
||||
return $this->activitypub_object_id;
|
||||
}
|
||||
|
||||
if ( $this->prepare_activitypub_data() ) {
|
||||
return $this->activitypub_object_id;
|
||||
}
|
||||
|
||||
$queried_object = $this->get_queried_object();
|
||||
$transformer = Factory::get_transformer( $queried_object );
|
||||
|
||||
if ( $transformer && ! \is_wp_error( $transformer ) ) {
|
||||
$this->activitypub_object_id = $transformer->to_id();
|
||||
}
|
||||
|
||||
return $this->activitypub_object_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and set both ActivityPub object and ID for Outbox activities and virtual objects.
|
||||
*
|
||||
* @return bool True if an object was found and set, false otherwise.
|
||||
*/
|
||||
private function prepare_activitypub_data() {
|
||||
$queried_object = $this->get_queried_object();
|
||||
|
||||
// Check for Outbox Activity.
|
||||
if (
|
||||
$queried_object instanceof \WP_Post &&
|
||||
Outbox::POST_TYPE === $queried_object->post_type
|
||||
) {
|
||||
$activitypub_object = Outbox::maybe_get_activity( $queried_object );
|
||||
|
||||
// Check if the Outbox Activity is public.
|
||||
if ( ! \is_wp_error( $activitypub_object ) ) {
|
||||
$this->activitypub_object = $activitypub_object;
|
||||
$this->activitypub_object_id = $this->activitypub_object->get_id();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $queried_object ) {
|
||||
// If the object is not a valid ActivityPub object, try to get a virtual object.
|
||||
$activitypub_object = $this->maybe_get_virtual_object();
|
||||
|
||||
if ( $activitypub_object ) {
|
||||
$this->activitypub_object = $activitypub_object;
|
||||
$this->activitypub_object_id = $this->activitypub_object->get_id();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the queried object.
|
||||
*
|
||||
* This adds support for Comments by `?c=123` IDs and Users by `?author=123` and `@username` IDs.
|
||||
*
|
||||
* @return \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null The queried object.
|
||||
*/
|
||||
public function get_queried_object() {
|
||||
$queried_object = \get_queried_object();
|
||||
|
||||
// Check Comment by ID.
|
||||
if ( ! $queried_object ) {
|
||||
$comment_id = \get_query_var( 'c' );
|
||||
if ( $comment_id ) {
|
||||
$queried_object = \get_comment( $comment_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Check Post by ID (works for custom post types).
|
||||
if ( ! $queried_object ) {
|
||||
$post_id = \get_query_var( 'p' );
|
||||
if ( $post_id ) {
|
||||
$queried_object = \get_post( $post_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get Author by ID.
|
||||
if ( ! $queried_object ) {
|
||||
$url = $this->get_request_url();
|
||||
$author_id = url_to_authorid( $url );
|
||||
if ( $author_id ) {
|
||||
$queried_object = \get_user_by( 'id', $author_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the queried object.
|
||||
*
|
||||
* @param \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null $queried_object The queried object.
|
||||
*/
|
||||
return apply_filters( 'activitypub_queried_object', $queried_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the virtual object.
|
||||
*
|
||||
* Virtual objects are objects that are not stored in the database, but are created on the fly.
|
||||
* The plugins currently supports two virtual objects: The Blog-Actor and the Application-Actor.
|
||||
*
|
||||
* @see \Activitypub\Model\Blog
|
||||
* @see \Activitypub\Model\Application
|
||||
*
|
||||
* @return object|null The virtual object.
|
||||
*/
|
||||
protected function maybe_get_virtual_object() {
|
||||
$url = $this->get_request_url();
|
||||
|
||||
if ( ! $url ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$author_id = url_to_authorid( $url );
|
||||
|
||||
if ( ! is_numeric( $author_id ) ) {
|
||||
$author_id = $url;
|
||||
}
|
||||
|
||||
$user = Actors::get_by_various( $author_id );
|
||||
|
||||
if ( \is_wp_error( $user ) || ! $user ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request URL.
|
||||
*
|
||||
* @return string|null The request URL.
|
||||
*/
|
||||
protected function get_request_url() {
|
||||
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$url = \wp_unslash( $_SERVER['REQUEST_URI'] );
|
||||
$url = \WP_Http::make_absolute_url( $url, \home_url() );
|
||||
$url = \sanitize_url( $url );
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current request is an ActivityPub request.
|
||||
*
|
||||
* @return bool True if the request is an ActivityPub request, false otherwise.
|
||||
*/
|
||||
public function is_activitypub_request() {
|
||||
if ( isset( $this->is_activitypub_request ) ) {
|
||||
return $this->is_activitypub_request;
|
||||
}
|
||||
|
||||
global $wp_query;
|
||||
|
||||
// One can trigger an ActivityPub request by adding `?activitypub` to the URL.
|
||||
if (
|
||||
isset( $wp_query->query_vars['activitypub'] ) ||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
isset( $_GET['activitypub'] )
|
||||
) {
|
||||
\defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
|
||||
$this->is_activitypub_request = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* The other (more common) option to make an ActivityPub request
|
||||
* is to send an Accept header.
|
||||
*/
|
||||
if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
|
||||
$accept = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
|
||||
|
||||
/*
|
||||
* $accept can be a single value, or a comma separated list of values.
|
||||
* We want to support both scenarios,
|
||||
* and return true when the header includes at least one of the following:
|
||||
* - application/activity+json
|
||||
* - application/ld+json
|
||||
* - application/json
|
||||
*/
|
||||
if ( \preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
|
||||
\defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
|
||||
$this->is_activitypub_request = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->is_activitypub_request = false;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current request is from the old host.
|
||||
*
|
||||
* @return bool True if the request is from the old host, false otherwise.
|
||||
*/
|
||||
public function is_old_host_request() {
|
||||
if ( isset( $this->is_old_host_request ) ) {
|
||||
return $this->is_old_host_request;
|
||||
}
|
||||
|
||||
$old_host = \get_option( 'activitypub_old_host' );
|
||||
|
||||
if ( ! $old_host ) {
|
||||
$this->is_old_host_request = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
$request_host = isset( $_SERVER['HTTP_HOST'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
|
||||
$referer_host = isset( $_SERVER['HTTP_REFERER'] ) ? \wp_parse_url( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_REFERER'] ) ), PHP_URL_HOST ) : '';
|
||||
|
||||
// Check if the domain matches either the request domain or referer.
|
||||
$check = $old_host === $request_host || $old_host === $referer_host;
|
||||
$this->is_old_host_request = $check;
|
||||
|
||||
return $check;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake an old host request.
|
||||
*
|
||||
* @param bool $state Optional. The state to set. Default true.
|
||||
*/
|
||||
public function set_old_host_request( $state = true ) {
|
||||
$this->is_old_host_request = $state;
|
||||
}
|
||||
}
|
122
wp-content/plugins/activitypub/includes/class-sanitize.php
Normal file
122
wp-content/plugins/activitypub/includes/class-sanitize.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/**
|
||||
* Sanitization file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Model\Blog;
|
||||
|
||||
/**
|
||||
* Sanitization class.
|
||||
*/
|
||||
class Sanitize {
|
||||
/**
|
||||
* Sanitize a list of URLs.
|
||||
*
|
||||
* @param string|array $value The value to sanitize.
|
||||
* @return array The sanitized list of URLs.
|
||||
*/
|
||||
public static function url_list( $value ) {
|
||||
if ( ! \is_array( $value ) ) {
|
||||
$value = \explode( PHP_EOL, $value );
|
||||
}
|
||||
|
||||
$value = \array_filter( $value );
|
||||
$value = \array_map( 'trim', $value );
|
||||
$value = \array_map( 'sanitize_url', $value );
|
||||
$value = \array_unique( $value );
|
||||
|
||||
return \array_values( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a list of hosts.
|
||||
*
|
||||
* @param string $value The value to sanitize.
|
||||
* @return string The sanitized list of hosts.
|
||||
*/
|
||||
public static function host_list( $value ) {
|
||||
$value = \explode( PHP_EOL, $value );
|
||||
$value = \array_map(
|
||||
function ( $host ) {
|
||||
$host = \trim( $host );
|
||||
$host = \strtolower( $host );
|
||||
$host = \set_url_scheme( $host );
|
||||
$host = \sanitize_url( $host, array( 'http', 'https' ) );
|
||||
|
||||
// Remove protocol.
|
||||
if ( \str_contains( $host, 'http' ) ) {
|
||||
$host = \wp_parse_url( $host, PHP_URL_HOST );
|
||||
}
|
||||
|
||||
return \filter_var( $host, FILTER_VALIDATE_DOMAIN );
|
||||
},
|
||||
$value
|
||||
);
|
||||
|
||||
return \implode( PHP_EOL, \array_filter( $value ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a blog identifier.
|
||||
*
|
||||
* @param string $value The value to sanitize.
|
||||
* @return string The sanitized blog identifier.
|
||||
*/
|
||||
public static function blog_identifier( $value ) {
|
||||
// Hack to allow dots in the username.
|
||||
$parts = \explode( '.', $value );
|
||||
$sanitized = \array_map( 'sanitize_title', $parts );
|
||||
$sanitized = \implode( '.', $sanitized );
|
||||
|
||||
// Check for login or nicename.
|
||||
$user = new \WP_User_Query(
|
||||
array(
|
||||
'search' => $sanitized,
|
||||
'search_columns' => array( 'user_login', 'user_nicename' ),
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->get_results() ) {
|
||||
\add_settings_error(
|
||||
'activitypub_blog_identifier',
|
||||
'activitypub_blog_identifier',
|
||||
\esc_html__( 'You cannot use an existing author’s name for the blog profile ID.', 'activitypub' )
|
||||
);
|
||||
|
||||
return Blog::get_default_username();
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sanitized value of a constant.
|
||||
*
|
||||
* @param mixed $value The constant value.
|
||||
*
|
||||
* @return string The sanitized value.
|
||||
*/
|
||||
public static function constant_value( $value ) {
|
||||
if ( is_bool( $value ) ) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if ( is_string( $value ) ) {
|
||||
return esc_attr( $value );
|
||||
}
|
||||
|
||||
if ( is_array( $value ) ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
|
||||
return print_r( $value, true );
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
@ -7,7 +7,15 @@
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Activity\Base_Object;
|
||||
use Activitypub\Scheduler\Post;
|
||||
use Activitypub\Scheduler\Actor;
|
||||
use Activitypub\Scheduler\Comment;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Outbox;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Transformer\Factory;
|
||||
|
||||
/**
|
||||
* Scheduler class.
|
||||
@ -16,67 +24,53 @@ use Activitypub\Collection\Followers;
|
||||
*/
|
||||
class Scheduler {
|
||||
|
||||
/**
|
||||
* Allowed batch callbacks.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $batch_callbacks = array();
|
||||
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
// Post transitions.
|
||||
\add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
|
||||
\add_action(
|
||||
'edit_attachment',
|
||||
function ( $post_id ) {
|
||||
self::schedule_post_activity( 'publish', 'publish', $post_id );
|
||||
}
|
||||
);
|
||||
\add_action(
|
||||
'add_attachment',
|
||||
function ( $post_id ) {
|
||||
self::schedule_post_activity( 'publish', '', $post_id );
|
||||
}
|
||||
);
|
||||
\add_action(
|
||||
'delete_attachment',
|
||||
function ( $post_id ) {
|
||||
self::schedule_post_activity( 'trash', '', $post_id );
|
||||
}
|
||||
);
|
||||
self::register_schedulers();
|
||||
|
||||
if ( ! ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS ) {
|
||||
// Comment transitions.
|
||||
\add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 );
|
||||
\add_action(
|
||||
'edit_comment',
|
||||
function ( $comment_id ) {
|
||||
self::schedule_comment_activity( 'approved', 'approved', $comment_id );
|
||||
}
|
||||
self::$batch_callbacks = array(
|
||||
Dispatcher::$callback,
|
||||
array( Dispatcher::class, 'retry_send_to_followers' ),
|
||||
);
|
||||
\add_action(
|
||||
'wp_insert_comment',
|
||||
function ( $comment_id ) {
|
||||
self::schedule_comment_activity( 'approved', '', $comment_id );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Follower Cleanups.
|
||||
\add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
|
||||
\add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
|
||||
|
||||
// Profile updates for blog options.
|
||||
if ( ! is_user_type_disabled( 'blog' ) ) {
|
||||
\add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) );
|
||||
\add_action( 'update_option_blogdescription', array( self::class, 'blog_user_update' ) );
|
||||
\add_action( 'update_option_blogname', array( self::class, 'blog_user_update' ) );
|
||||
\add_filter( 'pre_set_theme_mod_custom_logo', array( self::class, 'blog_user_update' ) );
|
||||
\add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) );
|
||||
// Event callbacks.
|
||||
\add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 );
|
||||
\add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) );
|
||||
\add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) );
|
||||
|
||||
\add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) );
|
||||
\add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_announce_activity' ), 10, 4 );
|
||||
|
||||
\add_action( 'update_option_activitypub_outbox_purge_days', array( self::class, 'handle_outbox_purge_days_update' ), 10, 2 );
|
||||
}
|
||||
|
||||
// Profile updates for user options.
|
||||
if ( ! is_user_type_disabled( 'user' ) ) {
|
||||
\add_action( 'wp_update_user', array( self::class, 'user_update' ) );
|
||||
\add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 );
|
||||
// @todo figure out a feasible way of updating the header image since it's not unique to any user.
|
||||
}
|
||||
/**
|
||||
* Register handlers.
|
||||
*/
|
||||
public static function register_schedulers() {
|
||||
Post::init();
|
||||
Actor::init();
|
||||
Comment::init();
|
||||
|
||||
/**
|
||||
* Register additional schedulers.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
do_action( 'activitypub_register_schedulers' );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -90,6 +84,14 @@ class Scheduler {
|
||||
if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) {
|
||||
\wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' );
|
||||
}
|
||||
|
||||
if ( ! \wp_next_scheduled( 'activitypub_reprocess_outbox' ) ) {
|
||||
\wp_schedule_event( time(), 'hourly', 'activitypub_reprocess_outbox' );
|
||||
}
|
||||
|
||||
if ( ! wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'activitypub_outbox_purge' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@ -100,125 +102,8 @@ class Scheduler {
|
||||
public static function deregister_schedules() {
|
||||
wp_unschedule_hook( 'activitypub_update_followers' );
|
||||
wp_unschedule_hook( 'activitypub_cleanup_followers' );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Schedule Activities.
|
||||
*
|
||||
* @param string $new_status New post status.
|
||||
* @param string $old_status Old post status.
|
||||
* @param \WP_Post $post Post object.
|
||||
*/
|
||||
public static function schedule_post_activity( $new_status, $old_status, $post ) {
|
||||
$post = get_post( $post );
|
||||
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'ap_extrafield' === $post->post_type ) {
|
||||
self::schedule_profile_update( $post->post_author );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( 'ap_extrafield_blog' === $post->post_type ) {
|
||||
self::schedule_profile_update( 0 );
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not send activities if post is password protected.
|
||||
if ( \post_password_required( $post ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if post-type supports ActivityPub.
|
||||
$post_types = \get_post_types_by_support( 'activitypub' );
|
||||
if ( ! \in_array( $post->post_type, $post_types, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = false;
|
||||
|
||||
if (
|
||||
'publish' === $new_status &&
|
||||
'publish' !== $old_status
|
||||
) {
|
||||
$type = 'Create';
|
||||
} elseif (
|
||||
'publish' === $new_status ||
|
||||
// We want to send updates for posts that are published and then moved to draft.
|
||||
( 'draft' === $new_status &&
|
||||
'publish' === $old_status )
|
||||
) {
|
||||
$type = 'Update';
|
||||
} elseif ( 'trash' === $new_status ) {
|
||||
$type = 'Delete';
|
||||
}
|
||||
|
||||
if ( empty( $type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hook = 'activitypub_send_post';
|
||||
$args = array( $post->ID, $type );
|
||||
|
||||
if ( false === wp_next_scheduled( $hook, $args ) ) {
|
||||
set_wp_object_state( $post, 'federate' );
|
||||
\wp_schedule_single_event( \time(), $hook, $args );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule Comment Activities.
|
||||
*
|
||||
* @see transition_comment_status()
|
||||
*
|
||||
* @param string $new_status New comment status.
|
||||
* @param string $old_status Old comment status.
|
||||
* @param \WP_Comment $comment Comment object.
|
||||
*/
|
||||
public static function schedule_comment_activity( $new_status, $old_status, $comment ) {
|
||||
$comment = get_comment( $comment );
|
||||
|
||||
// Federate only comments that are written by a registered user.
|
||||
if ( ! $comment || ! $comment->user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = false;
|
||||
|
||||
if (
|
||||
'approved' === $new_status &&
|
||||
'approved' !== $old_status
|
||||
) {
|
||||
$type = 'Create';
|
||||
} elseif ( 'approved' === $new_status ) {
|
||||
$type = 'Update';
|
||||
\update_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', time(), true );
|
||||
} elseif (
|
||||
'trash' === $new_status ||
|
||||
'spam' === $new_status
|
||||
) {
|
||||
$type = 'Delete';
|
||||
}
|
||||
|
||||
if ( empty( $type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if comment should be federated or not.
|
||||
if ( ! should_comment_be_federated( $comment ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hook = 'activitypub_send_comment';
|
||||
$args = array( $comment->comment_ID, $type );
|
||||
|
||||
if ( false === wp_next_scheduled( $hook, $args ) ) {
|
||||
set_wp_object_state( $comment, 'federate' );
|
||||
\wp_schedule_single_event( \time(), $hook, $args );
|
||||
}
|
||||
wp_unschedule_hook( 'activitypub_reprocess_outbox' );
|
||||
wp_unschedule_hook( 'activitypub_outbox_purge' );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -292,67 +177,268 @@ class Scheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a profile update when relevant user meta is updated.
|
||||
* Schedule the outbox item for federation.
|
||||
*
|
||||
* @param int $meta_id Meta ID being updated.
|
||||
* @param int $user_id User ID being updated.
|
||||
* @param string $meta_key Meta key being updated.
|
||||
* @param int $id The ID of the outbox item.
|
||||
* @param int $offset The offset to add to the scheduled time.
|
||||
*/
|
||||
public static function user_meta_update( $meta_id, $user_id, $meta_key ) {
|
||||
// Don't bother if the user can't publish.
|
||||
if ( ! \user_can( $user_id, 'activitypub' ) ) {
|
||||
return;
|
||||
}
|
||||
public static function schedule_outbox_activity_for_federation( $id, $offset = 0 ) {
|
||||
$hook = 'activitypub_process_outbox';
|
||||
$args = array( $id );
|
||||
|
||||
// The user meta fields that affect a profile.
|
||||
$fields = array(
|
||||
'activitypub_description',
|
||||
'activitypub_header_image',
|
||||
'description',
|
||||
'user_url',
|
||||
'display_name',
|
||||
);
|
||||
if ( in_array( $meta_key, $fields, true ) ) {
|
||||
self::schedule_profile_update( $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a profile update when a user is updated.
|
||||
*
|
||||
* @param int $user_id User ID being updated.
|
||||
*/
|
||||
public static function user_update( $user_id ) {
|
||||
// Don't bother if the user can't publish.
|
||||
if ( ! \user_can( $user_id, 'activitypub' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::schedule_profile_update( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme mods only have a dynamic filter so we fudge it like this.
|
||||
*
|
||||
* @param mixed $value Optional. The value to be updated. Default null.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function blog_user_update( $value = null ) {
|
||||
self::schedule_profile_update( 0 );
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a profile update to all followers. Gets hooked into all relevant options/meta etc.
|
||||
*
|
||||
* @param int $user_id The user ID to update (Could be 0 for Blog-User).
|
||||
*/
|
||||
public static function schedule_profile_update( $user_id ) {
|
||||
if ( false === wp_next_scheduled( $hook, $args ) ) {
|
||||
\wp_schedule_single_event(
|
||||
\time(),
|
||||
'activitypub_send_update_profile_activity',
|
||||
array( $user_id )
|
||||
\time() + $offset,
|
||||
$hook,
|
||||
$args
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reprocess the outbox.
|
||||
*/
|
||||
public static function reprocess_outbox() {
|
||||
// Bail if there is a pending batch.
|
||||
if ( self::next_scheduled_hook( 'activitypub_async_batch' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Bail if there is a batch in progress.
|
||||
$key = \md5( \serialize( Dispatcher::$callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
|
||||
if ( self::is_locked( $key ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$ids = \get_posts(
|
||||
array(
|
||||
'post_type' => Outbox::POST_TYPE,
|
||||
'post_status' => 'pending',
|
||||
'posts_per_page' => 10,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
self::schedule_outbox_activity_for_federation( $id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge outbox items based on a schedule.
|
||||
*/
|
||||
public static function purge_outbox() {
|
||||
$total_posts = (int) wp_count_posts( Outbox::POST_TYPE )->publish;
|
||||
if ( $total_posts <= 20 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$days = (int) get_option( 'activitypub_outbox_purge_days', 180 );
|
||||
$timezone = new \DateTimeZone( 'UTC' );
|
||||
$date = new \DateTime( 'now', $timezone );
|
||||
|
||||
$date->sub( \DateInterval::createFromDateString( "$days days" ) );
|
||||
|
||||
$post_ids = get_posts(
|
||||
array(
|
||||
'post_type' => Outbox::POST_TYPE,
|
||||
'post_status' => 'any',
|
||||
'fields' => 'ids',
|
||||
'numberposts' => -1,
|
||||
'date_query' => array(
|
||||
array(
|
||||
'before' => $date->format( 'Y-m-d' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
\wp_delete_post( $post_id, true );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update schedules when outbox purge days settings change.
|
||||
*
|
||||
* @param int $old_value The old value.
|
||||
* @param int $value The new value.
|
||||
*/
|
||||
public static function handle_outbox_purge_days_update( $old_value, $value ) {
|
||||
if ( 0 === (int) $value ) {
|
||||
wp_clear_scheduled_hook( 'activitypub_outbox_purge' );
|
||||
} elseif ( ! wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'activitypub_outbox_purge' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously runs batch processing routines.
|
||||
*
|
||||
* The batching part is optional and only comes into play if the callback returns anything.
|
||||
* Beyond that it's a helper to run a callback asynchronously with locking to prevent simultaneous processing.
|
||||
*
|
||||
* @param callable $callback Callable processing routine.
|
||||
* @params mixed ...$args Optional. Parameters that get passed to the callback.
|
||||
*/
|
||||
public static function async_batch( $callback ) {
|
||||
if ( ! in_array( $callback, self::$batch_callbacks, true ) || ! \is_callable( $callback ) ) {
|
||||
_doing_it_wrong( __METHOD__, 'The first argument must be a valid callback.', '5.2.0' );
|
||||
return;
|
||||
}
|
||||
|
||||
$args = \func_get_args(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue
|
||||
$key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
|
||||
|
||||
// Bail if the existing lock is still valid.
|
||||
if ( self::is_locked( $key ) ) {
|
||||
\wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_async_batch', $args );
|
||||
return;
|
||||
}
|
||||
|
||||
self::lock( $key );
|
||||
|
||||
$callback = array_shift( $args ); // Remove $callback from arguments.
|
||||
$next = \call_user_func_array( $callback, $args );
|
||||
|
||||
self::unlock( $key );
|
||||
|
||||
if ( ! empty( $next ) ) {
|
||||
// Schedule the next run, adding the result to the arguments.
|
||||
\wp_schedule_single_event(
|
||||
\time() + 30,
|
||||
'activitypub_async_batch',
|
||||
\array_merge( array( $callback ), \array_values( $next ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Locks the async batch process for individual callbacks to prevent simultaneous processing.
|
||||
*
|
||||
* @param string $key Serialized callback name.
|
||||
* @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
|
||||
*/
|
||||
public static function lock( $key ) {
|
||||
global $wpdb;
|
||||
|
||||
// Try to lock.
|
||||
$lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_async_batch_' . $key, \time() ) ); // phpcs:ignore WordPress.DB
|
||||
|
||||
if ( ! $lock_result ) {
|
||||
$lock_result = \get_option( 'activitypub_async_batch_' . $key );
|
||||
}
|
||||
|
||||
return $lock_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks processing for the async batch callback.
|
||||
*
|
||||
* @param string $key Serialized callback name.
|
||||
*/
|
||||
public static function unlock( $key ) {
|
||||
\delete_option( 'activitypub_async_batch_' . $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the async batch callback is locked.
|
||||
*
|
||||
* @param string $key Serialized callback name.
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_locked( $key ) {
|
||||
$lock = \get_option( 'activitypub_async_batch_' . $key );
|
||||
|
||||
if ( ! $lock ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lock = (int) $lock;
|
||||
|
||||
if ( $lock < \time() - 1800 ) {
|
||||
self::unlock( $key );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next scheduled hook.
|
||||
*
|
||||
* @param string $hook The hook name.
|
||||
* @return int|bool The timestamp of the next scheduled hook, or false if none found.
|
||||
*/
|
||||
private static function next_scheduled_hook( $hook ) {
|
||||
$crons = _get_cron_array();
|
||||
if ( empty( $crons ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get next event.
|
||||
$next = false;
|
||||
foreach ( $crons as $timestamp => $cron ) {
|
||||
if ( isset( $cron[ $hook ] ) ) {
|
||||
$next = $timestamp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send announces.
|
||||
*
|
||||
* @param int $outbox_activity_id The outbox activity ID.
|
||||
* @param \Activitypub\Activity\Activity $activity The activity object.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $content_visibility The content visibility.
|
||||
*/
|
||||
public static function schedule_announce_activity( $outbox_activity_id, $activity, $actor_id, $content_visibility ) {
|
||||
// Only if we're in both Blog and User modes.
|
||||
if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only if this isn't the Blog Actor.
|
||||
if ( Actors::BLOG_USER_ID === $actor_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only if the content is public or quiet public.
|
||||
if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only if the activity is a Create.
|
||||
if ( 'Create' !== $activity->get_type() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! is_object( $activity->get_object() ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the object is an article, image, audio, video, event, or document and ignore profile updates and other activities.
|
||||
if ( ! in_array( $activity->get_object()->get_type(), Base_Object::TYPES, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$announce = new Activity();
|
||||
$announce->set_type( 'Announce' );
|
||||
$announce->set_actor( Actors::get_by_id( Actors::BLOG_USER_ID )->get_id() );
|
||||
$announce->set_object( $activity );
|
||||
|
||||
$outbox_activity_id = Outbox::add( $announce, Actors::BLOG_USER_ID );
|
||||
|
||||
if ( ! $outbox_activity_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule the outbox item for federation.
|
||||
self::schedule_outbox_activity_for_federation( $outbox_activity_id, 120 );
|
||||
}
|
||||
}
|
||||
|
@ -54,6 +54,11 @@ class Shortcodes {
|
||||
$hash_tags = array();
|
||||
|
||||
foreach ( $tags as $tag ) {
|
||||
// Tag can be empty.
|
||||
if ( ! $tag ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hash_tags[] = \sprintf(
|
||||
'<a rel="tag" class="hashtag u-tag u-category" href="%s">%s</a>',
|
||||
\esc_url( \get_tag_link( $tag ) ),
|
||||
@ -67,16 +72,36 @@ class Shortcodes {
|
||||
/**
|
||||
* Generates output for the 'ap_title' Shortcode
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
* @param string $tag The tag/name of the Shortcode.
|
||||
*
|
||||
* @return string The post title.
|
||||
*/
|
||||
public static function title() {
|
||||
public static function title( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return \wp_strip_all_tags( \get_the_title( $item->ID ), true );
|
||||
$title = \wp_strip_all_tags( \get_the_title( $item->ID ), true );
|
||||
|
||||
if ( ! $title ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array( 'type' => 'plain' ),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
if ( 'html' !== $atts['type'] ) {
|
||||
return $title;
|
||||
}
|
||||
|
||||
return sprintf( '<h2>%s</h2>', $title );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -109,6 +134,7 @@ class Shortcodes {
|
||||
|
||||
$excerpt = generate_post_summary( $item, $excerpt_length );
|
||||
|
||||
/** This filter is documented in wp-includes/post-template.php */
|
||||
return \apply_filters( 'the_excerpt', $excerpt );
|
||||
}
|
||||
|
||||
@ -145,22 +171,27 @@ class Shortcodes {
|
||||
if ( empty( $content ) ) {
|
||||
$content = get_post_meta( $item->ID, '_wp_attachment_image_alt', true );
|
||||
}
|
||||
} else {
|
||||
}
|
||||
|
||||
if ( empty( $content ) ) {
|
||||
$content = \get_post_field( 'post_content', $item );
|
||||
}
|
||||
|
||||
if ( 'yes' === $atts['apply_filters'] ) {
|
||||
/** This filter is documented in wp-includes/post-template.php */
|
||||
$content = \apply_filters( 'the_content', $content );
|
||||
} else {
|
||||
$content = do_blocks( $content );
|
||||
$content = wptexturize( $content );
|
||||
$content = wp_filter_content_tags( $content );
|
||||
if ( site_supports_blocks() ) {
|
||||
$content = \do_blocks( $content );
|
||||
}
|
||||
$content = \wptexturize( $content );
|
||||
$content = \wp_filter_content_tags( $content );
|
||||
}
|
||||
|
||||
// Replace script and style elements.
|
||||
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
|
||||
$content = strip_shortcodes( $content );
|
||||
$content = \strip_shortcodes( $content );
|
||||
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
|
||||
}
|
||||
|
||||
add_shortcode( 'ap_content', array( 'Activitypub\Shortcodes', 'content' ) );
|
||||
|
||||
@ -191,7 +222,7 @@ class Shortcodes {
|
||||
$tag
|
||||
);
|
||||
|
||||
if ( 'url' === $atts['type'] ) {
|
||||
if ( 'html' !== $atts['type'] ) {
|
||||
return \esc_url( \get_permalink( $item->ID ) );
|
||||
}
|
||||
|
||||
@ -225,7 +256,7 @@ class Shortcodes {
|
||||
$tag
|
||||
);
|
||||
|
||||
if ( 'url' === $atts['type'] ) {
|
||||
if ( 'html' !== $atts['type'] ) {
|
||||
return \esc_url( \wp_get_shortlink( $item->ID ) );
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,7 @@ use WP_Error;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use WP_REST_Request;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
/**
|
||||
* ActivityPub Signature Class.
|
||||
@ -193,7 +193,7 @@ class Signature {
|
||||
* @return string The signature.
|
||||
*/
|
||||
public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) {
|
||||
$user = Users::get_by_id( $user_id );
|
||||
$user = Actors::get_by_id( $user_id );
|
||||
$key = self::get_private_key_for( $user->get__id() );
|
||||
|
||||
$url_parts = \wp_parse_url( $url );
|
||||
@ -223,7 +223,7 @@ class Signature {
|
||||
\openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 );
|
||||
$signature = \base64_encode( $signature ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
|
||||
$key_id = $user->get_url() . '#main-key';
|
||||
$key_id = $user->get_id() . '#main-key';
|
||||
|
||||
if ( ! empty( $digest ) ) {
|
||||
return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $key_id, $signature );
|
||||
@ -267,24 +267,15 @@ class Signature {
|
||||
$headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0];
|
||||
}
|
||||
|
||||
if ( ! isset( $headers['signature'] ) ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Request not signed', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
if ( array_key_exists( 'signature', $headers ) ) {
|
||||
$signature_block = self::parse_signature_header( $headers['signature'][0] );
|
||||
} elseif ( array_key_exists( 'authorization', $headers ) ) {
|
||||
$signature_block = self::parse_signature_header( $headers['authorization'][0] );
|
||||
}
|
||||
|
||||
if ( ! isset( $signature_block ) || ! $signature_block ) {
|
||||
} else {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Incompatible request signature. keyId and signature are required', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
$signed_headers = $signature_block['headers'];
|
||||
if ( ! $signed_headers ) {
|
||||
$signed_headers = array( 'date' );
|
||||
}
|
||||
|
||||
$signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers );
|
||||
if ( ! $signed_data ) {
|
||||
@ -321,7 +312,6 @@ class Signature {
|
||||
}
|
||||
|
||||
$verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0;
|
||||
|
||||
if ( ! $verified ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Invalid signature', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
@ -333,7 +323,7 @@ class Signature {
|
||||
*
|
||||
* @param string $key_id The URL to the public key.
|
||||
*
|
||||
* @return WP_Error|string The public key or WP_Error.
|
||||
* @return resource|WP_Error The public key resource or WP_Error.
|
||||
*/
|
||||
public static function get_remote_key( $key_id ) {
|
||||
$actor = get_remote_metadata_by_actor( strip_fragment_from_url( $key_id ) );
|
||||
@ -344,9 +334,14 @@ class Signature {
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $actor['publicKey']['publicKeyPem'] ) ) {
|
||||
return \rtrim( $actor['publicKey']['publicKeyPem'] );
|
||||
$key_resource = \openssl_pkey_get_public( \rtrim( $actor['publicKey']['publicKeyPem'] ) );
|
||||
if ( $key_resource ) {
|
||||
return $key_resource;
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_no_remote_key_found',
|
||||
__( 'No Public-Key found', 'activitypub' ),
|
||||
@ -403,7 +398,7 @@ class Signature {
|
||||
$parsed_header['signature'] = \base64_decode( preg_replace( '/\s+/', '', trim( $matches[1] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
}
|
||||
|
||||
if ( ( $parsed_header['signature'] ) && ( $parsed_header['algorithm'] ) && ( ! $parsed_header['headers'] ) ) {
|
||||
if ( empty( $parsed_header['headers'] ) ) {
|
||||
$parsed_header['headers'] = array( 'date' );
|
||||
}
|
||||
|
||||
@ -461,6 +456,10 @@ class Signature {
|
||||
}
|
||||
}
|
||||
if ( 'date' === $header ) {
|
||||
if ( empty( $headers[ $header ][0] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow a bit of leeway for misconfigured clocks.
|
||||
$d = new DateTime( $headers[ $header ][0] );
|
||||
$d->setTimeZone( new DateTimeZone( 'UTC' ) );
|
||||
@ -474,8 +473,11 @@ class Signature {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! empty( $headers[ $header ][0] ) ) {
|
||||
$signed_data .= $header . ': ' . $headers[ $header ][0] . "\n";
|
||||
}
|
||||
}
|
||||
return \rtrim( $signed_data, "\n" );
|
||||
}
|
||||
|
||||
|
@ -8,7 +8,7 @@
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
/**
|
||||
* ActivityPub WebFinger Class.
|
||||
@ -26,7 +26,7 @@ class Webfinger {
|
||||
* @return string The user-resource.
|
||||
*/
|
||||
public static function get_user_resource( $user_id ) {
|
||||
$user = Users::get_by_id( $user_id );
|
||||
$user = Actors::get_by_id( $user_id );
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
return '';
|
||||
}
|
||||
@ -62,6 +62,7 @@ class Webfinger {
|
||||
foreach ( $data['links'] as $link ) {
|
||||
if (
|
||||
'self' === $link['rel'] &&
|
||||
isset( $link['type'] ) &&
|
||||
(
|
||||
'application/activity+json' === $link['type'] ||
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' === $link['type']
|
||||
@ -84,6 +85,8 @@ class Webfinger {
|
||||
/**
|
||||
* Transform a URI to an acct <identifier>@<host>.
|
||||
*
|
||||
* @see https://swicg.github.io/activitypub-webfinger/#reverse-discovery
|
||||
*
|
||||
* @param string $uri The URI (acct:, mailto:, http:, https:).
|
||||
*
|
||||
* @return string|WP_Error Error or acct URI.
|
||||
|
@ -0,0 +1,378 @@
|
||||
<?php
|
||||
/**
|
||||
* Actors collection file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use WP_Error;
|
||||
use WP_User_Query;
|
||||
use Activitypub\Model\User;
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Model\Application;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\normalize_url;
|
||||
use function Activitypub\normalize_host;
|
||||
use function Activitypub\url_to_authorid;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
use function Activitypub\user_can_activitypub;
|
||||
|
||||
/**
|
||||
* Actors collection.
|
||||
*/
|
||||
class Actors {
|
||||
/**
|
||||
* The ID of the Blog User.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const BLOG_USER_ID = 0;
|
||||
|
||||
/**
|
||||
* The ID of the Application User.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const APPLICATION_USER_ID = -1;
|
||||
|
||||
/**
|
||||
* Get the Actor by ID.
|
||||
*
|
||||
* @param int $user_id The User-ID.
|
||||
*
|
||||
* @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_id( $user_id ) {
|
||||
if ( is_numeric( $user_id ) ) {
|
||||
$user_id = (int) $user_id;
|
||||
}
|
||||
|
||||
if ( ! user_can_activitypub( $user_id ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'Actor not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
switch ( $user_id ) {
|
||||
case self::BLOG_USER_ID:
|
||||
return new Blog();
|
||||
case self::APPLICATION_USER_ID:
|
||||
return new Application();
|
||||
default:
|
||||
return User::from_wp_user( $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Actor by username.
|
||||
*
|
||||
* @param string $username Name of the Actor.
|
||||
*
|
||||
* @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_username( $username ) {
|
||||
/**
|
||||
* Filter the username before we do anything else.
|
||||
*
|
||||
* @param null $pre The pre-existing value.
|
||||
* @param string $username The username.
|
||||
*/
|
||||
$pre = apply_filters( 'activitypub_pre_get_by_username', null, $username );
|
||||
if ( null !== $pre ) {
|
||||
return $pre;
|
||||
}
|
||||
|
||||
// Check for blog user.
|
||||
if ( Blog::get_default_username() === $username ) {
|
||||
return new Blog();
|
||||
}
|
||||
|
||||
if ( get_option( 'activitypub_blog_identifier' ) === $username ) {
|
||||
return new Blog();
|
||||
}
|
||||
|
||||
// Check for application user.
|
||||
if ( 'application' === $username ) {
|
||||
return new Application();
|
||||
}
|
||||
|
||||
// Check for 'activitypub_username' meta.
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'count_total' => false,
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => '_activitypub_user_identifier',
|
||||
'value' => $username,
|
||||
'compare' => 'LIKE',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
$actor = self::get_by_id( $user->results[0] );
|
||||
if ( ! \is_wp_error( $actor ) ) {
|
||||
return $actor;
|
||||
}
|
||||
}
|
||||
|
||||
$username = str_replace( array( '*', '%' ), '', $username );
|
||||
|
||||
// Check for login or nicename.
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'count_total' => false,
|
||||
'search' => $username,
|
||||
'search_columns' => array( 'user_login', 'user_nicename' ),
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
$actor = self::get_by_id( $user->results[0] );
|
||||
if ( ! \is_wp_error( $actor ) ) {
|
||||
return $actor;
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'Actor not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Actor by resource.
|
||||
*
|
||||
* @param string $uri The Actor resource.
|
||||
*
|
||||
* @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_resource( $uri ) {
|
||||
$uri = object_to_uri( $uri );
|
||||
|
||||
if ( ! $uri ) {
|
||||
return new WP_Error(
|
||||
'activitypub_no_uri',
|
||||
\__( 'No URI provided', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
$scheme = 'acct';
|
||||
$match = array();
|
||||
// Try to extract the scheme and the host.
|
||||
if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $uri, $match ) ) {
|
||||
// Extract the scheme.
|
||||
$scheme = \esc_attr( $match[1] );
|
||||
}
|
||||
|
||||
// @todo: handle old domain URIs here before we serve a new domain below when we shouldn't.
|
||||
// Although maybe passing through to ::get_by_username() is enough?
|
||||
|
||||
switch ( $scheme ) {
|
||||
// Check for http(s) URIs.
|
||||
case 'http':
|
||||
case 'https':
|
||||
$resource_path = \wp_parse_url( $uri, PHP_URL_PATH );
|
||||
|
||||
if ( $resource_path ) {
|
||||
$blog_path = \wp_parse_url( \home_url(), PHP_URL_PATH );
|
||||
|
||||
if ( $blog_path ) {
|
||||
$resource_path = \str_replace( $blog_path, '', $resource_path );
|
||||
}
|
||||
|
||||
$resource_path = \trim( $resource_path, '/' );
|
||||
|
||||
// Check for http(s)://blog.example.com/@username.
|
||||
if ( str_starts_with( $resource_path, '@' ) ) {
|
||||
$identifier = \str_replace( '@', '', $resource_path );
|
||||
$identifier = \trim( $identifier, '/' );
|
||||
|
||||
return self::get_by_username( $identifier );
|
||||
}
|
||||
}
|
||||
|
||||
// Check for http(s)://blog.example.com/author/username.
|
||||
$user_id = url_to_authorid( $uri );
|
||||
|
||||
if ( \is_int( $user_id ) ) {
|
||||
return self::get_by_id( $user_id );
|
||||
}
|
||||
|
||||
// Check for http(s)://blog.example.com/.
|
||||
$normalized_uri = normalize_url( $uri );
|
||||
|
||||
if (
|
||||
normalize_url( site_url() ) === $normalized_uri ||
|
||||
normalize_url( home_url() ) === $normalized_uri
|
||||
) {
|
||||
return self::get_by_id( self::BLOG_USER_ID );
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_no_user_found',
|
||||
\__( 'Actor not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
// Check for acct URIs.
|
||||
case 'acct':
|
||||
$uri = \str_replace( 'acct:', '', $uri );
|
||||
$identifier = \substr( $uri, 0, \strrpos( $uri, '@' ) );
|
||||
$host = normalize_host( \substr( \strrchr( $uri, '@' ), 1 ) );
|
||||
$blog_host = normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) );
|
||||
|
||||
if ( $blog_host !== $host && get_option( 'activitypub_old_host' ) !== $host ) {
|
||||
return new WP_Error(
|
||||
'activitypub_wrong_host',
|
||||
\__( 'Resource host does not match blog host', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare wildcards https://github.com/mastodon/mastodon/issues/22213.
|
||||
if ( in_array( $identifier, array( '_', '*', '' ), true ) ) {
|
||||
return self::get_by_id( self::BLOG_USER_ID );
|
||||
}
|
||||
|
||||
return self::get_by_username( $identifier );
|
||||
default:
|
||||
return new WP_Error(
|
||||
'activitypub_wrong_scheme',
|
||||
\__( 'Wrong scheme', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Actor by resource.
|
||||
*
|
||||
* @param string $id The Actor resource.
|
||||
*
|
||||
* @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_various( $id ) {
|
||||
$user = null;
|
||||
|
||||
if ( is_numeric( $id ) ) {
|
||||
$user = self::get_by_id( $id );
|
||||
} elseif (
|
||||
// Is URL.
|
||||
filter_var( $id, FILTER_VALIDATE_URL ) ||
|
||||
// Is acct.
|
||||
str_starts_with( $id, 'acct:' ) ||
|
||||
// Is email.
|
||||
filter_var( $id, FILTER_VALIDATE_EMAIL )
|
||||
) {
|
||||
$user = self::get_by_resource( $id );
|
||||
} else {
|
||||
$user = self::get_by_username( $id );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Actor collection.
|
||||
*
|
||||
* @return array The Actor collection.
|
||||
*/
|
||||
public static function get_collection() {
|
||||
if ( is_user_type_disabled( 'user' ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
|
||||
$return = array();
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$actor = User::from_wp_user( $user->ID );
|
||||
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$return[] = $actor;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active Actors including the Blog Actor.
|
||||
*
|
||||
* @return array The actor collection.
|
||||
*/
|
||||
public static function get_all() {
|
||||
$return = array();
|
||||
|
||||
if ( ! is_user_type_disabled( 'user' ) ) {
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$actor = User::from_wp_user( $user->ID );
|
||||
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$return[] = $actor;
|
||||
}
|
||||
}
|
||||
|
||||
// Also include the blog actor if active.
|
||||
if ( ! is_user_type_disabled( 'blog' ) ) {
|
||||
$blog_actor = self::get_by_id( self::BLOG_USER_ID );
|
||||
if ( ! \is_wp_error( $blog_actor ) ) {
|
||||
$return[] = $blog_actor;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actor type based on the user ID.
|
||||
*
|
||||
* @param int $user_id The user ID to check.
|
||||
* @return string The user type.
|
||||
*/
|
||||
public static function get_type_by_id( $user_id ) {
|
||||
$user_id = (int) $user_id;
|
||||
|
||||
if ( self::APPLICATION_USER_ID === $user_id ) {
|
||||
return 'application';
|
||||
}
|
||||
|
||||
if ( self::BLOG_USER_ID === $user_id ) {
|
||||
return 'blog';
|
||||
}
|
||||
|
||||
return 'user';
|
||||
}
|
||||
}
|
@ -42,6 +42,15 @@ class Extra_Fields {
|
||||
$query = new \WP_Query( $args );
|
||||
$fields = $query->posts ?? array();
|
||||
|
||||
/**
|
||||
* Filters the extra fields for an ActivityPub actor.
|
||||
*
|
||||
* This filter allows developers to modify or add custom fields to an actor's
|
||||
* profile.
|
||||
*
|
||||
* @param \WP_Post[] $fields Array of WP_Post objects representing the extra fields.
|
||||
* @param int $user_id The ID of the user whose fields are being retrieved.
|
||||
*/
|
||||
return apply_filters( 'activitypub_get_actor_extra_fields', $fields, $user_id );
|
||||
}
|
||||
|
||||
@ -54,7 +63,7 @@ class Extra_Fields {
|
||||
*/
|
||||
public static function get_formatted_content( $post ) {
|
||||
$content = \get_the_content( null, false, $post );
|
||||
$content = Link::the_content( $content, true );
|
||||
$content = Link::the_content( $content );
|
||||
if ( site_supports_blocks() ) {
|
||||
$content = \do_blocks( $content );
|
||||
}
|
||||
@ -84,14 +93,7 @@ class Extra_Fields {
|
||||
*/
|
||||
public static function fields_to_attachments( $fields ) {
|
||||
$attachments = array();
|
||||
\add_filter(
|
||||
'activitypub_link_rel',
|
||||
function ( $rel ) {
|
||||
$rel .= ' me';
|
||||
|
||||
return $rel;
|
||||
}
|
||||
);
|
||||
\add_filter( 'activitypub_link_rel', array( self::class, 'add_rel_me' ) );
|
||||
|
||||
foreach ( $fields as $post ) {
|
||||
$content = self::get_formatted_content( $post );
|
||||
@ -105,7 +107,7 @@ class Extra_Fields {
|
||||
),
|
||||
);
|
||||
|
||||
$link_added = false;
|
||||
$attachment = false;
|
||||
|
||||
// Add support for FEP-fb2a, for more information see FEDERATION.md.
|
||||
$link_content = \trim( \strip_tags( $content, '<a>' ) );
|
||||
@ -123,14 +125,17 @@ class Extra_Fields {
|
||||
'type' => 'Link',
|
||||
'name' => \get_the_title( $post ),
|
||||
'href' => \esc_url( $tags->get_attribute( 'href' ) ),
|
||||
'rel' => explode( ' ', $tags->get_attribute( 'rel' ) ),
|
||||
);
|
||||
|
||||
$link_added = true;
|
||||
$rel = $tags->get_attribute( 'rel' );
|
||||
|
||||
if ( $rel && \is_string( $rel ) ) {
|
||||
$attachment['rel'] = \explode( ' ', $rel );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $link_added ) {
|
||||
if ( ! $attachment ) {
|
||||
$attachment = array(
|
||||
'type' => 'Note',
|
||||
'name' => \get_the_title( $post ),
|
||||
@ -145,6 +150,8 @@ class Extra_Fields {
|
||||
$attachments[] = $attachment;
|
||||
}
|
||||
|
||||
\remove_filter( 'activitypub_link_rel', array( self::class, 'add_rel_me' ) );
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
@ -271,6 +278,16 @@ class Extra_Fields {
|
||||
return '<!-- wp:paragraph --><p>' . $content . '</p><!-- /wp:paragraph -->';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the 'me' rel to the link.
|
||||
*
|
||||
* @param string $rel The rel attribute.
|
||||
* @return string The modified rel attribute.
|
||||
*/
|
||||
public static function add_rel_me( $rel ) {
|
||||
return $rel . ' me';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is the blog user.
|
||||
*
|
||||
@ -278,6 +295,6 @@ class Extra_Fields {
|
||||
* @return bool True if the user is the blog user, otherwise false.
|
||||
*/
|
||||
private static function is_blog( $user_id ) {
|
||||
return Users::BLOG_USER_ID === $user_id;
|
||||
return Actors::BLOG_USER_ID === $user_id;
|
||||
}
|
||||
}
|
||||
|
@ -7,9 +7,9 @@
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use Activitypub\Model\Follower;
|
||||
use WP_Error;
|
||||
use WP_Query;
|
||||
use Activitypub\Model\Follower;
|
||||
|
||||
use function Activitypub\is_tombstone;
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
@ -52,11 +52,11 @@ class Followers {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$post_meta = get_post_meta( $id, 'activitypub_user_id', false );
|
||||
$post_meta = get_post_meta( $id, '_activitypub_user_id', false );
|
||||
|
||||
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
|
||||
if ( is_array( $post_meta ) && ! in_array( $user_id, $post_meta ) ) {
|
||||
add_post_meta( $id, 'activitypub_user_id', $user_id );
|
||||
add_post_meta( $id, '_activitypub_user_id', $user_id );
|
||||
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
|
||||
}
|
||||
|
||||
@ -80,7 +80,16 @@ class Followers {
|
||||
return false;
|
||||
}
|
||||
|
||||
return delete_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id );
|
||||
/**
|
||||
* Fires before a Follower is removed.
|
||||
*
|
||||
* @param Follower $follower The Follower object.
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param string $actor The Actor URL.
|
||||
*/
|
||||
do_action( 'activitypub_followers_pre_remove_follower', $follower, $user_id, $actor );
|
||||
|
||||
return delete_post_meta( $follower->get__id(), '_activitypub_user_id', $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -89,7 +98,7 @@ class Followers {
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param string $actor The Actor URL.
|
||||
*
|
||||
* @return Follower|null The Follower object or null
|
||||
* @return Follower|false|null The Follower object or null
|
||||
*/
|
||||
public static function get_follower( $user_id, $actor ) {
|
||||
global $wpdb;
|
||||
@ -97,7 +106,7 @@ class Followers {
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$post_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s",
|
||||
"SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s",
|
||||
array(
|
||||
esc_sql( self::POST_TYPE ),
|
||||
esc_sql( $user_id ),
|
||||
@ -119,7 +128,7 @@ class Followers {
|
||||
*
|
||||
* @param string $actor The Actor URL.
|
||||
*
|
||||
* @return \Activitypub\Activity\Base_Object|WP_Error|null
|
||||
* @return Follower|false|null The Follower object or false on failure.
|
||||
*/
|
||||
public static function get_follower_by_actor( $actor ) {
|
||||
global $wpdb;
|
||||
@ -147,7 +156,7 @@ class Followers {
|
||||
* @param int $number Maximum number of results to return.
|
||||
* @param int $page Page number.
|
||||
* @param array $args The WP_Query arguments.
|
||||
* @return array List of `Follower` objects.
|
||||
* @return Follower[] List of `Follower` objects.
|
||||
*/
|
||||
public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) {
|
||||
$data = self::get_followers_with_count( $user_id, $number, $page, $args );
|
||||
@ -165,7 +174,7 @@ class Followers {
|
||||
* @return array {
|
||||
* Data about the followers.
|
||||
*
|
||||
* @type array $followers List of `Follower` objects.
|
||||
* @type Follower[] $followers List of `Follower` objects.
|
||||
* @type int $total Total number of followers.
|
||||
* }
|
||||
*/
|
||||
@ -179,7 +188,7 @@ class Followers {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'activitypub_user_id',
|
||||
'key' => '_activitypub_user_id',
|
||||
'value' => $user_id,
|
||||
),
|
||||
),
|
||||
@ -188,12 +197,8 @@ class Followers {
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
$query = new WP_Query( $args );
|
||||
$total = $query->found_posts;
|
||||
$followers = array_map(
|
||||
function ( $post ) {
|
||||
return Follower::init_from_cpt( $post );
|
||||
},
|
||||
$query->get_posts()
|
||||
);
|
||||
$followers = array_map( array( Follower::class, 'init_from_cpt' ), $query->get_posts() );
|
||||
$followers = array_filter( $followers );
|
||||
|
||||
return compact( 'followers', 'total' );
|
||||
}
|
||||
@ -201,7 +206,7 @@ class Followers {
|
||||
/**
|
||||
* Get all Followers.
|
||||
*
|
||||
* @return array The Term list of Followers.
|
||||
* @return Follower[] The Term list of Followers.
|
||||
*/
|
||||
public static function get_all_followers() {
|
||||
$args = array(
|
||||
@ -210,11 +215,11 @@ class Followers {
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'key' => '_activitypub_actor_json',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
@ -238,15 +243,15 @@ class Followers {
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'activitypub_user_id',
|
||||
'key' => '_activitypub_user_id',
|
||||
'value' => $user_id,
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'key' => '_activitypub_actor_json',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
@ -257,7 +262,7 @@ class Followers {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all Inboxes for a Users Followers.
|
||||
* Returns all Inboxes for an Actor's Followers.
|
||||
*
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
*
|
||||
@ -271,7 +276,7 @@ class Followers {
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
// Get all Followers of a ID of the WordPress User.
|
||||
// Get all Followers of an ID of the WordPress User.
|
||||
$posts = new WP_Query(
|
||||
array(
|
||||
'nopaging' => true,
|
||||
@ -281,15 +286,15 @@ class Followers {
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_user_id',
|
||||
'key' => '_activitypub_user_id',
|
||||
'value' => $user_id,
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'value' => '',
|
||||
'compare' => '!=',
|
||||
),
|
||||
@ -309,7 +314,7 @@ class Followers {
|
||||
$wpdb->prepare(
|
||||
"SELECT DISTINCT meta_value FROM {$wpdb->postmeta}
|
||||
WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ")
|
||||
AND meta_key = 'activitypub_inbox'
|
||||
AND meta_key = '_activitypub_inbox'
|
||||
AND meta_value IS NOT NULL",
|
||||
$posts
|
||||
)
|
||||
@ -321,13 +326,63 @@ class Followers {
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Inboxes for a given Activity.
|
||||
*
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param int $actor_id The WordPress Actor ID.
|
||||
* @param int $batch_size Optional. The batch size. Default 50.
|
||||
* @param int $offset Optional. The offset. Default 0.
|
||||
*
|
||||
* @return array The list of Inboxes.
|
||||
*/
|
||||
public static function get_inboxes_for_activity( $json, $actor_id, $batch_size = 50, $offset = 0 ) {
|
||||
$inboxes = self::get_inboxes( $actor_id );
|
||||
|
||||
if ( self::maybe_add_inboxes_of_blog_user( $json, $actor_id ) ) {
|
||||
$inboxes = array_fill_keys( $inboxes, 1 );
|
||||
foreach ( self::get_inboxes( Actors::BLOG_USER_ID ) as $inbox ) {
|
||||
$inboxes[ $inbox ] = 1;
|
||||
}
|
||||
$inboxes = array_keys( $inboxes );
|
||||
}
|
||||
|
||||
return array_slice( $inboxes, $offset, $batch_size );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe add Inboxes of the Blog User.
|
||||
*
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param int $actor_id The WordPress Actor ID.
|
||||
* @return bool True if the Inboxes of the Blog User should be added, false otherwise.
|
||||
*/
|
||||
public static function maybe_add_inboxes_of_blog_user( $json, $actor_id ) {
|
||||
// Only if we're in both Blog and User modes.
|
||||
if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
|
||||
return false;
|
||||
}
|
||||
// Only if this isn't the Blog Actor.
|
||||
if ( Actors::BLOG_USER_ID === $actor_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$activity = json_decode( $json, true );
|
||||
// Only if this is an Update or Delete. Create handles its own "Announce" in dual user mode.
|
||||
if ( ! in_array( $activity['type'] ?? null, array( 'Update', 'Delete' ), true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Followers that have not been updated for a given time.
|
||||
*
|
||||
* @param int $number Optional. Limits the result. Default 50.
|
||||
* @param int $older_than Optional. The time in seconds. Default 86400 (1 day).
|
||||
*
|
||||
* @return array The Term list of Followers.
|
||||
* @return Follower[] The Term list of Followers.
|
||||
*/
|
||||
public static function get_outdated_followers( $number = 50, $older_than = 86400 ) {
|
||||
$args = array(
|
||||
@ -345,13 +400,9 @@ class Followers {
|
||||
);
|
||||
|
||||
$posts = new WP_Query( $args );
|
||||
$items = array();
|
||||
$items = array_map( array( Follower::class, 'init_from_cpt' ), $posts->get_posts() );
|
||||
|
||||
foreach ( $posts->get_posts() as $follower ) {
|
||||
$items[] = Follower::init_from_cpt( $follower );
|
||||
}
|
||||
|
||||
return $items;
|
||||
return array_filter( $items );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -359,7 +410,7 @@ class Followers {
|
||||
*
|
||||
* @param int $number Optional. The number of Followers to return. Default 20.
|
||||
*
|
||||
* @return array The Term list of Followers.
|
||||
* @return Follower[] The Term list of Followers.
|
||||
*/
|
||||
public static function get_faulty_followers( $number = 20 ) {
|
||||
$args = array(
|
||||
@ -369,24 +420,24 @@ class Followers {
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'activitypub_errors',
|
||||
'key' => '_activitypub_errors',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'key' => '_activitypub_actor_json',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'value' => '',
|
||||
'compare' => '=',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'key' => '_activitypub_actor_json',
|
||||
'value' => '',
|
||||
'compare' => '=',
|
||||
),
|
||||
@ -394,13 +445,9 @@ class Followers {
|
||||
);
|
||||
|
||||
$posts = new WP_Query( $args );
|
||||
$items = array();
|
||||
$items = array_map( array( Follower::class, 'init_from_cpt' ), $posts->get_posts() );
|
||||
|
||||
foreach ( $posts->get_posts() as $follower ) {
|
||||
$items[] = Follower::init_from_cpt( $follower );
|
||||
}
|
||||
|
||||
return $items;
|
||||
return array_filter( $items );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -428,7 +475,7 @@ class Followers {
|
||||
|
||||
return add_post_meta(
|
||||
$post_id,
|
||||
'activitypub_errors',
|
||||
'_activitypub_errors',
|
||||
$error_message
|
||||
);
|
||||
}
|
||||
|
@ -7,10 +7,12 @@
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use Activitypub\Webfinger;
|
||||
use WP_Comment_Query;
|
||||
use Activitypub\Comment;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\is_post_disabled;
|
||||
use function Activitypub\url_to_commentid;
|
||||
use function Activitypub\object_id_to_comment;
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
@ -27,7 +29,7 @@ class Interactions {
|
||||
*
|
||||
* @param array $activity The activity-object.
|
||||
*
|
||||
* @return array|false The comment data or false on failure.
|
||||
* @return int|false|\WP_Error The comment ID or false or WP_Error on failure.
|
||||
*/
|
||||
public static function add_comment( $activity ) {
|
||||
$commentdata = self::activity_to_comment( $activity );
|
||||
@ -36,7 +38,8 @@ class Interactions {
|
||||
return false;
|
||||
}
|
||||
|
||||
$in_reply_to = \esc_url_raw( $activity['object']['inReplyTo'] );
|
||||
$in_reply_to = object_to_uri( $activity['object']['inReplyTo'] );
|
||||
$in_reply_to = \esc_url_raw( $in_reply_to );
|
||||
$comment_post_id = \url_to_postid( $in_reply_to );
|
||||
$parent_comment_id = url_to_commentid( $in_reply_to );
|
||||
|
||||
@ -46,8 +49,7 @@ class Interactions {
|
||||
$comment_post_id = $parent_comment->comment_post_ID;
|
||||
}
|
||||
|
||||
// Not a reply to a post or comment.
|
||||
if ( ! $comment_post_id ) {
|
||||
if ( is_post_disabled( $comment_post_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -97,27 +99,26 @@ class Interactions {
|
||||
}
|
||||
|
||||
$url = object_to_uri( $activity['object'] );
|
||||
$comment_post_id = url_to_postid( $url );
|
||||
$comment_post_id = \url_to_postid( $url );
|
||||
$parent_comment_id = url_to_commentid( $url );
|
||||
|
||||
if ( ! $comment_post_id && $parent_comment_id ) {
|
||||
$parent_comment = get_comment( $parent_comment_id );
|
||||
$parent_comment = \get_comment( $parent_comment_id );
|
||||
$comment_post_id = $parent_comment->comment_post_ID;
|
||||
}
|
||||
|
||||
if ( ! $comment_post_id ) {
|
||||
if ( ! $comment_post_id || is_post_disabled( $comment_post_id ) ) {
|
||||
// Not a reply to a post or comment.
|
||||
return false;
|
||||
}
|
||||
|
||||
$type = $activity['type'];
|
||||
$comment_type = Comment::get_comment_type_by_activity_type( $activity['type'] );
|
||||
|
||||
if ( ! Comment::is_registered_comment_type( $type ) ) {
|
||||
if ( ! $comment_type ) {
|
||||
// Not a valid comment type.
|
||||
return false;
|
||||
}
|
||||
|
||||
$comment_type = Comment::get_comment_type( $type );
|
||||
$comment_content = $comment_type['excerpt'];
|
||||
|
||||
$commentdata['comment_post_ID'] = $comment_post_id;
|
||||
@ -186,12 +187,11 @@ class Interactions {
|
||||
array(
|
||||
'key' => 'protocol',
|
||||
'value' => 'activitypub',
|
||||
'compare' => '=',
|
||||
),
|
||||
),
|
||||
);
|
||||
$comment_query = new WP_Comment_Query( $args );
|
||||
return $comment_query->comments;
|
||||
|
||||
return get_comments( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -229,7 +229,7 @@ class Interactions {
|
||||
*/
|
||||
public static function activity_to_comment( $activity ) {
|
||||
$comment_content = null;
|
||||
$actor = object_to_uri( $activity['actor'] );
|
||||
$actor = object_to_uri( $activity['actor'] ?? null );
|
||||
$actor = get_remote_metadata_by_actor( $actor );
|
||||
|
||||
// Check Actor-Meta.
|
||||
@ -246,22 +246,29 @@ class Interactions {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = object_to_uri( $actor['url'] );
|
||||
$url = object_to_uri( $actor['url'] ?? $actor['id'] );
|
||||
|
||||
if ( ! $url ) {
|
||||
object_to_uri( $actor['id'] );
|
||||
$url = object_to_uri( $actor['id'] );
|
||||
}
|
||||
|
||||
if ( isset( $activity['object']['content'] ) ) {
|
||||
$comment_content = \addslashes( $activity['object']['content'] );
|
||||
}
|
||||
|
||||
$webfinger = Webfinger::uri_to_acct( $url );
|
||||
if ( is_wp_error( $webfinger ) ) {
|
||||
$webfinger = '';
|
||||
} else {
|
||||
$webfinger = str_replace( 'acct:', '', $webfinger );
|
||||
}
|
||||
|
||||
$commentdata = array(
|
||||
'comment_author' => \esc_attr( $comment_author ),
|
||||
'comment_author_url' => \esc_url_raw( $url ),
|
||||
'comment_content' => $comment_content,
|
||||
'comment_type' => 'comment',
|
||||
'comment_author_email' => '',
|
||||
'comment_author_email' => $webfinger,
|
||||
'comment_meta' => array(
|
||||
'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ),
|
||||
'protocol' => 'activitypub',
|
||||
@ -289,7 +296,7 @@ class Interactions {
|
||||
*/
|
||||
public static function persist( $commentdata, $action = self::INSERT ) {
|
||||
// Disable flood control.
|
||||
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
|
||||
\remove_action( 'check_comment_flood', 'check_comment_flood_db' );
|
||||
// Do not require email for AP entries.
|
||||
\add_filter( 'pre_option_require_name_email', '__return_false' );
|
||||
// No nonce possible for this submission route.
|
||||
@ -307,7 +314,7 @@ class Interactions {
|
||||
$state = \wp_update_comment( $commentdata, true );
|
||||
}
|
||||
|
||||
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
|
||||
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ) );
|
||||
\remove_filter( 'pre_option_require_name_email', '__return_false' );
|
||||
// Restore flood control.
|
||||
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
|
||||
@ -318,4 +325,25 @@ class Interactions {
|
||||
return $state; // Either WP_Comment, false, a WP_Error, 0, or 1!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of interactions by type for a given ID.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param string $type The type of interaction to count.
|
||||
*
|
||||
* @return int The total number of interactions.
|
||||
*/
|
||||
public static function count_by_type( $post_id, $type ) {
|
||||
return \get_comments(
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'status' => 'approve',
|
||||
'type' => $type,
|
||||
'count' => true,
|
||||
'paging' => false,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,351 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox collection file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use Activitypub\Dispatcher;
|
||||
use Activitypub\Scheduler;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Activity\Base_Object;
|
||||
|
||||
use function Activitypub\add_to_outbox;
|
||||
|
||||
/**
|
||||
* ActivityPub Outbox Collection
|
||||
*
|
||||
* @link https://www.w3.org/TR/activitypub/#outbox
|
||||
*/
|
||||
class Outbox {
|
||||
const POST_TYPE = 'ap_outbox';
|
||||
|
||||
/**
|
||||
* Add an Item to the outbox.
|
||||
*
|
||||
* @param Activity $activity Full Activity object that will be added to the outbox.
|
||||
* @param int $user_id The real or imaginary user ID of the actor that published the activity that will be added to the outbox.
|
||||
* @param string $visibility Optional. The visibility of the content. Default: `ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC`. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`.
|
||||
*
|
||||
* @return false|int|\WP_Error The added item or an error.
|
||||
*/
|
||||
public static function add( Activity $activity, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) {
|
||||
$actor_type = Actors::get_type_by_id( $user_id );
|
||||
$object_id = self::get_object_id( $activity );
|
||||
$title = self::get_object_title( $activity->get_object() );
|
||||
|
||||
if ( ! $activity->get_actor() ) {
|
||||
$activity->set_actor( Actors::get_by_id( $user_id )->get_id() );
|
||||
}
|
||||
|
||||
$outbox_item = array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_title' => sprintf(
|
||||
/* translators: 1. Activity type, 2. Object Title or Excerpt */
|
||||
__( '[%1$s] %2$s', 'activitypub' ),
|
||||
$activity->get_type(),
|
||||
\wp_trim_words( $title, 5 )
|
||||
),
|
||||
'post_content' => wp_slash( $activity->to_json() ),
|
||||
// ensure that user ID is not below 0.
|
||||
'post_author' => \max( $user_id, 0 ),
|
||||
'post_status' => 'pending',
|
||||
'meta_input' => array(
|
||||
'_activitypub_object_id' => $object_id,
|
||||
'_activitypub_activity_type' => $activity->get_type(),
|
||||
'_activitypub_activity_actor' => $actor_type,
|
||||
'activitypub_content_visibility' => $visibility,
|
||||
),
|
||||
);
|
||||
|
||||
$has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' );
|
||||
if ( $has_kses ) {
|
||||
// Prevent KSES from corrupting JSON in post_content.
|
||||
\kses_remove_filters();
|
||||
}
|
||||
|
||||
$id = \wp_insert_post( $outbox_item, true );
|
||||
|
||||
// Update the activity ID if the post was inserted successfully.
|
||||
if ( $id && ! \is_wp_error( $id ) ) {
|
||||
$activity->set_id( \get_the_guid( $id ) );
|
||||
|
||||
\wp_update_post(
|
||||
array(
|
||||
'ID' => $id,
|
||||
'post_content' => \wp_slash( $activity->to_json() ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( $has_kses ) {
|
||||
\kses_init_filters();
|
||||
}
|
||||
|
||||
if ( \is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
if ( ! $id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self::invalidate_existing_items( $object_id, $activity->get_type(), $id );
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate existing outbox items with the same activity type and object ID
|
||||
* by setting their status to 'publish'.
|
||||
*
|
||||
* @param string $object_id The ID of the activity object.
|
||||
* @param string $activity_type The type of the activity.
|
||||
* @param int $current_id The ID of the current outbox item to exclude.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function invalidate_existing_items( $object_id, $activity_type, $current_id ) {
|
||||
// Do not invalidate items for Announce activities.
|
||||
if ( 'Announce' === $activity_type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$meta_query = array(
|
||||
array(
|
||||
'key' => '_activitypub_object_id',
|
||||
'value' => $object_id,
|
||||
),
|
||||
);
|
||||
|
||||
// For non-Delete activities, only invalidate items of the same type.
|
||||
if ( 'Delete' !== $activity_type ) {
|
||||
$meta_query[] = array(
|
||||
'key' => '_activitypub_activity_type',
|
||||
'value' => $activity_type,
|
||||
);
|
||||
}
|
||||
|
||||
$existing_items = get_posts(
|
||||
array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => 'pending',
|
||||
'exclude' => array( $current_id ),
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => $meta_query,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $existing_items as $existing_item_id ) {
|
||||
$event_args = array(
|
||||
Dispatcher::$callback,
|
||||
$existing_item_id,
|
||||
Dispatcher::$batch_size,
|
||||
\get_post_meta( $existing_item_id, '_activitypub_outbox_offset', true ) ?: 0, // phpcs:ignore
|
||||
);
|
||||
|
||||
$timestamp = \wp_next_scheduled( 'activitypub_async_batch', $event_args );
|
||||
\wp_unschedule_event( $timestamp, 'activitypub_async_batch', $event_args );
|
||||
|
||||
$timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $existing_item_id ) );
|
||||
\wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $existing_item_id ) );
|
||||
|
||||
\wp_publish_post( $existing_item_id );
|
||||
\delete_post_meta( $existing_item_id, '_activitypub_outbox_offset' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Undo activity.
|
||||
*
|
||||
* @param int|\WP_Post $outbox_item The Outbox post or post ID.
|
||||
*
|
||||
* @return int|bool The ID of the outbox item or false on failure.
|
||||
*/
|
||||
public static function undo( $outbox_item ) {
|
||||
$outbox_item = get_post( $outbox_item );
|
||||
$activity = self::get_activity( $outbox_item );
|
||||
|
||||
$type = 'Undo';
|
||||
if ( 'Create' === $activity->get_type() ) {
|
||||
$type = 'Delete';
|
||||
} elseif ( 'Add' === $activity->get_type() ) {
|
||||
$type = 'Remove';
|
||||
}
|
||||
|
||||
return add_to_outbox( $activity, $type, $outbox_item->post_author );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reschedule an activity.
|
||||
*
|
||||
* @param int|\WP_Post $outbox_item The Outbox post or post ID.
|
||||
*
|
||||
* @return bool True if the activity was rescheduled, false otherwise.
|
||||
*/
|
||||
public static function reschedule( $outbox_item ) {
|
||||
$outbox_item = get_post( $outbox_item );
|
||||
|
||||
$outbox_item->post_status = 'pending';
|
||||
$outbox_item->post_date = current_time( 'mysql' );
|
||||
|
||||
wp_update_post( $outbox_item );
|
||||
|
||||
Scheduler::schedule_outbox_activity_for_federation( $outbox_item->ID );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Activity object from the Outbox item.
|
||||
*
|
||||
* @param int|\WP_Post $outbox_item The Outbox post or post ID.
|
||||
* @return Activity|\WP_Error The Activity object or WP_Error.
|
||||
*/
|
||||
public static function get_activity( $outbox_item ) {
|
||||
$outbox_item = get_post( $outbox_item );
|
||||
$actor = self::get_actor( $outbox_item );
|
||||
if ( is_wp_error( $actor ) ) {
|
||||
return $actor;
|
||||
}
|
||||
|
||||
$activity_object = \json_decode( $outbox_item->post_content, true );
|
||||
$type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
|
||||
|
||||
if ( $activity_object['type'] === $type ) {
|
||||
$activity = Activity::init_from_array( $activity_object );
|
||||
if ( ! $activity->get_actor() ) {
|
||||
$activity->set_actor( $actor->get_id() );
|
||||
}
|
||||
} else {
|
||||
$activity = new Activity();
|
||||
$activity->set_type( $type );
|
||||
$activity->set_id( $outbox_item->guid );
|
||||
$activity->set_actor( $actor->get_id() );
|
||||
// Pre-fill the Activity with data (for example cc and to).
|
||||
$activity->set_object( $activity_object );
|
||||
}
|
||||
|
||||
if ( 'Update' === $type ) {
|
||||
$activity->set_updated( gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, strtotime( $outbox_item->post_modified ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the Activity object before it is returned.
|
||||
*
|
||||
* @param Activity $activity The Activity object.
|
||||
* @param \WP_Post $outbox_item The outbox item post object.
|
||||
*/
|
||||
return apply_filters( 'activitypub_get_outbox_activity', $activity, $outbox_item );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Actor object from the Outbox item.
|
||||
*
|
||||
* @param \WP_Post $outbox_item The Outbox post.
|
||||
*
|
||||
* @return \Activitypub\Model\User|\Activitypub\Model\Blog|\WP_Error The Actor object or WP_Error.
|
||||
*/
|
||||
public static function get_actor( $outbox_item ) {
|
||||
$actor_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_actor', true );
|
||||
|
||||
switch ( $actor_type ) {
|
||||
case 'blog':
|
||||
$actor_id = Actors::BLOG_USER_ID;
|
||||
break;
|
||||
case 'application':
|
||||
$actor_id = Actors::APPLICATION_USER_ID;
|
||||
break;
|
||||
case 'user':
|
||||
default:
|
||||
$actor_id = $outbox_item->post_author;
|
||||
break;
|
||||
}
|
||||
|
||||
return Actors::get_by_id( $actor_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Activity object from the Outbox item.
|
||||
*
|
||||
* @param \WP_Post $outbox_item The Outbox post.
|
||||
*
|
||||
* @return Activity|\WP_Error The Activity object or WP_Error.
|
||||
*/
|
||||
public static function maybe_get_activity( $outbox_item ) {
|
||||
if ( ! $outbox_item instanceof \WP_Post ) {
|
||||
return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' );
|
||||
}
|
||||
|
||||
if ( 'ap_outbox' !== $outbox_item->post_type ) {
|
||||
return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' );
|
||||
}
|
||||
|
||||
// Check if Outbox Activity is public.
|
||||
$visibility = \get_post_meta( $outbox_item->ID, 'activitypub_content_visibility', true );
|
||||
|
||||
if ( ! in_array( $visibility, array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ), true ) ) {
|
||||
return new \WP_Error( 'private_outbox_item', 'Not a public Outbox item.' );
|
||||
}
|
||||
|
||||
$activity_types = \apply_filters( 'rest_activitypub_outbox_activity_types', array( 'Announce', 'Create', 'Like', 'Update' ) );
|
||||
$activity_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
|
||||
|
||||
if ( ! in_array( $activity_type, $activity_types, true ) ) {
|
||||
return new \WP_Error( 'private_outbox_item', 'Not public Outbox item type.' );
|
||||
}
|
||||
|
||||
return self::get_activity( $outbox_item );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object ID of an activity.
|
||||
*
|
||||
* @param Activity|Base_Object|string $data The activity object.
|
||||
*
|
||||
* @return string The object ID.
|
||||
*/
|
||||
private static function get_object_id( $data ) {
|
||||
$object = $data->get_object();
|
||||
|
||||
if ( is_object( $object ) ) {
|
||||
return self::get_object_id( $object );
|
||||
}
|
||||
|
||||
if ( is_string( $object ) ) {
|
||||
return $object;
|
||||
}
|
||||
|
||||
return $data->get_id() ?? $data->get_actor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title of an activity recursively.
|
||||
*
|
||||
* @param Base_Object $activity_object The activity object.
|
||||
*
|
||||
* @return string The title.
|
||||
*/
|
||||
private static function get_object_title( $activity_object ) {
|
||||
if ( ! $activity_object ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( is_string( $activity_object ) ) {
|
||||
$post_id = url_to_postid( $activity_object );
|
||||
|
||||
return $post_id ? get_the_title( $post_id ) : '';
|
||||
}
|
||||
|
||||
$title = $activity_object->get_name() ?? $activity_object->get_content();
|
||||
|
||||
if ( ! $title && $activity_object->get_object() instanceof Base_Object ) {
|
||||
$title = $activity_object->get_object()->get_name() ?? $activity_object->get_object()->get_content();
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
}
|
@ -12,9 +12,14 @@ use WP_Comment;
|
||||
use WP_Error;
|
||||
|
||||
use Activitypub\Comment;
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Transformer\Post as PostTransformer;
|
||||
use Activitypub\Transformer\Comment as CommentTransformer;
|
||||
|
||||
use function Activitypub\is_post_disabled;
|
||||
use function Activitypub\is_local_comment;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
|
||||
/**
|
||||
* Class containing code for getting replies Collections and CollectionPages of posts and comments.
|
||||
@ -23,13 +28,14 @@ class Replies {
|
||||
/**
|
||||
* Build base arguments for fetching the comments of either a WordPress post or comment.
|
||||
*
|
||||
* @param WP_Post|WP_Comment $wp_object The post or comment to fetch replies for.
|
||||
* @param WP_Post|WP_Comment|WP_Error $wp_object The post or comment to fetch replies for on success.
|
||||
*/
|
||||
private static function build_args( $wp_object ) {
|
||||
$args = array(
|
||||
'status' => 'approve',
|
||||
'orderby' => 'comment_date_gmt',
|
||||
'order' => 'ASC',
|
||||
'type' => 'comment',
|
||||
);
|
||||
|
||||
if ( $wp_object instanceof WP_Post ) {
|
||||
@ -44,23 +50,6 @@ class Replies {
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds pagination args comments query.
|
||||
*
|
||||
* @param array $args Query args built by self::build_args.
|
||||
* @param int $page The current pagination page.
|
||||
* @param int $comments_per_page The number of comments per page.
|
||||
*/
|
||||
private static function add_pagination_args( $args, $page, $comments_per_page ) {
|
||||
$args['number'] = $comments_per_page;
|
||||
|
||||
$offset = intval( $page ) * $comments_per_page;
|
||||
$args['offset'] = $offset;
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get the replies collections ID.
|
||||
*
|
||||
@ -74,22 +63,22 @@ class Replies {
|
||||
} elseif ( $wp_object instanceof WP_Comment ) {
|
||||
return get_rest_url_by_path( sprintf( 'comments/%d/replies', $wp_object->comment_ID ) );
|
||||
} else {
|
||||
return new WP_Error();
|
||||
return new WP_Error( 'unsupported_object', 'The object is not a post or comment.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the replies collection.
|
||||
* Get the Replies collection.
|
||||
*
|
||||
* @param WP_Post|WP_Comment $wp_object The post or comment to fetch replies for.
|
||||
*
|
||||
* @return array An associative array containing the replies collection without JSON-LD context.
|
||||
* @return array|\WP_Error|null An associative array containing the replies collection without JSON-LD context on success.
|
||||
*/
|
||||
public static function get_collection( $wp_object ) {
|
||||
$id = self::get_id( $wp_object );
|
||||
|
||||
if ( ! $id ) {
|
||||
return null;
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return \wp_is_serving_rest_request() ? $id : null;
|
||||
}
|
||||
|
||||
$replies = array(
|
||||
@ -97,38 +86,11 @@ class Replies {
|
||||
'type' => 'Collection',
|
||||
);
|
||||
|
||||
$replies['first'] = self::get_collection_page( $wp_object, 0, $replies['id'] );
|
||||
$replies['first'] = self::get_collection_page( $wp_object, 1, $replies['id'] );
|
||||
|
||||
return $replies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub ID's from a list of comments.
|
||||
*
|
||||
* It takes only federated/non-local comments into account, others also do not have an
|
||||
* ActivityPub ID available.
|
||||
*
|
||||
* @param WP_Comment[] $comments The comments to retrieve the ActivityPub ids from.
|
||||
*
|
||||
* @return string[] A list of the ActivityPub ID's.
|
||||
*/
|
||||
private static function get_reply_ids( $comments ) {
|
||||
$comment_ids = array();
|
||||
// Only add external comments from the fediverse.
|
||||
// Maybe use the Comment class more and the function is_local_comment etc.
|
||||
foreach ( $comments as $comment ) {
|
||||
if ( is_local_comment( $comment ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$public_comment_id = Comment::get_source_id( $comment->comment_ID );
|
||||
if ( $public_comment_id ) {
|
||||
$comment_ids[] = $public_comment_id;
|
||||
}
|
||||
}
|
||||
return $comment_ids;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a replies collection page as an associative array.
|
||||
*
|
||||
@ -136,33 +98,34 @@ class Replies {
|
||||
*
|
||||
* @param WP_Post|WP_Comment $wp_object The post of comment the replies are for.
|
||||
* @param int $page The current pagination page.
|
||||
* @param string $part_of The collection id/url the returned CollectionPage belongs to.
|
||||
* @param string $part_of Optional. The collection id/url the returned CollectionPage belongs to. Default null.
|
||||
*
|
||||
* @return array A CollectionPage as an associative array.
|
||||
* @return array|WP_Error|null A CollectionPage as an associative array on success, WP_Error or null on failure.
|
||||
*/
|
||||
public static function get_collection_page( $wp_object, $page, $part_of = null ) {
|
||||
// Build initial arguments for fetching approved comments.
|
||||
$args = self::build_args( $wp_object );
|
||||
if ( is_wp_error( $args ) ) {
|
||||
return \wp_is_serving_rest_request() ? $args : null;
|
||||
}
|
||||
|
||||
// Retrieve the partOf if not already given.
|
||||
$part_of = $part_of ?? self::get_id( $wp_object );
|
||||
|
||||
// If the collection page does not exist.
|
||||
if ( is_wp_error( $args ) || is_wp_error( $part_of ) ) {
|
||||
return null;
|
||||
if ( is_wp_error( $part_of ) ) {
|
||||
return \wp_is_serving_rest_request() ? $part_of : null;
|
||||
}
|
||||
|
||||
// Get to total replies count.
|
||||
$total_replies = \get_comments( array_merge( $args, array( 'count' => true ) ) );
|
||||
|
||||
// Modify query args to retrieve paginated results.
|
||||
$comments_per_page = \get_option( 'comments_per_page' );
|
||||
// If set to zero, we get errors below. You need at least one comment per page, here.
|
||||
$args['number'] = max( (int) \get_option( 'comments_per_page' ), 1 );
|
||||
$args['offset'] = intval( $page - 1 ) * $args['number'];
|
||||
|
||||
// Fetch internal and external comments for current page.
|
||||
$comments = get_comments( self::add_pagination_args( $args, $page, $comments_per_page ) );
|
||||
|
||||
// Get the ActivityPub ID's of the comments, without out local-only comments.
|
||||
$comment_ids = self::get_reply_ids( $comments );
|
||||
// Get the ActivityPub ID's of the comments, without local-only comments.
|
||||
$comment_ids = self::get_reply_ids( \get_comments( $args ) );
|
||||
|
||||
// Build the associative CollectionPage array.
|
||||
$collection_page = array(
|
||||
@ -172,10 +135,92 @@ class Replies {
|
||||
'items' => $comment_ids,
|
||||
);
|
||||
|
||||
if ( $total_replies / $comments_per_page > $page + 1 ) {
|
||||
if ( ( $total_replies / $args['number'] ) > $page ) {
|
||||
$collection_page['next'] = \add_query_arg( 'page', $page + 1, $part_of );
|
||||
}
|
||||
|
||||
if ( $page > 1 ) {
|
||||
$collection_page['prev'] = \add_query_arg( 'page', $page - 1, $part_of );
|
||||
}
|
||||
|
||||
return $collection_page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context collection for a post.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return array|false The context for the post or false if the post is not found or disabled.
|
||||
*/
|
||||
public static function get_context_collection( $post_id ) {
|
||||
$post = \get_post( $post_id );
|
||||
|
||||
if ( ! $post || is_post_disabled( $post_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$comments = \get_comments(
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'type' => 'comment',
|
||||
'status' => 'approve',
|
||||
'orderby' => 'comment_date_gmt',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
$ids = self::get_reply_ids( $comments, true );
|
||||
$post_uri = ( new PostTransformer( $post ) )->to_id();
|
||||
\array_unshift( $ids, $post_uri );
|
||||
|
||||
$author = Actors::get_by_id( $post->post_author );
|
||||
if ( is_wp_error( $author ) ) {
|
||||
if ( is_user_type_disabled( 'blog' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$author = new Blog();
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'OrderedCollection',
|
||||
'url' => \get_permalink( $post_id ),
|
||||
'attributedTo' => $author->get_id(),
|
||||
'totalItems' => count( $ids ),
|
||||
'items' => $ids,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub ID's from a list of comments.
|
||||
*
|
||||
* It takes only federated/non-local comments into account, others also do not have an
|
||||
* ActivityPub ID available.
|
||||
*
|
||||
* @param WP_Comment[] $comments The comments to retrieve the ActivityPub ids from.
|
||||
* @param boolean $include_blog_comments Optional. Include blog comments in the returned array. Default false.
|
||||
*
|
||||
* @return string[] A list of the ActivityPub ID's.
|
||||
*/
|
||||
private static function get_reply_ids( $comments, $include_blog_comments = false ) {
|
||||
$comment_ids = array();
|
||||
|
||||
foreach ( $comments as $comment ) {
|
||||
if ( is_local_comment( $comment ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$public_comment_id = Comment::get_source_id( $comment->comment_ID );
|
||||
if ( $public_comment_id ) {
|
||||
$comment_ids[] = $public_comment_id;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $include_blog_comments ) {
|
||||
$comment_ids[] = ( new CommentTransformer( $comment ) )->to_id();
|
||||
}
|
||||
}
|
||||
|
||||
return \array_unique( $comment_ids );
|
||||
}
|
||||
}
|
||||
|
@ -7,36 +7,12 @@
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use WP_Error;
|
||||
use WP_User_Query;
|
||||
use Activitypub\Model\User;
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Model\Application;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\normalize_url;
|
||||
use function Activitypub\normalize_host;
|
||||
use function Activitypub\url_to_authorid;
|
||||
use function Activitypub\is_user_disabled;
|
||||
|
||||
/**
|
||||
* Users collection.
|
||||
*/
|
||||
class Users {
|
||||
/**
|
||||
* The ID of the Blog User.
|
||||
*
|
||||
* @var int
|
||||
* @deprecated version 4.2.0
|
||||
*/
|
||||
const BLOG_USER_ID = 0;
|
||||
|
||||
/**
|
||||
* The ID of the Application User.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const APPLICATION_USER_ID = -1;
|
||||
|
||||
class Users extends Actors {
|
||||
/**
|
||||
* Get the User by ID.
|
||||
*
|
||||
@ -45,31 +21,9 @@ class Users {
|
||||
* @return User|Blog|Application|WP_Error The User or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_id( $user_id ) {
|
||||
if ( is_string( $user_id ) || is_numeric( $user_id ) ) {
|
||||
$user_id = (int) $user_id;
|
||||
}
|
||||
_deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_id' );
|
||||
|
||||
if ( is_user_disabled( $user_id ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
if ( self::BLOG_USER_ID === $user_id ) {
|
||||
return new Blog();
|
||||
} elseif ( self::APPLICATION_USER_ID === $user_id ) {
|
||||
return new Application();
|
||||
} elseif ( $user_id > 0 ) {
|
||||
return User::from_wp_user( $user_id );
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
return parent::get_by_id( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -80,66 +34,9 @@ class Users {
|
||||
* @return User|Blog|Application|WP_Error The User or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_username( $username ) {
|
||||
// Check for blog user.
|
||||
if ( Blog::get_default_username() === $username ) {
|
||||
return new Blog();
|
||||
}
|
||||
_deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_username' );
|
||||
|
||||
if ( get_option( 'activitypub_blog_identifier' ) === $username ) {
|
||||
return new Blog();
|
||||
}
|
||||
|
||||
// Check for application user.
|
||||
if ( 'application' === $username ) {
|
||||
return new Application();
|
||||
}
|
||||
|
||||
// Check for 'activitypub_username' meta.
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'count_total' => false,
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'activitypub_user_identifier',
|
||||
'value' => $username,
|
||||
'compare' => 'LIKE',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
return self::get_by_id( $user->results[0] );
|
||||
}
|
||||
|
||||
$username = str_replace( array( '*', '%' ), '', $username );
|
||||
|
||||
// Check for login or nicename.
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'count_total' => false,
|
||||
'search' => $username,
|
||||
'search_columns' => array( 'user_login', 'user_nicename' ),
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
return self::get_by_id( $user->results[0] );
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
return parent::get_by_username( $username );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -150,88 +47,9 @@ class Users {
|
||||
* @return User|WP_Error The User or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_resource( $uri ) {
|
||||
$uri = object_to_uri( $uri );
|
||||
_deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_resource' );
|
||||
|
||||
$scheme = 'acct';
|
||||
$match = array();
|
||||
// Try to extract the scheme and the host.
|
||||
if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $uri, $match ) ) {
|
||||
// Extract the scheme.
|
||||
$scheme = \esc_attr( $match[1] );
|
||||
}
|
||||
|
||||
switch ( $scheme ) {
|
||||
// Check for http(s) URIs.
|
||||
case 'http':
|
||||
case 'https':
|
||||
$resource_path = \wp_parse_url( $uri, PHP_URL_PATH );
|
||||
|
||||
if ( $resource_path ) {
|
||||
$blog_path = \wp_parse_url( \home_url(), PHP_URL_PATH );
|
||||
|
||||
if ( $blog_path ) {
|
||||
$resource_path = \str_replace( $blog_path, '', $resource_path );
|
||||
}
|
||||
|
||||
$resource_path = \trim( $resource_path, '/' );
|
||||
|
||||
// Check for http(s)://blog.example.com/@username.
|
||||
if ( str_starts_with( $resource_path, '@' ) ) {
|
||||
$identifier = \str_replace( '@', '', $resource_path );
|
||||
$identifier = \trim( $identifier, '/' );
|
||||
|
||||
return self::get_by_username( $identifier );
|
||||
}
|
||||
}
|
||||
|
||||
// Check for http(s)://blog.example.com/author/username.
|
||||
$user_id = url_to_authorid( $uri );
|
||||
|
||||
if ( $user_id ) {
|
||||
return self::get_by_id( $user_id );
|
||||
}
|
||||
|
||||
// Check for http(s)://blog.example.com/.
|
||||
if (
|
||||
normalize_url( site_url() ) === normalize_url( $uri ) ||
|
||||
normalize_url( home_url() ) === normalize_url( $uri )
|
||||
) {
|
||||
return self::get_by_id( self::BLOG_USER_ID );
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_no_user_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
// Check for acct URIs.
|
||||
case 'acct':
|
||||
$uri = \str_replace( 'acct:', '', $uri );
|
||||
$identifier = \substr( $uri, 0, \strrpos( $uri, '@' ) );
|
||||
$host = normalize_host( \substr( \strrchr( $uri, '@' ), 1 ) );
|
||||
$blog_host = normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) );
|
||||
|
||||
if ( $blog_host !== $host ) {
|
||||
return new WP_Error(
|
||||
'activitypub_wrong_host',
|
||||
\__( 'Resource host does not match blog host', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare wildcards https://github.com/mastodon/mastodon/issues/22213.
|
||||
if ( in_array( $identifier, array( '_', '*', '' ), true ) ) {
|
||||
return self::get_by_id( self::BLOG_USER_ID );
|
||||
}
|
||||
|
||||
return self::get_by_username( $identifier );
|
||||
default:
|
||||
return new WP_Error(
|
||||
'activitypub_wrong_scheme',
|
||||
\__( 'Wrong scheme', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
return parent::get_by_resource( $uri );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -242,26 +60,9 @@ class Users {
|
||||
* @return User|Blog|Application|WP_Error The User or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_various( $id ) {
|
||||
$user = null;
|
||||
_deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_various' );
|
||||
|
||||
if ( is_numeric( $id ) ) {
|
||||
$user = self::get_by_id( $id );
|
||||
} elseif (
|
||||
// Is URL.
|
||||
filter_var( $id, FILTER_VALIDATE_URL ) ||
|
||||
// Is acct.
|
||||
str_starts_with( $id, 'acct:' ) ||
|
||||
// Is email.
|
||||
filter_var( $id, FILTER_VALIDATE_EMAIL )
|
||||
) {
|
||||
$user = self::get_by_resource( $id );
|
||||
}
|
||||
|
||||
if ( $user && ! is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
return self::get_by_username( $id );
|
||||
return parent::get_by_various( $id );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -270,18 +71,8 @@ class Users {
|
||||
* @return array The User collection.
|
||||
*/
|
||||
public static function get_collection() {
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
_deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_collection' );
|
||||
|
||||
$return = array();
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$return[] = User::from_wp_user( $user->ID );
|
||||
}
|
||||
|
||||
return $return;
|
||||
return parent::get_collection();
|
||||
}
|
||||
}
|
||||
|
@ -25,25 +25,6 @@ if ( ! function_exists( 'str_starts_with' ) ) {
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_self_link' ) ) {
|
||||
/**
|
||||
* Returns the link for the currently displayed feed.
|
||||
*
|
||||
* @return string Correct link for the atom:self element.
|
||||
*/
|
||||
function get_self_link() {
|
||||
$host = wp_parse_url( home_url() );
|
||||
$path = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||
|
||||
/**
|
||||
* Filters the self link.
|
||||
*
|
||||
* @param string $link The self link.
|
||||
*/
|
||||
return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . $path ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'is_countable' ) ) {
|
||||
/**
|
||||
* Polyfill for `is_countable()` function added in PHP 7.3.
|
||||
@ -115,3 +96,16 @@ if ( ! function_exists( 'str_contains' ) ) {
|
||||
return false !== strpos( $haystack, $needle );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_is_serving_rest_request' ) ) {
|
||||
/**
|
||||
* Polyfill for `wp_is_serving_rest_request()` function added in WordPress 6.5.
|
||||
*
|
||||
* @see https://developer.wordpress.org/reference/functions/wp_is_serving_rest_request/
|
||||
*
|
||||
* @return bool True if it's a WordPress REST API request, false otherwise.
|
||||
*/
|
||||
function wp_is_serving_rest_request() {
|
||||
return defined( 'REST_REQUEST' ) && REST_REQUEST;
|
||||
}
|
||||
}
|
||||
|
76
wp-content/plugins/activitypub/includes/constants.php
Normal file
76
wp-content/plugins/activitypub/includes/constants.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin constants.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
// The following constants can be defined in your wp-config.php file to override the default values.
|
||||
|
||||
\defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || \define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' );
|
||||
\defined( 'ACTIVITYPUB_EXCERPT_LENGTH' ) || \define( 'ACTIVITYPUB_EXCERPT_LENGTH', 400 );
|
||||
\defined( 'ACTIVITYPUB_NOTE_LENGTH' ) || \define( 'ACTIVITYPUB_NOTE_LENGTH', 400 );
|
||||
\defined( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS' ) || \define( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS', true );
|
||||
\defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || \define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 3 );
|
||||
\defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=<p>)|(?<=<br>)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' );
|
||||
\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' );
|
||||
\defined( 'ACTIVITYPUB_URL_REGEXP' ) || \define( 'ACTIVITYPUB_URL_REGEXP', '(https?:|www\.)\S+[\w\/]' );
|
||||
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title type=\"html\"]\n\n[ap_content]\n\n[ap_hashtags]" );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS', false );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false );
|
||||
\defined( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE' ) || \define( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE', 'wordpress-post-format' );
|
||||
\defined( 'ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE' ) || \define( 'ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE', 100 );
|
||||
|
||||
// The following constants are invariable and define values used throughout the plugin.
|
||||
|
||||
/*
|
||||
* Mastodon HTML sanitizer.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#sanitization
|
||||
*/
|
||||
\define(
|
||||
'ACTIVITYPUB_MASTODON_HTML_SANITIZER',
|
||||
array(
|
||||
'p' => array(),
|
||||
'span' => array( 'class' => true ),
|
||||
'br' => array(),
|
||||
'a' => array(
|
||||
'href' => true,
|
||||
'rel' => true,
|
||||
'class' => true,
|
||||
),
|
||||
'del' => array(),
|
||||
'pre' => array(),
|
||||
'code' => array(),
|
||||
'em' => array(),
|
||||
'strong' => array(),
|
||||
'b' => array(),
|
||||
'i' => array(),
|
||||
'u' => array(),
|
||||
'ul' => array(),
|
||||
'ol' => array(
|
||||
'start' => true,
|
||||
'reversed' => true,
|
||||
),
|
||||
'li' => array( 'value' => true ),
|
||||
'blockquote' => array(),
|
||||
'h1' => array(),
|
||||
'h2' => array(),
|
||||
'h3' => array(),
|
||||
'h4' => array(),
|
||||
)
|
||||
);
|
||||
|
||||
\define( 'ACTIVITYPUB_DATE_TIME_RFC3339', 'Y-m-d\TH:i:s\Z' );
|
||||
|
||||
// Define Actor-Modes for the plugin.
|
||||
\define( 'ACTIVITYPUB_ACTOR_MODE', 'actor' );
|
||||
\define( 'ACTIVITYPUB_BLOG_MODE', 'blog' );
|
||||
\define( 'ACTIVITYPUB_ACTOR_AND_BLOG_MODE', 'actor_blog' );
|
||||
|
||||
// Post visibility constants.
|
||||
\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC', '' );
|
||||
\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC', 'quiet_public' );
|
||||
\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE', 'private' );
|
||||
\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL', 'local' );
|
@ -19,4 +19,61 @@ function allow_localhost( $parsed_args ) {
|
||||
|
||||
return $parsed_args;
|
||||
}
|
||||
add_filter( 'http_request_args', '\Activitypub\allow_localhost' );
|
||||
\add_filter( 'http_request_args', '\Activitypub\allow_localhost' );
|
||||
|
||||
/**
|
||||
* Debug the outbox post type.
|
||||
*
|
||||
* @param array $args The arguments for the post type.
|
||||
* @param string $post_type The post type.
|
||||
*
|
||||
* @return array The arguments for the post type.
|
||||
*/
|
||||
function debug_outbox_post_type( $args, $post_type ) {
|
||||
if ( 'ap_outbox' !== $post_type ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$args['show_ui'] = true;
|
||||
$args['menu_icon'] = 'dashicons-upload';
|
||||
|
||||
return $args;
|
||||
}
|
||||
\add_filter( 'register_post_type_args', '\Activitypub\debug_outbox_post_type', 10, 2 );
|
||||
|
||||
/**
|
||||
* Debug the outbox post type column.
|
||||
*
|
||||
* @param array $columns The columns.
|
||||
* @param string $post_type The post type.
|
||||
*
|
||||
* @return array The updated columns.
|
||||
*/
|
||||
function debug_outbox_post_type_column( $columns, $post_type ) {
|
||||
if ( 'ap_outbox' !== $post_type ) {
|
||||
return $columns;
|
||||
}
|
||||
|
||||
$columns['ap_outbox_meta'] = 'Meta';
|
||||
|
||||
return $columns;
|
||||
}
|
||||
\add_filter( 'manage_posts_columns', '\Activitypub\debug_outbox_post_type_column', 10, 2 );
|
||||
|
||||
/**
|
||||
* Debug the outbox post type meta.
|
||||
*
|
||||
* @param string $column_name The column name.
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function manage_posts_custom_column( $column_name, $post_id ) {
|
||||
if ( 'ap_outbox_meta' === $column_name ) {
|
||||
$meta = \get_post_meta( $post_id );
|
||||
foreach ( $meta as $key => $value ) {
|
||||
echo \esc_attr( $key ) . ': ' . \esc_html( $value[0] ) . '<br>';
|
||||
}
|
||||
}
|
||||
}
|
||||
\add_action( 'manage_posts_custom_column', '\Activitypub\manage_posts_custom_column', 10, 2 );
|
||||
|
@ -9,8 +9,13 @@ namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Activity\Base_Object;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Outbox;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Transformer\Post;
|
||||
use Activitypub\Transformer\Factory as Transformer_Factory;
|
||||
|
||||
/**
|
||||
* Returns the ActivityPub default JSON-context.
|
||||
@ -20,6 +25,15 @@ use Activitypub\Collection\Users;
|
||||
function get_context() {
|
||||
$context = Activity::JSON_LD_CONTEXT;
|
||||
|
||||
/**
|
||||
* Filters the ActivityPub JSON-LD context.
|
||||
*
|
||||
* This filter allows developers to modify or extend the JSON-LD context used
|
||||
* in ActivityPub responses. The context defines the vocabulary and terms used
|
||||
* in the ActivityPub JSON objects.
|
||||
*
|
||||
* @param array $context The default ActivityPub JSON-LD context array.
|
||||
*/
|
||||
return \apply_filters( 'activitypub_json_context', $context );
|
||||
}
|
||||
|
||||
@ -61,100 +75,28 @@ function get_webfinger_resource( $user_id ) {
|
||||
/**
|
||||
* Requests the Meta-Data from the Actors profile.
|
||||
*
|
||||
* @param string $actor The Actor URL.
|
||||
* @param array|string $actor The Actor array or URL.
|
||||
* @param bool $cached Optional. Whether the result should be cached. Default true.
|
||||
*
|
||||
* @return array|WP_Error The Actor profile as array or WP_Error on failure.
|
||||
*/
|
||||
function get_remote_metadata_by_actor( $actor, $cached = true ) {
|
||||
/**
|
||||
* Filters the metadata before it is retrieved from a remote actor.
|
||||
*
|
||||
* Passing a non-false value will effectively short-circuit the remote request,
|
||||
* returning that value instead.
|
||||
*
|
||||
* @param mixed $pre The value to return instead of the remote metadata.
|
||||
* Default false to continue with the remote request.
|
||||
* @param string $actor The actor URL.
|
||||
*/
|
||||
$pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor );
|
||||
if ( $pre ) {
|
||||
return $pre;
|
||||
}
|
||||
|
||||
if ( is_array( $actor ) ) {
|
||||
if ( array_key_exists( 'id', $actor ) ) {
|
||||
$actor = $actor['id'];
|
||||
} elseif ( array_key_exists( 'url', $actor ) ) {
|
||||
$actor = $actor['url'];
|
||||
} else {
|
||||
return new WP_Error(
|
||||
'activitypub_no_valid_actor_identifier',
|
||||
\__( 'The "actor" identifier is not valid', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
'actor' => $actor,
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $actor ) ) {
|
||||
$actor = Webfinger::resolve( $actor );
|
||||
}
|
||||
|
||||
if ( ! $actor ) {
|
||||
return new WP_Error(
|
||||
'activitypub_no_valid_actor_identifier',
|
||||
\__( 'The "actor" identifier is not valid', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
'actor' => $actor,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( is_wp_error( $actor ) ) {
|
||||
return $actor;
|
||||
}
|
||||
|
||||
$transient_key = 'activitypub_' . $actor;
|
||||
|
||||
// Only check the cache if needed.
|
||||
if ( $cached ) {
|
||||
$metadata = \get_transient( $transient_key );
|
||||
|
||||
if ( $metadata ) {
|
||||
return $metadata;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! \wp_http_validate_url( $actor ) ) {
|
||||
$metadata = new WP_Error(
|
||||
'activitypub_no_valid_actor_url',
|
||||
\__( 'The "actor" is no valid URL', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'actor' => $actor,
|
||||
)
|
||||
);
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$response = Http::get( $actor );
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$metadata = \wp_remote_retrieve_body( $response );
|
||||
$metadata = \json_decode( $metadata, true );
|
||||
|
||||
if ( ! $metadata ) {
|
||||
$metadata = new WP_Error(
|
||||
'activitypub_invalid_json',
|
||||
\__( 'No valid JSON data', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'actor' => $actor,
|
||||
)
|
||||
);
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
\set_transient( $transient_key, $metadata, WEEK_IN_SECONDS );
|
||||
|
||||
return $metadata;
|
||||
return Http::get_remote_object( $actor, $cached );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -186,22 +128,20 @@ function count_followers( $user_id ) {
|
||||
*
|
||||
* @param string $url Permalink to check.
|
||||
*
|
||||
* @return int User ID, or 0 on failure.
|
||||
* @return int|null User ID, or null on failure.
|
||||
*/
|
||||
function url_to_authorid( $url ) {
|
||||
global $wp_rewrite;
|
||||
|
||||
// Check if url hase the same host.
|
||||
if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) !== \wp_parse_url( $url, \PHP_URL_HOST ) ) {
|
||||
return 0;
|
||||
$request_host = \wp_parse_url( $url, \PHP_URL_HOST );
|
||||
if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) !== $request_host && get_option( 'activitypub_old_host' ) !== $request_host ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// First, check to see if there is a 'author=N' to match against.
|
||||
// First, check to see if there is an 'author=N' to match against.
|
||||
if ( \preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) {
|
||||
$id = \absint( $values[1] );
|
||||
if ( $id ) {
|
||||
return $id;
|
||||
}
|
||||
return \absint( $values[1] );
|
||||
}
|
||||
|
||||
// Check to see if we are using rewrite rules.
|
||||
@ -209,7 +149,7 @@ function url_to_authorid( $url ) {
|
||||
|
||||
// Not using rewrite rules, and 'author=N' method failed, so we're out of options.
|
||||
if ( empty( $rewrite ) ) {
|
||||
return 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
// Generate rewrite rule for the author url.
|
||||
@ -224,7 +164,7 @@ function url_to_authorid( $url ) {
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -346,112 +286,113 @@ function esc_hashtag( $input ) {
|
||||
* @return bool False by default.
|
||||
*/
|
||||
function is_activitypub_request() {
|
||||
global $wp_query;
|
||||
return Query::get_instance()->is_activitypub_request();
|
||||
}
|
||||
|
||||
/*
|
||||
* ActivityPub requests are currently only made for
|
||||
* author archives, singular posts, and the homepage.
|
||||
/**
|
||||
* Check if a post is disabled for ActivityPub.
|
||||
*
|
||||
* This function checks if the post type supports ActivityPub and if the post is set to be local.
|
||||
*
|
||||
* @param mixed $post The post object or ID.
|
||||
*
|
||||
* @return boolean True if the post is disabled, false otherwise.
|
||||
*/
|
||||
if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) ) {
|
||||
return false;
|
||||
}
|
||||
function is_post_disabled( $post ) {
|
||||
$post = \get_post( $post );
|
||||
$disabled = false;
|
||||
|
||||
// Check if the current post type supports ActivityPub.
|
||||
if ( \is_singular() ) {
|
||||
$queried_object = \get_queried_object();
|
||||
$post_type = \get_post_type( $queried_object );
|
||||
|
||||
if ( ! \post_type_supports( $post_type, 'activitypub' ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if header already sent.
|
||||
if ( ! \headers_sent() && ACTIVITYPUB_SEND_VARY_HEADER ) {
|
||||
// Send Vary header for Accept header.
|
||||
\header( 'Vary: Accept' );
|
||||
}
|
||||
|
||||
// One can trigger an ActivityPub request by adding ?activitypub to the URL.
|
||||
if ( isset( $wp_query->query_vars['activitypub'] ) ) {
|
||||
if ( ! $post ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* The other (more common) option to make an ActivityPub request
|
||||
* is to send an Accept header.
|
||||
*/
|
||||
if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
|
||||
$accept = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
|
||||
$visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
|
||||
|
||||
/*
|
||||
* $accept can be a single value, or a comma separated list of values.
|
||||
* We want to support both scenarios,
|
||||
* and return true when the header includes at least one of the following:
|
||||
* - application/activity+json
|
||||
* - application/ld+json
|
||||
* - application/json
|
||||
*/
|
||||
if ( preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
|
||||
return true;
|
||||
}
|
||||
if (
|
||||
ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL === $visibility ||
|
||||
ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE === $visibility ||
|
||||
! \post_type_supports( $post->post_type, 'activitypub' ) ||
|
||||
'private' === $post->post_status ||
|
||||
! empty( $post->post_password )
|
||||
) {
|
||||
$disabled = true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow plugins to disable posts for ActivityPub.
|
||||
*
|
||||
* @param boolean $disabled True if the post is disabled, false otherwise.
|
||||
* @param \WP_Post $post The post object.
|
||||
*/
|
||||
return \apply_filters( 'activitypub_is_post_disabled', $disabled, $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* This function checks if a user is enabled for ActivityPub.
|
||||
*
|
||||
* @param int|string $user_id The user ID.
|
||||
* @return boolean True if the user is enabled, false otherwise.
|
||||
*/
|
||||
function user_can_activitypub( $user_id ) {
|
||||
if ( ! is_numeric( $user_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
switch ( $user_id ) {
|
||||
case Actors::APPLICATION_USER_ID:
|
||||
$enabled = true; // Application user is always enabled.
|
||||
break;
|
||||
|
||||
case Actors::BLOG_USER_ID:
|
||||
$enabled = ! is_user_type_disabled( 'blog' );
|
||||
break;
|
||||
|
||||
default:
|
||||
if ( ! \get_user_by( 'id', $user_id ) ) {
|
||||
$enabled = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( is_user_type_disabled( 'user' ) ) {
|
||||
$enabled = false;
|
||||
break;
|
||||
}
|
||||
|
||||
$enabled = \user_can( $user_id, 'activitypub' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow plugins to disable users for ActivityPub.
|
||||
*
|
||||
* @deprecated 5.7.0 Use the `activitypub_user_can_activitypub` filter instead.
|
||||
*
|
||||
* @param boolean $disabled True if the user is disabled, false otherwise.
|
||||
* @param int $user_id The user ID.
|
||||
*/
|
||||
$enabled = ! \apply_filters_deprecated( 'activitypub_is_user_disabled', array( ! $enabled, $user_id ), '5.7.0', 'activitypub_user_can_activitypub' );
|
||||
|
||||
/**
|
||||
* Allow plugins to enable/disable users for ActivityPub.
|
||||
*
|
||||
* @param boolean $enabled True if the user is enabled, false otherwise.
|
||||
* @param int $user_id The user ID.
|
||||
*/
|
||||
return apply_filters( 'activitypub_user_can_activitypub', $enabled, $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* This function checks if a user is disabled for ActivityPub.
|
||||
*
|
||||
* @deprecated 5.7.0 Use the `user_can_activitypub` function instead.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return boolean True if the user is disabled, false otherwise.
|
||||
*/
|
||||
function is_user_disabled( $user_id ) {
|
||||
$return = false;
|
||||
_deprecated_function( __FUNCTION__, 'unreleased', 'user_can_activitypub' );
|
||||
|
||||
switch ( $user_id ) {
|
||||
// if the user is the application user, it's always enabled.
|
||||
case \Activitypub\Collection\Users::APPLICATION_USER_ID:
|
||||
$return = false;
|
||||
break;
|
||||
// if the user is the blog user, it's only enabled in single-user mode.
|
||||
case \Activitypub\Collection\Users::BLOG_USER_ID:
|
||||
if ( is_user_type_disabled( 'blog' ) ) {
|
||||
$return = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$return = false;
|
||||
break;
|
||||
// if the user is any other user, it's enabled if it can publish posts.
|
||||
default:
|
||||
if ( ! \get_user_by( 'id', $user_id ) ) {
|
||||
$return = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( is_user_type_disabled( 'user' ) ) {
|
||||
$return = true;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ! \user_can( $user_id, 'activitypub' ) ) {
|
||||
$return = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$return = false;
|
||||
break;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow plugins to disable users for ActivityPub.
|
||||
*
|
||||
* @param boolean $return True if the user is disabled, false otherwise.
|
||||
* @param int $user_id The User-ID.
|
||||
*/
|
||||
return apply_filters( 'activitypub_is_user_disabled', $return, $user_id );
|
||||
return ! user_can_activitypub( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -469,45 +410,45 @@ function is_user_type_disabled( $type ) {
|
||||
case 'blog':
|
||||
if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
|
||||
if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
|
||||
$return = false;
|
||||
$disabled = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) {
|
||||
$return = ACTIVITYPUB_DISABLE_BLOG_USER;
|
||||
$disabled = ACTIVITYPUB_DISABLE_BLOG_USER;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( '1' !== \get_option( 'activitypub_enable_blog_user', '0' ) ) {
|
||||
$return = true;
|
||||
if ( ACTIVITYPUB_ACTOR_MODE === \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
|
||||
$disabled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$return = false;
|
||||
$disabled = false;
|
||||
break;
|
||||
case 'user':
|
||||
if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
|
||||
if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
|
||||
$return = true;
|
||||
$disabled = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if ( \defined( 'ACTIVITYPUB_DISABLE_USER' ) ) {
|
||||
$return = ACTIVITYPUB_DISABLE_USER;
|
||||
$disabled = ACTIVITYPUB_DISABLE_USER;
|
||||
break;
|
||||
}
|
||||
|
||||
if ( '1' !== \get_option( 'activitypub_enable_users', '1' ) ) {
|
||||
$return = true;
|
||||
if ( ACTIVITYPUB_BLOG_MODE === \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
|
||||
$disabled = true;
|
||||
break;
|
||||
}
|
||||
|
||||
$return = false;
|
||||
$disabled = false;
|
||||
break;
|
||||
default:
|
||||
$return = new WP_Error(
|
||||
$disabled = new WP_Error(
|
||||
'activitypub_wrong_user_type',
|
||||
__( 'Wrong user type', 'activitypub' ),
|
||||
array( 'status' => 400 )
|
||||
@ -518,10 +459,10 @@ function is_user_type_disabled( $type ) {
|
||||
/**
|
||||
* Allow plugins to disable user types for ActivityPub.
|
||||
*
|
||||
* @param boolean $return True if the user type is disabled, false otherwise.
|
||||
* @param boolean $disabled True if the user type is disabled, false otherwise.
|
||||
* @param string $type The User-Type.
|
||||
*/
|
||||
return apply_filters( 'activitypub_is_user_type_disabled', $return, $type );
|
||||
return apply_filters( 'activitypub_is_user_type_disabled', $disabled, $type );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -546,14 +487,6 @@ function is_single_user() {
|
||||
* @return boolean True if the site supports the block editor, false otherwise.
|
||||
*/
|
||||
function site_supports_blocks() {
|
||||
if ( \version_compare( \get_bloginfo( 'version' ), '5.9', '<' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! \function_exists( 'register_block_type_from_metadata' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow plugins to disable block editor support,
|
||||
* thus disabling blocks registered by the ActivityPub plugin.
|
||||
@ -575,7 +508,7 @@ function is_json( $data ) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whther a blog is public based on the `blog_public` option.
|
||||
* Check whether a blog is public based on the `blog_public` option.
|
||||
*
|
||||
* @return bool True if public, false if not
|
||||
*/
|
||||
@ -588,21 +521,6 @@ function is_blog_public() {
|
||||
return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a URL.
|
||||
*
|
||||
* @param string $value The URL to sanitize.
|
||||
*
|
||||
* @return string|null The sanitized URL or null if invalid.
|
||||
*/
|
||||
function sanitize_url( $value ) {
|
||||
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return esc_url_raw( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract recipient URLs from Activity object.
|
||||
*
|
||||
@ -663,6 +581,17 @@ function is_activity_public( $data ) {
|
||||
return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if passed Activity is a reply.
|
||||
*
|
||||
* @param array $data The Activity object as array.
|
||||
*
|
||||
* @return boolean True if a reply, false if not.
|
||||
*/
|
||||
function is_activity_reply( $data ) {
|
||||
return ! empty( $data['object']['inReplyTo'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get active users based on a given duration.
|
||||
*
|
||||
@ -701,7 +630,7 @@ function get_active_users( $duration = 1 ) {
|
||||
}
|
||||
|
||||
// If blog user is disabled.
|
||||
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
|
||||
if ( ! user_can_activitypub( Actors::BLOG_USER_ID ) ) {
|
||||
return (int) $count;
|
||||
}
|
||||
|
||||
@ -733,7 +662,7 @@ function get_total_users() {
|
||||
}
|
||||
|
||||
// If blog user is disabled.
|
||||
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
|
||||
if ( ! user_can_activitypub( Actors::BLOG_USER_ID ) ) {
|
||||
return (int) $users;
|
||||
}
|
||||
|
||||
@ -776,6 +705,10 @@ function object_to_uri( $data ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if ( is_object( $data ) ) {
|
||||
$data = $data->to_array();
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if it is a list, then take first item.
|
||||
* This plugin does not support collections.
|
||||
@ -796,6 +729,9 @@ function object_to_uri( $data ) {
|
||||
|
||||
// Return part of Object that makes most sense.
|
||||
switch ( $type ) {
|
||||
case 'Image':
|
||||
$data = $data['url'];
|
||||
break;
|
||||
case 'Link':
|
||||
$data = $data['href'];
|
||||
break;
|
||||
@ -881,7 +817,6 @@ function set_wp_object_state( $wp_object, $state ) {
|
||||
* Allow plugins to mark WordPress objects as federated.
|
||||
*
|
||||
* @param \WP_Comment|\WP_Post $wp_object The WordPress object.
|
||||
* @param string $state The state of the object.
|
||||
*/
|
||||
\apply_filters( 'activitypub_mark_wp_object_as_federated', $wp_object );
|
||||
}
|
||||
@ -905,6 +840,7 @@ function get_wp_object_state( $wp_object ) {
|
||||
/**
|
||||
* Allow plugins to get the federation state of a WordPress object.
|
||||
*
|
||||
* @param false $state The state of the object.
|
||||
* @param \WP_Comment|\WP_Post $wp_object The WordPress object.
|
||||
*/
|
||||
return \apply_filters( 'activitypub_get_wp_object_state', false, $wp_object );
|
||||
@ -943,6 +879,7 @@ function get_post_type_description( $post_type ) {
|
||||
* Allow plugins to get the description of a post type.
|
||||
*
|
||||
* @param string $description The description of the post type.
|
||||
* @param string $post_type_name The post type name.
|
||||
* @param \WP_Post_Type $post_type The post type object.
|
||||
*/
|
||||
return apply_filters( 'activitypub_post_type_description', $description, $post_type->name, $post_type );
|
||||
@ -980,6 +917,11 @@ function get_enclosures( $post_id ) {
|
||||
|
||||
$enclosures = array_map(
|
||||
function ( $enclosure ) {
|
||||
// Check if the enclosure is a string.
|
||||
if ( ! $enclosure || ! is_string( $enclosure ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$attributes = explode( "\n", $enclosure );
|
||||
|
||||
if ( ! isset( $attributes[0] ) || ! \wp_http_validate_url( $attributes[0] ) ) {
|
||||
@ -988,8 +930,8 @@ function get_enclosures( $post_id ) {
|
||||
|
||||
return array(
|
||||
'url' => $attributes[0],
|
||||
'length' => isset( $attributes[1] ) ? trim( $attributes[1] ) : null,
|
||||
'mediaType' => isset( $attributes[2] ) ? trim( $attributes[2] ) : null,
|
||||
'length' => $attributes[1] ?? null,
|
||||
'mediaType' => $attributes[2] ?? 'application/octet-stream',
|
||||
);
|
||||
},
|
||||
$enclosures
|
||||
@ -1007,7 +949,7 @@ function get_enclosures( $post_id ) {
|
||||
*
|
||||
* @param int|\WP_Comment $comment Comment ID or comment object.
|
||||
*
|
||||
* @return \WP_Comment[] Array of ancestor comments or empty array if there are none.
|
||||
* @return int[] Array of ancestor IDs.
|
||||
*/
|
||||
function get_comment_ancestors( $comment ) {
|
||||
$comment = \get_comment( $comment );
|
||||
@ -1135,16 +1077,44 @@ function normalize_host( $host ) {
|
||||
return \str_replace( 'www.', '', $host );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reply intent URI as a JavaScript URI.
|
||||
*
|
||||
* @return string The reply intent URI.
|
||||
*/
|
||||
function get_reply_intent_js() {
|
||||
return sprintf(
|
||||
'javascript:(()=>{window.open(\'%s\'+encodeURIComponent(window.location.href));})();',
|
||||
get_reply_intent_url()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the reply intent URI.
|
||||
*
|
||||
* @return string The reply intent URI.
|
||||
*/
|
||||
function get_reply_intent_uri() {
|
||||
return sprintf(
|
||||
'javascript:(()=>{window.open(\'%s\'+encodeURIComponent(window.location.href));})();',
|
||||
esc_url( \admin_url( 'post-new.php?in_reply_to=' ) )
|
||||
);
|
||||
function get_reply_intent_url() {
|
||||
/**
|
||||
* Filters the reply intent parameters.
|
||||
*
|
||||
* @param array $params The reply intent parameters.
|
||||
*/
|
||||
$params = \apply_filters( 'activitypub_reply_intent_params', array() );
|
||||
|
||||
$params += array( 'in_reply_to' => '' );
|
||||
$query = \http_build_query( $params );
|
||||
$path = 'post-new.php?' . $query;
|
||||
$url = \admin_url( $path );
|
||||
|
||||
/**
|
||||
* Filters the reply intent URL.
|
||||
*
|
||||
* @param string $url The reply intent URL.
|
||||
*/
|
||||
$url = \apply_filters( 'activitypub_reply_intent_url', $url );
|
||||
|
||||
return esc_url_raw( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -1239,11 +1209,7 @@ function generate_post_summary( $post, $length = 500 ) {
|
||||
$content = \sanitize_post_field( 'post_excerpt', $post->post_excerpt, $post->ID );
|
||||
|
||||
if ( $content ) {
|
||||
/**
|
||||
* Filters the post excerpt.
|
||||
*
|
||||
* @param string $content The post excerpt.
|
||||
*/
|
||||
/** This filter is documented in wp-includes/post-template.php */
|
||||
return \apply_filters( 'the_excerpt', $content );
|
||||
}
|
||||
|
||||
@ -1281,6 +1247,7 @@ function generate_post_summary( $post, $length = 500 ) {
|
||||
|
||||
/*
|
||||
Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629
|
||||
/** This filter is documented in wp-includes/post-template.php
|
||||
return \apply_filters( 'the_excerpt', $content );
|
||||
*/
|
||||
return $content;
|
||||
@ -1306,3 +1273,334 @@ function get_content_warning( $post_id ) {
|
||||
|
||||
return $warning;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub ID of a User by the WordPress User ID.
|
||||
*
|
||||
* @param int $id The WordPress User ID.
|
||||
*
|
||||
* @return string The ActivityPub ID (a URL) of the User.
|
||||
*/
|
||||
function get_user_id( $id ) {
|
||||
$user = Actors::get_by_id( $id );
|
||||
|
||||
if ( ! $user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $user->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub ID of a Post by the WordPress Post ID.
|
||||
*
|
||||
* @param int $id The WordPress Post ID.
|
||||
*
|
||||
* @return string The ActivityPub ID (a URL) of the Post.
|
||||
*/
|
||||
function get_post_id( $id ) {
|
||||
$post = get_post( $id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$transformer = new Post( $post );
|
||||
return $transformer->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a URL is from the same domain as the site.
|
||||
*
|
||||
* @param string $url The URL to check.
|
||||
*
|
||||
* @return boolean True if the URL is from the same domain, false otherwise.
|
||||
*/
|
||||
function is_same_domain( $url ) {
|
||||
$remote = \wp_parse_url( $url, PHP_URL_HOST );
|
||||
|
||||
if ( ! $remote ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$remote = normalize_host( $remote );
|
||||
$self = normalize_host( home_host() );
|
||||
|
||||
return $remote === $self;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the visibility of a post.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return string|false The visibility of the post or false if not found.
|
||||
*/
|
||||
function get_content_visibility( $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
if ( ! $post ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
|
||||
$_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC;
|
||||
$options = array(
|
||||
ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC,
|
||||
ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE,
|
||||
ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL,
|
||||
);
|
||||
|
||||
if ( in_array( $visibility, $options, true ) ) {
|
||||
$_visibility = $visibility;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the visibility of a post.
|
||||
*
|
||||
* @param string $_visibility The visibility of the post. Possible values are:
|
||||
* - 'public': Post is public and federated.
|
||||
* - 'quiet_public': Post is public but not federated.
|
||||
* - 'local': Post is only visible locally.
|
||||
* @param \WP_Post $post The post object.
|
||||
*/
|
||||
return \apply_filters( 'activitypub_content_visibility', $_visibility, $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the Host for the current site where the front end is accessible.
|
||||
*
|
||||
* @return string The host for the current site.
|
||||
*/
|
||||
function home_host() {
|
||||
return \wp_parse_url( \home_url(), PHP_URL_HOST );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the website hosts allowed to credit this blog.
|
||||
*
|
||||
* @return array|null The attribution domains or null if not found.
|
||||
*/
|
||||
function get_attribution_domains() {
|
||||
if ( '1' !== \get_option( 'activitypub_use_opengraph', '1' ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$domains = \get_option( 'activitypub_attribution_domains', home_host() );
|
||||
$domains = explode( PHP_EOL, $domains );
|
||||
|
||||
if ( ! $domains ) {
|
||||
$domains = null;
|
||||
}
|
||||
|
||||
return $domains;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the base URL for uploads.
|
||||
*
|
||||
* @return string The upload base URL.
|
||||
*/
|
||||
function get_upload_baseurl() {
|
||||
/**
|
||||
* Early filter to allow plugins to set the upload base URL.
|
||||
*
|
||||
* @param string|false $maybe_upload_dir The upload base URL or false if not set.
|
||||
*/
|
||||
$maybe_upload_dir = apply_filters( 'pre_activitypub_get_upload_baseurl', false );
|
||||
if ( false !== $maybe_upload_dir ) {
|
||||
return $maybe_upload_dir;
|
||||
}
|
||||
|
||||
$upload_dir = \wp_get_upload_dir();
|
||||
|
||||
/**
|
||||
* Filters the upload base URL.
|
||||
*
|
||||
* @param string $upload_dir The upload base URL. Default \wp_get_upload_dir()['baseurl']
|
||||
*/
|
||||
return apply_filters( 'activitypub_get_upload_baseurl', $upload_dir['baseurl'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if Authorized-Fetch is enabled.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/admin/config/#authorized_fetch
|
||||
*
|
||||
* @return boolean True if Authorized-Fetch is enabled, false otherwise.
|
||||
*/
|
||||
function use_authorized_fetch() {
|
||||
$use = (bool) \get_option( 'activitypub_authorized_fetch' );
|
||||
|
||||
/**
|
||||
* Filters whether to use Authorized-Fetch.
|
||||
*
|
||||
* @param boolean $use_authorized_fetch True if Authorized-Fetch is enabled, false otherwise.
|
||||
*/
|
||||
return apply_filters( 'activitypub_use_authorized_fetch', $use );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an ID is from the same domain as the site.
|
||||
*
|
||||
* @param string $id The ID URI to check.
|
||||
*
|
||||
* @return boolean True if the ID is a self-pint, false otherwise.
|
||||
*/
|
||||
function is_self_ping( $id ) {
|
||||
$query_string = \wp_parse_url( $id, PHP_URL_QUERY );
|
||||
|
||||
if ( ! $query_string ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$query = array();
|
||||
\parse_str( $query_string, $query );
|
||||
|
||||
if (
|
||||
is_same_domain( $id ) &&
|
||||
in_array( 'c', array_keys( $query ), true )
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add an object to the outbox.
|
||||
*
|
||||
* @param mixed $data The object to add to the outbox.
|
||||
* @param string|null $activity_type Optional. The type of the Activity or null if `$data` is an Activity. Default null.
|
||||
* @param integer $user_id Optional. The User-ID. Default 0.
|
||||
* @param string $content_visibility Optional. The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`. Default null.
|
||||
*
|
||||
* @return boolean|int The ID of the outbox item or false on failure.
|
||||
*/
|
||||
function add_to_outbox( $data, $activity_type = null, $user_id = 0, $content_visibility = null ) {
|
||||
$transformer = Transformer_Factory::get_transformer( $data );
|
||||
|
||||
if ( ! $transformer || is_wp_error( $transformer ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $content_visibility ) {
|
||||
$transformer->set_content_visibility( $content_visibility );
|
||||
} else {
|
||||
$content_visibility = $transformer->get_content_visibility();
|
||||
}
|
||||
|
||||
if ( $activity_type ) {
|
||||
$activity = $transformer->to_activity( $activity_type );
|
||||
} else {
|
||||
$activity = $transformer->to_object();
|
||||
}
|
||||
|
||||
if ( ! $activity || \is_wp_error( $activity ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// If the user is disabled, fall back to the blog user when available.
|
||||
if ( ! user_can_activitypub( $user_id ) ) {
|
||||
if ( user_can_activitypub( Actors::BLOG_USER_ID ) ) {
|
||||
$user_id = Actors::BLOG_USER_ID;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
$outbox_activity_id = Outbox::add( $activity, $user_id, $content_visibility );
|
||||
|
||||
if ( ! $outbox_activity_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action triggered after an object has been added to the outbox.
|
||||
*
|
||||
* @param int $outbox_activity_id The ID of the outbox item.
|
||||
* @param Activity $activity The activity object.
|
||||
* @param int $user_id The User-ID.
|
||||
* @param string $content_visibility The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`.
|
||||
*/
|
||||
\do_action( 'post_activitypub_add_to_outbox', $outbox_activity_id, $activity, $user_id, $content_visibility );
|
||||
|
||||
set_wp_object_state( $data, 'federated' );
|
||||
|
||||
return $outbox_activity_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an `$data` is an Activity.
|
||||
*
|
||||
* @see https://www.w3.org/ns/activitystreams#activities
|
||||
*
|
||||
* @param array|object|string $data The data to check.
|
||||
*
|
||||
* @return boolean True if the `$data` is an Activity, false otherwise.
|
||||
*/
|
||||
function is_activity( $data ) {
|
||||
/**
|
||||
* Filters the activity types.
|
||||
*
|
||||
* @param array $types The activity types.
|
||||
*/
|
||||
$types = apply_filters( 'activitypub_activity_types', Activity::TYPES );
|
||||
|
||||
if ( is_string( $data ) ) {
|
||||
return in_array( $data, $types, true );
|
||||
}
|
||||
|
||||
if ( is_array( $data ) && isset( $data['type'] ) ) {
|
||||
return in_array( $data['type'], $types, true );
|
||||
}
|
||||
|
||||
if ( is_object( $data ) && $data instanceof Base_Object ) {
|
||||
return in_array( $data->get_type(), $types, true );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an `$data` is an Actor.
|
||||
*
|
||||
* @see https://www.w3.org/ns/activitystreams#actor
|
||||
*
|
||||
* @param array|object|string $data The data to check.
|
||||
*
|
||||
* @return boolean True if the `$data` is an Actor, false otherwise.
|
||||
*/
|
||||
function is_actor( $data ) {
|
||||
/**
|
||||
* Filters the actor types.
|
||||
*
|
||||
* @param array $types The actor types.
|
||||
*/
|
||||
$types = apply_filters( 'activitypub_actor_types', Actor::TYPES );
|
||||
|
||||
if ( is_string( $data ) ) {
|
||||
return in_array( $data, $types, true );
|
||||
}
|
||||
|
||||
if ( is_array( $data ) && isset( $data['type'] ) ) {
|
||||
return in_array( $data['type'], $types, true );
|
||||
}
|
||||
|
||||
if ( is_object( $data ) && $data instanceof Base_Object ) {
|
||||
return in_array( $data->get_type(), $types, true );
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ActivityPub embed HTML for a URL.
|
||||
*
|
||||
* @param string $url The URL to get the embed for.
|
||||
* @param boolean $inline_css Whether to inline CSS. Default true.
|
||||
*
|
||||
* @return string|false The embed HTML or false if not found.
|
||||
*/
|
||||
function get_embed_html( $url, $inline_css = true ) {
|
||||
return Embed::get_html( $url, $inline_css );
|
||||
}
|
||||
|
@ -44,10 +44,13 @@ class Announce {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! ACTIVITYPUB_DISABLE_REACTIONS ) {
|
||||
self::maybe_save_announce( $announcement, $user_id );
|
||||
// Check if reposts are allowed.
|
||||
if ( ! Comment::is_comment_type_enabled( 'repost' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::maybe_save_announce( $announcement, $user_id );
|
||||
|
||||
if ( is_string( $announcement['object'] ) ) {
|
||||
$object = Http::get_remote_object( $announcement['object'] );
|
||||
} else {
|
||||
@ -69,7 +72,8 @@ class Announce {
|
||||
*
|
||||
* @param array $object The object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
* @param array $activity The activity object.
|
||||
* @param string $type The type of the activity.
|
||||
* @param \Activitypub\Activity\Activity|null $activity The activity object.
|
||||
*/
|
||||
\do_action( 'activitypub_inbox', $object, $user_id, $type, $activity );
|
||||
|
||||
@ -78,7 +82,7 @@ class Announce {
|
||||
*
|
||||
* @param array $object The object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
* @param array $activity The activity object.
|
||||
* @param \Activitypub\Activity\Activity|null $activity The activity object.
|
||||
*/
|
||||
\do_action( "activitypub_inbox_{$type}", $object, $user_id, $activity );
|
||||
}
|
||||
|
@ -9,6 +9,8 @@ namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Collection\Interactions;
|
||||
|
||||
use function Activitypub\is_self_ping;
|
||||
use function Activitypub\is_activity_reply;
|
||||
use function Activitypub\is_activity_public;
|
||||
use function Activitypub\object_id_to_comment;
|
||||
|
||||
@ -44,8 +46,10 @@ class Create {
|
||||
*/
|
||||
public static function handle_create( $activity, $user_id, $activity_object = null ) {
|
||||
// Check if Activity is public or not.
|
||||
if ( ! is_activity_public( $activity ) ) {
|
||||
// @todo maybe send email.
|
||||
if (
|
||||
! is_activity_public( $activity ) ||
|
||||
! is_activity_reply( $activity )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -58,13 +62,16 @@ class Create {
|
||||
*
|
||||
* @param array $activity The activity-object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
* @param \WP_Comment|\WP_Error $check_dupe The comment object or WP_Error.
|
||||
* @param \Activitypub\Activity\Activity $activity_object The activity object.
|
||||
*/
|
||||
\do_action( 'activitypub_inbox_update', $activity, $user_id, $activity_object );
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_self_ping( $activity['object']['id'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$state = Interactions::add_comment( $activity );
|
||||
$reaction = null;
|
||||
|
||||
@ -95,6 +102,10 @@ class Create {
|
||||
public static function validate_object( $valid, $param, $request ) {
|
||||
$json_params = $request->get_json_params();
|
||||
|
||||
if ( empty( $json_params['type'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
'Create' !== $json_params['type'] ||
|
||||
is_wp_error( $request )
|
||||
@ -103,9 +114,13 @@ class Create {
|
||||
}
|
||||
|
||||
$object = $json_params['object'];
|
||||
|
||||
if ( ! is_array( $object ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$required = array(
|
||||
'id',
|
||||
'inReplyTo',
|
||||
'content',
|
||||
);
|
||||
|
||||
|
@ -12,6 +12,8 @@ use Activitypub\Http;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Interactions;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handles Delete requests.
|
||||
*/
|
||||
@ -20,24 +22,10 @@ class Delete {
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_delete',
|
||||
array( self::class, 'handle_delete' )
|
||||
);
|
||||
|
||||
// Defer signature verification for `Delete` requests.
|
||||
\add_filter(
|
||||
'activitypub_defer_signature_verification',
|
||||
array( self::class, 'defer_signature_verification' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
|
||||
// Side effect.
|
||||
\add_action(
|
||||
'activitypub_delete_actor_interactions',
|
||||
array( self::class, 'delete_interactions' )
|
||||
);
|
||||
\add_action( 'activitypub_inbox_delete', array( self::class, 'handle_delete' ) );
|
||||
\add_filter( 'activitypub_defer_signature_verification', array( self::class, 'defer_signature_verification' ), 10, 2 );
|
||||
\add_action( 'activitypub_delete_actor_interactions', array( self::class, 'delete_interactions' ) );
|
||||
\add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -114,6 +102,7 @@ class Delete {
|
||||
* @param array $activity The delete activity.
|
||||
*/
|
||||
public static function maybe_delete_follower( $activity ) {
|
||||
/* @var \Activitypub\Model\Follower $follower Follower object. */
|
||||
$follower = Followers::get_follower_by_actor( $activity['actor'] );
|
||||
|
||||
// Verify that Actor is deleted.
|
||||
@ -142,15 +131,13 @@ class Delete {
|
||||
/**
|
||||
* Delete comments from an Actor.
|
||||
*
|
||||
* @param array $actor The actor whose comments to delete.
|
||||
* @param string $actor The URL of the actor whose comments to delete.
|
||||
*/
|
||||
public static function delete_interactions( $actor ) {
|
||||
$comments = Interactions::get_interactions_by_actor( $actor );
|
||||
|
||||
if ( is_array( $comments ) ) {
|
||||
foreach ( $comments as $comment ) {
|
||||
wp_delete_comment( $comment->comment_ID );
|
||||
}
|
||||
wp_delete_comment( $comment, true );
|
||||
}
|
||||
}
|
||||
|
||||
@ -192,4 +179,18 @@ class Delete {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the object to the object ID.
|
||||
*
|
||||
* @param \Activitypub\Activity\Activity $activity The Activity object.
|
||||
* @return \Activitypub\Activity\Activity The filtered Activity object.
|
||||
*/
|
||||
public static function outbox_activity( $activity ) {
|
||||
if ( 'Delete' === $activity->get_type() ) {
|
||||
$activity->set_object( object_to_uri( $activity->get_object() ) );
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
}
|
||||
|
@ -7,12 +7,13 @@
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Notification;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Followers;
|
||||
|
||||
use function Activitypub\add_to_outbox;
|
||||
|
||||
/**
|
||||
* Handle Follow requests.
|
||||
*/
|
||||
@ -28,7 +29,7 @@ class Follow {
|
||||
|
||||
\add_action(
|
||||
'activitypub_followers_post_follow',
|
||||
array( self::class, 'send_follow_response' ),
|
||||
array( self::class, 'queue_accept' ),
|
||||
10,
|
||||
4
|
||||
);
|
||||
@ -40,7 +41,7 @@ class Follow {
|
||||
* @param array $activity The activity object.
|
||||
*/
|
||||
public static function handle_follow( $activity ) {
|
||||
$user = Users::get_by_resource( $activity['object'] );
|
||||
$user = Actors::get_by_resource( $activity['object'] );
|
||||
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
// If we can not find a user, we can not initiate a follow process.
|
||||
@ -55,13 +56,15 @@ class Follow {
|
||||
$activity['actor']
|
||||
);
|
||||
|
||||
do_action(
|
||||
'activitypub_followers_post_follow',
|
||||
$activity['actor'],
|
||||
$activity,
|
||||
$user_id,
|
||||
$follower
|
||||
);
|
||||
/**
|
||||
* Fires after a new follower has been added.
|
||||
*
|
||||
* @param string $actor The URL of the actor (follower) who initiated the follow.
|
||||
* @param array $activity The complete activity data of the follow request.
|
||||
* @param int $user_id The ID of the WordPress user being followed.
|
||||
* @param \Activitypub\Model\Follower|\WP_Error $follower The Follower object containing the new follower's data.
|
||||
*/
|
||||
do_action( 'activitypub_followers_post_follow', $activity['actor'], $activity, $user_id, $follower );
|
||||
|
||||
// Send notification.
|
||||
$notification = new Notification(
|
||||
@ -79,9 +82,9 @@ class Follow {
|
||||
* @param string $actor The Actor URL.
|
||||
* @param array $activity_object The Activity object.
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param \Activitypub\Model\Follower $follower The Follower object.
|
||||
* @param \Activitypub\Model\Follower|\WP_Error $follower The Follower object.
|
||||
*/
|
||||
public static function send_follow_response( $actor, $activity_object, $user_id, $follower ) {
|
||||
public static function queue_accept( $actor, $activity_object, $user_id, $follower ) {
|
||||
if ( \is_wp_error( $follower ) ) {
|
||||
// Impossible to send a "Reject" because we can not get the Remote-Inbox.
|
||||
return;
|
||||
@ -100,21 +103,12 @@ class Follow {
|
||||
)
|
||||
);
|
||||
|
||||
$user = Users::get_by_id( $user_id );
|
||||
|
||||
// Get inbox.
|
||||
$inbox = $follower->get_shared_inbox();
|
||||
|
||||
// Send "Accept" activity.
|
||||
$activity = new Activity();
|
||||
$activity->set_type( 'Accept' );
|
||||
$activity->set_actor( Actors::get_by_id( $user_id )->get_id() );
|
||||
$activity->set_object( $activity_object );
|
||||
$activity->set_actor( $user->get_id() );
|
||||
$activity->set_to( $actor );
|
||||
$activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() );
|
||||
$activity->set_to( array( $actor ) );
|
||||
|
||||
$activity = $activity->to_json();
|
||||
|
||||
Http::post( $inbox, $activity, $user_id );
|
||||
add_to_outbox( $activity, null, $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE );
|
||||
}
|
||||
}
|
||||
|
@ -20,12 +20,8 @@ class Like {
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_like',
|
||||
array( self::class, 'handle_like' ),
|
||||
10,
|
||||
3
|
||||
);
|
||||
\add_action( 'activitypub_inbox_like', array( self::class, 'handle_like' ), 10, 2 );
|
||||
\add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -35,7 +31,7 @@ class Like {
|
||||
* @param int $user_id The ID of the local blog user.
|
||||
*/
|
||||
public static function handle_like( $like, $user_id ) {
|
||||
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
|
||||
if ( ! Comment::is_comment_type_enabled( 'like' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -67,4 +63,18 @@ class Like {
|
||||
*/
|
||||
do_action( 'activitypub_handled_like', $like, $user_id, $state, $reaction );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the object to the object ID.
|
||||
*
|
||||
* @param \Activitypub\Activity\Activity $activity The Activity object.
|
||||
* @return \Activitypub\Activity\Activity The filtered Activity object.
|
||||
*/
|
||||
public static function outbox_activity( $activity ) {
|
||||
if ( 'Like' === $activity->get_type() ) {
|
||||
$activity->set_object( object_to_uri( $activity->get_object() ) );
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
}
|
||||
|
213
wp-content/plugins/activitypub/includes/handler/class-move.php
Normal file
213
wp-content/plugins/activitypub/includes/handler/class-move.php
Normal file
@ -0,0 +1,213 @@
|
||||
<?php
|
||||
/**
|
||||
* Move handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Collection\Followers;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handle Move requests.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move
|
||||
* @see https://docs.joinmastodon.org/user/moving/
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#Move
|
||||
*/
|
||||
class Move {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_inbox_move', array( self::class, 'handle_move' ) );
|
||||
\add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Move requests.
|
||||
*
|
||||
* @param array $activity The JSON "Move" Activity.
|
||||
*/
|
||||
public static function handle_move( $activity ) {
|
||||
$target = self::extract_target( $activity );
|
||||
$origin = self::extract_origin( $activity );
|
||||
|
||||
if ( ! $target || ! $origin ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target_object = Http::get_remote_object( $target );
|
||||
$origin_object = Http::get_remote_object( $origin );
|
||||
|
||||
$verified = self::verify_move( $target_object, $origin_object );
|
||||
|
||||
if ( ! $verified ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target_follower = Followers::get_follower_by_actor( $target );
|
||||
$origin_follower = Followers::get_follower_by_actor( $origin );
|
||||
|
||||
/*
|
||||
* If the new target is followed, but the origin is not,
|
||||
* everything is fine, so we can return.
|
||||
*/
|
||||
if ( $target_follower && ! $origin_follower ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the new target is not followed, but the origin is,
|
||||
* update the origin follower to the new target.
|
||||
*/
|
||||
if ( ! $target_follower && $origin_follower ) {
|
||||
$origin_follower->from_array( $target_object );
|
||||
$origin_follower->set_id( $target );
|
||||
$origin_id = $origin_follower->upsert();
|
||||
|
||||
global $wpdb;
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->update(
|
||||
$wpdb->posts,
|
||||
array( 'guid' => sanitize_url( $target ) ),
|
||||
array( 'ID' => sanitize_key( $origin_id ) )
|
||||
);
|
||||
|
||||
// Clear the cache.
|
||||
wp_cache_delete( $origin_id, 'posts' );
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the new target is followed, and the origin is followed,
|
||||
* move users and delete the origin follower.
|
||||
*/
|
||||
if ( $target_follower && $origin_follower ) {
|
||||
$origin_users = \get_post_meta( $origin_follower->get__id(), '_activitypub_user_id', false );
|
||||
$target_users = \get_post_meta( $target_follower->get__id(), '_activitypub_user_id', false );
|
||||
|
||||
// Get all user ids from $origin_users that are not in $target_users.
|
||||
$users = \array_diff( $origin_users, $target_users );
|
||||
|
||||
foreach ( $users as $user_id ) {
|
||||
\add_post_meta( $target_follower->get__id(), '_activitypub_user_id', $user_id );
|
||||
}
|
||||
|
||||
$origin_follower->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object and origin to the correct format.
|
||||
*
|
||||
* @param \Activitypub\Activity\Activity $activity The Activity object.
|
||||
* @return \Activitypub\Activity\Activity The filtered Activity object.
|
||||
*/
|
||||
public static function outbox_activity( $activity ) {
|
||||
if ( 'Move' === $activity->get_type() ) {
|
||||
$activity->set_object( object_to_uri( $activity->get_object() ) );
|
||||
$activity->set_origin( $activity->get_actor() );
|
||||
$activity->set_target( $activity->get_object() );
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the target from the activity.
|
||||
*
|
||||
* The ActivityStreams spec define the `target` attribute as the
|
||||
* destination of the activity, but Mastodon uses the `object`
|
||||
* attribute to move profiles.
|
||||
*
|
||||
* @param array $activity The JSON "Move" Activity.
|
||||
*
|
||||
* @return string|null The target URI or null if not found.
|
||||
*/
|
||||
private static function extract_target( $activity ) {
|
||||
if ( ! empty( $activity['target'] ) ) {
|
||||
return object_to_uri( $activity['target'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $activity['object'] ) ) {
|
||||
return object_to_uri( $activity['object'] );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the origin from the activity.
|
||||
*
|
||||
* The ActivityStreams spec define the `origin` attribute as source
|
||||
* of the activity, but Mastodon uses the `actor` attribute as source
|
||||
* to move profiles.
|
||||
*
|
||||
* @param array $activity The JSON "Move" Activity.
|
||||
*
|
||||
* @return string|null The origin URI or null if not found.
|
||||
*/
|
||||
private static function extract_origin( $activity ) {
|
||||
if ( ! empty( $activity['origin'] ) ) {
|
||||
return object_to_uri( $activity['origin'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $activity['actor'] ) ) {
|
||||
return object_to_uri( $activity['actor'] );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the move.
|
||||
*
|
||||
* @param array $target_object The target object.
|
||||
* @param array $origin_object The origin object.
|
||||
*
|
||||
* @return bool True if the move is verified, false otherwise.
|
||||
*/
|
||||
private static function verify_move( $target_object, $origin_object ) {
|
||||
// Check if both objects are valid.
|
||||
if ( \is_wp_error( $target_object ) || \is_wp_error( $origin_object ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if both objects are persons.
|
||||
if ( 'Person' !== $target_object['type'] || 'Person' !== $origin_object['type'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the target and origin are not the same.
|
||||
if ( $target_object['id'] === $origin_object['id'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the target has an alsoKnownAs property.
|
||||
if ( empty( $target_object['also_known_as'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the origin is in the alsoKnownAs property of the target.
|
||||
if ( ! in_array( $origin_object['id'], $target_object['also_known_as'], true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the origin has a movedTo property.
|
||||
if ( empty( $origin_object['movedTo'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the movedTo property of the origin is the target.
|
||||
if ( $origin_object['movedTo'] !== $target_object['id'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -7,7 +7,7 @@
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Comment;
|
||||
|
||||
@ -23,7 +23,9 @@ class Undo {
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_undo',
|
||||
array( self::class, 'handle_undo' )
|
||||
array( self::class, 'handle_undo' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
@ -31,8 +33,9 @@ class Undo {
|
||||
* Handle "Unfollow" requests.
|
||||
*
|
||||
* @param array $activity The JSON "Undo" Activity.
|
||||
* @param int|null $user_id The ID of the user who initiated the "Undo" activity.
|
||||
*/
|
||||
public static function handle_undo( $activity ) {
|
||||
public static function handle_undo( $activity, $user_id ) {
|
||||
if (
|
||||
! isset( $activity['object']['type'] ) ||
|
||||
! isset( $activity['object']['object'] )
|
||||
@ -41,11 +44,12 @@ class Undo {
|
||||
}
|
||||
|
||||
$type = $activity['object']['type'];
|
||||
$state = false;
|
||||
|
||||
// Handle "Unfollow" requests.
|
||||
if ( 'Follow' === $type ) {
|
||||
$user_id = object_to_uri( $activity['object']['object'] );
|
||||
$user = Users::get_by_resource( $user_id );
|
||||
$id = object_to_uri( $activity['object']['object'] );
|
||||
$user = Actors::get_by_resource( $id );
|
||||
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
// If we can not find a user, we can not initiate a follow process.
|
||||
@ -55,7 +59,7 @@ class Undo {
|
||||
$user_id = $user->get__id();
|
||||
$actor = object_to_uri( $activity['actor'] );
|
||||
|
||||
Followers::remove_follower( $user_id, $actor );
|
||||
$state = Followers::remove_follower( $user_id, $actor );
|
||||
}
|
||||
|
||||
// Handle "Undo" requests for "Like" and "Create" activities.
|
||||
@ -71,9 +75,16 @@ class Undo {
|
||||
return;
|
||||
}
|
||||
|
||||
$state = wp_trash_comment( $comment );
|
||||
$state = wp_delete_comment( $comment, true );
|
||||
}
|
||||
|
||||
do_action( 'activitypub_handled_undo', $activity, $user_id, isset( $state ) ? $state : null, null );
|
||||
}
|
||||
/**
|
||||
* Fires after an "Undo" activity has been handled.
|
||||
*
|
||||
* @param array $activity The JSON "Undo" Activity.
|
||||
* @param int|null $user_id The ID of the user who initiated the "Undo" activity otherwise null.
|
||||
* @param mixed $state The state of the "Undo" activity.
|
||||
*/
|
||||
do_action( 'activitypub_handled_undo', $activity, $user_id, $state );
|
||||
}
|
||||
}
|
||||
|
@ -7,6 +7,7 @@
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Interactions;
|
||||
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
@ -26,9 +27,9 @@ class Update {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "Update" requests
|
||||
* Handle "Update" requests.
|
||||
*
|
||||
* @param array $activity The activity-object.
|
||||
* @param array $activity The Activity object.
|
||||
*/
|
||||
public static function handle_update( $activity ) {
|
||||
$object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : '';
|
||||
@ -75,7 +76,7 @@ class Update {
|
||||
/**
|
||||
* Update an Interaction.
|
||||
*
|
||||
* @param array $activity The activity-object.
|
||||
* @param array $activity The Activity object.
|
||||
*/
|
||||
public static function update_interaction( $activity ) {
|
||||
$commentdata = Interactions::update_comment( $activity );
|
||||
@ -88,18 +89,37 @@ class Update {
|
||||
$state = $commentdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after an Update activity has been handled.
|
||||
*
|
||||
* @param array $activity The complete Update activity data.
|
||||
* @param null $user Always null for Update activities.
|
||||
* @param int|array $state 1 if comment was updated successfully, error data otherwise.
|
||||
* @param \WP_Comment|null $reaction The updated comment object if successful, null otherwise.
|
||||
*/
|
||||
\do_action( 'activitypub_handled_update', $activity, null, $state, $reaction );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an Actor.
|
||||
*
|
||||
* @param array $activity The activity-object.
|
||||
* @param array $activity The Activity object.
|
||||
*/
|
||||
public static function update_actor( $activity ) {
|
||||
// Update cache.
|
||||
get_remote_metadata_by_actor( $activity['actor'], false );
|
||||
$actor = get_remote_metadata_by_actor( $activity['actor'], false );
|
||||
|
||||
// @todo maybe also update all interactions.
|
||||
if ( ! $actor || \is_wp_error( $actor ) || ! isset( $actor['id'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$follower = Followers::get_follower_by_actor( $actor['id'] );
|
||||
|
||||
if ( ! $follower ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$follower->from_array( $actor );
|
||||
$follower->upsert();
|
||||
}
|
||||
}
|
||||
|
@ -1,80 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Help file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
\get_current_screen()->add_help_tab(
|
||||
array(
|
||||
'id' => 'template-tags',
|
||||
'title' => \__( 'Template Tags', 'activitypub' ),
|
||||
'content' =>
|
||||
'<p>' . __( 'The following Template Tags are available:', 'activitypub' ) . '</p>' .
|
||||
'<dl>' .
|
||||
'<dt><code>[ap_title]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s title.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_content apply_filters="yes"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s content. With <code>apply_filters</code> you can decide if filters (<code>apply_filters( \'the_content\', $content )</code>) should be applied or not (default is <code>yes</code>). The values can be <code>yes</code> or <code>no</code>. <code>apply_filters</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_excerpt length="400"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s excerpt (uses <code>the_excerpt</code> if that is set). If no excerpt is provided, will truncate at <code>length</code> (optional, default = 400).', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_permalink type="url"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s permalink. <code>type</code> can be either: <code>url</code> or <code>html</code> (an <a /> tag). <code>type</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_shortlink type="url"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s shortlink. <code>type</code> can be either <code>url</code> or <code>html</code> (an <a /> tag). I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>, to prettify the Shortlinks. <code>type</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_hashtags]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s tags as hashtags.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_hashcats]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s categories as hashtags.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_image type=full]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The URL for the post\'s featured image, defaults to full size. The type attribute can be any of the following: <code>thumbnail</code>, <code>medium</code>, <code>large</code>, <code>full</code>. <code>type</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_author]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The author\'s name.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_authorurl]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The URL to the author\'s profile page.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_date]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s date.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_time]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s time.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_datetime]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s date/time formated as "date @ time".', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_blogurl]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The URL to the site.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_blogname]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The name of the site.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_blogdesc]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The description of the site.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'</dl>' .
|
||||
'<p>' . __( 'You may also use any Shortcode normally available to you on your site, however be aware that Shortcodes may significantly increase the size of your content depending on what they do.', 'activitypub' ) . '</p>' .
|
||||
'<p>' . __( 'Note: the old Template Tags are now deprecated and automatically converted to the new ones.', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \wp_kses( \__( '<a href="https://github.com/pfefferle/wordpress-activitypub/issues/new" target="_blank">Let me know</a> if you miss a Template Tag.', 'activitypub' ), 'activitypub' ) . '</p>',
|
||||
)
|
||||
);
|
||||
|
||||
\get_current_screen()->add_help_tab(
|
||||
array(
|
||||
'id' => 'glossary',
|
||||
'title' => \__( 'Glossary', 'activitypub' ),
|
||||
'content' =>
|
||||
'<p><h2>' . \__( 'Fediverse', 'activitypub' ) . '</h2></p>' .
|
||||
'<p>' . \__( 'The Fediverse is a new word made of two words: "federation" + "universe"', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'It is a federated social network running on free open software on a myriad of computers across the globe. Many independent servers are interconnected and allow people to interact with one another. There\'s no one central site: you choose a server to register. This ensures some decentralization and sovereignty of data. Fediverse (also called Fedi) has no built-in advertisements, no tricky algorithms, no one big corporation dictating the rules. Instead we have small cozy communities of like-minded people. Welcome!', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'For more information please visit <a href="https://fediverse.party/" target="_blank">fediverse.party</a>', 'activitypub' ) . '</p>' .
|
||||
'<p><h2>' . \__( 'ActivityPub', 'activitypub' ) . '</h2></p>' .
|
||||
'<p>' . \__( 'ActivityPub is a decentralized social networking protocol based on the ActivityStreams 2.0 data format. ActivityPub is an official W3C recommended standard published by the W3C Social Web Working Group. It provides a client to server API for creating, updating and deleting content, as well as a federated server to server API for delivering notifications and subscribing to content.', 'activitypub' ) . '</p>' .
|
||||
'<p><h2>' . \__( 'WebFinger', 'activitypub' ) . '</h2></p>' .
|
||||
'<p>' . \__( 'WebFinger is used to discover information about people or other entities on the Internet that are identified by a URI using standard Hypertext Transfer Protocol (HTTP) methods over a secure transport. A WebFinger resource returns a JavaScript Object Notation (JSON) object describing the entity that is queried. The JSON object is referred to as the JSON Resource Descriptor (JRD).', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'For a person, the type of information that might be discoverable via WebFinger includes a personal profile address, identity service, telephone number, or preferred avatar. For other entities on the Internet, a WebFinger resource might return JRDs containing link relations that enable a client to discover, for example, that a printer can print in color on A4 paper, the physical location of a server, or other static information.', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'On Mastodon [and other Plattforms], user profiles can be hosted either locally on the same website as yours, or remotely on a completely different website. The same username may be used on a different domain. Therefore, a Mastodon user\'s full mention consists of both the username and the domain, in the form <code>@username@domain</code>. In practical terms, <code>@user@example.com</code> is not the same as <code>@user@example.org</code>. If the domain is not included, Mastodon will try to find a local user named <code>@username</code>. However, in order to deliver to someone over ActivityPub, the <code>@username@domain</code> mention is not enough – mentions must be translated to an HTTPS URI first, so that the remote actor\'s inbox and outbox can be found. (This paragraph is copied from the <a href="https://docs.joinmastodon.org/spec/webfinger/" target="_blank">Mastodon Documentation</a>)', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'For more information please visit <a href="https://webfinger.net/" target="_blank">webfinger.net</a>', 'activitypub' ) . '</p>' .
|
||||
'<p><h2>' . \__( 'NodeInfo', 'activitypub' ) . '</h2></p>' .
|
||||
'<p>' . \__( 'NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. The two key goals are being able to get better insights into the user base of distributed social networking and the ability to build tools that allow users to choose the best fitting software and server for their needs.', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'For more information please visit <a href="http://nodeinfo.diaspora.software/" target="_blank">nodeinfo.diaspora.software</a>', 'activitypub' ) . '</p>',
|
||||
)
|
||||
);
|
||||
|
||||
\get_current_screen()->set_help_sidebar(
|
||||
'<p><strong>' . \__( 'For more information:', 'activitypub' ) . '</strong></p>' .
|
||||
'<p>' . \__( '<a href="https://wordpress.org/support/plugin/activitypub/">Get support</a>', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( '<a href="https://github.com/automattic/wordpress-activitypub/issues">Report an issue</a>', 'activitypub' ) . '</p>'
|
||||
);
|
@ -10,12 +10,14 @@ namespace Activitypub\Model;
|
||||
use WP_Query;
|
||||
use Activitypub\Signature;
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
/**
|
||||
* Application class.
|
||||
*
|
||||
* @method int get__id() Gets the internal user ID for the application (always returns APPLICATION_USER_ID).
|
||||
*/
|
||||
class Application extends Actor {
|
||||
/**
|
||||
@ -23,7 +25,7 @@ class Application extends Actor {
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $_id = Users::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
|
||||
protected $_id = Actors::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
|
||||
|
||||
/**
|
||||
* Whether the Application is discoverable.
|
||||
@ -94,7 +96,7 @@ class Application extends Actor {
|
||||
* @return string The User-URL with @-Prefix for the username.
|
||||
*/
|
||||
public function get_alternate_url() {
|
||||
return $this->get_url();
|
||||
return $this->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -118,7 +120,7 @@ class Application extends Actor {
|
||||
/**
|
||||
* Get the User-Icon.
|
||||
*
|
||||
* @return array The User-Icon.
|
||||
* @return string[] The User-Icon.
|
||||
*/
|
||||
public function get_icon() {
|
||||
// Try site icon first.
|
||||
@ -152,7 +154,7 @@ class Application extends Actor {
|
||||
/**
|
||||
* Get the User-Header-Image.
|
||||
*
|
||||
* @return array|null The User-Header-Image.
|
||||
* @return string[]|null The User-Header-Image.
|
||||
*/
|
||||
public function get_header_image() {
|
||||
if ( \has_header_image() ) {
|
||||
@ -185,7 +187,7 @@ class Application extends Actor {
|
||||
$time = \time();
|
||||
}
|
||||
|
||||
return \gmdate( 'Y-m-d\TH:i:s\Z', $time );
|
||||
return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $time );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -218,13 +220,13 @@ class Application extends Actor {
|
||||
/**
|
||||
* Returns the public key.
|
||||
*
|
||||
* @return array The public key.
|
||||
* @return string[] The public key.
|
||||
*/
|
||||
public function get_public_key() {
|
||||
return array(
|
||||
'id' => $this->get_id() . '#main-key',
|
||||
'owner' => $this->get_id(),
|
||||
'publicKeyPem' => Signature::get_public_key_for( Users::APPLICATION_USER_ID ),
|
||||
'publicKeyPem' => Signature::get_public_key_for( Actors::APPLICATION_USER_ID ),
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -7,20 +7,22 @@
|
||||
|
||||
namespace Activitypub\Model;
|
||||
|
||||
use WP_Query;
|
||||
|
||||
use Activitypub\Signature;
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Extra_Fields;
|
||||
use Activitypub\Signature;
|
||||
use WP_Query;
|
||||
|
||||
use function Activitypub\esc_hashtag;
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\is_blog_public;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\get_attribution_domains;
|
||||
|
||||
/**
|
||||
* Blog class.
|
||||
*
|
||||
* @method int get__id() Gets the internal user ID for the blog (always returns BLOG_USER_ID).
|
||||
*/
|
||||
class Blog extends Actor {
|
||||
/**
|
||||
@ -51,7 +53,7 @@ class Blog extends Actor {
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $_id = Users::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
|
||||
protected $_id = Actors::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
|
||||
|
||||
/**
|
||||
* If the User is indexable.
|
||||
@ -89,6 +91,18 @@ class Blog extends Actor {
|
||||
*/
|
||||
protected $posting_restricted_to_mods;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*/
|
||||
public function __construct() {
|
||||
/**
|
||||
* Fires when a model actor is constructed.
|
||||
*
|
||||
* @param Blog $this The Blog model.
|
||||
*/
|
||||
\do_action( 'activitypub_construct_model_actor', $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the User manually approves followers.
|
||||
*
|
||||
@ -113,13 +127,25 @@ class Blog extends Actor {
|
||||
* @return string The User ID.
|
||||
*/
|
||||
public function get_id() {
|
||||
$id = parent::get_id();
|
||||
|
||||
if ( $id ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$permalink = \get_option( 'activitypub_use_permalink_as_id_for_blog', false );
|
||||
|
||||
if ( $permalink ) {
|
||||
return $this->get_url();
|
||||
}
|
||||
|
||||
return \add_query_arg( 'author', $this->_id, \trailingslashit( \home_url() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of the object.
|
||||
*
|
||||
* If the Blog is in "single user" mode, return "Person" insted of "Group".
|
||||
* If the Blog is in "single user" mode, return "Person" instead of "Group".
|
||||
*
|
||||
* @return string The type of the object.
|
||||
*/
|
||||
@ -197,7 +223,11 @@ class Blog extends Actor {
|
||||
/**
|
||||
* Filters the default blog username.
|
||||
*
|
||||
* @param string $host The default username.
|
||||
* This filter allows developers to modify the default username that is
|
||||
* generated for the blog, which by default is the site's host name
|
||||
* without the 'www.' prefix.
|
||||
*
|
||||
* @param string $host The default username (site's host name).
|
||||
*/
|
||||
return apply_filters( 'activitypub_default_blog_username', $host );
|
||||
}
|
||||
@ -220,7 +250,7 @@ class Blog extends Actor {
|
||||
/**
|
||||
* Get the User icon.
|
||||
*
|
||||
* @return array The User icon.
|
||||
* @return string[] The User icon.
|
||||
*/
|
||||
public function get_icon() {
|
||||
// Try site_logo, falling back to site_icon, first.
|
||||
@ -254,7 +284,7 @@ class Blog extends Actor {
|
||||
/**
|
||||
* Get the User-Header-Image.
|
||||
*
|
||||
* @return array|null The User-Header-Image.
|
||||
* @return string[]|null The User-Header-Image.
|
||||
*/
|
||||
public function get_image() {
|
||||
$header_image = get_option( 'activitypub_header_image' );
|
||||
@ -298,7 +328,7 @@ class Blog extends Actor {
|
||||
$time = \time();
|
||||
}
|
||||
|
||||
return \gmdate( 'Y-m-d\TH:i:s\Z', $time );
|
||||
return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $time );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -339,7 +369,7 @@ class Blog extends Actor {
|
||||
/**
|
||||
* Get the public key information.
|
||||
*
|
||||
* @return array The public key.
|
||||
* @return string[] The public key.
|
||||
*/
|
||||
public function get_public_key() {
|
||||
return array(
|
||||
@ -401,12 +431,12 @@ class Blog extends Actor {
|
||||
/**
|
||||
* Returns endpoints.
|
||||
*
|
||||
* @return array|null The endpoints.
|
||||
* @return string[]|null The endpoints.
|
||||
*/
|
||||
public function get_endpoints() {
|
||||
$endpoints = null;
|
||||
|
||||
if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) {
|
||||
if ( \get_option( 'activitypub_shared_inbox' ) ) {
|
||||
$endpoints = array(
|
||||
'sharedInbox' => get_rest_url_by_path( 'inbox' ),
|
||||
);
|
||||
@ -497,7 +527,7 @@ class Blog extends Actor {
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#Hashtag
|
||||
*
|
||||
* @return array The User - Hashtags.
|
||||
* @return string[] The User - Hashtags.
|
||||
*/
|
||||
public function get_tag() {
|
||||
$hashtags = array();
|
||||
@ -530,4 +560,41 @@ class Blog extends Actor {
|
||||
$extra_fields = Extra_Fields::get_actor_fields( $this->_id );
|
||||
return Extra_Fields::fields_to_attachments( $extra_fields );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the website hosts allowed to credit this blog.
|
||||
*
|
||||
* @return string[]|null The attribution domains or null if not found.
|
||||
*/
|
||||
public function get_attribution_domains() {
|
||||
return get_attribution_domains();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the alsoKnownAs.
|
||||
*
|
||||
* @return string[] The alsoKnownAs.
|
||||
*/
|
||||
public function get_also_known_as() {
|
||||
$also_known_as = array(
|
||||
\add_query_arg( 'author', $this->_id, \home_url( '/' ) ),
|
||||
$this->get_url(),
|
||||
$this->get_alternate_url(),
|
||||
);
|
||||
|
||||
$also_known_as = array_merge( $also_known_as, \get_option( 'activitypub_blog_user_also_known_as', array() ) );
|
||||
|
||||
return array_unique( $also_known_as );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the movedTo.
|
||||
*
|
||||
* @return string The movedTo.
|
||||
*/
|
||||
public function get_moved_to() {
|
||||
$moved_to = \get_option( 'activitypub_blog_user_moved_to' );
|
||||
|
||||
return $moved_to && $moved_to !== $this->get_id() ? $moved_to : null;
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,18 @@ use Activitypub\Collection\Followers;
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#follow-activity-inbox
|
||||
*
|
||||
* @method int get__id() Gets the post ID of the follower record.
|
||||
* @method string[]|null get_image() Gets the follower's profile image data.
|
||||
* @method string|null get_inbox() Gets the follower's ActivityPub inbox URL.
|
||||
* @method string[]|null get_endpoints() Gets the follower's ActivityPub endpoints.
|
||||
*
|
||||
* @method Follower set__id( int $id ) Sets the post ID of the follower record.
|
||||
* @method Follower set_id( string $guid ) Sets the follower's GUID.
|
||||
* @method Follower set_name( string $name ) Sets the follower's display name.
|
||||
* @method Follower set_summary( string $summary ) Sets the follower's bio/summary.
|
||||
* @method Follower set_published( string $datetime ) Sets the follower's published datetime in ISO 8601 format.
|
||||
* @method Follower set_updated( string $datetime ) Sets the follower's last updated datetime in ISO 8601 format.
|
||||
*/
|
||||
class Follower extends Actor {
|
||||
/**
|
||||
@ -36,7 +48,7 @@ class Follower extends Actor {
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_errors() {
|
||||
return get_post_meta( $this->_id, 'activitypub_errors', false );
|
||||
return get_post_meta( $this->_id, '_activitypub_errors', false );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -72,7 +84,7 @@ class Follower extends Actor {
|
||||
* Reset (delete) all errors.
|
||||
*/
|
||||
public function reset_errors() {
|
||||
delete_post_meta( $this->_id, 'activitypub_errors' );
|
||||
delete_post_meta( $this->_id, '_activitypub_errors' );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -217,8 +229,8 @@ class Follower extends Actor {
|
||||
*/
|
||||
protected function get_post_meta_input() {
|
||||
$meta_input = array();
|
||||
$meta_input['activitypub_inbox'] = $this->get_shared_inbox();
|
||||
$meta_input['activitypub_actor_json'] = $this->to_json();
|
||||
$meta_input['_activitypub_inbox'] = $this->get_shared_inbox();
|
||||
$meta_input['_activitypub_actor_json'] = wp_slash( $this->to_json() );
|
||||
|
||||
return $meta_input;
|
||||
}
|
||||
@ -228,7 +240,7 @@ class Follower extends Actor {
|
||||
*
|
||||
* Sets a fallback to better handle API and HTML outputs.
|
||||
*
|
||||
* @return array The icon.
|
||||
* @return string[] The icon.
|
||||
*/
|
||||
public function get_icon() {
|
||||
if ( isset( $this->icon['url'] ) ) {
|
||||
@ -331,11 +343,18 @@ class Follower extends Actor {
|
||||
* Convert a Custom-Post-Type input to an Activitypub\Model\Follower.
|
||||
*
|
||||
* @param \WP_Post $post The post object.
|
||||
* @return \Activitypub\Activity\Base_Object|WP_Error
|
||||
* @return Follower|false The Follower object or false on failure.
|
||||
*/
|
||||
public static function init_from_cpt( $post ) {
|
||||
$actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true );
|
||||
$actor_json = get_post_meta( $post->ID, '_activitypub_actor_json', true );
|
||||
|
||||
/* @var Follower $object Follower object. */
|
||||
$object = self::init_from_json( $actor_json );
|
||||
|
||||
if ( is_wp_error( $object ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$object->set__id( $post->ID );
|
||||
$object->set_id( $post->guid );
|
||||
$object->set_name( $post->post_title );
|
||||
|
@ -7,17 +7,20 @@
|
||||
|
||||
namespace Activitypub\Model;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Signature;
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Collection\Extra_Fields;
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Signature;
|
||||
|
||||
use function Activitypub\is_blog_public;
|
||||
use function Activitypub\is_user_disabled;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\get_attribution_domains;
|
||||
use function Activitypub\user_can_activitypub;
|
||||
|
||||
/**
|
||||
* User class.
|
||||
*
|
||||
* @method int get__id() Gets the WordPress user ID.
|
||||
*/
|
||||
class User extends Actor {
|
||||
/**
|
||||
@ -68,6 +71,24 @@ class User extends Actor {
|
||||
*/
|
||||
protected $webfinger;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param int $user_id Optional. The WordPress user ID. Default null.
|
||||
*/
|
||||
public function __construct( $user_id = null ) {
|
||||
if ( $user_id ) {
|
||||
$this->_id = $user_id;
|
||||
|
||||
/**
|
||||
* Fires when a model actor is constructed.
|
||||
*
|
||||
* @param User $this The User object.
|
||||
*/
|
||||
\do_action( 'activitypub_construct_model_actor', $this );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of the object.
|
||||
*
|
||||
@ -82,21 +103,18 @@ class User extends Actor {
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return WP_Error|User The User object or WP_Error if user not found.
|
||||
* @return \WP_Error|User The User object or \WP_Error if user not found.
|
||||
*/
|
||||
public static function from_wp_user( $user_id ) {
|
||||
if ( is_user_disabled( $user_id ) ) {
|
||||
return new WP_Error(
|
||||
if ( ! user_can_activitypub( $user_id ) ) {
|
||||
return new \WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
$object = new static();
|
||||
$object->_id = $user_id;
|
||||
|
||||
return $object;
|
||||
return new static( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -105,16 +123,28 @@ class User extends Actor {
|
||||
* @return string The user ID.
|
||||
*/
|
||||
public function get_id() {
|
||||
$id = parent::get_id();
|
||||
|
||||
if ( $id ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$permalink = \get_user_option( 'activitypub_use_permalink_as_id', $this->_id );
|
||||
|
||||
if ( '1' === $permalink ) {
|
||||
return $this->get_url();
|
||||
}
|
||||
|
||||
return \add_query_arg( 'author', $this->_id, \trailingslashit( \home_url() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Username.
|
||||
*
|
||||
* @return string The Username.
|
||||
*/
|
||||
public function get_name() {
|
||||
return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) );
|
||||
return \get_the_author_meta( 'display_name', $this->_id );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -154,17 +184,17 @@ class User extends Actor {
|
||||
* @return string The preferred username.
|
||||
*/
|
||||
public function get_preferred_username() {
|
||||
return \esc_attr( \get_the_author_meta( 'login', $this->_id ) );
|
||||
return \get_the_author_meta( 'login', $this->_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User icon.
|
||||
*
|
||||
* @return array The User icon.
|
||||
* @return string[] The User icon.
|
||||
*/
|
||||
public function get_icon() {
|
||||
$icon = \get_user_option( 'activitypub_icon', $this->_id );
|
||||
if ( wp_attachment_is_image( $icon ) ) {
|
||||
if ( false !== $icon && wp_attachment_is_image( $icon ) ) {
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'url' => esc_url( wp_get_attachment_url( $icon ) ),
|
||||
@ -187,7 +217,7 @@ class User extends Actor {
|
||||
/**
|
||||
* Returns the header image.
|
||||
*
|
||||
* @return array|null The header image.
|
||||
* @return string[]|null The header image.
|
||||
*/
|
||||
public function get_image() {
|
||||
$header_image = get_user_option( 'activitypub_header_image', $this->_id );
|
||||
@ -217,13 +247,13 @@ class User extends Actor {
|
||||
* @return false|string The date the user was created.
|
||||
*/
|
||||
public function get_published() {
|
||||
return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) );
|
||||
return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the public key.
|
||||
*
|
||||
* @return array The public key.
|
||||
* @return string[] The public key.
|
||||
*/
|
||||
public function get_public_key() {
|
||||
return array(
|
||||
@ -281,12 +311,12 @@ class User extends Actor {
|
||||
/**
|
||||
* Returns the endpoints.
|
||||
*
|
||||
* @return array|null The endpoints.
|
||||
* @return string[]|null The endpoints.
|
||||
*/
|
||||
public function get_endpoints() {
|
||||
$endpoints = null;
|
||||
|
||||
if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) {
|
||||
if ( \get_option( 'activitypub_shared_inbox' ) ) {
|
||||
$endpoints = array(
|
||||
'sharedInbox' => get_rest_url_by_path( 'inbox' ),
|
||||
);
|
||||
@ -358,7 +388,7 @@ class User extends Actor {
|
||||
* Update the username.
|
||||
*
|
||||
* @param string $value The new value.
|
||||
* @return int|WP_Error The updated user ID or WP_Error on failure.
|
||||
* @return int|\WP_Error The updated user ID or \WP_Error on failure.
|
||||
*/
|
||||
public function update_name( $value ) {
|
||||
$userdata = array(
|
||||
@ -403,4 +433,42 @@ class User extends Actor {
|
||||
}
|
||||
return \update_user_option( $this->_id, 'activitypub_header_image', $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the website hosts allowed to credit this blog.
|
||||
*
|
||||
* @return string[]|null The attribution domains or null if not found.
|
||||
*/
|
||||
public function get_attribution_domains() {
|
||||
return get_attribution_domains();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the alsoKnownAs.
|
||||
*
|
||||
* @return string[] The alsoKnownAs.
|
||||
*/
|
||||
public function get_also_known_as() {
|
||||
$also_known_as = array(
|
||||
\add_query_arg( 'author', $this->_id, \home_url( '/' ) ),
|
||||
$this->get_url(),
|
||||
$this->get_alternate_url(),
|
||||
);
|
||||
|
||||
// phpcs:ignore Universal.Operators.DisallowShortTernary.Found
|
||||
$also_known_as = array_merge( $also_known_as, \get_user_option( 'activitypub_also_known_as', $this->_id ) ?: array() );
|
||||
|
||||
return array_unique( $also_known_as );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the movedTo.
|
||||
*
|
||||
* @return string The movedTo.
|
||||
*/
|
||||
public function get_moved_to() {
|
||||
$moved_to = \get_user_option( 'activitypub_moved_to', $this->_id );
|
||||
|
||||
return $moved_to && $moved_to !== $this->get_id() ? $moved_to : null;
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,359 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub Actors REST-Class
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use Activitypub\Collection\Actors as Actor_Collection;
|
||||
use Activitypub\Webfinger;
|
||||
|
||||
use function Activitypub\is_activitypub_request;
|
||||
|
||||
/**
|
||||
* ActivityPub Actors REST-Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#followers
|
||||
*/
|
||||
class Actors_Controller extends \WP_REST_Controller {
|
||||
/**
|
||||
* The namespace of this controller's route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = ACTIVITYPUB_REST_NAMESPACE;
|
||||
|
||||
/**
|
||||
* The base of this controller's route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = '(?:users|actors)\/(?P<user_id>[\w\-\.]+)';
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
'args' => array(
|
||||
'user_id' => array(
|
||||
'description' => 'The ID or username of the actor.',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'pattern' => '[\w\-\.]+',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_item' ),
|
||||
'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ),
|
||||
),
|
||||
'schema' => array( $this, 'get_public_item_schema' ),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/remote-follow',
|
||||
array(
|
||||
'args' => array(
|
||||
'user_id' => array(
|
||||
'description' => 'The ID or username of the actor.',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'pattern' => '[\w\-\.]+',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_remote_follow_item' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'resource' => array(
|
||||
'description' => 'The resource to follow.',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a single actor.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_item( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = Actor_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( \is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action triggered prior to the ActivityPub profile being created and sent to the client.
|
||||
*/
|
||||
\do_action( 'activitypub_rest_users_pre' );
|
||||
|
||||
$data = $user->to_array();
|
||||
|
||||
$response = \rest_ensure_response( $data );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) );
|
||||
$response->header( 'Link', \sprintf( '<%1$s>; rel="alternate"; type="application/activity+json"', $user->get_id() ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the remote follow endpoint.
|
||||
*
|
||||
* @param \WP_REST_Request $request Full details about the request.
|
||||
* @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure.
|
||||
*/
|
||||
public function get_remote_follow_item( $request ) {
|
||||
$resource = $request->get_param( 'resource' );
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = Actor_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( \is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$template = Webfinger::get_remote_follow_endpoint( $resource );
|
||||
|
||||
if ( \is_wp_error( $template ) ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
$resource = $user->get_webfinger();
|
||||
$url = \str_replace( '{uri}', $resource, $template );
|
||||
|
||||
return \rest_ensure_response(
|
||||
array(
|
||||
'url' => $url,
|
||||
'template' => $template,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the actor schema, conforming to JSON Schema.
|
||||
*
|
||||
* @return array Item schema data.
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
if ( $this->schema ) {
|
||||
return $this->add_additional_fields_schema( $this->schema );
|
||||
}
|
||||
|
||||
$this->schema = array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'actor',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'@context' => array(
|
||||
'description' => 'The JSON-LD context for the response.',
|
||||
'type' => array( 'array', 'object' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'id' => array(
|
||||
'description' => 'The unique identifier for the actor.',
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
'readonly' => true,
|
||||
),
|
||||
'type' => array(
|
||||
'description' => 'The type of the actor.',
|
||||
'type' => 'string',
|
||||
'enum' => array( 'Person', 'Service', 'Organization', 'Application', 'Group' ),
|
||||
'readonly' => true,
|
||||
),
|
||||
'attachment' => array(
|
||||
'description' => 'Additional information attached to the actor.',
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'type' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'PropertyValue', 'Link' ),
|
||||
),
|
||||
'name' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'value' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'href' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'rel' => array(
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
'readonly' => true,
|
||||
),
|
||||
'name' => array(
|
||||
'description' => 'The display name of the actor.',
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'icon' => array(
|
||||
'description' => 'The icon/avatar of the actor.',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'type' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'url' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
),
|
||||
'readonly' => true,
|
||||
),
|
||||
'published' => array(
|
||||
'description' => 'The date the actor was published.',
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
'readonly' => true,
|
||||
),
|
||||
'summary' => array(
|
||||
'description' => 'A summary about the actor.',
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'tag' => array(
|
||||
'description' => 'Tags associated with the actor.',
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'type' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'href' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'name' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
),
|
||||
'readonly' => true,
|
||||
),
|
||||
'url' => array(
|
||||
'description' => 'The URL to the actor\'s profile page.',
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
'readonly' => true,
|
||||
),
|
||||
'inbox' => array(
|
||||
'description' => 'The inbox endpoint for the actor.',
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
'readonly' => true,
|
||||
),
|
||||
'outbox' => array(
|
||||
'description' => 'The outbox endpoint for the actor.',
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
'readonly' => true,
|
||||
),
|
||||
'following' => array(
|
||||
'description' => 'The following endpoint for the actor.',
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
'readonly' => true,
|
||||
),
|
||||
'followers' => array(
|
||||
'description' => 'The followers endpoint for the actor.',
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
'readonly' => true,
|
||||
),
|
||||
'streams' => array(
|
||||
'description' => 'The streams associated with the actor.',
|
||||
'type' => 'array',
|
||||
'readonly' => true,
|
||||
),
|
||||
'preferredUsername' => array(
|
||||
'description' => 'The preferred username of the actor.',
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'publicKey' => array(
|
||||
'description' => 'The public key information for the actor.',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'owner' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'publicKeyPem' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
'readonly' => true,
|
||||
),
|
||||
'manuallyApprovesFollowers' => array(
|
||||
'description' => 'Whether the actor manually approves followers.',
|
||||
'type' => 'boolean',
|
||||
'readonly' => true,
|
||||
),
|
||||
'attributionDomains' => array(
|
||||
'description' => 'The attribution domains for the actor.',
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'readonly' => true,
|
||||
),
|
||||
'featured' => array(
|
||||
'description' => 'The featured collection endpoint for the actor.',
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
'readonly' => true,
|
||||
),
|
||||
'indexable' => array(
|
||||
'description' => 'Whether the actor is indexable.',
|
||||
'type' => 'boolean',
|
||||
'readonly' => true,
|
||||
),
|
||||
'webfinger' => array(
|
||||
'description' => 'The webfinger identifier for the actor.',
|
||||
'type' => 'string',
|
||||
'readonly' => true,
|
||||
),
|
||||
'discoverable' => array(
|
||||
'description' => 'Whether the actor is discoverable.',
|
||||
'type' => 'boolean',
|
||||
'readonly' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $this->add_additional_fields_schema( $this->schema );
|
||||
}
|
||||
}
|
@ -0,0 +1,238 @@
|
||||
<?php
|
||||
/**
|
||||
* Actors_Inbox_Controller file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Debug;
|
||||
|
||||
use function Activitypub\get_context;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\get_masked_wp_version;
|
||||
|
||||
/**
|
||||
* Actors_Inbox_Controller class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#inbox
|
||||
*/
|
||||
class Actors_Inbox_Controller extends Actors_Controller {
|
||||
use Collection;
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/inbox',
|
||||
array(
|
||||
'args' => array(
|
||||
'user_id' => array(
|
||||
'description' => 'The ID or username of the actor.',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'pattern' => '[\w\-\.]+',
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_items' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'page' => array(
|
||||
'description' => 'Current page of the collection.',
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
// No default so we can differentiate between Collection and CollectionPage requests.
|
||||
),
|
||||
'per_page' => array(
|
||||
'description' => 'Maximum number of items to be returned in result set.',
|
||||
'type' => 'integer',
|
||||
'default' => 20,
|
||||
'minimum' => 1,
|
||||
),
|
||||
),
|
||||
'schema' => array( $this, 'get_collection_schema' ),
|
||||
),
|
||||
array(
|
||||
'methods' => \WP_REST_Server::CREATABLE,
|
||||
'callback' => array( $this, 'create_item' ),
|
||||
'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ),
|
||||
'args' => array(
|
||||
'id' => array(
|
||||
'description' => 'The unique identifier for the activity.',
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
'required' => true,
|
||||
),
|
||||
'actor' => array(
|
||||
'description' => 'The actor performing the activity.',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
'sanitize_callback' => '\Activitypub\object_to_uri',
|
||||
),
|
||||
'type' => array(
|
||||
'description' => 'The type of the activity.',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
'object' => array(
|
||||
'description' => 'The object of the activity.',
|
||||
'required' => true,
|
||||
'validate_callback' => function ( $param, $request, $key ) {
|
||||
/**
|
||||
* Filter the ActivityPub object validation.
|
||||
*
|
||||
* @param bool $validate The validation result.
|
||||
* @param array $param The object data.
|
||||
* @param object $request The request object.
|
||||
* @param string $key The key.
|
||||
*/
|
||||
return \apply_filters( 'activitypub_validate_object', true, $param, $request, $key );
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
'schema' => array( $this, 'get_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the user-inbox.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
* @return \WP_REST_Response|\WP_Error Response object or WP_Error.
|
||||
*/
|
||||
public function get_items( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = Actors::get_by_various( $user_id );
|
||||
|
||||
if ( \is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires before the ActivityPub inbox is created and sent to the client.
|
||||
*/
|
||||
\do_action( 'activitypub_rest_inbox_pre' );
|
||||
|
||||
$response = array(
|
||||
'@context' => get_context(),
|
||||
'id' => get_rest_url_by_path( \sprintf( 'actors/%d/inbox', $user->get__id() ) ),
|
||||
'generator' => 'https://wordpress.org/?v=' . get_masked_wp_version(),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => 0,
|
||||
'orderedItems' => array(),
|
||||
);
|
||||
|
||||
/**
|
||||
* Filters the ActivityPub inbox data before it is sent to the client.
|
||||
*
|
||||
* @param array $response The ActivityPub inbox array.
|
||||
*/
|
||||
$response = \apply_filters( 'activitypub_rest_inbox_array', $response );
|
||||
|
||||
$response = $this->prepare_collection_response( $response, $request );
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after the ActivityPub inbox has been created and sent to the client.
|
||||
*/
|
||||
\do_action( 'activitypub_inbox_post' );
|
||||
|
||||
$response = \rest_ensure_response( $response );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user-inbox requests.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error Response object or WP_Error.
|
||||
*/
|
||||
public function create_item( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = Actors::get_by_various( $user_id );
|
||||
|
||||
if ( \is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$data = $request->get_json_params();
|
||||
$activity = Activity::init_from_array( $data );
|
||||
$type = $request->get_param( 'type' );
|
||||
$type = \strtolower( $type );
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
|
||||
if ( \wp_check_comment_disallowed_list( $activity->to_json( false ), '', '', '', $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT'] ?? '' ) ) {
|
||||
Debug::write_log( 'Blocked activity from: ' . $activity->get_actor() );
|
||||
} else {
|
||||
/**
|
||||
* ActivityPub inbox action.
|
||||
*
|
||||
* @param array $data The data array.
|
||||
* @param int|null $user_id The user ID.
|
||||
* @param string $type The type of the activity.
|
||||
* @param Activity|\WP_Error $activity The Activity object.
|
||||
*/
|
||||
\do_action( 'activitypub_inbox', $data, $user->get__id(), $type, $activity );
|
||||
|
||||
/**
|
||||
* ActivityPub inbox action for specific activity types.
|
||||
*
|
||||
* @param array $data The data array.
|
||||
* @param int|null $user_id The user ID.
|
||||
* @param Activity|\WP_Error $activity The Activity object.
|
||||
*/
|
||||
\do_action( 'activitypub_inbox_' . $type, $data, $user->get__id(), $activity );
|
||||
}
|
||||
|
||||
$response = \rest_ensure_response( array() );
|
||||
$response->set_status( 202 );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the schema for the inbox collection, conforming to JSON Schema.
|
||||
*
|
||||
* @return array Collection schema data.
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
if ( $this->schema ) {
|
||||
return $this->add_additional_fields_schema( $this->schema );
|
||||
}
|
||||
|
||||
$item_schema = array(
|
||||
'type' => 'object',
|
||||
);
|
||||
|
||||
$schema = $this->get_collection_schema( $item_schema );
|
||||
|
||||
// Add inbox-specific properties.
|
||||
$schema['title'] = 'inbox';
|
||||
$schema['properties']['generator'] = array(
|
||||
'description' => 'The software used to generate the collection.',
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
);
|
||||
|
||||
$this->schema = $schema;
|
||||
|
||||
return $this->add_additional_fields_schema( $this->schema );
|
||||
}
|
||||
}
|
@ -1,161 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub Actors REST-Class
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Webfinger;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
|
||||
use function Activitypub\is_activitypub_request;
|
||||
|
||||
/**
|
||||
* ActivityPub Actors REST-Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#followers
|
||||
*/
|
||||
class Actors {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_routes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)/remote-follow',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'remote_follow_get' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'resource' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET request
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
|
||||
*/
|
||||
public static function get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$link_header = sprintf( '<%1$s>; rel="alternate"; type="application/activity+json"', $user->get_id() );
|
||||
|
||||
// Redirect to canonical URL if it is not an ActivityPub request.
|
||||
if ( ! is_activitypub_request() ) {
|
||||
header( 'Link: ' . $link_header );
|
||||
header( 'Location: ' . $user->get_canonical_url(), true, 301 );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Action triggered prior to the ActivityPub profile being created and sent to the client.
|
||||
*/
|
||||
\do_action( 'activitypub_rest_users_pre' );
|
||||
|
||||
$json = $user->to_array();
|
||||
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
$rest_response->header( 'Link', $link_header );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Endpoint for remote follow UI/Block.
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
|
||||
*/
|
||||
public static function remote_follow_get( WP_REST_Request $request ) {
|
||||
$resource = $request->get_param( 'resource' );
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$template = Webfinger::get_remote_follow_endpoint( $resource );
|
||||
|
||||
if ( is_wp_error( $template ) ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
$resource = $user->get_webfinger();
|
||||
$url = str_replace( '{uri}', $resource, $template );
|
||||
|
||||
return new WP_REST_Response(
|
||||
array(
|
||||
'url' => $url,
|
||||
'template' => $template,
|
||||
),
|
||||
200
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters.
|
||||
*
|
||||
* @return array List of parameters,
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
@ -0,0 +1,168 @@
|
||||
<?php
|
||||
/**
|
||||
* Application Controller file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use Activitypub\Model\Application;
|
||||
|
||||
/**
|
||||
* ActivityPub Application Controller.
|
||||
*/
|
||||
class Application_Controller extends \WP_REST_Controller {
|
||||
/**
|
||||
* The namespace of this controller's route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $namespace = ACTIVITYPUB_REST_NAMESPACE;
|
||||
|
||||
/**
|
||||
* The base of this controller's route.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $rest_base = 'application';
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base,
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_item' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
'schema' => array( $this, 'get_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the application actor profile.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
* @return \WP_REST_Response Response object.
|
||||
*/
|
||||
public function get_item( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
$json = ( new Application() )->to_array();
|
||||
|
||||
$rest_response = new \WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the schema for the application endpoint.
|
||||
*
|
||||
* @return array Schema data.
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
if ( $this->schema ) {
|
||||
return $this->add_additional_fields_schema( $this->schema );
|
||||
}
|
||||
|
||||
$this->schema = array(
|
||||
'$schema' => 'http://json-schema.org/draft-04/schema#',
|
||||
'title' => 'application',
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'@context' => array(
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => array( 'string', 'object' ),
|
||||
),
|
||||
),
|
||||
'id' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'type' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'Application' ),
|
||||
),
|
||||
'name' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'icon' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'type' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'url' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
),
|
||||
),
|
||||
'published' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'date-time',
|
||||
),
|
||||
'summary' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'url' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'inbox' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'outbox' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'streams' => array(
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
'preferredUsername' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
'publicKey' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'id' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'owner' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
),
|
||||
'publicKeyPem' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
),
|
||||
'manuallyApprovesFollowers' => array(
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'discoverable' => array(
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'indexable' => array(
|
||||
'type' => 'boolean',
|
||||
),
|
||||
'webfinger' => array(
|
||||
'type' => 'string',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $this->add_additional_fields_schema( $this->schema );
|
||||
}
|
||||
}
|
@ -1,324 +0,0 @@
|
||||
<?php
|
||||
/**
|
||||
* Collections REST-Class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Activity\Base_Object;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
use Activitypub\Collection\Replies;
|
||||
|
||||
use Activitypub\Transformer\Factory;
|
||||
use WP_Error;
|
||||
|
||||
use function Activitypub\esc_hashtag;
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
/**
|
||||
* ActivityPub Collections REST-Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#featured
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#featuredTags
|
||||
*/
|
||||
class Collection {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_routes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)/collections/tags',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'tags_get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)/collections/featured',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'featured_get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/collections/moderators',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'moderators_get' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(?P<type>[\w\-\.]+)s/(?P<id>[\w\-\.]+)/replies',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'replies_get' ),
|
||||
'args' => self::request_parameters_for_replies(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The endpoint for replies collections.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
|
||||
*/
|
||||
public static function replies_get( $request ) {
|
||||
$type = $request->get_param( 'type' );
|
||||
|
||||
// Get the WordPress object of that "owns" the requested replies.
|
||||
switch ( $type ) {
|
||||
case 'comment':
|
||||
$wp_object = \get_comment( $request->get_param( 'id' ) );
|
||||
break;
|
||||
case 'post':
|
||||
default:
|
||||
$wp_object = \get_post( $request->get_param( 'id' ) );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ! isset( $wp_object ) || is_wp_error( $wp_object ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_replies_collection_does_not_exist',
|
||||
\sprintf(
|
||||
// translators: %s: The type (post, comment, etc.) for which no replies collection exists.
|
||||
\__( 'No reply collection exists for the type %s.', 'activitypub' ),
|
||||
$type
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$page = intval( $request->get_param( 'page' ) );
|
||||
|
||||
// If the request parameter page is present get the CollectionPage otherwise the replies collection.
|
||||
if ( isset( $page ) ) {
|
||||
$response = Replies::get_collection_page( $wp_object, $page );
|
||||
} else {
|
||||
$response = Replies::get_collection( $wp_object );
|
||||
}
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
// Add ActivityPub Context.
|
||||
$response = array_merge(
|
||||
array( '@context' => Base_Object::JSON_LD_CONTEXT ),
|
||||
$response
|
||||
);
|
||||
|
||||
return new WP_REST_Response( $response, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* The Featured Tags endpoint
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
|
||||
*/
|
||||
public static function tags_get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$number = 4;
|
||||
|
||||
$tags = \get_terms(
|
||||
array(
|
||||
'taxonomy' => 'post_tag',
|
||||
'orderby' => 'count',
|
||||
'order' => 'DESC',
|
||||
'number' => $number,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $tags ) ) {
|
||||
$tags = array();
|
||||
}
|
||||
|
||||
$response = array(
|
||||
'@context' => Base_Object::JSON_LD_CONTEXT,
|
||||
'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/tags', $user->get__id() ) ),
|
||||
'type' => 'Collection',
|
||||
'totalItems' => is_countable( $tags ) ? count( $tags ) : 0,
|
||||
'items' => array(),
|
||||
);
|
||||
|
||||
foreach ( $tags as $tag ) {
|
||||
$response['items'][] = array(
|
||||
'type' => 'Hashtag',
|
||||
'href' => \esc_url( \get_tag_link( $tag ) ),
|
||||
'name' => esc_hashtag( $tag->name ),
|
||||
);
|
||||
}
|
||||
|
||||
$rest_response = new WP_REST_Response( $response, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Featured posts endpoint
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
|
||||
*/
|
||||
public static function featured_get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$sticky_posts = \get_option( 'sticky_posts' );
|
||||
|
||||
if ( ! is_single_user() && User_Collection::BLOG_USER_ID === $user->get__id() ) {
|
||||
$posts = array();
|
||||
} elseif ( $sticky_posts ) {
|
||||
$args = array(
|
||||
'post__in' => $sticky_posts,
|
||||
'ignore_sticky_posts' => 1,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
);
|
||||
|
||||
if ( $user->get__id() > 0 ) {
|
||||
$args['author'] = $user->get__id();
|
||||
}
|
||||
|
||||
$posts = \get_posts( $args );
|
||||
} else {
|
||||
$posts = array();
|
||||
}
|
||||
|
||||
$response = array(
|
||||
'@context' => Base_Object::JSON_LD_CONTEXT,
|
||||
'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $user_id ) ),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => is_countable( $posts ) ? count( $posts ) : 0,
|
||||
'orderedItems' => array(),
|
||||
);
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$transformer = Factory::get_transformer( $post );
|
||||
|
||||
if ( \is_wp_error( $transformer ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$response['orderedItems'][] = $transformer->to_object()->to_array( false );
|
||||
}
|
||||
|
||||
$rest_response = new WP_REST_Response( $response, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moderators endpoint.
|
||||
*
|
||||
* @return WP_REST_Response The response object.
|
||||
*/
|
||||
public static function moderators_get() {
|
||||
$response = array(
|
||||
'@context' => Actor::JSON_LD_CONTEXT,
|
||||
'id' => get_rest_url_by_path( 'collections/moderators' ),
|
||||
'type' => 'OrderedCollection',
|
||||
'orderedItems' => array(),
|
||||
);
|
||||
|
||||
$users = User_Collection::get_collection();
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$response['orderedItems'][] = $user->get_url();
|
||||
}
|
||||
|
||||
$rest_response = new WP_REST_Response( $response, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters.
|
||||
*
|
||||
* @return array List of parameters.
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters.
|
||||
*
|
||||
* @return array list of parameters.
|
||||
*/
|
||||
public static function request_parameters_for_replies() {
|
||||
$params = array();
|
||||
|
||||
$params['type'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'enum' => array( 'post', 'comment' ),
|
||||
);
|
||||
|
||||
$params['id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
@ -0,0 +1,272 @@
|
||||
<?php
|
||||
/**
|
||||
* Collections_Controller file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use Activitypub\Activity\Base_Object;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Model\Application;
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Model\User;
|
||||
use Activitypub\Transformer\Factory;
|
||||
|
||||
use function Activitypub\esc_hashtag;
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
/**
|
||||
* Collections_Controller class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#featured
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#featuredTags
|
||||
* @see https://www.w3.org/TR/activitypub/#collections
|
||||
*/
|
||||
class Collections_Controller extends Actors_Controller {
|
||||
use Collection;
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*/
|
||||
public function register_routes() {
|
||||
\register_rest_route(
|
||||
$this->namespace,
|
||||
'/' . $this->rest_base . '/collections/(?P<type>[\w\-\.]+)',
|
||||
array(
|
||||
'args' => array(
|
||||
'user_id' => array(
|
||||
'description' => 'The user ID or username.',
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
'type' => array(
|
||||
'description' => 'The type of collection to query.',
|
||||
'type' => 'string',
|
||||
'enum' => array( 'tags', 'featured' ),
|
||||
'required' => true,
|
||||
),
|
||||
),
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( $this, 'get_items' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'page' => array(
|
||||
'description' => 'Current page of the collection.',
|
||||
'type' => 'integer',
|
||||
'minimum' => 1,
|
||||
// No default so we can differentiate between Collection and CollectionPage requests.
|
||||
),
|
||||
'per_page' => array(
|
||||
'description' => 'Maximum number of items to be returned in result set.',
|
||||
'type' => 'integer',
|
||||
'default' => 20,
|
||||
'minimum' => 1,
|
||||
),
|
||||
),
|
||||
),
|
||||
'schema' => array( $this, 'get_item_schema' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a collection of featured tags.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error Response object or WP_Error object.
|
||||
*/
|
||||
public function get_items( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = Actors::get_by_various( $user_id );
|
||||
|
||||
if ( \is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
switch ( $request->get_param( 'type' ) ) {
|
||||
case 'tags':
|
||||
$response = $this->get_tags( $request, $user );
|
||||
break;
|
||||
|
||||
case 'featured':
|
||||
$response = $this->get_featured( $request, $user );
|
||||
break;
|
||||
|
||||
default:
|
||||
$response = new \WP_Error( 'rest_unknown_collection_type', 'Unknown collection type.', array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$response = \rest_ensure_response( $response );
|
||||
$response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a collection of featured tags.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
* @param User|Blog|Application $user Actor.
|
||||
*
|
||||
* @return array Collection of featured tags.
|
||||
*/
|
||||
public function get_tags( $request, $user ) {
|
||||
$tags = \get_terms(
|
||||
array(
|
||||
'taxonomy' => 'post_tag',
|
||||
'orderby' => 'count',
|
||||
'order' => 'DESC',
|
||||
'number' => 4,
|
||||
)
|
||||
);
|
||||
|
||||
if ( \is_wp_error( $tags ) ) {
|
||||
$tags = array();
|
||||
}
|
||||
|
||||
$response = array(
|
||||
'@context' => Base_Object::JSON_LD_CONTEXT,
|
||||
'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/tags', $user->get__id() ) ),
|
||||
'type' => 'Collection',
|
||||
'totalItems' => \is_countable( $tags ) ? \count( $tags ) : 0,
|
||||
'items' => array(),
|
||||
);
|
||||
|
||||
foreach ( $tags as $tag ) {
|
||||
$response['items'][] = array(
|
||||
'type' => 'Hashtag',
|
||||
'href' => \esc_url( \get_tag_link( $tag ) ),
|
||||
'name' => esc_hashtag( $tag->name ),
|
||||
);
|
||||
}
|
||||
|
||||
return $this->prepare_collection_response( $response, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves a collection of featured posts.
|
||||
*
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
* @param User|Blog|Application $user Actor.
|
||||
*
|
||||
* @return array Collection of featured posts.
|
||||
*/
|
||||
public function get_featured( $request, $user ) {
|
||||
$posts = array();
|
||||
|
||||
if ( is_single_user() || Actors::BLOG_USER_ID !== $user->get__id() ) {
|
||||
$sticky_posts = \get_option( 'sticky_posts' );
|
||||
|
||||
if ( $sticky_posts && is_array( $sticky_posts ) ) {
|
||||
// Only show public posts.
|
||||
$args = array(
|
||||
'post__in' => $sticky_posts,
|
||||
'ignore_sticky_posts' => 1,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'activitypub_content_visibility',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
if ( $user->get__id() > 0 ) {
|
||||
$args['author'] = $user->get__id();
|
||||
}
|
||||
|
||||
$posts = \get_posts( $args );
|
||||
}
|
||||
}
|
||||
|
||||
$response = array(
|
||||
'@context' => Base_Object::JSON_LD_CONTEXT,
|
||||
'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $request->get_param( 'user_id' ) ) ),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => \is_countable( $posts ) ? \count( $posts ) : 0,
|
||||
'orderedItems' => array(),
|
||||
);
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$transformer = Factory::get_transformer( $post );
|
||||
|
||||
if ( \is_wp_error( $transformer ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$response['orderedItems'][] = $transformer->to_object()->to_array( false );
|
||||
}
|
||||
|
||||
return $this->prepare_collection_response( $response, $request );
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the schema for the Collections endpoint.
|
||||
*
|
||||
* @return array Schema data.
|
||||
*/
|
||||
public function get_item_schema() {
|
||||
if ( $this->schema ) {
|
||||
return $this->add_additional_fields_schema( $this->schema );
|
||||
}
|
||||
|
||||
$schema = $this->get_collection_schema();
|
||||
|
||||
// Add collections-specific properties.
|
||||
$schema['title'] = 'featured';
|
||||
$schema['properties']['generator'] = array(
|
||||
'description' => 'The software used to generate the collection.',
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
);
|
||||
$schema['properties']['oneOf'] = array(
|
||||
'orderedItems' => array(
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
),
|
||||
),
|
||||
'items' => array(
|
||||
'type' => 'array',
|
||||
'items' => array(
|
||||
'type' => 'object',
|
||||
'properties' => array(
|
||||
'type' => array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'Hashtag' ),
|
||||
'required' => true,
|
||||
),
|
||||
'href' => array(
|
||||
'type' => 'string',
|
||||
'format' => 'uri',
|
||||
'required' => true,
|
||||
),
|
||||
'name' => array(
|
||||
'type' => 'string',
|
||||
'required' => true,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
unset( $schema['properties']['orderedItems'] );
|
||||
|
||||
$this->schema = $schema;
|
||||
|
||||
return $this->add_additional_fields_schema( $this->schema );
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user