diff --git a/wp-content/plugins/activitypub/activitypub.php b/wp-content/plugins/activitypub/activitypub.php index 145f5182..a6dca243 100644 --- a/wp-content/plugins/activitypub/activitypub.php +++ b/wp-content/plugins/activitypub/activitypub.php @@ -1,14 +1,14 @@ )|(?<=
)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' ); -\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); -\defined( 'ACTIVITYPUB_URL_REGEXP' ) || \define( 'ACTIVITYPUB_URL_REGEXP', '(https?:|www\.)\S+[\w\/]' ); -\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "

[ap_title]

\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" ); -\defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false ); -\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false ); -\defined( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS', false ); -// Disable reactions like `Like` and `Announce` by default. -\defined( 'ACTIVITYPUB_DISABLE_REACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_REACTIONS', true ); -\defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false ); -\defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) || \define( 'ACTIVITYPUB_SHARED_INBOX_FEATURE', false ); -\defined( 'ACTIVITYPUB_SEND_VARY_HEADER' ) || \define( 'ACTIVITYPUB_SEND_VARY_HEADER', false ); -\defined( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE' ) || \define( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE', 'note' ); +\define( 'ACTIVITYPUB_PLUGIN_VERSION', '5.8.0' ); +// Plugin related constants. \define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) ); -\define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) ); +\define( 'ACTIVITYPUB_PLUGIN_FILE', ACTIVITYPUB_PLUGIN_DIR . basename( __FILE__ ) ); \define( 'ACTIVITYPUB_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); +require_once __DIR__ . '/includes/class-autoloader.php'; +require_once __DIR__ . '/includes/compat.php'; +require_once __DIR__ . '/includes/functions.php'; +require_once __DIR__ . '/includes/constants.php'; +require_once __DIR__ . '/integration/load.php'; + +Autoloader::register_path( __NAMESPACE__, __DIR__ . '/includes' ); + /** * Initialize REST routes. */ function rest_init() { - Rest\Actors::init(); - Rest\Outbox::init(); - Rest\Inbox::init(); - Rest\Followers::init(); - Rest\Following::init(); - Rest\Webfinger::init(); - Rest\Comment::init(); Rest\Server::init(); - Rest\Collection::init(); - Rest\Interaction::init(); + Rest\Post::init(); + ( new Rest\Actors_Controller() )->register_routes(); + ( new Rest\Actors_Inbox_Controller() )->register_routes(); + ( new Rest\Application_Controller() )->register_routes(); + ( new Rest\Collections_Controller() )->register_routes(); + ( new Rest\Comments_Controller() )->register_routes(); + ( new Rest\Followers_Controller() )->register_routes(); + ( new Rest\Following_Controller() )->register_routes(); + ( new Rest\Inbox_Controller() )->register_routes(); + ( new Rest\Interaction_Controller() )->register_routes(); + ( new Rest\Moderators_Controller() )->register_routes(); + ( new Rest\Outbox_Controller() )->register_routes(); + ( new Rest\Replies_Controller() )->register_routes(); + ( new Rest\URL_Validator_Controller() )->register_routes(); + ( new Rest\Webfinger_Controller() )->register_routes(); // Load NodeInfo endpoints only if blog is public. if ( is_blog_public() ) { - Rest\NodeInfo::init(); + ( new Rest\Nodeinfo_Controller() )->register_routes(); } } \add_action( 'rest_api_init', __NAMESPACE__ . '\rest_init' ); @@ -76,17 +67,18 @@ function rest_init() { * Initialize plugin. */ function plugin_init() { - \add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) ); - \add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) ); - \add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) ); - \add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) ); - \add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) ); - \add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) ); - \add_action( 'init', array( __NAMESPACE__ . '\Health_Check', 'init' ) ); - \add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Comment', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Dispatcher', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) ); \add_action( 'init', array( __NAMESPACE__ . '\Link', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Mailer', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ), 1 ); + \add_action( 'init', array( __NAMESPACE__ . '\Move', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Options', 'init' ) ); + \add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ) ); if ( site_supports_blocks() ) { \add_action( 'init', array( __NAMESPACE__ . '\Blocks', 'init' ) ); @@ -100,61 +92,27 @@ function plugin_init() { } \add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' ); - /** - * Class Autoloader. + * Initialize plugin admin. */ -\spl_autoload_register( - function ( $full_class ) { - $base_dir = __DIR__ . '/includes/'; - $base = 'Activitypub\\'; +function plugin_admin_init() { + // Menus are registered before `admin_init`, because of course they are. + \add_action( 'admin_menu', array( __NAMESPACE__ . '\WP_Admin\Menu', 'admin_menu' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Admin', 'init' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Health_Check', 'init' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Settings', 'init' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Settings_Fields', 'init' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Welcome_Fields', 'init' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Advanced_Settings_Fields', 'init' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Blog_Settings_Fields', 'init' ) ); + \add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\User_Settings_Fields', 'init' ) ); - if ( strncmp( $full_class, $base, strlen( $base ) ) === 0 ) { - $maybe_uppercase = str_replace( $base, '', $full_class ); - $class = strtolower( $maybe_uppercase ); - // All classes should be capitalized. If this is instead looking for a lowercase method, we ignore that. - if ( $maybe_uppercase === $class ) { - return; - } - - if ( false !== strpos( $class, '\\' ) ) { - $parts = explode( '\\', $class ); - $class = array_pop( $parts ); - $sub_dir = strtr( implode( '/', $parts ), '_', '-' ); - $base_dir = $base_dir . $sub_dir . '/'; - } - - $filename = 'class-' . strtr( $class, '_', '-' ); - $file = $base_dir . $filename . '.php'; - - if ( file_exists( $file ) && is_readable( $file ) ) { - require_once $file; - } else { - // translators: %s is the class name. - $message = sprintf( esc_html__( 'Required class not found or not readable: %s', 'activitypub' ), esc_html( $full_class ) ); - Debug::write_log( $message ); - \wp_die( $message ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - } - } + if ( defined( 'WP_LOAD_IMPORTERS' ) && WP_LOAD_IMPORTERS ) { + require_once __DIR__ . '/includes/wp-admin/import/load.php'; + \add_action( 'admin_init', __NAMESPACE__ . '\WP_Admin\Import\load' ); } -); - -/** - * Add plugin settings link. - * - * @param array $actions The current actions. - */ -function plugin_settings_link( $actions ) { - $settings_link = array(); - $settings_link[] = \sprintf( - '%2s', - \menu_page_url( 'activitypub', false ), - \__( 'Settings', 'activitypub' ) - ); - - return \array_merge( $settings_link, $actions ); } -\add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), __NAMESPACE__ . '\plugin_settings_link' ); +\add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_admin_init' ); \register_activation_hook( __FILE__, @@ -164,6 +122,19 @@ function plugin_settings_link( $actions ) { ) ); +/** + * Redirect to the welcome page after plugin activation. + * + * @param string $plugin The plugin basename. + */ +function activation_redirect( $plugin ) { + if ( ACTIVITYPUB_PLUGIN_BASENAME === $plugin ) { + \wp_safe_redirect( \admin_url( 'options-general.php?page=activitypub' ) ); + exit; + } +} +\add_action( 'activated_plugin', __NAMESPACE__ . '\activation_redirect' ); + \register_deactivation_hook( __FILE__, array( @@ -180,16 +151,18 @@ function plugin_settings_link( $actions ) { ) ); -// Load integrations. -require_once __DIR__ . '/integration/load.php'; /** * `get_plugin_data` wrapper. * + * @deprecated 4.2.0 Use `get_plugin_data` instead. + * * @param array $default_headers Optional. The default plugin headers. Default empty array. * @return array The plugin metadata array. */ function get_plugin_meta( $default_headers = array() ) { + _deprecated_function( __FUNCTION__, '4.2.0', 'get_plugin_data' ); + if ( ! $default_headers ) { $default_headers = array( 'Name' => 'Plugin Name', @@ -212,15 +185,13 @@ function get_plugin_meta( $default_headers = array() ) { /** * Plugin Version Number used for caching. + * + * @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly. */ function get_plugin_version() { - if ( \defined( 'ACTIVITYPUB_PLUGIN_VERSION' ) ) { - return ACTIVITYPUB_PLUGIN_VERSION; - } + _deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' ); - $meta = get_plugin_meta( array( 'Version' => 'Version' ) ); - - return $meta['Version']; + return ACTIVITYPUB_PLUGIN_VERSION; } // Check for CLI env, to add the CLI commands. @@ -229,7 +200,7 @@ if ( defined( 'WP_CLI' ) && WP_CLI ) { 'activitypub', '\Activitypub\Cli', array( - 'shortdesc' => __( 'ActivityPub related commands: Meta-Infos, Delete and soon Self-Destruct.', 'activitypub' ), + 'shortdesc' => 'ActivityPub related commands to manage plugin functionality and the federation of posts and comments.', ) ); } diff --git a/wp-content/plugins/activitypub/assets/css/activitypub-admin.css b/wp-content/plugins/activitypub/assets/css/activitypub-admin.css index 29e78ee2..2e181f2a 100644 --- a/wp-content/plugins/activitypub/assets/css/activitypub-admin.css +++ b/wp-content/plugins/activitypub/assets/css/activitypub-admin.css @@ -1,12 +1,16 @@ .activitypub-settings { max-width: 800px; margin: 0 auto; + position: relative; } .settings_page_activitypub .notice { max-width: 800px; - margin: auto; - margin: 0px auto 30px; + margin: 0 auto 30px; +} + +.settings_page_activitypub .update-nag { + margin: 25px 20px 15px 22px; } .settings_page_activitypub .wrap { @@ -20,6 +24,15 @@ border-bottom: 1px solid #dcdcde; } +.activitypub-settings-header h1 { + display: inline-block; + font-weight: 600; + margin: 0 0.8rem 1rem; + font-size: 23px; + padding: 9px 0 4px; + line-height: 1.3; +} + .activitypub-settings-title-section { display: flex; align-items: center; @@ -33,11 +46,10 @@ } .activitypub-settings-tabs-wrapper { - display: -ms-inline-grid; - -ms-grid-columns: auto auto auto auto; + display: inline-flex; vertical-align: top; - display: inline-grid; - grid-template-columns: auto auto auto auto; + flex-wrap: nowrap; + gap: 0; } .activitypub-settings-tab.active { @@ -54,6 +66,20 @@ transition: box-shadow .5s ease-in-out; } +.activitypub-settings .row { + margin-bottom: 16px; +} + +.activitypub-settings .row > div { + max-width: calc(100% - 24px); + display: inline-flex; + flex-direction: column; +} + +.activitypub-settings .row .description { + margin-top: 0; +} + .wp-header-end { visibility: hidden; margin: -2px 0 0; @@ -168,8 +194,7 @@ input.blog-user-identifier { background-size: cover; } -.activitypub-settings -.logo { +.activitypub-settings .logo { height: 80px; width: 80px; position: relative; @@ -177,22 +202,6 @@ input.blog-user-identifier { left: 40px; } -.settings_page_activitypub .box { - border: 1px solid #c3c4c7; - background-color: #fff; - padding: 1em 1.5em; - margin-bottom: 1.5em; -} - -.settings_page_activitypub .activitypub-welcome-page .box label { - font-weight: bold; -} - -.settings_page_activitypub .activitypub-welcome-page input { - font-size: 20px; - width: 95%; -} - .settings_page_activitypub .plugin-recommendations { border-bottom: none; margin-bottom: 0; @@ -212,3 +221,52 @@ input.blog-user-identifier { .like .dashboard-comment-wrap .comment-author { margin-block: 0; } + +.activitypub-settings .welcome-tab-close { + position: absolute; + top: 0px; + right: 0px; + font-size: 13px; + padding: 0 5px 0 20px; + text-decoration: none; + z-index: 1; +} + +.activitypub-settings .welcome-tab-close::before { + position: absolute; + top: 0px; + left: 0; + transition: all .1s ease-in-out; + font: normal 16px/20px dashicons; + content: '\f335'; + font-size: 20px; +} + +.activitypub-notice .count { + display: inline-block; + vertical-align: top; + box-sizing: border-box; + margin: 1px 0 -1px 2px; + padding: 0 5px; + min-width: 18px; + height: 18px; + border-radius: 9px; + background-color: #dba617; + color: #fff; + font-size: 11px; + line-height: 1.6; + text-align: center; + z-index: 26; +} + +.activitypub-notice .dashicons-warning { + color: #dba617; +} + +.extra-fields-nav a + a { + margin-left: 8px; +} +.rtl .extra-fields-nav a + a { + margin-left: auto; + margin-right: 8px; +} diff --git a/wp-content/plugins/activitypub/assets/css/activitypub-embed.css b/wp-content/plugins/activitypub/assets/css/activitypub-embed.css new file mode 100644 index 00000000..0422fb40 --- /dev/null +++ b/wp-content/plugins/activitypub/assets/css/activitypub-embed.css @@ -0,0 +1,115 @@ +/** + * ActivityPub embed styles. + */ + +.activitypub-embed { + background: #fff; + border: 1px solid #e6e6e6; + border-radius: 12px; + padding: 0; + max-width: 100%; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; +} + +.activitypub-reply-block .activitypub-embed { + margin: 1em 0; +} + +.activitypub-embed-header { + padding: 15px; + display: flex; + align-items: center; + gap: 10px; +} + +.activitypub-embed-header img { + width: 48px; + height: 48px; + border-radius: 50%; +} + +.activitypub-embed-header-text { + flex-grow: 1; +} + +.activitypub-embed-header-text h2 { + color: #000; + font-size: 15px; + font-weight: 600; + margin: 0; + padding: 0; +} + +.activitypub-embed-header-text .ap-account { + color: #687684; + font-size: 14px; + text-decoration: none; +} + +.activitypub-embed-content { + padding: 0 15px 15px; +} + +.activitypub-embed-content .ap-title { + font-size: 23px; + font-weight: 600; + margin: 0 0 10px; + padding: 0; + color: #000; +} + +.activitypub-embed-content .ap-subtitle { + font-size: 15px; + color: #000; + margin: 0 0 15px; +} + +.activitypub-embed-content .ap-preview { + border: 1px solid #e6e6e6; + border-radius: 8px; + overflow: hidden; +} + +.activitypub-embed-content .ap-preview img { + width: 100%; + height: auto; + display: block; +} + +.activitypub-embed-content .ap-preview-text { + padding: 15px; +} + +.activitypub-embed-meta { + padding: 15px; + border-top: 1px solid #e6e6e6; + color: #687684; + font-size: 13px; + display: flex; + gap: 15px; +} + +.activitypub-embed-meta .ap-stat { + display: flex; + align-items: center; + gap: 5px; +} +@media only screen and (max-width: 399px) { + .activitypub-embed-meta span.ap-stat { + display: none !important; + } +} + +.activitypub-embed-meta a.ap-stat { + color: inherit; + text-decoration: none; +} + +.activitypub-embed-meta strong { + font-weight: 600; + color: #000; +} + +.activitypub-embed-meta .ap-stat-label { + color: #687684; +} diff --git a/wp-content/plugins/activitypub/assets/js/activitypub-header-image.js b/wp-content/plugins/activitypub/assets/js/activitypub-header-image.js index 8da23ed9..d43af9e9 100644 --- a/wp-content/plugins/activitypub/assets/js/activitypub-header-image.js +++ b/wp-content/plugins/activitypub/assets/js/activitypub-header-image.js @@ -128,14 +128,15 @@ title: $el.data( 'choose-text' ), library: wp.media.query( mediaQuery ), date: false, - suggestedWidth: $el.data( 'size' ), - suggestedHeight: $el.data( 'size' ), + suggestedWidth: $el.data( 'width' ), + suggestedHeight: $el.data( 'height' ), } ), new ImageCropperNoCustomizer( { control: { + id: 'activitypub-header-image', params: { - width: $el.data( 'size' ), - height: $el.data( 'size' ), + width: $el.data( 'width' ), + height: $el.data( 'height' ), }, }, imgSelectOptions: calculateImageSelectOptions, @@ -155,17 +156,26 @@ // When an image is selected, run a callback. frame.on( 'select', function () { // Grab the selected attachment. - var attachment = frame.state().get( 'selection' ).first(); + var attachment = frame.state().get( 'selection' ).first(), + targetRatio = $el.data( 'width' ) / $el.data( 'height' ), + currentRatio = attachment.attributes.width / attachment.attributes.height, + alreadyCropped = false; - if ( - attachment.attributes.height === $el.data( 'size' ) && - $el.data( 'size' ) === attachment.attributes.width - ) { + // Check if the image already has the correct aspect ratio (with a small tolerance). + if ( Math.abs( currentRatio - targetRatio ) < 0.01 ) { + // Check if this is the same image that was already selected. + if ( attachment.id !== parseInt( $hiddenDataField.val(), 10 ) ) { + // This is a new image with the correct aspect ratio. + $hiddenDataField.val( attachment.id ); + } + + alreadyCropped = true; + } + + if ( alreadyCropped ) { + // Skip cropping for already cropped images. switchToUpdate( attachment.attributes ); frame.close(); - - // Set the value of the hidden input to the attachment id. - $hiddenDataField.val( attachment.id ); } else { frame.setState( 'cropper' ); } diff --git a/wp-content/plugins/activitypub/build/editor-plugin/plugin.asset.php b/wp-content/plugins/activitypub/build/editor-plugin/plugin.asset.php index 67a3f181..b753ae85 100644 --- a/wp-content/plugins/activitypub/build/editor-plugin/plugin.asset.php +++ b/wp-content/plugins/activitypub/build/editor-plugin/plugin.asset.php @@ -1 +1 @@ - array('react', 'wp-components', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-i18n', 'wp-plugins'), 'version' => '88603987940fec29730d'); + array('react', 'wp-components', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => '293b8e75ac7a589c5096'); diff --git a/wp-content/plugins/activitypub/build/editor-plugin/plugin.js b/wp-content/plugins/activitypub/build/editor-plugin/plugin.js index c16cd66e..cad41787 100644 --- a/wp-content/plugins/activitypub/build/editor-plugin/plugin.js +++ b/wp-content/plugins/activitypub/build/editor-plugin/plugin.js @@ -1 +1 @@ -(()=>{"use strict";const t=window.React,e=window.wp.editor,n=window.wp.plugins,i=window.wp.components,o=window.wp.data,a=window.wp.coreData,r=window.wp.i18n;(0,n.registerPlugin)("activitypub-editor-plugin",{render:()=>{const n=(0,o.useSelect)((t=>t("core/editor").getCurrentPostType()),[]),[w,c]=(0,a.useEntityProp)("postType",n,"meta");return(0,t.createElement)(e.PluginDocumentSettingPanel,{name:"activitypub",title:(0,r.__)("Fediverse","activitypub")},(0,t.createElement)(i.TextControl,{label:(0,r.__)("Content Warning","activitypub"),value:w?.activitypub_content_warning,onChange:t=>{c({...w,activitypub_content_warning:t})},placeholder:(0,r.__)("Optional content warning","activitypub")}))}})})(); \ No newline at end of file +(()=>{"use strict";var e={20:(e,t,i)=>{var n=i(609),o=Symbol.for("react.element"),r=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=n.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,a={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,i){var n,c={},s=null,p=null;for(n in void 0!==i&&(s=""+i),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(p=t.ref),t)r.call(t,n)&&!a.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:p,props:c,_owner:l.current}}},848:(e,t,i)=>{e.exports=i(20)},609:e=>{e.exports=window.React}},t={};function i(n){var o=t[n];if(void 0!==o)return o.exports;var r=t[n]={exports:{}};return e[n](r,r.exports,i),r.exports}var n=i(609);const o=window.wp.editor,r=window.wp.plugins,l=window.wp.components,a=window.wp.element,c=(0,a.forwardRef)((function({icon:e,size:t=24,...i},n){return(0,a.cloneElement)(e,{width:t,height:t,...i,ref:n})})),s=window.wp.primitives;var p=i(848);const u=(0,p.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(s.Path,{d:"M12 3.3c-4.8 0-8.8 3.9-8.8 8.8 0 4.8 3.9 8.8 8.8 8.8 4.8 0 8.8-3.9 8.8-8.8s-4-8.8-8.8-8.8zm6.5 5.5h-2.6C15.4 7.3 14.8 6 14 5c2 .6 3.6 2 4.5 3.8zm.7 3.2c0 .6-.1 1.2-.2 1.8h-2.9c.1-.6.1-1.2.1-1.8s-.1-1.2-.1-1.8H19c.2.6.2 1.2.2 1.8zM12 18.7c-1-.7-1.8-1.9-2.3-3.5h4.6c-.5 1.6-1.3 2.9-2.3 3.5zm-2.6-4.9c-.1-.6-.1-1.1-.1-1.8 0-.6.1-1.2.1-1.8h5.2c.1.6.1 1.1.1 1.8s-.1 1.2-.1 1.8H9.4zM4.8 12c0-.6.1-1.2.2-1.8h2.9c-.1.6-.1 1.2-.1 1.8 0 .6.1 1.2.1 1.8H5c-.2-.6-.2-1.2-.2-1.8zM12 5.3c1 .7 1.8 1.9 2.3 3.5H9.7c.5-1.6 1.3-2.9 2.3-3.5zM10 5c-.8 1-1.4 2.3-1.8 3.8H5.5C6.4 7 8 5.6 10 5zM5.5 15.3h2.6c.4 1.5 1 2.8 1.8 3.7-1.8-.6-3.5-2-4.4-3.7zM14 19c.8-1 1.4-2.2 1.8-3.7h2.6C17.6 17 16 18.4 14 19z"})}),v=(0,p.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(s.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"})}),w=(0,p.jsx)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,p.jsx)(s.Path,{d:"M19.5 4.5h-7V6h4.44l-5.97 5.97 1.06 1.06L18 7.06v4.44h1.5v-7Zm-13 1a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-3H17v3a.5.5 0 0 1-.5.5h-10a.5.5 0 0 1-.5-.5v-10a.5.5 0 0 1 .5-.5h3V5.5h-3Z"})}),d=window.wp.data,_=window.wp.coreData,b=window.wp.url,h=window.wp.i18n,y=(0,n.createElement)(s.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,n.createElement)(s.Path,{fillRule:"evenodd",clipRule:"evenodd",d:"M12 18.5A6.5 6.5 0 0 1 6.93 7.931l9.139 9.138A6.473 6.473 0 0 1 12 18.5Zm5.123-2.498a6.5 6.5 0 0 0-9.124-9.124l9.124 9.124ZM4 12a8 8 0 1 1 16 0 8 8 0 0 1-16 0Z"}));(0,r.registerPlugin)("activitypub-editor-plugin",{render:()=>{const e=(0,d.useSelect)((e=>e("core/editor").getCurrentPostType()),[]),[t,i]=(0,_.useEntityProp)("postType",e,"meta"),r={verticalAlign:"middle",gap:"4px",justifyContent:"start",display:"inline-flex",alignItems:"center"},a=(e,t,i)=>(0,n.createElement)(l.Tooltip,{text:i},(0,n.createElement)(l.__experimentalText,{style:r},(0,n.createElement)(c,{icon:e}),t));return"wp_block"===e?null:(0,n.createElement)(o.PluginDocumentSettingPanel,{name:"activitypub",title:(0,h.__)("Fediverse ⁂","activitypub")},(0,n.createElement)(l.TextControl,{label:(0,h.__)("Content Warning","activitypub"),value:t?.activitypub_content_warning,onChange:e=>{i({...t,activitypub_content_warning:e})},placeholder:(0,h.__)("Optional content warning","activitypub"),help:(0,h.__)("Content warnings do not change the content on your site, only in the fediverse.","activitypub")}),(0,n.createElement)(l.RadioControl,{label:(0,h.__)("Visibility","activitypub"),help:(0,h.__)("This adjusts the visibility of a post in the fediverse, but note that it won't affect how the post appears on the blog.","activitypub"),selected:t?.activitypub_content_visibility||"public",options:[{label:a(u,(0,h.__)("Public","activitypub"),(0,h.__)("Post will be visible to everyone and appear in public timelines.","activitypub")),value:"public"},{label:a(v,(0,h.__)("Quiet public","activitypub"),(0,h.__)("Post will be visible to everyone but will not appear in public timelines.","activitypub")),value:"quiet_public"},{label:a(y,(0,h.__)("Do not federate","activitypub"),(0,h.__)("Post will not be shared to the Fediverse.","activitypub")),value:"local"}],onChange:e=>{i({...t,activitypub_content_visibility:e})},className:"activitypub-visibility"}))}}),(0,r.registerPlugin)("activitypub-editor-preview",{render:()=>{const e=(0,d.useSelect)((e=>e("core/editor").getCurrentPost().status));return(0,n.createElement)(n.Fragment,null,o.PluginPreviewMenuItem?(0,n.createElement)(o.PluginPreviewMenuItem,{onClick:()=>function(){const e=(0,d.select)("core/editor").getEditedPostPreviewLink(),t=(0,b.addQueryArgs)(e,{activitypub:"true"});window.open(t,"_blank")}(),icon:w,disabled:"auto-draft"===e},(0,h.__)("Fediverse preview ⁂","activitypub")):null)}})})(); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/follow-me/block.json b/wp-content/plugins/activitypub/build/follow-me/block.json index edeb67c7..e799fbb5 100644 --- a/wp-content/plugins/activitypub/build/follow-me/block.json +++ b/wp-content/plugins/activitypub/build/follow-me/block.json @@ -36,6 +36,23 @@ "selectedUser": { "type": "string", "default": "site" + }, + "buttonOnly": { + "type": "boolean", + "default": false + }, + "buttonText": { + "type": "string", + "default": "Follow" + }, + "buttonSize": { + "type": "string", + "default": "default", + "enum": [ + "small", + "default", + "compact" + ] } }, "usesContext": [ diff --git a/wp-content/plugins/activitypub/build/follow-me/index.asset.php b/wp-content/plugins/activitypub/build/follow-me/index.asset.php index 57106a2a..48631347 100644 --- a/wp-content/plugins/activitypub/build/follow-me/index.asset.php +++ b/wp-content/plugins/activitypub/build/follow-me/index.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '1ec66c1edf3d9b0b6678'); + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '8f1a6f7e5f76d58a3204'); diff --git a/wp-content/plugins/activitypub/build/follow-me/index.js b/wp-content/plugins/activitypub/build/follow-me/index.js index 8253fede..e6dae559 100644 --- a/wp-content/plugins/activitypub/build/follow-me/index.js +++ b/wp-content/plugins/activitypub/build/follow-me/index.js @@ -1,2 +1,3 @@ -(()=>{"use strict";var e,t={399:(e,t,r)=>{const o=window.wp.blocks,n=window.wp.primitives;var a=r(848);const l=(0,a.jsx)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,a.jsx)(n.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 i=r(609);const c=window.wp.blockEditor,s=window.wp.i18n,u=window.wp.data,p=window.wp.coreData,m=window.wp.components,d=window.wp.element,v=window._activityPubOptions?.enabled,f=window.wp.apiFetch;var y=r.n(f);function b(e){return`var(--wp--preset--color--${e})`}function _(e){if("string"!=typeof e)return null;if(e.match(/^#/))return e.substring(0,7);const[,,t]=e.split("|");return b(t)}function w(e,t,r=null,o=""){return r?`${e}${o} { ${t}: ${r}; }\n`:""}function h(e,t,r,o){return w(e,"background-color",t)+w(e,"color",r)+w(e,"background-color",o,":hover")+w(e,"background-color",o,":focus")}function g({selector:e,style:t,backgroundColor:r}){const o=function(e,t,r){const o=`${e} .components-button`,n=("string"==typeof(a=r)?b(a):a?.color?.background||null)||t?.color?.background;var a;return h(o,_(t?.elements?.link?.color?.text),n,_(t?.elements?.link?.[":hover"]?.color?.text))}(e,t,r);return(0,i.createElement)("style",null,o)}const E=(0,a.jsx)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,a.jsx)(n.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"})}),k=(0,a.jsx)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,a.jsx)(n.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),x=(0,d.forwardRef)((function({icon:e,size:t=24,...r},o){return(0,d.cloneElement)(e,{width:t,height:t,...r,ref:o})})),S=window.wp.compose,O="fediverse-remote-user";function C(e){try{return new URL(e),!0}catch(e){return!1}}function I({actionText:e,copyDescription:t,handle:r,resourceUrl:o,myProfile:n=!1,rememberProfile:a=!1}){const c=(0,s.__)("Loading...","activitypub"),u=(0,s.__)("Opening...","activitypub"),p=(0,s.__)("Error","activitypub"),v=(0,s.__)("Invalid","activitypub"),f=n||(0,s.__)("My Profile","activitypub"),[b,_]=(0,d.useState)(e),[w,h]=(0,d.useState)(E),g=(0,S.useCopyToClipboard)(r,(()=>{h(k),setTimeout((()=>h(E)),1e3)})),[I,N]=(0,d.useState)(""),[R,U]=(0,d.useState)(!0),{setRemoteUser:P}=function(){const[e,t]=(0,d.useState)(function(){const e=localStorage.getItem(O);return e?JSON.parse(e):{}}()),r=(0,d.useCallback)((e=>{!function(e){localStorage.setItem(O,JSON.stringify(e))}(e),t(e)}),[]),o=(0,d.useCallback)((()=>{localStorage.removeItem(O),t({})}),[]);return{template:e?.template||!1,profileURL:e?.profileURL||!1,setRemoteUser:r,deleteRemoteUser:o}}(),T=(0,d.useCallback)((()=>{let t;if(!C(I)&&!function(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&C(`https://${t[1]}`)}(I))return _(v),t=setTimeout((()=>_(e)),2e3),()=>clearTimeout(t);const r=o+I;_(c),y()({path:r}).then((({url:t,template:r})=>{R&&P({profileURL:I,template:r}),_(u),setTimeout((()=>{window.open(t,"_blank"),_(e)}),200)})).catch((()=>{_(p),setTimeout((()=>_(e)),2e3)}))}),[I]);return(0,i.createElement)("div",{className:"activitypub__dialog"},(0,i.createElement)("div",{className:"activitypub-dialog__section"},(0,i.createElement)("h4",null,f),(0,i.createElement)("div",{className:"activitypub-dialog__description"},t),(0,i.createElement)("div",{className:"activitypub-dialog__button-group"},(0,i.createElement)("input",{type:"text",value:r,readOnly:!0}),(0,i.createElement)(m.Button,{ref:g},(0,i.createElement)(x,{icon:w}),(0,s.__)("Copy","activitypub")))),(0,i.createElement)("div",{className:"activitypub-dialog__section"},(0,i.createElement)("h4",null,(0,s.__)("Your Profile","activitypub")),(0,i.createElement)("div",{className:"activitypub-dialog__description"},(0,d.createInterpolateElement)((0,s.__)("Or, if you know your own profile, we can start things that way! (eg @yourusername@example.com)","activitypub"),{code:(0,i.createElement)("code",null)})),(0,i.createElement)("div",{className:"activitypub-dialog__button-group"},(0,i.createElement)("input",{type:"text",value:I,onKeyDown:e=>{"Enter"===e?.code&&T()},onChange:e=>N(e.target.value)}),(0,i.createElement)(m.Button,{onClick:T},(0,i.createElement)(x,{icon:l}),b)),a&&(0,i.createElement)("div",{className:"activitypub-dialog__remember"},(0,i.createElement)(m.CheckboxControl,{checked:R,label:(0,s.__)("Remember me for easier comments","activitypub"),onChange:()=>{U(!R)}}))))}const{namespace:N}=window._activityPubOptions,R={avatar:"",webfinger:"@well@hello.dolly",name:(0,s.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function U(e){if(!e)return R;const t={...R,...e};return t.avatar=t?.icon?.url,t}function P({profile:e,popupStyles:t,userId:r}){const{webfinger:o,avatar:n,name:a}=e,l=o.startsWith("@")?o:`@${o}`;return(0,i.createElement)("div",{className:"activitypub-profile"},(0,i.createElement)("img",{className:"activitypub-profile__avatar",src:n,alt:a}),(0,i.createElement)("div",{className:"activitypub-profile__content"},(0,i.createElement)("div",{className:"activitypub-profile__name"},a),(0,i.createElement)("div",{className:"activitypub-profile__handle",title:l},l)),(0,i.createElement)(T,{profile:e,popupStyles:t,userId:r}))}function T({profile:e,popupStyles:t,userId:r}){const[o,n]=(0,d.useState)(!1),a=(0,s.sprintf)((0,s.__)("Follow %s","activitypub"),e?.name);return(0,i.createElement)(i.Fragment,null,(0,i.createElement)(m.Button,{className:"activitypub-profile__follow",onClick:()=>n(!0)},(0,s.__)("Follow","activitypub")),o&&(0,i.createElement)(m.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>n(!1),title:a},(0,i.createElement)($,{profile:e,userId:r}),(0,i.createElement)("style",null,t)))}function $({profile:e,userId:t}){const{webfinger:r}=e,o=(0,s.__)("Follow","activitypub"),n=`/${N}/actors/${t}/remote-follow?resource=`,a=(0,s.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),l=r.startsWith("@")?r:`@${r}`;return(0,i.createElement)(I,{actionText:o,copyDescription:a,handle:l,resourceUrl:n})}function j({selectedUser:e,style:t,backgroundColor:r,id:o,useId:n=!1,profileData:a=!1}){const[l,c]=(0,d.useState)(U()),s="site"===e?0:e,u=function(e){return h(".apfmd__button-group .components-button",_(e?.elements?.link?.color?.text)||"#111","#fff",_(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),p=n?{id:o}:{};function m(e){c(U(e))}return(0,d.useEffect)((()=>{if(a)return m(a);(function(e){const t={headers:{Accept:"application/activity+json"},path:`/${N}/actors/${e}`};return y()(t)})(s).then(m)}),[s,a]),(0,i.createElement)("div",{...p},(0,i.createElement)(g,{selector:`#${o}`,style:t,backgroundColor:r}),(0,i.createElement)(P,{profile:l,userId:s,popupStyles:u}))}function B({name:e}){const t=(0,s.sprintf)(/* translators: %s: block name */ -"This %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. It will be empty in other non-author contexts.",e);return(0,i.createElement)(m.Card,null,(0,i.createElement)(m.CardBody,null,(0,d.createInterpolateElement)(t,{strong:(0,i.createElement)("strong",null)})))}(0,o.registerBlockType)("activitypub/follow-me",{edit:function({attributes:e,setAttributes:t,context:{postType:r,postId:o}}){const n=(0,c.useBlockProps)({className:"activitypub-follow-me-block-wrapper"}),a=function({withInherit:e=!1}){const t=v?.users?(0,u.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,d.useMemo)((()=>{if(!t)return[];const r=[];return v?.site&&r.push({label:(0,s.__)("Site","activitypub"),value:"site"}),e&&v?.users&&r.push({label:(0,s.__)("Dynamic User","activitypub"),value:"inherit"}),t.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),r)}),[t])}({withInherit:!0}),{selectedUser:l}=e,f="inherit"===l,y=(0,u.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),n=t("postType",r,o)?.author;return null!=n?n:null}),[r,o]);return(0,d.useEffect)((()=>{a.length&&(a.find((({value:e})=>e===l))||t({selectedUser:a[0].value}))}),[l,a]),(0,i.createElement)("div",{...n},a.length>1&&(0,i.createElement)(c.InspectorControls,{key:"setting"},(0,i.createElement)(m.PanelBody,{title:(0,s.__)("Followers Options","activitypub")},(0,i.createElement)(m.SelectControl,{label:(0,s.__)("Select User","activitypub"),value:e.selectedUser,options:a,onChange:e=>t({selectedUser:e})}))),f?y?(0,i.createElement)(j,{...e,id:n.id,selectedUser:y}):(0,i.createElement)(B,{name:(0,s.__)("Follow Me","activitypub")}):(0,i.createElement)(j,{...e,id:n.id}))},save:()=>null,icon:l})},20:(e,t,r)=>{var o=r(609),n=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=o.__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,r){var o,c={},s=null,u=null;for(o in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(u=t.ref),t)a.call(t,o)&&!i.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:n,type:e,key:s,ref:u,props:c,_owner:l.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},r={};function o(e){var n=r[e];if(void 0!==n)return n.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,o),a.exports}o.m=t,e=[],o.O=(t,r,n,a)=>{if(!r){var l=1/0;for(u=0;u=a)&&Object.keys(o.O).every((e=>o.O[e](r[c])))?r.splice(c--,1):(i=!1,a0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[r,n,a]},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={338:0,301:0};o.O.j=t=>0===e[t];var t=(t,r)=>{var n,a,[l,i,c]=r,s=0;if(l.some((t=>0!==e[t]))){for(n in i)o.o(i,n)&&(o.m[n]=i[n]);if(c)var u=c(o)}for(t&&t(r);so(399)));n=o.O(n)})(); \ No newline at end of file +(()=>{"use strict";var e,t={919:(e,t,o)=>{const r=window.wp.blocks,n=window.wp.primitives;var l=o(848);const a=(0,l.jsx)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,l.jsx)(n.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 i=o(609);const c=window.wp.blockEditor,u=window.wp.i18n,s=window.wp.data,p=window.wp.coreData,d=window.wp.components,m=window.wp.element;function v(){return window._activityPubOptions||{}}const b=window.wp.apiFetch;var f=o.n(b);function y(e){return`var(--wp--preset--color--${e})`}function _(e){if("string"!=typeof e)return null;if(e.match(/^#/))return e.substring(0,7);const[,,t]=e.split("|");return y(t)}function h(e,t,o=null,r=""){return o?`${e}${r} { ${t}: ${o}; }\n`:""}function w(e,t,o,r){return h(e,"background-color",t)+h(e,"color",o)+h(e,"background-color",r,":hover")+h(e,"background-color",r,":focus")}function g({selector:e,style:t,backgroundColor:o}){const r=function(e,t,o){const r=`${e} .components-button`,n=("string"==typeof(l=o)?y(l):l?.color?.background||null)||t?.color?.background;var l;return w(r,_(t?.elements?.link?.color?.text),n,_(t?.elements?.link?.[":hover"]?.color?.text))}(e,t,o);return(0,i.createElement)("style",null,r)}const E=(0,l.jsx)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,l.jsx)(n.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"})}),x=(0,l.jsx)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,l.jsx)(n.Path,{d:"M16.7 7.1l-6.3 8.5-3.3-2.5-.9 1.2 4.5 3.4L17.9 8z"})}),S=(0,m.forwardRef)((function({icon:e,size:t=24,...o},r){return(0,m.cloneElement)(e,{width:t,height:t,...o,ref:r})})),k=window.wp.compose,C="fediverse-remote-user";function O(e){try{return new URL(e),!0}catch(e){return!1}}function T({actionText:e,copyDescription:t,handle:o,resourceUrl:r,myProfile:n="",rememberProfile:l=!1}){const c=(0,u.__)("Loading...","activitypub"),s=(0,u.__)("Opening...","activitypub"),p=(0,u.__)("Error","activitypub"),v=(0,u.__)("Invalid","activitypub"),b=n||(0,u.__)("My Profile","activitypub"),[y,_]=(0,m.useState)(e),[h,w]=(0,m.useState)(E),g=(0,k.useCopyToClipboard)(o,(()=>{w(x),setTimeout((()=>w(E)),1e3)})),[T,N]=(0,m.useState)(""),[I,R]=(0,m.useState)(!0),{setRemoteUser:U}=function(){const[e,t]=(0,m.useState)(function(){const e=localStorage.getItem(C);return e?JSON.parse(e):{}}()),o=(0,m.useCallback)((e=>{!function(e){localStorage.setItem(C,JSON.stringify(e))}(e),t(e)}),[]),r=(0,m.useCallback)((()=>{localStorage.removeItem(C),t({})}),[]);return{template:e?.template||!1,profileURL:e?.profileURL||!1,setRemoteUser:o,deleteRemoteUser:r}}(),z=(0,m.useCallback)((()=>{let t;if(!O(T)&&!function(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&O(`https://${t[1]}`)}(T))return _(v),t=setTimeout((()=>_(e)),2e3),()=>clearTimeout(t);const o=r+T;_(c),f()({path:o}).then((({url:t,template:o})=>{I&&U({profileURL:T,template:o}),_(s),setTimeout((()=>{window.open(t,"_blank"),_(e)}),200)})).catch((()=>{_(p),setTimeout((()=>_(e)),2e3)}))}),[T]);return(0,i.createElement)("div",{className:"activitypub__dialog",role:"dialog","aria-labelledby":"dialog-title"},(0,i.createElement)("div",{className:"activitypub-dialog__section"},(0,i.createElement)("h4",{id:"dialog-title"},b),(0,i.createElement)("div",{className:"activitypub-dialog__description",id:"copy-description"},t),(0,i.createElement)("div",{className:"activitypub-dialog__button-group"},(0,i.createElement)("label",{htmlFor:"profile-handle",className:"screen-reader-text"},t),(0,i.createElement)("input",{type:"text",id:"profile-handle",value:o,readOnly:!0}),(0,i.createElement)(d.Button,{ref:g,"aria-label":(0,u.__)("Copy handle to clipboard","activitypub")},(0,i.createElement)(S,{icon:h}),(0,u.__)("Copy","activitypub")))),(0,i.createElement)("div",{className:"activitypub-dialog__section"},(0,i.createElement)("h4",{id:"remote-profile-title"},(0,u.__)("Your Profile","activitypub")),(0,i.createElement)("div",{className:"activitypub-dialog__description",id:"remote-profile-description"},(0,m.createInterpolateElement)((0,u.__)("Or, if you know your own profile, we can start things that way! (eg @yourusername@example.com)","activitypub"),{code:(0,i.createElement)("code",null)})),(0,i.createElement)("div",{className:"activitypub-dialog__button-group"},(0,i.createElement)("label",{htmlFor:"remote-profile",className:"screen-reader-text"},(0,u.__)("Enter your ActivityPub profile","activitypub")),(0,i.createElement)("input",{type:"text",id:"remote-profile",value:T,onKeyDown:e=>{"Enter"===e?.code&&z()},onChange:e=>N(e.target.value),"aria-invalid":y===v}),(0,i.createElement)(d.Button,{onClick:z,"aria-label":(0,u.__)("Submit profile","activitypub")},(0,i.createElement)(S,{icon:a}),y)),l&&(0,i.createElement)("div",{className:"activitypub-dialog__remember"},(0,i.createElement)(d.CheckboxControl,{checked:I,label:(0,u.__)("Remember me for easier comments","activitypub"),onChange:()=>{R(!I)}}))))}const N={avatar:"",webfinger:"@well@hello.dolly",name:(0,u.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function I(e){if(!e)return N;const t={...N,...e};return t.avatar=t?.icon?.url,t}function R({profile:e,popupStyles:t,userId:o,buttonText:r,buttonOnly:n,buttonSize:l}){const{webfinger:a,avatar:c,name:u}=e,s=a.startsWith("@")?a:`@${a}`;return n?(0,i.createElement)("div",{className:"activitypub-profile"},(0,i.createElement)(U,{profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:l})):(0,i.createElement)("div",{className:"activitypub-profile"},(0,i.createElement)("img",{className:"activitypub-profile__avatar",src:c,alt:u}),(0,i.createElement)("div",{className:"activitypub-profile__content"},(0,i.createElement)("div",{className:"activitypub-profile__name"},u),(0,i.createElement)("div",{className:"activitypub-profile__handle",title:s},s)),(0,i.createElement)(U,{profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:l}))}function U({profile:e,popupStyles:t,userId:o,buttonText:r,buttonSize:n}){const[l,a]=(0,m.useState)(!1),c=(0,u.sprintf)(/* translators: %s: profile name */ /* translators: %s: profile name */ +(0,u.__)("Follow %s","activitypub"),e?.name);return(0,i.createElement)(i.Fragment,null,(0,i.createElement)(d.Button,{className:"activitypub-profile__follow",onClick:()=>a(!0),"aria-haspopup":"dialog","aria-expanded":l,"aria-label":(0,u.__)("Follow me on the Fediverse","activitypub"),size:n},r),l&&(0,i.createElement)(d.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>a(!1),title:c,"aria-label":c,role:"dialog"},(0,i.createElement)(z,{profile:e,userId:o}),(0,i.createElement)("style",null,t)))}function z({profile:e,userId:t}){const{namespace:o}=v(),{webfinger:r}=e,n=(0,u.__)("Follow","activitypub"),l=`/${o}/actors/${t}/remote-follow?resource=`,a=(0,u.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),c=r.startsWith("@")?r:`@${r}`;return(0,i.createElement)(T,{actionText:n,copyDescription:a,handle:c,resourceUrl:l})}function $({selectedUser:e,style:t,backgroundColor:o,id:r,useId:n=!1,profileData:l=!1,buttonOnly:a=!1,buttonText:c=(0,u.__)("Follow","activitypub"),buttonSize:s="default"}){const[p,d]=(0,m.useState)(I()),b="site"===e?0:e,y=function(e){return w(".apfmd__button-group .components-button",_(e?.elements?.link?.color?.text)||"#111","#fff",_(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),h=n?{id:r}:{};return(0,m.useEffect)((()=>{l?d(I(l)):function(e){const{namespace:t}=v(),o={headers:{Accept:"application/activity+json"},path:`/${t}/actors/${e}`};return f()(o)}(b).then((e=>{d(I(e))}))}),[b,l]),(0,i.createElement)("div",{...h,className:"activitypub-follow-me-block-wrapper"},(0,i.createElement)(g,{selector:`#${r}`,style:t,backgroundColor:o}),(0,i.createElement)(R,{profile:p,userId:b,popupStyles:y,buttonText:c,buttonOnly:a,buttonSize:s}))}function P({name:e}){const{enabled:t}=v(),o=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,o).trim();return(0,i.createElement)(d.Card,null,(0,i.createElement)(d.CardBody,null,(0,m.createInterpolateElement)(r,{strong:(0,i.createElement)("strong",null)})))}(0,r.registerBlockType)("activitypub/follow-me",{edit:function({attributes:e,setAttributes:t,context:{postType:o,postId:r}}){const n=(0,c.useBlockProps)({className:"activitypub-follow-me-block-wrapper"}),l=function({withInherit:e=!1}){const{enabled:t}=v(),o=t?.users?(0,s.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,m.useMemo)((()=>{if(!o)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"}),o.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),r)}),[o])}({withInherit:!0}),{selectedUser:a,buttonOnly:b,buttonText:f,buttonSize:y}=e,_="inherit"===a,h=(0,s.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),n=t("postType",o,r)?.author;return null!=n?n:null}),[o,r]);return(0,m.useEffect)((()=>{l.length&&(l.find((({value:e})=>e===a))||t({selectedUser:l[0].value}))}),[a,l]),(0,i.createElement)("div",{...n},(0,i.createElement)(c.InspectorControls,{key:"activitypub-follow-me"},(0,i.createElement)(d.PanelBody,{title:(0,u.__)("Follow Me Options","activitypub")},l.length>1&&(0,i.createElement)(d.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:e.selectedUser,options:l,onChange:e=>t({selectedUser:e})}),(0,i.createElement)(d.ToggleControl,{label:(0,u.__)("Button Only Mode","activitypub"),checked:b,onChange:e=>t({buttonOnly:e}),help:(0,u.__)("Only show the follow button without profile information","activitypub")}),(0,i.createElement)(d.TextControl,{label:(0,u.__)("Button Text","activitypub"),value:f,onChange:e=>t({buttonText:e})}),(0,i.createElement)(d.SelectControl,{label:(0,u.__)("Button Size","activitypub"),value:y,options:[{label:(0,u.__)("Default","activitypub"),value:"default"},{label:(0,u.__)("Compact","activitypub"),value:"compact"},{label:(0,u.__)("Small","activitypub"),value:"small"}],onChange:e=>t({buttonSize:e}),help:(0,u.__)("Choose the size of the follow button","activitypub")}))),_?h?(0,i.createElement)($,{...e,id:n.id,selectedUser:h}):(0,i.createElement)(P,{name:(0,u.__)("Follow Me","activitypub")}):(0,i.createElement)($,{...e,id:n.id}))},save:()=>null,icon:a})},20:(e,t,o)=>{var r=o(609),n=Symbol.for("react.element"),l=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),a=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,o){var r,c={},u=null,s=null;for(r in void 0!==o&&(u=""+o),void 0!==t.key&&(u=""+t.key),void 0!==t.ref&&(s=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:u,ref:s,props:c,_owner:a.current}}},848:(e,t,o)=>{e.exports=o(20)},609:e=>{e.exports=window.React}},o={};function r(e){var n=o[e];if(void 0!==n)return n.exports;var l=o[e]={exports:{}};return t[e](l,l.exports,r),l.exports}r.m=t,e=[],r.O=(t,o,n,l)=>{if(!o){var a=1/0;for(s=0;s=l)&&Object.keys(r.O).every((e=>r.O[e](o[c])))?o.splice(c--,1):(i=!1,l0&&e[s-1][2]>l;s--)e[s]=e[s-1];e[s]=[o,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 o in t)r.o(t,o)&&!r.o(e,o)&&Object.defineProperty(e,o,{enumerable:!0,get:t[o]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={338:0,301:0};r.O.j=t=>0===e[t];var t=(t,o)=>{var n,l,a=o[0],i=o[1],c=o[2],u=0;if(a.some((t=>0!==e[t]))){for(n in i)r.o(i,n)&&(r.m[n]=i[n]);if(c)var s=c(r)}for(t&&t(o);ur(919)));n=r.O(n)})(); \ No newline at end of file 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 index 782f5765..eb3c8508 100644 --- a/wp-content/plugins/activitypub/build/follow-me/style-view-rtl.css +++ b/wp-content/plugins/activitypub/build/follow-me/style-view-rtl.css @@ -1 +1 @@ -.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-right:1rem;padding-left:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-left:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white);margin-right:1rem} +.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-right:1rem;padding-left:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-left:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white)}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow:not(:only-child){margin-right:1rem} diff --git a/wp-content/plugins/activitypub/build/follow-me/style-view.css b/wp-content/plugins/activitypub/build/follow-me/style-view.css index fdc7405a..1c28cab0 100644 --- a/wp-content/plugins/activitypub/build/follow-me/style-view.css +++ b/wp-content/plugins/activitypub/build/follow-me/style-view.css @@ -1 +1 @@ -.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-left:1rem;padding-right:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white);margin-left:1rem} +.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-left:1rem;padding-right:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white)}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow:not(:only-child){margin-left:1rem} 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 b2652268..2f83bf76 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' => 'bc272e3d4aaa7992f4c7'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '635ed3e6db3230ae865f'); diff --git a/wp-content/plugins/activitypub/build/follow-me/view.js b/wp-content/plugins/activitypub/build/follow-me/view.js index 4b77f8c0..fd9c268e 100644 --- a/wp-content/plugins/activitypub/build/follow-me/view.js +++ b/wp-content/plugins/activitypub/build/follow-me/view.js @@ -1 +1,2 @@ -(()=>{"use strict";var e,t={729:(e,t,r)=>{var o=r(609);const n=window.wp.element,a=window.wp.domReady;var l=r.n(a);const i=window.wp.apiFetch;var c=r.n(i);const s=window.wp.components,u=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 v(e,t,r=null,o=""){return r?`${e}${o} { ${t}: ${r}; }\n`:""}function d(e,t,r,o){return v(e,"background-color",t)+v(e,"color",r)+v(e,"background-color",o,":hover")+v(e,"background-color",o,":focus")}function f({selector:e,style:t,backgroundColor:r}){const n=function(e,t,r){const o=`${e} .components-button`,n=("string"==typeof(a=r)?p(a):a?.color?.background||null)||t?.color?.background;var a;return d(o,m(t?.elements?.link?.color?.text),n,m(t?.elements?.link?.[":hover"]?.color?.text))}(e,t,r);return(0,o.createElement)("style",null,n)}const y=window.wp.primitives;var _=r(848);const b=(0,_.jsx)(y.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,_.jsx)(y.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,_.jsx)(y.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,_.jsx)(y.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,n.forwardRef)((function({icon:e,size:t=24,...r},o){return(0,n.cloneElement)(e,{width:t,height:t,...r,ref:o})})),g=(0,_.jsx)(y.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,_.jsx)(y.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,k="fediverse-remote-user";function x(e){try{return new URL(e),!0}catch(e){return!1}}function O({actionText:e,copyDescription:t,handle:r,resourceUrl:a,myProfile:l=!1,rememberProfile:i=!1}){const p=(0,u.__)("Loading...","activitypub"),m=(0,u.__)("Opening...","activitypub"),v=(0,u.__)("Error","activitypub"),d=(0,u.__)("Invalid","activitypub"),f=l||(0,u.__)("My Profile","activitypub"),[y,_]=(0,n.useState)(e),[O,S]=(0,n.useState)(b),C=(0,E.useCopyToClipboard)(r,(()=>{S(w),setTimeout((()=>S(b)),1e3)})),[N,R]=(0,n.useState)(""),[I,$]=(0,n.useState)(!0),{setRemoteUser:P}=function(){const[e,t]=(0,n.useState)(function(){const e=localStorage.getItem(k);return e?JSON.parse(e):{}}()),r=(0,n.useCallback)((e=>{!function(e){localStorage.setItem(k,JSON.stringify(e))}(e),t(e)}),[]),o=(0,n.useCallback)((()=>{localStorage.removeItem(k),t({})}),[]);return{template:e?.template||!1,profileURL:e?.profileURL||!1,setRemoteUser:r,deleteRemoteUser:o}}(),j=(0,n.useCallback)((()=>{let t;if(!x(N)&&!function(e){const t=e.replace(/^@/,"").split("@");return 2===t.length&&x(`https://${t[1]}`)}(N))return _(d),t=setTimeout((()=>_(e)),2e3),()=>clearTimeout(t);const r=a+N;_(p),c()({path:r}).then((({url:t,template:r})=>{I&&P({profileURL:N,template:r}),_(m),setTimeout((()=>{window.open(t,"_blank"),_(e)}),200)})).catch((()=>{_(v),setTimeout((()=>_(e)),2e3)}))}),[N]);return(0,o.createElement)("div",{className:"activitypub__dialog"},(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",null,f),(0,o.createElement)("div",{className:"activitypub-dialog__description"},t),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("input",{type:"text",value:r,readOnly:!0}),(0,o.createElement)(s.Button,{ref:C},(0,o.createElement)(h,{icon:O}),(0,u.__)("Copy","activitypub")))),(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",null,(0,u.__)("Your Profile","activitypub")),(0,o.createElement)("div",{className:"activitypub-dialog__description"},(0,n.createInterpolateElement)((0,u.__)("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)("input",{type:"text",value:N,onKeyDown:e=>{"Enter"===e?.code&&j()},onChange:e=>R(e.target.value)}),(0,o.createElement)(s.Button,{onClick:j},(0,o.createElement)(h,{icon:g}),y)),i&&(0,o.createElement)("div",{className:"activitypub-dialog__remember"},(0,o.createElement)(s.CheckboxControl,{checked:I,label:(0,u.__)("Remember me for easier comments","activitypub"),onChange:()=>{$(!I)}}))))}const{namespace:S}=window._activityPubOptions,C={avatar:"",webfinger:"@well@hello.dolly",name:(0,u.__)("Hello Dolly Fan Account","activitypub"),url:"#"};function N(e){if(!e)return C;const t={...C,...e};return t.avatar=t?.icon?.url,t}function R({profile:e,popupStyles:t,userId:r}){const{webfinger:n,avatar:a,name:l}=e,i=n.startsWith("@")?n:`@${n}`;return(0,o.createElement)("div",{className:"activitypub-profile"},(0,o.createElement)("img",{className:"activitypub-profile__avatar",src:a,alt:l}),(0,o.createElement)("div",{className:"activitypub-profile__content"},(0,o.createElement)("div",{className:"activitypub-profile__name"},l),(0,o.createElement)("div",{className:"activitypub-profile__handle",title:i},i)),(0,o.createElement)(I,{profile:e,popupStyles:t,userId:r}))}function I({profile:e,popupStyles:t,userId:r}){const[a,l]=(0,n.useState)(!1),i=(0,u.sprintf)((0,u.__)("Follow %s","activitypub"),e?.name);return(0,o.createElement)(o.Fragment,null,(0,o.createElement)(s.Button,{className:"activitypub-profile__follow",onClick:()=>l(!0)},(0,u.__)("Follow","activitypub")),a&&(0,o.createElement)(s.Modal,{className:"activitypub-profile__confirm activitypub__modal",onRequestClose:()=>l(!1),title:i},(0,o.createElement)($,{profile:e,userId:r}),(0,o.createElement)("style",null,t)))}function $({profile:e,userId:t}){const{webfinger:r}=e,n=(0,u.__)("Follow","activitypub"),a=`/${S}/actors/${t}/remote-follow?resource=`,l=(0,u.__)("Copy and paste my profile into the search field of your favorite fediverse app or server.","activitypub"),i=r.startsWith("@")?r:`@${r}`;return(0,o.createElement)(O,{actionText:n,copyDescription:l,handle:i,resourceUrl:a})}function P({selectedUser:e,style:t,backgroundColor:r,id:a,useId:l=!1,profileData:i=!1}){const[s,u]=(0,n.useState)(N()),p="site"===e?0:e,v=function(e){return d(".apfmd__button-group .components-button",m(e?.elements?.link?.color?.text)||"#111","#fff",m(e?.elements?.link?.[":hover"]?.color?.text)||"#333")}(t),y=l?{id:a}:{};function _(e){u(N(e))}return(0,n.useEffect)((()=>{if(i)return _(i);(function(e){const t={headers:{Accept:"application/activity+json"},path:`/${S}/actors/${e}`};return c()(t)})(p).then(_)}),[p,i]),(0,o.createElement)("div",{...y},(0,o.createElement)(f,{selector:`#${a}`,style:t,backgroundColor:r}),(0,o.createElement)(R,{profile:s,userId:p,popupStyles:v}))}let j=1;l()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follow-me-block-wrapper"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,n.createRoot)(e).render((0,o.createElement)(P,{...t,id:"activitypub-follow-me-block-"+j++,useId:!0}))}))}))},20:(e,t,r)=>{var o=r(609),n=Symbol.for("react.element"),a=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),l=o.__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,r){var o,c={},s=null,u=null;for(o in void 0!==r&&(s=""+r),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(u=t.ref),t)a.call(t,o)&&!i.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:n,type:e,key:s,ref:u,props:c,_owner:l.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},r={};function o(e){var n=r[e];if(void 0!==n)return n.exports;var a=r[e]={exports:{}};return t[e](a,a.exports,o),a.exports}o.m=t,e=[],o.O=(t,r,n,a)=>{if(!r){var l=1/0;for(u=0;u=a)&&Object.keys(o.O).every((e=>o.O[e](r[c])))?r.splice(c--,1):(i=!1,a0&&e[u-1][2]>a;u--)e[u]=e[u-1];e[u]=[r,n,a]},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 n,a,[l,i,c]=r,s=0;if(l.some((t=>0!==e[t]))){for(n in i)o.o(i,n)&&(o.m[n]=i[n]);if(c)var u=c(o)}for(t&&t(r);so(729)));n=o.O(n)})(); \ No newline at end of file +(()=>{"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 diff --git a/wp-content/plugins/activitypub/build/followers/index.asset.php b/wp-content/plugins/activitypub/build/followers/index.asset.php index e147e810..9b0afcfa 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' => '28a5bef9295566598f5c'); + 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'); diff --git a/wp-content/plugins/activitypub/build/followers/index.js b/wp-content/plugins/activitypub/build/followers/index.js index 2a5e6cf3..9bec7474 100644 --- a/wp-content/plugins/activitypub/build/followers/index.js +++ b/wp-content/plugins/activitypub/build/followers/index.js @@ -1,4 +1,4 @@ -(()=>{var e={20:(e,t,a)=>{"use strict";var r=a(609),n=Symbol.for("react.element"),l=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),o=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,a){var r,s={},c=null,p=null;for(r in void 0!==a&&(c=""+a),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(p=t.ref),t)l.call(t,r)&&!i.hasOwnProperty(r)&&(s[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===s[r]&&(s[r]=t[r]);return{$$typeof:n,type:e,key:c,ref:p,props:s,_owner:o.current}}},848:(e,t,a)=>{"use strict";e.exports=a(20)},609:e=>{"use strict";e.exports=window.React},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.primitives;var r=a(848);const n=(0,r.jsx)(t.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,r.jsx)(t.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})});var l=a(609);const o=window.wp.components,i=window.wp.element,s=window.wp.blockEditor,c=window.wp.data,p=window.wp.coreData,u=window.wp.i18n,m=window.wp.apiFetch;var v=a.n(m);const d=window.wp.url;var w=a(942),f=a.n(w);function b({active:e,children:t,page:a,pageClick:r,className:n}){const o=f()("wp-block activitypub-pager",n,{current:e});return(0,l.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&r(a)}},t)}const y={outlined:"outlined",minimal:"minimal"};function g({compact:e,nextLabel:t,page:a,pageClick:r,perPage:n,prevLabel:o,total:i,variant:s=y.outlined}){const c=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(i/n)),p=f()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":e});return(0,l.createElement)("nav",{className:p},o&&(0,l.createElement)(b,{key:"prev",page:a-1,pageClick:r,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,l.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},c.map((e=>(0,l.createElement)(b,{key:e,page:e,pageClick:r,active:e===a,className:"page-numbers"},e)))),t&&(0,l.createElement)(b,{key:"next",page:a+1,pageClick:r,active:a===Math.ceil(i/n),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}const{namespace:h}=window._activityPubOptions;function _({selectedUser:e,per_page:t,order:a,title:r,page:n,setPage:o,className:s="",followLinks:c=!0,followerData:p=!1}){const m="site"===e?0:e,[w,f]=(0,l.useState)([]),[b,y]=(0,l.useState)(0),[_,E]=(0,l.useState)(0),[x,S]=function(){const[e,t]=(0,l.useState)(1);return[e,t]}(),N=n||x,C=o||S,O=(0,i.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */ +(()=>{var e={20:(e,t,a)=>{"use strict";var r=a(609),n=Symbol.for("react.element"),l=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),o=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,a){var r,c={},s=null,p=null;for(r in void 0!==a&&(s=""+a),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(p=t.ref),t)l.call(t,r)&&!i.hasOwnProperty(r)&&(c[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===c[r]&&(c[r]=t[r]);return{$$typeof:n,type:e,key:s,ref:p,props:c,_owner:o.current}}},848:(e,t,a)=>{"use strict";e.exports=a(20)},609:e=>{"use strict";e.exports=window.React},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t{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)=>{f(e),E(a),y(Math.ceil(a/t))};return(0,l.useEffect)((()=>{if(p&&1===N)return I(p.followers,p.total);const e=function(e,t,a,r){const n=`/${h}/actors/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,d.addQueryArgs)(n,l)}(m,t,a,N);v()({path:e}).then((e=>I(e.orderedItems,e.totalItems))).catch((()=>{}))}),[m,t,a,N,p]),(0,l.createElement)("div",{className:"activitypub-follower-block "+s},(0,l.createElement)("h3",null,r),(0,l.createElement)("ul",null,w&&w.map((e=>(0,l.createElement)("li",{key:e.url},(0,l.createElement)(k,{...e,followLinks:c}))))),b>1&&(0,l.createElement)(g,{page:N,perPage:t,total:_,pageClick:C,nextLabel:P,prevLabel:O,compact:"is-style-compact"===s}))}function k({name:e,icon:t,url:a,preferredUsername:r,followLinks:n=!0}){const i=`@${r}`,s={};return n||(s.onClick=e=>e.preventDefault()),(0,l.createElement)(o.ExternalLink,{className:"activitypub-link",href:a,title:i,...s},(0,l.createElement)("img",{width:"40",height:"40",src:t.url,className:"avatar activitypub-avatar",alt:e}),(0,l.createElement)("span",{className:"activitypub-actor"},(0,l.createElement)("strong",{className:"activitypub-name"},e),(0,l.createElement)("span",{className:"sep"},"/"),(0,l.createElement)("span",{className:"activitypub-handle"},i)))}const E=window._activityPubOptions?.enabled;function x({name:e}){const t=(0,u.sprintf)(/* translators: %s: block name */ -"This %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. It will be empty in other non-author contexts.",e);return(0,l.createElement)(o.Card,null,(0,l.createElement)(o.CardBody,null,(0,i.createInterpolateElement)(t,{strong:(0,l.createElement)("strong",null)})))}(0,e.registerBlockType)("activitypub/followers",{edit:function({attributes:e,setAttributes:t,context:{postType:a,postId:r}}){const{order:n,per_page:m,selectedUser:v,title:d}=e,w=(0,s.useBlockProps)(),[f,b]=(0,i.useState)(1),y=[{label:(0,u.__)("New to old","activitypub"),value:"desc"},{label:(0,u.__)("Old to new","activitypub"),value:"asc"}],g=function({withInherit:e=!1}){const t=E?.users?(0,c.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,i.useMemo)((()=>{if(!t)return[];const a=[];return E?.site&&a.push({label:(0,u.__)("Site","activitypub"),value:"site"}),e&&E?.users&&a.push({label:(0,u.__)("Dynamic User","activitypub"),value:"inherit"}),t.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),a)}),[t])}({withInherit:!0}),h=e=>a=>{b(1),t({[e]:a})},k=(0,c.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),n=t("postType",a,r)?.author;return null!=n?n:null}),[a,r]);return(0,i.useEffect)((()=>{g.length&&(g.find((({value:e})=>e===v))||t({selectedUser:g[0].value}))}),[v,g]),(0,l.createElement)("div",{...w},(0,l.createElement)(s.InspectorControls,{key:"setting"},(0,l.createElement)(o.PanelBody,{title:(0,u.__)("Followers Options","activitypub")},(0,l.createElement)(o.TextControl,{label:(0,u.__)("Title","activitypub"),help:(0,u.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:d,onChange:e=>t({title:e})}),g.length>1&&(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:v,options:g,onChange:h("selectedUser")}),(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Sort","activitypub"),value:n,options:y,onChange:h("order")}),(0,l.createElement)(o.RangeControl,{label:(0,u.__)("Number of Followers","activitypub"),value:m,onChange:h("per_page"),min:1,max:10}))),"inherit"===v?k?(0,l.createElement)(_,{...e,page:f,setPage:b,followLinks:!1,selectedUser:k}):(0,l.createElement)(x,{name:(0,u.__)("Followers","activitypub")}):(0,l.createElement)(_,{...e,page:f,setPage:b,followLinks:!1}))},save:()=>null,icon:n})})()})(); \ No newline at end of file +(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 diff --git a/wp-content/plugins/activitypub/build/followers/view.asset.php b/wp-content/plugins/activitypub/build/followers/view.asset.php index 4e64279d..88e18217 100644 --- a/wp-content/plugins/activitypub/build/followers/view.asset.php +++ b/wp-content/plugins/activitypub/build/followers/view.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '111b88843c05346aadbf'); + array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '34299fc181d49292ada0'); diff --git a/wp-content/plugins/activitypub/build/followers/view.js b/wp-content/plugins/activitypub/build/followers/view.js index e9a5792a..227bb012 100644 --- a/wp-content/plugins/activitypub/build/followers/view.js +++ b/wp-content/plugins/activitypub/build/followers/view.js @@ -1,3 +1,3 @@ -(()=>{var e,t={250:(e,t,a)=>{"use strict";const r=window.React,n=window.wp.apiFetch;var l=a.n(n);const o=window.wp.url,i=window.wp.element,c=window.wp.i18n;var s=a(942),p=a.n(s);function u({active:e,children:t,page:a,pageClick:n,className:l}){const o=p()("wp-block activitypub-pager",l,{current:e});return(0,r.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&n(a)}},t)}const m={outlined:"outlined",minimal:"minimal"};function f({compact:e,nextLabel:t,page:a,pageClick:n,perPage:l,prevLabel:o,total:i,variant:c=m.outlined}){const s=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(i/l)),f=p()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${c}`,{"is-compact":e});return(0,r.createElement)("nav",{className:f},o&&(0,r.createElement)(u,{key:"prev",page:a-1,pageClick:n,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},s.map((e=>(0,r.createElement)(u,{key:e,page:e,pageClick:n,active:e===a,className:"page-numbers"},e)))),t&&(0,r.createElement)(u,{key:"next",page:a+1,pageClick:n,active:a===Math.ceil(i/l),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}const v=window.wp.components,{namespace:b}=window._activityPubOptions;function d({selectedUser:e,per_page:t,order:a,title:n,page:s,setPage:p,className:u="",followLinks:m=!0,followerData:v=!1}){const d="site"===e?0:e,[g,y]=(0,r.useState)([]),[k,h]=(0,r.useState)(0),[E,N]=(0,r.useState)(0),[x,_]=function(){const[e,t]=(0,r.useState)(1);return[e,t]}(),O=s||x,S=p||_,C=(0,i.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */ -(0,c.__)(" Less","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),L=(0,i.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */ -(0,c.__)("More ","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),q=(e,a)=>{y(e),N(a),h(Math.ceil(a/t))};return(0,r.useEffect)((()=>{if(v&&1===O)return q(v.followers,v.total);const e=function(e,t,a,r){const n=`/${b}/actors/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,o.addQueryArgs)(n,l)}(d,t,a,O);l()({path:e}).then((e=>q(e.orderedItems,e.totalItems))).catch((()=>{}))}),[d,t,a,O,v]),(0,r.createElement)("div",{className:"activitypub-follower-block "+u},(0,r.createElement)("h3",null,n),(0,r.createElement)("ul",null,g&&g.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(w,{...e,followLinks:m}))))),k>1&&(0,r.createElement)(f,{page:O,perPage:t,total:E,pageClick:S,nextLabel:L,prevLabel:C,compact:"is-style-compact"===u}))}function w({name:e,icon:t,url:a,preferredUsername:n,followLinks:l=!0}){const o=`@${n}`,i={};return l||(i.onClick=e=>e.preventDefault()),(0,r.createElement)(v.ExternalLink,{className:"activitypub-link",href:a,title:o,...i},(0,r.createElement)("img",{width:"40",height:"40",src:t.url,className:"avatar activitypub-avatar",alt:e}),(0,r.createElement)("span",{className:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},e),(0,r.createElement)("span",{className:"sep"},"/"),(0,r.createElement)("span",{className:"activitypub-handle"},o)))}const g=window.wp.domReady;a.n(g)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,i.createRoot)(e).render((0,r.createElement)(d,{...t}))}))}))},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t{if(!a){var o=1/0;for(p=0;p=l)&&Object.keys(r.O).every((e=>r.O[e](a[c])))?a.splice(c--,1):(i=!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,i,c]=a,s=0;if(o.some((t=>0!==e[t]))){for(n in i)r.o(i,n)&&(r.m[n]=i[n]);if(c)var p=c(r)}for(t&&t(a);sr(250)));n=r.O(n)})(); \ No newline at end of file +(()=>{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 diff --git a/wp-content/plugins/activitypub/build/reactions/block.json b/wp-content/plugins/activitypub/build/reactions/block.json new file mode 100644 index 00000000..e489c0f8 --- /dev/null +++ b/wp-content/plugins/activitypub/build/reactions/block.json @@ -0,0 +1,37 @@ +{ + "$schema": "https://schemas.wp.org/trunk/block.json", + "name": "activitypub/reactions", + "apiVersion": 2, + "version": "1.0.0", + "title": "Fediverse Reactions", + "category": "widgets", + "icon": "heart", + "description": "Display Fediverse likes and reposts", + "supports": { + "html": false, + "align": true, + "layout": { + "default": { + "type": "constrained", + "orientation": "vertical", + "justifyContent": "center" + } + } + }, + "attributes": { + "title": { + "type": "string", + "default": "Fediverse reactions" + } + }, + "blockHooks": { + "core/post-content": "after" + }, + "textdomain": "activitypub", + "editorScript": "file:./index.js", + "style": [ + "file:./style-index.css", + "wp-components" + ], + "viewScript": "file:./view.js" +} \ 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 new file mode 100644 index 00000000..6ac7d749 --- /dev/null +++ b/wp-content/plugins/activitypub/build/reactions/index.asset.php @@ -0,0 +1 @@ + array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '32631215c76c36b38e5e'); diff --git a/wp-content/plugins/activitypub/build/reactions/index.js b/wp-content/plugins/activitypub/build/reactions/index.js new file mode 100644 index 00000000..7ab57c58 --- /dev/null +++ b/wp-content/plugins/activitypub/build/reactions/index.js @@ -0,0 +1,3 @@ +(()=>{"use strict";var e,t={373:(e,t,a)=>{const n=window.wp.blocks,r=window.React,l=window.wp.blockEditor,o=window.wp.element,s=window.wp.i18n,i=window.wp.components,c=window.wp.apiFetch;var u=a.n(c);function m(){return window._activityPubOptions||{}}const p=({reactions:e})=>{const{defaultAvatarUrl:t}=m(),[a,n]=(0,o.useState)(new Set),[l,s]=(0,o.useState)(new Map),i=(0,o.useRef)([]),c=()=>{i.current.forEach((e=>clearTimeout(e))),i.current=[]},u=(t,a)=>{c();const r=100,l=e.length;a&&s((e=>{const a=new Map(e);return a.set(t,"clockwise"),a}));const o=e=>{const o="right"===e,c=o?l-1:0,u=o?1:-1;for(let e=o?t:t-1;o?e<=c:e>=c;e+=u){const l=Math.abs(e-t),o=setTimeout((()=>{n((t=>{const n=new Set(t);return a?n.add(e):n.delete(e),n})),a&&e!==t&&s((t=>{const a=new Map(t),n=e-u,r=a.get(n);return a.set(e,"clockwise"===r?"counter":"clockwise"),a}))}),l*r);i.current.push(o)}};if(o("right"),o("left"),!a){const e=Math.max((l-t)*r,t*r),a=setTimeout((()=>{s(new Map)}),e+r);i.current.push(a)}};return(0,o.useEffect)((()=>()=>c()),[]),(0,r.createElement)("ul",{className:"reaction-avatars"},e.map(((e,n)=>{const o=l.get(n),s=["reaction-avatar",a.has(n)?"wave-active":"",o?`rotate-${o}`:""].filter(Boolean).join(" "),i=e.avatar||t;return(0,r.createElement)("li",{key:n},(0,r.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer",onMouseEnter:()=>u(n,!0),onMouseLeave:()=>u(n,!1)},(0,r.createElement)("img",{src:i,alt:e.name,className:s,width:"32",height:"32"})))})))},f=({reactions:e,type:t})=>(0,r.createElement)("ul",{className:"activitypub-reaction-list"},e.map(((e,t)=>(0,r.createElement)("li",{key:t},(0,r.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,r.createElement)("img",{src:e.avatar,alt:e.name,width:"32",height:"32"}),(0,r.createElement)("span",null,e.name)))))),h=({items:e,label:t})=>{const[a,n]=(0,o.useState)(!1),[l,s]=(0,o.useState)(null),[c,u]=(0,o.useState)(e.length),m=(0,o.useRef)(null);(0,o.useEffect)((()=>{if(!m.current)return;const t=()=>{const t=m.current;if(!t)return;const a=t.offsetWidth-(l?.offsetWidth||0)-12,n=Math.max(1,Math.floor((a-32)/22));u(Math.min(n,e.length))};t();const a=new ResizeObserver(t);return a.observe(m.current),()=>{a.disconnect()}}),[l,e.length]);const h=e.slice(0,c);return(0,r.createElement)("div",{className:"reaction-group",ref:m},(0,r.createElement)(p,{reactions:h}),(0,r.createElement)(i.Button,{ref:s,className:"reaction-label is-link",onClick:()=>n(!a),"aria-expanded":a},t),a&&l&&(0,r.createElement)(i.Popover,{anchor:l,onClose:()=>n(!1)},(0,r.createElement)(f,{reactions:e})))};function d({title:e="",postId:t=null,reactions:a=null,titleComponent:n=null}){const{namespace:l}=m(),[s,i]=(0,o.useState)(a),[c,p]=(0,o.useState)(!a);return(0,o.useEffect)((()=>{if(a)return i(a),void p(!1);t?(p(!0),u()({path:`/${l}/posts/${t}/reactions`}).then((e=>{i(e),p(!1)})).catch((()=>p(!1)))):p(!1)}),[t,a]),c?null:s&&Object.values(s).some((e=>e.items?.length>0))?(0,r.createElement)("div",{className:"activitypub-reactions"},n||e&&(0,r.createElement)("h6",null,e),Object.entries(s).map((([e,t])=>t.items?.length?(0,r.createElement)(h,{key:e,items:t.items,label:t.label}):null))):null}const v=e=>{const t=["#FF6B6B","#4ECDC4","#45B7D1","#96CEB4","#FFEEAD","#D4A5A5","#9B59B6","#3498DB","#E67E22"],a=(()=>{const e=["Bouncy","Cosmic","Dancing","Fluffy","Giggly","Hoppy","Jazzy","Magical","Nifty","Perky","Quirky","Sparkly","Twirly","Wiggly","Zippy"],t=["Badger","Capybara","Dolphin","Echidna","Flamingo","Giraffe","Hedgehog","Iguana","Jellyfish","Koala","Lemur","Manatee","Narwhal","Octopus","Penguin"];return`${e[Math.floor(Math.random()*e.length)]} ${t[Math.floor(Math.random()*t.length)]}`})(),n=t[Math.floor(Math.random()*t.length)],r=a.charAt(0),l=document.createElement("canvas");l.width=64,l.height=64;const o=l.getContext("2d");return o.fillStyle=n,o.beginPath(),o.arc(32,32,32,0,2*Math.PI),o.fill(),o.fillStyle="#FFFFFF",o.font="32px sans-serif",o.textAlign="center",o.textBaseline="middle",o.fillText(r,32,32),{name:a,url:"#",avatar:l.toDataURL()}},g=JSON.parse('{"UU":"activitypub/reactions"}');(0,n.registerBlockType)(g.UU,{edit:function({attributes:e,setAttributes:t,__unstableLayoutClassNames:a}){const n=(0,l.useBlockProps)({className:a}),[i]=(0,o.useState)({likes:{label:(0,s.sprintf)(/* translators: %d: Number of likes */ /* translators: %d: Number of likes */ +(0,s._x)("%d likes","number of likes","activitypub"),9),items:Array.from({length:9},((e,t)=>v()))},reposts:{label:(0,s.sprintf)(/* translators: %d: Number of reposts */ /* translators: %d: Number of reposts */ +(0,s._x)("%d reposts","number of reposts","activitypub"),6),items:Array.from({length:6},((e,t)=>v()))}}),c=(0,r.createElement)(l.RichText,{tagName:"h6",value:e.title,onChange:e=>t({title:e}),placeholder:(0,s.__)("Fediverse Reactions","activitypub"),disableLineBreaks:!0,allowedFormats:[]});return(0,r.createElement)("div",{...n},(0,r.createElement)(d,{titleComponent:c,reactions:i}))}})}},a={};function n(e){var r=a[e];if(void 0!==r)return r.exports;var l=a[e]={exports:{}};return t[e](l,l.exports,n),l.exports}n.m=t,e=[],n.O=(t,a,r,l)=>{if(!a){var o=1/0;for(u=0;u=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 diff --git a/wp-content/plugins/activitypub/build/reactions/style-index-rtl.css b/wp-content/plugins/activitypub/build/reactions/style-index-rtl.css new file mode 100644 index 00000000..593e3174 --- /dev/null +++ b/wp-content/plugins/activitypub/build/reactions/style-index-rtl.css @@ -0,0 +1 @@ +.activitypub-reactions h6{border-top:1px solid;border-top-color:var(--wp--preset--color--contrast-2);display:inline-block;padding-top:.5em}.activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75em;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0;padding:0}.activitypub-reactions .reaction-avatars li{margin:0 0 0 -10px;padding:0}.activitypub-reactions .reaction-avatars li:last-child{margin-left:0}.activitypub-reactions .reaction-avatars li a{display:block;text-decoration:none}.activitypub-reactions .reaction-avatars .reaction-avatar{border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);height:32px;transition:transform .6s cubic-bezier(.34,1.56,.64,1);width:32px;will-change:transform}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active{transform:translateY(-5px)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-clockwise{transform:translateY(-5px) rotate(-30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-counter{transform:translateY(-5px) rotate(30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar:hover{position:relative;z-index:1}.activitypub-reactions .reaction-label.components-button{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#2271b1);flex:0 0 auto;height:auto;padding:0;text-decoration:none;white-space:nowrap}.activitypub-reactions .reaction-label.components-button:hover{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#135e96);text-decoration:underline}.activitypub-reactions .reaction-label.components-button:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-reaction-list{background-color:var(--wp--preset--color--background,var(--wp--preset--color--custom-background,var(--wp--preset--color--base)));list-style:none;margin:0;max-width:300px;padding:.25em .7em .25em 1.3em;width:-moz-max-content;width:max-content}.activitypub-reaction-list ul{margin:0;padding:0}.activitypub-reaction-list li{font-size:var(--wp--preset--font-size--small);margin:0;padding:0}.activitypub-reaction-list a{align-items:center;color:var(--wp--preset--color--contrast,var(--wp--preset--color--secondary));display:flex;font-size:var(--wp--preset--font-size--small,.75rem);gap:.5em;justify-content:flex-start;padding:.5em;text-decoration:none}.activitypub-reaction-list a:hover{text-decoration:underline}.activitypub-reaction-list a img{border-radius:50%;flex:none;height:24px;width:24px} diff --git a/wp-content/plugins/activitypub/build/reactions/style-index.css b/wp-content/plugins/activitypub/build/reactions/style-index.css new file mode 100644 index 00000000..769742af --- /dev/null +++ b/wp-content/plugins/activitypub/build/reactions/style-index.css @@ -0,0 +1 @@ +.activitypub-reactions h6{border-top:1px solid;border-top-color:var(--wp--preset--color--contrast-2);display:inline-block;padding-top:.5em}.activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75em;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0;padding:0}.activitypub-reactions .reaction-avatars li{margin:0 -10px 0 0;padding:0}.activitypub-reactions .reaction-avatars li:last-child{margin-right:0}.activitypub-reactions .reaction-avatars li a{display:block;text-decoration:none}.activitypub-reactions .reaction-avatars .reaction-avatar{border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);height:32px;transition:transform .6s cubic-bezier(.34,1.56,.64,1);width:32px;will-change:transform}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active{transform:translateY(-5px)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-clockwise{transform:translateY(-5px) rotate(30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-counter{transform:translateY(-5px) rotate(-30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar:hover{position:relative;z-index:1}.activitypub-reactions .reaction-label.components-button{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#2271b1);flex:0 0 auto;height:auto;padding:0;text-decoration:none;white-space:nowrap}.activitypub-reactions .reaction-label.components-button:hover{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#135e96);text-decoration:underline}.activitypub-reactions .reaction-label.components-button:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-reaction-list{background-color:var(--wp--preset--color--background,var(--wp--preset--color--custom-background,var(--wp--preset--color--base)));list-style:none;margin:0;max-width:300px;padding:.25em 1.3em .25em .7em;width:-moz-max-content;width:max-content}.activitypub-reaction-list ul{margin:0;padding:0}.activitypub-reaction-list li{font-size:var(--wp--preset--font-size--small);margin:0;padding:0}.activitypub-reaction-list a{align-items:center;color:var(--wp--preset--color--contrast,var(--wp--preset--color--secondary));display:flex;font-size:var(--wp--preset--font-size--small,.75rem);gap:.5em;justify-content:flex-start;padding:.5em;text-decoration:none}.activitypub-reaction-list a:hover{text-decoration:underline}.activitypub-reaction-list a img{border-radius:50%;flex:none;height:24px;width:24px} diff --git a/wp-content/plugins/activitypub/build/reactions/view.asset.php b/wp-content/plugins/activitypub/build/reactions/view.asset.php new file mode 100644 index 00000000..814279a1 --- /dev/null +++ b/wp-content/plugins/activitypub/build/reactions/view.asset.php @@ -0,0 +1 @@ + array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => 'd5cb95d9bd6062974b3c'); diff --git a/wp-content/plugins/activitypub/build/reactions/view.js b/wp-content/plugins/activitypub/build/reactions/view.js new file mode 100644 index 00000000..4e4ff64e --- /dev/null +++ b/wp-content/plugins/activitypub/build/reactions/view.js @@ -0,0 +1 @@ +(()=>{"use strict";var e={n:t=>{var n=t&&t.__esModule?()=>t.default:()=>t;return e.d(n,{a:n}),n},d:(t,n)=>{for(var a in n)e.o(n,a)&&!e.o(t,a)&&Object.defineProperty(t,a,{enumerable:!0,get:n[a]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.React,n=window.wp.element,a=window.wp.domReady;var r=e.n(a);const c=window.wp.components,o=window.wp.apiFetch;var l=e.n(o);function s(){return window._activityPubOptions||{}}window.wp.i18n;const i=({reactions:e})=>{const{defaultAvatarUrl:a}=s(),[r,c]=(0,n.useState)(new Set),[o,l]=(0,n.useState)(new Map),i=(0,n.useRef)([]),u=()=>{i.current.forEach((e=>clearTimeout(e))),i.current=[]},m=(t,n)=>{u();const a=100,r=e.length;n&&l((e=>{const n=new Map(e);return n.set(t,"clockwise"),n}));const o=e=>{const o="right"===e,s=o?r-1:0,u=o?1:-1;for(let e=o?t:t-1;o?e<=s:e>=s;e+=u){const r=Math.abs(e-t),o=setTimeout((()=>{c((t=>{const a=new Set(t);return n?a.add(e):a.delete(e),a})),n&&e!==t&&l((t=>{const n=new Map(t),a=e-u,r=n.get(a);return n.set(e,"clockwise"===r?"counter":"clockwise"),n}))}),r*a);i.current.push(o)}};if(o("right"),o("left"),!n){const e=Math.max((r-t)*a,t*a),n=setTimeout((()=>{l(new Map)}),e+a);i.current.push(n)}};return(0,n.useEffect)((()=>()=>u()),[]),(0,t.createElement)("ul",{className:"reaction-avatars"},e.map(((e,n)=>{const c=o.get(n),l=["reaction-avatar",r.has(n)?"wave-active":"",c?`rotate-${c}`:""].filter(Boolean).join(" "),s=e.avatar||a;return(0,t.createElement)("li",{key:n},(0,t.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer",onMouseEnter:()=>m(n,!0),onMouseLeave:()=>m(n,!1)},(0,t.createElement)("img",{src:s,alt:e.name,className:l,width:"32",height:"32"})))})))},u=({reactions:e,type:n})=>(0,t.createElement)("ul",{className:"activitypub-reaction-list"},e.map(((e,n)=>(0,t.createElement)("li",{key:n},(0,t.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,t.createElement)("img",{src:e.avatar,alt:e.name,width:"32",height:"32"}),(0,t.createElement)("span",null,e.name)))))),m=({items:e,label:a})=>{const[r,o]=(0,n.useState)(!1),[l,s]=(0,n.useState)(null),[m,p]=(0,n.useState)(e.length),h=(0,n.useRef)(null);(0,n.useEffect)((()=>{if(!h.current)return;const t=()=>{const t=h.current;if(!t)return;const n=t.offsetWidth-(l?.offsetWidth||0)-12,a=Math.max(1,Math.floor((n-32)/22));p(Math.min(a,e.length))};t();const n=new ResizeObserver(t);return n.observe(h.current),()=>{n.disconnect()}}),[l,e.length]);const f=e.slice(0,m);return(0,t.createElement)("div",{className:"reaction-group",ref:h},(0,t.createElement)(i,{reactions:f}),(0,t.createElement)(c.Button,{ref:s,className:"reaction-label is-link",onClick:()=>o(!r),"aria-expanded":r},a),r&&l&&(0,t.createElement)(c.Popover,{anchor:l,onClose:()=>o(!1)},(0,t.createElement)(u,{reactions:e})))};function p({title:e="",postId:a=null,reactions:r=null,titleComponent:c=null}){const{namespace:o}=s(),[i,u]=(0,n.useState)(r),[p,h]=(0,n.useState)(!r);return(0,n.useEffect)((()=>{if(r)return u(r),void h(!1);a?(h(!0),l()({path:`/${o}/posts/${a}/reactions`}).then((e=>{u(e),h(!1)})).catch((()=>h(!1)))):h(!1)}),[a,r]),p?null:i&&Object.values(i).some((e=>e.items?.length>0))?(0,t.createElement)("div",{className:"activitypub-reactions"},c||e&&(0,t.createElement)("h6",null,e),Object.entries(i).map((([e,n])=>n.items?.length?(0,t.createElement)(m,{key:e,items:n.items,label:n.label}):null))):null}r()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-reactions-block"),(e=>{const a=JSON.parse(e.dataset.attrs);(0,n.createRoot)(e).render((0,t.createElement)(p,{...a}))}))}))})(); \ No newline at end of file diff --git a/wp-content/plugins/activitypub/build/remote-reply/index.asset.php b/wp-content/plugins/activitypub/build/remote-reply/index.asset.php index c68fef98..70196da8 100644 --- a/wp-content/plugins/activitypub/build/remote-reply/index.asset.php +++ b/wp-content/plugins/activitypub/build/remote-reply/index.asset.php @@ -1 +1 @@ - array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '45f08e094782c24c4c34'); + 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 index dcbd0732..1494b656 100644 --- a/wp-content/plugins/activitypub/build/remote-reply/index.js +++ b/wp-content/plugins/activitypub/build/remote-reply/index.js @@ -1 +1,2 @@ -(()=>{"use strict";var e,t={456:(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.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"})}),v=window.wp.apiFetch;var d=r.n(v);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"})}),w=window.wp.compose,b="fediverse-remote-user";function h(){const[e,t]=(0,a.useState)(function(){const e=localStorage.getItem(b);return e?JSON.parse(e):{}}()),r=(0,a.useCallback)((e=>{!function(e){localStorage.setItem(b,JSON.stringify(e))}(e),t(e)}),[]),o=(0,a.useCallback)((()=>{localStorage.removeItem(b),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:n,myProfile:i=!1,rememberProfile:m=!1}){const p=(0,c.__)("Loading...","activitypub"),u=(0,c.__)("Opening...","activitypub"),v=(0,c.__)("Error","activitypub"),b=(0,c.__)("Invalid","activitypub"),E=i||(0,c.__)("My Profile","activitypub"),[C,R]=(0,a.useState)(e),[x,O]=(0,a.useState)(y),k=(0,w.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(b),t=setTimeout((()=>R(e)),2e3),()=>clearTimeout(t);const r=n+L;R(p),d()({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(v),setTimeout((()=>R(e)),2e3)}))}),[L]);return(0,o.createElement)("div",{className:"activitypub__dialog"},(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",null,E),(0,o.createElement)("div",{className:"activitypub-dialog__description"},t),(0,o.createElement)("div",{className:"activitypub-dialog__button-group"},(0,o.createElement)("input",{type:"text",value:r,readOnly:!0}),(0,o.createElement)(l.Button,{ref:k},(0,o.createElement)(s,{icon:x}),(0,c.__)("Copy","activitypub")))),(0,o.createElement)("div",{className:"activitypub-dialog__section"},(0,o.createElement)("h4",null,(0,c.__)("Your Profile","activitypub")),(0,o.createElement)("div",{className:"activitypub-dialog__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)("input",{type:"text",value:L,onKeyDown:e=>{"Enter"===e?.code&&j()},onChange:e=>S(e.target.value)}),(0,o.createElement)(l.Button,{onClick:j},(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)}}))))}const{namespace:C}=window._activityPubOptions;function R({selectedComment:e,commentId:t}){const r=(0,c.__)("Reply","activitypub"),a=`/${C}/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:r,copyDescription:n,handle:e,resourceUrl:a,myProfile:(0,c.__)("Original Comment URL","activitypub"),rememberProfile:!0})}function x({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")}},(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 O({selectedComment:e,commentId:t}){const[r,n]=(0,a.useState)(!1),i=(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)(x,{profileURL:s,template:m,commentURL:e,deleteRemoteUser:p}):(0,o.createElement)(l.Button,{variant:"link",className:"comment-reply-link activitypub-remote-reply__button",onClick:()=>n(!0)},(0,c.__)("Reply on the Fediverse","activitypub")),r&&(0,o.createElement)(l.Modal,{className:"activitypub-remote-reply__modal activitypub__modal",onRequestClose:()=>n(!1),title:i},(0,o.createElement)(R,{selectedComment:e,commentId:t})))}let k=1;i()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-remote-reply"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,a.createRoot)(e).render((0,o.createElement)(O,{...t,id:"activitypub-remote-reply-link-"+k++,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={},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)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:s,ref:m,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(m=0;m=n)&&Object.keys(o.O).every((e=>o.O[e](r[c])))?r.splice(c--,1):(l=!1,n0&&e[m-1][2]>n;m--)e[m]=e[m-1];e[m]=[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={227:0,739:0};o.O.j=t=>0===e[t];var t=(t,r)=>{var a,n,[i,l,c]=r,s=0;if(i.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(456)));a=o.O(a)})(); \ No newline at end of file +(()=>{"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/style-index-rtl.css b/wp-content/plugins/activitypub/build/remote-reply/style-index-rtl.css index ae34ff03..04857b9b 100644 --- a/wp-content/plugins/activitypub/build/remote-reply/style-index-rtl.css +++ b/wp-content/plugins/activitypub/build/remote-reply/style-index-rtl.css @@ -1 +1 @@ -.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex} +.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex} diff --git a/wp-content/plugins/activitypub/build/remote-reply/style-index.css b/wp-content/plugins/activitypub/build/remote-reply/style-index.css index a8a0f412..c07d1493 100644 --- a/wp-content/plugins/activitypub/build/remote-reply/style-index.css +++ b/wp-content/plugins/activitypub/build/remote-reply/style-index.css @@ -1 +1 @@ -.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex} +.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex} 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 79765ae6..72bbe977 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' => '488f0199fb69ddcf6c53'); + array('wp-block-editor', 'wp-blocks', 'wp-data', 'wp-element', 'wp-plugins'), 'version' => 'f65a7269b5abb57d3e73'); diff --git a/wp-content/plugins/activitypub/build/reply-intent/plugin.js b/wp-content/plugins/activitypub/build/reply-intent/plugin.js index 2a26b0f9..b4a7e993 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 t=window.wp.plugins,e=window.wp.blocks,n=window.wp.data,i=window.wp.blockEditor,r=window.wp.element;(0,t.registerPlugin)("activitypub-reply-intent",{render:()=>{const[t,o]=(0,r.useState)(!1);return(0,r.useEffect)((()=>{if(t)return;const r=new URLSearchParams(window.location.search).get("in_reply_to");r&&setTimeout((()=>{const t=(0,e.createBlock)("activitypub/reply",{url:r}),o=(0,n.dispatch)(i.store);o.insertBlock(t),o.insertAfterBlock(t.clientId)}),200),o(!0)}),[t]),null}})})(); \ 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 0e949165..c3a57338 100644 --- a/wp-content/plugins/activitypub/build/reply/block.json +++ b/wp-content/plugins/activitypub/build/reply/block.json @@ -8,14 +8,22 @@ "icon": "commentReplyLink", "description": "Respond to posts, notes, videos, and other content on the fediverse. Ensure the URL originates from a federated social network like Mastodon, as other URLs might not function as expected.", "supports": { - "html": false + "html": false, + "inserter": true, + "reusable": false, + "lock": false }, "textdomain": "activitypub", "editorScript": "file:./index.js", - "editorStyle": "file:./edit.css", + "editorStyle": "file:./style-index.css", + "style": "file:./index.css", "attributes": { "url": { "type": "string" + }, + "embedPost": { + "type": "boolean", + "default": null } } } \ 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 new file mode 100644 index 00000000..0a5e5c51 --- /dev/null +++ b/wp-content/plugins/activitypub/build/reply/index-rtl.css @@ -0,0 +1 @@ +.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6;border-radius:8px;overflow:hidden}.activitypub-embed-content .ap-preview img{display:block;height:auto;width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta .ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684}.wp-block-activitypub-reply .components-spinner{height:12px;margin-bottom:0;margin-top:0;width:12px} diff --git a/wp-content/plugins/activitypub/build/reply/index.asset.php b/wp-content/plugins/activitypub/build/reply/index.asset.php index 2e7cd843..f82365da 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-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '808c98599517db815fc5'); + 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'); diff --git a/wp-content/plugins/activitypub/build/reply/index.css b/wp-content/plugins/activitypub/build/reply/index.css new file mode 100644 index 00000000..0a5e5c51 --- /dev/null +++ b/wp-content/plugins/activitypub/build/reply/index.css @@ -0,0 +1 @@ +.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6;border-radius:8px;overflow:hidden}.activitypub-embed-content .ap-preview img{display:block;height:auto;width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta .ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684}.wp-block-activitypub-reply .components-spinner{height:12px;margin-bottom:0;margin-top:0;width:12px} diff --git a/wp-content/plugins/activitypub/build/reply/index.js b/wp-content/plugins/activitypub/build/reply/index.js index 9cabdce1..0e1c337b 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={20:(e,t,r)=>{var o=r(609),n=Symbol.for("react.element"),i=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),a=o.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,s={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,r){var o,l={},p=null,c=null;for(o in void 0!==r&&(p=""+r),void 0!==t.key&&(p=""+t.key),void 0!==t.ref&&(c=t.ref),t)i.call(t,o)&&!s.hasOwnProperty(o)&&(l[o]=t[o]);if(e&&e.defaultProps)for(o in t=e.defaultProps)void 0===l[o]&&(l[o]=t[o]);return{$$typeof:n,type:e,key:p,ref:c,props:l,_owner:a.current}}},848:(e,t,r)=>{e.exports=r(20)},609:e=>{e.exports=window.React}},t={};function r(o){var n=t[o];if(void 0!==n)return n.exports;var i=t[o]={exports:{}};return e[o](i,i.exports,r),i.exports}const o=window.wp.blocks,n=window.wp.primitives;var i=r(848);const a=(0,i.jsx)(n.SVG,{width:"24",height:"24",viewBox:"0 0 24 24",xmlns:"http://www.w3.org/2000/svg",children:(0,i.jsx)(n.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 s=r(609);const l=window.wp.i18n,p=window.wp.blockEditor,c=window.wp.components,w=window.wp.element,u=window.wp.data;(0,o.registerBlockType)("activitypub/reply",{edit:function({attributes:e,setAttributes:t,clientId:r,isSelected:o}){const[n,i]=(0,w.useState)(""),{insertAfterBlock:a,removeBlock:d}=(0,u.useDispatch)(p.store),v=(0,l.__)("For example: Paste a URL from a Mastodon post or note into the field above to leave a comment.","activitypub"),[f,y]=(0,w.useState)(v);return(0,s.createElement)("div",{...(0,p.useBlockProps)()},(0,s.createElement)(c.TextControl,{label:(0,l.__)("This post is a reply to the following URL","activitypub"),value:e.url,onChange:e=>{!function(e){try{return new URL(e),!0}catch(e){return!1}}(e)?(i("error"),y((0,l.__)("Please enter a valid URL.","activitypub"))):(i(""),y(v)),t({url:e})},onKeyDown:t=>{"Enter"===t.key&&a(r),!e.url&&["Backspace","Delete"].includes(t.key)&&d(r)},type:"url",placeholder:"https://example.org/path",className:n,help:o?f:""}))},save:()=>null,icon:a})})(); \ No newline at end of file +(()=>{"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 diff --git a/wp-content/plugins/activitypub/build/reply/style-index-rtl.css b/wp-content/plugins/activitypub/build/reply/style-index-rtl.css new file mode 100644 index 00000000..ec651ab6 --- /dev/null +++ b/wp-content/plugins/activitypub/build/reply/style-index-rtl.css @@ -0,0 +1 @@ +.activitypub-embed-container{margin-top:1em;min-height:100px;pointer-events:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.activitypub-embed-loading{align-items:center;display:flex;justify-content:center}.activitypub-embed-container .wp-block-embed{pointer-events:none!important}.activitypub-embed-preview,.activitypub-embed-preview iframe{pointer-events:none}.activitypub-reply-display{margin:1em 0}.activitypub-reply-display p{margin:0}.activitypub-reply-display a{color:#2271b1;text-decoration:none}.activitypub-reply-display a:hover{color:#135e96;text-decoration:underline} diff --git a/wp-content/plugins/activitypub/build/reply/style-index.css b/wp-content/plugins/activitypub/build/reply/style-index.css new file mode 100644 index 00000000..ec651ab6 --- /dev/null +++ b/wp-content/plugins/activitypub/build/reply/style-index.css @@ -0,0 +1 @@ +.activitypub-embed-container{margin-top:1em;min-height:100px;pointer-events:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.activitypub-embed-loading{align-items:center;display:flex;justify-content:center}.activitypub-embed-container .wp-block-embed{pointer-events:none!important}.activitypub-embed-preview,.activitypub-embed-preview iframe{pointer-events:none}.activitypub-reply-display{margin:1em 0}.activitypub-reply-display p{margin:0}.activitypub-reply-display a{color:#2271b1;text-decoration:none}.activitypub-reply-display a:hover{color:#135e96;text-decoration:underline} diff --git a/wp-content/plugins/activitypub/includes/activity/class-activity.php b/wp-content/plugins/activitypub/includes/activity/class-activity.php index b8418315..b4623ac2 100644 --- a/wp-content/plugins/activitypub/includes/activity/class-activity.php +++ b/wp-content/plugins/activitypub/includes/activity/class-activity.php @@ -9,7 +9,8 @@ namespace Activitypub\Activity; -use Activitypub\Link; +use Activitypub\Activity\Extended_Object\Event; +use Activitypub\Activity\Extended_Object\Place; /** * \Activitypub\Activity\Activity implements the common @@ -23,6 +24,43 @@ class Activity extends Base_Object { 'https://www.w3.org/ns/activitystreams', ); + /** + * The default types for Activities. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types + * + * @var array + */ + const TYPES = array( + 'Accept', + 'Add', + 'Announce', + 'Arrive', + 'Block', + 'Create', + 'Delete', + 'Dislike', + 'Follow', + 'Flag', + 'Ignore', + 'Invite', + 'Join', + 'Leave', + 'Like', + 'Listen', + 'Move', + 'Offer', + 'Read', + 'Reject', + 'Remove', + 'TentativeAccept', + 'TentativeReject', + 'Travel', + 'Undo', + 'Update', + 'View', + ); + /** * The type of the object. * @@ -37,10 +75,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object-term * - * @var string - * | Base_Object - * | Link - * | null + * @var string|Base_Object|null */ protected $object; @@ -52,11 +87,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-actor * - * @var string - * | \ActivityPhp\Type\Extended\AbstractActor - * | array - * | array - * | Link + * @var string|array */ protected $actor; @@ -71,11 +102,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-target * - * @var string - * | ObjectType - * | array - * | Link - * | array + * @var string|array */ protected $target; @@ -87,10 +114,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-result * - * @var string - * | ObjectType - * | Link - * | null + * @var string|Base_Object */ protected $result; @@ -103,9 +127,6 @@ class Activity extends Base_Object { * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies * * @var array - * | ObjectType - * | Link - * | null */ protected $replies; @@ -119,10 +140,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-origin * - * @var string - * | ObjectType - * | Link - * | null + * @var string|array */ protected $origin; @@ -132,10 +150,7 @@ class Activity extends Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-instrument * - * @var string - * | ObjectType - * | Link - * | null + * @var string|array */ protected $instrument; @@ -143,53 +158,93 @@ class Activity extends Base_Object { * Set the object and copy Object properties to the Activity. * * Any to, bto, cc, bcc, and audience properties specified on the object - * MUST be copied over to the new Create activity by the server. + * MUST be copied over to the new "Create" activity by the server. * * @see https://www.w3.org/TR/activitypub/#object-without-create * - * @param array|string|Base_Object|Link|null $data Activity object. - * - * @return void + * @param array|string|Base_Object|Activity|Actor|null $data Activity object. */ public function set_object( $data ) { - // Convert array to object. + $object = $data; + + // Convert array to appropriate object type. if ( is_array( $data ) ) { - $data = self::init_from_array( $data ); + $type = $data['type'] ?? null; + + if ( in_array( $type, self::TYPES, true ) ) { + $object = self::init_from_array( $data ); + } elseif ( in_array( $type, Actor::TYPES, true ) ) { + $object = Actor::init_from_array( $data ); + } elseif ( in_array( $type, Base_Object::TYPES, true ) ) { + switch ( $type ) { + case 'Event': + $object = Event::init_from_array( $data ); + break; + case 'Place': + $object = Place::init_from_array( $data ); + break; + default: + $object = Base_Object::init_from_array( $data ); + break; + } + } else { + $object = Generic_Object::init_from_array( $data ); + } } - // Set object. - $this->set( 'object', $data ); + $this->set( 'object', $object ); + $this->pre_fill_activity_from_object(); + } - if ( ! is_object( $data ) ) { + /** + * Fills the Activity with the specified activity object. + */ + public function pre_fill_activity_from_object() { + $object = $this->get_object(); + + // Check if `$data` is a URL and use it to generate an ID then. + if ( is_string( $object ) && filter_var( $object, FILTER_VALIDATE_URL ) && ! $this->get_id() ) { + $this->set( 'id', $object . '#activity-' . strtolower( $this->get_type() ) . '-' . time() ); + + return; + } + + // Check if `$data` is an object and copy some properties otherwise do nothing. + if ( ! is_object( $object ) ) { return; } foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) { - $this->set( $i, $data->get( $i ) ); + $value = $object->get( $i ); + if ( $value && ! $this->get( $i ) ) { + $this->set( $i, $value ); + } } - if ( $data->get_published() && ! $this->get_published() ) { - $this->set( 'published', $data->get_published() ); + if ( $object->get_published() && ! $this->get_published() ) { + $this->set( 'published', $object->get_published() ); } - if ( $data->get_updated() && ! $this->get_updated() ) { - $this->set( 'updated', $data->get_updated() ); + if ( $object->get_updated() && ! $this->get_updated() ) { + $this->set( 'updated', $object->get_updated() ); } - if ( $data->get_attributed_to() && ! $this->get_actor() ) { - $this->set( 'actor', $data->get_attributed_to() ); + if ( $object->get_attributed_to() && ! $this->get_actor() ) { + $this->set( 'actor', $object->get_attributed_to() ); } - if ( $data->get_in_reply_to() ) { - $this->set( 'in_reply_to', $data->get_in_reply_to() ); + if ( $this->get_type() !== 'Announce' && $object->get_in_reply_to() && ! $this->get_in_reply_to() ) { + $this->set( 'in_reply_to', $object->get_in_reply_to() ); } - if ( $data->get_id() && ! $this->get_id() ) { - $id = strtok( $data->get_id(), '#' ); - if ( $data->get_updated() ) { - $updated = $data->get_updated(); + if ( $object->get_id() && ! $this->get_id() ) { + $id = strtok( $object->get_id(), '#' ); + if ( $object->get_updated() ) { + $updated = $object->get_updated(); + } elseif ( $object->get_published() ) { + $updated = $object->get_published(); } else { - $updated = $data->get_published(); + $updated = time(); } $this->set( 'id', $id . '#activity-' . strtolower( $this->get_type() ) . '-' . $updated ); } diff --git a/wp-content/plugins/activitypub/includes/activity/class-actor.php b/wp-content/plugins/activitypub/includes/activity/class-actor.php index ddbef8ea..bbf2128b 100644 --- a/wp-content/plugins/activitypub/includes/activity/class-actor.php +++ b/wp-content/plugins/activitypub/includes/activity/class-actor.php @@ -43,12 +43,39 @@ class Actor extends Base_Object { '@id' => 'lemmy:moderators', '@type' => '@id', ), + 'alsoKnownAs' => array( + '@id' => 'as:alsoKnownAs', + '@type' => '@id', + ), + 'movedTo' => array( + '@id' => 'as:movedTo', + '@type' => '@id', + ), + 'attributionDomains' => array( + '@id' => 'toot:attributionDomains', + '@type' => '@id', + ), 'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods', 'discoverable' => 'toot:discoverable', 'indexable' => 'toot:indexable', ), ); + /** + * The default types for Actors. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types + * + * @var array + */ + const TYPES = array( + 'Application', + 'Group', + 'Organization', + 'Person', + 'Service', + ); + /** * The type of the object. * @@ -62,8 +89,7 @@ class Actor extends Base_Object { * * @see https://www.w3.org/TR/activitypub/#inbox * - * @var string - * | null + * @var string|null */ protected $inbox; @@ -73,8 +99,7 @@ class Actor extends Base_Object { * * @see https://www.w3.org/TR/activitypub/#outbox * - * @var string - * | null + * @var string|null */ protected $outbox; @@ -175,13 +200,26 @@ class Actor extends Base_Object { protected $manually_approves_followers = false; /** - * Used to mark an object as containing sensitive content. - * Mastodon displays a content warning, requiring users to click - * through to view the content. + * Domains allowed to use `fediverse:creator` for this actor in + * published articles. * - * @see https://docs.joinmastodon.org/spec/activitypub/#sensitive + * @see https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/ * - * @var boolean + * @var array */ - protected $sensitive = null; + protected $attribution_domains = null; + + /** + * The target of the actor. + * + * @var string|null + */ + protected $moved_to; + + /** + * The alsoKnownAs of the actor. + * + * @var array + */ + protected $also_known_as; } 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 ab765174..cc1d885e 100644 --- a/wp-content/plugins/activitypub/includes/activity/class-base-object.php +++ b/wp-content/plugins/activitypub/includes/activity/class-base-object.php @@ -9,13 +9,6 @@ namespace Activitypub\Activity; -use WP_Error; -use ReflectionClass; -use DateTime; - -use function Activitypub\camel_to_snake_case; -use function Activitypub\snake_to_camel_case; - /** * Base_Object is an implementation of one of the * Activity Streams Core Types. @@ -27,8 +20,56 @@ use function Activitypub\snake_to_camel_case; * 'Base_' for this reason. * * @see https://www.w3.org/TR/activitystreams-core/#object + * + * @method string|null get_actor() Gets one or more entities that performed or are expected to perform the activity. + * @method string|null get_attributed_to() Gets the entity attributed as the original author. + * @method array|null get_attachment() Gets the attachment property of the object. + * @method array|null get_cc() Gets the secondary recipients of the object. + * @method string|null get_content() Gets the content property of the object. + * @method array|null get_icon() Gets the icon property of the object. + * @method string|null get_id() Gets the object's unique global identifier. + * @method array|null get_image() Gets the image property of the object. + * @method array|string|null get_in_reply_to() Gets the objects this object is in reply to. + * @method string|null get_name() Gets the natural language name of the object. + * @method Base_Object|string|null get_object() Gets the direct object of the activity. + * @method string|null get_published() Gets the date and time the object was published in ISO 8601 format. + * @method string|null get_summary() Gets the natural language summary of the object. + * @method array|null get_tag() Gets the tag property of the object. + * @method array|string|null get_to() Gets the primary recipients of the object. + * @method string get_type() Gets the type of the object. + * @method string|null get_updated() Gets the date and time the object was updated in ISO 8601 format. + * @method string|null get_url() Gets the URL of the object. + * + * @method string|array add_cc( string|array $cc ) Adds one or more entities to the secondary audience of the object. + * @method string|array add_to( string|array $to ) Adds one or more entities to the primary audience of the object. + * + * @method Base_Object set_actor( string|array $actor ) Sets one or more entities that performed the activity. + * @method Base_Object set_attachment( array $attachment ) Sets the attachment property of the object. + * @method Base_Object set_attributed_to( string $attributed_to ) Sets the entity attributed as the original author. + * @method Base_Object set_cc( array|string $cc ) Sets the secondary recipients of the object. + * @method Base_Object set_content( string $content ) Sets the content property of the object. + * @method Base_Object set_content_map( array $content_map ) Sets the content property of the object. + * @method Base_Object set_icon( array $icon ) Sets the icon property of the object. + * @method Base_Object set_id( string $id ) Sets the object's unique global identifier. + * @method Base_Object set_image( array $image ) Sets the image property of the object. + * @method Base_Object set_name( string $name ) Sets the natural language name of the object. + * @method Base_Object set_origin( string $origin ) Sets the origin property of the object. + * @method Base_Object set_published( string $published ) Sets the date and time the object was published in ISO 8601 format. + * @method Base_Object set_sensitive( bool $sensitive ) Sets the sensitive property of the object. + * @method Base_Object set_summary( string $summary ) Sets the natural language summary of the object. + * @method Base_Object set_summary_map( array|null $summary_map ) Sets the summary property of the object. + * @method Base_Object set_target( string $target ) Sets the target property of the object. + * @method Base_Object set_to( array|string $to ) Sets the primary recipients of the object. + * @method Base_Object set_type( string $type ) Sets the type of the object. + * @method Base_Object set_updated( string $updated ) Sets the date and time the object was updated in ISO 8601 format. + * @method Base_Object set_url( string $url ) Sets the URL of the object. */ -class Base_Object { +class Base_Object extends Generic_Object { + /** + * The JSON-LD context for the object. + * + * @var array + */ const JSON_LD_CONTEXT = array( 'https://www.w3.org/ns/activitystreams', array( @@ -38,13 +79,26 @@ class Base_Object { ); /** - * The object's unique global identifier + * The default types for Objects. * - * @see https://www.w3.org/TR/activitypub/#obj-id + * @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types * - * @var string + * @var array */ - protected $id; + const TYPES = array( + 'Article', + 'Audio', + 'Document', + 'Event', + 'Image', + 'Note', + 'Page', + 'Place', + 'Profile', + 'Relationship', + 'Tombstone', + 'Video', + ); /** * The type of the object. @@ -61,12 +115,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attachment * - * @var string - * | ObjectType - * | Link - * | array - * | array - * | null + * @var string|null */ protected $attachment; @@ -77,12 +126,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attributedto * - * @var string - * | ObjectType - * | Link - * | array - * | array - * | null + * @var string|null */ protected $attributed_to; @@ -92,12 +136,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audience * - * @var string - * | ObjectType - * | Link - * | array - * | array - * | null + * @var string|null */ protected $audience; @@ -127,10 +166,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context * - * @var string - * | ObjectType - * | Link - * | null + * @var string|null */ protected $context; @@ -191,12 +227,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon * - * @var string - * | Image - * | Link - * | array - * | array - * | null + * @var string|array|null */ protected $icon; @@ -207,12 +238,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image-term * - * @var string - * | Image - * | Link - * | array - * | array - * | null + * @var string|array|null */ protected $image; @@ -222,12 +248,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-inreplyto * - * @var string - * | ObjectType - * | Link - * | array - * | array - * | null + * @var string|null */ protected $in_reply_to; @@ -237,12 +258,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-location * - * @var string - * | ObjectType - * | Link - * | array - * | array - * | null + * @var string|null */ protected $location; @@ -251,10 +267,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-preview * - * @var string - * | ObjectType - * | Link - * | null + * @var string|null */ protected $preview; @@ -286,10 +299,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary * - * @var string - * | ObjectType - * | Link - * | null + * @var string|null */ protected $summary; @@ -299,7 +309,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary * - * @var array|null + * @var string[]|null */ protected $summary_map; @@ -312,12 +322,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag * - * @var string - * | ObjectType - * | Link - * | array - * | array - * | null + * @var string|null */ protected $tag; @@ -333,11 +338,7 @@ class Base_Object { /** * One or more links to representations of the object. * - * @var string - * | array - * | Link - * | array - * | null + * @var string|null */ protected $url; @@ -347,12 +348,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-to * - * @var string - * | ObjectType - * | Link - * | array - * | array - * | null + * @var string|array|null */ protected $to; @@ -362,12 +358,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bto * - * @var string - * | ObjectType - * | Link - * | array - * | array - * | null + * @var string|array|null */ protected $bto; @@ -377,12 +368,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-cc * - * @var string - * | ObjectType - * | Link - * | array - * | array - * | null + * @var string|array|null */ protected $cc; @@ -392,12 +378,7 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bcc * - * @var string - * | ObjectType - * | Link - * | array - * | array - * | null + * @var string|array|null */ protected $bcc; @@ -443,13 +424,30 @@ class Base_Object { * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies * - * @var string - * | Collection - * | Link - * | null + * @var string|array|null */ protected $replies; + /** + * A Collection containing objects considered to be likes for + * this object. + * + * @see https://www.w3.org/TR/activitypub/#likes + * + * @var array + */ + protected $likes; + + /** + * A Collection containing objects considered to be shares for + * this object. + * + * @see https://www.w3.org/TR/activitypub/#shares + * + * @var array + */ + protected $shares; + /** * Used to mark an object as containing sensitive content. * Mastodon displays a content warning, requiring users to click @@ -459,51 +457,7 @@ class Base_Object { * * @var boolean */ - protected $sensitive = false; - - /** - * Magic function to implement getter and setter. - * - * @param string $method The method name. - * @param string $params The method params. - */ - public function __call( $method, $params ) { - $var = \strtolower( \substr( $method, 4 ) ); - - if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { - if ( ! $this->has( $var ) ) { - return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) ); - } - - return $this->$var; - } - - if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { - return $this->set( $var, $params[0] ); - } - - if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { - $this->add( $var, $params[0] ); - } - } - - /** - * Magic function, to transform the object to string. - * - * @return string The object id. - */ - public function __toString() { - return $this->to_string(); - } - - /** - * Function to transform the object to string. - * - * @return string The object id. - */ - public function to_string() { - return $this->get_id(); - } + protected $sensitive; /** * Generic getter. @@ -514,21 +468,10 @@ class Base_Object { */ public function get( $key ) { if ( ! $this->has( $key ) ) { - return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) ); + return new \WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) ); } - return call_user_func( array( $this, 'get_' . $key ) ); - } - - /** - * Check if the object has a key - * - * @param string $key The key to check. - * - * @return boolean True if the object has the key. - */ - public function has( $key ) { - return property_exists( $this, $key ); + return parent::get( $key ); } /** @@ -541,12 +484,10 @@ class Base_Object { */ public function set( $key, $value ) { if ( ! $this->has( $key ) ) { - return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) ); + return new \WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) ); } - $this->$key = $value; - - return $this; + return parent::set( $key, $value ); } /** @@ -559,188 +500,9 @@ class Base_Object { */ public function add( $key, $value ) { if ( ! $this->has( $key ) ) { - return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) ); + return new \WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) ); } - if ( ! isset( $this->$key ) ) { - $this->$key = array(); - } - - $attributes = $this->$key; - $attributes[] = $value; - - $this->$key = $attributes; - - return $this->$key; - } - - /** - * Convert JSON input to an array. - * - * @param string $json The JSON string. - * - * @return Base_Object An Object built from the JSON string. - */ - public static function init_from_json( $json ) { - $array = \json_decode( $json, true ); - - if ( ! is_array( $array ) ) { - $array = array(); - } - - return self::init_from_array( $array ); - } - - /** - * Convert input array to a Base_Object. - * - * @param array $data The object array. - * - * @return Base_Object|WP_Error An Object built from the input array or WP_Error when it's not an array. - */ - public static function init_from_array( $data ) { - if ( ! is_array( $data ) ) { - return new WP_Error( 'invalid_array', __( 'Invalid array', 'activitypub' ), array( 'status' => 404 ) ); - } - - $object = new static(); - - foreach ( $data as $key => $value ) { - $key = camel_to_snake_case( $key ); - call_user_func( array( $object, 'set_' . $key ), $value ); - } - - return $object; - } - - /** - * Convert JSON input to an array and pre-fill the object. - * - * @param string $json The JSON string. - */ - public function from_json( $json ) { - $array = \json_decode( $json, true ); - - $this->from_array( $array ); - } - - /** - * Convert JSON input to an array and pre-fill the object. - * - * @param array $data The array. - */ - public function from_array( $data ) { - foreach ( $data as $key => $value ) { - if ( $value ) { - $key = camel_to_snake_case( $key ); - call_user_func( array( $this, 'set_' . $key ), $value ); - } - } - } - - /** - * Convert Object to an array. - * - * It tries to get the object attributes if they exist - * and falls back to the getters. Empty values are ignored. - * - * @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true. - * - * @return array An array built from the Object. - */ - public function to_array( $include_json_ld_context = true ) { - $array = array(); - $vars = get_object_vars( $this ); - - foreach ( $vars as $key => $value ) { - // Ignore all _prefixed keys. - if ( '_' === substr( $key, 0, 1 ) ) { - continue; - } - - // If value is empty, try to get it from a getter. - if ( ! $value ) { - $value = call_user_func( array( $this, 'get_' . $key ) ); - } - - if ( is_object( $value ) ) { - $value = $value->to_array( false ); - } - - // If value is still empty, ignore it for the array and continue. - if ( isset( $value ) ) { - $array[ snake_to_camel_case( $key ) ] = $value; - } - } - - if ( $include_json_ld_context ) { - // Get JsonLD context and move it to '@context' at the top. - $array = array_merge( array( '@context' => $this->get_json_ld_context() ), $array ); - } - - $class = new ReflectionClass( $this ); - $class = strtolower( $class->getShortName() ); - - /** - * Filter the array of the ActivityPub object. - * - * @param array $array The array of the ActivityPub object. - * @param string $class The class of the ActivityPub object. - * @param int $id The ID of the ActivityPub object. - * @param Base_Object $object The ActivityPub object. - * - * @return array The filtered array of the ActivityPub object. - */ - $array = \apply_filters( 'activitypub_activity_object_array', $array, $class, $this->id, $this ); - - /** - * Filter the array of the ActivityPub object by class. - * - * @param array $array The array of the ActivityPub object. - * @param int $id The ID of the ActivityPub object. - * @param Base_Object $object The ActivityPub object. - * - * @return array The filtered array of the ActivityPub object. - */ - return \apply_filters( "activitypub_activity_{$class}_object_array", $array, $this->id, $this ); - } - - /** - * Convert Object to JSON. - * - * @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true. - * - * @return string The JSON string. - */ - public function to_json( $include_json_ld_context = true ) { - $array = $this->to_array( $include_json_ld_context ); - $options = \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT; - - /** - * Options to be passed to json_encode() - * - * @param int $options The current options flags. - */ - $options = \apply_filters( 'activitypub_json_encode_options', $options ); - - return \wp_json_encode( $array, $options ); - } - - /** - * Returns the keys of the object vars. - * - * @return array The keys of the object vars. - */ - public function get_object_var_keys() { - return \array_keys( \get_object_vars( $this ) ); - } - - /** - * Returns the JSON-LD context of this object. - * - * @return array $context A compacted JSON-LD context for the ActivityPub object. - */ - public function get_json_ld_context() { - return static::JSON_LD_CONTEXT; + return parent::add( $key, $value ); } } diff --git a/wp-content/plugins/activitypub/includes/activity/class-generic-object.php b/wp-content/plugins/activitypub/includes/activity/class-generic-object.php new file mode 100644 index 00000000..eceff1e8 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/activity/class-generic-object.php @@ -0,0 +1,325 @@ +to_string(); + } + + /** + * Function to transform the object to string. + * + * @return string The object id. + */ + public function to_string() { + return $this->get_id(); + } + + /** + * Magic function to implement getter and setter. + * + * @param string $method The method name. + * @param string $params The method params. + */ + public function __call( $method, $params ) { + $var = \strtolower( \substr( $method, 4 ) ); + + if ( \strncasecmp( $method, 'get', 3 ) === 0 ) { + if ( ! $this->has( $var ) ) { + return null; + } + + return $this->$var; + } + + if ( \strncasecmp( $method, 'set', 3 ) === 0 ) { + return $this->set( $var, $params[0] ); + } + + if ( \strncasecmp( $method, 'add', 3 ) === 0 ) { + return $this->add( $var, $params[0] ); + } + } + + /** + * Generic getter. + * + * @param string $key The key to get. + * + * @return mixed The value. + */ + public function get( $key ) { + return call_user_func( array( $this, 'get_' . $key ) ); + } + + /** + * Generic setter. + * + * @param string $key The key to set. + * @param string $value The value to set. + * + * @return mixed The value. + */ + public function set( $key, $value ) { + $this->$key = $value; + + return $this; + } + + /** + * Generic adder. + * + * @param string $key The key to set. + * @param mixed $value The value to add. + * + * @return mixed The value. + */ + public function add( $key, $value ) { + if ( empty( $value ) ) { + return; + } + + if ( ! isset( $this->$key ) ) { + $this->$key = array(); + } + + if ( is_string( $this->$key ) ) { + $this->$key = array( $this->$key ); + } + + $attributes = $this->$key; + + if ( is_array( $value ) ) { + $attributes = array_merge( $attributes, $value ); + } else { + $attributes[] = $value; + } + + $this->$key = array_unique( $attributes ); + + return $this->$key; + } + + /** + * Check if the object has a key + * + * @param string $key The key to check. + * + * @return boolean True if the object has the key. + */ + public function has( $key ) { + return property_exists( $this, $key ); + } + + /** + * Convert JSON input to an array. + * + * @param string $json The JSON string. + * + * @return Generic_Object|\WP_Error An Object built from the JSON string or WP_Error when it's not a JSON string. + */ + public static function init_from_json( $json ) { + $array = \json_decode( $json, true ); + + if ( ! is_array( $array ) ) { + return new \WP_Error( 'invalid_json', __( 'Invalid JSON', 'activitypub' ), array( 'status' => 400 ) ); + } + + return self::init_from_array( $array ); + } + + /** + * Convert input array to a Base_Object. + * + * @param array $data The object array. + * + * @return Generic_Object|\WP_Error An Object built from the input array or WP_Error when it's not an array. + */ + public static function init_from_array( $data ) { + if ( ! is_array( $data ) ) { + return new \WP_Error( 'invalid_array', __( 'Invalid array', 'activitypub' ), array( 'status' => 400 ) ); + } + + $object = new static(); + $object->from_array( $data ); + + return $object; + } + + /** + * Convert JSON input to an array and pre-fill the object. + * + * @param array $data The array. + */ + public function from_array( $data ) { + foreach ( $data as $key => $value ) { + if ( null !== $value ) { + $key = camel_to_snake_case( $key ); + call_user_func( array( $this, 'set_' . $key ), $value ); + } + } + } + + /** + * Convert JSON input to an array and pre-fill the object. + * + * @param string $json The JSON string. + */ + public function from_json( $json ) { + $array = \json_decode( $json, true ); + + $this->from_array( $array ); + } + + /** + * Convert Object to an array. + * + * It tries to get the object attributes if they exist + * and falls back to the getters. Empty values are ignored. + * + * @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true. + * + * @return array An array built from the Object. + */ + public function to_array( $include_json_ld_context = true ) { + $array = array(); + $vars = get_object_vars( $this ); + + foreach ( $vars as $key => $value ) { + if ( \is_wp_error( $value ) ) { + continue; + } + + // Ignore all _prefixed keys. + if ( '_' === substr( $key, 0, 1 ) ) { + continue; + } + + // If value is empty, try to get it from a getter. + if ( ! $value ) { + $value = call_user_func( array( $this, 'get_' . $key ) ); + } + + if ( is_object( $value ) ) { + $value = $value->to_array( false ); + } + + // If value is still empty, ignore it for the array and continue. + if ( isset( $value ) ) { + $array[ snake_to_camel_case( $key ) ] = $value; + } + } + + if ( $include_json_ld_context ) { + // Get JsonLD context and move it to '@context' at the top. + $array = array_merge( array( '@context' => $this->get_json_ld_context() ), $array ); + } + + $class = new \ReflectionClass( $this ); + $class = strtolower( $class->getShortName() ); + + /** + * Filter the array of the ActivityPub object. + * + * @param array $array The array of the ActivityPub object. + * @param string $class The class of the ActivityPub object. + * @param string $id The ID of the ActivityPub object. + * @param Generic_Object $object The ActivityPub object. + * + * @return array The filtered array of the ActivityPub object. + */ + $array = \apply_filters( 'activitypub_activity_object_array', $array, $class, $this->id, $this ); + + /** + * Filter the array of the ActivityPub object by class. + * + * @param array $array The array of the ActivityPub object. + * @param string $id The ID of the ActivityPub object. + * @param Generic_Object $object The ActivityPub object. + * + * @return array The filtered array of the ActivityPub object. + */ + return \apply_filters( "activitypub_activity_{$class}_object_array", $array, $this->id, $this ); + } + + /** + * Convert Object to JSON. + * + * @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true. + * + * @return string The JSON string. + */ + public function to_json( $include_json_ld_context = true ) { + $array = $this->to_array( $include_json_ld_context ); + $options = \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT | \JSON_UNESCAPED_SLASHES; + + /** + * Options to be passed to json_encode(). + * + * @param int $options The current options flags. + */ + $options = \apply_filters( 'activitypub_json_encode_options', $options ); + + return \wp_json_encode( $array, $options ); + } + + /** + * Returns the keys of the object vars. + * + * @return array The keys of the object vars. + */ + public function get_object_var_keys() { + return \array_keys( \get_object_vars( $this ) ); + } + + /** + * Returns the JSON-LD context of this object. + * + * @return array $context A compacted JSON-LD context for the ActivityPub object. + */ + public function get_json_ld_context() { + return static::JSON_LD_CONTEXT; + } +} 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 9ddb46ff..a44c2c66 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 @@ -50,7 +50,7 @@ class Event extends Base_Object { ); /** - * Mobilizon compatible values for repliesModertaionOption. + * Mobilizon compatible values for repliesModerationOption. * * @var array */ @@ -342,15 +342,15 @@ class Event extends Base_Object { /** * Custom setter for the event category. * - * Falls back to Mobilizons default category. + * Falls back to Mobilizon's default category. * - * @param string $category The category of the event. - * @param bool $mobilizon_compatibilty Optional. Whether the category must be compatibly with Mobilizon. Default true. + * @param string $category The category of the event. + * @param bool $mobilizon_compatibility Optional. Whether the category must be compatibly with Mobilizon. Default true. * * @return Event */ - public function set_category( $category, $mobilizon_compatibilty = true ) { - if ( $mobilizon_compatibilty ) { + public function set_category( $category, $mobilizon_compatibility = true ) { + if ( $mobilizon_compatibility ) { $this->category = in_array( $category, self::DEFAULT_EVENT_CATEGORIES, true ) ? $category : 'MEETING'; } else { $this->category = $category; 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 8b66cdcd..1bf7419b 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 @@ -38,8 +38,8 @@ class Place extends Base_Object { protected $accuracy; /** - * Indicates the altitude of a place. The measurement units is indicated using the units property. - * If units is not specified, the default is assumed to be "m" indicating meters. + * Indicates the altitude of a place. The measurement unit is indicated using the unit's property. + * If unit is not specified, the default is assumed to be "m" indicating meters. * * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-altitude * @var float xsd:float diff --git a/wp-content/plugins/activitypub/includes/class-activity-dispatcher.php b/wp-content/plugins/activitypub/includes/class-activity-dispatcher.php deleted file mode 100644 index 17281a33..00000000 --- a/wp-content/plugins/activitypub/includes/class-activity-dispatcher.php +++ /dev/null @@ -1,328 +0,0 @@ -change_wp_user_id( $user_id ); - } - - $user_id = $transformer->get_wp_user_id(); - - if ( is_user_disabled( $user_id ) ) { - return; - } - - $activity = $transformer->to_activity( $type ); - - self::send_activity_to_followers( $activity, $user_id, $wp_object ); - } - - /** - * Send Announces to followers and mentioned users. - * - * @param mixed $wp_object The ActivityPub Post. - * @param string $type The Activity-Type. - */ - public static function send_announce( $wp_object, $type ) { - if ( ! in_array( $type, array( 'Create', 'Update', 'Delete' ), true ) ) { - return; - } - - if ( is_user_disabled( Users::BLOG_USER_ID ) ) { - return; - } - - $transformer = Factory::get_transformer( $wp_object ); - - if ( \is_wp_error( $transformer ) ) { - return; - } - - $user_id = Users::BLOG_USER_ID; - $activity = $transformer->to_activity( $type ); - $user = Users::get_by_id( Users::BLOG_USER_ID ); - - $announce = new Activity(); - $announce->set_type( 'Announce' ); - $announce->set_object( $activity ); - $announce->set_actor( $user->get_id() ); - - self::send_activity_to_followers( $announce, $user_id, $wp_object ); - } - - /** - * Send a "Update" Activity when a user updates their profile. - * - * @param int $user_id The user ID to send an update for. - */ - public static function send_profile_update( $user_id ) { - $user = Users::get_by_various( $user_id ); - - // Bail if that's not a good user. - if ( is_wp_error( $user ) ) { - return; - } - - // Build the update. - $activity = new Activity(); - $activity->set_type( 'Update' ); - $activity->set_actor( $user->get_url() ); - $activity->set_object( $user->get_url() ); - $activity->set_to( 'https://www.w3.org/ns/activitystreams#Public' ); - - // Send the update. - self::send_activity_to_followers( $activity, $user_id, $user ); - } - - /** - * Send an Activity to all followers and mentioned users. - * - * @param Activity $activity The ActivityPub Activity. - * @param int $user_id The user ID. - * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. - */ - private static function send_activity_to_followers( $activity, $user_id, $wp_object ) { - /** - * Filter to prevent sending an Activity to followers. - * - * @param bool $send_activity_to_followers Whether to send the Activity to followers. - * @param Activity $activity The ActivityPub Activity. - * @param int $user_id The user ID. - * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. - */ - if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $user_id, $wp_object ) ) { - return; - } - - /** - * Filter to modify the Activity before sending it to followers. - * - * @param Activity $activity The ActivityPub Activity. - * @param int $user_id The user ID. - * @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object. - */ - $inboxes = apply_filters( 'activitypub_send_to_inboxes', array(), $user_id, $activity ); - $inboxes = array_unique( $inboxes ); - - if ( empty( $inboxes ) ) { - return; - } - - $json = $activity->to_json(); - - foreach ( $inboxes as $inbox ) { - safe_remote_post( $inbox, $json, $user_id ); - } - - set_wp_object_state( $wp_object, 'federated' ); - } - - /** - * Send a "Create" or "Update" Activity for a WordPress Post. - * - * @param int $id The WordPress Post ID. - * @param string $type The Activity-Type. - */ - public static function send_post( $id, $type ) { - $post = get_post( $id ); - - if ( ! $post ) { - return; - } - - /** - * Action to send an Activity for a Post. - * - * @param WP_Post $post The WordPress Post. - * @param string $type The Activity-Type. - */ - do_action( 'activitypub_send_activity', $post, $type ); - - /** - * Action to send a specific Activity for a Post. - * - * @param WP_Post $post The WordPress Post. - */ - do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $post ); - } - - /** - * Send a "Create" or "Update" Activity for a WordPress Comment. - * - * @param int $id The WordPress Comment ID. - * @param string $type The Activity-Type. - */ - public static function send_comment( $id, $type ) { - $comment = get_comment( $id ); - - if ( ! $comment ) { - return; - } - - /** - * Action to send an Activity for a Comment. - * - * @param WP_Comment $comment The WordPress Comment. - * @param string $type The Activity-Type. - */ - do_action( 'activitypub_send_activity', $comment, $type ); - - /** - * Action to send a specific Activity for a Comment. - * - * @param WP_Comment $comment The WordPress Comment. - */ - do_action( sprintf( 'activitypub_send_%s_activity', \strtolower( $type ) ), $comment ); - } - - /** - * Default filter to add Inboxes of Followers. - * - * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. - * - * @return array The filtered Inboxes - */ - public static function add_inboxes_of_follower( $inboxes, $user_id ) { - $follower_inboxes = Followers::get_inboxes( $user_id ); - - return array_merge( $inboxes, $follower_inboxes ); - } - - /** - * Default filter to add Inboxes of Mentioned Actors - * - * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. - * @param array $activity The ActivityPub Activity. - * - * @return array The filtered Inboxes. - */ - public static function add_inboxes_by_mentioned_actors( $inboxes, $user_id, $activity ) { - $cc = $activity->get_cc(); - if ( $cc ) { - $mentioned_inboxes = Mention::get_inboxes( $cc ); - - return array_merge( $inboxes, $mentioned_inboxes ); - } - - return $inboxes; - } - - /** - * Default filter to add Inboxes of Posts that are set as `in-reply-to` - * - * @param array $inboxes The list of Inboxes. - * @param int $user_id The WordPress User-ID. - * @param array $activity The ActivityPub Activity. - * - * @return array The filtered Inboxes - */ - public static function add_inboxes_of_replied_urls( $inboxes, $user_id, $activity ) { - $in_reply_to = $activity->get_in_reply_to(); - - if ( ! $in_reply_to ) { - return $inboxes; - } - - if ( ! is_array( $in_reply_to ) ) { - $in_reply_to = array( $in_reply_to ); - } - - foreach ( $in_reply_to as $url ) { - $object = Http::get_remote_object( $url ); - - if ( - ! $object || - \is_wp_error( $object ) || - empty( $object['attributedTo'] ) - ) { - continue; - } - - $actor = object_to_uri( $object['attributedTo'] ); - $actor = Http::get_remote_object( $actor ); - - if ( ! $actor || \is_wp_error( $actor ) ) { - continue; - } - - if ( ! empty( $actor['endpoints']['sharedInbox'] ) ) { - $inboxes[] = $actor['endpoints']['sharedInbox']; - } elseif ( ! empty( $actor['inbox'] ) ) { - $inboxes[] = $actor['inbox']; - } - } - - return $inboxes; - } -} diff --git a/wp-content/plugins/activitypub/includes/class-activitypub.php b/wp-content/plugins/activitypub/includes/class-activitypub.php index c85a5fb1..9299dc09 100644 --- a/wp-content/plugins/activitypub/includes/class-activitypub.php +++ b/wp-content/plugins/activitypub/includes/class-activitypub.php @@ -8,6 +8,8 @@ namespace Activitypub; use Exception; +use Activitypub\Collection\Actors; +use Activitypub\Collection\Outbox; use Activitypub\Collection\Followers; use Activitypub\Collection\Extra_Fields; @@ -21,13 +23,15 @@ class Activitypub { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_filter( 'template_include', array( self::class, 'render_json_template' ), 99 ); + \add_filter( 'template_include', array( self::class, 'render_activitypub_template' ), 99 ); \add_action( 'template_redirect', array( self::class, 'template_redirect' ) ); + \add_filter( 'redirect_canonical', array( self::class, 'redirect_canonical' ), 10, 2 ); + \add_filter( 'redirect_canonical', array( self::class, 'no_trailing_redirect' ), 10, 2 ); \add_filter( 'query_vars', array( self::class, 'add_query_vars' ) ); \add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 ); // Add support for ActivityPub to custom post types. - $post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post' ) ) : array(); + $post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) ); foreach ( $post_types as $post_type ) { \add_post_type_support( $post_type, 'activitypub' ); @@ -41,16 +45,18 @@ class Activitypub { \add_action( 'user_register', array( self::class, 'user_register' ) ); - \add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) ); - - if ( site_supports_blocks() ) { - \add_action( 'tool_box', array( self::class, 'tool_box' ) ); - } - \add_filter( 'activitypub_get_actor_extra_fields', array( Extra_Fields::class, 'default_actor_extra_fields' ), 10, 2 ); + \add_action( 'updated_postmeta', array( self::class, 'updated_postmeta' ), 10, 4 ); + \add_action( 'added_post_meta', array( self::class, 'updated_postmeta' ), 10, 4 ); + + \add_action( 'init', array( self::class, 'register_user_meta' ), 11 ); + // Register several post_types. self::register_post_types(); + + self::register_oembed_providers(); + Embed::init(); } /** @@ -59,6 +65,9 @@ class Activitypub { public static function activate() { self::flush_rewrite_rules(); Scheduler::register_schedules(); + + \add_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 10, 3 ); + Migration::update_comment_counts(); } /** @@ -67,6 +76,9 @@ class Activitypub { public static function deactivate() { self::flush_rewrite_rules(); Scheduler::deregister_schedules(); + + \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) ); + Migration::update_comment_counts( 2000 ); } /** @@ -74,6 +86,43 @@ class Activitypub { */ public static function uninstall() { Scheduler::deregister_schedules(); + + \remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) ); + Migration::update_comment_counts( 2000 ); + + \delete_option( 'activitypub_actor_mode' ); + \delete_option( 'activitypub_allow_likes' ); + \delete_option( 'activitypub_allow_replies' ); + \delete_option( 'activitypub_attribution_domains' ); + \delete_option( 'activitypub_authorized_fetch' ); + \delete_option( 'activitypub_application_user_private_key' ); + \delete_option( 'activitypub_application_user_public_key' ); + \delete_option( 'activitypub_blog_user_also_known_as' ); + \delete_option( 'activitypub_blog_user_mailer_new_dm' ); + \delete_option( 'activitypub_blog_user_mailer_new_follower' ); + \delete_option( 'activitypub_blog_user_mailer_new_mention' ); + \delete_option( 'activitypub_blog_user_moved_to' ); + \delete_option( 'activitypub_blog_user_private_key' ); + \delete_option( 'activitypub_blog_user_public_key' ); + \delete_option( 'activitypub_blog_description' ); + \delete_option( 'activitypub_blog_identifier' ); + \delete_option( 'activitypub_custom_post_content' ); + \delete_option( 'activitypub_db_version' ); + \delete_option( 'activitypub_default_extra_fields' ); + \delete_option( 'activitypub_enable_blog_user' ); + \delete_option( 'activitypub_enable_users' ); + \delete_option( 'activitypub_header_image' ); + \delete_option( 'activitypub_last_post_with_permalink_as_id' ); + \delete_option( 'activitypub_max_image_attachments' ); + \delete_option( 'activitypub_migration_lock' ); + \delete_option( 'activitypub_object_type' ); + \delete_option( 'activitypub_outbox_purge_days' ); + \delete_option( 'activitypub_shared_inbox' ); + \delete_option( 'activitypub_support_post_types' ); + \delete_option( 'activitypub_use_hashtags' ); + \delete_option( 'activitypub_use_opengraph' ); + \delete_option( 'activitypub_use_permalink_as_id_for_blog' ); + \delete_option( 'activitypub_vary_header' ); } /** @@ -83,25 +132,33 @@ class Activitypub { * * @return string The new path to the JSON template. */ - public static function render_json_template( $template ) { - if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) { + public static function render_activitypub_template( $template ) { + if ( \wp_is_serving_rest_request() || \wp_doing_ajax() ) { return $template; } + self::add_headers(); + if ( ! is_activitypub_request() ) { return $template; } - $json_template = false; + $activitypub_template = false; + $activitypub_object = Query::get_instance()->get_activitypub_object(); - if ( \is_author() && ! is_user_disabled( \get_the_author_meta( 'ID' ) ) ) { - $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/user-json.php'; - } elseif ( is_comment() ) { - $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/comment-json.php'; - } elseif ( \is_singular() ) { - $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php'; - } elseif ( \is_home() && ! is_user_type_disabled( 'blog' ) ) { - $json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php'; + if ( $activitypub_object ) { + if ( \get_query_var( 'preview' ) ) { + \define( 'ACTIVITYPUB_PREVIEW', true ); + + /** + * Filter the template used for the ActivityPub preview. + * + * @param string $activitypub_template Absolute path to the template file. + */ + $activitypub_template = apply_filters( 'activitypub_preview_template', ACTIVITYPUB_PLUGIN_DIR . '/templates/post-preview.php' ); + } else { + $activitypub_template = ACTIVITYPUB_PLUGIN_DIR . 'templates/activitypub-json.php'; + } } /* @@ -110,7 +167,7 @@ class Activitypub { * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch */ - if ( $json_template && ACTIVITYPUB_AUTHORIZED_FETCH ) { + if ( $activitypub_template && use_authorized_fetch() ) { $verification = Signature::verify_http_signature( $_SERVER ); if ( \is_wp_error( $verification ) ) { header( 'HTTP/1.1 401 Unauthorized' ); @@ -120,8 +177,16 @@ class Activitypub { } } - if ( $json_template ) { - return $json_template; + if ( $activitypub_template ) { + \set_query_var( 'is_404', false ); + + // Check if header already sent. + if ( ! \headers_sent() ) { + // Send 200 status header. + \status_header( 200 ); + } + + return $activitypub_template; } return $template; @@ -131,80 +196,132 @@ class Activitypub { * Add the 'self' link to the header. */ public static function add_headers() { - // phpcs:ignore WordPress.Security.ValidatedSanitizedInput - $request_uri = $_SERVER['REQUEST_URI']; + $id = Query::get_instance()->get_activitypub_object_id(); - if ( ! $request_uri ) { + if ( ! $id ) { return; } - // Only add self link to author pages... - if ( is_author() ) { - if ( is_user_disabled( get_queried_object_id() ) ) { - return; - } - } elseif ( is_singular() ) { // or posts/pages/custom-post-types... - if ( ! \post_type_supports( \get_post_type(), 'activitypub' ) ) { - return; - } - } else { // otherwise return. - return; - } - - // Add self link to html and http header. - $host = wp_parse_url( home_url() ); - - /** - * Filters the self link. - * - * @param string $self_link The self link. - */ - $self_link = apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . wp_unslash( $request_uri ) ) ); - $self_link = esc_url( $self_link ); - if ( ! headers_sent() ) { - header( 'Link: <' . $self_link . '>; rel="alternate"; type="application/activity+json"' ); + \header( 'Link: <' . esc_url( $id ) . '>; title="ActivityPub (JSON)"; rel="alternate"; type="application/activity+json"', false ); + + if ( \get_option( 'activitypub_vary_header' ) ) { + // Send Vary header for Accept header. + \header( 'Vary: Accept', false ); + } } add_action( 'wp_head', - function () use ( $self_link ) { - echo PHP_EOL . '' . PHP_EOL; + 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() { - self::add_headers(); + global $wp_query; $comment_id = get_query_var( 'c', null ); // Check if it seems to be a comment. - if ( ! $comment_id ) { - return; + if ( $comment_id ) { + $comment = get_comment( $comment_id ); + + // Load a 404-page if `c` is set but not valid. + 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; } - $comment = get_comment( $comment_id ); + $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; + } - // Load a 404 page if `c` is set but not valid. - if ( ! $comment ) { - global $wp_query; - $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; } - - // 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; } /** @@ -216,6 +333,9 @@ class Activitypub { */ public static function add_query_vars( $vars ) { $vars[] = 'activitypub'; + $vars[] = 'preview'; + $vars[] = 'author'; + $vars[] = 'actor'; $vars[] = 'c'; $vars[] = 'p'; @@ -254,7 +374,7 @@ class Activitypub { } // Check if comment has an avatar. - $avatar = self::get_avatar_url( $id_or_email->comment_ID ); + $avatar = \get_comment_meta( $id_or_email->comment_ID, 'avatar_url', true ); if ( $avatar ) { if ( empty( $args['class'] ) ) { @@ -272,20 +392,6 @@ class Activitypub { return $args; } - /** - * Function to retrieve Avatar URL if stored in meta. - * - * @param int|\WP_Comment $comment The comment ID or object. - * - * @return string The Avatar URL. - */ - public static function get_avatar_url( $comment ) { - if ( \is_numeric( $comment ) ) { - $comment = \get_comment( $comment ); - } - return \get_comment_meta( $comment->comment_ID, 'avatar_url', true ); - } - /** * Store permalink in meta, to send delete Activity. * @@ -294,7 +400,7 @@ class Activitypub { public static function trash_post( $post_id ) { \add_post_meta( $post_id, - 'activitypub_canonical_url', + '_activitypub_canonical_url', \get_permalink( $post_id ), true ); @@ -306,7 +412,7 @@ class Activitypub { * @param string $post_id The Post ID. */ public static function untrash_post( $post_id ) { - \delete_post_meta( $post_id, 'activitypub_canonical_url' ); + \delete_post_meta( $post_id, '_activitypub_canonical_url' ); } /** @@ -332,22 +438,12 @@ class Activitypub { if ( ! \class_exists( 'Nodeinfo_Endpoint' ) && true === (bool) \get_option( 'blog_public', 1 ) ) { \add_rewrite_rule( '^.well-known/nodeinfo', - 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo/discovery', - 'top' - ); - \add_rewrite_rule( - '^.well-known/x-nodeinfo2', - 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2', + 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo', 'top' ); } - \add_rewrite_rule( - '^@([\w\-\.]+)', - 'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/$matches[1]', - 'top' - ); - + \add_rewrite_rule( '^@([\w\-\.]+)\/?$', 'index.php?actor=$matches[1]', 'top' ); \add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES ); } @@ -359,15 +455,6 @@ class Activitypub { \flush_rewrite_rules(); } - /** - * Adds metabox on wp-admin/tools.php. - */ - public static function tool_box() { - if ( \current_user_can( 'edit_posts' ) ) { - \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/toolbox.php' ); - } - } - /** * Theme compatibility stuff. */ @@ -391,31 +478,7 @@ class Activitypub { } /** - * Display plugin upgrade notice to users. - * - * @param array $data The plugin data. - */ - public static function plugin_update_message( $data ) { - if ( ! isset( $data['upgrade_notice'] ) ) { - return; - } - - printf( - '
%s
', - wp_kses( - wpautop( $data['upgrade_notice '] ), - array( - 'p' => array(), - 'a' => array( 'href', 'title' ), - 'strong' => array(), - 'em' => array(), - ) - ) - ); - } - - /** - * Register the "Followers" Taxonomy. + * Register Custom Post Types. */ private static function register_post_types() { \register_post_type( @@ -437,7 +500,7 @@ class Activitypub { \register_post_meta( Followers::POST_TYPE, - 'activitypub_inbox', + '_activitypub_inbox', array( 'type' => 'string', 'single' => true, @@ -447,7 +510,7 @@ class Activitypub { \register_post_meta( Followers::POST_TYPE, - 'activitypub_errors', + '_activitypub_errors', array( 'type' => 'string', 'single' => false, @@ -463,7 +526,7 @@ class Activitypub { \register_post_meta( Followers::POST_TYPE, - 'activitypub_user_id', + '_activitypub_user_id', array( 'type' => 'string', 'single' => false, @@ -475,7 +538,7 @@ class Activitypub { \register_post_meta( Followers::POST_TYPE, - 'activitypub_actor_json', + '_activitypub_actor_json', array( 'type' => 'string', 'single' => true, @@ -485,6 +548,128 @@ class Activitypub { ) ); + // Register Outbox Post-Type. + register_post_type( + Outbox::POST_TYPE, + array( + 'labels' => array( + 'name' => _x( 'Outbox', 'post_type plural name', 'activitypub' ), + 'singular_name' => _x( 'Outbox Item', 'post_type single name', 'activitypub' ), + ), + 'capabilities' => array( + 'create_posts' => false, + ), + 'map_meta_cap' => true, + 'public' => false, + 'show_in_rest' => true, + 'rewrite' => false, + 'query_var' => false, + 'supports' => array( 'title', 'editor', 'author', 'custom-fields' ), + 'delete_with_user' => true, + 'can_export' => true, + 'exclude_from_search' => true, + ) + ); + + /** + * Register Activity Type meta for Outbox items. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types + */ + \register_post_meta( + Outbox::POST_TYPE, + '_activitypub_activity_type', + array( + 'type' => 'string', + 'description' => 'The type of the activity', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $value = ucfirst( strtolower( $value ) ); + $schema = array( + 'type' => 'string', + 'enum' => array( 'Accept', 'Add', 'Announce', 'Arrive', 'Block', 'Create', 'Delete', 'Dislike', 'Flag', 'Follow', 'Ignore', 'Invite', 'Join', 'Leave', 'Like', 'Listen', 'Move', 'Offer', 'Question', 'Reject', 'Read', 'Remove', 'TentativeReject', 'TentativeAccept', 'Travel', 'Undo', 'Update', 'View' ), + 'default' => 'Announce', + ); + + if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + + \register_post_meta( + Outbox::POST_TYPE, + '_activitypub_activity_actor', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => array( 'application', 'blog', 'user' ), + 'default' => 'user', + ); + + if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + + \register_post_meta( + Outbox::POST_TYPE, + '_activitypub_outbox_offset', + array( + 'type' => 'integer', + 'single' => true, + 'description' => 'Keeps track of the followers offset when processing outbox items.', + 'sanitize_callback' => 'absint', + 'default' => 0, + ) + ); + + \register_post_meta( + Outbox::POST_TYPE, + '_activitypub_object_id', + array( + 'type' => 'string', + 'single' => true, + 'description' => 'The ID (ActivityPub URI) of the object that the outbox item is about.', + 'sanitize_callback' => 'sanitize_url', + ) + ); + + \register_post_meta( + Outbox::POST_TYPE, + 'activitypub_content_visibility', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), + 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ); + + if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, + ) + ); + // Both User and Blog Extra Fields types have the same args. $args = array( 'labels' => array( @@ -515,6 +700,9 @@ class Activitypub { \register_post_type( Extra_Fields::USER_POST_TYPE, $args ); \register_post_type( Extra_Fields::BLOG_POST_TYPE, $args ); + /** + * Fires after ActivityPub custom post types have been registered. + */ \do_action( 'activitypub_after_register_post_type' ); } @@ -529,4 +717,182 @@ class Activitypub { $user->add_cap( 'activitypub' ); } } + + /** + * Delete `activitypub_content_visibility` when updated to an empty value. + * + * @param int $meta_id ID of updated metadata entry. + * @param int $object_id Post ID. + * @param string $meta_key Metadata key. + * @param mixed $meta_value Metadata value. This will be a PHP-serialized string representation of the value + * if the value is an array, an object, or itself a PHP-serialized string. + */ + public static function updated_postmeta( $meta_id, $object_id, $meta_key, $meta_value ) { + if ( 'activitypub_content_visibility' === $meta_key && empty( $meta_value ) ) { + \delete_post_meta( $object_id, 'activitypub_content_visibility' ); + } + } + + /** + * Register some Mastodon oEmbed providers. + */ + public static function register_oembed_providers() { + \wp_oembed_add_provider( '#https?://mastodon\.social/(@.+)/([0-9]+)#i', 'https://mastodon.social/api/oembed', true ); + \wp_oembed_add_provider( '#https?://mastodon\.online/(@.+)/([0-9]+)#i', 'https://mastodon.online/api/oembed', true ); + \wp_oembed_add_provider( '#https?://mastodon\.cloud/(@.+)/([0-9]+)#i', 'https://mastodon.cloud/api/oembed', true ); + \wp_oembed_add_provider( '#https?://mstdn\.social/(@.+)/([0-9]+)#i', 'https://mstdn.social/api/oembed', true ); + \wp_oembed_add_provider( '#https?://mastodon\.world/(@.+)/([0-9]+)#i', 'https://mastodon.world/api/oembed', true ); + \wp_oembed_add_provider( '#https?://mas\.to/(@.+)/([0-9]+)#i', 'https://mas.to/api/oembed', true ); + } + + /** + * Register user meta. + */ + public static function register_user_meta() { + $blog_prefix = $GLOBALS['wpdb']->get_blog_prefix(); + + \register_meta( + 'user', + $blog_prefix . 'activitypub_also_known_as', + array( + 'type' => 'array', + 'description' => 'An array of URLs that the user is known by.', + 'single' => true, + 'default' => array(), + 'sanitize_callback' => array( Sanitize::class, 'url_list' ), + ) + ); + + \register_meta( + 'user', + $blog_prefix . 'activitypub_old_host_data', + array( + 'description' => 'Actor object for the user on the old host.', + 'single' => true, + ) + ); + + \register_meta( + 'user', + $blog_prefix . 'activitypub_moved_to', + array( + 'type' => 'string', + 'description' => 'The new URL of the user.', + 'single' => true, + 'sanitize_callback' => 'sanitize_url', + ) + ); + + \register_meta( + 'user', + $blog_prefix . 'activitypub_description', + array( + 'type' => 'string', + 'description' => 'The user’s description.', + 'single' => true, + 'default' => '', + 'sanitize_callback' => function ( $value ) { + return wp_kses( $value, 'user_description' ); + }, + ) + ); + + \register_meta( + 'user', + $blog_prefix . 'activitypub_icon', + array( + 'type' => 'integer', + 'description' => 'The attachment ID for user’s profile image.', + 'single' => true, + 'default' => 0, + 'sanitize_callback' => 'absint', + ) + ); + + \register_meta( + 'user', + $blog_prefix . 'activitypub_header_image', + array( + 'type' => 'integer', + 'description' => 'The attachment ID for the user’s header image.', + 'single' => true, + 'default' => 0, + 'sanitize_callback' => 'absint', + ) + ); + + \register_meta( + 'user', + $blog_prefix . 'activitypub_mailer_new_dm', + array( + 'type' => 'integer', + 'description' => 'Send a notification when someone sends this user a direct message.', + 'single' => true, + 'sanitize_callback' => 'absint', + ) + ); + \add_filter( 'get_user_option_activitypub_mailer_new_dm', array( self::class, 'user_options_default' ) ); + + \register_meta( + 'user', + $blog_prefix . 'activitypub_mailer_new_follower', + array( + 'type' => 'integer', + 'description' => 'Send a notification when someone starts to follow this user.', + 'single' => true, + 'sanitize_callback' => 'absint', + ) + ); + \add_filter( 'get_user_option_activitypub_mailer_new_follower', array( self::class, 'user_options_default' ) ); + + \register_meta( + 'user', + $blog_prefix . 'activitypub_mailer_new_mention', + array( + 'type' => 'integer', + 'description' => 'Send a notification when someone mentions this user.', + 'single' => true, + 'sanitize_callback' => 'absint', + ) + ); + \add_filter( 'get_user_option_activitypub_mailer_new_mention', array( self::class, 'user_options_default' ) ); + + \register_meta( + 'user', + 'activitypub_show_welcome_tab', + array( + 'type' => 'integer', + 'description' => 'Whether to show the welcome tab.', + 'single' => true, + 'default' => 1, + 'sanitize_callback' => 'absint', + ) + ); + + \register_meta( + 'user', + 'activitypub_show_advanced_tab', + array( + 'type' => 'integer', + 'description' => 'Whether to show the advanced tab.', + 'single' => true, + 'default' => 0, + 'sanitize_callback' => 'absint', + ) + ); + } + + /** + * Set default values for user options. + * + * @param bool|string $value Option value. + * @return bool|string + */ + public static function user_options_default( $value ) { + if ( false === $value ) { + return '1'; + } + + return $value; + } } diff --git a/wp-content/plugins/activitypub/includes/class-admin.php b/wp-content/plugins/activitypub/includes/class-admin.php deleted file mode 100644 index 2d1d86cd..00000000 --- a/wp-content/plugins/activitypub/includes/class-admin.php +++ /dev/null @@ -1,784 +0,0 @@ -base && Extra_Fields::is_extra_fields_post_type( $current_screen->post_type ) ) { - ?> -
- -
- - -
-

-
- - 'string', - 'description' => \__( 'Use title and link, summary, full or custom content', 'activitypub' ), - 'show_in_rest' => array( - 'schema' => array( - 'enum' => array( - 'title', - 'excerpt', - 'content', - ), - ), - ), - 'default' => 'content', - ) - ); - \register_setting( - 'activitypub', - 'activitypub_custom_post_content', - array( - 'type' => 'string', - 'description' => \__( 'Define your own custom post template', 'activitypub' ), - 'show_in_rest' => true, - 'default' => ACTIVITYPUB_CUSTOM_POST_CONTENT, - ) - ); - \register_setting( - 'activitypub', - 'activitypub_max_image_attachments', - array( - 'type' => 'integer', - 'description' => \__( 'Number of images to attach to posts.', 'activitypub' ), - 'default' => ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS, - ) - ); - \register_setting( - 'activitypub', - 'activitypub_object_type', - array( - 'type' => 'string', - 'description' => \__( 'The Activity-Object-Type', 'activitypub' ), - 'show_in_rest' => array( - 'schema' => array( - 'enum' => array( - 'note', - 'wordpress-post-format', - ), - ), - ), - 'default' => 'note', - ) - ); - \register_setting( - 'activitypub', - 'activitypub_use_hashtags', - array( - 'type' => 'boolean', - 'description' => \__( 'Add hashtags in the content as native tags and replace the #tag with the tag-link', 'activitypub' ), - 'default' => '0', - ) - ); - \register_setting( - 'activitypub', - 'activitypub_use_opengraph', - array( - 'type' => 'boolean', - 'description' => \__( 'Automatically add "fediverse:creator" OpenGraph tags for Authors and the Blog-User.', 'activitypub' ), - 'default' => '1', - ) - ); - \register_setting( - 'activitypub', - 'activitypub_support_post_types', - array( - 'type' => 'string', - 'description' => \esc_html__( 'Enable ActivityPub support for post types', 'activitypub' ), - 'show_in_rest' => true, - 'default' => array( 'post' ), - ) - ); - \register_setting( - 'activitypub', - 'activitypub_enable_users', - array( - 'type' => 'boolean', - 'description' => \__( 'Every Author on this Blog (with the publish_posts capability) gets his own ActivityPub enabled Profile.', 'activitypub' ), - 'default' => '1', - ) - ); - \register_setting( - 'activitypub', - 'activitypub_enable_blog_user', - array( - 'type' => 'boolean', - 'description' => \__( 'Your Blog becomes an ActivityPub compatible Profile.', 'activitypub' ), - 'default' => '0', - ) - ); - - // Blog-User Settings. - \register_setting( - 'activitypub_blog', - 'activitypub_blog_description', - array( - 'type' => 'string', - 'description' => \esc_html__( 'The Description of the Blog-User', 'activitypub' ), - 'show_in_rest' => true, - 'default' => '', - ) - ); - \register_setting( - 'activitypub_blog', - 'activitypub_blog_identifier', - array( - 'type' => 'string', - 'description' => \esc_html__( 'The Identifier of the Blog-User', 'activitypub' ), - 'show_in_rest' => true, - 'default' => Blog::get_default_username(), - 'sanitize_callback' => function ( $value ) { - // Hack to allow dots in the username. - $parts = explode( '.', $value ); - $sanitized = array(); - - foreach ( $parts as $part ) { - $sanitized[] = \sanitize_title( $part ); - } - - $sanitized = implode( '.', $sanitized ); - - // Check for login or nicename. - $user = new WP_User_Query( - array( - 'search' => $sanitized, - 'search_columns' => array( 'user_login', 'user_nicename' ), - 'number' => 1, - 'hide_empty' => true, - 'fields' => 'ID', - ) - ); - - if ( $user->results ) { - add_settings_error( - 'activitypub_blog_identifier', - 'activitypub_blog_identifier', - \esc_html__( 'You cannot use an existing author\'s name for the blog profile ID.', 'activitypub' ), - 'error' - ); - - return Blog::get_default_username(); - } - - return $sanitized; - }, - ) - ); - \register_setting( - 'activitypub_blog', - 'activitypub_header_image', - array( - 'type' => 'integer', - 'description' => \__( 'The Attachment-ID of the Sites Header-Image', 'activitypub' ), - 'default' => null, - ) - ); - } - - /** - * Adds the ActivityPub settings to the Help tab. - */ - public static function add_settings_help_tab() { - require_once ACTIVITYPUB_PLUGIN_DIR . 'includes/help.php'; - } - - /** - * Adds the follower list to the Help tab. - */ - public static function add_followers_list_help_tab() { - // todo. - } - - /** - * Add the profile. - * - * @param \WP_User $user The user object. - */ - public static function add_profile( $user ) { - $description = \get_user_option( 'activitypub_description', $user->ID ); - - wp_enqueue_media(); - wp_enqueue_script( 'activitypub-header-image' ); - - \load_template( - ACTIVITYPUB_PLUGIN_DIR . 'templates/user-settings.php', - true, - array( - 'description' => $description, - ) - ); - } - - /** - * Save the user settings. - * - * Handles the saving of the ActivityPub settings. - * - * @param int $user_id The user ID. - */ - public static function save_user_settings( $user_id ) { - if ( ! isset( $_REQUEST['_apnonce'] ) ) { - return; - } - - $nonce = sanitize_text_field( wp_unslash( $_REQUEST['_apnonce'] ) ); - if ( - ! wp_verify_nonce( $nonce, 'activitypub-user-settings' ) || - ! current_user_can( 'edit_user', $user_id ) - ) { - return; - } - - $description = ! empty( $_POST['activitypub_description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['activitypub_description'] ) ) : false; - if ( $description ) { - \update_user_option( $user_id, 'activitypub_description', $description ); - } else { - \delete_user_option( $user_id, 'activitypub_description' ); - } - - $header_image = ! empty( $_POST['activitypub_header_image'] ) ? sanitize_text_field( wp_unslash( $_POST['activitypub_header_image'] ) ) : false; - if ( $header_image && \wp_attachment_is_image( $header_image ) ) { - \update_user_option( $user_id, 'activitypub_header_image', $header_image ); - } else { - \delete_user_option( $user_id, 'activitypub_header_image' ); - } - } - - /** - * Enqueue the admin scripts and styles. - * - * @param string $hook_suffix The current page. - */ - public static function enqueue_scripts( $hook_suffix ) { - wp_register_script( - 'activitypub-header-image', - plugins_url( - 'assets/js/activitypub-header-image.js', - ACTIVITYPUB_PLUGIN_FILE - ), - array( 'jquery' ), - get_plugin_version(), - false - ); - - if ( false !== strpos( $hook_suffix, 'activitypub' ) ) { - wp_enqueue_style( - 'activitypub-admin-styles', - plugins_url( - 'assets/css/activitypub-admin.css', - ACTIVITYPUB_PLUGIN_FILE - ), - array(), - get_plugin_version() - ); - wp_enqueue_script( - 'activitypub-admin-script', - plugins_url( - 'assets/js/activitypub-admin.js', - ACTIVITYPUB_PLUGIN_FILE - ), - array( 'jquery' ), - get_plugin_version(), - false - ); - } - - if ( 'index.php' === $hook_suffix ) { - wp_enqueue_style( - 'activitypub-admin-styles', - plugins_url( - 'assets/css/activitypub-admin.css', - ACTIVITYPUB_PLUGIN_FILE - ), - array(), - get_plugin_version() - ); - } - } - - /** - * Hook into the edit_comment functionality. - * - * Disables the edit_comment capability for federated comments. - */ - public static function edit_comment() { - // Disable the edit_comment capability for federated comments. - \add_filter( - 'user_has_cap', - function ( $allcaps, $caps, $arg ) { - if ( 'edit_comment' !== $arg[0] ) { - return $allcaps; - } - - if ( was_comment_received( $arg[2] ) ) { - return false; - } - - return $allcaps; - }, - 1, - 3 - ); - } - - /** - * Hook into the edit_post functionality. - * - * Disables the edit_post capability for federated posts. - */ - public static function edit_post() { - // Disable the edit_post capability for federated posts. - \add_filter( - 'user_has_cap', - function ( $allcaps, $caps, $arg ) { - if ( 'edit_post' !== $arg[0] ) { - return $allcaps; - } - - $post = get_post( $arg[2] ); - - if ( ! Extra_Fields::is_extra_field_post_type( $post->post_type ) ) { - return $allcaps; - } - - if ( (int) get_current_user_id() !== (int) $post->post_author ) { - return false; - } - - return $allcaps; - }, - 1, - 3 - ); - } - - /** - * Add ActivityPub specific actions/filters to the post list view. - */ - public static function list_posts() { - // Show only the user's extra fields. - \add_action( - 'pre_get_posts', - function ( $query ) { - if ( $query->get( 'post_type' ) === 'ap_extrafield' ) { - $query->set( 'author', get_current_user_id() ); - } - } - ); - - // Remove all views for the extra fields. - $screen_id = get_current_screen()->id; - - add_filter( - "views_{$screen_id}", - function ( $views ) { - if ( Extra_Fields::is_extra_fields_post_type( get_current_screen()->post_type ) ) { - return array(); - } - - return $views; - } - ); - } - - /** - * Comment row actions. - * - * @param array $actions The existing actions. - * @param int|\WP_Comment $comment The comment object or ID. - * - * @return array The modified actions. - */ - public static function comment_row_actions( $actions, $comment ) { - if ( was_comment_received( $comment ) ) { - unset( $actions['edit'] ); - unset( $actions['quickedit'] ); - } - - return $actions; - } - - /** - * Add a column "activitypub". - * - * This column shows if the user has the capability to use ActivityPub. - * - * @param array $columns The columns. - * - * @return array The columns extended by the activitypub. - */ - public static function manage_users_columns( $columns ) { - $columns['activitypub'] = __( 'ActivityPub', 'activitypub' ); - return $columns; - } - - /** - * Add "comment-type" and "protocol" as column in WP-Admin. - * - * @param array $columns The list of column names. - * - * @return array The extended list of column names. - */ - public static function manage_comment_columns( $columns ) { - $columns['comment_type'] = esc_attr__( 'Comment-Type', 'activitypub' ); - $columns['comment_protocol'] = esc_attr__( 'Protocol', 'activitypub' ); - - return $columns; - } - - /** - * Add "post_content" as column for Extra-Fields in WP-Admin. - * - * @param array $columns The list of column names. - * @param string $post_type The post type. - * - * @return array The extended list of column names. - */ - public static function manage_post_columns( $columns, $post_type ) { - if ( Extra_Fields::is_extra_fields_post_type( $post_type ) ) { - $after_key = 'title'; - $index = array_search( $after_key, array_keys( $columns ), true ); - $columns = array_slice( $columns, 0, $index + 1 ) + array( 'extra_field_content' => esc_attr__( 'Content', 'activitypub' ) ) + $columns; - } - - return $columns; - } - - /** - * Add "comment-type" and "protocol" as column in WP-Admin. - * - * @param array $column The column to implement. - * @param int $comment_id The comment id. - */ - public static function manage_comments_custom_column( $column, $comment_id ) { - if ( 'comment_type' === $column && ! defined( 'WEBMENTION_PLUGIN_DIR' ) ) { - echo esc_attr( ucfirst( get_comment_type( $comment_id ) ) ); - } elseif ( 'comment_protocol' === $column ) { - $protocol = get_comment_meta( $comment_id, 'protocol', true ); - - if ( $protocol ) { - echo esc_attr( ucfirst( str_replace( 'activitypub', 'ActivityPub', $protocol ) ) ); - } else { - esc_attr_e( 'Local', 'activitypub' ); - } - } - } - - /** - * Return the results for the activitypub column. - * - * @param string $output Custom column output. Default empty. - * @param string $column_name Column name. - * @param int $user_id ID of the currently-listed user. - * - * @return string The column contents. - */ - public static function manage_users_custom_column( $output, $column_name, $user_id ) { - if ( 'activitypub' !== $column_name ) { - return $output; - } - - if ( \user_can( $user_id, 'activitypub' ) ) { - return '' . esc_html__( 'ActivityPub enabled for this author', 'activitypub' ) . ''; - } else { - return '' . esc_html__( 'ActivityPub disabled for this author', 'activitypub' ) . ''; - } - } - - /** - * Add a column "extra_field_content" to the post list view. - * - * @param string $column_name The column name. - * @param int $post_id The post ID. - * - * @return void - */ - public static function manage_posts_custom_column( $column_name, $post_id ) { - if ( 'extra_field_content' === $column_name ) { - $post = get_post( $post_id ); - if ( Extra_Fields::is_extra_fields_post_type( $post->post_type ) ) { - echo esc_attr( wp_strip_all_tags( $post->post_content ) ); - } - } - } - - /** - * Add options to the Bulk dropdown on the users page. - * - * @param array $actions The existing bulk options. - * - * @return array The extended bulk options. - */ - public static function user_bulk_options( $actions ) { - $actions['add_activitypub_cap'] = __( 'Enable for ActivityPub', 'activitypub' ); - $actions['remove_activitypub_cap'] = __( 'Disable for ActivityPub', 'activitypub' ); - - return $actions; - } - - /** - * Handle bulk activitypub requests. - * - * * `add_activitypub_cap` - Add the activitypub capability to the selected users. - * * `remove_activitypub_cap` - Remove the activitypub capability from the selected users. - * - * @param string $sendback The URL to send the user back to. - * @param string $action The requested action. - * @param array $users The selected users. - * - * @return string The URL to send the user back to. - */ - public static function handle_bulk_request( $sendback, $action, $users ) { - if ( - 'remove_activitypub_cap' !== $action && - 'add_activitypub_cap' !== $action - ) { - return $sendback; - } - - foreach ( $users as $user_id ) { - $user = new \WP_User( $user_id ); - if ( - 'add_activitypub_cap' === $action && - user_can( $user_id, 'publish_posts' ) - ) { - $user->add_cap( 'activitypub' ); - } elseif ( 'remove_activitypub_cap' === $action ) { - $user->remove_cap( 'activitypub' ); - } - } - - return $sendback; - } - - /** - * Add ActivityPub infos to the dashboard glance items. - * - * @param array $items The existing glance items. - * - * @return array The extended glance items. - */ - public static function dashboard_glance_items( $items ) { - \add_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers', 10, 3 ); - - if ( ! is_user_disabled( get_current_user_id() ) ) { - $follower_count = sprintf( - // translators: %s: number of followers. - _n( - '%s Follower', - '%s Followers', - count_followers( \get_current_user_id() ), - 'activitypub' - ), - \number_format_i18n( count_followers( \get_current_user_id() ) ) - ); - $items['activitypub-followers-user'] = sprintf( - '%3$s', - \esc_url( \admin_url( 'users.php?page=activitypub-followers-list' ) ), - \esc_attr__( 'Your followers', 'activitypub' ), - \esc_html( $follower_count ) - ); - } - - if ( ! is_user_type_disabled( 'blog' ) && current_user_can( 'manage_options' ) ) { - $follower_count = sprintf( - // translators: %s: number of followers. - _n( - '%s Follower (Blog)', - '%s Followers (Blog)', - count_followers( Users::BLOG_USER_ID ), - 'activitypub' - ), - \number_format_i18n( count_followers( Users::BLOG_USER_ID ) ) - ); - $items['activitypub-followers-blog'] = sprintf( - '%3$s', - \esc_url( \admin_url( 'options-general.php?page=activitypub&tab=followers' ) ), - \esc_attr__( 'The Blog\'s followers', 'activitypub' ), - \esc_html( $follower_count ) - ); - } - - \remove_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers' ); - - return $items; - } -} diff --git a/wp-content/plugins/activitypub/includes/class-autoloader.php b/wp-content/plugins/activitypub/includes/class-autoloader.php new file mode 100644 index 00000000..232a8323 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-autoloader.php @@ -0,0 +1,106 @@ +prefix = $prefix; + $this->prefix_length = \strlen( $prefix ); + $this->path = \rtrim( $path . '/' ); + } + + /** + * Registers Autoloader's autoload function. + * + * @throws \Exception When autoload_function cannot be registered. + * + * @param string $prefix Namespace prefix all classes have in common. + * @param string $path Path to the files to be loaded. + */ + public static function register_path( $prefix, $path ) { + $loader = new self( $prefix, $path ); + \spl_autoload_register( array( $loader, 'load' ) ); + } + + /** + * Loads a class if its namespace starts with `$this->prefix`. + * + * @param string $class_name The class to be loaded. + */ + public function load( $class_name ) { + if ( \strpos( $class_name, $this->prefix . self::NS_SEPARATOR ) !== 0 ) { + return; + } + + // Strip prefix from the start (ala PSR-4). + $class_name = \substr( $class_name, $this->prefix_length + 1 ); + $class_name = \strtolower( $class_name ); + $dir = ''; + + $last_ns_pos = \strripos( $class_name, self::NS_SEPARATOR ); + if ( false !== $last_ns_pos ) { + $namespace = \substr( $class_name, 0, $last_ns_pos ); + $namespace = \str_replace( '_', '-', $namespace ); + $class_name = \substr( $class_name, $last_ns_pos + 1 ); + $dir = \str_replace( self::NS_SEPARATOR, DIRECTORY_SEPARATOR, $namespace ) . DIRECTORY_SEPARATOR; + } + + $path = $this->path . $dir . 'class-' . \str_replace( '_', '-', $class_name ) . '.php'; + + if ( ! \file_exists( $path ) ) { + $path = $this->path . $dir . 'interface-' . \str_replace( '_', '-', $class_name ) . '.php'; + } + + if ( ! \file_exists( $path ) ) { + $path = $this->path . $dir . 'trait-' . \str_replace( '_', '-', $class_name ) . '.php'; + } + + if ( \file_exists( $path ) ) { + require_once $path; + } + } +} diff --git a/wp-content/plugins/activitypub/includes/class-blocks.php b/wp-content/plugins/activitypub/includes/class-blocks.php index c42d2fc6..80989af2 100644 --- a/wp-content/plugins/activitypub/includes/class-blocks.php +++ b/wp-content/plugins/activitypub/includes/class-blocks.php @@ -7,8 +7,8 @@ namespace Activitypub; +use Activitypub\Collection\Actors; use Activitypub\Collection\Followers; -use Activitypub\Collection\Users as User_Collection; /** * Block class. @@ -21,12 +21,14 @@ class Blocks { // This is already being called on the init hook, so just add it. self::register_blocks(); - \add_action( 'wp_enqueue_scripts', array( self::class, 'add_data' ) ); - \add_action( 'enqueue_block_editor_assets', array( self::class, 'add_data' ) ); + \add_action( 'wp_head', array( self::class, 'inject_activitypub_options' ), 11 ); + \add_action( 'admin_print_scripts', array( self::class, 'inject_activitypub_options' ) ); \add_action( 'load-post-new.php', array( self::class, 'handle_in_reply_to_get_param' ) ); // Add editor plugin. \add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_editor_assets' ) ); \add_action( 'init', array( self::class, 'register_postmeta' ), 11 ); + + \add_filter( 'activitypub_import_mastodon_post_data', array( self::class, 'filter_import_mastodon_post_data' ), 10, 2 ); } /** @@ -42,7 +44,36 @@ class Blocks { 'show_in_rest' => true, 'single' => true, 'type' => 'string', - 'sanitize_callback' => 'sanitize_text_field', + 'sanitize_callback' => function ( $warning ) { + if ( $warning ) { + return \sanitize_text_field( $warning ); + } + + return null; + }, + ) + ); + + \register_post_meta( + $post_type, + 'activitypub_content_visibility', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'sanitize_callback' => function ( $value ) { + $schema = array( + 'type' => 'string', + 'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ), + 'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ); + + if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) { + return $schema['default']; + } + + return $value; + }, ) ); } @@ -79,22 +110,22 @@ class Blocks { } /** - * Add data to the block editor. + * Output ActivityPub options as a script tag. */ - public static function add_data() { - $context = is_admin() ? 'editor' : 'view'; - $followers_handle = 'activitypub-followers-' . $context . '-script'; - $follow_me_handle = 'activitypub-follow-me-' . $context . '-script'; - $data = array( - 'namespace' => ACTIVITYPUB_REST_NAMESPACE, - 'enabled' => array( + public static function inject_activitypub_options() { + $data = array( + 'namespace' => ACTIVITYPUB_REST_NAMESPACE, + 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', + 'enabled' => array( 'site' => ! is_user_type_disabled( 'blog' ), 'users' => ! is_user_type_disabled( 'user' ), ), ); - $js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) ); - \wp_add_inline_script( $followers_handle, $js, 'before' ); - \wp_add_inline_script( $follow_me_handle, $js, 'before' ); + + printf( + "\n", + wp_json_encode( $data ) + ); } /** @@ -119,6 +150,38 @@ class Blocks { 'render_callback' => array( self::class, 'render_reply_block' ), ) ); + + \register_block_type_from_metadata( + ACTIVITYPUB_PLUGIN_DIR . '/build/reactions', + array( + 'render_callback' => array( self::class, 'render_post_reactions_block' ), + ) + ); + } + + /** + * Render the post reactions block. + * + * @param array $attrs The block attributes. + * + * @return string The HTML to render. + */ + public static function render_post_reactions_block( $attrs ) { + if ( ! isset( $attrs['postId'] ) ) { + $attrs['postId'] = get_the_ID(); + } + + $wrapper_attributes = get_block_wrapper_attributes( + array( + 'class' => 'activitypub-reactions-block', + 'data-attrs' => wp_json_encode( $attrs ), + ) + ); + + return sprintf( + '
', + $wrapper_attributes + ); } /** @@ -134,7 +197,7 @@ class Blocks { // If the user string is 'site', return the Blog User ID. if ( 'site' === $user_string ) { - return User_Collection::BLOG_USER_ID; + return Actors::BLOG_USER_ID; } // The only other value should be 'inherit', which means to use the query context to determine the User. @@ -144,7 +207,7 @@ class Blocks { // For a homepage/front page, if the Blog User is active, use it. if ( ( is_front_page() || is_home() ) && ! is_user_type_disabled( 'blog' ) ) { - return User_Collection::BLOG_USER_ID; + return Actors::BLOG_USER_ID; } // If we're in a loop, use the post author. @@ -192,7 +255,7 @@ class Blocks { */ public static function render_follow_me_block( $attrs ) { $user_id = self::get_user_id( $attrs['selectedUser'] ); - $user = User_Collection::get_by_id( $user_id ); + $user = Actors::get_by_id( $user_id ); if ( is_wp_error( $user ) ) { if ( 'inherit' === $attrs['selectedUser'] ) { // If the user is 'inherit' and we couldn't determine the user, don't render anything. @@ -210,7 +273,6 @@ class Blocks { $wrapper_attributes = get_block_wrapper_attributes( array( - 'aria-label' => __( 'Follow me on the Fediverse', 'activitypub' ), 'class' => 'activitypub-follow-me-block-wrapper', 'data-attrs' => wp_json_encode( $attrs ), ) @@ -232,7 +294,7 @@ class Blocks { return ''; } - $user = User_Collection::get_by_id( $followee_user_id ); + $user = Actors::get_by_id( $followee_user_id ); if ( is_wp_error( $user ) ) { return ''; } @@ -279,23 +341,45 @@ class Blocks { * @return string The HTML to render. */ public static function render_reply_block( $attrs ) { - /** - * Filter the reply block. - * - * @param string $html The HTML to render. - * @param array $attrs The block attributes. - */ - return apply_filters( - 'activitypub_reply_block', - sprintf( + // Return early if no URL is provided. + if ( empty( $attrs['url'] ) ) { + return null; + } + + $show_embed = isset( $attrs['embedPost'] ) && $attrs['embedPost']; + + $wrapper_attrs = get_block_wrapper_attributes( + array( + 'aria-label' => __( 'Reply', 'activitypub' ), + 'class' => 'activitypub-reply-block', + 'data-in-reply-to' => $attrs['url'], + ) + ); + + $html = '
'; + + // Try to get and append the embed if requested. + if ( $show_embed ) { + $embed = wp_oembed_get( $attrs['url'] ); + if ( $embed ) { + $html .= $embed; + } + } + + // Only show the link if we're not showing the embed. + if ( ! $show_embed ) { + $html .= sprintf( '

%3$s

', esc_url( $attrs['url'] ), esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ), // translators: %s is the URL of the post being replied to. - sprintf( __( '↬%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', $attrs['url'] ) ) - ), - $attrs - ); + sprintf( __( '↬%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', esc_url( $attrs['url'] ) ) ) + ); + } + + $html .= '
'; + + return $html; } /** @@ -330,4 +414,33 @@ class Blocks { $external_svg ); } + + /** + * Converts content to blocks before saving to the database. + * + * @param array $data The post data to be inserted. + * @param object $post The Mastodon Create activity. + * + * @return array + */ + public static function filter_import_mastodon_post_data( $data, $post ) { + // Convert paragraphs to blocks. + \preg_match_all( '#

.*?

#is', $data['post_content'], $matches ); + $blocks = \array_map( + function ( $paragraph ) { + return '' . PHP_EOL . $paragraph . PHP_EOL . '' . PHP_EOL; + }, + $matches[0] ?? array() + ); + + $data['post_content'] = \rtrim( \implode( PHP_EOL, $blocks ), PHP_EOL ); + + // Add reply block if it's a reply. + if ( null !== $post->object->inReplyTo ) { + $reply_block = \sprintf( '' . PHP_EOL, \esc_url( $post->object->inReplyTo ) ); + $data['post_content'] = $reply_block . $data['post_content']; + } + + return $data; + } } diff --git a/wp-content/plugins/activitypub/includes/class-cli.php b/wp-content/plugins/activitypub/includes/class-cli.php index 64b4b8ab..b5c9d224 100644 --- a/wp-content/plugins/activitypub/includes/class-cli.php +++ b/wp-content/plugins/activitypub/includes/class-cli.php @@ -7,83 +7,14 @@ namespace Activitypub; -use WP_CLI; -use WP_CLI_Command; +use Activitypub\Collection\Outbox; /** * WP-CLI commands. * * @package Activitypub */ -class Cli extends WP_CLI_Command { - /** - * Check the Plugins Meta-Information. - * - * ## OPTIONS - * - * [--Name] - * The Plugin Name. - * - * [--PluginURI] - * The Plugin URI. - * - * [--Version] - * The Plugin Version. - * - * [--Description] - * The Plugin Description. - * - * [--Author] - * The Plugin Author. - * - * [--AuthorURI] - * The Plugin Author URI. - * - * [--TextDomain] - * The Plugin Text Domain. - * - * [--DomainPath] - * The Plugin Domain Path. - * - * [--Network] - * The Plugin Network. - * - * [--RequiresWP] - * The Plugin Requires at least. - * - * [--RequiresPHP] - * The Plugin Requires PHP. - * - * [--UpdateURI] - * The Plugin Update URI. - * - * See: https://developer.wordpress.org/reference/functions/get_plugin_data/#return - * - * ## EXAMPLES - * - * $ wp webmention meta - * - * $ wp webmention meta --Version - * Version: 1.0.0 - * - * @param array|null $args The arguments. - * @param array|null $assoc_args The associative arguments. - * - * @return void - */ - public function meta( $args, $assoc_args ) { - $plugin_data = get_plugin_meta(); - - if ( $assoc_args ) { - $plugin_data = array_intersect_key( $plugin_data, $assoc_args ); - } else { - WP_CLI::line( __( "ActivityPub Plugin Meta:\n", 'activitypub' ) ); - } - - foreach ( $plugin_data as $key => $value ) { - WP_CLI::line( $key . ': ' . $value ); - } - } +class Cli extends \WP_CLI_Command { /** * Remove the entire blog from the Fediverse. @@ -98,7 +29,7 @@ class Cli extends WP_CLI_Command { * @return void */ public function self_destruct( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable - WP_CLI::warning( __( 'Self-Destructing is not implemented yet.', 'activitypub' ) ); + \WP_CLI::warning( 'Self-Destructing is not implemented yet.' ); } /** @@ -123,28 +54,27 @@ class Cli extends WP_CLI_Command { * * @synopsis * - * @param array|null $args The arguments. + * @param array $args The arguments. */ public function post( $args ) { $post = get_post( $args[1] ); if ( ! $post ) { - WP_CLI::error( __( 'Post not found.', 'activitypub' ) ); + \WP_CLI::error( 'Post not found.' ); } switch ( $args[0] ) { case 'delete': - // translators: %s is the ID of the post. - WP_CLI::confirm( sprintf( __( 'Do you really want to delete the (Custom) Post with the ID: %s', 'activitypub' ), $args[1] ) ); - Scheduler::schedule_post_activity( 'trash', 'publish', $args[1] ); - WP_CLI::success( __( '"Delete"-Activity is queued.', 'activitypub' ) ); + \WP_CLI::confirm( 'Do you really want to delete the (Custom) Post with the ID: ' . $args[1] ); + add_to_outbox( $post, 'Delete', $post->post_author ); + \WP_CLI::success( '"Delete" activity is queued.' ); break; case 'update': - Scheduler::schedule_post_activity( 'publish', 'publish', $args[1] ); - WP_CLI::success( __( '"Update"-Activity is queued.', 'activitypub' ) ); + add_to_outbox( $post, 'Update', $post->post_author ); + \WP_CLI::success( '"Update" activity is queued.' ); break; default: - WP_CLI::error( __( 'Unknown action.', 'activitypub' ) ); + \WP_CLI::error( 'Unknown action.' ); } } @@ -170,32 +100,131 @@ class Cli extends WP_CLI_Command { * * @synopsis * - * @param array|null $args The arguments. + * @param array $args The arguments. */ public function comment( $args ) { $comment = get_comment( $args[1] ); if ( ! $comment ) { - WP_CLI::error( __( 'Comment not found.', 'activitypub' ) ); + \WP_CLI::error( 'Comment not found.' ); } if ( was_comment_received( $comment ) ) { - WP_CLI::error( __( 'This comment was received via ActivityPub and cannot be deleted or updated.', 'activitypub' ) ); + \WP_CLI::error( 'This comment was received via ActivityPub and cannot be deleted or updated.' ); } switch ( $args[0] ) { case 'delete': - // translators: %s is the ID of the comment. - WP_CLI::confirm( sprintf( __( 'Do you really want to delete the Comment with the ID: %s', 'activitypub' ), $args[1] ) ); - Scheduler::schedule_comment_activity( 'trash', 'approved', $args[1] ); - WP_CLI::success( __( '"Delete"-Activity is queued.', 'activitypub' ) ); + \WP_CLI::confirm( 'Do you really want to delete the Comment with the ID: ' . $args[1] ); + add_to_outbox( $comment, 'Delete', $comment->user_id ); + \WP_CLI::success( '"Delete" activity is queued.' ); break; case 'update': - Scheduler::schedule_comment_activity( 'approved', 'approved', $args[1] ); - WP_CLI::success( __( '"Update"-Activity is queued.', 'activitypub' ) ); + add_to_outbox( $comment, 'Update', $comment->user_id ); + \WP_CLI::success( '"Update" activity is queued.' ); break; default: - WP_CLI::error( __( 'Unknown action.', 'activitypub' ) ); + \WP_CLI::error( 'Unknown action.' ); + } + } + + /** + * Undo an activity that was sent to the Fediverse. + * + * ## OPTIONS + * + * + * The ID or URL of the outbox item to undo. + * + * ## EXAMPLES + * + * $ wp activitypub undo 123 + * $ wp activitypub undo "https://example.com/?post_type=ap_outbox&p=123" + * + * @synopsis + * + * @param array $args The arguments. + */ + public function undo( $args ) { + $outbox_item_id = $args[0]; + if ( ! is_numeric( $outbox_item_id ) ) { + $outbox_item_id = url_to_postid( $outbox_item_id ); + } + + $outbox_item_id = get_post( $outbox_item_id ); + if ( ! $outbox_item_id ) { + \WP_CLI::error( 'Activity not found.' ); + } + + $undo_id = Outbox::undo( $outbox_item_id ); + if ( ! $undo_id ) { + \WP_CLI::error( 'Failed to undo activity.' ); + } + \WP_CLI::success( 'Undo activity scheduled.' ); + } + + /** + * Re-Schedule an activity that was sent to the Fediverse before. + * + * ## OPTIONS + * + * + * The ID or URL of the outbox item to reschedule. + * + * ## EXAMPLES + * + * $ wp activitypub reschedule 123 + * $ wp activitypub reschedule "https://example.com/?post_type=ap_outbox&p=123" + * + * @synopsis + * + * @param array $args The arguments. + */ + public function reschedule( $args ) { + $outbox_item_id = $args[0]; + if ( ! is_numeric( $outbox_item_id ) ) { + $outbox_item_id = url_to_postid( $outbox_item_id ); + } + + $outbox_item_id = get_post( $outbox_item_id ); + if ( ! $outbox_item_id ) { + \WP_CLI::error( 'Activity not found.' ); + } + + Outbox::reschedule( $outbox_item_id ); + + \WP_CLI::success( 'Rescheduled activity.' ); + } + + /** + * Move the blog to a new URL. + * + * ## OPTIONS + * + * + * The current URL of the blog. + * + * + * The new URL of the blog. + * + * ## EXAMPLES + * + * $ wp activitypub move https://example.com/ https://newsite.com/ + * + * @synopsis + * + * @param array $args The arguments. + */ + public function move( $args ) { + $from = $args[0]; + $to = $args[1]; + + $outbox_item_id = Move::account( $from, $to ); + + if ( is_wp_error( $outbox_item_id ) ) { + \WP_CLI::error( $outbox_item_id->get_error_message() ); + } else { + \WP_CLI::success( 'Move Scheduled.' ); } } } diff --git a/wp-content/plugins/activitypub/includes/class-comment.php b/wp-content/plugins/activitypub/includes/class-comment.php index 525a3543..bfa8537e 100644 --- a/wp-content/plugins/activitypub/includes/class-comment.php +++ b/wp-content/plugins/activitypub/includes/class-comment.php @@ -7,7 +7,7 @@ namespace Activitypub; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; use WP_Comment_Query; /** @@ -25,11 +25,14 @@ class Comment { \add_filter( 'comment_reply_link', array( self::class, 'comment_reply_link' ), 10, 3 ); \add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 ); - \add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 3 ); + \add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 2 ); \add_action( 'wp_enqueue_scripts', array( self::class, 'enqueue_scripts' ) ); \add_action( 'pre_get_comments', array( static::class, 'comment_query' ) ); - + \add_filter( 'pre_comment_approved', array( static::class, 'pre_comment_approved' ), 10, 2 ); \add_filter( 'get_avatar_comment_types', array( static::class, 'get_avatar_comment_types' ), 99 ); + \add_action( 'update_option_activitypub_allow_likes', array( self::class, 'maybe_update_comment_counts' ), 10, 2 ); + \add_action( 'update_option_activitypub_allow_reposts', array( self::class, 'maybe_update_comment_counts' ), 10, 2 ); + \add_filter( 'pre_wp_update_comment_count_now', array( static::class, 'pre_wp_update_comment_count_now' ), 10, 3 ); } /** @@ -46,8 +49,7 @@ class Comment { */ public static function comment_reply_link( $link, $args, $comment ) { if ( self::are_comments_allowed( $comment ) ) { - $user_id = get_current_user_id(); - if ( $user_id && self::was_received( $comment ) && \user_can( $user_id, 'activitypub' ) ) { + if ( \current_user_can( 'activitypub' ) && self::was_received( $comment ) ) { return self::create_fediverse_reply_link( $link, $args ); } @@ -60,10 +62,17 @@ class Comment { ); $div = sprintf( - '
', + '
', esc_attr( wp_json_encode( $attrs ) ) ); + /** + * Filters the HTML markup for the ActivityPub remote comment reply container. + * + * @param string $div The HTML markup for the remote reply container. Default is a div + * with class 'activitypub-remote-reply' and data attributes for + * the selected comment ID and internal comment ID. + */ return apply_filters( 'activitypub_comment_reply_link', $div ); } @@ -113,16 +122,10 @@ class Comment { if ( is_single_user() && \user_can( $current_user, 'publish_posts' ) ) { // On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user. - $current_user = Users::BLOG_USER_ID; + $current_user = Actors::BLOG_USER_ID; } - $is_user_disabled = is_user_disabled( $current_user ); - - if ( $is_user_disabled ) { - return false; - } - - return true; + return user_can_activitypub( $current_user ); } /** @@ -221,15 +224,13 @@ class Comment { return false; } - if ( is_single_user() && \user_can( $user_id, 'publish_posts' ) ) { + if ( is_single_user() && \user_can( $user_id, 'activitypub' ) ) { // On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user. - $user_id = Users::BLOG_USER_ID; + $user_id = Actors::BLOG_USER_ID; } - $is_user_disabled = is_user_disabled( $user_id ); - - // User is disabled for federation. - if ( $is_user_disabled ) { + // User is not allowed to federate comments. + if ( ! user_can_activitypub( $user_id ) ) { return false; } @@ -256,6 +257,8 @@ class Comment { array( 'meta_key' => 'source_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key 'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value + 'orderby' => 'comment_date', + 'order' => 'DESC', ) ); @@ -263,10 +266,6 @@ class Comment { return false; } - if ( count( $comment_query->comments ) > 1 ) { - return false; - } - return $comment_query->comments[0]; } @@ -479,7 +478,8 @@ class Comment { $handle = 'activitypub-remote-reply'; $data = array( - 'namespace' => ACTIVITYPUB_REST_NAMESPACE, + 'namespace' => ACTIVITYPUB_REST_NAMESPACE, + 'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg', ); $js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) ); $asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/remote-reply/index.asset.php'; @@ -495,6 +495,7 @@ class Comment { true ); \wp_add_inline_script( $handle, $js, 'before' ); + \wp_set_script_translations( $handle, 'activitypub' ); \wp_enqueue_style( $handle, @@ -505,6 +506,27 @@ class Comment { } } + /** + * Get the comment type by activity type. + * + * @param string $activity_type The activity type. + * + * @return array|null The comment type. + */ + public static function get_comment_type_by_activity_type( $activity_type ) { + $activity_type = \strtolower( $activity_type ); + $activity_type = \sanitize_key( $activity_type ); + $comment_types = self::get_comment_types(); + + foreach ( $comment_types as $comment_type ) { + if ( in_array( $activity_type, $comment_type['activity_types'], true ) ) { + return $comment_type; + } + } + + return null; + } + /** * Return the registered custom comment types. * @@ -519,41 +541,63 @@ class Comment { /** * Is this a registered comment type. * - * @param string $slug The name of the type. + * @param string $slug The slug of the type. + * * @return boolean True if registered. */ public static function is_registered_comment_type( $slug ) { - $slug = strtolower( $slug ); - $slug = sanitize_key( $slug ); + $slug = \strtolower( $slug ); + $slug = \sanitize_key( $slug ); - return in_array( $slug, array_keys( self::get_comment_types() ), true ); + $comment_types = self::get_comment_types(); + + return isset( $comment_types[ $slug ] ); } /** - * Return the registered custom comment types names. + * Return the registered custom comment type slugs. * - * @return array The registered custom comment type names. + * @return array The registered custom comment type slugs. + */ + public static function get_comment_type_slugs() { + return array_keys( self::get_comment_types() ); + } + + /** + * Return the registered custom comment type slugs. + * + * @deprecated 4.5.0 Use get_comment_type_slugs instead. + * + * @return array The registered custom comment type slugs. */ public static function get_comment_type_names() { - return array_values( wp_list_pluck( self::get_comment_types(), 'type' ) ); + _deprecated_function( __METHOD__, '4.5.0', 'get_comment_type_slugs' ); + + return self::get_comment_type_slugs(); } /** - * Get a comment type. + * Get the custom comment type. + * + * Check if the type is registered, if not, check if it is a custom type. + * + * It looks for the array key in the registered types and returns the array. + * If it is not found, it looks for the type in the custom types and returns the array. * * @param string $type The comment type. * * @return array The comment type. */ public static function get_comment_type( $type ) { - $type = strtolower( $type ); - $type = sanitize_key( $type ); - $types = self::get_comment_types(); + $type = strtolower( $type ); + $type = sanitize_key( $type ); - if ( in_array( $type, array_keys( $types ), true ) ) { - $type_array = $types[ $type ]; - } else { - $type_array = array(); + $comment_types = self::get_comment_types(); + $type_array = array(); + + // Check array keys. + if ( in_array( $type, array_keys( $comment_types ), true ) ) { + $type_array = $comment_types[ $type ]; } /** @@ -595,30 +639,40 @@ class Comment { */ public static function register_comment_types() { register_comment_type( - 'announce', + 'repost', 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' ), - 'icon' => '♻️', - 'class' => 'p-repost', - 'type' => 'repost', - // translators: %1$s username, %2$s object format (post, audio, ...), %3$s URL, %4$s domain. - 'excerpt' => __( '… reposted this!', 'activitypub' ), + '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' ), + 'icon' => '♻️', + 'class' => 'p-repost', + 'type' => 'repost', + 'collection' => 'reposts', + 'activity_types' => array( 'announce' ), + 'excerpt' => html_entity_decode( \__( '… reposted this!', 'activitypub' ) ), + /* translators: %d: Number of reposts */ + 'count_single' => _x( '%d repost', 'number of reposts', 'activitypub' ), + /* translators: %d: Number of reposts */ + 'count_plural' => _x( '%d reposts', 'number of reposts', 'activitypub' ), ) ); register_comment_type( 'like', 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' ), - 'icon' => '👍', - 'class' => 'p-like', - 'type' => 'like', - // translators: %1$s username, %2$s object format (post, audio, ...), %3$s URL, %4$s domain. - 'excerpt' => __( '… liked this!', 'activitypub' ), + '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' ), + 'icon' => '👍', + 'class' => 'p-like', + 'type' => 'like', + 'collection' => 'likes', + 'activity_types' => array( 'like' ), + 'excerpt' => html_entity_decode( \__( '… liked this!', 'activitypub' ) ), + /* translators: %d: Number of likes */ + 'count_single' => _x( '%d like', 'number of likes', 'activitypub' ), + /* translators: %d: Number of likes */ + 'count_plural' => _x( '%d likes', 'number of likes', 'activitypub' ), ) ); } @@ -631,7 +685,7 @@ class Comment { * @return array show avatars on Activities */ public static function get_avatar_comment_types( $types ) { - $comment_types = self::get_comment_type_names(); + $comment_types = self::get_comment_type_slugs(); $types = array_merge( $types, $comment_types ); return array_unique( $types ); @@ -651,19 +705,113 @@ class Comment { return; } + // Do not exclude likes and reposts on ActivityPub requests. + if ( defined( 'ACTIVITYPUB_REQUEST' ) && ACTIVITYPUB_REQUEST ) { + return; + } + + // Do not exclude likes and reposts on REST requests. + if ( \wp_is_serving_rest_request() ) { + return; + } + + // Do not exclude likes and reposts on admin pages or on non-singular pages. if ( is_admin() || ! is_singular() ) { return; } - if ( ! empty( $query->query_vars['type__in'] ) ) { + // Do not exclude likes and reposts if the query is for comments. + if ( ! empty( $query->query_vars['type__in'] ) || ! empty( $query->query_vars['type'] ) ) { return; } - if ( isset( $query->query_vars['count'] ) && true === $query->query_vars['count'] ) { - return; + // Exclude likes and reposts by the ActivityPub plugin. + $query->query_vars['type__not_in'] = self::get_comment_type_slugs(); + } + + /** + * Filter the comment status before it is set. + * + * @param string $approved The approved comment status. + * @param array $commentdata The comment data. + * + * @return boolean `true` if the comment is approved, `false` otherwise. + */ + public static function pre_comment_approved( $approved, $commentdata ) { + if ( $approved || \is_wp_error( $approved ) ) { + return $approved; } - // Exclude likes and reposts by the Webmention plugin. - $query->query_vars['type__not_in'] = self::get_comment_type_names(); + if ( '1' !== \get_option( 'comment_previously_approved' ) ) { + return $approved; + } + + if ( + empty( $commentdata['comment_meta']['protocol'] ) || + 'activitypub' !== $commentdata['comment_meta']['protocol'] + ) { + return $approved; + } + + global $wpdb; + + $author = $commentdata['comment_author']; + $author_url = $commentdata['comment_author_url']; + // phpcs:ignore + $ok_to_comment = $wpdb->get_var( $wpdb->prepare( "SELECT comment_approved FROM $wpdb->comments WHERE comment_author = %s AND comment_author_url = %s and comment_approved = '1' LIMIT 1", $author, $author_url ) ); + + if ( 1 === (int) $ok_to_comment ) { + return 1; + } + + return $approved; + } + + /** + * Update comment counts when interaction settings are disabled. + * + * Triggers a recount when likes or reposts are disabled to ensure accurate comment counts. + * + * @param mixed $old_value The old option value. + * @param mixed $value The new option value. + */ + public static function maybe_update_comment_counts( $old_value, $value ) { + if ( '1' === $old_value && '1' !== $value ) { + Migration::update_comment_counts(); + } + } + + /** + * Filters the comment count to exclude ActivityPub comment types. + * + * @param int|null $new_count The new comment count. Default null. + * @param int $old_count The old comment count. + * @param int $post_id Post ID. + * + * @return int|null The updated comment count, or null to use the default query. + */ + public static function pre_wp_update_comment_count_now( $new_count, $old_count, $post_id ) { + if ( null === $new_count ) { + $excluded_types = array_filter( self::get_comment_type_slugs(), array( self::class, 'is_comment_type_enabled' ) ); + + if ( ! empty( $excluded_types ) ) { + global $wpdb; + + // phpcs:ignore WordPress.DB + $new_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type NOT IN ('" . implode( "','", $excluded_types ) . "')", $post_id ) ); + } + } + + return $new_count; + } + + /** + * Check if a comment type is enabled. + * + * @param string $comment_type The comment type. + * @return bool True if the comment type is enabled. + */ + public static function is_comment_type_enabled( $comment_type ) { + return '1' === get_option( "activitypub_allow_{$comment_type}s", '1' ); } } diff --git a/wp-content/plugins/activitypub/includes/class-debug.php b/wp-content/plugins/activitypub/includes/class-debug.php index ebca0534..af7608dd 100644 --- a/wp-content/plugins/activitypub/includes/class-debug.php +++ b/wp-content/plugins/activitypub/includes/class-debug.php @@ -7,9 +7,6 @@ namespace Activitypub; -use WP_DEBUG; -use WP_DEBUG_LOG; - /** * ActivityPub Debug Class. * @@ -20,9 +17,11 @@ class Debug { * Initialize the class, registering WordPress hooks. */ public static function init() { - if ( WP_DEBUG_LOG ) { + if ( \WP_DEBUG && \WP_DEBUG_LOG ) { \add_action( 'activitypub_safe_remote_post_response', array( self::class, 'log_remote_post_responses' ), 10, 2 ); \add_action( 'activitypub_inbox', array( self::class, 'log_inbox' ), 10, 3 ); + + \add_action( 'activitypub_sent_to_inbox', array( self::class, 'log_sent_to_inbox' ), 10, 2 ); } } @@ -48,12 +47,26 @@ class Debug { $type = strtolower( $type ); if ( 'delete' !== $type ) { - $url = object_to_uri( $data['actor'] ); + $actor = $data['actor'] ?? ''; + $url = object_to_uri( $actor ); // phpcs:ignore WordPress.PHP.DevelopmentFunctions \error_log( "[INBOX] Request From: {$url} with Activity: " . \print_r( $data, true ) ); } } + /** + * Log the sent to follower action. + * + * @param array $result The result of the remote post request. + * @param string $inbox The inbox URL. + */ + public static function log_sent_to_inbox( $result, $inbox ) { + if ( \is_wp_error( $result ) ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions + \error_log( "[DISPATCHER] Failed Request to: {$inbox} with Result: " . \print_r( $result, true ) ); + } + } + /** * Write a log entry. * diff --git a/wp-content/plugins/activitypub/includes/class-dispatcher.php b/wp-content/plugins/activitypub/includes/class-dispatcher.php new file mode 100644 index 00000000..b76f00d1 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-dispatcher.php @@ -0,0 +1,466 @@ +get__id(), $outbox_item ); + + if ( self::should_send_to_followers( $activity, $actor, $outbox_item ) ) { + Scheduler::async_batch( + self::$callback, + $outbox_item->ID, + self::$batch_size, + \get_post_meta( $outbox_item->ID, '_activitypub_outbox_offset', true ) ?: 0 // phpcs:ignore + ); + } else { + // No followers to process for this update. We're done. + \wp_publish_post( $outbox_item ); + \delete_post_meta( $outbox_item->ID, '_activitypub_outbox_offset' ); + } + } + + /** + * Asynchronously runs batch processing routines. + * + * @param int $outbox_item_id The Outbox item ID. + * @param int $batch_size Optional. The batch size. Default ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE. + * @param int $offset Optional. The offset. Default 0. + * + * @return array|void The next batch of followers to process, or void if done. + */ + public static function send_to_followers( $outbox_item_id, $batch_size = ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE, $offset = 0 ) { + $json = Outbox::get_activity( $outbox_item_id )->to_json(); + $actor = Outbox::get_actor( \get_post( $outbox_item_id ) ); + $inboxes = Followers::get_inboxes_for_activity( $json, $actor->get__id(), $batch_size, $offset ); + + $retries = self::send_to_inboxes( $inboxes, $outbox_item_id ); + + // Retry failed inboxes. + if ( ! empty( $retries ) ) { + self::schedule_retry( $retries, $outbox_item_id ); + } + + if ( is_countable( $inboxes ) && count( $inboxes ) < $batch_size ) { + \delete_post_meta( $outbox_item_id, '_activitypub_outbox_offset' ); + + /** + * Fires when the followers are complete. + * + * @param array $inboxes The inboxes. + * @param string $json The ActivityPub Activity JSON + * @param int $actor_id The actor ID. + * @param int $outbox_item_id The Outbox item ID. + * @param int $batch_size The batch size. + * @param int $offset The offset. + */ + \do_action( 'activitypub_outbox_processing_complete', $inboxes, $json, $actor->get__id(), $outbox_item_id, $batch_size, $offset ); + + // No more followers to process for this update. + \wp_publish_post( $outbox_item_id ); + } else { + \update_post_meta( $outbox_item_id, '_activitypub_outbox_offset', $offset + $batch_size ); + + /** + * Fires when the batch of followers is complete. + * + * @param array $inboxes The inboxes. + * @param string $json The ActivityPub Activity JSON + * @param int $actor_id The actor ID. + * @param int $outbox_item_id The Outbox item ID. + * @param int $batch_size The batch size. + * @param int $offset The offset. + */ + \do_action( 'activitypub_outbox_processing_batch_complete', $inboxes, $json, $actor->get__id(), $outbox_item_id, $batch_size, $offset ); + + return array( $outbox_item_id, $batch_size, $offset + $batch_size ); + } + } + + /** + * Retry sending to followers. + * + * @param string $transient_key The key to retrieve retry inboxes. + * @param int $outbox_item_id The Outbox item ID. + * @param int $attempt The attempt number. + */ + public static function retry_send_to_followers( $transient_key, $outbox_item_id, $attempt = 1 ) { + $inboxes = \get_transient( $transient_key ); + if ( false === $inboxes ) { + return; + } + + // Delete the transient as we no longer need it. + \delete_transient( $transient_key ); + + $retries = self::send_to_inboxes( $inboxes, $outbox_item_id ); + + // Retry failed inboxes. + if ( ++$attempt < 3 && ! empty( $retries ) ) { + self::schedule_retry( $retries, $outbox_item_id, $attempt ); + } + } + + /** + * Send to inboxes. + * + * @param array $inboxes The inboxes to notify. + * @param int $outbox_item_id The Outbox item ID. + * @return array The failed inboxes. + */ + private static function send_to_inboxes( $inboxes, $outbox_item_id ) { + $json = Outbox::get_activity( $outbox_item_id )->to_json(); + $actor = Outbox::get_actor( \get_post( $outbox_item_id ) ); + $retries = array(); + + /** + * Fires before sending an Activity to inboxes. + * + * @param string $json The ActivityPub Activity JSON. + * @param array $inboxes The inboxes to send to. + * @param int $outbox_item_id The Outbox item ID. + */ + \do_action( 'activitypub_pre_send_to_inboxes', $json, $inboxes, $outbox_item_id ); + + foreach ( $inboxes as $inbox ) { + $result = safe_remote_post( $inbox, $json, $actor->get__id() ); + + if ( is_wp_error( $result ) && in_array( $result->get_error_code(), self::$retry_error_codes, true ) ) { + $retries[] = $inbox; + } + + /** + * Fires after an Activity has been sent to an inbox. + * + * @param array $result The result of the remote post request. + * @param string $inbox The inbox URL. + * @param string $json The ActivityPub Activity JSON. + * @param int $actor_id The actor ID. + * @param int $outbox_item_id The Outbox item ID. + */ + \do_action( 'activitypub_sent_to_inbox', $result, $inbox, $json, $actor->get__id(), $outbox_item_id ); + } + + return $retries; + } + + /** + * Schedule a retry. + * + * @param array $retries The inboxes to retry. + * @param int $outbox_item_id The Outbox item ID. + * @param int $attempt Optional. The attempt number. Default 1. + */ + private static function schedule_retry( $retries, $outbox_item_id, $attempt = 1 ) { + $transient_key = 'activitypub_retry_' . \wp_generate_password( 12, false ); + \set_transient( $transient_key, $retries, WEEK_IN_SECONDS ); + + \wp_schedule_single_event( + \time() + ( $attempt * $attempt * HOUR_IN_SECONDS ), + 'activitypub_async_batch', + array( + array( self::class, 'retry_send_to_followers' ), + $transient_key, + $outbox_item_id, + $attempt, + ) + ); + } + + /** + * Send an Activity to a custom list of inboxes, like mentioned users or replied-to posts. + * + * For all custom implementations, please use the `activitypub_additional_inboxes` filter. + * + * @param Activity $activity The ActivityPub Activity. + * @param int $actor_id The actor ID. + * @param \WP_Post $outbox_item The WordPress object. + */ + private static function send_to_additional_inboxes( $activity, $actor_id, $outbox_item = null ) { + /** + * Filters the list of inboxes to send the Activity to. + * + * @param array $inboxes The list of inboxes to send to. + * @param int $actor_id The actor ID. + * @param Activity $activity The ActivityPub Activity. + */ + $inboxes = apply_filters( 'activitypub_additional_inboxes', array(), $actor_id, $activity ); + $inboxes = array_unique( $inboxes ); + + $retries = self::send_to_inboxes( $inboxes, $outbox_item->ID ); + + // Retry failed inboxes. + if ( ! empty( $retries ) ) { + self::schedule_retry( $retries, $outbox_item->ID ); + } + } + + /** + * Default filter to add Inboxes of Mentioned Actors + * + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. + * + * @return array The filtered Inboxes. + */ + public static function add_inboxes_by_mentioned_actors( $inboxes, $actor_id, $activity ) { + $cc = $activity->get_cc() ?? array(); + $to = $activity->get_to() ?? array(); + + $audience = array_merge( $cc, $to ); + + // Remove "public placeholder" and "same domain" from the audience. + $audience = array_filter( + $audience, + function ( $actor ) { + return 'https://www.w3.org/ns/activitystreams#Public' !== $actor && ! is_same_domain( $actor ); + } + ); + + if ( $audience ) { + $mentioned_inboxes = Mention::get_inboxes( $audience ); + + return array_merge( $inboxes, $mentioned_inboxes ); + } + + return $inboxes; + } + + /** + * Default filter to add Inboxes of Posts that are set as `in-reply-to` + * + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. + * + * @return array The filtered Inboxes + */ + public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activity ) { + $in_reply_to = $activity->get_in_reply_to(); + + if ( ! $in_reply_to ) { + return $inboxes; + } + + if ( ! is_array( $in_reply_to ) ) { + $in_reply_to = array( $in_reply_to ); + } + + foreach ( $in_reply_to as $url ) { + // No need to self-notify. + if ( is_same_domain( $url ) ) { + continue; + } + + $object = Http::get_remote_object( $url ); + + if ( + ! $object || + \is_wp_error( $object ) || + empty( $object['attributedTo'] ) + ) { + continue; + } + + $actor = object_to_uri( $object['attributedTo'] ); + $actor = Http::get_remote_object( $actor ); + + if ( ! $actor || \is_wp_error( $actor ) ) { + continue; + } + + if ( ! empty( $actor['endpoints']['sharedInbox'] ) ) { + $inboxes[] = $actor['endpoints']['sharedInbox']; + } elseif ( ! empty( $actor['inbox'] ) ) { + $inboxes[] = $actor['inbox']; + } + } + + return $inboxes; + } + + /** + * Adds Blog Actor inboxes to Updates so the Blog User's followers are notified of edits. + * + * @deprecated 5.2.0 Use {@see Followers::maybe_add_inboxes_of_blog_user} instead. + * + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The WordPress Actor-ID. + * @param Activity $activity The ActivityPub Activity. + * + * @return array The filtered Inboxes. + */ + public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $activity ) { // phpcs:ignore + _deprecated_function( __METHOD__, '5.2.0', 'Followers::maybe_add_inboxes_of_blog_user' ); + + return $inboxes; + } + + /** + * Check if passed Activity is public. + * + * @param Activity $activity The Activity object. + * @param \Activitypub\Model\User|\Activitypub\Model\Blog $actor The Actor object. + * @param \WP_Post $outbox_item The Outbox item. + * + * @return boolean True if public, false if not. + */ + protected static function should_send_to_followers( $activity, $actor, $outbox_item ) { + // Check if follower endpoint is set. + $cc = $activity->get_cc() ?? array(); + $to = $activity->get_to() ?? array(); + + $audience = array_merge( $cc, $to ); + + $send = ( + // Check if activity is public. + in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) || + // ...or check if follower endpoint is set. + in_array( $actor->get_followers(), $audience, true ) + ); + + if ( $send ) { + $followers = Followers::get_inboxes_for_activity( $activity->to_json(), $actor->get__id() ); + + // Only send if there are followers to send to. + $send = ! is_countable( $followers ) || 0 < count( $followers ); + } + + /** + * Filters whether to send an Activity to followers. + * + * @param bool $send_activity_to_followers Whether to send the Activity to followers. + * @param Activity $activity The ActivityPub Activity. + * @param int $actor_id The actor ID. + * @param \WP_Post $outbox_item The WordPress object. + */ + return apply_filters( 'activitypub_send_activity_to_followers', $send, $activity, $actor->get__id(), $outbox_item ); + } + + /** + * Add Inboxes of Relays. + * + * @param array $inboxes The list of Inboxes. + * @param int $actor_id The Actor-ID. + * @param Activity $activity The ActivityPub Activity. + * + * @return array The filtered Inboxes. + */ + public static function add_inboxes_of_relays( $inboxes, $actor_id, $activity ) { + // Check if follower endpoint is set. + $cc = $activity->get_cc() ?? array(); + $to = $activity->get_to() ?? array(); + + $audience = array_merge( $cc, $to ); + + // Check if activity is public. + if ( ! in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) ) { + return $inboxes; + } + + $relays = \get_option( 'activitypub_relays', array() ); + + if ( empty( $relays ) ) { + return $inboxes; + } + + return array_merge( $inboxes, $relays ); + } +} diff --git a/wp-content/plugins/activitypub/includes/class-embed.php b/wp-content/plugins/activitypub/includes/class-embed.php new file mode 100644 index 00000000..978e66f4 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-embed.php @@ -0,0 +1,263 @@ + $author_name, + 'author_url' => $author_url, + 'avatar_url' => $avatar_url, + 'published' => $published, + 'title' => $title, + 'content' => $content, + 'image' => $image, + 'boosts' => $boosts, + 'favorites' => $favorites, + 'url' => $activity_object['id'], + 'webfinger' => $author['webfinger'], + ) + ); + + if ( $inline_css ) { + // Grab the CSS. + $css = \file_get_contents( ACTIVITYPUB_PLUGIN_DIR . 'assets/css/activitypub-embed.css' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents + // We embed CSS directly because this may be in an iframe. + printf( '', $css ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + + // A little light whitespace cleanup. + return preg_replace( '/\s+/', ' ', ob_get_clean() ); + } + + /** + * Check if a real oEmbed result exists for the given URL. + * + * @param string $url The URL to check. + * @param array $args Additional arguments passed to wp_oembed_get(). + * @return bool True if a real oEmbed result exists, false otherwise. + */ + public static function has_real_oembed( $url, $args = array() ) { + // Temporarily remove our filter to avoid infinite loops. + \remove_filter( 'pre_oembed_result', array( self::class, 'maybe_use_activitypub_embed' ), 10, 3 ); + + // Try to get a "real" oEmbed result. If found, it'll be cached to avoid unnecessary HTTP requests in `wp_oembed_get`. + $oembed_result = \wp_oembed_get( $url, $args ); + + // Add our filter back. + \add_filter( 'pre_oembed_result', array( self::class, 'maybe_use_activitypub_embed' ), 10, 3 ); + + return false !== $oembed_result; + } + + /** + * Filter the oembed result to handle ActivityPub content when no oEmbed is found. + * Implementation is a bit weird because there's no way to filter on a false result, we have to use `pre_oembed_result`. + * + * @param null|string $result The UNSANITIZED (and potentially unsafe) HTML that should be used to embed. + * @param string $url The URL to the content that should be attempted to be embedded. + * @param array $args Additional arguments passed to wp_oembed_get(). + * @return null|string Return null to allow normal oEmbed processing, or string for ActivityPub embed. + */ + public static function maybe_use_activitypub_embed( $result, $url, $args ) { + // If we already have a result, return it. + if ( null !== $result ) { + return $result; + } + + // If we found a real oEmbed, return null to allow normal processing. + if ( self::has_real_oembed( $url, $args ) ) { + return null; + } + + // No oEmbed found, try to get ActivityPub representation. + $html = get_embed_html( $url ); + + // If we couldn't get an ActivityPub embed either, return null to allow normal processing. + if ( ! $html ) { + return null; + } + + // Return the ActivityPub embed HTML. + return $html; + } + + /** + * Handle cases where WordPress has filtered out the oEmbed result for security reasons, + * but we can provide a safe ActivityPub-specific markup. + * + * This runs after wp_filter_oembed_result has potentially nullified the result. + * + * @param string|false $html The returned oEmbed HTML. + * @param object $data A data object result from an oEmbed provider. + * @param string $url The URL of the content to be embedded. + * @return string|false The filtered oEmbed HTML or our ActivityPub embed. + */ + public static function handle_filtered_oembed_result( $html, $data, $url ) { + // If we already have valid HTML, return it. + if ( $html ) { + return $html; + } + + // If this isn't a rich or video type, we can't help. + if ( ! isset( $data->type ) || ! \in_array( $data->type, array( 'rich', 'video' ), true ) ) { + return $html; + } + + // If there's no HTML in the data, we can't help. + if ( empty( $data->html ) || ! \is_string( $data->html ) ) { + return $html; + } + + // Try to get ActivityPub representation. + $activitypub_html = get_embed_html( $url ); + if ( ! $activitypub_html ) { + return $html; + } + + // Return our safer ActivityPub embed HTML. + return $activitypub_html; + } + + /** + * Register the fallback hook for oEmbed requests. + * + * Avoids filtering every single API request. + * + * @param int $post_id The post ID. + * @return int The post ID. + */ + public static function register_fallback_hook( $post_id ) { + \add_filter( 'rest_request_after_callbacks', array( self::class, 'oembed_fediverse_fallback' ), 10, 3 ); + + return $post_id; + } + + /** + * Fallback for oEmbed requests to the Fediverse. + * + * @param \WP_REST_Response|\WP_Error $response Result to send to the client. + * @param array $handler Route handler used for the request. + * @param \WP_REST_Request $request Request used to generate the response. + * + * @return \WP_REST_Response|\WP_Error The response to send to the client. + */ + public static function oembed_fediverse_fallback( $response, $handler, $request ) { + if ( is_wp_error( $response ) && 'oembed_invalid_url' === $response->get_error_code() ) { + $url = $request->get_param( 'url' ); + $html = get_embed_html( $url ); + + if ( $html ) { + $args = $request->get_params(); + $data = (object) array( + 'provider_name' => 'Embed Handler', + 'html' => $html, + 'scripts' => array(), + ); + + /** This filter is documented in wp-includes/class-wp-oembed.php */ + $data->html = apply_filters( 'oembed_result', $data->html, $url, $args ); + + /** This filter is documented in wp-includes/class-wp-oembed-controller.php */ + $ttl = apply_filters( 'rest_oembed_ttl', DAY_IN_SECONDS, $url, $args ); + + set_transient( 'oembed_' . md5( serialize( $args ) ), $data, $ttl ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + + $response = new \WP_REST_Response( $data ); + } + } + + return $response; + } +} diff --git a/wp-content/plugins/activitypub/includes/class-handler.php b/wp-content/plugins/activitypub/includes/class-handler.php index a62d7d0e..b0ead9b4 100644 --- a/wp-content/plugins/activitypub/includes/class-handler.php +++ b/wp-content/plugins/activitypub/includes/class-handler.php @@ -12,6 +12,7 @@ use Activitypub\Handler\Create; use Activitypub\Handler\Delete; use Activitypub\Handler\Follow; use Activitypub\Handler\Like; +use Activitypub\Handler\Move; use Activitypub\Handler\Undo; use Activitypub\Handler\Update; @@ -36,10 +37,8 @@ class Handler { Follow::init(); Undo::init(); Update::init(); - - if ( ! ACTIVITYPUB_DISABLE_REACTIONS ) { - Like::init(); - } + Like::init(); + Move::init(); /** * Register additional handlers. diff --git a/wp-content/plugins/activitypub/includes/class-hashtag.php b/wp-content/plugins/activitypub/includes/class-hashtag.php index db62f436..8012f1df 100644 --- a/wp-content/plugins/activitypub/includes/class-hashtag.php +++ b/wp-content/plugins/activitypub/includes/class-hashtag.php @@ -33,11 +33,11 @@ class Hashtag { */ public static function filter_activity_object( $activity ) { /* phpcs:ignore Squiz.PHP.CommentedOutCode.Found - Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629 - if ( ! empty( $activity['summary'] ) ) { + Only changed it for Person and Group as long is not merged: https://github.com/mastodon/mastodon/pull/28629 + */ + if ( ! empty( $activity['summary'] ) && in_array( $activity['type'], array( 'Person', 'Group' ), true ) ) { $activity['summary'] = self::the_content( $activity['summary'] ); } - */ if ( ! empty( $activity['content'] ) ) { $activity['content'] = self::the_content( $activity['content'] ); @@ -53,19 +53,27 @@ class Hashtag { * @param \WP_Post $post Post object. */ public static function insert_post( $post_id, $post ) { + // Check if the post supports ActivityPub. + if ( ! \post_type_supports( \get_post_type( $post ), 'activitypub' ) ) { + return; + } + + // Check if the (custom) post supports tags. + $taxonomies = \get_object_taxonomies( $post ); + if ( ! in_array( 'post_tag', $taxonomies, true ) ) { + return; + } + $tags = array(); - if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $post->post_content, $match ) ) { - $tags = array_merge( $tags, $match[1] ); + // Skip hashtags in HTML attributes, like hex colors. + $content = wp_strip_all_tags( $post->post_content . "\n" . $post->post_excerpt ); + + if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $content, $match ) ) { + $tags = array_unique( $match[1] ); } - if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $post->post_excerpt, $match ) ) { - $tags = array_merge( $tags, $match[1] ); - } - - $tags = \implode( ', ', $tags ); - - \wp_add_post_tags( $post->ID, $tags ); + \wp_add_post_tags( $post->ID, \implode( ', ', $tags ) ); } /** diff --git a/wp-content/plugins/activitypub/includes/class-health-check.php b/wp-content/plugins/activitypub/includes/class-health-check.php deleted file mode 100644 index 65e6a9e4..00000000 --- a/wp-content/plugins/activitypub/includes/class-health-check.php +++ /dev/null @@ -1,374 +0,0 @@ - \__( 'Author URL test', 'activitypub' ), - 'test' => array( self::class, 'test_author_url' ), - ); - } - - $tests['direct']['activitypub_test_webfinger'] = array( - 'label' => __( 'WebFinger Test', 'activitypub' ), - 'test' => array( self::class, 'test_webfinger' ), - ); - - return $tests; - } - - /** - * Author URL tests. - * - * @return array - */ - public static function test_author_url() { - $result = array( - 'label' => \__( 'Author URL accessible', 'activitypub' ), - 'status' => 'good', - 'badge' => array( - 'label' => \__( 'ActivityPub', 'activitypub' ), - 'color' => 'green', - ), - 'description' => \sprintf( - '

%s

', - \__( 'Your author URL is accessible and supports the required "Accept" header.', 'activitypub' ) - ), - 'actions' => '', - 'test' => 'test_author_url', - ); - - $check = self::is_author_url_accessible(); - - if ( true === $check ) { - return $result; - } - - $result['status'] = 'critical'; - $result['label'] = \__( 'Author URL is not accessible', 'activitypub' ); - $result['badge']['color'] = 'red'; - $result['description'] = \sprintf( - '

%s

', - $check->get_error_message() - ); - - return $result; - } - - /** - * System Cron tests. - * - * @return array - */ - public static function test_system_cron() { - $result = array( - 'label' => \__( 'System Task Scheduler configured', 'activitypub' ), - 'status' => 'good', - 'badge' => array( - 'label' => \__( 'ActivityPub', 'activitypub' ), - 'color' => 'green', - ), - 'description' => \sprintf( - '

%s

', - \esc_html__( 'You seem to use the System Task Scheduler to process WP_Cron tasks.', 'activitypub' ) - ), - 'actions' => '', - 'test' => 'test_system_cron', - ); - - if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) { - return $result; - } - - $result['status'] = 'recommended'; - $result['label'] = \__( 'System Task Scheduler not configured', 'activitypub' ); - $result['badge']['color'] = 'orange'; - $result['description'] = \sprintf( - '

%s

', - \__( 'Enhance your WordPress site’s performance and mitigate potential heavy loads caused by plugins like ActivityPub by setting up a system cron job to run WP Cron. This ensures scheduled tasks are executed consistently and reduces the reliance on website traffic for trigger events.', 'activitypub' ) - ); - $result['actions'] .= sprintf( - '

%s %s

', - esc_url( __( 'https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/', 'activitypub' ) ), - __( 'Learn how to hook the WP-Cron into the System Task Scheduler.', 'activitypub' ), - /* translators: Hidden accessibility text. */ - __( '(opens in a new tab)', 'activitypub' ) - ); - - return $result; - } - - /** - * WebFinger tests. - * - * @return array - */ - public static function test_webfinger() { - $result = array( - 'label' => \__( 'WebFinger endpoint', 'activitypub' ), - 'status' => 'good', - 'badge' => array( - 'label' => \__( 'ActivityPub', 'activitypub' ), - 'color' => 'green', - ), - 'description' => \sprintf( - '

%s

', - \__( 'Your WebFinger endpoint is accessible and returns the correct information.', 'activitypub' ) - ), - 'actions' => '', - 'test' => 'test_webfinger', - ); - - $check = self::is_webfinger_endpoint_accessible(); - - if ( true === $check ) { - return $result; - } - - $result['status'] = 'critical'; - $result['label'] = \__( 'WebFinger endpoint is not accessible', 'activitypub' ); - $result['badge']['color'] = 'red'; - $result['description'] = \sprintf( - '

%s

', - $check->get_error_message() - ); - - return $result; - } - - /** - * Check if `author_posts_url` is accessible and that request returns correct JSON. - * - * @return bool|WP_Error True if the author URL is accessible, WP_Error otherwise. - */ - public static function is_author_url_accessible() { - $user = \wp_get_current_user(); - $author_url = \get_author_posts_url( $user->ID ); - $reference_author_url = self::get_author_posts_url( $user->ID, $user->user_nicename ); - - // Check for "author" in URL. - if ( $author_url !== $reference_author_url ) { - return new WP_Error( - 'author_url_not_accessible', - \sprintf( - // translators: %s: Author URL. - \__( - 'Your author URL %s was replaced, this is often done by plugins.', - 'activitypub' - ), - $author_url - ) - ); - } - - // Try to access author URL. - $response = \wp_remote_get( - $author_url, - array( - 'headers' => array( 'Accept' => 'application/activity+json' ), - 'redirection' => 0, - ) - ); - - if ( \is_wp_error( $response ) ) { - return new WP_Error( - 'author_url_not_accessible', - \sprintf( - // translators: %s: Author URL. - \__( - 'Your author URL %s is not accessible. Please check your WordPress setup or permalink structure. If the setup seems fine, maybe check if a plugin might restrict the access.', - 'activitypub' - ), - $author_url - ) - ); - } - - $response_code = \wp_remote_retrieve_response_code( $response ); - - // Check for redirects. - if ( \in_array( $response_code, array( 301, 302, 307, 308 ), true ) ) { - return new WP_Error( - 'author_url_not_accessible', - \sprintf( - // translators: %s: Author URL. - \__( - 'Your author URL %s is redirecting to another page, this is often done by SEO plugins like "Yoast SEO".', - 'activitypub' - ), - $author_url - ) - ); - } - - // Check if response is JSON. - $body = \wp_remote_retrieve_body( $response ); - - if ( ! \is_string( $body ) || ! \is_array( \json_decode( $body, true ) ) ) { - return new WP_Error( - 'author_url_not_accessible', - \sprintf( - // translators: %s: Author URL. - \__( - 'Your author URL %s does not return valid JSON for application/activity+json. Please check if your hosting supports alternate Accept headers.', - 'activitypub' - ), - $author_url - ) - ); - } - - return true; - } - - /** - * Check if WebFinger endpoint is accessible and profile request returns correct JSON - * - * @return boolean|WP_Error - */ - public static function is_webfinger_endpoint_accessible() { - $user = Users::get_by_id( Users::APPLICATION_USER_ID ); - $resource = $user->get_webfinger(); - - $url = Webfinger::resolve( $resource ); - if ( \is_wp_error( $url ) ) { - $allowed = array( 'code' => array() ); - - $not_accessible = wp_kses( - // translators: %s: Author URL. - \__( - 'Your WebFinger endpoint %s is not accessible. Please check your WordPress setup or permalink structure.', - 'activitypub' - ), - $allowed - ); - $invalid_response = wp_kses( - // translators: %s: Author URL. - \__( - 'Your WebFinger endpoint %s does not return valid JSON for application/jrd+json.', - 'activitypub' - ), - $allowed - ); - - $health_messages = array( - 'webfinger_url_not_accessible' => \sprintf( - $not_accessible, - $url->get_error_data()['data'] - ), - 'webfinger_url_invalid_response' => \sprintf( - // translators: %s: Author URL. - $invalid_response, - $url->get_error_data()['data'] - ), - ); - $message = null; - if ( isset( $health_messages[ $url->get_error_code() ] ) ) { - $message = $health_messages[ $url->get_error_code() ]; - } - - return new WP_Error( - $url->get_error_code(), - $message, - $url->get_error_data() - ); - } - - return true; - } - - /** - * Retrieve the URL to the author page for the user with the ID provided. - * - * @global \WP_Rewrite $wp_rewrite WordPress rewrite component. - * - * @param int $author_id Author ID. - * @param string $author_nicename Optional. The author's nicename (slug). Default empty. - * - * @return string The URL to the author's page. - */ - public static function get_author_posts_url( $author_id, $author_nicename = '' ) { - global $wp_rewrite; - - $auth_id = (int) $author_id; - $link = $wp_rewrite->get_author_permastruct(); - - if ( empty( $link ) ) { - $file = home_url( '/' ); - $link = $file . '?author=' . $auth_id; - } else { - if ( '' === $author_nicename ) { - $user = get_userdata( $author_id ); - if ( ! empty( $user->user_nicename ) ) { - $author_nicename = $user->user_nicename; - } - } - $link = str_replace( '%author%', $author_nicename, $link ); - $link = home_url( user_trailingslashit( $link ) ); - } - - return $link; - } - - /** - * Static function for generating site debug data when required. - * - * @param array $info The debug information to be added to the core information page. - * @return array The filtered information - */ - public static function debug_information( $info ) { - $info['activitypub'] = array( - 'label' => __( 'ActivityPub', 'activitypub' ), - 'fields' => array( - 'webfinger' => array( - 'label' => __( 'WebFinger Resource', 'activitypub' ), - 'value' => Webfinger::get_user_resource( wp_get_current_user()->ID ), - 'private' => true, - ), - 'author_url' => array( - 'label' => __( 'Author URL', 'activitypub' ), - 'value' => get_author_posts_url( wp_get_current_user()->ID ), - 'private' => true, - ), - 'plugin_version' => array( - 'label' => __( 'Plugin Version', 'activitypub' ), - 'value' => get_plugin_version(), - 'private' => true, - ), - ), - ); - - return $info; - } -} diff --git a/wp-content/plugins/activitypub/includes/class-http.php b/wp-content/plugins/activitypub/includes/class-http.php index 133a3469..9f9a8dd0 100644 --- a/wp-content/plugins/activitypub/includes/class-http.php +++ b/wp-content/plugins/activitypub/includes/class-http.php @@ -8,7 +8,7 @@ namespace Activitypub; use WP_Error; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; /** * ActivityPub HTTP Class @@ -26,6 +26,13 @@ class Http { * @return array|WP_Error The POST Response or an WP_Error. */ public static function post( $url, $body, $user_id ) { + /** + * Fires before an HTTP POST request is made. + * + * @param string $url The URL endpoint. + * @param string $body The POST body. + * @param int $user_id The WordPress User ID. + */ \do_action( 'activitypub_pre_http_post', $url, $body, $user_id ); $date = \gmdate( 'D, d M Y H:i:s T' ); @@ -35,7 +42,7 @@ class Http { $wp_version = get_masked_wp_version(); /** - * Filter the HTTP headers user agent. + * Filters the HTTP headers user agent string. * * @param string $user_agent The user agent string. */ @@ -59,7 +66,14 @@ class Http { $code = \wp_remote_retrieve_response_code( $response ); if ( $code >= 400 ) { - $response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) ); + $response = new WP_Error( + $code, + __( 'Failed HTTP Request', 'activitypub' ), + array( + 'status' => $code, + 'response' => $response, + ) + ); } /** @@ -84,6 +98,11 @@ class Http { * @return array|WP_Error The GET Response or a WP_Error. */ public static function get( $url, $cached = false ) { + /** + * Fires before an HTTP GET request is made. + * + * @param string $url The URL endpoint. + */ \do_action( 'activitypub_pre_http_get', $url ); if ( $cached ) { @@ -105,19 +124,29 @@ class Http { } $date = \gmdate( 'D, d M Y H:i:s T' ); - $signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date ); + $signature = Signature::generate_signature( Actors::APPLICATION_USER_ID, 'get', $url, $date ); $wp_version = get_masked_wp_version(); /** - * Filter the HTTP headers user agent. + * Filters the HTTP headers user agent string. + * + * This filter allows developers to modify the user agent string that is + * sent with HTTP requests. * * @param string $user_agent The user agent string. */ $user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) ); + /** + * Filters the timeout duration for remote GET requests in ActivityPub. + * + * @param int $timeout The timeout value in seconds. Default 100 seconds. + */ + $timeout = \apply_filters( 'activitypub_remote_get_timeout', 100 ); + $args = array( - 'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ), + 'timeout' => $timeout, 'limit_response_size' => 1048576, 'redirection' => 3, 'user-agent' => "$user_agent; ActivityPub", @@ -164,19 +193,25 @@ class Http { */ public static function is_tombstone( $url ) { /** - * Action before checking if the URL is a tombstone. + * Fires before checking if the URL is a tombstone. * * @param string $url The URL to check. */ \do_action( 'activitypub_pre_http_is_tombstone', $url ); - $response = \wp_safe_remote_get( $url ); + $response = \wp_safe_remote_get( $url, array( 'headers' => array( 'Accept' => 'application/activity+json' ) ) ); $code = \wp_remote_retrieve_response_code( $response ); if ( in_array( (int) $code, array( 404, 410 ), true ) ) { return true; } + $data = \wp_remote_retrieve_body( $response ); + $data = \json_decode( $data, true ); + if ( $data && isset( $data['type'] ) && 'Tombstone' === $data['type'] ) { + return true; + } + return false; } @@ -200,24 +235,7 @@ class Http { * @return array|WP_Error The Object data as array or WP_Error on failure. */ public static function get_remote_object( $url_or_object, $cached = true ) { - if ( is_array( $url_or_object ) ) { - if ( array_key_exists( 'id', $url_or_object ) ) { - $url = $url_or_object['id']; - } elseif ( array_key_exists( 'url', $url_or_object ) ) { - $url = $url_or_object['url']; - } else { - return new WP_Error( - 'activitypub_no_valid_actor_identifier', - \__( 'The "actor" identifier is not valid', 'activitypub' ), - array( - 'status' => 404, - 'object' => $url_or_object, - ) - ); - } - } else { - $url = $url_or_object; - } + $url = object_to_uri( $url_or_object ); if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $url ) ) { $url = Webfinger::resolve( $url ); diff --git a/wp-content/plugins/activitypub/includes/class-link.php b/wp-content/plugins/activitypub/includes/class-link.php index 3c53f3bb..783f1fec 100644 --- a/wp-content/plugins/activitypub/includes/class-link.php +++ b/wp-content/plugins/activitypub/includes/class-link.php @@ -29,11 +29,11 @@ class Link { */ public static function filter_activity_object( $activity ) { /* phpcs:ignore Squiz.PHP.CommentedOutCode.Found - Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629 - if ( ! empty( $activity['summary'] ) ) { + Only changed it for Person and Group as long is not merged: https://github.com/mastodon/mastodon/pull/28629 + */ + if ( ! empty( $activity['summary'] ) && in_array( $activity['type'], array( 'Person', 'Group' ), true ) ) { $activity['summary'] = self::the_content( $activity['summary'] ); } - */ if ( ! empty( $activity['content'] ) ) { $activity['content'] = self::the_content( $activity['content'] ); @@ -112,6 +112,11 @@ class Link { $display_class .= 'ellipsis'; } + /** + * Filters the rel attribute for ActivityPub links. + * + * @param string $rel The rel attribute string. Default 'nofollow noopener noreferrer'. + */ $rel = apply_filters( 'activitypub_link_rel', 'nofollow noopener noreferrer' ); return \sprintf( diff --git a/wp-content/plugins/activitypub/includes/class-mailer.php b/wp-content/plugins/activitypub/includes/class-mailer.php new file mode 100644 index 00000000..095d8396 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-mailer.php @@ -0,0 +1,337 @@ +comment_ID, 'protocol', true ); + + if ( 'activitypub' !== $type ) { + return $subject; + } + + $singular = Comment::get_comment_type_attr( $comment->comment_type, 'singular' ); + + if ( ! $singular ) { + return $subject; + } + + $post = \get_post( $comment->comment_post_ID ); + + /* translators: 1: Blog name, 2: Like or Repost, 3: Post title */ + return \sprintf( \esc_html__( '[%1$s] %2$s: %3$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $singular ), \esc_html( $post->post_title ) ); + } + + /** + * Filter the notification text for Like and Announce notifications. + * + * @param string $message The default notification text. + * @param int|string $comment_id The comment ID. + * + * @return string The filtered notification text. + */ + public static function comment_notification_text( $message, $comment_id ) { + $comment = \get_comment( $comment_id ); + + if ( ! $comment ) { + return $message; + } + + $type = \get_comment_meta( $comment->comment_ID, 'protocol', true ); + + if ( 'activitypub' !== $type ) { + return $message; + } + + $comment_type = Comment::get_comment_type( $comment->comment_type ); + + if ( ! $comment_type ) { + return $message; + } + + $post = \get_post( $comment->comment_post_ID ); + $comment_author_domain = \gethostbyaddr( $comment->comment_author_IP ); + + /* translators: 1: Comment type, 2: Post title */ + $notify_message = \sprintf( html_entity_decode( esc_html__( 'New %1$s on your post “%2$s”.', 'activitypub' ) ), \esc_html( $comment_type['singular'] ), \esc_html( $post->post_title ) ) . "\r\n\r\n"; + /* translators: 1: Website name, 2: Website IP address, 3: Website hostname. */ + $notify_message .= \sprintf( \esc_html__( 'From: %1$s (IP address: %2$s, %3$s)', 'activitypub' ), \esc_html( $comment->comment_author ), \esc_html( $comment->comment_author_IP ), \esc_html( $comment_author_domain ) ) . "\r\n"; + /* translators: Reaction author URL. */ + $notify_message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $comment->comment_author_url ) ) . "\r\n\r\n"; + /* translators: Comment type label */ + $notify_message .= \sprintf( \esc_html__( 'You can see all %s on this post here:', 'activitypub' ), \esc_html( $comment_type['label'] ) ) . "\r\n"; + $notify_message .= \get_permalink( $comment->comment_post_ID ) . '#' . \esc_attr( $comment_type['type'] ) . "\r\n\r\n"; + + return $notify_message; + } + + /** + * Send a notification email for every new follower. + * + * @param array $activity The activity object. + * @param int $user_id The id of the local blog-user. + */ + public static function new_follower( $activity, $user_id ) { + if ( $user_id > Actors::BLOG_USER_ID ) { + if ( ! \get_user_option( 'activitypub_mailer_new_follower', $user_id ) ) { + return; + } + + $email = \get_userdata( $user_id )->user_email; + $admin_url = '/users.php?page=activitypub-followers-list'; + } else { + if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_follower', '1' ) ) { + return; + } + + $email = \get_option( 'admin_email' ); + $admin_url = '/options-general.php?page=activitypub&tab=followers'; + } + + $actor = get_remote_metadata_by_actor( $activity['actor'] ); + if ( ! $actor || \is_wp_error( $actor ) ) { + return; + } + + if ( empty( $actor['webfinger'] ) ) { + $actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST ); + } + + $template_args = array_merge( + $actor, + array( + 'admin_url' => $admin_url, + 'user_id' => $user_id, + 'stats' => array( + 'outbox' => null, + 'followers' => null, + 'following' => null, + ), + ) + ); + + foreach ( $template_args['stats'] as $field => $value ) { + if ( empty( $actor[ $field ] ) ) { + continue; + } + + $result = Http::get( $actor[ $field ], true ); + if ( 200 === \wp_remote_retrieve_response_code( $result ) ) { + $body = \json_decode( \wp_remote_retrieve_body( $result ), true ); + if ( isset( $body['totalItems'] ) ) { + $template_args['stats'][ $field ] = $body['totalItems']; + } + } + } + + /* translators: 1: Blog name, 2: Follower name */ + $subject = \sprintf( \__( '[%1$s] New Follower: %2$s', 'activitypub' ), \get_option( 'blogname' ), $actor['name'] ); + + \ob_start(); + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-follower.php', false, $template_args ); + $html_message = \ob_get_clean(); + + $alt_function = function ( $mailer ) use ( $actor, $admin_url ) { + /* translators: 1: Follower name */ + $message = \sprintf( \__( 'New Follower: %1$s.', 'activitypub' ), $actor['name'] ) . "\r\n\r\n"; + /* translators: Follower URL */ + $message .= \sprintf( \__( 'URL: %s', 'activitypub' ), \esc_url( $actor['url'] ) ) . "\r\n\r\n"; + $message .= \__( 'You can see all followers here:', 'activitypub' ) . "\r\n"; + $message .= \esc_url( \admin_url( $admin_url ) ) . "\r\n\r\n"; + $mailer->{'AltBody'} = $message; + }; + \add_action( 'phpmailer_init', $alt_function ); + + \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) ); + + \remove_action( 'phpmailer_init', $alt_function ); + } + + /** + * Send a direct message. + * + * @param array $activity The activity object. + * @param int $user_id The id of the local blog-user. + */ + public static function direct_message( $activity, $user_id ) { + if ( + is_activity_public( $activity ) || + // Only accept messages that have the user in the "to" field. + empty( $activity['to'] ) || + ! in_array( Actors::get_by_id( $user_id )->get_id(), (array) $activity['to'], true ) + ) { + return; + } + + if ( $user_id > Actors::BLOG_USER_ID ) { + if ( ! \get_user_option( 'activitypub_mailer_new_dm', $user_id ) ) { + return; + } + + $email = \get_userdata( $user_id )->user_email; + } else { + if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_dm', '1' ) ) { + return; + } + + $email = \get_option( 'admin_email' ); + } + + $actor = get_remote_metadata_by_actor( $activity['actor'] ); + + if ( ! $actor || \is_wp_error( $actor ) || empty( $activity['object']['content'] ) ) { + return; + } + + if ( empty( $actor['webfinger'] ) ) { + $actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST ); + } + + $template_args = array( + 'activity' => $activity, + 'actor' => $actor, + 'user_id' => $user_id, + ); + + /* translators: 1: Blog name, 2 Actor name */ + $subject = \sprintf( \esc_html__( '[%1$s] Direct Message from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) ); + + \ob_start(); + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-dm.php', false, $template_args ); + $html_message = \ob_get_clean(); + + $alt_function = function ( $mailer ) use ( $actor, $activity ) { + $content = \html_entity_decode( + \wp_strip_all_tags( + str_replace( '

', PHP_EOL . PHP_EOL, $activity['object']['content'] ) + ), + ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401 + ); + + /* translators: Actor name */ + $message = \sprintf( \esc_html__( 'New Direct Message: %s', 'activitypub' ), $content ) . "\r\n\r\n"; + /* translators: Actor name */ + $message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n"; + /* translators: Message URL */ + $message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $activity['object']['id'] ) ) . "\r\n\r\n"; + + $mailer->{'AltBody'} = $message; + }; + \add_action( 'phpmailer_init', $alt_function ); + + \wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) ); + + \remove_action( 'phpmailer_init', $alt_function ); + } + + /** + * Send a mention notification. + * + * @param array $activity The activity object. + * @param int $user_id The id of the local blog-user. + */ + public static function mention( $activity, $user_id ) { + if ( + // Only accept messages that have the user in the "cc" field. + empty( $activity['cc'] ) || + ! in_array( Actors::get_by_id( $user_id )->get_id(), (array) $activity['cc'], true ) + ) { + return; + } + + if ( $user_id > Actors::BLOG_USER_ID ) { + if ( ! \get_user_option( 'activitypub_mailer_new_mention', $user_id ) ) { + return; + } + + $email = \get_userdata( $user_id )->user_email; + } else { + if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_mention', '1' ) ) { + return; + } + + $email = \get_option( 'admin_email' ); + } + + $actor = get_remote_metadata_by_actor( $activity['actor'] ); + if ( \is_wp_error( $actor ) ) { + return; + } + + if ( empty( $actor['webfinger'] ) ) { + $actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST ); + } + + $template_args = array( + 'activity' => $activity, + 'actor' => $actor, + 'user_id' => $user_id, + ); + + /* translators: 1: Blog name, 2 Actor name */ + $subject = \sprintf( \esc_html__( '[%1$s] Mention from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) ); + + \ob_start(); + \load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-mention.php', false, $template_args ); + $html_message = \ob_get_clean(); + + $alt_function = function ( $mailer ) use ( $actor, $activity ) { + $content = \html_entity_decode( + \wp_strip_all_tags( + str_replace( '

', 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 ); + } +} diff --git a/wp-content/plugins/activitypub/includes/class-mention.php b/wp-content/plugins/activitypub/includes/class-mention.php index 73f1010c..93ada0bf 100644 --- a/wp-content/plugins/activitypub/includes/class-mention.php +++ b/wp-content/plugins/activitypub/includes/class-mention.php @@ -82,7 +82,7 @@ class Mention { $url = isset( $metadata['url'] ) ? object_to_uri( $metadata['url'] ) : object_to_uri( $metadata['id'] ); - return \sprintf( '@%s', esc_url( $url ), esc_html( $username ) ); + return \sprintf( '@%2$s', esc_url( $url ), esc_html( $username ) ); } return $result[0]; diff --git a/wp-content/plugins/activitypub/includes/class-migration.php b/wp-content/plugins/activitypub/includes/class-migration.php index ca4353fd..9132f6b6 100644 --- a/wp-content/plugins/activitypub/includes/class-migration.php +++ b/wp-content/plugins/activitypub/includes/class-migration.php @@ -7,7 +7,11 @@ namespace Activitypub; +use Activitypub\Collection\Actors; +use Activitypub\Collection\Extra_Fields; use Activitypub\Collection\Followers; +use Activitypub\Collection\Outbox; +use Activitypub\Transformer\Factory; /** * ActivityPub Migration Class @@ -20,6 +24,8 @@ class Migration { */ public static function init() { \add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) ); + \add_action( 'activitypub_upgrade', array( self::class, 'async_upgrade' ), 10, 99 ); + \add_action( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ), 10, 2 ); self::maybe_migrate(); } @@ -30,10 +36,14 @@ class Migration { * This is the version that the database structure will be updated to. * It is the same as the plugin version. * + * @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly. + * * @return string The target version. */ public static function get_target_version() { - return get_plugin_version(); + _deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' ); + + return ACTIVITYPUB_PLUGIN_VERSION; } /** @@ -47,9 +57,20 @@ class Migration { /** * Locks the database migration process to prevent simultaneous migrations. + * + * @return bool|int True if the lock was successful, timestamp of existing lock otherwise. */ public static function lock() { - \update_option( 'activitypub_migration_lock', \time() ); + global $wpdb; + + // Try to lock. + $lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_migration_lock', \time() ) ); // phpcs:ignore WordPress.DB + + if ( ! $lock_result ) { + $lock_result = \get_option( 'activitypub_migration_lock' ); + } + + return $lock_result; } /** @@ -87,9 +108,9 @@ class Migration { * @return bool True if the database structure is up to date, false otherwise. */ public static function is_latest_version() { - return (bool) version_compare( + return (bool) \version_compare( self::get_version(), - self::get_target_version(), + ACTIVITYPUB_PLUGIN_VERSION, '==' ); } @@ -110,33 +131,91 @@ class Migration { $version_from_db = self::get_version(); - // Check for inital migration. + // Check for initial migration. if ( ! $version_from_db ) { self::add_default_settings(); - $version_from_db = self::get_target_version(); + $version_from_db = ACTIVITYPUB_PLUGIN_VERSION; } // Schedule the async migration. if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) { \wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) ); } - if ( version_compare( $version_from_db, '0.17.0', '<' ) ) { + if ( \version_compare( $version_from_db, '0.17.0', '<' ) ) { self::migrate_from_0_16(); } - if ( version_compare( $version_from_db, '1.3.0', '<' ) ) { + if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) { self::migrate_from_1_2_0(); } - if ( version_compare( $version_from_db, '2.1.0', '<' ) ) { + if ( \version_compare( $version_from_db, '2.1.0', '<' ) ) { self::migrate_from_2_0_0(); } - if ( version_compare( $version_from_db, '2.3.0', '<' ) ) { + if ( \version_compare( $version_from_db, '2.3.0', '<' ) ) { self::migrate_from_2_2_0(); } - if ( version_compare( $version_from_db, '3.0.0', '<' ) ) { + if ( \version_compare( $version_from_db, '3.0.0', '<' ) ) { self::migrate_from_2_6_0(); } + if ( \version_compare( $version_from_db, '4.0.0', '<' ) ) { + self::migrate_to_4_0_0(); + } + if ( \version_compare( $version_from_db, '4.1.0', '<' ) ) { + self::migrate_to_4_1_0(); + } + if ( \version_compare( $version_from_db, '4.5.0', '<' ) ) { + \wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_update_comment_counts' ); + } + if ( \version_compare( $version_from_db, '4.7.1', '<' ) ) { + self::migrate_to_4_7_1(); + } + if ( \version_compare( $version_from_db, '4.7.2', '<' ) ) { + self::migrate_to_4_7_2(); + } + if ( \version_compare( $version_from_db, '4.7.3', '<' ) ) { + add_action( 'init', 'flush_rewrite_rules', 20 ); + } + if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) { + Scheduler::register_schedules(); + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'create_post_outbox_items' ) ); + \wp_schedule_single_event( \time() + 15, 'activitypub_upgrade', array( 'create_comment_outbox_items' ) ); + add_action( 'init', 'flush_rewrite_rules', 20 ); + } + if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) { + Scheduler::register_schedules(); + } + if ( \version_compare( $version_from_db, '5.4.0', '<' ) ) { + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) ); + \wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) ); + \add_action( 'init', 'flush_rewrite_rules', 20 ); + } + if ( \version_compare( $version_from_db, '5.7.0', '<' ) ) { + self::delete_mastodon_api_orphaned_extra_fields(); + } + if ( \version_compare( $version_from_db, '5.8.0', '<' ) ) { + self::update_notification_options(); + } - update_option( 'activitypub_db_version', self::get_target_version() ); + /* + * Add new update routines above this comment. ^ + * + * Use 'unreleased' as the version number for new migrations and add tests for the callback directly. + * The release script will automatically replace it with the actual version number. + * Example: + * + * if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) { + * // Update routine. + * } + */ + + /** + * Fires when the system has to be migrated. + * + * @param string $version_from_db The version from which to migrate. + * @param string $target_version The target version to migrate to. + */ + \do_action( 'activitypub_migrate', $version_from_db, ACTIVITYPUB_PLUGIN_VERSION ); + + \update_option( 'activitypub_db_version', ACTIVITYPUB_PLUGIN_VERSION ); self::unlock(); } @@ -147,11 +226,43 @@ class Migration { * @param string $version_from_db The version from which to migrate. */ public static function async_migration( $version_from_db ) { - if ( version_compare( $version_from_db, '1.0.0', '<' ) ) { + if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) { self::migrate_from_0_17(); } } + /** + * Asynchronously runs upgrade routines. + * + * @param callable $callback Callable upgrade routine. Must be a method of this class. + * @params mixed ...$args Optional. Parameters that get passed to the callback. + */ + public static function async_upgrade( $callback ) { + $args = \func_get_args(); + + // Bail if the existing lock is still valid. + if ( self::is_locked() ) { + \wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_upgrade', $args ); + return; + } + + self::lock(); + + $callback = array_shift( $args ); // Remove $callback from arguments. + $next = \call_user_func_array( array( self::class, $callback ), $args ); + + self::unlock(); + + if ( ! empty( $next ) ) { + // Schedule the next run, adding the result to the arguments. + \wp_schedule_single_event( + \time() + 30, + 'activitypub_upgrade', + \array_merge( array( $callback ), \array_values( $next ) ) + ); + } + } + /** * Updates the custom template to use shortcodes instead of the deprecated templates. */ @@ -262,6 +373,369 @@ class Migration { self::update_options_key( 'activitypub_blog_user_identifier', 'activitypub_blog_identifier' ); } + /** + * * Update actor-mode settings. + * * Get the ID of the latest blog post and save it to the options table. + */ + private static function migrate_to_4_0_0() { + $latest_post_id = 0; + + // Get the ID of the latest blog post and save it to the options table. + $latest_post = get_posts( + array( + 'numberposts' => 1, + 'orderby' => 'ID', + 'order' => 'DESC', + 'post_type' => 'any', + 'post_status' => 'publish', + ) + ); + + if ( $latest_post ) { + $latest_post_id = $latest_post[0]->ID; + } + + \update_option( 'activitypub_last_post_with_permalink_as_id', $latest_post_id ); + + $users = \get_users( + array( + 'capability__in' => array( 'activitypub' ), + ) + ); + + foreach ( $users as $user ) { + $followers = Followers::get_followers( $user->ID ); + + if ( $followers ) { + \update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' ); + } + } + + $followers = Followers::get_followers( Actors::BLOG_USER_ID ); + + if ( $followers ) { + \update_option( 'activitypub_use_permalink_as_id_for_blog', '1' ); + } + + self::migrate_actor_mode(); + } + + /** + * Upate to 4.1.0 + * + * * Migrate the `activitypub_post_content_type` to only use `activitypub_custom_post_content`. + */ + public static function migrate_to_4_1_0() { + $content_type = \get_option( 'activitypub_post_content_type' ); + + switch ( $content_type ) { + case 'excerpt': + $template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]"; + break; + case 'title': + $template = "[ap_title type=\"html\"]\n\n[ap_permalink type=\"html\"]"; + break; + case 'content': + $template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]"; + break; + case 'custom': + $template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); + break; + default: + $template = ACTIVITYPUB_CUSTOM_POST_CONTENT; + break; + } + + \update_option( 'activitypub_custom_post_content', $template ); + + \delete_option( 'activitypub_post_content_type' ); + + $object_type = \get_option( 'activitypub_object_type', false ); + if ( ! $object_type ) { + \update_option( 'activitypub_object_type', 'note' ); + } + + // Clean up empty visibility meta. + global $wpdb; + $wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery + "DELETE FROM $wpdb->postmeta + WHERE meta_key = 'activitypub_content_visibility' + AND (meta_value IS NULL OR meta_value = '')" + ); + } + + /** + * Updates post meta keys to be prefixed with an underscore. + */ + public static function migrate_to_4_7_1() { + global $wpdb; + + $meta_keys = array( + 'activitypub_actor_json', + 'activitypub_canonical_url', + 'activitypub_errors', + 'activitypub_inbox', + 'activitypub_user_id', + ); + + foreach ( $meta_keys as $meta_key ) { + // phpcs:ignore WordPress.DB + $wpdb->update( $wpdb->postmeta, array( 'meta_key' => '_' . $meta_key ), array( 'meta_key' => $meta_key ) ); + } + } + + /** + * Clears the post cache for Followers, we should have done this in 4.7.1 when we renamed those keys. + */ + public static function migrate_to_4_7_2() { + global $wpdb; + // phpcs:ignore WordPress.DB + $followers = $wpdb->get_col( + $wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Followers::POST_TYPE ) + ); + foreach ( $followers as $id ) { + clean_post_cache( $id ); + } + } + + /** + * Update comment counts for posts in batches. + * + * @see Comment::pre_wp_update_comment_count_now() + * @param int $batch_size Optional. Number of posts to process per batch. Default 100. + * @param int $offset Optional. Number of posts to skip. Default 0. + */ + public static function update_comment_counts( $batch_size = 100, $offset = 0 ) { + global $wpdb; + + // Bail if the existing lock is still valid. + if ( self::is_locked() ) { + \wp_schedule_single_event( + time() + ( 5 * MINUTE_IN_SECONDS ), + 'activitypub_update_comment_counts', + array( + 'batch_size' => $batch_size, + 'offset' => $offset, + ) + ); + return; + } + + self::lock(); + + Comment::register_comment_types(); + $comment_types = Comment::get_comment_type_slugs(); + $type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')"; + + // Get and process this batch. + $post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB + $wpdb->prepare( + // phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared + "SELECT DISTINCT comment_post_ID FROM {$wpdb->comments} WHERE comment_approved = '1' {$type_inclusion} ORDER BY comment_post_ID LIMIT %d OFFSET %d", + $batch_size, + $offset + ) + ); + + foreach ( $post_ids as $post_id ) { + \wp_update_comment_count_now( $post_id ); + } + + if ( count( $post_ids ) === $batch_size ) { + // Schedule next batch. + \wp_schedule_single_event( + time() + MINUTE_IN_SECONDS, + 'activitypub_update_comment_counts', + array( + 'batch_size' => $batch_size, + 'offset' => $offset + $batch_size, + ) + ); + } + + self::unlock(); + } + + /** + * Create outbox items for posts in batches. + * + * @param int $batch_size Optional. Number of posts to process per batch. Default 50. + * @param int $offset Optional. Number of posts to skip. Default 0. + * @return array|null Array with batch size and offset if there are more posts to process, null otherwise. + */ + public static function create_post_outbox_items( $batch_size = 50, $offset = 0 ) { + $posts = \get_posts( + array( + // our own `ap_outbox` will be excluded from `any` by virtue of its `exclude_from_search` arg. + 'post_type' => 'any', + 'posts_per_page' => $batch_size, + 'offset' => $offset, + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => 'activitypub_status', + 'value' => 'federated', + ), + ), + ) + ); + + // Avoid multiple queries for post meta. + \update_postmeta_cache( \wp_list_pluck( $posts, 'ID' ) ); + + foreach ( $posts as $post ) { + $visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true ); + + self::add_to_outbox( $post, 'Create', $post->post_author, $visibility ); + + // Add Update activity when the post has been modified. + if ( $post->post_modified !== $post->post_date ) { + self::add_to_outbox( $post, 'Update', $post->post_author, $visibility ); + } + } + + if ( count( $posts ) === $batch_size ) { + return array( + 'batch_size' => $batch_size, + 'offset' => $offset + $batch_size, + ); + } + + return null; + } + + /** + * Create outbox items for comments in batches. + * + * @param int $batch_size Optional. Number of posts to process per batch. Default 50. + * @param int $offset Optional. Number of posts to skip. Default 0. + * @return array|null Array with batch size and offset if there are more posts to process, null otherwise. + */ + public static function create_comment_outbox_items( $batch_size = 50, $offset = 0 ) { + $comments = \get_comments( + array( + 'author__not_in' => array( 0 ), // Limit to comments by registered users. + 'number' => $batch_size, + 'offset' => $offset, + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => 'activitypub_status', + 'value' => 'federated', + ), + ), + ) + ); + + foreach ( $comments as $comment ) { + self::add_to_outbox( $comment, 'Create', $comment->user_id ); + } + + if ( count( $comments ) === $batch_size ) { + return array( + 'batch_size' => $batch_size, + 'offset' => $offset + $batch_size, + ); + } + + return null; + } + + /** + * Update _activitypub_actor_json meta values to ensure they are properly slashed. + * + * @param int $batch_size Optional. Number of meta values to process per batch. Default 100. + * @param int $offset Optional. Number of meta values to skip. Default 0. + * @return array|null Array with batch size and offset if there are more meta values to process, null otherwise. + */ + public static function update_actor_json_slashing( $batch_size = 100, $offset = 0 ) { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $meta_values = $wpdb->get_results( + $wpdb->prepare( + "SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d OFFSET %d", + $batch_size, + $offset + ) + ); + + foreach ( $meta_values as $meta ) { + $json = \json_decode( $meta->meta_value, true ); + + // If json_decode fails, try adding slashes. + if ( null === $json && \json_last_error() !== JSON_ERROR_NONE ) { + $escaped_value = \preg_replace( '#\\\\(?!["\\\\/bfnrtu])#', '\\\\\\\\', $meta->meta_value ); + $json = \json_decode( $escaped_value, true ); + + // Update the meta if json_decode succeeds with slashes. + if ( null !== $json && \json_last_error() === JSON_ERROR_NONE ) { + \update_post_meta( $meta->post_id, '_activitypub_actor_json', \wp_slash( $escaped_value ) ); + } + } + } + + if ( \count( $meta_values ) === $batch_size ) { + return array( + 'batch_size' => $batch_size, + 'offset' => $offset + $batch_size, + ); + } + + return null; + } + + /** + * Update comment author emails with webfinger addresses for ActivityPub comments. + * + * @param int $batch_size Optional. Number of comments to process per batch. Default 50. + * @param int $offset Optional. Number of comments to skip. Default 0. + * @return array|null Array with batch size and offset if there are more comments to process, null otherwise. + */ + public static function update_comment_author_emails( $batch_size = 50, $offset = 0 ) { + $comments = \get_comments( + array( + 'number' => $batch_size, + 'offset' => $offset, + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => 'protocol', + 'value' => 'activitypub', + ), + ), + ) + ); + + foreach ( $comments as $comment ) { + $comment_author_url = $comment->comment_author_url; + if ( empty( $comment_author_url ) ) { + continue; + } + + $webfinger = Webfinger::uri_to_acct( $comment_author_url ); + if ( \is_wp_error( $webfinger ) ) { + continue; + } + + \wp_update_comment( + array( + 'comment_ID' => $comment->comment_ID, + 'comment_author_email' => \str_replace( 'acct:', '', $webfinger ), + ) + ); + } + + if ( count( $comments ) === $batch_size ) { + return array( + 'batch_size' => $batch_size, + 'offset' => $offset + $batch_size, + ); + } + + return null; + } + /** * Set the defaults needed for the plugin to work. * @@ -269,6 +743,41 @@ class Migration { */ public static function add_default_settings() { self::add_activitypub_capability(); + self::add_default_extra_field(); + } + + /** + * Add an activity to the outbox without federating it. + * + * @param \WP_Post|\WP_Comment $comment The comment or post object. + * @param string $activity_type The type of activity. + * @param int $user_id The user ID. + * @param string $visibility Optional. The visibility of the content. Default 'public'. + */ + private static function add_to_outbox( $comment, $activity_type, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) { + $transformer = Factory::get_transformer( $comment ); + if ( ! $transformer || \is_wp_error( $transformer ) ) { + return; + } + + $activity = $transformer->to_activity( $activity_type ); + if ( ! $activity || \is_wp_error( $activity ) ) { + return; + } + + // If the user is disabled, fall back to the blog user when available. + if ( ! user_can_activitypub( $user_id ) ) { + if ( user_can_activitypub( Actors::BLOG_USER_ID ) ) { + $user_id = Actors::BLOG_USER_ID; + } else { + return; + } + } + + $post_id = Outbox::add( $activity, $user_id, $visibility ); + + // Immediately set to publish, no federation needed. + \wp_publish_post( $post_id ); } /** @@ -288,6 +797,43 @@ class Migration { } } + /** + * Add a default extra field for the user. + */ + private static function add_default_extra_field() { + $users = \get_users( + array( + 'capability__in' => array( 'activitypub' ), + ) + ); + + $title = \__( 'Powered by', 'activitypub' ); + $content = 'WordPress'; + + // Add a default extra field for each user. + foreach ( $users as $user ) { + \wp_insert_post( + array( + 'post_type' => Extra_Fields::USER_POST_TYPE, + 'post_author' => $user->ID, + 'post_status' => 'publish', + 'post_title' => $title, + 'post_content' => $content, + ) + ); + } + + \wp_insert_post( + array( + 'post_type' => Extra_Fields::BLOG_POST_TYPE, + 'post_author' => 0, + 'post_status' => 'publish', + 'post_title' => $title, + 'post_content' => $content, + ) + ); + } + /** * Rename meta keys. * @@ -323,4 +869,76 @@ class Migration { array( '%s' ) ); } + + /** + * Migrate the actor mode settings. + */ + public static function migrate_actor_mode() { + $blog_profile = \get_option( 'activitypub_enable_blog_user', '0' ); + $author_profiles = \get_option( 'activitypub_enable_users', '1' ); + + if ( + '1' === $blog_profile && + '1' === $author_profiles + ) { + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE ); + } elseif ( + '1' === $blog_profile && + '1' !== $author_profiles + ) { + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE ); + } elseif ( + '1' !== $blog_profile && + '1' === $author_profiles + ) { + \update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ); + } + } + + /** + * Deletes user extra fields where the author is the blog user. + * + * These extra fields were created when the Enable Mastodon Apps integration passed + * an author_url instead of a user_id to the mastodon_api_account filter. This caused + * Extra_Fields::default_actor_extra_fields() to run but fail to cache the fact it ran + * for non-existent users. The result is a number of user extra fields with no author. + * + * @ticket https://github.com/Automattic/wordpress-activitypub/pull/1554 + */ + public static function delete_mastodon_api_orphaned_extra_fields() { + global $wpdb; + + // phpcs:ignore WordPress.DB.DirectDatabaseQuery + $wpdb->delete( + $wpdb->posts, + array( + 'post_type' => Extra_Fields::USER_POST_TYPE, + 'post_author' => Actors::BLOG_USER_ID, + ) + ); + } + + /** + * Update notification options. + */ + public static function update_notification_options() { + $new_dm = \get_option( 'activitypub_mailer_new_dm', '1' ); + $new_follower = \get_option( 'activitypub_mailer_new_follower', '1' ); + + // Add the blog user notification options. + \add_option( 'activitypub_blog_user_mailer_new_dm', $new_dm ); + \add_option( 'activitypub_blog_user_mailer_new_follower', $new_follower ); + \add_option( 'activitypub_blog_user_mailer_new_mention', '1' ); + + // Add the actor notification options. + foreach ( Actors::get_collection() as $actor ) { + \update_user_option( $actor->get__id(), 'activitypub_mailer_new_dm', $new_dm ); + \update_user_option( $actor->get__id(), 'activitypub_mailer_new_follower', $new_follower ); + \update_user_option( $actor->get__id(), 'activitypub_mailer_new_mention', '1' ); + } + + // Delete the old notification options. + \delete_option( 'activitypub_mailer_new_dm' ); + \delete_option( 'activitypub_mailer_new_follower' ); + } } diff --git a/wp-content/plugins/activitypub/includes/class-move.php b/wp-content/plugins/activitypub/includes/class-move.php new file mode 100644 index 00000000..18e54ad4 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-move.php @@ -0,0 +1,313 @@ +get__id() > 0 ) { + \update_user_option( $user->get__id(), 'activitypub_moved_to', $to ); + } else { + \update_option( 'activitypub_blog_user_moved_to', $to ); + } + + $response = Http::get_remote_object( $to ); + + if ( \is_wp_error( $response ) ) { + return $response; + } + + $target_actor = new Actor(); + $target_actor->from_array( $response ); + + // Check if the `Move` Activity is valid. + $also_known_as = $target_actor->get_also_known_as() ?? array(); + if ( ! in_array( $from, $also_known_as, true ) ) { + return new \WP_Error( 'invalid_target', __( 'Invalid target', 'activitypub' ) ); + } + + $activity = new Activity(); + $activity->set_type( 'Move' ); + $activity->set_actor( $user->get_id() ); + $activity->set_origin( $user->get_id() ); + $activity->set_object( $user->get_id() ); + $activity->set_target( $target_actor->get_id() ); + + // Add to outbox. + return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ); + } + + /** + * Internal Move. + * + * Move an ActivityPub Actor from one location (internal) to another (internal). + * + * This helps migrating abandoned profiles to `Move` to other profiles: + * + * `Move::internally( 'https://example.com/?author=123', 'https://example.com/?author=321' );` + * + * ... or to change Actor-IDs like: + * + * `Move::internally( 'https://example.com/author/foo', 'https://example.com/?author=123' );` + * + * @param string $from The current account URL. + * @param string $to The new account URL. + * + * @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure. + */ + public static function internally( $from, $to ) { + $user = Actors::get_by_various( $from ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + // Add the old account URL to alsoKnownAs. + if ( $user->get__id() > 0 ) { + self::update_user_also_known_as( $user->get__id(), $from ); + \update_user_option( $user->get__id(), 'activitypub_moved_to', $to ); + } else { + self::update_blog_also_known_as( $from ); + \update_option( 'activitypub_blog_user_moved_to', $to ); + } + + // check if `$from` is a URL or an ID. + if ( \filter_var( $from, FILTER_VALIDATE_URL ) ) { + $actor = $from; + } else { + $actor = $user->get_id(); + } + + $activity = new Activity(); + $activity->set_type( 'Move' ); + $activity->set_actor( $actor ); + $activity->set_origin( $actor ); + $activity->set_object( $actor ); + $activity->set_target( $to ); + + return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ); + } + + /** + * Update the alsoKnownAs property of a user. + * + * @param int $user_id The user ID. + * @param string $from The current account URL. + */ + private static function update_user_also_known_as( $user_id, $from ) { + // phpcs:ignore Universal.Operators.DisallowShortTernary.Found + $also_known_as = \get_user_option( 'activitypub_also_known_as', $user_id ) ?: array(); + $also_known_as[] = $from; + + \update_user_option( $user_id, 'activitypub_also_known_as', $also_known_as ); + } + + /** + * Update the alsoKnownAs property of the blog. + * + * @param string $from The current account URL. + */ + private static function update_blog_also_known_as( $from ) { + $also_known_as = \get_option( 'activitypub_blog_user_also_known_as', array() ); + $also_known_as[] = $from; + + \update_option( 'activitypub_blog_user_also_known_as', $also_known_as ); + } + + /** + * Change domain for all ActivityPub Actors. + * + * This method handles domain migration according to the ActivityPub Data Portability spec. + * It stores the old host and calls Move::internally for each available profile. + * It also caches the JSON representation of the old Actor for future lookups. + * + * @param string $from The old domain. + * @param string $to The new domain. + * + * @return array Array of results from Move::internally calls. + */ + public static function change_domain( $from, $to ) { + // Get all actors that need to be migrated. + $actors = Actors::get_all(); + + $results = array(); + $to_host = \wp_parse_url( $to, \PHP_URL_HOST ); + $from_host = \wp_parse_url( $from, \PHP_URL_HOST ); + + // Store the old host for future reference. + \update_option( 'activitypub_old_host', $from_host ); + + // Process each actor. + foreach ( $actors as $actor ) { + $actor_id = $actor->get_id(); + + // Replace the new host with the old host in the actor ID. + $old_actor_id = str_replace( $to_host, $from_host, $actor_id ); + + // Call Move::internally for this actor. + $result = self::internally( $old_actor_id, $actor_id ); + + if ( \is_wp_error( $result ) ) { + // Log the error and continue with the next actor. + Debug::write_log( 'Error moving actor: ' . $actor_id . ' - ' . $result->get_error_message() ); + continue; + } + + $json = str_replace( $to_host, $from_host, $actor->to_json() ); + + // Save the current actor data after migration. + if ( $actor instanceof Blog ) { + \update_option( 'activitypub_blog_user_old_host_data', $json, false ); + } else { + \update_user_option( $actor->get__id(), 'activitypub_old_host_data', $json, false ); + } + + $results[] = array( + 'actor' => $actor_id, + 'result' => $result, + ); + } + + return $results; + } + + /** + * Maybe initiate old user. + * + * This method checks if the current request domain matches the old host. + * If it does, it retrieves the cached data for the user and populates the instance. + * + * @param Blog|User $instance The Blog or User instance to populate. + */ + public static function maybe_initiate_old_user( $instance ) { + if ( ! Query::get_instance()->is_old_host_request() ) { + return; + } + + if ( $instance instanceof Blog ) { + $cached_data = \get_option( 'activitypub_blog_user_old_host_data' ); + } elseif ( $instance instanceof User ) { + $cached_data = \get_user_option( 'activitypub_old_host_data', $instance->get__id() ); + } + + if ( ! empty( $cached_data ) ) { + $instance->from_json( $cached_data ); + } + } + + /** + * Pre-send to inboxes. + * + * @param string $json The ActivityPub Activity JSON. + */ + public static function pre_send_to_inboxes( $json ) { + $json = json_decode( $json, true ); + + if ( 'Move' !== $json['type'] ) { + return; + } + + if ( is_same_domain( $json['object'] ) ) { + return; + } + + Query::get_instance()->set_old_host_request(); + } + + /** + * Filter to return the old blog username. + * + * @param null $pre The pre-existing value. + * @param string $username The username to check. + * + * @return Blog|null The old blog instance or null. + */ + public static function old_blog_username( $pre, $username ) { + $old_host = \get_option( 'activitypub_old_host' ); + + // Special case for Blog Actor on old host. + if ( $old_host === $username && Query::get_instance()->is_old_host_request() ) { + // Return a new Blog instance which will load the cached data in its constructor. + $pre = new Blog(); + } + + return $pre; + } +} diff --git a/wp-content/plugins/activitypub/includes/class-notification.php b/wp-content/plugins/activitypub/includes/class-notification.php index 1dd65732..68283133 100644 --- a/wp-content/plugins/activitypub/includes/class-notification.php +++ b/wp-content/plugins/activitypub/includes/class-notification.php @@ -60,7 +60,18 @@ class Notification { public function send() { $type = \strtolower( $this->type ); + /** + * Action to send ActivityPub notifications. + * + * @param Notification $instance The notification object. + */ do_action( 'activitypub_notification', $this ); + + /** + * Type-specific action to send ActivityPub notifications. + * + * @param Notification $instance The notification object. + */ do_action( "activitypub_notification_{$type}", $this ); } } diff --git a/wp-content/plugins/activitypub/includes/class-options.php b/wp-content/plugins/activitypub/includes/class-options.php new file mode 100644 index 00000000..d30ef9ad --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-options.php @@ -0,0 +1,124 @@ +activitypub_object ) { + return $this->activitypub_object; + } + + if ( $this->prepare_activitypub_data() ) { + return $this->activitypub_object; + } + + $queried_object = $this->get_queried_object(); + $transformer = Factory::get_transformer( $queried_object ); + + if ( $transformer && ! \is_wp_error( $transformer ) ) { + $this->activitypub_object = $transformer->to_object(); + } + + return $this->activitypub_object; + } + + /** + * Get the ActivityPub object ID. + * + * @return string The ActivityPub object ID. + */ + public function get_activitypub_object_id() { + if ( $this->activitypub_object_id ) { + return $this->activitypub_object_id; + } + + if ( $this->prepare_activitypub_data() ) { + return $this->activitypub_object_id; + } + + $queried_object = $this->get_queried_object(); + $transformer = Factory::get_transformer( $queried_object ); + + if ( $transformer && ! \is_wp_error( $transformer ) ) { + $this->activitypub_object_id = $transformer->to_id(); + } + + return $this->activitypub_object_id; + } + + /** + * Prepare and set both ActivityPub object and ID for Outbox activities and virtual objects. + * + * @return bool True if an object was found and set, false otherwise. + */ + private function prepare_activitypub_data() { + $queried_object = $this->get_queried_object(); + + // Check for Outbox Activity. + if ( + $queried_object instanceof \WP_Post && + Outbox::POST_TYPE === $queried_object->post_type + ) { + $activitypub_object = Outbox::maybe_get_activity( $queried_object ); + + // Check if the Outbox Activity is public. + if ( ! \is_wp_error( $activitypub_object ) ) { + $this->activitypub_object = $activitypub_object; + $this->activitypub_object_id = $this->activitypub_object->get_id(); + return true; + } + } + + if ( ! $queried_object ) { + // If the object is not a valid ActivityPub object, try to get a virtual object. + $activitypub_object = $this->maybe_get_virtual_object(); + + if ( $activitypub_object ) { + $this->activitypub_object = $activitypub_object; + $this->activitypub_object_id = $this->activitypub_object->get_id(); + return true; + } + } + + return false; + } + + /** + * Get the queried object. + * + * This adds support for Comments by `?c=123` IDs and Users by `?author=123` and `@username` IDs. + * + * @return \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null The queried object. + */ + public function get_queried_object() { + $queried_object = \get_queried_object(); + + // Check Comment by ID. + if ( ! $queried_object ) { + $comment_id = \get_query_var( 'c' ); + if ( $comment_id ) { + $queried_object = \get_comment( $comment_id ); + } + } + + // Check Post by ID (works for custom post types). + if ( ! $queried_object ) { + $post_id = \get_query_var( 'p' ); + if ( $post_id ) { + $queried_object = \get_post( $post_id ); + } + } + + // Try to get Author by ID. + if ( ! $queried_object ) { + $url = $this->get_request_url(); + $author_id = url_to_authorid( $url ); + if ( $author_id ) { + $queried_object = \get_user_by( 'id', $author_id ); + } + } + + /** + * Filters the queried object. + * + * @param \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null $queried_object The queried object. + */ + return apply_filters( 'activitypub_queried_object', $queried_object ); + } + + /** + * Get the virtual object. + * + * Virtual objects are objects that are not stored in the database, but are created on the fly. + * The plugins currently supports two virtual objects: The Blog-Actor and the Application-Actor. + * + * @see \Activitypub\Model\Blog + * @see \Activitypub\Model\Application + * + * @return object|null The virtual object. + */ + protected function maybe_get_virtual_object() { + $url = $this->get_request_url(); + + if ( ! $url ) { + return null; + } + + $author_id = url_to_authorid( $url ); + + if ( ! is_numeric( $author_id ) ) { + $author_id = $url; + } + + $user = Actors::get_by_various( $author_id ); + + if ( \is_wp_error( $user ) || ! $user ) { + return null; + } + + return $user; + } + + /** + * Get the request URL. + * + * @return string|null The request URL. + */ + protected function get_request_url() { + if ( ! isset( $_SERVER['REQUEST_URI'] ) ) { + return null; + } + + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized + $url = \wp_unslash( $_SERVER['REQUEST_URI'] ); + $url = \WP_Http::make_absolute_url( $url, \home_url() ); + $url = \sanitize_url( $url ); + + return $url; + } + + /** + * Check if the current request is an ActivityPub request. + * + * @return bool True if the request is an ActivityPub request, false otherwise. + */ + public function is_activitypub_request() { + if ( isset( $this->is_activitypub_request ) ) { + return $this->is_activitypub_request; + } + + global $wp_query; + + // One can trigger an ActivityPub request by adding `?activitypub` to the URL. + if ( + isset( $wp_query->query_vars['activitypub'] ) || + // phpcs:ignore WordPress.Security.NonceVerification.Recommended + isset( $_GET['activitypub'] ) + ) { + \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true ); + $this->is_activitypub_request = true; + + return true; + } + + /* + * The other (more common) option to make an ActivityPub request + * is to send an Accept header. + */ + if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) { + $accept = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ); + + /* + * $accept can be a single value, or a comma separated list of values. + * We want to support both scenarios, + * and return true when the header includes at least one of the following: + * - application/activity+json + * - application/ld+json + * - application/json + */ + if ( \preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) { + \defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true ); + $this->is_activitypub_request = true; + + return true; + } + } + + $this->is_activitypub_request = false; + + return false; + } + + /** + * Check if the current request is from the old host. + * + * @return bool True if the request is from the old host, false otherwise. + */ + public function is_old_host_request() { + if ( isset( $this->is_old_host_request ) ) { + return $this->is_old_host_request; + } + + $old_host = \get_option( 'activitypub_old_host' ); + + if ( ! $old_host ) { + $this->is_old_host_request = false; + return false; + } + + $request_host = isset( $_SERVER['HTTP_HOST'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_HOST'] ) ) : ''; + $referer_host = isset( $_SERVER['HTTP_REFERER'] ) ? \wp_parse_url( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_REFERER'] ) ), PHP_URL_HOST ) : ''; + + // Check if the domain matches either the request domain or referer. + $check = $old_host === $request_host || $old_host === $referer_host; + $this->is_old_host_request = $check; + + return $check; + } + + /** + * Fake an old host request. + * + * @param bool $state Optional. The state to set. Default true. + */ + public function set_old_host_request( $state = true ) { + $this->is_old_host_request = $state; + } +} diff --git a/wp-content/plugins/activitypub/includes/class-sanitize.php b/wp-content/plugins/activitypub/includes/class-sanitize.php new file mode 100644 index 00000000..42d32491 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/class-sanitize.php @@ -0,0 +1,122 @@ + $sanitized, + 'search_columns' => array( 'user_login', 'user_nicename' ), + 'number' => 1, + 'hide_empty' => true, + 'fields' => 'ID', + ) + ); + + if ( $user->get_results() ) { + \add_settings_error( + 'activitypub_blog_identifier', + 'activitypub_blog_identifier', + \esc_html__( 'You cannot use an existing author’s name for the blog profile ID.', 'activitypub' ) + ); + + return Blog::get_default_username(); + } + + return $sanitized; + } + + /** + * Get the sanitized value of a constant. + * + * @param mixed $value The constant value. + * + * @return string The sanitized value. + */ + public static function constant_value( $value ) { + if ( is_bool( $value ) ) { + return $value ? 'true' : 'false'; + } + + if ( is_string( $value ) ) { + return esc_attr( $value ); + } + + if ( is_array( $value ) ) { + // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r + return print_r( $value, true ); + } + + return $value; + } +} diff --git a/wp-content/plugins/activitypub/includes/class-scheduler.php b/wp-content/plugins/activitypub/includes/class-scheduler.php index 0774f296..2e2ca070 100644 --- a/wp-content/plugins/activitypub/includes/class-scheduler.php +++ b/wp-content/plugins/activitypub/includes/class-scheduler.php @@ -7,7 +7,15 @@ namespace Activitypub; +use Activitypub\Activity\Activity; +use Activitypub\Activity\Base_Object; +use Activitypub\Scheduler\Post; +use Activitypub\Scheduler\Actor; +use Activitypub\Scheduler\Comment; +use Activitypub\Collection\Actors; +use Activitypub\Collection\Outbox; use Activitypub\Collection\Followers; +use Activitypub\Transformer\Factory; /** * Scheduler class. @@ -16,67 +24,53 @@ use Activitypub\Collection\Followers; */ class Scheduler { + /** + * Allowed batch callbacks. + * + * @var array + */ + private static $batch_callbacks = array(); + /** * Initialize the class, registering WordPress hooks. */ public static function init() { - // Post transitions. - \add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 ); - \add_action( - 'edit_attachment', - function ( $post_id ) { - self::schedule_post_activity( 'publish', 'publish', $post_id ); - } - ); - \add_action( - 'add_attachment', - function ( $post_id ) { - self::schedule_post_activity( 'publish', '', $post_id ); - } - ); - \add_action( - 'delete_attachment', - function ( $post_id ) { - self::schedule_post_activity( 'trash', '', $post_id ); - } - ); + self::register_schedulers(); - if ( ! ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS ) { - // Comment transitions. - \add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 ); - \add_action( - 'edit_comment', - function ( $comment_id ) { - self::schedule_comment_activity( 'approved', 'approved', $comment_id ); - } - ); - \add_action( - 'wp_insert_comment', - function ( $comment_id ) { - self::schedule_comment_activity( 'approved', '', $comment_id ); - } - ); - } + self::$batch_callbacks = array( + Dispatcher::$callback, + array( Dispatcher::class, 'retry_send_to_followers' ), + ); // Follower Cleanups. \add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) ); \add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) ); - // Profile updates for blog options. - if ( ! is_user_type_disabled( 'blog' ) ) { - \add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) ); - \add_action( 'update_option_blogdescription', array( self::class, 'blog_user_update' ) ); - \add_action( 'update_option_blogname', array( self::class, 'blog_user_update' ) ); - \add_filter( 'pre_set_theme_mod_custom_logo', array( self::class, 'blog_user_update' ) ); - \add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) ); - } + // Event callbacks. + \add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 ); + \add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) ); + \add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) ); - // Profile updates for user options. - if ( ! is_user_type_disabled( 'user' ) ) { - \add_action( 'wp_update_user', array( self::class, 'user_update' ) ); - \add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 ); - // @todo figure out a feasible way of updating the header image since it's not unique to any user. - } + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) ); + \add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_announce_activity' ), 10, 4 ); + + \add_action( 'update_option_activitypub_outbox_purge_days', array( self::class, 'handle_outbox_purge_days_update' ), 10, 2 ); + } + + /** + * Register handlers. + */ + public static function register_schedulers() { + Post::init(); + Actor::init(); + Comment::init(); + + /** + * Register additional schedulers. + * + * @since 5.0.0 + */ + do_action( 'activitypub_register_schedulers' ); } /** @@ -90,6 +84,14 @@ class Scheduler { if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) { \wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' ); } + + if ( ! \wp_next_scheduled( 'activitypub_reprocess_outbox' ) ) { + \wp_schedule_event( time(), 'hourly', 'activitypub_reprocess_outbox' ); + } + + if ( ! wp_next_scheduled( 'activitypub_outbox_purge' ) ) { + wp_schedule_event( time(), 'daily', 'activitypub_outbox_purge' ); + } } /** @@ -100,125 +102,8 @@ class Scheduler { public static function deregister_schedules() { wp_unschedule_hook( 'activitypub_update_followers' ); wp_unschedule_hook( 'activitypub_cleanup_followers' ); - } - - - /** - * Schedule Activities. - * - * @param string $new_status New post status. - * @param string $old_status Old post status. - * @param \WP_Post $post Post object. - */ - public static function schedule_post_activity( $new_status, $old_status, $post ) { - $post = get_post( $post ); - - if ( ! $post ) { - return; - } - - if ( 'ap_extrafield' === $post->post_type ) { - self::schedule_profile_update( $post->post_author ); - return; - } - - if ( 'ap_extrafield_blog' === $post->post_type ) { - self::schedule_profile_update( 0 ); - return; - } - - // Do not send activities if post is password protected. - if ( \post_password_required( $post ) ) { - return; - } - - // Check if post-type supports ActivityPub. - $post_types = \get_post_types_by_support( 'activitypub' ); - if ( ! \in_array( $post->post_type, $post_types, true ) ) { - return; - } - - $type = false; - - if ( - 'publish' === $new_status && - 'publish' !== $old_status - ) { - $type = 'Create'; - } elseif ( - 'publish' === $new_status || - // We want to send updates for posts that are published and then moved to draft. - ( 'draft' === $new_status && - 'publish' === $old_status ) - ) { - $type = 'Update'; - } elseif ( 'trash' === $new_status ) { - $type = 'Delete'; - } - - if ( empty( $type ) ) { - return; - } - - $hook = 'activitypub_send_post'; - $args = array( $post->ID, $type ); - - if ( false === wp_next_scheduled( $hook, $args ) ) { - set_wp_object_state( $post, 'federate' ); - \wp_schedule_single_event( \time(), $hook, $args ); - } - } - - /** - * Schedule Comment Activities. - * - * @see transition_comment_status() - * - * @param string $new_status New comment status. - * @param string $old_status Old comment status. - * @param \WP_Comment $comment Comment object. - */ - public static function schedule_comment_activity( $new_status, $old_status, $comment ) { - $comment = get_comment( $comment ); - - // Federate only comments that are written by a registered user. - if ( ! $comment || ! $comment->user_id ) { - return; - } - - $type = false; - - if ( - 'approved' === $new_status && - 'approved' !== $old_status - ) { - $type = 'Create'; - } elseif ( 'approved' === $new_status ) { - $type = 'Update'; - \update_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', time(), true ); - } elseif ( - 'trash' === $new_status || - 'spam' === $new_status - ) { - $type = 'Delete'; - } - - if ( empty( $type ) ) { - return; - } - - // Check if comment should be federated or not. - if ( ! should_comment_be_federated( $comment ) ) { - return; - } - - $hook = 'activitypub_send_comment'; - $args = array( $comment->comment_ID, $type ); - - if ( false === wp_next_scheduled( $hook, $args ) ) { - set_wp_object_state( $comment, 'federate' ); - \wp_schedule_single_event( \time(), $hook, $args ); - } + wp_unschedule_hook( 'activitypub_reprocess_outbox' ); + wp_unschedule_hook( 'activitypub_outbox_purge' ); } /** @@ -292,67 +177,268 @@ class Scheduler { } /** - * Send a profile update when relevant user meta is updated. + * Schedule the outbox item for federation. * - * @param int $meta_id Meta ID being updated. - * @param int $user_id User ID being updated. - * @param string $meta_key Meta key being updated. + * @param int $id The ID of the outbox item. + * @param int $offset The offset to add to the scheduled time. */ - public static function user_meta_update( $meta_id, $user_id, $meta_key ) { - // Don't bother if the user can't publish. - if ( ! \user_can( $user_id, 'activitypub' ) ) { + public static function schedule_outbox_activity_for_federation( $id, $offset = 0 ) { + $hook = 'activitypub_process_outbox'; + $args = array( $id ); + + if ( false === wp_next_scheduled( $hook, $args ) ) { + \wp_schedule_single_event( + \time() + $offset, + $hook, + $args + ); + } + } + + /** + * Reprocess the outbox. + */ + public static function reprocess_outbox() { + // Bail if there is a pending batch. + if ( self::next_scheduled_hook( 'activitypub_async_batch' ) ) { return; } - // The user meta fields that affect a profile. - $fields = array( - 'activitypub_description', - 'activitypub_header_image', - 'description', - 'user_url', - 'display_name', - ); - if ( in_array( $meta_key, $fields, true ) ) { - self::schedule_profile_update( $user_id ); - } - } - - /** - * Send a profile update when a user is updated. - * - * @param int $user_id User ID being updated. - */ - public static function user_update( $user_id ) { - // Don't bother if the user can't publish. - if ( ! \user_can( $user_id, 'activitypub' ) ) { + // Bail if there is a batch in progress. + $key = \md5( \serialize( Dispatcher::$callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + if ( self::is_locked( $key ) ) { return; } - self::schedule_profile_update( $user_id ); - } - - /** - * Theme mods only have a dynamic filter so we fudge it like this. - * - * @param mixed $value Optional. The value to be updated. Default null. - * - * @return mixed - */ - public static function blog_user_update( $value = null ) { - self::schedule_profile_update( 0 ); - return $value; - } - - /** - * Send a profile update to all followers. Gets hooked into all relevant options/meta etc. - * - * @param int $user_id The user ID to update (Could be 0 for Blog-User). - */ - public static function schedule_profile_update( $user_id ) { - \wp_schedule_single_event( - \time(), - 'activitypub_send_update_profile_activity', - array( $user_id ) + $ids = \get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'pending', + 'posts_per_page' => 10, + 'fields' => 'ids', + ) ); + + foreach ( $ids as $id ) { + self::schedule_outbox_activity_for_federation( $id ); + } + } + + /** + * Purge outbox items based on a schedule. + */ + public static function purge_outbox() { + $total_posts = (int) wp_count_posts( Outbox::POST_TYPE )->publish; + if ( $total_posts <= 20 ) { + return; + } + + $days = (int) get_option( 'activitypub_outbox_purge_days', 180 ); + $timezone = new \DateTimeZone( 'UTC' ); + $date = new \DateTime( 'now', $timezone ); + + $date->sub( \DateInterval::createFromDateString( "$days days" ) ); + + $post_ids = get_posts( + array( + 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'any', + 'fields' => 'ids', + 'numberposts' => -1, + 'date_query' => array( + array( + 'before' => $date->format( 'Y-m-d' ), + ), + ), + ) + ); + + foreach ( $post_ids as $post_id ) { + \wp_delete_post( $post_id, true ); + } + } + + /** + * Update schedules when outbox purge days settings change. + * + * @param int $old_value The old value. + * @param int $value The new value. + */ + public static function handle_outbox_purge_days_update( $old_value, $value ) { + if ( 0 === (int) $value ) { + wp_clear_scheduled_hook( 'activitypub_outbox_purge' ); + } elseif ( ! wp_next_scheduled( 'activitypub_outbox_purge' ) ) { + wp_schedule_event( time(), 'daily', 'activitypub_outbox_purge' ); + } + } + + /** + * Asynchronously runs batch processing routines. + * + * The batching part is optional and only comes into play if the callback returns anything. + * Beyond that it's a helper to run a callback asynchronously with locking to prevent simultaneous processing. + * + * @param callable $callback Callable processing routine. + * @params mixed ...$args Optional. Parameters that get passed to the callback. + */ + public static function async_batch( $callback ) { + if ( ! in_array( $callback, self::$batch_callbacks, true ) || ! \is_callable( $callback ) ) { + _doing_it_wrong( __METHOD__, 'The first argument must be a valid callback.', '5.2.0' ); + return; + } + + $args = \func_get_args(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue + $key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize + + // Bail if the existing lock is still valid. + if ( self::is_locked( $key ) ) { + \wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_async_batch', $args ); + return; + } + + self::lock( $key ); + + $callback = array_shift( $args ); // Remove $callback from arguments. + $next = \call_user_func_array( $callback, $args ); + + self::unlock( $key ); + + if ( ! empty( $next ) ) { + // Schedule the next run, adding the result to the arguments. + \wp_schedule_single_event( + \time() + 30, + 'activitypub_async_batch', + \array_merge( array( $callback ), \array_values( $next ) ) + ); + } + } + + + /** + * Locks the async batch process for individual callbacks to prevent simultaneous processing. + * + * @param string $key Serialized callback name. + * @return bool|int True if the lock was successful, timestamp of existing lock otherwise. + */ + public static function lock( $key ) { + global $wpdb; + + // Try to lock. + $lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_async_batch_' . $key, \time() ) ); // phpcs:ignore WordPress.DB + + if ( ! $lock_result ) { + $lock_result = \get_option( 'activitypub_async_batch_' . $key ); + } + + return $lock_result; + } + + /** + * Unlocks processing for the async batch callback. + * + * @param string $key Serialized callback name. + */ + public static function unlock( $key ) { + \delete_option( 'activitypub_async_batch_' . $key ); + } + + /** + * Whether the async batch callback is locked. + * + * @param string $key Serialized callback name. + * @return boolean + */ + public static function is_locked( $key ) { + $lock = \get_option( 'activitypub_async_batch_' . $key ); + + if ( ! $lock ) { + return false; + } + + $lock = (int) $lock; + + if ( $lock < \time() - 1800 ) { + self::unlock( $key ); + return false; + } + + return true; + } + + /** + * Get the next scheduled hook. + * + * @param string $hook The hook name. + * @return int|bool The timestamp of the next scheduled hook, or false if none found. + */ + private static function next_scheduled_hook( $hook ) { + $crons = _get_cron_array(); + if ( empty( $crons ) ) { + return false; + } + + // Get next event. + $next = false; + foreach ( $crons as $timestamp => $cron ) { + if ( isset( $cron[ $hook ] ) ) { + $next = $timestamp; + break; + } + } + + return $next; + } + + /** + * Send announces. + * + * @param int $outbox_activity_id The outbox activity ID. + * @param \Activitypub\Activity\Activity $activity The activity object. + * @param int $actor_id The actor ID. + * @param int $content_visibility The content visibility. + */ + public static function schedule_announce_activity( $outbox_activity_id, $activity, $actor_id, $content_visibility ) { + // Only if we're in both Blog and User modes. + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + return; + } + + // Only if this isn't the Blog Actor. + if ( Actors::BLOG_USER_ID === $actor_id ) { + return; + } + + // Only if the content is public or quiet public. + if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) { + return; + } + + // Only if the activity is a Create. + if ( 'Create' !== $activity->get_type() ) { + return; + } + + if ( ! is_object( $activity->get_object() ) ) { + return; + } + + // Check if the object is an article, image, audio, video, event, or document and ignore profile updates and other activities. + if ( ! in_array( $activity->get_object()->get_type(), Base_Object::TYPES, true ) ) { + return; + } + + $announce = new Activity(); + $announce->set_type( 'Announce' ); + $announce->set_actor( Actors::get_by_id( Actors::BLOG_USER_ID )->get_id() ); + $announce->set_object( $activity ); + + $outbox_activity_id = Outbox::add( $announce, Actors::BLOG_USER_ID ); + + if ( ! $outbox_activity_id ) { + return; + } + + // Schedule the outbox item for federation. + self::schedule_outbox_activity_for_federation( $outbox_activity_id, 120 ); } } diff --git a/wp-content/plugins/activitypub/includes/class-shortcodes.php b/wp-content/plugins/activitypub/includes/class-shortcodes.php index eb9c5135..63d7e6de 100644 --- a/wp-content/plugins/activitypub/includes/class-shortcodes.php +++ b/wp-content/plugins/activitypub/includes/class-shortcodes.php @@ -54,6 +54,11 @@ class Shortcodes { $hash_tags = array(); foreach ( $tags as $tag ) { + // Tag can be empty. + if ( ! $tag ) { + continue; + } + $hash_tags[] = \sprintf( '', \esc_url( \get_tag_link( $tag ) ), @@ -67,16 +72,36 @@ class Shortcodes { /** * Generates output for the 'ap_title' Shortcode * + * @param array $atts The Shortcode attributes. + * @param string $content The ActivityPub post-content. + * @param string $tag The tag/name of the Shortcode. + * * @return string The post title. */ - public static function title() { + public static function title( $atts, $content, $tag ) { $item = self::get_item(); if ( ! $item ) { return ''; } - return \wp_strip_all_tags( \get_the_title( $item->ID ), true ); + $title = \wp_strip_all_tags( \get_the_title( $item->ID ), true ); + + if ( ! $title ) { + return ''; + } + + $atts = shortcode_atts( + array( 'type' => 'plain' ), + $atts, + $tag + ); + + if ( 'html' !== $atts['type'] ) { + return $title; + } + + return sprintf( '

%s

', $title ); } /** @@ -109,6 +134,7 @@ class Shortcodes { $excerpt = generate_post_summary( $item, $excerpt_length ); + /** This filter is documented in wp-includes/post-template.php */ return \apply_filters( 'the_excerpt', $excerpt ); } @@ -145,23 +171,28 @@ class Shortcodes { if ( empty( $content ) ) { $content = get_post_meta( $item->ID, '_wp_attachment_image_alt', true ); } - } else { - $content = \get_post_field( 'post_content', $item ); - - if ( 'yes' === $atts['apply_filters'] ) { - $content = \apply_filters( 'the_content', $content ); - } else { - $content = do_blocks( $content ); - $content = wptexturize( $content ); - $content = wp_filter_content_tags( $content ); - } - - // Replace script and style elements. - $content = \preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $content ); - $content = strip_shortcodes( $content ); - $content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) ); } + if ( empty( $content ) ) { + $content = \get_post_field( 'post_content', $item ); + } + + if ( 'yes' === $atts['apply_filters'] ) { + /** This filter is documented in wp-includes/post-template.php */ + $content = \apply_filters( 'the_content', $content ); + } else { + if ( site_supports_blocks() ) { + $content = \do_blocks( $content ); + } + $content = \wptexturize( $content ); + $content = \wp_filter_content_tags( $content ); + } + + // Replace script and style elements. + $content = \preg_replace( '@<(script|style)[^>]*?>.*?@si', '', $content ); + $content = \strip_shortcodes( $content ); + $content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) ); + add_shortcode( 'ap_content', array( 'Activitypub\Shortcodes', 'content' ) ); return $content; @@ -191,7 +222,7 @@ class Shortcodes { $tag ); - if ( 'url' === $atts['type'] ) { + if ( 'html' !== $atts['type'] ) { return \esc_url( \get_permalink( $item->ID ) ); } @@ -225,7 +256,7 @@ class Shortcodes { $tag ); - if ( 'url' === $atts['type'] ) { + if ( 'html' !== $atts['type'] ) { return \esc_url( \wp_get_shortlink( $item->ID ) ); } diff --git a/wp-content/plugins/activitypub/includes/class-signature.php b/wp-content/plugins/activitypub/includes/class-signature.php index b8f8af88..350c4d3b 100644 --- a/wp-content/plugins/activitypub/includes/class-signature.php +++ b/wp-content/plugins/activitypub/includes/class-signature.php @@ -11,7 +11,7 @@ use WP_Error; use DateTime; use DateTimeZone; use WP_REST_Request; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; /** * ActivityPub Signature Class. @@ -193,7 +193,7 @@ class Signature { * @return string The signature. */ public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) { - $user = Users::get_by_id( $user_id ); + $user = Actors::get_by_id( $user_id ); $key = self::get_private_key_for( $user->get__id() ); $url_parts = \wp_parse_url( $url ); @@ -223,7 +223,7 @@ class Signature { \openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 ); $signature = \base64_encode( $signature ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode - $key_id = $user->get_url() . '#main-key'; + $key_id = $user->get_id() . '#main-key'; if ( ! empty( $digest ) ) { return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $key_id, $signature ); @@ -267,24 +267,15 @@ class Signature { $headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0]; } - if ( ! isset( $headers['signature'] ) ) { - return new WP_Error( 'activitypub_signature', __( 'Request not signed', 'activitypub' ), array( 'status' => 401 ) ); - } - if ( array_key_exists( 'signature', $headers ) ) { $signature_block = self::parse_signature_header( $headers['signature'][0] ); } elseif ( array_key_exists( 'authorization', $headers ) ) { $signature_block = self::parse_signature_header( $headers['authorization'][0] ); - } - - if ( ! isset( $signature_block ) || ! $signature_block ) { + } else { return new WP_Error( 'activitypub_signature', __( 'Incompatible request signature. keyId and signature are required', 'activitypub' ), array( 'status' => 401 ) ); } $signed_headers = $signature_block['headers']; - if ( ! $signed_headers ) { - $signed_headers = array( 'date' ); - } $signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers ); if ( ! $signed_data ) { @@ -321,7 +312,6 @@ class Signature { } $verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0; - if ( ! $verified ) { return new WP_Error( 'activitypub_signature', __( 'Invalid signature', 'activitypub' ), array( 'status' => 401 ) ); } @@ -333,7 +323,7 @@ class Signature { * * @param string $key_id The URL to the public key. * - * @return WP_Error|string The public key or WP_Error. + * @return resource|WP_Error The public key resource or WP_Error. */ public static function get_remote_key( $key_id ) { $actor = get_remote_metadata_by_actor( strip_fragment_from_url( $key_id ) ); @@ -344,9 +334,14 @@ class Signature { array( 'status' => 401 ) ); } + if ( isset( $actor['publicKey']['publicKeyPem'] ) ) { - return \rtrim( $actor['publicKey']['publicKeyPem'] ); + $key_resource = \openssl_pkey_get_public( \rtrim( $actor['publicKey']['publicKeyPem'] ) ); + if ( $key_resource ) { + return $key_resource; + } } + return new WP_Error( 'activitypub_no_remote_key_found', __( 'No Public-Key found', 'activitypub' ), @@ -403,7 +398,7 @@ class Signature { $parsed_header['signature'] = \base64_decode( preg_replace( '/\s+/', '', trim( $matches[1] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode } - if ( ( $parsed_header['signature'] ) && ( $parsed_header['algorithm'] ) && ( ! $parsed_header['headers'] ) ) { + if ( empty( $parsed_header['headers'] ) ) { $parsed_header['headers'] = array( 'date' ); } @@ -461,6 +456,10 @@ class Signature { } } if ( 'date' === $header ) { + if ( empty( $headers[ $header ][0] ) ) { + continue; + } + // Allow a bit of leeway for misconfigured clocks. $d = new DateTime( $headers[ $header ][0] ); $d->setTimeZone( new DateTimeZone( 'UTC' ) ); @@ -474,7 +473,10 @@ class Signature { return false; } } - $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; + + if ( ! empty( $headers[ $header ][0] ) ) { + $signed_data .= $header . ': ' . $headers[ $header ][0] . "\n"; + } } return \rtrim( $signed_data, "\n" ); } diff --git a/wp-content/plugins/activitypub/includes/class-webfinger.php b/wp-content/plugins/activitypub/includes/class-webfinger.php index f6a189af..b53aa285 100644 --- a/wp-content/plugins/activitypub/includes/class-webfinger.php +++ b/wp-content/plugins/activitypub/includes/class-webfinger.php @@ -8,7 +8,7 @@ namespace Activitypub; use WP_Error; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; /** * ActivityPub WebFinger Class. @@ -26,7 +26,7 @@ class Webfinger { * @return string The user-resource. */ public static function get_user_resource( $user_id ) { - $user = Users::get_by_id( $user_id ); + $user = Actors::get_by_id( $user_id ); if ( ! $user || is_wp_error( $user ) ) { return ''; } @@ -62,6 +62,7 @@ class Webfinger { foreach ( $data['links'] as $link ) { if ( 'self' === $link['rel'] && + isset( $link['type'] ) && ( 'application/activity+json' === $link['type'] || 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' === $link['type'] @@ -84,6 +85,8 @@ class Webfinger { /** * Transform a URI to an acct @. * + * @see https://swicg.github.io/activitypub-webfinger/#reverse-discovery + * * @param string $uri The URI (acct:, mailto:, http:, https:). * * @return string|WP_Error Error or acct URI. diff --git a/wp-content/plugins/activitypub/includes/collection/class-actors.php b/wp-content/plugins/activitypub/includes/collection/class-actors.php new file mode 100644 index 00000000..d4dbd6ec --- /dev/null +++ b/wp-content/plugins/activitypub/includes/collection/class-actors.php @@ -0,0 +1,378 @@ + 404 ) + ); + } + + switch ( $user_id ) { + case self::BLOG_USER_ID: + return new Blog(); + case self::APPLICATION_USER_ID: + return new Application(); + default: + return User::from_wp_user( $user_id ); + } + } + + /** + * Get the Actor by username. + * + * @param string $username Name of the Actor. + * + * @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found. + */ + public static function get_by_username( $username ) { + /** + * Filter the username before we do anything else. + * + * @param null $pre The pre-existing value. + * @param string $username The username. + */ + $pre = apply_filters( 'activitypub_pre_get_by_username', null, $username ); + if ( null !== $pre ) { + return $pre; + } + + // Check for blog user. + if ( Blog::get_default_username() === $username ) { + return new Blog(); + } + + if ( get_option( 'activitypub_blog_identifier' ) === $username ) { + return new Blog(); + } + + // Check for application user. + if ( 'application' === $username ) { + return new Application(); + } + + // Check for 'activitypub_username' meta. + $user = new WP_User_Query( + array( + 'count_total' => false, + 'number' => 1, + 'hide_empty' => true, + 'fields' => 'ID', + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + 'relation' => 'OR', + array( + 'key' => '_activitypub_user_identifier', + 'value' => $username, + 'compare' => 'LIKE', + ), + ), + ) + ); + + if ( $user->results ) { + $actor = self::get_by_id( $user->results[0] ); + if ( ! \is_wp_error( $actor ) ) { + return $actor; + } + } + + $username = str_replace( array( '*', '%' ), '', $username ); + + // Check for login or nicename. + $user = new WP_User_Query( + array( + 'count_total' => false, + 'search' => $username, + 'search_columns' => array( 'user_login', 'user_nicename' ), + 'number' => 1, + 'hide_empty' => true, + 'fields' => 'ID', + ) + ); + + if ( $user->results ) { + $actor = self::get_by_id( $user->results[0] ); + if ( ! \is_wp_error( $actor ) ) { + return $actor; + } + } + + return new WP_Error( + 'activitypub_user_not_found', + \__( 'Actor not found', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + /** + * Get the Actor by resource. + * + * @param string $uri The Actor resource. + * + * @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found. + */ + public static function get_by_resource( $uri ) { + $uri = object_to_uri( $uri ); + + if ( ! $uri ) { + return new WP_Error( + 'activitypub_no_uri', + \__( 'No URI provided', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + $scheme = 'acct'; + $match = array(); + // Try to extract the scheme and the host. + if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $uri, $match ) ) { + // Extract the scheme. + $scheme = \esc_attr( $match[1] ); + } + + // @todo: handle old domain URIs here before we serve a new domain below when we shouldn't. + // Although maybe passing through to ::get_by_username() is enough? + + switch ( $scheme ) { + // Check for http(s) URIs. + case 'http': + case 'https': + $resource_path = \wp_parse_url( $uri, PHP_URL_PATH ); + + if ( $resource_path ) { + $blog_path = \wp_parse_url( \home_url(), PHP_URL_PATH ); + + if ( $blog_path ) { + $resource_path = \str_replace( $blog_path, '', $resource_path ); + } + + $resource_path = \trim( $resource_path, '/' ); + + // Check for http(s)://blog.example.com/@username. + if ( str_starts_with( $resource_path, '@' ) ) { + $identifier = \str_replace( '@', '', $resource_path ); + $identifier = \trim( $identifier, '/' ); + + return self::get_by_username( $identifier ); + } + } + + // Check for http(s)://blog.example.com/author/username. + $user_id = url_to_authorid( $uri ); + + if ( \is_int( $user_id ) ) { + return self::get_by_id( $user_id ); + } + + // Check for http(s)://blog.example.com/. + $normalized_uri = normalize_url( $uri ); + + if ( + normalize_url( site_url() ) === $normalized_uri || + normalize_url( home_url() ) === $normalized_uri + ) { + return self::get_by_id( self::BLOG_USER_ID ); + } + + return new WP_Error( + 'activitypub_no_user_found', + \__( 'Actor not found', 'activitypub' ), + array( 'status' => 404 ) + ); + // Check for acct URIs. + case 'acct': + $uri = \str_replace( 'acct:', '', $uri ); + $identifier = \substr( $uri, 0, \strrpos( $uri, '@' ) ); + $host = normalize_host( \substr( \strrchr( $uri, '@' ), 1 ) ); + $blog_host = normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); + + if ( $blog_host !== $host && get_option( 'activitypub_old_host' ) !== $host ) { + return new WP_Error( + 'activitypub_wrong_host', + \__( 'Resource host does not match blog host', 'activitypub' ), + array( 'status' => 404 ) + ); + } + + // Prepare wildcards https://github.com/mastodon/mastodon/issues/22213. + if ( in_array( $identifier, array( '_', '*', '' ), true ) ) { + return self::get_by_id( self::BLOG_USER_ID ); + } + + return self::get_by_username( $identifier ); + default: + return new WP_Error( + 'activitypub_wrong_scheme', + \__( 'Wrong scheme', 'activitypub' ), + array( 'status' => 404 ) + ); + } + } + + /** + * Get the Actor by resource. + * + * @param string $id The Actor resource. + * + * @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found. + */ + public static function get_by_various( $id ) { + $user = null; + + if ( is_numeric( $id ) ) { + $user = self::get_by_id( $id ); + } elseif ( + // Is URL. + filter_var( $id, FILTER_VALIDATE_URL ) || + // Is acct. + str_starts_with( $id, 'acct:' ) || + // Is email. + filter_var( $id, FILTER_VALIDATE_EMAIL ) + ) { + $user = self::get_by_resource( $id ); + } else { + $user = self::get_by_username( $id ); + } + + return $user; + } + + /** + * Get the Actor collection. + * + * @return array The Actor collection. + */ + public static function get_collection() { + if ( is_user_type_disabled( 'user' ) ) { + return array(); + } + + $users = \get_users( + array( + 'capability__in' => array( 'activitypub' ), + ) + ); + + $return = array(); + + foreach ( $users as $user ) { + $actor = User::from_wp_user( $user->ID ); + + if ( \is_wp_error( $actor ) ) { + continue; + } + + $return[] = $actor; + } + + return $return; + } + + /** + * Get all active Actors including the Blog Actor. + * + * @return array The actor collection. + */ + public static function get_all() { + $return = array(); + + if ( ! is_user_type_disabled( 'user' ) ) { + $users = \get_users( + array( + 'capability__in' => array( 'activitypub' ), + ) + ); + + foreach ( $users as $user ) { + $actor = User::from_wp_user( $user->ID ); + + if ( \is_wp_error( $actor ) ) { + continue; + } + + $return[] = $actor; + } + } + + // Also include the blog actor if active. + if ( ! is_user_type_disabled( 'blog' ) ) { + $blog_actor = self::get_by_id( self::BLOG_USER_ID ); + if ( ! \is_wp_error( $blog_actor ) ) { + $return[] = $blog_actor; + } + } + + return $return; + } + + /** + * Returns the actor type based on the user ID. + * + * @param int $user_id The user ID to check. + * @return string The user type. + */ + public static function get_type_by_id( $user_id ) { + $user_id = (int) $user_id; + + if ( self::APPLICATION_USER_ID === $user_id ) { + return 'application'; + } + + if ( self::BLOG_USER_ID === $user_id ) { + return 'blog'; + } + + return 'user'; + } +} diff --git a/wp-content/plugins/activitypub/includes/collection/class-extra-fields.php b/wp-content/plugins/activitypub/includes/collection/class-extra-fields.php index 320ed478..417f24ea 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-extra-fields.php +++ b/wp-content/plugins/activitypub/includes/collection/class-extra-fields.php @@ -42,6 +42,15 @@ class Extra_Fields { $query = new \WP_Query( $args ); $fields = $query->posts ?? array(); + /** + * Filters the extra fields for an ActivityPub actor. + * + * This filter allows developers to modify or add custom fields to an actor's + * profile. + * + * @param \WP_Post[] $fields Array of WP_Post objects representing the extra fields. + * @param int $user_id The ID of the user whose fields are being retrieved. + */ return apply_filters( 'activitypub_get_actor_extra_fields', $fields, $user_id ); } @@ -54,7 +63,7 @@ class Extra_Fields { */ public static function get_formatted_content( $post ) { $content = \get_the_content( null, false, $post ); - $content = Link::the_content( $content, true ); + $content = Link::the_content( $content ); if ( site_supports_blocks() ) { $content = \do_blocks( $content ); } @@ -84,14 +93,7 @@ class Extra_Fields { */ public static function fields_to_attachments( $fields ) { $attachments = array(); - \add_filter( - 'activitypub_link_rel', - function ( $rel ) { - $rel .= ' me'; - - return $rel; - } - ); + \add_filter( 'activitypub_link_rel', array( self::class, 'add_rel_me' ) ); foreach ( $fields as $post ) { $content = self::get_formatted_content( $post ); @@ -105,7 +107,7 @@ class Extra_Fields { ), ); - $link_added = false; + $attachment = false; // Add support for FEP-fb2a, for more information see FEDERATION.md. $link_content = \trim( \strip_tags( $content, '' ) ); @@ -123,14 +125,17 @@ class Extra_Fields { 'type' => 'Link', 'name' => \get_the_title( $post ), 'href' => \esc_url( $tags->get_attribute( 'href' ) ), - 'rel' => explode( ' ', $tags->get_attribute( 'rel' ) ), ); - $link_added = true; + $rel = $tags->get_attribute( 'rel' ); + + if ( $rel && \is_string( $rel ) ) { + $attachment['rel'] = \explode( ' ', $rel ); + } } } - if ( ! $link_added ) { + if ( ! $attachment ) { $attachment = array( 'type' => 'Note', 'name' => \get_the_title( $post ), @@ -145,6 +150,8 @@ class Extra_Fields { $attachments[] = $attachment; } + \remove_filter( 'activitypub_link_rel', array( self::class, 'add_rel_me' ) ); + return $attachments; } @@ -271,6 +278,16 @@ class Extra_Fields { return '

' . $content . '

'; } + /** + * Add the 'me' rel to the link. + * + * @param string $rel The rel attribute. + * @return string The modified rel attribute. + */ + public static function add_rel_me( $rel ) { + return $rel . ' me'; + } + /** * Checks if the user is the blog user. * @@ -278,6 +295,6 @@ class Extra_Fields { * @return bool True if the user is the blog user, otherwise false. */ private static function is_blog( $user_id ) { - return Users::BLOG_USER_ID === $user_id; + return Actors::BLOG_USER_ID === $user_id; } } diff --git a/wp-content/plugins/activitypub/includes/collection/class-followers.php b/wp-content/plugins/activitypub/includes/collection/class-followers.php index 76611ff9..24be3507 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-followers.php +++ b/wp-content/plugins/activitypub/includes/collection/class-followers.php @@ -7,9 +7,9 @@ namespace Activitypub\Collection; +use Activitypub\Model\Follower; use WP_Error; use WP_Query; -use Activitypub\Model\Follower; use function Activitypub\is_tombstone; use function Activitypub\get_remote_metadata_by_actor; @@ -52,11 +52,11 @@ class Followers { return $id; } - $post_meta = get_post_meta( $id, 'activitypub_user_id', false ); + $post_meta = get_post_meta( $id, '_activitypub_user_id', false ); // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict if ( is_array( $post_meta ) && ! in_array( $user_id, $post_meta ) ) { - add_post_meta( $id, 'activitypub_user_id', $user_id ); + add_post_meta( $id, '_activitypub_user_id', $user_id ); wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' ); } @@ -80,7 +80,16 @@ class Followers { return false; } - return delete_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id ); + /** + * Fires before a Follower is removed. + * + * @param Follower $follower The Follower object. + * @param int $user_id The ID of the WordPress User. + * @param string $actor The Actor URL. + */ + do_action( 'activitypub_followers_pre_remove_follower', $follower, $user_id, $actor ); + + return delete_post_meta( $follower->get__id(), '_activitypub_user_id', $user_id ); } /** @@ -89,7 +98,7 @@ class Followers { * @param int $user_id The ID of the WordPress User. * @param string $actor The Actor URL. * - * @return Follower|null The Follower object or null + * @return Follower|false|null The Follower object or null */ public static function get_follower( $user_id, $actor ) { global $wpdb; @@ -97,7 +106,7 @@ class Followers { // phpcs:ignore WordPress.DB.DirectDatabaseQuery $post_id = $wpdb->get_var( $wpdb->prepare( - "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s", + "SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s", array( esc_sql( self::POST_TYPE ), esc_sql( $user_id ), @@ -119,7 +128,7 @@ class Followers { * * @param string $actor The Actor URL. * - * @return \Activitypub\Activity\Base_Object|WP_Error|null + * @return Follower|false|null The Follower object or false on failure. */ public static function get_follower_by_actor( $actor ) { global $wpdb; @@ -147,7 +156,7 @@ class Followers { * @param int $number Maximum number of results to return. * @param int $page Page number. * @param array $args The WP_Query arguments. - * @return array List of `Follower` objects. + * @return Follower[] List of `Follower` objects. */ public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) { $data = self::get_followers_with_count( $user_id, $number, $page, $args ); @@ -165,8 +174,8 @@ class Followers { * @return array { * Data about the followers. * - * @type array $followers List of `Follower` objects. - * @type int $total Total number of followers. + * @type Follower[] $followers List of `Follower` objects. + * @type int $total Total number of followers. * } */ public static function get_followers_with_count( $user_id, $number = -1, $page = null, $args = array() ) { @@ -179,7 +188,7 @@ class Followers { // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( - 'key' => 'activitypub_user_id', + 'key' => '_activitypub_user_id', 'value' => $user_id, ), ), @@ -188,12 +197,8 @@ class Followers { $args = wp_parse_args( $args, $defaults ); $query = new WP_Query( $args ); $total = $query->found_posts; - $followers = array_map( - function ( $post ) { - return Follower::init_from_cpt( $post ); - }, - $query->get_posts() - ); + $followers = array_map( array( Follower::class, 'init_from_cpt' ), $query->get_posts() ); + $followers = array_filter( $followers ); return compact( 'followers', 'total' ); } @@ -201,7 +206,7 @@ class Followers { /** * Get all Followers. * - * @return array The Term list of Followers. + * @return Follower[] The Term list of Followers. */ public static function get_all_followers() { $args = array( @@ -210,11 +215,11 @@ class Followers { 'meta_query' => array( 'relation' => 'AND', array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'compare' => 'EXISTS', ), array( - 'key' => 'activitypub_actor_json', + 'key' => '_activitypub_actor_json', 'compare' => 'EXISTS', ), ), @@ -238,15 +243,15 @@ class Followers { 'meta_query' => array( 'relation' => 'AND', array( - 'key' => 'activitypub_user_id', + 'key' => '_activitypub_user_id', 'value' => $user_id, ), array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'compare' => 'EXISTS', ), array( - 'key' => 'activitypub_actor_json', + 'key' => '_activitypub_actor_json', 'compare' => 'EXISTS', ), ), @@ -257,7 +262,7 @@ class Followers { } /** - * Returns all Inboxes for a Users Followers. + * Returns all Inboxes for an Actor's Followers. * * @param int $user_id The ID of the WordPress User. * @@ -271,7 +276,7 @@ class Followers { return $inboxes; } - // Get all Followers of a ID of the WordPress User. + // Get all Followers of an ID of the WordPress User. $posts = new WP_Query( array( 'nopaging' => true, @@ -281,15 +286,15 @@ class Followers { 'meta_query' => array( 'relation' => 'AND', array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'compare' => 'EXISTS', ), array( - 'key' => 'activitypub_user_id', + 'key' => '_activitypub_user_id', 'value' => $user_id, ), array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'value' => '', 'compare' => '!=', ), @@ -309,7 +314,7 @@ class Followers { $wpdb->prepare( "SELECT DISTINCT meta_value FROM {$wpdb->postmeta} WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ") - AND meta_key = 'activitypub_inbox' + AND meta_key = '_activitypub_inbox' AND meta_value IS NOT NULL", $posts ) @@ -321,13 +326,63 @@ class Followers { return $inboxes; } + /** + * Get all Inboxes for a given Activity. + * + * @param string $json The ActivityPub Activity JSON. + * @param int $actor_id The WordPress Actor ID. + * @param int $batch_size Optional. The batch size. Default 50. + * @param int $offset Optional. The offset. Default 0. + * + * @return array The list of Inboxes. + */ + public static function get_inboxes_for_activity( $json, $actor_id, $batch_size = 50, $offset = 0 ) { + $inboxes = self::get_inboxes( $actor_id ); + + if ( self::maybe_add_inboxes_of_blog_user( $json, $actor_id ) ) { + $inboxes = array_fill_keys( $inboxes, 1 ); + foreach ( self::get_inboxes( Actors::BLOG_USER_ID ) as $inbox ) { + $inboxes[ $inbox ] = 1; + } + $inboxes = array_keys( $inboxes ); + } + + return array_slice( $inboxes, $offset, $batch_size ); + } + + /** + * Maybe add Inboxes of the Blog User. + * + * @param string $json The ActivityPub Activity JSON. + * @param int $actor_id The WordPress Actor ID. + * @return bool True if the Inboxes of the Blog User should be added, false otherwise. + */ + public static function maybe_add_inboxes_of_blog_user( $json, $actor_id ) { + // Only if we're in both Blog and User modes. + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + return false; + } + // Only if this isn't the Blog Actor. + if ( Actors::BLOG_USER_ID === $actor_id ) { + return false; + } + + $activity = json_decode( $json, true ); + // Only if this is an Update or Delete. Create handles its own "Announce" in dual user mode. + if ( ! in_array( $activity['type'] ?? null, array( 'Update', 'Delete' ), true ) ) { + return false; + } + + return true; + } + /** * Get all Followers that have not been updated for a given time. * * @param int $number Optional. Limits the result. Default 50. * @param int $older_than Optional. The time in seconds. Default 86400 (1 day). * - * @return array The Term list of Followers. + * @return Follower[] The Term list of Followers. */ public static function get_outdated_followers( $number = 50, $older_than = 86400 ) { $args = array( @@ -345,13 +400,9 @@ class Followers { ); $posts = new WP_Query( $args ); - $items = array(); + $items = array_map( array( Follower::class, 'init_from_cpt' ), $posts->get_posts() ); - foreach ( $posts->get_posts() as $follower ) { - $items[] = Follower::init_from_cpt( $follower ); - } - - return $items; + return array_filter( $items ); } /** @@ -359,7 +410,7 @@ class Followers { * * @param int $number Optional. The number of Followers to return. Default 20. * - * @return array The Term list of Followers. + * @return Follower[] The Term list of Followers. */ public static function get_faulty_followers( $number = 20 ) { $args = array( @@ -369,24 +420,24 @@ class Followers { 'meta_query' => array( 'relation' => 'OR', array( - 'key' => 'activitypub_errors', + 'key' => '_activitypub_errors', 'compare' => 'EXISTS', ), array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'compare' => 'NOT EXISTS', ), array( - 'key' => 'activitypub_actor_json', + 'key' => '_activitypub_actor_json', 'compare' => 'NOT EXISTS', ), array( - 'key' => 'activitypub_inbox', + 'key' => '_activitypub_inbox', 'value' => '', 'compare' => '=', ), array( - 'key' => 'activitypub_actor_json', + 'key' => '_activitypub_actor_json', 'value' => '', 'compare' => '=', ), @@ -394,13 +445,9 @@ class Followers { ); $posts = new WP_Query( $args ); - $items = array(); + $items = array_map( array( Follower::class, 'init_from_cpt' ), $posts->get_posts() ); - foreach ( $posts->get_posts() as $follower ) { - $items[] = Follower::init_from_cpt( $follower ); - } - - return $items; + return array_filter( $items ); } /** @@ -428,7 +475,7 @@ class Followers { return add_post_meta( $post_id, - 'activitypub_errors', + '_activitypub_errors', $error_message ); } diff --git a/wp-content/plugins/activitypub/includes/collection/class-interactions.php b/wp-content/plugins/activitypub/includes/collection/class-interactions.php index 36262e86..dc1f9417 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-interactions.php +++ b/wp-content/plugins/activitypub/includes/collection/class-interactions.php @@ -7,10 +7,12 @@ namespace Activitypub\Collection; +use Activitypub\Webfinger; use WP_Comment_Query; use Activitypub\Comment; use function Activitypub\object_to_uri; +use function Activitypub\is_post_disabled; use function Activitypub\url_to_commentid; use function Activitypub\object_id_to_comment; use function Activitypub\get_remote_metadata_by_actor; @@ -27,7 +29,7 @@ class Interactions { * * @param array $activity The activity-object. * - * @return array|false The comment data or false on failure. + * @return int|false|\WP_Error The comment ID or false or WP_Error on failure. */ public static function add_comment( $activity ) { $commentdata = self::activity_to_comment( $activity ); @@ -36,7 +38,8 @@ class Interactions { return false; } - $in_reply_to = \esc_url_raw( $activity['object']['inReplyTo'] ); + $in_reply_to = object_to_uri( $activity['object']['inReplyTo'] ); + $in_reply_to = \esc_url_raw( $in_reply_to ); $comment_post_id = \url_to_postid( $in_reply_to ); $parent_comment_id = url_to_commentid( $in_reply_to ); @@ -46,8 +49,7 @@ class Interactions { $comment_post_id = $parent_comment->comment_post_ID; } - // Not a reply to a post or comment. - if ( ! $comment_post_id ) { + if ( is_post_disabled( $comment_post_id ) ) { return false; } @@ -97,27 +99,26 @@ class Interactions { } $url = object_to_uri( $activity['object'] ); - $comment_post_id = url_to_postid( $url ); + $comment_post_id = \url_to_postid( $url ); $parent_comment_id = url_to_commentid( $url ); if ( ! $comment_post_id && $parent_comment_id ) { - $parent_comment = get_comment( $parent_comment_id ); + $parent_comment = \get_comment( $parent_comment_id ); $comment_post_id = $parent_comment->comment_post_ID; } - if ( ! $comment_post_id ) { + if ( ! $comment_post_id || is_post_disabled( $comment_post_id ) ) { // Not a reply to a post or comment. return false; } - $type = $activity['type']; + $comment_type = Comment::get_comment_type_by_activity_type( $activity['type'] ); - if ( ! Comment::is_registered_comment_type( $type ) ) { + if ( ! $comment_type ) { // Not a valid comment type. return false; } - $comment_type = Comment::get_comment_type( $type ); $comment_content = $comment_type['excerpt']; $commentdata['comment_post_ID'] = $comment_post_id; @@ -178,20 +179,19 @@ class Interactions { $actor = object_to_uri( $meta['url'] ); } - $args = array( + $args = array( 'nopaging' => true, 'author_url' => $actor, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query 'meta_query' => array( array( - 'key' => 'protocol', - 'value' => 'activitypub', - 'compare' => '=', + 'key' => 'protocol', + 'value' => 'activitypub', ), ), ); - $comment_query = new WP_Comment_Query( $args ); - return $comment_query->comments; + + return get_comments( $args ); } /** @@ -229,7 +229,7 @@ class Interactions { */ public static function activity_to_comment( $activity ) { $comment_content = null; - $actor = object_to_uri( $activity['actor'] ); + $actor = object_to_uri( $activity['actor'] ?? null ); $actor = get_remote_metadata_by_actor( $actor ); // Check Actor-Meta. @@ -246,22 +246,29 @@ class Interactions { return false; } - $url = object_to_uri( $actor['url'] ); + $url = object_to_uri( $actor['url'] ?? $actor['id'] ); if ( ! $url ) { - object_to_uri( $actor['id'] ); + $url = object_to_uri( $actor['id'] ); } if ( isset( $activity['object']['content'] ) ) { $comment_content = \addslashes( $activity['object']['content'] ); } + $webfinger = Webfinger::uri_to_acct( $url ); + if ( is_wp_error( $webfinger ) ) { + $webfinger = ''; + } else { + $webfinger = str_replace( 'acct:', '', $webfinger ); + } + $commentdata = array( 'comment_author' => \esc_attr( $comment_author ), 'comment_author_url' => \esc_url_raw( $url ), 'comment_content' => $comment_content, 'comment_type' => 'comment', - 'comment_author_email' => '', + 'comment_author_email' => $webfinger, 'comment_meta' => array( 'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ), 'protocol' => 'activitypub', @@ -289,7 +296,7 @@ class Interactions { */ public static function persist( $commentdata, $action = self::INSERT ) { // Disable flood control. - \remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 ); + \remove_action( 'check_comment_flood', 'check_comment_flood_db' ); // Do not require email for AP entries. \add_filter( 'pre_option_require_name_email', '__return_false' ); // No nonce possible for this submission route. @@ -307,7 +314,7 @@ class Interactions { $state = \wp_update_comment( $commentdata, true ); } - \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 ); + \remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ) ); \remove_filter( 'pre_option_require_name_email', '__return_false' ); // Restore flood control. \add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 ); @@ -318,4 +325,25 @@ class Interactions { return $state; // Either WP_Comment, false, a WP_Error, 0, or 1! } } + + /** + * Get the total number of interactions by type for a given ID. + * + * @param int $post_id The post ID. + * @param string $type The type of interaction to count. + * + * @return int The total number of interactions. + */ + public static function count_by_type( $post_id, $type ) { + return \get_comments( + array( + 'post_id' => $post_id, + 'status' => 'approve', + 'type' => $type, + 'count' => true, + 'paging' => false, + 'fields' => 'ids', + ) + ); + } } diff --git a/wp-content/plugins/activitypub/includes/collection/class-outbox.php b/wp-content/plugins/activitypub/includes/collection/class-outbox.php new file mode 100644 index 00000000..cf1dff89 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/collection/class-outbox.php @@ -0,0 +1,351 @@ +get_object() ); + + if ( ! $activity->get_actor() ) { + $activity->set_actor( Actors::get_by_id( $user_id )->get_id() ); + } + + $outbox_item = array( + 'post_type' => self::POST_TYPE, + 'post_title' => sprintf( + /* translators: 1. Activity type, 2. Object Title or Excerpt */ + __( '[%1$s] %2$s', 'activitypub' ), + $activity->get_type(), + \wp_trim_words( $title, 5 ) + ), + 'post_content' => wp_slash( $activity->to_json() ), + // ensure that user ID is not below 0. + 'post_author' => \max( $user_id, 0 ), + 'post_status' => 'pending', + 'meta_input' => array( + '_activitypub_object_id' => $object_id, + '_activitypub_activity_type' => $activity->get_type(), + '_activitypub_activity_actor' => $actor_type, + 'activitypub_content_visibility' => $visibility, + ), + ); + + $has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' ); + if ( $has_kses ) { + // Prevent KSES from corrupting JSON in post_content. + \kses_remove_filters(); + } + + $id = \wp_insert_post( $outbox_item, true ); + + // Update the activity ID if the post was inserted successfully. + if ( $id && ! \is_wp_error( $id ) ) { + $activity->set_id( \get_the_guid( $id ) ); + + \wp_update_post( + array( + 'ID' => $id, + 'post_content' => \wp_slash( $activity->to_json() ), + ) + ); + } + + if ( $has_kses ) { + \kses_init_filters(); + } + + if ( \is_wp_error( $id ) ) { + return $id; + } + + if ( ! $id ) { + return false; + } + + self::invalidate_existing_items( $object_id, $activity->get_type(), $id ); + + return $id; + } + + /** + * Invalidate existing outbox items with the same activity type and object ID + * by setting their status to 'publish'. + * + * @param string $object_id The ID of the activity object. + * @param string $activity_type The type of the activity. + * @param int $current_id The ID of the current outbox item to exclude. + * + * @return void + */ + private static function invalidate_existing_items( $object_id, $activity_type, $current_id ) { + // Do not invalidate items for Announce activities. + if ( 'Announce' === $activity_type ) { + return; + } + + $meta_query = array( + array( + 'key' => '_activitypub_object_id', + 'value' => $object_id, + ), + ); + + // For non-Delete activities, only invalidate items of the same type. + if ( 'Delete' !== $activity_type ) { + $meta_query[] = array( + 'key' => '_activitypub_activity_type', + 'value' => $activity_type, + ); + } + + $existing_items = get_posts( + array( + 'post_type' => self::POST_TYPE, + 'post_status' => 'pending', + 'exclude' => array( $current_id ), + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => $meta_query, + 'fields' => 'ids', + ) + ); + + foreach ( $existing_items as $existing_item_id ) { + $event_args = array( + Dispatcher::$callback, + $existing_item_id, + Dispatcher::$batch_size, + \get_post_meta( $existing_item_id, '_activitypub_outbox_offset', true ) ?: 0, // phpcs:ignore + ); + + $timestamp = \wp_next_scheduled( 'activitypub_async_batch', $event_args ); + \wp_unschedule_event( $timestamp, 'activitypub_async_batch', $event_args ); + + $timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $existing_item_id ) ); + \wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $existing_item_id ) ); + + \wp_publish_post( $existing_item_id ); + \delete_post_meta( $existing_item_id, '_activitypub_outbox_offset' ); + } + } + + /** + * Creates an Undo activity. + * + * @param int|\WP_Post $outbox_item The Outbox post or post ID. + * + * @return int|bool The ID of the outbox item or false on failure. + */ + public static function undo( $outbox_item ) { + $outbox_item = get_post( $outbox_item ); + $activity = self::get_activity( $outbox_item ); + + $type = 'Undo'; + if ( 'Create' === $activity->get_type() ) { + $type = 'Delete'; + } elseif ( 'Add' === $activity->get_type() ) { + $type = 'Remove'; + } + + return add_to_outbox( $activity, $type, $outbox_item->post_author ); + } + + /** + * Reschedule an activity. + * + * @param int|\WP_Post $outbox_item The Outbox post or post ID. + * + * @return bool True if the activity was rescheduled, false otherwise. + */ + public static function reschedule( $outbox_item ) { + $outbox_item = get_post( $outbox_item ); + + $outbox_item->post_status = 'pending'; + $outbox_item->post_date = current_time( 'mysql' ); + + wp_update_post( $outbox_item ); + + Scheduler::schedule_outbox_activity_for_federation( $outbox_item->ID ); + + return true; + } + + /** + * Get the Activity object from the Outbox item. + * + * @param int|\WP_Post $outbox_item The Outbox post or post ID. + * @return Activity|\WP_Error The Activity object or WP_Error. + */ + public static function get_activity( $outbox_item ) { + $outbox_item = get_post( $outbox_item ); + $actor = self::get_actor( $outbox_item ); + if ( is_wp_error( $actor ) ) { + return $actor; + } + + $activity_object = \json_decode( $outbox_item->post_content, true ); + $type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); + + if ( $activity_object['type'] === $type ) { + $activity = Activity::init_from_array( $activity_object ); + if ( ! $activity->get_actor() ) { + $activity->set_actor( $actor->get_id() ); + } + } else { + $activity = new Activity(); + $activity->set_type( $type ); + $activity->set_id( $outbox_item->guid ); + $activity->set_actor( $actor->get_id() ); + // Pre-fill the Activity with data (for example cc and to). + $activity->set_object( $activity_object ); + } + + if ( 'Update' === $type ) { + $activity->set_updated( gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, strtotime( $outbox_item->post_modified ) ) ); + } + + /** + * Filters the Activity object before it is returned. + * + * @param Activity $activity The Activity object. + * @param \WP_Post $outbox_item The outbox item post object. + */ + return apply_filters( 'activitypub_get_outbox_activity', $activity, $outbox_item ); + } + + /** + * Get the Actor object from the Outbox item. + * + * @param \WP_Post $outbox_item The Outbox post. + * + * @return \Activitypub\Model\User|\Activitypub\Model\Blog|\WP_Error The Actor object or WP_Error. + */ + public static function get_actor( $outbox_item ) { + $actor_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_actor', true ); + + switch ( $actor_type ) { + case 'blog': + $actor_id = Actors::BLOG_USER_ID; + break; + case 'application': + $actor_id = Actors::APPLICATION_USER_ID; + break; + case 'user': + default: + $actor_id = $outbox_item->post_author; + break; + } + + return Actors::get_by_id( $actor_id ); + } + + /** + * Get the Activity object from the Outbox item. + * + * @param \WP_Post $outbox_item The Outbox post. + * + * @return Activity|\WP_Error The Activity object or WP_Error. + */ + public static function maybe_get_activity( $outbox_item ) { + if ( ! $outbox_item instanceof \WP_Post ) { + return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' ); + } + + if ( 'ap_outbox' !== $outbox_item->post_type ) { + return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' ); + } + + // Check if Outbox Activity is public. + $visibility = \get_post_meta( $outbox_item->ID, 'activitypub_content_visibility', true ); + + if ( ! in_array( $visibility, array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ), true ) ) { + return new \WP_Error( 'private_outbox_item', 'Not a public Outbox item.' ); + } + + $activity_types = \apply_filters( 'rest_activitypub_outbox_activity_types', array( 'Announce', 'Create', 'Like', 'Update' ) ); + $activity_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true ); + + if ( ! in_array( $activity_type, $activity_types, true ) ) { + return new \WP_Error( 'private_outbox_item', 'Not public Outbox item type.' ); + } + + return self::get_activity( $outbox_item ); + } + + /** + * Get the object ID of an activity. + * + * @param Activity|Base_Object|string $data The activity object. + * + * @return string The object ID. + */ + private static function get_object_id( $data ) { + $object = $data->get_object(); + + if ( is_object( $object ) ) { + return self::get_object_id( $object ); + } + + if ( is_string( $object ) ) { + return $object; + } + + return $data->get_id() ?? $data->get_actor(); + } + + /** + * Get the title of an activity recursively. + * + * @param Base_Object $activity_object The activity object. + * + * @return string The title. + */ + private static function get_object_title( $activity_object ) { + if ( ! $activity_object ) { + return ''; + } + + if ( is_string( $activity_object ) ) { + $post_id = url_to_postid( $activity_object ); + + return $post_id ? get_the_title( $post_id ) : ''; + } + + $title = $activity_object->get_name() ?? $activity_object->get_content(); + + if ( ! $title && $activity_object->get_object() instanceof Base_Object ) { + $title = $activity_object->get_object()->get_name() ?? $activity_object->get_object()->get_content(); + } + + return $title; + } +} diff --git a/wp-content/plugins/activitypub/includes/collection/class-replies.php b/wp-content/plugins/activitypub/includes/collection/class-replies.php index 34a5aa02..25b47d21 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-replies.php +++ b/wp-content/plugins/activitypub/includes/collection/class-replies.php @@ -12,9 +12,14 @@ use WP_Comment; use WP_Error; use Activitypub\Comment; +use Activitypub\Model\Blog; +use Activitypub\Transformer\Post as PostTransformer; +use Activitypub\Transformer\Comment as CommentTransformer; +use function Activitypub\is_post_disabled; use function Activitypub\is_local_comment; use function Activitypub\get_rest_url_by_path; +use function Activitypub\is_user_type_disabled; /** * Class containing code for getting replies Collections and CollectionPages of posts and comments. @@ -23,13 +28,14 @@ class Replies { /** * Build base arguments for fetching the comments of either a WordPress post or comment. * - * @param WP_Post|WP_Comment $wp_object The post or comment to fetch replies for. + * @param WP_Post|WP_Comment|WP_Error $wp_object The post or comment to fetch replies for on success. */ private static function build_args( $wp_object ) { $args = array( 'status' => 'approve', 'orderby' => 'comment_date_gmt', 'order' => 'ASC', + 'type' => 'comment', ); if ( $wp_object instanceof WP_Post ) { @@ -44,23 +50,6 @@ class Replies { return $args; } - /** - * Adds pagination args comments query. - * - * @param array $args Query args built by self::build_args. - * @param int $page The current pagination page. - * @param int $comments_per_page The number of comments per page. - */ - private static function add_pagination_args( $args, $page, $comments_per_page ) { - $args['number'] = $comments_per_page; - - $offset = intval( $page ) * $comments_per_page; - $args['offset'] = $offset; - - return $args; - } - - /** * Get the replies collections ID. * @@ -74,22 +63,22 @@ class Replies { } elseif ( $wp_object instanceof WP_Comment ) { return get_rest_url_by_path( sprintf( 'comments/%d/replies', $wp_object->comment_ID ) ); } else { - return new WP_Error(); + return new WP_Error( 'unsupported_object', 'The object is not a post or comment.' ); } } /** - * Get the replies collection. + * Get the Replies collection. * * @param WP_Post|WP_Comment $wp_object The post or comment to fetch replies for. * - * @return array An associative array containing the replies collection without JSON-LD context. + * @return array|\WP_Error|null An associative array containing the replies collection without JSON-LD context on success. */ public static function get_collection( $wp_object ) { $id = self::get_id( $wp_object ); - if ( ! $id ) { - return null; + if ( is_wp_error( $id ) ) { + return \wp_is_serving_rest_request() ? $id : null; } $replies = array( @@ -97,38 +86,11 @@ class Replies { 'type' => 'Collection', ); - $replies['first'] = self::get_collection_page( $wp_object, 0, $replies['id'] ); + $replies['first'] = self::get_collection_page( $wp_object, 1, $replies['id'] ); return $replies; } - /** - * Get the ActivityPub ID's from a list of comments. - * - * It takes only federated/non-local comments into account, others also do not have an - * ActivityPub ID available. - * - * @param WP_Comment[] $comments The comments to retrieve the ActivityPub ids from. - * - * @return string[] A list of the ActivityPub ID's. - */ - private static function get_reply_ids( $comments ) { - $comment_ids = array(); - // Only add external comments from the fediverse. - // Maybe use the Comment class more and the function is_local_comment etc. - foreach ( $comments as $comment ) { - if ( is_local_comment( $comment ) ) { - continue; - } - - $public_comment_id = Comment::get_source_id( $comment->comment_ID ); - if ( $public_comment_id ) { - $comment_ids[] = $public_comment_id; - } - } - return $comment_ids; - } - /** * Returns a replies collection page as an associative array. * @@ -136,33 +98,34 @@ class Replies { * * @param WP_Post|WP_Comment $wp_object The post of comment the replies are for. * @param int $page The current pagination page. - * @param string $part_of The collection id/url the returned CollectionPage belongs to. + * @param string $part_of Optional. The collection id/url the returned CollectionPage belongs to. Default null. * - * @return array A CollectionPage as an associative array. + * @return array|WP_Error|null A CollectionPage as an associative array on success, WP_Error or null on failure. */ public static function get_collection_page( $wp_object, $page, $part_of = null ) { // Build initial arguments for fetching approved comments. $args = self::build_args( $wp_object ); + if ( is_wp_error( $args ) ) { + return \wp_is_serving_rest_request() ? $args : null; + } // Retrieve the partOf if not already given. $part_of = $part_of ?? self::get_id( $wp_object ); // If the collection page does not exist. - if ( is_wp_error( $args ) || is_wp_error( $part_of ) ) { - return null; + if ( is_wp_error( $part_of ) ) { + return \wp_is_serving_rest_request() ? $part_of : null; } // Get to total replies count. $total_replies = \get_comments( array_merge( $args, array( 'count' => true ) ) ); - // Modify query args to retrieve paginated results. - $comments_per_page = \get_option( 'comments_per_page' ); + // If set to zero, we get errors below. You need at least one comment per page, here. + $args['number'] = max( (int) \get_option( 'comments_per_page' ), 1 ); + $args['offset'] = intval( $page - 1 ) * $args['number']; - // Fetch internal and external comments for current page. - $comments = get_comments( self::add_pagination_args( $args, $page, $comments_per_page ) ); - - // Get the ActivityPub ID's of the comments, without out local-only comments. - $comment_ids = self::get_reply_ids( $comments ); + // Get the ActivityPub ID's of the comments, without local-only comments. + $comment_ids = self::get_reply_ids( \get_comments( $args ) ); // Build the associative CollectionPage array. $collection_page = array( @@ -172,10 +135,92 @@ class Replies { 'items' => $comment_ids, ); - if ( $total_replies / $comments_per_page > $page + 1 ) { + if ( ( $total_replies / $args['number'] ) > $page ) { $collection_page['next'] = \add_query_arg( 'page', $page + 1, $part_of ); } + if ( $page > 1 ) { + $collection_page['prev'] = \add_query_arg( 'page', $page - 1, $part_of ); + } + return $collection_page; } + + /** + * Get the context collection for a post. + * + * @param int $post_id The post ID. + * + * @return array|false The context for the post or false if the post is not found or disabled. + */ + public static function get_context_collection( $post_id ) { + $post = \get_post( $post_id ); + + if ( ! $post || is_post_disabled( $post_id ) ) { + return false; + } + + $comments = \get_comments( + array( + 'post_id' => $post_id, + 'type' => 'comment', + 'status' => 'approve', + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + ) + ); + $ids = self::get_reply_ids( $comments, true ); + $post_uri = ( new PostTransformer( $post ) )->to_id(); + \array_unshift( $ids, $post_uri ); + + $author = Actors::get_by_id( $post->post_author ); + if ( is_wp_error( $author ) ) { + if ( is_user_type_disabled( 'blog' ) ) { + return false; + } + + $author = new Blog(); + } + + return array( + 'type' => 'OrderedCollection', + 'url' => \get_permalink( $post_id ), + 'attributedTo' => $author->get_id(), + 'totalItems' => count( $ids ), + 'items' => $ids, + ); + } + + /** + * Get the ActivityPub ID's from a list of comments. + * + * It takes only federated/non-local comments into account, others also do not have an + * ActivityPub ID available. + * + * @param WP_Comment[] $comments The comments to retrieve the ActivityPub ids from. + * @param boolean $include_blog_comments Optional. Include blog comments in the returned array. Default false. + * + * @return string[] A list of the ActivityPub ID's. + */ + private static function get_reply_ids( $comments, $include_blog_comments = false ) { + $comment_ids = array(); + + foreach ( $comments as $comment ) { + if ( is_local_comment( $comment ) ) { + continue; + } + + $public_comment_id = Comment::get_source_id( $comment->comment_ID ); + if ( $public_comment_id ) { + $comment_ids[] = $public_comment_id; + continue; + } + + if ( $include_blog_comments ) { + $comment_ids[] = ( new CommentTransformer( $comment ) )->to_id(); + } + } + + return \array_unique( $comment_ids ); + } } diff --git a/wp-content/plugins/activitypub/includes/collection/class-users.php b/wp-content/plugins/activitypub/includes/collection/class-users.php index 036d210a..12194903 100644 --- a/wp-content/plugins/activitypub/includes/collection/class-users.php +++ b/wp-content/plugins/activitypub/includes/collection/class-users.php @@ -7,36 +7,12 @@ namespace Activitypub\Collection; -use WP_Error; -use WP_User_Query; -use Activitypub\Model\User; -use Activitypub\Model\Blog; -use Activitypub\Model\Application; - -use function Activitypub\object_to_uri; -use function Activitypub\normalize_url; -use function Activitypub\normalize_host; -use function Activitypub\url_to_authorid; -use function Activitypub\is_user_disabled; - /** * Users collection. + * + * @deprecated version 4.2.0 */ -class Users { - /** - * The ID of the Blog User. - * - * @var int - */ - const BLOG_USER_ID = 0; - - /** - * The ID of the Application User. - * - * @var int - */ - const APPLICATION_USER_ID = -1; - +class Users extends Actors { /** * Get the User by ID. * @@ -45,31 +21,9 @@ class Users { * @return User|Blog|Application|WP_Error The User or WP_Error if user not found. */ public static function get_by_id( $user_id ) { - if ( is_string( $user_id ) || is_numeric( $user_id ) ) { - $user_id = (int) $user_id; - } + _deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_id' ); - if ( is_user_disabled( $user_id ) ) { - return new WP_Error( - 'activitypub_user_not_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); - } - - if ( self::BLOG_USER_ID === $user_id ) { - return new Blog(); - } elseif ( self::APPLICATION_USER_ID === $user_id ) { - return new Application(); - } elseif ( $user_id > 0 ) { - return User::from_wp_user( $user_id ); - } - - return new WP_Error( - 'activitypub_user_not_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); + return parent::get_by_id( $user_id ); } /** @@ -80,66 +34,9 @@ class Users { * @return User|Blog|Application|WP_Error The User or WP_Error if user not found. */ public static function get_by_username( $username ) { - // Check for blog user. - if ( Blog::get_default_username() === $username ) { - return new Blog(); - } + _deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_username' ); - if ( get_option( 'activitypub_blog_identifier' ) === $username ) { - return new Blog(); - } - - // Check for application user. - if ( 'application' === $username ) { - return new Application(); - } - - // Check for 'activitypub_username' meta. - $user = new WP_User_Query( - array( - 'count_total' => false, - 'number' => 1, - 'hide_empty' => true, - 'fields' => 'ID', - // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query - 'meta_query' => array( - 'relation' => 'OR', - array( - 'key' => 'activitypub_user_identifier', - 'value' => $username, - 'compare' => 'LIKE', - ), - ), - ) - ); - - if ( $user->results ) { - return self::get_by_id( $user->results[0] ); - } - - $username = str_replace( array( '*', '%' ), '', $username ); - - // Check for login or nicename. - $user = new WP_User_Query( - array( - 'count_total' => false, - 'search' => $username, - 'search_columns' => array( 'user_login', 'user_nicename' ), - 'number' => 1, - 'hide_empty' => true, - 'fields' => 'ID', - ) - ); - - if ( $user->results ) { - return self::get_by_id( $user->results[0] ); - } - - return new WP_Error( - 'activitypub_user_not_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); + return parent::get_by_username( $username ); } /** @@ -150,88 +47,9 @@ class Users { * @return User|WP_Error The User or WP_Error if user not found. */ public static function get_by_resource( $uri ) { - $uri = object_to_uri( $uri ); + _deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_resource' ); - $scheme = 'acct'; - $match = array(); - // Try to extract the scheme and the host. - if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $uri, $match ) ) { - // Extract the scheme. - $scheme = \esc_attr( $match[1] ); - } - - switch ( $scheme ) { - // Check for http(s) URIs. - case 'http': - case 'https': - $resource_path = \wp_parse_url( $uri, PHP_URL_PATH ); - - if ( $resource_path ) { - $blog_path = \wp_parse_url( \home_url(), PHP_URL_PATH ); - - if ( $blog_path ) { - $resource_path = \str_replace( $blog_path, '', $resource_path ); - } - - $resource_path = \trim( $resource_path, '/' ); - - // Check for http(s)://blog.example.com/@username. - if ( str_starts_with( $resource_path, '@' ) ) { - $identifier = \str_replace( '@', '', $resource_path ); - $identifier = \trim( $identifier, '/' ); - - return self::get_by_username( $identifier ); - } - } - - // Check for http(s)://blog.example.com/author/username. - $user_id = url_to_authorid( $uri ); - - if ( $user_id ) { - return self::get_by_id( $user_id ); - } - - // Check for http(s)://blog.example.com/. - if ( - normalize_url( site_url() ) === normalize_url( $uri ) || - normalize_url( home_url() ) === normalize_url( $uri ) - ) { - return self::get_by_id( self::BLOG_USER_ID ); - } - - return new WP_Error( - 'activitypub_no_user_found', - \__( 'User not found', 'activitypub' ), - array( 'status' => 404 ) - ); - // Check for acct URIs. - case 'acct': - $uri = \str_replace( 'acct:', '', $uri ); - $identifier = \substr( $uri, 0, \strrpos( $uri, '@' ) ); - $host = normalize_host( \substr( \strrchr( $uri, '@' ), 1 ) ); - $blog_host = normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) ); - - if ( $blog_host !== $host ) { - return new WP_Error( - 'activitypub_wrong_host', - \__( 'Resource host does not match blog host', 'activitypub' ), - array( 'status' => 404 ) - ); - } - - // Prepare wildcards https://github.com/mastodon/mastodon/issues/22213. - if ( in_array( $identifier, array( '_', '*', '' ), true ) ) { - return self::get_by_id( self::BLOG_USER_ID ); - } - - return self::get_by_username( $identifier ); - default: - return new WP_Error( - 'activitypub_wrong_scheme', - \__( 'Wrong scheme', 'activitypub' ), - array( 'status' => 404 ) - ); - } + return parent::get_by_resource( $uri ); } /** @@ -242,26 +60,9 @@ class Users { * @return User|Blog|Application|WP_Error The User or WP_Error if user not found. */ public static function get_by_various( $id ) { - $user = null; + _deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_various' ); - if ( is_numeric( $id ) ) { - $user = self::get_by_id( $id ); - } elseif ( - // Is URL. - filter_var( $id, FILTER_VALIDATE_URL ) || - // Is acct. - str_starts_with( $id, 'acct:' ) || - // Is email. - filter_var( $id, FILTER_VALIDATE_EMAIL ) - ) { - $user = self::get_by_resource( $id ); - } - - if ( $user && ! is_wp_error( $user ) ) { - return $user; - } - - return self::get_by_username( $id ); + return parent::get_by_various( $id ); } /** @@ -270,18 +71,8 @@ class Users { * @return array The User collection. */ public static function get_collection() { - $users = \get_users( - array( - 'capability__in' => array( 'activitypub' ), - ) - ); + _deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_collection' ); - $return = array(); - - foreach ( $users as $user ) { - $return[] = User::from_wp_user( $user->ID ); - } - - return $return; + return parent::get_collection(); } } diff --git a/wp-content/plugins/activitypub/includes/compat.php b/wp-content/plugins/activitypub/includes/compat.php index fa8627b7..c5a73a1a 100644 --- a/wp-content/plugins/activitypub/includes/compat.php +++ b/wp-content/plugins/activitypub/includes/compat.php @@ -25,25 +25,6 @@ if ( ! function_exists( 'str_starts_with' ) ) { } } -if ( ! function_exists( 'get_self_link' ) ) { - /** - * Returns the link for the currently displayed feed. - * - * @return string Correct link for the atom:self element. - */ - function get_self_link() { - $host = wp_parse_url( home_url() ); - $path = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : ''; - - /** - * Filters the self link. - * - * @param string $link The self link. - */ - return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . $path ) ) ); - } -} - if ( ! function_exists( 'is_countable' ) ) { /** * Polyfill for `is_countable()` function added in PHP 7.3. @@ -115,3 +96,16 @@ if ( ! function_exists( 'str_contains' ) ) { return false !== strpos( $haystack, $needle ); } } + +if ( ! function_exists( 'wp_is_serving_rest_request' ) ) { + /** + * Polyfill for `wp_is_serving_rest_request()` function added in WordPress 6.5. + * + * @see https://developer.wordpress.org/reference/functions/wp_is_serving_rest_request/ + * + * @return bool True if it's a WordPress REST API request, false otherwise. + */ + function wp_is_serving_rest_request() { + return defined( 'REST_REQUEST' ) && REST_REQUEST; + } +} diff --git a/wp-content/plugins/activitypub/includes/constants.php b/wp-content/plugins/activitypub/includes/constants.php new file mode 100644 index 00000000..8b6c92d0 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/constants.php @@ -0,0 +1,76 @@ +)|(?<=
)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' ); +\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' ); +\defined( 'ACTIVITYPUB_URL_REGEXP' ) || \define( 'ACTIVITYPUB_URL_REGEXP', '(https?:|www\.)\S+[\w\/]' ); +\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title type=\"html\"]\n\n[ap_content]\n\n[ap_hashtags]" ); +\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false ); +\defined( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS', false ); +\defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false ); +\defined( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE' ) || \define( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE', 'wordpress-post-format' ); +\defined( 'ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE' ) || \define( 'ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE', 100 ); + +// The following constants are invariable and define values used throughout the plugin. + +/* + * Mastodon HTML sanitizer. + * + * @see https://docs.joinmastodon.org/spec/activitypub/#sanitization + */ +\define( + 'ACTIVITYPUB_MASTODON_HTML_SANITIZER', + array( + 'p' => array(), + 'span' => array( 'class' => true ), + 'br' => array(), + 'a' => array( + 'href' => true, + 'rel' => true, + 'class' => true, + ), + 'del' => array(), + 'pre' => array(), + 'code' => array(), + 'em' => array(), + 'strong' => array(), + 'b' => array(), + 'i' => array(), + 'u' => array(), + 'ul' => array(), + 'ol' => array( + 'start' => true, + 'reversed' => true, + ), + 'li' => array( 'value' => true ), + 'blockquote' => array(), + 'h1' => array(), + 'h2' => array(), + 'h3' => array(), + 'h4' => array(), + ) +); + +\define( 'ACTIVITYPUB_DATE_TIME_RFC3339', 'Y-m-d\TH:i:s\Z' ); + +// Define Actor-Modes for the plugin. +\define( 'ACTIVITYPUB_ACTOR_MODE', 'actor' ); +\define( 'ACTIVITYPUB_BLOG_MODE', 'blog' ); +\define( 'ACTIVITYPUB_ACTOR_AND_BLOG_MODE', 'actor_blog' ); + +// Post visibility constants. +\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC', '' ); +\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC', 'quiet_public' ); +\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE', 'private' ); +\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL', 'local' ); diff --git a/wp-content/plugins/activitypub/includes/debug.php b/wp-content/plugins/activitypub/includes/debug.php index b289c80d..bc96e969 100644 --- a/wp-content/plugins/activitypub/includes/debug.php +++ b/wp-content/plugins/activitypub/includes/debug.php @@ -19,4 +19,61 @@ function allow_localhost( $parsed_args ) { return $parsed_args; } -add_filter( 'http_request_args', '\Activitypub\allow_localhost' ); +\add_filter( 'http_request_args', '\Activitypub\allow_localhost' ); + +/** + * Debug the outbox post type. + * + * @param array $args The arguments for the post type. + * @param string $post_type The post type. + * + * @return array The arguments for the post type. + */ +function debug_outbox_post_type( $args, $post_type ) { + if ( 'ap_outbox' !== $post_type ) { + return $args; + } + + $args['show_ui'] = true; + $args['menu_icon'] = 'dashicons-upload'; + + return $args; +} +\add_filter( 'register_post_type_args', '\Activitypub\debug_outbox_post_type', 10, 2 ); + +/** + * Debug the outbox post type column. + * + * @param array $columns The columns. + * @param string $post_type The post type. + * + * @return array The updated columns. + */ +function debug_outbox_post_type_column( $columns, $post_type ) { + if ( 'ap_outbox' !== $post_type ) { + return $columns; + } + + $columns['ap_outbox_meta'] = 'Meta'; + + return $columns; +} +\add_filter( 'manage_posts_columns', '\Activitypub\debug_outbox_post_type_column', 10, 2 ); + +/** + * Debug the outbox post type meta. + * + * @param string $column_name The column name. + * @param int $post_id The post ID. + * + * @return void + */ +function manage_posts_custom_column( $column_name, $post_id ) { + if ( 'ap_outbox_meta' === $column_name ) { + $meta = \get_post_meta( $post_id ); + foreach ( $meta as $key => $value ) { + echo \esc_attr( $key ) . ': ' . \esc_html( $value[0] ) . '
'; + } + } +} +\add_action( 'manage_posts_custom_column', '\Activitypub\manage_posts_custom_column', 10, 2 ); diff --git a/wp-content/plugins/activitypub/includes/functions.php b/wp-content/plugins/activitypub/includes/functions.php index 8aad9766..084ea546 100644 --- a/wp-content/plugins/activitypub/includes/functions.php +++ b/wp-content/plugins/activitypub/includes/functions.php @@ -9,8 +9,13 @@ namespace Activitypub; use WP_Error; use Activitypub\Activity\Activity; +use Activitypub\Activity\Actor; +use Activitypub\Activity\Base_Object; +use Activitypub\Collection\Actors; +use Activitypub\Collection\Outbox; use Activitypub\Collection\Followers; -use Activitypub\Collection\Users; +use Activitypub\Transformer\Post; +use Activitypub\Transformer\Factory as Transformer_Factory; /** * Returns the ActivityPub default JSON-context. @@ -20,6 +25,15 @@ use Activitypub\Collection\Users; function get_context() { $context = Activity::JSON_LD_CONTEXT; + /** + * Filters the ActivityPub JSON-LD context. + * + * This filter allows developers to modify or extend the JSON-LD context used + * in ActivityPub responses. The context defines the vocabulary and terms used + * in the ActivityPub JSON objects. + * + * @param array $context The default ActivityPub JSON-LD context array. + */ return \apply_filters( 'activitypub_json_context', $context ); } @@ -61,100 +75,28 @@ function get_webfinger_resource( $user_id ) { /** * Requests the Meta-Data from the Actors profile. * - * @param string $actor The Actor URL. - * @param bool $cached Optional. Whether the result should be cached. Default true. + * @param array|string $actor The Actor array or URL. + * @param bool $cached Optional. Whether the result should be cached. Default true. * * @return array|WP_Error The Actor profile as array or WP_Error on failure. */ function get_remote_metadata_by_actor( $actor, $cached = true ) { + /** + * Filters the metadata before it is retrieved from a remote actor. + * + * Passing a non-false value will effectively short-circuit the remote request, + * returning that value instead. + * + * @param mixed $pre The value to return instead of the remote metadata. + * Default false to continue with the remote request. + * @param string $actor The actor URL. + */ $pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor ); if ( $pre ) { return $pre; } - if ( is_array( $actor ) ) { - if ( array_key_exists( 'id', $actor ) ) { - $actor = $actor['id']; - } elseif ( array_key_exists( 'url', $actor ) ) { - $actor = $actor['url']; - } else { - return new WP_Error( - 'activitypub_no_valid_actor_identifier', - \__( 'The "actor" identifier is not valid', 'activitypub' ), - array( - 'status' => 404, - 'actor' => $actor, - ) - ); - } - } - - if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $actor ) ) { - $actor = Webfinger::resolve( $actor ); - } - - if ( ! $actor ) { - return new WP_Error( - 'activitypub_no_valid_actor_identifier', - \__( 'The "actor" identifier is not valid', 'activitypub' ), - array( - 'status' => 404, - 'actor' => $actor, - ) - ); - } - - if ( is_wp_error( $actor ) ) { - return $actor; - } - - $transient_key = 'activitypub_' . $actor; - - // Only check the cache if needed. - if ( $cached ) { - $metadata = \get_transient( $transient_key ); - - if ( $metadata ) { - return $metadata; - } - } - - if ( ! \wp_http_validate_url( $actor ) ) { - $metadata = new WP_Error( - 'activitypub_no_valid_actor_url', - \__( 'The "actor" is no valid URL', 'activitypub' ), - array( - 'status' => 400, - 'actor' => $actor, - ) - ); - return $metadata; - } - - $response = Http::get( $actor ); - - if ( \is_wp_error( $response ) ) { - return $response; - } - - $metadata = \wp_remote_retrieve_body( $response ); - $metadata = \json_decode( $metadata, true ); - - if ( ! $metadata ) { - $metadata = new WP_Error( - 'activitypub_invalid_json', - \__( 'No valid JSON data', 'activitypub' ), - array( - 'status' => 400, - 'actor' => $actor, - ) - ); - return $metadata; - } - - \set_transient( $transient_key, $metadata, WEEK_IN_SECONDS ); - - return $metadata; + return Http::get_remote_object( $actor, $cached ); } /** @@ -186,22 +128,20 @@ function count_followers( $user_id ) { * * @param string $url Permalink to check. * - * @return int User ID, or 0 on failure. + * @return int|null User ID, or null on failure. */ function url_to_authorid( $url ) { global $wp_rewrite; // Check if url hase the same host. - if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) !== \wp_parse_url( $url, \PHP_URL_HOST ) ) { - return 0; + $request_host = \wp_parse_url( $url, \PHP_URL_HOST ); + if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) !== $request_host && get_option( 'activitypub_old_host' ) !== $request_host ) { + return null; } - // First, check to see if there is a 'author=N' to match against. + // First, check to see if there is an 'author=N' to match against. if ( \preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) { - $id = \absint( $values[1] ); - if ( $id ) { - return $id; - } + return \absint( $values[1] ); } // Check to see if we are using rewrite rules. @@ -209,7 +149,7 @@ function url_to_authorid( $url ) { // Not using rewrite rules, and 'author=N' method failed, so we're out of options. if ( empty( $rewrite ) ) { - return 0; + return null; } // Generate rewrite rule for the author url. @@ -224,7 +164,7 @@ function url_to_authorid( $url ) { } } - return 0; + return null; } /** @@ -346,112 +286,113 @@ function esc_hashtag( $input ) { * @return bool False by default. */ function is_activitypub_request() { - global $wp_query; + return Query::get_instance()->is_activitypub_request(); +} - /* - * ActivityPub requests are currently only made for - * author archives, singular posts, and the homepage. - */ - if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) ) { - return false; - } +/** + * Check if a post is disabled for ActivityPub. + * + * This function checks if the post type supports ActivityPub and if the post is set to be local. + * + * @param mixed $post The post object or ID. + * + * @return boolean True if the post is disabled, false otherwise. + */ +function is_post_disabled( $post ) { + $post = \get_post( $post ); + $disabled = false; - // Check if the current post type supports ActivityPub. - if ( \is_singular() ) { - $queried_object = \get_queried_object(); - $post_type = \get_post_type( $queried_object ); - - if ( ! \post_type_supports( $post_type, 'activitypub' ) ) { - return false; - } - } - - // Check if header already sent. - if ( ! \headers_sent() && ACTIVITYPUB_SEND_VARY_HEADER ) { - // Send Vary header for Accept header. - \header( 'Vary: Accept' ); - } - - // One can trigger an ActivityPub request by adding ?activitypub to the URL. - if ( isset( $wp_query->query_vars['activitypub'] ) ) { + if ( ! $post ) { return true; } - /* - * The other (more common) option to make an ActivityPub request - * is to send an Accept header. - */ - if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) { - $accept = sanitize_text_field( wp_unslash( $_SERVER['HTTP_ACCEPT'] ) ); + $visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true ); - /* - * $accept can be a single value, or a comma separated list of values. - * We want to support both scenarios, - * and return true when the header includes at least one of the following: - * - application/activity+json - * - application/ld+json - * - application/json - */ - if ( preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) { - return true; - } + if ( + ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL === $visibility || + ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE === $visibility || + ! \post_type_supports( $post->post_type, 'activitypub' ) || + 'private' === $post->post_status || + ! empty( $post->post_password ) + ) { + $disabled = true; } - return false; + /** + * Allow plugins to disable posts for ActivityPub. + * + * @param boolean $disabled True if the post is disabled, false otherwise. + * @param \WP_Post $post The post object. + */ + return \apply_filters( 'activitypub_is_post_disabled', $disabled, $post ); +} + +/** + * This function checks if a user is enabled for ActivityPub. + * + * @param int|string $user_id The user ID. + * @return boolean True if the user is enabled, false otherwise. + */ +function user_can_activitypub( $user_id ) { + if ( ! is_numeric( $user_id ) ) { + return false; + } + + switch ( $user_id ) { + case Actors::APPLICATION_USER_ID: + $enabled = true; // Application user is always enabled. + break; + + case Actors::BLOG_USER_ID: + $enabled = ! is_user_type_disabled( 'blog' ); + break; + + default: + if ( ! \get_user_by( 'id', $user_id ) ) { + $enabled = false; + break; + } + + if ( is_user_type_disabled( 'user' ) ) { + $enabled = false; + break; + } + + $enabled = \user_can( $user_id, 'activitypub' ); + } + + /** + * Allow plugins to disable users for ActivityPub. + * + * @deprecated 5.7.0 Use the `activitypub_user_can_activitypub` filter instead. + * + * @param boolean $disabled True if the user is disabled, false otherwise. + * @param int $user_id The user ID. + */ + $enabled = ! \apply_filters_deprecated( 'activitypub_is_user_disabled', array( ! $enabled, $user_id ), '5.7.0', 'activitypub_user_can_activitypub' ); + + /** + * Allow plugins to enable/disable users for ActivityPub. + * + * @param boolean $enabled True if the user is enabled, false otherwise. + * @param int $user_id The user ID. + */ + return apply_filters( 'activitypub_user_can_activitypub', $enabled, $user_id ); } /** * This function checks if a user is disabled for ActivityPub. * + * @deprecated 5.7.0 Use the `user_can_activitypub` function instead. + * * @param int $user_id The user ID. * * @return boolean True if the user is disabled, false otherwise. */ function is_user_disabled( $user_id ) { - $return = false; + _deprecated_function( __FUNCTION__, 'unreleased', 'user_can_activitypub' ); - switch ( $user_id ) { - // if the user is the application user, it's always enabled. - case \Activitypub\Collection\Users::APPLICATION_USER_ID: - $return = false; - break; - // if the user is the blog user, it's only enabled in single-user mode. - case \Activitypub\Collection\Users::BLOG_USER_ID: - if ( is_user_type_disabled( 'blog' ) ) { - $return = true; - break; - } - - $return = false; - break; - // if the user is any other user, it's enabled if it can publish posts. - default: - if ( ! \get_user_by( 'id', $user_id ) ) { - $return = true; - break; - } - - if ( is_user_type_disabled( 'user' ) ) { - $return = true; - break; - } - - if ( ! \user_can( $user_id, 'activitypub' ) ) { - $return = true; - break; - } - - $return = false; - break; - } - - /** - * Allow plugins to disable users for ActivityPub. - * - * @param boolean $return True if the user is disabled, false otherwise. - * @param int $user_id The User-ID. - */ - return apply_filters( 'activitypub_is_user_disabled', $return, $user_id ); + return ! user_can_activitypub( $user_id ); } /** @@ -469,45 +410,45 @@ function is_user_type_disabled( $type ) { case 'blog': if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) { if ( ACTIVITYPUB_SINGLE_USER_MODE ) { - $return = false; + $disabled = false; break; } } if ( \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) { - $return = ACTIVITYPUB_DISABLE_BLOG_USER; + $disabled = ACTIVITYPUB_DISABLE_BLOG_USER; break; } - if ( '1' !== \get_option( 'activitypub_enable_blog_user', '0' ) ) { - $return = true; + if ( ACTIVITYPUB_ACTOR_MODE === \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + $disabled = true; break; } - $return = false; + $disabled = false; break; case 'user': if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) { if ( ACTIVITYPUB_SINGLE_USER_MODE ) { - $return = true; + $disabled = true; break; } } if ( \defined( 'ACTIVITYPUB_DISABLE_USER' ) ) { - $return = ACTIVITYPUB_DISABLE_USER; + $disabled = ACTIVITYPUB_DISABLE_USER; break; } - if ( '1' !== \get_option( 'activitypub_enable_users', '1' ) ) { - $return = true; + if ( ACTIVITYPUB_BLOG_MODE === \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) { + $disabled = true; break; } - $return = false; + $disabled = false; break; default: - $return = new WP_Error( + $disabled = new WP_Error( 'activitypub_wrong_user_type', __( 'Wrong user type', 'activitypub' ), array( 'status' => 400 ) @@ -518,10 +459,10 @@ function is_user_type_disabled( $type ) { /** * Allow plugins to disable user types for ActivityPub. * - * @param boolean $return True if the user type is disabled, false otherwise. - * @param string $type The User-Type. + * @param boolean $disabled True if the user type is disabled, false otherwise. + * @param string $type The User-Type. */ - return apply_filters( 'activitypub_is_user_type_disabled', $return, $type ); + return apply_filters( 'activitypub_is_user_type_disabled', $disabled, $type ); } /** @@ -546,14 +487,6 @@ function is_single_user() { * @return boolean True if the site supports the block editor, false otherwise. */ function site_supports_blocks() { - if ( \version_compare( \get_bloginfo( 'version' ), '5.9', '<' ) ) { - return false; - } - - if ( ! \function_exists( 'register_block_type_from_metadata' ) ) { - return false; - } - /** * Allow plugins to disable block editor support, * thus disabling blocks registered by the ActivityPub plugin. @@ -575,7 +508,7 @@ function is_json( $data ) { } /** - * Check whther a blog is public based on the `blog_public` option. + * Check whether a blog is public based on the `blog_public` option. * * @return bool True if public, false if not */ @@ -588,21 +521,6 @@ function is_blog_public() { return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) ); } -/** - * Sanitize a URL. - * - * @param string $value The URL to sanitize. - * - * @return string|null The sanitized URL or null if invalid. - */ -function sanitize_url( $value ) { - if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) { - return null; - } - - return esc_url_raw( $value ); -} - /** * Extract recipient URLs from Activity object. * @@ -663,6 +581,17 @@ function is_activity_public( $data ) { return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true ); } +/** + * Check if passed Activity is a reply. + * + * @param array $data The Activity object as array. + * + * @return boolean True if a reply, false if not. + */ +function is_activity_reply( $data ) { + return ! empty( $data['object']['inReplyTo'] ); +} + /** * Get active users based on a given duration. * @@ -701,7 +630,7 @@ function get_active_users( $duration = 1 ) { } // If blog user is disabled. - if ( is_user_disabled( Users::BLOG_USER_ID ) ) { + if ( ! user_can_activitypub( Actors::BLOG_USER_ID ) ) { return (int) $count; } @@ -733,7 +662,7 @@ function get_total_users() { } // If blog user is disabled. - if ( is_user_disabled( Users::BLOG_USER_ID ) ) { + if ( ! user_can_activitypub( Actors::BLOG_USER_ID ) ) { return (int) $users; } @@ -776,6 +705,10 @@ function object_to_uri( $data ) { return $data; } + if ( is_object( $data ) ) { + $data = $data->to_array(); + } + /* * Check if it is a list, then take first item. * This plugin does not support collections. @@ -796,6 +729,9 @@ function object_to_uri( $data ) { // Return part of Object that makes most sense. switch ( $type ) { + case 'Image': + $data = $data['url']; + break; case 'Link': $data = $data['href']; break; @@ -881,7 +817,6 @@ function set_wp_object_state( $wp_object, $state ) { * Allow plugins to mark WordPress objects as federated. * * @param \WP_Comment|\WP_Post $wp_object The WordPress object. - * @param string $state The state of the object. */ \apply_filters( 'activitypub_mark_wp_object_as_federated', $wp_object ); } @@ -905,6 +840,7 @@ function get_wp_object_state( $wp_object ) { /** * Allow plugins to get the federation state of a WordPress object. * + * @param false $state The state of the object. * @param \WP_Comment|\WP_Post $wp_object The WordPress object. */ return \apply_filters( 'activitypub_get_wp_object_state', false, $wp_object ); @@ -942,8 +878,9 @@ function get_post_type_description( $post_type ) { /** * Allow plugins to get the description of a post type. * - * @param string $description The description of the post type. - * @param \WP_Post_Type $post_type The post type object. + * @param string $description The description of the post type. + * @param string $post_type_name The post type name. + * @param \WP_Post_Type $post_type The post type object. */ return apply_filters( 'activitypub_post_type_description', $description, $post_type->name, $post_type ); } @@ -980,6 +917,11 @@ function get_enclosures( $post_id ) { $enclosures = array_map( function ( $enclosure ) { + // Check if the enclosure is a string. + if ( ! $enclosure || ! is_string( $enclosure ) ) { + return false; + } + $attributes = explode( "\n", $enclosure ); if ( ! isset( $attributes[0] ) || ! \wp_http_validate_url( $attributes[0] ) ) { @@ -988,8 +930,8 @@ function get_enclosures( $post_id ) { return array( 'url' => $attributes[0], - 'length' => isset( $attributes[1] ) ? trim( $attributes[1] ) : null, - 'mediaType' => isset( $attributes[2] ) ? trim( $attributes[2] ) : null, + 'length' => $attributes[1] ?? null, + 'mediaType' => $attributes[2] ?? 'application/octet-stream', ); }, $enclosures @@ -1007,7 +949,7 @@ function get_enclosures( $post_id ) { * * @param int|\WP_Comment $comment Comment ID or comment object. * - * @return \WP_Comment[] Array of ancestor comments or empty array if there are none. + * @return int[] Array of ancestor IDs. */ function get_comment_ancestors( $comment ) { $comment = \get_comment( $comment ); @@ -1135,16 +1077,44 @@ function normalize_host( $host ) { return \str_replace( 'www.', '', $host ); } +/** + * Get the reply intent URI as a JavaScript URI. + * + * @return string The reply intent URI. + */ +function get_reply_intent_js() { + return sprintf( + 'javascript:(()=>{window.open(\'%s\'+encodeURIComponent(window.location.href));})();', + get_reply_intent_url() + ); +} + /** * Get the reply intent URI. * * @return string The reply intent URI. */ -function get_reply_intent_uri() { - return sprintf( - 'javascript:(()=>{window.open(\'%s\'+encodeURIComponent(window.location.href));})();', - esc_url( \admin_url( 'post-new.php?in_reply_to=' ) ) - ); +function get_reply_intent_url() { + /** + * Filters the reply intent parameters. + * + * @param array $params The reply intent parameters. + */ + $params = \apply_filters( 'activitypub_reply_intent_params', array() ); + + $params += array( 'in_reply_to' => '' ); + $query = \http_build_query( $params ); + $path = 'post-new.php?' . $query; + $url = \admin_url( $path ); + + /** + * Filters the reply intent URL. + * + * @param string $url The reply intent URL. + */ + $url = \apply_filters( 'activitypub_reply_intent_url', $url ); + + return esc_url_raw( $url ); } /** @@ -1239,11 +1209,7 @@ function generate_post_summary( $post, $length = 500 ) { $content = \sanitize_post_field( 'post_excerpt', $post->post_excerpt, $post->ID ); if ( $content ) { - /** - * Filters the post excerpt. - * - * @param string $content The post excerpt. - */ + /** This filter is documented in wp-includes/post-template.php */ return \apply_filters( 'the_excerpt', $content ); } @@ -1281,6 +1247,7 @@ function generate_post_summary( $post, $length = 500 ) { /* Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629 + /** This filter is documented in wp-includes/post-template.php return \apply_filters( 'the_excerpt', $content ); */ return $content; @@ -1306,3 +1273,334 @@ function get_content_warning( $post_id ) { return $warning; } + +/** + * Get the ActivityPub ID of a User by the WordPress User ID. + * + * @param int $id The WordPress User ID. + * + * @return string The ActivityPub ID (a URL) of the User. + */ +function get_user_id( $id ) { + $user = Actors::get_by_id( $id ); + + if ( ! $user ) { + return false; + } + + return $user->get_id(); +} + +/** + * Get the ActivityPub ID of a Post by the WordPress Post ID. + * + * @param int $id The WordPress Post ID. + * + * @return string The ActivityPub ID (a URL) of the Post. + */ +function get_post_id( $id ) { + $post = get_post( $id ); + + if ( ! $post ) { + return false; + } + + $transformer = new Post( $post ); + return $transformer->get_id(); +} + +/** + * Check if a URL is from the same domain as the site. + * + * @param string $url The URL to check. + * + * @return boolean True if the URL is from the same domain, false otherwise. + */ +function is_same_domain( $url ) { + $remote = \wp_parse_url( $url, PHP_URL_HOST ); + + if ( ! $remote ) { + return false; + } + + $remote = normalize_host( $remote ); + $self = normalize_host( home_host() ); + + return $remote === $self; +} + +/** + * Get the visibility of a post. + * + * @param int $post_id The post ID. + * + * @return string|false The visibility of the post or false if not found. + */ +function get_content_visibility( $post_id ) { + $post = get_post( $post_id ); + if ( ! $post ) { + return false; + } + + $visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true ); + $_visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC; + $options = array( + ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, + ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, + ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL, + ); + + if ( in_array( $visibility, $options, true ) ) { + $_visibility = $visibility; + } + + /** + * Filters the visibility of a post. + * + * @param string $_visibility The visibility of the post. Possible values are: + * - 'public': Post is public and federated. + * - 'quiet_public': Post is public but not federated. + * - 'local': Post is only visible locally. + * @param \WP_Post $post The post object. + */ + return \apply_filters( 'activitypub_content_visibility', $_visibility, $post ); +} + +/** + * Retrieves the Host for the current site where the front end is accessible. + * + * @return string The host for the current site. + */ +function home_host() { + return \wp_parse_url( \home_url(), PHP_URL_HOST ); +} + +/** + * Returns the website hosts allowed to credit this blog. + * + * @return array|null The attribution domains or null if not found. + */ +function get_attribution_domains() { + if ( '1' !== \get_option( 'activitypub_use_opengraph', '1' ) ) { + return null; + } + + $domains = \get_option( 'activitypub_attribution_domains', home_host() ); + $domains = explode( PHP_EOL, $domains ); + + if ( ! $domains ) { + $domains = null; + } + + return $domains; +} + +/** + * Get the base URL for uploads. + * + * @return string The upload base URL. + */ +function get_upload_baseurl() { + /** + * Early filter to allow plugins to set the upload base URL. + * + * @param string|false $maybe_upload_dir The upload base URL or false if not set. + */ + $maybe_upload_dir = apply_filters( 'pre_activitypub_get_upload_baseurl', false ); + if ( false !== $maybe_upload_dir ) { + return $maybe_upload_dir; + } + + $upload_dir = \wp_get_upload_dir(); + + /** + * Filters the upload base URL. + * + * @param string $upload_dir The upload base URL. Default \wp_get_upload_dir()['baseurl'] + */ + return apply_filters( 'activitypub_get_upload_baseurl', $upload_dir['baseurl'] ); +} + +/** + * Check if Authorized-Fetch is enabled. + * + * @see https://docs.joinmastodon.org/admin/config/#authorized_fetch + * + * @return boolean True if Authorized-Fetch is enabled, false otherwise. + */ +function use_authorized_fetch() { + $use = (bool) \get_option( 'activitypub_authorized_fetch' ); + + /** + * Filters whether to use Authorized-Fetch. + * + * @param boolean $use_authorized_fetch True if Authorized-Fetch is enabled, false otherwise. + */ + return apply_filters( 'activitypub_use_authorized_fetch', $use ); +} + +/** + * Check if an ID is from the same domain as the site. + * + * @param string $id The ID URI to check. + * + * @return boolean True if the ID is a self-pint, false otherwise. + */ +function is_self_ping( $id ) { + $query_string = \wp_parse_url( $id, PHP_URL_QUERY ); + + if ( ! $query_string ) { + return false; + } + + $query = array(); + \parse_str( $query_string, $query ); + + if ( + is_same_domain( $id ) && + in_array( 'c', array_keys( $query ), true ) + ) { + return true; + } + + return false; +} + +/** + * Add an object to the outbox. + * + * @param mixed $data The object to add to the outbox. + * @param string|null $activity_type Optional. The type of the Activity or null if `$data` is an Activity. Default null. + * @param integer $user_id Optional. The User-ID. Default 0. + * @param string $content_visibility Optional. The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`. Default null. + * + * @return boolean|int The ID of the outbox item or false on failure. + */ +function add_to_outbox( $data, $activity_type = null, $user_id = 0, $content_visibility = null ) { + $transformer = Transformer_Factory::get_transformer( $data ); + + if ( ! $transformer || is_wp_error( $transformer ) ) { + return false; + } + + if ( $content_visibility ) { + $transformer->set_content_visibility( $content_visibility ); + } else { + $content_visibility = $transformer->get_content_visibility(); + } + + if ( $activity_type ) { + $activity = $transformer->to_activity( $activity_type ); + } else { + $activity = $transformer->to_object(); + } + + if ( ! $activity || \is_wp_error( $activity ) ) { + return false; + } + + // If the user is disabled, fall back to the blog user when available. + if ( ! user_can_activitypub( $user_id ) ) { + if ( user_can_activitypub( Actors::BLOG_USER_ID ) ) { + $user_id = Actors::BLOG_USER_ID; + } else { + return false; + } + } + + $outbox_activity_id = Outbox::add( $activity, $user_id, $content_visibility ); + + if ( ! $outbox_activity_id ) { + return false; + } + + /** + * Action triggered after an object has been added to the outbox. + * + * @param int $outbox_activity_id The ID of the outbox item. + * @param Activity $activity The activity object. + * @param int $user_id The User-ID. + * @param string $content_visibility The visibility of the content. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`. + */ + \do_action( 'post_activitypub_add_to_outbox', $outbox_activity_id, $activity, $user_id, $content_visibility ); + + set_wp_object_state( $data, 'federated' ); + + return $outbox_activity_id; +} + +/** + * Check if an `$data` is an Activity. + * + * @see https://www.w3.org/ns/activitystreams#activities + * + * @param array|object|string $data The data to check. + * + * @return boolean True if the `$data` is an Activity, false otherwise. + */ +function is_activity( $data ) { + /** + * Filters the activity types. + * + * @param array $types The activity types. + */ + $types = apply_filters( 'activitypub_activity_types', Activity::TYPES ); + + if ( is_string( $data ) ) { + return in_array( $data, $types, true ); + } + + if ( is_array( $data ) && isset( $data['type'] ) ) { + return in_array( $data['type'], $types, true ); + } + + if ( is_object( $data ) && $data instanceof Base_Object ) { + return in_array( $data->get_type(), $types, true ); + } + + return false; +} + +/** + * Check if an `$data` is an Actor. + * + * @see https://www.w3.org/ns/activitystreams#actor + * + * @param array|object|string $data The data to check. + * + * @return boolean True if the `$data` is an Actor, false otherwise. + */ +function is_actor( $data ) { + /** + * Filters the actor types. + * + * @param array $types The actor types. + */ + $types = apply_filters( 'activitypub_actor_types', Actor::TYPES ); + + if ( is_string( $data ) ) { + return in_array( $data, $types, true ); + } + + if ( is_array( $data ) && isset( $data['type'] ) ) { + return in_array( $data['type'], $types, true ); + } + + if ( is_object( $data ) && $data instanceof Base_Object ) { + return in_array( $data->get_type(), $types, true ); + } + + return false; +} + +/** + * Get an ActivityPub embed HTML for a URL. + * + * @param string $url The URL to get the embed for. + * @param boolean $inline_css Whether to inline CSS. Default true. + * + * @return string|false The embed HTML or false if not found. + */ +function get_embed_html( $url, $inline_css = true ) { + return Embed::get_html( $url, $inline_css ); +} diff --git a/wp-content/plugins/activitypub/includes/handler/class-announce.php b/wp-content/plugins/activitypub/includes/handler/class-announce.php index 377a3c6e..660bd028 100644 --- a/wp-content/plugins/activitypub/includes/handler/class-announce.php +++ b/wp-content/plugins/activitypub/includes/handler/class-announce.php @@ -44,10 +44,13 @@ class Announce { return; } - if ( ! ACTIVITYPUB_DISABLE_REACTIONS ) { - self::maybe_save_announce( $announcement, $user_id ); + // Check if reposts are allowed. + if ( ! Comment::is_comment_type_enabled( 'repost' ) ) { + return; } + self::maybe_save_announce( $announcement, $user_id ); + if ( is_string( $announcement['object'] ) ) { $object = Http::get_remote_object( $announcement['object'] ); } else { @@ -67,18 +70,19 @@ class Announce { /** * Fires after an Announce has been received. * - * @param array $object The object. - * @param int $user_id The id of the local blog-user. - * @param array $activity The activity object. + * @param array $object The object. + * @param int $user_id The id of the local blog-user. + * @param string $type The type of the activity. + * @param \Activitypub\Activity\Activity|null $activity The activity object. */ \do_action( 'activitypub_inbox', $object, $user_id, $type, $activity ); /** * Fires after an Announce of a specific type has been received. * - * @param array $object The object. - * @param int $user_id The id of the local blog-user. - * @param array $activity The activity object. + * @param array $object The object. + * @param int $user_id The id of the local blog-user. + * @param \Activitypub\Activity\Activity|null $activity The activity object. */ \do_action( "activitypub_inbox_{$type}", $object, $user_id, $activity ); } diff --git a/wp-content/plugins/activitypub/includes/handler/class-create.php b/wp-content/plugins/activitypub/includes/handler/class-create.php index ebc3722b..6a33eb75 100644 --- a/wp-content/plugins/activitypub/includes/handler/class-create.php +++ b/wp-content/plugins/activitypub/includes/handler/class-create.php @@ -9,6 +9,8 @@ namespace Activitypub\Handler; use Activitypub\Collection\Interactions; +use function Activitypub\is_self_ping; +use function Activitypub\is_activity_reply; use function Activitypub\is_activity_public; use function Activitypub\object_id_to_comment; @@ -44,8 +46,10 @@ class Create { */ public static function handle_create( $activity, $user_id, $activity_object = null ) { // Check if Activity is public or not. - if ( ! is_activity_public( $activity ) ) { - // @todo maybe send email. + if ( + ! is_activity_public( $activity ) || + ! is_activity_reply( $activity ) + ) { return; } @@ -58,13 +62,16 @@ class Create { * * @param array $activity The activity-object. * @param int $user_id The id of the local blog-user. - * @param \WP_Comment|\WP_Error $check_dupe The comment object or WP_Error. * @param \Activitypub\Activity\Activity $activity_object The activity object. */ \do_action( 'activitypub_inbox_update', $activity, $user_id, $activity_object ); return; } + if ( is_self_ping( $activity['object']['id'] ) ) { + return; + } + $state = Interactions::add_comment( $activity ); $reaction = null; @@ -95,6 +102,10 @@ class Create { public static function validate_object( $valid, $param, $request ) { $json_params = $request->get_json_params(); + if ( empty( $json_params['type'] ) ) { + return false; + } + if ( 'Create' !== $json_params['type'] || is_wp_error( $request ) @@ -102,10 +113,14 @@ class Create { return $valid; } - $object = $json_params['object']; + $object = $json_params['object']; + + if ( ! is_array( $object ) ) { + return false; + } + $required = array( 'id', - 'inReplyTo', 'content', ); diff --git a/wp-content/plugins/activitypub/includes/handler/class-delete.php b/wp-content/plugins/activitypub/includes/handler/class-delete.php index 5b514dfa..94faf57d 100644 --- a/wp-content/plugins/activitypub/includes/handler/class-delete.php +++ b/wp-content/plugins/activitypub/includes/handler/class-delete.php @@ -12,6 +12,8 @@ use Activitypub\Http; use Activitypub\Collection\Followers; use Activitypub\Collection\Interactions; +use function Activitypub\object_to_uri; + /** * Handles Delete requests. */ @@ -20,24 +22,10 @@ class Delete { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_action( - 'activitypub_inbox_delete', - array( self::class, 'handle_delete' ) - ); - - // Defer signature verification for `Delete` requests. - \add_filter( - 'activitypub_defer_signature_verification', - array( self::class, 'defer_signature_verification' ), - 10, - 2 - ); - - // Side effect. - \add_action( - 'activitypub_delete_actor_interactions', - array( self::class, 'delete_interactions' ) - ); + \add_action( 'activitypub_inbox_delete', array( self::class, 'handle_delete' ) ); + \add_filter( 'activitypub_defer_signature_verification', array( self::class, 'defer_signature_verification' ), 10, 2 ); + \add_action( 'activitypub_delete_actor_interactions', array( self::class, 'delete_interactions' ) ); + \add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) ); } /** @@ -114,6 +102,7 @@ class Delete { * @param array $activity The delete activity. */ public static function maybe_delete_follower( $activity ) { + /* @var \Activitypub\Model\Follower $follower Follower object. */ $follower = Followers::get_follower_by_actor( $activity['actor'] ); // Verify that Actor is deleted. @@ -142,15 +131,13 @@ class Delete { /** * Delete comments from an Actor. * - * @param array $actor The actor whose comments to delete. + * @param string $actor The URL of the actor whose comments to delete. */ public static function delete_interactions( $actor ) { $comments = Interactions::get_interactions_by_actor( $actor ); - if ( is_array( $comments ) ) { - foreach ( $comments as $comment ) { - wp_delete_comment( $comment->comment_ID ); - } + foreach ( $comments as $comment ) { + wp_delete_comment( $comment, true ); } } @@ -192,4 +179,18 @@ class Delete { return false; } + + /** + * Set the object to the object ID. + * + * @param \Activitypub\Activity\Activity $activity The Activity object. + * @return \Activitypub\Activity\Activity The filtered Activity object. + */ + public static function outbox_activity( $activity ) { + if ( 'Delete' === $activity->get_type() ) { + $activity->set_object( object_to_uri( $activity->get_object() ) ); + } + + return $activity; + } } diff --git a/wp-content/plugins/activitypub/includes/handler/class-follow.php b/wp-content/plugins/activitypub/includes/handler/class-follow.php index fb94ba6d..a1942211 100644 --- a/wp-content/plugins/activitypub/includes/handler/class-follow.php +++ b/wp-content/plugins/activitypub/includes/handler/class-follow.php @@ -7,12 +7,13 @@ namespace Activitypub\Handler; -use Activitypub\Http; use Activitypub\Notification; use Activitypub\Activity\Activity; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; use Activitypub\Collection\Followers; +use function Activitypub\add_to_outbox; + /** * Handle Follow requests. */ @@ -28,7 +29,7 @@ class Follow { \add_action( 'activitypub_followers_post_follow', - array( self::class, 'send_follow_response' ), + array( self::class, 'queue_accept' ), 10, 4 ); @@ -40,7 +41,7 @@ class Follow { * @param array $activity The activity object. */ public static function handle_follow( $activity ) { - $user = Users::get_by_resource( $activity['object'] ); + $user = Actors::get_by_resource( $activity['object'] ); if ( ! $user || is_wp_error( $user ) ) { // If we can not find a user, we can not initiate a follow process. @@ -55,13 +56,15 @@ class Follow { $activity['actor'] ); - do_action( - 'activitypub_followers_post_follow', - $activity['actor'], - $activity, - $user_id, - $follower - ); + /** + * Fires after a new follower has been added. + * + * @param string $actor The URL of the actor (follower) who initiated the follow. + * @param array $activity The complete activity data of the follow request. + * @param int $user_id The ID of the WordPress user being followed. + * @param \Activitypub\Model\Follower|\WP_Error $follower The Follower object containing the new follower's data. + */ + do_action( 'activitypub_followers_post_follow', $activity['actor'], $activity, $user_id, $follower ); // Send notification. $notification = new Notification( @@ -76,12 +79,12 @@ class Follow { /** * Send Accept response. * - * @param string $actor The Actor URL. - * @param array $activity_object The Activity object. - * @param int $user_id The ID of the WordPress User. - * @param \Activitypub\Model\Follower $follower The Follower object. + * @param string $actor The Actor URL. + * @param array $activity_object The Activity object. + * @param int $user_id The ID of the WordPress User. + * @param \Activitypub\Model\Follower|\WP_Error $follower The Follower object. */ - public static function send_follow_response( $actor, $activity_object, $user_id, $follower ) { + public static function queue_accept( $actor, $activity_object, $user_id, $follower ) { if ( \is_wp_error( $follower ) ) { // Impossible to send a "Reject" because we can not get the Remote-Inbox. return; @@ -100,21 +103,12 @@ class Follow { ) ); - $user = Users::get_by_id( $user_id ); - - // Get inbox. - $inbox = $follower->get_shared_inbox(); - - // Send "Accept" activity. $activity = new Activity(); $activity->set_type( 'Accept' ); + $activity->set_actor( Actors::get_by_id( $user_id )->get_id() ); $activity->set_object( $activity_object ); - $activity->set_actor( $user->get_id() ); - $activity->set_to( $actor ); - $activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() ); + $activity->set_to( array( $actor ) ); - $activity = $activity->to_json(); - - Http::post( $inbox, $activity, $user_id ); + add_to_outbox( $activity, null, $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE ); } } diff --git a/wp-content/plugins/activitypub/includes/handler/class-like.php b/wp-content/plugins/activitypub/includes/handler/class-like.php index 1e951576..10cbce85 100644 --- a/wp-content/plugins/activitypub/includes/handler/class-like.php +++ b/wp-content/plugins/activitypub/includes/handler/class-like.php @@ -20,12 +20,8 @@ class Like { * Initialize the class, registering WordPress hooks. */ public static function init() { - \add_action( - 'activitypub_inbox_like', - array( self::class, 'handle_like' ), - 10, - 3 - ); + \add_action( 'activitypub_inbox_like', array( self::class, 'handle_like' ), 10, 2 ); + \add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) ); } /** @@ -35,7 +31,7 @@ class Like { * @param int $user_id The ID of the local blog user. */ public static function handle_like( $like, $user_id ) { - if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) { + if ( ! Comment::is_comment_type_enabled( 'like' ) ) { return; } @@ -67,4 +63,18 @@ class Like { */ do_action( 'activitypub_handled_like', $like, $user_id, $state, $reaction ); } + + /** + * Set the object to the object ID. + * + * @param \Activitypub\Activity\Activity $activity The Activity object. + * @return \Activitypub\Activity\Activity The filtered Activity object. + */ + public static function outbox_activity( $activity ) { + if ( 'Like' === $activity->get_type() ) { + $activity->set_object( object_to_uri( $activity->get_object() ) ); + } + + return $activity; + } } diff --git a/wp-content/plugins/activitypub/includes/handler/class-move.php b/wp-content/plugins/activitypub/includes/handler/class-move.php new file mode 100644 index 00000000..71032a45 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/handler/class-move.php @@ -0,0 +1,213 @@ +from_array( $target_object ); + $origin_follower->set_id( $target ); + $origin_id = $origin_follower->upsert(); + + global $wpdb; + // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery + $wpdb->update( + $wpdb->posts, + array( 'guid' => sanitize_url( $target ) ), + array( 'ID' => sanitize_key( $origin_id ) ) + ); + + // Clear the cache. + wp_cache_delete( $origin_id, 'posts' ); + return; + } + + /* + * If the new target is followed, and the origin is followed, + * move users and delete the origin follower. + */ + if ( $target_follower && $origin_follower ) { + $origin_users = \get_post_meta( $origin_follower->get__id(), '_activitypub_user_id', false ); + $target_users = \get_post_meta( $target_follower->get__id(), '_activitypub_user_id', false ); + + // Get all user ids from $origin_users that are not in $target_users. + $users = \array_diff( $origin_users, $target_users ); + + foreach ( $users as $user_id ) { + \add_post_meta( $target_follower->get__id(), '_activitypub_user_id', $user_id ); + } + + $origin_follower->delete(); + } + } + + /** + * Convert the object and origin to the correct format. + * + * @param \Activitypub\Activity\Activity $activity The Activity object. + * @return \Activitypub\Activity\Activity The filtered Activity object. + */ + public static function outbox_activity( $activity ) { + if ( 'Move' === $activity->get_type() ) { + $activity->set_object( object_to_uri( $activity->get_object() ) ); + $activity->set_origin( $activity->get_actor() ); + $activity->set_target( $activity->get_object() ); + } + + return $activity; + } + + /** + * Extract the target from the activity. + * + * The ActivityStreams spec define the `target` attribute as the + * destination of the activity, but Mastodon uses the `object` + * attribute to move profiles. + * + * @param array $activity The JSON "Move" Activity. + * + * @return string|null The target URI or null if not found. + */ + private static function extract_target( $activity ) { + if ( ! empty( $activity['target'] ) ) { + return object_to_uri( $activity['target'] ); + } + + if ( ! empty( $activity['object'] ) ) { + return object_to_uri( $activity['object'] ); + } + + return null; + } + + /** + * Extract the origin from the activity. + * + * The ActivityStreams spec define the `origin` attribute as source + * of the activity, but Mastodon uses the `actor` attribute as source + * to move profiles. + * + * @param array $activity The JSON "Move" Activity. + * + * @return string|null The origin URI or null if not found. + */ + private static function extract_origin( $activity ) { + if ( ! empty( $activity['origin'] ) ) { + return object_to_uri( $activity['origin'] ); + } + + if ( ! empty( $activity['actor'] ) ) { + return object_to_uri( $activity['actor'] ); + } + + return null; + } + + /** + * Verify the move. + * + * @param array $target_object The target object. + * @param array $origin_object The origin object. + * + * @return bool True if the move is verified, false otherwise. + */ + private static function verify_move( $target_object, $origin_object ) { + // Check if both objects are valid. + if ( \is_wp_error( $target_object ) || \is_wp_error( $origin_object ) ) { + return false; + } + + // Check if both objects are persons. + if ( 'Person' !== $target_object['type'] || 'Person' !== $origin_object['type'] ) { + return false; + } + + // Check if the target and origin are not the same. + if ( $target_object['id'] === $origin_object['id'] ) { + return false; + } + + // Check if the target has an alsoKnownAs property. + if ( empty( $target_object['also_known_as'] ) ) { + return false; + } + + // Check if the origin is in the alsoKnownAs property of the target. + if ( ! in_array( $origin_object['id'], $target_object['also_known_as'], true ) ) { + return false; + } + + // Check if the origin has a movedTo property. + if ( empty( $origin_object['movedTo'] ) ) { + return false; + } + + // Check if the movedTo property of the origin is the target. + if ( $origin_object['movedTo'] !== $target_object['id'] ) { + return false; + } + + return true; + } +} diff --git a/wp-content/plugins/activitypub/includes/handler/class-undo.php b/wp-content/plugins/activitypub/includes/handler/class-undo.php index 137f0aaa..dcbc6f72 100644 --- a/wp-content/plugins/activitypub/includes/handler/class-undo.php +++ b/wp-content/plugins/activitypub/includes/handler/class-undo.php @@ -7,7 +7,7 @@ namespace Activitypub\Handler; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; use Activitypub\Collection\Followers; use Activitypub\Comment; @@ -23,16 +23,19 @@ class Undo { public static function init() { \add_action( 'activitypub_inbox_undo', - array( self::class, 'handle_undo' ) + array( self::class, 'handle_undo' ), + 10, + 2 ); } /** * Handle "Unfollow" requests. * - * @param array $activity The JSON "Undo" Activity. + * @param array $activity The JSON "Undo" Activity. + * @param int|null $user_id The ID of the user who initiated the "Undo" activity. */ - public static function handle_undo( $activity ) { + public static function handle_undo( $activity, $user_id ) { if ( ! isset( $activity['object']['type'] ) || ! isset( $activity['object']['object'] ) @@ -40,12 +43,13 @@ class Undo { return; } - $type = $activity['object']['type']; + $type = $activity['object']['type']; + $state = false; // Handle "Unfollow" requests. if ( 'Follow' === $type ) { - $user_id = object_to_uri( $activity['object']['object'] ); - $user = Users::get_by_resource( $user_id ); + $id = object_to_uri( $activity['object']['object'] ); + $user = Actors::get_by_resource( $id ); if ( ! $user || is_wp_error( $user ) ) { // If we can not find a user, we can not initiate a follow process. @@ -55,7 +59,7 @@ class Undo { $user_id = $user->get__id(); $actor = object_to_uri( $activity['actor'] ); - Followers::remove_follower( $user_id, $actor ); + $state = Followers::remove_follower( $user_id, $actor ); } // Handle "Undo" requests for "Like" and "Create" activities. @@ -71,9 +75,16 @@ class Undo { return; } - $state = wp_trash_comment( $comment ); - - do_action( 'activitypub_handled_undo', $activity, $user_id, isset( $state ) ? $state : null, null ); + $state = wp_delete_comment( $comment, true ); } + + /** + * Fires after an "Undo" activity has been handled. + * + * @param array $activity The JSON "Undo" Activity. + * @param int|null $user_id The ID of the user who initiated the "Undo" activity otherwise null. + * @param mixed $state The state of the "Undo" activity. + */ + do_action( 'activitypub_handled_undo', $activity, $user_id, $state ); } } diff --git a/wp-content/plugins/activitypub/includes/handler/class-update.php b/wp-content/plugins/activitypub/includes/handler/class-update.php index 5cc58efe..2a068b29 100644 --- a/wp-content/plugins/activitypub/includes/handler/class-update.php +++ b/wp-content/plugins/activitypub/includes/handler/class-update.php @@ -7,6 +7,7 @@ namespace Activitypub\Handler; +use Activitypub\Collection\Followers; use Activitypub\Collection\Interactions; use function Activitypub\get_remote_metadata_by_actor; @@ -26,9 +27,9 @@ class Update { } /** - * Handle "Update" requests + * Handle "Update" requests. * - * @param array $activity The activity-object. + * @param array $activity The Activity object. */ public static function handle_update( $activity ) { $object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : ''; @@ -75,7 +76,7 @@ class Update { /** * Update an Interaction. * - * @param array $activity The activity-object. + * @param array $activity The Activity object. */ public static function update_interaction( $activity ) { $commentdata = Interactions::update_comment( $activity ); @@ -88,18 +89,37 @@ class Update { $state = $commentdata; } + /** + * Fires after an Update activity has been handled. + * + * @param array $activity The complete Update activity data. + * @param null $user Always null for Update activities. + * @param int|array $state 1 if comment was updated successfully, error data otherwise. + * @param \WP_Comment|null $reaction The updated comment object if successful, null otherwise. + */ \do_action( 'activitypub_handled_update', $activity, null, $state, $reaction ); } /** * Update an Actor. * - * @param array $activity The activity-object. + * @param array $activity The Activity object. */ public static function update_actor( $activity ) { // Update cache. - get_remote_metadata_by_actor( $activity['actor'], false ); + $actor = get_remote_metadata_by_actor( $activity['actor'], false ); - // @todo maybe also update all interactions. + if ( ! $actor || \is_wp_error( $actor ) || ! isset( $actor['id'] ) ) { + return; + } + + $follower = Followers::get_follower_by_actor( $actor['id'] ); + + if ( ! $follower ) { + return; + } + + $follower->from_array( $actor ); + $follower->upsert(); } } diff --git a/wp-content/plugins/activitypub/includes/help.php b/wp-content/plugins/activitypub/includes/help.php deleted file mode 100644 index 0dc1b021..00000000 --- a/wp-content/plugins/activitypub/includes/help.php +++ /dev/null @@ -1,80 +0,0 @@ -add_help_tab( - array( - 'id' => 'template-tags', - 'title' => \__( 'Template Tags', 'activitypub' ), - 'content' => - '

' . __( 'The following Template Tags are available:', 'activitypub' ) . '

' . - '
' . - '
[ap_title]
' . - '
' . \wp_kses( __( 'The post\'s title.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_content apply_filters="yes"]
' . - '
' . \wp_kses( __( 'The post\'s content. With apply_filters you can decide if filters (apply_filters( \'the_content\', $content )) should be applied or not (default is yes). The values can be yes or no. apply_filters attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_excerpt length="400"]
' . - '
' . \wp_kses( __( 'The post\'s excerpt (uses the_excerpt if that is set). If no excerpt is provided, will truncate at length (optional, default = 400).', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_permalink type="url"]
' . - '
' . \wp_kses( __( 'The post\'s permalink. type can be either: url or html (an <a /> tag). type attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_shortlink type="url"]
' . - '
' . \wp_kses( __( 'The post\'s shortlink. type can be either url or html (an <a /> tag). I can recommend Hum, to prettify the Shortlinks. type attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_hashtags]
' . - '
' . \wp_kses( __( 'The post\'s tags as hashtags.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_hashcats]
' . - '
' . \wp_kses( __( 'The post\'s categories as hashtags.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_image type=full]
' . - '
' . \wp_kses( __( 'The URL for the post\'s featured image, defaults to full size. The type attribute can be any of the following: thumbnail, medium, large, full. type attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_author]
' . - '
' . \wp_kses( __( 'The author\'s name.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_authorurl]
' . - '
' . \wp_kses( __( 'The URL to the author\'s profile page.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_date]
' . - '
' . \wp_kses( __( 'The post\'s date.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_time]
' . - '
' . \wp_kses( __( 'The post\'s time.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_datetime]
' . - '
' . \wp_kses( __( 'The post\'s date/time formated as "date @ time".', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_blogurl]
' . - '
' . \wp_kses( __( 'The URL to the site.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_blogname]
' . - '
' . \wp_kses( __( 'The name of the site.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
[ap_blogdesc]
' . - '
' . \wp_kses( __( 'The description of the site.', 'activitypub' ), array( 'code' => array() ) ) . '
' . - '
' . - '

' . __( 'You may also use any Shortcode normally available to you on your site, however be aware that Shortcodes may significantly increase the size of your content depending on what they do.', 'activitypub' ) . '

' . - '

' . __( 'Note: the old Template Tags are now deprecated and automatically converted to the new ones.', 'activitypub' ) . '

' . - '

' . \wp_kses( \__( 'Let me know if you miss a Template Tag.', 'activitypub' ), 'activitypub' ) . '

', - ) -); - -\get_current_screen()->add_help_tab( - array( - 'id' => 'glossary', - 'title' => \__( 'Glossary', 'activitypub' ), - 'content' => - '

' . \__( 'Fediverse', 'activitypub' ) . '

' . - '

' . \__( 'The Fediverse is a new word made of two words: "federation" + "universe"', 'activitypub' ) . '

' . - '

' . \__( 'It is a federated social network running on free open software on a myriad of computers across the globe. Many independent servers are interconnected and allow people to interact with one another. There\'s no one central site: you choose a server to register. This ensures some decentralization and sovereignty of data. Fediverse (also called Fedi) has no built-in advertisements, no tricky algorithms, no one big corporation dictating the rules. Instead we have small cozy communities of like-minded people. Welcome!', 'activitypub' ) . '

' . - '

' . \__( 'For more information please visit fediverse.party', 'activitypub' ) . '

' . - '

' . \__( 'ActivityPub', 'activitypub' ) . '

' . - '

' . \__( 'ActivityPub is a decentralized social networking protocol based on the ActivityStreams 2.0 data format. ActivityPub is an official W3C recommended standard published by the W3C Social Web Working Group. It provides a client to server API for creating, updating and deleting content, as well as a federated server to server API for delivering notifications and subscribing to content.', 'activitypub' ) . '

' . - '

' . \__( 'WebFinger', 'activitypub' ) . '

' . - '

' . \__( 'WebFinger is used to discover information about people or other entities on the Internet that are identified by a URI using standard Hypertext Transfer Protocol (HTTP) methods over a secure transport. A WebFinger resource returns a JavaScript Object Notation (JSON) object describing the entity that is queried. The JSON object is referred to as the JSON Resource Descriptor (JRD).', 'activitypub' ) . '

' . - '

' . \__( 'For a person, the type of information that might be discoverable via WebFinger includes a personal profile address, identity service, telephone number, or preferred avatar. For other entities on the Internet, a WebFinger resource might return JRDs containing link relations that enable a client to discover, for example, that a printer can print in color on A4 paper, the physical location of a server, or other static information.', 'activitypub' ) . '

' . - '

' . \__( 'On Mastodon [and other Plattforms], user profiles can be hosted either locally on the same website as yours, or remotely on a completely different website. The same username may be used on a different domain. Therefore, a Mastodon user\'s full mention consists of both the username and the domain, in the form @username@domain. In practical terms, @user@example.com is not the same as @user@example.org. If the domain is not included, Mastodon will try to find a local user named @username. However, in order to deliver to someone over ActivityPub, the @username@domain mention is not enough – mentions must be translated to an HTTPS URI first, so that the remote actor\'s inbox and outbox can be found. (This paragraph is copied from the Mastodon Documentation)', 'activitypub' ) . '

' . - '

' . \__( 'For more information please visit webfinger.net', 'activitypub' ) . '

' . - '

' . \__( 'NodeInfo', 'activitypub' ) . '

' . - '

' . \__( 'NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. The two key goals are being able to get better insights into the user base of distributed social networking and the ability to build tools that allow users to choose the best fitting software and server for their needs.', 'activitypub' ) . '

' . - '

' . \__( 'For more information please visit nodeinfo.diaspora.software', 'activitypub' ) . '

', - ) -); - -\get_current_screen()->set_help_sidebar( - '

' . \__( 'For more information:', 'activitypub' ) . '

' . - '

' . \__( 'Get support', 'activitypub' ) . '

' . - '

' . \__( 'Report an issue', 'activitypub' ) . '

' -); diff --git a/wp-content/plugins/activitypub/includes/model/class-application.php b/wp-content/plugins/activitypub/includes/model/class-application.php index b7d38af2..0309941e 100644 --- a/wp-content/plugins/activitypub/includes/model/class-application.php +++ b/wp-content/plugins/activitypub/includes/model/class-application.php @@ -10,12 +10,14 @@ namespace Activitypub\Model; use WP_Query; use Activitypub\Signature; use Activitypub\Activity\Actor; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; use function Activitypub\get_rest_url_by_path; /** * Application class. + * + * @method int get__id() Gets the internal user ID for the application (always returns APPLICATION_USER_ID). */ class Application extends Actor { /** @@ -23,7 +25,7 @@ class Application extends Actor { * * @var int */ - protected $_id = Users::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore + protected $_id = Actors::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * Whether the Application is discoverable. @@ -94,7 +96,7 @@ class Application extends Actor { * @return string The User-URL with @-Prefix for the username. */ public function get_alternate_url() { - return $this->get_url(); + return $this->get_id(); } /** @@ -118,7 +120,7 @@ class Application extends Actor { /** * Get the User-Icon. * - * @return array The User-Icon. + * @return string[] The User-Icon. */ public function get_icon() { // Try site icon first. @@ -152,7 +154,7 @@ class Application extends Actor { /** * Get the User-Header-Image. * - * @return array|null The User-Header-Image. + * @return string[]|null The User-Header-Image. */ public function get_header_image() { if ( \has_header_image() ) { @@ -185,7 +187,7 @@ class Application extends Actor { $time = \time(); } - return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); + return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $time ); } /** @@ -218,13 +220,13 @@ class Application extends Actor { /** * Returns the public key. * - * @return array The public key. + * @return string[] The public key. */ public function get_public_key() { return array( 'id' => $this->get_id() . '#main-key', 'owner' => $this->get_id(), - 'publicKeyPem' => Signature::get_public_key_for( Users::APPLICATION_USER_ID ), + 'publicKeyPem' => Signature::get_public_key_for( Actors::APPLICATION_USER_ID ), ); } diff --git a/wp-content/plugins/activitypub/includes/model/class-blog.php b/wp-content/plugins/activitypub/includes/model/class-blog.php index 3cbd6f91..e46e5b02 100644 --- a/wp-content/plugins/activitypub/includes/model/class-blog.php +++ b/wp-content/plugins/activitypub/includes/model/class-blog.php @@ -7,20 +7,22 @@ namespace Activitypub\Model; -use WP_Query; - -use Activitypub\Signature; use Activitypub\Activity\Actor; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; use Activitypub\Collection\Extra_Fields; +use Activitypub\Signature; +use WP_Query; use function Activitypub\esc_hashtag; use function Activitypub\is_single_user; use function Activitypub\is_blog_public; use function Activitypub\get_rest_url_by_path; +use function Activitypub\get_attribution_domains; /** * Blog class. + * + * @method int get__id() Gets the internal user ID for the blog (always returns BLOG_USER_ID). */ class Blog extends Actor { /** @@ -51,7 +53,7 @@ class Blog extends Actor { * * @var int */ - protected $_id = Users::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore + protected $_id = Actors::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore /** * If the User is indexable. @@ -89,6 +91,18 @@ class Blog extends Actor { */ protected $posting_restricted_to_mods; + /** + * Constructor. + */ + public function __construct() { + /** + * Fires when a model actor is constructed. + * + * @param Blog $this The Blog model. + */ + \do_action( 'activitypub_construct_model_actor', $this ); + } + /** * Whether the User manually approves followers. * @@ -113,13 +127,25 @@ class Blog extends Actor { * @return string The User ID. */ public function get_id() { - return $this->get_url(); + $id = parent::get_id(); + + if ( $id ) { + return $id; + } + + $permalink = \get_option( 'activitypub_use_permalink_as_id_for_blog', false ); + + if ( $permalink ) { + return $this->get_url(); + } + + return \add_query_arg( 'author', $this->_id, \trailingslashit( \home_url() ) ); } /** * Get the type of the object. * - * If the Blog is in "single user" mode, return "Person" insted of "Group". + * If the Blog is in "single user" mode, return "Person" instead of "Group". * * @return string The type of the object. */ @@ -197,7 +223,11 @@ class Blog extends Actor { /** * Filters the default blog username. * - * @param string $host The default username. + * This filter allows developers to modify the default username that is + * generated for the blog, which by default is the site's host name + * without the 'www.' prefix. + * + * @param string $host The default username (site's host name). */ return apply_filters( 'activitypub_default_blog_username', $host ); } @@ -220,7 +250,7 @@ class Blog extends Actor { /** * Get the User icon. * - * @return array The User icon. + * @return string[] The User icon. */ public function get_icon() { // Try site_logo, falling back to site_icon, first. @@ -254,7 +284,7 @@ class Blog extends Actor { /** * Get the User-Header-Image. * - * @return array|null The User-Header-Image. + * @return string[]|null The User-Header-Image. */ public function get_image() { $header_image = get_option( 'activitypub_header_image' ); @@ -298,7 +328,7 @@ class Blog extends Actor { $time = \time(); } - return \gmdate( 'Y-m-d\TH:i:s\Z', $time ); + return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $time ); } /** @@ -339,7 +369,7 @@ class Blog extends Actor { /** * Get the public key information. * - * @return array The public key. + * @return string[] The public key. */ public function get_public_key() { return array( @@ -401,12 +431,12 @@ class Blog extends Actor { /** * Returns endpoints. * - * @return array|null The endpoints. + * @return string[]|null The endpoints. */ public function get_endpoints() { $endpoints = null; - if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) { + if ( \get_option( 'activitypub_shared_inbox' ) ) { $endpoints = array( 'sharedInbox' => get_rest_url_by_path( 'inbox' ), ); @@ -497,7 +527,7 @@ class Blog extends Actor { * * @see https://docs.joinmastodon.org/spec/activitypub/#Hashtag * - * @return array The User - Hashtags. + * @return string[] The User - Hashtags. */ public function get_tag() { $hashtags = array(); @@ -530,4 +560,41 @@ class Blog extends Actor { $extra_fields = Extra_Fields::get_actor_fields( $this->_id ); return Extra_Fields::fields_to_attachments( $extra_fields ); } + + /** + * Returns the website hosts allowed to credit this blog. + * + * @return string[]|null The attribution domains or null if not found. + */ + public function get_attribution_domains() { + return get_attribution_domains(); + } + + /** + * Returns the alsoKnownAs. + * + * @return string[] The alsoKnownAs. + */ + public function get_also_known_as() { + $also_known_as = array( + \add_query_arg( 'author', $this->_id, \home_url( '/' ) ), + $this->get_url(), + $this->get_alternate_url(), + ); + + $also_known_as = array_merge( $also_known_as, \get_option( 'activitypub_blog_user_also_known_as', array() ) ); + + return array_unique( $also_known_as ); + } + + /** + * Returns the movedTo. + * + * @return string The movedTo. + */ + public function get_moved_to() { + $moved_to = \get_option( 'activitypub_blog_user_moved_to' ); + + return $moved_to && $moved_to !== $this->get_id() ? $moved_to : null; + } } diff --git a/wp-content/plugins/activitypub/includes/model/class-follower.php b/wp-content/plugins/activitypub/includes/model/class-follower.php index 45ef6157..c7d50ea2 100644 --- a/wp-content/plugins/activitypub/includes/model/class-follower.php +++ b/wp-content/plugins/activitypub/includes/model/class-follower.php @@ -21,6 +21,18 @@ use Activitypub\Collection\Followers; * @author Matthias Pfefferle * * @see https://www.w3.org/TR/activitypub/#follow-activity-inbox + * + * @method int get__id() Gets the post ID of the follower record. + * @method string[]|null get_image() Gets the follower's profile image data. + * @method string|null get_inbox() Gets the follower's ActivityPub inbox URL. + * @method string[]|null get_endpoints() Gets the follower's ActivityPub endpoints. + * + * @method Follower set__id( int $id ) Sets the post ID of the follower record. + * @method Follower set_id( string $guid ) Sets the follower's GUID. + * @method Follower set_name( string $name ) Sets the follower's display name. + * @method Follower set_summary( string $summary ) Sets the follower's bio/summary. + * @method Follower set_published( string $datetime ) Sets the follower's published datetime in ISO 8601 format. + * @method Follower set_updated( string $datetime ) Sets the follower's last updated datetime in ISO 8601 format. */ class Follower extends Actor { /** @@ -36,7 +48,7 @@ class Follower extends Actor { * @return mixed */ public function get_errors() { - return get_post_meta( $this->_id, 'activitypub_errors', false ); + return get_post_meta( $this->_id, '_activitypub_errors', false ); } /** @@ -72,7 +84,7 @@ class Follower extends Actor { * Reset (delete) all errors. */ public function reset_errors() { - delete_post_meta( $this->_id, 'activitypub_errors' ); + delete_post_meta( $this->_id, '_activitypub_errors' ); } /** @@ -216,9 +228,9 @@ class Follower extends Actor { * Update the post meta. */ protected function get_post_meta_input() { - $meta_input = array(); - $meta_input['activitypub_inbox'] = $this->get_shared_inbox(); - $meta_input['activitypub_actor_json'] = $this->to_json(); + $meta_input = array(); + $meta_input['_activitypub_inbox'] = $this->get_shared_inbox(); + $meta_input['_activitypub_actor_json'] = wp_slash( $this->to_json() ); return $meta_input; } @@ -228,7 +240,7 @@ class Follower extends Actor { * * Sets a fallback to better handle API and HTML outputs. * - * @return array The icon. + * @return string[] The icon. */ public function get_icon() { if ( isset( $this->icon['url'] ) ) { @@ -331,11 +343,18 @@ class Follower extends Actor { * Convert a Custom-Post-Type input to an Activitypub\Model\Follower. * * @param \WP_Post $post The post object. - * @return \Activitypub\Activity\Base_Object|WP_Error + * @return Follower|false The Follower object or false on failure. */ public static function init_from_cpt( $post ) { - $actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true ); - $object = self::init_from_json( $actor_json ); + $actor_json = get_post_meta( $post->ID, '_activitypub_actor_json', true ); + + /* @var Follower $object Follower object. */ + $object = self::init_from_json( $actor_json ); + + if ( is_wp_error( $object ) ) { + return false; + } + $object->set__id( $post->ID ); $object->set_id( $post->guid ); $object->set_name( $post->post_title ); diff --git a/wp-content/plugins/activitypub/includes/model/class-user.php b/wp-content/plugins/activitypub/includes/model/class-user.php index 997c5210..964c7867 100644 --- a/wp-content/plugins/activitypub/includes/model/class-user.php +++ b/wp-content/plugins/activitypub/includes/model/class-user.php @@ -7,17 +7,20 @@ namespace Activitypub\Model; -use WP_Error; -use Activitypub\Signature; use Activitypub\Activity\Actor; use Activitypub\Collection\Extra_Fields; +use Activitypub\Http; +use Activitypub\Signature; use function Activitypub\is_blog_public; -use function Activitypub\is_user_disabled; use function Activitypub\get_rest_url_by_path; +use function Activitypub\get_attribution_domains; +use function Activitypub\user_can_activitypub; /** * User class. + * + * @method int get__id() Gets the WordPress user ID. */ class User extends Actor { /** @@ -68,6 +71,24 @@ class User extends Actor { */ protected $webfinger; + /** + * Constructor. + * + * @param int $user_id Optional. The WordPress user ID. Default null. + */ + public function __construct( $user_id = null ) { + if ( $user_id ) { + $this->_id = $user_id; + + /** + * Fires when a model actor is constructed. + * + * @param User $this The User object. + */ + \do_action( 'activitypub_construct_model_actor', $this ); + } + } + /** * The type of the object. * @@ -82,21 +103,18 @@ class User extends Actor { * * @param int $user_id The user ID. * - * @return WP_Error|User The User object or WP_Error if user not found. + * @return \WP_Error|User The User object or \WP_Error if user not found. */ public static function from_wp_user( $user_id ) { - if ( is_user_disabled( $user_id ) ) { - return new WP_Error( + if ( ! user_can_activitypub( $user_id ) ) { + return new \WP_Error( 'activitypub_user_not_found', \__( 'User not found', 'activitypub' ), array( 'status' => 404 ) ); } - $object = new static(); - $object->_id = $user_id; - - return $object; + return new static( $user_id ); } /** @@ -105,7 +123,19 @@ class User extends Actor { * @return string The user ID. */ public function get_id() { - return $this->get_url(); + $id = parent::get_id(); + + if ( $id ) { + return $id; + } + + $permalink = \get_user_option( 'activitypub_use_permalink_as_id', $this->_id ); + + if ( '1' === $permalink ) { + return $this->get_url(); + } + + return \add_query_arg( 'author', $this->_id, \trailingslashit( \home_url() ) ); } /** @@ -114,7 +144,7 @@ class User extends Actor { * @return string The Username. */ public function get_name() { - return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) ); + return \get_the_author_meta( 'display_name', $this->_id ); } /** @@ -154,17 +184,17 @@ class User extends Actor { * @return string The preferred username. */ public function get_preferred_username() { - return \esc_attr( \get_the_author_meta( 'login', $this->_id ) ); + return \get_the_author_meta( 'login', $this->_id ); } /** * Get the User icon. * - * @return array The User icon. + * @return string[] The User icon. */ public function get_icon() { $icon = \get_user_option( 'activitypub_icon', $this->_id ); - if ( wp_attachment_is_image( $icon ) ) { + if ( false !== $icon && wp_attachment_is_image( $icon ) ) { return array( 'type' => 'Image', 'url' => esc_url( wp_get_attachment_url( $icon ) ), @@ -187,7 +217,7 @@ class User extends Actor { /** * Returns the header image. * - * @return array|null The header image. + * @return string[]|null The header image. */ public function get_image() { $header_image = get_user_option( 'activitypub_header_image', $this->_id ); @@ -217,13 +247,13 @@ class User extends Actor { * @return false|string The date the user was created. */ public function get_published() { - return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) ); + return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) ); } /** * Returns the public key. * - * @return array The public key. + * @return string[] The public key. */ public function get_public_key() { return array( @@ -281,12 +311,12 @@ class User extends Actor { /** * Returns the endpoints. * - * @return array|null The endpoints. + * @return string[]|null The endpoints. */ public function get_endpoints() { $endpoints = null; - if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) { + if ( \get_option( 'activitypub_shared_inbox' ) ) { $endpoints = array( 'sharedInbox' => get_rest_url_by_path( 'inbox' ), ); @@ -358,7 +388,7 @@ class User extends Actor { * Update the username. * * @param string $value The new value. - * @return int|WP_Error The updated user ID or WP_Error on failure. + * @return int|\WP_Error The updated user ID or \WP_Error on failure. */ public function update_name( $value ) { $userdata = array( @@ -403,4 +433,42 @@ class User extends Actor { } return \update_user_option( $this->_id, 'activitypub_header_image', $value ); } + + /** + * Returns the website hosts allowed to credit this blog. + * + * @return string[]|null The attribution domains or null if not found. + */ + public function get_attribution_domains() { + return get_attribution_domains(); + } + + /** + * Returns the alsoKnownAs. + * + * @return string[] The alsoKnownAs. + */ + public function get_also_known_as() { + $also_known_as = array( + \add_query_arg( 'author', $this->_id, \home_url( '/' ) ), + $this->get_url(), + $this->get_alternate_url(), + ); + + // phpcs:ignore Universal.Operators.DisallowShortTernary.Found + $also_known_as = array_merge( $also_known_as, \get_user_option( 'activitypub_also_known_as', $this->_id ) ?: array() ); + + return array_unique( $also_known_as ); + } + + /** + * Returns the movedTo. + * + * @return string The movedTo. + */ + public function get_moved_to() { + $moved_to = \get_user_option( 'activitypub_moved_to', $this->_id ); + + return $moved_to && $moved_to !== $this->get_id() ? $moved_to : null; + } } diff --git a/wp-content/plugins/activitypub/includes/rest/class-actors-controller.php b/wp-content/plugins/activitypub/includes/rest/class-actors-controller.php new file mode 100644 index 00000000..15cd0c5c --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-actors-controller.php @@ -0,0 +1,359 @@ +[\w\-\.]+)'; + + /** + * Register routes. + */ + public function register_routes() { + \register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'args' => array( + 'user_id' => array( + 'description' => 'The ID or username of the actor.', + 'type' => 'string', + 'required' => true, + 'pattern' => '[\w\-\.]+', + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + + \register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/remote-follow', + array( + 'args' => array( + 'user_id' => array( + 'description' => 'The ID or username of the actor.', + 'type' => 'string', + 'required' => true, + 'pattern' => '[\w\-\.]+', + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_remote_follow_item' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'resource' => array( + 'description' => 'The resource to follow.', + 'type' => 'string', + 'required' => true, + ), + ), + ), + ) + ); + } + + /** + * Retrieves a single actor. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_item( $request ) { + $user_id = $request->get_param( 'user_id' ); + $user = Actor_Collection::get_by_various( $user_id ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. + */ + \do_action( 'activitypub_rest_users_pre' ); + + $data = $user->to_array(); + + $response = \rest_ensure_response( $data ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + $response->header( 'Link', \sprintf( '<%1$s>; rel="alternate"; type="application/activity+json"', $user->get_id() ) ); + + return $response; + } + + /** + * Retrieves the remote follow endpoint. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_remote_follow_item( $request ) { + $resource = $request->get_param( 'resource' ); + $user_id = $request->get_param( 'user_id' ); + $user = Actor_Collection::get_by_various( $user_id ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + $template = Webfinger::get_remote_follow_endpoint( $resource ); + + if ( \is_wp_error( $template ) ) { + return $template; + } + + $resource = $user->get_webfinger(); + $url = \str_replace( '{uri}', $resource, $template ); + + return \rest_ensure_response( + array( + 'url' => $url, + 'template' => $template, + ) + ); + } + + /** + * Retrieves the actor schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'actor', + 'type' => 'object', + 'properties' => array( + '@context' => array( + 'description' => 'The JSON-LD context for the response.', + 'type' => array( 'array', 'object' ), + 'readonly' => true, + ), + 'id' => array( + 'description' => 'The unique identifier for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'type' => array( + 'description' => 'The type of the actor.', + 'type' => 'string', + 'enum' => array( 'Person', 'Service', 'Organization', 'Application', 'Group' ), + 'readonly' => true, + ), + 'attachment' => array( + 'description' => 'Additional information attached to the actor.', + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'type' => 'string', + 'enum' => array( 'PropertyValue', 'Link' ), + ), + 'name' => array( + 'type' => 'string', + ), + 'value' => array( + 'type' => 'string', + ), + 'href' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'rel' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + ), + ), + 'readonly' => true, + ), + 'name' => array( + 'description' => 'The display name of the actor.', + 'type' => 'string', + 'readonly' => true, + ), + 'icon' => array( + 'description' => 'The icon/avatar of the actor.', + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'type' => 'string', + ), + 'url' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + 'readonly' => true, + ), + 'published' => array( + 'description' => 'The date the actor was published.', + 'type' => 'string', + 'format' => 'date-time', + 'readonly' => true, + ), + 'summary' => array( + 'description' => 'A summary about the actor.', + 'type' => 'string', + 'readonly' => true, + ), + 'tag' => array( + 'description' => 'Tags associated with the actor.', + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'type' => 'string', + ), + 'href' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'name' => array( + 'type' => 'string', + ), + ), + ), + 'readonly' => true, + ), + 'url' => array( + 'description' => 'The URL to the actor\'s profile page.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'inbox' => array( + 'description' => 'The inbox endpoint for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'outbox' => array( + 'description' => 'The outbox endpoint for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'following' => array( + 'description' => 'The following endpoint for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'followers' => array( + 'description' => 'The followers endpoint for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'streams' => array( + 'description' => 'The streams associated with the actor.', + 'type' => 'array', + 'readonly' => true, + ), + 'preferredUsername' => array( + 'description' => 'The preferred username of the actor.', + 'type' => 'string', + 'readonly' => true, + ), + 'publicKey' => array( + 'description' => 'The public key information for the actor.', + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'owner' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'publicKeyPem' => array( + 'type' => 'string', + ), + ), + 'readonly' => true, + ), + 'manuallyApprovesFollowers' => array( + 'description' => 'Whether the actor manually approves followers.', + 'type' => 'boolean', + 'readonly' => true, + ), + 'attributionDomains' => array( + 'description' => 'The attribution domains for the actor.', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'readonly' => true, + ), + 'featured' => array( + 'description' => 'The featured collection endpoint for the actor.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ), + 'indexable' => array( + 'description' => 'Whether the actor is indexable.', + 'type' => 'boolean', + 'readonly' => true, + ), + 'webfinger' => array( + 'description' => 'The webfinger identifier for the actor.', + 'type' => 'string', + 'readonly' => true, + ), + 'discoverable' => array( + 'description' => 'Whether the actor is discoverable.', + 'type' => 'boolean', + 'readonly' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-actors-inbox-controller.php b/wp-content/plugins/activitypub/includes/rest/class-actors-inbox-controller.php new file mode 100644 index 00000000..99fb5b9a --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-actors-inbox-controller.php @@ -0,0 +1,238 @@ +namespace, + '/' . $this->rest_base . '/inbox', + array( + 'args' => array( + 'user_id' => array( + 'description' => 'The ID or username of the actor.', + 'type' => 'string', + 'required' => true, + 'pattern' => '[\w\-\.]+', + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'page' => array( + 'description' => 'Current page of the collection.', + 'type' => 'integer', + 'minimum' => 1, + // No default so we can differentiate between Collection and CollectionPage requests. + ), + 'per_page' => array( + 'description' => 'Maximum number of items to be returned in result set.', + 'type' => 'integer', + 'default' => 20, + 'minimum' => 1, + ), + ), + 'schema' => array( $this, 'get_collection_schema' ), + ), + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ), + 'args' => array( + 'id' => array( + 'description' => 'The unique identifier for the activity.', + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + 'actor' => array( + 'description' => 'The actor performing the activity.', + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => '\Activitypub\object_to_uri', + ), + 'type' => array( + 'description' => 'The type of the activity.', + 'type' => 'string', + 'required' => true, + ), + 'object' => array( + 'description' => 'The object of the activity.', + 'required' => true, + 'validate_callback' => function ( $param, $request, $key ) { + /** + * Filter the ActivityPub object validation. + * + * @param bool $validate The validation result. + * @param array $param The object data. + * @param object $request The request object. + * @param string $key The key. + */ + return \apply_filters( 'activitypub_validate_object', true, $param, $request, $key ); + }, + ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Renders the user-inbox. + * + * @param \WP_REST_Request $request The request object. + * @return \WP_REST_Response|\WP_Error Response object or WP_Error. + */ + public function get_items( $request ) { + $user_id = $request->get_param( 'user_id' ); + $user = Actors::get_by_various( $user_id ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + /** + * Fires before the ActivityPub inbox is created and sent to the client. + */ + \do_action( 'activitypub_rest_inbox_pre' ); + + $response = array( + '@context' => get_context(), + 'id' => get_rest_url_by_path( \sprintf( 'actors/%d/inbox', $user->get__id() ) ), + 'generator' => 'https://wordpress.org/?v=' . get_masked_wp_version(), + 'type' => 'OrderedCollection', + 'totalItems' => 0, + 'orderedItems' => array(), + ); + + /** + * Filters the ActivityPub inbox data before it is sent to the client. + * + * @param array $response The ActivityPub inbox array. + */ + $response = \apply_filters( 'activitypub_rest_inbox_array', $response ); + + $response = $this->prepare_collection_response( $response, $request ); + if ( \is_wp_error( $response ) ) { + return $response; + } + + /** + * Fires after the ActivityPub inbox has been created and sent to the client. + */ + \do_action( 'activitypub_inbox_post' ); + + $response = \rest_ensure_response( $response ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Handles user-inbox requests. + * + * @param \WP_REST_Request $request The request object. + * + * @return \WP_REST_Response|\WP_Error Response object or WP_Error. + */ + public function create_item( $request ) { + $user_id = $request->get_param( 'user_id' ); + $user = Actors::get_by_various( $user_id ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + $data = $request->get_json_params(); + $activity = Activity::init_from_array( $data ); + $type = $request->get_param( 'type' ); + $type = \strtolower( $type ); + + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput + if ( \wp_check_comment_disallowed_list( $activity->to_json( false ), '', '', '', $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT'] ?? '' ) ) { + Debug::write_log( 'Blocked activity from: ' . $activity->get_actor() ); + } else { + /** + * ActivityPub inbox action. + * + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param string $type The type of the activity. + * @param Activity|\WP_Error $activity The Activity object. + */ + \do_action( 'activitypub_inbox', $data, $user->get__id(), $type, $activity ); + + /** + * ActivityPub inbox action for specific activity types. + * + * @param array $data The data array. + * @param int|null $user_id The user ID. + * @param Activity|\WP_Error $activity The Activity object. + */ + \do_action( 'activitypub_inbox_' . $type, $data, $user->get__id(), $activity ); + } + + $response = \rest_ensure_response( array() ); + $response->set_status( 202 ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Retrieves the schema for the inbox collection, conforming to JSON Schema. + * + * @return array Collection schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $item_schema = array( + 'type' => 'object', + ); + + $schema = $this->get_collection_schema( $item_schema ); + + // Add inbox-specific properties. + $schema['title'] = 'inbox'; + $schema['properties']['generator'] = array( + 'description' => 'The software used to generate the collection.', + 'type' => 'string', + 'format' => 'uri', + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-actors.php b/wp-content/plugins/activitypub/includes/rest/class-actors.php deleted file mode 100644 index 60f03d29..00000000 --- a/wp-content/plugins/activitypub/includes/rest/class-actors.php +++ /dev/null @@ -1,161 +0,0 @@ -[\w\-\.]+)', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'get' ), - 'args' => self::request_parameters(), - 'permission_callback' => '__return_true', - ), - ) - ); - - \register_rest_route( - ACTIVITYPUB_REST_NAMESPACE, - '/(users|actors)/(?P[\w\-\.]+)/remote-follow', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'remote_follow_get' ), - 'permission_callback' => '__return_true', - 'args' => array( - 'resource' => array( - 'required' => true, - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - ), - ) - ); - } - - /** - * Handle GET request - * - * @param \WP_REST_Request $request The request object. - * - * @return WP_REST_Response|\WP_Error The response object or WP_Error. - */ - public static function get( $request ) { - $user_id = $request->get_param( 'user_id' ); - $user = User_Collection::get_by_various( $user_id ); - - if ( is_wp_error( $user ) ) { - return $user; - } - - $link_header = sprintf( '<%1$s>; rel="alternate"; type="application/activity+json"', $user->get_id() ); - - // Redirect to canonical URL if it is not an ActivityPub request. - if ( ! is_activitypub_request() ) { - header( 'Link: ' . $link_header ); - header( 'Location: ' . $user->get_canonical_url(), true, 301 ); - exit; - } - - /** - * Action triggered prior to the ActivityPub profile being created and sent to the client. - */ - \do_action( 'activitypub_rest_users_pre' ); - - $json = $user->to_array(); - - $rest_response = new WP_REST_Response( $json, 200 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - $rest_response->header( 'Link', $link_header ); - - return $rest_response; - } - - - /** - * Endpoint for remote follow UI/Block. - * - * @param WP_REST_Request $request The request object. - * - * @return WP_REST_Response|\WP_Error The response object or WP_Error. - */ - public static function remote_follow_get( WP_REST_Request $request ) { - $resource = $request->get_param( 'resource' ); - $user_id = $request->get_param( 'user_id' ); - $user = User_Collection::get_by_various( $user_id ); - - if ( is_wp_error( $user ) ) { - return $user; - } - - $template = Webfinger::get_remote_follow_endpoint( $resource ); - - if ( is_wp_error( $template ) ) { - return $template; - } - - $resource = $user->get_webfinger(); - $url = str_replace( '{uri}', $resource, $template ); - - return new WP_REST_Response( - array( - 'url' => $url, - 'template' => $template, - ), - 200 - ); - } - - /** - * The supported parameters. - * - * @return array List of parameters, - */ - public static function request_parameters() { - $params = array(); - - $params['page'] = array( - 'type' => 'string', - ); - - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - - return $params; - } -} diff --git a/wp-content/plugins/activitypub/includes/rest/class-application-controller.php b/wp-content/plugins/activitypub/includes/rest/class-application-controller.php new file mode 100644 index 00000000..ad3c1b80 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-application-controller.php @@ -0,0 +1,168 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => '__return_true', + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Retrieves the application actor profile. + * + * @param \WP_REST_Request $request The request object. + * @return \WP_REST_Response Response object. + */ + public function get_item( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $json = ( new Application() )->to_array(); + + $rest_response = new \WP_REST_Response( $json, 200 ); + $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $rest_response; + } + + /** + * Retrieves the schema for the application endpoint. + * + * @return array Schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'application', + 'type' => 'object', + 'properties' => array( + '@context' => array( + 'type' => 'array', + 'items' => array( + 'type' => array( 'string', 'object' ), + ), + ), + 'id' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'type' => array( + 'type' => 'string', + 'enum' => array( 'Application' ), + ), + 'name' => array( + 'type' => 'string', + ), + 'icon' => array( + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'type' => 'string', + ), + 'url' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + ), + 'published' => array( + 'type' => 'string', + 'format' => 'date-time', + ), + 'summary' => array( + 'type' => 'string', + ), + 'url' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'inbox' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'outbox' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'streams' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + ), + 'preferredUsername' => array( + 'type' => 'string', + ), + 'publicKey' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'owner' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'publicKeyPem' => array( + 'type' => 'string', + ), + ), + ), + 'manuallyApprovesFollowers' => array( + 'type' => 'boolean', + ), + 'discoverable' => array( + 'type' => 'boolean', + ), + 'indexable' => array( + 'type' => 'boolean', + ), + 'webfinger' => array( + 'type' => 'string', + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-collection.php b/wp-content/plugins/activitypub/includes/rest/class-collection.php deleted file mode 100644 index 02cc99bf..00000000 --- a/wp-content/plugins/activitypub/includes/rest/class-collection.php +++ /dev/null @@ -1,324 +0,0 @@ -[\w\-\.]+)/collections/tags', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'tags_get' ), - 'args' => self::request_parameters(), - 'permission_callback' => '__return_true', - ), - ) - ); - - \register_rest_route( - ACTIVITYPUB_REST_NAMESPACE, - '/(users|actors)/(?P[\w\-\.]+)/collections/featured', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'featured_get' ), - 'args' => self::request_parameters(), - 'permission_callback' => '__return_true', - ), - ) - ); - - \register_rest_route( - ACTIVITYPUB_REST_NAMESPACE, - '/collections/moderators', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'moderators_get' ), - 'permission_callback' => '__return_true', - ), - ) - ); - - \register_rest_route( - ACTIVITYPUB_REST_NAMESPACE, - '/(?P[\w\-\.]+)s/(?P[\w\-\.]+)/replies', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'replies_get' ), - 'args' => self::request_parameters_for_replies(), - 'permission_callback' => '__return_true', - ), - ) - ); - } - - /** - * The endpoint for replies collections. - * - * @param \WP_REST_Request $request The request object. - * - * @return WP_REST_Response|\WP_Error The response object or WP_Error. - */ - public static function replies_get( $request ) { - $type = $request->get_param( 'type' ); - - // Get the WordPress object of that "owns" the requested replies. - switch ( $type ) { - case 'comment': - $wp_object = \get_comment( $request->get_param( 'id' ) ); - break; - case 'post': - default: - $wp_object = \get_post( $request->get_param( 'id' ) ); - break; - } - - if ( ! isset( $wp_object ) || is_wp_error( $wp_object ) ) { - return new WP_Error( - 'activitypub_replies_collection_does_not_exist', - \sprintf( - // translators: %s: The type (post, comment, etc.) for which no replies collection exists. - \__( 'No reply collection exists for the type %s.', 'activitypub' ), - $type - ) - ); - } - - $page = intval( $request->get_param( 'page' ) ); - - // If the request parameter page is present get the CollectionPage otherwise the replies collection. - if ( isset( $page ) ) { - $response = Replies::get_collection_page( $wp_object, $page ); - } else { - $response = Replies::get_collection( $wp_object ); - } - - if ( is_wp_error( $response ) ) { - return $response; - } - - // Add ActivityPub Context. - $response = array_merge( - array( '@context' => Base_Object::JSON_LD_CONTEXT ), - $response - ); - - return new WP_REST_Response( $response, 200 ); - } - - /** - * The Featured Tags endpoint - * - * @param \WP_REST_Request $request The request object. - * - * @return WP_REST_Response|\WP_Error The response object or WP_Error. - */ - public static function tags_get( $request ) { - $user_id = $request->get_param( 'user_id' ); - $user = User_Collection::get_by_various( $user_id ); - - if ( is_wp_error( $user ) ) { - return $user; - } - - $number = 4; - - $tags = \get_terms( - array( - 'taxonomy' => 'post_tag', - 'orderby' => 'count', - 'order' => 'DESC', - 'number' => $number, - ) - ); - - if ( is_wp_error( $tags ) ) { - $tags = array(); - } - - $response = array( - '@context' => Base_Object::JSON_LD_CONTEXT, - 'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/tags', $user->get__id() ) ), - 'type' => 'Collection', - 'totalItems' => is_countable( $tags ) ? count( $tags ) : 0, - 'items' => array(), - ); - - foreach ( $tags as $tag ) { - $response['items'][] = array( - 'type' => 'Hashtag', - 'href' => \esc_url( \get_tag_link( $tag ) ), - 'name' => esc_hashtag( $tag->name ), - ); - } - - $rest_response = new WP_REST_Response( $response, 200 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - - return $rest_response; - } - - /** - * Featured posts endpoint - * - * @param \WP_REST_Request $request The request object. - * - * @return WP_REST_Response|\WP_Error The response object or WP_Error. - */ - public static function featured_get( $request ) { - $user_id = $request->get_param( 'user_id' ); - $user = User_Collection::get_by_various( $user_id ); - - if ( is_wp_error( $user ) ) { - return $user; - } - - $sticky_posts = \get_option( 'sticky_posts' ); - - if ( ! is_single_user() && User_Collection::BLOG_USER_ID === $user->get__id() ) { - $posts = array(); - } elseif ( $sticky_posts ) { - $args = array( - 'post__in' => $sticky_posts, - 'ignore_sticky_posts' => 1, - 'orderby' => 'date', - 'order' => 'DESC', - ); - - if ( $user->get__id() > 0 ) { - $args['author'] = $user->get__id(); - } - - $posts = \get_posts( $args ); - } else { - $posts = array(); - } - - $response = array( - '@context' => Base_Object::JSON_LD_CONTEXT, - 'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $user_id ) ), - 'type' => 'OrderedCollection', - 'totalItems' => is_countable( $posts ) ? count( $posts ) : 0, - 'orderedItems' => array(), - ); - - foreach ( $posts as $post ) { - $transformer = Factory::get_transformer( $post ); - - if ( \is_wp_error( $transformer ) ) { - continue; - } - - $response['orderedItems'][] = $transformer->to_object()->to_array( false ); - } - - $rest_response = new WP_REST_Response( $response, 200 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - - return $rest_response; - } - - /** - * Moderators endpoint. - * - * @return WP_REST_Response The response object. - */ - public static function moderators_get() { - $response = array( - '@context' => Actor::JSON_LD_CONTEXT, - 'id' => get_rest_url_by_path( 'collections/moderators' ), - 'type' => 'OrderedCollection', - 'orderedItems' => array(), - ); - - $users = User_Collection::get_collection(); - - foreach ( $users as $user ) { - $response['orderedItems'][] = $user->get_url(); - } - - $rest_response = new WP_REST_Response( $response, 200 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - - return $rest_response; - } - - /** - * The supported parameters. - * - * @return array List of parameters. - */ - public static function request_parameters() { - $params = array(); - - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - - return $params; - } - - /** - * The supported parameters. - * - * @return array list of parameters. - */ - public static function request_parameters_for_replies() { - $params = array(); - - $params['type'] = array( - 'required' => true, - 'type' => 'string', - 'enum' => array( 'post', 'comment' ), - ); - - $params['id'] = array( - 'required' => true, - 'type' => 'string', - ); - - return $params; - } -} diff --git a/wp-content/plugins/activitypub/includes/rest/class-collections-controller.php b/wp-content/plugins/activitypub/includes/rest/class-collections-controller.php new file mode 100644 index 00000000..199feabd --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-collections-controller.php @@ -0,0 +1,272 @@ +namespace, + '/' . $this->rest_base . '/collections/(?P[\w\-\.]+)', + array( + 'args' => array( + 'user_id' => array( + 'description' => 'The user ID or username.', + 'type' => 'string', + 'required' => true, + ), + 'type' => array( + 'description' => 'The type of collection to query.', + 'type' => 'string', + 'enum' => array( 'tags', 'featured' ), + 'required' => true, + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'page' => array( + 'description' => 'Current page of the collection.', + 'type' => 'integer', + 'minimum' => 1, + // No default so we can differentiate between Collection and CollectionPage requests. + ), + 'per_page' => array( + 'description' => 'Maximum number of items to be returned in result set.', + 'type' => 'integer', + 'default' => 20, + 'minimum' => 1, + ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Retrieves a collection of featured tags. + * + * @param \WP_REST_Request $request The request object. + * + * @return \WP_REST_Response|\WP_Error Response object or WP_Error object. + */ + public function get_items( $request ) { + $user_id = $request->get_param( 'user_id' ); + $user = Actors::get_by_various( $user_id ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + switch ( $request->get_param( 'type' ) ) { + case 'tags': + $response = $this->get_tags( $request, $user ); + break; + + case 'featured': + $response = $this->get_featured( $request, $user ); + break; + + default: + $response = new \WP_Error( 'rest_unknown_collection_type', 'Unknown collection type.', array( 'status' => 404 ) ); + } + + if ( \is_wp_error( $response ) ) { + return $response; + } + + $response = \rest_ensure_response( $response ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Retrieves a collection of featured tags. + * + * @param \WP_REST_Request $request The request object. + * @param User|Blog|Application $user Actor. + * + * @return array Collection of featured tags. + */ + public function get_tags( $request, $user ) { + $tags = \get_terms( + array( + 'taxonomy' => 'post_tag', + 'orderby' => 'count', + 'order' => 'DESC', + 'number' => 4, + ) + ); + + if ( \is_wp_error( $tags ) ) { + $tags = array(); + } + + $response = array( + '@context' => Base_Object::JSON_LD_CONTEXT, + 'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/tags', $user->get__id() ) ), + 'type' => 'Collection', + 'totalItems' => \is_countable( $tags ) ? \count( $tags ) : 0, + 'items' => array(), + ); + + foreach ( $tags as $tag ) { + $response['items'][] = array( + 'type' => 'Hashtag', + 'href' => \esc_url( \get_tag_link( $tag ) ), + 'name' => esc_hashtag( $tag->name ), + ); + } + + return $this->prepare_collection_response( $response, $request ); + } + + /** + * Retrieves a collection of featured posts. + * + * @param \WP_REST_Request $request The request object. + * @param User|Blog|Application $user Actor. + * + * @return array Collection of featured posts. + */ + public function get_featured( $request, $user ) { + $posts = array(); + + if ( is_single_user() || Actors::BLOG_USER_ID !== $user->get__id() ) { + $sticky_posts = \get_option( 'sticky_posts' ); + + if ( $sticky_posts && is_array( $sticky_posts ) ) { + // Only show public posts. + $args = array( + 'post__in' => $sticky_posts, + 'ignore_sticky_posts' => 1, + 'orderby' => 'date', + 'order' => 'DESC', + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => 'activitypub_content_visibility', + 'compare' => 'NOT EXISTS', + ), + ), + ); + + if ( $user->get__id() > 0 ) { + $args['author'] = $user->get__id(); + } + + $posts = \get_posts( $args ); + } + } + + $response = array( + '@context' => Base_Object::JSON_LD_CONTEXT, + 'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $request->get_param( 'user_id' ) ) ), + 'type' => 'OrderedCollection', + 'totalItems' => \is_countable( $posts ) ? \count( $posts ) : 0, + 'orderedItems' => array(), + ); + + foreach ( $posts as $post ) { + $transformer = Factory::get_transformer( $post ); + + if ( \is_wp_error( $transformer ) ) { + continue; + } + + $response['orderedItems'][] = $transformer->to_object()->to_array( false ); + } + + return $this->prepare_collection_response( $response, $request ); + } + + /** + * Retrieves the schema for the Collections endpoint. + * + * @return array Schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = $this->get_collection_schema(); + + // Add collections-specific properties. + $schema['title'] = 'featured'; + $schema['properties']['generator'] = array( + 'description' => 'The software used to generate the collection.', + 'type' => 'string', + 'format' => 'uri', + ); + $schema['properties']['oneOf'] = array( + 'orderedItems' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + ), + ), + 'items' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'type' => 'string', + 'enum' => array( 'Hashtag' ), + 'required' => true, + ), + 'href' => array( + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + 'name' => array( + 'type' => 'string', + 'required' => true, + ), + ), + ), + ), + ); + + unset( $schema['properties']['orderedItems'] ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-comment.php b/wp-content/plugins/activitypub/includes/rest/class-comment.php deleted file mode 100644 index 095c8589..00000000 --- a/wp-content/plugins/activitypub/includes/rest/class-comment.php +++ /dev/null @@ -1,100 +0,0 @@ -\d+)/remote-reply', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'remote_reply_get' ), - 'permission_callback' => '__return_true', - 'args' => array( - 'resource' => array( - 'required' => true, - 'sanitize_callback' => 'sanitize_text_field', - ), - ), - ), - ) - ); - } - - /** - * Endpoint for remote follow UI/Block. - * - * @param WP_REST_Request $request The request object. - * - * @return array|string|WP_Error|WP_REST_Response The URL to the remote follow page or an error. - */ - public static function remote_reply_get( WP_REST_Request $request ) { - $resource = $request->get_param( 'resource' ); - $comment_id = $request->get_param( 'comment_id' ); - - $comment = get_comment( $comment_id ); - - if ( ! $comment ) { - return new WP_Error( 'activitypub_comment_not_found', __( 'Comment not found', 'activitypub' ), array( 'status' => 404 ) ); - } - - $is_local = Comment_Utils::is_local( $comment ); - - if ( $is_local ) { - return new WP_Error( 'activitypub_local_only_comment', __( 'Comment is local only', 'activitypub' ), array( 'status' => 403 ) ); - } - - $template = Webfinger_Utils::get_remote_follow_endpoint( $resource ); - - if ( is_wp_error( $template ) ) { - return $template; - } - - $resource = Comment_Utils::get_source_id( $comment_id ); - - if ( ! $resource ) { - $resource = Comment_Utils::generate_id( $comment ); - } - - $url = str_replace( '{uri}', $resource, $template ); - - return new WP_REST_Response( - array( - 'url' => $url, - 'template' => $template, - ), - 200 - ); - } -} diff --git a/wp-content/plugins/activitypub/includes/rest/class-comments-controller.php b/wp-content/plugins/activitypub/includes/rest/class-comments-controller.php new file mode 100644 index 00000000..adcfcfd5 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-comments-controller.php @@ -0,0 +1,156 @@ +\d+)'; + + /** + * Register routes. + */ + public function register_routes() { + \register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/remote-reply', + array( + 'args' => array( + 'comment_id' => array( + 'description' => 'The ID of the comment.', + 'type' => 'integer', + 'required' => true, + 'validate_callback' => array( $this, 'validate_comment' ), + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'resource' => array( + 'description' => 'The resource to reply to.', + 'type' => 'string', + 'required' => true, + ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Validates if a comment can be replied to remotely. + * + * @param mixed $param The parameter to validate. + * + * @return true|\WP_Error True if the comment can be replied to, WP_Error otherwise. + */ + public function validate_comment( $param ) { + $comment = \get_comment( $param ); + + if ( ! $comment ) { + return new \WP_Error( 'activitypub_comment_not_found', \__( 'Comment not found', 'activitypub' ), array( 'status' => 404 ) ); + } + + $is_local = Comment::is_local( $comment ); + + if ( $is_local ) { + return new \WP_Error( 'activitypub_local_only_comment', \__( 'Comment is local only', 'activitypub' ), array( 'status' => 403 ) ); + } + + return true; + } + + /** + * Retrieves the remote reply URL for a comment. + * + * @param \WP_REST_Request $request The request object. + * + * @return \WP_REST_Response|\WP_Error Response object or WP_Error object. + */ + public function get_item( $request ) { + $resource = $request->get_param( 'resource' ); + $comment_id = $request->get_param( 'comment_id' ); + + $template = Webfinger::get_remote_follow_endpoint( $resource ); + + if ( \is_wp_error( $template ) ) { + return $template; + } + + $resource = Comment::get_source_id( $comment_id ); + + if ( ! $resource ) { + $resource = Comment::generate_id( \get_comment( $comment_id ) ); + } + + $url = \str_replace( '{uri}', $resource, $template ); + + return \rest_ensure_response( + array( + 'url' => $url, + 'template' => $template, + ) + ); + } + + /** + * Retrieves the schema for the remote reply endpoint. + * + * @return array Schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'remote-reply', + 'type' => 'object', + 'properties' => array( + 'url' => array( + 'description' => 'The URL to the remote reply page.', + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + 'template' => array( + 'description' => 'The template URL for remote replies.', + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-followers-controller.php b/wp-content/plugins/activitypub/includes/rest/class-followers-controller.php new file mode 100644 index 00000000..1f09ad45 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-followers-controller.php @@ -0,0 +1,226 @@ +namespace, + '/' . $this->rest_base . '/followers', + array( + 'args' => array( + 'user_id' => array( + 'description' => 'The ID or username of the actor.', + 'type' => 'string', + 'required' => true, + 'pattern' => '[\w\-\.]+', + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ), + 'args' => array( + 'page' => array( + 'description' => 'Current page of the collection.', + 'type' => 'integer', + 'minimum' => 1, + // No default so we can differentiate between Collection and CollectionPage requests. + ), + 'per_page' => array( + 'description' => 'Maximum number of items to be returned in result set.', + 'type' => 'integer', + 'default' => 20, + 'minimum' => 1, + ), + 'order' => array( + 'description' => 'Order sort attribute ascending or descending.', + 'type' => 'string', + 'default' => 'desc', + 'enum' => array( 'asc', 'desc' ), + ), + 'context' => array( + 'description' => 'The context in which the request is made.', + 'type' => 'string', + 'default' => 'simple', + 'enum' => array( 'simple', 'full' ), + ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Retrieves followers list. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $user_id = $request->get_param( 'user_id' ); + $user = Actors::get_by_various( $user_id ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. + */ + \do_action( 'activitypub_rest_followers_pre' ); + + $order = $request->get_param( 'order' ); + $per_page = $request->get_param( 'per_page' ); + $page = $request->get_param( 'page' ) ?? 1; + $context = $request->get_param( 'context' ); + + $data = Followers::get_followers_with_count( $user_id, $per_page, $page, array( 'order' => \ucwords( $order ) ) ); + + $response = array( + '@context' => get_context(), + 'id' => get_rest_url_by_path( \sprintf( 'actors/%d/followers', $user->get__id() ) ), + 'generator' => 'https://wordpress.org/?v=' . get_masked_wp_version(), + 'actor' => $user->get_id(), + 'type' => 'OrderedCollection', + 'totalItems' => $data['total'], + 'orderedItems' => array_map( + function ( $item ) use ( $context ) { + if ( 'full' === $context ) { + return $item->to_array( false ); + } + return $item->get_id(); + }, + $data['followers'] + ), + ); + + $response = $this->prepare_collection_response( $response, $request ); + if ( is_wp_error( $response ) ) { + return $response; + } + + $response = \rest_ensure_response( $response ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Retrieves the followers schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + // Define the schema for items in the followers collection. + $item_schema = array( + 'oneOf' => array( + array( + 'type' => 'string', + 'format' => 'uri', + ), + array( + 'type' => 'object', + 'properties' => array( + 'id' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'type' => array( + 'type' => 'string', + ), + 'name' => array( + 'type' => 'string', + ), + 'icon' => array( + 'type' => 'object', + 'properties' => array( + 'type' => array( + 'type' => 'string', + ), + 'mediaType' => array( + 'type' => 'string', + ), + 'url' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + ), + 'published' => array( + 'type' => 'string', + 'format' => 'date-time', + ), + 'summary' => array( + 'type' => 'string', + ), + 'updated' => array( + 'type' => 'string', + 'format' => 'date-time', + ), + 'url' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'streams' => array( + 'type' => 'array', + ), + 'preferredUsername' => array( + 'type' => 'string', + ), + ), + ), + ), + ); + + $schema = $this->get_collection_schema( $item_schema ); + + // Add followers-specific properties. + $schema['title'] = 'followers'; + $schema['properties']['actor'] = array( + 'description' => 'The actor who owns the followers collection.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ); + $schema['properties']['generator'] = array( + 'description' => 'The generator of the followers collection.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-followers.php b/wp-content/plugins/activitypub/includes/rest/class-followers.php deleted file mode 100644 index fe97548e..00000000 --- a/wp-content/plugins/activitypub/includes/rest/class-followers.php +++ /dev/null @@ -1,154 +0,0 @@ -[\w\-\.]+)/followers', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'get' ), - 'args' => self::request_parameters(), - 'permission_callback' => '__return_true', - ), - ) - ); - } - - /** - * Handle GET request - * - * @param \WP_REST_Request $request The request object. - * - * @return WP_REST_Response|\WP_Error The response object or WP_Error. - */ - public static function get( $request ) { - $user_id = $request->get_param( 'user_id' ); - $user = User_Collection::get_by_various( $user_id ); - - if ( is_wp_error( $user ) ) { - return $user; - } - - $order = $request->get_param( 'order' ); - $per_page = (int) $request->get_param( 'per_page' ); - $page = (int) $request->get_param( 'page' ); - $context = $request->get_param( 'context' ); - - /** - * Action triggered prior to the ActivityPub profile being created and sent to the client - */ - \do_action( 'activitypub_rest_followers_pre' ); - - $data = Follower_Collection::get_followers_with_count( $user_id, $per_page, $page, array( 'order' => ucwords( $order ) ) ); - $json = new stdClass(); - - // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $json->{'@context'} = \Activitypub\get_context(); - $json->id = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) ); - $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); - $json->actor = $user->get_id(); - $json->type = 'OrderedCollectionPage'; - $json->totalItems = $data['total']; - $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) ); - - $json->first = \add_query_arg( 'page', 1, $json->partOf ); - $json->last = \add_query_arg( 'page', \ceil( $json->totalItems / $per_page ), $json->partOf ); - - if ( $page && ( ( \ceil( $json->totalItems / $per_page ) ) > $page ) ) { - $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); - } - - if ( $page && ( $page > 1 ) ) { - $json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); - } - - $json->orderedItems = array_map( - function ( $item ) use ( $context ) { - if ( 'full' === $context ) { - return $item->to_array( false ); - } - return $item->get_url(); - }, - $data['followers'] - ); - // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - - $rest_response = new WP_REST_Response( $json, 200 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - - return $rest_response; - } - - /** - * The supported parameters. - * - * @return array List of parameters. - */ - public static function request_parameters() { - $params = array(); - - $params['page'] = array( - 'type' => 'integer', - 'default' => 1, - ); - - $params['per_page'] = array( - 'type' => 'integer', - 'default' => 20, - ); - - $params['order'] = array( - 'type' => 'string', - 'default' => 'desc', - 'enum' => array( 'asc', 'desc' ), - ); - - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - - $params['context'] = array( - 'type' => 'string', - 'default' => 'simple', - 'enum' => array( 'simple', 'full' ), - ); - - return $params; - } -} diff --git a/wp-content/plugins/activitypub/includes/rest/class-following-controller.php b/wp-content/plugins/activitypub/includes/rest/class-following-controller.php new file mode 100644 index 00000000..19c9aede --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-following-controller.php @@ -0,0 +1,183 @@ +namespace, + '/' . $this->rest_base . '/following', + array( + 'args' => array( + 'user_id' => array( + 'description' => 'The ID or username of the actor.', + 'type' => 'string', + 'required' => true, + 'pattern' => '[\w\-\.]+', + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ), + 'args' => array( + 'page' => array( + 'description' => 'Current page of the collection.', + 'type' => 'integer', + 'minimum' => 1, + // No default so we can differentiate between Collection and CollectionPage requests. + ), + 'per_page' => array( + 'description' => 'Maximum number of items to be returned in result set.', + 'type' => 'integer', + 'default' => 10, + 'minimum' => 1, + 'maximum' => 100, + ), + ), + ), + 'schema' => array( $this, 'get_public_item_schema' ), + ) + ); + } + + /** + * Retrieves following list. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $user_id = $request->get_param( 'user_id' ); + $user = Actors::get_by_various( $user_id ); + + if ( \is_wp_error( $user ) ) { + return $user; + } + + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. + */ + \do_action( 'activitypub_rest_following_pre' ); + + $response = array( + '@context' => get_context(), + 'id' => get_rest_url_by_path( \sprintf( 'actors/%d/following', $user->get__id() ) ), + 'generator' => 'https://wordpress.org/?v=' . get_masked_wp_version(), + 'actor' => $user->get_id(), + 'type' => 'OrderedCollection', + ); + + /** + * Filter the list of following urls. + * + * @param array $items The array of following urls. + * @param \Activitypub\Model\User $user The user object. + */ + $items = \apply_filters( 'activitypub_rest_following', array(), $user ); + + $response['totalItems'] = \is_countable( $items ) ? \count( $items ) : 0; + $response['orderedItems'] = $items; + + $response = $this->prepare_collection_response( $response, $request ); + if ( is_wp_error( $response ) ) { + return $response; + } + + $response = \rest_ensure_response( $response ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Add the Blog Authors to the following list of the Blog Actor + * if Blog not in single mode. + * + * @param array $follow_list The array of following urls. + * @param \Activitypub\Model\User $user The user object. + * + * @return array The array of following urls. + */ + public static function default_following( $follow_list, $user ) { + if ( 0 !== $user->get__id() || is_single_user() ) { + return $follow_list; + } + + $users = Actors::get_collection(); + + foreach ( $users as $user ) { + $follow_list[] = $user->get_id(); + } + + return $follow_list; + } + + /** + * Retrieves the following schema, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $item_schema = array( + 'type' => 'string', + 'format' => 'uri', + ); + + $schema = $this->get_collection_schema( $item_schema ); + + // Add following-specific properties. + $schema['title'] = 'following'; + $schema['properties']['actor'] = array( + 'description' => 'The actor who owns the following collection.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ); + $schema['properties']['generator'] = array( + 'description' => 'The generator of the following collection.', + 'type' => 'string', + 'format' => 'uri', + 'readonly' => true, + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-following.php b/wp-content/plugins/activitypub/includes/rest/class-following.php deleted file mode 100644 index bf546e96..00000000 --- a/wp-content/plugins/activitypub/includes/rest/class-following.php +++ /dev/null @@ -1,144 +0,0 @@ -[\w\-\.]+)/following', - array( - array( - 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( self::class, 'get' ), - 'args' => self::request_parameters(), - 'permission_callback' => '__return_true', - ), - ) - ); - } - - /** - * Handle GET request - * - * @param \WP_REST_Request $request The request object. - * - * @return WP_REST_Response|\WP_Error The response object or WP_Error. - */ - public static function get( $request ) { - $user_id = $request->get_param( 'user_id' ); - $user = User_Collection::get_by_various( $user_id ); - - if ( is_wp_error( $user ) ) { - return $user; - } - - /** - * Action triggered prior to the ActivityPub profile being created and sent to the client. - */ - \do_action( 'activitypub_rest_following_pre' ); - - $json = new \stdClass(); - - $json->{'@context'} = \Activitypub\get_context(); - - // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $json->id = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) ); - $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); - $json->actor = $user->get_id(); - $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) ); - - /** - * Filter the list of following urls. - * - * @param array $items The array of following urls. - * @param \Activitypub\Model\User $user The user object. - */ - $items = apply_filters( 'activitypub_rest_following', array(), $user ); - - $json->totalItems = is_countable( $items ) ? count( $items ) : 0; - $json->orderedItems = $items; - $json->first = $json->partOf; - // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - - $rest_response = new WP_REST_Response( $json, 200 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - - return $rest_response; - } - - /** - * The supported parameters. - * - * @return array List of parameters. - */ - public static function request_parameters() { - $params = array(); - - $params['page'] = array( - 'type' => 'integer', - ); - - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - - return $params; - } - - /** - * Add the Blog Authors to the following list of the Blog Actor - * if Blog not in single mode. - * - * @param array $follow_list The array of following urls. - * @param \Activitypub\Model\User $user The user object. - * - * @return array The array of following urls. - */ - public static function default_following( $follow_list, $user ) { - if ( 0 !== $user->get__id() || is_single_user() ) { - return $follow_list; - } - - $users = User_Collection::get_collection(); - - foreach ( $users as $user ) { - $follow_list[] = $user->get_url(); - } - - return $follow_list; - } -} diff --git a/wp-content/plugins/activitypub/includes/rest/class-inbox-controller.php b/wp-content/plugins/activitypub/includes/rest/class-inbox-controller.php new file mode 100644 index 00000000..4fdd2a62 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-inbox-controller.php @@ -0,0 +1,255 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::CREATABLE, + 'callback' => array( $this, 'create_item' ), + 'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ), + 'args' => array( + 'id' => array( + 'description' => 'The unique identifier for the activity.', + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + 'actor' => array( + 'description' => 'The actor performing the activity.', + 'type' => 'string', + 'required' => true, + 'sanitize_callback' => '\Activitypub\object_to_uri', + ), + 'type' => array( + 'description' => 'The type of the activity.', + 'type' => 'string', + 'required' => true, + ), + 'object' => array( + 'description' => 'The object of the activity.', + 'required' => true, + 'validate_callback' => function ( $param, $request, $key ) { + /** + * Filter the ActivityPub object validation. + * + * @param bool $validate The validation result. + * @param array $param The object data. + * @param object $request The request object. + * @param string $key The key. + */ + return \apply_filters( 'activitypub_validate_object', true, $param, $request, $key ); + }, + ), + 'to' => array( + 'description' => 'The primary recipients of the activity.', + 'type' => array( 'string', 'array' ), + 'required' => false, + 'sanitize_callback' => function ( $param ) { + if ( \is_string( $param ) ) { + $param = array( $param ); + } + + return $param; + }, + ), + 'cc' => array( + 'description' => 'The secondary recipients of the activity.', + 'type' => array( 'string', 'array' ), + 'sanitize_callback' => function ( $param ) { + if ( \is_string( $param ) ) { + $param = array( $param ); + } + + return $param; + }, + ), + 'bcc' => array( + 'description' => 'The private recipients of the activity.', + 'type' => array( 'string', 'array' ), + 'sanitize_callback' => function ( $param ) { + if ( \is_string( $param ) ) { + $param = array( $param ); + } + + return $param; + }, + ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * The shared inbox. + * + * @param \WP_REST_Request $request The request object. + * + * @return \WP_REST_Response|\WP_Error Response object or WP_Error. + */ + public function create_item( $request ) { + $data = $request->get_json_params(); + $activity = Activity::init_from_array( $data ); + $type = \strtolower( $request->get_param( 'type' ) ); + + // phpcs:ignore WordPress.Security.ValidatedSanitizedInput + if ( \wp_check_comment_disallowed_list( $activity->to_json( false ), '', '', '', $_SERVER['REMOTE_ADDR'], $_SERVER['HTTP_USER_AGENT'] ?? '' ) ) { + Debug::write_log( 'Blocked activity from: ' . $activity->get_actor() ); + } else { + $recipients = extract_recipients_from_activity( $data ); + + foreach ( $recipients as $recipient ) { + if ( ! is_same_domain( $recipient ) ) { + continue; + } + + $actor = Actors::get_by_various( $recipient ); + + if ( ! $actor || \is_wp_error( $actor ) ) { + continue; + } + + /** + * ActivityPub inbox action. + * + * @param array $data The data array. + * @param int $user_id The user ID. + * @param string $type The type of the activity. + * @param Activity|\WP_Error $activity The Activity object. + */ + \do_action( 'activitypub_inbox', $data, $actor->get__id(), $type, $activity ); + + /** + * ActivityPub inbox action for specific activity types. + * + * @param array $data The data array. + * @param int $user_id The user ID. + * @param Activity|\WP_Error $activity The Activity object. + */ + \do_action( 'activitypub_inbox_' . $type, $data, $actor->get__id(), $activity ); + } + } + + $response = \rest_ensure_response( array() ); + $response->set_status( 202 ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Retrieves the schema for a single inbox item, conforming to JSON Schema. + * + * @return array Item schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'https://json-schema.org/draft-04/schema#', + 'title' => 'activity', + 'type' => 'object', + 'properties' => array( + '@context' => array( + 'description' => 'The JSON-LD context for the activity.', + 'type' => array( 'string', 'array', 'object' ), + 'required' => true, + ), + 'id' => array( + 'description' => 'The unique identifier for the activity.', + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + 'type' => array( + 'description' => 'The type of the activity.', + 'type' => 'string', + 'required' => true, + ), + 'actor' => array( + 'description' => 'The actor performing the activity.', + 'type' => array( 'string', 'object' ), + 'format' => 'uri', + 'required' => true, + ), + 'object' => array( + 'description' => 'The object of the activity.', + 'type' => array( 'string', 'object' ), + 'required' => true, + ), + 'to' => array( + 'description' => 'The primary recipients of the activity.', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + 'cc' => array( + 'description' => 'The secondary recipients of the activity.', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + 'bcc' => array( + 'description' => 'The private recipients of the activity.', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-inbox.php b/wp-content/plugins/activitypub/includes/rest/class-inbox.php deleted file mode 100644 index ea550295..00000000 --- a/wp-content/plugins/activitypub/includes/rest/class-inbox.php +++ /dev/null @@ -1,336 +0,0 @@ - WP_REST_Server::CREATABLE, - 'callback' => array( self::class, 'shared_inbox_post' ), - 'args' => self::shared_inbox_post_parameters(), - 'permission_callback' => '__return_true', - ), - ) - ); - - \register_rest_route( - ACTIVITYPUB_REST_NAMESPACE, - '/(users|actors)/(?P[\w\-\.]+)/inbox', - array( - array( - 'methods' => WP_REST_Server::CREATABLE, - 'callback' => array( self::class, 'user_inbox_post' ), - 'args' => self::user_inbox_post_parameters(), - 'permission_callback' => '__return_true', - ), - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'user_inbox_get' ), - 'args' => self::user_inbox_get_parameters(), - 'permission_callback' => '__return_true', - ), - ) - ); - } - - /** - * Renders the user-inbox. - * - * @param \WP_REST_Request $request The request object. - * @return WP_REST_Response|\WP_Error The response object or WP_Error. - */ - public static function user_inbox_get( $request ) { - $user_id = $request->get_param( 'user_id' ); - $user = User_Collection::get_by_various( $user_id ); - - if ( is_wp_error( $user ) ) { - return $user; - } - - /** - * Action triggered prior to the ActivityPub profile being created and sent to the client. - */ - \do_action( 'activitypub_rest_inbox_pre' ); - - $json = new \stdClass(); - - // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $json->{'@context'} = get_context(); - $json->id = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) ); - $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); - $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/inbox', $user->get__id() ) ); - $json->totalItems = 0; - $json->orderedItems = array(); - $json->first = $json->partOf; - // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - - /** - * Filter the ActivityPub inbox array. - * - * @param array $json The ActivityPub inbox array. - */ - $json = \apply_filters( 'activitypub_rest_inbox_array', $json ); - - /** - * Action triggered after the ActivityPub profile has been created and sent to the client. - */ - \do_action( 'activitypub_inbox_post' ); - - $rest_response = new WP_REST_Response( $json, 200 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - - return $rest_response; - } - - /** - * Handles user-inbox requests. - * - * @param \WP_REST_Request $request The request object. - * - * @return WP_REST_Response|\WP_Error The response object or WP_Error. - */ - public static function user_inbox_post( $request ) { - $user_id = $request->get_param( 'user_id' ); - $user = User_Collection::get_by_various( $user_id ); - - if ( is_wp_error( $user ) ) { - return $user; - } - - $data = $request->get_json_params(); - $activity = Activity::init_from_array( $data ); - $type = $request->get_param( 'type' ); - $type = \strtolower( $type ); - - /** - * ActivityPub inbox action. - * - * @param array $data The data array. - * @param int|null $user_id The user ID. - * @param string $type The type of the activity. - * @param Activity $activity The Activity object. - */ - \do_action( 'activitypub_inbox', $data, $user->get__id(), $type, $activity ); - - /** - * ActivityPub inbox action for specific activity types. - * - * @param array $data The data array. - * @param int|null $user_id The user ID. - * @param Activity $activity The Activity object. - */ - \do_action( "activitypub_inbox_{$type}", $data, $user->get__id(), $activity ); - - $rest_response = new WP_REST_Response( array(), 202 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - - return $rest_response; - } - - /** - * The shared inbox. - * - * @param \WP_REST_Request $request The request object. - * - * @return WP_REST_Response - */ - public static function shared_inbox_post( $request ) { - $data = $request->get_json_params(); - $activity = Activity::init_from_array( $data ); - $type = $request->get_param( 'type' ); - $type = \strtolower( $type ); - - /** - * ActivityPub inbox action. - * - * @param array $data The data array. - * @param int|null $user_id The user ID. - * @param string $type The type of the activity. - * @param Activity $activity The Activity object. - */ - \do_action( 'activitypub_inbox', $data, null, $type, $activity ); - - /** - * ActivityPub inbox action for specific activity types. - * - * @param array $data The data array. - * @param int|null $user_id The user ID. - * @param Activity $activity The Activity object. - */ - \do_action( "activitypub_inbox_{$type}", $data, null, $activity ); - - $rest_response = new WP_REST_Response( array(), 202 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - - return $rest_response; - } - - /** - * The supported parameters. - * - * @return array List of parameters. - */ - public static function user_inbox_get_parameters() { - $params = array(); - - $params['page'] = array( - 'type' => 'integer', - ); - - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - - return $params; - } - - /** - * The supported parameters. - * - * @return array List of parameters. - */ - public static function user_inbox_post_parameters() { - $params = array(); - - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - - $params['id'] = array( - 'required' => true, - 'sanitize_callback' => 'esc_url_raw', - ); - - $params['actor'] = array( - 'required' => true, - 'sanitize_callback' => '\Activitypub\object_to_uri', - ); - - $params['type'] = array( - 'required' => true, - ); - - $params['object'] = array( - 'required' => true, - 'validate_callback' => function ( $param, $request, $key ) { - /** - * Filter the ActivityPub object validation. - * - * @param bool $validate The validation result. - * @param array $param The object data. - * @param object $request The request object. - * @param string $key The key. - */ - return apply_filters( 'activitypub_validate_object', true, $param, $request, $key ); - }, - ); - - return $params; - } - - /** - * The supported parameters. - * - * @return array List of parameters. - */ - public static function shared_inbox_post_parameters() { - $params = self::user_inbox_post_parameters(); - - $params['to'] = array( - 'required' => false, - 'sanitize_callback' => function ( $param ) { - if ( \is_string( $param ) ) { - $param = array( $param ); - } - - return $param; - }, - ); - - $params['cc'] = array( - 'sanitize_callback' => function ( $param ) { - if ( \is_string( $param ) ) { - $param = array( $param ); - } - - return $param; - }, - ); - - $params['bcc'] = array( - 'sanitize_callback' => function ( $param ) { - if ( \is_string( $param ) ) { - $param = array( $param ); - } - - return $param; - }, - ); - - return $params; - } - - /** - * Get local user recipients. - * - * @param array $data The data array. - * - * @return array The list of local users. - */ - public static function get_recipients( $data ) { - $recipients = extract_recipients_from_activity( $data ); - $users = array(); - - foreach ( $recipients as $recipient ) { - $user_id = url_to_authorid( $recipient ); - - $user = get_user_by( 'id', $user_id ); - - if ( $user ) { - $users[] = $user; - } - } - - return $users; - } -} diff --git a/wp-content/plugins/activitypub/includes/rest/class-interaction-controller.php b/wp-content/plugins/activitypub/includes/rest/class-interaction-controller.php new file mode 100644 index 00000000..f4ba270a --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-interaction-controller.php @@ -0,0 +1,139 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'uri' => array( + 'description' => 'The URI of the object to interact with.', + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + ), + ), + ) + ); + } + + /** + * Retrieves the interaction URL for a given URI. + * + * @param \WP_REST_Request $request The request object. + * + * @return \WP_REST_Response Response object on success, dies on failure. + */ + public function get_item( $request ) { + $uri = $request->get_param( 'uri' ); + $redirect_url = ''; + $object = Http::get_remote_object( $uri ); + + if ( \is_wp_error( $object ) || ! isset( $object['type'] ) ) { + // Use wp_die as this can be called from the front-end. See https://github.com/Automattic/wordpress-activitypub/pull/1149/files#r1915297109. + \wp_die( + esc_html__( 'The URL is not supported!', 'activitypub' ), + '', + array( + 'response' => 400, + 'back_link' => true, + ) + ); + } + + if ( ! empty( $object['url'] ) ) { + $uri = \esc_url( $object['url'] ); + } + + switch ( $object['type'] ) { + case 'Group': + case 'Person': + case 'Service': + case 'Application': + case 'Organization': + /** + * Filters the URL used for following an ActivityPub actor. + * + * @param string $redirect_url The URL to redirect to. + * @param string $uri The URI of the actor to follow. + * @param array $object The full actor object data. + */ + $redirect_url = \apply_filters( 'activitypub_interactions_follow_url', $redirect_url, $uri, $object ); + break; + default: + $redirect_url = \admin_url( 'post-new.php?in_reply_to=' . $uri ); + /** + * Filters the URL used for replying to an ActivityPub object. + * + * By default, this redirects to the WordPress post editor with the in_reply_to parameter set. + * + * @param string $redirect_url The URL to redirect to. + * @param string $uri The URI of the object to reply to. + * @param array $object The full object data being replied to. + */ + $redirect_url = \apply_filters( 'activitypub_interactions_reply_url', $redirect_url, $uri, $object ); + } + + /** + * Filters the redirect URL. + * + * This filter runs after the type-specific filters and allows for final modifications + * to the interaction URL regardless of the object type. + * + * @param string $redirect_url The URL to redirect to. + * @param string $uri The URI of the object. + * @param array $object The object being interacted with. + */ + $redirect_url = \apply_filters( 'activitypub_interactions_url', $redirect_url, $uri, $object ); + + // Check if hook is implemented. + if ( ! $redirect_url ) { + // Use wp_die as this can be called from the front-end. See https://github.com/Automattic/wordpress-activitypub/pull/1149/files#r1915297109. + \wp_die( + esc_html__( 'This Interaction type is not supported yet!', 'activitypub' ), + '', + array( + 'response' => 400, + 'back_link' => true, + ) + ); + } + + return new \WP_REST_Response( null, 302, array( 'Location' => \esc_url( $redirect_url ) ) ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-interaction.php b/wp-content/plugins/activitypub/includes/rest/class-interaction.php deleted file mode 100644 index 8bf78267..00000000 --- a/wp-content/plugins/activitypub/includes/rest/class-interaction.php +++ /dev/null @@ -1,118 +0,0 @@ - \WP_REST_Server::READABLE, - 'callback' => array( self::class, 'get' ), - 'permission_callback' => '__return_true', - 'args' => array( - 'uri' => array( - 'type' => 'string', - 'required' => true, - 'sanitize_callback' => 'esc_url', - ), - ), - ), - ) - ); - } - - /** - * Handle GET request. - * - * @param \WP_REST_Request $request The request object. - * - * @return WP_REST_Response Redirect to the editor or die. - */ - public static function get( $request ) { - $uri = $request->get_param( 'uri' ); - $redirect_url = null; - $object = Http::get_remote_object( $uri ); - - if ( - \is_wp_error( $object ) || - ! isset( $object['type'] ) - ) { - \wp_die( - \esc_html__( - 'The URL is not supported!', - 'activitypub' - ), - 400 - ); - } - - if ( ! empty( $object['url'] ) ) { - $uri = \esc_url( $object['url'] ); - } - - switch ( $object['type'] ) { - case 'Group': - case 'Person': - case 'Service': - case 'Application': - case 'Organization': - $redirect_url = \apply_filters( 'activitypub_interactions_follow_url', $redirect_url, $uri, $object ); - break; - default: - $redirect_url = \admin_url( 'post-new.php?in_reply_to=' . $uri ); - $redirect_url = \apply_filters( 'activitypub_interactions_reply_url', $redirect_url, $uri, $object ); - } - - /** - * Filter the redirect URL. - * - * @param string $redirect_url The URL to redirect to. - * @param string $uri The URI of the object. - * @param array $object The object. - */ - $redirect_url = \apply_filters( 'activitypub_interactions_url', $redirect_url, $uri, $object ); - - // Check if hook is implemented. - if ( ! $redirect_url ) { - \wp_die( - esc_html__( - 'This Interaction type is not supported yet!', - 'activitypub' - ), - 400 - ); - } - - return new WP_REST_Response( - null, - 302, - array( - 'Location' => \esc_url( $redirect_url ), - ) - ); - } -} diff --git a/wp-content/plugins/activitypub/includes/rest/class-moderators-controller.php b/wp-content/plugins/activitypub/includes/rest/class-moderators-controller.php new file mode 100644 index 00000000..eba84725 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-moderators-controller.php @@ -0,0 +1,132 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => '__return_true', + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Retrieves a collection of moderators. + * + * @param \WP_REST_Request $request The request object. + * @return \WP_REST_Response|\WP_Error Response object or WP_Error object. + */ + public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $actors = array(); + + foreach ( Actors::get_collection() as $user ) { + $actors[] = $user->get_id(); + } + + /** + * Filter the list of moderators. + * + * @param array $actors The list of moderators. + */ + $actors = apply_filters( 'activitypub_rest_moderators', $actors ); + + $response = array( + '@context' => get_context(), + 'id' => get_rest_url_by_path( 'collections/moderators' ), + 'type' => 'OrderedCollection', + 'orderedItems' => $actors, + ); + + $response = \rest_ensure_response( $response ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Retrieves the schema for the Moderators endpoint. + * + * @return array Schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'moderators', + 'type' => 'object', + 'properties' => array( + '@context' => array( + 'type' => 'array', + 'items' => array( + 'type' => array( 'string', 'object' ), + ), + 'required' => true, + ), + 'id' => array( + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + 'type' => array( + 'type' => 'string', + 'enum' => array( 'OrderedCollection' ), + 'required' => true, + ), + 'orderedItems' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'required' => true, + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-nodeinfo-controller.php b/wp-content/plugins/activitypub/includes/rest/class-nodeinfo-controller.php new file mode 100644 index 00000000..4bd19e04 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-nodeinfo-controller.php @@ -0,0 +1,172 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => '__return_true', + ), + ) + ); + + \register_rest_route( + $this->namespace, + '/' . $this->rest_base . '/(?P\d\.\d)', + array( + 'args' => array( + 'version' => array( + 'description' => 'The version of the NodeInfo schema.', + 'type' => 'string', + 'required' => true, + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => '__return_true', + ), + ) + ); + } + + /** + * Retrieves the NodeInfo discovery profile. + * + * @param \WP_REST_Request $request The request object. + * + * @return \WP_REST_Response Response object. + */ + public function get_items( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $response = array( + 'links' => array( + + /* + * Needs http protocol for spec compliance. + * @ticket https://github.com/Automattic/wordpress-activitypub/pull/1275 + */ + array( + 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', + 'href' => get_rest_url_by_path( '/nodeinfo/2.0' ), + ), + array( + 'rel' => 'https://nodeinfo.diaspora.software/ns/schema/2.0', + 'href' => get_rest_url_by_path( '/nodeinfo/2.0' ), + ), + array( + 'rel' => 'https://www.w3.org/ns/activitystreams#Application', + 'href' => get_rest_url_by_path( 'application' ), + ), + ), + ); + + return \rest_ensure_response( $response ); + } + + /** + * Retrieves the NodeInfo profile. + * + * @param \WP_REST_Request $request The request object. + * @return \WP_REST_Response Response object. + */ + public function get_item( $request ) { + $version = $request->get_param( 'version' ); + + /** + * Fires before the NodeInfo data is created and sent to the client. + * + * @param string $version The NodeInfo version. + */ + \do_action( 'activitypub_rest_nodeinfo_pre', $version ); + + switch ( $version ) { + case '2.0': + $response = $this->get_version_2_0(); + break; + + default: + $response = new \WP_Error( 'activitypub_rest_nodeinfo_invalid_version', 'Unsupported NodeInfo version.', array( 'status' => 405 ) ); + break; + } + + return \rest_ensure_response( $response ); + } + + /** + * Get the NodeInfo 2.0 data. + * + * @return array + */ + public function get_version_2_0() { + $posts = \wp_count_posts(); + $comments = \wp_count_comments(); + + return array( + 'version' => '2.0', + 'software' => array( + 'name' => 'wordpress', + 'version' => get_masked_wp_version(), + ), + 'protocols' => array( 'activitypub' ), + 'services' => array( + 'inbound' => array(), + 'outbound' => array(), + ), + 'openRegistrations' => (bool) get_option( 'users_can_register' ), + 'usage' => array( + 'users' => array( + 'total' => get_total_users(), + 'activeHalfyear' => get_active_users( 6 ), + 'activeMonth' => get_active_users(), + ), + 'localPosts' => $posts->publish, + 'localComments' => $comments->approved, + ), + 'metadata' => array( + 'nodeName' => \get_bloginfo( 'name' ), + 'nodeDescription' => \get_bloginfo( 'description' ), + 'nodeIcon' => \get_site_icon_url(), + ), + ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-nodeinfo.php b/wp-content/plugins/activitypub/includes/rest/class-nodeinfo.php deleted file mode 100644 index 3eb5fc00..00000000 --- a/wp-content/plugins/activitypub/includes/rest/class-nodeinfo.php +++ /dev/null @@ -1,187 +0,0 @@ - \WP_REST_Server::READABLE, - 'callback' => array( self::class, 'discovery' ), - 'permission_callback' => '__return_true', - ), - ) - ); - - \register_rest_route( - ACTIVITYPUB_REST_NAMESPACE, - '/nodeinfo', - array( - array( - 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( self::class, 'nodeinfo' ), - 'permission_callback' => '__return_true', - ), - ) - ); - - \register_rest_route( - ACTIVITYPUB_REST_NAMESPACE, - '/nodeinfo2', - array( - array( - 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( self::class, 'nodeinfo2' ), - 'permission_callback' => '__return_true', - ), - ) - ); - } - - /** - * Render NodeInfo file. - * - * @return WP_REST_Response The JSON profile of the NodeInfo. - */ - public static function nodeinfo() { - /** - * Action triggered prior to the ActivityPub profile being created and sent to the client. - */ - \do_action( 'activitypub_rest_nodeinfo_pre' ); - - $nodeinfo = array(); - - $nodeinfo['version'] = '2.0'; - $nodeinfo['software'] = array( - 'name' => 'wordpress', - 'version' => get_masked_wp_version(), - ); - - $posts = \wp_count_posts(); - $comments = \wp_count_comments(); - - $nodeinfo['usage'] = array( - 'users' => array( - 'total' => get_total_users(), - 'activeMonth' => get_active_users( '1 month ago' ), - 'activeHalfyear' => get_active_users( '6 month ago' ), - ), - 'localPosts' => (int) $posts->publish, - 'localComments' => (int) $comments->approved, - ); - - $nodeinfo['openRegistrations'] = false; - $nodeinfo['protocols'] = array( 'activitypub' ); - - $nodeinfo['services'] = array( - 'inbound' => array(), - 'outbound' => array(), - ); - - $nodeinfo['metadata'] = array( - 'nodeName' => \get_bloginfo( 'name' ), - 'nodeDescription' => \get_bloginfo( 'description' ), - 'nodeIcon' => \get_site_icon_url(), - ); - - return new WP_REST_Response( $nodeinfo, 200 ); - } - - /** - * Render NodeInfo file. - * - * @return WP_REST_Response The JSON profile of the NodeInfo. - */ - public static function nodeinfo2() { - /** - * Action triggered prior to the ActivityPub profile being created and sent to the client. - */ - \do_action( 'activitypub_rest_nodeinfo2_pre' ); - - $nodeinfo = array(); - - $nodeinfo['version'] = '2.0'; - $nodeinfo['server'] = array( - 'baseUrl' => \home_url( '/' ), - 'name' => \get_bloginfo( 'name' ), - 'software' => 'wordpress', - 'version' => get_masked_wp_version(), - ); - - $posts = \wp_count_posts(); - $comments = \wp_count_comments(); - - $nodeinfo['usage'] = array( - 'users' => array( - 'total' => get_total_users(), - 'activeMonth' => get_active_users( 1 ), - 'activeHalfyear' => get_active_users( 6 ), - ), - 'localPosts' => (int) $posts->publish, - 'localComments' => (int) $comments->approved, - ); - - $nodeinfo['openRegistrations'] = false; - $nodeinfo['protocols'] = array( 'activitypub' ); - - $nodeinfo['services'] = array( - 'inbound' => array(), - 'outbound' => array(), - ); - - return new WP_REST_Response( $nodeinfo, 200 ); - } - - /** - * Render NodeInfo discovery file. - * - * @return WP_REST_Response - */ - public static function discovery() { - $discovery = array(); - $discovery['links'] = array( - array( - 'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0', - 'href' => get_rest_url_by_path( 'nodeinfo' ), - ), - array( - 'rel' => 'https://www.w3.org/ns/activitystreams#Application', - 'href' => get_rest_url_by_path( 'application' ), - ), - ); - - return new \WP_REST_Response( $discovery, 200 ); - } -} diff --git a/wp-content/plugins/activitypub/includes/rest/class-outbox-controller.php b/wp-content/plugins/activitypub/includes/rest/class-outbox-controller.php new file mode 100644 index 00000000..001eefdd --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-outbox-controller.php @@ -0,0 +1,257 @@ +[\w\-\.]+)/outbox'; + + /** + * Register routes. + */ + public function register_routes() { + \register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'args' => array( + 'user_id' => array( + 'description' => 'The ID of the user or actor.', + 'type' => 'string', + 'validate_callback' => array( $this, 'validate_user_id' ), + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( 'Activitypub\Rest\Server', 'verify_signature' ), + 'args' => array( + 'page' => array( + 'description' => 'Current page of the collection.', + 'type' => 'integer', + 'minimum' => 1, + // No default so we can differentiate between Collection and CollectionPage requests. + ), + 'per_page' => array( + 'description' => 'Maximum number of items to be returned in result set.', + 'type' => 'integer', + 'default' => 20, + 'minimum' => 1, + 'maximum' => 100, + ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Validates the user_id parameter. + * + * @param mixed $user_id The user_id parameter. + * @return bool|\WP_Error True if the user_id is valid, WP_Error otherwise. + */ + public function validate_user_id( $user_id ) { + $user = Actors::get_by_various( $user_id ); + if ( \is_wp_error( $user ) ) { + return $user; + } + + return true; + } + + /** + * Retrieves a collection of outbox items. + * + * @param \WP_REST_Request $request Full details about the request. + * @return \WP_REST_Response|\WP_Error Response object on success, or WP_Error object on failure. + */ + public function get_items( $request ) { + $page = $request->get_param( 'page' ) ?? 1; + $user = Actors::get_by_various( $request->get_param( 'user_id' ) ); + $user_id = $user->get__id(); + + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. + * + * @param \WP_REST_Request $request The request object. + */ + \do_action( 'activitypub_rest_outbox_pre', $request ); + + /** + * Filters the list of activity types to include in the outbox. + * + * @param string[] $activity_types The list of activity types. + */ + $activity_types = apply_filters( 'rest_activitypub_outbox_activity_types', array( 'Announce', 'Create', 'Like', 'Update' ) ); + + $args = array( + 'posts_per_page' => $request->get_param( 'per_page' ), + 'author' => $user_id > 0 ? $user_id : null, + 'paged' => $page, + 'post_type' => Outbox::POST_TYPE, + 'post_status' => 'any', + + // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query + 'meta_query' => array( + array( + 'key' => '_activitypub_activity_actor', + 'value' => Actors::get_type_by_id( $user_id ), + ), + ), + ); + + if ( get_current_user_id() !== $user_id && ! current_user_can( 'activitypub' ) ) { + $args['meta_query'][] = array( + 'key' => '_activitypub_activity_type', + 'value' => $activity_types, + 'compare' => 'IN', + ); + + $args['meta_query'][] = array( + 'relation' => 'OR', + array( + 'key' => 'activitypub_content_visibility', + 'compare' => 'NOT EXISTS', + ), + array( + 'key' => 'activitypub_content_visibility', + 'value' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, + ), + ); + } + + /** + * Filters WP_Query arguments when querying Outbox items via the REST API. + * + * Enables adding extra arguments or setting defaults for an outbox collection request. + * + * @param array $args Array of arguments for WP_Query. + * @param \WP_REST_Request $request The REST API request. + */ + $args = apply_filters( 'rest_activitypub_outbox_query', $args, $request ); + + $outbox_query = new \WP_Query(); + $query_result = $outbox_query->query( $args ); + + $response = array( + '@context' => Base_Object::JSON_LD_CONTEXT, + 'id' => get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ), + 'generator' => 'https://wordpress.org/?v=' . get_masked_wp_version(), + 'actor' => $user->get_id(), + 'type' => 'OrderedCollection', + 'totalItems' => $outbox_query->found_posts, + 'orderedItems' => array(), + ); + + \update_postmeta_cache( \wp_list_pluck( $query_result, 'ID' ) ); + foreach ( $query_result as $outbox_item ) { + $response['orderedItems'][] = $this->prepare_item_for_response( $outbox_item, $request ); + } + + $response = $this->prepare_collection_response( $response, $request ); + if ( is_wp_error( $response ) ) { + return $response; + } + + /** + * Filter the ActivityPub outbox array. + * + * @param array $response The ActivityPub outbox array. + * @param \WP_REST_Request $request The request object. + */ + $response = \apply_filters( 'activitypub_rest_outbox_array', $response, $request ); + + /** + * Action triggered after the ActivityPub profile has been created and sent to the client. + * + * @param \WP_REST_Request $request The request object. + */ + \do_action( 'activitypub_outbox_post', $request ); + + $response = \rest_ensure_response( $response ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Prepares the item for the REST response. + * + * @param mixed $item WordPress representation of the item. + * @param \WP_REST_Request $request Request object. + * @return array Response object on success, or WP_Error object on failure. + */ + public function prepare_item_for_response( $item, $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + $activity = Outbox::get_activity( $item->ID ); + + return $activity->to_array( false ); + } + + /** + * Retrieves the outbox schema, conforming to JSON Schema. + * + * @return array Collection schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $item_schema = array( + 'type' => 'object', + ); + + $schema = $this->get_collection_schema( $item_schema ); + + // Add outbox-specific properties. + $schema['title'] = 'outbox'; + $schema['properties']['actor'] = array( + 'description' => 'The actor who owns this outbox.', + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ); + $schema['properties']['generator'] = array( + 'description' => 'The software used to generate the collection.', + 'type' => 'string', + 'format' => 'uri', + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-outbox.php b/wp-content/plugins/activitypub/includes/rest/class-outbox.php deleted file mode 100644 index 461d4286..00000000 --- a/wp-content/plugins/activitypub/includes/rest/class-outbox.php +++ /dev/null @@ -1,173 +0,0 @@ -[\w\-\.]+)/outbox', - array( - array( - 'methods' => WP_REST_Server::READABLE, - 'callback' => array( self::class, 'user_outbox_get' ), - 'args' => self::request_parameters(), - 'permission_callback' => '__return_true', - ), - ) - ); - } - - /** - * Renders the user-outbox - * - * @param \WP_REST_Request $request The request object. - * @return WP_REST_Response|\WP_Error The response object or WP_Error. - */ - public static function user_outbox_get( $request ) { - $user_id = $request->get_param( 'user_id' ); - $user = User_Collection::get_by_various( $user_id ); - - if ( is_wp_error( $user ) ) { - return $user; - } - - $post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) ); - - $page = $request->get_param( 'page', 1 ); - - /** - * Action triggered prior to the ActivityPub profile being created and sent to the client. - */ - \do_action( 'activitypub_rest_outbox_pre' ); - - $json = new stdClass(); - - // phpcs:disable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - $json->{'@context'} = get_context(); - $json->id = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ); - $json->generator = 'http://wordpress.org/?v=' . get_masked_wp_version(); - $json->actor = $user->get_id(); - $json->type = 'OrderedCollectionPage'; - $json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/outbox', $user_id ) ); - $json->totalItems = 0; - - if ( $user_id > 0 ) { - $count_posts = \count_user_posts( $user_id, $post_types, true ); - $json->totalItems = \intval( $count_posts ); - } else { - foreach ( $post_types as $post_type ) { - $count_posts = \wp_count_posts( $post_type ); - $json->totalItems += \intval( $count_posts->publish ); - } - } - - $json->first = \add_query_arg( 'page', 1, $json->partOf ); - $json->last = \add_query_arg( 'page', \ceil( $json->totalItems / 10 ), $json->partOf ); - - if ( $page && ( ( \ceil( $json->totalItems / 10 ) ) > $page ) ) { - $json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); - } - - if ( $page && ( $page > 1 ) ) { - $json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); - } - // phpcs:enable WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - - if ( $page ) { - $posts = \get_posts( - array( - 'posts_per_page' => 10, - 'author' => $user_id > 0 ? $user_id : null, - 'paged' => $page, - 'post_type' => $post_types, - ) - ); - - foreach ( $posts as $post ) { - $transformer = Factory::get_transformer( $post ); - - if ( \is_wp_error( $transformer ) ) { - continue; - } - - $post = $transformer->to_object(); - $activity = new Activity(); - $activity->set_type( 'Create' ); - $activity->set_object( $post ); - $json->orderedItems[] = $activity->to_array( false ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase - } - } - - /** - * Filter the ActivityPub outbox array. - * - * @param array $json The ActivityPub outbox array. - */ - $json = \apply_filters( 'activitypub_rest_outbox_array', $json ); - - /** - * Action triggered after the ActivityPub profile has been created and sent to the client - */ - \do_action( 'activitypub_outbox_post' ); - - $rest_response = new WP_REST_Response( $json, 200 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - - return $rest_response; - } - - /** - * The supported parameters. - * - * @return array List of parameters. - */ - public static function request_parameters() { - $params = array(); - - $params['page'] = array( - 'type' => 'integer', - 'default' => 1, - ); - - $params['user_id'] = array( - 'required' => true, - 'type' => 'string', - ); - - return $params; - } -} diff --git a/wp-content/plugins/activitypub/includes/rest/class-post.php b/wp-content/plugins/activitypub/includes/rest/class-post.php new file mode 100644 index 00000000..182ea69d --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-post.php @@ -0,0 +1,160 @@ +\d+)/reactions', + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( static::class, 'get_reactions' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'id' => array( + 'required' => true, + 'type' => 'integer', + ), + ), + ) + ); + + register_rest_route( + ACTIVITYPUB_REST_NAMESPACE, + '/posts/(?P\d+)/context', + array( + 'methods' => WP_REST_Server::READABLE, + 'callback' => array( static::class, 'get_context' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'id' => array( + 'required' => true, + 'type' => 'integer', + ), + ), + ) + ); + } + + /** + * Get reactions for a post. + * + * @param \WP_REST_Request $request The request. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public static function get_reactions( $request ) { + $post_id = $request->get_param( 'id' ); + $post = get_post( $post_id ); + + if ( ! $post ) { + return new WP_Error( 'post_not_found', 'Post not found', array( 'status' => 404 ) ); + } + + $reactions = array(); + + foreach ( Comment::get_comment_types() as $type_object ) { + $comments = get_comments( + array( + 'post_id' => $post_id, + 'type' => $type_object['type'], + 'status' => 'approve', + ) + ); + + 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_object['collection'] ] = array( + 'label' => $label, + 'items' => array_map( + function ( $comment ) { + return array( + 'name' => $comment->comment_author, + 'url' => $comment->comment_author_url, + 'avatar' => get_comment_meta( $comment->comment_ID, 'avatar_url', true ), + ); + }, + $comments + ), + ); + } + + return new WP_REST_Response( $reactions ); + } + + /** + * Get the context for a post. + * + * @param \WP_REST_Request $request The request. + * + * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure. + */ + public static function get_context( $request ) { + $post_id = $request->get_param( 'id' ); + + $collection = Replies::get_context_collection( $post_id ); + + if ( false === $collection ) { + return new WP_Error( 'post_not_found', 'Post not found', array( 'status' => 404 ) ); + } + + $response = array_merge( + array( + '@context' => Base_Object::JSON_LD_CONTEXT, + 'id' => get_rest_url_by_path( sprintf( 'posts/%d/context', $post_id ) ), + ), + $collection + ); + + $response = \rest_ensure_response( $response ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-replies-controller.php b/wp-content/plugins/activitypub/includes/rest/class-replies-controller.php new file mode 100644 index 00000000..27e97920 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-replies-controller.php @@ -0,0 +1,256 @@ +[\w\-\.]+)s/(?P[\w\-\.]+)/(?P[\w\-\.]+)'; + + /** + * Register routes. + */ + public function register_routes() { + \register_rest_route( + $this->namespace, + '/' . $this->rest_base, + array( + 'args' => array( + 'object_type' => array( + 'description' => 'The type of object to get replies for.', + 'type' => 'string', + 'enum' => array( 'post', 'comment' ), + 'required' => true, + ), + 'id' => array( + 'description' => 'The ID of the object.', + 'type' => 'string', + 'required' => true, + ), + 'type' => array( + 'description' => 'The type of collection to query.', + 'type' => 'string', + 'enum' => array( 'replies', 'likes', 'shares' ), + 'required' => true, + ), + ), + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'page' => array( + 'description' => 'Current page of the collection.', + 'type' => 'integer', + 'minimum' => 1, + // No default so we can differentiate between Collection and CollectionPage requests. + ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Retrieves a collection of replies. + * + * @param \WP_REST_Request $request The request object. + * + * @return \WP_REST_Response|\WP_Error Response object or WP_Error object. + */ + public function get_items( $request ) { + $object_type = $request->get_param( 'object_type' ); + $id = (int) $request->get_param( 'id' ); + + if ( 'comment' === $object_type ) { + $wp_object = \get_comment( $id ); + } else { + $wp_object = \get_post( $id ); + } + + if ( ! isset( $wp_object ) || \is_wp_error( $wp_object ) ) { + return new \WP_Error( + 'activitypub_replies_collection_does_not_exist', + \sprintf( + // translators: %s: The type (post, comment, etc.) for which no replies collection exists. + \__( 'No reply collection exists for the type %s.', 'activitypub' ), + $object_type + ), + array( 'status' => 404 ) + ); + } + + switch ( $request->get_param( 'type' ) ) { + case 'replies': + $response = $this->get_replies( $request, $wp_object ); + break; + + case 'likes': + $response = $this->get_likes( $request, $wp_object ); + break; + + case 'shares': + $response = $this->get_shares( $request, $wp_object ); + break; + + default: + $response = new \WP_Error( 'rest_unknown_collection_type', 'Unknown collection type.', array( 'status' => 404 ) ); + } + + // Prepend ActivityPub Context. + $response = array_merge( array( '@context' => Base_Object::JSON_LD_CONTEXT ), $response ); + $response = \rest_ensure_response( $response ); + $response->header( 'Content-Type', 'application/activity+json; charset=' . \get_option( 'blog_charset' ) ); + + return $response; + } + + /** + * Retrieves a collection of replies. + * + * @param \WP_REST_Request $request The request object. + * @param \WP_Post|\WP_Comment $wp_object The WordPress object. + * + * @return array Response collection of replies. + */ + public function get_replies( $request, $wp_object ) { + $page = $request->get_param( 'page' ); + + // If the request parameter page is present get the CollectionPage otherwise the Replies collection. + if ( null === $page ) { + $response = Replies::get_collection( $wp_object ); + } else { + $response = Replies::get_collection_page( $wp_object, $page ); + } + + return $response; + } + + /** + * Retrieves a collection of likes. + * + * @param \WP_REST_Request $request The request object. + * @param \WP_Post|\WP_Comment $wp_object The WordPress object. + * + * @return array Response collection of likes. + */ + public function get_likes( $request, $wp_object ) { + if ( $wp_object instanceof \WP_Post ) { + $likes = Interactions::count_by_type( $wp_object->ID, 'like' ); + } else { + $likes = 0; + } + + $response = array( + 'id' => get_rest_url_by_path( sprintf( 'posts/%d/likes', $wp_object->ID ) ), + 'type' => 'Collection', + 'totalItems' => $likes, + ); + + return $response; + } + + /** + * Retrieves a collection of shares. + * + * @param \WP_REST_Request $request The request object. + * @param \WP_Post|\WP_Comment $wp_object The WordPress object. + * + * @return array Response collection of shares. + */ + public function get_shares( $request, $wp_object ) { + if ( $wp_object instanceof \WP_Post ) { + $shares = Interactions::count_by_type( $wp_object->ID, 'repost' ); + } else { + $shares = 0; + } + + $response = array( + 'id' => get_rest_url_by_path( sprintf( 'posts/%d/shares', $wp_object->ID ) ), + 'type' => 'Collection', + 'totalItems' => $shares, + ); + + return $response; + } + + /** + * Retrieves the schema for the Replies endpoint. + * + * @return array Schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'replies', + 'type' => 'object', + 'properties' => array( + '@context' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'string', + ), + 'required' => true, + ), + 'id' => array( + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + 'type' => array( + 'type' => 'string', + 'enum' => array( 'Collection', 'OrderedCollection', 'CollectionPage', 'OrderedCollectionPage' ), + 'required' => true, + ), + 'first' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'last' => array( + 'type' => 'string', + 'format' => 'uri', + ), + 'items' => array( + 'type' => 'array', + 'items' => array( + 'type' => 'object', + ), + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-server.php b/wp-content/plugins/activitypub/includes/rest/class-server.php index aac34982..ecc4a905 100644 --- a/wp-content/plugins/activitypub/includes/rest/class-server.php +++ b/wp-content/plugins/activitypub/includes/rest/class-server.php @@ -8,9 +8,11 @@ namespace Activitypub\Rest; use WP_Error; +use WP_REST_Server; use WP_REST_Response; use Activitypub\Signature; -use Activitypub\Model\Application; + +use function Activitypub\use_authorized_fetch; /** * ActivityPub Server REST-Class. @@ -24,79 +26,36 @@ class Server { * Initialize the class, registering WordPress hooks. */ public static function init() { - self::register_routes(); - - \add_filter( 'rest_request_before_callbacks', array( self::class, 'validate_activitypub_requests' ), 9, 3 ); - \add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 ); + self::add_hooks(); } /** - * Register routes + * Add sever hooks. */ - public static function register_routes() { - \register_rest_route( - ACTIVITYPUB_REST_NAMESPACE, - '/application', - array( - array( - 'methods' => \WP_REST_Server::READABLE, - 'callback' => array( self::class, 'application_actor' ), - 'permission_callback' => '__return_true', - ), - ) - ); + public static function add_hooks() { + \add_filter( 'rest_request_before_callbacks', array( self::class, 'validate_requests' ), 9, 3 ); + \add_filter( 'rest_request_parameter_order', array( self::class, 'request_parameter_order' ), 10, 2 ); } /** - * Render Application actor profile + * Callback function to authorize an api request. * - * @return WP_REST_Response The JSON profile of the Application Actor. - */ - public static function application_actor() { - $user = new Application(); - - $json = $user->to_array(); - - $rest_response = new WP_REST_Response( $json, 200 ); - $rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) ); - - return $rest_response; - } - - /** - * Callback function to authorize each api requests + * The function is meant to be used as part of permission callbacks for rest api endpoints. * - * @see WP_REST_Request + * It verifies the signature of POST, PUT, PATCH, and DELETE requests, as well as GET requests in secure mode. + * You can use the filter 'activitypub_defer_signature_verification' to defer the signature verification. + * HEAD requests are always bypassed. * * @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch * @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch * - * @param WP_REST_Response|\WP_HTTP_Response|WP_Error|mixed $response Result to send to the client. - * Usually a WP_REST_Response or WP_Error. - * @param array $handler Route handler used for the request. - * @param \WP_REST_Request $request Request used to generate the response. + * @param \WP_REST_Request $request The request object. * - * @return mixed|WP_Error The response, error, or modified response. + * @return bool|\WP_Error True if the request is authorized, WP_Error if not. */ - public static function authorize_activitypub_requests( $response, $handler, $request ) { + public static function verify_signature( $request ) { if ( 'HEAD' === $request->get_method() ) { - return $response; - } - - if ( \is_wp_error( $response ) ) { - return $response; - } - - $route = $request->get_route(); - - // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints. - if ( - ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) || - \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) || - \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' ) || - \str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'application' ) - ) { - return $response; + return true; } /** @@ -113,14 +72,14 @@ class Server { $defer = \apply_filters( 'activitypub_defer_signature_verification', false, $request ); if ( $defer ) { - return $response; + return true; } if ( - // POST-Requests are always signed. + // POST-Requests always have to be signed. 'GET' !== $request->get_method() || // GET-Requests only require a signature in secure mode. - ( 'GET' === $request->get_method() && ACTIVITYPUB_AUTHORIZED_FETCH ) + ( 'GET' === $request->get_method() && use_authorized_fetch() ) ) { $verified_request = Signature::verify_http_signature( $request ); if ( \is_wp_error( $verified_request ) ) { @@ -132,7 +91,7 @@ class Server { } } - return $response; + return true; } /** @@ -145,7 +104,7 @@ class Server { * * @return mixed|WP_Error The response, error, or modified response. */ - public static function validate_activitypub_requests( $response, $handler, $request ) { + public static function validate_requests( $response, $handler, $request ) { if ( 'HEAD' === $request->get_method() ) { return $response; } @@ -181,4 +140,34 @@ class Server { return $response; } + + /** + * Modify the parameter priority order for a REST API request. + * + * @param string[] $order Array of types to check, in order of priority. + * @param \WP_REST_Request $request The request object. + * + * @return string[] The modified order of types to check. + */ + public static function request_parameter_order( $order, $request ) { + $route = $request->get_route(); + + // Check if it is an activitypub request and exclude webfinger and nodeinfo endpoints. + if ( ! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ) { + return $order; + } + + $method = $request->get_method(); + + if ( WP_REST_Server::CREATABLE !== $method ) { + return $order; + } + + return array( + 'JSON', + 'POST', + 'URL', + 'defaults', + ); + } } diff --git a/wp-content/plugins/activitypub/includes/rest/class-url-validator-controller.php b/wp-content/plugins/activitypub/includes/rest/class-url-validator-controller.php new file mode 100644 index 00000000..9c9474eb --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-url-validator-controller.php @@ -0,0 +1,133 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_items' ), + 'permission_callback' => array( $this, 'get_items_permissions_check' ), + 'args' => array( + 'url' => array( + 'type' => 'string', + 'format' => 'uri', + 'required' => true, + ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Check if a given request has access to validate URLs. + * + * @param \WP_REST_Request $request The request. + * + * @return bool True if the request has access to validate URLs, false otherwise. + */ + public function get_items_permissions_check( $request ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable + return current_user_can( 'edit_posts' ); + } + + /** + * Check if URL is a valid ActivityPub endpoint. + * + * @param \WP_REST_Request $request The request. + * + * @return \WP_REST_Response|\WP_Error + */ + public function get_items( $request ) { + $url = $request->get_param( 'url' ); + $object = Http::get_remote_object( $url ); + + if ( is_wp_error( $object ) ) { + return new \WP_Error( + 'activitypub_invalid_url', + __( 'Invalid URL.', 'activitypub' ), + array( 'status' => 400 ) + ); + } + + $response = array( + 'is_activitypub' => ! empty( $object['type'] ), + 'is_real_oembed' => Embed::has_real_oembed( $url ), + 'html' => false, + ); + + if ( $response['is_activitypub'] ) { + $response['html'] = wp_oembed_get( $url ); + } + + return rest_ensure_response( $response ); + } + + /** + * Get the URL validation schema. + * + * @return array + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'validated-url', + 'type' => 'object', + 'properties' => array( + 'is_activitypub' => array( + 'type' => 'boolean', + 'default' => false, + ), + 'is_real_oembed' => array( + 'type' => 'boolean', + 'default' => false, + ), + 'html' => array( + 'type' => 'string', + 'default' => false, + ), + ), + ); + + $this->schema = $schema; + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-webfinger-controller.php b/wp-content/plugins/activitypub/includes/rest/class-webfinger-controller.php new file mode 100644 index 00000000..ee75e7ad --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/class-webfinger-controller.php @@ -0,0 +1,173 @@ +namespace, + '/' . $this->rest_base, + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => array( $this, 'get_item' ), + 'permission_callback' => '__return_true', + 'args' => array( + 'resource' => array( + 'description' => 'The WebFinger resource.', + 'type' => 'string', + 'required' => true, + 'pattern' => '^(acct:)|^(https?://)(.+)$', + ), + ), + ), + 'schema' => array( $this, 'get_item_schema' ), + ) + ); + } + + /** + * Retrieves the WebFinger profile. + * + * @param \WP_REST_Request $request The request object. + * + * @return \WP_REST_Response Response object. + */ + public function get_item( $request ) { + /** + * Action triggered prior to the ActivityPub profile being created and sent to the client. + */ + \do_action( 'activitypub_rest_webfinger_pre' ); + + $resource = $request->get_param( 'resource' ); + $response = $this->get_profile( $resource ); + $code = 200; + + if ( \is_wp_error( $response ) ) { + $code = 400; + $error_data = $response->get_error_data(); + + if ( isset( $error_data['status'] ) ) { + $code = $error_data['status']; + } + } + + return new \WP_REST_Response( + $response, + $code, + array( + 'Access-Control-Allow-Origin' => '*', + 'Content-Type' => 'application/jrd+json; charset=' . \get_option( 'blog_charset' ), + ) + ); + } + + /** + * Get the WebFinger profile. + * + * @param string $webfinger The WebFinger resource. + * + * @return array|\WP_Error The WebFinger profile or WP_Error if not found. + */ + public function get_profile( $webfinger ) { + /** + * Filter the WebFinger data. + * + * @param array $data The WebFinger data. + * @param string $webfinger The WebFinger resource. + */ + return \apply_filters( 'webfinger_data', array(), $webfinger ); + } + + /** + * Retrieves the schema for the WebFinger endpoint. + * + * @return array Schema data. + */ + public function get_item_schema() { + if ( $this->schema ) { + return $this->add_additional_fields_schema( $this->schema ); + } + + $this->schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'webfinger', + 'type' => 'object', + 'required' => array( 'subject', 'links' ), + 'properties' => array( + 'subject' => array( + 'description' => 'The subject of this WebFinger record.', + 'type' => 'string', + 'format' => 'uri', + ), + 'aliases' => array( + 'description' => 'Alternative identifiers for the subject.', + 'type' => 'array', + 'items' => array( + 'type' => 'string', + 'format' => 'uri', + ), + ), + 'links' => array( + 'description' => 'Links associated with the subject.', + 'type' => 'array', + 'items' => array( + 'type' => 'object', + 'properties' => array( + 'rel' => array( + 'description' => 'The relation type of the link.', + 'type' => 'string', + 'required' => true, + ), + 'type' => array( + 'description' => 'The content type of the link.', + 'type' => 'string', + ), + 'href' => array( + 'description' => 'The target URL of the link.', + 'type' => 'string', + 'format' => 'uri', + ), + 'template' => array( + 'description' => 'A URI template for the link.', + 'type' => 'string', + 'format' => 'uri', + ), + ), + ), + ), + ), + ); + + return $this->add_additional_fields_schema( $this->schema ); + } +} diff --git a/wp-content/plugins/activitypub/includes/rest/class-webfinger.php b/wp-content/plugins/activitypub/includes/rest/class-webfinger.php deleted file mode 100644 index 868ed50d..00000000 --- a/wp-content/plugins/activitypub/includes/rest/class-webfinger.php +++ /dev/null @@ -1,115 +0,0 @@ - \WP_REST_Server::READABLE, - 'callback' => array( self::class, 'webfinger' ), - 'args' => self::request_parameters(), - 'permission_callback' => '__return_true', - ), - ) - ); - } - - /** - * WebFinger endpoint. - * - * @param \WP_REST_Request $request The request object. - * - * @return WP_REST_Response The response object. - */ - public static function webfinger( $request ) { - /** - * Action triggered prior to the ActivityPub profile being created and sent to the client. - */ - \do_action( 'activitypub_rest_webfinger_pre' ); - - $code = 200; - - $resource = $request->get_param( 'resource' ); - $response = self::get_profile( $resource ); - - if ( \is_wp_error( $response ) ) { - $code = 400; - $error_data = $response->get_error_data(); - - if ( isset( $error_data['status'] ) ) { - $code = $error_data['status']; - } - } - - return new WP_REST_Response( - $response, - $code, - array( - 'Access-Control-Allow-Origin' => '*', - 'Content-Type' => 'application/jrd+json; charset=' . get_option( 'blog_charset' ), - ) - ); - } - - /** - * The supported parameters. - * - * @return array list of parameters - */ - public static function request_parameters() { - $params = array(); - - $params['resource'] = array( - 'required' => true, - 'type' => 'string', - 'pattern' => '^(acct:)|^(https?://)(.+)$', - ); - - return $params; - } - - /** - * Get the WebFinger profile. - * - * @param string $webfinger the WebFinger resource. - * - * @return array|\WP_Error The WebFinger profile or WP_Error if not found. - */ - public static function get_profile( $webfinger ) { - /** - * Filter the WebFinger data. - * - * @param array $data The WebFinger data. - * @param string $webfinger The WebFinger resource. - */ - return apply_filters( 'webfinger_data', array(), $webfinger ); - } -} diff --git a/wp-content/plugins/activitypub/includes/rest/trait-collection.php b/wp-content/plugins/activitypub/includes/rest/trait-collection.php new file mode 100644 index 00000000..d1757cd0 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/rest/trait-collection.php @@ -0,0 +1,146 @@ +get_param( 'page' ); + $per_page = $request->get_param( 'per_page' ); + $max_pages = \ceil( $response['totalItems'] / $per_page ); + + if ( $page > $max_pages ) { + return new \WP_Error( + 'rest_post_invalid_page_number', + 'The page number requested is larger than the number of pages available.', + array( 'status' => 400 ) + ); + } + + // No need to add links if there's only one page. + if ( 1 >= $max_pages && null === $page ) { + return $response; + } + + $response['first'] = \add_query_arg( 'page', 1, $response['id'] ); + $response['last'] = \add_query_arg( 'page', $max_pages, $response['id'] ); + + // If this is a Collection request, return early. + if ( null === $page ) { + // No items in Collections, only links to CollectionPages. + unset( $response['items'], $response['orderedItems'] ); + + return $response; + } + + // Still here, so this is a Page request. Append the type. + $response['type'] .= 'Page'; + $response['partOf'] = $response['id']; + $response['id'] .= '?page=' . $page; + + if ( $max_pages > $page ) { + $response['next'] = \add_query_arg( 'page', $page + 1, $response['id'] ); + } + + if ( $page > 1 ) { + $response['prev'] = \add_query_arg( 'page', $page - 1, $response['id'] ); + } + + return $response; + } + + /** + * Get the schema for an ActivityPub Collection. + * + * Returns a schema definition for ActivityPub (Ordered)Collection and (Ordered)CollectionPage + * that controllers can use to compose their full schema by passing in their item schema. + * + * @param array $item_schema Optional. The schema for the items in the collection. Default empty array. + * @return array The collection schema. + */ + public function get_collection_schema( $item_schema = array() ) { + $collection_schema = array( + '$schema' => 'http://json-schema.org/draft-04/schema#', + 'title' => 'collection', + 'type' => 'object', + 'properties' => array( + '@context' => array( + 'description' => 'The JSON-LD context of the OrderedCollection.', + 'type' => array( 'string', 'array', 'object' ), + ), + 'id' => array( + 'description' => 'The unique identifier for the OrderedCollection.', + 'type' => 'string', + 'format' => 'uri', + ), + 'type' => array( + 'description' => 'The type of the object. Either OrderedCollection or OrderedCollectionPage.', + 'type' => 'string', + 'enum' => array( 'Collection', 'CollectionPage', 'OrderedCollection', 'OrderedCollectionPage' ), + ), + 'totalItems' => array( + 'description' => 'The total number of items in the collection.', + 'type' => 'integer', + 'minimum' => 0, + ), + 'orderedItems' => array( + 'description' => 'The ordered items in the collection.', + 'type' => 'array', + ), + 'first' => array( + 'description' => 'Link to the first page of the collection.', + 'type' => 'string', + 'format' => 'uri', + ), + 'last' => array( + 'description' => 'Link to the last page of the collection.', + 'type' => 'string', + 'format' => 'uri', + ), + 'next' => array( + 'description' => 'Link to the next page of the collection.', + 'type' => 'string', + 'format' => 'uri', + ), + 'prev' => array( + 'description' => 'Link to the previous page of the collection.', + 'type' => 'string', + 'format' => 'uri', + ), + 'partOf' => array( + 'description' => 'The OrderedCollection to which this OrderedCollectionPage belongs.', + 'type' => 'string', + 'format' => 'uri', + ), + ), + ); + + // Add the orderedItems property based on the provided item schema. + if ( ! empty( $item_schema ) ) { + $collection_schema['properties']['orderedItems']['items'] = $item_schema; + } + + return $collection_schema; + } +} diff --git a/wp-content/plugins/activitypub/includes/scheduler/class-actor.php b/wp-content/plugins/activitypub/includes/scheduler/class-actor.php new file mode 100644 index 00000000..80b90b52 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/scheduler/class-actor.php @@ -0,0 +1,146 @@ +get_blog_prefix(); + + // The user meta fields that affect a profile. + $fields = array( + $blog_prefix . 'activitypub_description', + $blog_prefix . 'activitypub_header_image', + $blog_prefix . 'activitypub_icon', + 'description', + 'display_name', + 'user_url', + ); + + if ( in_array( $meta_key, $fields, true ) ) { + self::schedule_profile_update( $user_id ); + } + } + + /** + * Send a profile update when a user is updated. + * + * @param int $user_id User ID being updated. + */ + public static function user_update( $user_id ) { + // Don't bother if the user can't publish. + if ( ! \user_can( $user_id, 'activitypub' ) ) { + return; + } + + self::schedule_profile_update( $user_id ); + } + + /** + * Theme mods only have a dynamic filter so we fudge it like this. + * + * @param mixed $value Optional. The value to be updated. Default null. + * + * @return mixed + */ + public static function blog_user_update( $value = null ) { + self::schedule_profile_update( Actors::BLOG_USER_ID ); + return $value; + } + + /** + * Schedule Activities. + * + * @param string $new_status New post status. + * @param string $old_status Old post status. + * @param \WP_Post $post Post object. + */ + public static function schedule_post_activity( $new_status, $old_status, $post ) { + if ( $post instanceof \WP_Post ) { + if ( Extra_Fields::USER_POST_TYPE === $post->post_type ) { + self::schedule_profile_update( $post->post_author ); + } elseif ( Extra_Fields::BLOG_POST_TYPE === $post->post_type ) { + self::schedule_profile_update( Actors::BLOG_USER_ID ); + } + } + } + + /** + * Send a profile update to all followers. Gets hooked into all relevant options/meta etc. + * + * @param int $user_id The user ID to update (Could be 0 for Blog-User). + */ + public static function schedule_profile_update( $user_id ) { + if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) { + return; + } + + $actor = Actors::get_by_id( $user_id ); + + if ( ! $actor || \is_wp_error( $actor ) ) { + return; + } + + $actor->set_updated( gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, time() ) ); + + add_to_outbox( $actor, 'Update', $user_id ); + } +} diff --git a/wp-content/plugins/activitypub/includes/scheduler/class-comment.php b/wp-content/plugins/activitypub/includes/scheduler/class-comment.php new file mode 100644 index 00000000..1d63f01e --- /dev/null +++ b/wp-content/plugins/activitypub/includes/scheduler/class-comment.php @@ -0,0 +1,91 @@ +user_id ) { + return; + } + + $type = false; + + if ( + 'approved' === $new_status && + 'approved' !== $old_status + ) { + $type = 'Create'; + } elseif ( 'approved' === $new_status ) { + $type = 'Update'; + \update_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', time(), true ); + } elseif ( + 'trash' === $new_status || + 'spam' === $new_status + ) { + $type = 'Delete'; + } + + if ( empty( $type ) ) { + return; + } + + // Check if comment should be federated or not. + if ( ! should_comment_be_federated( $comment ) ) { + return; + } + + add_to_outbox( $comment, $type, $comment->user_id ); + } + + /** + * Schedule Comment Activities on insert. + * + * @param int $comment_id Comment ID. + * @param \WP_Comment $comment Comment object. + */ + public static function schedule_comment_activity_on_insert( $comment_id, $comment ) { + if ( 1 === (int) $comment->comment_approved ) { + self::schedule_comment_activity( 'approved', '', $comment ); + } + } +} diff --git a/wp-content/plugins/activitypub/includes/scheduler/class-post.php b/wp-content/plugins/activitypub/includes/scheduler/class-post.php new file mode 100644 index 00000000..8ae694f3 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/scheduler/class-post.php @@ -0,0 +1,113 @@ +post_author ); + } + + /** + * Schedules Activities for attachment transitions. + * + * @param int $post_id Attachment ID. + */ + public static function transition_attachment_status( $post_id ) { + if ( \defined( 'WP_IMPORTING' ) && WP_IMPORTING ) { + return; + } + + if ( ! \post_type_supports( 'attachment', 'activitypub' ) ) { + return; + } + + $post = \get_post( $post_id ); + + switch ( \current_action() ) { + case 'add_attachment': + // Add the post to the outbox. + add_to_outbox( $post, 'Create', $post->post_author ); + break; + case 'edit_attachment': + // Update the post to the outbox. + add_to_outbox( $post, 'Update', $post->post_author ); + break; + case 'delete_attachment': + // Delete the post from the outbox. + add_to_outbox( $post, 'Delete', $post->post_author ); + break; + } + } +} diff --git a/wp-content/plugins/activitypub/includes/table/class-followers.php b/wp-content/plugins/activitypub/includes/table/class-followers.php index 991f97c1..93a89271 100644 --- a/wp-content/plugins/activitypub/includes/table/class-followers.php +++ b/wp-content/plugins/activitypub/includes/table/class-followers.php @@ -8,7 +8,7 @@ namespace Activitypub\Table; use WP_List_Table; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; use Activitypub\Collection\Followers as FollowerCollection; use function Activitypub\object_to_uri; @@ -33,7 +33,7 @@ class Followers extends WP_List_Table { */ public function __construct() { if ( get_current_screen()->id === 'settings_page_activitypub' ) { - $this->user_id = Users::BLOG_USER_ID; + $this->user_id = Actors::BLOG_USER_ID; } else { $this->user_id = \get_current_user_id(); } @@ -55,8 +55,8 @@ class Followers extends WP_List_Table { public function get_columns() { return array( 'cb' => '', - 'avatar' => \__( 'Avatar', 'activitypub' ), 'post_title' => \__( 'Name', 'activitypub' ), + 'avatar' => \__( 'Avatar', 'activitypub' ), 'username' => \__( 'Username', 'activitypub' ), 'url' => \__( 'URL', 'activitypub' ), 'published' => \__( 'Followed', 'activitypub' ), diff --git a/wp-content/plugins/activitypub/includes/transformer/class-activity-object.php b/wp-content/plugins/activitypub/includes/transformer/class-activity-object.php new file mode 100644 index 00000000..79f0983a --- /dev/null +++ b/wp-content/plugins/activitypub/includes/transformer/class-activity-object.php @@ -0,0 +1,159 @@ +transform_object_properties( $this->item ); + + if ( \is_wp_error( $activity_object ) ) { + return $activity_object; + } + + $activity_object = $this->set_audience( $activity_object ); + + return $activity_object; + } + + /** + * Get the ID of the object. + * + * @return string The ID of the object. + */ + public function get_id() { + return $this->item->get_id(); + } + + /** + * Get the attributed to. + * + * @return string The attributed to. + */ + public function get_attributed_to() { + return $this->item->get_attributed_to(); + } + + /** + * Helper function to get the @-Mentions from the post content. + * + * @return array The list of @-Mentions. + */ + protected function get_mentions() { + /** + * Filter the mentions in the post content. + * + * @param array $mentions The mentions. + * @param string $content The post content. + * @param \Activitypub\Activity\Activity $item The Activity object. + * + * @return array The filtered mentions. + */ + return apply_filters( + 'activitypub_extract_mentions', + array(), + $this->item->get_content() . ' ' . $this->item->get_summary(), + $this->item + ); + } + + /** + * Returns the content map for the post. + * + * @return array The content map for the post. + */ + protected function get_content_map() { + $content = $this->item->get_content(); + + if ( ! $content ) { + return null; + } + + return array( + $this->get_locale() => $content, + ); + } + + /** + * Returns the name map for the post. + * + * @return array The name map for the post. + */ + protected function get_name_map() { + $name = $this->item->get_name(); + + if ( ! $name ) { + return null; + } + + return array( + $this->get_locale() => $name, + ); + } + + /** + * Returns the summary map for the post. + * + * @return array The summary map for the post. + */ + protected function get_summary_map() { + $summary = $this->item->get_summary(); + + if ( ! $summary ) { + return null; + } + + return array( + $this->get_locale() => $summary, + ); + } + + /** + * Returns a list of Tags, used in the Comment. + * + * This includes Hash-Tags and Mentions. + * + * @return array The list of Tags. + */ + protected function get_tag() { + $tags = $this->item->get_tag(); + + if ( ! $tags ) { + $tags = array(); + } + + $mentions = $this->get_mentions(); + + if ( $mentions ) { + foreach ( $mentions as $mention => $url ) { + $tag = array( + 'type' => 'Mention', + 'href' => \esc_url( $url ), + 'name' => \esc_html( $mention ), + ); + $tags[] = $tag; + } + } + + return \array_unique( $tags, SORT_REGULAR ); + } +} diff --git a/wp-content/plugins/activitypub/includes/transformer/class-attachment.php b/wp-content/plugins/activitypub/includes/transformer/class-attachment.php index 98aaf8bf..65f500ca 100644 --- a/wp-content/plugins/activitypub/includes/transformer/class-attachment.php +++ b/wp-content/plugins/activitypub/includes/transformer/class-attachment.php @@ -24,11 +24,11 @@ class Attachment extends Post { * @return array The Attachments. */ protected function get_attachment() { - $mime_type = get_post_mime_type( $this->wp_object->ID ); - $media_type = preg_replace( '/(\/[a-zA-Z]+)/i', '', $mime_type ); - $type = ''; + $mime_type = \get_post_mime_type( $this->item->ID ); + $mime_type_parts = \explode( '/', $mime_type ); + $type = ''; - switch ( $media_type ) { + switch ( $mime_type_parts[0] ) { case 'audio': case 'video': $type = 'Document'; @@ -40,11 +40,11 @@ class Attachment extends Post { $attachment = array( 'type' => $type, - 'url' => wp_get_attachment_url( $this->wp_object->ID ), + 'url' => wp_get_attachment_url( $this->item->ID ), 'mediaType' => $mime_type, ); - $alt = \get_post_meta( $this->wp_object->ID, '_wp_attachment_image_alt', true ); + $alt = \get_post_meta( $this->item->ID, '_wp_attachment_image_alt', true ); if ( $alt ) { $attachment['name'] = $alt; } diff --git a/wp-content/plugins/activitypub/includes/transformer/class-base.php b/wp-content/plugins/activitypub/includes/transformer/class-base.php index 6d4c202a..792c8606 100644 --- a/wp-content/plugins/activitypub/includes/transformer/class-base.php +++ b/wp-content/plugins/activitypub/includes/transformer/class-base.php @@ -7,12 +7,13 @@ namespace Activitypub\Transformer; -use WP_Post; use WP_Comment; +use WP_Post; +use WP_Term; use Activitypub\Activity\Activity; +use Activitypub\Collection\Actors; use Activitypub\Activity\Base_Object; -use Activitypub\Collection\Replies; /** * WordPress Base Transformer. @@ -26,67 +27,182 @@ abstract class Base { * * This is the source object of the transformer. * + * @var WP_Post|WP_Comment|Base_Object|string|array|WP_Term + */ + protected $item; + + /** + * The WP_Post or WP_Comment object. + * + * @deprecated version 5.0.0 + * * @var WP_Post|WP_Comment */ protected $wp_object; + /** + * The content visibility. + * + * @var string + */ + protected $content_visibility; + /** * Static function to Transform a WordPress Object. * * This helps to chain the output of the Transformer. * - * @param WP_Post|WP_Comment $wp_object The WordPress object. + * @param WP_Post|WP_Comment|Base_Object|string|array|WP_term $item The item that should be transformed. * * @return Base */ - public static function transform( $wp_object ) { - return new static( $wp_object ); + public static function transform( $item ) { + return new static( $item ); } /** * Base constructor. * - * @param WP_Post|WP_Comment $wp_object The WordPress object. + * @param WP_Post|WP_Comment|Base_Object|string|array|WP_Term $item The item that should be transformed. */ - public function __construct( $wp_object ) { - $this->wp_object = $wp_object; + public function __construct( $item ) { + $this->item = $item; + $this->wp_object = $item; } /** * Transform all properties with available get(ter) functions. * - * @param Base_Object|object $activitypub_object The ActivityPub Object. + * @param Base_Object $activity_object The ActivityPub Object. * - * @return Base_Object|object + * @return Base_Object|\WP_Error The transformed ActivityPub Object or WP_Error on failure. */ - protected function transform_object_properties( $activitypub_object ) { - $vars = $activitypub_object->get_object_var_keys(); + protected function transform_object_properties( $activity_object ) { + if ( ! $activity_object || \is_wp_error( $activity_object ) ) { + return $activity_object; + } + + $vars = $activity_object->get_object_var_keys(); foreach ( $vars as $var ) { $getter = 'get_' . $var; - if ( method_exists( $this, $getter ) ) { - $value = call_user_func( array( $this, $getter ) ); + if ( \method_exists( $this, $getter ) ) { + $value = \call_user_func( array( $this, $getter ) ); - if ( isset( $value ) ) { + if ( null !== $value ) { $setter = 'set_' . $var; - call_user_func( array( $activitypub_object, $setter ), $value ); + /** + * Filter the value before it is set to the Activity-Object `$activity_object`. + * + * @param mixed $value The value that should be set. + * @param mixed $item The Object. + */ + $value = \apply_filters( "activitypub_transform_{$setter}", $value, $this->item ); + + /** + * Filter the value before it is set to the Activity-Object `$activity_object`. + * + * @param mixed $value The value that should be set. + * @param string $var The variable name. + * @param mixed $item The Object. + */ + $value = \apply_filters( 'activitypub_transform_set', $value, $var, $this->item ); + + \call_user_func( array( $activity_object, $setter ), $value ); } } } - return $activitypub_object; + + return $activity_object; } /** - * Transform the WordPress Object into an ActivityPub Object. + * Transform the item into an ActivityPub Object. * - * @return Base_Object|object The ActivityPub Object. + * @return Base_Object|object The Activity-Object. */ public function to_object() { - $activitypub_object = new Base_Object(); + $activity_object = new Base_Object(); + $activity_object = $this->transform_object_properties( $activity_object ); - return $this->transform_object_properties( $activitypub_object ); + if ( \is_wp_error( $activity_object ) ) { + return $activity_object; + } + + $activity_object = $this->set_audience( $activity_object ); + + return $activity_object; + } + + /** + * Get the content visibility. + * + * @return string The content visibility. + */ + public function get_content_visibility() { + if ( ! $this->content_visibility ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC; + } + + return $this->content_visibility; + } + + /** + * Set the content visibility. + * + * @param string $content_visibility The content visibility. + */ + public function set_content_visibility( $content_visibility ) { + $this->content_visibility = $content_visibility; + + return $this; + } + + /** + * Set the audience. + * + * @param Base_Object $activity_object The ActivityPub Object. + * + * @return Base_Object The ActivityPub Object. + */ + protected function set_audience( $activity_object ) { + $public = 'https://www.w3.org/ns/activitystreams#Public'; + $actor = Actors::get_by_resource( $this->get_attributed_to() ); + if ( ! $actor || is_wp_error( $actor ) ) { + $followers = null; + } else { + $followers = $actor->get_followers(); + } + $mentions = array_values( $this->get_mentions() ); + + switch ( $this->get_content_visibility() ) { + case ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC: + $activity_object->add_to( $public ); + $activity_object->add_cc( $followers ); + $activity_object->add_cc( $mentions ); + break; + case ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC: + $activity_object->add_to( $followers ); + $activity_object->add_to( $mentions ); + $activity_object->add_cc( $public ); + break; + case ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE: + $activity_object->add_to( $mentions ); + } + + return $activity_object; + } + + /** + * Transform the item to an ActivityPub ID. + * + * @return string The ID of the WordPress Object. + */ + public function to_id() { + /* @var Attachment|Comment|Json|Post|User $this Object transformer. */ + return $this->get_id(); } /** @@ -106,7 +222,7 @@ abstract class Base { $activity->set_object( $object ); // Use simple Object (only ID-URI) for Like and Announce. - if ( in_array( $type, array( 'Like', 'Announce' ), true ) ) { + if ( 'Like' === $type ) { $activity->set_object( $object->get_id() ); } @@ -114,26 +230,159 @@ abstract class Base { } /** - * Get the ID of the WordPress Object. + * Returns a generic locale based on the Blog settings. + * + * @return string The locale of the blog. */ - abstract protected function get_id(); + protected function get_locale() { + $lang = \strtolower( \strtok( \get_locale(), '_-' ) ); - /** - * Get the replies Collection. - */ - public function get_replies() { - return Replies::get_collection( $this->wp_object ); + if ( $this->item instanceof \WP_Post ) { + /** + * Deprecates the `activitypub_post_locale` filter. + * + * @param string $lang The locale of the post. + * @param mixed $item The post object. + * + * @return string The filtered locale of the post. + */ + $lang = apply_filters_deprecated( + 'activitypub_post_locale', + array( + $lang, + $this->item->ID, + $this->item, + ), + '5.4.0', + 'activitypub_locale', + 'Use the `activitypub_locale` filter instead.' + ); + } + + /** + * Filter the locale of the post. + * + * @param string $lang The locale of the post. + * @param mixed $item The post object. + * + * @return string The filtered locale of the post. + */ + return apply_filters( 'activitypub_locale', $lang, $this->item ); } /** - * Returns the ID of the WordPress Object. + * Returns the default media type for an Object. + * + * @return string The media type. */ - abstract public function get_wp_user_id(); + public function get_media_type() { + return 'text/html'; + } /** - * Change the User-ID of the WordPress Post. + * Returns the content map for the post. * - * @param int $user_id The new user ID. + * @return array|null The content map for the post or null if not set. */ - abstract public function change_wp_user_id( $user_id ); + protected function get_content_map() { + if ( ! \method_exists( $this, 'get_content' ) || ! $this->get_content() ) { + return null; + } + + return array( + $this->get_locale() => $this->get_content(), + ); + } + + /** + * Returns the name map for the post. + * + * @return array|null The name map for the post or null if not set. + */ + protected function get_name_map() { + if ( ! \method_exists( $this, 'get_name' ) || ! $this->get_name() ) { + return null; + } + + return array( + $this->get_locale() => $this->get_name(), + ); + } + + /** + * Returns the summary map for the post. + * + * @return array|null The summary map for the post or null if not set. + */ + protected function get_summary_map() { + if ( ! \method_exists( $this, 'get_summary' ) || ! $this->get_summary() ) { + return null; + } + + return array( + $this->get_locale() => $this->get_summary(), + ); + } + + /** + * Returns the tags for the post. + * + * @return array The tags for the post. + */ + protected function get_tag() { + $tags = array(); + $mentions = $this->get_mentions(); + + foreach ( $mentions as $mention => $url ) { + $tags[] = array( + 'type' => 'Mention', + 'href' => \esc_url( $url ), + 'name' => \esc_html( $mention ), + ); + } + + return \array_unique( $tags, SORT_REGULAR ); + } + + /** + * Get the attributed to. + * + * @return string The attributed to. + */ + protected function get_attributed_to() { + return null; + } + + /** + * Extracts mentions from the content. + * + * @return array The mentions. + */ + protected function get_mentions() { + $content = ''; + + if ( method_exists( $this, 'get_content' ) ) { + $content = $content . ' ' . $this->get_content(); + } + + if ( method_exists( $this, 'get_summary' ) ) { + $content = $content . ' ' . $this->get_summary(); + } + + /** + * Filter the mentions in the post content. + * + * @param array $mentions The mentions. + * @param string $content The post content. + * @param WP_Post $post The post object. + * + * @return array The filtered mentions. + */ + return apply_filters( + 'activitypub_extract_mentions', + array(), + $content, + $this->item + ); + } } diff --git a/wp-content/plugins/activitypub/includes/transformer/class-comment.php b/wp-content/plugins/activitypub/includes/transformer/class-comment.php index 7f0d3f68..5e9562dd 100644 --- a/wp-content/plugins/activitypub/includes/transformer/class-comment.php +++ b/wp-content/plugins/activitypub/includes/transformer/class-comment.php @@ -10,10 +10,12 @@ namespace Activitypub\Transformer; use Activitypub\Webfinger; use Activitypub\Comment as Comment_Utils; use Activitypub\Model\Blog; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; +use Activitypub\Collection\Replies; use function Activitypub\is_single_user; use function Activitypub\get_rest_url_by_path; +use function Activitypub\was_comment_received; use function Activitypub\get_comment_ancestors; /** @@ -28,43 +30,30 @@ use function Activitypub\get_comment_ancestors; */ class Comment extends Base { /** - * Returns the User-ID of the WordPress Comment. + * The User as Actor Object. * - * @return int The User-ID of the WordPress Comment + * @var \Activitypub\Activity\Actor */ - public function get_wp_user_id() { - return $this->wp_object->user_id; - } - - /** - * Change the User-ID of the WordPress Comment. - * - * @param int $user_id The new user ID. - */ - public function change_wp_user_id( $user_id ) { - $this->wp_object->user_id = $user_id; - } + private $actor_object = null; /** * Transforms the WP_Comment object to an ActivityPub Object. * - * @see \Activitypub\Activity\Base_Object - * * @return \Activitypub\Activity\Base_Object The ActivityPub Object. */ public function to_object() { - $comment = $this->wp_object; + $comment = $this->item; $object = parent::to_object(); $object->set_url( $this->get_id() ); $object->set_type( 'Note' ); $published = \strtotime( $comment->comment_date_gmt ); - $object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) ); + $object->set_published( \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $published ) ); $updated = \get_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', true ); if ( $updated > $published ) { - $object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) ); + $object->set_updated( \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $updated ) ); } $object->set_content_map( @@ -72,18 +61,38 @@ class Comment extends Base { $this->get_locale() => $this->get_content(), ) ); - $path = sprintf( 'actors/%d/followers', intval( $comment->comment_author ) ); - - $object->set_to( - array( - 'https://www.w3.org/ns/activitystreams#Public', - get_rest_url_by_path( $path ), - ) - ); return $object; } + /** + * Get the content visibility. + * + * @return string The content visibility. + */ + public function get_content_visibility() { + if ( $this->content_visibility ) { + return $this->content_visibility; + } + + $comment = $this->item; + $post = \get_post( $comment->comment_post_ID ); + + if ( ! $post ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC; + } + + $content_visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true ); + + if ( ! $content_visibility ) { + return ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC; + } + + $this->content_visibility = $content_visibility; + + return $this->content_visibility; + } + /** * Returns the User-URL of the Author of the Post. * @@ -92,12 +101,12 @@ class Comment extends Base { * @return string The User-URL. */ protected function get_attributed_to() { - if ( is_single_user() ) { - $user = new Blog(); - return $user->get_url(); + // If the comment was received via ActivityPub, return the author URL. + if ( was_comment_received( $this->item ) ) { + return $this->item->comment_author_url; } - return Users::get_by_id( $this->wp_object->user_id )->get_url(); + return $this->get_actor_object()->get_id(); } /** @@ -108,8 +117,19 @@ class Comment extends Base { * @return string The content. */ protected function get_content() { - $comment = $this->wp_object; - $content = $comment->comment_content; + $comment = $this->item; + $content = $comment->comment_content; + $mentions = ''; + + foreach ( $this->extract_reply_context() as $acct => $url ) { + $mentions .= sprintf( + '%3$s ', + esc_url( $url ), + esc_attr( $acct ), + esc_html( '@' . strtok( $acct, '@' ) ) + ); + } + $content = $mentions . $content; /** * Filter the content of the comment. @@ -141,7 +161,7 @@ class Comment extends Base { * @return false|string|null The URL of the in-reply-to. */ protected function get_in_reply_to() { - $comment = $this->wp_object; + $comment = $this->item; $parent_comment = null; if ( $comment->comment_parent ) { @@ -169,53 +189,37 @@ class Comment extends Base { * @return string ActivityPub URI for comment */ protected function get_id() { - $comment = $this->wp_object; + $comment = $this->item; return Comment_Utils::generate_id( $comment ); } /** - * Returns a list of Mentions, used in the Comment. + * Returns the User-Object of the Author of the Post. * - * @see https://docs.joinmastodon.org/spec/activitypub/#Mention + * If `single_user` mode is enabled, the Blog-User is returned. * - * @return array The list of Mentions. + * @return \Activitypub\Activity\Actor The User-Object. */ - protected function get_cc() { - $cc = array(); - - $mentions = $this->get_mentions(); - if ( $mentions ) { - foreach ( $mentions as $url ) { - $cc[] = $url; - } + protected function get_actor_object() { + if ( $this->actor_object ) { + return $this->actor_object; } - return array_unique( $cc ); - } + $blog_user = new Blog(); + $this->actor_object = $blog_user; - /** - * Returns a list of Tags, used in the Comment. - * - * This includes Hash-Tags and Mentions. - * - * @return array The list of Tags. - */ - protected function get_tag() { - $tags = array(); - - $mentions = $this->get_mentions(); - if ( $mentions ) { - foreach ( $mentions as $mention => $url ) { - $tag = array( - 'type' => 'Mention', - 'href' => \esc_url( $url ), - 'name' => \esc_html( $mention ), - ); - $tags[] = $tag; - } + if ( is_single_user() ) { + return $blog_user; } - return \array_unique( $tags, SORT_REGULAR ); + $user = Actors::get_by_id( $this->item->user_id ); + + if ( $user && ! is_wp_error( $user ) ) { + $this->actor_object = $user; + return $user; + } + + return $blog_user; } /** @@ -235,7 +239,7 @@ class Comment extends Base { * * @return array The filtered list of mentions. */ - return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_object->comment_content, $this->wp_object ); + return apply_filters( 'activitypub_extract_mentions', array(), $this->item->comment_content, $this->item ); } /** @@ -244,7 +248,7 @@ class Comment extends Base { * @return array The list of ancestors. */ protected function get_comment_ancestors() { - $ancestors = get_comment_ancestors( $this->wp_object ); + $ancestors = get_comment_ancestors( $this->item ); // Now that we have the full tree of ancestors, only return the ones received from the fediverse. return array_filter( @@ -259,13 +263,13 @@ class Comment extends Base { * Collect all other Users that participated in this comment-thread * to send them a notification about the new reply. * - * @param array $mentions The already mentioned ActivityPub users. + * @param array $mentions Optional. The already mentioned ActivityPub users. Default empty array. * * @return array The list of all Repliers. */ - public function extract_reply_context( $mentions ) { - // Check if `$this->wp_object` is a WP_Comment. - if ( 'WP_Comment' !== get_class( $this->wp_object ) ) { + public function extract_reply_context( $mentions = array() ) { + // Check if `$this->item` is a WP_Comment. + if ( 'WP_Comment' !== get_class( $this->item ) ) { return $mentions; } @@ -289,23 +293,69 @@ class Comment extends Base { } /** - * Returns the locale of the post. + * Returns the updated date of the comment. * - * @return string The locale of the post. + * @return string|null The updated date of the comment. */ - public function get_locale() { - $comment_id = $this->wp_object->ID; - $lang = \strtolower( \strtok( \get_locale(), '_-' ) ); + public function get_updated() { + $updated = \get_comment_meta( $this->item->comment_ID, 'activitypub_comment_modified', true ); + $published = \get_comment_meta( $this->item->comment_ID, 'activitypub_comment_published', true ); - /** - * Filter the locale of the comment. - * - * @param string $lang The locale of the comment. - * @param int $comment_id The comment ID. - * @param \WP_Post $post The comment object. - * - * @return string The filtered locale of the comment. - */ - return apply_filters( 'activitypub_comment_locale', $lang, $comment_id, $this->wp_object ); + if ( $updated > $published ) { + return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $updated ); + } + + return null; + } + + /** + * Returns the published date of the comment. + * + * @return string The published date of the comment. + */ + public function get_published() { + return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, \strtotime( $this->item->comment_date_gmt ) ); + } + + /** + * Returns the URL of the comment. + * + * @return string The URL of the comment. + */ + public function get_url() { + return $this->get_id(); + } + + /** + * Returns the type of the comment. + * + * @return string The type of the comment. + */ + public function get_type() { + return 'Note'; + } + + /** + * Get the context of the post. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context + * + * @return string The context of the post. + */ + protected function get_context() { + if ( $this->item->comment_post_ID ) { + return get_rest_url_by_path( sprintf( 'posts/%d/context', $this->item->comment_post_ID ) ); + } + + return null; + } + + /** + * Get the replies Collection. + * + * @return array|null The replies collection on success or null on failure. + */ + public function get_replies() { + return Replies::get_collection( $this->item ); } } diff --git a/wp-content/plugins/activitypub/includes/transformer/class-factory.php b/wp-content/plugins/activitypub/includes/transformer/class-factory.php index a619423a..a24300d0 100644 --- a/wp-content/plugins/activitypub/includes/transformer/class-factory.php +++ b/wp-content/plugins/activitypub/includes/transformer/class-factory.php @@ -8,6 +8,11 @@ namespace Activitypub\Transformer; use WP_Error; +use Activitypub\Http; +use Activitypub\Comment as Comment_Helper; + +use function Activitypub\is_post_disabled; +use function Activitypub\user_can_activitypub; /** * Transformer Factory. @@ -21,12 +26,23 @@ class Factory { * @return Base|WP_Error The transformer to use, or an error. */ public static function get_transformer( $data ) { - if ( ! \is_object( $data ) ) { + if ( \is_string( $data ) && \filter_var( $data, FILTER_VALIDATE_URL ) ) { + $response = Http::get_remote_object( $data ); + + if ( \is_wp_error( $response ) ) { + return $response; + } + + $class = 'json'; + $data = $response; + } elseif ( \is_array( $data ) || \is_string( $data ) ) { + $class = 'json'; + } elseif ( \is_object( $data ) ) { + $class = \get_class( $data ); + } else { return new WP_Error( 'invalid_object', __( 'Invalid object', 'activitypub' ) ); } - $class = \get_class( $data ); - /** * Filter the transformer for a given object. * @@ -50,9 +66,9 @@ class Factory { * return $transformer; * }, 10, 3 ); * - * @param Base $transformer The transformer to use. - * @param mixed $data The object to transform. - * @param string $object_class The class of the object to transform. + * @param null|Base $transformer The transformer to use. Default null. + * @param mixed $data The object to transform. + * @param string $object_class The class of the object to transform. * * @return mixed The transformer to use. */ @@ -72,14 +88,30 @@ class Factory { // Use default transformer. switch ( $class ) { case 'WP_Post': - if ( 'attachment' === $data->post_type ) { + if ( 'attachment' === $data->post_type && ! is_post_disabled( $data ) ) { return new Attachment( $data ); + } elseif ( ! is_post_disabled( $data ) ) { + return new Post( $data ); } - return new Post( $data ); + break; case 'WP_Comment': - return new Comment( $data ); - default: - return null; + if ( Comment_Helper::should_be_federated( $data ) ) { + return new Comment( $data ); + } + break; + case 'WP_User': + if ( user_can_activitypub( $data->ID ) ) { + return new User( $data ); + } + break; + case 'json': + return new Json( $data ); } + + if ( $data instanceof \Activitypub\Activity\Base_Object ) { + return new Activity_Object( $data ); + } + + return new WP_Error( 'invalid_object', __( 'Invalid object', 'activitypub' ) ); } } diff --git a/wp-content/plugins/activitypub/includes/transformer/class-json.php b/wp-content/plugins/activitypub/includes/transformer/class-json.php new file mode 100644 index 00000000..9829c220 --- /dev/null +++ b/wp-content/plugins/activitypub/includes/transformer/class-json.php @@ -0,0 +1,41 @@ +wp_object->post_author; - } - - /** - * Change the User-ID of the WordPress Post. - * - * @param int $user_id The new user ID. - * - * @return Post The Post Object. - */ - public function change_wp_user_id( $user_id ) { - $this->wp_object->post_author = $user_id; - - return $this; - } - /** * Transforms the WP_Post object to an ActivityPub Object * - * @see \Activitypub\Activity\Base_Object - * * @return \Activitypub\Activity\Base_Object The ActivityPub Object */ public function to_object() { - $post = $this->wp_object; + $post = $this->item; $object = parent::to_object(); $content_warning = get_content_warning( $post ); @@ -80,6 +62,19 @@ class Post extends Base { return $object; } + /** + * Get the content visibility. + * + * @return string The content visibility. + */ + public function get_content_visibility() { + if ( ! $this->content_visibility ) { + return get_content_visibility( $this->item ); + } + + return $this->content_visibility; + } + /** * Returns the User-Object of the Author of the Post. * @@ -87,7 +82,7 @@ class Post extends Base { * * @return \Activitypub\Activity\Actor The User-Object. */ - protected function get_actor_object() { + public function get_actor_object() { if ( $this->actor_object ) { return $this->actor_object; } @@ -99,7 +94,7 @@ class Post extends Base { return $blog_user; } - $user = Users::get_by_id( $this->wp_object->post_author ); + $user = Actors::get_by_id( $this->item->post_author ); if ( $user && ! is_wp_error( $user ) ) { $this->actor_object = $user; @@ -114,7 +109,15 @@ class Post extends Base { * * @return string The Posts ID. */ - protected function get_id() { + public function get_id() { + $last_legacy_id = (int) \get_option( 'activitypub_last_post_with_permalink_as_id', 0 ); + $post_id = (int) $this->item->ID; + + if ( $post_id > $last_legacy_id ) { + // Generate URI based on post ID. + return \add_query_arg( 'p', $post_id, \trailingslashit( \home_url() ) ); + } + return $this->get_url(); } @@ -124,11 +127,11 @@ class Post extends Base { * @return string The Posts URL. */ public function get_url() { - $post = $this->wp_object; + $post = $this->item; switch ( \get_post_status( $post ) ) { case 'trash': - $permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true ); + $permalink = \get_post_meta( $post->ID, '_activitypub_canonical_url', true ); break; case 'draft': // Get_sample_permalink is in wp-admin, not always loaded. @@ -154,7 +157,116 @@ class Post extends Base { * @return string The User-URL. */ protected function get_attributed_to() { - return $this->get_actor_object()->get_url(); + return $this->get_actor_object()->get_id(); + } + + /** + * Returns the featured image as `Image`. + * + * @return array|null The Image or null if no image is available. + */ + protected function get_image() { + $post_id = $this->item->ID; + + // List post thumbnail first if this post has one. + if ( + ! \function_exists( 'has_post_thumbnail' ) || + ! \has_post_thumbnail( $post_id ) + ) { + return null; + } + + $id = \get_post_thumbnail_id( $post_id ); + $image_size = 'large'; + + /** + * Filter the image URL returned for each post. + * + * @param array|false $thumbnail The image URL, or false if no image is available. + * @param int $id The attachment ID. + * @param string $image_size The image size to retrieve. Set to 'large' by default. + */ + $thumbnail = apply_filters( + 'activitypub_get_image', + $this->get_wordpress_attachment( $id, $image_size ), + $id, + $image_size + ); + + if ( ! $thumbnail ) { + return null; + } + + $mime_type = \get_post_mime_type( $id ); + + $image = array( + 'type' => 'Image', + 'url' => \esc_url( $thumbnail[0] ), + 'mediaType' => \esc_attr( $mime_type ), + ); + + $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); + if ( $alt ) { + $image['name'] = \wp_strip_all_tags( \html_entity_decode( $alt ) ); + } + + return $image; + } + + /** + * Returns an Icon, based on the Featured Image with a fallback to the site-icon. + * + * @return array|null The Icon or null if no icon is available. + */ + protected function get_icon() { + $post_id = $this->item->ID; + + // List post thumbnail first if this post has one. + if ( \has_post_thumbnail( $post_id ) ) { + $id = \get_post_thumbnail_id( $post_id ); + } else { + // Try site_logo, falling back to site_icon, first. + $id = get_option( 'site_icon' ); + } + + if ( ! $id ) { + return null; + } + + $image_size = 'thumbnail'; + + /** + * Filter the image URL returned for each post. + * + * @param array|false $thumbnail The image URL, or false if no image is available. + * @param int $id The attachment ID. + * @param string $image_size The image size to retrieve. Set to 'large' by default. + */ + $thumbnail = apply_filters( + 'activitypub_get_image', + $this->get_wordpress_attachment( $id, $image_size ), + $id, + $image_size + ); + + if ( ! $thumbnail ) { + return null; + } + + $mime_type = \get_post_mime_type( $id ); + + $image = array( + 'type' => 'Image', + 'url' => \esc_url( $thumbnail[0] ), + 'mediaType' => \esc_attr( $mime_type ), + ); + + $alt = \get_post_meta( $id, '_wp_attachment_image_alt', true ); + if ( $alt ) { + $image['name'] = \wp_strip_all_tags( \html_entity_decode( $alt ) ); + } + + return $image; } /** @@ -164,12 +276,19 @@ class Post extends Base { */ protected function get_attachment() { // Remove attachments from drafts. - if ( 'draft' === \get_post_status( $this->wp_object ) ) { + if ( 'draft' === \get_post_status( $this->item ) ) { return array(); } - // Once upon a time we only supported images, but we now support audio/video as well. - // We maintain the image-centric naming for backwards compatibility. + /** + * Filters the maximum number of media attachments allowed in a post. + * + * Despite the name suggesting only images, this filter controls the maximum number + * of all media attachments (images, audio, and video) that can be included in an + * ActivityPub post. The name is maintained for backwards compatibility. + * + * @param int $max_media Maximum number of media attachments. Default ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS. + */ $max_media = \intval( \apply_filters( 'activitypub_max_image_attachments', @@ -178,11 +297,11 @@ class Post extends Base { ); $media = array( + 'image' => array(), 'audio' => array(), 'video' => array(), - 'image' => array(), ); - $id = $this->wp_object->ID; + $id = $this->item->ID; // List post thumbnail first if this post has one. if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) { @@ -191,13 +310,13 @@ class Post extends Base { $media = $this->get_enclosures( $media ); - if ( site_supports_blocks() && \has_blocks( $this->wp_object->post_content ) ) { + if ( site_supports_blocks() && \has_blocks( $this->item->post_content ) ) { $media = $this->get_block_attachments( $media, $max_media ); } else { - $media = $this->get_classic_editor_images( $media, $max_media ); + $media = $this->get_classic_editor_image_embeds( $media, $max_media ); } - $media = self::filter_media_by_object_type( $media, \get_post_format( $this->wp_object ), $this->wp_object ); + $media = $this->filter_media_by_object_type( $media, \get_post_format( $this->item ), $this->item ); $unique_ids = \array_unique( \array_column( $media, 'id' ) ); $media = \array_intersect_key( $media, $unique_ids ); $media = \array_slice( $media, 0, $max_media ); @@ -205,24 +324,383 @@ class Post extends Base { /** * Filter the attachment IDs for a post. * - * @param array $media The media array grouped by type. - * @param WP_Post $this->wp_object The post object. + * @param array $media The media array grouped by type. + * @param WP_Post $item The post object. * * @return array The filtered attachment IDs. */ - $media = \apply_filters( 'activitypub_attachment_ids', $media, $this->wp_object ); + $media = \apply_filters( 'activitypub_attachment_ids', $media, $this->item ); - $attachments = \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media ) ); + $attachments = \array_filter( \array_map( array( $this, 'wp_attachment_to_activity_attachment' ), $media ) ); /** * Filter the attachments for a post. * - * @param array $attachments The attachments. - * @param WP_Post $this->wp_object The post object. + * @param array $attachments The attachments. + * @param WP_Post $item The post object. * * @return array The filtered attachments. */ - return \apply_filters( 'activitypub_attachments', $attachments, $this->wp_object ); + return \apply_filters( 'activitypub_attachments', $attachments, $this->item ); + } + + /** + * Returns the ActivityStreams 2.0 Object-Type for a Post based on the + * settings and the Post-Type. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types + * + * @return string The Object-Type. + */ + protected function get_type() { + $post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ); + + if ( 'wordpress-post-format' !== $post_format_setting ) { + return \ucfirst( $post_format_setting ); + } + + $has_title = \post_type_supports( $this->item->post_type, 'title' ); + $content = \wp_strip_all_tags( $this->item->post_content ); + + // Check if the post has a title. + if ( + ! $has_title || + ! $this->item->post_title || + \strlen( $content ) <= ACTIVITYPUB_NOTE_LENGTH + ) { + return 'Note'; + } + + // Default to Note. + $object_type = 'Note'; + $post_type = \get_post_type( $this->item ); + + if ( 'page' === $post_type ) { + $object_type = 'Page'; + } elseif ( ! \get_post_format( $this->item ) ) { + $object_type = 'Article'; + } + + return $object_type; + } + + /** + * Returns the Audience for the Post. + * + * @return string|null The audience. + */ + public function get_audience() { + $actor_mode = \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ); + + if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE === $actor_mode ) { + $blog = new Blog(); + return $blog->get_id(); + } + + return null; + } + + /** + * Returns a list of Tags, used in the Post. + * + * This includes Hash-Tags and Mentions. + * + * @return array The list of Tags. + */ + protected function get_tag() { + $tags = parent::get_tag(); + + $post_tags = \get_the_tags( $this->item->ID ); + if ( $post_tags ) { + foreach ( $post_tags as $post_tag ) { + // Tag can be empty. + if ( ! $post_tag ) { + continue; + } + + $tags[] = array( + 'type' => 'Hashtag', + 'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ), + 'name' => esc_hashtag( $post_tag->name ), + ); + } + } + + return \array_unique( $tags, SORT_REGULAR ); + } + + /** + * Returns the summary for the ActivityPub Item. + * + * The summary will be generated based on the user settings and only if the + * object type is not set to `note`. + * + * @return string|null The summary or null if the object type is `note`. + */ + protected function get_summary() { + if ( 'Note' === $this->get_type() ) { + return null; + } + + // Remove Teaser from drafts. + if ( ! $this->is_preview() && 'draft' === \get_post_status( $this->item ) ) { + return \__( '(This post is being modified)', 'activitypub' ); + } + + return generate_post_summary( $this->item ); + } + + /** + * Returns the title for the ActivityPub Item. + * + * The title will be generated based on the user settings and only if the + * object type is not set to `note`. + * + * @return string|null The title or null if the object type is `note`. + */ + protected function get_name() { + if ( 'Note' === $this->get_type() ) { + return null; + } + + $title = \get_the_title( $this->item->ID ); + + if ( ! $title ) { + return null; + } + + return \wp_strip_all_tags( + \html_entity_decode( + $title + ) + ); + } + + /** + * Returns the content for the ActivityPub Item. + * + * The content will be generated based on the user settings. + * + * @return string The content. + */ + protected function get_content() { + \add_filter( 'activitypub_reply_block', '__return_empty_string' ); + + // Remove Content from drafts. + if ( ! $this->is_preview() && 'draft' === \get_post_status( $this->item ) ) { + return \__( '(This post is being modified)', 'activitypub' ); + } + + global $post; + + /** + * Provides an action hook so plugins can add their own hooks/filters before AP content is generated. + * + * Example: if a plugin adds a filter to `the_content` to add a button to the end of posts, it can also remove that filter here. + * + * @param WP_Post $post The post object. + */ + \do_action( 'activitypub_before_get_content', $post ); + + \add_filter( 'render_block_core/embed', array( $this, 'revert_embed_links' ), 10, 2 ); + \add_filter( 'render_block_activitypub/reply', array( $this, 'generate_reply_link' ), 10, 2 ); + + // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited + $post = $this->item; + $content = $this->get_post_content_template(); + + // It seems that shortcodes are only applied to published posts. + if ( is_preview() ) { + $post->post_status = 'publish'; + } + + // Register our shortcodes just in time. + Shortcodes::register(); + // Fill in the shortcodes. + \setup_postdata( $post ); + $content = \do_shortcode( $content ); + \wp_reset_postdata(); + + $content = \wpautop( $content ); + $content = \preg_replace( '/[\n\r\t]/', '', $content ); + $content = \trim( $content ); + + /** + * Filters the post content before it is transformed for ActivityPub. + * + * @param string $content The post content to be transformed. + * @param WP_Post $post The post object being transformed. + */ + $content = \apply_filters( 'activitypub_the_content', $content, $post ); + + // Don't need these anymore, should never appear in a post. + Shortcodes::unregister(); + + // Get rid of the reply block filter. + \remove_filter( 'render_block_activitypub/reply', array( $this, 'generate_reply_link' ), 10, 2 ); + \remove_filter( 'render_block_core/embed', array( $this, 'revert_embed_links' ) ); + \remove_filter( 'activitypub_reply_block', '__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 function generate_reply_link( $block_content, $block ) { + // 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 = \Activitypub\Http::get_remote_object( $url ); + if ( \is_wp_error( $object ) ) { + return ''; + } + + $author_url = $object['attributedTo'] ?? ''; + if ( ! $author_url ) { + return ''; + } + + // Fetch author information. + $author = \Activitypub\Http::get_remote_object( $author_url ); + if ( \is_wp_error( $author ) ) { + return ''; + } + + // Get webfinger identifier. + $webfinger = ''; + if ( ! empty( $author['webfinger'] ) ) { + $webfinger = $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, '@' ) ) + ); + } + + /** + * Returns the in-reply-to URL of the post. + * + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-inreplyto + * + * @return string|null The in-reply-to URL of the post. + */ + protected function get_in_reply_to() { + if ( ! site_supports_blocks() ) { + return null; + } + + $blocks = \parse_blocks( $this->item->post_content ); + + foreach ( $blocks as $block ) { + if ( 'activitypub/reply' === $block['blockName'] && isset( $block['attrs']['url'] ) ) { + // We only support one reply block per post for now. + return $block['attrs']['url']; + } + } + + return null; + } + + /** + * Returns the published date of the post. + * + * @return string The published date of the post. + */ + protected function get_published() { + $published = \strtotime( $this->item->post_date_gmt ); + + return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $published ); + } + + /** + * Returns the updated date of the post. + * + * @return string|null The updated date of the post. + */ + protected function get_updated() { + $published = \strtotime( $this->item->post_date_gmt ); + $updated = \strtotime( $this->item->post_modified_gmt ); + + if ( $updated > $published ) { + return \gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, $updated ); + } + + return null; + } + + /** + * Helper function to extract the @-Mentions from the post content. + * + * @return array The list of @-Mentions. + */ + protected function get_mentions() { + /** + * Filter the mentions in the post content. + * + * @param array $mentions The mentions. + * @param string $content The post content. + * @param WP_Post $post The post object. + * + * @return array The filtered mentions. + */ + return apply_filters( + 'activitypub_extract_mentions', + array(), + $this->item->post_content . ' ' . $this->item->post_excerpt, + $this->item + ); + } + + /** + * 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 function revert_embed_links( $block_content, $block ) { + if ( ! isset( $block['attrs']['url'] ) ) { + return $block_content; + } + return '

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

'; + } + + /** + * Check if the post is a preview. + * + * @return boolean True if the post is a preview, false otherwise. + */ + private function is_preview() { + return defined( 'ACTIVITYPUB_PREVIEW' ) && ACTIVITYPUB_PREVIEW; } /** @@ -232,8 +710,8 @@ class Post extends Base { * * @return array The media array extended with enclosures. */ - public function get_enclosures( $media ) { - $enclosures = get_enclosures( $this->wp_object->ID ); + protected function get_enclosures( $media ) { + $enclosures = get_enclosures( $this->item->ID ); if ( ! $enclosures ) { return $media; @@ -242,14 +720,16 @@ class Post extends Base { foreach ( $enclosures as $enclosure ) { // Check if URL is an attachment. $attachment_id = \attachment_url_to_postid( $enclosure['url'] ); + if ( $attachment_id ) { $enclosure['id'] = $attachment_id; $enclosure['url'] = \wp_get_attachment_url( $attachment_id ); $enclosure['mediaType'] = \get_post_mime_type( $attachment_id ); } - $mime_type = $enclosure['mediaType']; - $mime_type_parts = \explode( '/', $mime_type ); + $mime_type = $enclosure['mediaType']; + $mime_type_parts = \explode( '/', $mime_type ); + $enclosure['type'] = \ucfirst( $mime_type_parts[0] ); switch ( $mime_type_parts[0] ) { case 'image': @@ -281,9 +761,9 @@ class Post extends Base { return array(); } - $blocks = \parse_blocks( $this->wp_object->post_content ); + $blocks = \parse_blocks( $this->item->post_content ); - return self::get_media_from_blocks( $blocks, $media ); + return $this->get_media_from_blocks( $blocks, $media ); } /** @@ -294,11 +774,11 @@ class Post extends Base { * * @return array The image IDs. */ - protected static function get_media_from_blocks( $blocks, $media ) { + protected function get_media_from_blocks( $blocks, $media ) { foreach ( $blocks as $block ) { // Recurse into inner blocks. if ( ! empty( $block['innerBlocks'] ) ) { - $media = self::get_media_from_blocks( $block['innerBlocks'], $media ); + $media = $this->get_media_from_blocks( $block['innerBlocks'], $media ); } switch ( $block['blockName'] ) { @@ -312,10 +792,21 @@ class Post extends Base { $alt = $match[2]; } - $media['image'][] = array( - 'id' => $block['attrs']['id'], - 'alt' => $alt, - ); + $found = false; + foreach ( $media['image'] as $i => $image ) { + if ( isset( $image['id'] ) && $image['id'] === $block['attrs']['id'] ) { + $media['image'][ $i ]['alt'] = $alt; + $found = true; + break; + } + } + + if ( ! $found ) { + $media['image'][] = array( + 'id' => $block['attrs']['id'], + 'alt' => $alt, + ); + } } break; case 'core/audio': @@ -358,58 +849,41 @@ class Post extends Base { } /** - * Get post images from the classic editor. - * Note that audio/video attachments are only supported in the block editor. + * Get image embeds from the classic editor by parsing HTML. * * @param array $media The media array grouped by type. * @param int $max_images The maximum number of images to return. * * @return array The attachments. */ - protected function get_classic_editor_images( $media, $max_images ) { - // Max images can't be negative or zero. - if ( $max_images <= 0 ) { - return array(); - } - - if ( \count( $media['image'] ) <= $max_images ) { - if ( \class_exists( '\WP_HTML_Tag_Processor' ) ) { - $media['image'] = \array_merge( $media['image'], $this->get_classic_editor_image_embeds( $max_images ) ); - } else { - $media['image'] = \array_merge( $media['image'], $this->get_classic_editor_image_attachments( $max_images ) ); - } - } - - return $media; - } - - /** - * Get image embeds from the classic editor by parsing HTML. - * - * @param int $max_images The maximum number of images to return. - * - * @return array The attachments. - */ - protected function get_classic_editor_image_embeds( $max_images ) { + protected function get_classic_editor_image_embeds( $media, $max_images ) { // If someone calls that function directly, bail. if ( ! \class_exists( '\WP_HTML_Tag_Processor' ) ) { - return array(); + return $media; } // Max images can't be negative or zero. if ( $max_images <= 0 ) { - return array(); + return $media; } $images = array(); - $base = \wp_get_upload_dir()['baseurl']; - $content = \get_post_field( 'post_content', $this->wp_object ); + $base = get_upload_baseurl(); + $content = \get_post_field( 'post_content', $this->item ); $tags = new \WP_HTML_Tag_Processor( $content ); // This linter warning is a false positive - we have to re-count each time here as we modify $images. // phpcs:ignore Squiz.PHP.DisallowSizeFunctionsInLoops.Found while ( $tags->next_tag( 'img' ) && ( \count( $images ) <= $max_images ) ) { - $src = $tags->get_attribute( 'src' ); + /** + * Filter the image source URL. + * + * This can be used to modify the image source URL before it is used to + * determine the attachment ID. + * + * @param string $src The image source URL. + */ + $src = \apply_filters( 'activitypub_image_src', $tags->get_attribute( 'src' ) ); /* * If the img source is in our uploads dir, get the @@ -424,16 +898,22 @@ class Post extends Base { if ( null !== $src && \str_starts_with( $src, $base ) ) { $img_id = \attachment_url_to_postid( $src ); + if ( 0 === $img_id ) { + $count = 0; + $src = \strtok( $src, '?' ); + $img_id = \attachment_url_to_postid( $src ); + } + if ( 0 === $img_id ) { $count = 0; - $src = preg_replace( '/-(?:\d+x\d+)(\.[a-zA-Z]+)$/', '$1', $src, 1, $count ); + $src = \preg_replace( '/-(?:\d+x\d+)(\.[a-zA-Z]+)$/', '$1', $src, 1, $count ); if ( $count > 0 ) { $img_id = \attachment_url_to_postid( $src ); } } if ( 0 === $img_id ) { - $src = preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src ); + $src = \preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src ); $img_id = \attachment_url_to_postid( $src ); } @@ -446,65 +926,32 @@ class Post extends Base { } } - return $images; - } - - /** - * Get image attachments from the classic editor. - * This is imperfect as the contained images aren't necessarily the - * same as the attachments. - * - * @param int $max_images The maximum number of images to return. - * - * @return array The attachment IDs. - */ - protected function get_classic_editor_image_attachments( $max_images ) { - // Max images can't be negative or zero. - if ( $max_images <= 0 ) { - return array(); + if ( \count( $media['image'] ) <= $max_images ) { + $media['image'] = \array_merge( $media['image'], $images ); } - $images = array(); - $query = new \WP_Query( - array( - 'post_parent' => $this->wp_object->ID, - 'post_status' => 'inherit', - 'post_type' => 'attachment', - 'post_mime_type' => 'image', - 'order' => 'ASC', - 'orderby' => 'menu_order ID', - 'posts_per_page' => $max_images, - ) - ); - - foreach ( $query->get_posts() as $attachment ) { - if ( ! \in_array( $attachment->ID, $images, true ) ) { - $images[] = array( 'id' => $attachment->ID ); - } - } - - return $images; + return $media; } /** * Filter media IDs by object type. * - * @param array $media The media array grouped by type. - * @param string $type The object type. - * @param WP_Post $wp_object The post object. + * @param array $media The media array grouped by type. + * @param string $type The object type. + * @param WP_Post $item The post object. * * @return array The filtered media IDs. */ - protected static function filter_media_by_object_type( $media, $type, $wp_object ) { + protected function filter_media_by_object_type( $media, $type, $item ) { /** * Filter the object type for media attachments. * * @param string $type The object type. - * @param WP_Post $wp_object The post object. + * @param WP_Post $item The post object. * * @return string The filtered object type. */ - $type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ), $wp_object ); + $type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ), $item ); if ( ! empty( $media[ $type ] ) ) { return $media[ $type ]; @@ -520,7 +967,7 @@ class Post extends Base { * * @return array The ActivityPub Attachment. */ - public static function wp_attachment_to_activity_attachment( $media ) { + public function wp_attachment_to_activity_attachment( $media ) { if ( ! isset( $media['id'] ) ) { return $media; } @@ -543,7 +990,7 @@ class Post extends Base { */ $thumbnail = apply_filters( 'activitypub_get_image', - self::get_wordpress_attachment( $id, $image_size ), + $this->get_wordpress_attachment( $id, $image_size ), $id, $image_size ); @@ -582,7 +1029,11 @@ class Post extends Base { $attachment['width'] = \esc_attr( $meta['width'] ); $attachment['height'] = \esc_attr( $meta['height'] ); } - // @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail? + + if ( $this->get_icon() ) { + $attachment['icon'] = object_to_uri( $this->get_icon() ); + } + break; } @@ -605,7 +1056,7 @@ class Post extends Base { * * @return array|false Array of image data, or boolean false if no image is available. */ - protected static function get_wordpress_attachment( $id, $image_size = 'large' ) { + protected function get_wordpress_attachment( $id, $image_size = 'large' ) { /** * Hook into the image retrieval process. Before image retrieval. * @@ -628,225 +1079,14 @@ class Post extends Base { } /** - * Returns the ActivityStreams 2.0 Object-Type for a Post based on the - * settings and the Post-Type. + * Get the context of the post. * - * @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types + * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context * - * @return string The Object-Type. + * @return string The context of the post. */ - protected function get_type() { - $post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ); - - if ( 'wordpress-post-format' !== $post_format_setting ) { - return \ucfirst( $post_format_setting ); - } - - $has_title = post_type_supports( $this->wp_object->post_type, 'title' ); - - if ( ! $has_title ) { - return 'Note'; - } - - // Default to Article. - $object_type = 'Article'; - $post_format = 'standard'; - - if ( \get_theme_support( 'post-formats' ) ) { - $post_format = \get_post_format( $this->wp_object ); - } - - $post_type = \get_post_type( $this->wp_object ); - switch ( $post_type ) { - case 'post': - switch ( $post_format ) { - case 'standard': - case '': - $object_type = 'Article'; - break; - default: - $object_type = 'Note'; - break; - } - break; - case 'page': - $object_type = 'Page'; - break; - default: - $object_type = 'Article'; - break; - } - - return $object_type; - } - - /** - * Returns a list of Mentions, used in the Post. - * - * @see https://docs.joinmastodon.org/spec/activitypub/#Mention - * - * @return array The list of Mentions. - */ - protected function get_cc() { - $cc = array(); - - $mentions = $this->get_mentions(); - if ( $mentions ) { - foreach ( $mentions as $url ) { - $cc[] = $url; - } - } - - return $cc; - } - - /** - * Returns the Audience for the Post. - * - * @return string|null The audience. - */ - public function get_audience() { - if ( is_single_user() ) { - return null; - } else { - $blog = new Blog(); - return $blog->get_id(); - } - } - - /** - * Returns a list of Tags, used in the Post. - * - * This includes Hash-Tags and Mentions. - * - * @return array The list of Tags. - */ - protected function get_tag() { - $tags = array(); - - $post_tags = \get_the_tags( $this->wp_object->ID ); - if ( $post_tags ) { - foreach ( $post_tags as $post_tag ) { - $tag = array( - 'type' => 'Hashtag', - 'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ), - 'name' => esc_hashtag( $post_tag->name ), - ); - $tags[] = $tag; - } - } - - $mentions = $this->get_mentions(); - if ( $mentions ) { - foreach ( $mentions as $mention => $url ) { - $tag = array( - 'type' => 'Mention', - 'href' => \esc_url( $url ), - 'name' => \esc_html( $mention ), - ); - $tags[] = $tag; - } - } - - return $tags; - } - - /** - * Returns the summary for the ActivityPub Item. - * - * The summary will be generated based on the user settings and only if the - * object type is not set to `note`. - * - * @return string|null The summary or null if the object type is `note`. - */ - protected function get_summary() { - if ( 'Note' === $this->get_type() ) { - return null; - } - - // Remove Teaser from drafts. - if ( 'draft' === \get_post_status( $this->wp_object ) ) { - return \__( '(This post is being modified)', 'activitypub' ); - } - - return generate_post_summary( $this->wp_object ); - } - - /** - * Returns the title for the ActivityPub Item. - * - * The title will be generated based on the user settings and only if the - * object type is not set to `note`. - * - * @return string|null The title or null if the object type is `note`. - */ - protected function get_name() { - if ( 'Note' === $this->get_type() ) { - return null; - } - - $title = \get_the_title( $this->wp_object->ID ); - - if ( $title ) { - return \wp_strip_all_tags( - \html_entity_decode( - $title - ) - ); - } - - return null; - } - - /** - * Returns the content for the ActivityPub Item. - * - * The content will be generated based on the user settings. - * - * @return string The content. - */ - protected function get_content() { - add_filter( 'activitypub_reply_block', '__return_empty_string' ); - - // Remove Content from drafts. - if ( 'draft' === \get_post_status( $this->wp_object ) ) { - return \__( '(This post is being modified)', 'activitypub' ); - } - - global $post; - - /** - * Provides an action hook so plugins can add their own hooks/filters before AP content is generated. - * - * Example: if a plugin adds a filter to `the_content` to add a button to the end of posts, it can also remove that filter here. - * - * @param WP_Post $post The post object. - */ - do_action( 'activitypub_before_get_content', $post ); - - add_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ), 10, 2 ); - - // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited - $post = $this->wp_object; - $content = $this->get_post_content_template(); - - // Register our shortcodes just in time. - Shortcodes::register(); - // Fill in the shortcodes. - setup_postdata( $post ); - $content = do_shortcode( $content ); - wp_reset_postdata(); - - $content = \wpautop( $content ); - $content = \preg_replace( '/[\n\r\t]/', '', $content ); - $content = \trim( $content ); - - $content = \apply_filters( 'activitypub_the_content', $content, $post ); - - // Don't need these anymore, should never appear in a post. - Shortcodes::unregister(); - - return $content; + protected function get_context() { + return get_rest_url_by_path( sprintf( 'posts/%d/context', $this->item->ID ) ); } /** @@ -855,193 +1095,83 @@ class Post extends Base { * @return string The Template. */ protected function get_post_content_template() { - $type = \get_option( 'activitypub_post_content_type', 'content' ); - - switch ( $type ) { - case 'excerpt': - $template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]"; - break; - case 'title': - $template = "

[ap_title]

\n\n[ap_permalink type=\"html\"]"; - break; - case 'content': - $template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]"; - break; - default: - $content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); - $template = empty( $content ) ? ACTIVITYPUB_CUSTOM_POST_CONTENT : $content; - break; - } + $content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ); + $template = $content ?? ACTIVITYPUB_CUSTOM_POST_CONTENT; $post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ); if ( 'wordpress-post-format' === $post_format_setting ) { - $template = '[ap_content]'; - } + $template = ''; - return apply_filters( 'activitypub_object_content_template', $template, $this->wp_object ); - } - - /** - * Helper function to get the @-Mentions from the post content. - * - * @return array The list of @-Mentions. - */ - protected function get_mentions() { - /** - * Filter the mentions in the post content. - * - * @param array $mentions The mentions. - * @param string $content The post content. - * @param WP_Post $post The post object. - * - * @return array The filtered mentions. - */ - return apply_filters( - 'activitypub_extract_mentions', - array(), - $this->wp_object->post_content . ' ' . $this->wp_object->post_excerpt, - $this->wp_object - ); - } - - /** - * Returns the locale of the post. - * - * @return string The locale of the post. - */ - public function get_locale() { - $post_id = $this->wp_object->ID; - $lang = \strtolower( \strtok( \get_locale(), '_-' ) ); - - /** - * Filter the locale of the post. - * - * @param string $lang The locale of the post. - * @param int $post_id The post ID. - * @param WP_Post $post The post object. - * - * @return string The filtered locale of the post. - */ - return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_object ); - } - - /** - * Returns the in-reply-to URL of the post. - * - * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-inreplyto - * - * @return string|null The in-reply-to URL of the post. - */ - public function get_in_reply_to() { - $blocks = \parse_blocks( $this->wp_object->post_content ); - - foreach ( $blocks as $block ) { - if ( 'activitypub/reply' === $block['blockName'] ) { - // We only support one reply block per post for now. - return $block['attrs']['url']; + if ( 'Note' === $this->get_type() ) { + $template .= "[ap_title type=\"html\"]\n\n"; } + + $template .= '[ap_content]'; } - return null; + /** + * Filters the template used to generate ActivityPub object content. + * + * This filter allows developers to modify the template that determines how post + * content is formatted in ActivityPub objects. The template can include special + * shortcodes like [ap_title] and [ap_content] that are processed during content + * generation. + * + * @param string $template The template string containing shortcodes. + * @param WP_Post $item The WordPress post object being transformed. + */ + return apply_filters( 'activitypub_object_content_template', $template, $this->item ); } /** - * Returns the recipient of the post. + * Get the replies Collection. * - * @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-to - * - * @return array The recipient URLs of the post. + * @return array|null The replies collection on success or null on failure. */ - public function get_to() { + public function get_replies() { + return Replies::get_collection( $this->item ); + } + + /** + * Get the likes Collection. + * + * @return array The likes collection. + */ + public function get_likes() { return array( - 'https://www.w3.org/ns/activitystreams#Public', - $this->get_actor_object()->get_followers(), + 'id' => get_rest_url_by_path( sprintf( 'posts/%d/likes', $this->item->ID ) ), + 'type' => 'Collection', + 'totalItems' => Interactions::count_by_type( $this->item->ID, 'like' ), ); } /** - * Returns the published date of the post. + * Get the shares Collection. * - * @return string The published date of the post. + * @return array The Shares collection. */ - public function get_published() { - $published = \strtotime( $this->wp_object->post_date_gmt ); - - return \gmdate( 'Y-m-d\TH:i:s\Z', $published ); - } - - /** - * Returns the updated date of the post. - * - * @return string|null The updated date of the post. - */ - public function get_updated() { - $published = \strtotime( $this->wp_object->post_date_gmt ); - $updated = \strtotime( $this->wp_object->post_modified_gmt ); - - if ( $updated > $published ) { - return \gmdate( 'Y-m-d\TH:i:s\Z', $updated ); - } - - return null; - } - - /** - * Returns the content map for the post. - * - * @return array The content map for the post. - */ - public function get_content_map() { + public function get_shares() { return array( - $this->get_locale() => $this->get_content(), + 'id' => get_rest_url_by_path( sprintf( 'posts/%d/shares', $this->item->ID ) ), + 'type' => 'Collection', + 'totalItems' => Interactions::count_by_type( $this->item->ID, 'repost' ), ); } /** - * Returns the name map for the post. + * Get the preview of the post. * - * @return array The name map for the post. + * @return array|null The preview of the post or null if the post is not an Article. */ - public function get_name_map() { - if ( ! $this->get_name() ) { + public function get_preview() { + if ( 'Article' !== $this->get_type() ) { return null; } return array( - $this->get_locale() => $this->get_name(), + 'type' => 'Note', + 'content' => $this->get_summary(), ); } - - /** - * Returns the summary map for the post. - * - * @return array The summary map for the post. - */ - public function get_summary_map() { - if ( ! $this->get_summary() ) { - return null; - } - - return array( - $this->get_locale() => $this->get_summary(), - ); - } - - /** - * 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 ) { - return '

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

'; - } } diff --git a/wp-content/plugins/activitypub/includes/transformer/class-user.php b/wp-content/plugins/activitypub/includes/transformer/class-user.php new file mode 100644 index 00000000..418913be --- /dev/null +++ b/wp-content/plugins/activitypub/includes/transformer/class-user.php @@ -0,0 +1,41 @@ +transform_object_properties( Actors::get_by_id( $this->item->ID ) ); + + if ( \is_wp_error( $activity_object ) ) { + return $activity_object; + } + + return $activity_object; + } + + /** + * Get the Actor ID. + * + * @return string The Actor ID. + */ + public function to_id() { + return Actors::get_by_id( $this->item->ID )->get_id(); + } +} diff --git a/wp-content/plugins/activitypub/integration/class-akismet.php b/wp-content/plugins/activitypub/integration/class-akismet.php new file mode 100644 index 00000000..511cef39 --- /dev/null +++ b/wp-content/plugins/activitypub/integration/class-akismet.php @@ -0,0 +1,40 @@ + $field ) { @@ -190,7 +193,7 @@ class Enable_Mastodon_Apps { if ( $acct && ! is_wp_error( $acct ) ) { $acct = \str_replace( 'acct:', '', $acct ); } else { - $acct = $item->get_url(); + $acct = $item->get_id(); } $account = new Account(); @@ -239,7 +242,7 @@ class Enable_Mastodon_Apps { return $user_data; } - $user = Users::get_by_various( $user_id ); + $user = Actors::get_by_various( $user_id ); if ( $user && ! is_wp_error( $user ) ) { return $user_data; @@ -269,7 +272,7 @@ class Enable_Mastodon_Apps { */ public static function api_account_internal( $user_data, $user_id ) { $user_id_to_use = self::maybe_map_user_to_blog( $user_id ); - $user = Users::get_by_id( $user_id_to_use ); + $user = Actors::get_by_id( $user_id_to_use ); if ( ! $user || is_wp_error( $user ) ) { return $user_data; @@ -324,6 +327,44 @@ class Enable_Mastodon_Apps { return $account; } + /** + * Use our representation of posts to power each status item. + * Includes proper referncing of 3rd party comments that arrived via federation. + * + * @param null|Status $status The status, typically null to allow later filters their shot. + * @param int $post_id The post ID. + * @return Status|null The status. + */ + public static function api_status( $status, $post_id ) { + $post = \get_post( $post_id ); + if ( ! $post ) { + return $status; + } + + return self::api_post_status( $post_id ); + } + + /** + * Transforms a WordPress post into a Mastodon-compatible status object. + * + * Takes a post ID, transforms it into an ActivityPub object, and converts + * it to a Mastodon API status format including the author's account info. + * + * @param int $post_id The WordPress post ID to transform. + * @return Status|null The Mastodon API status object, or null if the post is not found + */ + private static function api_post_status( $post_id ) { + $post = Factory::get_transformer( get_post( $post_id ) ); + if ( is_wp_error( $post ) ) { + return null; + } + + $data = $post->to_object()->to_array(); + $account = self::api_account_internal( null, get_post_field( 'post_author', $post_id ) ); + + return self::activity_to_status( $data, $account, $post_id ); + } + /** * Get account for actor. * @@ -332,7 +373,7 @@ class Enable_Mastodon_Apps { * @return Account|null The account. */ private static function get_account_for_actor( $uri ) { - if ( ! is_string( $uri ) ) { + if ( ! is_string( $uri ) || empty( $uri ) ) { return null; } $data = get_remote_metadata_by_actor( $uri ); @@ -343,6 +384,10 @@ class Enable_Mastodon_Apps { $account = new Account(); $acct = Webfinger_Util::uri_to_acct( $uri ); + if ( ! $acct || is_wp_error( $acct ) ) { + return null; + } + if ( str_starts_with( $acct, 'acct:' ) ) { $acct = substr( $acct, 5 ); } @@ -489,22 +534,23 @@ class Enable_Mastodon_Apps { * * @param array $item The activity. * @param Account $account The account. + * @param int $post_id The post ID. Optional, but will be preferred in the Status. * * @return Status|null The status. */ - private static function activity_to_status( $item, $account ) { + private static function activity_to_status( $item, $account, $post_id = null ) { if ( isset( $item['object'] ) ) { $object = $item['object']; } else { $object = $item; } - if ( ! isset( $object['type'] ) || 'Note' !== $object['type'] ) { + if ( ! isset( $object['type'] ) || 'Note' !== $object['type'] || ! $account ) { return null; } $status = new Status(); - $status->id = $object['id']; + $status->id = $post_id ?? $object['id']; $status->created_at = new DateTime( $object['published'] ); $status->content = $object['content']; $status->account = $account; @@ -624,7 +670,7 @@ class Enable_Mastodon_Apps { $posts['orderedItems'] ); $activitypub_statuses = array_merge( $activitypub_statuses, array_filter( $new_statuses ) ); - $url = $posts['next']; + $url = $posts['next'] ?? null; if ( count( $activitypub_statuses ) >= $limit ) { break; @@ -649,20 +695,37 @@ class Enable_Mastodon_Apps { return $context; } - $replies_url = $meta['replies']['first']['next']; - $replies = Http::get_remote_object( $replies_url, true ); - if ( is_wp_error( $replies ) || ! isset( $replies['items'] ) ) { + if ( ! empty( $meta['replies']['first']['items'] ) ) { + $replies = $meta['replies']['first']; + } elseif ( isset( $meta['replies']['first']['next'] ) ) { + $replies_url = $meta['replies']['first']['next']; + $replies = Http::get_remote_object( $replies_url, true ); + if ( is_wp_error( $replies ) || ! isset( $replies['items'] ) ) { + return $context; + } + } else { return $context; } - foreach ( $replies['items'] as $url ) { - $response = Http::get( $url, true ); - if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { - continue; - } - $status = json_decode( wp_remote_retrieve_body( $response ), true ); - if ( ! $status || is_wp_error( $status ) ) { - continue; + foreach ( $replies['items'] as $reply ) { + if ( isset( $reply['id'] ) && is_string( $reply['id'] ) && isset( $reply['content'] ) && is_string( $reply['content'] ) ) { + $status = $reply; + } else { + if ( is_string( $reply ) ) { + $url = $reply; + } elseif ( isset( $reply['url'] ) && is_string( $reply['url'] ) ) { + $url = $reply['url']; + } else { + continue; + } + $response = Http::get( $url, true ); + if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) { + continue; + } + $status = json_decode( wp_remote_retrieve_body( $response ), true ); + if ( ! $status || is_wp_error( $status ) ) { + continue; + } } $account = self::get_account_for_actor( $status['attributedTo'] ); diff --git a/wp-content/plugins/activitypub/integration/class-jetpack.php b/wp-content/plugins/activitypub/integration/class-jetpack.php index 002de2cb..5586cf79 100644 --- a/wp-content/plugins/activitypub/integration/class-jetpack.php +++ b/wp-content/plugins/activitypub/integration/class-jetpack.php @@ -7,6 +7,8 @@ namespace Activitypub\Integration; +use Activitypub\Comment; + /** * Jetpack integration class. */ @@ -17,6 +19,8 @@ class Jetpack { */ public static function init() { \add_filter( 'jetpack_sync_post_meta_whitelist', array( self::class, 'add_sync_meta' ) ); + \add_filter( 'jetpack_json_api_comment_types', array( self::class, 'add_comment_types' ) ); + \add_filter( 'jetpack_api_include_comment_types_count', array( self::class, 'add_comment_types' ) ); } /** @@ -31,10 +35,20 @@ class Jetpack { return $allow_list; } $activitypub_meta_keys = array( - 'activitypub_user_id', - 'activitypub_inbox', - 'activitypub_actor_json', + '_activitypub_user_id', + '_activitypub_inbox', + '_activitypub_actor_json', ); return \array_merge( $allow_list, $activitypub_meta_keys ); } + + /** + * Add custom comment types to the list of comment types. + * + * @param array $comment_types Default comment types. + * @return array + */ + public static function add_comment_types( $comment_types ) { + return array_unique( \array_merge( $comment_types, Comment::get_comment_type_slugs() ) ); + } } diff --git a/wp-content/plugins/activitypub/integration/class-multisite-language-switcher.php b/wp-content/plugins/activitypub/integration/class-multisite-language-switcher.php new file mode 100644 index 00000000..53e18f96 --- /dev/null +++ b/wp-content/plugins/activitypub/integration/class-multisite-language-switcher.php @@ -0,0 +1,49 @@ +post_type ) { + \add_action( 'msls_main_save', '__return_null' ); + } + } + + /** + * Remove short-circuit for Multisite Language Switcher data. + * + * @param int $post_id The post id. + * @param WP_Post $post The post object. + */ + public static function unignore_outbox_post( $post_id, $post ) { + if ( Outbox::POST_TYPE === $post->post_type ) { + \remove_action( 'msls_main_save', '__return_null' ); + } + } +} diff --git a/wp-content/plugins/activitypub/integration/class-nodeinfo.php b/wp-content/plugins/activitypub/integration/class-nodeinfo.php index c5972fb3..dfbedcbf 100644 --- a/wp-content/plugins/activitypub/integration/class-nodeinfo.php +++ b/wp-content/plugins/activitypub/integration/class-nodeinfo.php @@ -24,7 +24,7 @@ class Nodeinfo { \add_filter( 'nodeinfo_data', array( self::class, 'add_nodeinfo_data' ), 10, 2 ); \add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_data' ) ); - \add_filter( 'wellknown_nodeinfo_data', array( self::class, 'add_wellknown_nodeinfo_data' ), 10, 2 ); + \add_filter( 'wellknown_nodeinfo_data', array( self::class, 'add_wellknown_nodeinfo_data' ) ); } /** @@ -45,8 +45,8 @@ class Nodeinfo { $nodeinfo['usage']['users'] = array( 'total' => get_total_users(), - 'activeMonth' => get_active_users( '1 month ago' ), - 'activeHalfyear' => get_active_users( '6 month ago' ), + 'activeMonth' => get_active_users(), + 'activeHalfyear' => get_active_users( 6 ), ); return $nodeinfo; @@ -64,8 +64,8 @@ class Nodeinfo { $nodeinfo['usage']['users'] = array( 'total' => get_total_users(), - 'activeMonth' => get_active_users( '1 month ago' ), - 'activeHalfyear' => get_active_users( '6 month ago' ), + 'activeMonth' => get_active_users(), + 'activeHalfyear' => get_active_users( 6 ), ); return $nodeinfo; diff --git a/wp-content/plugins/activitypub/integration/class-opengraph.php b/wp-content/plugins/activitypub/integration/class-opengraph.php index 4eb1b1e0..c22e2958 100644 --- a/wp-content/plugins/activitypub/integration/class-opengraph.php +++ b/wp-content/plugins/activitypub/integration/class-opengraph.php @@ -8,7 +8,7 @@ namespace Activitypub\Integration; use Activitypub\Model\Blog; -use Activitypub\Collection\Users; +use Activitypub\Collection\Actors; use function Activitypub\is_single_user; use function Activitypub\is_user_type_disabled; @@ -72,13 +72,13 @@ class Opengraph { $user_id = \get_post_field( 'post_author', \get_queried_object_id() ); } elseif ( ! is_user_type_disabled( 'blog' ) ) { // Use the Blog-User for any other page, if the Blog-User is not disabled. - $user_id = Users::BLOG_USER_ID; + $user_id = Actors::BLOG_USER_ID; } else { // Do not add any metadata otherwise. return $metadata; } - $user = Users::get_by_id( $user_id ); + $user = Actors::get_by_id( $user_id ); if ( ! $user || \is_wp_error( $user ) ) { return $metadata; diff --git a/wp-content/plugins/activitypub/integration/class-seriously-simple-podcasting.php b/wp-content/plugins/activitypub/integration/class-seriously-simple-podcasting.php index c678f6d3..7533db98 100644 --- a/wp-content/plugins/activitypub/integration/class-seriously-simple-podcasting.php +++ b/wp-content/plugins/activitypub/integration/class-seriously-simple-podcasting.php @@ -9,6 +9,7 @@ namespace Activitypub\Integration; use Activitypub\Transformer\Post; +use function Activitypub\object_to_uri; use function Activitypub\generate_post_summary; /** @@ -28,20 +29,23 @@ class Seriously_Simple_Podcasting extends Post { * @return array The attachments array. */ public function get_attachment() { - $post = $this->wp_object; - $attachments = parent::get_attachment(); - + $post = $this->item; $attachment = array( - 'type' => \esc_attr( \get_post_meta( $post->ID, 'episode_type', true ) ), + 'type' => \esc_attr( ucfirst( \get_post_meta( $post->ID, 'episode_type', true ) ?? 'Audio' ) ), 'url' => \esc_url( \get_post_meta( $post->ID, 'audio_file', true ) ), - 'name' => \esc_attr( \get_the_title( $post->ID ) ), - 'icon' => \esc_url( \get_post_meta( $post->ID, 'cover_image', true ) ), + 'name' => \esc_attr( \get_the_title( $post->ID ) ?? '' ), ); - $attachment = array_filter( $attachment ); - array_unshift( $attachments, $attachment ); + $icon = \get_post_meta( $post->ID, 'cover_image', true ); + if ( ! $icon ) { + $icon = $this->get_icon(); + } - return $attachments; + if ( $icon ) { + $attachment['icon'] = \esc_url( object_to_uri( $icon ) ); + } + + return array( $attachment ); } /** @@ -63,6 +67,6 @@ class Seriously_Simple_Podcasting extends Post { * @return string The content. */ public function get_content() { - return generate_post_summary( $this->wp_object ); + return generate_post_summary( $this->item ); } } diff --git a/wp-content/plugins/activitypub/integration/class-stream-connector.php b/wp-content/plugins/activitypub/integration/class-stream-connector.php index 18ffb5da..e59c6009 100644 --- a/wp-content/plugins/activitypub/integration/class-stream-connector.php +++ b/wp-content/plugins/activitypub/integration/class-stream-connector.php @@ -7,6 +7,10 @@ namespace Activitypub\Integration; +use Activitypub\Collection\Actors; +use function Activitypub\url_to_authorid; +use function Activitypub\url_to_commentid; + /** * Stream Connector for ActivityPub. * @@ -29,6 +33,9 @@ class Stream_Connector extends \WP_Stream\Connector { */ public $actions = array( 'activitypub_notification_follow', + 'activitypub_sent_to_inbox', + 'activitypub_outbox_processing_complete', + 'activitypub_outbox_processing_batch_complete', ); /** @@ -55,7 +62,49 @@ class Stream_Connector extends \WP_Stream\Connector { * @return array */ public function get_action_labels() { - return array(); + return array( + 'processed' => __( 'Processed', 'activitypub' ), + ); + } + + /** + * Add action links to Stream drop row in admin list screen + * + * @filter wp_stream_action_links_{connector} + * + * @param array $links Previous links registered. + * @param Record $record Stream record. + * + * @return array Action links + */ + public function action_links( $links, $record ) { + if ( 'processed' === $record->action ) { + $error = json_decode( $record->get_meta( 'error', true ), true ); + + if ( $error ) { + $message = sprintf( + '
%1$s
%2$s
', + __( 'Inbox Error', 'activitypub' ), + wp_json_encode( $error ) + ); + + $links[ $message ] = ''; + } + + $debug = json_decode( $record->get_meta( 'debug', true ), true ); + + if ( $debug ) { + $message = sprintf( + '
%1$s
%2$s
', + __( 'Debug', 'activitypub' ), + wp_json_encode( $debug ) + ); + + $links[ $message ] = ''; + } + } + + return $links; } /** @@ -79,4 +128,123 @@ class Stream_Connector extends \WP_Stream\Connector { $notification->target ); } + + /** + * Callback for activitypub_outbox_processing_complete. + * + * @param array $inboxes The inboxes. + * @param string $json The ActivityPub Activity JSON. + * @param int $actor_id The actor ID. + * @param int $outbox_item_id The Outbox item ID. + */ + public function callback_activitypub_outbox_processing_complete( $inboxes, $json, $actor_id, $outbox_item_id ) { + $outbox_item = \get_post( $outbox_item_id ); + $outbox_data = $this->prepare_outbox_data_for_response( $outbox_item ); + + $this->log( + sprintf( + // translators: %s is a URL. + __( 'Outbox processing complete: %s', 'activitypub' ), + $outbox_data['title'] + ), + array( + 'debug' => wp_json_encode( + array( + 'actor_id' => $actor_id, + 'outbox_item_id' => $outbox_item_id, + ) + ), + ), + $outbox_data['id'], + $outbox_data['type'], + 'processed' + ); + } + + /** + * Callback for activitypub_outbox_processing_batch_complete. + * + * @param array $inboxes The inboxes. + * @param string $json The ActivityPub Activity JSON. + * @param int $actor_id The actor ID. + * @param int $outbox_item_id The Outbox item ID. + * @param int $batch_size The batch size. + * @param int $offset The offset. + */ + public function callback_activitypub_outbox_processing_batch_complete( $inboxes, $json, $actor_id, $outbox_item_id, $batch_size, $offset ) { + $outbox_item = \get_post( $outbox_item_id ); + $outbox_data = $this->prepare_outbox_data_for_response( $outbox_item ); + + $this->log( + sprintf( + // translators: %s is a URL. + __( 'Outbox processing batch complete: %s', 'activitypub' ), + $outbox_data['title'] + ), + array( + 'debug' => wp_json_encode( + array( + 'actor_id' => $actor_id, + 'outbox_item_id' => $outbox_item_id, + 'batch_size' => $batch_size, + 'offset' => $offset, + ) + ), + ), + $outbox_data['id'], + $outbox_data['type'], + 'processed' + ); + } + + /** + * Get the title of the outbox object. + * + * @param \WP_Post $outbox_item The outbox item. + * + * @return array The title, object ID, and object type of the outbox object. + */ + protected function prepare_outbox_data_for_response( $outbox_item ) { + $object_id = $outbox_item->ID; + $object_type = $outbox_item->post_type; + $object_title = $outbox_item->post_title; + + $post_id = url_to_postid( $outbox_item->post_title ); + if ( $post_id ) { + $post = get_post( $post_id ); + + $object_id = $post_id; + $object_type = $post->post_type; + $object_title = $post->post_title; + } else { + $comment_id = url_to_commentid( $outbox_item->post_title ); + if ( $comment_id ) { + $comment = get_comment( $comment_id ); + + $object_id = $comment_id; + $object_type = 'comments'; + $object_title = $comment->comment_content; + } else { + $author_id = url_to_authorid( $outbox_item->post_title ); + if ( null !== $author_id ) { + $object_id = $author_id; + $object_type = 'profiles'; + + if ( $author_id ) { + $object_title = get_userdata( $author_id )->display_name; + } elseif ( Actors::BLOG_USER_ID === $author_id ) { + $object_title = __( 'Blog User', 'activitypub' ); + } elseif ( Actors::APPLICATION_USER_ID === $author_id ) { + $object_title = __( 'Application User', 'activitypub' ); + } + } + } + } + + return array( + 'id' => $object_id, + 'type' => $object_type, + 'title' => $object_title, + ); + } } diff --git a/wp-content/plugins/activitypub/integration/class-webfinger.php b/wp-content/plugins/activitypub/integration/class-webfinger.php index 7ae9607d..6b6ed537 100644 --- a/wp-content/plugins/activitypub/integration/class-webfinger.php +++ b/wp-content/plugins/activitypub/integration/class-webfinger.php @@ -7,7 +7,7 @@ namespace Activitypub\Integration; -use Activitypub\Collection\Users as User_Collection; +use Activitypub\Collection\Actors; use function Activitypub\get_rest_url_by_path; @@ -35,7 +35,7 @@ class Webfinger { * @return array The jrd array. */ public static function add_user_discovery( $jrd, $uri, $user ) { - $user = User_Collection::get_by_id( $user->ID ); + $user = Actors::get_by_id( $user->ID ); if ( ! $user || is_wp_error( $user ) ) { return $jrd; @@ -43,13 +43,16 @@ class Webfinger { $jrd['subject'] = sprintf( 'acct:%s', $user->get_webfinger() ); + $jrd['aliases'][] = $user->get_id(); $jrd['aliases'][] = $user->get_url(); $jrd['aliases'][] = $user->get_alternate_url(); + $jrd['aliases'] = array_unique( $jrd['aliases'] ); + $jrd['aliases'] = array_values( $jrd['aliases'] ); $jrd['links'][] = array( 'rel' => 'self', 'type' => 'application/activity+json', - 'href' => $user->get_url(), + 'href' => $user->get_id(), ); $jrd['links'][] = array( @@ -69,32 +72,34 @@ class Webfinger { * @return array|\WP_Error The jrd array or WP_Error. */ public static function add_pseudo_user_discovery( $jrd, $uri ) { - $user = User_Collection::get_by_resource( $uri ); + $user = Actors::get_by_resource( $uri ); if ( \is_wp_error( $user ) ) { return $user; } $aliases = array( + $user->get_id(), $user->get_url(), $user->get_alternate_url(), ); $aliases = array_unique( $aliases ); + $aliases = array_values( $aliases ); $profile = array( 'subject' => sprintf( 'acct:%s', $user->get_webfinger() ), - 'aliases' => array_values( array_unique( $aliases ) ), + 'aliases' => $aliases, 'links' => array( array( 'rel' => 'self', 'type' => 'application/activity+json', - 'href' => $user->get_url(), + 'href' => $user->get_id(), ), array( 'rel' => 'http://webfinger.net/rel/profile-page', 'type' => 'text/html', - 'href' => $user->get_url(), + 'href' => $user->get_id(), ), array( 'rel' => 'http://ostatus.org/schema/1.0/subscribe', diff --git a/wp-content/plugins/activitypub/integration/class-wpml.php b/wp-content/plugins/activitypub/integration/class-wpml.php new file mode 100644 index 00000000..3bef729f --- /dev/null +++ b/wp-content/plugins/activitypub/integration/class-wpml.php @@ -0,0 +1,44 @@ +ID ); + + if ( is_array( $language_details ) && isset( $language_details['language_code'] ) ) { + $lang = $language_details['language_code']; + } + + return $lang; + } +} diff --git a/wp-content/plugins/activitypub/integration/load.php b/wp-content/plugins/activitypub/integration/load.php index 860f2dc7..fcb75467 100644 --- a/wp-content/plugins/activitypub/integration/load.php +++ b/wp-content/plugins/activitypub/integration/load.php @@ -7,6 +7,8 @@ namespace Activitypub\Integration; +\Activitypub\Autoloader::register_path( __NAMESPACE__, __DIR__ ); + /** * Initialize the ActivityPub integrations. */ @@ -19,7 +21,6 @@ function plugin_init() { * * @see https://wordpress.org/plugins/webfinger/ */ - require_once __DIR__ . '/class-webfinger.php'; Webfinger::init(); /** @@ -30,7 +31,6 @@ function plugin_init() { * * @see https://wordpress.org/plugins/nodeinfo/ */ - require_once __DIR__ . '/class-nodeinfo.php'; Nodeinfo::init(); /** @@ -41,7 +41,6 @@ function plugin_init() { * @see https://wordpress.org/plugins/enable-mastodon-apps/ */ if ( \defined( 'ENABLE_MASTODON_APPS_VERSION' ) ) { - require_once __DIR__ . '/class-enable-mastodon-apps.php'; Enable_Mastodon_Apps::init(); } @@ -53,7 +52,6 @@ function plugin_init() { * @see https://wordpress.org/plugins/opengraph/ */ if ( '1' === \get_option( 'activitypub_use_opengraph', '1' ) ) { - require_once __DIR__ . '/class-opengraph.php'; Opengraph::init(); } @@ -65,10 +63,31 @@ function plugin_init() { * @see https://jetpack.com/ */ if ( \defined( 'JETPACK__VERSION' ) && ! \defined( 'IS_WPCOM' ) ) { - require_once __DIR__ . '/class-jetpack.php'; Jetpack::init(); } + /** + * Adds Akismet support. + * + * This class handles the compatibility with the Akismet plugin. + * + * @see https://wordpress.org/plugins/akismet/ + */ + if ( \defined( 'AKISMET_VERSION' ) ) { + Akismet::init(); + } + + /** + * Adds Multisite Language Switcher support. + * + * This class handles the compatibility with the Multisite Language Switcher plugin. + * + * @see https://wordpress.org/plugins/multisite-language-switcher/ + */ + if ( \defined( 'MSLS_PLUGIN_VERSION' ) ) { + Multisite_Language_Switcher::init(); + } + /** * Adds Seriously Simple Podcasting support. * @@ -84,7 +103,6 @@ function plugin_init() { 'WP_Post' === $object_class && \get_post_meta( $data->ID, 'audio_file', true ) ) { - require_once __DIR__ . '/class-seriously-simple-podcasting.php'; return new Seriously_Simple_Podcasting( $data ); } return $transformer; @@ -93,6 +111,17 @@ function plugin_init() { 3 ); } + + /** + * Adds WPML Multilingual CMS (plugin) support. + * + * This class handles the compatibility with the WPML plugin. + * + * @see https://wpml.org/ + */ + if ( \defined( 'ICL_SITEPRESS_VERSION' ) ) { + WPML::init(); + } } \add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' ); @@ -104,22 +133,9 @@ function plugin_init() { * @return array The Stream connectors with the ActivityPub connector. */ function register_stream_connector( $classes ) { - require plugin_dir_path( __FILE__ ) . '/class-stream-connector.php'; + $class = new Stream_Connector(); - $class_name = '\Activitypub\Integration\Stream_Connector'; - - if ( ! class_exists( $class_name ) ) { - return; - } - - wp_stream_get_instance(); - $class = new $class_name(); - - if ( ! method_exists( $class, 'is_dependency_satisfied' ) ) { - return; - } - - if ( $class->is_dependency_satisfied() ) { + if ( method_exists( $class, 'is_dependency_satisfied' ) && $class->is_dependency_satisfied() ) { $classes[] = $class; } @@ -145,11 +161,4 @@ add_filter( * * @see https://buddypress.org/ */ -add_action( - 'bp_include', - function () { - require_once __DIR__ . '/class-buddypress.php'; - Buddypress::init(); - }, - 0 -); +add_action( 'bp_include', array( __NAMESPACE__ . '\Buddypress', 'init' ), 0 ); diff --git a/wp-content/plugins/activitypub/readme.txt b/wp-content/plugins/activitypub/readme.txt index ebe777b0..12be8545 100644 --- a/wp-content/plugins/activitypub/readme.txt +++ b/wp-content/plugins/activitypub/readme.txt @@ -1,10 +1,10 @@ === ActivityPub === -Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nuriapena, cavalierlife +Contributors: automattic, pfefferle, mattwiebe, obenland, akirk, jeherve, mediaformat, nuriapena, cavalierlife, andremenrath Tags: OStatus, fediverse, activitypub, activitystream -Requires at least: 5.5 -Tested up to: 6.6 -Stable tag: 3.3.3 -Requires PHP: 7.0 +Requires at least: 6.4 +Tested up to: 6.8 +Stable tag: 5.8.0 +Requires PHP: 7.2 License: MIT License URI: http://opensource.org/licenses/MIT @@ -14,6 +14,8 @@ The ActivityPub protocol is a decentralized social networking protocol based upo Enter the fediverse with **ActivityPub**, broadcasting your blog to a wider audience! Attract followers, deliver updates, and receive comments from a diverse user base of **ActivityPub**\-compliant platforms. +https://www.youtube.com/watch?v=QzYozbNneVc + With the ActivityPub plugin installed, your WordPress blog itself function as a federated profile, along with profiles for each author. For instance, if your website is `example.com`, then the blog-wide profile can be found at `@example.com@example.com`, and authors like Jane and Bob would have their individual profiles at `@jane@example.com` and `@bobz@example.com`, respectively. An example: I give you my Mastodon profile name: `@pfefferle@mastodon.social`. You search, see my profile, and hit follow. Now, any post I make appears in your Home feed. Similarly, with the ActivityPub plugin, you can find and follow Jane's profile at `@jane@example.com`. @@ -31,7 +33,6 @@ The plugin works with the following tested federated platforms, but there may be * [Pixelfed](https://pixelfed.org/) * [Socialhome](https://socialhome.network/) * [Misskey](https://join.misskey.page/) -* [Firefish](https://joinfirefish.org/) (rebrand of Calckey) Some things to note: @@ -56,24 +57,6 @@ So what’s the process? This plugin connects your WordPress blog to popular social platforms like Mastodon, making your posts more accessible to a wider audience. Once installed, your blog can be followed by users on these platforms, allowing them to receive your new posts in their feeds. -= What is the status of this plugin? = - -Implemented: - -* blog profile pages (JSON representation) -* author profile pages (JSON representation) -* custom links -* functional inbox/outbox -* follow (accept follows) -* share posts -* receive comments/reactions -* signature verification -* threaded comments support - -To implement: - -* replace shortcodes with blocks for layout - = What is "ActivityPub for WordPress" = *ActivityPub for WordPress* extends WordPress with some Fediverse features, but it does not compete with platforms like Friendica or Mastodon. If you want to run a **decentralized social network**, please use [Mastodon](https://joinmastodon.org/) or [GNU social](https://gnusocial.network/). @@ -86,7 +69,7 @@ In order for webfinger to work, it must be mapped to the root directory of the U Add the following to the .htaccess file in the root directory: - RedirectMatch "^\/\.well-known/(webfinger|nodeinfo|x-nodeinfo2)(.*)$" /blog/.well-known/$1$2 + RedirectMatch "^\/\.well-known/(webfinger|nodeinfo)(.*)$" /blog/.well-known/$1$2 Where 'blog' is the path to the subdirectory at which your blog resides. @@ -101,8 +84,6 @@ Add the following to the site.conf in sites-available: Where 'blog' is the path to the subdirectory at which your blog resides. -= What if you are running your blog in a subdirectory? = - If you are running your blog in a subdirectory, but have a different [wp_siteurl](https://wordpress.org/documentation/article/giving-wordpress-its-own-directory/), you don't need the redirect, because the index.php will take care of that. = What if you are running your blog behind a reverse proxy with Apache? = @@ -123,7 +104,7 @@ The plugin uses PHP Constants to enable, disable or change its default behaviour * `ACTIVITYPUB_USERNAME_REGEXP` - Change the default regex to detect @-replies in a text. Default: `(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))`. * `ACTIVITYPUB_URL_REGEXP` - Change the default regex to detect urls in a text. Default: `(www.|http:|https:)+[^\s]+[\w\/]`. * `ACTIVITYPUB_CUSTOM_POST_CONTENT` - Change the default template for Activities. Default: `[ap_title]\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]`. -* `ACTIVITYPUB_AUTHORIZED_FETCH` - Enable AUTHORIZED_FETCH. Default: `false`. +* `ACTIVITYPUB_AUTHORIZED_FETCH` - Enable AUTHORIZED_FETCH. * `ACTIVITYPUB_DISABLE_REWRITES` - Disable auto generation of `mod_rewrite` rules. Default: `false`. * `ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS` - Block incoming replies/comments/likes. Default: `false`. * `ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS` - Disable outgoing replies/comments/likes. Default: `false`. @@ -148,91 +129,250 @@ For reasons of data protection, it is not possible to see the followers of other == Changelog == -= 3.3.3 = +### 5.8.0 - 2025-04-24 +#### Added +- An option to receive notification emails when an Actor was mentioned in the Fediverse. +- Enable direct linking to Help Tabs. +- Fallback embed support for Fediverse content that lacks native oEmbed responses. +- Support for all media types in the Mastodon Importer. -* Fixed: Sanitization callback -* Improved: A lot of PHPCS cleanups -* Improved: Prepare multi-lang support +#### Changed +- Added WordPress disallowed list filtering to block unwanted ActivityPub interactions. +- Mastodon imports now support blocks, with automatic reply embedding for conversations. +- Tested and compatible with the latest version of WordPress. +- Updated design of new follower notification email and added meta information. +- Update DM email notification to include an embed display of the DM. +- Updated notification settings to be user-specific for more personalization. -= 3.3.2 = +#### Fixed +- Add support for Multisite Language Switcher +- Better check for an empty `headers` array key in the Signature class. +- Include user context in Global-Inbox actions. +- No more PHP warning when Mastodon Apps run out of posts to process. +- Reply links and popup modals are now properly translated for logged-out visitors. -* Fixed: Keep priority of Icons -* Fixed: Fatal error if remote-object is `WP_Error` -* Improved: Adopt WordPress PHP Coding Standards +### 5.7.0 - 2025-04-11 +#### Added +- Advanced Settings tab, with special settings for advanced users. +- Check if pretty permalinks are enabled and recommend to use threaded comments. +- Reply block: show embeds where available. +- Support same-server domain migrations. +- Upgrade routine that removes any erroneously created extra field entries. -= 3.3.1 = +#### Changed +- Add option to enable/disable the "shared inbox" to the "Advanced Settings". +- Add option to enable/disable the `Vary` Header to the "Advanced Settings". +- Configure the "Follow Me" button to have a button-only mode. +- Importers are loaded on admin-specific hook. +- Improve the troubleshooting UI and show Site-Health stats in ActivityPub settings. +- Increased compatibility with Mobilizon and other platforms by improving signature verification for different key formats. -* Fixed: PHP Warnings -* Fixed: PHPCS issues +#### Fixed +- Ensure that an `Activity` has an `Actor` before adding it to the Outbox. +- Fixed some bugs and added additional information on the Debug tab of the Site-Health page. +- Follow-up to the reply block changes that makes sure Mastodon embeds are displayed in the editor. +- Outbox endpoint bug where non-numeric usernames caused errors when querying Outbox data. +- Show Site Health error if site uses old "Almost Pretty Permalinks" structure. +- Sites with comments from the Fediverse no longer create uncached extra fields posts that flood the Outbox. +- Transformers allow settings values to false again, a regression from 5.5.0. -= 3.3.0 = +### 5.6.1 - 2025-04-02 +#### Fixed +- "Post Interactions" settings will now be saved to the options table. +- So not show `movedTo` attribute instead of setting it to `false` if empty. +- Use specified date format for `updated` field in Outbox-Activites. -* Added: Content warning support -* Added: Replies collection -* Added: Enable Mastodon Apps: support profile editing, blog user -* Added: Follow Me/Followers: add inherit mode for dynamic templating -* Fixed: Cropping Header Images for users without the 'customize' capability -* Improved: OpenSSL handling -* Improved: Added missing @ in Follow-Me block +### 5.6.0 - 2025-04-01 +#### Added +- Added a Mastodon importer to move your Mastodon posts to your WordPress site. +- A default Extra-Field to do a little advertising for WordPress. +- Move: Differentiate between `internal` and 'external' Move. +- Redirect user to the welcome page after ActivityPub plugin is activated. +- The option to show/hide the "Welcome Page". +- User setting to enable/disable Likes and Reblogs -= 3.2.5 = +#### Changed +- Logged-out remote reply button markup to look closer to logged-in version. +- No longer federates `Delete` activities for posts that were not federated. +- OrderedCollection and OrderedCollectionPage behave closer to spec now. +- Outbox items now contain the full activity, not just activity objects. +- Standardized mentions to use usernames only in comments and posts. -* Fixed: Enable Mastodon Apps check -* Fixed: Fediverse replies were not threaded properly +#### Fixed +- Changelog entries: allow automating changelog entry generation from forks as well. +- Comments from Fediverse actors will now be purged as expected. +- Importing attachments no longer creates Outbox items for them. +- Improved readability in Mastodon Apps plugin string. +- No more PHP warnings when previewing posts without attachments. +- Outbox batch processing adheres to passed batch size. +- Permanently delete reactions that were `Undo` instead of trashing them. +- PHP warnings when scheduling post activities for an invalid post. +- PHP Warning when there's no actor information in comment activities. +- Prevent self-replies on local comments. +- Properly set `to` audience of `Activity` instead of changing the `Follow` Object. +- Run all Site-Health checks with the required headers and a valid signature. +- Set `updated` field for profile updates, otherwise the `Update`-`Activity` wouldn't be handled by Mastodon. +- Support multiple layers of nested Outbox activities when searching for the Object ID. +- The Custom-Avatar getter on WP.com. +- Use the $from account for the object in Move activity for external Moves +- Use the `$from` account for the object in Move activity for internal Moves +- Use `add_to_outbox` instead of the changed scheduler hooks. +- Use `JSON_UNESCAPED_SLASHES` because Mastodon seems to have problems with encoded URLs. +- `Scheduler::schedule_announce_activity` to handle Activities instead of Activity-Objects. -= 3.2.4 = +### 5.5.0 - 2025-03-19 +#### Added +- Added "Enable Mastodon Apps" and "Event Bridge for ActivityPub" to the recommended plugins section. +- Added Constants to the Site-Health debug informations. +- Development environment: add Changelogger tool to environment dependencies. +- Development environment: allow contributors to specify a changelog entry directly from their Pull Request description. +- Documentation for migrating from a Mastodon instance to WordPress. +- Support for sending Activities to ActivityPub Relays, to improve discoverability of public content. -* Improved: Inbox validation +#### Changed +- Documentation: expand Pull Request process docs, and mention the new changelog process as well as the updated release process. +- Don't redirect @-name URLs to trailing slashed versions +- Improved and simplified Query code. +- Improved readability for actor mode setting. +- Improved title case for NodeInfo settings. +- Introduced utility function to determine actor type based on user ID. +- Outbox items only get sent to followers when there are any. +- Restricted modifications to settings if they are predefined as constants. +- The Welcome page now uses WordPress's Settings API and the classic design of the WP Admin. +- Uses two-digit version numbers in Outbox and NodeInfo responses. -= 3.2.3 = +#### Removed +- Our version of `sanitize_url()` was unused—use Core's `sanitize_url()` instead. -* Fixed: NodeInfo endpoint -* Fixed: (Temporarily) Remove HTML from `summary`, because it seems that Mastodon has issues with it -* Improved: Accessibility for Reply-Context -* Improved: Use `Article` Object-Type as default +#### Fixed +- Ensured that Query::get_object_id() returns an ID instead of an Object. +- Fix a fatal error in the Preview when a post contains no (hash)tags. +- Fixed an issue with the Content Carousel and Blog Posts block: https://github.com/Automattic/wp-calypso/issues/101220 +- Fixed default value for `activitypub_authorized_fetch` option. +- Follow-Me blocks now show the correct avatar on attachment pages. +- Images with the correct aspect ratio no longer get sent through the crop step again. +- No more PHP warnings when a header image gets cropped. +- PHP warnings when trying to process empty tags or image blocks without ID attributes. +- Properly re-added support for `Update` and `Delete` `Announce`ments. +- Updates to certain user meta fields did not trigger an Update activity. +- When viewing Reply Contexts, we'll now attribute the post to the blog user when the post author is disabled. -= 3.2.2 = +### 5.4.1 - 2025-03-04 +#### Fixed +- Fixed transition handling of posts to ensure that `Create` and `Update` activities are properly processed. +- Show "full content" preview even if post is in still in draft mode. -* Fixed: Extra-Fields check +### 5.4.0 - 2025-03-03 +#### Added +- Upgrade script to fix Follower json representations with unescaped backslashes. +- Centralized place for sanitization functions. -= 3.2.1 = +#### Changed +- Bumped minimum required WordPress version to 6.4. +- Use a later hook for Posts to get published to the Outbox, to get sure all `post_meta`s and `taxonomy`s are set stored properly. +- Use webfinger as author email for comments from the Fediverse. +- Remove the special handling of comments from Enable Mastodon Apps. -* Fixed: Use `Excerpt` for Podcast Episodes +#### Fixed +- Do not redirect `/@username` URLs to the API any more, to improve `AUTHORIZED_FETCH` handling. -= 3.2.0 = +### 5.3.2 - 2025-02-27 +#### Fixed +- Remove `activitypub_reply_block` filter after Activity-JSON is rendered, to not affect the HTML representation. +- Remove `render_block_core/embed` filter after Activity-JSON is rendered, to not affect the HTML representation. -* Added: Support for Seriously Simple Podcasting -* Added: Blog extra fields -* Added: Support "read more" for Activity-Summary -* Added: `Like` and `Announce` (Boost) handler -* Added: Simple Remote-Reply endpoint -* Added: "Stream" Plugin support -* Added: New Fediverse symbol -* Improved: Replace hashtags, urls and mentions in summary with links -* Improved: Hide Bookmarklet if site does not support Blocks -* Fixed: Link detection for extra fields when spaces after the link and fix when two links in the content -* Fixed: `Undo` for `Likes` and `Announces` -* Fixed: Show Avatars on `Likes` and `Announces` -* Fixed: Remove proprietary WebFinger resource -* Fixed: Wrong followers URL in "to" attribute of posts +### 5.3.1 - 2025-02-26 +#### Fixed +- Blog profile settings can be saved again without errors. +- Followers with backslashes in their descriptions no longer break their actor representation. -= 3.1.0 = +### 5.3.0 - 2025-02-25 +#### Added +- A fallback `Note` for `Article` objects to improve previews on services that don't support Articles yet. +- A reply `context` for Posts and Comments to allow relying parties to discover the whole conversation of a thread. +- Setting to adjust the number of days Outbox items are kept before being purged. +- Failed Follower notifications for Outbox items now get retried for two more times. +- Undo API for Outbox items. +- Metadata to New Follower E-Mail. +- Allow Activities on URLs instead of requiring Activity-Objects. This is useful especially for sending Announces and Likes. +- Outbox Activity IDs can now be resolved when the ActivityPub `Accept header is used. +- Support for incoming `Move` activities and ensure that followed persons are updated accordingly. +- Labels to add context to visibility settings in the block editor. +- WP CLI command to reschedule Outbox-Activities. -* Added: `menu_order` to `ap_extrafield` so that user can decide in with order they will be displayed -* Added: Line brakes to user biography -* Added: Blueprint -* Fixed: Changed missing `activitypub_user_description` to `activitypub_description` -* Fixed: Undefined `get_sample_permalink` -* Fixed: Only send Update for previously-published posts -* Improved: Simplified WebFinger code +#### Changed +- Outbox now precesses the first batch of followers right away to avoid delays in processing new Activities. +- Post bulk edits no longer create Outbox items, unless author or post status change. +- Properly process `Update` activities on profiles and ensure all properties of a followed person are updated accordingly. +- Outbox processing accounts for shared inboxes again. +- Improved check for `?activitypub` query-var. +- Rewrite rules: be more specific in author rewrite rules to avoid conflicts on sites that use the "@author" pattern in their permalinks. +- Deprecate the `activitypub_post_locale` filter in favor of the `activitypub_locale` filter. + +#### Fixed +- The Outbox purging routine no longer is limited to deleting 5 items at a time. +- Ellipses now display correctly in notification emails for Likes and Reposts. +- Send Update-Activity when "Actor-Mode" is changed. +- Added delay to `Announce` Activity from the Blog-Actor, to not have race conditions. +- `Actor` validation in several REST API endpoints. +- Bring back the `activitypub_post_locale` filter to allow overriding the post's locale. + +### 5.2.0 - 2025-02-13 +#### Added +- Batch Outbox-Processing. +- Outbox processed events get logged in Stream and show any errors returned from inboxes. +- Outbox items older than 6 months will be purged to avoid performance issues. +- REST API endpoints for likes and shares. + +#### Changed +- Increased probability of Outbox items being processed with the correct author. +- Enabled querying of Outbox posts through the REST API to improve troubleshooting and debugging. +- Updated terminology to be client-neutral in the Federated Reply block. + +#### Fixed +- Fixed an issue where the outbox could not send object types other than `Base_Object` (introduced in 5.0.0). +- Enforce 200 status header for valid ActivityPub requests. +- `object_id_to_comment` returns a commment now, even if there are more than one matching comment in the DB. +- Integration of content-visibility setup in the block editor. +- Update CLI commands to the new scheduler refactorings. +- Do not add an audience to the Actor-Profiles. +- `Activity::set_object` falsely overwrites the Activity-ID with a default. + +### 5.1.0 - 2025-02-06 +#### Added +- Cleanup of option values when the plugin is uninstalled. +- Third-party plugins can filter settings tabs to add their own settings pages for ActivityPub. +- Show ActivityPub preview in row actions when Block Editor is enabled but not used for the post type. + +#### Changed +- Manually granting `activitypub` cap no longer requires the receiving user to have `publish_post`. +- Allow omitting replies in ActivityPub representations instead of setting them as empty. +- Allow Base Transformer to handle WP_Term objects for transformation. +- Improved Query extensibility for third party plugins. + +#### Fixed +- Negotiation of ActivityPub requests for custom post types when queried by the ActivityPub ID. +- Avoid PHP warnings when using Debug mode and when the `actor` is not set. +- No longer creates Outbox items when importing content/users. +- Fix NodeInfo 2.0 URL to be HTTP instead of HTTPS. + +### 5.0.0 - 2025-02-03 +#### Changed +- Improved content negotiation and AUTHORIZED_FETCH support for third-party plugins. +- Moved password check to `is_post_disabled` function. + +#### Fixed +- Handle deletes from remote servers that leave behind an accessible Tombstone object. +- No longer parses tags for post types that don't support Activitypub. +- rel attribute will now contain no more than one "me" value. See full Changelog on [GitHub](https://github.com/Automattic/wordpress-activitypub/blob/trunk/CHANGELOG.md). == Upgrade Notice == -= 1.0.0 = += 5.4.0 = -For version 1.0.0 we have completely rebuilt the followers lists. There is a migration from the old format to the new, but it may take some time until the migration is complete. No data will be lost in the process, please give the migration some time. +Note: This update requires WordPress 6.4+. Please ensure your site meets this requirement before upgrading. == Installation == diff --git a/wp-content/plugins/activitypub/templates/activitypub-json.php b/wp-content/plugins/activitypub/templates/activitypub-json.php new file mode 100644 index 00000000..036a99b8 --- /dev/null +++ b/wp-content/plugins/activitypub/templates/activitypub-json.php @@ -0,0 +1,25 @@ +get_activitypub_object(); + +/** + * Fires before an ActivityPub object is generated and sent to the client. + * + * @param object $object The ActivityPub object. + */ +\do_action( 'activitypub_json_pre', $object ); + +\header( 'Content-Type: application/activity+json' ); +echo $object->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + +/** + * Fires after an ActivityPub object is generated and sent to the client. + * + * @param object $object The ActivityPub object. + */ +\do_action( 'activitypub_json_post', $object ); diff --git a/wp-content/plugins/activitypub/templates/admin-header.php b/wp-content/plugins/activitypub/templates/admin-header.php index c453328d..0c655dca 100644 --- a/wp-content/plugins/activitypub/templates/admin-header.php +++ b/wp-content/plugins/activitypub/templates/admin-header.php @@ -6,15 +6,7 @@ */ /* @var array $args Template arguments. */ -$args = wp_parse_args( - $args, - array( - 'welcome' => '', - 'settings' => '', - 'blog-profile' => '', - 'followers' => '', - ) -); +$args = wp_parse_args( $args ?? array() ); ?>
@@ -22,25 +14,21 @@ $args = wp_parse_args(

diff --git a/wp-content/plugins/activitypub/templates/advanced-settings.php b/wp-content/plugins/activitypub/templates/advanced-settings.php new file mode 100644 index 00000000..47c1e84c --- /dev/null +++ b/wp-content/plugins/activitypub/templates/advanced-settings.php @@ -0,0 +1,16 @@ + + +
+
+ + + +
+
diff --git a/wp-content/plugins/activitypub/templates/blog-followers-list.php b/wp-content/plugins/activitypub/templates/blog-followers-list.php index b7d20063..74273630 100644 --- a/wp-content/plugins/activitypub/templates/blog-followers-list.php +++ b/wp-content/plugins/activitypub/templates/blog-followers-list.php @@ -5,14 +5,6 @@ * @package Activitypub */ -\load_template( - __DIR__ . '/admin-header.php', - true, - array( - 'followers' => 'active', - ) -); - $table = new \Activitypub\Table\Followers(); $follower_count = $table->get_user_count(); // translators: The follower count. diff --git a/wp-content/plugins/activitypub/templates/blog-json.php b/wp-content/plugins/activitypub/templates/blog-json.php deleted file mode 100644 index f2d1e002..00000000 --- a/wp-content/plugins/activitypub/templates/blog-json.php +++ /dev/null @@ -1,21 +0,0 @@ -get__id() ); - -\header( 'Content-Type: application/activity+json' ); -echo $user->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - -/** - * Action triggered after the ActivityPub profile has been created and sent to the client - */ -\do_action( 'activitypub_json_author_post', $user->get__id() ); diff --git a/wp-content/plugins/activitypub/templates/blog-settings.php b/wp-content/plugins/activitypub/templates/blog-settings.php index b090d23d..078c7d0c 100644 --- a/wp-content/plugins/activitypub/templates/blog-settings.php +++ b/wp-content/plugins/activitypub/templates/blog-settings.php @@ -5,186 +5,12 @@ * @package Activitypub */ -\load_template( - __DIR__ . '/admin-header.php', - true, - array( - 'blog-profile' => 'active', - ) -); ?>
- -
-

- - - - - - - - - - - - - - - - - - - - - - - -
- - - -

- -

- General Settings" of WordPress.', 'activitypub' ), - \esc_url( \admin_url( 'options-general.php' ) ) - ), - 'default' - ); - ?> -

-
- - - -
- ' style="max-width: 100%;" /> -
- - - '> -
- - - -

- -

-

- - - -

-
- - - -

- -

-
- - -

- -

- - - - - - - - - - - - - - - -

- - - - - - -

-
-
- - - +
diff --git a/wp-content/plugins/activitypub/templates/comment-json.php b/wp-content/plugins/activitypub/templates/comment-json.php deleted file mode 100644 index 49c3d33c..00000000 --- a/wp-content/plugins/activitypub/templates/comment-json.php +++ /dev/null @@ -1,29 +0,0 @@ -get_error_message() ), - 404 - ); -} - -/** - * Action triggered prior to the ActivityPub profile being created and sent to the client - */ -\do_action( 'activitypub_json_comment_pre' ); - -\header( 'Content-Type: application/activity+json' ); -echo $transformer->to_object()->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - -/** - * Action triggered after the ActivityPub profile has been created and sent to the client - */ -\do_action( 'activitypub_json_comment_post' ); diff --git a/wp-content/plugins/activitypub/templates/emails/new-dm.php b/wp-content/plugins/activitypub/templates/emails/new-dm.php new file mode 100644 index 00000000..a0dd32d8 --- /dev/null +++ b/wp-content/plugins/activitypub/templates/emails/new-dm.php @@ -0,0 +1,49 @@ + + +

+

+ ' . esc_html( $args['actor']['webfinger'] ) . '' ); + ?> +

+ +
+ +
+ + + +

+ +

+ +

+ ' . esc_html( $args['webfinger'] ) . '' ); + ?> +

+ +
+ +
+ + +
+ + <?php echo esc_attr( $args['name'] ); ?> + +
+

+ + +

+ + + +
+ +
+ ' . esc_html( number_format_i18n( $args['stats']['outbox'] ) ) . '' + ); + ?> +
+ + +
+ ' . esc_html( number_format_i18n( $args['stats']['followers'] ) ) . '' + ); + ?> +
+ + +
+ ' . esc_html( number_format_i18n( $args['stats']['following'] ) ) . '' + ); + ?> +
+ +
+ +
+
+
+ +

+ + + +

+ +

+ followers list to see all followers.', 'activitypub' ), array( 'a' => array( 'href' => array() ) ) ), + esc_url( admin_url( $args['admin_url'] ) ) + ); + ?> +

+ + + +

+ +

+ +

+ ' . esc_html( $args['actor']['webfinger'] ) . '' ); + ?> +

+ +
+ +
+ +

+ + + +

+ + + + diff --git a/wp-content/plugins/activitypub/templates/emails/parts/header.php b/wp-content/plugins/activitypub/templates/emails/parts/header.php new file mode 100644 index 00000000..6ea7e471 --- /dev/null +++ b/wp-content/plugins/activitypub/templates/emails/parts/header.php @@ -0,0 +1,60 @@ + + + +
diff --git a/wp-content/plugins/activitypub/templates/post-json.php b/wp-content/plugins/activitypub/templates/post-json.php deleted file mode 100644 index 745629f5..00000000 --- a/wp-content/plugins/activitypub/templates/post-json.php +++ /dev/null @@ -1,30 +0,0 @@ -get_error_message() ), - 404 - ); -} - - -/** - * Action triggered prior to the ActivityPub profile being created and sent to the client - */ -\do_action( 'activitypub_json_post_pre' ); - -\header( 'Content-Type: application/activity+json' ); -echo $transformer->to_object()->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - -/** - * Action triggered after the ActivityPub profile has been created and sent to the client - */ -\do_action( 'activitypub_json_post_post' ); diff --git a/wp-content/plugins/activitypub/templates/post-preview.php b/wp-content/plugins/activitypub/templates/post-preview.php new file mode 100644 index 00000000..f88e82db --- /dev/null +++ b/wp-content/plugins/activitypub/templates/post-preview.php @@ -0,0 +1,265 @@ +get_error_message() ), + 404 + ); +} + +$object = $transformer->to_object(); +$user = $transformer->get_actor_object(); + +?> + + + + + <?php echo esc_html( $object->get_name() ); ?> + + + +
+ +
+

+ Home +

+
+
+ <?php echo esc_attr( $user->get_name() ); ?> +
+
+ get_name() ); ?> +
+
+ get_webfinger() ); ?> +
+
+
+
+ get_type() && $object->get_name() ) : ?> +

get_name() ); ?>

+ + get_type() ? $object->get_summary() : $object->get_content(), ACTIVITYPUB_MASTODON_HTML_SANITIZER ); ?> +
+ get_attachment() ) : ?> +
+ get_attachment() as $attachment ) : ?> + + <?php echo esc_attr( $attachment['name'] ?? '' ); ?> + + +
+ + get_tag() ) : ?> +
+ get_tag() as $hashtag ) : ?> + + + + +
+ +
+
+ +
+ + diff --git a/wp-content/plugins/activitypub/templates/reply-embed.php b/wp-content/plugins/activitypub/templates/reply-embed.php new file mode 100644 index 00000000..a948195b --- /dev/null +++ b/wp-content/plugins/activitypub/templates/reply-embed.php @@ -0,0 +1,81 @@ + '', + 'author_name' => '', + 'author_url' => '', + 'title' => '', + 'content' => '', + 'image' => '', + 'published' => '', + 'url' => '', + 'boosts' => null, + 'favorites' => null, + 'webfinger' => '', + ) +); + +\wp_enqueue_style( 'activitypub-embed', ACTIVITYPUB_PLUGIN_URL . 'assets/css/activitypub-embed.css', array(), ACTIVITYPUB_PLUGIN_VERSION ); +?> + +
+
+ + + +
+

+ + + +
+
+ +
+ +

+ + + +
+ + + +
+ +
+ +
+ +
+ + + + + + + ' . \esc_html( \number_format_i18n( $args['boosts'] ) ) . '' ); + ?> + + + + + + ' . \esc_html( \number_format_i18n( $args['favorites'] ) ) . '' ); + ?> + + +
+
diff --git a/wp-content/plugins/activitypub/templates/settings.php b/wp-content/plugins/activitypub/templates/settings.php index f73b785c..e4a61c96 100644 --- a/wp-content/plugins/activitypub/templates/settings.php +++ b/wp-content/plugins/activitypub/templates/settings.php @@ -5,262 +5,12 @@ * @package Activitypub */ -\load_template( - __DIR__ . '/admin-header.php', - true, - array( - 'settings' => 'active', - ) -); ?>
- -
-

- - - - - - - -
- - -

- -

-

- activitypub capability) gets their own ActivityPub profile.', 'activitypub' ), array( 'code' => array() ) ); ?> - - user settings.', 'activitypub' ), admin_url( '/users.php' ) ), array( 'a' => array( 'href' => array() ) ) ); ?> - array() ) ); ?> -

-

- -

-

- -

-
- - -
- -
-

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

- -

-

- -

- -
- - -

-

- -

-

- -

-

- -

-

- -

-

- -

- -
-
    -
  • [ap_title] -
  • -
  • [ap_content] -
  • -
  • [ap_excerpt] -
  • -
  • [ap_permalink] -
  • -
  • [ap_shortlink] - Hum.', 'activitypub' ), 'default' ); ?>
  • -
  • [ap_hashtags] -
  • -
-

-
-
-

-
- - - -

- %s', 'activitypub' ), - \esc_html( ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) - ), - 'default' - ); - ?> -

-

- - - -

-
-
- - -
    - true ), 'objects' ); - $supported_post_types = (array) \get_option( 'activitypub_support_post_types', array( 'post' ) ); - - foreach ( $post_types as $_post_type ) : - ?> -
  • - name, $supported_post_types, true ) ); ?> /> - - - - -
  • - -
-
-
- - -

- -

-
- - -
- -
-

- - - - - - - - - - - -
- - -

- -

-
- - -

- Disallowed Comment Keys" list.', 'activitypub' ), - \esc_url( \admin_url( 'options-discussion.php#disallowed_keys' ) ) - ), - 'default' - ); - ?> -

-
- - -
- - +
diff --git a/wp-content/plugins/activitypub/templates/toolbox.php b/wp-content/plugins/activitypub/templates/toolbox.php index adc0b80b..19fa797d 100644 --- a/wp-content/plugins/activitypub/templates/toolbox.php +++ b/wp-content/plugins/activitypub/templates/toolbox.php @@ -8,7 +8,7 @@ ?>
-

+

@@ -16,7 +16,7 @@

- + @@ -26,7 +26,7 @@

- +

@@ -68,6 +68,10 @@ in_reply_to + + post_type + +

diff --git a/wp-content/plugins/activitypub/templates/user-json.php b/wp-content/plugins/activitypub/templates/user-json.php deleted file mode 100644 index a149ed1d..00000000 --- a/wp-content/plugins/activitypub/templates/user-json.php +++ /dev/null @@ -1,21 +0,0 @@ -get__id() ); - -\header( 'Content-Type: application/activity+json' ); -echo $user->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - -/** - * Action triggered after the ActivityPub profile has been created and sent to the client - */ -\do_action( 'activitypub_json_author_post', $user->get__id() ); diff --git a/wp-content/plugins/activitypub/templates/user-settings.php b/wp-content/plugins/activitypub/templates/user-settings.php deleted file mode 100644 index fb88fbcf..00000000 --- a/wp-content/plugins/activitypub/templates/user-settings.php +++ /dev/null @@ -1,140 +0,0 @@ - '' ) ); - -$user = \Activitypub\Collection\Users::get_by_id( \get_current_user_id() ); ?> -

- -

- -

- - - - - - - - - - - - - - - - - - - - -
- - -

- get_webfinger() ); ?> or - get_url() ); ?> -

- -

get_webfinger() ) ); ?>

-
- - - -

-
- - - -
- -
- - - -
- - -

- -

- - - - - - - - - - - -

- - - - - - -

-
- - diff --git a/wp-content/plugins/activitypub/templates/welcome.php b/wp-content/plugins/activitypub/templates/welcome.php index 7a27f141..35782a90 100644 --- a/wp-content/plugins/activitypub/templates/welcome.php +++ b/wp-content/plugins/activitypub/templates/welcome.php @@ -5,191 +5,8 @@ * @package Activitypub */ -\load_template( - __DIR__ . '/admin-header.php', - true, - array( - 'welcome' => 'active', - ) -); ?>
-
-

- -

ActivityPub, broadcasting your blog to a wider audience. Attract followers, deliver updates, and receive comments from a diverse user base on Mastodon, Friendica, Pleroma, Pixelfed, and all ActivityPub-compliant platforms.', 'activitypub' ), array( 'strong' => array() ) ); ?>

-
- - -
-

- -

- %s', - esc_url( $bookmarklet_url ), // Need to escape quotes for the bookmarklet. - sprintf( $reply_from_template, \wp_parse_url( \home_url(), PHP_URL_HOST ) ) - ); - /* translators: %s is where the button HTML will be rendered. */ - $button_and_explanation_template = \__( - '%s Save this bookmarklet to reply to posts on other sites from your own blog! When visiting a post on another site, click the bookmarklet to start a reply.', - 'activitypub' - ); - - printf( $button_and_explanation_template, $button ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - - printf( ' %s', esc_url( \admin_url( 'tools.php#activitypub' ) ), esc_html__( 'For additional information, please visit the Tools page.', 'activitypub' ) ); - ?> -

-
- -
-

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-

- - - -

-
- - - ID ); - ?> -
-

-

- -

-

- -

-

- -

-

- -

-

- -

-

- -

-

- - - -

-
- - -
-

-

- Site Health page to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).', - 'activitypub' - ), - \esc_url( admin_url( 'site-health.php' ) ) - ), - 'default' - ); - ?> -

-
- - -
-

- -

-
-
- -

- -

-
-

-

-
- - -

- -

- - - -

- -

- - - -

- -

- - -
- +