diff --git a/wp-content/plugins/activitypub/activitypub.php b/wp-content/plugins/activitypub/activitypub.php index a6dca243..100f29ff 100644 --- a/wp-content/plugins/activitypub/activitypub.php +++ b/wp-content/plugins/activitypub/activitypub.php @@ -3,12 +3,12 @@ * Plugin Name: 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: 5.8.0 + * Version: 8.3.0 * Author: Matthias Pfefferle & Automattic * Author URI: https://automattic.com/ * License: MIT * License URI: http://opensource.org/licenses/MIT - * Requires PHP: 7.2 + * Requires PHP: 7.4 * Text Domain: activitypub * Domain Path: /languages * @@ -17,9 +17,7 @@ namespace Activitypub; -use WP_CLI; - -\define( 'ACTIVITYPUB_PLUGIN_VERSION', '5.8.0' ); +\define( 'ACTIVITYPUB_PLUGIN_VERSION', '8.3.0' ); // Plugin related constants. \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); @@ -29,37 +27,58 @@ use WP_CLI; 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__ . '/includes/functions.php'; +require_once __DIR__ . '/includes/functions-activity.php'; +require_once __DIR__ . '/includes/functions-comment.php'; +require_once __DIR__ . '/includes/functions-federation.php'; +require_once __DIR__ . '/includes/functions-media.php'; +require_once __DIR__ . '/includes/functions-post.php'; +require_once __DIR__ . '/includes/functions-request.php'; +require_once __DIR__ . '/includes/functions-url.php'; +require_once __DIR__ . '/includes/functions-user.php'; require_once __DIR__ . '/integration/load.php'; Autoloader::register_path( __NAMESPACE__, __DIR__ . '/includes' ); +\register_activation_hook( __FILE__, array( Activitypub::class, 'activate' ) ); +\register_deactivation_hook( __FILE__, array( Activitypub::class, 'deactivate' ) ); +\register_uninstall_hook( __FILE__, array( Activitypub::class, 'uninstall' ) ); + /** * Initialize REST routes. */ function rest_init() { Rest\Server::init(); - Rest\Post::init(); ( new Rest\Actors_Controller() )->register_routes(); ( new Rest\Actors_Inbox_Controller() )->register_routes(); + ( new Rest\Admin\Actions_Controller() )->register_routes(); + ( new Rest\Admin\Statistics_Controller() )->register_routes(); ( new Rest\Application_Controller() )->register_routes(); + ( new Rest\Stats_Image_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\Liked_Controller() )->register_routes(); ( new Rest\Inbox_Controller() )->register_routes(); ( new Rest\Interaction_Controller() )->register_routes(); ( new Rest\Moderators_Controller() )->register_routes(); + if ( \get_option( 'activitypub_api', false ) ) { + ( new Rest\OAuth\Authorization_Controller() )->register_routes(); + ( new Rest\OAuth\Clients_Controller() )->register_routes(); + ( new Rest\OAuth\Token_Controller() )->register_routes(); + } ( new Rest\Outbox_Controller() )->register_routes(); + ( new Rest\Post_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() ) { ( new Rest\Nodeinfo_Controller() )->register_routes(); } + ( new Rest\Proxy_Controller() )->register_routes(); } \add_action( 'rest_api_init', __NAMESPACE__ . '\rest_init' ); @@ -68,8 +87,14 @@ function rest_init() { */ function plugin_init() { \add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Avatars', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Cache', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Comment', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Dispatcher', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Embed', 'init' ) ); + if ( \get_option( 'activitypub_api', false ) ) { + \add_action( 'init', array( __NAMESPACE__ . '\Event_Stream', 'init' ) ); + } \add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Link', 'init' ) ); @@ -78,16 +103,32 @@ function plugin_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' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Post_Types', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Router', 'init' ) ); + // Priority 0 ensures Scheduler hooks are registered before Migration (priority 1) runs. + \add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ), 0 ); + \add_action( 'init', array( __NAMESPACE__ . '\Search', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Signature', 'init' ) ); + // Only load OAuth Server if the ActivityPub API is enabled. + if ( \get_option( 'activitypub_api', false ) ) { + \add_action( 'init', array( __NAMESPACE__ . '\OAuth\Server', 'init' ) ); + } if ( site_supports_blocks() ) { \add_action( 'init', array( __NAMESPACE__ . '\Blocks', 'init' ) ); } - $debug_file = __DIR__ . '/includes/debug.php'; - if ( \WP_DEBUG && file_exists( $debug_file ) && is_readable( $debug_file ) ) { - require_once $debug_file; - Debug::init(); + // Only load relay if relay mode is enabled. + if ( \get_option( 'activitypub_relay_mode', false ) ) { + \add_action( 'init', array( __NAMESPACE__ . '\Relay', 'init' ) ); + } + + // Load development tools. + if ( 'local' === wp_get_environment_type() ) { + $loader_file = __DIR__ . '/local/load.php'; + if ( \file_exists( $loader_file ) && \is_readable( $loader_file ) ) { + require_once $loader_file; + } } } \add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' ); @@ -96,16 +137,21 @@ function plugin_init() { * Initialize plugin admin. */ 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' ) ); + // Screen Options and Menus are set before `admin_init`. + \add_action( 'init', array( __NAMESPACE__ . '\WP_Admin\Heartbeat', 'init' ), 9 ); // Before script loader. + \add_filter( 'init', array( __NAMESPACE__ . '\WP_Admin\Screen_Options', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\WP_Admin\Menu', 'init' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Admin', 'init' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Advanced_Settings_Fields', 'init' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\App', 'init' ), 0 ); // Before admin bar init. + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Blog_Settings_Fields', '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\Dashboard', 'init' ) ); \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\User_Settings_Fields', 'init' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Welcome_Fields', 'init' ) ); if ( defined( 'WP_LOAD_IMPORTERS' ) && WP_LOAD_IMPORTERS ) { require_once __DIR__ . '/includes/wp-admin/import/load.php'; @@ -114,14 +160,6 @@ function plugin_admin_init() { } \add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_admin_init' ); -\register_activation_hook( - __FILE__, - array( - __NAMESPACE__ . '\Activitypub', - 'activate', - ) -); - /** * Redirect to the welcome page after plugin activation. * @@ -135,72 +173,10 @@ function activation_redirect( $plugin ) { } \add_action( 'activated_plugin', __NAMESPACE__ . '\activation_redirect' ); -\register_deactivation_hook( - __FILE__, - array( - __NAMESPACE__ . '\Activitypub', - 'deactivate', - ) -); - -\register_uninstall_hook( - __FILE__, - array( - __NAMESPACE__ . '\Activitypub', - 'uninstall', - ) -); - - -/** - * `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', - 'PluginURI' => 'Plugin URI', - 'Version' => 'Version', - 'Description' => 'Description', - 'Author' => 'Author', - 'AuthorURI' => 'Author URI', - 'TextDomain' => 'Text Domain', - 'DomainPath' => 'Domain Path', - 'Network' => 'Network', - 'RequiresWP' => 'Requires at least', - 'RequiresPHP' => 'Requires PHP', - 'UpdateURI' => 'Update URI', - ); - } - - return \get_file_data( __FILE__, $default_headers, 'plugin' ); -} - -/** - * Plugin Version Number used for caching. - * - * @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly. - */ -function get_plugin_version() { - _deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' ); - - return ACTIVITYPUB_PLUGIN_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 to manage plugin functionality and the federation of posts and comments.', - ) - ); + Cli::register(); } + +// Register OAuth login form handler early (before wp-login.php processes). +\add_action( 'login_form_activitypub_authorize', array( __NAMESPACE__ . '\OAuth\Server', 'login_form_authorize' ) ); diff --git a/wp-content/plugins/activitypub/assets/css/activitypub-admin-bar.css b/wp-content/plugins/activitypub/assets/css/activitypub-admin-bar.css new file mode 100644 index 00000000..689969bb --- /dev/null +++ b/wp-content/plugins/activitypub/assets/css/activitypub-admin-bar.css @@ -0,0 +1,7 @@ +/* Admin Bar Social Web Icon */ +#wpadminbar .activitypub-admin-bar-social-web .ab-item::before { + content: "\2042"; /* ⁂ Asterism */ + font-family: Arial, Helvetica, sans-serif; + font-weight: 700; + top: 2px; +} diff --git a/wp-content/plugins/activitypub/assets/css/activitypub-admin.css b/wp-content/plugins/activitypub/assets/css/activitypub-admin.css index 2e181f2a..225b95bb 100644 --- a/wp-content/plugins/activitypub/assets/css/activitypub-admin.css +++ b/wp-content/plugins/activitypub/assets/css/activitypub-admin.css @@ -4,12 +4,12 @@ position: relative; } -.settings_page_activitypub .notice { +.settings_page_activitypub div:not(.wrap) > .notice { max-width: 800px; margin: 0 auto 30px; } -.settings_page_activitypub .update-nag { +.settings_page_activitypub div:not(.wrap) > .update-nag { margin: 25px 20px 15px 22px; } @@ -17,6 +17,10 @@ padding-left: 22px; } +.activitypub-settings p.interactions { + margin-bottom: 1em; +} + .activitypub-settings-header { text-align: center; margin: 0 0 1rem; @@ -45,6 +49,14 @@ padding-left: 0; } +.activitypub-settings-tabs-scroller { + overflow-x: auto; + width: 100%; + padding-top: 2px; + -webkit-overflow-scrolling: touch; + scroll-behavior: smooth; +} + .activitypub-settings-tabs-wrapper { display: inline-flex; vertical-align: top; @@ -61,9 +73,10 @@ display: block; text-decoration: none; color: inherit; - padding: .5rem 1rem 1rem; + padding: 0.5rem 1rem 1rem; margin: 0 1rem; - transition: box-shadow .5s ease-in-out; + transition: box-shadow 0.5s ease-in-out; + white-space: nowrap; } .activitypub-settings .row { @@ -91,6 +104,25 @@ summary { color: #2271b1; } +.activitypub-site-block-details { + margin: 10px 0; +} + +.activitypub-site-block-details summary { + padding: 8px 0; + color: inherit; + text-decoration: none; +} + +.activitypub-site-block-details table { + max-width: 500px; + margin-top: 10px; +} + +.activitypub-site-block-details td:last-child { + width: 80px; +} + .activitypub-settings-accordion { border: 1px solid #c3c4c7; } @@ -133,13 +165,6 @@ summary { user-select: auto; } -.activitypub-settings-accordion-trigger { - color: #2c3338; - cursor: pointer; - font-weight: 400; - text-align: left; -} - .activitypub-settings-accordion-trigger .title { pointer-events: none; font-weight: 600; @@ -150,19 +175,24 @@ summary { .activitypub-settings-accordion-viewed .icon { border: solid #50575e medium; border-width: 0 2px 2px 0; - height: .5rem; + height: 0.5rem; pointer-events: none; position: absolute; right: 1.5em; top: 50%; transform: translateY(-70%) rotate(45deg); - width: .5rem; + width: 0.5rem; } .activitypub-settings-accordion-trigger[aria-expanded="true"] .icon { transform: translateY(-30%) rotate(-135deg); } +table.followings .dashicons { + font-size: 1em; + line-height: 1.7; +} + .activitypub-settings-accordion-trigger:active, .activitypub-settings-accordion-trigger:hover { background: #f6f7f7; @@ -189,8 +219,8 @@ input.blog-user-identifier { position: relative; display: block; margin-bottom: 40px; - background-image: rgb(168,165,175); - background-image: linear-gradient(180deg, red, yellow); + background-image: rgb(168, 165, 175); + background-image: linear-gradient(180deg, #f00, #ff0); background-size: cover; } @@ -207,66 +237,178 @@ input.blog-user-identifier { margin-bottom: 0; } +/* stylelint-disable-next-line selector-id-pattern -- WordPress core ID */ #dashboard_right_now li a.activitypub-followers::before { content: "\f307"; + /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */ font-family: dashicons; } -.repost .dashboard-comment-wrap, -.like .dashboard-comment-wrap { +.activitypub-comment .dashboard-comment-wrap { padding-inline-start: 63px; } -.repost .dashboard-comment-wrap .comment-author, -.like .dashboard-comment-wrap .comment-author { +.activitypub-comment .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; } + +.contextual-help-tabs-wrap dt { + font-weight: 600; +} + +.contextual-help-tabs-wrap .activitypub-block-screenshot { + margin: 10px 0; +} + +.contextual-help-tabs-wrap .activitypub-block-screenshot img { + border: 1px solid #ddd; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + height: auto; + max-width: 100%; +} + +.contextual-help-tabs-wrap .activitypub-block-screenshot figcaption { + color: #555; + font-style: italic; + font-size: 0.9em; + margin-top: 5px; +} + +/* Blockquote Styles */ +.contextual-help-tabs-wrap blockquote { + border-left: 4px solid #3582c4; + background-color: #f6f7f7; + padding: 16px 20px; + margin: 0 0 20px; +} + +.contextual-help-tabs-wrap blockquote p { + margin: 0 0 10px; + line-height: 1.5; +} + +.contextual-help-tabs-wrap blockquote p:last-child { + margin-bottom: 0; +} + +.contextual-help-tabs-wrap blockquote cite { + display: block; + font-weight: 600; + margin-top: 8px; + font-size: 0.9em; + color: #50575e; +} + +.contextual-help-tabs-wrap blockquote cite::before { + content: "—"; +} + +/* Plugin List Styles */ +.plugin-list { + display: flex; + flex-wrap: wrap; + gap: 16px; + align-items: stretch; +} + +.plugin-list .plugin-card { + flex: 1 1 300px; + display: flex; + flex-direction: column; + box-sizing: border-box; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + margin: 0; +} + +.plugin-list .plugin-card .desc { + flex: 1 1 auto; +} + +.plugin-list .plugin-action-buttons li { + display: inline-block; + vertical-align: middle; + margin: 0 0 10px 0; +} + +.plugin-list .plugin-card .action-links { + position: static; + margin-left: 148px; + width: auto; +} + +.plugin-list .plugin-action-buttons { + float: none; + margin: 1em 0 0; + text-align: left; +} + +.plugin-list .plugin-action-buttons li .button { + margin-right: 20px; +} + +.plugin-list .plugin-card h3 { + margin-right: 24px; +} + +/* stylelint-disable no-descending-specificity */ +.plugin-list .plugin-card .name, +.plugin-list .plugin-card .desc, +.plugin-card .desc > p { + margin-right: 0; +} +/* stylelint-enable no-descending-specificity */ + +.plugin-list .plugin-card .desc p:first-of-type { + margin-top: 0; +} + +/* RTL Support for blockquotes */ +.rtl .contextual-help-tabs-wrap blockquote { + border-left: none; + border-right: 4px solid #3582c4; + padding: 16px 20px; +} + +#activitypub-follow-form .highlight { + animation: highlight-fade 3s ease-in-out; + border-color: #3582c4 !important; + box-shadow: 0 0 0 1px #3582c4; +} + +@keyframes highlight-fade { + + 0% { + background-color: #e7f3ff; + border-color: #3582c4; + box-shadow: 0 0 0 1px #3582c4; + } + + 100% { + background-color: #fff; + border-color: #8c8f94; + box-shadow: none; + } +} + +@media screen and (max-width: 782px) { + + .activitypub-settings { + margin: 0 22px; + } + + .activitypub-settings .row > div { + max-width: calc(100% - 36px); + width: 100%; + } +} diff --git a/wp-content/plugins/activitypub/assets/css/activitypub-embed.css b/wp-content/plugins/activitypub/assets/css/activitypub-embed.css index 0422fb40..35332029 100644 --- a/wp-content/plugins/activitypub/assets/css/activitypub-embed.css +++ b/wp-content/plugins/activitypub/assets/css/activitypub-embed.css @@ -67,13 +67,53 @@ .activitypub-embed-content .ap-preview { border: 1px solid #e6e6e6; border-radius: 8px; + box-sizing: border-box; + display: grid; + gap: 2px; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + margin: 1em 0 0; + min-height: 64px; overflow: hidden; + position: relative; + width: 100%; } .activitypub-embed-content .ap-preview img { - width: 100%; - height: auto; + border: 0; + box-sizing: border-box; display: block; + height: 100%; + object-fit: cover; + overflow: hidden; + position: relative; + width: 100%; +} + +.activitypub-embed-content .ap-preview video, +.activitypub-embed-content .ap-preview audio { + max-width: 100%; + display: block; + grid-column: 1 / span 2; +} + +.activitypub-embed-content .ap-preview audio { + width: 100%; +} + +.activitypub-embed-content .ap-preview.layout-1 { + grid-template-columns: 1fr; + grid-template-rows: 1fr; +} + +.activitypub-embed-content .ap-preview.layout-2 { + aspect-ratio: auto; + grid-template-rows: 1fr; + height: auto; +} + +.activitypub-embed-content .ap-preview.layout-3 > img:first-child { + grid-row: span 2; } .activitypub-embed-content .ap-preview-text { @@ -94,7 +134,9 @@ align-items: center; gap: 5px; } + @media only screen and (max-width: 399px) { + .activitypub-embed-meta span.ap-stat { display: none !important; } diff --git a/wp-content/plugins/activitypub/assets/css/activitypub-post-preview.css b/wp-content/plugins/activitypub/assets/css/activitypub-post-preview.css new file mode 100644 index 00000000..18fb71de --- /dev/null +++ b/wp-content/plugins/activitypub/assets/css/activitypub-post-preview.css @@ -0,0 +1,201 @@ +body { + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif; + font-size: 1em; + line-height: 1.5; + margin: 0; + padding: 0; +} + +main { + flex: 1; + border: 1px solid #ccc; + border-radius: 4px; + background-color: #fff; + margin: 1em; + max-width: 600px; +} + +main p { + margin-bottom: 1em; +} + +hr { + background: transparent; + border: 0; + border-top: 1px solid #ccc; + flex: 0 0 auto; + margin: 10px 0; +} + +.columns { + display: flex; + flex-direction: row; + justify-content: space-between; + margin: 0 auto; + max-width: 1200px; +} + +.sidebar { + flex: 1; + padding: 1em; + max-width: 285px; +} + +.sidebar h1 { + font-size: 1.5em; + margin-bottom: 1em; + margin-top: 0; + padding: 5px 10px; + border-radius: 4px; + background-color: #6364ff; + color: #fff; + display: inline-block; +} + +.sidebar ul { + list-style-type: none; + padding: 0; +} + +.sidebar ul li { + padding: 5px; + color: #ccc; +} + +.sidebar input[type="search"], +.sidebar textarea { + background-color: #f6f6f6; + border: 1px solid #ccc; + border-radius: 4px; + box-sizing: border-box; + color: #333; + display: block; + font-size: 1em; + margin-bottom: 1em; + padding: 0.5em; + width: 100%; +} + +.sidebar > div, +main address { + align-items: center; + display: flex; + margin-bottom: 1em; + font-style: normal; +} + +.name { + color: #ccc; + font-weight: 700; + display: block; +} + +.webfinger { + color: #ccc; + font-size: 0.8em; + font-weight: 700; + display: block; + margin-top: 0.5em; +} + +main address .name, +main address .webfinger { + color: #000; +} + +address img, +.sidebar .fake-image { + border-radius: 8px; + margin-right: 1em; + width: 48px; + height: 48px; + background-color: #333; +} + +main article { + padding: 1em; +} + +main .content { + margin: 1em 0; + font-size: 1.2em; +} + +main .content h2 { + font-size: 1.2em; +} + +main .attachments { + border-radius: 8px; + box-sizing: border-box; + display: grid; + gap: 2px; + grid-template-columns: 1fr 1fr; + grid-template-rows: 1fr 1fr; + margin: 1em 0; + min-height: 64px; + overflow: hidden; + position: relative; + width: 100%; +} + +main .attachments.layout-1 { + grid-template-columns: 1fr; + grid-template-rows: 1fr; +} + +main .attachments.layout-2 { + aspect-ratio: auto; + grid-template-rows: 1fr; + height: auto; +} + +main .attachments img { + border: 0; + box-sizing: border-box; + display: inline-block; + height: 100%; + object-fit: cover; + overflow: hidden; + position: relative; + width: 100%; +} + +main .attachments.layout-3 > img:first-child { + grid-row: span 2; +} + +main .attachments video, +main .attachments audio { + max-width: 100%; + margin: 1em 0; + display: block; + grid-column: 1 / span 2; +} + +main .attachments audio { + width: 100%; +} + +main .tags a { + background-color: #f6f6f6; + border-radius: 4px; + color: #333; + display: inline-block; + margin-right: 0.5em; + padding: 0.5em; + text-decoration: none; +} + +main .tags a:hover { + background-color: #e6e6e6; + text-decoration: underline; +} + +main .column-header { + font-size: 1.5em; + margin: 0; + padding: 5px 10px; + border-bottom: 1px solid #ccc; + vertical-align: middle; +} diff --git a/wp-content/plugins/activitypub/assets/css/activitypub-welcome.css b/wp-content/plugins/activitypub/assets/css/activitypub-welcome.css new file mode 100644 index 00000000..3a5ff2e1 --- /dev/null +++ b/wp-content/plugins/activitypub/assets/css/activitypub-welcome.css @@ -0,0 +1,310 @@ +/* ActivityPub Welcome Page Styles */ + +.activitypub-welcome-container { + max-width: 800px; + margin: 40px auto; + background-color: #fff; + border-radius: 8px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); + padding: 30px; +} + +/* Header Styles */ +.activitypub-welcome-header { + text-align: center; + margin-bottom: 30px; + position: relative; +} + +/* Progress Circle */ +.activitypub-progress-circle { + position: relative; + width: 120px; + height: 120px; + margin: 0 auto 20px; +} + +.activitypub-progress-circle-content { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + font-size: 16px; + font-weight: 500; + color: #1e1e1e; + z-index: 2; +} + +.activitypub-progress-ring { + transform: rotate(-90deg); + overflow: visible; +} + +.activitypub-progress-ring-bg { + fill: none; + stroke: #f0f0f1; + stroke-width: 6; +} + +.activitypub-progress-ring-circle { + fill: none; + stroke: #2271b1; + stroke-width: 6; + stroke-linecap: round; + transition: stroke-dashoffset 0.5s ease; +} + +.activitypub-welcome-title { + font-size: 28px; + margin: 20px 0 10px; + font-weight: 400; +} + +.activitypub-welcome-subtitle { + font-size: 16px; + color: #646970; + margin: 0 0 20px; + font-weight: 400; +} + +/* Steps Styles */ +.activitypub-onboarding-step { + display: flex; + align-items: center; + padding: 20px; + border-radius: 4px; + background-color: #f6f7f7; + margin-bottom: 15px; + transition: background-color 0.2s ease; +} + +.activitypub-onboarding-step:last-child { + margin-bottom: 0; +} + +.activitypub-onboarding-step:hover { + background-color: #f0f0f1; +} + +.step-indicator { + margin-right: 15px; + flex-shrink: 0; +} + +.step-icon { + width: 24px; + height: 24px; + font-size: 24px; + display: flex; + align-items: center; + justify-content: center; +} + +.dashicons-warning { + color: #dba617; +} + +.step-content { + flex-grow: 1; + display: flex; + justify-content: space-between; + align-items: center; + width: 100%; +} + +.step-text { + flex-grow: 1; +} + +.step-text h3 { + margin: 0 0 5px; + font-size: 16px; + font-weight: 500; +} + +.step-text p { + margin: 0; + color: #646970; + font-size: 14px; +} + +.step-action { + flex-shrink: 0; + margin-left: 20px; +} + +.activitypub-step-completed { + background-color: #f0f7ee; +} + +.activitypub-step-completed:hover { + background-color: #e2f1dc; +} + +.activitypub-step-completed .step-text h3 { + margin: 0; +} + +.activitypub-step-completed .step-text h3::after { + content: "."; +} + +.activitypub-step-completed .step-text p, +.activitypub-step-completed .step-action { + display: none; +} + +.activitypub-step-completed .step-icon { + color: #008a20; +} + +.step-action .button { + min-width: 120px; + text-align: center; +} + +/* Profiles Section */ +.activitypub-profiles-section { + margin-top: 40px; + border-top: 1px solid #f0f0f1; + padding-top: 30px; +} + +.profiles-description { + margin-bottom: 20px; + font-size: 16px; + color: #1e1e1e; +} + +.activitypub-profiles-container { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 30px; +} + +.activitypub-profile-card { + flex: 1; + min-width: 300px; + background-color: #fff; + border: 1px solid #c3c4c7; + border-radius: 4px; + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1); +} + +.profile-card-header { + background-color: #f0f0f1; + padding: 15px; + border-bottom: 1px solid #c3c4c7; + display: flex; + align-items: center; +} + +.profile-icon { + margin-right: 10px; +} + +.profile-icon .dashicons { + font-size: 20px; + width: 20px; + height: 20px; +} + +/* stylelint-disable-next-line no-descending-specificity */ +.profile-card-header h3 { + margin: 0; + font-size: 16px; + font-weight: 500; +} + +.profile-card-content { + padding: 15px; +} + +.extra-field { + margin-bottom: 15px; +} + +.extra-field label { + display: block; + margin-bottom: 5px; + font-weight: 500; + font-size: 13px; + color: #646970; +} + +.extra-field input { + width: 100%; + padding: 8px; + font-size: 13px; + background-color: #f6f7f7; + border: 1px solid #dcdcde; + border-radius: 3px; +} + +.profile-description { + font-size: 13px; + color: #646970; + margin: 15px 0; + line-height: 1.5; +} + +.profile-card-content .button { + width: 100%; + text-align: center; + margin-top: 10px; +} + +/* Footer Styles */ +.activitypub-welcome-footer { + margin-top: 30px; + text-align: center; +} + +.skip-steps-link { + color: #2271b1; + text-decoration: none; + font-size: 14px; +} + +.skip-steps-link:hover { + color: #135e96; + text-decoration: underline; +} + +/* Responsive Adjustments */ +@media screen and (max-width: 782px) { + + .activitypub-welcome-container { + margin: 20px; + padding: 20px; + } + + .step-content { + flex-direction: column; + align-items: flex-start; + } + + .step-action { + margin-left: 0; + margin-top: 15px; + width: 100%; + } + + .step-action .button { + width: 100%; + text-align: center; + } + + .activitypub-profiles-container { + flex-direction: column; + } + + .activitypub-profile-card { + width: 100%; + } +} diff --git a/wp-content/plugins/activitypub/assets/img/follow-me.png b/wp-content/plugins/activitypub/assets/img/follow-me.png new file mode 100644 index 00000000..a81ec882 Binary files /dev/null and b/wp-content/plugins/activitypub/assets/img/follow-me.png differ diff --git a/wp-content/plugins/activitypub/assets/img/followers.png b/wp-content/plugins/activitypub/assets/img/followers.png new file mode 100644 index 00000000..c34738fd Binary files /dev/null and b/wp-content/plugins/activitypub/assets/img/followers.png differ diff --git a/wp-content/plugins/activitypub/assets/img/mp.jpg b/wp-content/plugins/activitypub/assets/img/mp.jpg index 05964b49..f4558175 100644 Binary files a/wp-content/plugins/activitypub/assets/img/mp.jpg and b/wp-content/plugins/activitypub/assets/img/mp.jpg differ diff --git a/wp-content/plugins/activitypub/assets/img/reactions.png b/wp-content/plugins/activitypub/assets/img/reactions.png new file mode 100644 index 00000000..91bdbde3 Binary files /dev/null and b/wp-content/plugins/activitypub/assets/img/reactions.png differ diff --git a/wp-content/plugins/activitypub/assets/img/reply.png b/wp-content/plugins/activitypub/assets/img/reply.png new file mode 100644 index 00000000..ca6a5523 Binary files /dev/null and b/wp-content/plugins/activitypub/assets/img/reply.png differ diff --git a/wp-content/plugins/activitypub/assets/img/wp-logo.png b/wp-content/plugins/activitypub/assets/img/wp-logo.png index b48f08e8..309e6311 100644 Binary files a/wp-content/plugins/activitypub/assets/img/wp-logo.png and b/wp-content/plugins/activitypub/assets/img/wp-logo.png differ diff --git a/wp-content/plugins/activitypub/assets/js/activitypub-admin.js b/wp-content/plugins/activitypub/assets/js/activitypub-admin.js index 37117cab..764449b2 100644 --- a/wp-content/plugins/activitypub/assets/js/activitypub-admin.js +++ b/wp-content/plugins/activitypub/assets/js/activitypub-admin.js @@ -17,5 +17,4 @@ jQuery( function( $ ) { $( '.activate-now' ).removeClass( 'thickbox open-plugin-details-modal' ); }, 1200 ); } ); - } ); diff --git a/wp-content/plugins/activitypub/assets/js/activitypub-connected-apps.js b/wp-content/plugins/activitypub/assets/js/activitypub-connected-apps.js new file mode 100644 index 00000000..84062db6 --- /dev/null +++ b/wp-content/plugins/activitypub/assets/js/activitypub-connected-apps.js @@ -0,0 +1,364 @@ +/** + * ActivityPub Connected Applications JavaScript. + * + * Handles registering OAuth clients, deleting clients, and revoking + * OAuth tokens from the user profile, following the WordPress core + * Application Passwords UI pattern. + */ + +/* global activitypubConnectedApps, jQuery, ClipboardJS */ + +( function( $ ) { + var $section = $( '#activitypub-connected-apps-section' ), + $newAppForm = $section.find( '.create-application-password' ), + $newAppFields = $newAppForm.find( '.input' ), + $newAppButton = $newAppForm.find( '.button' ), + $appsWrapper = $section.find( '#activitypub-registered-apps-wrapper' ), + $appsTbody = $section.find( '#activitypub-registered-apps-tbody' ), + $tokensWrapper = $section.find( '.activitypub-connected-apps-list-table-wrapper' ), + $tokensTbody = $section.find( '#activitypub-connected-apps-tbody' ), + $revokeAll = $section.find( '#activitypub-revoke-all-tokens' ), + $deleteAll = $section.find( '#activitypub-delete-all-clients' ); + + // Register a new application. + $newAppButton.on( 'click', function( e ) { + e.preventDefault(); + + if ( $newAppButton.prop( 'aria-disabled' ) ) { + return; + } + + var $name = $( '#activitypub-new-app-name' ); + var $redirectUri = $( '#activitypub-new-app-redirect-uri' ); + + if ( 0 === $name.val().trim().length ) { + $name.trigger( 'focus' ); + return; + } + + if ( 0 === $redirectUri.val().trim().length ) { + $redirectUri.trigger( 'focus' ); + return; + } + + clearNotices(); + $newAppButton.prop( 'aria-disabled', true ).addClass( 'disabled' ); + + $.ajax( { + url: activitypubConnectedApps.ajaxUrl, + method: 'POST', + data: { + action: 'activitypub_register_oauth_client', + name: $name.val().trim(), + redirect_uri: $redirectUri.val().trim(), + _wpnonce: activitypubConnectedApps.nonce + } + } ).always( function() { + $newAppButton.removeProp( 'aria-disabled' ).removeClass( 'disabled' ); + } ).done( function( response ) { + if ( ! response.success ) { + addNotice( + response.data && response.data.message ? response.data.message : activitypubConnectedApps.registerError, + 'error' + ); + return; + } + + // Build credential notice (matches core's tmpl-new-application-password). + var $notice = $( '
' ) + .attr( 'role', 'alert' ) + .attr( 'tabindex', '-1' ) + .addClass( 'notice notice-success is-dismissible new-application-password-notice' ); + + // Client ID row. + var $clientIdRow = $( '

' ).addClass( 'application-password-display' ) + .append( $( '' ).text( activitypubConnectedApps.clientIdLabel ) ) + .append( $( '' ).attr( { type: 'text', readonly: 'readonly' } ).addClass( 'code' ).val( response.data.client_id ) ) + .append( + $( '' ) + .attr( 'type', 'button' ) + .addClass( 'notice-dismiss' ) + .append( $( '' ).addClass( 'screen-reader-text' ).text( activitypubConnectedApps.dismiss ) ) + ); + + $newAppForm.after( $notice ); + + return $notice; + } + + /** + * Clears notice messages from the Connected Applications section. + */ + function clearNotices() { + $( '.notice', $section ).remove(); + } +}( jQuery ) ); diff --git a/wp-content/plugins/activitypub/assets/js/activitypub-following.js b/wp-content/plugins/activitypub/assets/js/activitypub-following.js new file mode 100644 index 00000000..a0218b57 --- /dev/null +++ b/wp-content/plugins/activitypub/assets/js/activitypub-following.js @@ -0,0 +1,129 @@ +/** + * ActivityPub Following List Table Polling. + * + * Adds polling functionality to the Following list table to check for status updates + * of pending follow requests without requiring manual page refresh. + * + * @package Activitypub + */ + +( function ( $ ) { + 'use strict'; + + /** + * Following List Table Polling. + */ + var ActivityPubFollowing = { + /** + * Initialize the polling functionality. + */ + init: function () { + this.setupHeartbeatListeners(); + + // Check every 5 seconds. It'll automatically slow down after 2 mins 30 secs. + window.wp.heartbeat.interval( 'fast' ); + }, + + /** + * Set up WordPress Heartbeat API listeners. + */ + setupHeartbeatListeners: function () { + // Add our data to the Heartbeat API request. + $( document ).on( 'heartbeat-send.activitypub_following', function ( e, data ) { + data.activitypub_following_check = { + user_id: ActivityPubFollowingSettings.user_id, + pending_ids: ActivityPubFollowing.getPendingIds(), + }; + } ); + + // Process the Heartbeat API response. + $( document ).on( 'heartbeat-tick.activitypub_following', function ( e, data ) { + if ( data.activitypub_following ) { + ActivityPubFollowing.processUpdates( data.activitypub_following ); + } + } ); + }, + + /** + * Get IDs of all pending follow requests currently displayed in the table. + * + * @return {Array} Array of pending follow request IDs. + */ + getPendingIds: function () { + var pendingIds = []; + + // Find all rows with pending status. + $( '.wp-list-table tr.status-pending' ).each( function () { + var id = $( this ).attr( 'id' ); + + if ( id ) { + // Extract the numeric ID from the row ID (e.g., "following-123" -> "123"). + pendingIds.push( id.replace( /^following-(\d+)$/, '$1' ) ); + } + } ); + + return pendingIds; + }, + + /** + * Process updates received from the server. + * + * @param {Object} response Response data from the server. + */ + processUpdates: function ( response ) { + if ( response.counts ) { + // Update the counts in the views navigation. + if ( Object.hasOwn( response.counts, 'all' ) ) { + $( '.subsubsub .all .count' ).text( '(' + response.counts.all + ')' ); + } + if ( Object.hasOwn( response.counts, 'accepted' ) ) { + $( '.subsubsub .accepted .count' ).text( '(' + response.counts.accepted + ')' ); + } + if ( Object.hasOwn( response.counts, 'pending' ) ) { + $( '.subsubsub .pending .count' ).text( '(' + response.counts.pending + ')' ); + + // Remove heartbeat listeners when there are no more pending follows. + if ( 0 === response.counts.pending ) { + $( document ).off( 'heartbeat-send.activitypub_following' ); + $( document ).off( 'heartbeat-tick.activitypub_following' ); + window.wp.heartbeat.interval( 60 ); + } + } + } + + if ( ! response.updated_items || ! response.updated_items.length ) { + return; + } + + // Remove any existing notices. + $( 'div.notice' ).remove(); + + var $listTable = $( '#the-list' ); + + // Process each updated item. + $.each( response.updated_items, function ( index, item ) { + var $row = $( '#following-' + item.id ); + + if ( $row.length && item.status === 'accepted' ) { + // Remove the row when we're in the "Pending" view. + if ( 'pending' === new URLSearchParams( window.location.search ).get( 'status' ) ) { + $row.remove(); + } else { + $row.find( 'strong.pending' ).remove(); + } + + if ( 0 === $listTable.children().length ) { + $listTable.append( + '' + response.no_items + '' + ); + } + } + } ); + }, + }; + + // Initialize on document ready. + $( document ).ready( function () { + ActivityPubFollowing.init(); + } ); +} )( jQuery ); diff --git a/wp-content/plugins/activitypub/assets/js/activitypub-moderation-admin.js b/wp-content/plugins/activitypub/assets/js/activitypub-moderation-admin.js new file mode 100644 index 00000000..204253db --- /dev/null +++ b/wp-content/plugins/activitypub/assets/js/activitypub-moderation-admin.js @@ -0,0 +1,440 @@ +/** + * ActivityPub Moderation Admin JavaScript + */ + +/* global activitypubModerationL10n, jQuery */ + +/** + * @param {Object} $ - jQuery + * @param {Object} wp - WordPress global object + * @param {Object} wp.i18n - Internationalization functions + * @param {Object} wp.a11y - Accessibility functions + * @param {Object} wp.ajax - AJAX functions + */ +(function( $, wp ) { + 'use strict'; + + var __ = wp.i18n.__; + var _n = wp.i18n._n; + var sprintf = wp.i18n.sprintf; + + /** + * Helper function to show a message using wp.a11y and alert + * + * @param {string} message - The message to display + */ + function showMessage( message ) { + if ( wp.a11y && wp.a11y.speak ) { + wp.a11y.speak( message, 'assertive' ); + } + alert( message ); + } + + /** + * Helper function to validate domain format + * + * @param {string} domain - The domain to validate + * @return {boolean} Whether the domain is valid + */ + function isValidDomain( domain ) { + // Basic domain validation - must contain at least one dot and valid characters + var domainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; + return domainRegex.test( domain ) && domain.includes( '.' ) && domain.length > 3; + } + + /** + * Helper function to check if a term already exists in the UI + * + * @param {string} type - The type of block (domain or keyword) + * @param {string} value - The value to check + * @param {string} context - The context (user or site) + * @param {number|null} userId - The user ID (for user context) + * @return {boolean} Whether the term is already blocked + */ + function isTermAlreadyBlocked( type, value, context, userId ) { + var selector; + if ( context === 'user' ) { + selector = '.activitypub-user-block-list[data-user-id="' + userId + '"] .remove-user-block-btn[data-type="' + type + '"][data-value="' + value + '"]'; + } else if ( context === 'site' ) { + selector = '.remove-site-block-btn[data-type="' + type + '"][data-value="' + value + '"]'; + } + return $( selector ).length > 0; + } + + /** + * Validate a blocked term value + * + * @param {string} type - The type of block (domain or keyword) + * @param {string} value - The value to validate + * @param {string} context - The context (user or site) + * @param {number|null} userId - The user ID (for user context) + * @return {boolean} Whether the value is valid + */ + function validateBlockedTerm( type, value, context, userId ) { + if ( ! value ) { + showMessage( __( 'Please enter a value to block.', 'activitypub' ) ); + return false; + } + + if ( type === 'domain' && ! isValidDomain( value ) ) { + showMessage( __( 'Please enter a valid domain (e.g., example.com).', 'activitypub' ) ); + return false; + } + + if ( isTermAlreadyBlocked( type, value, context, userId ) ) { + showMessage( __( 'This term is already blocked.', 'activitypub' ) ); + return false; + } + + return true; + } + + /** + * Create a table row for a blocked term. + * + * @param {string} type - The type of block (domain or keyword) + * @param {string} value - The blocked value + * @param {string} context - The context (user or site) + * @return {jQuery} The constructed table row + */ + function createBlockedTermRow( type, value, context ) { + var $button = $( ' + + +
+

+
+ + +
+
+ + + +
+
+
+ +
+ +> +
+ +
+ + +
+ <?php echo esc_attr( $actor->get_name() ); ?> + +
+
+
get_name() ); ?>
+ +
+
+ + + + get_summary() ) : ?> +
+ get_summary() ); ?> +
+ + +
+ +
+ ' . esc_html( number_format_i18n( $stats['posts'] ) ) . '' + ); + ?> +
+ + +
+ ' . esc_html( number_format_i18n( $stats['followers'] ) ) . '' + ); + ?> +
+ +
+
+
+
+ + $block_id . '-modal', + 'content' => $modal_content, + /* translators: %s: Profile name. */ + 'title' => sprintf( esc_html__( 'Follow %s', 'activitypub' ), esc_html( $actor->get_name() ) ), + ) + ); + ?> +
diff --git a/wp-content/plugins/activitypub/build/follow-me/style-index-rtl.css b/wp-content/plugins/activitypub/build/follow-me/style-index-rtl.css new file mode 100644 index 00000000..7837bb7b --- /dev/null +++ b/wp-content/plugins/activitypub/build/follow-me/style-index-rtl.css @@ -0,0 +1 @@ +body.modal-open{overflow:hidden}.activitypub-modal__overlay{--activitypub-modal-background-color:var(--wp--preset--color--base,var(--wp--preset--color--background,#fff));--activitypub-modal-border-color:var(--wp--preset--color--contrast-3,var(--wp--preset--color--light-gray,#e2e4e7));--activitypub-modal-radius:var(--wp--custom--border--radius--medium,8px);--activitypub-modal-shadow:var(--wp--preset--shadow--natural,0 5px 15px rgba(0,0,0,.3));--activitypub-modal-compact-shadow:var(--wp--preset--shadow--natural,0 2px 8px rgba(0,0,0,.1));--activitypub-modal-text-color:var(--wp--preset--color--contrast,var(--wp--preset--color--foreground,inherit));align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:var(--activitypub-modal-text-color);display:flex;justify-content:center;right:0;margin:0!important;max-width:none!important;padding:1rem;position:fixed;left:0;top:0;z-index:100000}.activitypub-modal__overlay.compact{align-items:flex-start;background-color:transparent;bottom:auto;justify-content:flex-start;right:auto;padding:0;position:absolute;left:auto;top:auto;z-index:100}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--activitypub-modal-background-color);border:1px solid var(--activitypub-modal-border-color);border-radius:var(--activitypub-modal-radius);box-shadow:var(--activitypub-modal-shadow);box-sizing:border-box;color:var(--wp--preset--color--contrast,#1e1e1e);display:flex;flex-direction:column;font-size:inherit;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.compact .activitypub-modal__frame{box-shadow:var(--activitypub-modal-compact-shadow);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;width:auto}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--activitypub-modal-border-color);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.compact .activitypub-modal__header{display:none}.activitypub-modal__header .activitypub-modal__close{align-items:center;background:transparent;border:none;border-radius:var(--activitypub-modal-radius);color:inherit;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}.activitypub-dialog__section{border-bottom:1px solid var(--activitypub-modal-border-color,var(--wp--preset--color--light-gray,#f0f0f0));padding:1.5rem 2rem}.activitypub-dialog__section:last-child{border-bottom:none;padding-bottom:2rem}.activitypub-dialog__section h4{font-size:110%;margin-bottom:.5rem;margin-top:0}.activitypub-dialog__description{color:inherit;margin-bottom:1rem}.activitypub-dialog__button-group{align-items:center;display:flex;gap:.5rem;margin-bottom:.5rem;width:100%}.activitypub-dialog__button-group input[type]{background-color:var(--activitypub-modal-background-color,#fff)!important;border:1px solid var(--activitypub-modal-border-color,#949494)!important;border-radius:0;box-sizing:border-box;color:inherit!important;flex:1;font-family:inherit;font-size:1em;line-height:1;margin:0;min-width:0;padding:calc(.667em + 2px)!important}.activitypub-dialog__button-group input[type]::-moz-placeholder{opacity:.5}.activitypub-dialog__button-group input[type]::placeholder{opacity:.5}.activitypub-dialog__button-group input[type][aria-invalid=true]{border-color:var(--wp--preset--color--vivid-red,#b32d2e)!important}.activitypub-dialog__button-group button{background-color:var(--wp--preset--color--contrast,#1e1e1e);border:none;box-sizing:border-box;color:var(--wp--preset--color--base,#fff);cursor:pointer;font-family:inherit;font-size:1em;line-height:1;min-width:22.5%;padding:calc(.667em + 2px) 1.5em;width:auto}.activitypub-dialog__error{color:var(--wp--preset--color--vivid-red,#b32d2e);font-size:90%;margin-top:.5rem}.activitypub-dialog__remember{font-size:90%;margin-top:1rem}.activitypub-dialog__remember label{align-items:center;display:flex;gap:.5rem}.activitypub-dialog__remember input[type=checkbox]{margin:0;position:relative;top:0}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.activitypub-follow-me-block-wrapper{display:block;margin:1rem 0;position:relative}.activitypub-follow-me-block-wrapper .activitypub-profile{padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile__body{display:flex;flex-wrap:wrap}.activitypub-follow-me-block-wrapper .activitypub-profile__avatar{border-radius:50%;height:75px;margin-left:1rem;-o-object-fit:cover;object-fit:cover;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile__content{align-items:center;display:flex;flex:1;flex-wrap:wrap;justify-content:space-between;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile__info{display:block;flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile__name{font-size:1.25em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile__name{color:inherit;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile div.wp-block-button{align-items:center;display:flex;margin:0 1rem 0 0}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button__link{margin:0}.activitypub-follow-me-block-wrapper .activitypub-profile .is-small{font-size:.8rem;padding:.25rem .5rem}.activitypub-follow-me-block-wrapper .activitypub-profile .is-compact{font-size:.9rem;padding:.4rem .8rem}.activitypub-follow-me-block-wrapper:not(.is-style-button-only):not(.is-style-profile) .activitypub-profile__bio,.activitypub-follow-me-block-wrapper:not(.is-style-button-only):not(.is-style-profile) .activitypub-profile__stats{display:none}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile{padding:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__body{display:block;padding:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__content{display:inline}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button{display:inline-block;margin:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__avatar,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__bio,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__handle,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__name,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__stats{display:none}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button.has-custom-width{display:block;max-width:none}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button.has-custom-width .wp-block-button__link{justify-content:center;width:100%}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button__width-25{width:calc(25% - var(--wp--style--block-gap, .5em)*.75)}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button__width-50{width:calc(50% - var(--wp--style--block-gap, .5em)*.5)}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button__width-75{width:calc(75% - var(--wp--style--block-gap, .5em)*.25)}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button__width-100{width:100%}.activitypub-follow-me-block-wrapper.is-style-profile{border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);overflow:hidden}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile,.activitypub-follow-me-block-wrapper.is-style-profile.has-background .activitypub-profile{padding:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__header{background-color:#ccc;background-position:50%;background-size:cover;height:120px;width:100%}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__body{padding:1rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__avatar{height:64px;width:64px}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__name{margin-bottom:.25rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio{font-size:90%;line-height:1.4;margin-top:16px;width:100%}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio p{margin:0 0 .5rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio p:last-child{margin-bottom:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__stats{display:flex;font-size:.9em;gap:16px;margin-top:1rem;width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border .activitypub-profile{padding-right:1rem;padding-left:1rem} diff --git a/wp-content/plugins/activitypub/build/follow-me/style-index.css b/wp-content/plugins/activitypub/build/follow-me/style-index.css new file mode 100644 index 00000000..add70c55 --- /dev/null +++ b/wp-content/plugins/activitypub/build/follow-me/style-index.css @@ -0,0 +1 @@ +body.modal-open{overflow:hidden}.activitypub-modal__overlay{--activitypub-modal-background-color:var(--wp--preset--color--base,var(--wp--preset--color--background,#fff));--activitypub-modal-border-color:var(--wp--preset--color--contrast-3,var(--wp--preset--color--light-gray,#e2e4e7));--activitypub-modal-radius:var(--wp--custom--border--radius--medium,8px);--activitypub-modal-shadow:var(--wp--preset--shadow--natural,0 5px 15px rgba(0,0,0,.3));--activitypub-modal-compact-shadow:var(--wp--preset--shadow--natural,0 2px 8px rgba(0,0,0,.1));--activitypub-modal-text-color:var(--wp--preset--color--contrast,var(--wp--preset--color--foreground,inherit));align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:var(--activitypub-modal-text-color);display:flex;justify-content:center;left:0;margin:0!important;max-width:none!important;padding:1rem;position:fixed;right:0;top:0;z-index:100000}.activitypub-modal__overlay.compact{align-items:flex-start;background-color:transparent;bottom:auto;justify-content:flex-start;left:auto;padding:0;position:absolute;right:auto;top:auto;z-index:100}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--activitypub-modal-background-color);border:1px solid var(--activitypub-modal-border-color);border-radius:var(--activitypub-modal-radius);box-shadow:var(--activitypub-modal-shadow);box-sizing:border-box;color:var(--wp--preset--color--contrast,#1e1e1e);display:flex;flex-direction:column;font-size:inherit;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.compact .activitypub-modal__frame{box-shadow:var(--activitypub-modal-compact-shadow);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;width:auto}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--activitypub-modal-border-color);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.compact .activitypub-modal__header{display:none}.activitypub-modal__header .activitypub-modal__close{align-items:center;background:transparent;border:none;border-radius:var(--activitypub-modal-radius);color:inherit;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}.activitypub-dialog__section{border-bottom:1px solid var(--activitypub-modal-border-color,var(--wp--preset--color--light-gray,#f0f0f0));padding:1.5rem 2rem}.activitypub-dialog__section:last-child{border-bottom:none;padding-bottom:2rem}.activitypub-dialog__section h4{font-size:110%;margin-bottom:.5rem;margin-top:0}.activitypub-dialog__description{color:inherit;margin-bottom:1rem}.activitypub-dialog__button-group{align-items:center;display:flex;gap:.5rem;margin-bottom:.5rem;width:100%}.activitypub-dialog__button-group input[type]{background-color:var(--activitypub-modal-background-color,#fff)!important;border:1px solid var(--activitypub-modal-border-color,#949494)!important;border-radius:0;box-sizing:border-box;color:inherit!important;flex:1;font-family:inherit;font-size:1em;line-height:1;margin:0;min-width:0;padding:calc(.667em + 2px)!important}.activitypub-dialog__button-group input[type]::-moz-placeholder{opacity:.5}.activitypub-dialog__button-group input[type]::placeholder{opacity:.5}.activitypub-dialog__button-group input[type][aria-invalid=true]{border-color:var(--wp--preset--color--vivid-red,#b32d2e)!important}.activitypub-dialog__button-group button{background-color:var(--wp--preset--color--contrast,#1e1e1e);border:none;box-sizing:border-box;color:var(--wp--preset--color--base,#fff);cursor:pointer;font-family:inherit;font-size:1em;line-height:1;min-width:22.5%;padding:calc(.667em + 2px) 1.5em;width:auto}.activitypub-dialog__error{color:var(--wp--preset--color--vivid-red,#b32d2e);font-size:90%;margin-top:.5rem}.activitypub-dialog__remember{font-size:90%;margin-top:1rem}.activitypub-dialog__remember label{align-items:center;display:flex;gap:.5rem}.activitypub-dialog__remember input[type=checkbox]{margin:0;position:relative;top:0}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.activitypub-follow-me-block-wrapper{display:block;margin:1rem 0;position:relative}.activitypub-follow-me-block-wrapper .activitypub-profile{padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile__body{display:flex;flex-wrap:wrap}.activitypub-follow-me-block-wrapper .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;-o-object-fit:cover;object-fit:cover;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile__content{align-items:center;display:flex;flex:1;flex-wrap:wrap;justify-content:space-between;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile__info{display:block;flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile__name{font-size:1.25em;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile__name{color:inherit;line-height:1.2;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile div.wp-block-button{align-items:center;display:flex;margin:0 0 0 1rem}.activitypub-follow-me-block-wrapper .activitypub-profile .wp-block-button__link{margin:0}.activitypub-follow-me-block-wrapper .activitypub-profile .is-small{font-size:.8rem;padding:.25rem .5rem}.activitypub-follow-me-block-wrapper .activitypub-profile .is-compact{font-size:.9rem;padding:.4rem .8rem}.activitypub-follow-me-block-wrapper:not(.is-style-button-only):not(.is-style-profile) .activitypub-profile__bio,.activitypub-follow-me-block-wrapper:not(.is-style-button-only):not(.is-style-profile) .activitypub-profile__stats{display:none}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile{padding:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__body{display:block;padding:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__content{display:inline}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button{display:inline-block;margin:0}.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__avatar,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__bio,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__handle,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__name,.activitypub-follow-me-block-wrapper.is-style-button-only .activitypub-profile__stats{display:none}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button.has-custom-width{display:block;max-width:none}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button.has-custom-width .wp-block-button__link{justify-content:center;width:100%}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button__width-25{width:calc(25% - var(--wp--style--block-gap, .5em)*.75)}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button__width-50{width:calc(50% - var(--wp--style--block-gap, .5em)*.5)}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button__width-75{width:calc(75% - var(--wp--style--block-gap, .5em)*.25)}.activitypub-follow-me-block-wrapper.is-style-button-only div.wp-block-button__width-100{width:100%}.activitypub-follow-me-block-wrapper.is-style-profile{border-radius:8px;box-shadow:0 2px 8px rgba(0,0,0,.1);overflow:hidden}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile,.activitypub-follow-me-block-wrapper.is-style-profile.has-background .activitypub-profile{padding:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__header{background-color:#ccc;background-position:50%;background-size:cover;height:120px;width:100%}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__body{padding:1rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__avatar{height:64px;width:64px}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__name{margin-bottom:.25rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio{font-size:90%;line-height:1.4;margin-top:16px;width:100%}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio p{margin:0 0 .5rem}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__bio p:last-child{margin-bottom:0}.activitypub-follow-me-block-wrapper.is-style-profile .activitypub-profile__stats{display:flex;font-size:.9em;gap:16px;margin-top:1rem;width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border .activitypub-profile{padding-left:1rem;padding-right:1rem} diff --git a/wp-content/plugins/activitypub/build/follow-me/style-view-rtl.css b/wp-content/plugins/activitypub/build/follow-me/style-view-rtl.css deleted file mode 100644 index eb3c8508..00000000 --- a/wp-content/plugins/activitypub/build/follow-me/style-view-rtl.css +++ /dev/null @@ -1 +0,0 @@ -.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} diff --git a/wp-content/plugins/activitypub/build/follow-me/style-view.css b/wp-content/plugins/activitypub/build/follow-me/style-view.css deleted file mode 100644 index 1c28cab0..00000000 --- a/wp-content/plugins/activitypub/build/follow-me/style-view.css +++ /dev/null @@ -1 +0,0 @@ -.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} diff --git a/wp-content/plugins/activitypub/build/follow-me/view.asset.php b/wp-content/plugins/activitypub/build/follow-me/view.asset.php index 2f83bf76..56d4b12d 100644 --- a/wp-content/plugins/activitypub/build/follow-me/view.asset.php +++ b/wp-content/plugins/activitypub/build/follow-me/view.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '635ed3e6db3230ae865f'); + array('@wordpress/interactivity'), 'version' => '5b88efee3f3b7a1605df', 'type' => 'module'); diff --git a/wp-content/plugins/activitypub/build/follow-me/view.js b/wp-content/plugins/activitypub/build/follow-me/view.js index fd9c268e..f9de5e7d 100644 --- a/wp-content/plugins/activitypub/build/follow-me/view.js +++ b/wp-content/plugins/activitypub/build/follow-me/view.js @@ -1,2 +1 @@ -(()=>{"use strict";var e,t={5:(e,t,r)=>{var o=r(609);const a=window.wp.element,n=window.wp.domReady;var i=r.n(n);const l=window.wp.apiFetch;var c=r.n(l);const u=window.wp.components,s=window.wp.i18n;function p(e){return`var(--wp--preset--color--${e})`}function m(e){if("string"!=typeof e)return null;if(e.match(/^#/))return e.substring(0,7);const[,,t]=e.split("|");return p(t)}function d(e,t,r=null,o=""){return r?`${e}${o} { ${t}: ${r}; }\n`:""}function v(e,t,r,o){return d(e,"background-color",t)+d(e,"color",r)+d(e,"background-color",o,":hover")+d(e,"background-color",o,":focus")}function f({selector:e,style:t,backgroundColor:r}){const a=function(e,t,r){const o=`${e} .components-button`,a=("string"==typeof(n=r)?p(n):n?.color?.background||null)||t?.color?.background;var n;return v(o,m(t?.elements?.link?.color?.text),a,m(t?.elements?.link?.[":hover"]?.color?.text))}(e,t,r);return(0,o.createElement)("style",null,a)}const b=window.wp.primitives;var y=r(848);const _=(0,y.jsx)(b.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,y.jsx)(b.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5 4.5h11a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 1 .5-.5ZM3 5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm17 3v10.75c0 .69-.56 1.25-1.25 1.25H6v1.5h12.75a2.75 2.75 0 0 0 2.75-2.75V8H20Z"})}),w=(0,y.jsx)(b.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,y.jsx)(b.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),h=(0,a.forwardRef)((function({icon:e,size:t=24,...r},o){return(0,a.cloneElement)(e,{width:t,height:t,...r,ref:o})})),g=(0,y.jsx)(b.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,y.jsx)(b.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"})}),E=window.wp.compose,x="fediverse-remote-user";function S(e){try{return new URL(e),!0}catch(e){return!1}}function k({actionText:e,copyDescription:t,handle:r,resourceUrl:n,myProfile:i="",rememberProfile:l=!1}){const p=(0,s.__)("Loading...","activitypub"),m=(0,s.__)("Opening...","activitypub"),d=(0,s.__)("Error","activitypub"),v=(0,s.__)("Invalid","activitypub"),f=i||(0,s.__)("My Profile","activitypub"),[b,y]=(0,a.useState)(e),[k,O]=(0,a.useState)(_),N=(0,E.useCopyToClipboard)(r,(()=>{O(w),setTimeout((()=>O(_)),1e3)})),[C,R]=(0,a.useState)(""),[I,T]=(0,a.useState)(!0),{setRemoteUser:$}=function(){const[e,t]=(0,a.useState)(function(){const e=localStorage.getItem(x);return e?JSON.parse(e):{}}()),r=(0,a.useCallback)((e=>{!function(e){localStorage.setItem(x,JSON.stringify(e))}(e),t(e)}),[]),o=(0,a.useCallback)((()=>{localStorage.removeItem(x),t({})}),[]);return{template:e?.template||!1,profileURL:e?.profileURL||!1,setRemoteUser:r,deleteRemoteUser:o}}(),z=(0,a.useCallback)((()=>{let t;if(!S(C)&&!function(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&S(`https://${t[1]}`)}(C))return y(v),t=setTimeout((()=>y(e)),2e3),()=>clearTimeout(t);const r=n+C;y(p),c()({path:r}).then((({url:t,template:r})=>{I&&$({profileURL:C,template:r}),y(m),setTimeout((()=>{window.open(t,"_blank"),y(e)}),200)})).catch((()=>{y(d),setTimeout((()=>y(e)),2e3)}))}),[C]);return(0,o.createElement)("div",{className:"activitypub__dialog",role:"dialog","aria-labelledby":"dialog-title"},(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"dialog-title"},f),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"copy-description"},t),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"profile-handle",className:"screen-reader-text"},t),(0,o.createElement)("input",{type:"text",id:"profile-handle",value:r,readOnly:!0}),(0,o.createElement)(u.Button,{ref:N,"aria-label":(0,s.__)("Copy handle to clipboard","activitypub")},(0,o.createElement)(h,{icon:k}),(0,s.__)("Copy","activitypub")))),(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"remote-profile-title"},(0,s.__)("Your Profile","activitypub")),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"remote-profile-description"},(0,a.createInterpolateElement)((0,s.__)("Or, if you know your own profile, we can start things that way! (eg @yourusername@example.com)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,s.__)("Enter your ActivityPub profile","activitypub")),(0,o.createElement)("input",{type:"text",id:"remote-profile",value:C,onKeyDown:e=>{"Enter"===e?.code&&z()},onChange:e=>R(e.target.value),"aria-invalid":b===v}),(0,o.createElement)(u.Button,{onClick:z,"aria-label":(0,s.__)("Submit profile","activitypub")},(0,o.createElement)(h,{icon:g}),b)),l&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(u.CheckboxControl,{checked:I,label:(0,s.__)("Remember me for easier comments","activitypub"),onChange:()=>{T(!I)}}))))}function O(){return window._activityPubOptions||{}}const N={avatar:"",webfinger:"@well@hello.dolly",name:(0,s.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function C(e){if(!e)return N;const t={...N,...e};return t.avatar=t?.icon?.url,t}function R({profile:e,popupStyles:t,userId:r,buttonText:a,buttonOnly:n,buttonSize:i}){const{webfinger:l,avatar:c,name:u}=e,s=l.startsWith("@")?l:`@${l}`;return n?(0,o.createElement)("div",{className:"activitypub-profile"},(0,o.createElement)(I,{profile:e,popupStyles:t,userId:r,buttonText:a,buttonSize:i})):(0,o.createElement)("div",{className:"activitypub-profile"},(0,o.createElement)("img",{className:"activitypub-profile__avatar",src:c,alt:u}),(0,o.createElement)("div",{className:"activitypub-profile__content"},(0,o.createElement)("div",{className:"activitypub-profile__name"},u),(0,o.createElement)("div",{className:"activitypub-profile__handle",title:s},s)),(0,o.createElement)(I,{profile:e,popupStyles:t,userId:r,buttonText:a,buttonSize:i}))}function I({profile:e,popupStyles:t,userId:r,buttonText:n,buttonSize:i}){const[l,c]=(0,a.useState)(!1),p=(0,s.sprintf)(/* translators: %s: profile name */ /* translators: %s: profile name */ -(0,s.__)("Follow %s","activitypub"),e?.name);return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(u.Button,{className:"activitypub-profile__follow",onClick:()=>c(!0),"aria-haspopup":"dialog","aria-expanded":l,"aria-label":(0,s.__)("Follow me on the Fediverse","activitypub"),size:i},n),l&&(0,o.createElement)(u.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>c(!1),title:p,"aria-label":p,role:"dialog"},(0,o.createElement)(T,{profile:e,userId:r}),(0,o.createElement)("style",null,t)))}function T({profile:e,userId:t}){const{namespace:r}=O(),{webfinger:a}=e,n=(0,s.__)("Follow","activitypub"),i=`/${r}/actors/${t}/remote-follow?resource=`,l=(0,s.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),c=a.startsWith("@")?a:`@${a}`;return(0,o.createElement)(k,{actionText:n,copyDescription:l,handle:c,resourceUrl:i})}function $({selectedUser:e,style:t,backgroundColor:r,id:n,useId:i=!1,profileData:l=!1,buttonOnly:u=!1,buttonText:p=(0,s.__)("Follow","activitypub"),buttonSize:d="default"}){const[b,y]=(0,a.useState)(C()),_="site"===e?0:e,w=function(e){return v(".apfmd__button-group .components-button",m(e?.elements?.link?.color?.text)||"#111","#fff",m(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),h=i?{id:n}:{};return(0,a.useEffect)((()=>{l?y(C(l)):function(e){const{namespace:t}=O(),r={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return c()(r)}(_).then((e=>{y(C(e))}))}),[_,l]),(0,o.createElement)("div",{...h,className:"activitypub-follow-me-block-wrapper"},(0,o.createElement)(f,{selector:`#${n}`,style:t,backgroundColor:r}),(0,o.createElement)(R,{profile:b,userId:_,popupStyles:w,buttonText:p,buttonOnly:u,buttonSize:d}))}let z=1;i()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follow-me-block-wrapper"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,a.createRoot)(e).render((0,o.createElement)($,{...t,id:"activitypub-follow-me-block-"+z++,useId:!0}))}))}))},20:(e,t,r)=>{var o=r(609),a=Symbol.for("react.element"),n=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},u=null,s=null;for(o in void 0!==r&&(u=""+r),void 0!==t.key&&(u=""+t.key),void 0!==t.ref&&(s=t.ref),t)n.call(t,o)&&!l.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:a,type:e,key:u,ref:s,props:c,_owner:i.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},r={};function o(e){var a=r[e];if(void 0!==a)return a.exports;var n=r[e]={exports:{}};return t[e](n,n.exports,o),n.exports}o.m=t,e=[],o.O=(t,r,a,n)=>{if(!r){var i=1/0;for(s=0;s=n)&&Object.keys(o.O).every((e=>o.O[e](r[c])))?r.splice(c--,1):(l=!1,n0&&e[s-1][2]>n;s--)e[s]=e[s-1];e[s]=[r,a,n]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={41:0,301:0};o.O.j=t=>0===e[t];var t=(t,r)=>{var a,n,i=r[0],l=r[1],c=r[2],u=0;if(i.some((t=>0!==e[t]))){for(a in l)o.o(l,a)&&(o.m[a]=l[a]);if(c)var s=c(o)}for(t&&t(r);uo(5)));a=o.O(a)})(); \ No newline at end of file +import*as t from"@wordpress/interactivity";const e=t,o=e.withSyncEvent??(t=>t),n={computedStyles:null,variables:{}};function r(t){if("undefined"==typeof window||!window.getComputedStyle)return!1;if(n.variables.hasOwnProperty(t))return n.variables[t];n.computedStyles||(n.computedStyles=window.getComputedStyle(document.documentElement));const e=n.computedStyles.getPropertyValue(t).trim();return n.variables[t]=""!==e,n.variables[t]}function l(t){if("string"!=typeof t)return null;if(t.match(/^#/))return t.substring(0,7);const[,,e]=t.split("|"),o=`--wp--preset--color--${e}`;return r(o)?`var(${o})`:null}function c(t,e,o=null,n=""){return o?`${t}${n} { ${e}: ${o}; }\n`:""}function i(t,e,o,n){return c(t,"background-color",e)+c(t,"color",o)+c(t,"background-color",n,":hover")+c(t,"background-color",n,":focus")}!function(){const{actions:t,callbacks:n}=(0,e.store)("activitypub/follow-me",{actions:{openModal(t){const o=(0,e.getContext)();if(o.modal.isOpen=!0,o.modal.isCompact)setTimeout(n.positionModal,0);else{const t=document.getElementById(o.blockId);if(t){const e=t.querySelector(".activitypub-modal__overlay");e&&["top","left","right","bottom"].forEach(t=>{e.style.removeProperty(t)})}setTimeout(()=>{if(t){const e=t.querySelector(".activitypub-modal__frame");e&&n.trapFocus(e)}},50)}"function"==typeof n.onModalOpen&&n.onModalOpen(t)},closeModal(t){const o=(0,e.getContext)();o.modal.isOpen=!1;const r=(0,e.getElement)();if("actions.toggleModal"===r.ref.dataset["wpOn-Click"])r.ref.focus();else{const t=document.getElementById(o.blockId);if(t){const e=t.querySelector('[data-wp-on--click="actions.toggleModal"]');e&&e.focus()}}"function"==typeof n.onModalClose&&n.onModalClose(t)},toggleModal:o(o=>{o?.preventDefault?.();const{modal:n}=(0,e.getContext)();n.isOpen?t.closeModal(o):t.openModal(o)})},callbacks:{_abortController:null,handleModalEffects(){const{modal:t}=(0,e.getContext)();if(t.isOpen&&!t.isCompact?document.body.classList.add("modal-open"):document.body.classList.remove("modal-open"),n._abortController&&(n._abortController.abort(),n._abortController=null),t.isOpen){n._abortController=new AbortController;const{signal:t}=n._abortController;document.addEventListener("keydown",n.documentKeydown,{signal:t}),document.addEventListener("click",n.documentClick,{signal:t})}},documentKeydown(o){const{modal:n}=(0,e.getContext)();n.isOpen&&"Escape"===o.key&&t.closeModal()},documentClick(o){const{blockId:n,modal:r}=(0,e.getContext)();if(!r.isOpen)return;const l=document.getElementById(n);if(!l)return;const c=l.querySelectorAll('[data-wp-on--click="actions.toggleModal"]');for(const t of c)if(t===o.target||t.contains(o.target))return;const i=l.querySelector(".activitypub-modal__frame");i&&!i.contains(o.target)&&t.closeModal()},positionModal(){const{blockId:t}=(0,e.getContext)(),o=document.getElementById(t);if(!o)return;const n=o.querySelector(".activitypub-modal__overlay");if(!n)return;n.style.top="",n.style.left="",n.style.right="",n.style.bottom="";const r=(0,e.getElement)().ref.getBoundingClientRect(),l=window.innerWidth,c=o.getBoundingClientRect(),i={top:r.bottom-c.top+8+"px",left:r.left-c.left-2+"px"};l-r.right<250&&(i.left="auto",i.right=c.right-r.right+"px"),Object.assign(n.style,i)},trapFocus(t){const e=t.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]):not([readonly]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'),o=e[0],n=e[e.length-1];o&&o.classList.contains("activitypub-modal__close")&&e.length>1?e[1].focus():o.focus(),t.addEventListener("keydown",function(e){if("Tab"!==e.key&&9!==e.keyCode)return;const r=t.ownerDocument.activeElement;e.shiftKey?r===o&&(n.focus(),e.preventDefault()):r===n&&(o.focus(),e.preventDefault())})}}})}();const{actions:s,callbacks:a}=(0,e.store)("activitypub/follow-me",{actions:{copyToClipboard(){const t=(0,e.getContext)(),{i18n:o}=(0,e.getConfig)();navigator.clipboard.writeText(t.webfinger).then(()=>{t.copyButtonText=o.copied,setTimeout(()=>{t.copyButtonText=o.copy},1e3)},t=>{console.error("Could not copy text: ",t)})},updateRemoteProfile(t){const o=(0,e.getContext)();o.remoteProfile=t.target.value,o.isError=!1,o.errorMessage=""},onKeydown:o(t=>{"A"!==(0,e.getElement)().ref.tagName||"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),s.toggleModal(t))}),handleKeyDown(t){"Enter"===t.key&&(t.preventDefault(),s.submitRemoteProfile())},*submitRemoteProfile(){const t=(0,e.getContext)(),{namespace:o,i18n:n}=(0,e.getConfig)(),{apiFetch:r}=window.wp,l=t.remoteProfile.trim();if(!l)return t.isError=!0,void(t.errorMessage=n.emptyProfileError);if(!a.isHandle(l))return t.isError=!0,void(t.errorMessage=n.invalidProfileError);t.isLoading=!0,t.isError=!1;const c=`/${o}/actors/${t.userId}/remote-follow?resource=${encodeURIComponent(l)}`;try{const e=yield r({path:c});t.isLoading=!1,window.open(e.url,"_blank"),s.closeModal(new Event("click"))}catch(e){console.error("Error submitting profile:",e),t.isLoading=!1,t.isError=!0,t.errorMessage=e.message||n.genericError}}},callbacks:{initButtonStyles:()=>{const{buttonStyle:t,backgroundColor:o,blockId:n}=(0,e.getContext)();if(n&&t){const e=document.createElement("style"),c=`#${n}`;e.textContent=function(t,e,o){const n=`${t} .wp-block-button__link`,c=function(t){if("string"==typeof t){const e=`--wp--preset--color--${t}`;return r(e)?`var(${e})`:null}return t?.color?.background||null}(o)||e?.color?.background;return i(n,l(e?.elements?.link?.color?.text),c,l(e?.elements?.link?.[":hover"]?.color?.text))}(c,t,o),document.head.appendChild(e);const s=document.createElement("style");s.textContent=function(t){const e=l(t?.elements?.link?.color?.text)||"var(--wp--preset--color--contrast, var(--wp--preset--color--foreground, #1e1e1e))";return i(".activitypub-dialog__button-group .wp-block-button__link",e,"var(--wp--preset--color--base, var(--wp--preset--color--background, #fff))",l(t?.elements?.link?.[":hover"]?.color?.text)||e)}(t),document.head.appendChild(s)}},isHandle(t){const e=t.replace(/^@/,"").split("@");return 2===e.length&&a.isUrl(`https://${e[1]}`)},isUrl(t){try{return new URL(t),!0}catch(t){return!1}},onModalClose(){(0,e.getContext)().isError=!1}}}); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/followers/block.json b/wp-content/plugins/activitypub/build/followers/block.json index 078b82f3..d0f0317f 100644 --- a/wp-content/plugins/activitypub/build/followers/block.json +++ b/wp-content/plugins/activitypub/build/followers/block.json @@ -2,23 +2,26 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "name": "activitypub/followers", "apiVersion": 3, - "version": "1.0.0", + "version": "8.3.0", "title": "Fediverse Followers", "category": "widgets", "description": "Display your followers from the Fediverse on your website.", "textdomain": "activitypub", "icon": "groups", + "keywords": [ + "fediverse", + "activitypub", + "followers", + "mastodon" + ], "supports": { - "html": false + "html": false, + "interactivity": true }, "attributes": { - "title": { - "type": "string", - "default": "Fediverse Followers" - }, "selectedUser": { "type": "string", - "default": "site" + "default": "blog" }, "per_page": { "type": "number", @@ -40,12 +43,12 @@ "styles": [ { "name": "default", - "label": "No Lines", + "label": "Default", "isDefault": true }, { - "name": "with-lines", - "label": "Lines" + "name": "card", + "label": "Card" }, { "name": "compact", @@ -53,9 +56,11 @@ } ], "editorScript": "file:./index.js", - "viewScript": "file:./view.js", + "editorStyle": "file:./index.css", + "viewScriptModule": "file:./view.js", + "viewScript": "wp-api-fetch", "style": [ - "file:./style-view.css", - "wp-block-query-pagination" - ] + "file:./style-index.css" + ], + "render": "file:./render.php" } \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/followers/index.asset.php b/wp-content/plugins/activitypub/build/followers/index.asset.php index 9b0afcfa..c3fe904d 100644 --- a/wp-content/plugins/activitypub/build/followers/index.asset.php +++ b/wp-content/plugins/activitypub/build/followers/index.asset.php @@ -1 +1 @@ - 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'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => 'da88cfc334f994481a92'); diff --git a/wp-content/plugins/activitypub/build/followers/index.js b/wp-content/plugins/activitypub/build/followers/index.js index 9bec7474..6d6ff015 100644 --- a/wp-content/plugins/activitypub/build/followers/index.js +++ b/wp-content/plugins/activitypub/build/followers/index.js @@ -1,4 +1,3 @@ -(()=>{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{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.__)(" 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 ","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 %1$s 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})})()})(); \ No newline at end of file +(()=>{"use strict";var e,t={4736(e,t,i){const a=window.wp.blocks,r=window.wp.primitives,s=window.ReactJSXRuntime;var n=(0,s.jsx)(r.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,s.jsx)(r.Path,{fillRule:"evenodd",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"})});const o=[{attributes:{title:{type:"string",default:"Fediverse Followers"},selectedUser:{type:"string",default:"blog"},per_page:{type:"number",default:10},order:{type:"string",default:"desc",enum:["asc","desc"]}},supports:{html:!1},isEligible:({title:e})=>!!e,migrate:({title:e,...t})=>[t,[(0,a.createBlock)("core/heading",{content:e,level:3})]]}],l=window.wp.components,c=window.wp.blockEditor,p=window.wp.coreData,u=window.wp.data,d=window.wp.element,v=window.wp.i18n,h=window.wp.apiFetch;var b=i.n(h);function g(){return window._activityPubOptions||{}}function f({name:e}){const{enabled:t}=g(),i=t?.blog?"":(0,v.__)("It will be empty in other non-author contexts.","activitypub"),a=(0,v.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,v.__)("This %1$s 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,i).trim();return(0,s.jsx)(l.Card,{children:(0,s.jsx)(l.CardBody,{children:(0,d.createInterpolateElement)(a,{strong:(0,s.jsx)("strong",{})})})})}const w=window.wp.url;function y({name:e,icon:t,url:i,webfinger:a}){const r=`@${a}`,{defaultAvatarUrl:n,showAvatars:o}=g(),l=t?.url||n;return(0,s.jsxs)("a",{className:"activitypub-actor-link",href:i,title:r,onClick:e=>e.preventDefault(),children:[o&&(0,s.jsx)("img",{width:"48",height:"48",src:l,className:"activitypub-actor-avatar",alt:e,onError:e=>{e.target.src=n}}),(0,s.jsxs)("div",{className:"activitypub-actor-info",children:[(0,s.jsx)("span",{className:"activitypub-actor-name",children:e}),(0,s.jsx)("span",{className:"activitypub-actor-handle",children:r})]}),(0,s.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"24",height:"24",className:"external-link-icon","aria-hidden":"true",focusable:"false",fill:"currentColor",children:(0,s.jsx)("path",{d:"M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"})})]})}function m({page:e,pages:t,setPage:i,navLabel:a=(0,v.__)("Navigation","activitypub")}){if(t<=1)return null;const r=e<=1,n=e>=t;return(0,s.jsxs)("nav",{className:"activitypub-actor-list-pagination",role:"navigation",children:[(0,s.jsx)("h1",{className:"screen-reader-text",children:a}),(0,s.jsx)("a",{role:"button",tabIndex:0,className:"pagination-previous","aria-disabled":r,"aria-label":(0,v.__)("Previous page","activitypub"),onClick:()=>{r||i(e-1)},onKeyDown:t=>{r||"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),i(e-1))},children:(0,v.__)("Previous","activitypub")}),(0,s.jsx)("div",{className:"pagination-info",children:`${e} / ${t}`}),(0,s.jsx)("a",{role:"button",tabIndex:0,className:"pagination-next","aria-disabled":n,"aria-label":(0,v.__)("Next page","activitypub"),onClick:()=>{n||i(e+1)},onKeyDown:t=>{n||"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),i(e+1))},children:(0,v.__)("Next","activitypub")})]})}function _({selectedUser:e,perPage:t,order:i,endpoint:a="followers",page:r,setPage:n,initialData:o=!1,emptyMessage:l=(0,v.__)("No results found.","activitypub"),navLabel:c=(0,v.__)("Navigation","activitypub")}){const{namespace:p}=g(),u="blog"===e?0:e,[h,f]=(0,d.useState)([]),[_,x]=(0,d.useState)(0),[j,k]=(0,d.useState)(1),N=r||j,S=n||k,U=(0,d.useCallback)((e,i)=>{f(e),x(Math.ceil(i/t))},[t]);return(0,d.useEffect)(()=>{if(o&&1===N)return U(o.items,o.total);const e=(0,w.addQueryArgs)(`/${p}/actors/${u}/${a}`,{per_page:t,order:i,page:N,context:"full"});b()({path:e}).then(({orderedItems:e=[],totalItems:t=0})=>U(e,t)).catch(()=>U([],0))},[p,u,t,i,N,a,o,U]),(0,s.jsxs)("div",{className:"activitypub-actor-list-container",children:[h.length?(0,s.jsx)("ul",{className:"activitypub-actor-list",children:h.map(e=>(0,s.jsx)("li",{className:"activitypub-actor-item",children:(0,s.jsx)(y,{...e})},e.url))}):(0,s.jsx)("p",{className:"activitypub-actor-list-placeholder",children:l}),(0,s.jsx)(m,{page:N,pages:_,setPage:S,navLabel:c})]})}function x(e){return!!e&&Object.entries(e).some(([e,t])=>e.endsWith("activitypub_hide_social_graph")&&t)}const j=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","name":"activitypub/followers","apiVersion":3,"version":"8.2.1","title":"Fediverse Followers","category":"widgets","description":"Display your followers from the Fediverse on your website.","textdomain":"activitypub","icon":"groups","keywords":["fediverse","activitypub","followers","mastodon"],"supports":{"html":false,"interactivity":true},"attributes":{"selectedUser":{"type":"string","default":"blog"},"per_page":{"type":"number","default":10},"order":{"type":"string","default":"desc","enum":["asc","desc"]}},"usesContext":["postType","postId"],"styles":[{"name":"default","label":"Default","isDefault":true},{"name":"card","label":"Card"},{"name":"compact","label":"Compact"}],"editorScript":"file:./index.js","editorStyle":"file:./index.css","viewScriptModule":"file:./view.js","viewScript":"wp-api-fetch","style":["file:./style-index.css"],"render":"file:./render.php"}');(0,a.registerBlockType)(j,{deprecated:o,edit:function({attributes:e,setAttributes:t,context:{postType:i,postId:a}}){const{className:r="",order:n,per_page:o,selectedUser:h}=e,w=(0,c.useBlockProps)(),[y,m]=(0,d.useState)(1),j=[{label:(0,v.__)("New to old","activitypub"),value:"desc"},{label:(0,v.__)("Old to new","activitypub"),value:"asc"}],k=function({withInherit:e=!1}){const{enabled:t,namespace:i}=g(),[a,r]=(0,d.useState)(!1),{fetchedUsers:s,isLoadingUsers:n}=(0,u.useSelect)(e=>{const{getUsers:i,getIsResolving:a}=e("core");return{fetchedUsers:t?.users?i({capabilities:"activitypub"}):null,isLoadingUsers:!!t?.users&&a("getUsers",[{capabilities:"activitypub"}])}},[t?.users]),o=(0,u.useSelect)(e=>s||n?null:e("core").getCurrentUser(),[s,n]);(0,d.useEffect)(()=>{!s&&!n&&o&&i&&b()({path:`/${i}/actors/${o.id}`,method:"HEAD",headers:{Accept:"application/activity+json"},parse:!1}).then(()=>r(!0)).catch(()=>r(!1))},[s,n,o,i]);const l=(0,d.useMemo)(()=>s||(o&&a?[{id:o.id,name:o.name}]:[]),[s,o,a]);return(0,d.useMemo)(()=>{if(!l.length)return[];const i=[];return t?.blog&&s&&i.push({label:(0,v.__)("Blog","activitypub"),value:"blog"}),e&&t?.users&&s&&i.push({label:(0,v.__)("Dynamic User","activitypub"),value:"inherit"}),l.reduce((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e),i)},[l,t?.blog,t?.users,s,e])}({withInherit:!0}),N=e=>i=>{m(1),t({[e]:i})},{blogSocialGraphHidden:S,currentUser:U,usersWithMeta:C,siteUrl:M,canManageOptions:O}=(0,u.useSelect)(e=>{const{getCurrentUser:t,getUsers:i,getEntityRecord:a,canUser:r}=e(p.store),s=a("root","site"),n=a("root","__unstableBase");return{blogSocialGraphHidden:!!s?.activitypub_hide_social_graph,currentUser:t(),usersWithMeta:i({capabilities:"activitypub",context:"edit"}),siteUrl:n?.home,canManageOptions:r("update",{kind:"root",name:"site"})}},[]),P=(0,u.useSelect)(e=>{const{getEditedEntityRecord:t}=e(p.store),r=t("postType",i,a)?.author;return r??null},[i,a]),D=(0,d.useMemo)(()=>k.length&&C?k.filter(({value:e})=>{if("inherit"===e)return!0;if("blog"===e)return!S;const t=C?.find(t=>String(t.id)===e);return!x(t?.meta)}):[],[k,S,C]),E=(0,d.useMemo)(()=>{if(!C)return!1;if("blog"===h)return S;if("inherit"===h){if(!P)return!1;const e=C.find(e=>e.id===P);return!!e&&x(e.meta)}return!1},[h,P,C,S]),F=(0,d.useMemo)(()=>!(!E||!U)&&("blog"===h?O:U.id===P),[E,U,h,P,O]),I=(0,d.useMemo)(()=>F&&M?"blog"===h?M+"/wp-admin/options-general.php?page=activitypub&tab=blog-profile":M+"/wp-admin/profile.php#activitypub":null,[F,M,h]);(0,d.useEffect)(()=>{D.length&&("blog"===h||"inherit"===h||D.find(({value:e})=>e===h)||t({selectedUser:D[0].value}))},[h,D,t]);const B=[["core/heading",{level:3,placeholder:(0,v.__)("Fediverse Followers","activitypub"),content:(0,v.__)("Fediverse Followers","activitypub")}]];return(0,s.jsxs)("div",{...w,children:[(0,s.jsx)(c.InspectorControls,{children:(0,s.jsxs)(l.PanelBody,{title:(0,v.__)("Followers Options","activitypub"),children:[D.length>1&&(0,s.jsx)(l.SelectControl,{label:(0,v.__)("Select User","activitypub"),value:h,options:D,onChange:N("selectedUser"),__next40pxDefaultSize:!0}),(0,s.jsx)(l.SelectControl,{label:(0,v.__)("Sort","activitypub"),value:n,options:j,onChange:N("order"),__next40pxDefaultSize:!0}),(0,s.jsx)(l.RangeControl,{label:(0,v.__)("Number of Followers","activitypub"),value:o,onChange:N("per_page"),min:1,max:10,__next40pxDefaultSize:!0})]})},"setting"),(0,s.jsxs)("div",{className:"wp-block-activitypub-followers "+r,children:[(0,s.jsx)(c.InnerBlocks,{template:B,allowedBlocks:["core/heading"],templateLock:"all",renderAppender:!1}),E&&(0,s.jsx)(l.Notice,{status:"warning",isDismissible:!1,children:I?(0,d.createInterpolateElement)(/* translators: is a link to the profile settings page. */ /* translators: is a link to the profile settings page. */ +(0,v.__)("The selected user has their social graph hidden. This block will not display followers on the frontend. Edit privacy settings","activitypub"),{a:(0,s.jsx)("a",{href:I,target:"_blank",rel:"noopener noreferrer"})}):(0,v.__)("The selected user has their social graph hidden. This block will not display followers on the frontend.","activitypub")}),!E&&"inherit"===h&&P&&(0,s.jsx)(_,{selectedUser:P,perPage:o,order:n,endpoint:"followers",page:y,setPage:m,emptyMessage:(0,v.__)("No followers found.","activitypub"),navLabel:(0,v.__)("Follower navigation","activitypub")}),!E&&"inherit"===h&&!P&&(0,s.jsx)(f,{name:(0,v.__)("Followers","activitypub")}),!E&&"inherit"!==h&&(0,s.jsx)(_,{selectedUser:h,perPage:o,order:n,endpoint:"followers",page:y,setPage:m,emptyMessage:(0,v.__)("No followers found.","activitypub"),navLabel:(0,v.__)("Follower navigation","activitypub")})]})]})},save:function(){const e=c.useBlockProps.save(),t=c.useInnerBlocksProps.save(e);return(0,s.jsx)("div",{...t})},icon:n})}},i={};function a(e){var r=i[e];if(void 0!==r)return r.exports;var s=i[e]={exports:{}};return t[e](s,s.exports,a),s.exports}a.m=t,e=[],a.O=(t,i,r,s)=>{if(!i){var n=1/0;for(p=0;p=s)&&Object.keys(a.O).every(e=>a.O[e](i[l]))?i.splice(l--,1):(o=!1,s0&&e[p-1][2]>s;p--)e[p]=e[p-1];e[p]=[i,r,s]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var i in t)a.o(t,i)&&!a.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={149:0,17:0};a.O.j=t=>0===e[t];var t=(t,i)=>{var r,s,[n,o,l]=i,c=0;if(n.some(t=>0!==e[t])){for(r in o)a.o(o,r)&&(a.m[r]=o[r]);if(l)var p=l(a)}for(t&&t(i);ca(4736));r=a.O(r)})(); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/followers/render.php b/wp-content/plugins/activitypub/build/followers/render.php new file mode 100644 index 00000000..fc5cc81e --- /dev/null +++ b/wp-content/plugins/activitypub/build/followers/render.php @@ -0,0 +1,20 @@ + array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '34299fc181d49292ada0'); + array('@wordpress/interactivity'), 'version' => '2f63eedc944f22daa7c0', 'type' => 'module'); diff --git a/wp-content/plugins/activitypub/build/followers/view.js b/wp-content/plugins/activitypub/build/followers/view.js index 227bb012..06e1914d 100644 --- a/wp-content/plugins/activitypub/build/followers/view.js +++ b/wp-content/plugins/activitypub/build/followers/view.js @@ -1,3 +1 @@ -(()=>{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.__)(" 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 ","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{if(!a){var o=1/0;for(p=0;p=l)&&Object.keys(r.O).every((e=>r.O[e](a[i])))?a.splice(i--,1):(c=!1,l0&&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);sr(73)));n=r.O(n)})(); \ No newline at end of file +import*as e from"@wordpress/interactivity";const t=e,a=t.withSyncEvent??(e=>e);!function(){const{actions:e}=(0,t.store)("activitypub/followers",{state:{get paginationText(){const{page:e,pages:a}=(0,t.getContext)();return`${e} / ${a}`},get disablePreviousLink(){const{page:e}=(0,t.getContext)();return e<=1},get disableNextLink(){const{page:e,pages:a}=(0,t.getContext)();return e>=a}},actions:{async fetchItems(){const e=(0,t.getContext)(),{userId:a,page:n,perPage:r,order:o,endpoint:s}=e,{apiFetch:g,url:i}=window.wp;e.isLoading=!0;try{const{namespace:c}=(0,t.getConfig)(),p=i.addQueryArgs(`/${c}/actors/${a}/${s}`,{context:"full",per_page:r,order:o,page:n}),{orderedItems:l,totalItems:d}=await g({path:p});e.items=l.map(e=>({handle:"@"+e.webfinger,icon:e.icon,name:e.name||e.preferredUsername,url:e.url||e.id})),e.total=d,e.pages=Math.ceil(d/r)}catch(e){console.error(`Error fetching ${s}:`,e)}finally{e.isLoading=!1}},previousPage:a(a=>{a.preventDefault();const n=(0,t.getContext)();n.page>1&&(n.page--,e.fetchItems())}),nextPage:a(a=>{a.preventDefault();const n=(0,t.getContext)();n.page array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '579447d958b5f1081d10'); diff --git a/wp-content/plugins/activitypub/build/following/index.js b/wp-content/plugins/activitypub/build/following/index.js new file mode 100644 index 00000000..cb2c8053 --- /dev/null +++ b/wp-content/plugins/activitypub/build/following/index.js @@ -0,0 +1,3 @@ +(()=>{"use strict";var e,t={3508(e,t,i){const a=window.wp.blocks,s=window.wp.primitives,n=window.ReactJSXRuntime;var r=(0,n.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,n.jsx)(s.Path,{fillRule:"evenodd",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"})});const o=[{attributes:{title:{type:"string",default:"Fediverse Following"},selectedUser:{type:"string",default:"blog"},per_page:{type:"number",default:10},order:{type:"string",default:"desc",enum:["asc","desc"]}},supports:{html:!1},isEligible:({title:e})=>!!e,migrate:({title:e,...t})=>[t,[(0,a.createBlock)("core/heading",{content:e,level:3})]]}],l=window.wp.components,c=window.wp.blockEditor,p=window.wp.coreData,u=window.wp.data,d=window.wp.element,h=window.wp.i18n,v=window.wp.apiFetch;var g=i.n(v);function b(){return window._activityPubOptions||{}}function y({name:e}){const{enabled:t}=b(),i=t?.blog?"":(0,h.__)("It will be empty in other non-author contexts.","activitypub"),a=(0,h.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,h.__)("This %1$s 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,i).trim();return(0,n.jsx)(l.Card,{children:(0,n.jsx)(l.CardBody,{children:(0,d.createInterpolateElement)(a,{strong:(0,n.jsx)("strong",{})})})})}const f=window.wp.url;function w({name:e,icon:t,url:i,webfinger:a}){const s=`@${a}`,{defaultAvatarUrl:r,showAvatars:o}=b(),l=t?.url||r;return(0,n.jsxs)("a",{className:"activitypub-actor-link",href:i,title:s,onClick:e=>e.preventDefault(),children:[o&&(0,n.jsx)("img",{width:"48",height:"48",src:l,className:"activitypub-actor-avatar",alt:e,onError:e=>{e.target.src=r}}),(0,n.jsxs)("div",{className:"activitypub-actor-info",children:[(0,n.jsx)("span",{className:"activitypub-actor-name",children:e}),(0,n.jsx)("span",{className:"activitypub-actor-handle",children:s})]}),(0,n.jsx)("svg",{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",width:"24",height:"24",className:"external-link-icon","aria-hidden":"true",focusable:"false",fill:"currentColor",children:(0,n.jsx)("path",{d:"M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"})})]})}function m({page:e,pages:t,setPage:i,navLabel:a=(0,h.__)("Navigation","activitypub")}){if(t<=1)return null;const s=e<=1,r=e>=t;return(0,n.jsxs)("nav",{className:"activitypub-actor-list-pagination",role:"navigation",children:[(0,n.jsx)("h1",{className:"screen-reader-text",children:a}),(0,n.jsx)("a",{role:"button",tabIndex:0,className:"pagination-previous","aria-disabled":s,"aria-label":(0,h.__)("Previous page","activitypub"),onClick:()=>{s||i(e-1)},onKeyDown:t=>{s||"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),i(e-1))},children:(0,h.__)("Previous","activitypub")}),(0,n.jsx)("div",{className:"pagination-info",children:`${e} / ${t}`}),(0,n.jsx)("a",{role:"button",tabIndex:0,className:"pagination-next","aria-disabled":r,"aria-label":(0,h.__)("Next page","activitypub"),onClick:()=>{r||i(e+1)},onKeyDown:t=>{r||"Enter"!==t.key&&" "!==t.key||(t.preventDefault(),i(e+1))},children:(0,h.__)("Next","activitypub")})]})}function _({selectedUser:e,perPage:t,order:i,endpoint:a="followers",page:s,setPage:r,initialData:o=!1,emptyMessage:l=(0,h.__)("No results found.","activitypub"),navLabel:c=(0,h.__)("Navigation","activitypub")}){const{namespace:p}=b(),u="blog"===e?0:e,[v,y]=(0,d.useState)([]),[_,x]=(0,d.useState)(0),[j,k]=(0,d.useState)(1),N=s||j,S=r||k,U=(0,d.useCallback)((e,i)=>{y(e),x(Math.ceil(i/t))},[t]);return(0,d.useEffect)(()=>{if(o&&1===N)return U(o.items,o.total);const e=(0,f.addQueryArgs)(`/${p}/actors/${u}/${a}`,{per_page:t,order:i,page:N,context:"full"});g()({path:e}).then(({orderedItems:e=[],totalItems:t=0})=>U(e,t)).catch(()=>U([],0))},[p,u,t,i,N,a,o,U]),(0,n.jsxs)("div",{className:"activitypub-actor-list-container",children:[v.length?(0,n.jsx)("ul",{className:"activitypub-actor-list",children:v.map(e=>(0,n.jsx)("li",{className:"activitypub-actor-item",children:(0,n.jsx)(w,{...e})},e.url))}):(0,n.jsx)("p",{className:"activitypub-actor-list-placeholder",children:l}),(0,n.jsx)(m,{page:N,pages:_,setPage:S,navLabel:c})]})}function x(e){return!!e&&Object.entries(e).some(([e,t])=>e.endsWith("activitypub_hide_social_graph")&&t)}const j=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","name":"activitypub/following","apiVersion":3,"version":"8.2.1","title":"Fediverse Following","category":"widgets","description":"Display the accounts you follow in the Fediverse on your website.","textdomain":"activitypub","icon":"groups","keywords":["fediverse","activitypub","following","mastodon"],"supports":{"html":false,"interactivity":true},"attributes":{"selectedUser":{"type":"string","default":"blog"},"per_page":{"type":"number","default":10},"order":{"type":"string","default":"desc","enum":["asc","desc"]}},"usesContext":["postType","postId"],"styles":[{"name":"default","label":"Default","isDefault":true},{"name":"card","label":"Card"},{"name":"compact","label":"Compact"}],"editorScript":"file:./index.js","editorStyle":"file:./index.css","viewScriptModule":"file:./view.js","viewScript":"wp-api-fetch","style":["file:./style-index.css"],"render":"file:./render.php"}');(0,a.registerBlockType)(j,{deprecated:o,edit:function({attributes:e,setAttributes:t,context:{postType:i,postId:a}}){const{className:s="",order:r,per_page:o,selectedUser:v}=e,f=(0,c.useBlockProps)(),[w,m]=(0,d.useState)(1),j=[{label:(0,h.__)("New to old","activitypub"),value:"desc"},{label:(0,h.__)("Old to new","activitypub"),value:"asc"}],k=function({withInherit:e=!1}){const{enabled:t,namespace:i}=b(),[a,s]=(0,d.useState)(!1),{fetchedUsers:n,isLoadingUsers:r}=(0,u.useSelect)(e=>{const{getUsers:i,getIsResolving:a}=e("core");return{fetchedUsers:t?.users?i({capabilities:"activitypub"}):null,isLoadingUsers:!!t?.users&&a("getUsers",[{capabilities:"activitypub"}])}},[t?.users]),o=(0,u.useSelect)(e=>n||r?null:e("core").getCurrentUser(),[n,r]);(0,d.useEffect)(()=>{!n&&!r&&o&&i&&g()({path:`/${i}/actors/${o.id}`,method:"HEAD",headers:{Accept:"application/activity+json"},parse:!1}).then(()=>s(!0)).catch(()=>s(!1))},[n,r,o,i]);const l=(0,d.useMemo)(()=>n||(o&&a?[{id:o.id,name:o.name}]:[]),[n,o,a]);return(0,d.useMemo)(()=>{if(!l.length)return[];const i=[];return t?.blog&&n&&i.push({label:(0,h.__)("Blog","activitypub"),value:"blog"}),e&&t?.users&&n&&i.push({label:(0,h.__)("Dynamic User","activitypub"),value:"inherit"}),l.reduce((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e),i)},[l,t?.blog,t?.users,n,e])}({withInherit:!0}),N=e=>i=>{m(1),t({[e]:i})},{blogSocialGraphHidden:S,currentUser:U,usersWithMeta:C,siteUrl:M,canManageOptions:O}=(0,u.useSelect)(e=>{const{getCurrentUser:t,getUsers:i,getEntityRecord:a,canUser:s}=e(p.store),n=a("root","site"),r=a("root","__unstableBase");return{blogSocialGraphHidden:!!n?.activitypub_hide_social_graph,currentUser:t(),usersWithMeta:i({capabilities:"activitypub",context:"edit"}),siteUrl:r?.home,canManageOptions:s("update",{kind:"root",name:"site"})}},[]),P=(0,u.useSelect)(e=>{const{getEditedEntityRecord:t}=e(p.store),s=t("postType",i,a)?.author;return s??null},[i,a]),E=(0,d.useMemo)(()=>k.length&&C?k.filter(({value:e})=>{if("inherit"===e)return!0;if("blog"===e)return!S;const t=C?.find(t=>String(t.id)===e);return!x(t?.meta)}):[],[k,S,C]),D=(0,d.useMemo)(()=>{if(!C)return!1;if("blog"===v)return S;if("inherit"===v){if(!P)return!1;const e=C.find(e=>e.id===P);return!!e&&x(e.meta)}return!1},[v,P,C,S]),F=(0,d.useMemo)(()=>!(!D||!U)&&("blog"===v?O:U.id===P),[D,U,v,P,O]),I=(0,d.useMemo)(()=>F&&M?"blog"===v?M+"/wp-admin/options-general.php?page=activitypub&tab=blog-profile":M+"/wp-admin/profile.php#activitypub":null,[F,M,v]);(0,d.useEffect)(()=>{E.length&&("blog"===v||"inherit"===v||E.find(({value:e})=>e===v)||t({selectedUser:E[0].value}))},[v,E,t]);const B=[["core/heading",{level:3,placeholder:(0,h.__)("Fediverse Following","activitypub"),content:(0,h.__)("Fediverse Following","activitypub")}]];return(0,n.jsxs)("div",{...f,children:[(0,n.jsx)(c.InspectorControls,{children:(0,n.jsxs)(l.PanelBody,{title:(0,h.__)("Following Options","activitypub"),children:[E.length>1&&(0,n.jsx)(l.SelectControl,{label:(0,h.__)("Select User","activitypub"),value:v,options:E,onChange:N("selectedUser"),__next40pxDefaultSize:!0}),(0,n.jsx)(l.SelectControl,{label:(0,h.__)("Sort","activitypub"),value:r,options:j,onChange:N("order"),__next40pxDefaultSize:!0}),(0,n.jsx)(l.RangeControl,{label:(0,h.__)("Number of Following","activitypub"),value:o,onChange:N("per_page"),min:1,max:10,__next40pxDefaultSize:!0})]})},"setting"),(0,n.jsxs)("div",{className:`wp-block-activitypub-following ${s}`,children:[(0,n.jsx)(c.InnerBlocks,{template:B,allowedBlocks:["core/heading"],templateLock:"all",renderAppender:!1}),D&&(0,n.jsx)(l.Notice,{status:"warning",isDismissible:!1,children:I?(0,d.createInterpolateElement)(/* translators: is a link to the profile settings page. */ /* translators: is a link to the profile settings page. */ +(0,h.__)("The selected user has their social graph hidden. This block will not display following on the frontend. Edit privacy settings","activitypub"),{a:(0,n.jsx)(l.ExternalLink,{href:I})}):(0,h.__)("The selected user has their social graph hidden. This block will not display following on the frontend.","activitypub")}),!D&&"inherit"===v&&P&&(0,n.jsx)(_,{selectedUser:P,perPage:o,order:r,endpoint:"following",page:w,setPage:m,emptyMessage:(0,h.__)("Not following anyone yet.","activitypub"),navLabel:(0,h.__)("Following navigation","activitypub")}),!D&&"inherit"===v&&!P&&(0,n.jsx)(y,{name:(0,h.__)("Following","activitypub")}),!D&&"inherit"!==v&&(0,n.jsx)(_,{selectedUser:v,perPage:o,order:r,endpoint:"following",page:w,setPage:m,emptyMessage:(0,h.__)("Not following anyone yet.","activitypub"),navLabel:(0,h.__)("Following navigation","activitypub")})]})]})},save:function(){const e=c.useBlockProps.save(),t=c.useInnerBlocksProps.save(e);return(0,n.jsx)("div",{...t})},icon:r})}},i={};function a(e){var s=i[e];if(void 0!==s)return s.exports;var n=i[e]={exports:{}};return t[e](n,n.exports,a),n.exports}a.m=t,e=[],a.O=(t,i,s,n)=>{if(!i){var r=1/0;for(p=0;p=n)&&Object.keys(a.O).every(e=>a.O[e](i[l]))?i.splice(l--,1):(o=!1,n0&&e[p-1][2]>n;p--)e[p]=e[p-1];e[p]=[i,s,n]},a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var i in t)a.o(t,i)&&!a.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={801:0,509:0};a.O.j=t=>0===e[t];var t=(t,i)=>{var s,n,[r,o,l]=i,c=0;if(r.some(t=>0!==e[t])){for(s in o)a.o(o,s)&&(a.m[s]=o[s]);if(l)var p=l(a)}for(t&&t(i);ca(3508));s=a.O(s)})(); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/following/render.php b/wp-content/plugins/activitypub/build/following/render.php new file mode 100644 index 00000000..8bc91290 --- /dev/null +++ b/wp-content/plugins/activitypub/build/following/render.php @@ -0,0 +1,20 @@ + array('@wordpress/interactivity'), 'version' => '94e5efb023ccc7caf799', 'type' => 'module'); diff --git a/wp-content/plugins/activitypub/build/following/view.js b/wp-content/plugins/activitypub/build/following/view.js new file mode 100644 index 00000000..0d3263d7 --- /dev/null +++ b/wp-content/plugins/activitypub/build/following/view.js @@ -0,0 +1 @@ +import*as e from"@wordpress/interactivity";const t=e,a=t.withSyncEvent??(e=>e);!function(){const{actions:e}=(0,t.store)("activitypub/following",{state:{get paginationText(){const{page:e,pages:a}=(0,t.getContext)();return`${e} / ${a}`},get disablePreviousLink(){const{page:e}=(0,t.getContext)();return e<=1},get disableNextLink(){const{page:e,pages:a}=(0,t.getContext)();return e>=a}},actions:{async fetchItems(){const e=(0,t.getContext)(),{userId:a,page:n,perPage:r,order:o,endpoint:s}=e,{apiFetch:g,url:i}=window.wp;e.isLoading=!0;try{const{namespace:c}=(0,t.getConfig)(),p=i.addQueryArgs(`/${c}/actors/${a}/${s}`,{context:"full",per_page:r,order:o,page:n}),{orderedItems:l,totalItems:d}=await g({path:p});e.items=l.map(e=>({handle:"@"+e.webfinger,icon:e.icon,name:e.name||e.preferredUsername,url:e.url||e.id})),e.total=d,e.pages=Math.ceil(d/r)}catch(e){console.error(`Error fetching ${s}:`,e)}finally{e.isLoading=!1}},previousPage:a(a=>{a.preventDefault();const n=(0,t.getContext)();n.page>1&&(n.page--,e.fetchItems())}),nextPage:a(a=>{a.preventDefault();const n=(0,t.getContext)();n.page array('react-jsx-runtime', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-i18n'), 'version' => 'c8dfeeece9537c9d6183'); diff --git a/wp-content/plugins/activitypub/build/posts-and-replies/index.js b/wp-content/plugins/activitypub/build/posts-and-replies/index.js new file mode 100644 index 00000000..0fa2b88e --- /dev/null +++ b/wp-content/plugins/activitypub/build/posts-and-replies/index.js @@ -0,0 +1 @@ +(()=>{"use strict";var e,t={5879(){const e=window.wp.blocks,t=window.wp.blockEditor,i=window.wp.components,s=window.wp.i18n,r=window.ReactJSXRuntime,o=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","name":"activitypub/posts-and-replies","apiVersion":3,"version":"unreleased","title":"Posts and Replies","category":"widgets","description":"Display a tab bar to filter between posts only and posts with replies on author archives.","textdomain":"activitypub","icon":"admin-post","keywords":["fediverse","activitypub","posts","replies","tabs"],"supports":{"html":false},"editorScript":"file:./index.js","style":["file:./style-index.css"],"render":"file:./render.php"}');(0,e.registerBlockType)(o,{edit:function(){const e=(0,t.useBlockProps)();return(0,r.jsx)("div",{...e,children:(0,r.jsx)(i.Placeholder,{icon:"admin-post",label:(0,s.__)("Posts and Replies","activitypub"),instructions:(0,s.__)('Displays a tab bar to filter between "Posts" (excluding replies) and "Posts & Replies" on author archives. Place above a Query Loop block with "Inherit query from template" enabled.',"activitypub")})})},save:()=>null})}},i={};function s(e){var r=i[e];if(void 0!==r)return r.exports;var o=i[e]={exports:{}};return t[e](o,o.exports,s),o.exports}s.m=t,e=[],s.O=(t,i,r,o)=>{if(!i){var n=1/0;for(c=0;c=o)&&Object.keys(s.O).every(e=>s.O[e](i[p]))?i.splice(p--,1):(a=!1,o0&&e[c-1][2]>o;c--)e[c]=e[c-1];e[c]=[i,r,o]},s.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={642:0,6:0};s.O.j=t=>0===e[t];var t=(t,i)=>{var r,o,[n,a,p]=i,l=0;if(n.some(t=>0!==e[t])){for(r in a)s.o(a,r)&&(s.m[r]=a[r]);if(p)var c=p(s)}for(t&&t(i);ls(5879));r=s.O(r)})(); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/posts-and-replies/render.php b/wp-content/plugins/activitypub/build/posts-and-replies/render.php new file mode 100644 index 00000000..dcf9e7a6 --- /dev/null +++ b/wp-content/plugins/activitypub/build/posts-and-replies/render.php @@ -0,0 +1,54 @@ + + diff --git a/wp-content/plugins/activitypub/build/posts-and-replies/style-index-rtl.css b/wp-content/plugins/activitypub/build/posts-and-replies/style-index-rtl.css new file mode 100644 index 00000000..1b83fa7b --- /dev/null +++ b/wp-content/plugins/activitypub/build/posts-and-replies/style-index-rtl.css @@ -0,0 +1 @@ +.wp-block-activitypub-posts-and-replies .ap-tabs{border-bottom:1px solid;display:flex;gap:0;margin-bottom:1.5em}.wp-block-activitypub-posts-and-replies .ap-tabs__tab{background:none;border:none;border-bottom:2px solid transparent;color:inherit;cursor:pointer;font-family:inherit;font-size:inherit;margin-bottom:-1px;opacity:.6;padding:.5em 1em;text-decoration:none;transition:opacity .2s,border-color .2s}.wp-block-activitypub-posts-and-replies .ap-tabs__tab:focus-visible,.wp-block-activitypub-posts-and-replies .ap-tabs__tab:hover{opacity:1}.wp-block-activitypub-posts-and-replies .ap-tabs__tab.is-active{border-bottom-color:currentcolor;opacity:1} diff --git a/wp-content/plugins/activitypub/build/posts-and-replies/style-index.css b/wp-content/plugins/activitypub/build/posts-and-replies/style-index.css new file mode 100644 index 00000000..1b83fa7b --- /dev/null +++ b/wp-content/plugins/activitypub/build/posts-and-replies/style-index.css @@ -0,0 +1 @@ +.wp-block-activitypub-posts-and-replies .ap-tabs{border-bottom:1px solid;display:flex;gap:0;margin-bottom:1.5em}.wp-block-activitypub-posts-and-replies .ap-tabs__tab{background:none;border:none;border-bottom:2px solid transparent;color:inherit;cursor:pointer;font-family:inherit;font-size:inherit;margin-bottom:-1px;opacity:.6;padding:.5em 1em;text-decoration:none;transition:opacity .2s,border-color .2s}.wp-block-activitypub-posts-and-replies .ap-tabs__tab:focus-visible,.wp-block-activitypub-posts-and-replies .ap-tabs__tab:hover{opacity:1}.wp-block-activitypub-posts-and-replies .ap-tabs__tab.is-active{border-bottom-color:currentcolor;opacity:1} diff --git a/wp-content/plugins/activitypub/build/pre-publish-panel/block.json b/wp-content/plugins/activitypub/build/pre-publish-panel/block.json new file mode 100644 index 00000000..dd1d305d --- /dev/null +++ b/wp-content/plugins/activitypub/build/pre-publish-panel/block.json @@ -0,0 +1,11 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "activitypub/pre-publish-panel", + "title": "ActivityPub Post Format Suggestions", + "category": "widgets", + "description": "Suggests optimal post formats for ActivityPub federation before publishing.", + "icon": "layout", + "textdomain": "activitypub", + "editorScript": "file:./plugin.js" +} \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/pre-publish-panel/plugin.asset.php b/wp-content/plugins/activitypub/build/pre-publish-panel/plugin.asset.php new file mode 100644 index 00000000..100a49bc --- /dev/null +++ b/wp-content/plugins/activitypub/build/pre-publish-panel/plugin.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-block-editor', 'wp-components', 'wp-data', 'wp-dom', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins'), 'version' => '8bf8219b700555911ae5'); diff --git a/wp-content/plugins/activitypub/build/pre-publish-panel/plugin.js b/wp-content/plugins/activitypub/build/pre-publish-panel/plugin.js new file mode 100644 index 00000000..23ea2053 --- /dev/null +++ b/wp-content/plugins/activitypub/build/pre-publish-panel/plugin.js @@ -0,0 +1,3 @@ +(()=>{"use strict";const t=window.wp.editor,e=window.wp.blockEditor,o=window.wp.plugins,i=window.wp.components,n=window.wp.data,a=window.wp.element,s=window.wp.i18n,r=window.wp.dom,u=window._activityPubOptions?.noteLength||500,l=["youtube","vimeo","dailymotion","tiktok","videopress"],d=["spotify","soundcloud","mixcloud"],c=["core/paragraph","core/heading","core/list-item","core/preformatted","core/verse","core/pullquote"],p=["core/gallery","jetpack/tiled-gallery","jetpack/slideshow"],g={gallery:(0,s.__)("Gallery","activitypub"),video:(0,s.__)("Video","activitypub"),audio:(0,s.__)("Audio","activitypub"),image:(0,s.__)("Image","activitypub"),status:(0,s.__)("Status","activitypub")},m=(t,e)=>"core/pullquote"===t?e?.value||"":e?.content||"",h=t=>{const e={imageCount:0,galleryCount:0,videoCount:0,audioCount:0,textLength:0,textBlockCount:0};if(!t||!t.length)return e;for(const o of t){const{name:t,attributes:i,innerBlocks:n}=o;if("core/image"===t)e.imageCount++;else if(p.includes(t))e.galleryCount++;else if("core/video"===t)e.videoCount++;else if("core/audio"===t)e.audioCount++;else if("core/embed"===t){const t=(i?.providerNameSlug||"").toLowerCase();l.includes(t)?e.videoCount++:d.includes(t)&&e.audioCount++}if(c.includes(t)){const o=(0,r.__unstableStripHTML)(m(t,i));e.textLength+=o.length,e.textBlockCount++}if(n&&n.length){const t=h(n);e.imageCount+=t.imageCount,e.galleryCount+=t.galleryCount,e.videoCount+=t.videoCount,e.audioCount+=t.audioCount,e.textLength+=t.textLength,e.textBlockCount+=t.textBlockCount}}return e},v=window.ReactJSXRuntime;(0,o.registerPlugin)("activitypub-pre-publish",{render:()=>{const{blocks:o,postFormat:r}=(0,n.useSelect)(o=>({blocks:o(e.store).getBlocks(),postFormat:o(t.store).getEditedPostAttribute("format")}),[]),{editPost:l}=(0,n.useDispatch)(t.store),d=(0,a.useMemo)(()=>((t,e)=>{if(e&&"standard"!==e)return null;const o=h(t),i=o.imageCount>0||o.galleryCount>0||o.videoCount>0||o.audioCount>0;return(o.galleryCount>0||o.imageCount>1)&&o.textLength0&&o.textLength0&&o.textLength0&&o.textLength<280&&o.textBlockCount<=3?{format:"status",message:(0,s.__)("This is a short post with no media. Changing the format to Status won't change your post content, but will share it as a Note on the Fediverse, which is the standard format on platforms like Mastodon.","activitypub")}:null})(o,r),[o,r]);if("wordpress-post-format"!==window._activityPubOptions?.objectType)return null;if(!d){if(r&&"standard"!==r&&g[r]){const e=g[r]||r;return(0,v.jsx)(t.PluginPrePublishPanel,{title:(0,s.__)("Fediverse ⁂","activitypub"),initialOpen:!0,children:(0,v.jsx)("p",{children:(0,s.sprintf)(/* translators: %s: The current post format name (e.g., "Image", "Gallery", "Video"). */ /* translators: %s: The current post format name (e.g., "Image", "Gallery", "Video"). */ +(0,s.__)("This post will be shared in %s format on the Fediverse.","activitypub"),e)})})}return null}return(0,v.jsxs)(t.PluginPrePublishPanel,{title:(0,s.__)("Fediverse ⁂","activitypub"),initialOpen:!0,children:[(0,v.jsx)("p",{children:d.message}),(0,v.jsx)(i.Button,{variant:"secondary",onClick:()=>l({format:d.format}),children:(0,s.sprintf)(/* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ /* translators: %s: The suggested post format name (e.g., "Image", "Gallery", "Video"). */ +(0,s.__)("Set format to %s","activitypub"),g[d.format]||d.format)})]})}})})(); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/reactions/block.json b/wp-content/plugins/activitypub/build/reactions/block.json index e489c0f8..9b86041d 100644 --- a/wp-content/plugins/activitypub/build/reactions/block.json +++ b/wp-content/plugins/activitypub/build/reactions/block.json @@ -1,27 +1,66 @@ { "$schema": "https://schemas.wp.org/trunk/block.json", "name": "activitypub/reactions", - "apiVersion": 2, - "version": "1.0.0", + "apiVersion": 3, + "version": "8.3.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" - } + "keywords": [ + "fediverse", + "activitypub", + "likes", + "reposts" + ], + "description": "Display Fediverse likes and reposts for your posts.", + "example": { + "attributes": { + "className": "is-style-facepile" } }, + "styles": [ + { + "name": "facepile", + "label": "Facepile", + "isDefault": true + }, + { + "name": "compact", + "label": "Compact" + } + ], "attributes": { - "title": { + "displayStyle": { "type": "string", - "default": "Fediverse reactions" + "default": "facepile" + }, + "showActions": { + "type": "boolean", + "default": false + } + }, + "supports": { + "align": [ + "wide", + "full" + ], + "color": { + "gradients": true + }, + "__experimentalBorder": { + "radius": true, + "width": true, + "color": true, + "style": true + }, + "html": false, + "interactivity": true, + "shadow": true, + "typography": { + "fontSize": true, + "__experimentalDefaultControls": { + "fontSize": true + } } }, "blockHooks": { @@ -29,9 +68,9 @@ }, "textdomain": "activitypub", "editorScript": "file:./index.js", - "style": [ - "file:./style-index.css", - "wp-components" - ], - "viewScript": "file:./view.js" + "style": "file:./style-index.css", + "viewStyle": "file:./view.css", + "viewScriptModule": "file:./view.js", + "viewScript": "wp-api-fetch", + "render": "file:./render.php" } \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/reactions/index.asset.php b/wp-content/plugins/activitypub/build/reactions/index.asset.php index 6ac7d749..17b7abd7 100644 --- a/wp-content/plugins/activitypub/build/reactions/index.asset.php +++ b/wp-content/plugins/activitypub/build/reactions/index.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '32631215c76c36b38e5e'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n'), 'version' => '1e13568c338d5be171c1'); diff --git a/wp-content/plugins/activitypub/build/reactions/index.js b/wp-content/plugins/activitypub/build/reactions/index.js index 7ab57c58..19b80e02 100644 --- a/wp-content/plugins/activitypub/build/reactions/index.js +++ b/wp-content/plugins/activitypub/build/reactions/index.js @@ -1,3 +1,4 @@ -(()=>{"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=l)&&Object.keys(n.O).every((e=>n.O[e](a[i])))?a.splice(i--,1):(s=!1,l0&&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);cn(373)));r=n.O(r)})(); \ No newline at end of file +(()=>{"use strict";var e,t={2091(e,t,s){const i=window.wp.blocks,r=window.wp.blockEditor,a=window.ReactJSXRuntime,n=[{attributes:{},supports:{html:!1,align:!0,layout:{default:{type:"constrained",orientation:"vertical",justifyContent:"center"}}},save:()=>(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(r.InnerBlocks.Content,{}),(0,a.jsx)("div",{className:"activitypub-reactions-block"})]})},{attributes:{title:{type:"string",default:"Fediverse reactions"}},supports:{html:!1,align:!0,layout:{default:{type:"constrained",orientation:"vertical",justifyContent:"center"}}},save:()=>null,isEligible:({title:e})=>!!e,migrate:({title:e,...t})=>[t,[(0,i.createBlock)("core/heading",{content:e,level:6})]]}];function o(e){var t,s,i="";if("string"==typeof e||"number"==typeof e)i+=e;else if("object"==typeof e)if(Array.isArray(e)){var r=e.length;for(t=0;t{const{defaultAvatarUrl:s}=y();return"compact"===t?null:(0,a.jsx)("ul",{className:"reaction-avatars",children:e.map((e,t)=>{const i=["reaction-avatar"].filter(Boolean).join(" "),r=e.avatar||s;return(0,a.jsx)("li",{children:(0,a.jsx)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer",children:(0,a.jsx)("img",{src:r,alt:e.name,className:i,width:"32",height:"32",onError:e=>{e.target.src=s}})})},t)})})},b=({reactions:e,displayStyle:t})=>{const{defaultAvatarUrl:s}=y();return(0,a.jsx)("ul",{className:"reactions-list",children:e.map((e,i)=>{const r=e.avatar||s;return(0,a.jsx)("li",{className:"reaction-item",children:(0,a.jsxs)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer",children:["facepile"===t&&(0,a.jsx)("img",{src:r,alt:e.name,width:"32",height:"32",onError:e=>{e.target.src=s}}),(0,a.jsx)("span",{className:"reaction-name",children:e.name})]})},i)})})},w=({items:e,label:t,displayStyle:s,showActions:i,actionLabel:r})=>{const[n,o]=(0,d.useState)(!1),[l,p]=(0,d.useState)(null),u=(0,d.useRef)(null),f=e.slice(0,20);return(0,a.jsxs)("div",{className:"reaction-group",ref:u,children:[i&&r&&(0,a.jsx)("button",{className:"reaction-action-button wp-element-button",type:"button",disabled:!0,children:r}),(0,a.jsx)(h,{reactions:f,displayStyle:s}),(0,a.jsx)(c.Button,{ref:p,className:"reaction-label is-link",onClick:()=>o(!n),"aria-expanded":n,children:t}),n&&l&&(0,a.jsx)(c.Popover,{anchor:l,onClose:()=>o(!1),className:"activitypub-popover",children:(0,a.jsx)(b,{reactions:e,displayStyle:s})})]})};function g({postId:e=null,reactions:t=null,fallbackReactions:s=null,displayStyle:i="facepile",showActions:r=!1}){const{namespace:n}=y(),[o,l]=(0,d.useState)(t),[c,p]=(0,d.useState)(!t),u=(0,d.useCallback)(()=>{s&&l(s),p(!1)},[s]);return(0,d.useEffect)(()=>{if(t)return l(t),void p(!1);e&&"number"==typeof e?(p(!0),v()({path:`/${n}/posts/${e}/reactions`}).then(e=>{const t=Object.values(e).some(e=>e.items?.length>0);l(!t&&s?s:e),p(!1)}).catch(u)):u()},[e,t,s,n,u]),c?null:o&&Object.values(o).some(e=>e.items?.length>0)?(0,a.jsx)("div",{className:"activitypub-reactions",children:Object.entries(o).map(([e,t])=>t.items?.length?(0,a.jsx)(w,{items:t.items,label:t.label,displayStyle:i,showActions:r,actionLabel:m[e]},e):null)}):null}const x=(e,t,s,i)=>Array.from({length:e},(e,r)=>({name:`${t} ${r+1}`,url:"#",avatar:`data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 64 64'%3E%3Ccircle cx='32' cy='32' r='32' fill='%23${i[r%i.length]}'/%3E%3Ctext x='32' y='38' font-family='sans-serif' font-size='24' fill='white' text-anchor='middle'%3E${String.fromCharCode(s+r)}%3C/text%3E%3C/svg%3E`})),j=["FF6B6B","4ECDC4","45B7D1","96CEB4","D4A5A5","9B59B6","3498DB","E67E22"],k={likes:{label:(0,p.sprintf)(/* translators: %d: Number of likes */ /* translators: %d: Number of likes */ +(0,p._x)("%d likes","number of likes","activitypub"),9),items:x(9,"User",65,j)},reposts:{label:(0,p.sprintf)(/* translators: %d: Number of reposts */ /* translators: %d: Number of reposts */ +(0,p._x)("%d reposts","number of reposts","activitypub"),6),items:x(6,"Reposter",82,j)},quotes:{label:(0,p.sprintf)(/* translators: %d: Number of quotes */ /* translators: %d: Number of quotes */ +(0,p._x)("%d quotes","number of quotes","activitypub"),7),items:x(7,"Quoter",81,j)}},_=JSON.parse('{"$schema":"https://schemas.wp.org/trunk/block.json","name":"activitypub/reactions","apiVersion":3,"version":"8.2.1","title":"Fediverse Reactions","category":"widgets","icon":"heart","keywords":["fediverse","activitypub","likes","reposts"],"description":"Display Fediverse likes and reposts for your posts.","example":{"attributes":{"className":"is-style-facepile"}},"styles":[{"name":"facepile","label":"Facepile","isDefault":true},{"name":"compact","label":"Compact"}],"attributes":{"displayStyle":{"type":"string","default":"facepile"},"showActions":{"type":"boolean","default":false}},"supports":{"align":["wide","full"],"color":{"gradients":true},"__experimentalBorder":{"radius":true,"width":true,"color":true,"style":true},"html":false,"interactivity":true,"shadow":true,"typography":{"fontSize":true,"__experimentalDefaultControls":{"fontSize":true}}},"blockHooks":{"core/post-content":"after"},"textdomain":"activitypub","editorScript":"file:./index.js","style":"file:./style-index.css","viewStyle":"file:./view.css","viewScriptModule":"file:./view.js","viewScript":"wp-api-fetch","render":"file:./render.php"}');(0,i.registerBlockType)(_,{deprecated:n,edit:function({attributes:e,setAttributes:t}){const{className:s="",displayStyle:i="facepile",showActions:n=!1}=e,o=(0,r.useBlockProps)(),{getCurrentPostId:f}=(0,u.select)("core/editor"),{showAvatars:v=!0}=y(),m=(0,d.useRef)(!1);(0,d.useEffect)(()=>{if(m.current)return;m.current=!0;const e=s?.includes("is-style-");if(!e){const e=v?"facepile":"compact";t({className:l(s,`is-style-${e}`),displayStyle:e})}},[s,v,t]);const h=s?.includes("is-style-compact")?"compact":"facepile";(0,d.useEffect)(()=>{h!==i&&t({displayStyle:h})},[h,i,t]);const b=[["core/heading",{level:6,placeholder:(0,p.__)("Fediverse Reactions","activitypub"),content:(0,p.__)("Fediverse Reactions","activitypub")}]];return(0,a.jsxs)(a.Fragment,{children:[(0,a.jsx)(r.InspectorControls,{children:(0,a.jsx)(c.PanelBody,{title:(0,p.__)("Action Buttons","activitypub"),children:(0,a.jsx)(c.ToggleControl,{__nextHasNoMarginBottom:!0,label:(0,p.__)("Show action buttons","activitypub"),help:(0,p.__)("Display Like and Boost buttons so visitors can interact from their Fediverse server.","activitypub"),checked:n,onChange:e=>t({showActions:e})})})}),(0,a.jsxs)("div",{...o,children:[(0,a.jsx)(r.InnerBlocks,{template:b,allowedBlocks:["core/heading"],templateLock:"all",renderAppender:!1}),(0,a.jsx)(g,{postId:f(),fallbackReactions:k,displayStyle:i,showActions:n})]})]})},save:function(){return(0,a.jsx)("div",{...r.useBlockProps.save(),children:(0,a.jsx)(r.InnerBlocks.Content,{})})}})}},s={};function i(e){var r=s[e];if(void 0!==r)return r.exports;var a=s[e]={exports:{}};return t[e](a,a.exports,i),a.exports}i.m=t,e=[],i.O=(t,s,r,a)=>{if(!s){var n=1/0;for(p=0;p=a)&&Object.keys(i.O).every(e=>i.O[e](s[l]))?s.splice(l--,1):(o=!1,a0&&e[p-1][2]>a;p--)e[p]=e[p-1];e[p]=[s,r,a]},i.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return i.d(t,{a:t}),t},i.d=(e,t)=>{for(var s in t)i.o(t,s)&&!i.o(e,s)&&Object.defineProperty(e,s,{enumerable:!0,get:t[s]})},i.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={608:0,104:0};i.O.j=t=>0===e[t];var t=(t,s)=>{var r,a,[n,o,l]=s,c=0;if(n.some(t=>0!==e[t])){for(r in o)i.o(o,r)&&(i.m[r]=o[r]);if(l)var p=l(i)}for(t&&t(s);ci(2091));r=i.O(r)})(); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/reactions/render.php b/wp-content/plugins/activitypub/build/reactions/render.php new file mode 100644 index 00000000..68b7fb26 --- /dev/null +++ b/wp-content/plugins/activitypub/build/reactions/render.php @@ -0,0 +1,418 @@ + null, + 'displayStyle' => $default_display_style, + 'showActions' => false, + ) +); + +/* @var \WP_Block $block Current block. */ +$block = $block ?? ''; + +/* @var string $content Block content. */ +$content = $content ?? ''; + +if ( empty( $content ) ) { + // Fallback for v1.0.0 blocks. + $_title = $attributes['title'] ?? __( 'Fediverse Reactions', 'activitypub' ); + $content = '
' . esc_html( $_title ) . '
'; + unset( $attributes['title'] ); +} else { + $content = implode( PHP_EOL, wp_list_pluck( $block->parsed_block['innerBlocks'], 'innerHTML' ) ); + // Hide empty headings. + if ( empty( wp_strip_all_tags( $content ) ) ) { + $content = ''; + } +} + +// Get the Post ID from attributes or use the current post. +$_post_id = $attributes['postId'] ?? get_the_ID(); + +// Don't leak reaction metadata for posts that are not currently publicly queryable. +if ( ! is_post_publicly_queryable( $_post_id ) ) { + return; +} + +// Generate a unique ID for the block. +$block_id = 'activitypub-reactions-block-' . wp_unique_id(); + +/* + * Determine display style - compact style hides avatars. + * For auto-hooked blocks without explicit style, use avatar setting to determine style. + */ +$has_style_class = isset( $attributes['className'] ) && strpos( $attributes['className'], 'is-style-' ) !== false; +if ( ! $has_style_class ) { + $attributes['className'] = trim( ( $attributes['className'] ?? '' ) . ' is-style-' . $default_display_style ); + $attributes['displayStyle'] = $default_display_style; +} + +$show_avatars = 'facepile' === $attributes['displayStyle']; + +// Fetch reactions. +$reactions = array(); + +foreach ( Comment::get_comment_types() as $_type => $type_object ) { + $_comments = get_comments( + array( + 'post_id' => $_post_id, + 'type' => $_type, + 'status' => 'approve', + 'parent' => 0, + ) + ); + + if ( empty( $_comments ) ) { + continue; + } + + $count = count( $_comments ); + // phpcs:disable WordPress.WP.I18n + $label = sprintf( + _n( + $type_object['count_single'], + $type_object['count_plural'], + $count, + 'activitypub' + ), + number_format_i18n( $count ) + ); + // phpcs:enable WordPress.WP.I18n + + $reactions[ $_type ] = array( + 'label' => $label, + 'count' => $count, + 'items' => array_map( + static function ( $comment ) { + return array( + 'name' => html_entity_decode( $comment->comment_author ), + 'url' => $comment->comment_author_url, + 'avatar' => get_avatar_url( $comment ), + ); + }, + $_comments + ), + ); +} + +if ( empty( $reactions ) && ! $attributes['showActions'] ) { + echo ''; + return; +} + +// Set up the Interactivity API config. +$config = array( + 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + 'namespace' => ACTIVITYPUB_REST_NAMESPACE, +); + +if ( $attributes['showActions'] ) { + $config['i18n'] = array( + 'copied' => __( 'Copied!', 'activitypub' ), + 'copy' => __( 'Copy', 'activitypub' ), + 'emptyProfileError' => __( 'Please enter a profile URL or handle.', 'activitypub' ), + 'genericError' => __( 'An error occurred. Please try again.', 'activitypub' ), + 'intentLabelLike' => __( 'Like this post', 'activitypub' ), + 'intentLabelAnnounce' => __( 'Boost this post', 'activitypub' ), + 'invalidProfileError' => __( 'Please enter a valid profile URL or handle.', 'activitypub' ), + ); +} + +wp_interactivity_config( 'activitypub/reactions', $config ); + +// Set up the Interactivity API state. +wp_interactivity_state( 'activitypub/reactions', array( 'reactions' => array( $_post_id => $reactions ) ) ); + +// Render a subset of the most recent reactions for facepile. +$reactions = array_map( + static function ( $reaction ) use ( $attributes ) { + $count = 20; + if ( 'wide' === $attributes['align'] ) { + $count = 40; + } elseif ( 'full' === $attributes['align'] ) { + $count = 60; + } + + $reaction['items'] = array_slice( array_reverse( $reaction['items'] ), 0, $count ); + + return $reaction; + }, + $reactions +); + +// Initialize the context for the block. +$context = array( + 'blockId' => $block_id, + 'modal' => array( + 'isCompact' => true, + 'isOpen' => false, + 'items' => array(), + 'title' => '', + ), + 'postId' => $_post_id, + 'reactions' => $reactions, +); + +if ( $attributes['showActions'] ) { + $context['modal']['intent'] = ''; + $context['copyButtonText'] = __( 'Copy', 'activitypub' ); + $context['errorMessage'] = ''; + $context['isError'] = false; + $context['isLoading'] = false; + $context['postUrl'] = get_post_id( $_post_id ); + $context['remoteProfile'] = ''; + $context['shouldSaveProfile'] = true; +} + +// Map comment types to remote intent types. +$intent_map = array( + 'like' => 'like', + 'repost' => 'announce', + 'quote' => 'announce', +); + +// Build reactions content. +ob_start(); +?> +
+ $reaction ) : + /* translators: %s: reaction type. */ + $aria_label = sprintf( __( 'View all %s', 'activitypub' ), Comment::get_comment_type_attr( $_type, 'label' ) ); + $intent = isset( $intent_map[ $_type ] ) ? $intent_map[ $_type ] : ''; + ?> +
+ + + + +
    + +
+ + +
+ + +
+ $intent ) : ?> + + +
+ +
+ +
+
    + +
+
+ +
+
+

+
+ +
+
+ + + +
+
+
+

+
+ + +
+
+ + + +
+
+
+ +
+
+
+ + $modal_content, +); + +if ( $attributes['showActions'] ) { + $modal_args['title_binding'] = 'context.modal.title'; +} else { + $modal_args['is_compact'] = true; +} + +ob_start(); +Blocks::render_modal( $modal_args ); +$inner_content = $reactions_content . ob_get_clean(); + +$wrapper_attrs = array( + 'id' => $block_id, + 'class' => $attributes['className'] ?? '', + 'data-wp-interactive' => 'activitypub/reactions', + 'data-wp-context' => wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + 'data-wp-init' => 'callbacks.initReactions', +); + +$wrapper_attributes = get_block_wrapper_attributes( $wrapper_attrs ); + +// Render the block with common wrapper. +?> +
> + + +
diff --git a/wp-content/plugins/activitypub/build/reactions/style-index-rtl.css b/wp-content/plugins/activitypub/build/reactions/style-index-rtl.css index 593e3174..f08cdc13 100644 --- a/wp-content/plugins/activitypub/build/reactions/style-index-rtl.css +++ b/wp-content/plugins/activitypub/build/reactions/style-index-rtl.css @@ -1 +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} +body.modal-open{overflow:hidden}.activitypub-modal__overlay{--activitypub-modal-background-color:var(--wp--preset--color--base,var(--wp--preset--color--background,#fff));--activitypub-modal-border-color:var(--wp--preset--color--contrast-3,var(--wp--preset--color--light-gray,#e2e4e7));--activitypub-modal-radius:var(--wp--custom--border--radius--medium,8px);--activitypub-modal-shadow:var(--wp--preset--shadow--natural,0 5px 15px rgba(0,0,0,.3));--activitypub-modal-compact-shadow:var(--wp--preset--shadow--natural,0 2px 8px rgba(0,0,0,.1));--activitypub-modal-text-color:var(--wp--preset--color--contrast,var(--wp--preset--color--foreground,inherit));align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:var(--activitypub-modal-text-color);display:flex;justify-content:center;right:0;margin:0!important;max-width:none!important;padding:1rem;position:fixed;left:0;top:0;z-index:100000}.activitypub-modal__overlay.compact{align-items:flex-start;background-color:transparent;bottom:auto;justify-content:flex-start;right:auto;padding:0;position:absolute;left:auto;top:auto;z-index:100}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--activitypub-modal-background-color);border:1px solid var(--activitypub-modal-border-color);border-radius:var(--activitypub-modal-radius);box-shadow:var(--activitypub-modal-shadow);box-sizing:border-box;color:var(--wp--preset--color--contrast,#1e1e1e);display:flex;flex-direction:column;font-size:inherit;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.compact .activitypub-modal__frame{box-shadow:var(--activitypub-modal-compact-shadow);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;width:auto}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--activitypub-modal-border-color);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.compact .activitypub-modal__header{display:none}.activitypub-modal__header .activitypub-modal__close{align-items:center;background:transparent;border:none;border-radius:var(--activitypub-modal-radius);color:inherit;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}.activitypub-dialog__section{border-bottom:1px solid var(--activitypub-modal-border-color,var(--wp--preset--color--light-gray,#f0f0f0));padding:1.5rem 2rem}.activitypub-dialog__section:last-child{border-bottom:none;padding-bottom:2rem}.activitypub-dialog__section h4{font-size:110%;margin-bottom:.5rem;margin-top:0}.activitypub-dialog__description{color:inherit;margin-bottom:1rem}.activitypub-dialog__button-group{align-items:center;display:flex;gap:.5rem;margin-bottom:.5rem;width:100%}.activitypub-dialog__button-group input[type]{background-color:var(--activitypub-modal-background-color,#fff)!important;border:1px solid var(--activitypub-modal-border-color,#949494)!important;border-radius:0;box-sizing:border-box;color:inherit!important;flex:1;font-family:inherit;font-size:1em;line-height:1;margin:0;min-width:0;padding:calc(.667em + 2px)!important}.activitypub-dialog__button-group input[type]::-moz-placeholder{opacity:.5}.activitypub-dialog__button-group input[type]::placeholder{opacity:.5}.activitypub-dialog__button-group input[type][aria-invalid=true]{border-color:var(--wp--preset--color--vivid-red,#b32d2e)!important}.activitypub-dialog__button-group button{background-color:var(--wp--preset--color--contrast,#1e1e1e);border:none;box-sizing:border-box;color:var(--wp--preset--color--base,#fff);cursor:pointer;font-family:inherit;font-size:1em;line-height:1;min-width:22.5%;padding:calc(.667em + 2px) 1.5em;width:auto}.activitypub-dialog__error{color:var(--wp--preset--color--vivid-red,#b32d2e);font-size:90%;margin-top:.5rem}.activitypub-dialog__remember{font-size:90%;margin-top:1rem}.activitypub-dialog__remember label{align-items:center;display:flex;gap:.5rem}.activitypub-dialog__remember input[type=checkbox]{margin:0;position:relative;top:0}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.wp-block-activitypub-reactions{box-sizing:border-box;margin:2rem auto;max-width:var(--wp--style--global--content-size);position:relative}.wp-block-activitypub-reactions.alignwide{max-width:var(--wp--style--global--wide-size)}.wp-block-activitypub-reactions.alignfull{max-width:none}.wp-block-activitypub-reactions.has-background,.wp-block-activitypub-reactions.has-border{padding:2rem}.wp-block-activitypub-reactions .activitypub-reactions{display:flex;flex-direction:column;flex-wrap:wrap}.wp-block-activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75rem;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}.wp-block-activitypub-reactions .reaction-group .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0!important;padding:0}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li{margin:0 0 0 -10px;padding:0;transition:transform .2s ease}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li:not([hidden]):not(:has(~li:not([hidden]))){margin-left:0}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li:hover{transform:translateY(-2px);z-index:2}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li a{border-radius:50%;box-shadow:none;display:block;line-height:1;text-decoration:none}.wp-block-activitypub-reactions .reaction-group .reaction-avatar{max-height:32px;max-width:32px;overflow:hidden;-moz-force-broken-image-icon:1;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);transition:transform .6s cubic-bezier(.34,1.56,.64,1);will-change:transform}.wp-block-activitypub-reactions .reaction-group .reaction-avatar:focus-visible,.wp-block-activitypub-reactions .reaction-group .reaction-avatar:hover{position:relative;transform:translateY(-5px);z-index:1}.wp-block-activitypub-reactions .reaction-group .reaction-label{align-items:center;background:none;border:none;color:currentcolor;cursor:pointer;display:flex;flex:0 0 auto;font:inherit;font-size:var(--wp--preset--font-size--small,.875rem);gap:.25rem;padding:.25rem 0;text-decoration:none;white-space:nowrap}.wp-block-activitypub-reactions .reaction-group .reaction-label:hover{color:currentcolor;text-decoration:underline}.wp-block-activitypub-reactions .reaction-group .reaction-label:focus{box-shadow:none;outline:none}.wp-block-activitypub-reactions .reaction-group .reaction-label:focus-visible{outline:1px solid currentcolor;outline-offset:2px}.wp-block-activitypub-reactions .reaction-actions-only{display:flex;flex-direction:row;flex-wrap:wrap;gap:.5rem;margin:.5em 0}.wp-block-activitypub-reactions .reaction-action-button{background:none;border:1px solid;border-radius:4px;color:currentcolor;cursor:pointer;flex:0 0 auto;font-size:var(--wp--preset--font-size--small,.875rem);min-width:5.5em;padding:.25rem .75rem;text-align:center;text-decoration:none;transition:background-color .2s ease;white-space:nowrap}.wp-block-activitypub-reactions .reaction-action-button:hover{background-color:rgba(0,0,0,.05);color:currentcolor}.wp-block-activitypub-reactions .reaction-action-button:focus:not(:disabled){box-shadow:none;outline:1px solid currentcolor;outline-offset:2px}.reactions-list{list-style:none;margin:0!important;padding:.5rem}.components-popover__content>.reactions-list{padding:0}.reactions-list .reaction-item{margin:0 0 .5rem}.reactions-list .reaction-item:last-child{margin-bottom:0}.reactions-list .reaction-item a{align-items:center;border-radius:4px;box-shadow:none;color:inherit;display:flex;gap:.75rem;padding:.5rem;text-decoration:none;transition:background-color .2s ease}.reactions-list .reaction-item a:hover{background-color:rgba(0,0,0,.03)}.reactions-list .reaction-item img{border:1px solid var(--wp--preset--color--light-gray,#f0f0f0);border-radius:50%;box-shadow:none;height:36px;width:36px}.reactions-list .reaction-item .reaction-name{font-size:var(--wp--preset--font-size--small,.875rem)}.activitypub-popover .components-popover__content{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;padding:.5rem;width:auto}.wp-block-activitypub-reactions.is-style-compact .wp-block-heading{margin-bottom:.5em}.wp-block-activitypub-reactions.is-style-compact .activitypub-reactions{display:inline-flex;flex-direction:row;flex-wrap:wrap;gap:.5em}.wp-block-activitypub-reactions.is-style-compact .reaction-group{display:inline-flex;margin:0;width:auto}.wp-block-activitypub-reactions.is-style-compact .reaction-actions-only{display:inline-flex;margin:0} diff --git a/wp-content/plugins/activitypub/build/reactions/style-index.css b/wp-content/plugins/activitypub/build/reactions/style-index.css index 769742af..1ff699cd 100644 --- a/wp-content/plugins/activitypub/build/reactions/style-index.css +++ b/wp-content/plugins/activitypub/build/reactions/style-index.css @@ -1 +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} +body.modal-open{overflow:hidden}.activitypub-modal__overlay{--activitypub-modal-background-color:var(--wp--preset--color--base,var(--wp--preset--color--background,#fff));--activitypub-modal-border-color:var(--wp--preset--color--contrast-3,var(--wp--preset--color--light-gray,#e2e4e7));--activitypub-modal-radius:var(--wp--custom--border--radius--medium,8px);--activitypub-modal-shadow:var(--wp--preset--shadow--natural,0 5px 15px rgba(0,0,0,.3));--activitypub-modal-compact-shadow:var(--wp--preset--shadow--natural,0 2px 8px rgba(0,0,0,.1));--activitypub-modal-text-color:var(--wp--preset--color--contrast,var(--wp--preset--color--foreground,inherit));align-items:center;background-color:rgba(0,0,0,.5);bottom:0;color:var(--activitypub-modal-text-color);display:flex;justify-content:center;left:0;margin:0!important;max-width:none!important;padding:1rem;position:fixed;right:0;top:0;z-index:100000}.activitypub-modal__overlay.compact{align-items:flex-start;background-color:transparent;bottom:auto;justify-content:flex-start;left:auto;padding:0;position:absolute;right:auto;top:auto;z-index:100}.activitypub-modal__overlay[hidden]{display:none}.activitypub-modal__frame{animation:activitypub-modal-appear .2s ease-out;background-color:var(--activitypub-modal-background-color);border:1px solid var(--activitypub-modal-border-color);border-radius:var(--activitypub-modal-radius);box-shadow:var(--activitypub-modal-shadow);box-sizing:border-box;color:var(--wp--preset--color--contrast,#1e1e1e);display:flex;flex-direction:column;font-size:inherit;max-height:calc(100vh - 2rem);max-width:660px;overflow:hidden;width:100%}.compact .activitypub-modal__frame{box-shadow:var(--activitypub-modal-compact-shadow);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;width:auto}.activitypub-modal__header{align-items:center;border-bottom:1px solid var(--activitypub-modal-border-color);display:flex;flex-shrink:0;justify-content:space-between;padding:2rem 2rem 1.5rem}.compact .activitypub-modal__header{display:none}.activitypub-modal__header .activitypub-modal__close{align-items:center;background:transparent;border:none;border-radius:var(--activitypub-modal-radius);color:inherit;cursor:pointer;display:flex;justify-content:center;padding:.5rem;width:auto}.activitypub-modal__header .activitypub-modal__close:active{border:none;padding:.5rem}.activitypub-modal__title{font-size:130%;font-weight:600;line-height:1.4;margin:0!important}.activitypub-modal__content{overflow-y:auto}.activitypub-dialog__section{border-bottom:1px solid var(--activitypub-modal-border-color,var(--wp--preset--color--light-gray,#f0f0f0));padding:1.5rem 2rem}.activitypub-dialog__section:last-child{border-bottom:none;padding-bottom:2rem}.activitypub-dialog__section h4{font-size:110%;margin-bottom:.5rem;margin-top:0}.activitypub-dialog__description{color:inherit;margin-bottom:1rem}.activitypub-dialog__button-group{align-items:center;display:flex;gap:.5rem;margin-bottom:.5rem;width:100%}.activitypub-dialog__button-group input[type]{background-color:var(--activitypub-modal-background-color,#fff)!important;border:1px solid var(--activitypub-modal-border-color,#949494)!important;border-radius:0;box-sizing:border-box;color:inherit!important;flex:1;font-family:inherit;font-size:1em;line-height:1;margin:0;min-width:0;padding:calc(.667em + 2px)!important}.activitypub-dialog__button-group input[type]::-moz-placeholder{opacity:.5}.activitypub-dialog__button-group input[type]::placeholder{opacity:.5}.activitypub-dialog__button-group input[type][aria-invalid=true]{border-color:var(--wp--preset--color--vivid-red,#b32d2e)!important}.activitypub-dialog__button-group button{background-color:var(--wp--preset--color--contrast,#1e1e1e);border:none;box-sizing:border-box;color:var(--wp--preset--color--base,#fff);cursor:pointer;font-family:inherit;font-size:1em;line-height:1;min-width:22.5%;padding:calc(.667em + 2px) 1.5em;width:auto}.activitypub-dialog__error{color:var(--wp--preset--color--vivid-red,#b32d2e);font-size:90%;margin-top:.5rem}.activitypub-dialog__remember{font-size:90%;margin-top:1rem}.activitypub-dialog__remember label{align-items:center;display:flex;gap:.5rem}.activitypub-dialog__remember input[type=checkbox]{margin:0;position:relative;top:0}@keyframes activitypub-modal-appear{0%{opacity:0;transform:translateY(20px)}to{opacity:1;transform:translateY(0)}}.wp-block-activitypub-reactions{box-sizing:border-box;margin:2rem auto;max-width:var(--wp--style--global--content-size);position:relative}.wp-block-activitypub-reactions.alignwide{max-width:var(--wp--style--global--wide-size)}.wp-block-activitypub-reactions.alignfull{max-width:none}.wp-block-activitypub-reactions.has-background,.wp-block-activitypub-reactions.has-border{padding:2rem}.wp-block-activitypub-reactions .activitypub-reactions{display:flex;flex-direction:column;flex-wrap:wrap}.wp-block-activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75rem;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}.wp-block-activitypub-reactions .reaction-group .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0!important;padding:0}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li{margin:0 -10px 0 0;padding:0;transition:transform .2s ease}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li:not([hidden]):not(:has(~li:not([hidden]))){margin-right:0}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li:hover{transform:translateY(-2px);z-index:2}.wp-block-activitypub-reactions .reaction-group .reaction-avatars li a{border-radius:50%;box-shadow:none;display:block;line-height:1;text-decoration:none}.wp-block-activitypub-reactions .reaction-group .reaction-avatar{max-height:32px;max-width:32px;overflow:hidden;-moz-force-broken-image-icon:1;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);transition:transform .6s cubic-bezier(.34,1.56,.64,1);will-change:transform}.wp-block-activitypub-reactions .reaction-group .reaction-avatar:focus-visible,.wp-block-activitypub-reactions .reaction-group .reaction-avatar:hover{position:relative;transform:translateY(-5px);z-index:1}.wp-block-activitypub-reactions .reaction-group .reaction-label{align-items:center;background:none;border:none;color:currentcolor;cursor:pointer;display:flex;flex:0 0 auto;font:inherit;font-size:var(--wp--preset--font-size--small,.875rem);gap:.25rem;padding:.25rem 0;text-decoration:none;white-space:nowrap}.wp-block-activitypub-reactions .reaction-group .reaction-label:hover{color:currentcolor;text-decoration:underline}.wp-block-activitypub-reactions .reaction-group .reaction-label:focus{box-shadow:none;outline:none}.wp-block-activitypub-reactions .reaction-group .reaction-label:focus-visible{outline:1px solid currentcolor;outline-offset:2px}.wp-block-activitypub-reactions .reaction-actions-only{display:flex;flex-direction:row;flex-wrap:wrap;gap:.5rem;margin:.5em 0}.wp-block-activitypub-reactions .reaction-action-button{background:none;border:1px solid;border-radius:4px;color:currentcolor;cursor:pointer;flex:0 0 auto;font-size:var(--wp--preset--font-size--small,.875rem);min-width:5.5em;padding:.25rem .75rem;text-align:center;text-decoration:none;transition:background-color .2s ease;white-space:nowrap}.wp-block-activitypub-reactions .reaction-action-button:hover{background-color:rgba(0,0,0,.05);color:currentcolor}.wp-block-activitypub-reactions .reaction-action-button:focus:not(:disabled){box-shadow:none;outline:1px solid currentcolor;outline-offset:2px}.reactions-list{list-style:none;margin:0!important;padding:.5rem}.components-popover__content>.reactions-list{padding:0}.reactions-list .reaction-item{margin:0 0 .5rem}.reactions-list .reaction-item:last-child{margin-bottom:0}.reactions-list .reaction-item a{align-items:center;border-radius:4px;box-shadow:none;color:inherit;display:flex;gap:.75rem;padding:.5rem;text-decoration:none;transition:background-color .2s ease}.reactions-list .reaction-item a:hover{background-color:rgba(0,0,0,.03)}.reactions-list .reaction-item img{border:1px solid var(--wp--preset--color--light-gray,#f0f0f0);border-radius:50%;box-shadow:none;height:36px;width:36px}.reactions-list .reaction-item .reaction-name{font-size:var(--wp--preset--font-size--small,.875rem)}.activitypub-popover .components-popover__content{box-shadow:0 2px 8px rgba(0,0,0,.1);max-height:300px;max-width:-moz-min-content;max-width:min-content;min-width:250px;padding:.5rem;width:auto}.wp-block-activitypub-reactions.is-style-compact .wp-block-heading{margin-bottom:.5em}.wp-block-activitypub-reactions.is-style-compact .activitypub-reactions{display:inline-flex;flex-direction:row;flex-wrap:wrap;gap:.5em}.wp-block-activitypub-reactions.is-style-compact .reaction-group{display:inline-flex;margin:0;width:auto}.wp-block-activitypub-reactions.is-style-compact .reaction-actions-only{display:inline-flex;margin:0} diff --git a/wp-content/plugins/activitypub/build/reactions/view.asset.php b/wp-content/plugins/activitypub/build/reactions/view.asset.php index 814279a1..fe305c93 100644 --- a/wp-content/plugins/activitypub/build/reactions/view.asset.php +++ b/wp-content/plugins/activitypub/build/reactions/view.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => 'd5cb95d9bd6062974b3c'); + array('@wordpress/interactivity'), 'version' => 'c92a83cbb3c92ac4f478', 'type' => 'module'); diff --git a/wp-content/plugins/activitypub/build/reactions/view.css b/wp-content/plugins/activitypub/build/reactions/view.css new file mode 100644 index 00000000..0cfa09f5 --- /dev/null +++ b/wp-content/plugins/activitypub/build/reactions/view.css @@ -0,0 +1 @@ +.wp-block-activitypub-reactions .activitypub-modal__overlay.compact .activitypub-modal__frame{max-width:100%;min-width:250px} diff --git a/wp-content/plugins/activitypub/build/reactions/view.js b/wp-content/plugins/activitypub/build/reactions/view.js index 4e4ff64e..71c1fe17 100644 --- a/wp-content/plugins/activitypub/build/reactions/view.js +++ b/wp-content/plugins/activitypub/build/reactions/view.js @@ -1 +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}))}))}))})(); \ No newline at end of file +import*as t from"@wordpress/interactivity";const e=t,o=e.withSyncEvent??(t=>t);!function(){const{actions:t,callbacks:n}=(0,e.store)("activitypub/reactions",{actions:{openModal(t){const o=(0,e.getContext)();if(o.modal.isOpen=!0,o.modal.isCompact)setTimeout(n.positionModal,0);else{const t=document.getElementById(o.blockId);if(t){const e=t.querySelector(".activitypub-modal__overlay");e&&["top","left","right","bottom"].forEach(t=>{e.style.removeProperty(t)})}setTimeout(()=>{if(t){const e=t.querySelector(".activitypub-modal__frame");e&&n.trapFocus(e)}},50)}"function"==typeof n.onModalOpen&&n.onModalOpen(t)},closeModal(t){const o=(0,e.getContext)();o.modal.isOpen=!1;const r=(0,e.getElement)();if("actions.toggleModal"===r.ref.dataset["wpOn-Click"])r.ref.focus();else{const t=document.getElementById(o.blockId);if(t){const e=t.querySelector('[data-wp-on--click="actions.toggleModal"]');e&&e.focus()}}"function"==typeof n.onModalClose&&n.onModalClose(t)},toggleModal:o(o=>{o?.preventDefault?.();const{modal:n}=(0,e.getContext)();n.isOpen?t.closeModal(o):t.openModal(o)})},callbacks:{_abortController:null,handleModalEffects(){const{modal:t}=(0,e.getContext)();if(t.isOpen&&!t.isCompact?document.body.classList.add("modal-open"):document.body.classList.remove("modal-open"),n._abortController&&(n._abortController.abort(),n._abortController=null),t.isOpen){n._abortController=new AbortController;const{signal:t}=n._abortController;document.addEventListener("keydown",n.documentKeydown,{signal:t}),document.addEventListener("click",n.documentClick,{signal:t})}},documentKeydown(o){const{modal:n}=(0,e.getContext)();n.isOpen&&"Escape"===o.key&&t.closeModal()},documentClick(o){const{blockId:n,modal:r}=(0,e.getContext)();if(!r.isOpen)return;const a=document.getElementById(n);if(!a)return;const s=a.querySelectorAll('[data-wp-on--click="actions.toggleModal"]');for(const t of s)if(t===o.target||t.contains(o.target))return;const i=a.querySelector(".activitypub-modal__frame");i&&!i.contains(o.target)&&t.closeModal()},positionModal(){const{blockId:t}=(0,e.getContext)(),o=document.getElementById(t);if(!o)return;const n=o.querySelector(".activitypub-modal__overlay");if(!n)return;n.style.top="",n.style.left="",n.style.right="",n.style.bottom="";const r=(0,e.getElement)().ref.getBoundingClientRect(),a=window.innerWidth,s=o.getBoundingClientRect(),i={top:r.bottom-s.top+8+"px",left:r.left-s.left-2+"px"};a-r.right<250&&(i.left="auto",i.right=s.right-r.right+"px"),Object.assign(n.style,i)},trapFocus(t){const e=t.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]):not([readonly]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'),o=e[0],n=e[e.length-1];o&&o.classList.contains("activitypub-modal__close")&&e.length>1?e[1].focus():o.focus(),t.addEventListener("keydown",function(e){if("Tab"!==e.key&&9!==e.keyCode)return;const r=t.ownerDocument.activeElement;e.shiftKey?r===o&&(n.focus(),e.preventDefault()):r===n&&(o.focus(),e.preventDefault())})}}})}();const n={like:"intentLabelLike",announce:"intentLabelAnnounce"},r="fediverse-remote-user",{actions:a,callbacks:s,state:i}=(0,e.store)("activitypub/reactions",{actions:{async fetchReactions(){const t=(0,e.getContext)();if(!t.postId)return;const{namespace:o}=(0,e.getConfig)(),{apiFetch:n}=window.wp;try{t.reactions=await n({path:`/${o}/posts/${t.postId}/reactions`})}catch(t){console.error("Error fetching reactions:",t)}},openIntentModal(){const t=(0,e.getContext)(),o=(0,e.getElement)().ref.dataset.intent;t.modal.isCompact=!1,t.modal.intent=o;const{i18n:r}=(0,e.getConfig)();t.modal.title=r[n[o]]||o;const{profileURL:i}=s.getStore();i&&(t.remoteProfile=i),a.openModal()},updateIntentProfile(t){const o=(0,e.getContext)();o.remoteProfile=t.target.value,o.isError=!1,o.errorMessage=""},onIntentKeydown(t){"Enter"===t.key&&(t.preventDefault(),a.submitIntent())},*submitIntent(){const t=(0,e.getContext)(),{namespace:o,i18n:n}=(0,e.getConfig)(),{apiFetch:r}=window.wp,i=t.remoteProfile.trim();if(!i)return t.isError=!0,void(t.errorMessage=n.emptyProfileError);if(!s.isHandle(i)&&!s.isUrl(i))return t.isError=!0,void(t.errorMessage=n.invalidProfileError);t.isLoading=!0,t.isError=!1,t.errorMessage="";const l=`/${o}/posts/${t.postId}/remote-intent?resource=${encodeURIComponent(i)}&intent=${encodeURIComponent(t.modal.intent)}`;try{const e=yield r({path:l});t.isLoading=!1,window.open(e.url,"_blank","noopener,noreferrer"),a.closeModal(),t.shouldSaveProfile&&s.setStore({profileURL:i})}catch(e){console.error("Error submitting intent:",e),t.isLoading=!1,t.isError=!0,t.errorMessage=e.message||n.genericError}},copyPostUrl(){const t=(0,e.getContext)(),{i18n:o}=(0,e.getConfig)();navigator.clipboard.writeText(t.postUrl).then(()=>{t.copyButtonText=o.copied,setTimeout(()=>{t.copyButtonText=o.copy},1e3)},t=>{console.error("Could not copy text: ",t)})},toggleRememberProfile(){const t=(0,e.getContext)();t.shouldSaveProfile=!t.shouldSaveProfile}},callbacks:{initReactions(){const t=new ResizeObserver((0,e.withScope)(s.calculateVisibleAvatars));return(0,e.getElement)().ref.querySelectorAll(".reaction-group").forEach(e=>{t.observe(e)}),()=>{t.disconnect()}},calculateVisibleAvatars(){const{postId:t}=(0,e.getContext)();(i.reactions&&i.reactions[t]?Object.keys(i.reactions[t]):[]).forEach(o=>{i.reactions?.[t][o]?.items?.length&&(0,e.getElement)().ref.querySelectorAll(`.reaction-group[data-reaction-type="${o}"]`).forEach(e=>{const n=e.querySelector(".reaction-label").offsetWidth||0,r=e.querySelector(".reaction-action-button"),a=r?r.offsetWidth+12:0,s=e.offsetWidth-n-a-12;let l=1;s>32&&(l+=Math.floor((s-32)/22));const c=i.reactions[t][o].items,d=Math.min(l,c.length),u=e.querySelector(".reaction-avatars");u&&u.querySelectorAll("li").forEach((t,e)=>{e array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '7160b6399cd924e1c7be'); diff --git a/wp-content/plugins/activitypub/build/remote-reply/index.js b/wp-content/plugins/activitypub/build/remote-reply/index.js deleted file mode 100644 index 1494b656..00000000 --- a/wp-content/plugins/activitypub/build/remote-reply/index.js +++ /dev/null @@ -1,2 +0,0 @@ -(()=>{"use strict";var e,t={170:(e,t,r)=>{var o=r(609);const a=window.wp.element,i=window.wp.domReady;var n=r.n(i);const l=window.wp.components,c=window.wp.i18n,s=(0,a.forwardRef)((function({icon:e,size:t=24,...r},o){return(0,a.cloneElement)(e,{width:t,height:t,...r,ref:o})})),m=window.wp.primitives;var p=r(848);const u=(0,p.jsx)(m.SVG,{viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,p.jsx)(m.Path,{d:"M12 21C16.9706 21 21 16.9706 21 12C21 7.02944 16.9706 3 12 3C7.02944 3 3 7.02944 3 12C3 16.9706 7.02944 21 12 21ZM15.5303 8.46967C15.8232 8.76256 15.8232 9.23744 15.5303 9.53033L13.0607 12L15.5303 14.4697C15.8232 14.7626 15.8232 15.2374 15.5303 15.5303C15.2374 15.8232 14.7626 15.8232 14.4697 15.5303L12 13.0607L9.53033 15.5303C9.23744 15.8232 8.76256 15.8232 8.46967 15.5303C8.17678 15.2374 8.17678 14.7626 8.46967 14.4697L10.9393 12L8.46967 9.53033C8.17678 9.23744 8.17678 8.76256 8.46967 8.46967C8.76256 8.17678 9.23744 8.17678 9.53033 8.46967L12 10.9393L14.4697 8.46967C14.7626 8.17678 15.2374 8.17678 15.5303 8.46967Z"})}),d=window.wp.apiFetch;var v=r.n(d);const y=(0,p.jsx)(m.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(m.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M5 4.5h11a.5.5 0 0 1 .5.5v11a.5.5 0 0 1-.5.5H5a.5.5 0 0 1-.5-.5V5a.5.5 0 0 1 .5-.5ZM3 5a2 2 0 0 1 2-2h11a2 2 0 0 1 2 2v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5Zm17 3v10.75c0 .69-.56 1.25-1.25 1.25H6v1.5h12.75a2.75 2.75 0 0 0 2.75-2.75V8H20Z"})}),_=(0,p.jsx)(m.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(m.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),f=(0,p.jsx)(m.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(m.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"})}),b=window.wp.compose,w="fediverse-remote-user";function h(){const[e,t]=(0,a.useState)(function(){const e=localStorage.getItem(w);return e?JSON.parse(e):{}}()),r=(0,a.useCallback)((e=>{!function(e){localStorage.setItem(w,JSON.stringify(e))}(e),t(e)}),[]),o=(0,a.useCallback)((()=>{localStorage.removeItem(w),t({})}),[]);return{template:e?.template||!1,profileURL:e?.profileURL||!1,setRemoteUser:r,deleteRemoteUser:o}}function g(e){try{return new URL(e),!0}catch(e){return!1}}function E({actionText:e,copyDescription:t,handle:r,resourceUrl:i,myProfile:n="",rememberProfile:m=!1}){const p=(0,c.__)("Loading...","activitypub"),u=(0,c.__)("Opening...","activitypub"),d=(0,c.__)("Error","activitypub"),w=(0,c.__)("Invalid","activitypub"),E=n||(0,c.__)("My Profile","activitypub"),[C,R]=(0,a.useState)(e),[x,O]=(0,a.useState)(y),k=(0,b.useCopyToClipboard)(r,(()=>{O(_),setTimeout((()=>O(y)),1e3)})),[L,S]=(0,a.useState)(""),[U,N]=(0,a.useState)(!0),{setRemoteUser:P}=h(),j=(0,a.useCallback)((()=>{let t;if(!g(L)&&!function(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&g(`https://${t[1]}`)}(L))return R(w),t=setTimeout((()=>R(e)),2e3),()=>clearTimeout(t);const r=i+L;R(p),v()({path:r}).then((({url:t,template:r})=>{U&&P({profileURL:L,template:r}),R(u),setTimeout((()=>{window.open(t,"_blank"),R(e)}),200)})).catch((()=>{R(d),setTimeout((()=>R(e)),2e3)}))}),[L]);return(0,o.createElement)("div",{className:"activitypub__dialog",role:"dialog","aria-labelledby":"dialog-title"},(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"dialog-title"},E),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"copy-description"},t),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"profile-handle",className:"screen-reader-text"},t),(0,o.createElement)("input",{type:"text",id:"profile-handle",value:r,readOnly:!0}),(0,o.createElement)(l.Button,{ref:k,"aria-label":(0,c.__)("Copy handle to clipboard","activitypub")},(0,o.createElement)(s,{icon:x}),(0,c.__)("Copy","activitypub")))),(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",{id:"remote-profile-title"},(0,c.__)("Your Profile","activitypub")),(0,o.createElement)("div",{className:"activitypub-dialog__description",id:"remote-profile-description"},(0,a.createInterpolateElement)((0,c.__)("Or, if you know your own profile, we can start things that way! (eg @yourusername@example.com)","activitypub"),{code:(0,o.createElement)("code",null)})),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,c.__)("Enter your ActivityPub profile","activitypub")),(0,o.createElement)("input",{type:"text",id:"remote-profile",value:L,onKeyDown:e=>{"Enter"===e?.code&&j()},onChange:e=>S(e.target.value),"aria-invalid":C===w}),(0,o.createElement)(l.Button,{onClick:j,"aria-label":(0,c.__)("Submit profile","activitypub")},(0,o.createElement)(s,{icon:f}),C)),m&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(l.CheckboxControl,{checked:U,label:(0,c.__)("Remember me for easier comments","activitypub"),onChange:()=>{N(!U)}}))))}function C({selectedComment:e,commentId:t}){const{namespace:r}=window._activityPubOptions||{},a=(0,c.__)("Reply","activitypub"),i=`/${r}/comments/${t}/remote-reply?resource=`,n=(0,c.__)("Copy and paste the Comment URL into the search field of your favorite fediverse app or server.","activitypub");return(0,o.createElement)(E,{actionText:a,copyDescription:n,handle:e,resourceUrl:i,myProfile:(0,c.__)("Original Comment URL","activitypub"),rememberProfile:!0})}function R({profileURL:e,template:t,commentURL:r,deleteRemoteUser:a}){return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(l.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>{const e=t.replace("{uri}",r);window.open(e,"_blank")}},/* translators: %s: profile name */ /* translators: %s: profile name */ -(0,c.sprintf)((0,c.__)("Reply as %s","activitypub"),e)),(0,o.createElement)(l.Button,{className:"activitypub-remote-profile-delete",onClick:a,title:(0,c.__)("Delete Remote Profile","activitypub")},(0,o.createElement)(s,{icon:u,size:18})))}function x({selectedComment:e,commentId:t}){const[r,i]=(0,a.useState)(!1),n=(0,c.__)("Remote Reply","activitypub"),{profileURL:s,template:m,deleteRemoteUser:p}=h(),u=s&&m;return(0,o.createElement)(o.Fragment,null,u?(0,o.createElement)(R,{profileURL:s,template:m,commentURL:e,deleteRemoteUser:p}):(0,o.createElement)(l.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>i(!0)},(0,c.__)("Reply on the Fediverse","activitypub")),r&&(0,o.createElement)(l.Modal,{className:"activitypub-remote-reply__modal activitypub__modal",onRequestClose:()=>i(!1),title:n},(0,o.createElement)(C,{selectedComment:e,commentId:t})))}let O=1;n()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-remote-reply"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,a.createRoot)(e).render((0,o.createElement)(x,{...t,id:"activitypub-remote-reply-link-"+O++,useId:!0}))}))}))},20:(e,t,r)=>{var o=r(609),a=Symbol.for("react.element"),i=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),n=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,c={},s=null,m=null;for(o in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(m=t.ref),t)i.call(t,o)&&!l.hasOwnProperty(o)&&(c[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===c[o]&&(c[o]=t[o]);return{$$typeof:a,type:e,key:s,ref:m,props:c,_owner:n.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},r={};function o(e){var a=r[e];if(void 0!==a)return a.exports;var i=r[e]={exports:{}};return t[e](i,i.exports,o),i.exports}o.m=t,e=[],o.O=(t,r,a,i)=>{if(!r){var n=1/0;for(m=0;m=i)&&Object.keys(o.O).every((e=>o.O[e](r[c])))?r.splice(c--,1):(l=!1,i0&&e[m-1][2]>i;m--)e[m]=e[m-1];e[m]=[r,a,i]},o.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return o.d(t,{a:t}),t},o.d=(e,t)=>{for(var r in t)o.o(t,r)&&!o.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},o.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={227:0,739:0};o.O.j=t=>0===e[t];var t=(t,r)=>{var a,i,n=r[0],l=r[1],c=r[2],s=0;if(n.some((t=>0!==e[t]))){for(a in l)o.o(l,a)&&(o.m[a]=l[a]);if(c)var m=c(o)}for(t&&t(r);so(170)));a=o.O(a)})(); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/remote-reply/render.php b/wp-content/plugins/activitypub/build/remote-reply/render.php new file mode 100644 index 00000000..2591a187 --- /dev/null +++ b/wp-content/plugins/activitypub/build/remote-reply/render.php @@ -0,0 +1,215 @@ + ACTIVITYPUB_REST_NAMESPACE, + 'i18n' => array( + 'copied' => __( 'Copied!', 'activitypub' ), + 'copy' => __( 'Copy', 'activitypub' ), + 'emptyProfileError' => __( 'Please enter a profile URL or handle.', 'activitypub' ), + 'genericError' => __( 'An error occurred. Please try again.', 'activitypub' ), + 'invalidProfileError' => __( 'Please enter a valid profile URL or handle.', 'activitypub' ), + ), + ) +); + +// Add the block wrapper attributes. +$wrapper_attributes = get_block_wrapper_attributes( + array( + 'id' => $block_id, + 'class' => 'activitypub-remote-reply reply', + 'data-wp-interactive' => 'activitypub/remote-reply', + 'data-wp-init' => 'callbacks.init', + ) +); + +$wrapper_context = wp_interactivity_data_wp_context( + array( + 'blockId' => $block_id, + 'commentId' => $comment_id, + 'commentURL' => $selected_comment, + 'copyButtonText' => __( 'Copy', 'activitypub' ), + 'errorMessage' => '', + 'isError' => false, + 'isLoading' => false, + 'modal' => array( 'isOpen' => false ), + 'remoteProfile' => '', + 'shouldSaveProfile' => true, + ) +); + +wp_interactivity_state( + 'activitypub/remote-reply', + array( + 'hasRemoteUser' => false, + 'profileURL' => '', + 'template' => '', + ) +); + +ob_start(); +?> +
+

+
+ +
+
+ + + +
+
+
+

+
+ + +
+
+ + + +
+
+
+ +
+
+ +
+ +> + + + + + $block_id . '-modal', + 'title' => __( 'Remote Reply', 'activitypub' ), + 'content' => $modal_content, + ) + ); + ?> +
+ array('@wordpress/interactivity'), 'version' => '283e9946a5209ff839e1', 'type' => 'module'); diff --git a/wp-content/plugins/activitypub/build/remote-reply/view.js b/wp-content/plugins/activitypub/build/remote-reply/view.js new file mode 100644 index 00000000..60383afb --- /dev/null +++ b/wp-content/plugins/activitypub/build/remote-reply/view.js @@ -0,0 +1 @@ +import*as e from"@wordpress/interactivity";var t,o,r={475(){const t=e,o=t.withSyncEvent??(e=>e);!function(){const{actions:e,callbacks:r}=(0,t.store)("activitypub/remote-reply",{actions:{openModal(e){const o=(0,t.getContext)();if(o.modal.isOpen=!0,o.modal.isCompact)setTimeout(r.positionModal,0);else{const e=document.getElementById(o.blockId);if(e){const t=e.querySelector(".activitypub-modal__overlay");t&&["top","left","right","bottom"].forEach(e=>{t.style.removeProperty(e)})}setTimeout(()=>{if(e){const t=e.querySelector(".activitypub-modal__frame");t&&r.trapFocus(t)}},50)}"function"==typeof r.onModalOpen&&r.onModalOpen(e)},closeModal(e){const o=(0,t.getContext)();o.modal.isOpen=!1;const n=(0,t.getElement)();if("actions.toggleModal"===n.ref.dataset["wpOn-Click"])n.ref.focus();else{const e=document.getElementById(o.blockId);if(e){const t=e.querySelector('[data-wp-on--click="actions.toggleModal"]');t&&t.focus()}}"function"==typeof r.onModalClose&&r.onModalClose(e)},toggleModal:o(o=>{o?.preventDefault?.();const{modal:r}=(0,t.getContext)();r.isOpen?e.closeModal(o):e.openModal(o)})},callbacks:{_abortController:null,handleModalEffects(){const{modal:e}=(0,t.getContext)();if(e.isOpen&&!e.isCompact?document.body.classList.add("modal-open"):document.body.classList.remove("modal-open"),r._abortController&&(r._abortController.abort(),r._abortController=null),e.isOpen){r._abortController=new AbortController;const{signal:e}=r._abortController;document.addEventListener("keydown",r.documentKeydown,{signal:e}),document.addEventListener("click",r.documentClick,{signal:e})}},documentKeydown(o){const{modal:r}=(0,t.getContext)();r.isOpen&&"Escape"===o.key&&e.closeModal()},documentClick(o){const{blockId:r,modal:n}=(0,t.getContext)();if(!n.isOpen)return;const l=document.getElementById(r);if(!l)return;const s=l.querySelectorAll('[data-wp-on--click="actions.toggleModal"]');for(const e of s)if(e===o.target||e.contains(o.target))return;const a=l.querySelector(".activitypub-modal__frame");a&&!a.contains(o.target)&&e.closeModal()},positionModal(){const{blockId:e}=(0,t.getContext)(),o=document.getElementById(e);if(!o)return;const r=o.querySelector(".activitypub-modal__overlay");if(!r)return;r.style.top="",r.style.left="",r.style.right="",r.style.bottom="";const n=(0,t.getElement)().ref.getBoundingClientRect(),l=window.innerWidth,s=o.getBoundingClientRect(),a={top:n.bottom-s.top+8+"px",left:n.left-s.left-2+"px"};l-n.right<250&&(a.left="auto",a.right=s.right-n.right+"px"),Object.assign(r.style,a)},trapFocus(e){const t=e.querySelectorAll('a[href]:not([disabled]), button:not([disabled]), textarea:not([disabled]), input[type="text"]:not([disabled]):not([readonly]), input[type="radio"]:not([disabled]), input[type="checkbox"]:not([disabled]), select:not([disabled])'),o=t[0],r=t[t.length-1];o&&o.classList.contains("activitypub-modal__close")&&t.length>1?t[1].focus():o.focus(),e.addEventListener("keydown",function(t){if("Tab"!==t.key&&9!==t.keyCode)return;const n=e.ownerDocument.activeElement;t.shiftKey?n===o&&(r.focus(),t.preventDefault()):n===r&&(o.focus(),t.preventDefault())})}}})}();const{actions:r,callbacks:n,state:l}=(0,t.store)("activitypub/remote-reply",{state:{get remoteProfileUrl(){const{commentURL:e}=(0,t.getContext)();return l.template.replace("{uri}",encodeURIComponent(e))}},actions:{onReplyLinkKeydown:o(e=>{"Enter"!==e.key&&" "!==e.key||(e.preventDefault(),r.toggleModal(e))}),copyToClipboard(){const e=(0,t.getContext)(),{i18n:o}=(0,t.getConfig)();navigator.clipboard.writeText(e.commentURL).then(()=>{e.copyButtonText=o.copied,setTimeout(()=>{e.copyButtonText=o.copy},1e3)},e=>{console.error("Could not copy text: ",e)})},updateRemoteProfile(e){const o=(0,t.getContext)();o.remoteProfile=e.target.value,o.isError=!1,o.errorMessage=""},onInputKeydown(e){if("Enter"===e.key)return e.preventDefault(),r.submitRemoteProfile()},*submitRemoteProfile(){const e=(0,t.getContext)(),{namespace:o,i18n:s}=(0,t.getConfig)(),{apiFetch:a}=window.wp,i=e.remoteProfile.trim();if(!i)return e.isError=!0,void(e.errorMessage=s.emptyProfileError);if(!n.isHandle(i)&&!n.isUrl(i))return e.isError=!0,void(e.errorMessage=s.invalidProfileError);e.isLoading=!0,e.isError=!1,e.errorMessage="";const c=`/${o}/comments/${e.commentId}/remote-reply?resource=${encodeURIComponent(i)}`;try{const{template:t,url:o}=yield a({path:c});e.isLoading=!1,window.open(o,"_blank"),r.closeModal(),e.shouldSaveProfile&&(n.setStore({profileURL:i,template:t}),Object.assign(l,{hasRemoteUser:!0,profileURL:i,template:t}))}catch(t){console.error("Error submitting profile:",t),e.isLoading=!1,e.isError=!0,e.errorMessage=t.message||s.genericError}},toggleRememberProfile(){const e=(0,t.getContext)();e.shouldSaveProfile=!e.shouldSaveProfile},deleteRemoteUser(){n.deleteStore(),l.hasRemoteUser=!1,l.profileURL="",l.template=""}},callbacks:{storageKey:"fediverse-remote-user",init(){const{profileURL:e,template:t}=n.getStore();e&&t&&Object.assign(l,{hasRemoteUser:!0,profileURL:e,template:t})},getStore(){const e=localStorage.getItem(n.storageKey);return e?JSON.parse(e):{}},setStore(e){localStorage.setItem(n.storageKey,JSON.stringify(e))},deleteStore(){localStorage.removeItem(n.storageKey)},isHandle(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&n.isUrl(`https://${t[1]}`)},isUrl(e){try{return new URL(e),!0}catch(e){return!1}}}})}},n={};function l(e){var t=n[e];if(void 0!==t)return t.exports;var o=n[e]={exports:{}};return r[e](o,o.exports,l),o.exports}l.m=r,t=[],l.O=(e,o,r,n)=>{if(!o){var s=1/0;for(d=0;d=n)&&Object.keys(l.O).every(e=>l.O[e](o[i]))?o.splice(i--,1):(a=!1,n0&&t[d-1][2]>n;d--)t[d]=t[d-1];t[d]=[o,r,n]},l.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t);export{l as __webpack_require__};o={466:0,354:0},l.C=e=>{var t,r,{__webpack_esm_ids__:n,__webpack_esm_modules__:s,__webpack_esm_runtime__:a}=e,i=0;for(t in s)l.o(s,t)&&(l.m[t]=s[t]);for(a&&a(l);i0===o[e];var s=l.O(void 0,[354],()=>l(475));s=l.O(s); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/reply-intent/block.json b/wp-content/plugins/activitypub/build/reply-intent/block.json index c2817199..2805e634 100644 --- a/wp-content/plugins/activitypub/build/reply-intent/block.json +++ b/wp-content/plugins/activitypub/build/reply-intent/block.json @@ -1,12 +1,11 @@ { - "name": "reply-handler", - "title": "Reply Handler: not a block, but block.json is very useful.", + "$schema": "https://schemas.wp.org/trunk/block.json", + "apiVersion": 3, + "name": "activitypub/reply-intent", + "title": "ActivityPub Reply Intent Handler", "category": "widgets", - "icon": "admin-comments", - "keywords": [ - "reply", - "handler", - "comments" - ], + "description": "Handles reply intents for federated conversations in the block editor.", + "icon": "comments", + "textdomain": "activitypub", "editorScript": "file:./plugin.js" } \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/reply-intent/plugin.asset.php b/wp-content/plugins/activitypub/build/reply-intent/plugin.asset.php index 72bbe977..2bb3bb5f 100644 --- a/wp-content/plugins/activitypub/build/reply-intent/plugin.asset.php +++ b/wp-content/plugins/activitypub/build/reply-intent/plugin.asset.php @@ -1 +1 @@ - array('wp-block-editor', 'wp-blocks', 'wp-data', 'wp-element', 'wp-plugins'), 'version' => 'f65a7269b5abb57d3e73'); + array('wp-block-editor', 'wp-blocks', 'wp-data', 'wp-element', 'wp-plugins'), 'version' => 'ee563d746b20aabb037a'); diff --git a/wp-content/plugins/activitypub/build/reply-intent/plugin.js b/wp-content/plugins/activitypub/build/reply-intent/plugin.js index b4a7e993..b0183a39 100644 --- a/wp-content/plugins/activitypub/build/reply-intent/plugin.js +++ b/wp-content/plugins/activitypub/build/reply-intent/plugin.js @@ -1 +1 @@ -(()=>{"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)})})(); \ No newline at end of file +(()=>{"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)})})(); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/reply/block.json b/wp-content/plugins/activitypub/build/reply/block.json index c3a57338..29421d14 100644 --- a/wp-content/plugins/activitypub/build/reply/block.json +++ b/wp-content/plugins/activitypub/build/reply/block.json @@ -2,16 +2,27 @@ "$schema": "https://schemas.wp.org/trunk/block.json", "apiVersion": 3, "name": "activitypub/reply", - "version": "0.1.0", + "version": "8.3.0", "title": "Federated Reply", "category": "widgets", - "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.", + "icon": "admin-comments", + "description": "Reply to posts, notes, and other content on the Fediverse directly from the block editor.", + "keywords": [ + "fediverse", + "activitypub", + "reply", + "federation" + ], "supports": { "html": false, "inserter": true, "reusable": false, - "lock": false + "lock": false, + "innerBlocks": { + "allowedBlocks": [ + "core/embed" + ] + } }, "textdomain": "activitypub", "editorScript": "file:./index.js", @@ -24,6 +35,10 @@ "embedPost": { "type": "boolean", "default": null + }, + "isValidActivityPub": { + "type": "boolean", + "default": true } } } \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/reply/index-rtl.css b/wp-content/plugins/activitypub/build/reply/index-rtl.css index 0a5e5c51..b92ee49a 100644 --- a/wp-content/plugins/activitypub/build/reply/index-rtl.css +++ b/wp-content/plugins/activitypub/build/reply/index-rtl.css @@ -1 +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} +.activitypub-embed-container{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}.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;box-sizing:border-box;display:grid;gap:2px;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;margin:1em 0 0;min-height:64px;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview img{border:0;box-sizing:border-box;display:block;height:100%;-o-object-fit:cover;object-fit:cover;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview audio,.activitypub-embed-content .ap-preview video{display:block;grid-column:1/span 2;max-width:100%}.activitypub-embed-content .ap-preview audio{width:100%}.activitypub-embed-content .ap-preview.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}.activitypub-embed-content .ap-preview.layout-2{aspect-ratio:auto;grid-template-rows:1fr;height:auto}.activitypub-embed-content .ap-preview.layout-3>img:first-child{grid-row:span 2}.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 span.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} diff --git a/wp-content/plugins/activitypub/build/reply/index.asset.php b/wp-content/plugins/activitypub/build/reply/index.asset.php index f82365da..63d98455 100644 --- a/wp-content/plugins/activitypub/build/reply/index.asset.php +++ b/wp-content/plugins/activitypub/build/reply/index.asset.php @@ -1 +1 @@ - 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'); + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '9528bc906518de9ee951'); diff --git a/wp-content/plugins/activitypub/build/reply/index.css b/wp-content/plugins/activitypub/build/reply/index.css index 0a5e5c51..b92ee49a 100644 --- a/wp-content/plugins/activitypub/build/reply/index.css +++ b/wp-content/plugins/activitypub/build/reply/index.css @@ -1 +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} +.activitypub-embed-container{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}.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;box-sizing:border-box;display:grid;gap:2px;grid-template-columns:1fr 1fr;grid-template-rows:1fr 1fr;margin:1em 0 0;min-height:64px;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview img{border:0;box-sizing:border-box;display:block;height:100%;-o-object-fit:cover;object-fit:cover;overflow:hidden;position:relative;width:100%}.activitypub-embed-content .ap-preview audio,.activitypub-embed-content .ap-preview video{display:block;grid-column:1/span 2;max-width:100%}.activitypub-embed-content .ap-preview audio{width:100%}.activitypub-embed-content .ap-preview.layout-1{grid-template-columns:1fr;grid-template-rows:1fr}.activitypub-embed-content .ap-preview.layout-2{aspect-ratio:auto;grid-template-rows:1fr;height:auto}.activitypub-embed-content .ap-preview.layout-3>img:first-child{grid-row:span 2}.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 span.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} diff --git a/wp-content/plugins/activitypub/build/reply/index.js b/wp-content/plugins/activitypub/build/reply/index.js index 0e1c337b..956d28c9 100644 --- a/wp-content/plugins/activitypub/build/reply/index.js +++ b/wp-content/plugins/activitypub/build/reply/index.js @@ -1 +1 @@ -(()=>{"use strict";var e,t={238:(e,t,r)=>{const n=window.wp.blocks,o=window.wp.primitives;var a=r(848);const i=(0,a.jsx)(o.SVG,{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,a.jsx)(o.Path,{d:"M6.68822 10.625L6.24878 11.0649L5.5 11.8145L5.5 5.5L12.5 5.5V8L14 6.5V5C14 4.44772 13.5523 4 13 4H5C4.44772 4 4 4.44771 4 5V13.5247C4 13.8173 4.16123 14.086 4.41935 14.2237C4.72711 14.3878 5.10601 14.3313 5.35252 14.0845L7.31 12.125H8.375L9.875 10.625H7.31H6.68822ZM14.5605 10.4983L11.6701 13.75H16.9975C17.9963 13.75 18.7796 14.1104 19.3553 14.7048C19.9095 15.2771 20.2299 16.0224 20.4224 16.7443C20.7645 18.0276 20.7543 19.4618 20.7487 20.2544C20.7481 20.345 20.7475 20.4272 20.7475 20.4999L19.2475 20.5001C19.2475 20.4191 19.248 20.3319 19.2484 20.2394V20.2394C19.2526 19.4274 19.259 18.2035 18.973 17.1307C18.8156 16.5401 18.586 16.0666 18.2778 15.7483C17.9909 15.4521 17.5991 15.25 16.9975 15.25H11.8106L14.5303 17.9697L13.4696 19.0303L8.96956 14.5303L13.4394 9.50171L14.5605 10.4983Z"})});var l=r(609);const c=window.wp.blockEditor,s=window.wp.components,d=window.wp.i18n,u=window.wp.element,m=window.wp.compose,p=window.wp.apiFetch;var f=r.n(p);const h=window.wp.url,w=window.wp.data;function b({html:e}){const t=(0,u.useRef)(null),[r,n]=(0,u.useState)(300),o=(0,u.useRef)(300),a=(0,u.useCallback)((()=>{if(t.current)try{const e=t.current;let r=300;try{e.contentDocument&&e.contentDocument.body?r=e.contentDocument.body.scrollHeight:e.contentWindow&&e.contentWindow.document&&e.contentWindow.document.body&&(r=e.contentWindow.document.body.scrollHeight)}catch(e){console.log("Could not access iframe content document:",e)}r+=5,Math.abs(r-o.current)>5&&(o.current=r,n(r))}catch(e){console.error("Error adjusting iframe height:",e)}}),[]),i=(0,u.useCallback)((()=>{if(t.current)try{a()}catch(e){console.error("Error setting up iframe height adjustment:",e)}}),[a]);return(0,u.useEffect)((()=>{t.current&&t.current.addEventListener("load",i);const e=setInterval(a,1e3);return()=>{clearInterval(e),t.current&&t.current.removeEventListener("load",i)}}),[i,a]),(0,u.useEffect)((()=>{if(t.current){const e=setTimeout((()=>{a()}),100);return()=>clearTimeout(e)}}),[e,a]),{iframeRef:t,iframeHeight:r,adjustIframeHeight:a,handleIframeLoad:i}}const v={class:"className",frameborder:"frameBorder",allowfullscreen:"allowFullScreen",allowtransparency:"allowTransparency",marginheight:"marginHeight",marginwidth:"marginWidth"};function y({onClick:e}){return(0,l.createElement)("div",{className:"activitypub-embed-overlay",onClick:e,style:{position:"absolute",top:0,left:0,width:"100%",height:"100%",cursor:"pointer",zIndex:1}})}function g({html:e,onSelectBlock:t}){const r=(0,u.useRef)(),[n,o]=(0,u.useState)(282),[a,i]=(0,u.useState)(!1),c=(0,u.useCallback)((()=>{const t=(new window.DOMParser).parseFromString(e,"text/html").querySelector("iframe"),r={};return t?(Array.from(t.attributes).forEach((({name:e,value:t})=>{"style"!==e&&(r[v[e]||e]=t)})),r):r}),[e]),s=c();return(0,u.useEffect)((()=>{if(!r.current)return;const{ownerDocument:e}=r.current,{defaultView:t}=e;function n({data:{secret:e,message:t,value:r}={}}){"height"===t&&e===s["data-secret"]&&o(r)}return t.addEventListener("message",n),()=>{t.removeEventListener("message",n)}}),[s]),s.src?(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("iframe",{ref:r,title:s.title||(0,d.__)("Embedded WordPress content","activitypub"),...s,height:n,style:{width:"100%",maxWidth:"100%"}}),!a&&(0,l.createElement)(y,{onClick:t})):(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("div",{dangerouslySetInnerHTML:{__html:e}}),(0,l.createElement)(y,{onClick:t}))}function _({html:e,onClick:t,isSelected:r}){const{iframeRef:n,iframeHeight:o,adjustIframeHeight:a,handleIframeLoad:i}=b({html:e}),c=(0,u.useCallback)((()=>`\n\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\t\n\t\t\t\n\t\t\t\n\t\t\t\t${e}\n\t\t\t\n\t\t\t\n\t\t`),[e]);return(0,l.createElement)("div",{className:"wp-block-embed__wrapper",style:{position:"relative"}},(0,l.createElement)("iframe",{ref:n,srcDoc:c(),sandbox:"allow-scripts allow-same-origin allow-popups allow-forms",style:{width:"100%",height:`${o}px`,border:"none",overflow:"hidden"},onLoad:i}),r&&(0,l.createElement)("div",{onClick:t,style:{position:"absolute",top:0,left:0,width:"100%",height:"100%",cursor:"pointer",zIndex:1,display:r?"block":"none"}}))}const E={default:(0,d.__)("Enter the URL of a post from the Fediverse (Mastodon, Pixelfed, etc.) that you want to reply to.","activitypub"),checking:()=>(0,l.createElement)(l.Fragment,null,(0,l.createElement)(s.Spinner,null)," "+(0,d.__)("Checking if this URL supports ActivityPub replies...","activitypub")),valid:(0,d.__)("The author will be notified of your response.","activitypub"),error:(0,d.__)("This URL probably won't receive your reply. We'll still try.","activitypub")},k={valid:(0,d.__)("This post can be embedded with your reply.","activitypub"),invalid:(0,d.__)("This post cannot be embedded.","activitypub")};(0,n.registerBlockType)("activitypub/reply",{edit:function({attributes:e,setAttributes:t,clientId:r,isSelected:n}){const{url:o}=e,{namespace:a}=window._activityPubOptions||{},[i,p]=(0,u.useState)(E.default),[v,y]=(0,u.useState)(!1),[C,L]=(0,u.useState)(!1),[S,O]=(0,u.useState)(!1),[P,x]=(0,u.useState)(!0===e.embedPost||!o),[R,T]=(0,u.useState)(null),{iframeRef:H,iframeHeight:I,adjustIframeHeight:j,handleIframeLoad:D}=b({html:R}),{insertAfterBlock:B,removeBlock:N}=(0,w.useDispatch)("core/block-editor"),W=(0,c.useBlockProps)(),F=(0,u.useRef)(),M=((0,u.useRef)(),(0,u.useRef)(P)),U=()=>{setTimeout((()=>F.current?.focus()),50)};(0,u.useEffect)((()=>{M.current=P}),[P]);const A=(0,u.useCallback)((e=>{y(e),M.current&&e&&t({embedPost:!0})}),[t]),V=(e=!1)=>{O(e),y(!1),L(!1),T("")},$=(0,m.useDebounce)((async e=>{if(e)try{V(!0),p(E.checking());const t=await f()({path:(0,h.addQueryArgs)(`${a}/url/validate`,{url:e})});A(t.is_activitypub),L(t.is_real_oembed),T(t.html||""),p(E.valid)}catch(e){V(),p(E.error)}finally{O(!1)}else V()}),250);return(0,u.useEffect)((()=>{o&&$(o)}),[o]),(0,l.createElement)(l.Fragment,null,(0,l.createElement)(c.InspectorControls,null,(0,l.createElement)(s.PanelBody,{title:(0,d.__)("Settings","activitypub")},(0,l.createElement)(s.ToggleControl,{label:(0,d.__)("Embed Post","activitypub"),checked:e.embedPost,onChange:e=>{t({embedPost:e}),x(e)},disabled:!v,help:v?k.valid:k.invalid}))),(0,l.createElement)("div",{...W},n&&(0,l.createElement)(s.TextControl,{label:(0,d.__)("Your post is a reply to the following URL","activitypub"),value:o,onChange:e=>t({url:e}),help:i,onKeyDown:t=>{"Enter"===t.key&&B(r),!e.url&&["Backspace","Delete"].includes(t.key)&&N(r)},ref:F}),v&&e.embedPost&&R&&(0,l.createElement)("div",{className:"activitypub-embed-container"},C&&(Y=R)&&(Y.includes("wp-embedded-content")||Y.includes("wp-embed/")||Y.includes('class="wp-embed"'))?(0,l.createElement)(g,{html:R,onSelectBlock:U}):(0,l.createElement)(_,{html:R,onClick:U,isSelected:n})),o&&(!e.embedPost||!R)&&(0,l.createElement)("div",{className:"activitypub-reply-block-editor__preview",contentEditable:!1,onClick:U,style:{cursor:"pointer"}},(0,l.createElement)("a",{href:o,className:"u-in-reply-to",target:"_blank",rel:"noreferrer"},"↬"+o.replace(/^https?:\/\//,"")))));var Y},save:()=>null,icon:i})},20:(e,t,r)=>{var n=r(609),o=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),i=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,l={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var n,c={},s=null,d=null;for(n in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(d=t.ref),t)a.call(t,n)&&!l.hasOwnProperty(n)&&(c[n]=t[n]);if(e&&e.defaultProps)for(n in t=e.defaultProps)void 0===c[n]&&(c[n]=t[n]);return{$$typeof:o,type:e,key:s,ref:d,props:c,_owner:i.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},r={};function n(e){var o=r[e];if(void 0!==o)return o.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,n),a.exports}n.m=t,e=[],n.O=(t,r,o,a)=>{if(!r){var i=1/0;for(d=0;d=a)&&Object.keys(n.O).every((e=>n.O[e](r[c])))?r.splice(c--,1):(l=!1,a0&&e[d-1][2]>a;d--)e[d]=e[d-1];e[d]=[r,o,a]},n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var r in t)n.o(t,r)&&!n.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={780:0,356:0};n.O.j=t=>0===e[t];var t=(t,r)=>{var o,a,i=r[0],l=r[1],c=r[2],s=0;if(i.some((t=>0!==e[t]))){for(o in l)n.o(l,o)&&(n.m[o]=l[o]);if(c)var d=c(n)}for(t&&t(r);sn(238)));o=n.O(o)})(); \ No newline at end of file +(()=>{"use strict";var e={n:t=>{var o=t&&t.__esModule?()=>t.default:()=>t;return e.d(o,{a:o}),o},d:(t,o)=>{for(var r in o)e.o(o,r)&&!e.o(t,r)&&Object.defineProperty(t,r,{enumerable:!0,get:o[r]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.wp.blocks,o=window.wp.primitives,r=window.ReactJSXRuntime;var i=(0,r.jsx)(o.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,r.jsx)(o.Path,{d:"M6.68822 10.625L6.24878 11.0649L5.5 11.8145L5.5 5.5L12.5 5.5V8L14 6.5V5C14 4.44772 13.5523 4 13 4H5C4.44772 4 4 4.44771 4 5V13.5247C4 13.8173 4.16123 14.086 4.41935 14.2237C4.72711 14.3878 5.10601 14.3313 5.35252 14.0845L7.31 12.125H8.375L9.875 10.625H7.31H6.68822ZM14.5605 10.4983L11.6701 13.75H16.9975C17.9963 13.75 18.7796 14.1104 19.3553 14.7048C19.9095 15.2771 20.2299 16.0224 20.4224 16.7443C20.7645 18.0276 20.7543 19.4618 20.7487 20.2544C20.7481 20.345 20.7475 20.4272 20.7475 20.4999L19.2475 20.5001C19.2475 20.4191 19.248 20.3319 19.2484 20.2394V20.2394C19.2526 19.4274 19.259 18.2035 18.973 17.1307C18.8156 16.5401 18.586 16.0666 18.2778 15.7483C17.9909 15.4521 17.5991 15.25 16.9975 15.25H11.8106L14.5303 17.9697L13.4696 19.0303L8.96956 14.5303L13.4394 9.50171L14.5605 10.4983Z"})});const c=window.wp.blockEditor,l=window.wp.components,n=window.wp.i18n,s=window.wp.element,a=window.wp.compose,d=window.wp.data,u=window.wp.apiFetch;var p=e.n(u);const b=window.wp.url,w={default:(0,n.__)("Enter the URL of a post from the Fediverse (Mastodon, Pixelfed, etc.) that you want to reply to.","activitypub"),checking:()=>(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(l.Spinner,{})," "+(0,n.__)("Checking URL…","activitypub")]}),valid:(0,n.__)("The author will be notified of your response.","activitypub"),error:(0,n.__)("This site doesn’t have ActivityPub enabled and won’t receive your reply.","activitypub")};(0,t.registerBlockType)("activitypub/reply",{edit:function({attributes:e,setAttributes:o,clientId:i,isSelected:u}){const{url:y="",embedPost:v=!1}=e,[m,h]=(0,s.useState)(w.default),[f,k]=(0,s.useState)(!1),[_,C]=(0,s.useState)(!1),x=(0,s.useRef)(),{insertAfterBlock:L,removeBlock:g,replaceInnerBlocks:P}=(0,d.useDispatch)("core/block-editor"),j=v&&!_&&f,B=(0,c.useInnerBlocksProps)({className:"activitypub-embed-container"},{allowedBlocks:["core/embed"],template:y&&j?[["core/embed",{url:y}]]:[],templateLock:"all"});(0,s.useEffect)(()=>{P(i,y&&j?[(0,t.createBlock)("core/embed",{url:y})]:[])},[y,j,i,P]),(0,s.useEffect)(()=>{h(y?_?w.checking():f?w.valid:w.error:w.default)},[y,_,f]);const S=()=>{setTimeout(()=>x.current?.focus(),50)},V=(0,s.useCallback)(async e=>{if(e)try{C(!0),new URL(e);try{const t=await p()({path:(0,b.addQueryArgs)("/oembed/1.0/proxy",{url:e,activitypub:!0})});t&&t.provider_name?(o({embedPost:!0,isValidActivityPub:!0}),k(!0)):(o({isValidActivityPub:!1}),k(!1))}catch(e){console.log("Could not fetch embed:",e),o({isValidActivityPub:!1}),k(!1)}}catch(e){o({isValidActivityPub:!1}),k(!1)}finally{C(!1)}else k(!1)},[o,k,C]),A=(0,a.useDebounce)(V,250);return(0,s.useEffect)(()=>{y&&A(y)},[y,A]),(0,r.jsxs)(r.Fragment,{children:[(0,r.jsx)(c.InspectorControls,{children:(0,r.jsx)(l.PanelBody,{title:(0,n.__)("Settings","activitypub"),children:(0,r.jsx)(l.ToggleControl,{label:(0,n.__)("Embed Post","activitypub"),checked:!!v,onChange:e=>o({embedPost:e}),disabled:!f,help:(0,n.__)("Show embedded content from the URL.","activitypub")})})}),(0,r.jsxs)("div",{onClick:S,...(0,c.useBlockProps)(),children:[u&&(0,r.jsx)(l.TextControl,{label:(0,n.__)("Your post is a reply to the following URL","activitypub"),value:y,onChange:e=>o({url:e}),help:m,onKeyDown:e=>{"Enter"===e.key&&L(i),!y&&["Backspace","Delete"].includes(e.key)&&g(i)},ref:x,__next40pxDefaultSize:!0}),j&&(0,r.jsx)("div",{...B}),y&&!j&&!u&&(0,r.jsx)("div",{className:"activitypub-reply-block-editor__preview",contentEditable:!1,onClick:S,style:{cursor:"pointer"},children:(0,r.jsx)("a",{href:y,className:"u-in-reply-to",target:"_blank",rel:"noreferrer",children:"↬"+y.replace(/^https?:\/\//,"")})})]})]})},save:()=>null,icon:i,transforms:{from:[{type:"block",blocks:["core/embed"],transform:e=>(0,t.createBlock)("activitypub/reply",{url:e.url||"",embedPost:!0})}],to:[{type:"block",blocks:["core/embed"],transform:e=>(0,t.createBlock)("core/embed",{url:e.url||""})}]}})})(); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/reply/style-index-rtl.css b/wp-content/plugins/activitypub/build/reply/style-index-rtl.css deleted file mode 100644 index ec651ab6..00000000 --- a/wp-content/plugins/activitypub/build/reply/style-index-rtl.css +++ /dev/null @@ -1 +0,0 @@ -.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} diff --git a/wp-content/plugins/activitypub/build/reply/style-index.css b/wp-content/plugins/activitypub/build/reply/style-index.css deleted file mode 100644 index ec651ab6..00000000 --- a/wp-content/plugins/activitypub/build/reply/style-index.css +++ /dev/null @@ -1 +0,0 @@ -.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} diff --git a/wp-content/plugins/activitypub/build/stats/block.json b/wp-content/plugins/activitypub/build/stats/block.json new file mode 100644 index 00000000..1ef1a2fe --- /dev/null +++ b/wp-content/plugins/activitypub/build/stats/block.json @@ -0,0 +1,54 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "name": "activitypub/stats", + "apiVersion": 3, + "version": "unreleased", + "title": "ActivityPub Stats", + "category": "widgets", + "description": "Display your annual Fediverse stats as a shareable card.", + "textdomain": "activitypub", + "icon": "chart-bar", + "keywords": [ + "fediverse", + "activitypub", + "stats", + "statistics", + "annual", + "year" + ], + "supports": { + "html": false, + "align": [ + "wide", + "full" + ], + "color": false, + "typography": false, + "spacing": { + "margin": true, + "padding": true, + "__experimentalDefaultControls": { + "padding": true + } + }, + "__experimentalBorder": { + "color": true, + "radius": true, + "style": true, + "width": true, + "__experimentalSkipSerialization": true + }, + "shadow": true + }, + "attributes": { + "selectedUser": { + "type": "string" + }, + "year": { + "type": "number" + } + }, + "editorScript": "file:./index.js", + "style": "file:./style-index.css", + "render": "file:./render.php" +} \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/stats/index.asset.php b/wp-content/plugins/activitypub/build/stats/index.asset.php new file mode 100644 index 00000000..c68afb28 --- /dev/null +++ b/wp-content/plugins/activitypub/build/stats/index.asset.php @@ -0,0 +1 @@ + array('react-jsx-runtime', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-server-side-render'), 'version' => '7042ce0f90bb9fbbfc2a'); diff --git a/wp-content/plugins/activitypub/build/stats/index.js b/wp-content/plugins/activitypub/build/stats/index.js new file mode 100644 index 00000000..1db61d5a --- /dev/null +++ b/wp-content/plugins/activitypub/build/stats/index.js @@ -0,0 +1 @@ +(()=>{"use strict";var e,t={1868(e,t,i){const s=window.wp.blocks,r=window.wp.serverSideRender;var n=i.n(r);const a=window.wp.components,o=window.wp.blockEditor,l=window.wp.i18n,c=window.wp.element,u=window.wp.apiFetch;var p=i.n(u);const d=window.wp.data;const v=window.ReactJSXRuntime,b=(new Date).getFullYear();function h(){const e=[];for(let t=b;t>=b-5;t--)e.push({label:String(t),value:String(t)});return e}const g=JSON.parse('{"UU":"activitypub/stats"}');(0,s.registerBlockType)(g.UU,{edit:function({attributes:e,setAttributes:t}){const{selectedUser:i,year:s}=e,r=(0,o.useBlockProps)(),u=function({withInherit:e=!1}){const{enabled:t,namespace:i}=window._activityPubOptions||{},[s,r]=(0,c.useState)(!1),{fetchedUsers:n,isLoadingUsers:a}=(0,d.useSelect)(e=>{const{getUsers:i,getIsResolving:s}=e("core");return{fetchedUsers:t?.users?i({capabilities:"activitypub"}):null,isLoadingUsers:!!t?.users&&s("getUsers",[{capabilities:"activitypub"}])}},[t?.users]),o=(0,d.useSelect)(e=>n||a?null:e("core").getCurrentUser(),[n,a]);(0,c.useEffect)(()=>{!n&&!a&&o&&i&&p()({path:`/${i}/actors/${o.id}`,method:"HEAD",headers:{Accept:"application/activity+json"},parse:!1}).then(()=>r(!0)).catch(()=>r(!1))},[n,a,o,i]);const u=(0,c.useMemo)(()=>n||(o&&s?[{id:o.id,name:o.name}]:[]),[n,o,s]);return(0,c.useMemo)(()=>{if(!u.length)return[];const i=[];return t?.blog&&n&&i.push({label:(0,l.__)("Blog","activitypub"),value:"blog"}),e&&t?.users&&n&&i.push({label:(0,l.__)("Dynamic User","activitypub"),value:"inherit"}),u.reduce((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e),i)},[u,t?.blog,t?.users,n,e])}({}),[g,w]=(0,c.useState)(!1);(0,c.useEffect)(()=>{!i&&u.length&&t({selectedUser:u[0].value})},[u]);const y=s||b-1,[f,_]=(0,c.useState)(""),m=(0,c.useCallback)(()=>{if(!i)return;const e=function(e,t){const i=window._activityPubOptions?.statsImageUrlEndpoint||"";if(!i)return"";const s=e&&"blog"!==e?e:0;return i.replace("{user_id}",s).replace("{year}",t)}(i,y);e&&p()({url:e}).then(e=>_(e.url||"")).catch(()=>_(""))},[i,y]);return(0,c.useEffect)(()=>{m()},[m]),(0,v.jsxs)("div",{...r,children:[(0,v.jsxs)(o.InspectorControls,{children:[(0,v.jsxs)(a.PanelBody,{title:(0,l.__)("Settings","activitypub"),children:[u.length>1&&(0,v.jsx)(a.SelectControl,{label:(0,l.__)("Select User","activitypub"),value:i,options:u,onChange:e=>t({selectedUser:e})}),(0,v.jsx)(a.SelectControl,{label:(0,l.__)("Year","activitypub"),value:String(y),options:h(),onChange:e=>t({year:parseInt(e,10)})})]}),f&&(0,v.jsxs)(a.PanelBody,{title:(0,l.__)("Share Image","activitypub"),initialOpen:!1,children:[(0,v.jsx)("p",{className:"description",children:(0,l.__)("Use this URL to share your stats as an image on social media.","activitypub")}),(0,v.jsx)(a.TextControl,{label:(0,l.__)("Share image URL","activitypub"),hideLabelFromVision:!0,__nextHasNoMarginBottom:!0,value:f,readOnly:!0,onClick:e=>e.target.select()}),(0,v.jsxs)("div",{style:{display:"flex",gap:"8px",alignItems:"center"},children:[(0,v.jsx)(a.Button,{variant:"secondary",onClick:()=>{navigator.clipboard.writeText(f).then(()=>{w(!0),setTimeout(()=>w(!1),2e3)})},children:g?(0,l.__)("Copied!","activitypub"):(0,l.__)("Copy URL","activitypub")}),(0,v.jsx)(a.ExternalLink,{href:f,children:(0,l.__)("Preview","activitypub")})]})]})]}),(0,v.jsx)(a.Disabled,{children:(0,v.jsx)(n(),{block:"activitypub/stats",attributes:{...e,year:y}})})]})}})}},i={};function s(e){var r=i[e];if(void 0!==r)return r.exports;var n=i[e]={exports:{}};return t[e](n,n.exports,s),n.exports}s.m=t,e=[],s.O=(t,i,r,n)=>{if(!i){var a=1/0;for(u=0;u=n)&&Object.keys(s.O).every(e=>s.O[e](i[l]))?i.splice(l--,1):(o=!1,n0&&e[u-1][2]>n;u--)e[u]=e[u-1];e[u]=[i,r,n]},s.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return s.d(t,{a:t}),t},s.d=(e,t)=>{for(var i in t)s.o(t,i)&&!s.o(e,i)&&Object.defineProperty(e,i,{enumerable:!0,get:t[i]})},s.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={85:0,721:0};s.O.j=t=>0===e[t];var t=(t,i)=>{var r,n,[a,o,l]=i,c=0;if(a.some(t=>0!==e[t])){for(r in o)s.o(o,r)&&(s.m[r]=o[r]);if(l)var u=l(s)}for(t&&t(i);cs(1868));r=s.O(r)})(); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/stats/render.php b/wp-content/plugins/activitypub/build/stats/render.php new file mode 100644 index 00000000..93f2625d --- /dev/null +++ b/wp-content/plugins/activitypub/build/stats/render.php @@ -0,0 +1,239 @@ +
%s
%s
', + \esc_html__( 'Fediverse Stats', 'activitypub' ), + \sprintf( + /* translators: %d: The year */ + \esc_html__( 'No stats available for %d. Stats are collected monthly and compiled at the end of each year.', 'activitypub' ), + (int) $stats_year + ) + ); + } + return; +} + +// Get comment types for dynamic display. +$comment_types = Statistics::get_comment_types_for_stats(); + +// Calculate total engagement. +$total_engagement = 0; +foreach ( array_keys( $comment_types ) as $ct_slug ) { + $total_engagement += $summary[ $ct_slug . '_count' ] ?? 0; +} + +// Most active month name. +$most_active_month_name = ''; +if ( ! empty( $summary['most_active_month'] ) ) { + $most_active_month_name = gmdate( 'F', gmmktime( 0, 0, 0, $summary['most_active_month'], 1, $stats_year ) ); +} + +// Follower growth. +$followers_start = $summary['followers_start'] ?? 0; +$followers_end = $summary['followers_end'] ?? 0; +$followers_net_change = $summary['followers_net_change'] ?? ( $followers_end - $followers_start ); +$change_sign = $followers_net_change >= 0 ? '+' : ''; + +// Get actor webfinger for the card header. +$actor = Actors::get_by_id( $user_id ); + +if ( \is_wp_error( $actor ) ) { + // Fall back to direct model instantiation for blog/application actors. + if ( Actors::BLOG_USER_ID === $user_id ) { + $actor = new \Activitypub\Model\Blog(); + } elseif ( Actors::APPLICATION_USER_ID === $user_id ) { + $actor = new \Activitypub\Model\Application(); + } +} + +$actor_webfinger = ! \is_wp_error( $actor ) ? $actor->get_webfinger() : ''; + +// Site name for branding. +$site_name = \get_bloginfo( 'name' ); + +$block_id = 'activitypub-stats-' . \wp_unique_id(); +$title_text = \sprintf( + /* translators: %d: The year */ + \__( 'Fediverse Stats %d', 'activitypub' ), + (int) $stats_year +); + +/* + * Build border styles using WP_Style_Engine for sanitization. + * Border serialization is skipped in block.json to avoid double + * rendering in the editor, so we apply it here manually. + */ +$border_result = \wp_style_engine_get_styles( array( 'border' => $attributes['style']['border'] ?? array() ) ); +$extra_styles = $border_result['css'] ?? ''; + +// Handle preset border color slug (not part of style.border). +if ( ! empty( $attributes['borderColor'] ) ) { + $preset_color = 'var(--wp--preset--color--' . \sanitize_key( $attributes['borderColor'] ) . ')'; + $extra_styles = 'border-color:' . $preset_color . ';' . $extra_styles; +} + +// Resolve the border color for inner elements via CSS variable. +$border_color = ''; +if ( ! empty( $attributes['style']['border']['color'] ) ) { + $border_color = $attributes['style']['border']['color']; +} elseif ( ! empty( $attributes['borderColor'] ) ) { + $border_color = 'var(--wp--preset--color--' . \sanitize_key( $attributes['borderColor'] ) . ')'; +} + +if ( $border_color ) { + $extra_styles .= '--activitypub-stats--border-color:' . \esc_attr( $border_color ) . ';'; +} + +$wrapper_attrs = array( + 'id' => $block_id, + 'class' => 'activitypub-stats', +); + +$wrapper_html = \get_block_wrapper_attributes( $wrapper_attrs ); + +// Merge border styles into the existing style attribute. +if ( $extra_styles ) { + if ( \str_contains( $wrapper_html, 'style="' ) ) { + $wrapper_html = \str_replace( 'style="', 'style="' . \esc_attr( $extra_styles ), $wrapper_html ); + } else { + $wrapper_html .= ' style="' . \esc_attr( $extra_styles ) . '"'; + } +} +?> +
+ data-year="" +> +
+

+ +

+ +
+ +
+
+ + +
+
+ + +
+
+ + + +
+
+ + + + + + + +
+ + +
+ + +
+ + + +
+ + + + + + + +
+ +
+ + +
+

+
    + +
  1. + + + + + + +
  2. + +
+
+ + + +
diff --git a/wp-content/plugins/activitypub/build/stats/style-index-rtl.css b/wp-content/plugins/activitypub/build/stats/style-index-rtl.css new file mode 100644 index 00000000..3b244618 --- /dev/null +++ b/wp-content/plugins/activitypub/build/stats/style-index-rtl.css @@ -0,0 +1 @@ +.wp-block-activitypub-stats{background-color:var(--wp--preset--color--base,var(--wp--preset--color--white,#fff));color:var(--wp--preset--color--contrast,var(--wp--preset--color--black,inherit));max-width:var(--wp--style--global--content-size,600px);--activitypub-stats--border-color:color-mix(in srgb,currentcolor 20%,transparent)}.wp-block-activitypub-stats.alignwide{max-width:var(--wp--style--global--wide-size)}.wp-block-activitypub-stats.alignfull{max-width:none}.activitypub-stats__header{margin-bottom:1.5rem;text-align:center}.activitypub-stats__title{color:inherit;font-size:1.75em;font-weight:800;letter-spacing:-.02em;margin:0 0 .25rem}.activitypub-stats__subtitle{color:color-mix(in srgb,currentcolor 60%,transparent);font-size:1em;margin:0}.activitypub-stats__stats{display:flex;gap:1rem;margin-bottom:1.25rem}.activitypub-stats__stat{flex:1;padding:.75rem .5rem;text-align:center}.activitypub-stats__stat--highlight{border:1px solid var(--activitypub-stats--border-color);border-radius:8px}.activitypub-stats__stat-value{color:inherit;display:block;font-size:2em;font-weight:800;line-height:1.2}.activitypub-stats__stat--highlight .activitypub-stats__stat-value{font-size:2.5em}.activitypub-stats__stat-label{color:color-mix(in srgb,currentcolor 50%,transparent);display:block;font-size:.8em;letter-spacing:.05em;margin-top:.25rem;text-transform:uppercase}.activitypub-stats__engagement{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1.5rem}.activitypub-stats__engagement .activitypub-stats__stat{border:1px solid var(--activitypub-stats--border-color);border-radius:8px;flex:1 1 calc(33.333% - 0.5rem);min-width:5rem;padding:.625rem .5rem}.activitypub-stats__details{display:flex;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem}.activitypub-stats__detail{border:1px solid var(--activitypub-stats--border-color);border-radius:8px;flex:1 1 calc(50% - 0.5rem);min-width:7.5rem;padding:.875rem}.activitypub-stats__detail-label{color:color-mix(in srgb,currentcolor 50%,transparent);display:block;font-size:.75em;letter-spacing:.05em;margin-bottom:.25rem;text-transform:uppercase}.activitypub-stats__detail-value{color:inherit;display:block;font-size:1.25em;font-weight:700}.activitypub-stats__detail-value a{color:inherit;text-decoration:underline;text-underline-offset:.15em}.activitypub-stats__detail-value a:hover{color:color-mix(in srgb,currentcolor 70%,transparent)}.activitypub-stats__detail-value--negative,.activitypub-stats__detail-value--positive{color:inherit}.activitypub-stats__detail-extra{color:color-mix(in srgb,currentcolor 45%,transparent);display:block;font-size:.8em;margin-top:.125rem}.activitypub-stats__top-posts{margin-bottom:1.25rem}.activitypub-stats__section-title{color:color-mix(in srgb,currentcolor 50%,transparent);font-size:.85em;letter-spacing:.05em;margin:0 0 .75rem;text-transform:uppercase}.activitypub-stats__top-posts ol{list-style:decimal;margin:0;padding-right:1.5em}.activitypub-stats__top-posts li{padding:.5rem 0}.activitypub-stats__top-posts li:last-child{padding-bottom:0}.activitypub-stats__top-posts li a{color:inherit;text-decoration:none}.activitypub-stats__top-posts li a:hover{text-decoration:underline}.activitypub-stats__post-engagement{color:color-mix(in srgb,currentcolor 45%,transparent);font-size:.8em;margin-right:.25rem}.activitypub-stats__footer{margin-top:1.25rem;text-align:center}.activitypub-stats__branding{color:color-mix(in srgb,currentcolor 45%,transparent);font-size:.75em} diff --git a/wp-content/plugins/activitypub/build/stats/style-index.css b/wp-content/plugins/activitypub/build/stats/style-index.css new file mode 100644 index 00000000..f9bbaec2 --- /dev/null +++ b/wp-content/plugins/activitypub/build/stats/style-index.css @@ -0,0 +1 @@ +.wp-block-activitypub-stats{background-color:var(--wp--preset--color--base,var(--wp--preset--color--white,#fff));color:var(--wp--preset--color--contrast,var(--wp--preset--color--black,inherit));max-width:var(--wp--style--global--content-size,600px);--activitypub-stats--border-color:color-mix(in srgb,currentcolor 20%,transparent)}.wp-block-activitypub-stats.alignwide{max-width:var(--wp--style--global--wide-size)}.wp-block-activitypub-stats.alignfull{max-width:none}.activitypub-stats__header{margin-bottom:1.5rem;text-align:center}.activitypub-stats__title{color:inherit;font-size:1.75em;font-weight:800;letter-spacing:-.02em;margin:0 0 .25rem}.activitypub-stats__subtitle{color:color-mix(in srgb,currentcolor 60%,transparent);font-size:1em;margin:0}.activitypub-stats__stats{display:flex;gap:1rem;margin-bottom:1.25rem}.activitypub-stats__stat{flex:1;padding:.75rem .5rem;text-align:center}.activitypub-stats__stat--highlight{border:1px solid var(--activitypub-stats--border-color);border-radius:8px}.activitypub-stats__stat-value{color:inherit;display:block;font-size:2em;font-weight:800;line-height:1.2}.activitypub-stats__stat--highlight .activitypub-stats__stat-value{font-size:2.5em}.activitypub-stats__stat-label{color:color-mix(in srgb,currentcolor 50%,transparent);display:block;font-size:.8em;letter-spacing:.05em;margin-top:.25rem;text-transform:uppercase}.activitypub-stats__engagement{display:flex;flex-wrap:wrap;gap:.5rem;margin-bottom:1.5rem}.activitypub-stats__engagement .activitypub-stats__stat{border:1px solid var(--activitypub-stats--border-color);border-radius:8px;flex:1 1 calc(33.333% - 0.5rem);min-width:5rem;padding:.625rem .5rem}.activitypub-stats__details{display:flex;flex-wrap:wrap;gap:1rem;margin-bottom:1.5rem}.activitypub-stats__detail{border:1px solid var(--activitypub-stats--border-color);border-radius:8px;flex:1 1 calc(50% - 0.5rem);min-width:7.5rem;padding:.875rem}.activitypub-stats__detail-label{color:color-mix(in srgb,currentcolor 50%,transparent);display:block;font-size:.75em;letter-spacing:.05em;margin-bottom:.25rem;text-transform:uppercase}.activitypub-stats__detail-value{color:inherit;display:block;font-size:1.25em;font-weight:700}.activitypub-stats__detail-value a{color:inherit;text-decoration:underline;text-underline-offset:.15em}.activitypub-stats__detail-value a:hover{color:color-mix(in srgb,currentcolor 70%,transparent)}.activitypub-stats__detail-value--negative,.activitypub-stats__detail-value--positive{color:inherit}.activitypub-stats__detail-extra{color:color-mix(in srgb,currentcolor 45%,transparent);display:block;font-size:.8em;margin-top:.125rem}.activitypub-stats__top-posts{margin-bottom:1.25rem}.activitypub-stats__section-title{color:color-mix(in srgb,currentcolor 50%,transparent);font-size:.85em;letter-spacing:.05em;margin:0 0 .75rem;text-transform:uppercase}.activitypub-stats__top-posts ol{list-style:decimal;margin:0;padding-left:1.5em}.activitypub-stats__top-posts li{padding:.5rem 0}.activitypub-stats__top-posts li:last-child{padding-bottom:0}.activitypub-stats__top-posts li a{color:inherit;text-decoration:none}.activitypub-stats__top-posts li a:hover{text-decoration:underline}.activitypub-stats__post-engagement{color:color-mix(in srgb,currentcolor 45%,transparent);font-size:.8em;margin-left:.25rem}.activitypub-stats__footer{margin-top:1.25rem;text-align:center}.activitypub-stats__branding{color:color-mix(in srgb,currentcolor 45%,transparent);font-size:.75em} diff --git a/wp-content/plugins/activitypub/includes/activity/class-activity.php b/wp-content/plugins/activitypub/includes/activity/class-activity.php index b4623ac2..28a5e30a 100644 --- a/wp-content/plugins/activitypub/includes/activity/class-activity.php +++ b/wp-content/plugins/activitypub/includes/activity/class-activity.php @@ -18,10 +18,29 @@ use Activitypub\Activity\Extended_Object\Place; * * @see https://www.w3.org/TR/activitystreams-core/#activities * @see https://www.w3.org/TR/activitystreams-core/#intransitiveactivities + * + * @method string|array get_actor() Gets one or more entities that performed or are expected to perform the activity. + * @method string|array|null get_instrument() Gets one or more objects used in the completion of an Activity. + * @method Base_Object|string|array|null get_object() Gets the direct object of the activity. + * @method string|string[]|null get_origin() Gets the origin property of the activity. + * @method array|null get_replies() Gets the collection of responses to this activity. + * @method string|null get_result() Gets the result property of the activity. + * @method string|string[]|null get_target() Gets the target property of the activity. + * + * @method Activity set_actor( string|array $actor ) Sets one or more entities that performed the activity. + * @method Activity set_instrument( string|array $instrument ) Sets one or more objects used in the completion of an Activity. + * @method Activity set_origin( string|array|null $origin ) Sets the origin property of the activity. + * @method Activity set_replies( array $replies ) Sets the collection of responses to this activity. + * @method Activity set_result( string|null $result ) Sets the result property of the activity. + * @method Activity set_target( string|array|null $target ) Sets the target property of the activity. */ class Activity extends Base_Object { const JSON_LD_CONTEXT = array( 'https://www.w3.org/ns/activitystreams', + array( + 'toot' => 'http://joinmastodon.org/ns#', + 'QuoteRequest' => 'toot:QuoteRequest', + ), ); /** @@ -50,6 +69,7 @@ class Activity extends Base_Object { 'Listen', 'Move', 'Offer', + 'QuoteRequest', // @see https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md 'Read', 'Reject', 'Remove', @@ -75,7 +95,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object-term * - * @var string|Base_Object|null + * @var string|Base_Object|array|null */ protected $object; @@ -102,7 +122,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-target * - * @var string|array + * @var string|array|null */ protected $target; @@ -114,7 +134,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-result * - * @var string|Base_Object + * @var string|Base_Object|null */ protected $result; @@ -126,7 +146,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies * - * @var array + * @var array|null */ protected $replies; @@ -140,7 +160,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-origin * - * @var string|array + * @var string|array|null */ protected $origin; @@ -150,7 +170,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-instrument * - * @var string|array + * @var string|array|null */ protected $instrument; @@ -162,33 +182,17 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitypub/#object-without-create * - * @param array|string|Base_Object|Activity|Actor|null $data Activity object. + * @param array|string|Base_Object|Activity|Actor|null $data Activity object. */ public function set_object( $data ) { $object = $data; // Convert array to appropriate object type. if ( is_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; - } + if ( array_is_list( $data ) ) { + $object = array_map( array( $this, 'maybe_convert_to_object' ), $data ); } else { - $object = Generic_Object::init_from_array( $data ); + $object = $this->maybe_convert_to_object( $data ); } } @@ -202,14 +206,14 @@ class Activity extends Base_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. + // Check if `$object` 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. + // Check if `$object` is an object and copy some properties otherwise do nothing. if ( ! is_object( $object ) ) { return; } @@ -237,6 +241,10 @@ class Activity extends Base_Object { $this->set( 'in_reply_to', $object->get_in_reply_to() ); } + if ( $object->get_interaction_policy() && ! $this->get_interaction_policy() ) { + $this->set( 'interaction_policy', $object->get_interaction_policy() ); + } + if ( $object->get_id() && ! $this->get_id() ) { $id = strtok( $object->get_id(), '#' ); if ( $object->get_updated() ) { @@ -256,7 +264,7 @@ class Activity extends Base_Object { * @return array $context A compacted JSON-LD context. */ public function get_json_ld_context() { - if ( $this->object instanceof Base_Object ) { + if ( \is_object( $this->object ) ) { $class = get_class( $this->object ); if ( $class && $class::JSON_LD_CONTEXT ) { // Without php 5.6 support this could be just: 'return $this->object::JSON_LD_CONTEXT;'. @@ -266,4 +274,41 @@ class Activity extends Base_Object { return static::JSON_LD_CONTEXT; } + + /** + * Convert data to the appropriate object type if it has an ActivityPub type. + * + * @param array|string|Base_Object|Activity|Actor|null $data The data to convert. + * + * @return Activity|Actor|Base_Object|Generic_Object|string|\WP_Error|null The converted object or original data. + */ + private function maybe_convert_to_object( $data ) { + if ( ! is_array( $data ) ) { + return $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 ); + } + + return $object; + } } diff --git a/wp-content/plugins/activitypub/includes/activity/class-actor.php b/wp-content/plugins/activitypub/includes/activity/class-actor.php index bbf2128b..064ab418 100644 --- a/wp-content/plugins/activitypub/includes/activity/class-actor.php +++ b/wp-content/plugins/activitypub/includes/activity/class-actor.php @@ -16,6 +16,52 @@ namespace Activitypub\Activity; * Represents an individual actor. * * @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types + * + * @method string[]|null get_also_known_as() Gets the also known as property of the actor. + * @method array|null get_attribution_domains() Gets domains allowed to use fediverse:creator for this actor. + * @method bool|null get_discoverable() Gets whether the actor is discoverable. + * @method string[]|null get_endpoints() Gets the endpoint property of the actor. + * @method string|null get_featured() Gets the featured posts collection of the actor. + * @method string|null get_featured_tags() Gets the featured tags collection of the actor. + * @method string|null get_followers() Gets the followers collection of the actor. + * @method string|null get_following() Gets the following collection of the actor. + * @method array|null get_implements() Gets the list of implemented specifications. + * @method string|null get_inbox() Gets the inbox property of the actor. + * @method bool|null get_indexable() Gets whether the actor is indexable. + * @method bool|null get_invisible() Gets whether the actor is invisible. + * @method string|null get_liked() Gets the liked collection of the actor. + * @method bool|null get_manually_approves_followers() Gets whether the actor manually approves followers. + * @method string|null get_moderators() Gets the moderators endpoint URL. + * @method string|null get_moved_to() Gets the target of the actor move. + * @method string|null get_outbox() Gets the outbox property of the actor. + * @method bool|null get_posting_restricted_to_mods() Gets whether posting is restricted to moderators. + * @method string|null get_preferred_username() Gets the preferred username of the actor. + * @method string|array|null get_public_key() Gets the public key of the actor. + * @method array get_streams() Gets the list of supplementary collections. + * @method string|null get_webfinger() Gets the WebFinger resource. + * + * @method Actor set_also_known_as( array $also_known_as ) Sets the also known as property of the actor. + * @method Actor set_attribution_domains( array $attribution_domains ) Sets domains allowed to use fediverse:creator for this actor. + * @method Actor set_discoverable( bool $discoverable ) Sets whether the actor is discoverable. + * @method Actor set_endpoints( string|array $endpoints ) Sets the endpoint property of the actor. + * @method Actor set_featured( string $featured ) Sets the featured posts collection of the actor. + * @method Actor set_featured_tags( string $featured_tags ) Sets the featured tags collection of the actor. + * @method Actor set_followers( string $followers ) Sets the followers collection of the actor. + * @method Actor set_following( string $following ) Sets the following collection of the actor. + * @method Actor set_implements( array $implements ) Sets the list of implemented specifications. + * @method Actor set_inbox( string $inbox ) Sets the inbox property of the actor. + * @method Actor set_indexable( bool $indexable ) Sets whether the actor is indexable. + * @method Actor set_invisible( bool $invisible ) Sets whether the actor is invisible. + * @method Actor set_liked( string $liked ) Sets the liked collection of the actor. + * @method Actor set_manually_approves_followers( bool $manually_approves_followers ) Sets whether the actor manually approves followers. + * @method Actor set_moderators( string $moderators ) Sets the moderators endpoint URL. + * @method Actor set_moved_to( string $moved_to ) Sets the target of the actor move. + * @method Actor set_outbox( string $outbox ) Sets the outbox property of the actor. + * @method Actor set_posting_restricted_to_mods( bool $posting_restricted_to_mods ) Sets whether posting is restricted to moderators. + * @method Actor set_preferred_username( string $preferred_username ) Sets the preferred username of the actor. + * @method Actor set_public_key( string|array $public_key ) Sets the public key of the actor. + * @method Actor set_streams( array $streams ) Sets the list of supplementary collections. + * @method Actor set_webfinger( string $webfinger ) Sets the WebFinger resource. */ class Actor extends Base_Object { // Reduced context for actors. TODO: still unused. @@ -27,6 +73,7 @@ class Actor extends Base_Object { 'schema' => 'http://schema.org#', 'toot' => 'http://joinmastodon.org/ns#', 'lemmy' => 'https://join-lemmy.org/ns#', + 'litepub' => 'http://litepub.social/ns#', 'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers', 'PropertyValue' => 'schema:PropertyValue', 'value' => 'schema:value', @@ -55,9 +102,15 @@ class Actor extends Base_Object { '@id' => 'toot:attributionDomains', '@type' => '@id', ), + 'implements' => array( + '@id' => 'https://w3id.org/fep/844e/implements', + '@type' => '@id', + '@container' => '@list', + ), 'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods', 'discoverable' => 'toot:discoverable', 'indexable' => 'toot:indexable', + 'invisible' => 'litepub:invisible', ), ); @@ -166,14 +219,13 @@ class Actor extends Base_Object { protected $endpoints; /** - * It's not part of the ActivityPub protocol but it's a quite common + * It's not part of the ActivityPub protocol, but it's a quite common * practice to handle an actor public key with a publicKey array: * [ - * 'id' => 'https://my-example.com/actor#main-key' - * 'owner' => 'https://my-example.com/actor', + * 'id' => 'https://my-example.com/actor#main-key' + * 'owner' => 'https://my-example.com/actor', * 'publicKeyPem' => '-----BEGIN PUBLIC KEY----- - * MIIBI [...] - * DQIDAQAB + * [...] * -----END PUBLIC KEY-----' * ] * @@ -184,8 +236,8 @@ class Actor extends Base_Object { protected $public_key; /** - * It's not part of the ActivityPub protocol but it's a quite common - * practice to lock an account. If anabled, new followers will not be + * It's not part of the ActivityPub protocol, but it's a quite common + * practice to lock an account. If enabled, new followers will not be * automatically accepted, but will instead require you to manually * approve them. * @@ -195,7 +247,7 @@ class Actor extends Base_Object { * * @context as:manuallyApprovesFollowers * - * @var boolean + * @var boolean|null */ protected $manually_approves_followers = false; @@ -205,7 +257,7 @@ class Actor extends Base_Object { * * @see https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/ * - * @var array + * @var array|null */ protected $attribution_domains = null; @@ -219,7 +271,102 @@ class Actor extends Base_Object { /** * The alsoKnownAs of the actor. * - * @var array + * @var array|null */ protected $also_known_as; + + /** + * The Featured-Posts. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#featured + * + * @context { + * "@id": "http://joinmastodon.org/ns#featured", + * "@type": "@id" + * } + * + * @var string|null + */ + protected $featured; + + /** + * The Featured-Tags. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#featuredTags + * + * @context { + * "@id": "http://joinmastodon.org/ns#featuredTags", + * "@type": "@id" + * } + * + * @var string|null + */ + protected $featured_tags; + + /** + * Whether the User is discoverable. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#discoverable + * + * @context http://joinmastodon.org/ns#discoverable + * + * @var boolean|null + */ + protected $discoverable; + + /** + * Whether the User is indexable. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#indexable + * + * @context http://joinmastodon.org/ns#indexable + * + * @var boolean|null + */ + protected $indexable; + + /** + * The WebFinger Resource. + * + * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/2c59/fep-2c59.md + * + * @var string|null + */ + protected $webfinger; + + /** + * URL to the Moderators endpoint. + * + * @see https://join-lemmy.org/docs/contributors/05-federation.html + * + * @var string|null + */ + protected $moderators; + + /** + * Restrict posting to mods. + * + * @see https://join-lemmy.org/docs/contributors/05-federation.html + * + * @var boolean|null + */ + protected $posting_restricted_to_mods; + + /** + * Listing Implemented Specifications on the Application Actor + * + * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/844e/fep-844e.md + * + * @var array|null + */ + protected $implements; + + /** + * Whether the User is invisible. + * + * @see https://litepub.social/ + * + * @var boolean|null + */ + protected $invisible = null; } diff --git a/wp-content/plugins/activitypub/includes/activity/class-base-object.php b/wp-content/plugins/activitypub/includes/activity/class-base-object.php index cc1d885e..5668cd0e 100644 --- a/wp-content/plugins/activitypub/includes/activity/class-base-object.php +++ b/wp-content/plugins/activitypub/includes/activity/class-base-object.php @@ -9,6 +9,8 @@ namespace Activitypub\Activity; +use Activitypub\Activity\Extended_Object\Place; + /** * Base_Object is an implementation of one of the * Activity Streams Core Types. @@ -21,48 +23,96 @@ namespace Activitypub\Activity; * * @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 array|string|null get_attachment() Gets the attachment property of the object. + * @method array|string|null get_attributed_to() Gets the entity attributed as the original author. + * @method string|null get_audience() Gets the total population of entities for which the object can be considered relevant. + * @method string[]|string|null get_bcc() Gets the private secondary audience of the object. + * @method string[]|string|null get_bto() Gets the private primary audience of the object. + * @method string[]|string|null get_cc() Gets the secondary recipients of the object. + * @method string|null get_content() Gets the content property of the object. + * @method string[]|null get_content_map() Gets the content map property of the object. + * @method string|null get_context() Gets the context within which the object exists. + * @method array|null get_dcterms() Gets the Dublin Core terms property of the object. + * @method string|null get_duration() Gets the duration property of time-bound resources. + * @method string|null get_end_time() Gets the date and time describing the ending time of the object. + * @method string|null get_generator() Gets the entity that generated the object. + * @method string[]|null get_icon() Gets the icon property of the object. + * @method string|null get_id() Gets the object's unique global identifier. + * @method string[]|null get_image() Gets the image property of the object. + * @method string[]|string|null get_in_reply_to() Gets the objects this object is in reply to. + * @method array|null get_interaction_policy() Gets the interaction policy property of the object. + * @method array|null get_likes() Gets the collection of likes for this object. + * @method array|string|null|Place get_location() Gets the physical or logical locations associated with the object. + * @method string|null get_media_type() Gets the MIME media type of the content property. + * @method string|null get_name() Gets the natural language name of the object. + * @method string[]|null get_name_map() Gets the name map property of the object. + * @method string|null get_preview() Gets the entity that provides a preview of this object. + * @method string|null get_published() Gets the date and time the object was published in ISO 8601 format. + * @method string|null get_quote() Gets the quote property of the object (FEP-044f). + * @method string|null get_quote_url() Gets the quoteUrl property of the object. + * @method string|null get_quote_uri() Gets the quoteUri property of the object. + * @method string|null get__misskey_quote() Gets the _misskey_quote property of the object. + * @method string|array|null get_replies() Gets the collection of responses to this object. + * @method bool|null get_sensitive() Gets the sensitive property of the object. + * @method array|null get_shares() Gets the collection of shares for this object. + * @method array|null get_source() Gets the source property indicating content markup derivation. + * @method string|null get_start_time() Gets the date and time describing the starting time of the object. + * @method string|null get_summary() Gets the natural language summary of the object. + * @method string[]|null get_summary_map() Gets the summary map property of the object. + * @method array[]|null get_tag() Gets the tag property of the object. + * @method string[]|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|null get_former_type() Gets the former type of a Tombstone object. + * @method string|null get_deleted() Gets the date and time the object was deleted in ISO 8601 format. * - * @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 string|string[] add_cc( string|array $cc ) Adds one or more entities to the secondary audience of the object. + * @method string|string[] 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. + * @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_audience( string $audience ) Sets the total population of entities for which the object can be considered relevant. + * @method Base_Object set_bcc( array|string $bcc ) Sets the private secondary audience of the object. + * @method Base_Object set_bto( array|string $bto ) Sets the private primary audience of the object. + * @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_context( string $context ) Sets the context within which the object exists. + * @method Base_Object set_dcterms( array $dcterms ) Sets the Dublin Core terms property of the object. + * @method Base_Object set_duration( string $duration ) Sets the duration property of time-bound resources. + * @method Base_Object set_end_time( string $end_time ) Sets the date and time describing the ending time of the object. + * @method Base_Object set_generator( string $generator ) Sets the entity that generated 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_in_reply_to( string|string[] $in_reply_to ) Sets the is in reply to property of the object. + * @method Base_Object set_interaction_policy( array|null $policy ) Sets the interaction policy property of the object. + * @method Base_Object set_likes( array $likes ) Sets the collection of likes for this object. + * @method Base_Object set_location( array|string|Place $location ) Sets the physical or logical locations associated with the object. + * @method Base_Object set_media_type( string $media_type ) Sets the MIME media type of the content property. + * @method Base_Object set_name( string $name ) Sets the natural language name of the object. + * @method Base_Object set_name_map( array|null $name_map ) Sets the name map property of the object. + * @method Base_Object set_preview( string $preview ) Sets the entity that provides a preview of this object. + * @method Base_Object set_published( string|null $published ) Sets the date and time the object was published in ISO 8601 format. + * @method Base_Object set_quote( string $quote ) Sets the quote property of the object (FEP-044f). + * @method Base_Object set_quote_url( string $quote_url ) Sets the quoteUrl property of the object. + * @method Base_Object set_quote_uri( string $quote_uri ) Sets the quoteUri property of the object. + * @method Base_Object set__misskey_quote( mixed $misskey_quote ) Sets the _misskey_quote property of the object. + * @method Base_Object set_replies( string|array $replies ) Sets the collection of responses to this object. + * @method Base_Object set_sensitive( bool|null $sensitive ) Sets the sensitive property of the object. + * @method Base_Object set_shares( array $shares ) Sets the collection of shares for this object. + * @method Base_Object set_source( array $source ) Sets the source property indicating content markup derivation. + * @method Base_Object set_start_time( string $start_time ) Sets the date and time describing the starting time 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_tag( array|null $tag ) Sets the tag property of the object. + * @method Base_Object set_to( string|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. + * @method Base_Object set_former_type( string $former_type ) Sets the former type of a Tombstone object. + * @method Base_Object set_deleted( string $deleted ) Sets the date and time the object was deleted in ISO 8601 format. */ class Base_Object extends Generic_Object { /** @@ -73,8 +123,45 @@ class Base_Object extends Generic_Object { const JSON_LD_CONTEXT = array( 'https://www.w3.org/ns/activitystreams', array( - 'Hashtag' => 'as:Hashtag', - 'sensitive' => 'as:sensitive', + 'Hashtag' => 'as:Hashtag', + 'sensitive' => 'as:sensitive', + 'dcterms' => 'http://purl.org/dc/terms/', + 'gts' => 'https://gotosocial.org/ns#', + 'schema' => 'http://schema.org/', + 'exifData' => 'schema:exifData', + 'PropertyValue' => 'schema:PropertyValue', + 'interactionPolicy' => array( + '@id' => 'gts:interactionPolicy', + '@type' => '@id', + ), + 'canQuote' => array( + '@id' => 'gts:canQuote', + '@type' => '@id', + ), + 'canReply' => array( + '@id' => 'gts:canReply', + '@type' => '@id', + ), + 'canLike' => array( + '@id' => 'gts:canLike', + '@type' => '@id', + ), + 'canAnnounce' => array( + '@id' => 'gts:canAnnounce', + '@type' => '@id', + ), + 'automaticApproval' => array( + '@id' => 'gts:automaticApproval', + '@type' => '@id', + ), + 'manualApproval' => array( + '@id' => 'gts:manualApproval', + '@type' => '@id', + ), + 'always' => array( + '@id' => 'gts:always', + '@type' => '@id', + ), ), ); @@ -132,7 +219,7 @@ class Base_Object extends Generic_Object { /** * One or more entities that represent the total population of - * entities for which the object can considered to be relevant. + * entities for which the object can be considered to be relevant. * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audience * @@ -180,6 +267,25 @@ class Base_Object extends Generic_Object { */ protected $content_map; + /** + * The date and time at which the object was deleted. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-deleted + * + * @var string|null + */ + protected $deleted; + + /** + * The former type of the object. Used in Tombstone objects to + * indicate the type of the object prior to deletion. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-formertype + * + * @var string|null + */ + protected $former_type; + /** * A simple, human-readable, plain-text name for the object. * HTML markup MUST NOT be included. @@ -258,7 +364,7 @@ class Base_Object extends Generic_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-location * - * @var string|null + * @var string|null|Place */ protected $location; @@ -395,9 +501,9 @@ class Base_Object extends Generic_Object { /** * When the object describes a time-bound resource, such as an audio - * or video, a meeting, etc, the duration property indicates the + * or video, a meeting, etc., the duration property indicates the * object's approximate duration. - * The value MUST be expressed as an xsd:duration as defined by + * The value MUST be expressed as a xsd:duration as defined by * xmlschema11-2, section 3.3.6 (e.g. a period of 5 seconds is * represented as "PT5S"). * @@ -414,7 +520,7 @@ class Base_Object extends Generic_Object { * * @see https://www.w3.org/TR/activitypub/#source-property * - * @var array + * @var array|null */ protected $source; @@ -434,7 +540,7 @@ class Base_Object extends Generic_Object { * * @see https://www.w3.org/TR/activitypub/#likes * - * @var array + * @var array|null */ protected $likes; @@ -444,7 +550,7 @@ class Base_Object extends Generic_Object { * * @see https://www.w3.org/TR/activitypub/#shares * - * @var array + * @var array|null */ protected $shares; @@ -455,10 +561,70 @@ class Base_Object extends Generic_Object { * * @see https://docs.joinmastodon.org/spec/activitypub/#sensitive * - * @var boolean + * @var boolean|null */ protected $sensitive; + /** + * The dcterms namespace. + * + * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/b2b8/fep-b2b8.md#sensitive + * @see https://www.dublincore.org/specifications/dublin-core/dcmi-terms/ + * + * @var array|null + */ + protected $dcterms; + + /** + * Interaction policy is an attempt to limit the harmful effects of unwanted replies and + * other interactions on a user's posts (e.g., "reply guys"). + * + * It is also used by Mastodon to limit the ability to quote posts. + * + * @see https://docs.gotosocial.org/en/latest/federation/interaction_policy/ + * @see https://blog.joinmastodon.org/2025/09/introducing-quote-posts/ + * + * @var array|null + */ + protected $interaction_policy; + + /** + * Fediverse Enhancement Proposal 044f: Quote Property + * + * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/044f/fep-044f.md + * @see https://w3id.org/fep/044f#quote + * + * @var string|null + */ + protected $quote; + + /** + * ActivityStreams quoteUrl property. + * + * @see https://www.w3.org/ns/activitystreams#quoteUrl + * + * @var string|null + */ + protected $quote_url; + + /** + * Fedibird-specific quoteUri property. + * + * @see https://fedibird.com/ns#quoteUri + * + * @var string|null + */ + protected $quote_uri; + + /** + * Misskey-specific quote property. + * + * @see https://misskey-hub.net/ns/#_misskey_quote + * + * @var string|null + */ + protected $_misskey_quote; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore + /** * Generic getter. * diff --git a/wp-content/plugins/activitypub/includes/activity/class-generic-object.php b/wp-content/plugins/activitypub/includes/activity/class-generic-object.php index eceff1e8..e4802792 100644 --- a/wp-content/plugins/activitypub/includes/activity/class-generic-object.php +++ b/wp-content/plugins/activitypub/includes/activity/class-generic-object.php @@ -17,6 +17,10 @@ use function Activitypub\snake_to_camel_case; * It is used to create objects that might be unknown by the plugin but * conform to the ActivityStreams vocabulary. * + * Provides generic magic methods for getting, setting, and adding properties + * through __call(). Specific property documentation is in the classes where + * the properties are actually defined. + * * @since 5.3.0 */ #[\AllowDynamicProperties] @@ -62,6 +66,8 @@ class Generic_Object { * * @param string $method The method name. * @param string $params The method params. + * + * @return mixed */ public function __call( $method, $params ) { $var = \strtolower( \substr( $method, 4 ) ); @@ -81,6 +87,8 @@ class Generic_Object { if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { return $this->add( $var, $params[0] ); } + + return null; } /** @@ -114,7 +122,7 @@ class Generic_Object { * @param string $key The key to set. * @param mixed $value The value to add. * - * @return mixed The value. + * @return mixed|void The value. */ public function add( $key, $value ) { if ( empty( $value ) ) { @@ -158,7 +166,7 @@ class Generic_Object { * * @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. + * @return static|\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 ); @@ -175,7 +183,7 @@ class Generic_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. + * @return static|\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 ) ) { @@ -196,7 +204,11 @@ class Generic_Object { public function from_array( $data ) { foreach ( $data as $key => $value ) { if ( null !== $value ) { - $key = camel_to_snake_case( $key ); + // Convert camelCase to snake_case if not prefixed with '_'. + if ( ! \str_starts_with( $key, '_' ) ) { + $key = camel_to_snake_case( $key ); + } + call_user_func( array( $this, 'set_' . $key ), $value ); } } @@ -219,11 +231,18 @@ class Generic_Object { * It tries to get the object attributes if they exist * and falls back to the getters. Empty values are ignored. * + * By default, `bto` and `bcc` (the blind audience fields) are stripped + * from the output per ActivityPub Section 6, so every serialization path + * is safe for emission. Internal callers that need to persist the blind + * audience (e.g., outbox/inbox storage) can opt in by passing + * `$include_blind_audience = true`. + * * @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true. + * @param bool $include_blind_audience Whether to keep `bto` and `bcc` in the output. Default false. * * @return array An array built from the Object. */ - public function to_array( $include_json_ld_context = true ) { + public function to_array( $include_json_ld_context = true, $include_blind_audience = false ) { $array = array(); $vars = get_object_vars( $this ); @@ -243,11 +262,14 @@ class Generic_Object { } if ( is_object( $value ) ) { - $value = $value->to_array( false ); + $value = $value->to_array( false, $include_blind_audience ); } - // If value is still empty, ignore it for the array and continue. - if ( isset( $value ) ) { + if ( is_array( $value ) && $this->is_namespaced( $key ) ) { + foreach ( $value as $sub_key => $sub_value ) { + $array[ snake_to_camel_case( $key ) . ':' . snake_to_camel_case( $sub_key ) ] = $sub_value; + } + } elseif ( isset( $value ) ) { $array[ snake_to_camel_case( $key ) ] = $value; } } @@ -281,18 +303,33 @@ class Generic_Object { * * @return array The filtered array of the ActivityPub object. */ - return \apply_filters( "activitypub_activity_{$class}_object_array", $array, $this->id, $this ); + $array = \apply_filters( "activitypub_activity_{$class}_object_array", $array, $this->id, $this ); + + if ( ! $include_blind_audience ) { + /* + * Strip `bto` and `bcc` from the serialized array per ActivityPub Section 6. + * Callers that need the blind audience either read it from the object via + * `get_bto()` / `get_bcc()` or opt in with `$include_blind_audience = true`. + */ + unset( $array['bto'], $array['bcc'] ); + if ( isset( $array['object'] ) && \is_array( $array['object'] ) ) { + unset( $array['object']['bto'], $array['object']['bcc'] ); + } + } + + return $array; } /** * Convert Object to JSON. * * @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true. + * @param bool $include_blind_audience Whether to keep `bto` and `bcc` in the output. Default false. * * @return string The JSON string. */ - public function to_json( $include_json_ld_context = true ) { - $array = $this->to_array( $include_json_ld_context ); + public function to_json( $include_json_ld_context = true, $include_blind_audience = false ) { + $array = $this->to_array( $include_json_ld_context, $include_blind_audience ); $options = \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT | \JSON_UNESCAPED_SLASHES; /** @@ -322,4 +359,23 @@ class Generic_Object { public function get_json_ld_context() { return static::JSON_LD_CONTEXT; } + + /** + * Checks if an attribute is in a namespace. + * + * @param string $attribute The attribute to check. + * + * @return bool Whether the attribute is namespaced. + */ + private function is_namespaced( $attribute ) { + $namespaces = array(); + + foreach ( static::JSON_LD_CONTEXT as $context ) { + if ( is_array( $context ) ) { + $namespaces = \array_merge( $namespaces, $context ); + } + } + + return isset( $namespaces[ $attribute ] ) && \wp_http_validate_url( $namespaces[ $attribute ] ); + } } diff --git a/wp-content/plugins/activitypub/includes/activity/extended-object/README.md b/wp-content/plugins/activitypub/includes/activity/extended-object/README.md new file mode 100644 index 00000000..c5ffee43 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/activity/extended-object/README.md @@ -0,0 +1,5 @@ +# Extended Objects + +This folder contains more complex ActivityPub objects. + +Although they could be created using the `Base_Object`, they provide an easier structure for creating objects with maximum compatibility. diff --git a/wp-content/plugins/activitypub/includes/activity/extended-object/class-event.php b/wp-content/plugins/activitypub/includes/activity/extended-object/class-event.php index a44c2c66..65f5be4c 100644 --- a/wp-content/plugins/activitypub/includes/activity/extended-object/class-event.php +++ b/wp-content/plugins/activitypub/includes/activity/extended-object/class-event.php @@ -10,11 +10,47 @@ namespace Activitypub\Activity\Extended_Object; use Activitypub\Activity\Base_Object; /** - * Event is an implementation of one of the Activity Streams Event object type. + * Event is an implementation of Activity Streams Event object type. * * This class contains extra keys as used by Mobilizon to ensure compatibility. * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event + * + * @method string|null get_actor() Gets which actor created the event. + * @method float|null get_altitude() Gets the altitude of the event location. + * @method bool|null get_anonymous_participation_enabled() Gets whether anonymous participation is enabled. + * @method string|null get_category() Gets the event's category. + * @method bool|null get_comments_enabled() Gets whether comments/replies are enabled. + * @method array|null get_contacts() Gets the event's contacts. + * @method string|null get_external_participation_url() Gets the external participation URL. + * @method string|null get_in_language() Gets the language of the event. + * @method bool|null get_is_online() Gets whether the event is online. + * @method string|null get_join_mode() Gets how new members may be able to join. + * @method int|null get_maximum_attendee_capacity() Gets how many places there can be for an event. + * @method string|null get_name() Gets the title of the event. + * @method int|null get_participant_count() Gets the participant count of the event. + * @method int|null get_remaining_attendee_capacity() Gets the number of attendee places that remain unallocated. + * @method string|null get_replies_moderation_option() Gets the moderation option for replies. + * @method string|null get_status() Gets the event's status. + * @method string|null get_timezone() Gets the timezone of the event. + * + * @method Event set_actor( string $actor ) Sets which actor created the event. + * @method Event set_altitude( float $altitude ) Sets the altitude of the event location. + * @method Event set_anonymous_participation_enabled( bool $enabled ) Sets whether anonymous participation is enabled. + * @method Event set_category( string $category, bool $mobilizon_compatibility ) Sets the event's category. + * @method Event set_comments_enabled( bool $comments_enabled ) Sets whether comments/replies are enabled. + * @method Event set_contacts( array $contacts ) Sets the event's contacts. + * @method Event set_external_participation_url( string $url ) Sets the external participation URL. + * @method Event set_in_language( string $language ) Sets the language of the event. + * @method Event set_is_online( bool $is_online ) Sets whether the event is online. + * @method Event set_join_mode( string $join_mode ) Sets how new members may be able to join. + * @method Event set_maximum_attendee_capacity( int $capacity ) Sets how many places there can be for an event. + * @method Event set_name( string $name ) Sets the title of the event. + * @method Event set_participant_count( int $count ) Sets the participant count of the event. + * @method Event set_remaining_attendee_capacity( int $capacity ) Sets the number of attendee places that remain unallocated. + * @method Event set_replies_moderation_option( string $type ) Sets the moderation option for replies. + * @method Event set_status( string $status ) Sets the event's status. + * @method Event set_timezone( string $timezone ) Sets the timezone of the event. */ class Event extends Base_Object { // Human friendly minimal context for full Mobilizon compatible ActivityPub events. @@ -123,7 +159,7 @@ class Event extends Base_Object { protected $name; /** - * The events contacts. + * The event's contacts. * * @context { * '@id' => 'mz:contacts', diff --git a/wp-content/plugins/activitypub/includes/activity/extended-object/class-place.php b/wp-content/plugins/activitypub/includes/activity/extended-object/class-place.php index 1bf7419b..16e682ca 100644 --- a/wp-content/plugins/activitypub/includes/activity/extended-object/class-place.php +++ b/wp-content/plugins/activitypub/includes/activity/extended-object/class-place.php @@ -11,13 +11,27 @@ namespace Activitypub\Activity\Extended_Object; use Activitypub\Activity\Base_Object; /** - * Event is an implementation of one of the - * Activity Streams Event object type + * Place is an implementation of the Activity Streams Place object type. * - * The Object is the primary base type for the Activity Streams - * vocabulary. + * The Place object represents a logical or physical location. * - * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-place + * + * @method float|null get_accuracy() Gets the accuracy of position coordinates. + * @method array|string|null get_address() Gets the address of the place. + * @method float|null get_altitude() Gets the altitude of the place. + * @method float|null get_latitude() Gets the latitude of the place. + * @method float|null get_longitude() Gets the longitude of the place. + * @method float|null get_radius() Gets the radius from the given latitude and longitude. + * @method string|null get_units() Gets the measurement units for radius and altitude. + * + * @method Place set_accuracy( float $accuracy ) Sets the accuracy of position coordinates. + * @method Place set_address( array|string $address ) Sets the address of the place. + * @method Place set_altitude( float $altitude ) Sets the altitude of the place. + * @method Place set_latitude( float $latitude ) Sets the latitude of the place. + * @method Place set_longitude( float $longitude ) Sets the longitude of the place. + * @method Place set_radius( float $radius ) Sets the radius from the given latitude and longitude. + * @method Place set_units( string $units ) Sets the measurement units for radius and altitude. */ class Place extends Base_Object { /** diff --git a/wp-content/plugins/activitypub/includes/activity/extended-object/class-quote-authorization.php b/wp-content/plugins/activitypub/includes/activity/extended-object/class-quote-authorization.php new file mode 100644 index 00000000..74860e53 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/activity/extended-object/class-quote-authorization.php @@ -0,0 +1,70 @@ + 'https://w3id.org/fep/044f#QuoteAuthorization', + 'gts' => 'https://gotosocial.org/ns#', + 'interactingObject' => array( + '@id' => 'gts:interactingObject', + '@type' => '@id', + ), + 'interactionTarget' => array( + '@id' => 'gts:interactionTarget', + '@type' => '@id', + ), + ), + ); + + /** + * The type of the object. + * + * @var string + */ + protected $type = 'QuoteAuthorization'; + + /** + * The object that is being interacted with. + * + * @var Base_Object|string|array|null + */ + protected $interacting_object; + + /** + * The target of the interaction. + * + * @var Base_Object|string|array|null + */ + protected $interaction_target; +} diff --git a/wp-content/plugins/activitypub/includes/cache/class-avatar.php b/wp-content/plugins/activitypub/includes/cache/class-avatar.php new file mode 100644 index 00000000..1dbce368 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/cache/class-avatar.php @@ -0,0 +1,172 @@ + self::MAX_DIMENSION ) ); + + return $cached_url ?: $url; + } + + /** + * Maybe clean up cached avatar when actor is deleted. + * + * @param int $post_id The post ID being deleted. + */ + public static function maybe_cleanup( $post_id ) { + if ( Remote_Actors::POST_TYPE !== \get_post_type( $post_id ) ) { + return; + } + + self::invalidate_entity( $post_id ); + } + + /** + * Save an avatar for an actor. + * + * This is a convenience method that wraps get_or_cache with the correct options. + * It also invalidates any existing avatar before caching the new one. + * + * @param int $actor_id The actor post ID. + * @param string $avatar_url The remote avatar URL. + * + * @return string|false The local avatar URL on success, false on failure. + */ + public static function save( $actor_id, $avatar_url ) { + // Validate actor_id is a positive integer. + $actor_id = (int) $actor_id; + if ( $actor_id <= 0 ) { + return false; + } + + if ( empty( $avatar_url ) || ! \filter_var( $avatar_url, FILTER_VALIDATE_URL ) ) { + return false; + } + + // Delete existing avatar files before saving new one. + self::invalidate_entity( $actor_id ); + + return self::cache( + $avatar_url, + $actor_id, + array( 'max_dimension' => self::MAX_DIMENSION ) + ); + } +} diff --git a/wp-content/plugins/activitypub/includes/cache/class-emoji.php b/wp-content/plugins/activitypub/includes/cache/class-emoji.php new file mode 100644 index 00000000..bf76c331 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/cache/class-emoji.php @@ -0,0 +1,184 @@ + self::MAX_DIMENSION ); + if ( $updated ) { + $options['updated'] = $updated; + } + + return self::get_or_cache( $emoji_url, $domain, $options ); + } + + /** + * Generate a hash for an emoji URL. + * + * Uses full URL path hash to prevent collisions between emoji with the same + * filename but different paths (e.g., /set1/kappa.png vs /set2/kappa.png). + * + * @param string $url The URL to hash. + * + * @return string The hash string. + */ + protected static function generate_hash( $url ) { + $url_path = \wp_parse_url( $url, PHP_URL_PATH ); + if ( $url_path ) { + return \md5( $url_path ); + } + + // Fall back to full URL hash. + return parent::generate_hash( $url ); + } +} diff --git a/wp-content/plugins/activitypub/includes/cache/class-file.php b/wp-content/plugins/activitypub/includes/cache/class-file.php new file mode 100644 index 00000000..14fe4922 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/cache/class-file.php @@ -0,0 +1,671 @@ + $upload_dir['basedir'] . static::get_base_dir() . $entity_id, + 'baseurl' => $upload_dir['baseurl'] . static::get_base_dir() . $entity_id, + ); + } + + /** + * Get a cached file URL if it exists. + * + * @param string $url The remote URL. + * @param string|int $entity_id The entity identifier. + * + * @return string|false The local URL if cached, false otherwise. + */ + public static function get( $url, $entity_id ) { + if ( empty( $url ) || ! \filter_var( $url, FILTER_VALIDATE_URL ) ) { + return false; + } + + $paths = static::get_storage_paths( $entity_id ); + + if ( ! \is_dir( $paths['basedir'] ) ) { + return false; + } + + $hash = static::generate_hash( $url ); + $pattern = static::escape_glob_pattern( $paths['basedir'] . '/' . $hash ) . '.*'; + $matches = \glob( $pattern ); + + if ( ! empty( $matches ) && \is_file( $matches[0] ) ) { + return $paths['baseurl'] . '/' . \basename( $matches[0] ); + } + + return false; + } + + /** + * Get a cached file or cache it if not present. + * + * This is the main entry point for lazy caching. Called via filter hooks. + * + * @param string $url The remote URL. + * @param string|int $entity_id The entity identifier. + * @param array $options Optional. Additional options like 'updated' timestamp. + * + * @return string|false The local URL on success, false on failure. + */ + public static function get_or_cache( $url, $entity_id, $options = array() ) { + if ( empty( $url ) || ! \filter_var( $url, FILTER_VALIDATE_URL ) ) { + return false; + } + + // Check if already cached. + $cached_url = static::get( $url, $entity_id ); + if ( $cached_url ) { + // Check for staleness if updated timestamp provided. + if ( ! empty( $options['updated'] ) ) { + $paths = static::get_storage_paths( $entity_id ); + $hash = static::generate_hash( $url ); + $pattern = static::escape_glob_pattern( $paths['basedir'] . '/' . $hash ) . '.*'; + $matches = \glob( $pattern ); + $file_path = ( $matches && \is_file( $matches[0] ) ) ? $matches[0] : null; + $local_time = $file_path ? \filemtime( $file_path ) : 0; + $remote_time = \strtotime( $options['updated'] ); + + if ( $remote_time && $local_time >= $remote_time ) { + return $cached_url; + } + // Stale - continue to re-download. + } else { + return $cached_url; + } + } + + // Download and cache the file. + return static::cache( $url, $entity_id, $options ); + } + + /** + * Cache a remote file locally. + * + * Downloads the file, validates it, optimizes images, and stores locally. + * + * @param string $url The remote URL. + * @param string|int $entity_id The entity identifier. + * @param array $options Optional. Additional options. + * + * @return string|false The local URL on success, false on failure. + */ + public static function cache( $url, $entity_id, $options = array() ) { + $result = static::download_and_validate( $url ); + + if ( \is_wp_error( $result ) || empty( $result['file'] ) ) { + return false; + } + + $tmp_file = $result['file']; + $paths = static::get_storage_paths( $entity_id ); + + // Create directory if it doesn't exist. + if ( ! \wp_mkdir_p( $paths['basedir'] ) ) { + \wp_delete_file( $tmp_file ); + return false; + } + + // Generate hash-based filename. + $hash = static::generate_hash( $url ); + $ext = \pathinfo( $tmp_file, PATHINFO_EXTENSION ); + if ( empty( $ext ) ) { + $ext = \wp_get_default_extension_for_mime_type( $result['mime_type'] ); + } + $file_name = $hash . '.' . $ext; + $file_path = $paths['basedir'] . '/' . $file_name; + + // Move file to destination. + if ( ! static::get_filesystem()->move( $tmp_file, $file_path, true ) ) { + \wp_delete_file( $tmp_file ); + return false; + } + + // Optimize image if applicable. + $max_dimension = $options['max_dimension'] ?? static::get_max_dimension(); + $file_path = static::optimize_image( $file_path, $max_dimension ); + $file_name = \basename( $file_path ); + + $local_url = $paths['baseurl'] . '/' . $file_name; + + /** + * Fires after a remote media file has been successfully cached. + * + * Use this hook for logging, analytics, or post-processing. + * + * @since 5.6.0 + * + * @param string $local_url The local URL of the cached file. + * @param string $url The original remote URL. + * @param string|int $entity_id The entity identifier. + * @param string $type The cache type ('avatar', 'media', 'emoji'). + * @param string $file_path The local file system path. + */ + \do_action( 'activitypub_media_cached', $local_url, $url, $entity_id, static::get_type(), $file_path ); + + return $local_url; + } + + /** + * Invalidate cached files for an entity. + * + * Deletes the entire entity directory and all its contents. + * + * @param string|int $entity_id The entity identifier. + * + * @return bool True on success, false on failure. + */ + public static function invalidate_entity( $entity_id ) { + $paths = static::get_storage_paths( $entity_id ); + + return static::delete_directory( $paths['basedir'] ); + } + + /** + * Get a direct filesystem instance. + * + * Uses WP_Filesystem_Direct explicitly instead of WP_Filesystem(), + * which may fall back to FTP on servers where ABSPATH is not writable. + * The uploads directory (where cache files live) is always writable by + * the web server — the same assumption WordPress core makes for media + * uploads in _wp_handle_upload(). + * + * @since 8.0.0 + * + * @return \WP_Filesystem_Direct The direct filesystem instance. + */ + protected static function get_filesystem() { + static $filesystem = null; + + if ( null === $filesystem ) { + require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; + + $filesystem = new \WP_Filesystem_Direct( null ); + } + + return $filesystem; + } + + /** + * Delete a directory and all its contents. + * + * @since 8.0.0 + * + * @param string $basedir The directory path to delete. + * + * @return bool True on success or if directory doesn't exist, false on failure. + */ + public static function delete_directory( $basedir ) { + if ( ! \is_dir( $basedir ) ) { + return true; + } + + return static::get_filesystem()->rmdir( $basedir, true ); + } + + /** + * Generate a hash for a URL. + * + * Uses full MD5 hash (32 characters) for better collision resistance. + * With truncated hashes, collision probability increases significantly + * at scale. + * + * @param string $url The URL to hash. + * + * @return string The full MD5 hash string (32 characters). + */ + protected static function generate_hash( $url ) { + return \md5( $url ); + } + + /** + * Escape glob metacharacters in a pattern. + * + * This prevents special characters (*, ?, [, ]) from being interpreted + * as glob patterns when searching for files. + * + * @param string $pattern The pattern to escape. + * + * @return string The escaped pattern safe for use in glob(). + */ + protected static function escape_glob_pattern( $pattern ) { + return \preg_replace( '/([*?\[\]])/', '[$1]', $pattern ); + } + + /** + * Validate a URL is safe to fetch. + * + * @param string $url The URL to validate. + * + * @return bool True if URL is safe to fetch, false otherwise. + */ + protected static function is_safe_url( $url ) { + if ( empty( $url ) || ! \filter_var( $url, FILTER_VALIDATE_URL ) ) { + return false; + } + + /** + * Filters whether a URL passes safety validation. + * + * By default, uses wp_http_validate_url() which prevents SSRF attacks + * by blocking private IPs and localhost. This filter allows overriding + * for testing or custom validation needs. + * + * @since 5.6.0 + * + * @param bool|null $is_safe Whether the URL is safe. Return true/false to override, + * or null to use default wp_http_validate_url() check. + * @param string $url The URL being validated. + */ + $is_safe = \apply_filters( 'activitypub_cache_is_safe_url', null, $url ); + + if ( null !== $is_safe ) { + return (bool) $is_safe; + } + + return (bool) \wp_http_validate_url( $url ); + } + + /** + * Get allowed MIME types for this cache type. + * + * @return array Array of allowed MIME types. + */ + protected static function get_allowed_mime_types() { + $type = static::get_type(); + + /** + * Filters the allowed MIME types for a cache type. + * + * Use this filter to add or remove allowed MIME types. + * + * @since 5.6.0 + * + * @param array $mime_types Array of allowed MIME types. + * @param string $type The cache type ('avatar', 'media', 'emoji'). + */ + return (array) \apply_filters( 'activitypub_cache_allowed_mime_types', static::DEFAULT_ALLOWED_MIME_TYPES, $type ); + } + + /** + * Download and validate a remote file. + * + * @param string $url The remote URL to download. + * + * @return array|\WP_Error { + * Array on success, WP_Error on failure. + * + * @type string $file Path to downloaded file. + * @type string $mime_type Validated MIME type. + * } + */ + protected static function download_and_validate( $url ) { + $type = static::get_type(); + + /** + * Filters the download result before fetching a URL. + * + * Allows short-circuiting the download process by providing a pre-downloaded + * file path. Useful for testing or when files are already available locally. + * + * @since 5.6.0 + * + * @param array|null $result { + * Return null to proceed with download, or array with file info. + * + * @type string $file Path to the downloaded file. + * @type string $mime_type The file's MIME type. + * } + * @param string $url The URL that would be downloaded. + * @param string $type The cache type ('avatar', 'media', 'emoji'). + */ + $pre_download = \apply_filters( 'activitypub_pre_download_url', null, $url, $type ); + + if ( null !== $pre_download ) { + return $pre_download; + } + + /** + * Filters whether a URL should be cached. + * + * Allows preventing specific URLs from being downloaded and cached. + * Return false to skip caching this URL. + * + * @since 5.6.0 + * + * @param bool $should_cache Whether to cache this URL. Default true. + * @param string $url The remote URL. + * @param string $type The cache type ('avatar', 'media', 'emoji'). + */ + $should_cache = \apply_filters( 'activitypub_should_cache_url', true, $url, $type ); + + if ( ! $should_cache ) { + return new \WP_Error( 'cache_skipped', \__( 'URL caching was skipped by filter.', 'activitypub' ) ); + } + + // Validate URL is safe to fetch. + if ( ! static::is_safe_url( $url ) ) { + return new \WP_Error( 'invalid_url', \__( 'URL is not allowed.', 'activitypub' ) ); + } + + if ( ! \function_exists( 'download_url' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + $tmp_file = \download_url( $url, 15 ); // 15 second timeout. + + if ( \is_wp_error( $tmp_file ) ) { + return $tmp_file; + } + + // Validate file size. + $file_size = \filesize( $tmp_file ); + if ( $file_size > static::MAX_FILE_SIZE ) { + \wp_delete_file( $tmp_file ); + return new \WP_Error( 'file_too_large', \__( 'File exceeds maximum size limit.', 'activitypub' ) ); + } + + // Validate MIME type. + $validation = static::validate_mime_type( $tmp_file ); + if ( \is_wp_error( $validation ) ) { + \wp_delete_file( $tmp_file ); + return $validation; + } + + // Get the validated file path (may have been renamed). + $file_path = \is_string( $validation ) ? $validation : $tmp_file; + $mime_type = static::get_file_mime_type( $file_path ); + + return array( + 'file' => $file_path, + 'mime_type' => $mime_type, + ); + } + + /** + * Validate MIME type of a file using multiple methods. + * + * This method addresses potential wp_get_image_mime() bypass concerns + * by using finfo, getimagesize, and wp_check_filetype_and_ext for validation. + * + * @param string $file_path Path to the file. + * + * @return string|\WP_Error File path (possibly renamed) on success, WP_Error on failure. + */ + protected static function validate_mime_type( $file_path ) { + $allowed_mime_types = static::get_allowed_mime_types(); + + // Require fileinfo extension for validation. + if ( ! \extension_loaded( 'fileinfo' ) ) { + return new \WP_Error( 'finfo_failed', \__( 'Fileinfo extension not available.', 'activitypub' ) ); + } + + // Method 1: Use cached finfo instance for reliable MIME detection. + if ( null === self::$finfo ) { + self::$finfo = new \finfo( FILEINFO_MIME_TYPE ); + } + + $mime = self::$finfo->file( $file_path ); + + if ( ! \in_array( $mime, $allowed_mime_types, true ) ) { + return new \WP_Error( 'invalid_mime', \__( 'File type not allowed.', 'activitypub' ) ); + } + + // Method 2: Verify it's actually a valid image. + $image_info = @\getimagesize( $file_path ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + if ( false === $image_info ) { + return new \WP_Error( 'invalid_image', \__( 'File is not a valid image.', 'activitypub' ) ); + } + + // Verify image can actually be rendered. + if ( ! \function_exists( 'file_is_displayable_image' ) ) { + require_once ABSPATH . 'wp-admin/includes/image.php'; + } + + if ( ! \file_is_displayable_image( $file_path ) ) { + return new \WP_Error( 'not_displayable', \__( 'Image cannot be displayed.', 'activitypub' ) ); + } + + /* + * Method 3: Use WordPress's wp_check_filetype_and_ext for additional validation. + * MIME type restriction is already enforced by finfo in Method 1; this cross-checks + * that file content matches the declared type using WordPress defaults. + */ + $expected_ext = \wp_get_default_extension_for_mime_type( $mime ); + + // Use the detected extension since temp files from download_url() have a .tmp extension. + $file_name = \pathinfo( \wp_basename( $file_path ), PATHINFO_FILENAME ) . '.' . $expected_ext; + $file_info = \wp_check_filetype_and_ext( $file_path, $file_name ); + + // If WordPress couldn't validate the file type, reject it. + if ( empty( $file_info['type'] ) || ! \str_starts_with( $file_info['type'], 'image/' ) ) { + return new \WP_Error( 'invalid_file_type', \__( 'File type validation failed.', 'activitypub' ) ); + } + + // Method 4: Ensure file extension matches MIME type. + $ext = \pathinfo( $file_path, PATHINFO_EXTENSION ); + + if ( strtolower( $ext ) !== $expected_ext ) { + $new_path = \preg_replace( '/\.[^.]+$/', '.' . $expected_ext, $file_path ); + if ( empty( $new_path ) || $new_path === $file_path ) { + $new_path = $file_path . '.' . $expected_ext; + } + + if ( static::get_filesystem()->move( $file_path, $new_path, true ) ) { + return $new_path; + } + } + + return $file_path; + } + + /** + * Get the MIME type of a file. + * + * @param string $file_path Path to the file. + * + * @return string The MIME type. + */ + protected static function get_file_mime_type( $file_path ) { + if ( \extension_loaded( 'fileinfo' ) ) { + if ( null === self::$finfo ) { + self::$finfo = new \finfo( FILEINFO_MIME_TYPE ); + } + return self::$finfo->file( $file_path ); + } + + // Fallback to WordPress function. + return \wp_check_filetype( $file_path )['type'] ?? ''; + } + + /** + * Optimize an image file by resizing and converting to WebP. + * + * Uses WordPress image editor to resize large images and convert them + * to WebP format for better compression while maintaining quality. + * + * @param string $file_path Path to the image file. + * @param int $max_dimension Maximum width/height in pixels. + * + * @return string The optimized file path. + */ + protected static function optimize_image( $file_path, $max_dimension ) { + // Check if it's an image. + $mime_type = static::get_file_mime_type( $file_path ); + if ( ! $mime_type || ! \str_starts_with( $mime_type, 'image/' ) ) { + return $file_path; + } + + $editor = \wp_get_image_editor( $file_path ); + if ( \is_wp_error( $editor ) ) { + return $file_path; + } + + $size = $editor->get_size(); + $needs_resize = $size['width'] > $max_dimension || $size['height'] > $max_dimension; + + // Resize if needed. + if ( $needs_resize ) { + $editor->resize( $max_dimension, $max_dimension, false ); + } + + // Check if WebP is supported. + $can_webp = $editor->supports_mime_type( 'image/webp' ); + + // Determine output format and save. + $dir = \dirname( $file_path ); + + if ( $can_webp ) { + // Convert to WebP. + $new_name = \wp_unique_filename( $dir, \preg_replace( '/\.[^.]+$/', '.webp', \basename( $file_path ) ) ); + $result = $editor->save( $dir . '/' . $new_name, 'image/webp' ); + } elseif ( \in_array( $mime_type, array( 'image/png', 'image/webp' ), true ) ) { + // Keep original format for potentially transparent images when WebP not available. + if ( ! $needs_resize ) { + return $file_path; + } + $result = $editor->save( $file_path ); + } else { + // Convert to JPEG when WebP not available. + $new_name = \wp_unique_filename( $dir, \preg_replace( '/\.[^.]+$/', '.jpg', \basename( $file_path ) ) ); + $result = $editor->save( $dir . '/' . $new_name, 'image/jpeg' ); + } + + if ( \is_wp_error( $result ) ) { + return $file_path; + } + + // Handle result. + $result_path = $result['path'] ?? $file_path; + + // If path changed (format conversion), delete the original file. + if ( $result_path !== $file_path ) { + \wp_delete_file( $file_path ); + } + + return $result_path; + } +} diff --git a/wp-content/plugins/activitypub/includes/cache/class-media.php b/wp-content/plugins/activitypub/includes/cache/class-media.php new file mode 100644 index 00000000..e314f98c --- /dev/null +++ b/wp-content/plugins/activitypub/includes/cache/class-media.php @@ -0,0 +1,191 @@ + $upload_dir['basedir'] . $base_dir . $entity_id, + 'baseurl' => $upload_dir['baseurl'] . $base_dir . $entity_id, + ); + } + + /** + * Initialize the cache handler. + */ + public static function init() { + // Only register local caching filter when caching is enabled. + if ( self::is_enabled() ) { + \add_filter( 'activitypub_remote_media_url', array( self::class, 'maybe_cache' ), 10, 4 ); + + // Clean up when post is deleted. + \add_action( 'before_delete_post', array( self::class, 'maybe_cleanup' ) ); + } + } + + /** + * Maybe cache a media URL. + * + * Hooked to the activitypub_remote_media_url filter. + * Downloads and caches the file locally if not already cached. + * + * @param string $url The remote URL. + * @param string $context The context ('avatar', 'media', 'emoji', etc.). + * @param string|int $entity_id The entity identifier (post ID). + * @param array $options Optional. Additional options. + * + * @return string The local URL if cached successfully, otherwise the original URL. + */ + public static function maybe_cache( $url, $context, $entity_id = null, $options = array() ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable -- Required for filter signature. + if ( self::CONTEXT !== $context || empty( $url ) || empty( $entity_id ) ) { + return $url; + } + + $cached_url = self::get_or_cache( $url, $entity_id ); + + return $cached_url ?: $url; + } + + /** + * Maybe clean up cached media when post is deleted. + * + * @param int $post_id The post ID being deleted. + */ + public static function maybe_cleanup( $post_id ) { + if ( Remote_Posts::POST_TYPE !== \get_post_type( $post_id ) ) { + return; + } + + self::invalidate_entity( $post_id ); + } + + /** + * Invalidate cached media for a comment. + * + * @param int $comment_id The comment ID. + * + * @return bool True on success, false on failure. + */ + public static function invalidate_comment( $comment_id ) { + $paths = self::get_storage_paths_for_context( $comment_id, self::CONTEXT_COMMENT ); + + return static::delete_directory( $paths['basedir'] ); + } +} diff --git a/wp-content/plugins/activitypub/includes/cache/class-stats-image.php b/wp-content/plugins/activitypub/includes/cache/class-stats-image.php new file mode 100644 index 00000000..1d26b68d --- /dev/null +++ b/wp-content/plugins/activitypub/includes/cache/class-stats-image.php @@ -0,0 +1,684 @@ + 501 ) ); + } + + // If local caching is disabled, use the REST endpoint for on-the-fly generation. + if ( ! static::is_enabled() ) { + $url = \get_rest_url( null, ACTIVITYPUB_REST_NAMESPACE . '/stats/image/' . $user_id . '/' . $year ); + + /** + * Filters the stats image URL. + * + * Can be used to route through a CDN or image proxy like Photon. + * + * @since 8.1.0 + * + * @param string $url The image URL. + * @param int $user_id The user ID. + * @param int $year The year. + */ + return \apply_filters( 'activitypub_stats_image_url', $url, $user_id, $year ); + } + + $hash = self::get_hash( $user_id, $year ); + $paths = static::get_storage_paths( $user_id ); + + // Check for cached file using the base class glob pattern. + $pattern = static::escape_glob_pattern( $paths['basedir'] . '/stats-' . $year . '-' . $hash ) . '.*'; + $matches = \glob( $pattern ); + + if ( ! empty( $matches ) && \is_file( $matches[0] ) ) { + $url = $paths['baseurl'] . '/' . \basename( $matches[0] ); + + /** This filter is documented in includes/cache/class-stats-image.php */ + return \apply_filters( 'activitypub_stats_image_url', $url, $user_id, $year ); + } + + // Generate the image. + $result = self::generate( $user_id, $year ); + + if ( \is_wp_error( $result ) ) { + return $result; + } + + $url = $paths['baseurl'] . '/' . \basename( $result ); + + /** This filter is documented in includes/cache/class-stats-image.php */ + return \apply_filters( 'activitypub_stats_image_url', $url, $user_id, $year ); + } + + /** + * Serve a stats image, generating it if needed. + * + * Outputs headers and image data, then exits. + * + * @param int $user_id The user ID. + * @param int $year The year. + * + * @return \WP_Error|void Error on failure, exits on success. + */ + public static function serve( $user_id, $year ) { + if ( ! self::is_available() ) { + return new \WP_Error( 'gd_not_available', \__( 'GD library is not available.', 'activitypub' ), array( 'status' => 501 ) ); + } + + $hash = self::get_hash( $user_id, $year ); + $paths = static::get_storage_paths( $user_id ); + + // Check for cached file. + $pattern = static::escape_glob_pattern( $paths['basedir'] . '/stats-' . $year . '-' . $hash ) . '.*'; + $matches = \glob( $pattern ); + $file = ( ! empty( $matches ) && \is_file( $matches[0] ) ) ? $matches[0] : null; + + if ( ! $file ) { + $file = self::generate( $user_id, $year ); + } + + if ( \is_wp_error( $file ) ) { + return $file; + } + + $mime_type = static::get_file_mime_type( $file ); + + \header( 'Content-Type: ' . ( $mime_type ?: 'image/png' ) ); + \header( 'Content-Length: ' . \filesize( $file ) ); + \header( 'Cache-Control: public, max-age=86400' ); + \header( 'X-Content-Type-Options: nosniff' ); + + \readfile( $file ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_readfile + exit; + } + + /** + * Generate the stats image and save to cache. + * + * @param int $user_id The user ID. + * @param int $year The year. + * + * @return string|\WP_Error Cached file path or error. + */ + public static function generate( $user_id, $year ) { + if ( ! self::is_available() ) { + return new \WP_Error( 'gd_not_available', \__( 'GD library is not available.', 'activitypub' ), array( 'status' => 501 ) ); + } + + $summary = Statistics::get_annual_summary( $user_id, $year ); + + if ( ! $summary ) { + $summary = Statistics::compile_annual_summary( $user_id, $year ); + } + + if ( ! $summary || empty( $summary['posts_count'] ) ) { + return new \WP_Error( 'no_stats', \__( 'No statistics available for this period.', 'activitypub' ), array( 'status' => 404 ) ); + } + + $actor = Actors::get_by_id( $user_id ); + + if ( \is_wp_error( $actor ) ) { + if ( Actors::BLOG_USER_ID === $user_id ) { + $actor = new Blog(); + } elseif ( Actors::APPLICATION_USER_ID === $user_id ) { + $actor = new Application(); + } + } + + $actor_webfinger = ! \is_wp_error( $actor ) ? $actor->get_webfinger() : ''; + $site_name = \get_bloginfo( 'name' ); + + if ( ! \function_exists( 'wp_tempnam' ) ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + } + + $tmp_file = self::render( $summary, $actor_webfinger, $site_name, $year ); + + if ( \is_wp_error( $tmp_file ) ) { + return $tmp_file; + } + + // Use the base class storage paths and optimization. + $paths = static::get_storage_paths( $user_id ); + + if ( ! \wp_mkdir_p( $paths['basedir'] ) ) { + \wp_delete_file( $tmp_file ); + return new \WP_Error( 'cache_dir_failed', \__( 'Failed to create cache directory.', 'activitypub' ), array( 'status' => 500 ) ); + } + + // Remove old cached images for this year before saving the new one. + $old_files = \glob( static::escape_glob_pattern( $paths['basedir'] . '/stats-' . $year . '-' ) . '*.*' ); + if ( $old_files ) { + foreach ( $old_files as $old_file ) { + \wp_delete_file( $old_file ); + } + } + + $hash = self::get_hash( $user_id, $year ); + $dest_name = \sprintf( 'stats-%d-%s.png', $year, $hash ); + $dest_path = $paths['basedir'] . '/' . $dest_name; + + static::get_filesystem()->move( $tmp_file, $dest_path, true ); + + // Keep as PNG for maximum compatibility when sharing on social networks. + return $dest_path; + } + + /** + * Generate a hash for cache invalidation. + * + * Includes the theme stylesheet, version, and stats compilation + * timestamp so cached images are regenerated when the theme or + * the underlying stats data changes. + * + * @param int $user_id The user ID. + * @param int $year The year. + * + * @return string The hash string. + */ + private static function get_hash( $user_id = 0, $year = 0 ) { + $parts = array( + \get_stylesheet(), + \wp_get_theme()->get( 'Version' ), + ); + + if ( $user_id && $year ) { + $summary = Statistics::get_annual_summary( $user_id, $year ); + + if ( $summary && ! empty( $summary['compiled_at'] ) ) { + $parts[] = $summary['compiled_at']; + } + } + + return \md5( \wp_json_encode( $parts ) ); + } + + /** + * Render the stats image as a temporary PNG file. + * + * @param array $summary The annual stats summary. + * @param string $actor_webfinger The actor webfinger identifier. + * @param string $site_name The site name. + * @param int $year The year. + * @return string|\WP_Error Path to temporary PNG file or error. + */ + private static function render( $summary, $actor_webfinger, $site_name, $year ) { + $width = self::WIDTH; + $height = self::HEIGHT; + + $image = \imagecreatetruecolor( $width, $height ); + + if ( ! $image ) { + return new \WP_Error( 'image_create_failed', \__( 'Failed to create image.', 'activitypub' ), array( 'status' => 500 ) ); + } + + \imageantialias( $image, true ); + + $colors = self::resolve_colors(); + $bg = \imagecolorallocate( $image, $colors['bg'][0], $colors['bg'][1], $colors['bg'][2] ); + $fg = \imagecolorallocate( $image, $colors['fg'][0], $colors['fg'][1], $colors['fg'][2] ); + $muted = \imagecolorallocate( $image, $colors['muted'][0], $colors['muted'][1], $colors['muted'][2] ); + + \imagefill( $image, 0, 0, $bg ); + + $font = self::resolve_font(); + + // Total engagement. + $comment_types = Statistics::get_comment_types_for_stats(); + $total_engagement = 0; + foreach ( \array_keys( $comment_types ) as $slug ) { + $total_engagement += $summary[ $slug . '_count' ] ?? 0; + } + + // Title. + $title = \sprintf( + /* translators: %d: The year */ + \__( 'Fediverse Stats %d', 'activitypub' ), + $year + ); + self::draw_text( $image, $title, null, 100, 36, $fg, $font ); + + // Actor webfinger. + if ( $actor_webfinger ) { + self::draw_text( $image, $actor_webfinger, null, 150, 20, $muted, $font ); + } + + // Three big stats in a row. + $stats = array( + array( + 'value' => \number_format_i18n( $summary['posts_count'] ), + 'label' => \__( 'Posts', 'activitypub' ), + ), + array( + 'value' => \number_format_i18n( $total_engagement ), + 'label' => \__( 'Engagements', 'activitypub' ), + ), + array( + 'value' => \number_format_i18n( $summary['followers_end'] ?? 0 ), + 'label' => \__( 'Followers', 'activitypub' ), + ), + ); + + $col_width = (int) ( $width / 3 ); + + foreach ( $stats as $i => $stat ) { + $center_x = (int) ( $col_width * $i + $col_width / 2 ); + self::draw_text( $image, $stat['value'], $center_x, 300, 56, $fg, $font ); + self::draw_text( $image, $stat['label'], $center_x, 355, 18, $muted, $font ); + } + + // Follower growth line. + $followers_net = $summary['followers_net_change'] ?? 0; + $change_sign = $followers_net >= 0 ? '+' : ''; + $growth_text = \sprintf( + /* translators: %s: follower net change */ + \__( '%s followers this year', 'activitypub' ), + $change_sign . \number_format_i18n( $followers_net ) + ); + self::draw_text( $image, $growth_text, null, 450, 20, $muted, $font ); + + // Branding. + $branding = $site_name . ' - ' . \__( 'Powered by ActivityPub', 'activitypub' ); + self::draw_text( $image, $branding, null, $height - 40, 14, $muted, $font ); + + // Save to temp file. + $tmp_file = \wp_tempnam( 'activitypub-stats-' ); + + if ( ! $tmp_file ) { + return new \WP_Error( 'temp_file_failed', \__( 'Could not create temporary file.', 'activitypub' ), array( 'status' => 500 ) ); + } + + $saved = \imagepng( $image, $tmp_file ); + + // imagedestroy() is deprecated since PHP 8.5 and a no-op since 8.0. + if ( \PHP_VERSION_ID < 80000 ) { + \imagedestroy( $image ); + } + + if ( ! $saved ) { + \wp_delete_file( $tmp_file ); + return new \WP_Error( 'image_write_failed', \__( 'Failed to write stats image.', 'activitypub' ), array( 'status' => 500 ) ); + } + + return $tmp_file; + } + + /** + * Draw text on the image, centered on the canvas or at a specific x position. + * + * Uses TrueType rendering when a font is available, falls back to + * GD built-in fonts. + * + * @param resource $image The image resource. + * @param string $text The text to draw. + * @param int|null $x The center x position, or null to center on canvas. + * @param int $y The y position. + * @param int|float $size Font size in points (TTF) or 1-5 (built-in). + * @param int $color The text color. + * @param string|false $font Path to TTF file, or false for built-in. + */ + private static function draw_text( $image, $text, $x, $y, $size, $color, $font = false ) { + if ( $font && \function_exists( 'imagefttext' ) ) { + $bbox = \imageftbbox( $size, 0, $font, $text ); + $text_width = $bbox[2] - $bbox[0]; + $draw_x = null === $x + ? (int) ( ( self::WIDTH - $text_width ) / 2 ) + : (int) ( $x - $text_width / 2 ); + \imagefttext( $image, $size, 0, $draw_x, $y, $color, $font, $text ); + } else { + $builtin_size = \min( 5, \max( 1, (int) ( $size / 10 ) ) ); + $font_width = \imagefontwidth( $builtin_size ); + $text_width = $font_width * \strlen( $text ); + $draw_x = null === $x + ? (int) ( ( self::WIDTH - $text_width ) / 2 ) + : (int) ( $x - $text_width / 2 ); + \imagestring( $image, $builtin_size, $draw_x, $y, $text, $color ); + } + } + + /** + * Resolve colors from theme Global Styles or overrides. + * + * @return array Associative array with 'bg', 'fg', and 'muted' RGB arrays. + */ + private static function resolve_colors() { + $bg_rgb = array( 255, 255, 255 ); + $fg_rgb = array( 17, 17, 17 ); + + $palette = array(); + $settings = \wp_get_global_settings(); + if ( ! empty( $settings['color']['palette'] ) ) { + foreach ( $settings['color']['palette'] as $colors ) { + foreach ( $colors as $color ) { + $palette[ $color['slug'] ] = $color['color']; + } + } + } + + $styles = \wp_get_global_styles( array( 'color' ) ); + $bg_resolved = self::resolve_style_color( $styles['background'] ?? '', $palette ); + $fg_resolved = self::resolve_style_color( $styles['text'] ?? '', $palette ); + + if ( $bg_resolved ) { + $bg_rgb = $bg_resolved; + } + + if ( $fg_resolved ) { + $fg_rgb = $fg_resolved; + } + + if ( ! $bg_resolved || ! $fg_resolved ) { + $bg_slugs = array( 'base', 'background', 'white' ); + $fg_slugs = array( 'contrast', 'foreground', 'black', 'dark-gray' ); + + if ( ! $bg_resolved ) { + foreach ( $bg_slugs as $slug ) { + if ( ! empty( $palette[ $slug ] ) ) { + $parsed = self::parse_hex( $palette[ $slug ] ); + if ( $parsed ) { + $bg_rgb = $parsed; + break; + } + } + } + } + + if ( ! $fg_resolved ) { + foreach ( $fg_slugs as $slug ) { + if ( ! empty( $palette[ $slug ] ) ) { + $parsed = self::parse_hex( $palette[ $slug ] ); + if ( $parsed ) { + $fg_rgb = $parsed; + break; + } + } + } + } + } + + return self::build_color_set( $bg_rgb, $fg_rgb ); + } + + /** + * Build a color set with a derived muted color. + * + * @param array $bg_rgb Background RGB. + * @param array $fg_rgb Foreground RGB. + * + * @return array { bg, fg, muted } RGB arrays. + */ + private static function build_color_set( $bg_rgb, $fg_rgb ) { + return array( + 'bg' => $bg_rgb, + 'fg' => $fg_rgb, + 'muted' => array( + (int) ( ( $fg_rgb[0] + $bg_rgb[0] ) / 2 ), + (int) ( ( $fg_rgb[1] + $bg_rgb[1] ) / 2 ), + (int) ( ( $fg_rgb[2] + $bg_rgb[2] ) / 2 ), + ), + ); + } + + /** + * Resolve a color value from Global Styles. + * + * @param string $value The color value (hex or CSS variable). + * @param array $palette The merged color palette (slug => hex). + * + * @return array|false RGB array or false. + */ + private static function resolve_style_color( $value, $palette ) { + if ( empty( $value ) ) { + return false; + } + + if ( '#' === $value[0] ) { + return self::parse_hex( $value ); + } + + if ( \preg_match( '/--color--([a-z0-9-]+)/', $value, $matches ) ) { + if ( ! empty( $palette[ $matches[1] ] ) ) { + return self::parse_hex( $palette[ $matches[1] ] ); + } + } + + return false; + } + + /** + * Parse a hex color string into an RGB array. + * + * @param string $hex The hex color (e.g. '#FF0000' or '#F00'). + * + * @return array|false Array of [r, g, b] or false on failure. + */ + private static function parse_hex( $hex ) { + $hex = \ltrim( $hex, '#' ); + + if ( 3 === \strlen( $hex ) ) { + $hex = $hex[0] . $hex[0] . $hex[1] . $hex[1] . $hex[2] . $hex[2]; + } + + if ( 6 !== \strlen( $hex ) ) { + return false; + } + + $result = \sscanf( $hex, '%02x%02x%02x' ); + + return ( 3 === \count( $result ) ) ? $result : false; + } + + /** + * Resolve a TTF font file from the active theme or Font Library. + * + * @return string|false Path to a TTF file, or false if none found. + */ + private static function resolve_font() { + $body_slug = ''; + $styles = \wp_get_global_styles( array( 'typography' ) ); + if ( ! empty( $styles['fontFamily'] ) && \preg_match( '/--font-family--([a-z0-9-]+)/', $styles['fontFamily'], $matches ) ) { + $body_slug = $matches[1]; + } + + $settings = \wp_get_global_settings(); + if ( ! empty( $settings['typography']['fontFamilies'] ) ) { + $all_families = array(); + foreach ( $settings['typography']['fontFamilies'] as $families ) { + foreach ( $families as $family ) { + $all_families[] = $family; + } + } + + // Sort so the body font family is tried first. + if ( $body_slug ) { + \usort( + $all_families, + function ( $a, $b ) use ( $body_slug ) { + return ( ( $a['slug'] ?? '' ) === $body_slug ? 0 : 1 ) - ( ( $b['slug'] ?? '' ) === $body_slug ? 0 : 1 ); + } + ); + } + + $font = self::find_ttf_in_families( $all_families ); + if ( $font ) { + return $font; + } + } + + // Try the Font Library (WP 6.5+). + $font = self::find_ttf_in_font_library(); + if ( $font ) { + return $font; + } + + return false; + } + + /** + * Find a TTF/OTF file in font family definitions. + * + * @param array $families The font families to search. + * + * @return string|false Path to TTF file or false. + */ + private static function find_ttf_in_families( $families ) { + $theme_dir = \get_theme_root(); + + foreach ( $families as $family ) { + if ( empty( $family['fontFace'] ) ) { + continue; + } + foreach ( $family['fontFace'] as $face ) { + $src = \is_array( $face['src'] ) ? $face['src'][0] : $face['src']; + + if ( ! \preg_match( '/\.(ttf|otf)$/i', $src ) ) { + continue; + } + + // Resolve theme-relative paths. + if ( 0 === \strpos( $src, 'file:./' ) ) { + $src = \get_theme_file_path( \substr( $src, 7 ) ); + } + + // Only allow fonts within the themes directory for security. + $real_path = \realpath( $src ); + if ( ! $real_path || 0 !== \strpos( $real_path, \realpath( $theme_dir ) ) ) { + continue; + } + + return $real_path; + } + } + + return false; + } + + /** + * Find a TTF/OTF file from the WordPress Font Library. + * + * @return string|false Path to TTF file or false. + */ + private static function find_ttf_in_font_library() { + $font_families = \get_posts( + array( + 'post_type' => 'wp_font_family', + 'posts_per_page' => 10, + 'post_status' => 'publish', + ) + ); + + foreach ( $font_families as $font_family ) { + $faces = \get_posts( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => $font_family->ID, + 'posts_per_page' => 10, + 'post_status' => 'publish', + ) + ); + + foreach ( $faces as $face ) { + $file = \get_post_meta( $face->ID, '_wp_font_face_file', true ); + if ( $file && \preg_match( '/\.(ttf|otf)$/i', $file ) ) { + $path = \path_join( \wp_get_font_dir()['path'], $file ); + if ( \file_exists( $path ) ) { + return $path; + } + } + } + } + + return false; + } +} diff --git a/wp-content/plugins/activitypub/includes/class-activitypub.php b/wp-content/plugins/activitypub/includes/class-activitypub.php index 9299dc09..2583328c 100644 --- a/wp-content/plugins/activitypub/includes/class-activitypub.php +++ b/wp-content/plugins/activitypub/includes/class-activitypub.php @@ -7,11 +7,10 @@ namespace Activitypub; -use Exception; -use Activitypub\Collection\Actors; -use Activitypub\Collection\Outbox; use Activitypub\Collection\Followers; -use Activitypub\Collection\Extra_Fields; +use Activitypub\Collection\Following; +use Activitypub\Collection\Remote_Posts; +use Activitypub\OAuth\Client; /** * ActivityPub Class. @@ -23,62 +22,60 @@ class Activitypub { * Initialize the class, registering WordPress hooks. */ public static function init() { - \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' ) ); - - foreach ( $post_types as $post_type ) { - \add_post_type_support( $post_type, 'activitypub' ); - } + \add_action( 'init', array( self::class, 'theme_compat' ), 11 ); + \add_action( 'init', array( self::class, 'register_user_meta' ), 11 ); \add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 ); \add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 ); - \add_action( 'init', array( self::class, 'add_rewrite_rules' ), 11 ); - \add_action( 'init', array( self::class, 'theme_compat' ), 11 ); - \add_action( 'user_register', array( self::class, 'user_register' ) ); - \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(); + \add_action( 'activitypub_add_user_block', array( Followers::class, 'remove_blocked_actors' ), 10, 3 ); + \add_action( 'activitypub_add_user_block', array( Following::class, 'remove_blocked_actors' ), 10, 3 ); } /** * Activation Hook. + * + * @param bool $network_wide Whether to activate the plugin for all sites in the network or just the current site. */ - public static function activate() { + public static function activate( $network_wide ) { 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 ); + \add_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 5, 3 ); Migration::update_comment_counts(); + + if ( \is_multisite() && $network_wide && ! \wp_is_large_network() ) { + $sites = \get_sites( array( 'fields' => 'ids' ) ); + foreach ( $sites as $site ) { + \switch_to_blog( $site ); + self::flush_rewrite_rules(); + \restore_current_blog(); + } + } } /** * Deactivation Hook. + * + * @param bool $network_wide Whether to deactivate the plugin for all sites in the network or just the current site. */ - public static function deactivate() { + public static function deactivate( $network_wide ) { self::flush_rewrite_rules(); Scheduler::deregister_schedules(); - \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) ); + \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 5 ); Migration::update_comment_counts( 2000 ); + + if ( \is_multisite() && $network_wide && ! \wp_is_large_network() ) { + $sites = \get_sites( array( 'fields' => 'ids' ) ); + foreach ( $sites as $site ) { + \switch_to_blog( $site ); + self::flush_rewrite_rules(); + \restore_current_blog(); + } + } } /** @@ -87,309 +84,14 @@ 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' ) ); + \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 5 ); 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' ); - } + Remote_Posts::delete_all(); + Tombstone::delete_all(); + Client::delete_all(); - /** - * Return a AS2 JSON version of an author, post or page. - * - * @param string $template The path to the template object. - * - * @return string The new path to the JSON template. - */ - 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; - } - - $activitypub_template = false; - $activitypub_object = Query::get_instance()->get_activitypub_object(); - - 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'; - } - } - - /* - * Check if the request is authorized. - * - * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch - * @see https://swicg.github.io/activitypub-http-signature/#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' ); - - // Fallback as template_loader can't return http headers. - return $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; - } - - /** - * Add the 'self' link to the header. - */ - public static function add_headers() { - $id = Query::get_instance()->get_activitypub_object_id(); - - if ( ! $id ) { - return; - } - - if ( ! headers_sent() ) { - \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 ( $id ) { - echo PHP_EOL . '' . 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() { - global $wp_query; - - $comment_id = get_query_var( 'c', null ); - - // Check if it seems to be a comment. - if ( $comment_id ) { - $comment = get_comment( $comment_id ); - - // Load a 404-page if `c` is set but not valid. - if ( ! $comment ) { - $wp_query->set_404(); - return; - } - - // Stop if it's not an ActivityPub comment. - if ( is_activitypub_request() && ! is_local_comment( $comment ) ) { - return; - } - - wp_safe_redirect( get_comment_link( $comment ) ); - 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. - * - * @param array $vars The query variables. - * - * @return array The query variables. - */ - public static function add_query_vars( $vars ) { - $vars[] = 'activitypub'; - $vars[] = 'preview'; - $vars[] = 'author'; - $vars[] = 'actor'; - $vars[] = 'c'; - $vars[] = 'p'; - - return $vars; - } - - /** - * Replaces the default avatar. - * - * @param array $args Arguments passed to get_avatar_data(), after processing. - * @param int|string|object $id_or_email A user ID, email address, or comment object. - * - * @return array $args - */ - public static function pre_get_avatar_data( $args, $id_or_email ) { - if ( - ! $id_or_email instanceof \WP_Comment || - ! isset( $id_or_email->comment_type ) || - $id_or_email->user_id - ) { - return $args; - } - - $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) ); - if ( - ! empty( $id_or_email->comment_type ) && - ! \in_array( - $id_or_email->comment_type, - (array) $allowed_comment_types, - true - ) - ) { - $args['url'] = false; - /** This filter is documented in wp-includes/link-template.php */ - return \apply_filters( 'get_avatar_data', $args, $id_or_email ); - } - - // Check if comment has an avatar. - $avatar = \get_comment_meta( $id_or_email->comment_ID, 'avatar_url', true ); - - if ( $avatar ) { - if ( empty( $args['class'] ) ) { - $args['class'] = array(); - } elseif ( \is_string( $args['class'] ) ) { - $args['class'] = \explode( ' ', $args['class'] ); - } - - $args['url'] = $avatar; - $args['class'][] = 'avatar-activitypub'; - $args['class'][] = 'u-photo'; - $args['class'] = \array_unique( $args['class'] ); - } - - return $args; + Options::delete(); } /** @@ -415,46 +117,25 @@ class Activitypub { \delete_post_meta( $post_id, '_activitypub_canonical_url' ); } - /** - * Add rewrite rules. - */ - public static function add_rewrite_rules() { - /* - * If another system needs to take precedence over the ActivityPub rewrite rules, - * they can define their own and will manually call the appropriate functions as required. - */ - if ( ACTIVITYPUB_DISABLE_REWRITES ) { - return; - } - - if ( ! \class_exists( 'Webfinger' ) ) { - \add_rewrite_rule( - '^.well-known/webfinger', - 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger', - 'top' - ); - } - - 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', - 'top' - ); - } - - \add_rewrite_rule( '^@([\w\-\.]+)\/?$', 'index.php?actor=$matches[1]', 'top' ); - \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES ); - } - /** * Flush rewrite rules. */ public static function flush_rewrite_rules() { - self::add_rewrite_rules(); + Router::add_rewrite_rules(); \flush_rewrite_rules(); } + /** + * Add rewrite rules. + * + * @deprecated 7.5.0 Use {@see Router::add_rewrite_rules()}. + */ + public static function add_rewrite_rules() { + _deprecated_function( __FUNCTION__, '7.5.0', '\Activitypub\Router::add_rewrite_rules()' ); + + Router::add_rewrite_rules(); + } + /** * Theme compatibility stuff. */ @@ -477,235 +158,6 @@ class Activitypub { } } - /** - * Register Custom Post Types. - */ - private static function register_post_types() { - \register_post_type( - Followers::POST_TYPE, - array( - 'labels' => array( - 'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ), - 'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ), - ), - 'public' => false, - 'hierarchical' => false, - 'rewrite' => false, - 'query_var' => false, - 'delete_with_user' => false, - 'can_export' => true, - 'supports' => array(), - ) - ); - - \register_post_meta( - Followers::POST_TYPE, - '_activitypub_inbox', - array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => 'sanitize_url', - ) - ); - - \register_post_meta( - Followers::POST_TYPE, - '_activitypub_errors', - array( - 'type' => 'string', - 'single' => false, - 'sanitize_callback' => function ( $value ) { - if ( ! is_string( $value ) ) { - throw new Exception( 'Error message is no valid string' ); - } - - return esc_sql( $value ); - }, - ) - ); - - \register_post_meta( - Followers::POST_TYPE, - '_activitypub_user_id', - array( - 'type' => 'string', - 'single' => false, - 'sanitize_callback' => function ( $value ) { - return esc_sql( $value ); - }, - ) - ); - - \register_post_meta( - Followers::POST_TYPE, - '_activitypub_actor_json', - array( - 'type' => 'string', - 'single' => true, - 'sanitize_callback' => function ( $value ) { - return sanitize_text_field( $value ); - }, - ) - ); - - // 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( - 'name' => _x( 'Extra fields', 'post_type plural name', 'activitypub' ), - 'singular_name' => _x( 'Extra field', 'post_type single name', 'activitypub' ), - 'add_new' => __( 'Add new', 'activitypub' ), - 'add_new_item' => __( 'Add new extra field', 'activitypub' ), - 'new_item' => __( 'New extra field', 'activitypub' ), - 'edit_item' => __( 'Edit extra field', 'activitypub' ), - 'view_item' => __( 'View extra field', 'activitypub' ), - 'all_items' => __( 'All extra fields', 'activitypub' ), - ), - 'public' => false, - 'hierarchical' => false, - 'query_var' => false, - 'has_archive' => false, - 'publicly_queryable' => false, - 'show_in_menu' => false, - 'delete_with_user' => true, - 'can_export' => true, - 'exclude_from_search' => true, - 'show_in_rest' => true, - 'map_meta_cap' => true, - 'show_ui' => true, - 'supports' => array( 'title', 'editor', 'page-attributes' ), - ); - - \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' ); - } - /** * Add the 'activitypub' capability to users who can publish posts. * @@ -718,33 +170,6 @@ class 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. */ @@ -759,7 +184,20 @@ class Activitypub { 'description' => 'An array of URLs that the user is known by.', 'single' => true, 'default' => array(), - 'sanitize_callback' => array( Sanitize::class, 'url_list' ), + 'sanitize_callback' => array( Sanitize::class, 'identifier_list' ), + ) + ); + + \register_meta( + 'user', + $blog_prefix . 'activitypub_hide_social_graph', + array( + 'type' => 'integer', + 'description' => 'Hide Followers and Followings on Profile.', + 'single' => true, + 'default' => 0, + 'sanitize_callback' => 'absint', + 'show_in_rest' => true, ) ); @@ -788,10 +226,10 @@ class Activitypub { $blog_prefix . 'activitypub_description', array( 'type' => 'string', - 'description' => 'The user’s description.', + 'description' => 'The user description.', 'single' => true, 'default' => '', - 'sanitize_callback' => function ( $value ) { + 'sanitize_callback' => static function ( $value ) { return wp_kses( $value, 'user_description' ); }, ) @@ -802,7 +240,7 @@ class Activitypub { $blog_prefix . 'activitypub_icon', array( 'type' => 'integer', - 'description' => 'The attachment ID for user’s profile image.', + 'description' => 'The attachment ID for user profile image.', 'single' => true, 'default' => 0, 'sanitize_callback' => 'absint', @@ -814,7 +252,7 @@ class Activitypub { $blog_prefix . 'activitypub_header_image', array( 'type' => 'integer', - 'description' => 'The attachment ID for the user’s header image.', + 'description' => 'The attachment ID for the user header image.', 'single' => true, 'default' => 0, 'sanitize_callback' => 'absint', @@ -857,6 +295,29 @@ class Activitypub { ); \add_filter( 'get_user_option_activitypub_mailer_new_mention', array( self::class, 'user_options_default' ) ); + \register_meta( + 'user', + $blog_prefix . 'activitypub_mailer_annual_report', + array( + 'type' => 'integer', + 'description' => 'Send the annual Fediverse Year in Review email.', + 'single' => true, + 'sanitize_callback' => 'absint', + ) + ); + \add_filter( 'get_user_option_activitypub_mailer_annual_report', array( self::class, 'user_options_default' ) ); + + \register_meta( + 'user', + $blog_prefix . 'activitypub_mailer_monthly_report', + array( + 'type' => 'integer', + 'description' => 'Send a monthly Fediverse stats report email.', + 'single' => true, + 'sanitize_callback' => 'absint', + ) + ); + \register_meta( 'user', 'activitypub_show_welcome_tab', @@ -880,6 +341,47 @@ class Activitypub { 'sanitize_callback' => 'absint', ) ); + + // Moderation user meta. + \register_meta( + 'user', + 'activitypub_blocked_actors', + array( + 'type' => 'array', + 'description' => 'User-specific blocked ActivityPub actors.', + 'single' => true, + 'default' => array(), + 'sanitize_callback' => array( Sanitize::class, 'identifier_list' ), + ) + ); + + \register_meta( + 'user', + 'activitypub_blocked_domains', + array( + 'type' => 'array', + 'description' => 'User-specific blocked ActivityPub domains.', + 'single' => true, + 'default' => array(), + 'sanitize_callback' => static function ( $value ) { + return \array_unique( \array_map( array( Sanitize::class, 'host_list' ), $value ) ); + }, + ) + ); + + \register_meta( + 'user', + 'activitypub_blocked_keywords', + array( + 'type' => 'array', + 'description' => 'User-specific blocked ActivityPub keywords.', + 'single' => true, + 'default' => array(), + 'sanitize_callback' => static function ( $value ) { + return \array_map( 'sanitize_text_field', $value ); + }, + ) + ); } /** diff --git a/wp-content/plugins/activitypub/includes/class-attachments.php b/wp-content/plugins/activitypub/includes/class-attachments.php new file mode 100644 index 00000000..0bfe805a --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-attachments.php @@ -0,0 +1,759 @@ +ID, '_source_url', true ) === $source_url ) { + return $attachment->ID; + } + } + + return false; + } + + /** + * Process inline images from post content. + * + * @param int $post_id The post ID. + * @param int $author_id Optional. User ID to set as attachment author. Default 0. + * + * @return array Array of URL mappings (old URL => new URL). + */ + private static function import_inline_images( $post_id, $author_id = 0 ) { + $post = \get_post( $post_id ); + if ( ! $post || empty( $post->post_content ) ) { + return array(); + } + + // Find all img tags in the content. + preg_match_all( '/]+src=["\']([^"\']+)["\'][^>]*>/i', $post->post_content, $matches ); + + if ( empty( $matches[1] ) ) { + return array(); + } + + $url_mappings = array(); + $content = $post->post_content; + + foreach ( $matches[1] as $image_url ) { + // Skip if already processed or is a local URL. + if ( isset( $url_mappings[ $image_url ] ) ) { + continue; + } + + // Check if this image was already processed as an attachment. + $attachment_id = self::get_existing_attachment( $image_url, $post_id ); + if ( ! $attachment_id ) { + $attachment_id = self::save_attachment( array( 'url' => $image_url ), $post_id, $author_id ); + + if ( \is_wp_error( $attachment_id ) ) { + continue; + } + } + + $new_url = \wp_get_attachment_url( $attachment_id ); + if ( $new_url ) { + $url_mappings[ $image_url ] = $new_url; + $content = \str_replace( $image_url, $new_url, $content ); + } + } + + // Update post content if URLs were replaced. + if ( ! empty( $url_mappings ) ) { + \wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => $content, + ) + ); + } + + return $url_mappings; + } + + /** + * Normalize an ActivityPub attachment object to a standard format. + * + * @param mixed $attachment The attachment data (array or object). + * + * @return array|false Normalized attachment data or false on failure. + */ + private static function normalize_attachment( $attachment ) { + // Convert object to array if needed. + if ( \is_object( $attachment ) ) { + $attachment = \get_object_vars( $attachment ); + } + + if ( ! is_array( $attachment ) || empty( $attachment['url'] ) ) { + return false; + } + + return array( + 'url' => $attachment['url'], + 'mediaType' => $attachment['mediaType'] ?? '', + 'name' => $attachment['name'] ?? '', + 'type' => $attachment['type'] ?? 'Document', + ); + } + + /** + * Save an attachment (local file or remote URL) to the media library. + * + * @param array $attachment_data The normalized attachment data. + * @param int $post_id The post ID to attach to. + * @param int $author_id Optional. User ID to set as attachment author. Default 0. + * + * @return int|\WP_Error The attachment ID or WP_Error on failure. + */ + private static function save_attachment( $attachment_data, $post_id, $author_id = 0 ) { + // Ensure required WordPress functions are loaded. + if ( ! \function_exists( 'media_handle_sideload' ) || ! \function_exists( 'download_url' ) ) { + require_once ABSPATH . 'wp-admin/includes/media.php'; + require_once ABSPATH . 'wp-admin/includes/file.php'; + require_once ABSPATH . 'wp-admin/includes/image.php'; + } + + // Use WP_Filesystem_Direct explicitly to avoid FTP fallback from WP_Filesystem(). + require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; + + $filesystem = new \WP_Filesystem_Direct( null ); + + $is_local = ! preg_match( '#^https?://#i', $attachment_data['url'] ); + + if ( $is_local ) { + // Validate local path is within allowed directories to prevent file disclosure. + $allowed = self::is_allowed_local_path( $attachment_data['url'] ); + if ( ! $allowed ) { + return new \WP_Error( 'invalid_path', \__( 'Local file path is not within allowed directories.', 'activitypub' ) ); + } + + // Read local file from disk. + if ( ! $filesystem->exists( $attachment_data['url'] ) ) { + /* translators: %s: file path */ + return new \WP_Error( 'file_not_found', sprintf( \__( 'File not found: %s', 'activitypub' ), $attachment_data['url'] ) ); + } + + // Copy to temp file so media_handle_sideload doesn't move the original. + $tmp_file = \wp_tempnam( \basename( $attachment_data['url'] ) ); + $filesystem->copy( $attachment_data['url'], $tmp_file, true ); + } else { + // Validate remote URL before downloading. + if ( ! \wp_http_validate_url( $attachment_data['url'] ) ) { + return new \WP_Error( 'invalid_url', \__( 'URL is not allowed.', 'activitypub' ) ); + } + + // Download remote URL. + $tmp_file = \download_url( $attachment_data['url'] ); + + if ( \is_wp_error( $tmp_file ) ) { + return $tmp_file; + } + } + + // Get original filename from URL. + $original_name = \basename( \wp_parse_url( $attachment_data['url'], PHP_URL_PATH ) ); + + // Rename temp file to have proper extension for optimize_image to detect mime type. + $original_ext = \pathinfo( $original_name, PATHINFO_EXTENSION ); + if ( $original_ext ) { + $renamed_tmp = $tmp_file . '.' . $original_ext; + if ( $filesystem->move( $tmp_file, $renamed_tmp, true ) ) { + $tmp_file = $renamed_tmp; + } + } + + // Optimize images before sideloading (resize and convert to WebP). + $tmp_file = self::optimize_image( $tmp_file, self::MAX_IMAGE_DIMENSION ); + + // Update filename extension to match optimized file. + $new_ext = \pathinfo( $tmp_file, PATHINFO_EXTENSION ); + if ( $new_ext ) { + $original_name = \preg_replace( '/\.[^.]+$/', '.' . $new_ext, $original_name ); + } + + $file_array = array( + 'name' => $original_name, + 'tmp_name' => $tmp_file, + ); + + // Prepare attachment post data. + // Let WordPress auto-detect the mime type from the file. + $post_data = array( + 'post_title' => $attachment_data['name'] ?? '', + 'post_content' => $attachment_data['name'] ?? '', + 'post_author' => $author_id, + 'meta_input' => array( + '_source_url' => $attachment_data['url'], + ), + ); + + // Add alt text for images. + if ( ! empty( $attachment_data['name'] ) ) { + $original_mime = $attachment_data['mediaType'] ?? ''; + if ( 'image' === strtok( $original_mime, '/' ) ) { + $post_data['meta_input']['_wp_attachment_image_alt'] = $attachment_data['name']; + } + } + + // Sideload the attachment into WordPress. + $attachment_id = \media_handle_sideload( $file_array, $post_id, '', $post_data ); + + // Clean up temp file if there was an error. + if ( \is_wp_error( $attachment_id ) ) { + \wp_delete_file( $tmp_file ); + } + + return $attachment_id; + } + + /** + * Get a unique file path by appending a counter if the file already exists. + * + * @param string $file_path The desired file path. + * + * @return string A unique file path that doesn't exist. + */ + private static function get_unique_path( $file_path ) { + if ( ! \file_exists( $file_path ) ) { + return $file_path; + } + + $path_info = \pathinfo( $file_path ); + $dir = $path_info['dirname']; + $base_name = $path_info['filename']; + $extension = isset( $path_info['extension'] ) ? '.' . $path_info['extension'] : ''; + $counter = 1; + + do { + $new_path = $dir . '/' . $base_name . '-' . $counter . $extension; + ++$counter; + } while ( \file_exists( $new_path ) ); + + return $new_path; + } + + /** + * Check if a local file path is within allowed directories. + * + * Prevents arbitrary file access by restricting local paths to known safe + * directories like the uploads folder or WordPress temp directory. + * + * @param string $file_path The local file path to validate. + * + * @return bool True if the path is allowed, false otherwise. + */ + private static function is_allowed_local_path( $file_path ) { + // Normalize the path and resolve any relative components. + $real_path = \realpath( $file_path ); + if ( false === $real_path ) { + // If file doesn't exist yet, check the directory. + $dir_path = \realpath( \dirname( $file_path ) ); + if ( false === $dir_path ) { + return false; + } + $real_path = $dir_path . '/' . \basename( $file_path ); + } + + // Get allowed base directories. + $upload_dir = \wp_upload_dir(); + $allowed_dirs = array( + \realpath( $upload_dir['basedir'] ), + \realpath( \get_temp_dir() ), + \realpath( ABSPATH . 'wp-content' ), + ); + + /** + * Filters the allowed directories for local file imports. + * + * @since 5.6.0 + * + * @param string[] $allowed_dirs Array of allowed directory paths. + * @param string $file_path The file path being validated. + */ + $allowed_dirs = \apply_filters( 'activitypub_allowed_import_directories', $allowed_dirs, $file_path ); + + // Remove any false values from realpath failures. + $allowed_dirs = \array_filter( $allowed_dirs ); + + // Check if the file is within any allowed directory. + foreach ( $allowed_dirs as $allowed_dir ) { + if ( \str_starts_with( $real_path, $allowed_dir ) ) { + return true; + } + } + + return false; + } + + /** + * Optimize an image file by resizing and converting to WebP. + * + * Uses WordPress image editor to resize large images and convert them + * to WebP format for better compression while maintaining quality. + * + * @param string $file_path Path to the image file. + * @param int $max_dimension Maximum width/height in pixels. + * + * @return string The optimized file path. + */ + private static function optimize_image( $file_path, $max_dimension ) { + // Check if it's an image. + $mime_type = \wp_check_filetype( $file_path )['type'] ?? ''; + if ( ! $mime_type || ! \str_starts_with( $mime_type, 'image/' ) ) { + return $file_path; + } + + // Skip SVG and GIF files (GIFs may be animated). + if ( \in_array( $mime_type, array( 'image/svg+xml', 'image/gif' ), true ) ) { + return $file_path; + } + + $editor = \wp_get_image_editor( $file_path ); + if ( \is_wp_error( $editor ) ) { + return $file_path; + } + + $size = $editor->get_size(); + $needs_resize = $size['width'] > $max_dimension || $size['height'] > $max_dimension; + + // Resize if needed. + if ( $needs_resize ) { + $editor->resize( $max_dimension, $max_dimension, false ); + } + + // Check if WebP is supported. + $can_webp = $editor->supports_mime_type( 'image/webp' ); + + // Determine output format and save. + if ( $can_webp ) { + // Convert to WebP. + $new_path = self::get_unique_path( \preg_replace( '/\.[^.]+$/', '.webp', $file_path ) ); + $result = $editor->save( $new_path, 'image/webp' ); + } elseif ( \in_array( $mime_type, array( 'image/png', 'image/webp' ), true ) ) { + // Keep original format for potentially transparent images when WebP not available. + if ( ! $needs_resize ) { + // No changes needed. + return $file_path; + } + $result = $editor->save( $file_path ); + } else { + // Convert to JPEG when WebP not available. + $new_path = self::get_unique_path( \preg_replace( '/\.[^.]+$/', '.jpg', $file_path ) ); + $result = $editor->save( $new_path, 'image/jpeg' ); + } + + if ( \is_wp_error( $result ) ) { + return $file_path; + } + + // Handle result - $result is always an array from $editor->save(). + $result_path = $result['path'] ?? $file_path; + + // If path changed (format conversion), delete the original file. + if ( $result_path !== $file_path ) { + \wp_delete_file( $file_path ); + } + + return $result_path; + } + + /** + * Append media to post content. + * + * @param int $post_id The post ID. + * @param int[] $attachment_ids Array of attachment IDs. + */ + private static function append_media_to_post_content( $post_id, $attachment_ids ) { + $post = \get_post( $post_id ); + if ( ! $post ) { + return; + } + + $media = self::generate_media_markup( $attachment_ids ); + $separator = empty( trim( $post->post_content ) ) ? '' : "\n\n"; + + \wp_update_post( + array( + 'ID' => $post_id, + 'post_content' => $post->post_content . $separator . $media, + ) + ); + } + + /** + * Generate media markup for attachments. + * + * @param int[] $attachment_ids Array of attachment IDs. + * + * @return string The generated markup. + */ + private static function generate_media_markup( $attachment_ids ) { + if ( empty( $attachment_ids ) ) { + return ''; + } + + /** + * Filters the media markup for ActivityPub attachments. + * + * Allows plugins to provide custom markup for attachments. + * If this filter returns a non-empty string, it will be used instead of + * the default block markup. + * + * @param string $markup The custom markup. Default empty string. + * @param int[] $attachment_ids Array of attachment IDs. + */ + $custom_markup = \apply_filters( 'activitypub_attachments_media_markup', '', $attachment_ids ); + + if ( ! empty( $custom_markup ) ) { + return $custom_markup; + } + + // Default to block markup. + $type = strtok( \get_post_mime_type( $attachment_ids[0] ), '/' ); + + // Single video or audio file. + if ( 1 === \count( $attachment_ids ) && ( 'video' === $type || 'audio' === $type ) ) { + return sprintf( + '
<%1$s controls src="%3$s">
', + \esc_attr( $type ), + \esc_attr( $attachment_ids[0] ), + \esc_url( \wp_get_attachment_url( $attachment_ids[0] ) ) + ); + } + + // Single image: use standalone image block. + if ( 1 === \count( $attachment_ids ) && 'image' === $type ) { + return self::get_image_block( $attachment_ids[0] ); + } + + // Multiple attachments: use gallery block. + return self::get_gallery_block( $attachment_ids ); + } + + /** + * Get standalone image block markup. + * + * @param int $attachment_id The attachment ID. + * + * @return string The image block markup. + */ + private static function get_image_block( $attachment_id ) { + $image_src = \wp_get_attachment_image_src( $attachment_id, 'large' ); + if ( ! $image_src ) { + return ''; + } + + $alt = \get_post_meta( $attachment_id, '_wp_attachment_image_alt', true ); + if ( ! $alt ) { + $alt = \get_post_field( 'post_excerpt', $attachment_id ); + } + + $block = '' . "\n"; + $block .= '
'; + $block .= '' . \esc_attr( $alt ) . ''; + $block .= '
' . "\n"; + $block .= ''; + + return $block; + } + + /** + * Get gallery block markup. + * + * @param int[] $attachment_ids The attachment IDs to use. + * + * @return string The gallery block markup. + */ + private static function get_gallery_block( $attachment_ids ) { + $gallery = '' . "\n"; + $gallery .= '\n"; + $gallery .= ''; + + return $gallery; + } + + /** + * Get content from an object based on its type. + * + * @param int $object_id The object ID (post or comment). + * @param string $object_type The object type ('post' or 'comment'). + * + * @return string The object content. + */ + private static function get_object_content( $object_id, $object_type ) { + if ( 'comment' === $object_type ) { + $comment = \get_comment( $object_id ); + return $comment ? $comment->comment_content : ''; + } + + return \get_post_field( 'post_content', $object_id ); + } + + /** + * Update content for an object based on its type. + * + * @param int $object_id The object ID (post or comment). + * @param string $object_type The object type ('post' or 'comment'). + * @param string $content The new content. + */ + private static function update_object_content( $object_id, $object_type, $content ) { + if ( 'comment' === $object_type ) { + \wp_update_comment( + array( + 'comment_ID' => $object_id, + 'comment_content' => $content, + ) + ); + } else { + \wp_update_post( + array( + 'ID' => $object_id, + 'post_content' => $content, + ) + ); + } + } + + /** + * Append file-based media markup to an object's content. + * + * Used for cached remote media (via Cache classes) that doesn't go through + * the Media Library. Works with posts and comments. + * + * @param int $object_id The object ID (post or comment). + * @param array $files Array of file data arrays with 'url', 'mime_type', and 'alt' keys. + * @param string $object_type The object type ('post' or 'comment'). + */ + public static function append_files_to_content( $object_id, $files, $object_type = 'post' ) { + $content = self::get_object_content( $object_id, $object_type ); + if ( empty( $content ) ) { + return; + } + + $media = self::generate_files_markup( $files ); + $separator = empty( trim( $content ) ) ? '' : "\n\n"; + + self::update_object_content( $object_id, $object_type, $content . $separator . $media ); + } + + /** + * Generate media markup for file-based attachments. + * + * Creates WordPress block markup from file data arrays. Used for cached + * remote media that doesn't have WordPress attachment posts. + * + * @param array[] $files { + * Array of file data arrays. + * + * @type string $url Full URL to the file. + * @type string $mime_type MIME type of the file. + * @type string $alt Alt text for the file. + * } + * + * @return string The generated markup. + */ + public static function generate_files_markup( $files ) { + if ( empty( $files ) ) { + return ''; + } + + /** + * Filters the media markup for ActivityPub file-based attachments. + * + * Allows plugins to provide custom markup for file-based attachments. + * If this filter returns a non-empty string, it will be used instead of + * the default block markup. + * + * @param string $markup The custom markup. Default empty string. + * @param array $files Array of file data arrays. + */ + $custom_markup = \apply_filters( 'activitypub_files_media_markup', '', $files ); + + if ( ! empty( $custom_markup ) ) { + return $custom_markup; + } + + // Default to block markup. + $type = strtok( $files[0]['mime_type'], '/' ); + + // Single video or audio file. + if ( 1 === \count( $files ) && ( 'video' === $type || 'audio' === $type ) ) { + return sprintf( + '
<%1$s controls src="%2$s">
', + \esc_attr( $type ), + \esc_url( $files[0]['url'] ) + ); + } + + // Single image: use standalone image block. + if ( 1 === \count( $files ) && 'image' === $type ) { + return self::get_files_image_block( $files[0] ); + } + + // Multiple attachments: use gallery block. + return self::get_files_gallery_block( $files ); + } + + /** + * Get standalone image block markup for file-based attachments. + * + * @param array $file { + * File data array. + * + * @type string $url Full URL to the file. + * @type string $mime_type MIME type of the file. + * @type string $alt Alt text for the file. + * } + * + * @return string The image block markup. + */ + public static function get_files_image_block( $file ) { + $block = '' . "\n"; + $block .= '
'; + $block .= '' . \esc_attr( $file['alt'] ?? '' ) . ''; + $block .= '
' . "\n"; + $block .= ''; + + return $block; + } + + /** + * Get gallery block markup for file-based attachments. + * + * @param array[] $files { + * Array of file data arrays. + * + * @type string $url Full URL to the file. + * @type string $mime_type MIME type of the file. + * @type string $alt Alt text for the file. + * } + * + * @return string The gallery block markup. + */ + public static function get_files_gallery_block( $files ) { + $gallery = '' . "\n"; + $gallery .= '\n"; + $gallery .= ''; + + return $gallery; + } +} diff --git a/wp-content/plugins/activitypub/includes/class-avatars.php b/wp-content/plugins/activitypub/includes/class-avatars.php new file mode 100644 index 00000000..b755bf94 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-avatars.php @@ -0,0 +1,87 @@ +comment_type ) || + $id_or_email->user_id + ) { + return $args; + } + + /** + * Filter allowed comment types for avatars. + * + * @param array $allowed_comment_types Array of allowed comment types. + */ + $allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) ); + if ( ! \in_array( $id_or_email->comment_type ?: 'comment', $allowed_comment_types, true ) ) { + return $args; + } + + // Respect WordPress "show avatars" setting. + if ( ! \get_option( 'show_avatars' ) ) { + return $args; + } + + $avatar = null; + + // First, try to get avatar from remote actor. + $remote_actor_id = \get_comment_meta( $id_or_email->comment_ID, '_activitypub_remote_actor_id', true ); + if ( $remote_actor_id ) { + $avatar = Remote_Actors::get_avatar_url( $remote_actor_id ); + } + + // Fall back to avatar_url comment meta for backward compatibility. + if ( ! $avatar ) { + $avatar = \get_comment_meta( $id_or_email->comment_ID, 'avatar_url', true ); + } + + if ( $avatar ) { + if ( empty( $args['class'] ) ) { + $args['class'] = array(); + } elseif ( \is_string( $args['class'] ) ) { + $args['class'] = \explode( ' ', $args['class'] ); + } + + /** This filter is documented in wp-includes/link-template.php */ + $args['url'] = \apply_filters( 'get_avatar_url', $avatar, $id_or_email, $args ); + $args['class'][] = 'avatar'; + $args['class'][] = 'avatar-activitypub'; + $args['class'][] = 'avatar-' . (int) $args['size']; + $args['class'][] = 'photo'; + $args['class'][] = 'u-photo'; + $args['class'] = \array_unique( $args['class'] ); + } + + return $args; + } +} diff --git a/wp-content/plugins/activitypub/includes/class-blocklist-subscriptions.php b/wp-content/plugins/activitypub/includes/class-blocklist-subscriptions.php new file mode 100644 index 00000000..0b93da02 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-blocklist-subscriptions.php @@ -0,0 +1,222 @@ + timestamp pairs. + */ + public static function get_all() { + return \get_option( self::OPTION_KEY, array() ); + } + + /** + * Add a subscription. + * + * Only adds the URL to the subscription list. Does not sync. + * Call sync() separately to fetch and import domains. + * + * @param string $url The blocklist URL to subscribe to. + * @return bool True on success, false on failure. + */ + public static function add( $url ) { + $url = \sanitize_url( $url ); + + if ( empty( $url ) || ! \filter_var( $url, FILTER_VALIDATE_URL ) ) { + return false; + } + + $subscriptions = self::get_all(); + + // Not already subscribed. + if ( ! isset( $subscriptions[ $url ] ) ) { + // Add subscription with timestamp 0 (never synced). + $subscriptions[ $url ] = 0; + \update_option( self::OPTION_KEY, $subscriptions ); + } + + return true; + } + + /** + * Remove a subscription. + * + * @param string $url The blocklist URL to unsubscribe from. + * @return bool True on success, false if not found. + */ + public static function remove( $url ) { + $subscriptions = self::get_all(); + + if ( ! isset( $subscriptions[ $url ] ) ) { + return false; + } + + unset( $subscriptions[ $url ] ); + \update_option( self::OPTION_KEY, $subscriptions ); + + return true; + } + + /** + * Sync a single subscription. + * + * Fetches the blocklist URL, parses domains, and adds new ones to the blocklist. + * Updates the subscription timestamp on success. + * + * @param string $url The blocklist URL to sync. + * @return int|false Number of domains added, or false on failure. + */ + public static function sync( $url ) { + $response = \wp_safe_remote_get( + $url, + array( + 'timeout' => 30, + 'redirection' => 5, + ) + ); + + if ( \is_wp_error( $response ) ) { + return false; + } + + $response_code = \wp_remote_retrieve_response_code( $response ); + if ( 200 !== $response_code ) { + return false; + } + + $body = \wp_remote_retrieve_body( $response ); + if ( empty( $body ) ) { + return false; + } + + $domains = self::parse_csv_string( $body ); + + if ( empty( $domains ) ) { + return false; + } + + // Get existing blocks and find new ones. + $existing = Moderation::get_site_blocks()[ Moderation::TYPE_DOMAIN ] ?? array(); + $new_domains = \array_diff( $domains, $existing ); + + if ( ! empty( $new_domains ) ) { + Moderation::add_site_blocks( Moderation::TYPE_DOMAIN, $new_domains ); + } + + // Update timestamp if this is a subscription. + $subscriptions = self::get_all(); + if ( isset( $subscriptions[ $url ] ) ) { + $subscriptions[ $url ] = \time(); + \update_option( self::OPTION_KEY, $subscriptions ); + } + + return \count( $new_domains ); + } + + /** + * Sync all subscriptions. + * + * Called by cron job. + */ + public static function sync_all() { + \array_map( array( __CLASS__, 'sync' ), \array_keys( self::get_all() ) ); + } + + /** + * Parse CSV content from a string and extract domain names. + * + * Supports Mastodon CSV format (with #domain header) and simple + * one-domain-per-line format. + * + * @param string $content CSV content as a string. + * @return array Array of unique, valid domain names. + */ + public static function parse_csv_string( $content ) { + $domains = array(); + + if ( empty( $content ) ) { + return $domains; + } + + // Split into lines. + $lines = \preg_split( '/\r\n|\r|\n/', $content ); + if ( empty( $lines ) ) { + return $domains; + } + + // Parse first line to detect format. + $first_line = \str_getcsv( $lines[0], ',', '"', '\\' ); + $first_cell = \trim( $first_line[0] ?? '' ); + $has_header = \str_starts_with( $first_cell, '#' ) || 'domain' === \strtolower( $first_cell ); + + // Find domain column index. + $domain_index = 0; + if ( $has_header ) { + foreach ( $first_line as $i => $col ) { + $col = \ltrim( \strtolower( \trim( $col ) ), '#' ); + if ( 'domain' === $col ) { + $domain_index = $i; + break; + } + } + // Remove header from lines. + \array_shift( $lines ); + } + + // Process each line. + foreach ( $lines as $line ) { + $row = \str_getcsv( $line, ',', '"', '\\' ); + $domain = \trim( $row[ $domain_index ] ?? '' ); + + // Skip empty lines and comments. + if ( empty( $domain ) || \str_starts_with( $domain, '#' ) ) { + continue; + } + + if ( self::is_valid_domain( $domain ) ) { + $domains[] = \strtolower( $domain ); + } + } + + return \array_unique( $domains ); + } + + /** + * Validate a domain name. + * + * @param string $domain The domain to validate. + * @return bool True if valid, false otherwise. + */ + public static function is_valid_domain( $domain ) { + // Must contain at least one dot (filter_var would accept "localhost"). + if ( ! \str_contains( $domain, '.' ) ) { + return false; + } + + return (bool) \filter_var( $domain, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME ); + } +} diff --git a/wp-content/plugins/activitypub/includes/class-blocks.php b/wp-content/plugins/activitypub/includes/class-blocks.php index 80989af2..b32d768f 100644 --- a/wp-content/plugins/activitypub/includes/class-blocks.php +++ b/wp-content/plugins/activitypub/includes/class-blocks.php @@ -7,91 +7,120 @@ namespace Activitypub; +use Activitypub\Cache\Stats_Image; use Activitypub\Collection\Actors; -use Activitypub\Collection\Followers; /** * Block class. */ class Blocks { + + /** + * HTML tags to skip during block conversion. + * + * @var array + */ + const SKIP_TAGS = array( 'BR', 'CITE', 'SOURCE' ); + + /** + * HTML void elements that have no closing tag. + * + * @var array + */ + const VOID_TAGS = array( 'AREA', 'BASE', 'BR', 'COL', 'EMBED', 'HR', 'IMG', 'INPUT', 'LINK', 'META', 'SOURCE', 'TRACK', 'WBR' ); + + /** + * Map of HTML tag names to WordPress block types. + * + * @var array + */ + const BLOCK_MAP = array( + 'UL' => 'list', + 'OL' => 'list', + 'IMG' => 'image', + 'BLOCKQUOTE' => 'quote', + 'H1' => 'heading', + 'H2' => 'heading', + 'H3' => 'heading', + 'H4' => 'heading', + 'H5' => 'heading', + 'H6' => 'heading', + 'P' => 'paragraph', + 'A' => 'paragraph', + 'ABBR' => 'paragraph', + 'B' => 'paragraph', + 'CODE' => 'paragraph', + 'EM' => 'paragraph', + 'I' => 'paragraph', + 'STRONG' => 'paragraph', + 'SUB' => 'paragraph', + 'SUP' => 'paragraph', + 'SPAN' => 'paragraph', + 'U' => 'paragraph', + 'FIGURE' => 'image', + 'HR' => 'separator', + ); + /** * Initialize the class, registering WordPress hooks. */ public static function init() { // This is already being called on the init hook, so just add it. self::register_blocks(); + self::register_patterns(); + self::register_templates(); + + \add_action( 'pre_get_posts', array( self::class, 'filter_query_loop_vars' ) ); - \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_action( 'rest_api_init', array( self::class, 'register_rest_fields' ) ); \add_filter( 'activitypub_import_mastodon_post_data', array( self::class, 'filter_import_mastodon_post_data' ), 10, 2 ); - } + \add_filter( 'activitypub_attachments', array( self::class, 'add_stats_image_attachment' ), 10, 2 ); - /** - * Register post meta for content warnings. - */ - public static function register_postmeta() { - $ap_post_types = \get_post_types_by_support( 'activitypub' ); - foreach ( $ap_post_types as $post_type ) { - \register_post_meta( - $post_type, - 'activitypub_content_warning', - array( - 'show_in_rest' => true, - 'single' => true, - 'type' => 'string', - '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; - }, - ) - ); - } + \add_action( 'activitypub_before_get_content', array( self::class, 'add_post_transformation_callbacks' ) ); + \add_filter( 'activitypub_the_content', array( self::class, 'remove_post_transformation_callbacks' ) ); } /** * Enqueue the block editor assets. */ public static function enqueue_editor_assets() { + $data = array( + 'namespace' => ACTIVITYPUB_REST_NAMESPACE, + 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + 'enabled' => array( + 'blog' => ! is_user_type_disabled( 'blog' ), + 'users' => ! is_user_type_disabled( 'user' ), + ), + 'profileUrls' => array( + 'user' => \admin_url( 'profile.php#activitypub' ), + 'blog' => \admin_url( 'options-general.php?page=activitypub&tab=blog-profile' ), + ), + 'showAvatars' => (bool) \get_option( 'show_avatars' ), + 'defaultQuotePolicy' => \get_option( 'activitypub_default_quote_policy', ACTIVITYPUB_INTERACTION_POLICY_ANYONE ), + 'objectType' => \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ), + 'noteLength' => ACTIVITYPUB_NOTE_LENGTH, + 'statsImageUrlEndpoint' => Stats_Image::is_available() ? \get_rest_url( null, ACTIVITYPUB_REST_NAMESPACE . '/stats/image-url/{user_id}/{year}' ) : '', + ); + wp_localize_script( 'wp-editor', '_activityPubOptions', $data ); + // Check for our supported post types. $current_screen = \get_current_screen(); $ap_post_types = \get_post_types_by_support( 'activitypub' ); if ( ! $current_screen || ! in_array( $current_screen->post_type, $ap_post_types, true ) ) { return; } + $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/editor-plugin/plugin.asset.php'; $plugin_url = plugins_url( 'build/editor-plugin/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); wp_enqueue_script( 'activitypub-block-editor', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); + + $asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/pre-publish-panel/plugin.asset.php'; + $plugin_url = plugins_url( 'build/pre-publish-panel/plugin.js', ACTIVITYPUB_PLUGIN_FILE ); + wp_enqueue_script( 'activitypub-pre-publish-panel', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); } /** @@ -109,41 +138,27 @@ class Blocks { wp_enqueue_script( 'activitypub-reply-intent', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true ); } - /** - * Output ActivityPub options as a script tag. - */ - 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' ), - ), - ); - - printf( - "\n", - wp_json_encode( $data ) - ); - } - /** * Register the blocks. */ public static function register_blocks() { - \register_block_type_from_metadata( - ACTIVITYPUB_PLUGIN_DIR . '/build/followers', - array( - 'render_callback' => array( self::class, 'render_follower_block' ), - ) - ); - \register_block_type_from_metadata( - ACTIVITYPUB_PLUGIN_DIR . '/build/follow-me', - array( - 'render_callback' => array( self::class, 'render_follow_me_block' ), - ) - ); + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/extra-fields' ); + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/follow-me' ); + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/followers' ); + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/posts-and-replies' ); + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/stats' ); + + // Only register the Following block if the Following feature is enabled. + if ( '1' === \get_option( 'activitypub_following_ui', '0' ) ) { + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/following' ); + } + // Register reactions block, conditionally removing facepile style if avatars are disabled. + $reactions_args = array(); + if ( ! \get_option( 'show_avatars', true ) ) { + $reactions_args['styles'] = array(); + } + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/reactions', $reactions_args ); + \register_block_type_from_metadata( ACTIVITYPUB_PLUGIN_DIR . '/build/reply', array( @@ -151,52 +166,166 @@ class Blocks { ) ); - \register_block_type_from_metadata( - ACTIVITYPUB_PLUGIN_DIR . '/build/reactions', + // Register remote media blocks (server-side only, no editor UI). + \register_block_type( + 'activitypub/emoji', array( - 'render_callback' => array( self::class, 'render_post_reactions_block' ), + 'attributes' => array( + 'url' => array( 'type' => 'string' ), + 'updated' => array( 'type' => 'string' ), + ), + 'render_callback' => array( self::class, 'render_emoji_block' ), + ) + ); + + \register_block_type( + 'activitypub/image', + array( + 'attributes' => array( + 'url' => array( 'type' => 'string' ), + ), + 'render_callback' => array( self::class, 'render_image_block' ), + ) + ); + + \register_block_type( + 'activitypub/audio', + array( + 'attributes' => array( + 'url' => array( 'type' => 'string' ), + ), + 'render_callback' => array( self::class, 'render_audio_block' ), + ) + ); + + \register_block_type( + 'activitypub/video', + array( + 'attributes' => array( + 'url' => array( 'type' => 'string' ), + ), + 'render_callback' => array( self::class, 'render_video_block' ), ) ); } /** - * Render the post reactions block. - * - * @param array $attrs The block attributes. - * - * @return string The HTML to render. + * Register block patterns for ActivityPub. */ - public static function render_post_reactions_block( $attrs ) { - if ( ! isset( $attrs['postId'] ) ) { - $attrs['postId'] = get_the_ID(); - } - - $wrapper_attributes = get_block_wrapper_attributes( + public static function register_patterns() { + // Register the ActivityPub pattern category. + \register_block_pattern_category( + 'activitypub', array( - 'class' => 'activitypub-reactions-block', - 'data-attrs' => wp_json_encode( $attrs ), + 'label' => \__( 'Fediverse', 'activitypub' ), ) ); - return sprintf( - '
', - $wrapper_attributes + // Register each pattern. + require ACTIVITYPUB_PLUGIN_DIR . '/patterns/author-header.php'; + require ACTIVITYPUB_PLUGIN_DIR . '/patterns/author-profile.php'; + require ACTIVITYPUB_PLUGIN_DIR . '/patterns/follow-page.php'; + require ACTIVITYPUB_PLUGIN_DIR . '/patterns/profile-page.php'; + require ACTIVITYPUB_PLUGIN_DIR . '/patterns/social-sidebar.php'; + + // Only register the Following page pattern if the Following feature is enabled. + if ( '1' === \get_option( 'activitypub_following_ui', '0' ) ) { + require ACTIVITYPUB_PLUGIN_DIR . '/patterns/following-page.php'; + } + + // Only register the Stats post starter pattern in December and January. + $month = (int) \gmdate( 'n' ); + if ( 12 === $month || 1 === $month ) { + require ACTIVITYPUB_PLUGIN_DIR . '/patterns/stats-post.php'; + } + } + + /** + * Register FSE templates for block themes. + */ + public static function register_templates() { + // Only register templates for block themes on WP 6.7+. + if ( ! \function_exists( 'register_block_template' ) || ! \wp_is_block_theme() ) { + return; + } + + // Use the core `author` hierarchy slug so WP can resolve this for author archives. + \register_block_template( + 'activitypub//author', + array( + 'title' => \__( 'Author Archive (Fediverse)', 'activitypub' ), + 'description' => \__( 'Displays an author archive with Fediverse profile and follow options.', 'activitypub' ), + 'content' => ' + +
+ + + + + + +
+ + + + + + + + + +
+ +
+ +', + 'post_types' => array(), + ) + ); + } + + /** + * Register REST fields needed for blocks. + */ + public static function register_rest_fields() { + // Register the post_count field for Follow Me block. + register_rest_field( + 'user', + 'post_count', + array( + /** + * Get the number of published posts. + * + * @param array $response Prepared response array. + * @param string $field_name The field name. + * @param \WP_REST_Request $request The request object. + * @return int The number of published posts. + */ + 'get_callback' => static function ( $response, $field_name, $request ) { + return (int) count_user_posts( $request->get_param( 'id' ), 'post', true ); + }, + 'schema' => array( + 'description' => 'Number of published posts', + 'type' => 'integer', + 'context' => array( 'activitypub' ), + ), + ) ); } /** * Get the user ID from a user string. * - * @param string $user_string The user string. Can be a user ID, 'site', or 'inherit'. + * @param string $user_string The user string. Can be a user ID, 'blog', or 'inherit'. * @return int|null The user ID, or null if the 'inherit' string is not supported in this context. */ - private static function get_user_id( $user_string ) { + public static function get_user_id( $user_string ) { if ( is_numeric( $user_string ) ) { return absint( $user_string ); } - // If the user string is 'site', return the Blog User ID. - if ( 'site' === $user_string ) { + // If the user string is 'blog', return the Blog User ID. + if ( 'blog' === $user_string ) { return Actors::BLOG_USER_ID; } @@ -237,100 +366,295 @@ class Blocks { } /** - * Filter an array by a list of keys. + * Render an actor list block (followers or following). * - * @param array $data The array to filter. - * @param array $keys The keys to keep. - * @return array The filtered array. - */ - protected static function filter_array_by_keys( $data, $keys ) { - return array_intersect_key( $data, array_flip( $keys ) ); - } - - /** - * Render the follow me block. + * @param string $endpoint The endpoint type ('followers' or 'following'). + * @param array $attributes Block attributes. + * @param \WP_Block $block Block instance. + * @param string $content Block content. * - * @param array $attrs The block attributes. - * @return string The HTML to render. + * @return string|void The HTML to render, or void to render nothing. */ - public static function render_follow_me_block( $attrs ) { - $user_id = self::get_user_id( $attrs['selectedUser'] ); - $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. - return ''; - } else { - // If the user is a specific ID and we couldn't find it, render an error message. - return ''; - } + public static function render_actor_list_block( $endpoint, $attributes, $block, $content ) { + if ( is_activitypub_request() || \is_feed() ) { + return ''; } - $attrs['profileData'] = self::filter_array_by_keys( - $user->to_array(), - array( 'icon', 'name', 'webfinger' ) - ); + $attributes = \wp_parse_args( $attributes ); + $block_name = 'followers' === $endpoint ? __( 'Followers', 'activitypub' ) : __( 'Following', 'activitypub' ); - $wrapper_attributes = get_block_wrapper_attributes( - array( - 'class' => 'activitypub-follow-me-block-wrapper', - 'data-attrs' => wp_json_encode( $attrs ), - ) - ); - // todo: render more than an empty div? - return '
'; - } - - /** - * Render the follower block. - * - * @param array $attrs The block attributes. - * - * @return string The HTML to render. - */ - public static function render_follower_block( $attrs ) { - $followee_user_id = self::get_user_id( $attrs['selectedUser'] ); - if ( is_null( $followee_user_id ) ) { - return ''; + if ( empty( $content ) ) { + // Fallback for v1.0.0 blocks. + /* translators: %s: Block type (Followers or Following) */ + $_title = $attributes['title'] ?? \sprintf( __( 'Fediverse %s', 'activitypub' ), $block_name ); + $content = '

' . \esc_html( $_title ) . '

'; + unset( $attributes['title'], $attributes['className'] ); + } else { + $content = \implode( PHP_EOL, \wp_list_pluck( $block->parsed_block['innerBlocks'], 'innerHTML' ) ); } - $user = Actors::get_by_id( $followee_user_id ); - if ( is_wp_error( $user ) ) { - return ''; + $user_id = self::get_user_id( $attributes['selectedUser'] ); + if ( \is_null( $user_id ) ) { + /* translators: %s: Block type (Followers or Following) */ + return \sprintf( '', $block_name ); } - $per_page = absint( $attrs['per_page'] ); - $follower_data = Followers::get_followers_with_count( $followee_user_id, $per_page ); + $user = Actors::get_by_id( $user_id ); + if ( \is_wp_error( $user ) ) { + /* translators: 1: Block type (Followers or Following), 2: User ID */ + return \sprintf( '', $block_name, $user_id ); + } - $attrs['followerData']['total'] = $follower_data['total']; - $attrs['followerData']['followers'] = array_map( - function ( $follower ) { - return self::filter_array_by_keys( - $follower->to_array(), - array( 'icon', 'name', 'preferredUsername', 'url' ) + if ( ! Actors::show_social_graph( $user_id ) ) { + /* translators: %s: Block type (Followers or Following) */ + return \sprintf( '', $block_name ); + } + + $_per_page = \max( 1, \absint( $attributes['per_page'] ) ); + $_show_avatars = (bool) \get_option( 'show_avatars' ); + + // Query the appropriate collection. + if ( 'followers' === $endpoint ) { + $data = \Activitypub\Collection\Followers::query( $user_id, $_per_page ); + $items = $data['followers']; + } else { + $data = \Activitypub\Collection\Following::query( $user_id, $_per_page ); + $items = $data['following']; + } + + // Prepare items data for the Interactivity API context. + $prepared_items = \array_map( + static function ( $item ) { + $actor = \Activitypub\Collection\Remote_Actors::get_actor( $item ); + + // Restrict URLs to http/https schemes to prevent XSS via javascript: URIs. + $url = object_to_uri( $actor->get_url() ) ?: $actor->get_id(); + + return array( + 'handle' => '@' . $actor->get_webfinger(), + 'icon' => $actor->get_icon(), + 'name' => $actor->get_name() ?: $actor->get_preferred_username(), + 'url' => \esc_url( $url, array( 'http', 'https' ) ), ); }, - $follower_data['followers'] + $items ); - $wrapper_attributes = get_block_wrapper_attributes( + + $store_name = 'activitypub/' . $endpoint; + + // Set up the Interactivity API config. + \wp_interactivity_config( + $store_name, array( - 'aria-label' => __( 'Fediverse Followers', 'activitypub' ), - 'class' => 'activitypub-follower-block', - 'data-attrs' => wp_json_encode( $attrs ), + 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + 'namespace' => ACTIVITYPUB_REST_NAMESPACE, ) ); - $html = '
'; - if ( $attrs['title'] ) { - $html .= '

' . esc_html( $attrs['title'] ) . '

'; + // Set initial context data. + $context = array( + 'items' => $prepared_items, + 'isLoading' => false, + 'order' => $attributes['order'], + 'page' => 1, + 'pages' => \ceil( $data['total'] / $_per_page ), + 'perPage' => $_per_page, + 'total' => $data['total'], + 'userId' => $user_id, + 'endpoint' => $endpoint, + ); + + // Get block wrapper attributes with the data-wp-interactive attribute. + $wrapper_attributes = \get_block_wrapper_attributes( + array( + 'id' => \wp_unique_id( 'activitypub-' . $endpoint . '-block-' ), + 'data-wp-interactive' => $store_name, + 'data-wp-context' => \wp_json_encode( $context, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP ), + ) + ); + + /* translators: %s: Block type (Followers or Following) */ + $nav_label = \sprintf( __( '%s navigation', 'activitypub' ), $block_name ); + + \ob_start(); + ?> +
> + + + $_show_avatars, + 'total' => $data['total'], + 'per_page' => $_per_page, + 'nav_label' => $nav_label, + ) + ); + ?> +
+ '; - foreach ( $follower_data['followers'] as $follower ) { - $html .= '
  • ' . self::render_follower( $follower ) . '
  • '; + + $url = $attrs['url']; + $shortcode = trim( $content ); + $name = trim( $shortcode, ':' ); + + /** + * Filters a remote media URL for caching. + * + * @param string $url The remote media URL. + * @param string $context The context ('emoji'). + * @param int|null $entity_id The entity ID. + * @param array $options Additional options. + */ + $cached_url = \apply_filters( + 'activitypub_remote_media_url', + $url, + 'emoji', + null, + array( 'updated' => $attrs['updated'] ?? null ) + ); + + return Emoji::get_img_tag( $cached_url ?: $url, $name ); + } + + /** + * Render the image block. + * + * Replaces remote image URL with cached URL at runtime. + * + * @param array $attrs The block attributes. + * @param string $content The block inner content (img tag). + * + * @return string The rendered content with cached URL. + */ + public static function render_image_block( $attrs, $content ) { + if ( empty( $attrs['url'] ) || empty( $content ) ) { + return $content; } - // We are only pagination on the JS side. Could be revisited but we gotta ship! - $html .= '
    '; - return $html; + + $url = $attrs['url']; + + // Get entity ID from context. + $entity_id = null; + $post = \get_post(); + if ( $post ) { + $entity_id = $post->ID; + } + + /** + * Filters a remote image URL for caching. + * + * @param string $url The remote image URL. + * @param string $context The context ('media'). + * @param int|null $entity_id The entity ID. + * @param array $options Additional options. + */ + $cached_url = \apply_filters( 'activitypub_remote_media_url', $url, 'media', $entity_id, array() ); + + if ( $cached_url && $cached_url !== $url ) { + return \str_replace( $url, $cached_url, $content ); + } + + return $content; + } + + /** + * Render the audio block. + * + * Replaces remote audio URL with cached URL at runtime. + * + * @param array $attrs The block attributes. + * @param string $content The block inner content (audio tag). + * + * @return string The rendered content with cached URL. + */ + public static function render_audio_block( $attrs, $content ) { + if ( empty( $attrs['url'] ) || empty( $content ) ) { + return $content; + } + + $url = $attrs['url']; + + // Get entity ID from context. + $entity_id = null; + $post = \get_post(); + if ( $post ) { + $entity_id = $post->ID; + } + + /** + * Filters a remote audio URL for caching. + * + * @param string $url The remote audio URL. + * @param string $context The context ('audio'). + * @param int|null $entity_id The entity ID. + * @param array $options Additional options. + */ + $cached_url = \apply_filters( 'activitypub_remote_media_url', $url, 'audio', $entity_id, array() ); + + if ( $cached_url && $cached_url !== $url ) { + return \str_replace( $url, $cached_url, $content ); + } + + return $content; + } + + /** + * Render the video block. + * + * Replaces remote video URL with cached URL at runtime. + * + * @param array $attrs The block attributes. + * @param string $content The block inner content (video tag). + * + * @return string The rendered content with cached URL. + */ + public static function render_video_block( $attrs, $content ) { + if ( empty( $attrs['url'] ) || empty( $content ) ) { + return $content; + } + + $url = $attrs['url']; + + // Get entity ID from context. + $entity_id = null; + $post = \get_post(); + if ( $post ) { + $entity_id = $post->ID; + } + + /** + * Filters a remote video URL for caching. + * + * @param string $url The remote video URL. + * @param string $context The context ('video'). + * @param int|null $entity_id The entity ID. + * @param array $options Additional options. + */ + $cached_url = \apply_filters( 'activitypub_remote_media_url', $url, 'video', $entity_id, array() ); + + if ( $cached_url && $cached_url !== $url ) { + return \str_replace( $url, $cached_url, $content ); + } + + return $content; } /** @@ -341,6 +665,10 @@ class Blocks { * @return string The HTML to render. */ public static function render_reply_block( $attrs ) { + if ( is_activitypub_request() ) { + $attrs['embedPost'] = false; + } + // Return early if no URL is provided. if ( empty( $attrs['url'] ) ) { return null; @@ -359,15 +687,19 @@ class Blocks { $html = '
    '; // Try to get and append the embed if requested. + $embed = null; if ( $show_embed ) { - $embed = wp_oembed_get( $attrs['url'] ); + // Use the theme's content width or a reasonable default to avoid narrow embeds. + $embed_width = ! empty( $GLOBALS['content_width'] ) ? $GLOBALS['content_width'] : 600; + $embed = wp_oembed_get( $attrs['url'], array( 'width' => $embed_width ) ); if ( $embed ) { $html .= $embed; + \wp_enqueue_script( 'wp-embed' ); } } - // Only show the link if we're not showing the embed. - if ( ! $show_embed ) { + // Show the link if embed is not requested or if embed failed. + if ( ! $show_embed || ! $embed ) { $html .= sprintf( '

    %3$s

    ', esc_url( $attrs['url'] ), @@ -383,43 +715,195 @@ class Blocks { } /** - * Render a follower. + * Renders a modal component that can be used by different blocks. * - * @param \Activitypub\Model\Follower $follower The follower to render. + * @param array $args { + * Arguments for the modal. * - * @return string The HTML to render. + * @type string $content The modal content HTML. + * @type string $id Optional ID prefix for the modal elements. + * @type bool $is_compact Whether the modal is compact (popover-style). Default false. + * @type string $title Static title text for the modal header. + * @type string $title_binding Optional Interactivity API binding for a dynamic title + * (e.g. 'context.modal.title'). When set, uses data-wp-text + * on the title element and enables dynamic compact toggling. + * } */ - public static function render_follower( $follower ) { - $external_svg = ''; - $template = - ' - - - %s - / - @%s - - %s - '; - - $data = $follower->to_array(); - - return sprintf( - $template, - esc_url( object_to_uri( $data['url'] ) ), - esc_attr( $data['name'] ), - esc_attr( $data['icon']['url'] ), - esc_html( $data['name'] ), - esc_html( $data['preferredUsername'] ), - $external_svg + public static function render_modal( $args = array() ) { + $defaults = array( + 'content' => '', + 'id' => '', + 'is_compact' => false, + 'title' => '', + 'title_binding' => '', ); + + $args = \wp_parse_args( $args, $defaults ); + ?> + +
    + data-wp-class--compact="context.modal.isCompact" + + role="dialog" + aria-modal="true" + hidden + > +
    + +
    +

    + id="" + + + data-wp-text="" + + >

    + +
    + +
    + +
    +
    +
    + ` element that explains decentralized + * interactions to users unfamiliar with the Fediverse. + * + * @since 8.0.0 + */ + public static function render_modal_help() { + ?> +
    + +

    + +

    +

    + +

    +
    + true, + 'show_pagination' => true, + 'total' => 0, + 'per_page' => 10, + 'nav_label' => __( 'Actor navigation', 'activitypub' ), + ); + + $args = \wp_parse_args( $args, $defaults ); + + // Sanitize numeric values, ensuring per_page is at least 1 to avoid division by zero. + $args['total'] = \absint( $args['total'] ); + $args['per_page'] = \max( 1, \absint( $args['per_page'] ) ); + ?> + +
    +
      + +
    + + $args['per_page'] ) : ?> + + +
    +
    +
    + +
    + .*?

    #is', $data['post_content'], $matches ); $blocks = \array_map( - function ( $paragraph ) { + static function ( $paragraph ) { return '' . PHP_EOL . $paragraph . PHP_EOL . '' . PHP_EOL; }, $matches[0] ?? array() @@ -436,11 +920,410 @@ class Blocks { $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( '' . PHP_EOL, \esc_url( $post->object->inReplyTo ) ); + if ( ! empty( $post['object']['inReplyTo'] ) ) { + $reply_block = \sprintf( '' . PHP_EOL, \esc_url( $post['object']['inReplyTo'] ) ); $data['post_content'] = $reply_block . $data['post_content']; } return $data; } + + /** + * Add Interactivity directions to the specified element. + * + * @param string $content The block content. + * @param string[] $selector The selector for the element to add directions to. + * @param string[] $attributes The attributes to add to the element. + * + * @return string The updated content. + */ + public static function add_directions( $content, $selector, $attributes ) { + $tags = new \WP_HTML_Tag_Processor( $content ); + + while ( $tags->next_tag( $selector ) ) { + foreach ( $attributes as $key => $value ) { + if ( 'class' === $key ) { + $tags->add_class( $value ); + continue; + } + + $tags->set_attribute( $key, $value ); + } + } + + return $tags->get_updated_html(); + } + + /** + * Add post transformation callbacks. + * + * @param object $post The post object. + */ + public static function add_post_transformation_callbacks( $post ) { + \add_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ), 10, 2 ); + \add_filter( 'render_block_activitypub/stats', '__return_empty_string' ); + + // Only transform reply link if it's the first block in the post. + $blocks = \parse_blocks( $post->post_content ); + if ( ! empty( $blocks ) && 'activitypub/reply' === $blocks[0]['blockName'] ) { + \add_filter( 'render_block_activitypub/reply', array( self::class, 'generate_reply_link' ), 10, 2 ); + } + } + + /** + * Remove post transformation callbacks. + * + * @param string $content The post content. + * + * @return string The updated content. + */ + public static function remove_post_transformation_callbacks( $content ) { + \remove_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ) ); + \remove_filter( 'render_block_activitypub/reply', array( self::class, 'generate_reply_link' ) ); + \remove_filter( 'render_block_activitypub/stats', '__return_empty_string' ); + + return $content; + } + + /** + * Generate HTML @ link for reply block. + * + * @param string $block_content The block content. + * @param array $block The block data. + * + * @return string The HTML @ link. + */ + public static function generate_reply_link( $block_content, $block ) { + // Unhook ourselves after first execution to ensure only the first reply block gets transformed. + \remove_filter( 'render_block_activitypub/reply', array( self::class, 'generate_reply_link' ) ); + + // Return empty string if no URL is provided. + if ( empty( $block['attrs']['url'] ) ) { + return ''; + } + + $url = $block['attrs']['url']; + + // Try to get ActivityPub representation. Is likely already cached. + $object = Http::get_remote_object( $url ); + if ( \is_wp_error( $object ) ) { + return ''; + } + + $author_url = $object['attributedTo'] ?? ''; + if ( ! $author_url ) { + return ''; + } + + // Fetch author information. + $author = Http::get_remote_object( $author_url ); + if ( \is_wp_error( $author ) ) { + return ''; + } + + // Get webfinger identifier. + $webfinger = ''; + if ( ! empty( $author['webfinger'] ) ) { + $webfinger = \str_replace( 'acct:', '', $author['webfinger'] ); + } elseif ( ! empty( $author['preferredUsername'] ) && ! empty( $author['url'] ) ) { + // Construct webfinger-style identifier from username and domain. + $domain = \wp_parse_url( $author['url'], PHP_URL_HOST ); + $webfinger = '@' . $author['preferredUsername'] . '@' . $domain; + } + + if ( ! $webfinger ) { + return ''; + } + + // Generate HTML @ link. + return \sprintf( + '

    %3$s

    ', + \esc_url( $url ), + \esc_attr( $webfinger ), + \esc_html( '@' . strtok( $webfinger, '@' ) ) + ); + } + + /** + * Add the stats image as an attachment when a post contains the stats block. + * + * Parses the post content for activitypub/stats blocks and appends each + * as an Image attachment to the ActivityPub object. + * + * @since 8.1.0 + * + * @param array $attachments The existing attachments. + * @param \WP_Post $post The post object. + * + * @return array The attachments with stats images appended. + */ + public static function add_stats_image_attachment( $attachments, $post ) { + if ( ! Stats_Image::is_available() ) { + return $attachments; + } + + /* + * The stats image intentionally bypasses the `activitypub_max_image_attachments` + * limit because it replaces the block content rather than being an inline image + * extracted from the post. It is always appended so that the share-pic is + * included in the federated activity regardless of the attachment cap. + */ + $blocks = \parse_blocks( $post->post_content ); + $stats_blocks = self::find_blocks_recursive( $blocks, 'activitypub/stats' ); + + foreach ( $stats_blocks as $block ) { + $user_id = self::get_user_id( $block['attrs']['selectedUser'] ?? 'blog' ); + + if ( null === $user_id ) { + continue; + } + + $year = (int) ( $block['attrs']['year'] ?? (int) \gmdate( 'Y' ) - 1 ); + $url = Stats_Image::get_url( $user_id, $year ); + + if ( \is_wp_error( $url ) ) { + continue; + } + + // Determine mime type from URL extension. + $mime_type = \str_ends_with( $url, '.webp' ) ? 'image/webp' : 'image/png'; + + $attachments[] = array( + 'type' => 'Image', + 'mediaType' => $mime_type, + 'url' => $url, + 'name' => \sprintf( + /* translators: %d: The year */ + \__( 'Fediverse Stats %d', 'activitypub' ), + $year + ), + ); + } + + return $attachments; + } + + /** + * Recursively find blocks of a given type in a block tree. + * + * @since 8.1.0 + * + * @param array $blocks The parsed blocks. + * @param string $block_name The block name to search for. + * + * @return array The matching blocks. + */ + private static function find_blocks_recursive( $blocks, $block_name ) { + $found = array(); + + foreach ( $blocks as $block ) { + if ( $block_name === $block['blockName'] ) { + $found[] = $block; + } + + if ( ! empty( $block['innerBlocks'] ) ) { + $found = \array_merge( $found, self::find_blocks_recursive( $block['innerBlocks'], $block_name ) ); + } + } + + return $found; + } + + /** + * Transform Embed blocks to block level link. + * + * Remote servers will simply drop iframe elements, rendering incomplete content. + * + * @see https://www.w3.org/TR/activitypub/#security-sanitizing-content + * @see https://www.w3.org/wiki/ActivityPub/Primer/HTML + * + * @param string $block_content The block content (html). + * @param object $block The block object. + * + * @return string A block level link + */ + public static function revert_embed_links( $block_content, $block ) { + if ( ! isset( $block['attrs']['url'] ) ) { + return $block_content; + } + return '

    ' . $block['attrs']['url'] . '

    '; + } + + /** + * Convert HTML content to blocks. + * + * Tokenizes the content with wp_html_split(), tracks nesting depth, + * and wraps each top-level element in block comment delimiters. + * + * @since 8.1.0 + * + * @param string $content The HTML content. + * + * @return string The content converted to blocks. + */ + public static function convert_from_html( $content ) { + if ( empty( $content ) ) { + return ''; + } + + $tokens = \wp_html_split( $content ); + $_content = ''; + $depth = 0; + $current_tag = ''; + $current_html = ''; + + foreach ( $tokens as $token ) { + if ( '' === $token ) { + continue; + } + + // Text content — accumulate only inside a top-level element. + if ( '<' !== $token[0] ) { + if ( $depth > 0 ) { + $current_html .= $token; + } + continue; + } + + // Closing tag. + if ( '/' === $token[1] ) { + $current_html .= $token; + --$depth; + + if ( 0 === $depth && '' !== $current_tag ) { + $_content .= self::to_block( $current_tag, $current_html ); + $current_tag = ''; + $current_html = ''; + } + continue; + } + + // Extract the tag name from the opening tag. + if ( ! \preg_match( '/^<([a-zA-Z][a-zA-Z0-9]*)/', $token, $m ) ) { + if ( $depth > 0 ) { + $current_html .= $token; + } + continue; + } + + $tag = \strtoupper( $m[1] ); + + // Start of a new top-level element. + if ( 0 === $depth ) { + $current_tag = $tag; + $current_html = $token; + } else { + $current_html .= $token; + } + + // Void elements don't increase depth — flush immediately at top level. + if ( \in_array( $tag, self::VOID_TAGS, true ) ) { + if ( 0 === $depth && '' !== $current_tag ) { + $_content .= self::to_block( $current_tag, $current_html ); + $current_tag = ''; + $current_html = ''; + } + } else { + ++$depth; + } + } + + return $_content; + } + + /** + * Wrap an HTML element in block comment delimiters. + * + * @since 8.1.0 + * + * @param string $tag The uppercase tag name. + * @param string $html The element HTML. + * + * @return string The block-wrapped HTML, or empty string for skipped tags. + */ + private static function to_block( $tag, $html ) { + if ( \in_array( $tag, self::SKIP_TAGS, true ) ) { + return ''; + } + + $block_type = self::BLOCK_MAP[ $tag ] ?? 'html'; + $block_attrs = array(); + + if ( 'OL' === $tag ) { + $block_attrs['ordered'] = true; + } + + return \get_comment_delimited_block_content( $block_type, $block_attrs, \trim( $html ) ); + } + + /** + * Filter the main query to exclude replies. + * + * Adds a WHERE clause to exclude posts containing the `activitypub/reply` + * block when the visitor has explicitly requested the "Posts" tab via + * `?filter=posts`. This filters the main query so that Query Loop blocks + * with `inherit: true` also pick up the filter. + * + * The filter only attaches on that explicit opt-in. Admin, feed, and any + * regular frontend request (front page, archives, search…) are never + * touched, which is why no block-presence probing is needed: the only + * way `?filter=posts` appears in a URL is from a click on the + * `activitypub/posts-and-replies` tab block. + * + * @since 8.1.0 + * + * @param WP_Query $query The WP_Query instance. + */ + public static function filter_query_loop_vars( $query ) { + // Never touch admin or feed queries. + if ( \is_admin() || $query->is_feed() ) { + return; + } + + if ( ! $query->is_main_query() || $query->is_singular() ) { + return; + } + + // Skip the reply-exclusion filter for queries that only target + // non-ActivityPub post types to avoid a full table scan. + $query_post_type = $query->get( 'post_type' ); + if ( ! empty( $query_post_type ) && 'any' !== $query_post_type ) { + $query_post_types = (array) $query_post_type; + if ( ! array_intersect( $query_post_types, \get_post_types_by_support( 'activitypub' ) ) ) { + return; + } + } + + // Only filter when the "Posts" tab has been explicitly selected. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + if ( ! isset( $_GET['filter'] ) || 'posts' !== \sanitize_key( \wp_unslash( $_GET['filter'] ) ) ) { + return; + } + + \add_filter( 'posts_where', array( self::class, 'exclude_replies_where' ) ); + } + + /** + * Exclude posts containing the activitypub/reply block. + * + * Removes itself after the first execution to avoid + * affecting secondary queries on the same page. + * + * @since 8.1.0 + * + * @param string $where The WHERE clause. + * @return string Modified WHERE clause. + */ + public static function exclude_replies_where( $where ) { + \remove_filter( 'posts_where', array( self::class, 'exclude_replies_where' ) ); + + global $wpdb; + + $where .= $wpdb->prepare( + " AND {$wpdb->posts}.post_content NOT LIKE %s", + '%', \wp_json_encode( $attributes ) ) ); /** * 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. + * @param string $block The HTML markup for the remote reply container. */ - return apply_filters( 'activitypub_comment_reply_link', $div ); - } - - /** - * Create a link to reply to a federated comment. - * - * This function adds a title attribute to the reply link to inform the user - * that the comment was received from the fediverse and the reply will be sent - * to the original author. - * - * @param string $link The HTML markup for the comment reply link. - * @param array $args The args provided by the `comment_reply_link` filter. - * - * @return string The modified HTML markup for the comment reply link. - */ - private static function create_fediverse_reply_link( $link, $args ) { - $str_to_replace = sprintf( '>%s<', $args['reply_text'] ); - $replace_with = sprintf( - ' title="%s">%s<', - esc_attr__( 'This comment was received from the fediverse and your reply will be sent to the original author', 'activitypub' ), - esc_html__( 'Reply with federation', 'activitypub' ) - ); - return str_replace( $str_to_replace, $replace_with, $link ); + return \apply_filters( 'activitypub_comment_reply_link', $block ); } /** @@ -120,11 +184,12 @@ class Comment { return false; } - 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. + if ( is_single_user() && \user_can( $current_user, 'activitypub' ) ) { + // On a single user site, comments by users with the `activitypub` capability will be federated as the blog user. $current_user = Actors::BLOG_USER_ID; } + // User is not allowed to federate comments. return user_can_activitypub( $current_user ); } @@ -225,7 +290,7 @@ class Comment { } 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. + // On a single user site, comments by users with the `activitypub` capability will be federated as the blog user. $user_id = Actors::BLOG_USER_ID; } @@ -253,7 +318,7 @@ class Comment { * @return \WP_Comment|false Comment object, or false on failure. */ public static function object_id_to_comment( $id ) { - $comment_query = new WP_Comment_Query( + $comment_query = new \WP_Comment_Query( 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 @@ -278,16 +343,16 @@ class Comment { * @return string|null Comment ID or null if not found. */ public static function url_to_commentid( $url ) { - if ( ! $url || ! filter_var( $url, \FILTER_VALIDATE_URL ) ) { + if ( ! $url || ! \filter_var( $url, \FILTER_VALIDATE_URL ) ) { return null; } // Check for local comment. - if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) { + if ( is_same_domain( $url ) ) { $query = \wp_parse_url( $url, \PHP_URL_QUERY ); if ( $query ) { - parse_str( $query, $params ); + \parse_str( $query, $params ); if ( ! empty( $params['c'] ) ) { $comment = \get_comment( $params['c'] ); @@ -314,7 +379,7 @@ class Comment { ), ); - $query = new WP_Comment_Query(); + $query = new \WP_Comment_Query(); $comments = $query->query( $args ); if ( $comments && is_array( $comments ) ) { @@ -342,6 +407,38 @@ class Comment { return $classes; } + /** + * Makes the comment feed filterable by comment type. + * + * Also excludes ActivityPub comment types from the feed when no type is specified. + * + * @param string $where The `WHERE` clause for the comment feed query. + * + * @return string The modified `WHERE` clause. + */ + public static function comment_feed_where( $where ) { + global $wpdb; + + $comment_type = \get_query_var( 'type' ); + + if ( 'all' === $comment_type ) { + return $where; + } + + $comment_types = self::get_comment_type_slugs(); + + if ( \in_array( $comment_type, $comment_types, true ) ) { + $where .= $wpdb->prepare( ' AND comment_type = %s', $comment_type ); + } else { + $comment_types = \array_map( 'esc_sql', $comment_types ); + $placeholders = implode( ', ', array_fill( 0, count( $comment_types ), '%s' ) ); + // phpcs:ignore WordPress.DB.PreparedSQLPlaceholders.ReplacementsWrongNumber, WordPress.DB.PreparedSQL.NotPrepared + $where .= $wpdb->prepare( sprintf( ' AND comment_type NOT IN (%s)', $placeholders ), ...$comment_types ); + } + + return $where; + } + /** * Gets the public comment id via the WordPress comments meta. * @@ -391,13 +488,16 @@ class Comment { * @return string $url */ public static function remote_comment_link( $comment_link, $comment ) { - if ( ! $comment || is_admin() ) { + if ( ! $comment || \is_admin() || \is_search() ) { return $comment_link; } - $public_comment_link = self::get_source_url( $comment->comment_ID ); + $remote_comment_link = null; + if ( 'comment' === $comment->comment_type ) { + $remote_comment_link = self::get_source_url( $comment->comment_ID ); + } - return $public_comment_link ?? $comment_link; + return $remote_comment_link ?? $comment_link; } @@ -419,7 +519,7 @@ class Comment { } // Generate URI based on comment ID. - return \add_query_arg( 'c', $comment->comment_ID, \trailingslashit( \home_url() ) ); + return \add_query_arg( 'c', $comment->comment_ID, \home_url( '/' ) ); } /** @@ -452,60 +552,6 @@ class Comment { return ! empty( $comments ); } - /** - * Enqueue scripts for remote comments - */ - public static function enqueue_scripts() { - if ( ! \is_singular() || \is_user_logged_in() ) { - // Only on single pages, only for logged-out users. - return; - } - - if ( ! \post_type_supports( \get_post_type(), 'activitypub' ) ) { - // Post type does not support ActivityPub. - return; - } - - if ( ! \comments_open() || ! \get_comments_number() ) { - // No comments, no need to load the script. - return; - } - - if ( ! self::post_has_remote_comments( \get_the_ID() ) ) { - // No remote comments, no need to load the script. - return; - } - - $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'; - - if ( \file_exists( $asset_file ) ) { - $assets = require_once $asset_file; - - \wp_enqueue_script( - $handle, - \plugins_url( 'build/remote-reply/index.js', __DIR__ ), - $assets['dependencies'], - $assets['version'], - true - ); - \wp_add_inline_script( $handle, $js, 'before' ); - \wp_set_script_translations( $handle, 'activitypub' ); - - \wp_enqueue_style( - $handle, - \plugins_url( 'build/remote-reply/style-index.css', __DIR__ ), - array( 'wp-components' ), - $assets['version'] - ); - } - } - /** * Get the comment type by activity type. * @@ -535,7 +581,7 @@ class Comment { public static function get_comment_types() { global $activitypub_comment_types; - return $activitypub_comment_types; + return (array) $activitypub_comment_types; } /** @@ -560,22 +606,15 @@ class Comment { * @return array The registered custom comment type slugs. */ public static function get_comment_type_slugs() { + if ( ! did_action( 'init' ) ) { + _doing_it_wrong( __METHOD__, 'This function should not be called before the init action has run. Comment types are only available after init.', '7.5.0' ); + + return array(); + } + 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() { - _deprecated_function( __METHOD__, '4.5.0', 'get_comment_type_slugs' ); - - return self::get_comment_type_slugs(); - } - /** * Get the custom comment type. * @@ -643,7 +682,7 @@ class Comment { array( 'label' => __( 'Reposts', 'activitypub' ), 'singular' => __( 'Repost', 'activitypub' ), - 'description' => __( 'A repost on the indieweb is a post that is purely a 100% re-publication of another (typically someone else\'s) post.', 'activitypub' ), + 'description' => 'A repost (or Announce) is when a post appears in the timeline because someone else shared it, while still showing the original author as the source.', 'icon' => '♻️', 'class' => 'p-repost', 'type' => 'repost', @@ -662,7 +701,7 @@ class Comment { array( 'label' => __( 'Likes', 'activitypub' ), 'singular' => __( 'Like', 'activitypub' ), - 'description' => __( 'A like is a popular webaction button and in some cases post type on various silos such as Facebook and Instagram.', 'activitypub' ), + 'description' => 'A like is a small positive reaction that shows appreciation for a post without sharing it further.', 'icon' => '👍', 'class' => 'p-like', 'type' => 'like', @@ -675,6 +714,25 @@ class Comment { 'count_plural' => _x( '%d likes', 'number of likes', 'activitypub' ), ) ); + + register_comment_type( + 'quote', + array( + 'label' => __( 'Quotes', 'activitypub' ), + 'singular' => __( 'Quote', 'activitypub' ), + 'description' => 'A quote is when a post is shared along with an added comment, so the original post appears together with the sharer’s own words.', + 'icon' => '❞', + 'class' => 'p-quote', + 'type' => 'quote', + 'collection' => 'quotes', + 'activity_types' => array( 'quote' ), + 'excerpt' => html_entity_decode( \__( '… quoted this!', 'activitypub' ) ), + /* translators: %d: Number of quotes */ + 'count_single' => _x( '%d quote', 'number of quotes', 'activitypub' ), + /* translators: %d: Number of quotes */ + 'count_plural' => _x( '%d quotes', 'number of quotes', 'activitypub' ), + ) + ); } /** @@ -698,10 +756,10 @@ class Comment { * * @see https://github.com/janboddez/indieblocks/blob/a2d59de358031056a649ee47a1332ce9e39d4ce2/includes/functions.php#L423-L432 * - * @param WP_Comment_Query $query Comment count. + * @param \WP_Comment_Query $query Comment count. */ public static function comment_query( $query ) { - if ( ! $query instanceof WP_Comment_Query ) { + if ( ! $query instanceof \WP_Comment_Query ) { return; } @@ -710,53 +768,125 @@ class Comment { return; } - // Do not exclude likes and reposts on REST requests. + // Do not exclude likes and reposts on REST requests (handled by rest_comment_query). 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() ) { + // Filter post types for admin requests. + if ( \is_admin() ) { + $query->query_vars['post_type'] = self::get_allowed_comment_post_types(); return; } - // Do not exclude likes and reposts if the query is for comments. + // Do not exclude likes and reposts on non-singular pages. + if ( ! \is_singular() ) { + return; + } + + // Do not exclude likes and reposts if the query is for specific types. if ( ! empty( $query->query_vars['type__in'] ) || ! empty( $query->query_vars['type'] ) ) { return; } + // Do not exclude likes and reposts if the query is already excluding other comment types. + if ( ! empty( $query->query_vars['type__not_in'] ) ) { + return; + } + // Exclude likes and reposts by the ActivityPub plugin. $query->query_vars['type__not_in'] = self::get_comment_type_slugs(); } + /** + * Filters comments in REST API requests. + * + * Excludes comments on ActivityPub post types and ActivityPub comment + * types (likes, reposts) from the REST API. + * + * @param array $prepared_args Array of arguments for WP_Comment_Query. + * + * @return array Modified array of arguments. + */ + public static function rest_comment_query( $prepared_args ) { + // Exclude comments on ActivityPub post types. + $prepared_args['post_type'] = self::get_allowed_comment_post_types(); + + // Exclude ActivityPub comment types (likes, reposts) unless explicitly requested. + if ( empty( $prepared_args['type'] ) && empty( $prepared_args['type__in'] ) ) { + $prepared_args['type__not_in'] = self::get_comment_type_slugs(); + } + + return $prepared_args; + } + + /** + * Returns post types that should show comments (excluding hidden post types). + * + * @return array Array of post type names. + */ + private static function get_allowed_comment_post_types() { + $hide_for = self::hide_for(); + + if ( empty( $hide_for ) ) { + return \get_post_types_by_support( 'comments' ); + } + + return \array_diff( \get_post_types_by_support( 'comments' ), $hide_for ); + } + /** * Filter the comment status before it is set. * - * @param string $approved The approved comment status. - * @param array $commentdata The comment data. + * @param int|string|\WP_Error $approved The approved comment status. + * @param array $comment_data The comment data. * - * @return boolean `true` if the comment is approved, `false` otherwise. + * @return int|string|\WP_Error The approval status. 1, 0, 'spam', 'trash', or WP_Error. */ - public static function pre_comment_approved( $approved, $commentdata ) { - if ( $approved || \is_wp_error( $approved ) ) { + public static function pre_comment_approved( $approved, $comment_data ) { + /* + * Only return early for already-approved comments, trash, or errors. + * Don't short-circuit on 'spam' - we may want to override Akismet. + * Respect 'trash' since it comes from the WordPress disallowed list. + */ + if ( 1 === $approved || '1' === $approved || 'trash' === $approved || \is_wp_error( $approved ) ) { return $approved; } + // Maybe auto-approve likes and reposts. + if ( + \in_array( $comment_data['comment_type'], self::get_comment_type_slugs(), true ) && + '1' === \get_option( 'activitypub_auto_approve_reactions' ) + ) { + return 1; + } + + /* + * Always auto-approve comments on remote posts (ap_post) since + * they are not visible in the WP admin comment moderation screen. + */ + $post_id = $comment_data['comment_post_ID']; + $post = \get_post( $post_id ); + + if ( $post && \in_array( $post->post_type, self::hide_for(), true ) ) { + return 1; + } + if ( '1' !== \get_option( 'comment_previously_approved' ) ) { return $approved; } if ( - empty( $commentdata['comment_meta']['protocol'] ) || - 'activitypub' !== $commentdata['comment_meta']['protocol'] + empty( $comment_data['comment_meta']['protocol'] ) || + 'activitypub' !== $comment_data['comment_meta']['protocol'] ) { return $approved; } global $wpdb; - $author = $commentdata['comment_author']; - $author_url = $commentdata['comment_author_url']; + $author = $comment_data['comment_author']; + $author_url = $comment_data['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 ) ); @@ -795,6 +925,29 @@ class Comment { $excluded_types = array_filter( self::get_comment_type_slugs(), array( self::class, 'is_comment_type_enabled' ) ); if ( ! empty( $excluded_types ) ) { + /* + * Include 'note' type when Gutenberg's filter is registered, so a + * single query excludes both ActivityPub and Gutenberg types. + */ + if ( \has_filter( 'pre_wp_update_comment_count_now', 'gutenberg_exclude_notes_from_comment_count' ) ) { + $excluded_types[] = 'note'; + } + + /** + * Filters the comment types excluded from the comment count. + * + * Runs at priority 5 on `pre_wp_update_comment_count_now` so that + * a single query can exclude types from multiple plugins. Other + * plugins can hook here to add their own comment types. + * + * @since 8.0.0 + * + * @param string[] $excluded_types The comment type slugs to exclude. + * @param int $post_id The post ID. + */ + $excluded_types = \apply_filters( 'activitypub_excluded_comment_types', $excluded_types, $post_id ); + $excluded_types = array_unique( array_filter( $excluded_types ) ); + global $wpdb; // phpcs:ignore WordPress.DB @@ -814,4 +967,72 @@ class Comment { public static function is_comment_type_enabled( $comment_type ) { return '1' === get_option( "activitypub_allow_{$comment_type}s", '1' ); } + + /** + * Get post types to hide comments for in admin. + * + * These are non-public post types whose comments should not appear + * in the main comments list in the WordPress admin. + * + * @return string[] Array of post type names to hide comments for. + */ + public static function hide_for() { + $post_types = array( Remote_Posts::POST_TYPE ); + + /** + * Filters the list of post types to hide comments for. + * + * @param string[] $post_types Array of post type names to hide comments for. + */ + return \apply_filters( 'activitypub_hide_comments_for', $post_types ); + } + + /** + * Render emoji in comment author name. + * + * Replaces emoji shortcodes with img tags on the get_comment_author filter. + * Emoji data is retrieved from the linked remote actor. + * + * @param string $author The comment author name. + * @param string $comment_id The comment ID as a numeric string. + * + * @return string The comment author name with rendered emoji. + */ + public static function render_emoji( $author, $comment_id ) { + $remote_actor_id = \get_comment_meta( $comment_id, '_activitypub_remote_actor_id', true ); + + if ( empty( $remote_actor_id ) ) { + return $author; + } + + $emoji_data = \get_post_meta( $remote_actor_id, '_activitypub_emoji', true ); + + if ( empty( $emoji_data ) ) { + return $author; + } + + return Emoji::replace_from_json( $author, $emoji_data ); + } + + /** + * Selectively unescape emoji images in comment author. + * + * This runs at priority 20 after WordPress's esc_html() filter on comment_author. + * + * @param string $author The comment author name (already escaped by WordPress). + * + * @return string The comment author name with emoji images unescaped. + */ + public static function unescape_emoji( $author ) { + // Only attempt to unescape if there are emoji images present in the escaped string. + if ( false === \strpos( $author, 'class="emoji"' ) ) { + return $author; + } + + // Decode entities so we can selectively restore emoji tags. + $decoded = \html_entity_decode( $author, ENT_QUOTES | ENT_HTML5, 'UTF-8' ); + + // Use strict KSES validation to only allow valid emoji img tags. + return \wp_kses( $decoded, Emoji::get_kses_allowed_html() ); + } } diff --git a/wp-content/plugins/activitypub/includes/class-debug.php b/wp-content/plugins/activitypub/includes/class-debug.php deleted file mode 100644 index af7608dd..00000000 --- a/wp-content/plugins/activitypub/includes/class-debug.php +++ /dev/null @@ -1,79 +0,0 @@ -ID, '_activitypub_activity_type', true ); $actor = Outbox::get_actor( $outbox_item ); - if ( \is_wp_error( $actor ) ) { + if ( \is_wp_error( $actor ) && 'Delete' !== $type ) { // If the actor is not found, publish the post and don't try again. \wp_publish_post( $outbox_item ); return; @@ -100,13 +132,13 @@ class Dispatcher { $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 ); + self::send_to_additional_inboxes( $activity, $outbox_item->post_author, $outbox_item ); if ( self::should_send_to_followers( $activity, $actor, $outbox_item ) ) { - Scheduler::async_batch( - self::$callback, + \do_action( + 'activitypub_send_activity', $outbox_item->ID, - self::$batch_size, + self::get_batch_size(), \get_post_meta( $outbox_item->ID, '_activitypub_outbox_offset', true ) ?: 0 // phpcs:ignore ); } else { @@ -119,17 +151,31 @@ class Dispatcher { /** * 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. + * @param int $outbox_item_id The Outbox item ID. + * @param int|null $batch_size Optional. The batch size. Default null (uses filtered 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 ); + if ( null === $batch_size ) { + $batch_size = self::get_batch_size(); + } + $outbox_item = \get_post( $outbox_item_id ); + + if ( ! $outbox_item ) { + return; + } + + $activity = Outbox::get_activity( $outbox_item_id ); + + if ( \is_wp_error( $activity ) ) { + return; + } + + $json = $activity->to_json(); + $inboxes = Followers::get_inboxes_for_activity( $json, $outbox_item->post_author, $batch_size, $offset ); $retries = self::send_to_inboxes( $inboxes, $outbox_item_id ); // Retry failed inboxes. @@ -150,7 +196,7 @@ class Dispatcher { * @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 ); + \do_action( 'activitypub_outbox_processing_complete', $inboxes, $json, $outbox_item->post_author, $outbox_item_id, $batch_size, $offset ); // No more followers to process for this update. \wp_publish_post( $outbox_item_id ); @@ -167,7 +213,7 @@ class Dispatcher { * @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 ); + \do_action( 'activitypub_outbox_processing_batch_complete', $inboxes, $json, $outbox_item->post_author, $outbox_item_id, $batch_size, $offset ); return array( $outbox_item_id, $batch_size, $offset + $batch_size ); } @@ -192,7 +238,7 @@ class Dispatcher { $retries = self::send_to_inboxes( $inboxes, $outbox_item_id ); // Retry failed inboxes. - if ( ++$attempt < 3 && ! empty( $retries ) ) { + if ( ++$attempt < self::get_retry_max_attempts() && ! empty( $retries ) ) { self::schedule_retry( $retries, $outbox_item_id, $attempt ); } } @@ -205,8 +251,16 @@ class Dispatcher { * @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 ) ); + $outbox_item = \get_post( $outbox_item_id ); + + $activity = Outbox::get_activity( $outbox_item_id ); + + if ( \is_wp_error( $activity ) ) { + return array(); + } + + $json = $activity->to_json(); + $retries = array(); /** @@ -219,27 +273,67 @@ class Dispatcher { \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() ); + // Handle local inboxes via internal REST API, remote via HTTP. + if ( is_same_domain( $inbox ) ) { + $result = self::send_to_local_inbox( $inbox, $json ); + } else { + $result = safe_remote_post( $inbox, $json, $outbox_item->post_author ); + } - if ( is_wp_error( $result ) && in_array( $result->get_error_code(), self::$retry_error_codes, true ) ) { + if ( \is_wp_error( $result ) && in_array( $result->get_error_code(), self::get_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 array $result The result of the internal or 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 ); + \do_action( 'activitypub_sent_to_inbox', $result, $inbox, $json, $outbox_item->post_author, $outbox_item_id ); } return $retries; } + /** + * Send an activity to a local inbox via internal REST API request. + * + * @param string $inbox_url The local inbox URL. + * @param string $json The ActivityPub Activity JSON. + * @return array|\WP_Error The result in the format of a remote post response, or WP_Error on failure. + */ + private static function send_to_local_inbox( $inbox_url, $json ) { + // Parse the inbox URL to extract the REST route. + $path = \wp_parse_url( $inbox_url, PHP_URL_PATH ) ?? ''; + $rest_route = \preg_replace( '#^/' . preg_quote( \rest_get_url_prefix(), '#' ) . '#', '', $path ); + + // Create a REST request. + $request = new \WP_REST_Request( 'POST', $rest_route ); + $request->set_header( 'Content-Type', 'application/activity+json' ); + $request->set_body( $json ); + $request->get_json_params(); + + \add_filter( 'activitypub_defer_signature_verification', '__return_true' ); + $response = \rest_do_request( $request ); + \remove_filter( 'activitypub_defer_signature_verification', '__return_true' ); + + // Return result in format similar to remote post response. + if ( $response->is_error() ) { + return $response->as_error(); + } + + return array( + 'response' => array( + 'code' => $response->get_status(), + ), + 'body' => \wp_json_encode( $response->get_data() ), + ); + } + /** * Schedule a retry. * @@ -252,14 +346,9 @@ class Dispatcher { \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, - ) + \time() + ( $attempt * $attempt * self::get_retry_delay() ), + 'activitypub_retry_activity', + array( $transient_key, $outbox_item_id, $attempt ) ); } @@ -306,13 +395,8 @@ class Dispatcher { $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 ); - } - ); + // Remove "public placeholder" from the audience. + $audience = array_diff( $audience, ACTIVITYPUB_PUBLIC_AUDIENCE_IDENTIFIERS ); if ( $audience ) { $mentioned_inboxes = Mention::get_inboxes( $audience ); @@ -377,47 +461,31 @@ class Dispatcher { } /** - * 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. + * Check if an Activity should be sent to followers. * * @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. + * @return boolean True if the Activity should be sent to followers, 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(); + $cc = (array) ( $activity->get_cc() ?? array() ); + $to = (array) ( $activity->get_to() ?? array() ); + $bcc = (array) ( $activity->get_bcc() ?? array() ); + $bto = (array) ( $activity->get_bto() ?? array() ); - $audience = array_merge( $cc, $to ); + $audience = array_merge( $cc, $to, $bcc, $bto ); $send = ( // Check if activity is public. - in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) || + is_activity_public( $activity ) || // ...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() ); + $followers = Followers::get_inboxes_for_activity( $activity->to_json(), $outbox_item->post_author ); // Only send if there are followers to send to. $send = ! is_countable( $followers ) || 0 < count( $followers ); @@ -431,7 +499,7 @@ class Dispatcher { * @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 ); + return apply_filters( 'activitypub_send_activity_to_followers', $send, $activity, $outbox_item->post_author, $outbox_item ); } /** @@ -444,14 +512,8 @@ class Dispatcher { * @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 ) ) { + if ( ! is_activity_public( $activity ) ) { return $inboxes; } @@ -463,4 +525,54 @@ class Dispatcher { return array_merge( $inboxes, $relays ); } + + /** + * Fire outbox handlers for activities. + * + * Triggers activity type-specific handlers to process outbox activities, + * allowing handlers to create WordPress posts or perform other side effects. + * + * @param int $outbox_id The Outbox item ID. + * @param Activity $activity The Activity that was just added to the Outbox. + */ + public static function fire_outbox_handlers( $outbox_id, $activity ) { + $outbox_item = \get_post( $outbox_id ); + + if ( ! $outbox_item ) { + return; + } + + $type = $activity->get_type(); + $user_id = $outbox_item->post_author; + $data = $activity->to_array( false ); + + /** + * Fires when an activity has been added to the outbox. + * + * Handlers can implement side effects like creating WordPress posts. + * + * @param array $data The activity data array. + * @param int $user_id The user ID. + * @param Activity $activity The Activity object. + * @param int $outbox_id The outbox post ID. + */ + \do_action( 'activitypub_handled_outbox_' . \strtolower( $type ), $data, $user_id, $activity, $outbox_id ); + } + + /** + * Send an immediate Accept activity for the given Outbox item. + * + * @param int $outbox_id The Outbox item ID. + * @param Activity $activity The Activity that was just added to the Outbox. + */ + public static function send_immediate_accept( $outbox_id, $activity ) { + $outbox_item = \get_post( $outbox_id ); + + if ( ! $outbox_item || 'Accept' !== $activity->get_type() ) { + return; + } + + // Send to mentioned and replied-to users. Everyone other than followers. + self::send_to_additional_inboxes( $activity, $outbox_item->post_author, $outbox_item ); + } } diff --git a/wp-content/plugins/activitypub/includes/class-embed.php b/wp-content/plugins/activitypub/includes/class-embed.php index 978e66f4..c06a8be2 100644 --- a/wp-content/plugins/activitypub/includes/class-embed.php +++ b/wp-content/plugins/activitypub/includes/class-embed.php @@ -8,12 +8,12 @@ namespace Activitypub; /** - * Class to handle embedding ActivityPub content + * Class to handle embedding ActivityPub content. */ class Embed { /** - * Initialize the embed handler + * Initialize the embed handler. */ public static function init() { \add_filter( 'pre_oembed_result', array( self::class, 'maybe_use_activitypub_embed' ), 10, 3 ); @@ -32,7 +32,8 @@ class Embed { 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 ) ) { + + if ( \is_wp_error( $object ) || ! is_activity_object( $object ) ) { return false; } @@ -55,9 +56,11 @@ class Embed { // 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 ) ) { + if ( is_wp_error( $author ) ) { + $author = array(); + } else { $avatar_url = $author['icon']['url'] ?? ''; - $author_name = $author['name'] ?? $author_name; + $author_name = empty( $author['name'] ) ? $author_name : $author['name']; } } @@ -65,7 +68,7 @@ class Embed { 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 ); + $domain = \wp_parse_url( object_to_uri( $author['url'] ), PHP_URL_HOST ); $author['webfinger'] = '@' . $author['preferredUsername'] . '@' . $domain; } else { // Fallback to URL. @@ -79,33 +82,53 @@ class Embed { $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 = ''; + $audio = null; + $images = array(); + $video = null; if ( isset( $activity_object['image']['url'] ) ) { - $image = $activity_object['image']['url']; + $images = array( + array( + 'type' => 'Image', + 'url' => $activity_object['image']['url'], + 'name' => $activity_object['image']['name'] ?? '', + ), + ); } 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; + $type = isset( $attachment['mediaType'] ) ? strtok( $attachment['mediaType'], '/' ) : strtolower( $attachment['type'] ); + + switch ( $type ) { + case 'image': + $images[] = $attachment; + break; + case 'video': + $video = $attachment; + break 2; + case 'audio': + $audio = $attachment; + break 2; } } + $images = \array_slice( $images, 0, 4 ); } ob_start(); load_template( - ACTIVITYPUB_PLUGIN_DIR . 'templates/reply-embed.php', + ACTIVITYPUB_PLUGIN_DIR . 'templates/embed.php', false, array( + 'audio' => $audio, 'author_name' => $author_name, 'author_url' => $author_url, 'avatar_url' => $avatar_url, + 'boosts' => $boosts, + 'content' => $content, + 'favorites' => $favorites, + 'images' => $images, 'published' => $published, 'title' => $title, - 'content' => $content, - 'image' => $image, - 'boosts' => $boosts, - 'favorites' => $favorites, 'url' => $activity_object['id'], + 'video' => $video, 'webfinger' => $author['webfinger'], ) ); @@ -130,7 +153,7 @@ class Embed { */ 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 ); + \remove_filter( 'pre_oembed_result', array( self::class, 'maybe_use_activitypub_embed' ) ); // 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 ); @@ -201,7 +224,7 @@ class Embed { } // Try to get ActivityPub representation. - $activitypub_html = get_embed_html( $url ); + $activitypub_html = self::get_html( $url ); if ( ! $activitypub_html ) { return $html; } @@ -234,14 +257,18 @@ class Embed { * @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() ) { + if ( '/oembed/1.0/proxy' !== $request->get_route() ) { + return $response; + } + + if ( ( is_wp_error( $response ) && 'oembed_invalid_url' === $response->get_error_code() ) || empty( $response->html ) ) { $url = $request->get_param( 'url' ); - $html = get_embed_html( $url ); + $html = self::get_html( $url ); if ( $html ) { $args = $request->get_params(); $data = (object) array( - 'provider_name' => 'Embed Handler', + 'provider_name' => 'ActivityPub oEmbed', 'html' => $html, 'scripts' => array(), ); @@ -256,6 +283,21 @@ class Embed { $response = new \WP_REST_Response( $data ); } + } elseif ( ! empty( $request->get_param( 'activitypub' ) ) ) { + /* + * If the 'activitypub' parameter is present, perform an additional validation step: + * Ensure the provided URL resolves to a valid ActivityPub object. + * + * This differs from the standard oEmbed flow, which does not explicitly validate + * the URL as an ActivityPub object unless the initial oEmbed lookup fails. + * This block is triggered for requests from the Federated Reply block, where we + * want to inform users whether post authors will be notified of the reply. + */ + $object = Http::get_remote_object( $request->get_param( 'url' ) ); + + if ( \is_wp_error( $object ) || ! is_activity_object( $object ) ) { + $response = new \WP_Error( 'oembed_invalid_url', \get_status_header_desc( 404 ), array( 'status' => 404 ) ); + } } return $response; diff --git a/wp-content/plugins/activitypub/includes/class-emoji.php b/wp-content/plugins/activitypub/includes/class-emoji.php new file mode 100644 index 00000000..390866ce --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-emoji.php @@ -0,0 +1,277 @@ + \esc_url( $url ) ); + + if ( ! empty( $tag['updated'] ) && \is_string( $tag['updated'] ) && \strtotime( $tag['updated'] ) ) { + $block_attrs['updated'] = \sanitize_text_field( $tag['updated'] ); + } + + $wrapped = \sprintf( + '%s', + \wp_json_encode( $block_attrs ), + $shortcode + ); + + // Case-insensitive replacement, avoid already wrapped shortcodes. + $pattern = '/(?)' . \preg_quote( $shortcode, '/' ) . '(?!)/i'; + $content = \preg_replace_callback( + $pattern, + function () use ( $wrapped ) { + return $wrapped; + }, + $content + ); + } + + return $content; + } + + /** + * Generate an emoji img tag. + * + * @param string $url The emoji image URL. + * @param string $name The emoji name (without colons). + * + * @return string The emoji img tag HTML. + */ + public static function get_img_tag( $url, $name ) { + return \sprintf( + '%s', + \esc_url( $url ), + \esc_attr( $name ), + \esc_attr( $name ) + ); + } + + /** + * Get the allowed HTML structure for emoji img tags. + * + * Used by Comment class for KSES validation of emoji in author names. + * + * @return array The allowed HTML structure for use with wp_kses. + */ + public static function get_kses_allowed_html() { + return array( + 'img' => array( + 'class' => array( + 'required' => true, + 'values' => array( 'emoji' ), + ), + 'src' => array( + 'required' => true, + 'value_callback' => array( self::class, 'validate_emoji_src' ), + ), + 'alt' => array( 'required' => true ), + 'title' => array( 'required' => true ), + 'height' => array( + 'required' => true, + 'values' => array( '20' ), + ), + 'width' => array( + 'required' => true, + 'values' => array( '20' ), + ), + 'draggable' => array( + 'required' => true, + 'values' => array( 'false' ), + ), + ), + ); + } + + /** + * Validate emoji src attribute for wp_kses. + * + * By default, only allows locally cached emoji URLs for privacy. + * Remote URLs are only allowed when caching is explicitly disabled. + * + * @param string $value The src attribute value. + * + * @return bool True if the src is valid, false otherwise. + */ + public static function validate_emoji_src( $value ) { + $upload_dir = \wp_upload_dir(); + $emoji_base = $upload_dir['baseurl'] . '/activitypub/emoji/'; + + // Allow local cached emoji. + if ( \str_starts_with( $value, $emoji_base ) ) { + return true; + } + + // Only allow remote URLs when caching is explicitly disabled. + // This protects user privacy by defaulting to local-only emoji. + $allow_remote = ! Cache::is_enabled(); + + // Validate the URL format if remote is allowed. + if ( $allow_remote ) { + $allow_remote = (bool) \wp_http_validate_url( $value ); + } + + /** + * Filters whether a remote emoji URL is valid. + * + * Use this filter to explicitly allow remote emoji URLs when needed + * (e.g., for CDN proxying). + * + * @since 5.6.0 + * + * @param bool $valid Whether the URL is valid. + * @param string $value The emoji src URL. + */ + return \apply_filters( 'activitypub_validate_emoji_src', $allow_remote, $value ); + } + + /** + * Prepare actor meta for emoji storage. + * + * Used for storing actor emoji data for comment author name rendering. + * + * @param array $actor The actor array containing potential emoji in tags. + * + * @return array Meta input array with emoji data, or empty array if no emoji. + */ + public static function prepare_actor_meta( $actor ) { + if ( empty( $actor['tag'] ) || ! \is_array( $actor['tag'] ) ) { + return array(); + } + + $emoji_tags = \array_values( + \array_filter( + $actor['tag'], + function ( $tag ) { + return \is_array( $tag ) && isset( $tag['type'] ) && 'Emoji' === $tag['type']; + } + ) + ); + + if ( empty( $emoji_tags ) ) { + return array(); + } + + return array( + '_activitypub_emoji' => \wp_json_encode( $emoji_tags ), + ); + } + + /** + * Replace emoji from stored JSON data. + * + * Used for comment author name replacement at display time. + * + * @param string $text The text to process. + * @param string $emoji_json JSON-encoded emoji tag data. + * + * @return string The processed text with emoji replacements. + */ + public static function replace_from_json( $text, $emoji_json ) { + $tags = \json_decode( $emoji_json, true ); + + if ( empty( $tags ) || ! \is_array( $tags ) ) { + return $text; + } + + foreach ( $tags as $tag ) { + if ( empty( $tag['name'] ) ) { + continue; + } + + $url = object_to_uri( $tag['icon'] ?? null ); + if ( empty( $url ) ) { + continue; + } + + /** + * Filters a remote media URL for caching. + * + * @param string $url The remote media URL. + * @param string $context The context ('emoji'). + * @param string|null $entity_id The entity ID. + * @param array $options Additional options. + */ + $cached_url = \apply_filters( + 'activitypub_remote_media_url', + $url, + 'emoji', + null, + array( 'updated' => $tag['updated'] ?? null ) + ); + + $name = \trim( $tag['name'], ':' ); + $img = self::get_img_tag( $cached_url ?: $url, $name ); + + $text = \str_ireplace( $tag['name'], $img, $text ); + } + + return $text; + } + + /** + * Replace emoji in text using a remote actor's stored emoji data. + * + * Used by Mailer class for actor name/summary in emails. + * + * @param string $text The text to process. + * @param string $actor_url The actor's URL to look up emoji data. + * + * @return string The processed text with emoji replacements. + */ + public static function replace_for_actor( $text, $actor_url ) { + $actor_post = Collection\Remote_Actors::get_by_uri( $actor_url ); + if ( ! $actor_post || \is_wp_error( $actor_post ) ) { + return $text; + } + + $emoji_data = \get_post_meta( $actor_post->ID, '_activitypub_emoji', true ); + if ( empty( $emoji_data ) ) { + return $text; + } + + return self::replace_from_json( $text, $emoji_data ); + } +} diff --git a/wp-content/plugins/activitypub/includes/class-event-stream.php b/wp-content/plugins/activitypub/includes/class-event-stream.php new file mode 100644 index 00000000..ffb6cd32 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-event-stream.php @@ -0,0 +1,61 @@ +post_content . "\n" . $post->post_excerpt; + $content = self::extract_text_outside_protected_tags( $content ); + $tags = array(); - - // 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] ); + $tags = \array_unique( $match[1] ); } \wp_add_post_tags( $post->ID, \implode( ', ', $tags ) ); } + /** + * Extract text content from outside protected HTML elements. + * + * Uses WP_HTML_Tag_Processor to properly parse HTML and skip content inside + * protected tags, matching the behavior of enrich_content_data(). + * + * @param string $content The HTML content to process. + * + * @return string Text content from non-protected areas only. + */ + private static function extract_text_outside_protected_tags( $content ) { + $processor = new \WP_HTML_Tag_Processor( $content ); + + /* + * Do not process content inside protected tags. + * + * Note: SCRIPT, STYLE, and TEXTAREA are "atomic" elements in + * WP_HTML_Tag_Processor, meaning their content is bundled with the tag + * token and won't appear as separate #text nodes. Because of this they + * do not need to be listed in $protected_tags: their inner text is + * never surfaced as #text tokens for us to process. + * See https://github.com/WordPress/wordpress-develop/blob/0fb3bb29596918864d808d156268a2df63c83620/src/wp-includes/html-api/class-wp-html-tag-processor.php#L276 + */ + $protected_tags = array( 'PRE', 'CODE', 'A' ); + $tag_stack = array(); + $filtered_content = ''; + + while ( $processor->next_token() ) { + $token_type = $processor->get_token_type(); + + if ( '#tag' === $token_type ) { + $tag_name = $processor->get_tag(); + + if ( $processor->is_tag_closer() ) { + // Closing tag: remove from stack. + $i = \array_search( $tag_name, $tag_stack, true ); + if ( false !== $i ) { + $tag_stack = \array_slice( $tag_stack, 0, $i ); + } + } elseif ( \in_array( $tag_name, $protected_tags, true ) ) { + // Opening tag: add to stack. + $tag_stack[] = $tag_name; + } + } elseif ( '#text' === $token_type && empty( $tag_stack ) ) { + // Only include text chunks that are outside protected tags. + $filtered_content .= $processor->get_modifiable_text(); + } + } + + return $filtered_content; + } + /** * Filter to replace the #tags in the content with links. * diff --git a/wp-content/plugins/activitypub/includes/class-http.php b/wp-content/plugins/activitypub/includes/class-http.php index 9f9a8dd0..1ddc0ebb 100644 --- a/wp-content/plugins/activitypub/includes/class-http.php +++ b/wp-content/plugins/activitypub/includes/class-http.php @@ -7,7 +7,6 @@ namespace Activitypub; -use WP_Error; use Activitypub\Collection\Actors; /** @@ -23,7 +22,7 @@ class Http { * @param string $body The Post Body. * @param int $user_id The WordPress User-ID. * - * @return array|WP_Error The POST Response or an WP_Error. + * @return array|\WP_Error The POST Response or an WP_Error. */ public static function post( $url, $body, $user_id ) { /** @@ -35,38 +34,42 @@ class Http { */ \do_action( 'activitypub_pre_http_post', $url, $body, $user_id ); - $date = \gmdate( 'D, d M Y H:i:s T' ); - $digest = Signature::generate_digest( $body ); - $signature = Signature::generate_signature( $user_id, 'post', $url, $date, $digest ); - - $wp_version = get_masked_wp_version(); - /** * Filters the HTTP headers user agent string. * * @param string $user_agent The user agent string. + * @param string $url The request URL. */ - $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); - $args = array( - 'timeout' => 100, + $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . get_masked_wp_version() . '; ' . \get_bloginfo( 'url' ), $url ); + + /** + * Filters the timeout duration for remote POST requests in ActivityPub. + * + * @param int $timeout The timeout value in seconds. Default 10 seconds. + */ + $timeout = \apply_filters( 'activitypub_remote_post_timeout', 10 ); + + $args = array( + 'timeout' => $timeout, 'limit_response_size' => 1048576, 'redirection' => 3, 'user-agent' => "$user_agent; ActivityPub", 'headers' => array( 'Accept' => 'application/activity+json', 'Content-Type' => 'application/activity+json', - 'Digest' => $digest, - 'Signature' => $signature, - 'Date' => $date, + 'Date' => \gmdate( 'D, d M Y H:i:s T' ), ), 'body' => $body, + 'key_id' => \json_decode( $body )->actor . '#main-key', + 'private_key' => Actors::get_private_key( $user_id ), + 'user_id' => $user_id, ); $response = \wp_safe_remote_post( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); if ( $code >= 400 ) { - $response = new WP_Error( + $response = new \WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( @@ -79,10 +82,10 @@ class Http { /** * Action to save the response of the remote POST request. * - * @param array|WP_Error $response The response of the remote POST request. - * @param string $url The URL endpoint. - * @param string $body The Post Body. - * @param int $user_id The WordPress User-ID. + * @param array|\WP_Error $response The response of the remote POST request. + * @param string $url The URL endpoint. + * @param string $body The Post Body. + * @param int $user_id The WordPress User-ID. */ \do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id ); @@ -93,11 +96,24 @@ class Http { * Send a GET Request with the needed HTTP Headers. * * @param string $url The URL endpoint. - * @param bool|int $cached Optional. Whether the result should be cached, or its duration. Default false. + * @param array $args Optional. Additional arguments to customize the request. + * - 'headers': Array of headers to override defaults. + * @param bool|int $cached Optional. Whether to return cached results, or cache duration. Default false. * - * @return array|WP_Error The GET Response or a WP_Error. + * @return array|\WP_Error The GET Response or a WP_Error. */ - public static function get( $url, $cached = false ) { + public static function get( $url, $args = array(), $cached = false ) { + // Backward compatibility: if $args is boolean/int, it's the old $cached parameter. + if ( ! \is_array( $args ) ) { + \_deprecated_argument( + __METHOD__, + '7.9.0', + \esc_html__( 'The $cached parameter should now be passed as the third argument.', 'activitypub' ) + ); + $cached = $args; + $args = array(); + } + /** * Fires before an HTTP GET request is made. * @@ -105,17 +121,18 @@ class Http { */ \do_action( 'activitypub_pre_http_get', $url ); - if ( $cached ) { - $transient_key = self::generate_cache_key( $url ); + $transient_key = self::generate_cache_key( $url ); + // Check cache only if caching is requested. + if ( $cached ) { $response = \get_transient( $transient_key ); if ( $response ) { /** * Action to save the response of the remote GET request. * - * @param array|WP_Error $response The response of the remote GET request. - * @param string $url The URL endpoint. + * @param array|\WP_Error $response The response of the remote GET request. + * @param string $url The URL endpoint. */ \do_action( 'activitypub_safe_remote_get_response', $response, $url ); @@ -123,29 +140,22 @@ class Http { } } - $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( Actors::APPLICATION_USER_ID, 'get', $url, $date ); - - $wp_version = get_masked_wp_version(); - /** * 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. + * @param string $url The request URL. */ - $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); + $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . get_masked_wp_version() . '; ' . \get_bloginfo( 'url' ), $url ); /** * Filters the timeout duration for remote GET requests in ActivityPub. * - * @param int $timeout The timeout value in seconds. Default 100 seconds. + * @param int $timeout The timeout value in seconds. Default 10 seconds. */ - $timeout = \apply_filters( 'activitypub_remote_get_timeout', 100 ); + $timeout = \apply_filters( 'activitypub_remote_get_timeout', 10 ); - $args = array( + $defaults = array( 'timeout' => $timeout, 'limit_response_size' => 1048576, 'redirection' => 3, @@ -153,33 +163,54 @@ class Http { 'headers' => array( 'Accept' => 'application/activity+json', 'Content-Type' => 'application/activity+json', - 'Signature' => $signature, - 'Date' => $date, + 'Date' => \gmdate( 'D, d M Y H:i:s T' ), ), + 'key_id' => Actors::get_by_id( Actors::APPLICATION_USER_ID )->get_id() . '#main-key', + 'private_key' => Actors::get_private_key( Actors::APPLICATION_USER_ID ), ); + $args = \wp_parse_args( $args, $defaults ); + $args['headers'] = \wp_parse_args( $args['headers'], $defaults['headers'] ); + $response = \wp_safe_remote_get( $url, $args ); $code = \wp_remote_retrieve_response_code( $response ); - if ( $code >= 400 ) { - $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) ); + if ( \is_wp_error( $response ) || $code >= 400 ) { + if ( ! $code ) { + $code = 0; + } + $response = new \WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) ); + + /* + * Always cache errors to prevent repeated timeout waits. + * - Retriable errors (timeouts, 5xx): 1 minute (server may recover quickly). + * - Other errors (4xx): 15 minutes (client errors are more permanent). + */ + if ( \in_array( $code, ACTIVITYPUB_RETRY_ERROR_CODES, true ) || 0 === $code ) { + $cache_duration = MINUTE_IN_SECONDS; + } else { + $cache_duration = 15 * MINUTE_IN_SECONDS; + } + + \set_transient( $transient_key, $response, $cache_duration ); + + return $response; } /** * Action to save the response of the remote GET request. * - * @param array|WP_Error $response The response of the remote GET request. - * @param string $url The URL endpoint. + * @param array|\WP_Error $response The response of the remote GET request. + * @param string $url The URL endpoint. */ \do_action( 'activitypub_safe_remote_get_response', $response, $url ); - if ( $cached ) { - $cache_duration = $cached; - if ( ! is_int( $cache_duration ) ) { - $cache_duration = HOUR_IN_SECONDS; - } - \set_transient( $transient_key, $response, $cache_duration ); + // Always cache successful responses. + $cache_duration = $cached; + if ( ! is_int( $cache_duration ) ) { + $cache_duration = HOUR_IN_SECONDS; } + \set_transient( $transient_key, $response, $cache_duration ); return $response; } @@ -192,27 +223,9 @@ class Http { * @return bool True if the URL is a tombstone. */ public static function is_tombstone( $url ) { - /** - * Fires before checking if the URL is a tombstone. - * - * @param string $url The URL to check. - */ - \do_action( 'activitypub_pre_http_is_tombstone', $url ); + _deprecated_function( __METHOD__, '7.3.0', 'Activitypub\Tombstone::exists_remote' ); - $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; + return Tombstone::exists_remote( $url ); } /** @@ -232,17 +245,28 @@ class Http { * @param array|string $url_or_object The Object or the Object URL. * @param bool $cached Optional. Whether the result should be cached. Default true. * - * @return array|WP_Error The Object data as array or WP_Error on failure. + * @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 ) { + /** + * Filters the preemptive return value of a remote object request. + * + * @param array|string|null $response The response. + * @param array|string|null $url_or_object The Object or the Object URL. + */ + $response = apply_filters( 'activitypub_pre_http_get_remote_object', null, $url_or_object ); + if ( null !== $response ) { + return $response; + } + $url = object_to_uri( $url_or_object ); - if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $url ) ) { + if ( Webfinger::is_acct( $url ) ) { $url = Webfinger::resolve( $url ); } if ( ! $url ) { - return new WP_Error( + return new \WP_Error( 'activitypub_no_valid_actor_identifier', \__( 'The "actor" identifier is not valid', 'activitypub' ), array( @@ -252,23 +276,12 @@ class Http { ); } - if ( is_wp_error( $url ) ) { + if ( \is_wp_error( $url ) ) { return $url; } - $transient_key = self::generate_cache_key( $url ); - - // Only check the cache if needed. - if ( $cached ) { - $data = \get_transient( $transient_key ); - - if ( $data ) { - return $data; - } - } - if ( ! \wp_http_validate_url( $url ) ) { - return new WP_Error( + return new \WP_Error( 'activitypub_no_valid_object_url', \__( 'The "object" is/has no valid URL', 'activitypub' ), array( @@ -278,7 +291,7 @@ class Http { ); } - $response = self::get( $url ); + $response = self::get( $url, array(), $cached ); if ( \is_wp_error( $response ) ) { return $response; @@ -288,7 +301,7 @@ class Http { $data = \json_decode( $data, true ); if ( ! $data ) { - return new WP_Error( + return new \WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), array( @@ -298,8 +311,6 @@ class Http { ); } - \set_transient( $transient_key, $data, WEEK_IN_SECONDS ); - return $data; } } diff --git a/wp-content/plugins/activitypub/includes/class-link.php b/wp-content/plugins/activitypub/includes/class-link.php index 783f1fec..cb9e68a5 100644 --- a/wp-content/plugins/activitypub/includes/class-link.php +++ b/wp-content/plugins/activitypub/includes/class-link.php @@ -28,10 +28,7 @@ class Link { * @return array Rhe activity object array. */ public static function filter_activity_object( $activity ) { - /* phpcs:ignore Squiz.PHP.CommentedOutCode.Found - 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 ) ) { + if ( ! empty( $activity['summary'] ) && is_actor( $activity ) ) { $activity['summary'] = self::the_content( $activity['summary'] ); } diff --git a/wp-content/plugins/activitypub/includes/class-mailer.php b/wp-content/plugins/activitypub/includes/class-mailer.php index 095d8396..7be76afe 100644 --- a/wp-content/plugins/activitypub/includes/class-mailer.php +++ b/wp-content/plugins/activitypub/includes/class-mailer.php @@ -2,7 +2,7 @@ /** * Mailer Class. * - * @package ActivityPub + * @package Activitypub */ namespace Activitypub; @@ -20,9 +20,13 @@ class Mailer { \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_handled_follow', array( self::class, 'new_follower' ), 10, 3 ); + \add_action( 'activitypub_inbox_create', array( self::class, 'direct_message' ), 10, 2 ); - \add_action( 'activitypub_inbox_create', array( self::class, 'mention' ), 10, 2 ); + \add_action( 'activitypub_inbox_create', array( self::class, 'mention' ), 20, 2 ); /** After @see \Activitypub\Handler\Create::handle_create() */ + + \add_filter( 'notify_post_author', array( self::class, 'maybe_prevent_comment_notification' ), 10, 2 ); + \add_filter( 'notify_moderator', array( self::class, 'maybe_prevent_comment_notification' ), 10, 2 ); } /** @@ -86,10 +90,33 @@ class Mailer { } $post = \get_post( $comment->comment_post_ID ); - $comment_author_domain = \gethostbyaddr( $comment->comment_author_IP ); + $comment_author_domain = ''; + + // Only attempt to resolve hostname if we have a valid IP address. + if ( \filter_var( $comment->comment_author_IP, FILTER_VALIDATE_IP ) ) { + $comment_author_domain = \gethostbyaddr( $comment->comment_author_IP ); + } + + // Check if this is a reaction to a post or a comment. + if ( 0 === (int) $comment->comment_parent ) { + $notify_message = \sprintf( + /* translators: 1: Comment type, 2: Post title */ + \html_entity_decode( esc_html__( 'New %1$s on your post “%2$s”.', 'activitypub' ) ), + \esc_html( $comment_type['singular'] ), + \esc_html( $post->post_title ) + ) . PHP_EOL . PHP_EOL; + + } else { + $parent_comment = \get_comment( $comment->comment_parent ); + $notify_message = \sprintf( + /* translators: 1: Comment type, 2: Post title, 3: Parent comment author */ + \html_entity_decode( esc_html__( 'New %1$s on your post “%2$s” in reply to %3$s’s comment.', 'activitypub' ) ), + \esc_html( $comment_type['singular'] ), + \esc_html( $post->post_title ), + \esc_html( $parent_comment->comment_author ) + ) . PHP_EOL . PHP_EOL; + } - /* 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. */ @@ -104,10 +131,24 @@ class Mailer { /** * 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. + * @param array $activity The activity object. + * @param int|int[] $user_ids The id(s) of the local blog-user(s). + * @param bool $success True on success, false otherwise. */ - public static function new_follower( $activity, $user_id ) { + public static function new_follower( $activity, $user_ids, $success ) { + // Only send notification if the follow was successful. + if ( ! $success ) { + return; + } + + // Extract the user ID (follows are always for a single user). + $user_id = \is_array( $user_ids ) ? \reset( $user_ids ) : $user_ids; + + // Do not send notifications to the Application user. + if ( Actors::APPLICATION_USER_ID === $user_id ) { + return; + } + if ( $user_id > Actors::BLOG_USER_ID ) { if ( ! \get_user_option( 'activitypub_mailer_new_follower', $user_id ) ) { return; @@ -129,8 +170,14 @@ class Mailer { return; } - if ( empty( $actor['webfinger'] ) ) { - $actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST ); + $actor = self::normalize_actor( $actor ); + + // Replace emoji in actor name and summary. + if ( ! empty( $actor['name'] ) ) { + $actor['name'] = Emoji::replace_for_actor( $actor['name'], $actor['url'] ); + } + if ( ! empty( $actor['summary'] ) ) { + $actor['summary'] = Emoji::replace_for_actor( $actor['summary'], $actor['url'] ); } $template_args = array_merge( @@ -151,7 +198,7 @@ class Mailer { continue; } - $result = Http::get( $actor[ $field ], true ); + $result = Http::get( $actor[ $field ], array(), true ); if ( 200 === \wp_remote_retrieve_response_code( $result ) ) { $body = \json_decode( \wp_remote_retrieve_body( $result ), true ); if ( isset( $body['totalItems'] ) ) { @@ -167,7 +214,7 @@ class Mailer { \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 ) { + $alt_function = static 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 */ @@ -186,152 +233,318 @@ class Mailer { /** * Send a direct message. * - * @param array $activity The activity object. - * @param int $user_id The id of the local blog-user. + * @param array $activity The activity object. + * @param int|int[] $user_ids The id(s) of the local blog-user(s). */ - 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 ) - ) { + public static function direct_message( $activity, $user_ids ) { + // Early return if activity is public or has no recipients. + if ( is_activity_public( $activity ) || empty( $activity['to'] ) ) { return; } - if ( $user_id > Actors::BLOG_USER_ID ) { - if ( ! \get_user_option( 'activitypub_mailer_new_dm', $user_id ) ) { - return; + // Normalize to array. + $user_ids = (array) $user_ids; + + // Build a map of user_id => actor_id and filter to only users in the "to" field. + $recipients = array(); + foreach ( $user_ids as $user_id ) { + $actor = Actors::get_by_id( $user_id ); + if ( \is_wp_error( $actor ) ) { + continue; } - $email = \get_userdata( $user_id )->user_email; - } else { - if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_dm', '1' ) ) { - return; + $actor_id = $actor->get_id(); + if ( \in_array( $actor_id, (array) $activity['to'], true ) ) { + $recipients[ $user_id ] = $actor_id; } - - $email = \get_option( 'admin_email' ); } - $actor = get_remote_metadata_by_actor( $activity['actor'] ); + // No matching recipients. + if ( empty( $recipients ) ) { + return; + } + // Get actor metadata once (shared for all emails). + $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 ); - } + $actor = self::normalize_actor( $actor ); - $template_args = array( - 'activity' => $activity, - 'actor' => $actor, - 'user_id' => $user_id, - ); + // Send email to each recipient. + foreach ( $recipients as $user_id => $actor_id ) { + // Check user preferences. + if ( $user_id > Actors::BLOG_USER_ID ) { + if ( ! \get_user_option( 'activitypub_mailer_new_dm', $user_id ) ) { + continue; + } - /* 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'] ) ); + $email = \get_userdata( $user_id )->user_email; + } else { + if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_dm', '1' ) ) { + continue; + } - \ob_start(); - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-dm.php', false, $template_args ); - $html_message = \ob_get_clean(); + $email = \get_option( 'admin_email' ); + } - $alt_function = function ( $mailer ) use ( $actor, $activity ) { - $content = \html_entity_decode( - \wp_strip_all_tags( - str_replace( '

    ', PHP_EOL . PHP_EOL, $activity['object']['content'] ) - ), - ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 + $template_args = array( + 'activity' => $activity, + 'actor' => $actor, + 'user_id' => $user_id, ); - /* 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"; + /* 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'] ) ); - $mailer->{'AltBody'} = $message; - }; - \add_action( 'phpmailer_init', $alt_function ); + \ob_start(); + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-dm.php', false, $template_args ); + $html_message = \ob_get_clean(); - \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) ); + $alt_function = static function ( $mailer ) use ( $actor, $activity ) { + $content = \html_entity_decode( + \wp_strip_all_tags( + str_replace( '

    ', PHP_EOL . PHP_EOL, $activity['object']['content'] ) + ), + ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 + ); - \remove_action( 'phpmailer_init', $alt_function ); + /* 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. + * @param array $activity The activity object. + * @param int|int[] $user_ids The id(s) of the local blog-user(s). */ - 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 ) - ) { + public static function mention( $activity, $user_ids ) { + // Early return if activity has no mentions. + if ( empty( $activity['object']['tag'] ) ) { 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' ); + // Do not send a mention notification if the activity is a reply to a local post or comment. + if ( is_activity_reply( $activity ) && object_id_to_comment( $activity['object']['id'] ) ) { + return; } + $recipients = array(); + $mentions = wp_list_filter( (array) $activity['object']['tag'], array( 'type' => 'Mention' ) ); + $mentions = array_map( '\Activitypub\object_to_uri', $mentions ); + foreach ( (array) $user_ids as $user_id ) { + $actor = Actors::get_by_id( $user_id ); + if ( \is_wp_error( $actor ) ) { + continue; + } + + $actor_id = $actor->get_id(); + if ( \in_array( $actor_id, $mentions, true ) ) { + $recipients[ $user_id ] = $actor_id; + } + } + + // No matching recipients. + if ( empty( $recipients ) ) { + return; + } + + // Get actor metadata once (shared for all emails). $actor = get_remote_metadata_by_actor( $activity['actor'] ); if ( \is_wp_error( $actor ) ) { return; } + $actor = self::normalize_actor( $actor ); + + // Send email to each recipient. + foreach ( $recipients as $user_id => $actor_id ) { + // Check user preferences. + if ( $user_id > Actors::BLOG_USER_ID ) { + if ( ! \get_user_option( 'activitypub_mailer_new_mention', $user_id ) ) { + continue; + } + + $email = \get_userdata( $user_id )->user_email; + } else { + if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_mention', '1' ) ) { + continue; + } + + $email = \get_option( 'admin_email' ); + } + + $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 = static function ( $mailer ) use ( $actor, $activity ) { + $content = \html_entity_decode( + \wp_strip_all_tags( + str_replace( '

    ', 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 ); + } + } + + /** + * Send a templated email to a user. + * + * @param int $user_id The user ID (or BLOG_USER_ID for blog actor). + * @param string $subject The email subject. + * @param string $template The template name (without path/extension). + * @param array $args Template arguments. + * @param string $alt_body Optional plain text alternative. Auto-generated from HTML if empty. + * + * @return bool True if email was sent, false otherwise. + */ + public static function send( $user_id, $subject, $template, $args = array(), $alt_body = '' ) { + // Get the recipient email address. + if ( $user_id > Actors::BLOG_USER_ID ) { + $user = \get_userdata( $user_id ); + if ( ! $user || empty( $user->user_email ) ) { + return false; + } + $email = $user->user_email; + } else { + $email = \get_option( 'admin_email' ); + } + + // Load the HTML template. + $template_file = ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/' . \sanitize_file_name( $template ) . '.php'; + + /** + * Filter the email template file path. + * + * @param string $template_file The template file path. + * @param string $template The template name. + * @param int $user_id The user ID. + * @param array $args Template arguments. + */ + $template_file = \apply_filters( 'activitypub_email_template', $template_file, $template, $user_id, $args ); + + if ( ! \file_exists( $template_file ) ) { + return false; + } + + \ob_start(); + \load_template( $template_file, false, $args ); + $html_message = \ob_get_clean(); + + // Build plain text alternative from HTML if not provided. + if ( empty( $alt_body ) ) { + $alt_body = \wp_strip_all_tags( $html_message ); + } + $alt_function = static function ( $mailer ) use ( $alt_body ) { + $mailer->{'AltBody'} = $alt_body; + }; + \add_action( 'phpmailer_init', $alt_function ); + + $result = \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) ); + + \remove_action( 'phpmailer_init', $alt_function ); + + return $result; + } + + /** + * Apply defaults to the actor object. + * + * Ensure that the actor object has a name, url, and webfinger. + * + * @param array $actor The actor object. + * + * @return array The inflated actor object. + */ + private static function normalize_actor( $actor ) { + if ( empty( $actor['name'] ) ) { + $actor['name'] = $actor['preferredUsername']; + } + + if ( empty( $actor['url'] ) ) { + $actor['url'] = $actor['id']; + } + $actor['url'] = object_to_uri( $actor['url'] ); + 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, - ); + return $actor; + } - /* 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'] ) ); + /** + * Maybe prevent email notifications for comments. + * + * This filter can prevent both post author and moderator notifications + * for comments on specific post types, such as ActivityPub custom post types. + * + * @param bool $maybe_notify Whether to send the notification. + * @param int $comment_id The comment ID. + * + * @return bool False to prevent notification, original value otherwise. + */ + public static function maybe_prevent_comment_notification( $maybe_notify, $comment_id ) { + // If already disabled, respect that. + if ( ! $maybe_notify ) { + return $maybe_notify; + } - \ob_start(); - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-mention.php', false, $template_args ); - $html_message = \ob_get_clean(); + $comment = \get_comment( $comment_id ); + if ( ! $comment ) { + return $maybe_notify; + } - $alt_function = function ( $mailer ) use ( $actor, $activity ) { - $content = \html_entity_decode( - \wp_strip_all_tags( - str_replace( '

    ', PHP_EOL . PHP_EOL, $activity['object']['content'] ) - ), - ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 - ); + $post = \get_post( $comment->comment_post_ID ); + if ( ! $post ) { + return $maybe_notify; + } - /* 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"; + // Prevent notifications for comments on ap_post. + if ( is_ap_post( $post ) ) { + return false; + } - $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 ); + return $maybe_notify; } } diff --git a/wp-content/plugins/activitypub/includes/class-mention.php b/wp-content/plugins/activitypub/includes/class-mention.php index 93ada0bf..41a25e4d 100644 --- a/wp-content/plugins/activitypub/includes/class-mention.php +++ b/wp-content/plugins/activitypub/includes/class-mention.php @@ -7,7 +7,7 @@ namespace Activitypub; -use WP_Error; +use Activitypub\Collection\Remote_Actors; /** * ActivityPub Mention Class. @@ -19,9 +19,9 @@ class Mention { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_filter( 'the_content', array( self::class, 'the_content' ), 99, 1 ); - \add_filter( 'comment_text', array( self::class, 'the_content' ), 10, 1 ); - \add_filter( 'activitypub_extra_field_content', array( self::class, 'the_content' ), 10, 1 ); + \add_filter( 'the_content', array( self::class, 'the_content' ), 99 ); + \add_filter( 'comment_text', array( self::class, 'the_content' ) ); + \add_filter( 'activitypub_extra_field_content', array( self::class, 'the_content' ) ); \add_filter( 'activitypub_extract_mentions', array( self::class, 'extract_mentions' ), 99, 2 ); \add_filter( 'activitypub_activity_object_array', array( self::class, 'filter_activity_object' ), 99 ); } @@ -65,27 +65,22 @@ class Mention { * @return string The final string. */ public static function replace_with_links( $result ) { - $metadata = get_remote_metadata_by_actor( $result[0] ); + $post = Remote_Actors::fetch_by_acct( $result[0] ); - if ( - ! empty( $metadata ) && - ! is_wp_error( $metadata ) && - ( ! empty( $metadata['id'] ) || ! empty( $metadata['url'] ) ) - ) { - $username = ltrim( $result[0], '@' ); - if ( ! empty( $metadata['name'] ) ) { - $username = $metadata['name']; - } - if ( ! empty( $metadata['preferredUsername'] ) ) { - $username = $metadata['preferredUsername']; - } - - $url = isset( $metadata['url'] ) ? object_to_uri( $metadata['url'] ) : object_to_uri( $metadata['id'] ); - - return \sprintf( '@%2$s', esc_url( $url ), esc_html( $username ) ); + if ( \is_wp_error( $post ) ) { + return $result[0]; } - return $result[0]; + $actor = Remote_Actors::get_actor( $post ); + + if ( \is_wp_error( $actor ) ) { + return $result[0]; + } + + $username = $actor->get_preferred_username() ?: $actor->get_name() ?: Sanitize::webfinger( $result[0] ); + $url = object_to_uri( $actor->get_url() ?: $actor->get_id() ); + + return \sprintf( '@%2$s', esc_url( $url ), esc_html( $username ) ); } /** @@ -114,7 +109,7 @@ class Mention { * * @param string $actor The Actor URL. * - * @return string|WP_Error The Inbox-URL or WP_Error if not found. + * @return string|\WP_Error The Inbox-URL or WP_Error if not found. */ public static function get_inbox_by_mentioned_actor( $actor ) { $metadata = get_remote_metadata_by_actor( $actor ); @@ -131,7 +126,7 @@ class Mention { return $metadata['inbox']; } - return new WP_Error( 'activitypub_no_inbox', \__( 'No "Inbox" found', 'activitypub' ), $metadata ); + return new \WP_Error( 'activitypub_no_inbox', \__( 'No "Inbox" found', 'activitypub' ), $metadata ); } /** diff --git a/wp-content/plugins/activitypub/includes/class-migration.php b/wp-content/plugins/activitypub/includes/class-migration.php index 9132f6b6..7c4c7df8 100644 --- a/wp-content/plugins/activitypub/includes/class-migration.php +++ b/wp-content/plugins/activitypub/includes/class-migration.php @@ -10,7 +10,9 @@ namespace Activitypub; use Activitypub\Collection\Actors; use Activitypub\Collection\Extra_Fields; use Activitypub\Collection\Followers; +use Activitypub\Collection\Following; use Activitypub\Collection\Outbox; +use Activitypub\Collection\Remote_Actors; use Activitypub\Transformer\Factory; /** @@ -23,27 +25,16 @@ class Migration { * Initialize the class, registering WordPress hooks. */ 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(); - } - /** - * Get the target version. - * - * 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() { - _deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' ); - - return ACTIVITYPUB_PLUGIN_VERSION; + Scheduler::register_async_batch_callback( 'activitypub_migrate_from_0_17', array( self::class, 'migrate_from_0_17' ) ); + Scheduler::register_async_batch_callback( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ) ); + Scheduler::register_async_batch_callback( 'activitypub_create_post_outbox_items', array( self::class, 'create_post_outbox_items' ) ); + Scheduler::register_async_batch_callback( 'activitypub_create_comment_outbox_items', array( self::class, 'create_comment_outbox_items' ) ); + Scheduler::register_async_batch_callback( 'activitypub_migrate_avatar_to_remote_actors', array( self::class, 'migrate_avatar_to_remote_actors' ) ); + Scheduler::register_async_batch_callback( 'activitypub_migrate_actor_emoji', array( self::class, 'migrate_actor_emoji' ) ); + Scheduler::register_async_batch_callback( 'activitypub_backfill_statistics', array( Statistics::class, 'backfill_historical_stats' ) ); + Scheduler::register_async_batch_callback( 'activitypub_tombstone_migrate', array( self::class, 'migrate_tombstones_to_cpt' ) ); } /** @@ -137,13 +128,12 @@ class Migration { $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', '<' ) ) { self::migrate_from_0_16(); } + if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) { + \wp_schedule_single_event( \time(), 'activitypub_migrate_from_0_17' ); + } if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) { self::migrate_from_1_2_0(); } @@ -171,22 +161,14 @@ class Migration { 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(); + \wp_schedule_single_event( \time(), 'activitypub_create_post_outbox_items' ); + \wp_schedule_single_event( \time() + 15, 'activitypub_create_comment_outbox_items' ); } 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(); @@ -194,6 +176,59 @@ class Migration { if ( \version_compare( $version_from_db, '5.8.0', '<' ) ) { self::update_notification_options(); } + if ( \version_compare( $version_from_db, '6.0.0', '<' ) ) { + self::migrate_followers_to_ap_actor_cpt(); + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) ); + } + if ( \version_compare( $version_from_db, '6.0.1', '<' ) ) { + self::migrate_followers_to_ap_actor_cpt(); + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_storage' ) ); + } + if ( \version_compare( $version_from_db, '7.0.0', '<' ) ) { + wp_unschedule_hook( 'activitypub_update_followers' ); + wp_unschedule_hook( 'activitypub_cleanup_followers' ); + + if ( ! \wp_next_scheduled( 'activitypub_update_remote_actors' ) ) { + \wp_schedule_event( time(), 'hourly', 'activitypub_update_remote_actors' ); + } + + if ( ! \wp_next_scheduled( 'activitypub_cleanup_remote_actors' ) ) { + \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_remote_actors' ); + } + } + if ( \version_compare( $version_from_db, '7.3.0', '<' ) ) { + self::remove_pending_application_user_follow_requests(); + } + if ( \version_compare( $version_from_db, '7.5.0', '<' ) ) { + self::sync_jetpack_following_meta(); + } + if ( \version_compare( $version_from_db, '7.6.0', '<' ) ) { + self::clean_up_inbox(); + \wp_schedule_single_event( \time(), 'activitypub_migrate_avatar_to_remote_actors' ); + } + if ( \version_compare( $version_from_db, '7.9.0', '<' ) ) { + \wp_schedule_single_event( \time(), 'activitypub_migrate_actor_emoji' ); + } + if ( \version_compare( $version_from_db, '8.1.0', '<' ) && ! \wp_next_scheduled( 'activitypub_backfill_statistics' ) ) { + // Backfill historical statistics data (delay + jitter to avoid load spikes on hosts running many sites). + \wp_schedule_single_event( \time() + HOUR_IN_SECONDS + \wp_rand( 0, 6 * HOUR_IN_SECONDS ), 'activitypub_backfill_statistics' ); + } + if ( \version_compare( $version_from_db, '8.3.0', '<' ) ) { + if ( ! \wp_next_scheduled( 'activitypub_tombstone_migrate' ) ) { + \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_tombstone_migrate' ); + } + } + + /* + * Defer the flush to late in the `init` cycle (priority 20). Migration::init + * runs at priority 1, which is earlier than most plugins register their + * rewrite rules. Flushing synchronously here would persist a truncated + * ruleset that omits third-party rules added on `init` at priority 10. + */ + \add_action( 'init', array( Activitypub::class, 'flush_rewrite_rules' ), 20 ); + + // Ensure all required cron schedules are registered. + Scheduler::register_schedules(); /* * Add new update routines above this comment. ^ @@ -220,49 +255,6 @@ class Migration { self::unlock(); } - /** - * Asynchronously migrates the database structure. - * - * @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', '<' ) ) { - 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. */ @@ -310,12 +302,10 @@ class Migration { if ( $followers ) { foreach ( $followers as $actor ) { - Followers::add_follower( $user_id, $actor ); + Followers::add( $user_id, $actor ); } } } - - Activitypub::flush_rewrite_rules(); } /** @@ -404,14 +394,14 @@ class Migration { ); foreach ( $users as $user ) { - $followers = Followers::get_followers( $user->ID ); + $followers = Followers::get_many( $user->ID ); if ( $followers ) { \update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' ); } } - $followers = Followers::get_followers( Actors::BLOG_USER_ID ); + $followers = Followers::get_many( Actors::BLOG_USER_ID ); if ( $followers ) { \update_option( 'activitypub_use_permalink_as_id_for_blog', '1' ); @@ -421,7 +411,7 @@ class Migration { } /** - * Upate to 4.1.0 + * Update to 4.1.0 * * * Migrate the `activitypub_post_content_type` to only use `activitypub_custom_post_content`. */ @@ -491,7 +481,7 @@ class Migration { global $wpdb; // phpcs:ignore WordPress.DB $followers = $wpdb->get_col( - $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Followers::POST_TYPE ) + $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Remote_Actors::POST_TYPE ) ); foreach ( $followers as $id ) { clean_post_cache( $id ); @@ -504,25 +494,12 @@ class Migration { * @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. + * + * @return int[]|void Array with batch size and offset if there are more posts to process. */ 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 ) . "')"; @@ -543,17 +520,8 @@ class Migration { 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, - ) - ); + return array( $batch_size, $offset + $batch_size ); } - - self::unlock(); } /** @@ -574,7 +542,7 @@ class Migration { 'meta_query' => array( array( 'key' => 'activitypub_status', - 'value' => 'federated', + 'value' => ACTIVITYPUB_OBJECT_STATE_FEDERATED, ), ), ) @@ -621,7 +589,7 @@ class Migration { 'meta_query' => array( array( 'key' => 'activitypub_status', - 'value' => 'federated', + 'value' => ACTIVITYPUB_OBJECT_STATE_FEDERATED, ), ), ) @@ -697,6 +665,7 @@ class Migration { array( 'number' => $batch_size, 'offset' => $offset, + 'orderby' => 'comment_ID', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( @@ -835,7 +804,7 @@ class Migration { } /** - * Rename meta keys. + * Rename user meta keys. * * @param string $old_key The old comment meta key. * @param string $new_key The new comment meta key. @@ -852,6 +821,24 @@ class Migration { ); } + /** + * Update post meta keys. + * + * @param string $old_key The old post meta key. + * @param string $new_key The new post meta key. + */ + private static function update_postmeta_key( $old_key, $new_key ) { + global $wpdb; + + $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->postmeta, + array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + array( '%s' ), + array( '%s' ) + ); + } + /** * Rename option keys. * @@ -930,15 +917,417 @@ class Migration { \add_option( 'activitypub_blog_user_mailer_new_follower', $new_follower ); \add_option( 'activitypub_blog_user_mailer_new_mention', '1' ); + $user_ids = \get_users( + array( + 'capability__in' => array( 'activitypub' ), + 'fields' => 'id', + ) + ); + // 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' ); + foreach ( $user_ids as $user_id ) { + \update_user_option( $user_id, 'activitypub_mailer_new_dm', $new_dm ); + \update_user_option( $user_id, 'activitypub_mailer_new_follower', $new_follower ); + \update_user_option( $user_id, 'activitypub_mailer_new_mention', '1' ); } // Delete the old notification options. \delete_option( 'activitypub_mailer_new_dm' ); \delete_option( 'activitypub_mailer_new_follower' ); } + + /** + * Migrate followers to the new CPT. + */ + public static function migrate_followers_to_ap_actor_cpt() { + global $wpdb; + + $wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->posts, + array( 'post_type' => Remote_Actors::POST_TYPE ), + array( 'post_type' => 'ap_follower' ), + array( '%s' ), + array( '%s' ) + ); + + self::update_postmeta_key( '_activitypub_user_id', Followers::FOLLOWER_META_KEY ); + } + + /** + * 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. + * + * @return array|void Array with batch size and offset if there are more meta values to process, void otherwise. + */ + public static function update_actor_json_storage( $batch_size = 100 ) { + 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", + $batch_size + ) + ); + + $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(); + } + + foreach ( $meta_values as $meta ) { + $post = \get_post( $meta->post_id ); + + if ( ! $post ) { + \delete_post_meta( $meta->post_id, '_activitypub_actor_json' ); + continue; + } + + $post_content = \json_decode( $meta->meta_value, true ); + + if ( \json_last_error() !== JSON_ERROR_NONE ) { + $post_content = Http::get_remote_object( $post->guid ); + + if ( \is_wp_error( $post_content ) ) { + \delete_post_meta( $post->ID, '_activitypub_actor_json' ); + continue; + } + } + + \wp_update_post( + array( + 'ID' => $post->ID, + 'post_content' => \wp_slash( \wp_json_encode( $post_content ) ), + ) + ); + + \delete_post_meta( $post->ID, '_activitypub_actor_json' ); + } + + if ( $has_kses ) { + // Restore KSES filters. + \kses_init_filters(); + } + + if ( \count( $meta_values ) === $batch_size ) { + return array( + 'batch_size' => $batch_size, + ); + } + } + + /** + * Removes pending follow requests for the application user. + */ + public static function remove_pending_application_user_follow_requests() { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->delete( + $wpdb->postmeta, + array( + 'meta_key' => '_activitypub_following', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key + 'meta_value' => Actors::APPLICATION_USER_ID, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + ) + ); + } + + /** + * Sync Jetpack meta for all followings. + * + * Replays the added_post_meta sync action for Jetpack with the Following::FOLLOWING_META_KEY meta key. + */ + public static function sync_jetpack_following_meta() { + if ( ! \class_exists( 'Jetpack' ) || ! \Jetpack::is_connection_ready() ) { + return; + } + + global $wpdb; + + // Get all posts that have the following meta key. + $posts_with_following = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->prepare( + "SELECT meta_id, post_id, meta_key, meta_value FROM {$wpdb->postmeta} WHERE meta_key = %s", + Following::FOLLOWING_META_KEY + ), + ARRAY_N + ); + + // Trigger the added_post_meta action for each following relationship. + foreach ( $posts_with_following as $meta ) { + /** + * Fires when post meta is added. + * + * @param int $meta_id ID of the metadata entry. + * @param int $object_id Post ID. + * @param string $meta_key Metadata key. + * @param mixed $meta_value Metadata value. + */ + \do_action( 'added_post_meta', ...$meta ); + } + } + + /** + * Clean up inbox items for shared inbox migration. + * + * Deletes all existing inbox items to prepare for the new shared inbox structure + * where activities are stored once with multiple recipients as metadata. + */ + private static function clean_up_inbox() { + global $wpdb; + + // Get all inbox post IDs. + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $inbox_ids = $wpdb->get_col( + $wpdb->prepare( + "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", + \Activitypub\Collection\Inbox::POST_TYPE + ) + ); + + // Delete all inbox items and their metadata. + foreach ( $inbox_ids as $post_id ) { + \wp_delete_post( $post_id, true ); + } + } + + /** + * Migrate URLs from the legacy `activitypub_tombstone_urls` option into the + * `ap_tombstone` custom post type. + * + * Chunked async migration. Locking and rescheduling is handled by + * Scheduler::async_batch — the callback returns `array( 'batch_size' => N )` + * to request another run, or `null` when the option is fully drained. + * + * Legacy entries are already-normalized strings (no scheme), so we bypass + * URL validation and insert directly via wp_insert_post. + * + * @since 8.3.0 + * + * @param int $batch_size Optional. Number of URLs to process per call. Default 500. + * @return array|null Args for the next run, or null when migration is complete. + */ + public static function migrate_tombstones_to_cpt( $batch_size = 500 ) { + global $wpdb; + + $urls = \get_option( 'activitypub_tombstone_urls', null ); + + if ( null === $urls || ! \is_array( $urls ) || empty( $urls ) ) { + \delete_option( 'activitypub_tombstone_urls' ); + return null; + } + + $chunk = \array_slice( $urls, 0, (int) $batch_size ); + $remaining = \array_slice( $urls, (int) $batch_size ); + $progressed = false; + + foreach ( $chunk as $normalized ) { + if ( ! \is_string( $normalized ) || '' === $normalized ) { + // Drop garbage entries — counts as progress. + $progressed = true; + continue; + } + + $hash = \md5( $normalized ); + + /* + * Light existence check. `get_page_by_path()` would hydrate a + * full `WP_Post` per loop iteration; on a large registry that + * adds up fast. We only need a boolean here. + */ + $exists = $wpdb->get_var( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->prepare( + "SELECT 1 FROM {$wpdb->posts} WHERE post_type = %s AND post_name = %s LIMIT 1", + Tombstone::POST_TYPE, + $hash + ) + ); + if ( $exists ) { + $progressed = true; + continue; + } + + /* + * `guid` is intentionally omitted: the legacy option only kept + * the normalized (schemeless) form, so we can't reconstruct the + * original URL. Storing the schemeless string would be mangled + * by `esc_url()`. Leave WordPress to auto-generate the guid + * — it's not used for lookups, only for debugging. + */ + $result = \wp_insert_post( + array( + 'post_type' => Tombstone::POST_TYPE, + 'post_status' => 'publish', + 'post_name' => $hash, + 'post_author' => 0, + ), + true + ); + + if ( \is_wp_error( $result ) || ! $result ) { + /* + * Keep failed inserts in the legacy option so the next batch + * retries them. `Tombstone::exists_local()` still falls back + * to the option, so the tombstone remains discoverable. + */ + $remaining[] = $normalized; + } else { + $progressed = true; + } + } + + if ( empty( $remaining ) ) { + \delete_option( 'activitypub_tombstone_urls' ); + return null; + } + + /* + * Disable autoload while we drain. The point of the migration is to + * stop this option from contributing to `alloptions` pressure, so + * flip the flag immediately rather than waiting for the option to + * be fully empty before the relief kicks in. + */ + \update_option( 'activitypub_tombstone_urls', \array_values( $remaining ), false ); + + /* + * If nothing in this batch was drained — every insert errored and + * nothing was already migrated — halt the scheduler so we don't loop + * forever on a persistent failure. The legacy option still backs + * exists_local(), so the data isn't lost; an admin can re-trigger + * the migration via `wp cron event run activitypub_tombstone_migrate` + * after fixing the underlying cause. + */ + if ( ! $progressed ) { + return null; + } + + return array( 'batch_size' => (int) $batch_size ); + } + + /** + * Migrate avatar URLs from comment meta to remote actors in batches. + * + * This migration: + * 1. Finds all comments with ActivityPub protocol and avatar_url meta + * 2. Looks up the remote actor by comment_author_url + * 3. Adds _activitypub_remote_actor_id to comment meta + * 4. Stores avatar_url in remote actor post meta + * + * Note: We don't use offset because as we add _activitypub_remote_actor_id, + * comments are filtered out of the query. We just keep fetching the next + * batch until no more comments match the criteria. + * + * @param int $batch_size Optional. Number of comments to process per batch. Default 50. + * @return array|null Array with batch size if there are more comments to process, null otherwise. + */ + public static function migrate_avatar_to_remote_actors( $batch_size = 50 ) { + global $wpdb; + + /* + * Get comments with avatar_url meta that don't have _activitypub_remote_actor_id yet. + * Uses conditional aggregation to reduce JOINs from 3 to 1, improving query performance. + * Filters meta_key before GROUP BY to reduce rows processed during aggregation. + * No offset needed - as we process comments, they're filtered out by the HAVING clause. + */ + $comments = $wpdb->get_results( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->prepare( + "SELECT c.comment_ID, c.comment_author_url, + MAX(CASE WHEN cm.meta_key = 'avatar_url' THEN cm.meta_value END) AS avatar_url, + MAX(CASE WHEN cm.meta_key = 'protocol' THEN cm.meta_value END) AS protocol, + MAX(CASE WHEN cm.meta_key = '_activitypub_remote_actor_id' THEN cm.meta_value END) AS remote_actor_id + FROM {$wpdb->comments} c + INNER JOIN {$wpdb->commentmeta} cm ON c.comment_ID = cm.comment_id + WHERE cm.meta_key IN ('avatar_url', 'protocol', '_activitypub_remote_actor_id') + GROUP BY c.comment_ID, c.comment_author_url + HAVING protocol = 'activitypub' + AND avatar_url IS NOT NULL + AND (remote_actor_id IS NULL OR remote_actor_id = '') + LIMIT %d", + $batch_size + ) + ); + + foreach ( $comments as $comment ) { + if ( empty( $comment->comment_author_url ) ) { + continue; + } + + // Try to get the remote actor by URI. + $remote_actor = Remote_Actors::fetch_by_uri( $comment->comment_author_url ); + + // If we have a valid remote actor, store the reference. + if ( ! \is_wp_error( $remote_actor ) ) { + // Add _activitypub_remote_actor_id to comment meta. + \add_comment_meta( $comment->comment_ID, '_activitypub_remote_actor_id', $remote_actor->ID, true ); + + // Ensure avatar is stored on remote actor if not already present. + $existing_avatar = \get_post_meta( $remote_actor->ID, '_activitypub_avatar_url', true ); + if ( empty( $existing_avatar ) && ! empty( $comment->avatar_url ) ) { + \update_post_meta( $remote_actor->ID, '_activitypub_avatar_url', \esc_url_raw( $comment->avatar_url ) ); + } + } + } + + // Return batch info if there are more comments to process. + if ( count( $comments ) === $batch_size ) { + return array( + 'batch_size' => $batch_size, + ); + } + + return null; + } + + /** + * Migrate emoji data from stored actor JSON to post meta. + * + * This migration: + * 1. Finds all remote actor posts without _activitypub_emoji meta + * 2. Extracts emoji from stored JSON in post_content + * 3. Stores as _activitypub_emoji post meta + * + * @param int $batch_size Optional. Number of actors to process per batch. Default 50. + * @param int $offset Optional. Offset for pagination. Default 0. + * @return array|null Array with batch size if there are more actors to process, null otherwise. + */ + public static function migrate_actor_emoji( $batch_size = 50, $offset = 0 ) { + $actors = \get_posts( + array( + 'post_type' => Remote_Actors::POST_TYPE, + 'posts_per_page' => $batch_size, + 'offset' => $offset, + 'post_status' => 'any', + 'orderby' => 'ID', + 'order' => 'ASC', + ) + ); + + foreach ( $actors as $actor_post ) { + if ( empty( $actor_post->post_content ) ) { + continue; + } + + $actor_data = \json_decode( $actor_post->post_content, true ); + if ( ! $actor_data ) { + continue; + } + + $emoji_meta = Emoji::prepare_actor_meta( $actor_data ); + if ( ! empty( $emoji_meta['_activitypub_emoji'] ) ) { + \update_post_meta( $actor_post->ID, '_activitypub_emoji', $emoji_meta['_activitypub_emoji'] ); + } + } + + // Return batch info if there are more actors to process. + if ( count( $actors ) === $batch_size ) { + return array( + 'batch_size' => $batch_size, + 'offset' => $offset + $batch_size, + ); + } + + return null; + } } diff --git a/wp-content/plugins/activitypub/includes/class-moderation.php b/wp-content/plugins/activitypub/includes/class-moderation.php new file mode 100644 index 00000000..b28164b0 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-moderation.php @@ -0,0 +1,426 @@ + 'activitypub_blocked_domains', + self::TYPE_KEYWORD => 'activitypub_blocked_keywords', + ); + + /** + * Option key for site-wide blocked keywords. + */ + const OPTION_KEYS = array( + self::TYPE_DOMAIN => 'activitypub_site_blocked_domains', + self::TYPE_KEYWORD => 'activitypub_site_blocked_keywords', + ); + + /** + * Check if an activity should be blocked for a specific user. + * + * @param Activity $activity The activity. + * @param int|null $user_id The user ID to check blocks for. + * @return bool True if blocked, false otherwise. + */ + public static function activity_is_blocked( $activity, $user_id = null ) { + if ( ! $activity instanceof Activity ) { + return false; + } + + // First check site-wide blocks (admin moderation). + if ( self::activity_is_blocked_site_wide( $activity ) ) { + return true; + } + + // Then check user-specific blocks. + if ( $user_id && self::activity_is_blocked_for_user( $activity, $user_id ) ) { + return true; + } + + $remote_addr = \sanitize_text_field( \wp_unslash( $_SERVER['REMOTE_ADDR'] ?? '' ) ); + $user_agent = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_USER_AGENT'] ?? '' ) ); + + // Fall back to WordPress comment disallowed list. + return \wp_check_comment_disallowed_list( $activity->to_json( false ), '', '', $activity->get_content(), $remote_addr, $user_agent ); + } + + /** + * Check if an activity is blocked site-wide. + * + * @param Activity $activity The activity. + * @return bool True if blocked, false otherwise. + */ + public static function activity_is_blocked_site_wide( $activity ) { + $blocks = self::get_site_blocks(); + + return self::check_activity_against_blocks( $activity, $blocks['actors'], $blocks['domains'], $blocks['keywords'] ); + } + + /** + * Check if an activity is blocked for a specific user. + * + * @param Activity $activity The activity. + * @param int $user_id The user ID. + * @return bool True if blocked, false otherwise. + */ + public static function activity_is_blocked_for_user( $activity, $user_id ) { + $blocks = self::get_user_blocks( $user_id ); + + return self::check_activity_against_blocks( $activity, $blocks['actors'], $blocks['domains'], $blocks['keywords'] ); + } + + /** + * Add a block for a user. + * + * @param int $user_id The user ID. + * @param string $type The block type (actor, domain, keyword). + * @param string $value The value to block. + * @return bool True on success, false on failure. + */ + public static function add_user_block( $user_id, $type, $value ) { + switch ( $type ) { + case self::TYPE_ACTOR: + return Blocked_Actors::add( $user_id, $value ); + + case self::TYPE_DOMAIN: + case self::TYPE_KEYWORD: + $blocks = \get_user_meta( $user_id, self::USER_META_KEYS[ $type ], true ) ?: array(); + + if ( ! \in_array( $value, $blocks, true ) ) { + /** + * Fired when a domain or keyword is blocked. + * + * @param string $value The blocked domain or keyword. + * @param string $type The block type (actor, domain, keyword). + * @param int $user_id The user ID. + */ + \do_action( 'activitypub_add_user_block', $value, $type, $user_id ); + + $blocks[] = $value; + return (bool) \update_user_meta( $user_id, self::USER_META_KEYS[ $type ], $blocks ); + } + break; + } + + return true; // Already blocked. + } + + /** + * Remove a block for a user. + * + * @param int $user_id The user ID. + * @param string $type The block type (actor, domain, keyword). + * @param string $value The value to unblock. + * @return bool True on success, false on failure. + */ + public static function remove_user_block( $user_id, $type, $value ) { + switch ( $type ) { + case self::TYPE_ACTOR: + return Blocked_Actors::remove( $user_id, $value ); + + case self::TYPE_DOMAIN: + case self::TYPE_KEYWORD: + $blocks = \get_user_meta( $user_id, self::USER_META_KEYS[ $type ], true ) ?: array(); + $key = \array_search( $value, $blocks, true ); + + if ( false !== $key ) { + /** + * Fired when a domain or keyword is unblocked. + * + * @param string $value The unblocked domain or keyword. + * @param string $type The block type (actor, domain, keyword). + * @param int $user_id The user ID. + */ + \do_action( 'activitypub_remove_user_block', $value, $type, $user_id ); + + unset( $blocks[ $key ] ); + return \update_user_meta( $user_id, self::USER_META_KEYS[ $type ], \array_values( $blocks ) ); + } + break; + } + + return true; // Not blocked anyway. + } + + /** + * Get all blocks for a user. + * + * @param int $user_id The user ID. + * @return array Array of blocks organized by type. + */ + public static function get_user_blocks( $user_id ) { + return array( + 'actors' => \wp_list_pluck( Blocked_Actors::get_many( $user_id ), 'guid' ), + 'domains' => \get_user_meta( $user_id, self::USER_META_KEYS[ self::TYPE_DOMAIN ], true ) ?: array(), + 'keywords' => \get_user_meta( $user_id, self::USER_META_KEYS[ self::TYPE_KEYWORD ], true ) ?: array(), + ); + } + + /** + * Add a site-wide block. + * + * @param string $type The block type (actor, domain, keyword). + * @param string $value The value to block. + * @return bool True on success, false on failure. + */ + public static function add_site_block( $type, $value ) { + switch ( $type ) { + case self::TYPE_ACTOR: + // Site-wide actor blocking uses the BLOG_USER_ID. + return self::add_user_block( Actors::BLOG_USER_ID, self::TYPE_ACTOR, $value ); + + case self::TYPE_DOMAIN: + case self::TYPE_KEYWORD: + $blocks = \get_option( self::OPTION_KEYS[ $type ], array() ); + + if ( ! \in_array( $value, $blocks, true ) ) { + /** + * Fired when a domain or keyword is blocked site-wide. + * + * @param string $value The blocked domain or keyword. + * @param string $type The block type (actor, domain, keyword). + */ + \do_action( 'activitypub_add_site_block', $value, $type ); + + $blocks[] = $value; + return \update_option( self::OPTION_KEYS[ $type ], $blocks ); + } + break; + } + + return true; // Already blocked. + } + + /** + * Add multiple site-wide blocks at once. + * + * More efficient than calling add_site_block() in a loop as it + * performs a single database update. + * + * @param string $type The block type (domain or keyword only). + * @param array $values Array of values to block. + */ + public static function add_site_blocks( $type, $values ) { + if ( ! in_array( $type, array( self::TYPE_DOMAIN, self::TYPE_KEYWORD ), true ) ) { + return; + } + + if ( empty( $values ) ) { + return; + } + + foreach ( $values as $value ) { + /** + * Fired when a domain or keyword is blocked site-wide. + * + * @param string $value The blocked domain or keyword. + * @param string $type The block type (actor, domain, keyword). + */ + \do_action( 'activitypub_add_site_block', $value, $type ); + } + + $existing = \get_option( self::OPTION_KEYS[ $type ], array() ); + \update_option( self::OPTION_KEYS[ $type ], array_unique( array_merge( $existing, $values ) ) ); + } + + /** + * Remove a site-wide block. + * + * @param string $type The block type (actor, domain, keyword). + * @param string $value The value to unblock. + * @return bool True on success, false on failure. + */ + public static function remove_site_block( $type, $value ) { + switch ( $type ) { + case self::TYPE_ACTOR: + // Site-wide actor unblocking uses the BLOG_USER_ID. + return self::remove_user_block( Actors::BLOG_USER_ID, self::TYPE_ACTOR, $value ); + + case self::TYPE_DOMAIN: + case self::TYPE_KEYWORD: + $blocks = \get_option( self::OPTION_KEYS[ $type ], array() ); + $key = \array_search( $value, $blocks, true ); + + if ( false !== $key ) { + /** + * Fired when a domain or keyword is unblocked site-wide. + * + * @param string $value The unblocked domain or keyword. + * @param string $type The block type (actor, domain, keyword). + */ + \do_action( 'activitypub_remove_site_block', $value, $type ); + + unset( $blocks[ $key ] ); + return \update_option( self::OPTION_KEYS[ $type ], \array_values( $blocks ) ); + } + break; + } + + return true; // Not blocked anyway. + } + + /** + * Get all site-wide blocks. + * + * @return array Array of blocks organized by type. + */ + public static function get_site_blocks() { + return array( + 'actors' => \wp_list_pluck( Blocked_Actors::get_many( Actors::BLOG_USER_ID ), 'guid' ), + 'domains' => \get_option( self::OPTION_KEYS[ self::TYPE_DOMAIN ], array() ), + 'keywords' => \get_option( self::OPTION_KEYS[ self::TYPE_KEYWORD ], array() ), + ); + } + + /** + * Check if an actor is blocked by user or site-wide. + * + * @param string $actor_uri Actor URI to check. + * @param int $user_id Optional. User ID to check user blocks for. Defaults to 0 (site-wide only). + * @return bool True if blocked, false otherwise. + */ + public static function is_actor_blocked( $actor_uri, $user_id = 0 ) { + if ( ! $actor_uri ) { + return false; + } + + // Check site-wide blocks. + $site_blocks = self::get_site_blocks(); + if ( \in_array( $actor_uri, $site_blocks['actors'], true ) ) { + return true; + } + + // Check site-wide domain blocks. + $actor_domain = \wp_parse_url( $actor_uri, PHP_URL_HOST ); + if ( $actor_domain && \in_array( $actor_domain, $site_blocks['domains'], true ) ) { + return true; + } + + // Check user-specific blocks if user_id is provided. + if ( $user_id > 0 ) { + $user_blocks = self::get_user_blocks( $user_id ); + if ( \in_array( $actor_uri, $user_blocks['actors'], true ) ) { + return true; + } + + // Check user-specific domain blocks. + if ( $actor_domain && \in_array( $actor_domain, $user_blocks['domains'], true ) ) { + return true; + } + } + + return false; + } + + /** + * Check activity against blocklists. + * + * @param Activity $activity The activity. + * @param array $blocked_actors List of blocked actors. + * @param array $blocked_domains List of blocked domains. + * @param array $blocked_keywords List of blocked keywords. + * @return bool True if blocked, false otherwise. + */ + private static function check_activity_against_blocks( $activity, $blocked_actors, $blocked_domains, $blocked_keywords ) { + $has_object = \is_object( $activity->get_object() ); + + // Extract actor information. + $actor_id = object_to_uri( $activity->get_actor() ); + + // Check blocked actors. + if ( $actor_id ) { + // If actor_id is not a URL, resolve it via webfinger. + if ( ! \str_starts_with( $actor_id, 'http' ) ) { + $resolved_url = Webfinger::resolve( $actor_id ); + if ( ! \is_wp_error( $resolved_url ) ) { + $actor_id = $resolved_url; + } + } + + if ( \in_array( $actor_id, $blocked_actors, true ) ) { + return true; + } + } + + // Check blocked domains. + $urls = array( + \wp_parse_url( $actor_id, PHP_URL_HOST ), + \wp_parse_url( $activity->get_id(), PHP_URL_HOST ), + \wp_parse_url( object_to_uri( $activity->get_object() ) ?? '', PHP_URL_HOST ), + ); + foreach ( $blocked_domains as $domain ) { + if ( \in_array( $domain, $urls, true ) ) { + return true; + } + } + + // Check blocked keywords in activity content. + if ( $has_object ) { + $object = $activity->get_object(); + $content_map = array(); + $content_map[] = $object->get_content(); + $content_map[] = $object->get_summary(); + $content_map[] = $object->get_name(); + + if ( is_actor( $object ) ) { + /* @var Actor $object Actor object */ + $content_map[] = $object->get_preferred_username(); + } + + if ( \is_array( $object->get_content_map() ) ) { + $content_map = \array_merge( $content_map, \array_values( $object->get_content_map() ) ); + } + + if ( \is_array( $object->get_summary_map() ) ) { + $content_map = \array_merge( $content_map, \array_values( $object->get_summary_map() ) ); + } + + if ( \is_array( $object->get_name_map() ) ) { + $content_map = \array_merge( $content_map, \array_values( $object->get_name_map() ) ); + } + + $content_map = \array_filter( $content_map ); + $content = \implode( ' ', $content_map ); + + foreach ( $blocked_keywords as $keyword ) { + if ( \stripos( $content, $keyword ) !== false ) { + return true; + } + } + } + + return false; + } +} diff --git a/wp-content/plugins/activitypub/includes/class-move.php b/wp-content/plugins/activitypub/includes/class-move.php index 18e54ad4..4f553442 100644 --- a/wp-content/plugins/activitypub/includes/class-move.php +++ b/wp-content/plugins/activitypub/includes/class-move.php @@ -7,8 +7,8 @@ namespace Activitypub; -use Activitypub\Activity\Actor; use Activitypub\Activity\Activity; +use Activitypub\Activity\Actor; use Activitypub\Collection\Actors; use Activitypub\Model\Blog; use Activitypub\Model\User; @@ -65,7 +65,7 @@ class Move { /** * Move an ActivityPub Actor from one location (internal) to another (external). * - * This helps migrating local profiles to a new external profile: + * This helps with migrating local profiles to a new external profile: * * `Move::externally( 'https://example.com/?author=123', 'https://mastodon.example/users/foo' );` * @@ -119,7 +119,7 @@ class Move { * * Move an ActivityPub Actor from one location (internal) to another (internal). * - * This helps migrating abandoned profiles to `Move` to other profiles: + * This helps with migrating abandoned profiles to `Move` to other profiles: * * `Move::internally( 'https://example.com/?author=123', 'https://example.com/?author=321' );` * @@ -172,7 +172,6 @@ class Move { * @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; @@ -225,8 +224,15 @@ class Move { $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() ); + /** + * Fires when an actor move fails during domain change. + * + * @since 8.1.0 + * + * @param \WP_Error $result The error that occurred. + * @param string $actor_id The actor ID that failed to move. + */ + \do_action( 'activitypub_move_failed', $result, $actor_id ); continue; } @@ -236,7 +242,7 @@ class Move { 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 ); + \update_user_option( $actor->get__id(), 'activitypub_old_host_data', $json ); } $results[] = array( diff --git a/wp-content/plugins/activitypub/includes/class-notification.php b/wp-content/plugins/activitypub/includes/class-notification.php index 68283133..9b22d4bc 100644 --- a/wp-content/plugins/activitypub/includes/class-notification.php +++ b/wp-content/plugins/activitypub/includes/class-notification.php @@ -9,6 +9,8 @@ namespace Activitypub; /** * Notification class. + * + * @deprecated 7.5.0 Use action hooks like 'activitypub_handled_{type}' instead. */ class Notification { /** @@ -48,6 +50,8 @@ class Notification { * @param int $target The WordPress User-Id. */ public function __construct( $type, $actor, $activity, $target ) { + \_deprecated_class( __CLASS__, '7.5.0', 'Use action hooks like "activitypub_handled_{type}" instead.' ); + $this->type = $type; $this->actor = $actor; $this->object = $activity; @@ -63,15 +67,19 @@ class Notification { /** * Action to send ActivityPub notifications. * + * @deprecated 7.5.0 Use "activitypub_handled_{$type}" instead. + * * @param Notification $instance The notification object. */ - do_action( 'activitypub_notification', $this ); + \do_action_deprecated( 'activitypub_notification', array( $this ), '7.5.0', "activitypub_handled_{$type}" ); /** * Type-specific action to send ActivityPub notifications. * + * @deprecated 7.5.0 Use "activitypub_handled_{$type}" instead. + * * @param Notification $instance The notification object. */ - do_action( "activitypub_notification_{$type}", $this ); + \do_action_deprecated( "activitypub_notification_{$type}", array( $this ), '7.5.0', "activitypub_handled_{$type}" ); } } diff --git a/wp-content/plugins/activitypub/includes/class-options.php b/wp-content/plugins/activitypub/includes/class-options.php index d30ef9ad..083dc60e 100644 --- a/wp-content/plugins/activitypub/includes/class-options.php +++ b/wp-content/plugins/activitypub/includes/class-options.php @@ -2,15 +2,15 @@ /** * Options file. * - * @package ActivityPub + * @package Activitypub */ -namespace ActivityPub; +namespace Activitypub; + +use Activitypub\Model\Blog; /** * Options class. - * - * @package ActivityPub */ class Options { @@ -18,15 +18,478 @@ class Options { * Initialize the options. */ public static function init() { + \add_action( 'admin_init', array( self::class, 'register_settings' ) ); + \add_action( 'rest_api_init', array( self::class, 'register_settings' ) ); + \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_following_ui', array( self::class, 'pre_option_activitypub_following_ui' ) ); + \add_filter( 'pre_option_activitypub_create_posts', array( self::class, 'pre_option_activitypub_create_posts' ) ); \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' ) ); + + \add_filter( 'default_option_activitypub_negotiate_content', array( self::class, 'default_option_activitypub_negotiate_content' ) ); + \add_filter( 'option_activitypub_max_image_attachments', array( self::class, 'default_max_image_attachments' ) ); + \add_filter( 'option_activitypub_support_post_types', array( self::class, 'support_post_types_ensure_array' ) ); + \add_filter( 'option_activitypub_object_type', array( self::class, 'default_object_type' ) ); + + \add_filter( 'option_activitypub_outbox_purge_days', array( self::class, 'sanitize_purge_days' ) ); + \add_filter( 'option_activitypub_inbox_purge_days', array( self::class, 'sanitize_purge_days' ) ); + \add_filter( 'option_activitypub_ap_post_purge_days', array( self::class, 'sanitize_purge_days' ) ); + + \add_action( 'update_option_activitypub_relay_mode', array( self::class, 'relay_mode_changed' ), 10, 2 ); } + /** + * Register ActivityPub settings. + */ + public static function register_settings() { + /* + * Options Group: activitypub + */ + \register_setting( + 'activitypub', + 'activitypub_post_content_type', + array( + 'type' => 'string', + 'description' => 'Use title and link, summary, full or custom content', + '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', + '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.', + 'default' => ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS, + 'sanitize_callback' => static function ( $value ) { + return \is_numeric( $value ) ? \absint( $value ) : ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS; + }, + ) + ); + + \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', + 'default' => '0', + ) + ); + + \register_setting( + 'activitypub', + 'activitypub_use_opengraph', + array( + 'type' => 'boolean', + 'description' => 'Automatically add "fediverse:creator" OpenGraph tags for Authors and the Blog-User.', + 'default' => '1', + ) + ); + + \register_setting( + 'activitypub', + 'activitypub_support_post_types', + array( + 'type' => 'string', + 'description' => 'Enable ActivityPub support for post types', + 'show_in_rest' => true, + 'default' => array( 'post' ), + ) + ); + + \register_setting( + 'activitypub', + 'activitypub_actor_mode', + array( + 'type' => 'string', + 'description' => 'Choose your preferred Actor-Mode.', + 'default' => ACTIVITYPUB_ACTOR_MODE, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'string', + 'enum' => array( + ACTIVITYPUB_ACTOR_MODE, + ACTIVITYPUB_BLOG_MODE, + ACTIVITYPUB_ACTOR_AND_BLOG_MODE, + ), + ), + ), + ) + ); + + \register_setting( + 'activitypub', + 'activitypub_attribution_domains', + array( + 'type' => 'string', + 'description' => 'Websites allowed to credit you.', + 'default' => home_host(), + 'sanitize_callback' => array( Sanitize::class, 'host_list' ), + ) + ); + + \register_setting( + 'activitypub', + 'activitypub_allow_likes', + array( + 'type' => 'integer', + 'description' => 'Allow likes.', + 'default' => '1', + 'sanitize_callback' => 'absint', + ) + ); + + \register_setting( + 'activitypub', + 'activitypub_allow_reposts', + array( + 'type' => 'integer', + 'description' => 'Allow reposts.', + 'default' => '1', + 'sanitize_callback' => 'absint', + ) + ); + + \register_setting( + 'activitypub', + 'activitypub_auto_approve_reactions', + array( + 'type' => 'integer', + 'description' => 'Auto-approve Reactions.', + 'default' => '0', + 'sanitize_callback' => 'absint', + ) + ); + + \register_setting( + 'activitypub', + 'activitypub_default_quote_policy', + array( + 'type' => 'string', + 'description' => 'Default quote policy for new posts.', + 'default' => ACTIVITYPUB_INTERACTION_POLICY_ANYONE, + 'sanitize_callback' => static function ( $value ) { + $allowed = array( + ACTIVITYPUB_INTERACTION_POLICY_ANYONE, + ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS, + ACTIVITYPUB_INTERACTION_POLICY_ME, + ); + return \in_array( $value, $allowed, true ) ? $value : ACTIVITYPUB_INTERACTION_POLICY_ANYONE; + }, + ) + ); + + \register_setting( + 'activitypub', + 'activitypub_relays', + array( + 'type' => 'array', + 'description' => 'Relays', + 'default' => array(), + 'sanitize_callback' => array( Sanitize::class, 'url_list' ), + ) + ); + + \register_setting( + 'activitypub', + 'activitypub_site_blocked_actors', + array( + 'type' => 'array', + 'description' => 'Site-wide blocked ActivityPub actors.', + 'default' => array(), + 'sanitize_callback' => array( Sanitize::class, 'identifier_list' ), + ) + ); + + /* + * Options Group: activitypub_advanced + */ + \register_setting( + 'activitypub_advanced', + 'activitypub_outbox_purge_days', + array( + 'type' => 'integer', + 'description' => 'Number of days to keep items in the Outbox.', + 'default' => ACTIVITYPUB_OUTBOX_PURGE_DAYS, + 'sanitize_callback' => static function ( $value ) { + return \max( 1, \absint( $value ) ); + }, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_inbox_purge_days', + array( + 'type' => 'integer', + 'description' => 'Number of days to keep items in the Inbox.', + 'default' => ACTIVITYPUB_INBOX_PURGE_DAYS, + 'sanitize_callback' => static function ( $value ) { + return \max( 1, \absint( $value ) ); + }, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_ap_post_purge_days', + array( + 'type' => 'integer', + 'description' => 'Number of days to keep remote posts.', + 'default' => ACTIVITYPUB_AP_POST_PURGE_DAYS, + 'sanitize_callback' => static function ( $value ) { + return \max( 1, \absint( $value ) ); + }, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_vary_header', + array( + 'type' => 'boolean', + 'description' => 'Add the Vary header to the ActivityPub response.', + 'default' => true, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_content_negotiation', + array( + 'type' => 'boolean', + 'description' => 'Enable content negotiation.', + 'default' => true, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_authorized_fetch', + array( + 'type' => 'boolean', + 'description' => 'Require HTTP signature authentication.', + 'default' => false, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_rfc9421_signature', + array( + 'type' => 'boolean', + 'description' => 'Use RFC-9421 signature.', + 'default' => false, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_following_ui', + array( + 'type' => 'boolean', + 'description' => 'Show Following UI in admin menus and settings.', + 'default' => false, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_reader_ui', + array( + 'type' => 'boolean', + 'description' => 'Enable the Reader to view posts from accounts you follow.', + 'default' => false, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_create_posts', + array( + 'type' => 'boolean', + 'description' => 'Allow creating posts via ActivityPub.', + 'default' => false, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_api', + array( + 'type' => 'boolean', + 'description' => 'Enable the ActivityPub API to allow third-party clients.', + 'default' => false, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_object_type', + array( + 'type' => 'string', + 'description' => 'The Activity-Object-Type', + 'show_in_rest' => array( + 'schema' => array( + 'enum' => array( 'note', 'wordpress-post-format' ), + ), + ), + 'default' => ACTIVITYPUB_DEFAULT_OBJECT_TYPE, + ) + ); + + \register_setting( + 'activitypub_advanced', + 'activitypub_relay_mode', + array( + 'type' => 'integer', + 'description' => 'Enable relay mode to forward public activities to all followers.', + 'default' => 0, + 'sanitize_callback' => 'absint', + ) + ); + + /* + * Options Group: activitypub_blog + */ + \register_setting( + 'activitypub_blog', + 'activitypub_blog_description', + array( + 'type' => 'string', + 'description' => 'The Description of the Blog-User', + 'show_in_rest' => true, + 'default' => '', + ) + ); + + \register_setting( + 'activitypub_blog', + 'activitypub_blog_identifier', + array( + 'type' => 'string', + 'description' => 'The Identifier of the Blog-User', + 'show_in_rest' => true, + 'default' => Blog::get_default_username(), + 'sanitize_callback' => array( Sanitize::class, 'blog_identifier' ), + ) + ); + + \register_setting( + 'activitypub_blog', + 'activitypub_header_image', + array( + 'type' => 'integer', + 'description' => 'The Attachment-ID of the Sites Header-Image', + 'default' => null, + ) + ); + + \register_setting( + 'activitypub_blog', + 'activitypub_blog_user_mailer_new_dm', + array( + 'type' => 'integer', + 'description' => 'Send a notification when someone sends a user of the blog a direct message.', + 'default' => 1, + ) + ); + + \register_setting( + 'activitypub_blog', + 'activitypub_blog_user_mailer_new_follower', + array( + 'type' => 'integer', + 'description' => 'Send a notification when someone starts to follow a user of the blog.', + 'default' => 1, + ) + ); + + \register_setting( + 'activitypub_blog', + 'activitypub_blog_user_mailer_new_mention', + array( + 'type' => 'integer', + 'description' => 'Send a notification when someone mentions a user of the blog.', + 'default' => 1, + ) + ); + + \register_setting( + 'activitypub_blog', + 'activitypub_mailer_annual_report', + array( + 'type' => 'integer', + 'description' => 'Send the annual Fediverse Year in Review email.', + 'default' => 1, + ) + ); + + \register_setting( + 'activitypub_blog', + 'activitypub_mailer_monthly_report', + array( + 'type' => 'integer', + 'description' => 'Send a monthly Fediverse stats report email.', + 'default' => 0, + ) + ); + + \register_setting( + 'activitypub_blog', + 'activitypub_blog_user_also_known_as', + array( + 'type' => 'array', + 'description' => 'An array of URLs that the blog user is known by.', + 'default' => array(), + 'sanitize_callback' => array( Sanitize::class, 'identifier_list' ), + ) + ); + + \register_setting( + 'activitypub_blog', + 'activitypub_hide_social_graph', + array( + 'type' => 'integer', + 'description' => 'Hide Followers and Followings on Profile.', + 'default' => 0, + 'sanitize_callback' => 'absint', + 'show_in_rest' => true, + ) + ); + } + + /** + * Delete all options. + */ + public static function delete() { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->query( "DELETE FROM $wpdb->options WHERE option_name LIKE 'activitypub_%'" ); + } /** * Pre-get option filter for the Actor-Mode. @@ -70,25 +533,6 @@ class Options { 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. * @@ -108,17 +552,190 @@ class Options { return '0'; } + /** + * Pre-get option filter for the Following UI. + * + * Forces the Following UI to be enabled when the Reader is enabled. + * + * @param string $pre The pre-get option value. + * + * @return string If the Reader is enabled, return '1', otherwise return the pre-get option value. + */ + public static function pre_option_activitypub_following_ui( $pre ) { + /* + * Bypass the filter to get the actual stored value for activitypub_reader_ui. + * This avoids infinite loops if activitypub_reader_ui also had a pre_option filter. + */ + if ( \get_option( 'activitypub_reader_ui', '0' ) ) { + return '1'; + } + + return $pre; + } + + /** + * Pre-get option filter for the Create Posts setting. + * + * Forces the Create Posts setting to be enabled when the Reader is enabled. + * + * @param string $pre The pre-get option value. + * + * @return string If the Reader is enabled, return '1', otherwise return the pre-get option value. + */ + public static function pre_option_activitypub_create_posts( $pre ) { + if ( \get_option( 'activitypub_reader_ui', '0' ) ) { + return '1'; + } + + return $pre; + } + /** * Disallow interactions if the constant is set. * - * @param bool $pre_option The value of the option. + * @param bool $pre The value of the option. + * * @return bool|string The value of the option. */ - public static function maybe_disable_interactions( $pre_option ) { + public static function maybe_disable_interactions( $pre ) { if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) { return '0'; } - return $pre_option; + return $pre; + } + + /** + * Default option filter for the Content-Negotiation. + * + * @see https://github.com/Automattic/wordpress-activitypub/wiki/Caching + * + * @param string $default_value The default value of the option. + * + * @return string The default value of the option. + */ + public static function default_option_activitypub_negotiate_content( $default_value ) { + $disable_for_plugins = array( + 'wp-optimize/wp-optimize.php', + 'wp-rocket/wp-rocket.php', + 'w3-total-cache/w3-total-cache.php', + 'wp-fastest-cache/wp-fastest-cache.php', + 'sg-cachepress/sg-cachepress.php', + ); + + foreach ( $disable_for_plugins as $plugin ) { + if ( \is_plugin_active( $plugin ) ) { + return '0'; + } + } + + return $default_value; + } + + /** + * Default max image attachments. + * + * @param string $value The value of the option. + * + * @return string|int The value of the option. + */ + public static function default_max_image_attachments( $value ) { + if ( ! \is_numeric( $value ) ) { + $value = ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS; + } + + return $value; + } + + /** + * Ensure support post types is an array. + * + * @param string[] $value The value of the option. + * + * @return string[] The value of the option. + */ + public static function support_post_types_ensure_array( $value ) { + return (array) $value; + } + + /** + * Default object type. + * + * @param string $value The value of the option. + * + * @return string The value of the option. + */ + public static function default_object_type( $value ) { + if ( ! $value ) { + $value = ACTIVITYPUB_DEFAULT_OBJECT_TYPE; + } + + return $value; + } + + /** + * Sanitize purge day values. + * + * Ensures the value is a non-negative integer. Returns the + * registered default when the stored value is empty or false + * (option not properly set), but allows 0 to disable purging. + * + * @since 8.1.0 + * + * @param mixed $value The stored option value. + * + * @return int The sanitized value. + */ + public static function sanitize_purge_days( $value ) { + if ( '' === $value || false === $value ) { + $filter = \current_filter(); + $defaults = array( + 'option_activitypub_outbox_purge_days' => ACTIVITYPUB_OUTBOX_PURGE_DAYS, + 'option_activitypub_inbox_purge_days' => ACTIVITYPUB_INBOX_PURGE_DAYS, + 'option_activitypub_ap_post_purge_days' => ACTIVITYPUB_AP_POST_PURGE_DAYS, + ); + + return $defaults[ $filter ] ?? ACTIVITYPUB_OUTBOX_PURGE_DAYS; + } + + return \max( 1, \absint( $value ) ); + } + + /** + * Handle relay mode option changes. + * + * When relay mode is enabled, switch to blog-only mode and set username to "relay". + * When disabled, restore previous settings. + * + * @param mixed $old_value The old option value. + * @param mixed $new_value The new option value. + */ + public static function relay_mode_changed( $old_value, $new_value ) { + if ( $new_value && ! $old_value ) { + // Enabling relay mode. + // Store previous username and actor mode for restoration. + \update_option( 'activitypub_relay_previous_blog_identifier', \get_option( 'activitypub_blog_identifier' ) ); + \update_option( 'activitypub_relay_previous_actor_mode', \get_option( 'activitypub_actor_mode' ) ); + + // Set blog username to "relay". + \update_option( 'activitypub_blog_identifier', 'relay' ); + + // Switch to blog-only mode. + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE ); + } elseif ( ! $new_value && $old_value ) { + // Disabling relay mode - restore previous settings. + $previous_identifier = \get_option( 'activitypub_relay_previous_blog_identifier' ); + $previous_actor_mode = \get_option( 'activitypub_relay_previous_actor_mode' ); + + if ( $previous_identifier ) { + \update_option( 'activitypub_blog_identifier', $previous_identifier ); + \delete_option( 'activitypub_relay_previous_blog_identifier' ); + } + + if ( $previous_actor_mode ) { + \update_option( 'activitypub_actor_mode', $previous_actor_mode ); + \delete_option( 'activitypub_relay_previous_actor_mode' ); + } + } } } diff --git a/wp-content/plugins/activitypub/includes/class-post-types.php b/wp-content/plugins/activitypub/includes/class-post-types.php new file mode 100644 index 00000000..88a69f74 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-post-types.php @@ -0,0 +1,1022 @@ + array( + 'name' => \_x( 'Followers', 'post_type plural name', 'activitypub' ), + 'singular_name' => \_x( 'Follower', 'post_type single name', 'activitypub' ), + ), + 'public' => false, + 'show_in_rest' => true, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => true, + 'supports' => array( 'custom-fields' ), + ) + ); + + // Register meta for Remote Actors post type. + \register_post_meta( + Remote_Actors::POST_TYPE, + '_activitypub_inbox', + array( + 'type' => 'string', + 'single' => true, + 'sanitize_callback' => 'sanitize_url', + ) + ); + + \register_post_meta( + Remote_Actors::POST_TYPE, + '_activitypub_errors', + array( + 'type' => 'string', + 'single' => false, + 'sanitize_callback' => 'sanitize_text_field', + ) + ); + + \register_post_meta( + Remote_Actors::POST_TYPE, + Followers::FOLLOWER_META_KEY, + array( + 'type' => 'string', + 'single' => false, + 'show_in_rest' => true, + 'sanitize_callback' => 'sanitize_text_field', + ) + ); + } + + /** + * Register the Inbox post type and its meta. + */ + public static function register_inbox_post_type() { + \register_post_type( + Inbox::POST_TYPE, + array( + 'labels' => array( + 'name' => \_x( 'Inbox', 'post_type plural name', 'activitypub' ), + 'singular_name' => \_x( 'Inbox Item', 'post_type single name', 'activitypub' ), + ), + 'capabilities' => array( + 'create_posts' => false, + ), + 'map_meta_cap' => true, + 'public' => false, + 'show_in_rest' => false, + 'rewrite' => false, + 'query_var' => false, + 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ), + 'delete_with_user' => true, + 'can_export' => true, + 'exclude_from_search' => true, + ) + ); + + // Register meta for Inbox post type. + \register_post_meta( + Inbox::POST_TYPE, + '_activitypub_object_id', + array( + 'type' => 'string', + 'single' => true, + 'description' => 'The ID (ActivityPub URI) of the object that the inbox item is about.', + 'sanitize_callback' => 'sanitize_url', + ) + ); + + \register_post_meta( + Inbox::POST_TYPE, + '_activitypub_activity_type', + array( + 'type' => 'string', + 'description' => 'The type of the activity', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => static function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => Activity::TYPES, + 'default' => 'Create', + ); + + if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + + \register_post_meta( + Inbox::POST_TYPE, + '_activitypub_activity_remote_actor', + array( + 'type' => 'string', + 'single' => true, + 'description' => 'The ID (ActivityPub URI) of the remote actor that sent the activity.', + 'sanitize_callback' => 'sanitize_url', + ) + ); + + \register_post_meta( + Inbox::POST_TYPE, + 'activitypub_content_visibility', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => static 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; + }, + ) + ); + + \register_post_meta( + Inbox::POST_TYPE, + '_activitypub_user_id', + array( + 'type' => 'integer', + 'single' => false, // Allow multiple values - one per recipient. + 'description' => 'User ID of a recipient of this activity. Multiple entries allowed.', + 'sanitize_callback' => 'absint', + 'show_in_rest' => true, + ) + ); + } + + /** + * Register the Outbox post type and its meta. + */ + public static function 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' => false, + 'rewrite' => false, + 'query_var' => false, + 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ), + 'delete_with_user' => true, + 'can_export' => true, + 'exclude_from_search' => true, + ) + ); + + // Register meta for Outbox post type. + /** + * 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' => static function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => Activity::TYPES, + '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' => static 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' => static 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; + }, + ) + ); + } + + /** + * Register the Post post type. + */ + public static function register_post_post_type() { + \register_post_type( + Remote_Posts::POST_TYPE, + array( + 'labels' => array( + 'name' => \_x( 'Posts', 'post_type plural name', 'activitypub' ), + 'singular_name' => \_x( 'Post', 'post_type single name', 'activitypub' ), + ), + 'capabilities' => array( + 'activitypub' => true, + ), + 'map_meta_cap' => true, + 'public' => false, + 'show_in_rest' => true, + 'rewrite' => false, + 'query_var' => false, + 'supports' => array( 'title', 'editor', 'author', 'custom-fields', 'excerpt', 'comments' ), + 'delete_with_user' => true, + 'can_export' => true, + 'exclude_from_search' => true, + 'taxonomies' => array( 'ap_tag', 'ap_object_type' ), + ) + ); + + \register_taxonomy( + 'ap_tag', + array( Remote_Posts::POST_TYPE ), + array( + 'public' => false, + 'query_var' => true, + 'show_in_rest' => true, + ) + ); + + \register_taxonomy( + 'ap_object_type', + array( Remote_Posts::POST_TYPE ), + array( + 'public' => false, + 'query_var' => true, + 'show_in_rest' => true, + ) + ); + + \register_post_meta( + Remote_Posts::POST_TYPE, + '_activitypub_remote_actor_id', + array( + 'type' => 'integer', + 'single' => true, + 'description' => 'The local ID of the remote actor that created the object.', + 'sanitize_callback' => 'absint', + ) + ); + + \register_post_meta( + Remote_Posts::POST_TYPE, + '_activitypub_user_id', + array( + 'type' => 'integer', + 'single' => true, + 'description' => 'The ID of the local user that received the activity.', + 'sanitize_callback' => 'absint', + ) + ); + } + + /** + * Register the Extra Fields post types. + */ + public static function register_extra_fields_post_types() { + $extra_field_args = array( + 'labels' => array( + 'name' => \_x( 'Extra fields', 'post_type plural name', 'activitypub' ), + 'singular_name' => \_x( 'Extra field', 'post_type single name', 'activitypub' ), + 'add_new' => \__( 'Add new', 'activitypub' ), + 'add_new_item' => \__( 'Add new extra field', 'activitypub' ), + 'new_item' => \__( 'New extra field', 'activitypub' ), + 'edit_item' => \__( 'Edit extra field', 'activitypub' ), + 'view_item' => \__( 'View extra field', 'activitypub' ), + 'all_items' => \__( 'All extra fields', 'activitypub' ), + ), + 'public' => false, + 'hierarchical' => false, + 'query_var' => false, + 'has_archive' => false, + 'publicly_queryable' => false, + 'show_in_menu' => false, + 'delete_with_user' => true, + 'can_export' => true, + 'exclude_from_search' => true, + 'show_in_rest' => true, + 'map_meta_cap' => true, + 'show_ui' => true, + 'supports' => array( 'title', 'editor', 'page-attributes', 'author' ), + 'capabilities' => array( + 'create_posts' => 'activitypub', // Require activitypub capability to create extra fields. + 'edit_others_posts' => 'do_not_allow', // Disallow editing others' Extra Fields (only own ones). + ), + ); + + \register_post_type( Extra_Fields::USER_POST_TYPE, $extra_field_args ); + + // Blog Extra Fields require manage_options capability. + $extra_field_args['capabilities'] = array( 'create_posts' => 'manage_options' ); + \register_post_type( Extra_Fields::BLOG_POST_TYPE, $extra_field_args ); + + /** + * Fires after ActivityPub custom post types have been registered. + */ + \do_action( 'activitypub_after_register_post_type' ); + } + + /** + * Register OAuth 2.0 post types for C2S support. + * + * Registers post type for OAuth clients. + * Note: Tokens are stored in user meta and authorization codes in transients. + */ + public static function register_oauth_post_types() { + // OAuth Clients post type. + \register_post_type( + Client::POST_TYPE, + array( + 'labels' => array( + 'name' => \_x( 'OAuth Clients', 'post_type plural name', 'activitypub' ), + 'singular_name' => \_x( 'OAuth Client', 'post_type single name', 'activitypub' ), + ), + 'public' => false, + 'show_in_rest' => false, + 'hierarchical' => false, + 'rewrite' => false, + 'query_var' => false, + 'delete_with_user' => false, + 'can_export' => true, + 'supports' => array( 'title', 'editor', 'custom-fields' ), + 'exclude_from_search' => true, + ) + ); + + // OAuth Client meta. + \register_post_meta( + Client::POST_TYPE, + '_activitypub_client_id', + array( + 'type' => 'string', + 'single' => true, + 'description' => 'Unique OAuth client identifier (UUID).', + 'sanitize_callback' => 'sanitize_text_field', + ) + ); + + \register_post_meta( + Client::POST_TYPE, + '_activitypub_client_secret_hash', + array( + 'type' => 'string', + 'single' => true, + 'description' => 'SHA-256 hash of the client secret (null for public clients).', + 'sanitize_callback' => 'sanitize_text_field', + ) + ); + + \register_post_meta( + Client::POST_TYPE, + '_activitypub_redirect_uris', + array( + 'type' => 'array', + 'single' => true, + 'description' => 'Allowed redirect URIs for this client.', + 'sanitize_callback' => static function ( $value ) { + if ( ! is_array( $value ) ) { + return array(); + } + return array_map( array( Sanitize::class, 'redirect_uri' ), $value ); + }, + ) + ); + + \register_post_meta( + Client::POST_TYPE, + '_activitypub_allowed_scopes', + array( + 'type' => 'array', + 'single' => true, + 'description' => 'Allowed OAuth scopes for this client.', + 'sanitize_callback' => array( Scope::class, 'sanitize' ), + ) + ); + + \register_post_meta( + Client::POST_TYPE, + '_activitypub_is_public', + array( + 'type' => 'boolean', + 'single' => true, + 'description' => 'Whether this is a public client (PKCE-only, no secret).', + 'sanitize_callback' => 'rest_sanitize_boolean', + 'default' => true, + ) + ); + + \register_post_meta( + Client::POST_TYPE, + Token::USER_META_KEY, + array( + 'type' => 'integer', + 'single' => false, + 'description' => 'User IDs that have active tokens for this client.', + 'sanitize_callback' => 'absint', + ) + ); + } + + /** + * Register the ap_tombstone post type. + * + * Stores local tombstone URLs out of the autoloaded options row. + * The post type is fully internal — never queried publicly, never shown in UI. + * + * @since 8.3.0 + */ + public static function register_tombstone_post_type() { + \register_post_type( + Tombstone::POST_TYPE, + array( + 'public' => false, + 'publicly_queryable' => false, + 'show_ui' => false, + 'show_in_menu' => false, + 'show_in_nav_menus' => false, + 'show_in_admin_bar' => false, + 'show_in_rest' => false, + 'exclude_from_search' => true, + 'has_archive' => false, + 'rewrite' => false, + 'query_var' => false, + 'can_export' => false, + 'delete_with_user' => false, + 'supports' => array(), + ) + ); + } + + /** + * Register post meta for ActivityPub supported post types. + */ + public static function register_activitypub_post_meta() { + $ap_post_types = \get_post_types_by_support( 'activitypub' ); + foreach ( $ap_post_types as $post_type ) { + \register_post_meta( + $post_type, + 'activitypub_content_warning', + array( + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ) + ); + + \register_post_meta( + $post_type, + 'activitypub_content_visibility', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => static 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; + }, + ) + ); + + \register_post_meta( + $post_type, + 'activitypub_max_image_attachments', + array( + 'type' => 'integer', + 'single' => true, + 'show_in_rest' => true, + 'default' => \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ), + 'sanitize_callback' => 'absint', + ) + ); + + \register_post_meta( + $post_type, + 'activitypub_interaction_policy_quote', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'default' => \get_option( 'activitypub_default_quote_policy', ACTIVITYPUB_INTERACTION_POLICY_ANYONE ), + 'sanitize_callback' => static function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => array( ACTIVITYPUB_INTERACTION_POLICY_ANYONE, ACTIVITYPUB_INTERACTION_POLICY_FOLLOWERS, ACTIVITYPUB_INTERACTION_POLICY_ME ), + 'default' => ACTIVITYPUB_INTERACTION_POLICY_ANYONE, + ); + + if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + + \register_post_meta( + $post_type, + 'activitypub_status', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => static function ( $value ) { + // Allow empty values to pass through without setting a default. + if ( empty( $value ) ) { + return ''; + } + + $schema = array( + 'type' => 'string', + 'enum' => array( + ACTIVITYPUB_OBJECT_STATE_PENDING, + ACTIVITYPUB_OBJECT_STATE_FEDERATED, + ACTIVITYPUB_OBJECT_STATE_FAILED, + ACTIVITYPUB_OBJECT_STATE_DELETED, + ), + 'default' => '', + ); + + if ( \is_wp_error( \rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + } + } + + /** + * Register REST field for ap_actor posts. + */ + public static function register_ap_actor_rest_field() { + \register_rest_field( + Remote_Actors::POST_TYPE, + 'activitypub_json', + array( + /** + * Get the raw post content without WordPress content filtering. + * + * @param array $response Prepared response array. + * @return string The raw post content. + */ + 'get_callback' => static function ( $response ) { + return \get_post_field( 'post_content', $response['id'] ); + }, + 'schema' => array( + 'description' => 'Raw ActivityPub JSON data without WordPress content filtering', + 'type' => 'string', + 'context' => array( 'view', 'edit' ), + ), + ) + ); + + // Add formatted actor data field. + \register_rest_field( + Remote_Actors::POST_TYPE, + 'actor_info', + array( + 'get_callback' => function ( $response ) { + $actor = Remote_Actors::get_actor( $response['id'] ); + if ( \is_wp_error( $actor ) ) { + return null; + } + return array( + 'username' => $actor->get_preferred_username(), + 'name' => $actor->get_name() ?? $actor->get_preferred_username(), + 'icon' => object_to_uri( $actor->get_icon() ), + 'url' => object_to_uri( $actor->get_url() ?? $actor->get_id() ), + 'webfinger' => Remote_Actors::get_acct( $response['id'] ), + 'identifier' => $actor->get_id(), + ); + }, + 'schema' => array( + 'description' => 'Parsed ActivityPub actor information', + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + ), + ) + ); + + // Add follow status field. + \register_rest_field( + Remote_Actors::POST_TYPE, + 'follow_status', + array( + 'get_callback' => function ( $response ) { + $current_user_id = \get_current_user_id(); + if ( ! $current_user_id ) { + return array( 'follows_back' => false ); + } + return array( + 'follows_back' => Following::check_status( $current_user_id, $response['id'] ), + ); + }, + 'schema' => array( + 'description' => 'Follow relationship status', + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + ), + ) + ); + + // Add custom query parameter for filtering by follower relationships. + \add_filter( 'rest_ap_actor_query', array( self::class, 'filter_ap_actor_query_by_follower' ), 10, 2 ); + } + + /** + * Filter WP_Query args to support follower_of parameter. + * + * @param array $args Array of arguments for WP_Query. + * @param \WP_REST_Request $request The REST API request. + * @return array Modified query arguments. + */ + public static function filter_ap_actor_query_by_follower( $args, $request ) { + if ( ! empty( $request['follower_of'] ) ) { + // Add meta_query to filter by _activitypub_following. + if ( ! isset( $args['meta_query'] ) ) { + $args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + + $args['meta_query'][] = array( + 'key' => Followers::FOLLOWER_META_KEY, + 'value' => $request['follower_of'], + ); + } + + return $args; + } + + /** + * Register a REST field for the ap_post post type to embed remote actor data. + */ + public static function register_ap_post_actor_rest_field() { + \register_rest_field( + Remote_Posts::POST_TYPE, + 'actor_info', + array( + /** + * Get the remote actor data for an ap_post. + * + * @param array $response Prepared response array. + * @return array|null The actor data or null if not found. + */ + 'get_callback' => function ( $response ) { + $id = \get_post_meta( $response['id'], '_activitypub_remote_actor_id', true ); + $actor = Remote_Actors::get_actor( $id ); + + if ( \is_wp_error( $actor ) ) { + return null; + } + + return array( + 'username' => $actor->get_preferred_username(), + 'name' => $actor->get_name() ?? $actor->get_preferred_username(), + 'icon' => object_to_uri( $actor->get_icon() ), + 'url' => object_to_uri( $actor->get_url() ?? $actor->get_id() ), + 'webfinger' => Remote_Actors::get_acct( $id ), + 'identifier' => $actor->get_id(), + ); + }, + 'schema' => array( + 'description' => 'Remote actor data', + 'type' => 'object', + 'context' => array( 'view', 'edit' ), + ), + ) + ); + } + + /** + * Register custom REST API parameters for ap_post endpoint. + */ + public static function register_ap_post_rest_params() { + \add_filter( + 'rest_' . Remote_Posts::POST_TYPE . '_collection_params', + function ( $params ) { + $params['user_id'] = array( + 'description' => __( 'Filter posts by user ID (0 for site/blog actor).', 'activitypub' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ); + + $params['ap_object_type'] = array( + 'description' => 'Filter posts by ActivityPub object type.', + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + ); + + $params['ap_tag'] = array( + 'description' => 'Filter posts by ActivityPub tag (term IDs).', + 'type' => 'array', + 'items' => array( + 'type' => 'integer', + 'minimum' => 0, + ), + ); + + return $params; + } + ); + } + + /** + * Filter ap_post REST query to only show posts for the current user. + * + * @param array $args Query arguments. + * @param \WP_REST_Request $request The REST API request. + * + * @return array Modified query arguments. + */ + public static function filter_ap_post_by_user( $args, $request ) { + $ap_tag = $request->get_param( 'ap_tag' ); + if ( ! empty( $ap_tag ) ) { + if ( ! isset( $args['tax_query'] ) ) { + $args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + } + + $args['tax_query'][] = array( + 'taxonomy' => 'ap_tag', + 'field' => 'term_id', + 'terms' => $ap_tag, + ); + + return $args; + } + + // Filter by user_id (defaults to current user, use 0 for site/blog actor). + $user_id = isset( $request['user_id'] ) ? (int) $request['user_id'] : \get_current_user_id(); + + if ( ! isset( $args['meta_query'] ) ) { + $args['meta_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + } + + $args['meta_query'][] = array( + 'key' => '_activitypub_user_id', + 'value' => $user_id, + 'compare' => '=', + ); + + // Filter by object type if provided. + if ( ! empty( $request['ap_object_type'] ) ) { + if ( ! isset( $args['tax_query'] ) ) { + $args['tax_query'] = array(); // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query + } + + $args['tax_query'][] = array( + 'taxonomy' => 'ap_object_type', + 'field' => 'term_id', + 'terms' => $request['ap_object_type'], + ); + } + + return $args; + } + + /** + * Register user_id parameter for ap_object_type taxonomy REST API. + * + * @param array $params Existing collection parameters. + * + * @return array Modified collection parameters. + */ + public static function register_object_type_user_param( $params ) { + $params['user_id'] = array( + 'description' => __( 'Filter terms to those with posts from this user ID.', 'activitypub' ), + 'type' => 'integer', + 'sanitize_callback' => 'absint', + ); + + return $params; + } + + /** + * Filter ap_object_type REST query to only return terms that have posts for the given user. + * + * Uses a direct SQL query to efficiently get term IDs without loading all post IDs. + * + * @param array $args Query arguments. + * @param \WP_REST_Request $request The REST API request. + * + * @return array Modified query arguments. + */ + public static function filter_object_type_by_user( $args, $request ) { + $user_id = $request->get_param( 'user_id' ); + if ( null === $user_id ) { + return $args; + } + + global $wpdb; + + // Get term IDs that have at least one ap_post for this user. + $term_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->prepare( + "SELECT DISTINCT tt.term_id + FROM {$wpdb->term_taxonomy} tt + INNER JOIN {$wpdb->term_relationships} tr ON tt.term_taxonomy_id = tr.term_taxonomy_id + INNER JOIN {$wpdb->posts} p ON tr.object_id = p.ID + INNER JOIN {$wpdb->postmeta} pm ON p.ID = pm.post_id + WHERE tt.taxonomy = 'ap_object_type' + AND p.post_type = 'ap_post' + AND pm.meta_key = '_activitypub_user_id' + AND pm.meta_value = %s", + $user_id + ) + ); + + if ( empty( $term_ids ) ) { + // Force empty result. + $term_ids = array( 0 ); + } + + $args['include'] = \array_map( 'intval', $term_ids ); + + return $args; + } + + /** + * Prevent empty or default meta values. + * + * @param null|bool $check Whether to allow updating metadata for the given type. + * @param int $object_id ID of the object metadata is for. + * @param string $meta_key Metadata key. + * @param mixed $meta_value Metadata value. Must be serializable if non-scalar. + */ + public static function prevent_empty_post_meta( $check, $object_id, $meta_key, $meta_value ) { + $post_metas = array( + 'activitypub_content_visibility' => '', + 'activitypub_content_warning' => '', + 'activitypub_max_image_attachments' => (string) \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ), + ); + + if ( isset( $post_metas[ $meta_key ] ) && $post_metas[ $meta_key ] === (string) $meta_value ) { + if ( 'update_post_metadata' === current_action() ) { + \delete_post_meta( $object_id, $meta_key ); + } + + $check = true; + } + + return $check; + } +} diff --git a/wp-content/plugins/activitypub/includes/class-query.php b/wp-content/plugins/activitypub/includes/class-query.php index d2200c0f..89c44390 100644 --- a/wp-content/plugins/activitypub/includes/class-query.php +++ b/wp-content/plugins/activitypub/includes/class-query.php @@ -7,6 +7,7 @@ namespace Activitypub; +use Activitypub\Activity\Extended_Object\Quote_Authorization; use Activitypub\Collection\Actors; use Activitypub\Collection\Outbox; use Activitypub\Transformer\Factory; @@ -138,6 +139,10 @@ class Query { private function prepare_activitypub_data() { $queried_object = $this->get_queried_object(); + if ( $queried_object instanceof \WP_Post && \get_query_var( 'stamp' ) ) { + return $this->maybe_get_stamp(); + } + // Check for Outbox Activity. if ( $queried_object instanceof \WP_Post && @@ -193,6 +198,14 @@ class Query { } } + // Check Term by ID. + if ( ! $queried_object ) { + $term_id = \get_query_var( 'term_id' ); + if ( $term_id ) { + $queried_object = \get_term( $term_id ); + } + } + // Try to get Author by ID. if ( ! $queried_object ) { $url = $this->get_request_url(); @@ -248,7 +261,7 @@ class Query { * * @return string|null The request URL. */ - protected function get_request_url() { + public function get_request_url() { if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { return null; } @@ -267,50 +280,74 @@ class Query { * @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; - } + if ( ! isset( $this->is_activitypub_request ) ) { + global $wp_query; - global $wp_query; + $this->is_activitypub_request = false; - // 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 ) ) { + // One can trigger an ActivityPub request by adding `?activitypub` to the URL. + if ( isset( $wp_query->query_vars['activitypub'] ) || isset( $_GET['activitypub'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended \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. + } elseif ( 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; + } } } - $this->is_activitypub_request = false; + /** + * Filters whether the current request is an ActivityPub request. + * + * @param bool $is_activitypub_request True if the request is an ActivityPub request, false otherwise. + */ + return \apply_filters( 'activitypub_is_activitypub_request', $this->is_activitypub_request ); + } - return false; + /** + * Check if content negotiation is allowed for a request. + * + * @return bool True if content negotiation is allowed, false otherwise. + */ + public function should_negotiate_content() { + $return = false; + $always_negotiate = array( 'p', 'c', 'author', 'actor', 'stamp', 'preview', 'activitypub' ); + $url = \wp_parse_url( $this->get_request_url(), PHP_URL_QUERY ); + $query = array(); + \wp_parse_str( $url, $query ); + + // Check if any of the query params are in the `$always_negotiate` array. + if ( \array_intersect( \array_keys( $query ), $always_negotiate ) ) { + $return = true; + } + + if ( \get_option( 'activitypub_content_negotiation', '1' ) ) { + $return = true; + } + + if ( \is_author() && \get_user_option( 'activitypub_use_permalink_as_id', \get_queried_object_id() ) ) { + $return = true; + } + + /** + * Filters whether content negotiation should be forced. + * + * @param bool $return Whether content negotiation should be forced. + */ + return \apply_filters( 'activitypub_should_negotiate_content', $return ); } /** @@ -348,4 +385,52 @@ class Query { public function set_old_host_request( $state = true ) { $this->is_old_host_request = $state; } + + /** + * Maybe get a QuoteAuthorization object from a stamp. + * + * @return bool True if the object was prepared, false otherwise. + */ + private function maybe_get_stamp() { + require_once ABSPATH . 'wp-admin/includes/post.php'; + + $stamp = \get_query_var( 'stamp' ); + $meta = \get_post_meta_by_id( (int) $stamp ); + + if ( ! $meta ) { + return false; + } + + $post = $this->get_queried_object(); + + // Ensure the meta belongs to the queried post to prevent arbitrary meta disclosure. + if ( (int) $meta->post_id !== $post->ID ) { + return false; + } + + $user_uri = get_user_id( $post->post_author ); + + if ( ! $user_uri ) { + return false; + } + + $stamp_uri = \add_query_arg( + array( + 'p' => $post->ID, + 'stamp' => $meta->meta_id, + ), + \home_url( '/' ) + ); + + $activitypub_object = new Quote_Authorization(); + $activitypub_object->set_id( $stamp_uri ); + $activitypub_object->set_attributed_to( $user_uri ); + $activitypub_object->set_interacting_object( $meta->meta_value ); + $activitypub_object->set_interaction_target( get_post_id( $post->ID ) ); + + $this->activitypub_object = $activitypub_object; + $this->activitypub_object_id = $activitypub_object->get_id(); + + return true; + } } diff --git a/wp-content/plugins/activitypub/includes/class-relay.php b/wp-content/plugins/activitypub/includes/class-relay.php new file mode 100644 index 00000000..769437f3 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-relay.php @@ -0,0 +1,81 @@ +set_type( 'Announce' ); + $announce->set_actor( Actors::BLOG_USER_ID ); + $announce->set_object( $activity ); + $announce->set_published( gmdate( ACTIVITYPUB_DATE_TIME_RFC3339 ) ); + + // Add to outbox for distribution. The outbox will generate the ID. + Outbox::add( $announce, Actors::BLOG_USER_ID ); + } + + /** + * Unhook settings fields when relay mode is enabled. + * + * Removes all settings sections except moderation when relay mode is active. + */ + public static function unhook_settings_fields() { + global $wp_settings_sections; + + if ( ! isset( $wp_settings_sections['activitypub_settings'] ) ) { + return; + } + + // Keep only the moderation section. + foreach ( $wp_settings_sections['activitypub_settings'] as $section_id => $section ) { + if ( 'activitypub_moderation' !== $section_id ) { + unset( $wp_settings_sections['activitypub_settings'][ $section_id ] ); + } + } + } +} diff --git a/wp-content/plugins/activitypub/includes/class-router.php b/wp-content/plugins/activitypub/includes/class-router.php new file mode 100644 index 00000000..e51cced7 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-router.php @@ -0,0 +1,383 @@ +get_activitypub_object(); + + if ( Tombstone::exists_local( Query::get_instance()->get_request_url() ) ) { + // Set 410 Gone for permanently deleted posts, 200 OK for soft-deleted. + if ( ! $activitypub_object ) { + \status_header( 410 ); + } + + return ACTIVITYPUB_PLUGIN_DIR . 'templates/tombstone-json.php'; + } + + $activitypub_template = false; + + 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'; + } + } + + /* + * Check if the request is authorized. + * + * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch + * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch + */ + if ( $activitypub_template && use_authorized_fetch() ) { + $verification = Signature::verify_http_signature( $_SERVER ); + if ( \is_wp_error( $verification ) ) { + \status_header( 401 ); + + // Fallback as template_loader can't return http headers. + return $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; + } + + /** + * Add the 'self' link to the header. + */ + public static function add_headers() { + $id = Query::get_instance()->get_activitypub_object_id(); + + /* + * Send CORS headers for resolved ActivityPub objects and outbox + * items. Outbox items need CORS even when the object ID doesn't + * resolve, because browser preflight requests don't carry the + * Authorization header needed to authenticate private items. + */ + $post_id = \get_query_var( 'p' ); + $is_outbox_url = $post_id && Outbox::POST_TYPE === \get_post_type( $post_id ); + + if ( ! \headers_sent() && ( $id || $is_outbox_url ) ) { + \header( 'Access-Control-Allow-Origin: *' ); + \header( 'Access-Control-Allow-Methods: GET, OPTIONS' ); + \header( 'Access-Control-Allow-Headers: Accept, Authorization, Content-Type' ); + } + + if ( ! $id ) { + return; + } + + if ( ! \headers_sent() ) { + \header( 'Link: <' . esc_url( $id ) . '>; title="ActivityPub (JSON)"; rel="alternate"; type="application/activity+json"', false ); + + if ( \get_option( 'activitypub_vary_header', '1' ) ) { + // Send Vary header for Accept header. + \header( 'Vary: Accept', false ); + } + } + + \add_action( + 'wp_head', + static function () use ( $id ) { + echo PHP_EOL . '' . 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'] ); + unset( $query_params['stamp'] ); + + 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() { + global $wp_query; + + $comment_id = \get_query_var( 'c', null ); + + // Check if it seems to be a comment. + if ( $comment_id ) { + $comment = \get_comment( $comment_id ); + + // Load a 404-page if `c` is set but not valid. + if ( ! $comment ) { + $wp_query->set_404(); + return; + } + + // Stop if it's not an ActivityPub comment. + if ( is_activitypub_request() && ! is_local_comment( $comment ) ) { + return; + } + + \wp_safe_redirect( get_comment_link( $comment ) ); + 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; + } + + \wp_safe_redirect( $actor->get_url(), 301 ); + exit; + } + + $term_id = \get_query_var( 'term_id', null ); + if ( $term_id ) { + $term = \get_term( $term_id ); + + // Load a 404-page if `term_id` is set but not valid. + if ( ! $term || \is_wp_error( $term ) ) { + $wp_query->set_404(); + return; + } + + /** + * Filters the taxonomies supported for term redirects. + * + * @since 7.8.3 + * + * @param array $supported_taxonomies Array of taxonomy names. Default array( 'category', 'post_tag' ). + */ + $supported_taxonomies = \apply_filters( 'activitypub_supported_taxonomies', array( 'category', 'post_tag' ) ); + + if ( ! in_array( $term->taxonomy, $supported_taxonomies, true ) ) { + return; + } + + // Don't redirect for ActivityPub requests. + if ( is_activitypub_request() ) { + return; + } + + $term_link = \get_term_link( $term ); + if ( ! \is_wp_error( $term_link ) ) { + \wp_safe_redirect( $term_link, 301 ); + exit; + } + } + } + + /** + * Add the 'activitypub' query variable so WordPress won't mangle it. + * + * @param array $vars The query variables. + * + * @return array The query variables. + */ + public static function add_query_vars( $vars ) { + $vars[] = 'activitypub'; + $vars[] = 'preview'; + $vars[] = 'author'; + $vars[] = 'actor'; + $vars[] = 'stamp'; + $vars[] = 'type'; + $vars[] = 'c'; + $vars[] = 'p'; + $vars[] = 'term_id'; + + return $vars; + } + + /** + * Optimize home page query for ActivityPub requests. + * + * Skip the database query entirely for ActivityPub requests on the home page + * since we only need to return the blog actor, not posts. + * + * @param \WP_Query $wp_query The WP_Query instance. + */ + public static function fix_is_home_check( $wp_query ) { + if ( + $wp_query->get( 'actor' ) || + $wp_query->get( 'stamp' ) || + $wp_query->get( 'c' ) + ) { + $wp_query->is_home = false; + } + } +} diff --git a/wp-content/plugins/activitypub/includes/class-sanitize.php b/wp-content/plugins/activitypub/includes/class-sanitize.php index 42d32491..73e1be8e 100644 --- a/wp-content/plugins/activitypub/includes/class-sanitize.php +++ b/wp-content/plugins/activitypub/includes/class-sanitize.php @@ -7,12 +7,57 @@ namespace Activitypub; +use Activitypub\Collection\Remote_Actors; use Activitypub\Model\Blog; /** * Sanitization class. */ class Sanitize { + + /** + * Elements to strip including their inner content. + * + * WordPress's wp_kses removes disallowed tags but preserves their inner text. + * These elements contain content that is meaningless or harmful + * without the surrounding tag (scripts, styles, interactive UI, + * embedded objects), so we remove them entirely before wp_kses runs. + * + * @var array + */ + const STRIP_ELEMENTS = array( + 'script', + 'style', + 'button', + 'nav', + 'form', + 'textarea', + 'select', + 'input', + 'fieldset', + 'iframe', + 'embed', + 'object', + ); + + /** + * MathML global attributes allowed per the W3C MathML safe list. + * + * @see https://w3c.github.io/mathml-docs/mathml-safe-list + * + * @var array + */ + const MATHML_GLOBAL_ATTRS = array( + 'dir' => true, + 'displaystyle' => true, + 'mathbackground' => true, + 'mathcolor' => true, + 'mathsize' => true, + 'scriptlevel' => true, + 'intent' => true, + 'arg' => true, + ); + /** * Sanitize a list of URLs. * @@ -21,7 +66,7 @@ class Sanitize { */ public static function url_list( $value ) { if ( ! \is_array( $value ) ) { - $value = \explode( PHP_EOL, $value ); + $value = \explode( PHP_EOL, (string) $value ); } $value = \array_filter( $value ); @@ -32,6 +77,50 @@ class Sanitize { return \array_values( $value ); } + /** + * Sanitize and normalize a list of account identifiers to ActivityPub IDs. + * + * This function processes various identifier formats, such as URLs and + * webfinger identifiers, and normalizes them into a consistent format. + * + * @param string|array $value The value to sanitize. + * + * @return array The sanitized and normalized list of account identifiers. + */ + public static function identifier_list( $value ) { + if ( ! \is_array( $value ) ) { + $value = \explode( PHP_EOL, (string) $value ); + } + + $value = \array_filter( $value ); + $uris = array(); + + foreach ( $value as $uri ) { + $uri = \trim( $uri ); + $uri = \ltrim( $uri, '@' ); + + if ( \is_email( $uri ) ) { + $_uri = Webfinger::resolve( $uri ); + if ( \is_wp_error( $_uri ) ) { + $uris[] = $uri; + continue; + } + + $uri = $_uri; + } + + $uri = \sanitize_url( $uri ); + $actor = Remote_Actors::fetch_by_uri( $uri ); + if ( \is_wp_error( $actor ) ) { + $uris[] = $uri; + } else { + $uris[] = \sanitize_url( $actor->guid ); + } + } + + return \array_values( \array_unique( $uris ) ); + } + /** * Sanitize a list of hosts. * @@ -39,9 +128,9 @@ class Sanitize { * @return string The sanitized list of hosts. */ public static function host_list( $value ) { - $value = \explode( PHP_EOL, $value ); + $value = \explode( PHP_EOL, (string) $value ); $value = \array_map( - function ( $host ) { + static function ( $host ) { $host = \trim( $host ); $host = \strtolower( $host ); $host = \set_url_scheme( $host ); @@ -68,10 +157,14 @@ class Sanitize { */ public static function blog_identifier( $value ) { // Hack to allow dots in the username. - $parts = \explode( '.', $value ); + $parts = \explode( '.', (string) $value ); $sanitized = \array_map( 'sanitize_title', $parts ); $sanitized = \implode( '.', $sanitized ); + if ( empty( $sanitized ) ) { + return Blog::get_default_username(); + } + // Check for login or nicename. $user = new \WP_User_Query( array( @@ -119,4 +212,349 @@ class Sanitize { return $value; } + + /** + * Sanitize a webfinger identifier. + * + * @param string $value The value to sanitize. + * + * @return string The sanitized webfinger identifier. + */ + public static function webfinger( $value ) { + $value = \str_replace( 'acct:', '', $value ); + $value = \trim( $value, '@' ); + + return $value; + } + + /** + * Sanitize content for ActivityPub. + * + * @param string $content The content to convert. + * + * @return string The converted content. + */ + public static function content( $content ) { + // Only make URLs clickable if no anchor tags exist, to avoid corrupting existing links. + if ( false === \strpos( $content, '[\n\r\t]+<', $content ) ); + } + + /** + * Sanitize a redirect URI, preserving custom protocol schemes. + * + * WordPress's sanitize_url() and esc_url_raw() strip unknown protocols. + * This method extracts the scheme and passes it as allowed so custom + * URI schemes for native apps (RFC 8252 Section 7.1) are preserved. + * + * @since 8.1.0 + * + * @param string $uri The redirect URI to sanitize. + * @return string The sanitized URI. + */ + public static function redirect_uri( $uri ) { + /* + * Extract scheme manually because wp_parse_url() returns false + * for URIs like "myapp://" (scheme + empty authority, no path). + */ + if ( ! preg_match( '/^([a-zA-Z][a-zA-Z0-9+.\-]*):/', $uri, $matches ) ) { + return ''; + } + + $scheme = \strtolower( $matches[1] ); + + // For standard schemes, use default sanitization. + if ( in_array( $scheme, array( 'http', 'https' ), true ) ) { + return \sanitize_url( $uri ); + } + + // For custom schemes, include the scheme in allowed protocols. + return \sanitize_url( $uri, array_merge( \wp_allowed_protocols(), array( $scheme ) ) ); + } + + /** + * Clean HTML for ActivityPub federation. + * + * Uses a positive allowlist based on FEP-b2b8 (Long-form Text) for the + * `content` property, extended with common WordPress content elements. + * Interactive, navigational, and scripting elements are stripped entirely. + * + * @see https://codeberg.org/fediverse/fep/src/branch/main/fep/b2b8/fep-b2b8.md + * @see https://github.com/Automattic/wordpress-activitypub/issues/2619 + * + * @param string $content The HTML content to clean. + * + * @return string The cleaned HTML content. + */ + public static function clean_html( $content ) { + if ( empty( $content ) ) { + return $content; + } + + /* + * Strip elements whose inner content is noise (scripts, styles, interactive UI, embeds). + * This runs before wp_kses because wp_kses strips tags but keeps inner text, + * and content inside