updated plugin ActivityPub version 3.3.3

This commit is contained in:
KawaiiPunk 2024-10-09 12:44:17 +00:00 committed by Gitium
parent fb4b27bbc6
commit c54fa007bd
106 changed files with 7070 additions and 2918 deletions

View File

@ -3,7 +3,7 @@
* Plugin Name: ActivityPub
* Plugin URI: https://github.com/pfefferle/wordpress-activitypub/
* Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.
* Version: 2.6.1
* Version: 3.3.3
* Author: Matthias Pfefferle & Automattic
* Author URI: https://automattic.com/
* License: MIT
@ -11,17 +11,18 @@
* Requires PHP: 7.0
* Text Domain: activitypub
* Domain Path: /languages
*
* @package Activitypub
*/
namespace Activitypub;
use function Activitypub\is_blog_public;
use function Activitypub\site_supports_blocks;
use WP_CLI;
require_once __DIR__ . '/includes/compat.php';
require_once __DIR__ . '/includes/functions.php';
\define( 'ACTIVITYPUB_PLUGIN_VERSION', '2.6.1' );
\define( 'ACTIVITYPUB_PLUGIN_VERSION', '3.3.3' );
/**
* Initialize the plugin constants.
@ -32,10 +33,13 @@ require_once __DIR__ . '/includes/functions.php';
\defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || \define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 3 );
\defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=<p>)|(?<=<br>)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' );
\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' );
\defined( 'ACTIVITYPUB_URL_REGEXP' ) || \define( 'ACTIVITYPUB_URL_REGEXP', '(https?:|www\.)\S+[\w\/]' );
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "<h2>[ap_title]</h2>\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" );
\defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false );
\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false );
\defined( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS', false );
// Disable reactions like `Like` and `Announce` by default.
\defined( 'ACTIVITYPUB_DISABLE_REACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_REACTIONS', true );
\defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false );
\defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) || \define( 'ACTIVITYPUB_SHARED_INBOX_FEATURE', false );
\defined( 'ACTIVITYPUB_SEND_VARY_HEADER' ) || \define( 'ACTIVITYPUB_SEND_VARY_HEADER', false );
@ -59,8 +63,9 @@ function rest_init() {
Rest\Comment::init();
Rest\Server::init();
Rest\Collection::init();
Rest\Interaction::init();
// load NodeInfo endpoints only if blog is public
// Load NodeInfo endpoints only if blog is public.
if ( is_blog_public() ) {
Rest\NodeInfo::init();
}
@ -81,6 +86,7 @@ function plugin_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__ . '\Link', 'init' ) );
if ( site_supports_blocks() ) {
\add_action( 'init', array( __NAMESPACE__ . '\Blocks', 'init' ) );
@ -91,29 +97,12 @@ function plugin_init() {
require_once $debug_file;
Debug::init();
}
require_once __DIR__ . '/integration/class-webfinger.php';
Integration\Webfinger::init();
require_once __DIR__ . '/integration/class-nodeinfo.php';
Integration\Nodeinfo::init();
require_once __DIR__ . '/integration/class-enable-mastodon-apps.php';
Integration\Enable_Mastodon_Apps::init();
require_once __DIR__ . '/integration/class-opengraph.php';
Integration\Opengraph::init();
if ( \defined( 'JETPACK__VERSION' ) && ! \defined( 'IS_WPCOM' ) ) {
require_once __DIR__ . '/integration/class-jetpack.php';
Integration\Jetpack::init();
}
}
\add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' );
/**
* Class Autoloader
* Class Autoloader.
*/
\spl_autoload_register(
function ( $full_class ) {
@ -141,15 +130,19 @@ function plugin_init() {
if ( file_exists( $file ) && is_readable( $file ) ) {
require_once $file;
} else {
// translators: %s is the class name
\wp_die( sprintf( esc_html__( 'Required class not found or not readable: %s', 'activitypub' ), esc_html( $full_class ) ) );
// 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
}
}
}
);
/**
* Add plugin settings link
* Add plugin settings link.
*
* @param array $actions The current actions.
*/
function plugin_settings_link( $actions ) {
$settings_link = array();
@ -187,22 +180,14 @@ function plugin_settings_link( $actions ) {
)
);
/**
* Only load code that needs BuddyPress to run once BP is loaded and initialized.
*/
add_action(
'bp_include',
function () {
require_once __DIR__ . '/integration/class-buddypress.php';
Integration\Buddypress::init();
},
0
);
// Load integrations.
require_once __DIR__ . '/integration/load.php';
/**
* `get_plugin_data` wrapper
* `get_plugin_data` wrapper.
*
* @return array The plugin metadata array
* @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() ) {
if ( ! $default_headers ) {
@ -237,3 +222,14 @@ function get_plugin_version() {
return $meta['Version'];
}
// Check for CLI env, to add the CLI commands.
if ( defined( 'WP_CLI' ) && WP_CLI ) {
WP_CLI::add_command(
'activitypub',
'\Activitypub\Cli',
array(
'shortdesc' => __( 'ActivityPub related commands: Meta-Infos, Delete and soon Self-Destruct.', 'activitypub' ),
)
);
}

View File

@ -34,10 +34,10 @@
.activitypub-settings-tabs-wrapper {
display: -ms-inline-grid;
-ms-grid-columns: auto auto auto;
-ms-grid-columns: auto auto auto auto;
vertical-align: top;
display: inline-grid;
grid-template-columns: auto auto auto;
grid-template-columns: auto auto auto auto;
}
.activitypub-settings-tab.active {
@ -202,3 +202,13 @@ input.blog-user-identifier {
content: "\f307";
font-family: dashicons;
}
.repost .dashboard-comment-wrap,
.like .dashboard-comment-wrap {
padding-inline-start: 63px;
}
.repost .dashboard-comment-wrap .comment-author,
.like .dashboard-comment-wrap .comment-author {
margin-block: 0;
}

View File

@ -0,0 +1,261 @@
/**
* Handle the header image setting in
*
* This is based on site-icon.js
*
* @see wp-admin/js/site-icon.js
*/
/* global jQuery, wp */
( function ( $ ) {
var $chooseButton = $( '#activitypub-choose-from-library-button' ),
$headerImagePreviewWrapper = $( '#activitypub-header-image-preview-wrapper' ),
$headerImagePreview = $( '#activitypub-header-image-preview' ),
$hiddenDataField = $( '#activitypub_header_image' ),
$removeButton = $( '#activitypub-remove-header-image' ),
frame,
ImageCropperNoCustomizer;
/**
* We register our own handler because the Core one invokes the Customizer, which fails the request unnecessarily
* for users who don't have the 'customize' capability.
* See https://github.com/Automattic/wordpress-activitypub/issues/846
*/
ImageCropperNoCustomizer = wp.media.controller.CustomizeImageCropper.extend( {
doCrop: function( attachment ) {
var cropDetails = attachment.get( 'cropDetails' ),
control = this.get( 'control' ),
ratio = cropDetails.width / cropDetails.height;
// Use crop measurements when flexible in both directions.
if ( control.params.flex_width && control.params.flex_height ) {
cropDetails.dst_width = cropDetails.width;
cropDetails.dst_height = cropDetails.height;
// Constrain flexible side based on image ratio and size of the fixed side.
} else {
cropDetails.dst_width = control.params.flex_width ? control.params.height * ratio : control.params.width;
cropDetails.dst_height = control.params.flex_height ? control.params.width / ratio : control.params.height;
}
return wp.ajax.post( 'crop-image', {
// where wp_customize: 'on' would be in Core, for no good reason I understand.
nonce: attachment.get( 'nonces' ).edit,
id: attachment.get( 'id' ),
context: control.id,
cropDetails: cropDetails
} );
}
} );
/**
* Calculate image selection options based on the attachment dimensions.
*
* @since 6.5.0
*
* @param {Object} attachment The attachment object representing the image.
* @return {Object} The image selection options.
*/
function calculateImageSelectOptions( attachment ) {
var realWidth = attachment.get( 'width' ),
realHeight = attachment.get( 'height' ),
xInit = 1500,
yInit = 500,
ratio = xInit / yInit,
xImg = xInit,
yImg = yInit,
x1,
y1,
imgSelectOptions;
if ( realWidth / realHeight > ratio ) {
yInit = realHeight;
xInit = yInit * ratio;
} else {
xInit = realWidth;
yInit = xInit / ratio;
}
x1 = ( realWidth - xInit ) / 2;
y1 = ( realHeight - yInit ) / 2;
imgSelectOptions = {
aspectRatio: xInit + ':' + yInit,
handles: true,
keys: true,
instance: true,
persistent: true,
imageWidth: realWidth,
imageHeight: realHeight,
minWidth: xImg > xInit ? xInit : xImg,
minHeight: yImg > yInit ? yInit : yImg,
x1: x1,
y1: y1,
x2: xInit + x1,
y2: yInit + y1,
};
return imgSelectOptions;
}
/**
* Initializes the media frame for selecting or cropping an image.
*
* @since 6.5.0
*/
$chooseButton.on( 'click', function () {
var $el = $( this );
var userId = $el.data( 'userId' );
var mediaQuery = { type: 'image' };
if ( userId ) {
mediaQuery.author = userId;
}
// Create the media frame.
frame = wp.media( {
button: {
// Set the text of the button.
text: $el.data( 'update' ),
// Don't close, we might need to crop.
close: false,
},
states: [
new wp.media.controller.Library( {
title: $el.data( 'choose-text' ),
library: wp.media.query( mediaQuery ),
date: false,
suggestedWidth: $el.data( 'size' ),
suggestedHeight: $el.data( 'size' ),
} ),
new ImageCropperNoCustomizer( {
control: {
params: {
width: $el.data( 'size' ),
height: $el.data( 'size' ),
},
},
imgSelectOptions: calculateImageSelectOptions,
} ),
],
} );
frame.on( 'cropped', function ( attachment ) {
$hiddenDataField.val( attachment.id );
switchToUpdate( attachment );
frame.close();
// Start over with a frame that is so fresh and so clean clean.
frame = null;
} );
// When an image is selected, run a callback.
frame.on( 'select', function () {
// Grab the selected attachment.
var attachment = frame.state().get( 'selection' ).first();
if (
attachment.attributes.height === $el.data( 'size' ) &&
$el.data( 'size' ) === attachment.attributes.width
) {
switchToUpdate( attachment.attributes );
frame.close();
// Set the value of the hidden input to the attachment id.
$hiddenDataField.val( attachment.id );
} else {
frame.setState( 'cropper' );
}
} );
frame.open();
} );
/**
* Update the UI when a header is selected.
*
* @since 6.5.0
*
* @param {array} attributes The attributes for the attachment.
*/
function switchToUpdate( attributes ) {
var i18nAppAlternativeString, i18nBrowserAlternativeString;
if ( attributes.alt ) {
i18nBrowserAlternativeString = wp.i18n.sprintf(
/* translators: %s: The selected image alt text. */
wp.i18n.__( 'Header Image preview: Current image: %s' ),
attributes.alt
);
} else {
i18nAppAlternativeString = wp.i18n.sprintf(
/* translators: %s: The selected image filename. */
wp.i18n.__(
'Header Image preview: The current image has no alternative text. The file name is: %s'
),
attributes.filename
);
i18nBrowserAlternativeString = wp.i18n.sprintf(
/* translators: %s: The selected image filename. */
wp.i18n.__(
'Header Image preview: The current image has no alternative text. The file name is: %s'
),
attributes.filename
);
}
// Set activitypub-header-image-preview src.
$headerImagePreview.attr( {
src: attributes.url,
alt: i18nAppAlternativeString,
} );
// Remove hidden class from header image preview div and remove button.
$headerImagePreviewWrapper.removeClass( 'hidden' );
$removeButton.removeClass( 'hidden' );
// If the choose button is not in the update state, swap the classes.
if ( $chooseButton.attr( 'data-state' ) !== '1' ) {
$chooseButton.attr( {
class: $chooseButton.attr( 'data-alt-classes' ),
'data-alt-classes': $chooseButton.attr( 'class' ),
'data-state': '1',
} );
}
// Swap the text of the choose button.
$chooseButton.text( $chooseButton.attr( 'data-update-text' ) );
}
/**
* Handles the click event of the remove button.
*
* @since 6.5.0
*/
$removeButton.on( 'click', function () {
$hiddenDataField.val( 'false' );
$( this ).toggleClass( 'hidden' );
$headerImagePreviewWrapper.toggleClass( 'hidden' );
$headerImagePreview.attr( {
src: '',
alt: '',
} );
/**
* Resets state to the button, for correct visual style and state.
* Updates the text of the button.
* Sets focus state to the button.
*/
$chooseButton
.attr( {
class: $chooseButton.attr( 'data-alt-classes' ),
'data-alt-classes': $chooseButton.attr( 'class' ),
'data-state': '',
} )
.text( $chooseButton.attr( 'data-choose-text' ) )
.trigger( 'focus' );
} );
} )( jQuery );

View File

@ -0,0 +1,8 @@
{
"name": "editor-plugin",
"title": "Editor Plugin: not a block, but block.json is very useful.",
"category": "widgets",
"icon": "admin-comments",
"keywords": [],
"editorScript": "file:./plugin.js"
}

View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'wp-components', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-i18n', 'wp-plugins'), 'version' => '88603987940fec29730d');

View File

@ -0,0 +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")}))}})})();

View File

@ -38,6 +38,10 @@
"default": "site"
}
},
"usesContext": [
"postType",
"postId"
],
"editorScript": "file:./index.js",
"viewScript": "file:./view.js",
"style": [

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '7be9f9b97d08a20bde26');
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '1ec66c1edf3d9b0b6678');

File diff suppressed because one or more lines are too long

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'ab8c0dad126bb0a61ed6');
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'bc272e3d4aaa7992f4c7');

File diff suppressed because one or more lines are too long

View File

@ -33,6 +33,10 @@
]
}
},
"usesContext": [
"postType",
"postId"
],
"styles": [
{
"name": "default",

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '3d39b46b3415c2d57654');
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '28a5bef9295566598f5c');

View File

@ -1,3 +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,c={},s=null,p=null;for(r in void 0!==a&&(s=""+a),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(p=t.ref),t)l.call(t,r)&&!i.hasOwnProperty(r)&&(c[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===c[r]&&(c[r]=t[r]);return{$$typeof:n,type:e,key:s,ref:p,props:c,_owner:o.current}}},848:(e,t,a)=>{"use strict";e.exports=a(20)},609:e=>{"use strict";e.exports=window.React},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t<arguments.length;t++){var a=arguments[t];a&&(e=o(e,l(a)))}return e}function l(e){if("string"==typeof e||"number"==typeof e)return e;if("object"!=typeof e)return"";if(Array.isArray(e))return n.apply(null,e);if(e.toString!==Object.prototype.toString&&!e.toString.toString().includes("[native code]"))return e.toString();var t="";for(var a in e)r.call(e,a)&&e[a]&&(t=o(t,a));return t}function o(e,t){return t?e?e+" "+t:e+t:e}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},t={};function a(r){var n=t[r];if(void 0!==n)return n.exports;var l=t[r]={exports:{}};return e[r](l,l.exports,a),l.exports}a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.primitives;var r=a(848);const n=(0,r.jsx)(t.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,r.jsx)(t.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})});var l=a(609);const o=window.wp.components,i=window.wp.element,c=window.wp.blockEditor,s=window.wp.i18n,p=window.wp.apiFetch;var u=a.n(p);const v=window.wp.url;var m=a(942),w=a.n(m);function d({active:e,children:t,page:a,pageClick:r,className:n}){const o=w()("wp-block activitypub-pager",n,{current:e});return(0,l.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&r(a)}},t)}const f={outlined:"outlined",minimal:"minimal"};function b({compact:e,nextLabel:t,page:a,pageClick:r,perPage:n,prevLabel:o,total:i,variant:c=f.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=w()("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)(d,{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)(d,{key:e,page:e,pageClick:r,active:e===a,className:"page-numbers"},e)))),t&&(0,l.createElement)(d,{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:y}=window._activityPubOptions;function g({selectedUser:e,per_page:t,order:a,title:r,page:n,setPage:o,className:c="",followLinks:p=!0,followerData:m=!1}){const w="site"===e?0:e,[d,f]=(0,l.useState)([]),[g,k]=(0,l.useState)(0),[h,E]=(0,l.useState)(0),[x,N]=function(){const[e,t]=(0,l.useState)(1);return[e,t]}(),S=n||x,C=o||N,O=(0,i.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */
(0,s.__)("<span>←</span> Less","activitypub"),{span:(0,l.createElement)("span",{className:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),P=(0,i.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */
(0,s.__)("More <span>→</span>","activitypub"),{span:(0,l.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),L=(e,a)=>{f(e),E(a),k(Math.ceil(a/t))};return(0,l.useEffect)((()=>{if(m&&1===S)return L(m.followers,m.total);const e=function(e,t,a,r){const n=`/${y}/actors/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,v.addQueryArgs)(n,l)}(w,t,a,S);u()({path:e}).then((e=>L(e.orderedItems,e.totalItems))).catch((()=>{}))}),[w,t,a,S,m]),(0,l.createElement)("div",{className:"activitypub-follower-block "+c},(0,l.createElement)("h3",null,r),(0,l.createElement)("ul",null,d&&d.map((e=>(0,l.createElement)("li",{key:e.url},(0,l.createElement)(_,{...e,followLinks:p}))))),g>1&&(0,l.createElement)(b,{page:S,perPage:t,total:h,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)))}const k=window.wp.data,h=window._activityPubOptions?.enabled;(0,e.registerBlockType)("activitypub/followers",{edit:function({attributes:e,setAttributes:t}){const{order:a,per_page:r,selectedUser:n,title:p}=e,u=(0,c.useBlockProps)(),[v,m]=(0,i.useState)(1),w=[{label:(0,s.__)("New to old","activitypub"),value:"desc"},{label:(0,s.__)("Old to new","activitypub"),value:"asc"}],d=function(){const e=h?.users?(0,k.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,i.useMemo)((()=>{if(!e)return[];const t=h?.site?[{label:(0,s.__)("Whole Site","activitypub"),value:"site"}]:[];return e.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),t)}),[e])}(),f=e=>a=>{m(1),t({[e]:a})};return(0,i.useEffect)((()=>{d.length&&(d.find((({value:e})=>e===n))||t({selectedUser:d[0].value}))}),[n,d]),(0,l.createElement)("div",{...u},(0,l.createElement)(c.InspectorControls,{key:"setting"},(0,l.createElement)(o.PanelBody,{title:(0,s.__)("Followers Options","activitypub")},(0,l.createElement)(o.TextControl,{label:(0,s.__)("Title","activitypub"),help:(0,s.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:p,onChange:e=>t({title:e})}),d.length>1&&(0,l.createElement)(o.SelectControl,{label:(0,s.__)("Select User","activitypub"),value:n,options:d,onChange:f("selectedUser")}),(0,l.createElement)(o.SelectControl,{label:(0,s.__)("Sort","activitypub"),value:a,options:w,onChange:f("order")}),(0,l.createElement)(o.RangeControl,{label:(0,s.__)("Number of Followers","activitypub"),value:r,onChange:f("per_page"),min:1,max:10}))),(0,l.createElement)(g,{...e,page:v,setPage:m,followLinks:!1}))},save:()=>null,icon:n})})()})();
(()=>{var e={20:(e,t,a)=>{"use strict";var r=a(609),n=Symbol.for("react.element"),l=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),o=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,a){var r,s={},c=null,p=null;for(r in void 0!==a&&(c=""+a),void 0!==t.key&&(c=""+t.key),void 0!==t.ref&&(p=t.ref),t)l.call(t,r)&&!i.hasOwnProperty(r)&&(s[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===s[r]&&(s[r]=t[r]);return{$$typeof:n,type:e,key:c,ref:p,props:s,_owner:o.current}}},848:(e,t,a)=>{"use strict";e.exports=a(20)},609:e=>{"use strict";e.exports=window.React},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t<arguments.length;t++){var a=arguments[t];a&&(e=o(e,l(a)))}return e}function l(e){if("string"==typeof e||"number"==typeof e)return e;if("object"!=typeof e)return"";if(Array.isArray(e))return n.apply(null,e);if(e.toString!==Object.prototype.toString&&!e.toString.toString().includes("[native code]"))return e.toString();var t="";for(var a in e)r.call(e,a)&&e[a]&&(t=o(t,a));return t}function o(e,t){return t?e?e+" "+t:e+t:e}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},t={};function a(r){var n=t[r];if(void 0!==n)return n.exports;var l=t[r]={exports:{}};return e[r](l,l.exports,a),l.exports}a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.primitives;var r=a(848);const n=(0,r.jsx)(t.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,r.jsx)(t.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})});var l=a(609);const o=window.wp.components,i=window.wp.element,s=window.wp.blockEditor,c=window.wp.data,p=window.wp.coreData,u=window.wp.i18n,m=window.wp.apiFetch;var v=a.n(m);const d=window.wp.url;var w=a(942),f=a.n(w);function b({active:e,children:t,page:a,pageClick:r,className:n}){const o=f()("wp-block activitypub-pager",n,{current:e});return(0,l.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&r(a)}},t)}const y={outlined:"outlined",minimal:"minimal"};function g({compact:e,nextLabel:t,page:a,pageClick:r,perPage:n,prevLabel:o,total:i,variant:s=y.outlined}){const c=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(i/n)),p=f()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":e});return(0,l.createElement)("nav",{className:p},o&&(0,l.createElement)(b,{key:"prev",page:a-1,pageClick:r,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,l.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},c.map((e=>(0,l.createElement)(b,{key:e,page:e,pageClick:r,active:e===a,className:"page-numbers"},e)))),t&&(0,l.createElement)(b,{key:"next",page:a+1,pageClick:r,active:a===Math.ceil(i/n),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}const{namespace:h}=window._activityPubOptions;function _({selectedUser:e,per_page:t,order:a,title:r,page:n,setPage:o,className:s="",followLinks:c=!0,followerData:p=!1}){const m="site"===e?0:e,[w,f]=(0,l.useState)([]),[b,y]=(0,l.useState)(0),[_,E]=(0,l.useState)(0),[x,S]=function(){const[e,t]=(0,l.useState)(1);return[e,t]}(),N=n||x,C=o||S,O=(0,i.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */
(0,u.__)("<span>←</span> Less","activitypub"),{span:(0,l.createElement)("span",{className:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),P=(0,i.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */
(0,u.__)("More <span>→</span>","activitypub"),{span:(0,l.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),I=(e,a)=>{f(e),E(a),y(Math.ceil(a/t))};return(0,l.useEffect)((()=>{if(p&&1===N)return I(p.followers,p.total);const e=function(e,t,a,r){const n=`/${h}/actors/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,d.addQueryArgs)(n,l)}(m,t,a,N);v()({path:e}).then((e=>I(e.orderedItems,e.totalItems))).catch((()=>{}))}),[m,t,a,N,p]),(0,l.createElement)("div",{className:"activitypub-follower-block "+s},(0,l.createElement)("h3",null,r),(0,l.createElement)("ul",null,w&&w.map((e=>(0,l.createElement)("li",{key:e.url},(0,l.createElement)(k,{...e,followLinks:c}))))),b>1&&(0,l.createElement)(g,{page:N,perPage:t,total:_,pageClick:C,nextLabel:P,prevLabel:O,compact:"is-style-compact"===s}))}function k({name:e,icon:t,url:a,preferredUsername:r,followLinks:n=!0}){const i=`@${r}`,s={};return n||(s.onClick=e=>e.preventDefault()),(0,l.createElement)(o.ExternalLink,{className:"activitypub-link",href:a,title:i,...s},(0,l.createElement)("img",{width:"40",height:"40",src:t.url,className:"avatar activitypub-avatar",alt:e}),(0,l.createElement)("span",{className:"activitypub-actor"},(0,l.createElement)("strong",{className:"activitypub-name"},e),(0,l.createElement)("span",{className:"sep"},"/"),(0,l.createElement)("span",{className:"activitypub-handle"},i)))}const E=window._activityPubOptions?.enabled;function x({name:e}){const t=(0,u.sprintf)(/* translators: %s: block name */
"This <strong>%s</strong> block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. It will be <strong>empty</strong> in other non-author contexts.",e);return(0,l.createElement)(o.Card,null,(0,l.createElement)(o.CardBody,null,(0,i.createInterpolateElement)(t,{strong:(0,l.createElement)("strong",null)})))}(0,e.registerBlockType)("activitypub/followers",{edit:function({attributes:e,setAttributes:t,context:{postType:a,postId:r}}){const{order:n,per_page:m,selectedUser:v,title:d}=e,w=(0,s.useBlockProps)(),[f,b]=(0,i.useState)(1),y=[{label:(0,u.__)("New to old","activitypub"),value:"desc"},{label:(0,u.__)("Old to new","activitypub"),value:"asc"}],g=function({withInherit:e=!1}){const t=E?.users?(0,c.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,i.useMemo)((()=>{if(!t)return[];const a=[];return E?.site&&a.push({label:(0,u.__)("Site","activitypub"),value:"site"}),e&&E?.users&&a.push({label:(0,u.__)("Dynamic User","activitypub"),value:"inherit"}),t.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),a)}),[t])}({withInherit:!0}),h=e=>a=>{b(1),t({[e]:a})},k=(0,c.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),n=t("postType",a,r)?.author;return null!=n?n:null}),[a,r]);return(0,i.useEffect)((()=>{g.length&&(g.find((({value:e})=>e===v))||t({selectedUser:g[0].value}))}),[v,g]),(0,l.createElement)("div",{...w},(0,l.createElement)(s.InspectorControls,{key:"setting"},(0,l.createElement)(o.PanelBody,{title:(0,u.__)("Followers Options","activitypub")},(0,l.createElement)(o.TextControl,{label:(0,u.__)("Title","activitypub"),help:(0,u.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:d,onChange:e=>t({title:e})}),g.length>1&&(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:v,options:g,onChange:h("selectedUser")}),(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Sort","activitypub"),value:n,options:y,onChange:h("order")}),(0,l.createElement)(o.RangeControl,{label:(0,u.__)("Number of Followers","activitypub"),value:m,onChange:h("per_page"),min:1,max:10}))),"inherit"===v?k?(0,l.createElement)(_,{...e,page:f,setPage:b,followLinks:!1,selectedUser:k}):(0,l.createElement)(x,{name:(0,u.__)("Followers","activitypub")}):(0,l.createElement)(_,{...e,page:f,setPage:b,followLinks:!1}))},save:()=>null,icon:n})})()})();

View File

@ -1 +1 @@
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'ab787305c7ed07812b96');
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '45f08e094782c24c4c34');

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,12 @@
{
"name": "reply-handler",
"title": "Reply Handler: not a block, but block.json is very useful.",
"category": "widgets",
"icon": "admin-comments",
"keywords": [
"reply",
"handler",
"comments"
],
"editorScript": "file:./plugin.js"
}

View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('wp-block-editor', 'wp-blocks', 'wp-data', 'wp-element', 'wp-plugins'), 'version' => '488f0199fb69ddcf6c53');

View File

@ -0,0 +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}})})();

View File

@ -0,0 +1,21 @@
{
"$schema": "https://schemas.wp.org/trunk/block.json",
"apiVersion": 3,
"name": "activitypub/reply",
"version": "0.1.0",
"title": "Federated Reply",
"category": "widgets",
"icon": "commentReplyLink",
"description": "Respond to posts, notes, videos, and other content on the fediverse. Ensure the URL originates from a federated social network like Mastodon, as other URLs might not function as expected.",
"supports": {
"html": false
},
"textdomain": "activitypub",
"editorScript": "file:./index.js",
"editorStyle": "file:./edit.css",
"attributes": {
"url": {
"type": "string"
}
}
}

View File

@ -0,0 +1 @@
<?php return array('dependencies' => array('react', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '808c98599517db815fc5');

View File

@ -0,0 +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})})();

View File

@ -3,11 +3,13 @@
* Inspired by the PHP ActivityPub Library by @Landrok
*
* @link https://github.com/landrok/activitypub
*
* @package Activitypub
*/
namespace Activitypub\Activity;
use Activitypub\Activity\Base_Object;
use Activitypub\Link;
/**
* \Activitypub\Activity\Activity implements the common
@ -22,6 +24,8 @@ class Activity extends Base_Object {
);
/**
* The type of the object.
*
* @var string
*/
protected $type = 'Activity';
@ -90,6 +94,21 @@ class Activity extends Base_Object {
*/
protected $result;
/**
* Identifies a Collection containing objects considered to be responses
* to this object.
* WordPress has a strong core system of approving replies. We only include
* approved replies here.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies
*
* @var array
* | ObjectType
* | Link
* | null
*/
protected $replies;
/**
* An indirect object of the activity from which the
* activity is directed.
@ -128,45 +147,49 @@ class Activity extends Base_Object {
*
* @see https://www.w3.org/TR/activitypub/#object-without-create
*
* @param string|Base_Objectr|Link|null $object
* @param array|string|Base_Object|Link|null $data Activity object.
*
* @return void
*/
public function set_object( $object ) {
// convert array to object
if ( is_array( $object ) ) {
$object = self::init_from_array( $object );
public function set_object( $data ) {
// Convert array to object.
if ( is_array( $data ) ) {
$data = self::init_from_array( $data );
}
// set object
$this->set( 'object', $object );
// Set object.
$this->set( 'object', $data );
if ( ! is_object( $object ) ) {
if ( ! is_object( $data ) ) {
return;
}
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
$this->set( $i, $object->get( $i ) );
$this->set( $i, $data->get( $i ) );
}
if ( $object->get_published() && ! $this->get_published() ) {
$this->set( 'published', $object->get_published() );
if ( $data->get_published() && ! $this->get_published() ) {
$this->set( 'published', $data->get_published() );
}
if ( $object->get_updated() && ! $this->get_updated() ) {
$this->set( 'updated', $object->get_updated() );
if ( $data->get_updated() && ! $this->get_updated() ) {
$this->set( 'updated', $data->get_updated() );
}
if ( $object->get_attributed_to() && ! $this->get_actor() ) {
$this->set( 'actor', $object->get_attributed_to() );
if ( $data->get_attributed_to() && ! $this->get_actor() ) {
$this->set( 'actor', $data->get_attributed_to() );
}
if ( $object->get_id() && ! $this->get_id() ) {
$id = strtok( $object->get_id(), '#' );
if ( $object->get_updated() ) {
$updated = $object->get_updated();
if ( $data->get_in_reply_to() ) {
$this->set( 'in_reply_to', $data->get_in_reply_to() );
}
if ( $data->get_id() && ! $this->get_id() ) {
$id = strtok( $data->get_id(), '#' );
if ( $data->get_updated() ) {
$updated = $data->get_updated();
} else {
$updated = $object->get_published();
$updated = $data->get_published();
}
$this->set( 'id', $id . '#activity-' . strtolower( $this->get_type() ) . '-' . $updated );
}
@ -181,7 +204,7 @@ class Activity extends Base_Object {
if ( $this->object instanceof Base_Object ) {
$class = get_class( $this->object );
if ( $class && $class::JSON_LD_CONTEXT ) {
// Without php 5.6 support this could be just: 'return $this->object::JSON_LD_CONTEXT;'
// Without php 5.6 support this could be just: 'return $this->object::JSON_LD_CONTEXT;'.
return $class::JSON_LD_CONTEXT;
}
}

View File

@ -3,6 +3,8 @@
* Inspired by the PHP ActivityPub Library by @Landrok
*
* @link https://github.com/landrok/activitypub
*
* @package Activitypub
*/
namespace Activitypub\Activity;
@ -24,7 +26,6 @@ class Actor extends Base_Object {
array(
'schema' => 'http://schema.org#',
'toot' => 'http://joinmastodon.org/ns#',
'webfinger' => 'https://webfinger.net/#',
'lemmy' => 'https://join-lemmy.org/ns#',
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
'PropertyValue' => 'schema:PropertyValue',
@ -45,11 +46,12 @@ class Actor extends Base_Object {
'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods',
'discoverable' => 'toot:discoverable',
'indexable' => 'toot:indexable',
'resource' => 'webfinger:resource',
),
);
/**
* The type of the object.
*
* @var string
*/
protected $type;
@ -171,4 +173,15 @@ class Actor extends Base_Object {
* @var boolean
*/
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.
*
* @see https://docs.joinmastodon.org/spec/activitypub/#sensitive
*
* @var boolean
*/
protected $sensitive = null;
}

View File

@ -3,6 +3,8 @@
* Inspired by the PHP ActivityPub Library by @Landrok
*
* @link https://github.com/landrok/activitypub
*
* @package Activitypub
*/
namespace Activitypub\Activity;
@ -31,6 +33,7 @@ class Base_Object {
'https://www.w3.org/ns/activitystreams',
array(
'Hashtag' => 'as:Hashtag',
'sensitive' => 'as:sensitive',
),
);
@ -44,6 +47,8 @@ class Base_Object {
protected $id;
/**
* The type of the object.
*
* @var string
*/
protected $type = 'Object';
@ -428,7 +433,7 @@ class Base_Object {
*
* @see https://www.w3.org/TR/activitypub/#source-property
*
* @var ObjectType
* @var array
*/
protected $source;
@ -446,12 +451,21 @@ class Base_Object {
protected $replies;
/**
* Magic function to implement getter and setter
* Used to mark an object as containing sensitive content.
* Mastodon displays a content warning, requiring users to click
* through to view the content.
*
* @see https://docs.joinmastodon.org/spec/activitypub/#sensitive
*
* @var boolean
*/
protected $sensitive = false;
/**
* Magic function to implement getter and setter.
*
* @param string $method The method name.
* @param string $params The method params.
*
* @return void
*/
public function __call( $method, $params ) {
$var = \strtolower( \substr( $method, 4 ) );
@ -563,9 +577,9 @@ class Base_Object {
/**
* Convert JSON input to an array.
*
* @return string The JSON string.
* @param string $json The JSON string.
*
* @return \Activitypub\Activity\Base_Object An Object built from 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 );
@ -578,20 +592,20 @@ class Base_Object {
}
/**
* Convert JSON input to an array.
* Convert input array to a Base_Object.
*
* @return string The object array.
* @param array $data The object array.
*
* @return \Activitypub\Activity\Base_Object An Object built from the JSON string.
* @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( $array ) {
if ( ! is_array( $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 ( $array as $key => $value ) {
foreach ( $data as $key => $value ) {
$key = camel_to_snake_case( $key );
call_user_func( array( $object, 'set_' . $key ), $value );
}
@ -613,10 +627,10 @@ class Base_Object {
/**
* Convert JSON input to an array and pre-fill the object.
*
* @param array $array The array.
* @param array $data The array.
*/
public function from_array( $array ) {
foreach ( $array as $key => $value ) {
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 );
@ -639,12 +653,12 @@ class Base_Object {
$vars = get_object_vars( $this );
foreach ( $vars as $key => $value ) {
// ignotre all _prefixed keys.
// Ignore all _prefixed keys.
if ( '_' === substr( $key, 0, 1 ) ) {
continue;
}
// if value is empty, try to get it from a getter.
// If value is empty, try to get it from a getter.
if ( ! $value ) {
$value = call_user_func( array( $this, 'get_' . $key ) );
}
@ -653,7 +667,7 @@ class Base_Object {
$value = $value->to_array( false );
}
// if value is still empty, ignore it for the array and continue.
// If value is still empty, ignore it for the array and continue.
if ( isset( $value ) ) {
$array[ snake_to_camel_case( $key ) ] = $value;
}
@ -667,10 +681,28 @@ class Base_Object {
$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 );
$array = \apply_filters( "activitypub_activity_{$class}_object_array", $array, $this->id, $this );
return $array;
/**
* 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 );
}
/**
@ -684,10 +716,10 @@ class Base_Object {
$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
* @param int $options The current options flags.
*/
$options = \apply_filters( 'activitypub_json_encode_options', $options );

View File

@ -19,8 +19,8 @@ use Activitypub\Activity\Base_Object;
class Event extends Base_Object {
// Human friendly minimal context for full Mobilizon compatible ActivityPub events.
const JSON_LD_CONTEXT = array(
'https://schema.org/', // The base context is schema.org, cause it is used a lot.
'https://www.w3.org/ns/activitystreams', // The ActivityStreams context overrides everyting also defined in schema.org.
'https://schema.org/', // The base context is schema.org, because it is used a lot.
'https://www.w3.org/ns/activitystreams', // The ActivityStreams context overrides everything also defined in schema.org.
array( // The keys here override/extend the context even more.
'pt' => 'https://joinpeertube.org/ns#',
'mz' => 'https://joinmobilizon.org/ns#',
@ -51,6 +51,7 @@ class Event extends Base_Object {
/**
* Mobilizon compatible values for repliesModertaionOption.
*
* @var array
*/
const REPLIES_MODERATION_OPTION_TYPES = array( 'allow_all', 'closed' );
@ -58,10 +59,11 @@ class Event extends Base_Object {
/**
* Mobilizon compatible values for joinModeTypes.
*/
const JOIN_MODE_TYPES = array( 'free', 'restricted', 'external' ); // and 'invite', but not used by mobilizon atm
const JOIN_MODE_TYPES = array( 'free', 'restricted', 'external' ); // and 'invite', but not used by mobilizon atm.
/**
* Allowed values for ical VEVENT STATUS.
*
* @var array
*/
const ICAL_EVENT_STATUS_TYPES = array( 'TENTATIVE', 'CONFIRMED', 'CANCELLED' );
@ -70,6 +72,7 @@ class Event extends Base_Object {
* Default event categories.
*
* These values currently reflect the default set as proposed by Mobilizon to maximize interoperability.
*
* @var array
*/
const DEFAULT_EVENT_CATEGORIES = array(
@ -106,8 +109,7 @@ class Event extends Base_Object {
);
/**
* Event is an implementation of one of the
* Activity Streams
* Event is an implementation of one of the Activity Streams.
*
* @var string
*/
@ -115,11 +117,13 @@ class Event extends Base_Object {
/**
* The Title of the event.
*
* @var string
*/
protected $name;
/**
* The events contacts
* The events contacts.
*
* @context {
* '@id' => 'mz:contacts',
@ -142,12 +146,16 @@ class Event extends Base_Object {
protected $comments_enabled;
/**
* Timezone of the event.
*
* @context https://joinmobilizon.org/ns#timezone
* @var string
*/
protected $timezone;
/**
* Moderation option for replies.
*
* @context https://joinmobilizon.org/ns#repliesModerationOption
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#repliesmoderation
* @var string
@ -155,6 +163,8 @@ class Event extends Base_Object {
protected $replies_moderation_option;
/**
* Whether anonymous participation is enabled.
*
* @context https://joinmobilizon.org/ns#anonymousParticipationEnabled
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#anonymousparticipationenabled
* @var bool
@ -162,26 +172,34 @@ class Event extends Base_Object {
protected $anonymous_participation_enabled;
/**
* The event's category.
*
* @context https://schema.org/category
* @var enum
* @var string
*/
protected $category;
/**
* Language of the event.
*
* @context https://schema.org/inLanguage
* @var
* @var string
*/
protected $in_language;
/**
* Whether the event is online.
*
* @context https://joinmobilizon.org/ns#isOnline
* @var bool
*/
protected $is_online;
/**
* The event's status.
*
* @context https://www.w3.org/2002/12/cal/ical#status
* @var enum
* @var string
*/
protected $status;
@ -196,25 +214,33 @@ class Event extends Base_Object {
protected $actor;
/**
* The external participation URL.
*
* @context https://joinmobilizon.org/ns#externalParticipationUrl
* @var string
*/
protected $external_participation_url;
/**
* Indicator of how new members may be able to join.
*
* @context https://joinmobilizon.org/ns#joinMode
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#joinmode
* @var
* @var string
*/
protected $join_mode;
/**
* The participant count of the event.
*
* @context https://joinmobilizon.org/ns#participantCount
* @var int
*/
protected $participant_count;
/**
* How many places there can be for an event.
*
* @context https://schema.org/maximumAttendeeCapacity
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#maximumattendeecapacity
* @var int
@ -222,6 +248,8 @@ class Event extends Base_Object {
protected $maximum_attendee_capacity;
/**
* The number of attendee places for an event that remain unallocated.
*
* @context https://schema.org/remainingAttendeeCapacity
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#remainignattendeecapacity
* @var int
@ -234,6 +262,7 @@ class Event extends Base_Object {
* The passed timezone is only set when it is a valid one, otherwise the site's timezone is used.
*
* @param string $timezone The timezone string to be set, e.g. 'Europe/Berlin'.
* @return Event
*/
public function set_timezone( $timezone ) {
if ( in_array( $timezone, timezone_identifiers_list(), true ) ) {
@ -246,9 +275,11 @@ class Event extends Base_Object {
}
/**
* Custom setter for repliesModerationOption which also directy sets commentsEnabled accordingly.
* Custom setter for repliesModerationOption which also directly sets commentsEnabled accordingly.
*
* @param string $type
* @param string $type The type of the replies moderation option.
*
* @return Event
*/
public function set_replies_moderation_option( $type ) {
if ( in_array( $type, self::REPLIES_MODERATION_OPTION_TYPES, true ) ) {
@ -268,7 +299,9 @@ class Event extends Base_Object {
/**
* Custom setter for commentsEnabled which also directly sets repliesModerationOption accordingly.
*
* @param bool $comments_enabled
* @param bool $comments_enabled Whether comments are enabled.
*
* @return Event
*/
public function set_comments_enabled( $comments_enabled ) {
if ( is_bool( $comments_enabled ) ) {
@ -288,7 +321,9 @@ class Event extends Base_Object {
/**
* Custom setter for the ical status that checks whether the status is an ical event status.
*
* @param string $status
* @param string $status The status of the event.
*
* @return Event
*/
public function set_status( $status ) {
if ( in_array( $status, self::ICAL_EVENT_STATUS_TYPES, true ) ) {
@ -309,8 +344,10 @@ class Event extends Base_Object {
*
* Falls back to Mobilizons default category.
*
* @param string $category
* @param bool $mobilizon_compatibilty Whether the category must be compatibly with Mobilizon.
* @param string $category The category of the event.
* @param bool $mobilizon_compatibilty Optional. Whether the category must be compatibly with Mobilizon. Default true.
*
* @return Event
*/
public function set_category( $category, $mobilizon_compatibilty = true ) {
if ( $mobilizon_compatibilty ) {
@ -327,7 +364,9 @@ class Event extends Base_Object {
*
* Automatically sets the joinMode to true if called.
*
* @param string $url
* @param string $url The URL for external participation.
*
* @return Event
*/
public function set_external_participation_url( $url ) {
if ( preg_match( '/^https?:\/\/.*/i', $url ) ) {

View File

@ -63,22 +63,34 @@ class Place extends Base_Object {
protected $longitude;
/**
* The radius from the given latitude and longitude for a Place.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-radius
* @var float
*/
protected $radius;
/**
* Specifies the measurement units for the `radius` and `altitude` properties.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-units
* @var string
*/
protected $units;
/**
* @var Postal_Address|string
* The address of the place.
*
* @see https://schema.org/PostalAddress
* @var array|string
*/
protected $address;
/**
* Set the address of the place.
*
* @param array|string $address The address of the place.
*/
public function set_address( $address ) {
if ( is_string( $address ) || is_array( $address ) ) {
$this->address = $address;

View File

@ -1,22 +1,21 @@
<?php
/**
* ActivityPub Activity_Dispatcher Class.
*
* @package Activitypub
*/
namespace Activitypub;
use WP_Post;
use WP_Comment;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Users;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Followers;
use Activitypub\Transformer\Factory;
use Activitypub\Transformer\Post;
use Activitypub\Transformer\Comment;
use function Activitypub\is_single_user;
use function Activitypub\is_user_disabled;
use function Activitypub\safe_remote_post;
use function Activitypub\set_wp_object_state;
/**
* ActivityPub Activity_Dispatcher Class
* ActivityPub Activity_Dispatcher Class.
*
* @author Matthias Pfefferle
*
@ -32,7 +31,12 @@ class Activity_Dispatcher {
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 );
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 );
\add_action( 'activitypub_send_update_profile_activity', array( self::class, 'send_profile_update' ), 10, 1 );
\add_action( 'activitypub_send_update_profile_activity', array( self::class, 'send_profile_update' ) );
// Default filters to add Inboxes to sent to.
\add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_follower' ), 10, 2 );
\add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 );
\add_filter( 'activitypub_send_to_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 );
}
/**
@ -40,8 +44,6 @@ class Activity_Dispatcher {
*
* @param mixed $wp_object The ActivityPub Post.
* @param string $type The Activity-Type.
*
* @return void
*/
public static function send_activity_or_announce( $wp_object, $type ) {
if ( is_user_type_disabled( 'blog' ) ) {
@ -60,8 +62,7 @@ class Activity_Dispatcher {
*
* @param mixed $wp_object The ActivityPub Post.
* @param string $type The Activity-Type.
*
* @return void
* @param int $user_id Optional. The WordPress User-ID.
*/
public static function send_activity( $wp_object, $type, $user_id = null ) {
$transformer = Factory::get_transformer( $wp_object ); // Could potentially return a `\WP_Error` instance.
@ -90,8 +91,6 @@ class Activity_Dispatcher {
*
* @param mixed $wp_object The ActivityPub Post.
* @param string $type The Activity-Type.
*
* @return void
*/
public static function send_announce( $wp_object, $type ) {
if ( ! in_array( $type, array( 'Create', 'Update', 'Delete' ), true ) ) {
@ -124,26 +123,23 @@ class Activity_Dispatcher {
* Send a "Update" Activity when a user updates their profile.
*
* @param int $user_id The user ID to send an update for.
*
* @return void
*/
public static function send_profile_update( $user_id ) {
$user = Users::get_by_various( $user_id );
// bail if that's not a good user
// Bail if that's not a good user.
if ( is_wp_error( $user ) ) {
return;
}
// build the update
// Build the update.
$activity = new Activity();
$activity->set_id( $user->get_url() . '#update' );
$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
// Send the update.
self::send_activity_to_followers( $activity, $user_id, $user );
}
@ -152,25 +148,29 @@ class Activity_Dispatcher {
*
* @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.
*
* @return void
* @param \WP_User|WP_Post|WP_Comment $wp_object The WordPress object.
*/
private static function send_activity_to_followers( $activity, $user_id, $wp_object ) {
// check if the Activity should be send to the followers
/**
* 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;
}
$follower_inboxes = Followers::get_inboxes( $user_id );
$mentioned_inboxes = array();
$cc = $activity->get_cc();
if ( $cc ) {
$mentioned_inboxes = Mention::get_inboxes( $cc );
}
$inboxes = array_merge( $follower_inboxes, $mentioned_inboxes );
/**
* 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 ) ) {
@ -191,8 +191,6 @@ class Activity_Dispatcher {
*
* @param int $id The WordPress Post ID.
* @param string $type The Activity-Type.
*
* @return void
*/
public static function send_post( $id, $type ) {
$post = get_post( $id );
@ -201,14 +199,20 @@ class Activity_Dispatcher {
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 );
do_action(
sprintf(
'activitypub_send_%s_activity',
\strtolower( $type )
),
$post
);
/**
* 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 );
}
/**
@ -216,8 +220,6 @@ class Activity_Dispatcher {
*
* @param int $id The WordPress Comment ID.
* @param string $type The Activity-Type.
*
* @return void
*/
public static function send_comment( $id, $type ) {
$comment = get_comment( $id );
@ -226,13 +228,101 @@ class Activity_Dispatcher {
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 );
do_action(
sprintf(
'activitypub_send_%s_activity',
\strtolower( $type )
),
$comment
);
/**
* 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;
}
}

View File

@ -1,20 +1,18 @@
<?php
/**
* ActivityPub Class.
*
* @package Activitypub
*/
namespace Activitypub;
use Exception;
use Activitypub\Signature;
use Activitypub\Collection\Users;
use Activitypub\Collection\Followers;
use function Activitypub\is_comment;
use function Activitypub\sanitize_url;
use function Activitypub\is_local_comment;
use function Activitypub\is_user_type_disabled;
use function Activitypub\is_activitypub_request;
use function Activitypub\should_comment_be_federated;
use Activitypub\Collection\Extra_Fields;
/**
* ActivityPub Class
* ActivityPub Class.
*
* @author Matthias Pfefferle
*/
@ -28,7 +26,7 @@ class Activitypub {
\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
// 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();
foreach ( $post_types as $post_type ) {
@ -45,16 +43,18 @@ class Activitypub {
\add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) );
\add_filter( 'activitypub_get_actor_extra_fields', array( self::class, 'default_actor_extra_fields' ), 10, 2 );
if ( site_supports_blocks() ) {
\add_action( 'tool_box', array( self::class, 'tool_box' ) );
}
// register several post_types
\add_filter( 'activitypub_get_actor_extra_fields', array( Extra_Fields::class, 'default_actor_extra_fields' ), 10, 2 );
// Register several post_types.
self::register_post_types();
}
/**
* Activation Hook
*
* @return void
* Activation Hook.
*/
public static function activate() {
self::flush_rewrite_rules();
@ -62,9 +62,7 @@ class Activitypub {
}
/**
* Deactivation Hook
*
* @return void
* Deactivation Hook.
*/
public static function deactivate() {
self::flush_rewrite_rules();
@ -72,9 +70,7 @@ class Activitypub {
}
/**
* Uninstall Hook
*
* @return void
* Uninstall Hook.
*/
public static function uninstall() {
Scheduler::deregister_schedules();
@ -99,7 +95,7 @@ class Activitypub {
$json_template = false;
if ( \is_author() && ! is_user_disabled( \get_the_author_meta( 'ID' ) ) ) {
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/author-json.php';
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/user-json.php';
} elseif ( is_comment() ) {
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/comment-json.php';
} elseif ( \is_singular() ) {
@ -119,7 +115,7 @@ class Activitypub {
if ( \is_wp_error( $verification ) ) {
header( 'HTTP/1.1 401 Unauthorized' );
// fallback as template_loader can't return http headers
// Fallback as template_loader can't return http headers.
return $template;
}
}
@ -131,29 +127,78 @@ class Activitypub {
return $template;
}
/**
* Add the 'self' link to the header.
*/
public static function add_headers() {
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput
$request_uri = $_SERVER['REQUEST_URI'];
if ( ! $request_uri ) {
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"' );
}
add_action(
'wp_head',
function () use ( $self_link ) {
echo PHP_EOL . '<link rel="alternate" type="application/activity+json" href="' . esc_url( $self_link ) . '" />' . PHP_EOL;
}
);
}
/**
* Custom redirects for ActivityPub requests.
*
* @return void
*/
public static function template_redirect() {
self::add_headers();
$comment_id = get_query_var( 'c', null );
// check if it seems to be a comment
// Check if it seems to be a comment.
if ( ! $comment_id ) {
return;
}
$comment = get_comment( $comment_id );
// load a 404 page if `c` is set but not valid
// Load a 404 page if `c` is set but not valid.
if ( ! $comment ) {
global $wp_query;
$wp_query->set_404();
return;
}
// stop if it's not an ActivityPub comment
// Stop if it's not an ActivityPub comment.
if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
return;
}
@ -164,6 +209,10 @@ class Activitypub {
/**
* Add the 'activitypub' query variable so WordPress won't mangle it.
*
* @param array $vars The query variables.
*
* @return array The query variables.
*/
public static function add_query_vars( $vars ) {
$vars[] = 'activitypub';
@ -226,9 +275,9 @@ class Activitypub {
/**
* Function to retrieve Avatar URL if stored in meta.
*
* @param int|WP_Comment $comment
* @param int|\WP_Comment $comment The comment ID or object.
*
* @return string $url
* @return string The Avatar URL.
*/
public static function get_avatar_url( $comment ) {
if ( \is_numeric( $comment ) ) {
@ -241,8 +290,6 @@ class Activitypub {
* Store permalink in meta, to send delete Activity.
*
* @param string $post_id The Post ID.
*
* @return void
*/
public static function trash_post( $post_id ) {
\add_post_meta(
@ -254,22 +301,22 @@ class Activitypub {
}
/**
* Delete permalink from meta
* Delete permalink from meta.
*
* @param string $post_id The Post ID
*
* @return void
* @param string $post_id The Post ID.
*/
public static function untrash_post( $post_id ) {
\delete_post_meta( $post_id, 'activitypub_canonical_url' );
}
/**
* Add rewrite rules
* Add rewrite rules.
*/
public static function add_rewrite_rules() {
// If another system needs to take precedence over the ActivityPub rewrite rules,
// they can define their own and will manually call the appropriate functions as required.
/*
* If another system needs to take precedence over the ActivityPub rewrite rules,
* they can define their own and will manually call the appropriate functions as required.
*/
if ( ACTIVITYPUB_DISABLE_REWRITES ) {
return;
}
@ -305,7 +352,7 @@ class Activitypub {
}
/**
* Flush rewrite rules;
* Flush rewrite rules.
*/
public static function flush_rewrite_rules() {
self::add_rewrite_rules();
@ -313,37 +360,19 @@ class Activitypub {
}
/**
* Theme compatibility stuff
*
* @return void
* 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.
*/
public static function theme_compat() {
$site_icon = get_theme_support( 'custom-logo' );
if ( ! $site_icon ) {
// custom logo support
add_theme_support(
'custom-logo',
array(
'height' => 80,
'width' => 80,
)
);
}
$custom_header = get_theme_support( 'custom-header' );
if ( ! $custom_header ) {
// This theme supports a custom header
$custom_header_args = array(
'width' => 1250,
'height' => 600,
'header-text' => true,
);
add_theme_support( 'custom-header', $custom_header_args );
}
// We assume that you want to use Post-Formats when enabling the setting
// We assume that you want to use Post-Formats when enabling the setting.
if ( 'wordpress-post-format' === \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ) ) {
if ( ! get_theme_support( 'post-formats' ) ) {
// Add support for the Aside, Gallery Post Formats...
@ -362,11 +391,9 @@ class Activitypub {
}
/**
* Display plugin upgrade notice to users
* Display plugin upgrade notice to users.
*
* @param array $data The plugin data
*
* @return void
* @param array $data The plugin data.
*/
public static function plugin_update_message( $data ) {
if ( ! isset( $data['upgrade_notice'] ) ) {
@ -388,9 +415,7 @@ class Activitypub {
}
/**
* Register the "Followers" Taxonomy
*
* @return void
* Register the "Followers" Taxonomy.
*/
private static function register_post_types() {
\register_post_type(
@ -460,9 +485,8 @@ class Activitypub {
)
);
\register_post_type(
'ap_extrafield',
array(
// Both User and Blog Extra Fields types have the same args.
$args = array(
'labels' => array(
'name' => _x( 'Extra fields', 'post_type plural name', 'activitypub' ),
'singular_name' => _x( 'Extra field', 'post_type single name', 'activitypub' ),
@ -485,10 +509,12 @@ class Activitypub {
'show_in_rest' => true,
'map_meta_cap' => true,
'show_ui' => true,
'supports' => array( 'title', 'editor' ),
)
'supports' => array( 'title', 'editor', 'page-attributes' ),
);
\register_post_type( Extra_Fields::USER_POST_TYPE, $args );
\register_post_type( Extra_Fields::BLOG_POST_TYPE, $args );
\do_action( 'activitypub_after_register_post_type' );
}
@ -496,8 +522,6 @@ class Activitypub {
* Add the 'activitypub' capability to users who can publish posts.
*
* @param int $user_id User ID.
*
* @param array $userdata The raw array of data passed to wp_insert_user().
*/
public static function user_register( $user_id ) {
if ( \user_can( $user_id, 'publish_posts' ) ) {
@ -505,57 +529,4 @@ class Activitypub {
$user->add_cap( 'activitypub' );
}
}
/**
* Add default extra fields to an actor.
*
* @param array $extra_fields The extra fields.
* @param int $user_id The User-ID.
*
* @return array The extra fields.
*/
public static function default_actor_extra_fields( $extra_fields, $user_id ) {
if ( $extra_fields || ! $user_id ) {
return $extra_fields;
}
$already_migrated = \get_user_meta( $user_id, 'activitypub_default_extra_fields', true );
if ( $already_migrated ) {
return $extra_fields;
}
$defaults = array(
\__( 'Blog', 'activitypub' ) => \home_url( '/' ),
\__( 'Profile', 'activitypub' ) => \get_author_posts_url( $user_id ),
\__( 'Homepage', 'activitypub' ) => \get_the_author_meta( 'user_url', $user_id ),
);
foreach ( $defaults as $title => $url ) {
if ( ! $url ) {
continue;
}
$extra_field = array(
'post_type' => 'ap_extrafield',
'post_title' => $title,
'post_status' => 'publish',
'post_author' => $user_id,
'post_content' => sprintf(
'<!-- wp:paragraph --><p><a rel="me" title="%s" target="_blank" href="%s">%s</a></p><!-- /wp:paragraph -->',
\esc_attr( $url ),
$url,
\wp_parse_url( $url, \PHP_URL_HOST )
),
'comment_status' => 'closed',
);
$extra_field_id = wp_insert_post( $extra_field );
$extra_fields[] = get_post( $extra_field_id );
}
\update_user_meta( $user_id, 'activitypub_default_extra_fields', true );
return $extra_fields;
}
}

View File

@ -1,25 +1,25 @@
<?php
/**
* Admin Class.
*
* @package Activitypub
*/
namespace Activitypub;
use WP_User_Query;
use Activitypub\Model\Blog;
use Activitypub\Activitypub;
use Activitypub\Collection\Users;
use function Activitypub\count_followers;
use function Activitypub\is_user_disabled;
use function Activitypub\was_comment_received;
use function Activitypub\is_comment_federatable;
use function Activitypub\add_default_actor_extra_fields;
use Activitypub\Collection\Extra_Fields;
/**
* ActivityPub Admin Class
* ActivityPub Admin Class.
*
* @author Matthias Pfefferle
*/
class Admin {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks,
*/
public static function init() {
\add_action( 'admin_menu', array( self::class, 'admin_menu' ) );
@ -27,7 +27,7 @@ class Admin {
\add_action( 'load-comment.php', array( self::class, 'edit_comment' ) );
\add_action( 'load-post.php', array( self::class, 'edit_post' ) );
\add_action( 'load-edit.php', array( self::class, 'list_posts' ) );
\add_action( 'personal_options_update', array( self::class, 'save_user_description' ) );
\add_action( 'personal_options_update', array( self::class, 'save_user_settings' ) );
\add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
\add_action( 'admin_notices', array( self::class, 'admin_notices' ) );
@ -38,7 +38,7 @@ class Admin {
\add_filter( 'manage_posts_columns', array( static::class, 'manage_post_columns' ), 10, 2 );
\add_action( 'manage_posts_custom_column', array( self::class, 'manage_posts_custom_column' ), 10, 2 );
\add_filter( 'manage_users_columns', array( self::class, 'manage_users_columns' ), 10, 1 );
\add_filter( 'manage_users_columns', array( self::class, 'manage_users_columns' ) );
\add_action( 'manage_users_custom_column', array( self::class, 'manage_users_custom_column' ), 10, 3 );
\add_filter( 'bulk_actions-users', array( self::class, 'user_bulk_options' ) );
\add_filter( 'handle_bulk_actions-users', array( self::class, 'handle_bulk_request' ), 10, 3 );
@ -51,7 +51,7 @@ class Admin {
}
/**
* Add admin menu entry
* Add admin menu entry.
*/
public static function admin_menu() {
$settings_page = \add_options_page(
@ -62,22 +62,40 @@ class Admin {
array( self::class, 'settings_page' )
);
\add_action( 'load-' . $settings_page, array( self::class, 'add_settings_help_tab' ) );
\add_action(
'load-' . $settings_page,
array( self::class, 'add_settings_help_tab' )
);
// user has to be able to publish posts
// User has to be able to publish posts.
if ( ! is_user_disabled( get_current_user_id() ) ) {
$followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) );
$followers_list_page = \add_users_page(
\__( '⁂ Followers', 'activitypub' ),
\__( '⁂ Followers', 'activitypub' ),
'read',
'activitypub-followers-list',
array(
self::class,
'followers_list_page',
)
);
\add_action( 'load-' . $followers_list_page, array( self::class, 'add_followers_list_help_tab' ) );
\add_action(
'load-' . $followers_list_page,
array( self::class, 'add_followers_list_help_tab' )
);
\add_users_page( \__( 'Extra Fields', 'activitypub' ), \__( 'Extra Fields', 'activitypub' ), 'read', esc_url( admin_url( '/edit.php?post_type=ap_extrafield' ) ) );
\add_users_page(
\__( '⁂ Extra Fields', 'activitypub' ),
\__( '⁂ Extra Fields', 'activitypub' ),
'read',
\esc_url( \admin_url( '/edit.php?post_type=ap_extrafield' ) )
);
}
}
/**
* Display admin menu notices about configuration problems or conflicts.
*
* @return void
*/
public static function admin_notices() {
$permalink_structure = \get_option( 'permalink_structure' );
@ -87,11 +105,15 @@ class Admin {
}
$current_screen = get_current_screen();
if ( isset( $current_screen->id ) && 'edit-ap_extrafield' === $current_screen->id ) {
if ( ! $current_screen ) {
return;
}
if ( 'edit' === $current_screen->base && Extra_Fields::is_extra_fields_post_type( $current_screen->post_type ) ) {
?>
<div class="notice" style="margin: 0; background: none; border: none; box-shadow: none; padding: 15px 0 0 0; font-size: 14px;">
<?php esc_html_e( 'These are extra fields that are used for your ActivityPub profile. You can use your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' ); ?>
<?php
esc_html_e( 'These are extra fields that are used for your ActivityPub profile. You can use your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' );
?>
</div>
<?php
}
@ -102,8 +124,6 @@ class Admin {
*
* @param string $admin_notice The notice to display.
* @param string $level The level of the notice (error, warning, success, info).
*
* @return void
*/
private static function show_admin_notice( $admin_notice, $level ) {
?>
@ -116,7 +136,7 @@ class Admin {
}
/**
* Load settings page
* Load settings page.
*/
public static function settings_page() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
@ -131,8 +151,14 @@ class Admin {
case 'settings':
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' );
break;
case 'blog-profile':
wp_enqueue_media();
wp_enqueue_script( 'activitypub-header-image' );
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/blog-settings.php' );
break;
case 'followers':
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/blog-user-followers-list.php' );
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/blog-followers-list.php' );
break;
case 'welcome':
default:
@ -149,7 +175,7 @@ class Admin {
* Load user settings page
*/
public static function followers_list_page() {
// user has to be able to publish posts
// User has to be able to publish posts.
if ( ! is_user_disabled( get_current_user_id() ) ) {
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/user-followers-list.php' );
}
@ -222,6 +248,15 @@ class Admin {
'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',
@ -232,51 +267,6 @@ class Admin {
'default' => array( 'post' ),
)
);
\register_setting(
'activitypub',
'activitypub_blog_user_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_user_identifier',
'activitypub_blog_user_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',
'activitypub_enable_users',
@ -295,18 +285,98 @@ class Admin {
'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
// todo.
}
/**
* Add the profile.
*
* @param \WP_User $user The user object.
*/
public static function add_profile( $user ) {
$description = get_user_meta( $user->ID, 'activitypub_user_description', true );
$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',
@ -317,40 +387,97 @@ class Admin {
);
}
public static function save_user_description( $user_id ) {
/**
* 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 false;
return;
}
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_apnonce'] ) );
if (
! wp_verify_nonce( $nonce, 'activitypub-user-description' ) ||
! wp_verify_nonce( $nonce, 'activitypub-user-settings' ) ||
! current_user_can( 'edit_user', $user_id )
) {
return false;
return;
}
$description = ! empty( $_POST['activitypub-user-description'] ) ? sanitize_text_field( wp_unslash( $_POST['activitypub-user-description'] ) ) : false;
$description = ! empty( $_POST['activitypub_description'] ) ? sanitize_textarea_field( wp_unslash( $_POST['activitypub_description'] ) ) : false;
if ( $description ) {
update_user_meta( $user_id, 'activitypub_user_description', $description );
}
\update_user_option( $user_id, 'activitypub_description', $description );
} else {
\delete_user_option( $user_id, 'activitypub_description' );
}
public static function enqueue_scripts( $hook_suffix ) {
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() );
$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' );
}
}
/**
* Hook into the edit_comment functionality
* Enqueue the admin scripts and styles.
*
* * Disable the edit_comment capability for federated comments.
* @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.
*
* @return void
* Disables the edit_comment capability for federated comments.
*/
public static function edit_comment() {
// Disable the edit_comment capability for federated comments.
@ -372,6 +499,11 @@ class Admin {
);
}
/**
* 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(
@ -383,7 +515,7 @@ class Admin {
$post = get_post( $arg[2] );
if ( 'ap_extrafield' !== $post->post_type ) {
if ( ! Extra_Fields::is_extra_field_post_type( $post->post_type ) ) {
return $allcaps;
}
@ -399,9 +531,7 @@ class Admin {
}
/**
* Add ActivityPub specific actions/filters to the post list view
*
* @return void
* Add ActivityPub specific actions/filters to the post list view.
*/
public static function list_posts() {
// Show only the user's extra fields.
@ -420,20 +550,23 @@ class Admin {
add_filter(
"views_{$screen_id}",
function ( $views ) {
if ( 'ap_extrafield' === get_post_type() ) {
if ( Extra_Fields::is_extra_fields_post_type( get_current_screen()->post_type ) ) {
return array();
}
return $views;
}
);
// Set defaults for new extra fields.
if ( 'edit-ap_extrafield' === $screen_id ) {
Activitypub::default_actor_extra_fields( array(), get_current_user_id() );
}
}
/**
* 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'] );
@ -444,7 +577,7 @@ class Admin {
}
/**
* Add a column "activitypub"
* Add a column "activitypub".
*
* This column shows if the user has the capability to use ActivityPub.
*
@ -458,9 +591,11 @@ class Admin {
}
/**
* Add "comment-type" and "protocol" as column in WP-Admin
* Add "comment-type" and "protocol" as column in WP-Admin.
*
* @param array $columns the list of column names
* @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' );
@ -470,13 +605,15 @@ class Admin {
}
/**
* Add "post_content" as column for Extra-Fields in WP-Admin
* Add "post_content" as column for Extra-Fields in WP-Admin.
*
* @param array $columns Tthe list of column names.
* @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 ( 'ap_extrafield' === $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;
@ -486,10 +623,10 @@ class Admin {
}
/**
* Add "comment-type" and "protocol" as column in WP-Admin
* Add "comment-type" and "protocol" as column in WP-Admin.
*
* @param array $column The column to implement
* @param int $comment_id The comment id
* @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' ) ) {
@ -527,7 +664,7 @@ class Admin {
}
/**
* Add a column "extra_field_content" to the post list view
* 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.
@ -535,18 +672,16 @@ class Admin {
* @return void
*/
public static function manage_posts_custom_column( $column_name, $post_id ) {
$post = get_post( $post_id );
if ( 'extra_field_content' === $column_name ) {
$post = get_post( $post_id );
if ( 'ap_extrafield' === $post->post_type ) {
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
* Add options to the Bulk dropdown on the users page.
*
* @param array $actions The existing bulk options.
*
@ -560,7 +695,7 @@ class Admin {
}
/**
* Handle bulk activitypub requests
* 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.
@ -595,7 +730,7 @@ class Admin {
}
/**
* Add ActivityPub infos to the dashboard glance items
* Add ActivityPub infos to the dashboard glance items.
*
* @param array $items The existing glance items.
*
@ -606,7 +741,7 @@ class Admin {
if ( ! is_user_disabled( get_current_user_id() ) ) {
$follower_count = sprintf(
// translators: %s: number of followers
// translators: %s: number of followers.
_n(
'%s Follower',
'%s Followers',
@ -625,7 +760,7 @@ class Admin {
if ( ! is_user_type_disabled( 'blog' ) && current_user_can( 'manage_options' ) ) {
$follower_count = sprintf(
// translators: %s: number of followers
// translators: %s: number of followers.
_n(
'%s Follower (Blog)',
'%s Followers (Blog)',
@ -642,7 +777,7 @@ class Admin {
);
}
\remove_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers', 10, 3 );
\remove_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers' );
return $items;
}

View File

@ -1,20 +1,86 @@
<?php
/**
* Blocks file.
*
* @package Activitypub
*/
namespace Activitypub;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Users as User_Collection;
use function Activitypub\object_to_uri;
use function Activitypub\is_user_type_disabled;
/**
* Block class.
*/
class Blocks {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
// this is already being called on the init hook, so just add it.
// 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( '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 );
}
/**
* Register post meta for content warnings.
*/
public static function register_postmeta() {
$ap_post_types = \get_post_types_by_support( 'activitypub' );
foreach ( $ap_post_types as $post_type ) {
\register_post_meta(
$post_type,
'activitypub_content_warning',
array(
'show_in_rest' => true,
'single' => true,
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
)
);
}
}
/**
* Enqueue the block editor assets.
*/
public static function enqueue_editor_assets() {
// Check for our supported post types.
$current_screen = \get_current_screen();
$ap_post_types = \get_post_types_by_support( 'activitypub' );
if ( ! $current_screen || ! in_array( $current_screen->post_type, $ap_post_types, true ) ) {
return;
}
$asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/editor-plugin/plugin.asset.php';
$plugin_url = plugins_url( 'build/editor-plugin/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
wp_enqueue_script( 'activitypub-block-editor', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
}
/**
* Enqueue the reply handle script if the in_reply_to GET param is set.
*/
public static function handle_in_reply_to_get_param() {
// Only load the script if the in_reply_to GET param is set, action happens there, not here.
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( ! isset( $_GET['in_reply_to'] ) ) {
return;
}
$asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/reply-intent/plugin.asset.php';
$plugin_url = plugins_url( 'build/reply-intent/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
wp_enqueue_script( 'activitypub-reply-intent', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
}
/**
* Add data to the block editor.
*/
public static function add_data() {
$context = is_admin() ? 'editor' : 'view';
$followers_handle = 'activitypub-followers-' . $context . '-script';
@ -31,6 +97,9 @@ class Blocks {
\wp_add_inline_script( $follow_me_handle, $js, 'before' );
}
/**
* Register the blocks.
*/
public static function register_blocks() {
\register_block_type_from_metadata(
ACTIVITYPUB_PLUGIN_DIR . '/build/followers',
@ -44,45 +113,100 @@ class Blocks {
'render_callback' => array( self::class, 'render_follow_me_block' ),
)
);
\register_block_type_from_metadata(
ACTIVITYPUB_PLUGIN_DIR . '/build/reply',
array(
'render_callback' => array( self::class, 'render_reply_block' ),
)
);
}
/**
* Get the user ID from a user string.
*
* @param string $user_string The user string. Can be a user ID, 'site', or 'inherit'.
* @return int|null The user ID, or null if the 'inherit' string is not supported in this context.
*/
private static function get_user_id( $user_string ) {
if ( is_numeric( $user_string ) ) {
return absint( $user_string );
}
// any other non-numeric falls back to 0, including the `site` string used in the UI
return 0;
// If the user string is 'site', return the Blog User ID.
if ( 'site' === $user_string ) {
return User_Collection::BLOG_USER_ID;
}
// The only other value should be 'inherit', which means to use the query context to determine the User.
if ( 'inherit' !== $user_string ) {
return null;
}
// 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;
}
// If we're in a loop, use the post author.
$author_id = get_the_author_meta( 'ID' );
if ( $author_id ) {
return $author_id;
}
// For other pages, the queried object will clue us in.
$queried_object = get_queried_object();
if ( ! $queried_object ) {
return null;
}
// If we're on a user archive page, use that user's ID.
if ( is_a( $queried_object, 'WP_User' ) ) {
return $queried_object->ID;
}
// For a single post, use the post author's ID.
if ( is_a( $queried_object, 'WP_Post' ) ) {
return get_the_author_meta( 'ID' );
}
// We won't properly account for some conditions, like tag archives.
return null;
}
/**
* Filter an array by a list of keys.
* @param array $array The array to filter.
*
* @param array $data The array to filter.
* @param array $keys The keys to keep.
* @return array The filtered array.
*/
protected static function filter_array_by_keys( $array, $keys ) {
return array_intersect_key( $array, array_flip( $keys ) );
protected static function filter_array_by_keys( $data, $keys ) {
return array_intersect_key( $data, array_flip( $keys ) );
}
/**
* Render the follow me block.
*
* @param array $attrs The block attributes.
* @return string The HTML to render.
*/
public static function render_follow_me_block( $attrs ) {
$user_id = self::get_user_id( $attrs['selectedUser'] );
$user = User_Collection::get_by_id( $user_id );
if ( ! is_wp_error( $user ) ) {
if ( is_wp_error( $user ) ) {
if ( 'inherit' === $attrs['selectedUser'] ) {
// If the user is 'inherit' and we couldn't determine the user, don't render anything.
return '<!-- Follow Me block: `inherit` mode does not display on this type of page -->';
} else {
// If the user is a specific ID and we couldn't find it, render an error message.
return '<!-- Follow Me block: user not found -->';
}
}
$attrs['profileData'] = self::filter_array_by_keys(
$user->to_array(),
array( 'icon', 'name', 'webfinger' )
);
}
// add `@` prefix if it's missing
if ( '@' !== substr( $attrs['profileData']['webfinger'], 0, 1 ) ) {
$attrs['profileData']['webfinger'] = '@' . $attrs['profileData']['webfinger'];
}
$wrapper_attributes = get_block_wrapper_attributes(
array(
@ -95,8 +219,24 @@ class Blocks {
return '<div ' . $wrapper_attributes . '></div>';
}
/**
* Render the follower block.
*
* @param array $attrs The block attributes.
*
* @return string The HTML to render.
*/
public static function render_follower_block( $attrs ) {
$followee_user_id = self::get_user_id( $attrs['selectedUser'] );
if ( is_null( $followee_user_id ) ) {
return '<!-- Followers block: `inherit` mode does not display on this type of page -->';
}
$user = User_Collection::get_by_id( $followee_user_id );
if ( is_wp_error( $user ) ) {
return '<!-- Followers block: `' . $followee_user_id . '` not an active ActivityPub user -->';
}
$per_page = absint( $attrs['per_page'] );
$follower_data = Followers::get_followers_with_count( $followee_user_id, $per_page );
@ -131,6 +271,40 @@ class Blocks {
return $html;
}
/**
* Render the reply block.
*
* @param array $attrs The block attributes.
*
* @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(
'<p><a title="%2$s" aria-label="%2$s" href="%1$s" class="u-in-reply-to" target="_blank">%3$s</a></p>',
esc_url( $attrs['url'] ),
esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ),
// translators: %s is the URL of the post being replied to.
sprintf( __( '&#8620;%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', $attrs['url'] ) )
),
$attrs
);
}
/**
* Render a follower.
*
* @param \Activitypub\Model\Follower $follower The follower to render.
*
* @return string The HTML to render.
*/
public static function render_follower( $follower ) {
$external_svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="components-external-link__icon css-rvs7bx esh4a730" aria-hidden="true" focusable="false"><path d="M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"></path></svg>';
$template =

View File

@ -0,0 +1,201 @@
<?php
/**
* WP-CLI file.
*
* @package Activitypub
*/
namespace Activitypub;
use WP_CLI;
use WP_CLI_Command;
/**
* 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 );
}
}
/**
* Remove the entire blog from the Fediverse.
*
* ## EXAMPLES
*
* $ wp activitypub self-destruct
*
* @param array|null $args The arguments.
* @param array|null $assoc_args The associative arguments.
*
* @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' ) );
}
/**
* Delete or Update a Post, Page, Custom Post Type or Attachment.
*
* ## OPTIONS
*
* <action>
* : The action to perform. Either `delete` or `update`.
* ---
* options:
* - delete
* - update
* ---
*
* <id>
* : The id of the Post, Page, Custom Post Type or Attachment.
*
* ## EXAMPLES
*
* $ wp activitypub post delete 1
*
* @synopsis <action> <id>
*
* @param array|null $args The arguments.
*/
public function post( $args ) {
$post = get_post( $args[1] );
if ( ! $post ) {
WP_CLI::error( __( 'Post not found.', 'activitypub' ) );
}
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' ) );
break;
case 'update':
Scheduler::schedule_post_activity( 'publish', 'publish', $args[1] );
WP_CLI::success( __( '"Update"-Activity is queued.', 'activitypub' ) );
break;
default:
WP_CLI::error( __( 'Unknown action.', 'activitypub' ) );
}
}
/**
* Delete or Update a Comment.
*
* ## OPTIONS
*
* <action>
* : The action to perform. Either `delete` or `update`.
* ---
* options:
* - delete
* - update
* ---
*
* <id>
* : The id of the Comment.
*
* ## EXAMPLES
*
* $ wp activitypub comment delete 1
*
* @synopsis <action> <id>
*
* @param array|null $args The arguments.
*/
public function comment( $args ) {
$comment = get_comment( $args[1] );
if ( ! $comment ) {
WP_CLI::error( __( 'Comment not found.', 'activitypub' ) );
}
if ( was_comment_received( $comment ) ) {
WP_CLI::error( __( 'This comment was received via ActivityPub and cannot be deleted or updated.', 'activitypub' ) );
}
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' ) );
break;
case 'update':
Scheduler::schedule_comment_activity( 'approved', 'approved', $args[1] );
WP_CLI::success( __( '"Update"-Activity is queued.', 'activitypub' ) );
break;
default:
WP_CLI::error( __( 'Unknown action.', 'activitypub' ) );
}
}
}

View File

@ -1,28 +1,35 @@
<?php
/**
* ActivityPub Comment Class
*
* @package Activitypub
*/
namespace Activitypub;
use Activitypub\Collection\Users;
use WP_Comment_Query;
use function Activitypub\is_user_disabled;
use function Activitypub\is_single_user;
/**
* ActivityPub Comment Class
* ActivityPub Comment Class.
*
* This class is a helper/utils class that provides a collection of static
* methods that are used to handle comments.
*/
class Comment {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_comment_types();
\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_action( 'wp_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
\add_action( 'pre_get_comments', array( static::class, 'comment_query' ) );
\add_filter( 'get_avatar_comment_types', array( static::class, 'get_avatar_comment_types' ), 99 );
}
/**
@ -33,7 +40,7 @@ class Comment {
*
* @param string $link The HTML markup for the comment reply link.
* @param array $args An array of arguments overriding the defaults.
* @param WP_Comment $comment The object of the comment being replied.
* @param \WP_Comment $comment The object of the comment being replied.
*
* @return string The filtered HTML markup for the comment reply link.
*/
@ -62,6 +69,7 @@ class Comment {
/**
* Create a link to reply to a federated comment.
*
* This function adds a title attribute to the reply link to inform the user
* that the comment was received from the fediverse and the reply will be sent
* to the original author.
@ -104,7 +112,7 @@ 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
// 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;
}
@ -200,7 +208,7 @@ class Comment {
* @return boolean True if the comment should be federated, false otherwise.
*/
public static function should_be_federated( $comment ) {
// we should not federate federated comments
// We should not federate federated comments.
if ( self::was_received( $comment ) ) {
return false;
}
@ -208,29 +216,29 @@ class Comment {
$comment = \get_comment( $comment );
$user_id = $comment->user_id;
// comments without user can't be federated
// Comments without user can't be federated.
if ( ! $user_id ) {
return false;
}
if ( is_single_user() && \user_can( $user_id, 'publish_posts' ) ) {
// On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user
// On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user.
$user_id = Users::BLOG_USER_ID;
}
$is_user_disabled = is_user_disabled( $user_id );
// user is disabled for federation
// User is disabled for federation.
if ( $is_user_disabled ) {
return false;
}
// it is a comment to the post and can be federated
// It is a comment to the post and can be federated.
if ( empty( $comment->comment_parent ) ) {
return true;
}
// check if parent comment is federated
// Check if parent comment is federated.
$parent_comment = \get_comment( $comment->comment_parent );
return ! self::is_local( $parent_comment );
@ -264,18 +272,18 @@ class Comment {
/**
* Verify if URL is a local comment, or if it is a previously received
* remote comment (For threading comments locally)
* remote comment (For threading comments locally).
*
* @param string $url The URL to check.
*
* @return int comment_ID or null if not found
* @return string|null Comment ID or null if not found.
*/
public static function url_to_commentid( $url ) {
if ( ! $url || ! filter_var( $url, \FILTER_VALIDATE_URL ) ) {
return null;
}
// check for local comment
// Check for local comment.
if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) {
$query = \wp_parse_url( $url, \PHP_URL_QUERY );
@ -327,7 +335,7 @@ class Comment {
* @return string[] An array of classes.
*/
public static function comment_class( $classes, $css_class, $comment_id ) {
// check if ActivityPub comment
// Check if ActivityPub comment.
if ( 'activitypub' === get_comment_meta( $comment_id, 'protocol', true ) ) {
$classes[] = 'activitypub-comment';
}
@ -335,11 +343,51 @@ class Comment {
return $classes;
}
/**
* Gets the public comment id via the WordPress comments meta.
*
* @param int $wp_comment_id The internal WordPress comment ID.
* @param bool $fallback Whether the code should fall back to `source_url` if `source_id` is not set.
*
* @return string|null The ActivityPub id/url of the comment.
*/
public static function get_source_id( $wp_comment_id, $fallback = true ) {
$comment_meta = \get_comment_meta( $wp_comment_id );
if ( ! empty( $comment_meta['source_id'][0] ) ) {
return $comment_meta['source_id'][0];
} elseif ( ! empty( $comment_meta['source_url'][0] ) && $fallback ) {
return $comment_meta['source_url'][0];
}
return null;
}
/**
* Gets the public comment url via the WordPress comments meta.
*
* @param int $wp_comment_id The internal WordPress comment ID.
* @param bool $fallback Whether the code should fall back to `source_id` if `source_url` is not set.
*
* @return string|null The ActivityPub id/url of the comment.
*/
public static function get_source_url( $wp_comment_id, $fallback = true ) {
$comment_meta = \get_comment_meta( $wp_comment_id );
if ( ! empty( $comment_meta['source_url'][0] ) ) {
return $comment_meta['source_url'][0];
} elseif ( ! empty( $comment_meta['source_id'][0] ) && $fallback ) {
return $comment_meta['source_id'][0];
}
return null;
}
/**
* Link remote comments to source url.
*
* @param string $comment_link
* @param object|WP_Comment $comment
* @param string $comment_link The comment link.
* @param object|\WP_Comment $comment The comment object.
*
* @return string $url
*/
@ -348,37 +396,30 @@ class Comment {
return $comment_link;
}
$comment_meta = \get_comment_meta( $comment->comment_ID );
$public_comment_link = self::get_source_url( $comment->comment_ID );
if ( ! empty( $comment_meta['source_url'][0] ) ) {
return $comment_meta['source_url'][0];
} elseif ( ! empty( $comment_meta['source_id'][0] ) ) {
return $comment_meta['source_id'][0];
}
return $comment_link;
return $public_comment_link ?? $comment_link;
}
/**
* Generates an ActivityPub URI for a comment
*
* @param WP_Comment|int $comment A comment object or comment ID
* @param \WP_Comment|int $comment A comment object or comment ID.
*
* @return string ActivityPub URI for comment
*/
public static function generate_id( $comment ) {
$comment = \get_comment( $comment );
$comment_meta = \get_comment_meta( $comment->comment_ID );
// show external comment ID if it exists
if ( ! empty( $comment_meta['source_id'][0] ) ) {
return $comment_meta['source_id'][0];
} elseif ( ! empty( $comment_meta['source_url'][0] ) ) {
return $comment_meta['source_url'][0];
// Show external comment ID if it exists.
$public_comment_link = self::get_source_id( $comment->comment_ID );
if ( $public_comment_link ) {
return $public_comment_link;
}
// generate URI based on comment ID
// Generate URI based on comment ID.
return \add_query_arg( 'c', $comment->comment_ID, \trailingslashit( \home_url() ) );
}
@ -393,6 +434,7 @@ class Comment {
$comments = \get_comments(
array(
'post_id' => $post_id,
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
'meta_query' => array(
'relation' => 'AND',
array(
@ -416,22 +458,22 @@ class Comment {
*/
public static function enqueue_scripts() {
if ( ! \is_singular() || \is_user_logged_in() ) {
// only on single pages, only for logged out users
// Only on single pages, only for logged-out users.
return;
}
if ( ! \post_type_supports( \get_post_type(), 'activitypub' ) ) {
// post type does not support ActivityPub
// Post type does not support ActivityPub.
return;
}
if ( ! \comments_open() || ! \get_comments_number() ) {
// no comments, no need to load the script
// No comments, no need to load the script.
return;
}
if ( ! self::post_has_remote_comments( \get_the_ID() ) ) {
// no remote comments, no need to load the script
// No remote comments, no need to load the script.
return;
}
@ -457,9 +499,171 @@ class Comment {
\wp_enqueue_style(
$handle,
\plugins_url( 'build/remote-reply/style-index.css', __DIR__ ),
[ 'wp-components' ],
array( 'wp-components' ),
$assets['version']
);
}
}
/**
* Return the registered custom comment types.
*
* @return array The registered custom comment types
*/
public static function get_comment_types() {
global $activitypub_comment_types;
return $activitypub_comment_types;
}
/**
* Is this a registered comment type.
*
* @param string $slug The name of the type.
* @return boolean True if registered.
*/
public static function is_registered_comment_type( $slug ) {
$slug = strtolower( $slug );
$slug = sanitize_key( $slug );
return in_array( $slug, array_keys( self::get_comment_types() ), true );
}
/**
* Return the registered custom comment types names.
*
* @return array The registered custom comment type names.
*/
public static function get_comment_type_names() {
return array_values( wp_list_pluck( self::get_comment_types(), 'type' ) );
}
/**
* Get a comment type.
*
* @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();
if ( in_array( $type, array_keys( $types ), true ) ) {
$type_array = $types[ $type ];
} else {
$type_array = array();
}
/**
* Filter the comment type.
*
* @param array $type_array The comment type.
*/
return apply_filters( "activitypub_comment_type_{$type}", $type_array );
}
/**
* Get a comment type attribute.
*
* @param string $type The comment type.
* @param string $attr The attribute to get.
*
* @return mixed The value of the attribute.
*/
public static function get_comment_type_attr( $type, $attr ) {
$type_array = self::get_comment_type( $type );
if ( $type_array && isset( $type_array[ $attr ] ) ) {
$value = $type_array[ $attr ];
} else {
$value = '';
}
/**
* Filter the comment type attribute.
*
* @param mixed $value The value of the attribute.
* @param string $type The comment type.
*/
return apply_filters( "activitypub_comment_type_{$attr}", $value, $type );
}
/**
* Register the comment types used by the ActivityPub plugin.
*/
public static function register_comment_types() {
register_comment_type(
'announce',
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' => __( '&hellip; reposted this!', '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' => __( '&hellip; liked this!', 'activitypub' ),
)
);
}
/**
* Show avatars on Activities if set.
*
* @param array $types List of avatar enabled comment types.
*
* @return array show avatars on Activities
*/
public static function get_avatar_comment_types( $types ) {
$comment_types = self::get_comment_type_names();
$types = array_merge( $types, $comment_types );
return array_unique( $types );
}
/**
* Excludes likes and reposts from comment queries.
*
* @author Jan Boddez
*
* @see https://github.com/janboddez/indieblocks/blob/a2d59de358031056a649ee47a1332ce9e39d4ce2/includes/functions.php#L423-L432
*
* @param WP_Comment_Query $query Comment count.
*/
public static function comment_query( $query ) {
if ( ! $query instanceof WP_Comment_Query ) {
return;
}
if ( is_admin() || ! is_singular() ) {
return;
}
if ( ! empty( $query->query_vars['type__in'] ) ) {
return;
}
if ( isset( $query->query_vars['count'] ) && true === $query->query_vars['count'] ) {
return;
}
// Exclude likes and reposts by the Webmention plugin.
$query->query_vars['type__not_in'] = self::get_comment_type_names();
}
}

View File

@ -1,37 +1,66 @@
<?php
/**
* Debug Class.
*
* @package Activitypub
*/
namespace Activitypub;
use WP_DEBUG;
use WP_DEBUG_LOG;
/**
* ActivityPub Debug Class
* ActivityPub Debug Class.
*
* @author Matthias Pfefferle
*/
class Debug {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
if ( WP_DEBUG_LOG ) {
\add_action( 'activitypub_safe_remote_post_response', array( self::class, 'log_remote_post_responses' ), 10, 4 );
\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 );
}
}
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
public static function log_remote_post_responses( $response, $url, $body, $user_id ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r
\error_log( "Request to: {$url} with response: " . \print_r( $response, true ) );
/**
* Log the responses of remote post requests.
*
* @param array $response The response from the remote server.
* @param string $url The URL of the remote server.
*/
public static function log_remote_post_responses( $response, $url ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
\error_log( "[OUTBOX] Request to: {$url} with Response: " . \print_r( $response, true ) );
}
/**
* Log the inbox requests.
*
* @param array $data The Activity array.
* @param int $user_id The ID of the local blog user.
* @param string $type The type of the request.
*/
public static function log_inbox( $data, $user_id, $type ) {
$type = strtolower( $type );
if ( 'delete' !== $type ) {
$url = object_to_uri( $data['actor'] );
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
\error_log( "[INBOX] Request From: {$url} with Activity: " . \print_r( $data, true ) );
}
}
/**
* Write a log entry.
*
* @param mixed $log The log entry.
*/
public static function write_log( $log ) {
if ( \is_array( $log ) || \is_object( $log ) ) {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
\error_log( \print_r( $log, true ) );
} else {
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
\error_log( $log );
}
}
}

View File

@ -1,10 +1,17 @@
<?php
/**
* Handler class.
*
* @package Activitypub
*/
namespace Activitypub;
use Activitypub\Handler\Announce;
use Activitypub\Handler\Create;
use Activitypub\Handler\Delete;
use Activitypub\Handler\Follow;
use Activitypub\Handler\Like;
use Activitypub\Handler\Undo;
use Activitypub\Handler\Update;
@ -13,7 +20,7 @@ use Activitypub\Handler\Update;
*/
class Handler {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_handlers();
@ -30,6 +37,15 @@ class Handler {
Undo::init();
Update::init();
if ( ! ACTIVITYPUB_DISABLE_REACTIONS ) {
Like::init();
}
/**
* Register additional handlers.
*
* @since 1.3.0
*/
do_action( 'activitypub_register_handlers' );
}
}

View File

@ -1,117 +1,100 @@
<?php
/**
* Hashtag Class.
*
* @package Activitypub
*/
namespace Activitypub;
/**
* ActivityPub Hashtag Class
* ActivityPub Hashtag Class.
*
* @author Matthias Pfefferle
*/
class Hashtag {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
if ( '1' === \get_option( 'activitypub_use_hashtags', '1' ) ) {
\add_action( 'wp_insert_post', array( self::class, 'insert_post' ), 10, 2 );
\add_filter( 'the_content', array( self::class, 'the_content' ), 10, 1 );
\add_filter( 'the_content', array( self::class, 'the_content' ) );
\add_filter( 'activitypub_activity_object_array', array( self::class, 'filter_activity_object' ), 99 );
}
}
/**
* Filter to save #tags as real WordPress tags
* Filter only the activity object and replace summery it with URLs.
*
* @param int $id the rev-id
* @param WP_Post $post the post
* @param array $activity The activity object array.
*
* @return
* @return array The filtered activity object array.
*/
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'] ) ) {
$activity['summary'] = self::the_content( $activity['summary'] );
}
*/
public static function insert_post( $id, $post ) {
if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $post->post_content, $match ) ) {
$tags = \implode( ', ', $match[1] );
\wp_add_post_tags( $post->post_parent, $tags );
if ( ! empty( $activity['content'] ) ) {
$activity['content'] = self::the_content( $activity['content'] );
}
return $id;
return $activity;
}
/**
* Filter to replace the #tags in the content with links
* Filter to save #tags as real WordPress tags.
*
* @param string $the_content the post-content
* @param int $post_id Post ID.
* @param \WP_Post $post Post object.
*/
public static function insert_post( $post_id, $post ) {
$tags = array();
if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $post->post_content, $match ) ) {
$tags = array_merge( $tags, $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 );
}
/**
* Filter to replace the #tags in the content with links.
*
* @return string the filtered post-content
* @param string $the_content The post content.
*
* @return string The filtered post content.
*/
public static function the_content( $the_content ) {
// small protection against execution timeouts: limit to 1 MB
if ( mb_strlen( $the_content ) > MB_IN_BYTES ) {
return $the_content;
}
$tag_stack = array();
$protected_tags = array(
'pre',
'code',
'textarea',
'style',
'a',
);
$content_with_links = '';
$in_protected_tag = false;
foreach ( wp_html_split( $the_content ) as $chunk ) {
if ( preg_match( '#^<!--[\s\S]*-->$#i', $chunk, $m ) ) {
$content_with_links .= $chunk;
continue;
}
if ( preg_match( '#^<(/)?([a-z-]+)\b[^>]*>$#i', $chunk, $m ) ) {
$tag = strtolower( $m[2] );
if ( '/' === $m[1] ) {
// Closing tag.
$i = array_search( $tag, $tag_stack, true );
// We can only remove the tag from the stack if it is in the stack.
if ( false !== $i ) {
$tag_stack = array_slice( $tag_stack, 0, $i );
}
} else {
// Opening tag, add it to the stack.
$tag_stack[] = $tag;
}
// If we're in a protected tag, the tag_stack contains at least one protected tag string.
// The protected tag state can only change when we encounter a start or end tag.
$in_protected_tag = array_intersect( $tag_stack, $protected_tags );
// Never inspect tags.
$content_with_links .= $chunk;
continue;
}
if ( $in_protected_tag ) {
// Don't inspect a chunk inside an inspected tag.
$content_with_links .= $chunk;
continue;
}
// Only reachable when there is no protected tag in the stack.
$content_with_links .= \preg_replace_callback( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', array( '\Activitypub\Hashtag', 'replace_with_links' ), $chunk );
}
return $content_with_links;
return enrich_content_data( $the_content, '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', array( self::class, 'replace_with_links' ) );
}
/**
* A callback for preg_replace to build the term links
* A callback for preg_replace to build the term links.
*
* @param array $result the preg_match results
* @param array $result The preg_match results.
* @return string the final string
*/
public static function replace_with_links( $result ) {
$tag = $result[1];
$tag_object = \get_term_by( 'name', $tag, 'post_tag' );
if ( ! $tag_object ) {
$tag_object = \get_term_by( 'name', $tag, 'category' );
}
if ( $tag_object ) {
$link = \get_term_link( $tag_object, 'post_tag' );
return \sprintf( '<a rel="tag" class="hashtag u-tag u-category" href="%s">#%s</a>', $link, $tag );
return \sprintf( '<a rel="tag" class="hashtag u-tag u-category" href="%s">#%s</a>', esc_url( $link ), $tag );
}
return '#' . $tag;

View File

@ -1,31 +1,37 @@
<?php
/**
* Health_Check class.
*
* @package Activitypub
*/
namespace Activitypub;
use WP_Error;
use Activitypub\Webfinger;
use Activitypub\Collection\Users;
use function Activitypub\get_plugin_version;
use function Activitypub\is_user_type_disabled;
use function Activitypub\get_webfinger_resource;
/**
* ActivityPub Health_Check Class
* ActivityPub Health_Check Class.
*
* @author Matthias Pfefferle
*/
class Health_Check {
/**
* Initialize health checks
*
* @return void
* Initialize health checks.
*/
public static function init() {
\add_filter( 'site_status_tests', array( self::class, 'add_tests' ) );
\add_filter( 'debug_information', array( self::class, 'debug_information' ) );
}
/**
* Add tests to the Site Health Check.
*
* @param array $tests The test array.
*
* @return array The filtered test array.
*/
public static function add_tests( $tests ) {
if ( ! is_user_disabled( get_current_user_id() ) ) {
$tests['direct']['activitypub_test_author_url'] = array(
@ -43,7 +49,7 @@ class Health_Check {
}
/**
* Author URL tests
* Author URL tests.
*
* @return array
*/
@ -81,7 +87,7 @@ class Health_Check {
}
/**
* System Cron tests
* System Cron tests.
*
* @return array
*/
@ -114,7 +120,7 @@ class Health_Check {
);
$result['actions'] .= sprintf(
'<p><a href="%s" target="_blank" rel="noopener">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
__( 'https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/', 'activitypub' ),
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' )
@ -124,7 +130,7 @@ class Health_Check {
}
/**
* WebFinger tests
* WebFinger tests.
*
* @return array
*/
@ -162,21 +168,21 @@ class Health_Check {
}
/**
* Check if `author_posts_url` is accessible and that request returns correct JSON
* Check if `author_posts_url` is accessible and that request returns correct JSON.
*
* @return boolean|WP_Error
* @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
// Check for "author" in URL.
if ( $author_url !== $reference_author_url ) {
return new WP_Error(
'author_url_not_accessible',
\sprintf(
// translators: %s: Author URL
// translators: %s: Author URL.
\__(
'Your author URL <code>%s</code> was replaced, this is often done by plugins.',
'activitypub'
@ -186,7 +192,7 @@ class Health_Check {
);
}
// try to access author URL
// Try to access author URL.
$response = \wp_remote_get(
$author_url,
array(
@ -199,7 +205,7 @@ class Health_Check {
return new WP_Error(
'author_url_not_accessible',
\sprintf(
// translators: %s: Author URL
// translators: %s: Author URL.
\__(
'Your author URL <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure. If the setup seems fine, maybe check if a plugin might restrict the access.',
'activitypub'
@ -211,12 +217,12 @@ class Health_Check {
$response_code = \wp_remote_retrieve_response_code( $response );
// check for redirects
// 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
// translators: %s: Author URL.
\__(
'Your author URL <code>%s</code> is redirecting to another page, this is often done by SEO plugins like "Yoast SEO".',
'activitypub'
@ -226,14 +232,14 @@ class Health_Check {
);
}
// check if response is JSON
// 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
// translators: %s: Author URL.
\__(
'Your author URL <code>%s</code> does not return valid JSON for <code>application/activity+json</code>. Please check if your hosting supports alternate <code>Accept</code> headers.',
'activitypub'
@ -258,8 +264,9 @@ class Health_Check {
$url = Webfinger::resolve( $resource );
if ( \is_wp_error( $url ) ) {
$allowed = array( 'code' => array() );
$not_accessible = wp_kses(
// translators: %s: Author URL
// translators: %s: Author URL.
\__(
'Your WebFinger endpoint <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure.',
'activitypub'
@ -267,7 +274,7 @@ class Health_Check {
$allowed
);
$invalid_response = wp_kses(
// translators: %s: Author URL
// translators: %s: Author URL.
\__(
'Your WebFinger endpoint <code>%s</code> does not return valid JSON for <code>application/jrd+json</code>.',
'activitypub'
@ -278,18 +285,19 @@ class Health_Check {
$health_messages = array(
'webfinger_url_not_accessible' => \sprintf(
$not_accessible,
$url->get_error_data()
$url->get_error_data()['data']
),
'webfinger_url_invalid_response' => \sprintf(
// translators: %s: Author URL
// translators: %s: Author URL.
$invalid_response,
$url->get_error_data()
$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,
@ -303,7 +311,7 @@ class Health_Check {
/**
* Retrieve the URL to the author page for the user with the ID provided.
*
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
* @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.
@ -312,6 +320,7 @@ class Health_Check {
*/
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();

View File

@ -1,11 +1,15 @@
<?php
/**
* ActivityPub HTTP Class.
*
* @package Activitypub
*/
namespace Activitypub;
use WP_Error;
use Activitypub\Collection\Users;
use function Activitypub\get_masked_wp_version;
/**
* ActivityPub HTTP Class
*
@ -15,11 +19,11 @@ class Http {
/**
* Send a POST Request with the needed HTTP Headers
*
* @param string $url The URL endpoint
* @param string $body The Post Body
* @param int $user_id The WordPress User-ID
* @param string $url The URL endpoint.
* @param string $body The Post Body.
* @param int $user_id The WordPress User-ID.
*
* @return array|WP_Error The POST Response or an WP_ERROR
* @return array|WP_Error The POST Response or an WP_Error.
*/
public static function post( $url, $body, $user_id ) {
\do_action( 'activitypub_pre_http_post', $url, $body, $user_id );
@ -58,18 +62,26 @@ class Http {
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
}
/**
* Action to save the response of the remote POST request.
*
* @param array|WP_Error $response The response of the remote POST request.
* @param string $url The URL endpoint.
* @param string $body The Post Body.
* @param int $user_id The WordPress User-ID.
*/
\do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id );
return $response;
}
/**
* Send a GET Request with the needed HTTP Headers
* Send a GET Request with the needed HTTP Headers.
*
* @param string $url The URL endpoint
* @param bool|int $cached If the result should be cached, or its duration. Default: 1hr.
* @param string $url The URL endpoint.
* @param bool|int $cached Optional. Whether the result should be cached, or its duration. Default false.
*
* @return array|WP_Error The GET Response or an WP_ERROR
* @return array|WP_Error The GET Response or a WP_Error.
*/
public static function get( $url, $cached = false ) {
\do_action( 'activitypub_pre_http_get', $url );
@ -80,6 +92,12 @@ class Http {
$response = \get_transient( $transient_key );
if ( $response ) {
/**
* Action to save the response of the remote GET request.
*
* @param array|WP_Error $response The response of the remote GET request.
* @param string $url The URL endpoint.
*/
\do_action( 'activitypub_safe_remote_get_response', $response, $url );
return $response;
@ -118,6 +136,12 @@ class Http {
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
}
/**
* Action to save the response of the remote GET request.
*
* @param array|WP_Error $response The response of the remote GET request.
* @param string $url The URL endpoint.
*/
\do_action( 'activitypub_safe_remote_get_response', $response, $url );
if ( $cached ) {
@ -139,6 +163,11 @@ class Http {
* @return bool True if the URL is a tombstone.
*/
public static function is_tombstone( $url ) {
/**
* Action 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 );
@ -151,15 +180,22 @@ class Http {
return false;
}
/**
* Generate a cache key for the URL.
*
* @param string $url The URL to generate the cache key for.
*
* @return string The cache key.
*/
public static function generate_cache_key( $url ) {
return 'activitypub_http_' . \md5( $url );
}
/**
* Requests the Data from the Object-URL or Object-Array
* Requests the Data from the Object-URL or Object-Array.
*
* @param array|string $url_or_object The Object or the Object URL.
* @param bool $cached If the result should be cached.
* @param bool $cached Optional. Whether the result should be cached. Default true.
*
* @return array|WP_Error The Object data as array or WP_Error on failure.
*/
@ -204,7 +240,7 @@ class Http {
$transient_key = self::generate_cache_key( $url );
// only check the cache if needed.
// Only check the cache if needed.
if ( $cached ) {
$data = \get_transient( $transient_key );

View File

@ -0,0 +1,127 @@
<?php
/**
* Link class.
*
* @package Activitypub
*/
namespace Activitypub;
/**
* ActivityPub Summery Links Class.
*/
class Link {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_extra_field_content', array( self::class, 'the_content' ) );
\add_filter( 'activitypub_activity_object_array', array( self::class, 'filter_activity_object' ), 99 );
}
/**
* Filter only the activity object and replace the summary with URLs.
*
* @param array $activity The activity object array.
*
* @return array Rhe activity object array.
*/
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'] ) ) {
$activity['summary'] = self::the_content( $activity['summary'] );
}
*/
if ( ! empty( $activity['content'] ) ) {
$activity['content'] = self::the_content( $activity['content'] );
}
return $activity;
}
/**
* Filter to replace the URLS in the content with links
*
* @param string $the_content The post content.
*
* @return string the filtered post content.
*/
public static function the_content( $the_content ) {
return enrich_content_data( $the_content, '/' . ACTIVITYPUB_URL_REGEXP . '/i', array( self::class, 'replace_with_links' ) );
}
/**
* A callback for preg_replace to build the links.
*
* Link shortening https://docs.joinmastodon.org/api/guidelines/#links
*
* @param array $result The preg_match results.
*
* @return string The final string.
*/
public static function replace_with_links( $result ) {
if ( 'www.' === substr( $result[0], 0, 4 ) ) {
$result[0] = 'https://' . $result[0];
}
$parsed_url = \wp_parse_url( html_entity_decode( $result[0] ) );
if ( ! $parsed_url || empty( $parsed_url['host'] ) ) {
return $result[0];
}
if ( empty( $parsed_url['scheme'] ) ) {
$invisible_prefix = 'https://';
} else {
$invisible_prefix = $parsed_url['scheme'] . '://';
}
if ( ! empty( $parsed_url['user'] ) ) {
$invisible_prefix .= $parsed_url['user'];
}
if ( ! empty( $parsed_url['pass'] ) ) {
$invisible_prefix .= ':' . $parsed_url['pass'];
}
if ( ! empty( $parsed_url['user'] ) ) {
$invisible_prefix .= '@';
}
$text_url = $parsed_url['host'];
if ( 'www.' === substr( $text_url, 0, 4 ) ) {
$text_url = substr( $text_url, 4 );
$invisible_prefix .= 'www.';
}
if ( ! empty( $parsed_url['port'] ) ) {
$text_url .= ':' . $parsed_url['port'];
}
if ( ! empty( $parsed_url['path'] ) ) {
$text_url .= $parsed_url['path'];
}
if ( ! empty( $parsed_url['query'] ) ) {
$text_url .= '?' . $parsed_url['query'];
}
if ( ! empty( $parsed_url['fragment'] ) ) {
$text_url .= '#' . $parsed_url['fragment'];
}
$display = \substr( $text_url, 0, 30 );
$invisible_suffix = \substr( $text_url, 30 );
$display_class = '';
if ( $invisible_suffix ) {
$display_class .= 'ellipsis';
}
$rel = apply_filters( 'activitypub_link_rel', 'nofollow noopener noreferrer' );
return \sprintf(
'<a href="%s" target="_blank" rel="%s" translate="no"><span class="invisible">%s</span><span class="%s">%s</span><span class="invisible">%s</span></a>',
esc_url( $result[0] ),
$rel,
esc_html( $invisible_prefix ),
$display_class,
esc_html( $display ),
esc_html( $invisible_suffix )
);
}
}

View File

@ -1,96 +1,68 @@
<?php
/**
* Mention class file.
*
* @package Activitypub
*/
namespace Activitypub;
use WP_Error;
use Activitypub\Webfinger;
use function Activitypub\object_to_uri;
/**
* ActivityPub Mention Class
* ActivityPub Mention Class.
*
* @author Alex Kirk
*/
class Mention {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'the_content', array( self::class, 'the_content' ), 99, 1 );
\add_filter( 'comment_text', array( self::class, 'the_content' ), 10, 1 );
\add_filter( 'activitypub_extra_field_content', array( self::class, 'the_content' ), 10, 1 );
\add_filter( 'activitypub_extract_mentions', array( self::class, 'extract_mentions' ), 99, 2 );
\add_filter( 'activitypub_activity_object_array', array( self::class, 'filter_activity_object' ), 99 );
}
/**
* Filter to replace the mentions in the content with links
* Filter only the activity object and replace summery it with URLs
* add tag to user.
*
* @param string $the_content the post-content
* @param array $object_array Array of activity.
*
* @return string the filtered post-content
* @return array The activity object array.
*/
public static function filter_activity_object( $object_array ) {
if ( ! empty( $object_array['summary'] ) ) {
$object_array['summary'] = self::the_content( $object_array['summary'] );
}
if ( ! empty( $object_array['content'] ) ) {
$object_array['content'] = self::the_content( $object_array['content'] );
}
return $object_array;
}
/**
* Filter to replace the mentions in the content with links.
*
* @param string $the_content The post content.
*
* @return string The filtered post-content.
*/
public static function the_content( $the_content ) {
// small protection against execution timeouts: limit to 1 MB
if ( mb_strlen( $the_content ) > MB_IN_BYTES ) {
return $the_content;
}
$tag_stack = array();
$protected_tags = array(
'pre',
'code',
'textarea',
'style',
'a',
);
$content_with_links = '';
$in_protected_tag = false;
foreach ( wp_html_split( $the_content ) as $chunk ) {
if ( preg_match( '#^<!--[\s\S]*-->$#i', $chunk, $m ) ) {
$content_with_links .= $chunk;
continue;
}
if ( preg_match( '#^<(/)?([a-z-]+)\b[^>]*>$#i', $chunk, $m ) ) {
$tag = strtolower( $m[2] );
if ( '/' === $m[1] ) {
// Closing tag.
$i = array_search( $tag, $tag_stack );
// We can only remove the tag from the stack if it is in the stack.
if ( false !== $i ) {
$tag_stack = array_slice( $tag_stack, 0, $i );
}
} else {
// Opening tag, add it to the stack.
$tag_stack[] = $tag;
}
// If we're in a protected tag, the tag_stack contains at least one protected tag string.
// The protected tag state can only change when we encounter a start or end tag.
$in_protected_tag = array_intersect( $tag_stack, $protected_tags );
// Never inspect tags.
$content_with_links .= $chunk;
continue;
}
if ( $in_protected_tag ) {
// Don't inspect a chunk inside an inspected tag.
$content_with_links .= $chunk;
continue;
}
// Only reachable when there is no protected tag in the stack.
$content_with_links .= \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( self::class, 'replace_with_links' ), $chunk );
}
return $content_with_links;
return enrich_content_data( $the_content, '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( self::class, 'replace_with_links' ) );
}
/**
* A callback for preg_replace to build the user links
* A callback for preg_replace to build the user links.
*
* @param array $result the preg_match results
* @param array $result The preg_match results.
*
* @return string the final string
* @return string The final string.
*/
public static function replace_with_links( $result ) {
$metadata = get_remote_metadata_by_actor( $result[0] );
@ -117,11 +89,11 @@ class Mention {
}
/**
* Get the Inboxes for the mentioned Actors
* Get the Inboxes for the mentioned Actors.
*
* @param array $mentioned The list of Actors that were mentioned
* @param array $mentioned The list of Actors that were mentioned.
*
* @return array The list of Inboxes
* @return array The list of Inboxes.
*/
public static function get_inboxes( $mentioned ) {
$inboxes = array();
@ -138,11 +110,11 @@ class Mention {
}
/**
* Get the inbox from the Remote-Profile of a mentioned Actor
* Get the inbox from the Remote-Profile of a mentioned Actor.
*
* @param string $actor The Actor-URL
* @param string $actor The Actor URL.
*
* @return string The Inbox-URL
* @return string|WP_Error The Inbox-URL or WP_Error if not found.
*/
public static function get_inbox_by_mentioned_actor( $actor ) {
$metadata = get_remote_metadata_by_actor( $actor );
@ -151,7 +123,7 @@ class Mention {
return $metadata;
}
if ( isset( $metadata['endpoints'] ) && isset( $metadata['endpoints']['sharedInbox'] ) ) {
if ( isset( $metadata['endpoints']['sharedInbox'] ) ) {
return $metadata['endpoints']['sharedInbox'];
}
@ -168,7 +140,7 @@ class Mention {
* @param array $mentions The already found mentions.
* @param string $post_content The post content.
*
* @return mixed The discovered mentions.
* @return array The discovered mentions.
*/
public static function extract_mentions( $mentions, $post_content ) {
\preg_match_all( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/i', $post_content, $matches );
@ -178,6 +150,6 @@ class Mention {
$mentions[ $match ] = $link;
}
}
return $mentions;
return \array_unique( $mentions );
}
}

View File

@ -1,8 +1,12 @@
<?php
/**
* Migration class file.
*
* @package Activitypub
*/
namespace Activitypub;
use Activitypub\Activitypub;
use Activitypub\Model\Blog;
use Activitypub\Collection\Followers;
/**
@ -12,7 +16,7 @@ use Activitypub\Collection\Followers;
*/
class Migration {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) );
@ -43,8 +47,6 @@ class Migration {
/**
* Locks the database migration process to prevent simultaneous migrations.
*
* @return void
*/
public static function lock() {
\update_option( 'activitypub_migration_lock', \time() );
@ -52,8 +54,6 @@ class Migration {
/**
* Unlocks the database migration process.
*
* @return void
*/
public static function unlock() {
\delete_option( 'activitypub_migration_lock' );
@ -110,13 +110,13 @@ class Migration {
$version_from_db = self::get_version();
// check for inital migration
// Check for inital migration.
if ( ! $version_from_db ) {
self::add_default_settings();
$version_from_db = self::get_target_version();
}
// schedule the async migration
// Schedule the async migration.
if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) {
\wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) );
}
@ -132,6 +132,9 @@ class Migration {
if ( version_compare( $version_from_db, '2.3.0', '<' ) ) {
self::migrate_from_2_2_0();
}
if ( version_compare( $version_from_db, '3.0.0', '<' ) ) {
self::migrate_from_2_6_0();
}
update_option( 'activitypub_db_version', self::get_target_version() );
@ -151,15 +154,15 @@ class Migration {
/**
* Updates the custom template to use shortcodes instead of the deprecated templates.
*
* @return void
*/
private static function migrate_from_0_16() {
// Get the custom template.
$old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
// If the old content exists but is a blank string, we're going to need a flag to updated it even
// after setting it to the default contents.
/*
* If the old content exists but is a blank string, we're going to need a flag to updated it even
* after setting it to the default contents.
*/
$need_update = false;
// If the old contents is blank, use the defaults.
@ -187,12 +190,10 @@ class Migration {
}
/**
* Updates the DB-schema of the followers-list
*
* @return void
* Updates the DB-schema of the followers-list.
*/
public static function migrate_from_0_17() {
// migrate followers
// Migrate followers.
foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
$followers = get_user_meta( $user_id, 'activitypub_followers', true );
@ -207,9 +208,7 @@ class Migration {
}
/**
* Clear the cache after updating to 1.3.0
*
* @return void
* Clear the cache after updating to 1.3.0.
*/
private static function migrate_from_1_2_0() {
$user_ids = \get_users(
@ -225,9 +224,7 @@ class Migration {
}
/**
* Unschedule Hooks after updating to 2.0.0
*
* @return void
* Unschedule Hooks after updating to 2.0.0.
*/
private static function migrate_from_2_0_0() {
wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
@ -246,42 +243,84 @@ class Migration {
/**
* Add the ActivityPub capability to all users that can publish posts
* Delete old meta to store followers
*
* @return void
* Delete old meta to store followers.
*/
private static function migrate_from_2_2_0() {
// add the ActivityPub capability to all users that can publish posts
// Add the ActivityPub capability to all users that can publish posts.
self::add_activitypub_capability();
}
/**
* Set the defaults needed for the plugin to work
* Rename DB fields.
*/
private static function migrate_from_2_6_0() {
wp_cache_flush();
self::update_usermeta_key( 'activitypub_user_description', 'activitypub_description' );
self::update_options_key( 'activitypub_blog_user_description', 'activitypub_blog_description' );
self::update_options_key( 'activitypub_blog_user_identifier', 'activitypub_blog_identifier' );
}
/**
* Set the defaults needed for the plugin to work.
*
* * Add the ActivityPub capability to all users that can publish posts
*
* @return void
* Add the ActivityPub capability to all users that can publish posts.
*/
public static function add_default_settings() {
self::add_activitypub_capability();
}
/**
* Add the ActivityPub capability to all users that can publish posts
*
* @return void
* Add the ActivityPub capability to all users that can publish posts.
*/
private static function add_activitypub_capability() {
// get all WP_User objects that can publish posts
// Get all WP_User objects that can publish posts.
$users = \get_users(
array(
'capability__in' => array( 'publish_posts' ),
)
);
// add ActivityPub capability to all users that can publish posts
// Add ActivityPub capability to all users that can publish posts.
foreach ( $users as $user ) {
$user->add_cap( 'activitypub' );
}
}
/**
* Rename meta keys.
*
* @param string $old_key The old comment meta key.
* @param string $new_key The new comment meta key.
*/
private static function update_usermeta_key( $old_key, $new_key ) {
global $wpdb;
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->usermeta,
array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
array( '%s' ),
array( '%s' )
);
}
/**
* Rename option keys.
*
* @param string $old_key The old option key.
* @param string $new_key The new option key.
*/
private static function update_options_key( $old_key, $new_key ) {
global $wpdb;
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
$wpdb->options,
array( 'option_name' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
array( 'option_name' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
array( '%s' ),
array( '%s' )
);
}
}

View File

@ -1,4 +1,9 @@
<?php
/**
* Notification file.
*
* @package Activitypub
*/
namespace Activitypub;
@ -39,13 +44,13 @@ class Notification {
*
* @param string $type The type of the notification.
* @param string $actor The actor URL.
* @param array $object The Activity object.
* @param array $activity The Activity object.
* @param int $target The WordPress User-Id.
*/
public function __construct( $type, $actor, $object, $target ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
public function __construct( $type, $actor, $activity, $target ) {
$this->type = $type;
$this->actor = $actor;
$this->object = $object;
$this->object = $activity;
$this->target = $target;
}
@ -53,6 +58,9 @@ class Notification {
* Send the notification.
*/
public function send() {
$type = \strtolower( $this->type );
do_action( 'activitypub_notification', $this );
do_action( "activitypub_notification_{$type}", $this );
}
}

View File

@ -1,28 +1,26 @@
<?php
/**
* Scheduler class file.
*
* @package Activitypub
*/
namespace Activitypub;
use Activitypub\Transformer\Post;
use Activitypub\Collection\Users;
use Activitypub\Collection\Followers;
use function Activitypub\was_comment_sent;
use function Activitypub\is_user_type_disabled;
use function Activitypub\should_comment_be_federated;
use function Activitypub\get_remote_metadata_by_actor;
/**
* ActivityPub Scheduler Class
* Scheduler class.
*
* @author Matthias Pfefferle
*/
class Scheduler {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
// Post transitions
// Post transitions.
\add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
\add_action(
'edit_attachment',
@ -44,7 +42,7 @@ class Scheduler {
);
if ( ! ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS ) {
// Comment transitions
// Comment transitions.
\add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 );
\add_action(
'edit_comment',
@ -60,11 +58,11 @@ class Scheduler {
);
}
// Follower Cleanups
// 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
// 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' ) );
@ -73,7 +71,7 @@ class Scheduler {
\add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) );
}
// profile updates for user options
// 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 );
@ -83,8 +81,6 @@ class Scheduler {
/**
* Schedule all ActivityPub schedules.
*
* @return void
*/
public static function register_schedules() {
if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) {
@ -97,7 +93,7 @@ class Scheduler {
}
/**
* Unscedule all ActivityPub schedules.
* Un-schedule all ActivityPub schedules.
*
* @return void
*/
@ -112,16 +108,25 @@ class Scheduler {
*
* @param string $new_status New post status.
* @param string $old_status Old post status.
* @param WP_Post $post Post object.
* @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;
@ -142,8 +147,9 @@ class Scheduler {
$type = 'Create';
} elseif (
'publish' === $new_status ||
// We want to send updates for posts that are published and then moved to draft.
( 'draft' === $new_status &&
'draft' !== $old_status )
'publish' === $old_status )
) {
$type = 'Update';
} elseif ( 'trash' === $new_status ) {
@ -164,19 +170,19 @@ class Scheduler {
}
/**
* Schedule Comment Activities
* Schedule Comment Activities.
*
* transition_comment_status()
* @see transition_comment_status()
*
* @param string $new_status New comment status.
* @param string $old_status Old comment status.
* @param WP_Comment $comment Comment object.
* @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->user_id ) {
// Federate only comments that are written by a registered user.
if ( ! $comment || ! $comment->user_id ) {
return;
}
@ -201,7 +207,7 @@ class Scheduler {
return;
}
// check if comment should be federated or not
// Check if comment should be federated or not.
if ( ! should_comment_be_federated( $comment ) ) {
return;
}
@ -216,9 +222,7 @@ class Scheduler {
}
/**
* Update followers
*
* @return void
* Update followers.
*/
public static function update_followers() {
$number = 5;
@ -227,6 +231,11 @@ class Scheduler {
$number = 50;
}
/**
* Filter the number of followers to update.
*
* @param int $number The number of followers to update.
*/
$number = apply_filters( 'activitypub_update_followers_number', $number );
$followers = Followers::get_outdated_followers( $number );
@ -243,9 +252,7 @@ class Scheduler {
}
/**
* Cleanup followers
*
* @return void
* Cleanup followers.
*/
public static function cleanup_followers() {
$number = 5;
@ -254,6 +261,11 @@ class Scheduler {
$number = 50;
}
/**
* Filter the number of followers to clean up.
*
* @param int $number The number of followers to clean up.
*/
$number = apply_filters( 'activitypub_update_followers_number', $number );
$followers = Followers::get_faulty_followers( $number );
@ -285,17 +297,17 @@ class Scheduler {
* @param int $meta_id Meta ID being updated.
* @param int $user_id User ID being updated.
* @param string $meta_key Meta key being updated.
*
* @return void
*/
public static function user_meta_update( $meta_id, $user_id, $meta_key ) {
// don't bother if the user can't publish
// Don't bother if the user can't publish.
if ( ! \user_can( $user_id, 'activitypub' ) ) {
return;
}
// the user meta fields that affect a profile.
// The user meta fields that affect a profile.
$fields = array(
'activitypub_user_description',
'activitypub_description',
'activitypub_header_image',
'description',
'user_url',
'display_name',
@ -309,11 +321,9 @@ class Scheduler {
* Send a profile update when a user is updated.
*
* @param int $user_id User ID being updated.
*
* @return void
*/
public static function user_update( $user_id ) {
// don't bother if the user can't publish
// Don't bother if the user can't publish.
if ( ! \user_can( $user_id, 'activitypub' ) ) {
return;
}
@ -324,7 +334,7 @@ class Scheduler {
/**
* Theme mods only have a dynamic filter so we fudge it like this.
*
* @param mixed $value
* @param mixed $value Optional. The value to be updated. Default null.
*
* @return mixed
*/

View File

@ -1,11 +1,18 @@
<?php
/**
* Shortcodes class file.
*
* @package Activitypub
*/
namespace Activitypub;
use function Activitypub\esc_hashtag;
/**
* Shortcodes class.
*/
class Shortcodes {
/**
* Register the shortcodes
* Register the shortcodes.
*/
public static function register() {
foreach ( get_class_methods( self::class ) as $shortcode ) {
@ -16,7 +23,7 @@ class Shortcodes {
}
/**
* Unregister the shortcodes
* Unregister the shortcodes.
*/
public static function unregister() {
foreach ( get_class_methods( self::class ) as $shortcode ) {
@ -27,15 +34,11 @@ class Shortcodes {
}
/**
* Generates output for the 'ap_hashtags' shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
* Generates output for the 'ap_hashtags' shortcode.
*
* @return string The post tags as hashtags.
*/
public static function hashtags( $atts, $content, $tag ) {
public static function hashtags() {
$item = self::get_item();
if ( ! $item ) {
@ -64,13 +67,9 @@ 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( $atts, $content, $tag ) {
public static function title() {
$item = self::get_item();
if ( ! $item ) {
@ -108,87 +107,13 @@ class Shortcodes {
$excerpt_length = ACTIVITYPUB_EXCERPT_LENGTH;
}
$excerpt = \get_post_field( 'post_excerpt', $item );
if ( 'attachment' === $item->post_type ) {
// get title of attachment with fallback to alt text.
$content = wp_get_attachment_caption( $item->ID );
if ( empty( $content ) ) {
$content = get_post_meta( $item->ID, '_wp_attachment_image_alt', true );
}
} elseif ( '' === $excerpt ) {
$content = \get_post_field( 'post_content', $item );
// An empty string will make wp_trim_excerpt do stuff we do not want.
if ( '' !== $content ) {
$excerpt = \strip_shortcodes( $content );
/** This filter is documented in wp-includes/post-template.php */
$excerpt = \apply_filters( 'the_content', $excerpt );
$excerpt = \str_replace( ']]>', ']]&gt;', $excerpt );
}
}
// Strip out any remaining tags.
$excerpt = \wp_strip_all_tags( $excerpt );
$excerpt_more = \apply_filters( 'activitypub_excerpt_more', ' [&hellip;]' );
$excerpt_more_len = strlen( $excerpt_more );
// We now have a excerpt, but we need to check it's length, it may be longer than we want for two reasons:
//
// * The user has entered a manual excerpt which is longer that what we want.
// * No manual excerpt exists so we've used the content which might be longer than we want.
//
// Either way, let's trim it up if we need too. Also, don't forget to take into account the more indicator
// as part of the total length.
//
// Setup a variable to hold the current excerpts length.
$current_excerpt_length = strlen( $excerpt );
// Setup a variable to keep track of our target length.
$target_excerpt_length = $excerpt_length - $excerpt_more_len;
// Setup a variable to keep track of the current max length.
$current_excerpt_max = $target_excerpt_length;
// This is a loop since we can't calculate word break the string after 'the_excpert' filter has run (we would break
// all kinds of html tags), so we have to cut the excerpt down a bit at a time until we hit our target length.
while ( $current_excerpt_length > $target_excerpt_length && $current_excerpt_max > 0 ) {
// Trim the excerpt based on wordwrap() positioning.
// Note: we're using <br> as the linebreak just in case there are any newlines existing in the excerpt from the user.
// There won't be any <br> left after we've run wp_strip_all_tags() in the code above, so they're
// safe to use here. It won't be included in the final excerpt as the substr() will trim it off.
$excerpt = substr( $excerpt, 0, strpos( wordwrap( $excerpt, $current_excerpt_max, '<br>' ), '<br>' ) );
// If something went wrong, or we're in a language that wordwrap() doesn't understand,
// just chop it off and don't worry about breaking in the middle of a word.
if ( strlen( $excerpt ) > $excerpt_length - $excerpt_more_len ) {
$excerpt = substr( $excerpt, 0, $current_excerpt_max );
}
// Add in the more indicator.
$excerpt = $excerpt . $excerpt_more;
// Run it through the excerpt filter which will add some html tags back in.
$excerpt_filtered = apply_filters( 'the_excerpt', $excerpt );
// Now set the current excerpt length to this new filtered length.
$current_excerpt_length = strlen( $excerpt_filtered );
// Check to see if we're over the target length.
if ( $current_excerpt_length > $target_excerpt_length ) {
// If so, remove 20 characters from the current max and run the loop again.
$current_excerpt_max = $current_excerpt_max - 20;
}
}
$excerpt = generate_post_summary( $item, $excerpt_length );
return \apply_filters( 'the_excerpt', $excerpt );
}
/**
* Generates output for the 'ap_content' Shortcode
* Generates output for the 'ap_content' Shortcode.
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
@ -203,7 +128,7 @@ class Shortcodes {
return '';
}
// prevent inception
// Prevent inception.
remove_shortcode( 'ap_content' );
$atts = shortcode_atts(
@ -215,7 +140,7 @@ class Shortcodes {
$content = '';
if ( 'attachment' === $item->post_type ) {
// get title of attachment with fallback to alt text.
// Get title of attachment with fallback to alt text.
$content = wp_get_attachment_caption( $item->ID );
if ( empty( $content ) ) {
$content = get_post_meta( $item->ID, '_wp_attachment_image_alt', true );
@ -231,7 +156,7 @@ class Shortcodes {
$content = wp_filter_content_tags( $content );
}
// replace script and style elements
// Replace script and style elements.
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
$content = strip_shortcodes( $content );
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
@ -243,7 +168,7 @@ class Shortcodes {
}
/**
* Generates output for the 'ap_permalink' Shortcode
* Generates output for the 'ap_permalink' Shortcode.
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
@ -277,7 +202,7 @@ class Shortcodes {
}
/**
* Generates output for the 'ap_shortlink' Shortcode
* Generates output for the 'ap_shortlink' Shortcode.
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
@ -311,7 +236,7 @@ class Shortcodes {
}
/**
* Generates output for the 'ap_image' Shortcode
* Generates output for the 'ap_image' Shortcode.
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
@ -354,15 +279,11 @@ class Shortcodes {
}
/**
* Generates output for the 'ap_hashcats' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
* Generates output for the 'ap_hashcats' Shortcode.
*
* @return string The post categories as hashtags.
*/
public static function hashcats( $atts, $content, $tag ) {
public static function hashcats() {
$item = self::get_item();
if ( ! $item ) {
@ -389,15 +310,11 @@ class Shortcodes {
}
/**
* Generates output for the 'ap_author' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
* Generates output for the 'ap_author' Shortcode.
*
* @return string The author name.
*/
public static function author( $atts, $content, $tag ) {
public static function author() {
$item = self::get_item();
if ( ! $item ) {
@ -415,15 +332,11 @@ class Shortcodes {
}
/**
* Generates output for the 'ap_authorurl' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
* Generates output for the 'ap_authorurl' Shortcode.
*
* @return string The author URL.
*/
public static function authorurl( $atts, $content, $tag ) {
public static function authorurl() {
$item = self::get_item();
if ( ! $item ) {
@ -441,54 +354,38 @@ class Shortcodes {
}
/**
* Generates output for the 'ap_blogurl' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
* Generates output for the 'ap_blogurl' Shortcode.
*
* @return string The site URL.
*/
public static function blogurl( $atts, $content, $tag ) {
public static function blogurl() {
return \esc_url( \get_bloginfo( 'url' ) );
}
/**
* Generates output for the 'ap_blogname' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
* Generates output for the 'ap_blogname' Shortcode.
*
* @return string
*/
public static function blogname( $atts, $content, $tag ) {
public static function blogname() {
return \wp_strip_all_tags( \get_bloginfo( 'name' ) );
}
/**
* Generates output for the 'ap_blogdesc' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
* Generates output for the 'ap_blogdesc' Shortcode.
*
* @return string The site description.
*/
public static function blogdesc( $atts, $content, $tag ) {
public static function blogdesc() {
return \wp_strip_all_tags( \get_bloginfo( 'description' ) );
}
/**
* Generates output for the 'ap_date' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
* Generates output for the 'ap_date' Shortcode.
*
* @return string The post date.
*/
public static function date( $atts, $content, $tag ) {
public static function date() {
$item = self::get_item();
if ( ! $item ) {
@ -497,7 +394,6 @@ class Shortcodes {
$datetime = \get_post_datetime( $item );
$dateformat = \get_option( 'date_format' );
$timeformat = \get_option( 'time_format' );
$date = $datetime->format( $dateformat );
@ -509,15 +405,11 @@ class Shortcodes {
}
/**
* Generates output for the 'ap_time' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
* Generates output for the 'ap_time' Shortcode.
*
* @return string The post time.
*/
public static function time( $atts, $content, $tag ) {
public static function time() {
$item = self::get_item();
if ( ! $item ) {
@ -525,7 +417,6 @@ class Shortcodes {
}
$datetime = \get_post_datetime( $item );
$dateformat = \get_option( 'date_format' );
$timeformat = \get_option( 'time_format' );
$date = $datetime->format( $timeformat );
@ -538,15 +429,11 @@ class Shortcodes {
}
/**
* Generates output for the 'ap_datetime' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
* Generates output for the 'ap_datetime' Shortcode.
*
* @return string The post date/time.
*/
public static function datetime( $atts, $content, $tag ) {
public static function datetime() {
$item = self::get_item();
if ( ! $item ) {
@ -572,7 +459,7 @@ class Shortcodes {
* Checks if item (WP_Post) is "public", a supported post type
* and not password protected.
*
* @return null|WP_Post The WordPress item.
* @return null|\WP_Post The WordPress item.
*/
protected static function get_item() {
$post = \get_post();

View File

@ -1,4 +1,10 @@
<?php
/**
* Signature class file.
*
* @package Activitypub
*/
namespace Activitypub;
use WP_Error;
@ -8,7 +14,7 @@ use WP_REST_Request;
use Activitypub\Collection\Users;
/**
* ActivityPub Signature Class
* ActivityPub Signature Class.
*
* @author Matthias Pfefferle
* @author Django Doucet
@ -19,7 +25,7 @@ class Signature {
* Return the public key for a given user.
*
* @param int $user_id The WordPress User ID.
* @param bool $force Force the generation of a new key pair.
* @param bool $force Optional. Force the generation of a new key pair. Default false.
*
* @return mixed The public key.
*/
@ -37,7 +43,7 @@ class Signature {
* Return the private key for a given user.
*
* @param int $user_id The WordPress User ID.
* @param bool $force Force the generation of a new key pair.
* @param bool $force Optional. Force the generation of a new key pair. Default false.
*
* @return mixed The private key.
*/
@ -94,12 +100,14 @@ class Signature {
$key = \openssl_pkey_new( $config );
$priv_key = null;
$detail = array();
if ( $key ) {
\openssl_pkey_export( $key, $priv_key );
$detail = \openssl_pkey_get_details( $key );
}
// check if keys are valid
// Check if keys are valid.
if (
empty( $priv_key ) || ! is_string( $priv_key ) ||
! isset( $detail['key'] ) || ! is_string( $detail['key'] )
@ -115,7 +123,7 @@ class Signature {
'public_key' => $detail['key'],
);
// persist keys
// Persist keys.
\add_option( $option_key, $key_pair );
return $key_pair;
@ -133,7 +141,7 @@ class Signature {
if ( $user_id > 0 ) {
$user = \get_userdata( $user_id );
// sanatize username because it could include spaces and special chars
// Sanitize username because it could include spaces and special chars.
$id = sanitize_title( $user->user_login );
}
@ -174,13 +182,13 @@ class Signature {
}
/**
* Generates the Signature for a HTTP Request
* Generates the Signature for an HTTP Request.
*
* @param int $user_id The WordPress User ID.
* @param string $http_method The HTTP method.
* @param string $url The URL to send the request to.
* @param string $date The date the request is sent.
* @param string $digest The digest of the request body.
* @param string $digest Optional. The digest of the request body. Default null.
*
* @return string The signature.
*/
@ -193,12 +201,12 @@ class Signature {
$host = $url_parts['host'];
$path = '/';
// add path
// Add path.
if ( ! empty( $url_parts['path'] ) ) {
$path = $url_parts['path'];
}
// add query
// Add query.
if ( ! empty( $url_parts['query'] ) ) {
$path .= '?' . $url_parts['query'];
}
@ -213,7 +221,7 @@ class Signature {
$signature = null;
\openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 );
$signature = \base64_encode( $signature ); // phpcs:ignore
$signature = \base64_encode( $signature ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
$key_id = $user->get_url() . '#main-key';
@ -229,18 +237,18 @@ class Signature {
*
* @param WP_REST_Request|array $request The request object or $_SERVER array.
*
* @return mixed A boolean or WP_Error.
* @return bool|WP_Error A boolean or WP_Error.
*/
public static function verify_http_signature( $request ) {
if ( is_object( $request ) ) { // REST Request object
// check if route starts with "index.php"
if ( is_object( $request ) ) { // REST Request object.
// Check if route starts with "index.php".
if ( str_starts_with( $request->get_route(), '/index.php' ) || ! rest_get_url_prefix() ) {
$route = $request->get_route();
} else {
$route = '/' . rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' );
}
// fix route for subdirectory installs
// Fix route for subdirectory installs.
$path = \wp_parse_url( \get_home_url(), PHP_URL_PATH );
if ( \is_string( $path ) ) {
@ -321,14 +329,14 @@ class Signature {
}
/**
* Get public key from key_id
* Get public key from key_id.
*
* @param string $key_id The URL to the public key.
*
* @return WP_Error|string The public key or WP_Error.
*/
public static function get_remote_key( $key_id ) { // phpcs:ignore
$actor = get_remote_metadata_by_actor( strip_fragment_from_url( $key_id ) ); // phpcs:ignore
public static function get_remote_key( $key_id ) {
$actor = get_remote_metadata_by_actor( strip_fragment_from_url( $key_id ) );
if ( \is_wp_error( $actor ) ) {
return new WP_Error(
'activitypub_no_remote_profile_found',
@ -337,7 +345,7 @@ class Signature {
);
}
if ( isset( $actor['publicKey']['publicKeyPem'] ) ) {
return \rtrim( $actor['publicKey']['publicKeyPem'] ); // phpcs:ignore
return \rtrim( $actor['publicKey']['publicKeyPem'] );
}
return new WP_Error(
'activitypub_no_remote_key_found',
@ -347,9 +355,9 @@ class Signature {
}
/**
* Gets the signature algorithm from the signature header
* Gets the signature algorithm from the signature header.
*
* @param array $signature_block
* @param array $signature_block The signature block.
*
* @return string The signature algorithm.
*/
@ -357,7 +365,7 @@ class Signature {
if ( $signature_block['algorithm'] ) {
switch ( $signature_block['algorithm'] ) {
case 'rsa-sha-512':
return 'sha512'; //hs2019 https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
return 'sha512'; // hs2019 https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12.
default:
return 'sha256';
}
@ -366,11 +374,11 @@ class Signature {
}
/**
* Parses the Signature header
* Parses the Signature header.
*
* @param string $signature The signature header.
*
* @return array signature parts
* @return array Signature parts.
*/
public static function parse_signature_header( $signature ) {
$parsed_header = array();
@ -392,7 +400,7 @@ class Signature {
$parsed_header['headers'] = \explode( ' ', trim( $matches[1] ) );
}
if ( \preg_match( '/signature="(.*?)"/ism', $signature, $matches ) ) {
$parsed_header['signature'] = \base64_decode( preg_replace( '/\s+/', '', trim( $matches[1] ) ) ); // phpcs:ignore
$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'] ) ) {
@ -403,16 +411,17 @@ class Signature {
}
/**
* Gets the header data from the included pseudo headers
* Gets the header data from the included pseudo headers.
*
* @param array $signed_headers The signed headers.
* @param array $signature_block (pseudo-headers)
* @param array $headers (http headers)
* @param array $signature_block The signature block.
* @param array $headers The HTTP headers.
*
* @return string signed headers for comparison
*/
public static function get_signed_data( $signed_headers, $signature_block, $headers ) {
$signed_data = '';
// This also verifies time-based values by returning false if any of these are out of range.
foreach ( $signed_headers as $header ) {
if ( 'host' === $header ) {
@ -431,7 +440,7 @@ class Signature {
}
if ( '(created)' === $header ) {
if ( ! empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) {
// created in future
// Created in the future.
return false;
}
@ -442,7 +451,7 @@ class Signature {
}
if ( '(expires)' === $header ) {
if ( ! empty( $signature_block['(expires)'] ) && \intval( $signature_block['(expires)'] ) < \time() ) {
// expired in past
// Expired in the past.
return false;
}
@ -452,7 +461,7 @@ class Signature {
}
}
if ( 'date' === $header ) {
// allow a bit of leeway for misconfigured clocks.
// Allow a bit of leeway for misconfigured clocks.
$d = new DateTime( $headers[ $header ][0] );
$d->setTimeZone( new DateTimeZone( 'UTC' ) );
$c = $d->format( 'U' );
@ -461,7 +470,7 @@ class Signature {
$dminus = time() - ( 3 * HOUR_IN_SECONDS );
if ( $c > $dplus || $c < $dminus ) {
// time out of range
// Time out of range.
return false;
}
}
@ -471,22 +480,22 @@ class Signature {
}
/**
* Generates the digest for a HTTP Request
* Generates the digest for an HTTP Request.
*
* @param string $body The body of the request.
*
* @return string The digest.
*/
public static function generate_digest( $body ) {
$digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore
$digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
return "SHA-256=$digest";
}
/**
* Formats the $_SERVER to resemble the WP_REST_REQUEST array,
* for use with verify_http_signature()
* for use with verify_http_signature().
*
* @param array $_SERVER The $_SERVER array.
* @param array $server The $_SERVER array.
*
* @return array $request The formatted request array.
*/

View File

@ -1,11 +1,17 @@
<?php
/**
* WebFinger class file.
*
* @package Activitypub
*/
namespace Activitypub;
use WP_Error;
use Activitypub\Collection\Users;
/**
* ActivityPub WebFinger Class
* ActivityPub WebFinger Class.
*
* @author Matthias Pfefferle
*
@ -13,11 +19,11 @@ use Activitypub\Collection\Users;
*/
class Webfinger {
/**
* Returns a users WebFinger "resource"
* Returns a users WebFinger "resource".
*
* @param int $user_id The WordPress user id
* @param int $user_id The WordPress user id.
*
* @return string The user-resource
* @return string The user-resource.
*/
public static function get_user_resource( $user_id ) {
$user = Users::get_by_id( $user_id );
@ -29,11 +35,11 @@ class Webfinger {
}
/**
* Resolve a WebFinger resource
* Resolve a WebFinger resource.
*
* @param string $uri The WebFinger Resource
* @param string $uri The WebFinger Resource.
*
* @return string|WP_Error The URL or WP_Error
* @return string|WP_Error The URL or WP_Error.
*/
public static function resolve( $uri ) {
$data = self::get_data( $uri );
@ -76,11 +82,11 @@ class Webfinger {
}
/**
* Transform a URI to an acct <identifier>@<host>
* Transform a URI to an acct <identifier>@<host>.
*
* @param string $uri The URI (acct:, mailto:, http:, https:)
* @param string $uri The URI (acct:, mailto:, http:, https:).
*
* @return string|WP_Error Error or acct URI
* @return string|WP_Error Error or acct URI.
*/
public static function uri_to_acct( $uri ) {
$data = self::get_data( $uri );
@ -89,7 +95,7 @@ class Webfinger {
return $data;
}
// check if subject is an acct URI
// Check if subject is an acct URI.
if (
isset( $data['subject'] ) &&
\str_starts_with( $data['subject'], 'acct:' )
@ -97,7 +103,7 @@ class Webfinger {
return $data['subject'];
}
// search for an acct URI in the aliases
// Search for an acct URI in the aliases.
if ( isset( $data['aliases'] ) ) {
foreach ( $data['aliases'] as $alias ) {
if ( \str_starts_with( $alias, 'acct:' ) ) {
@ -120,10 +126,9 @@ class Webfinger {
* Convert a URI string to an identifier and its host.
* Automatically adds acct: if it's missing.
*
* @param string $url The URI (acct:, mailto:, http:, https:)
* @param string $url The URI (acct:, mailto:, http:, https:).
*
* @return WP_Error|array Error reaction or array with
* identifier and host as values
* @return WP_Error|array Error reaction or array with identifier and host as values.
*/
public static function get_identifier_and_host( $url ) {
if ( ! $url ) {
@ -137,7 +142,7 @@ class Webfinger {
);
}
// remove leading @
// Remove leading @.
$url = ltrim( $url, '@' );
if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
@ -178,12 +183,11 @@ class Webfinger {
}
/**
* Get the WebFinger data for a given URI
* Get the WebFinger data for a given URI.
*
* @param string $uri The Identifier: <identifier>@<host> or URI
* @param string $uri The Identifier: <identifier>@<host> or URI.
*
* @return WP_Error|array Error reaction or array with
* identifier and host as values
* @return WP_Error|array Error reaction or array with identifier and host as values.
*/
public static function get_data( $uri ) {
$identifier_and_host = self::get_identifier_and_host( $uri );
@ -201,7 +205,11 @@ class Webfinger {
return $data;
}
$webfinger_url = sprintf( 'https://%s/.well-known/webfinger?resource=%s', $host, rawurlencode( $identifier ) );
$webfinger_url = sprintf(
'https://%s/.well-known/webfinger?resource=%s',
$host,
rawurlencode( $identifier )
);
$response = wp_safe_remote_get(
$webfinger_url,
@ -230,7 +238,9 @@ class Webfinger {
}
/**
* Get the Remote-Follow endpoint for a given URI
* Get the Remote-Follow endpoint for a given URI.
*
* @param string $uri The WebFinger Resource URI.
*
* @return string|WP_Error Error or the Remote-Follow endpoint URI.
*/
@ -269,11 +279,11 @@ class Webfinger {
}
/**
* Generate a cache key for a given URI
* Generate a cache key for a given URI.
*
* @param string $uri A WebFinger Resource URI
* @param string $uri A WebFinger Resource URI.
*
* @return string The cache key
* @return string The cache key.
*/
public static function generate_cache_key( $uri ) {
$uri = ltrim( $uri, '@' );

View File

@ -0,0 +1,283 @@
<?php
/**
* Extra Fields collection file.
*
* @package Activitypub
*/
namespace Activitypub\Collection;
use Activitypub\Link;
use function Activitypub\site_supports_blocks;
/**
* Extra Fields collection.
*/
class Extra_Fields {
const USER_POST_TYPE = 'ap_extrafield';
const BLOG_POST_TYPE = 'ap_extrafield_blog';
/**
* Get the extra fields for a user.
*
* @param int $user_id The user ID.
*
* @return \WP_Post[] The extra fields.
*/
public static function get_actor_fields( $user_id ) {
$is_blog = self::is_blog( $user_id );
$post_type = $is_blog ? self::BLOG_POST_TYPE : self::USER_POST_TYPE;
$args = array(
'post_type' => $post_type,
'nopaging' => true,
'orderby' => 'menu_order',
'order' => 'ASC',
);
if ( ! $is_blog ) {
$args['author'] = $user_id;
}
$query = new \WP_Query( $args );
$fields = $query->posts ?? array();
return apply_filters( 'activitypub_get_actor_extra_fields', $fields, $user_id );
}
/**
* Get formatted content for an extra field.
*
* @param \WP_Post $post The post.
*
* @return string The formatted content.
*/
public static function get_formatted_content( $post ) {
$content = \get_the_content( null, false, $post );
$content = Link::the_content( $content, true );
if ( site_supports_blocks() ) {
$content = \do_blocks( $content );
}
$content = \wptexturize( $content );
$content = \wp_filter_content_tags( $content );
// Replace script and style elements.
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
$content = \strip_shortcodes( $content );
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
/**
* Filters the content of an extra field.
*
* @param string $content The content.
* @param \WP_Post $post The post.
*/
return \apply_filters( 'activitypub_extra_field_content', $content, $post );
}
/**
* Transforms the Extra Fields (Custom Post Types) to ActivityPub Actor-Attachments.
*
* @param \WP_Post[] $fields The extra fields.
*
* @return array ActivityPub attachments.
*/
public static function fields_to_attachments( $fields ) {
$attachments = array();
\add_filter(
'activitypub_link_rel',
function ( $rel ) {
$rel .= ' me';
return $rel;
}
);
foreach ( $fields as $post ) {
$content = self::get_formatted_content( $post );
$attachments[] = array(
'type' => 'PropertyValue',
'name' => \get_the_title( $post ),
'value' => \html_entity_decode(
$content,
\ENT_QUOTES,
'UTF-8'
),
);
$link_added = false;
// Add support for FEP-fb2a, for more information see FEDERATION.md.
$link_content = \trim( \strip_tags( $content, '<a>' ) );
if (
\stripos( $link_content, '<a' ) === 0 &&
\stripos( $link_content, '<a', 3 ) === false &&
\stripos( $link_content, '</a>', \strlen( $link_content ) - 4 ) !== false &&
\class_exists( '\WP_HTML_Tag_Processor' )
) {
$tags = new \WP_HTML_Tag_Processor( $link_content );
$tags->next_tag( 'A' );
if ( 'A' === $tags->get_tag() ) {
$attachment = array(
'type' => 'Link',
'name' => \get_the_title( $post ),
'href' => \esc_url( $tags->get_attribute( 'href' ) ),
'rel' => explode( ' ', $tags->get_attribute( 'rel' ) ),
);
$link_added = true;
}
}
if ( ! $link_added ) {
$attachment = array(
'type' => 'Note',
'name' => \get_the_title( $post ),
'content' => \html_entity_decode(
$content,
\ENT_QUOTES,
'UTF-8'
),
);
}
$attachments[] = $attachment;
}
return $attachments;
}
/**
* Check if a post type is an extra fields post type.
*
* @param string $post_type The post type.
*
* @return bool True if the post type is an extra fields post type, otherwise false.
*/
public static function is_extra_fields_post_type( $post_type ) {
return \in_array( $post_type, array( self::USER_POST_TYPE, self::BLOG_POST_TYPE ), true );
}
/**
* Check if a post type is the `ap_extrafield` post type.
*
* @param string $post_type The post type.
*
* @return bool True if the post type is `ap_extrafield`, otherwise false.
*/
public static function is_extra_field_post_type( $post_type ) {
return self::USER_POST_TYPE === $post_type;
}
/**
* Check if a post type is the `ap_extrafield_blog` post type.
*
* @param string $post_type The post type.
*
* @return bool True if the post type is `ap_extrafield_blog`, otherwise false.
*/
public static function is_extra_field_blog_post_type( $post_type ) {
return self::BLOG_POST_TYPE === $post_type;
}
/**
* Add default extra fields to an actor.
*
* @param array $extra_fields The extra fields.
* @param int $user_id The User-ID.
*
* @return array The extra fields.
*/
public static function default_actor_extra_fields( $extra_fields, $user_id ) {
// We'll only take action when there are none yet.
if ( ! empty( $extra_fields ) ) {
return $extra_fields;
}
$is_blog = self::is_blog( $user_id );
$already_migrated = $is_blog
? \get_option( 'activitypub_default_extra_fields' )
: \get_user_meta( $user_id, 'activitypub_default_extra_fields', true );
if ( $already_migrated ) {
return $extra_fields;
}
\add_filter(
'activitypub_link_rel',
function ( $rel ) {
$rel .= ' me';
return $rel;
}
);
$defaults = array(
\__( 'Blog', 'activitypub' ) => \home_url( '/' ),
);
if ( ! $is_blog ) {
$author_url = \get_the_author_meta( 'user_url', $user_id );
$author_posts_url = \get_author_posts_url( $user_id );
$defaults[ \__( 'Profile', 'activitypub' ) ] = $author_posts_url;
if ( $author_url !== $author_posts_url ) {
$defaults[ \__( 'Homepage', 'activitypub' ) ] = $author_url;
}
}
$post_type = $is_blog ? self::BLOG_POST_TYPE : self::USER_POST_TYPE;
$menu_order = 10;
foreach ( $defaults as $title => $url ) {
if ( ! $url ) {
continue;
}
$extra_field = array(
'post_type' => $post_type,
'post_title' => $title,
'post_status' => 'publish',
'post_author' => $user_id,
'post_content' => self::make_paragraph_block( Link::the_content( $url ) ),
'comment_status' => 'closed',
'menu_order' => $menu_order,
);
$menu_order += 10;
$extra_field_id = wp_insert_post( $extra_field );
$extra_fields[] = get_post( $extra_field_id );
}
$is_blog
? \update_option( 'activitypub_default_extra_fields', true )
: \update_user_meta( $user_id, 'activitypub_default_extra_fields', true );
return $extra_fields;
}
/**
* Create a paragraph block.
*
* @param string $content The content.
*
* @return string The paragraph block.
*/
public static function make_paragraph_block( $content ) {
if ( ! site_supports_blocks() ) {
return $content;
}
return '<!-- wp:paragraph --><p>' . $content . '</p><!-- /wp:paragraph -->';
}
/**
* Checks if the user is the blog user.
*
* @param int $user_id The user ID.
* @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;
}
}

View File

@ -1,17 +1,21 @@
<?php
/**
* Followers collection file.
*
* @package Activitypub
*/
namespace Activitypub\Collection;
use WP_Error;
use WP_Query;
use Activitypub\Http;
use Activitypub\Webfinger;
use Activitypub\Model\Follower;
use function Activitypub\is_tombstone;
use function Activitypub\get_remote_metadata_by_actor;
/**
* ActivityPub Followers Collection
* ActivityPub Followers Collection.
*
* @author Matt Wiebe
* @author Matthias Pfefferle
@ -21,12 +25,12 @@ class Followers {
const CACHE_KEY_INBOXES = 'follower_inboxes_%s';
/**
* Add new Follower
* Add new Follower.
*
* @param int $user_id The ID of the WordPress User
* @param string $actor The Actor URL
* @param int $user_id The ID of the WordPress User.
* @param string $actor The Actor URL.
*
* @return array|WP_Error The Follower (WP_Post array) or an WP_Error
* @return Follower|WP_Error The Follower (WP_Post array) or an WP_Error.
*/
public static function add_follower( $user_id, $actor ) {
$meta = get_remote_metadata_by_actor( $actor );
@ -48,7 +52,7 @@ class Followers {
return $id;
}
$post_meta = get_post_meta( $id, 'activitypub_user_id' );
$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 ) ) {
@ -60,12 +64,12 @@ class Followers {
}
/**
* Remove a Follower
* Remove a Follower.
*
* @param int $user_id The ID of the WordPress User
* @param string $actor The Actor URL
* @param int $user_id The ID of the WordPress User.
* @param string $actor The Actor URL.
*
* @return bool|WP_Error True on success, false or WP_Error on failure.
* @return bool True on success, false on failure.
*/
public static function remove_follower( $user_id, $actor ) {
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
@ -82,15 +86,15 @@ class Followers {
/**
* Get a Follower.
*
* @param int $user_id The ID of the WordPress User
* @param string $actor The Actor URL
* @param int $user_id The ID of the WordPress User.
* @param string $actor The Actor URL.
*
* @return \Activitypub\Model\Follower|null The Follower object or null
* @return Follower|null The Follower object or null
*/
public static function get_follower( $user_id, $actor ) {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
// 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",
@ -111,16 +115,16 @@ class Followers {
}
/**
* Get a Follower by Actor indepenent from the User.
* Get a Follower by Actor independent of the User.
*
* @param string $actor The Actor URL.
*
* @return \Activitypub\Model\Follower|null The Follower object or null
* @return \Activitypub\Activity\Base_Object|WP_Error|null
*/
public static function get_follower_by_actor( $actor ) {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$post_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT ID FROM $wpdb->posts WHERE guid=%s",
@ -137,7 +141,7 @@ class Followers {
}
/**
* Get the Followers of a given user
* Get the Followers of a given user.
*
* @param int $user_id The ID of the WordPress User.
* @param int $number Maximum number of results to return.
@ -158,9 +162,12 @@ class Followers {
* @param int $page Page number.
* @param array $args The WP_Query arguments.
*
* @return array
* followers List of `Follower` objects.
* total Total number of followers.
* @return array {
* Data about the followers.
*
* @type array $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() ) {
$defaults = array(
@ -187,13 +194,12 @@ class Followers {
},
$query->get_posts()
);
return compact( 'followers', 'total' );
}
/**
* Get all Followers
*
* @param array $args The WP_Query arguments.
* Get all Followers.
*
* @return array The Term list of Followers.
*/
@ -219,7 +225,7 @@ class Followers {
/**
* Count the total number of followers
*
* @param int $user_id The ID of the WordPress User
* @param int $user_id The ID of the WordPress User.
*
* @return int The number of Followers
*/
@ -251,11 +257,11 @@ class Followers {
}
/**
* Returns all Inboxes fo a Users Followers
* Returns all Inboxes for a Users Followers.
*
* @param int $user_id The ID of the WordPress User
* @param int $user_id The ID of the WordPress User.
*
* @return array The list of Inboxes
* @return array The list of Inboxes.
*/
public static function get_inboxes( $user_id ) {
$cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id );
@ -265,7 +271,7 @@ class Followers {
return $inboxes;
}
// get all Followers of a ID of the WordPress User
// Get all Followers of a ID of the WordPress User.
$posts = new WP_Query(
array(
'nopaging' => true,
@ -316,13 +322,12 @@ class Followers {
}
/**
* Get all Followers that have not been updated for a given time
* Get all Followers that have not been updated for a given time.
*
* @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT.
* @param int $number Limits the result.
* @param int $older_than The time in seconds.
* @param int $number Optional. Limits the result. Default 50.
* @param int $older_than Optional. The time in seconds. Default 86400 (1 day).
*
* @return mixed The Term list of Followers, the format depends on $output.
* @return array The Term list of Followers.
*/
public static function get_outdated_followers( $number = 50, $older_than = 86400 ) {
$args = array(
@ -330,7 +335,7 @@ class Followers {
'posts_per_page' => $number,
'orderby' => 'modified',
'order' => 'ASC',
'post_status' => 'any', // 'any' includes 'trash
'post_status' => 'any', // 'any' includes 'trash'.
'date_query' => array(
array(
'column' => 'post_modified_gmt',
@ -343,19 +348,18 @@ class Followers {
$items = array();
foreach ( $posts->get_posts() as $follower ) {
$items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore
$items[] = Follower::init_from_cpt( $follower );
}
return $items;
}
/**
* Get all Followers that had errors
* Get all Followers that had errors.
*
* @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT
* @param integer $number The number of Followers to return.
* @param int $number Optional. The number of Followers to return. Default 20.
*
* @return mixed The Term list of Followers, the format depends on $output.
* @return array The Term list of Followers.
*/
public static function get_faulty_followers( $number = 20 ) {
$args = array(
@ -393,7 +397,7 @@ class Followers {
$items = array();
foreach ( $posts->get_posts() as $follower ) {
$items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore
$items[] = Follower::init_from_cpt( $follower );
}
return $items;
@ -403,8 +407,7 @@ class Followers {
* This function is used to store errors that occur when
* sending an ActivityPub message to a Follower.
*
* The error will be stored in the
* post meta.
* The error will be stored in post meta.
*
* @param int $post_id The ID of the WordPress Custom-Post-Type.
* @param mixed $error The error message. Can be a string or a WP_Error.

View File

@ -1,8 +1,14 @@
<?php
/**
* Interactions collection file.
*
* @package Activitypub
*/
namespace Activitypub\Collection;
use WP_Error;
use WP_Comment_Query;
use Activitypub\Comment;
use function Activitypub\object_to_uri;
use function Activitypub\url_to_commentid;
@ -10,25 +16,23 @@ use function Activitypub\object_id_to_comment;
use function Activitypub\get_remote_metadata_by_actor;
/**
* ActivityPub Interactions Collection
* ActivityPub Interactions Collection.
*/
class Interactions {
const INSERT = 'insert';
const UPDATE = 'update';
/**
* Add a comment to a post
* Add a comment to a post.
*
* @param array $activity The activity-object
* @param array $activity The activity-object.
*
* @return array|false The commentdata or false on failure
* @return array|false The comment data or false on failure.
*/
public static function add_comment( $activity ) {
if (
! isset( $activity['object'] ) ||
! isset( $activity['object']['id'] )
) {
return false;
}
$commentdata = self::activity_to_comment( $activity );
if ( ! isset( $activity['object']['inReplyTo'] ) ) {
if ( ! $commentdata || ! isset( $activity['object']['inReplyTo'] ) ) {
return false;
}
@ -36,82 +40,34 @@ class Interactions {
$comment_post_id = \url_to_postid( $in_reply_to );
$parent_comment_id = url_to_commentid( $in_reply_to );
// save only replys and reactions
// Save only replies and reactions.
if ( ! $comment_post_id && $parent_comment_id ) {
$parent_comment = get_comment( $parent_comment_id );
$comment_post_id = $parent_comment->comment_post_ID;
}
// not a reply to a post or comment
// Not a reply to a post or comment.
if ( ! $comment_post_id ) {
return false;
}
$actor = object_to_uri( $activity['actor'] );
$meta = get_remote_metadata_by_actor( $actor );
$commentdata['comment_post_ID'] = $comment_post_id;
$commentdata['comment_parent'] = $parent_comment_id ? $parent_comment_id : 0;
if ( ! $meta || \is_wp_error( $meta ) ) {
return false;
}
$url = object_to_uri( $meta['url'] );
$commentdata = array(
'comment_post_ID' => $comment_post_id,
'comment_author' => isset( $meta['name'] ) ? \esc_attr( $meta['name'] ) : \esc_attr( $meta['preferredUsername'] ),
'comment_author_url' => \esc_url_raw( $url ),
'comment_content' => \addslashes( $activity['object']['content'] ),
'comment_type' => 'comment',
'comment_author_email' => '',
'comment_parent' => $parent_comment_id ? $parent_comment_id : 0,
'comment_meta' => array(
'source_id' => \esc_url_raw( $activity['object']['id'] ),
'protocol' => 'activitypub',
),
);
if ( isset( $meta['icon']['url'] ) ) {
$commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] );
}
if ( isset( $activity['object']['url'] ) ) {
$commentdata['comment_meta']['source_url'] = \esc_url_raw( object_to_uri( $activity['object']['url'] ) );
}
// disable flood control
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
// do not require email for AP entries
\add_filter( 'pre_option_require_name_email', '__return_false' );
// No nonce possible for this submission route
\add_filter(
'akismet_comment_nonce',
function () {
return 'inactive';
}
);
\add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
$comment = \wp_new_comment( $commentdata, true );
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
\remove_filter( 'pre_option_require_name_email', '__return_false' );
// re-add flood control
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
return $comment;
return self::persist( $commentdata, self::INSERT );
}
/**
* Update a comment
* Update a comment.
*
* @param array $activity The activity-object
* @param array $activity The activity object.
*
* @return array|string|int|\WP_Error|false The commentdata or false on failure
* @return array|string|int|\WP_Error|false The comment data or false on failure.
*/
public static function update_comment( $activity ) {
$meta = get_remote_metadata_by_actor( $activity['actor'] );
//Determine comment_ID
// Determine comment_ID.
$comment = object_id_to_comment( \esc_url_raw( $activity['object']['id'] ) );
$commentdata = \get_comment( $comment, ARRAY_A );
@ -119,44 +75,63 @@ class Interactions {
return false;
}
//found a local comment id
// Found a local comment id.
$commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] );
$commentdata['comment_content'] = \addslashes( $activity['object']['content'] );
if ( isset( $meta['icon']['url'] ) ) {
$commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] );
return self::persist( $commentdata, self::UPDATE );
}
// disable flood control
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
// do not require email for AP entries
\add_filter( 'pre_option_require_name_email', '__return_false' );
// No nonce possible for this submission route
\add_filter(
'akismet_comment_nonce',
function () {
return 'inactive';
/**
* Adds an incoming Like, Announce, ... as a comment to a post.
*
* @param array $activity Activity array.
*
* @return array|false Comment data or `false` on failure.
*/
public static function add_reaction( $activity ) {
$commentdata = self::activity_to_comment( $activity );
if ( ! $commentdata ) {
return false;
}
);
\add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
$state = \wp_update_comment( $commentdata, true );
$url = object_to_uri( $activity['object'] );
$comment_post_id = url_to_postid( $url );
$parent_comment_id = url_to_commentid( $url );
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
\remove_filter( 'pre_option_require_name_email', '__return_false' );
// re-add flood control
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
if ( 1 === $state ) {
return $commentdata;
} else {
return $state; // Either `false` or a `WP_Error` instance or `0` or `1`!
if ( ! $comment_post_id && $parent_comment_id ) {
$parent_comment = get_comment( $parent_comment_id );
$comment_post_id = $parent_comment->comment_post_ID;
}
if ( ! $comment_post_id ) {
// Not a reply to a post or comment.
return false;
}
$type = $activity['type'];
if ( ! Comment::is_registered_comment_type( $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;
$commentdata['comment_content'] = \esc_html( $comment_content );
$commentdata['comment_type'] = \esc_attr( $comment_type['type'] );
$commentdata['comment_meta']['source_id'] = \esc_url_raw( $activity['id'] );
return self::persist( $commentdata, self::INSERT );
}
/**
* Get interaction(s) for a given URL/ID.
*
* @param strin $url The URL/ID to get interactions for.
* @param string $url The URL/ID to get interactions for.
*
* @return array The interactions as WP_Comment objects.
*/
@ -198,7 +173,7 @@ class Interactions {
public static function get_interactions_by_actor( $actor ) {
$meta = get_remote_metadata_by_actor( $actor );
// get URL, because $actor seems to be the ID
// Get URL, because $actor seems to be the ID.
if ( $meta && ! is_wp_error( $meta ) && isset( $meta['url'] ) ) {
$actor = object_to_uri( $meta['url'] );
}
@ -223,7 +198,7 @@ class Interactions {
* Adds line breaks to the list of allowed comment tags.
*
* @param array $allowed_tags Allowed HTML tags.
* @param string $context Context.
* @param string $context Optional. Context. Default empty.
*
* @return array Filtered tag list.
*/
@ -244,4 +219,103 @@ class Interactions {
return $allowed_tags;
}
/**
* Convert an Activity to a WP_Comment
*
* @param array $activity The Activity array.
*
* @return array|false The comment data or false on failure.
*/
public static function activity_to_comment( $activity ) {
$comment_content = null;
$actor = object_to_uri( $activity['actor'] );
$actor = get_remote_metadata_by_actor( $actor );
// Check Actor-Meta.
if ( ! $actor || is_wp_error( $actor ) ) {
return false;
}
// Check Actor-Name.
if ( isset( $actor['name'] ) ) {
$comment_author = $actor['name'];
} elseif ( isset( $actor['preferredUsername'] ) ) {
$comment_author = $actor['preferredUsername'];
} else {
return false;
}
$url = object_to_uri( $actor['url'] );
if ( ! $url ) {
object_to_uri( $actor['id'] );
}
if ( isset( $activity['object']['content'] ) ) {
$comment_content = \addslashes( $activity['object']['content'] );
}
$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_meta' => array(
'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ),
'protocol' => 'activitypub',
),
);
if ( isset( $actor['icon']['url'] ) ) {
$commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $actor['icon']['url'] );
}
if ( isset( $activity['object']['url'] ) ) {
$commentdata['comment_meta']['source_url'] = \esc_url_raw( object_to_uri( $activity['object']['url'] ) );
}
return $commentdata;
}
/**
* Persist a comment.
*
* @param array $commentdata The commentdata array.
* @param string $action Optional. Either 'insert' or 'update'. Default 'insert'.
*
* @return array|string|int|\WP_Error|false The comment data or false on failure
*/
public static function persist( $commentdata, $action = self::INSERT ) {
// Disable flood control.
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
// Do not require email for AP entries.
\add_filter( 'pre_option_require_name_email', '__return_false' );
// No nonce possible for this submission route.
\add_filter(
'akismet_comment_nonce',
function () {
return 'inactive';
}
);
\add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
if ( self::INSERT === $action ) {
$state = \wp_new_comment( $commentdata, true );
} else {
$state = \wp_update_comment( $commentdata, true );
}
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
\remove_filter( 'pre_option_require_name_email', '__return_false' );
// Restore flood control.
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
if ( 1 === $state ) {
return $commentdata;
} else {
return $state; // Either WP_Comment, false, a WP_Error, 0, or 1!
}
}
}

View File

@ -0,0 +1,181 @@
<?php
/**
* Replies collection file.
*
* @package Activitypub
*/
namespace Activitypub\Collection;
use WP_Post;
use WP_Comment;
use WP_Error;
use Activitypub\Comment;
use function Activitypub\is_local_comment;
use function Activitypub\get_rest_url_by_path;
/**
* Class containing code for getting replies Collections and CollectionPages of posts and comments.
*/
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.
*/
private static function build_args( $wp_object ) {
$args = array(
'status' => 'approve',
'orderby' => 'comment_date_gmt',
'order' => 'ASC',
);
if ( $wp_object instanceof WP_Post ) {
$args['parent'] = 0; // TODO: maybe this is unnecessary.
$args['post_id'] = $wp_object->ID;
} elseif ( $wp_object instanceof WP_Comment ) {
$args['parent'] = $wp_object->comment_ID;
} else {
return new WP_Error();
}
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.
*
* @param WP_Post|WP_Comment $wp_object The post or comment to fetch replies for.
*
* @return string|WP_Error The rest URL of the replies collection or WP_Error if the object is not a post or comment.
*/
private static function get_id( $wp_object ) {
if ( $wp_object instanceof WP_Post ) {
return get_rest_url_by_path( sprintf( 'posts/%d/replies', $wp_object->ID ) );
} 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();
}
}
/**
* 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.
*/
public static function get_collection( $wp_object ) {
$id = self::get_id( $wp_object );
if ( ! $id ) {
return null;
}
$replies = array(
'id' => $id,
'type' => 'Collection',
);
$replies['first'] = self::get_collection_page( $wp_object, 0, $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.
*
* @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage
*
* @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.
*
* @return array A CollectionPage as an associative array.
*/
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 );
// 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;
}
// 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' );
// 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 );
// Build the associative CollectionPage array.
$collection_page = array(
'id' => \add_query_arg( 'page', $page, $part_of ),
'type' => 'CollectionPage',
'partOf' => $part_of,
'items' => $comment_ids,
);
if ( $total_replies / $comments_per_page > $page + 1 ) {
$collection_page['next'] = \add_query_arg( 'page', $page + 1, $part_of );
}
return $collection_page;
}
}

View File

@ -1,4 +1,10 @@
<?php
/**
* Users collection file.
*
* @package Activitypub
*/
namespace Activitypub\Collection;
use WP_Error;
@ -13,27 +19,30 @@ use function Activitypub\normalize_host;
use function Activitypub\url_to_authorid;
use function Activitypub\is_user_disabled;
/**
* Users collection.
*/
class Users {
/**
* The ID of the Blog User
* The ID of the Blog User.
*
* @var int
*/
const BLOG_USER_ID = 0;
/**
* The ID of the Application User
* The ID of the Application User.
*
* @var int
*/
const APPLICATION_USER_ID = -1;
/**
* Get the User by ID
* Get the User by ID.
*
* @param int $user_id The User-ID.
*
* @return \Acitvitypub\Model\User The User.
* @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 ) ) {
@ -68,24 +77,24 @@ class Users {
*
* @param string $username The User-Name.
*
* @return \Acitvitypub\Model\User The User.
* @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.
// Check for blog user.
if ( Blog::get_default_username() === $username ) {
return new Blog();
}
if ( get_option( 'activitypub_blog_user_identifier' ) === $username ) {
if ( get_option( 'activitypub_blog_identifier' ) === $username ) {
return new Blog();
}
// check for application user.
// Check for application user.
if ( 'application' === $username ) {
return new Application();
}
// check for 'activitypub_username' meta
// Check for 'activitypub_username' meta.
$user = new WP_User_Query(
array(
'count_total' => false,
@ -110,7 +119,7 @@ class Users {
$username = str_replace( array( '*', '%' ), '', $username );
// check for login or nicename.
// Check for login or nicename.
$user = new WP_User_Query(
array(
'count_total' => false,
@ -136,26 +145,26 @@ class Users {
/**
* Get the User by resource.
*
* @param string $resource The User-Resource.
* @param string $uri The User-Resource.
*
* @return \Acitvitypub\Model\User The User.
* @return User|WP_Error The User or WP_Error if user not found.
*/
public static function get_by_resource( $resource ) {
$resource = object_to_uri( $resource );
public static function get_by_resource( $uri ) {
$uri = object_to_uri( $uri );
$scheme = 'acct';
$match = array();
// try to extract the scheme and the host
if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $resource, $match ) ) {
// extract the scheme
// 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
// Check for http(s) URIs.
case 'http':
case 'https':
$resource_path = \wp_parse_url( $resource, PHP_URL_PATH );
$resource_path = \wp_parse_url( $uri, PHP_URL_PATH );
if ( $resource_path ) {
$blog_path = \wp_parse_url( \home_url(), PHP_URL_PATH );
@ -166,7 +175,7 @@ class Users {
$resource_path = \trim( $resource_path, '/' );
// check for http(s)://blog.example.com/@username
// Check for http(s)://blog.example.com/@username.
if ( str_starts_with( $resource_path, '@' ) ) {
$identifier = \str_replace( '@', '', $resource_path );
$identifier = \trim( $identifier, '/' );
@ -175,17 +184,17 @@ class Users {
}
}
// check for http(s)://blog.example.com/author/username
$user_id = url_to_authorid( $resource );
// 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/
// Check for http(s)://blog.example.com/.
if (
normalize_url( site_url() ) === normalize_url( $resource ) ||
normalize_url( home_url() ) === normalize_url( $resource )
normalize_url( site_url() ) === normalize_url( $uri ) ||
normalize_url( home_url() ) === normalize_url( $uri )
) {
return self::get_by_id( self::BLOG_USER_ID );
}
@ -195,11 +204,11 @@ class Users {
\__( 'User not found', 'activitypub' ),
array( 'status' => 404 )
);
// check for acct URIs
// Check for acct URIs.
case 'acct':
$resource = \str_replace( 'acct:', '', $resource );
$identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) );
$host = normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) );
$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 ) {
@ -210,7 +219,7 @@ class Users {
);
}
// prepare wildcards https://github.com/mastodon/mastodon/issues/22213
// Prepare wildcards https://github.com/mastodon/mastodon/issues/22213.
if ( in_array( $identifier, array( '_', '*', '' ), true ) ) {
return self::get_by_id( self::BLOG_USER_ID );
}
@ -228,9 +237,9 @@ class Users {
/**
* Get the User by resource.
*
* @param string $resource The User-Resource.
* @param string $id The User-Resource.
*
* @return \Acitvitypub\Model\User The User.
* @return User|Blog|Application|WP_Error The User or WP_Error if user not found.
*/
public static function get_by_various( $id ) {
$user = null;
@ -238,11 +247,11 @@ class Users {
if ( is_numeric( $id ) ) {
$user = self::get_by_id( $id );
} elseif (
// is URL
// Is URL.
filter_var( $id, FILTER_VALIDATE_URL ) ||
// is acct
// Is acct.
str_starts_with( $id, 'acct:' ) ||
// is email
// Is email.
filter_var( $id, FILTER_VALIDATE_EMAIL )
) {
$user = self::get_by_resource( $id );

View File

@ -1,6 +1,8 @@
<?php
/**
* ActivityPub implementation for WordPress/PHP functions either missing from older WordPress/PHP versions or not included by default.
*
* @package Activitypub
*/
if ( ! function_exists( 'str_starts_with' ) ) {
@ -32,6 +34,12 @@ if ( ! function_exists( 'get_self_link' ) ) {
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 ) ) );
}
}
@ -56,18 +64,28 @@ if ( ! function_exists( 'is_countable' ) ) {
* @return bool True if `$array` is a list, otherwise false.
*/
if ( ! function_exists( 'array_is_list' ) ) {
function array_is_list( $array ) {
if ( ! is_array( $array ) ) {
/**
* Check if an array is a list.
*
* An array is considered a list if its keys are a range of numbers
* starting from 0 and ending at count( $array ) - 1.
*
* @param array $input The array to check.
*
* @return bool True if `$input` is a list, otherwise false.
*/
function array_is_list( $input ) {
if ( ! is_array( $input ) ) {
return false;
}
if ( array_values( $array ) === $array ) {
if ( array_values( $input ) === $input ) {
return true;
}
$next_key = -1;
foreach ( $array as $k => $v ) {
foreach ( $input as $k => $v ) {
if ( ++$next_key !== $k ) {
return false;
}

View File

@ -1,17 +1,22 @@
<?php
/**
* Debugging functions.
*
* @package Activitypub
*/
namespace Activitypub;
/**
* Allow localhost URLs if WP_DEBUG is true.
*
* @param array $r Array of HTTP request args.
* @param string $url The request URL.
* @param array $parsed_args An array of HTTP request arguments.
*
* @return array Array or string of HTTP request arguments.
*/
function allow_localhost( $r, $url ) {
$r['reject_unsafe_urls'] = false;
function allow_localhost( $parsed_args ) {
$parsed_args['reject_unsafe_urls'] = false;
return $r;
return $parsed_args;
}
add_filter( 'http_request_args', '\Activitypub\allow_localhost', 10, 2 );
add_filter( 'http_request_args', '\Activitypub\allow_localhost' );

View File

@ -1,19 +1,21 @@
<?php
/**
* Functions file.
*
* @package Activitypub
*/
namespace Activitypub;
use WP_Query;
use WP_Error;
use Activitypub\Http;
use Activitypub\Comment;
use Activitypub\Webfinger;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Users;
/**
* Returns the ActivityPub default JSON-context
* Returns the ActivityPub default JSON-context.
*
* @return array the activitypub context
* @return array The activitypub context.
*/
function get_context() {
$context = Activity::JSON_LD_CONTEXT;
@ -21,30 +23,46 @@ function get_context() {
return \apply_filters( 'activitypub_json_context', $context );
}
/**
* Send a POST request to a remote server.
*
* @param string $url The URL endpoint.
* @param string $body The Post Body.
* @param int $user_id The WordPress user ID.
*
* @return array|WP_Error The POST Response or an WP_Error.
*/
function safe_remote_post( $url, $body, $user_id ) {
return Http::post( $url, $body, $user_id );
}
/**
* Send a GET request to a remote server.
*
* @param string $url The URL endpoint.
*
* @return array|WP_Error The GET Response or an WP_Error.
*/
function safe_remote_get( $url ) {
return Http::get( $url );
}
/**
* Returns a users WebFinger "resource"
* Returns a users WebFinger "resource".
*
* @param int $user_id The User-ID.
* @param int $user_id The user ID.
*
* @return string The User-Resource.
* @return string The User resource.
*/
function get_webfinger_resource( $user_id ) {
return Webfinger::get_user_resource( $user_id );
}
/**
* Requests the Meta-Data from the Actors profile
* Requests the Meta-Data from the Actors profile.
*
* @param string $actor The Actor URL.
* @param bool $cached If the result should be cached.
* @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.
*/
@ -60,7 +78,14 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) {
} 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 ) );
return new WP_Error(
'activitypub_no_valid_actor_identifier',
\__( 'The "actor" identifier is not valid', 'activitypub' ),
array(
'status' => 404,
'actor' => $actor,
)
);
}
}
@ -69,7 +94,14 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) {
}
if ( ! $actor ) {
return new WP_Error( 'activitypub_no_valid_actor_identifier', \__( 'The "actor" identifier is not valid', 'activitypub' ), array( 'status' => 404, 'actor' => $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 ) ) {
@ -78,7 +110,7 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) {
$transient_key = 'activitypub_' . $actor;
// only check the cache if needed.
// Only check the cache if needed.
if ( $cached ) {
$metadata = \get_transient( $transient_key );
@ -88,7 +120,14 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) {
}
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 ) );
$metadata = new WP_Error(
'activitypub_no_valid_actor_url',
\__( 'The "actor" is no valid URL', 'activitypub' ),
array(
'status' => 400,
'actor' => $actor,
)
);
return $metadata;
}
@ -102,7 +141,14 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) {
$metadata = \json_decode( $metadata, true );
if ( ! $metadata ) {
$metadata = new WP_Error( 'activitypub_invalid_json', \__( 'No valid JSON data', 'activitypub' ), array( 'status' => 400, 'actor' => $actor ) );
$metadata = new WP_Error(
'activitypub_invalid_json',
\__( 'No valid JSON data', 'activitypub' ),
array(
'status' => 400,
'actor' => $actor,
)
);
return $metadata;
}
@ -114,7 +160,7 @@ function get_remote_metadata_by_actor( $actor, $cached = true ) {
/**
* Returns the followers of a given user.
*
* @param int $user_id The User-ID.
* @param int $user_id The user ID.
*
* @return array The followers.
*/
@ -125,7 +171,7 @@ function get_followers( $user_id ) {
/**
* Count the number of followers for a given user.
*
* @param int $user_id The User-ID.
* @param int $user_id The user ID.
*
* @return int The number of followers.
*/
@ -145,12 +191,12 @@ function count_followers( $user_id ) {
function url_to_authorid( $url ) {
global $wp_rewrite;
// check if url hase the same host
// 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;
}
// first, check to see if there is a 'author=N' to match against
// First, check to see if there is a 'author=N' to match against.
if ( \preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) {
$id = \absint( $values[1] );
if ( $id ) {
@ -158,19 +204,19 @@ function url_to_authorid( $url ) {
}
}
// check to see if we are using rewrite rules
// Check to see if we are using rewrite rules.
$rewrite = $wp_rewrite->wp_rewrite_rules();
// not using rewrite rules, and 'author=N' method failed, so we're out of options
// Not using rewrite rules, and 'author=N' method failed, so we're out of options.
if ( empty( $rewrite ) ) {
return 0;
}
// generate rewrite rule for the author url
// Generate rewrite rule for the author url.
$author_rewrite = $wp_rewrite->get_author_permastruct();
$author_regexp = \str_replace( '%author%', '', $author_rewrite );
// match the rewrite rule with the passed url
// Match the rewrite rule with the passed url.
if ( \preg_match( '/https?:\/\/(.+)' . \preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) {
$user = \get_user_by( 'slug', $match[2] );
if ( $user ) {
@ -182,10 +228,9 @@ function url_to_authorid( $url ) {
}
/**
* Verify if url is a wp_ap_comment,
* Or if it is a previously received remote comment
* Verify that url is a wp_ap_comment or a previously received remote comment.
*
* @return int comment_id
* @return int|bool Comment ID or false if not found.
*/
function is_comment() {
$comment_id = get_query_var( 'c', null );
@ -193,8 +238,7 @@ function is_comment() {
if ( ! is_null( $comment_id ) ) {
$comment = \get_comment( $comment_id );
// Only return local origin comments
if ( $comment && $comment->user_id ) {
if ( $comment ) {
return $comment_id;
}
}
@ -203,13 +247,13 @@ function is_comment() {
}
/**
* Check for Tombstone Objects
* Check for Tombstone Objects.
*
* @see https://www.w3.org/TR/activitypub/#delete-activity-outbox
*
* @param WP_Error $wp_error A WP_Error-Response of an HTTP-Request
* @param WP_Error $wp_error A WP_Error-Response of an HTTP-Request.
*
* @return boolean true if HTTP-Code is 410 or 404
* @return boolean True if HTTP-Code is 410 or 404.
*/
function is_tombstone( $wp_error ) {
if ( ! is_wp_error( $wp_error ) ) {
@ -226,12 +270,12 @@ function is_tombstone( $wp_error ) {
/**
* Get the REST URL relative to this plugin's namespace.
*
* @param string $path Optional. REST route path. Otherwise this plugin's namespaced root.
* @param string $path Optional. REST route path. Default ''.
*
* @return string REST URL relative to this plugin's namespace.
*/
function get_rest_url_by_path( $path = '' ) {
// we'll handle the leading slash.
// We'll handle the leading slash.
$path = ltrim( $path, '/' );
$namespaced_path = sprintf( '/%s/%s', ACTIVITYPUB_REST_NAMESPACE, $path );
return \get_rest_url( null, $namespaced_path );
@ -240,37 +284,35 @@ function get_rest_url_by_path( $path = '' ) {
/**
* Convert a string from camelCase to snake_case.
*
* @param string $string The string to convert.
* @param string $input The string to convert.
*
* @return string The converted string.
*/
// phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound
function camel_to_snake_case( $string ) {
return strtolower( preg_replace( '/(?<!^)[A-Z]/', '_$0', $string ) );
function camel_to_snake_case( $input ) {
return strtolower( preg_replace( '/(?<!^)[A-Z]/', '_$0', $input ) );
}
/**
* Convert a string from snake_case to camelCase.
*
* @param string $string The string to convert.
* @param string $input The string to convert.
*
* @return string The converted string.
*/
// phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.stringFound
function snake_to_camel_case( $string ) {
return lcfirst( str_replace( '_', '', ucwords( $string, '_' ) ) );
function snake_to_camel_case( $input ) {
return lcfirst( str_replace( '_', '', ucwords( $input, '_' ) ) );
}
/**
* Escapes a Tag, to be used as a hashtag.
*
* @param string $string The string to escape.
* @param string $input The string to escape.
*
* @return string The escaped hastag.
* @return string The escaped hashtag.
*/
function esc_hashtag( $string ) {
function esc_hashtag( $input ) {
$hashtag = \wp_specialchars_decode( $string, ENT_QUOTES );
$hashtag = \wp_specialchars_decode( $input, ENT_QUOTES );
// Remove all characters that are not letters, numbers, or underscores.
$hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}_]+/u', '_', $hashtag );
@ -278,7 +320,7 @@ function esc_hashtag( $string ) {
$hashtag = preg_replace_callback(
'/_(.)/',
function ( $matches ) {
return '' . strtoupper( $matches[1] );
return strtoupper( $matches[1] );
},
$hashtag
);
@ -291,9 +333,9 @@ function esc_hashtag( $string ) {
* Allow defining your own custom hashtag generation rules.
*
* @param string $hashtag The hashtag to be returned.
* @param string $string The original string.
* @param string $input The original string.
*/
$hashtag = apply_filters( 'activitypub_esc_hashtag', $hashtag, $string );
$hashtag = apply_filters( 'activitypub_esc_hashtag', $hashtag, $input );
return esc_html( $hashtag );
}
@ -331,8 +373,6 @@ function is_activitypub_request() {
}
// One can trigger an ActivityPub request by adding ?activitypub to the URL.
// phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.VariableRedeclaration
global $wp_query;
if ( isset( $wp_query->query_vars['activitypub'] ) ) {
return true;
}
@ -363,7 +403,7 @@ function is_activitypub_request() {
/**
* This function checks if a user is disabled for ActivityPub.
*
* @param int $user_id The User-ID.
* @param int $user_id The user ID.
*
* @return boolean True if the user is disabled, false otherwise.
*/
@ -405,6 +445,12 @@ function is_user_disabled( $user_id ) {
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 );
}
@ -414,7 +460,7 @@ function is_user_disabled( $user_id ) {
* This function is used to check if the 'blog' or 'user'
* type is disabled for ActivityPub.
*
* @param enum $type Can be 'blog' or 'user'.
* @param string $type User type. 'blog' or 'user'.
*
* @return boolean True if the user type is disabled, false otherwise.
*/
@ -461,10 +507,20 @@ function is_user_type_disabled( $type ) {
$return = false;
break;
default:
$return = new WP_Error( 'activitypub_wrong_user_type', __( 'Wrong user type', 'activitypub' ), array( 'status' => 400 ) );
$return = new WP_Error(
'activitypub_wrong_user_type',
__( 'Wrong user type', 'activitypub' ),
array( 'status' => 400 )
);
break;
}
/**
* 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.
*/
return apply_filters( 'activitypub_is_user_type_disabled', $return, $type );
}
@ -519,20 +575,25 @@ function is_json( $data ) {
}
/**
* Check if a blog is public based on the `blog_public` option
* Check whther a blog is public based on the `blog_public` option.
*
* @return bollean True if public, false if not
* @return bool True if public, false if not
*/
function is_blog_public() {
/**
* Filter whether the blog is public.
*
* @param bool $public Whether the blog is public.
*/
return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
}
/**
* Sanitize a URL
* Sanitize a URL.
*
* @param string $value The URL to sanitize
* @param string $value The URL to sanitize.
*
* @return string|null The sanitized URL or null if invalid
* @return string|null The sanitized URL or null if invalid.
*/
function sanitize_url( $value ) {
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
@ -543,11 +604,11 @@ function sanitize_url( $value ) {
}
/**
* Extract recipient URLs from Activity object
* Extract recipient URLs from Activity object.
*
* @param array $data
* @param array $data The Activity object as array.
*
* @return array The list of user URLs
* @return array The list of user URLs.
*/
function extract_recipients_from_activity( $data ) {
$recipient_items = array();
@ -574,10 +635,10 @@ function extract_recipients_from_activity( $data ) {
$recipients = array();
// flatten array
// Flatten array.
foreach ( $recipient_items as $recipient ) {
if ( is_array( $recipient ) ) {
// check if recipient is an object
// Check if recipient is an object.
if ( array_key_exists( 'id', $recipient ) ) {
$recipients[] = $recipient['id'];
}
@ -590,11 +651,11 @@ function extract_recipients_from_activity( $data ) {
}
/**
* Check if passed Activity is Public
* Check if passed Activity is Public.
*
* @param array $data The Activity object as array
* @param array $data The Activity object as array.
*
* @return boolean True if public, false if not
* @return boolean True if public, false if not.
*/
function is_activity_public( $data ) {
$recipients = extract_recipients_from_activity( $data );
@ -603,11 +664,11 @@ function is_activity_public( $data ) {
}
/**
* Get active users based on a given duration
* Get active users based on a given duration.
*
* @param int $duration The duration to check in month(s)
* @param int $duration Optional. The duration to check in month(s). Default 1.
*
* @return int The number of active users
* @return int The number of active users.
*/
function get_active_users( $duration = 1 ) {
@ -617,39 +678,44 @@ function get_active_users( $duration = 1 ) {
if ( false === $count ) {
global $wpdb;
$query = "SELECT COUNT( DISTINCT post_author ) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date <= DATE_SUB( NOW(), INTERVAL %d MONTH )";
$query = $wpdb->prepare( $query, $duration );
$count = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT( DISTINCT post_author ) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date <= DATE_SUB( NOW(), INTERVAL %d MONTH )",
$duration
)
);
set_transient( $transient_key, $count, DAY_IN_SECONDS );
}
// if 0 authors where active
// If 0 authors where active.
if ( 0 === $count ) {
return 0;
}
// if single user mode
// If single user mode.
if ( is_single_user() ) {
return 1;
}
// if blog user is disabled
// If blog user is disabled.
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
return $count;
return (int) $count;
}
// also count blog user
return $count + 1;
// Also count blog user.
return (int) $count + 1;
}
/**
* Get the total number of users
* Get the total number of users.
*
* @return int The total number of users
* @return int The total number of users.
*/
function get_total_users() {
// if single user mode
// If single user mode.
if ( is_single_user() ) {
return 1;
}
@ -666,12 +732,12 @@ function get_total_users() {
$users = 1;
}
// if blog user is disabled
// If blog user is disabled.
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
return $users;
return (int) $users;
}
return $users + 1;
return (int) $users + 1;
}
/**
@ -679,60 +745,66 @@ function get_total_users() {
*
* @param string $id ActivityPub object ID (usually a URL) to check.
*
* @return int|boolean Comment ID, or false on failure.
* @return \WP_Comment|boolean Comment, or false on failure.
*/
function object_id_to_comment( $id ) {
return Comment::object_id_to_comment( $id );
}
/**
* Verify if URL is a local comment,
* Or if it is a previously received remote comment
* Verify that URL is a local comment or a previously received remote comment.
* (For threading comments locally)
*
* @param string $url The URL to check.
*
* @return int comment_ID or null if not found
* @return string|null Comment ID or null if not found
*/
function url_to_commentid( $url ) {
return Comment::url_to_commentid( $url );
}
/**
* Get the URI of an ActivityPub object
* Get the URI of an ActivityPub object.
*
* @param array $object The ActivityPub object
* @param array|string $data The ActivityPub object.
*
* @return string The URI of the ActivityPub object
*/
function object_to_uri( $object ) {
// check if it is already simple
if ( ! $object || is_string( $object ) ) {
return $object;
function object_to_uri( $data ) {
// Check whether it is already simple.
if ( ! $data || is_string( $data ) ) {
return $data;
}
// check if it is a list, then take first item
// this plugin does not support collections
if ( array_is_list( $object ) ) {
$object = $object[0];
/*
* Check if it is a list, then take first item.
* This plugin does not support collections.
*/
if ( array_is_list( $data ) ) {
$data = $data[0];
}
// check if it is simplified now
if ( is_string( $object ) ) {
return $object;
// Check if it is simplified now.
if ( is_string( $data ) ) {
return $data;
}
// return part of Object that makes most sense
switch ( $object['type'] ) {
$type = 'Object';
if ( isset( $data['type'] ) ) {
$type = $data['type'];
}
// Return part of Object that makes most sense.
switch ( $type ) {
case 'Link':
$object = $object['href'];
$data = $data['href'];
break;
default:
$object = $object['id'];
$data = $data['id'];
break;
}
return $object;
return $data;
}
/**
@ -794,9 +866,8 @@ function is_local_comment( $comment ) {
/**
* Mark a WordPress object as federated.
*
* @param WP_Comment|WP_Post|mixed $wp_object
*
* @return void
* @param \WP_Comment|\WP_Post $wp_object The WordPress object.
* @param string $state The state of the object.
*/
function set_wp_object_state( $wp_object, $state ) {
$meta_key = 'activitypub_status';
@ -806,6 +877,12 @@ function set_wp_object_state( $wp_object, $state ) {
} elseif ( $wp_object instanceof \WP_Comment ) {
\update_comment_meta( $wp_object->comment_ID, $meta_key, $state );
} else {
/**
* 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 );
}
}
@ -813,7 +890,7 @@ function set_wp_object_state( $wp_object, $state ) {
/**
* Get the federation state of a WordPress object.
*
* @param WP_Comment|WP_Post|mixed $wp_object
* @param \WP_Comment|\WP_Post $wp_object The WordPress object.
*
* @return string|false The state of the object or false if not found.
*/
@ -825,6 +902,11 @@ function get_wp_object_state( $wp_object ) {
} elseif ( $wp_object instanceof \WP_Comment ) {
return \get_comment_meta( $wp_object->comment_ID, $meta_key, true );
} else {
/**
* Allow plugins to get the federation state of a WordPress object.
*
* @param \WP_Comment|\WP_Post $wp_object The WordPress object.
*/
return \apply_filters( 'activitypub_get_wp_object_state', false, $wp_object );
}
}
@ -834,7 +916,7 @@ function get_wp_object_state( $wp_object ) {
*
* Set some default descriptions for the default post types.
*
* @param WP_Post_Type $post_type The post type object.
* @param \WP_Post_Type $post_type The post type object.
*
* @return string The description of the post type.
*/
@ -857,6 +939,12 @@ 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.
*/
return apply_filters( 'activitypub_post_type_description', $description, $post_type->name, $post_type );
}
@ -866,9 +954,9 @@ function get_post_type_description( $post_type ) {
* @return string The masked version.
*/
function get_masked_wp_version() {
// only show the major and minor version
// Only show the major and minor version.
$version = get_bloginfo( 'version' );
// strip the RC or beta part
// Strip the RC or beta part.
$version = preg_replace( '/-.*$/', '', $version );
$version = explode( '.', $version );
$version = array_slice( $version, 0, 2 );
@ -884,7 +972,7 @@ function get_masked_wp_version() {
* @return array The enclosures.
*/
function get_enclosures( $post_id ) {
$enclosures = get_post_meta( $post_id, 'enclosure' );
$enclosures = get_post_meta( $post_id, 'enclosure', false );
if ( ! $enclosures ) {
return array();
@ -917,15 +1005,14 @@ function get_enclosures( $post_id ) {
*
* @see https://developer.wordpress.org/reference/functions/get_post_ancestors/
*
* @param int|WP_Comment $comment Comment ID or comment object.
* @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 \WP_Comment[] Array of ancestor comments or empty array if there are none.
*/
function get_comment_ancestors( $comment ) {
$comment = \get_comment( $comment );
// phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
if ( ! $comment || empty( $comment->comment_parent ) || $comment->comment_parent == $comment->comment_ID ) {
if ( ! $comment || empty( $comment->comment_parent ) || (int) $comment->comment_parent === (int) $comment->comment_ID ) {
return array();
}
@ -934,7 +1021,6 @@ function get_comment_ancestors( $comment ) {
$id = (int) $comment->comment_parent;
$ancestors[] = $id;
// phpcs:ignore Generic.CodeAnalysis.AssignmentInCondition.FoundInWhileCondition
while ( $id > 0 ) {
$ancestor = \get_comment( $id );
$parent_id = (int) $ancestor->comment_parent;
@ -977,13 +1063,13 @@ function custom_large_numbers( $formatted, $number, $decimals ) {
$thousands_sep = $wp_locale->number_format['thousands_sep'];
}
if ( $number < 1000 ) { // any number less than a Thousand.
if ( $number < 1000 ) { // Any number less than a Thousand.
return \number_format( $number, $decimals, $decimal_point, $thousands_sep );
} elseif ( $number < 1000000 ) { // any number less than a million
} elseif ( $number < 1000000 ) { // Any number less than a million.
return \number_format( $number / 1000, $decimals, $decimal_point, $thousands_sep ) . 'K';
} elseif ( $number < 1000000000 ) { // any number less than a billion
} elseif ( $number < 1000000000 ) { // Any number less than a billion.
return \number_format( $number / 1000000, $decimals, $decimal_point, $thousands_sep ) . 'M';
} else { // at least a billion
} else { // At least a billion.
return \number_format( $number / 1000000000, $decimals, $decimal_point, $thousands_sep ) . 'B';
}
@ -991,6 +1077,37 @@ function custom_large_numbers( $formatted, $number, $decimals ) {
return $formatted;
}
/**
* Registers a ActivityPub comment type.
*
* @param string $comment_type Key for comment type.
* @param array $args Optional. Array of arguments for registering a comment type. Default empty array.
*
* @return array The registered Activitypub comment type.
*/
function register_comment_type( $comment_type, $args = array() ) {
global $activitypub_comment_types;
if ( ! is_array( $activitypub_comment_types ) ) {
$activitypub_comment_types = array();
}
// Sanitize comment type name.
$comment_type = sanitize_key( $comment_type );
$activitypub_comment_types[ $comment_type ] = $args;
/**
* Fires after a ActivityPub comment type is registered.
*
* @param string $comment_type Comment type.
* @param array $args Arguments used to register the comment type.
*/
do_action( 'activitypub_registered_comment_type', $comment_type, $args );
return $args;
}
/**
* Normalize a URL.
*
@ -1019,27 +1136,173 @@ function normalize_host( $host ) {
}
/**
* Get the Extra Fields of an Actor
* Get the reply intent URI.
*
* @param int $user_id The User-ID.
*
* @return array The extra fields.
* @return string The reply intent URI.
*/
function get_actor_extra_fields( $user_id ) {
$extra_fields = new WP_Query(
array(
'post_type' => 'ap_extrafield',
'nopaging' => true,
'status' => 'publish',
'author' => $user_id,
)
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=' ) )
);
}
if ( $extra_fields->have_posts() ) {
$extra_fields = $extra_fields->posts;
/**
* Replace content with links, mentions or hashtags by Regex callback and not affect protected tags.
*
* @param string $content The content that should be changed.
* @param string $regex The regex to use.
* @param callable $regex_callback Callback for replacement logic.
*
* @return string The content with links, mentions, hashtags, etc.
*/
function enrich_content_data( $content, $regex, $regex_callback ) {
// Small protection against execution timeouts: limit to 1 MB.
if ( mb_strlen( $content ) > MB_IN_BYTES ) {
return $content;
}
$tag_stack = array();
$protected_tags = array(
'pre',
'code',
'textarea',
'style',
'a',
);
$content_with_links = '';
$in_protected_tag = false;
foreach ( wp_html_split( $content ) as $chunk ) {
if ( preg_match( '#^<!--[\s\S]*-->$#i', $chunk, $m ) ) {
$content_with_links .= $chunk;
continue;
}
if ( preg_match( '#^<(/)?([a-z-]+)\b[^>]*>$#i', $chunk, $m ) ) {
$tag = strtolower( $m[2] );
if ( '/' === $m[1] ) {
// Closing tag.
$i = array_search( $tag, $tag_stack, true );
// We can only remove the tag from the stack if it is in the stack.
if ( false !== $i ) {
$tag_stack = array_slice( $tag_stack, 0, $i );
}
} else {
$extra_fields = array();
// Opening tag, add it to the stack.
$tag_stack[] = $tag;
}
return apply_filters( 'activitypub_get_actor_extra_fields', $extra_fields, $user_id );
// If we're in a protected tag, the tag_stack contains at least one protected tag string.
// The protected tag state can only change when we encounter a start or end tag.
$in_protected_tag = array_intersect( $tag_stack, $protected_tags );
// Never inspect tags.
$content_with_links .= $chunk;
continue;
}
if ( $in_protected_tag ) {
// Don't inspect a chunk inside an inspected tag.
$content_with_links .= $chunk;
continue;
}
// Only reachable when there is no protected tag in the stack.
$content_with_links .= \preg_replace_callback( $regex, $regex_callback, $chunk );
}
return $content_with_links;
}
/**
* Generate a summary of a post.
*
* This function generates a summary of a post by extracting:
*
* 1. The post excerpt if it exists.
* 2. The first part of the post content if it contains the <!--more--> tag.
* 3. An excerpt of the post content if it is longer than the specified length.
*
* @param int|\WP_Post $post The post ID or post object.
* @param integer $length The maximum length of the summary.
* Default is 500. It will be ignored if the post excerpt
* and the content above the <!--more--> tag.
*
* @return string The generated post summary.
*/
function generate_post_summary( $post, $length = 500 ) {
$post = get_post( $post );
if ( ! $post ) {
return '';
}
$content = \sanitize_post_field( 'post_excerpt', $post->post_excerpt, $post->ID );
if ( $content ) {
/**
* Filters the post excerpt.
*
* @param string $content The post excerpt.
*/
return \apply_filters( 'the_excerpt', $content );
}
$content = \sanitize_post_field( 'post_content', $post->post_content, $post->ID );
$content_parts = \get_extended( $content );
/**
* Filters the excerpt more value.
*
* @param string $excerpt_more The excerpt more.
*/
$excerpt_more = \apply_filters( 'activitypub_excerpt_more', '[…]' );
$length = $length - strlen( $excerpt_more );
// Check for the <!--more--> tag.
if (
! empty( $content_parts['extended'] ) &&
! empty( $content_parts['main'] )
) {
$content = $content_parts['main'] . ' ' . $excerpt_more;
$length = null;
}
$content = \html_entity_decode( $content );
$content = \wp_strip_all_tags( $content );
$content = \trim( $content );
$content = \preg_replace( '/\R+/m', "\n\n", $content );
$content = \preg_replace( '/[\r\t]/', '', $content );
if ( $length && \strlen( $content ) > $length ) {
$content = \wordwrap( $content, $length, '</activitypub-summary>' );
$content = \explode( '</activitypub-summary>', $content, 2 );
$content = $content[0] . ' ' . $excerpt_more;
}
/*
Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629
return \apply_filters( 'the_excerpt', $content );
*/
return $content;
}
/**
* Get the content warning of a post.
*
* @param int|\WP_Post $post_id The post ID or post object.
*
* @return string|false The content warning or false if not found.
*/
function get_content_warning( $post_id ) {
$post = get_post( $post_id );
if ( ! $post ) {
return false;
}
$warning = get_post_meta( $post->ID, 'activitypub_content_warning', true );
if ( empty( $warning ) ) {
return false;
}
return $warning;
}

View File

@ -1,16 +1,25 @@
<?php
/**
* Announce handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler;
use Activitypub\Http;
use Activitypub\Comment;
use Activitypub\Collection\Interactions;
use function Activitypub\object_to_uri;
use function Activitypub\is_activity_public;
/**
* Handle Create requests
* Handle Create requests.
*/
class Announce {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_action(
@ -22,35 +31,27 @@ class Announce {
}
/**
* Handles "Announce" requests
* Handles "Announce" requests.
*
* @param array $array The activity-object
* @param int $user_id The id of the local blog-user
* @param Activitypub\Activity $activity The activity object
*
* @return void
* @param array $announcement The activity-object.
* @param int $user_id The id of the local blog-user.
* @param \Activitypub\Activity\Activity $activity The activity object.
*/
public static function handle_announce( $array, $user_id, $activity = null ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.arrayFound
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
return;
}
if ( ! isset( $array['object'] ) ) {
return;
}
// check if Activity is public or not
if ( ! is_activity_public( $array ) ) {
public static function handle_announce( $announcement, $user_id, $activity = null ) {
// Check if Activity is public or not.
if ( ! is_activity_public( $announcement ) ) {
// @todo maybe send email
return;
}
// @todo save the `Announce`-Activity itself
if ( ! ACTIVITYPUB_DISABLE_REACTIONS ) {
self::maybe_save_announce( $announcement, $user_id );
}
if ( is_string( $array['object'] ) ) {
$object = Http::get_remote_object( $array['object'] );
if ( is_string( $announcement['object'] ) ) {
$object = Http::get_remote_object( $announcement['object'] );
} else {
$object = $array['object'];
$object = $announcement['object'];
}
if ( ! $object || is_wp_error( $object ) ) {
@ -63,7 +64,58 @@ class Announce {
$type = \strtolower( $object['type'] );
/**
* 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.
*/
\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.
*/
\do_action( "activitypub_inbox_{$type}", $object, $user_id, $activity );
}
/**
* Try to save the Announce.
*
* @param array $activity The activity-object.
* @param int $user_id The id of the local blog-user.
*/
public static function maybe_save_announce( $activity, $user_id ) {
$url = object_to_uri( $activity['object'] );
if ( empty( $url ) ) {
return;
}
$exists = Comment::object_id_to_comment( esc_url_raw( $url ) );
if ( $exists ) {
return;
}
$state = Interactions::add_reaction( $activity );
$reaction = null;
if ( $state && ! is_wp_error( $state ) ) {
$reaction = get_comment( $state );
}
/**
* Fires after an Announce has been saved.
*
* @param array $activity The activity-object.
* @param int $user_id The id of the local blog-user.
* @param mixed $state The state of the reaction.
* @param mixed $reaction The reaction.
*/
do_action( 'activitypub_handled_announce', $activity, $user_id, $state, $reaction );
}
}

View File

@ -1,18 +1,23 @@
<?php
/**
* Create handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler;
use WP_Error;
use Activitypub\Collection\Interactions;
use function Activitypub\is_activity_public;
use function Activitypub\object_id_to_comment;
/**
* Handle Create requests
* Handle Create requests.
*/
class Create {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_action(
@ -21,50 +26,93 @@ class Create {
10,
3
);
\add_filter(
'activitypub_validate_object',
array( self::class, 'validate_object' ),
10,
3
);
}
/**
* Handles "Create" requests
* Handles "Create" requests.
*
* @param array $array The activity-object
* @param int $user_id The id of the local blog-user
* @param Activitypub\Activity $object The activity object
*
* @return void
* @param array $activity The activity-object.
* @param int $user_id The id of the local blog-user.
* @param \Activitypub\Activity\Activity $activity_object Optional. The activity object. Default null.
*/
public static function handle_create( $array, $user_id, $object = null ) {
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
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.
return;
}
if (
! isset( $array['object'] ) ||
! isset( $array['object']['id'] )
) {
return;
}
$check_dupe = object_id_to_comment( $activity['object']['id'] );
// check if Activity is public or not
if ( ! is_activity_public( $array ) ) {
// @todo maybe send email
return;
}
$check_dupe = object_id_to_comment( $array['object']['id'] );
// if comment exists, call update action
// If comment exists, call update action.
if ( $check_dupe ) {
\do_action( 'activitypub_inbox_update', $array, $user_id, $object );
/**
* Fires when a Create activity is received for an existing comment.
*
* @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;
}
$state = Interactions::add_comment( $array );
$state = Interactions::add_comment( $activity );
$reaction = null;
if ( $state && ! \is_wp_error( $state ) ) {
$reaction = \get_comment( $state );
}
\do_action( 'activitypub_handled_create', $array, $user_id, $state, $reaction );
/**
* Fires after a Create activity has been handled.
*
* @param array $activity The activity-object.
* @param int $user_id The id of the local blog-user.
* @param \WP_Comment|\WP_Error $state The comment object or WP_Error.
* @param \WP_Comment|\WP_Error|null $reaction The reaction object or WP_Error.
*/
\do_action( 'activitypub_handled_create', $activity, $user_id, $state, $reaction );
}
/**
* Validate the object.
*
* @param bool $valid The validation state.
* @param string $param The object parameter.
* @param \WP_REST_Request $request The request object.
*
* @return bool The validation state: true if valid, false if not.
*/
public static function validate_object( $valid, $param, $request ) {
$json_params = $request->get_json_params();
if (
'Create' !== $json_params['type'] ||
is_wp_error( $request )
) {
return $valid;
}
$object = $json_params['object'];
$required = array(
'id',
'inReplyTo',
'content',
);
if ( array_intersect( $required, array_keys( $object ) ) !== $required ) {
return false;
}
return $valid;
}
}

View File

@ -1,7 +1,12 @@
<?php
/**
* Delete handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler;
use WP_Error;
use WP_REST_Request;
use Activitypub\Http;
use Activitypub\Collection\Followers;
@ -12,7 +17,7 @@ use Activitypub\Collection\Interactions;
*/
class Delete {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_action(
@ -20,7 +25,7 @@ class Delete {
array( self::class, 'handle_delete' )
);
// defer signature verification for `Delete` requests.
// Defer signature verification for `Delete` requests.
\add_filter(
'activitypub_defer_signature_verification',
array( self::class, 'defer_signature_verification' ),
@ -28,7 +33,7 @@ class Delete {
2
);
// side effect
// Side effect.
\add_action(
'activitypub_delete_actor_interactions',
array( self::class, 'delete_interactions' )
@ -39,14 +44,16 @@ class Delete {
* Handles "Delete" requests.
*
* @param array $activity The delete activity.
* @param int $user_id The ID of the user performing the delete activity.
*/
public static function handle_delete( $activity ) {
$object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : '';
switch ( $object_type ) {
// Actor Types
// @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
/*
* Actor Types.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
*/
case 'Person':
case 'Group':
case 'Organization':
@ -54,8 +61,12 @@ class Delete {
case 'Application':
self::maybe_delete_follower( $activity );
break;
// Object and Link Types
// @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
/*
* Object and Link Types.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
*/
case 'Note':
case 'Article':
case 'Image':
@ -65,26 +76,34 @@ class Delete {
case 'Document':
self::maybe_delete_interaction( $activity );
break;
// Tombstone Type
// @see: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
/*
* Tombstone Type.
*
* @see: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
*/
case 'Tombstone':
self::maybe_delete_interaction( $activity );
break;
// Minimal Activity
// @see https://www.w3.org/TR/activitystreams-core/#example-1
/*
* Minimal Activity.
*
* @see https://www.w3.org/TR/activitystreams-core/#example-1
*/
default:
// ignore non Minimal Activities.
// Ignore non Minimal Activities.
if ( ! is_string( $activity['object'] ) ) {
return;
}
// check if Object is an Actor.
// Check if Object is an Actor.
if ( $activity['actor'] === $activity['object'] ) {
self::maybe_delete_follower( $activity );
} else { // assume a interaction otherwise.
} else { // Assume an interaction otherwise.
self::maybe_delete_interaction( $activity );
}
// maybe handle Delete Activity for other Object Types.
// Maybe handle Delete Activity for other Object Types.
break;
}
}
@ -97,7 +116,7 @@ class Delete {
public static function maybe_delete_follower( $activity ) {
$follower = Followers::get_follower_by_actor( $activity['actor'] );
// verify if Actor is deleted.
// Verify that Actor is deleted.
if ( $follower && Http::is_tombstone( $activity['actor'] ) ) {
$follower->delete();
self::maybe_delete_interactions( $activity );
@ -110,7 +129,7 @@ class Delete {
* @param array $activity The delete activity.
*/
public static function maybe_delete_interactions( $activity ) {
// verify if Actor is deleted.
// Verify that Actor is deleted.
if ( Http::is_tombstone( $activity['actor'] ) ) {
\wp_schedule_single_event(
\time(),
@ -123,7 +142,7 @@ class Delete {
/**
* Delete comments from an Actor.
*
* @param array $comments The comments to delete.
* @param array $actor The actor whose comments to delete.
*/
public static function delete_interactions( $actor ) {
$comments = Interactions::get_interactions_by_actor( $actor );
@ -139,8 +158,6 @@ class Delete {
* Delete a Reaction if URL is a Tombstone.
*
* @param array $activity The delete activity.
*
* @return void
*/
public static function maybe_delete_interaction( $activity ) {
if ( is_array( $activity['object'] ) ) {

View File

@ -1,4 +1,10 @@
<?php
/**
* Follow handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler;
use Activitypub\Http;
@ -8,11 +14,11 @@ use Activitypub\Collection\Users;
use Activitypub\Collection\Followers;
/**
* Handle Follow requests
* Handle Follow requests.
*/
class Follow {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_action(
@ -29,23 +35,21 @@ class Follow {
}
/**
* Handle "Follow" requests
* Handle "Follow" requests.
*
* @param array $activity The activity object
* @param int $user_id The user ID
* @param array $activity The activity object.
*/
public static function handle_follow( $activity ) {
$user = Users::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
// If we can not find a user, we can not initiate a follow process.
return;
}
$user_id = $user->get__id();
// save follower
// Save follower.
$follower = Followers::add_follower(
$user_id,
$activity['actor']
@ -59,7 +63,7 @@ class Follow {
$follower
);
// send notification
// Send notification.
$notification = new Notification(
'follow',
$activity['actor'],
@ -70,25 +74,22 @@ class Follow {
}
/**
* Send Accept response
* Send Accept response.
*
* @param string $actor The Actor URL
* @param array $object The Activity object
* @param int $user_id The ID of the WordPress User
* @param Activitypub\Model\Follower $follower The Follower object
*
* @return void
* @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.
*/
public static function send_follow_response( $actor, $object, $user_id, $follower ) {
public static function send_follow_response( $actor, $activity_object, $user_id, $follower ) {
if ( \is_wp_error( $follower ) ) {
// it is not even possible to send a "Reject" because
// we can not get the Remote-Inbox
// Impossible to send a "Reject" because we can not get the Remote-Inbox.
return;
}
// only send minimal data
$object = array_intersect_key(
$object,
// Only send minimal data.
$activity_object = array_intersect_key(
$activity_object,
array_flip(
array(
'id',
@ -101,13 +102,13 @@ class Follow {
$user = Users::get_by_id( $user_id );
// get inbox
// Get inbox.
$inbox = $follower->get_shared_inbox();
// send "Accept" activity
// Send "Accept" activity.
$activity = new Activity();
$activity->set_type( 'Accept' );
$activity->set_object( $object );
$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() );

View File

@ -0,0 +1,70 @@
<?php
/**
* Like handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler;
use Activitypub\Comment;
use Activitypub\Collection\Interactions;
use function Activitypub\object_to_uri;
/**
* Handle Like requests.
*/
class Like {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_action(
'activitypub_inbox_like',
array( self::class, 'handle_like' ),
10,
3
);
}
/**
* Handles "Like" requests.
*
* @param array $like The Activity array.
* @param int $user_id The ID of the local blog user.
*/
public static function handle_like( $like, $user_id ) {
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
return;
}
$url = object_to_uri( $like['object'] );
if ( empty( $url ) ) {
return;
}
$exists = Comment::object_id_to_comment( esc_url_raw( $url ) );
if ( $exists ) {
return;
}
$state = Interactions::add_reaction( $like );
$reaction = null;
if ( $state && ! is_wp_error( $state ) ) {
$reaction = get_comment( $state );
}
/**
* Fires after a Like has been handled.
*
* @param array $like The Activity array.
* @param int $user_id The ID of the local blog user.
* @param mixed $state The state of the reaction.
* @param mixed $reaction The reaction object.
*/
do_action( 'activitypub_handled_like', $like, $user_id, $state, $reaction );
}
}

View File

@ -1,17 +1,24 @@
<?php
/**
* Undo handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler;
use Activitypub\Collection\Users;
use Activitypub\Collection\Followers;
use Activitypub\Comment;
use function Activitypub\object_to_uri;
/**
* Handle Undo requests
* Handle Undo requests.
*/
class Undo {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_action(
@ -21,23 +28,27 @@ class Undo {
}
/**
* Handle "Unfollow" requests
* Handle "Unfollow" requests.
*
* @param array $activity The JSON "Undo" Activity
* @param int $user_id The ID of the ID of the WordPress User
* @param array $activity The JSON "Undo" Activity.
*/
public static function handle_undo( $activity ) {
if (
isset( $activity['object']['type'] ) &&
'Follow' === $activity['object']['type'] &&
isset( $activity['object']['object'] )
! isset( $activity['object']['type'] ) ||
! isset( $activity['object']['object'] )
) {
return;
}
$type = $activity['object']['type'];
// Handle "Unfollow" requests.
if ( 'Follow' === $type ) {
$user_id = object_to_uri( $activity['object']['object'] );
$user = Users::get_by_resource( $user_id );
if ( ! $user || is_wp_error( $user ) ) {
// If we can not find a user,
// we can not initiate a follow process
// If we can not find a user, we can not initiate a follow process.
return;
}
@ -46,5 +57,23 @@ class Undo {
Followers::remove_follower( $user_id, $actor );
}
// Handle "Undo" requests for "Like" and "Create" activities.
if ( in_array( $type, array( 'Like', 'Create', 'Announce' ), true ) ) {
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
return;
}
$object_id = object_to_uri( $activity['object'] );
$comment = Comment::object_id_to_comment( esc_url_raw( $object_id ) );
if ( empty( $comment ) ) {
return;
}
$state = wp_trash_comment( $comment );
do_action( 'activitypub_handled_undo', $activity, $user_id, isset( $state ) ? $state : null, null );
}
}
}

View File

@ -1,7 +1,12 @@
<?php
/**
* Update handler file.
*
* @package Activitypub
*/
namespace Activitypub\Handler;
use WP_Error;
use Activitypub\Collection\Interactions;
use function Activitypub\get_remote_metadata_by_actor;
@ -11,7 +16,7 @@ use function Activitypub\get_remote_metadata_by_actor;
*/
class Update {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_action(
@ -23,24 +28,30 @@ class Update {
/**
* Handle "Update" requests
*
* @param array $array The activity-object
* @param int $user_id The id of the local blog-user
* @param array $activity The activity-object.
*/
public static function handle_update( $array ) {
$object_type = isset( $array['object']['type'] ) ? $array['object']['type'] : '';
public static function handle_update( $activity ) {
$object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : '';
switch ( $object_type ) {
// Actor Types
// @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
/*
* Actor Types.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
*/
case 'Person':
case 'Group':
case 'Organization':
case 'Service':
case 'Application':
self::update_actor( $array );
self::update_actor( $activity );
break;
// Object and Link Types
// @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
/*
* Object and Link Types.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
*/
case 'Note':
case 'Article':
case 'Image':
@ -48,22 +59,23 @@ class Update {
case 'Video':
case 'Event':
case 'Document':
self::update_interaction( $array );
self::update_interaction( $activity );
break;
// Minimal Activity
// @see https://www.w3.org/TR/activitystreams-core/#example-1
/*
* Minimal Activity.
*
* @see https://www.w3.org/TR/activitystreams-core/#example-1
*/
default:
break;
}
}
/**
* Update an Interaction
* Update an Interaction.
*
* @param array $activity The activity-object
* @param int $user_id The id of the local blog-user
*
* @return void
* @param array $activity The activity-object.
*/
public static function update_interaction( $activity ) {
$commentdata = Interactions::update_comment( $activity );
@ -80,16 +92,14 @@ class Update {
}
/**
* Update an Actor
* Update an Actor.
*
* @param array $activity The activity-object
*
* @return void
* @param array $activity The activity-object.
*/
public static function update_actor( $activity ) {
// update cache
// Update cache.
get_remote_metadata_by_actor( $activity['actor'], false );
// @todo maybe also update all interactions
// @todo maybe also update all interactions.
}
}

View File

@ -1,4 +1,9 @@
<?php
/**
* Help file.
*
* @package Activitypub
*/
\get_current_screen()->add_help_tab(
array(
@ -54,17 +59,17 @@
'<p><h2>' . \__( 'Fediverse', 'activitypub' ) . '</h2></p>' .
'<p>' . \__( 'The Fediverse is a new word made of two words: "federation" + "universe"', 'activitypub' ) . '</p>' .
'<p>' . \__( 'It is a federated social network running on free open software on a myriad of computers across the globe. Many independent servers are interconnected and allow people to interact with one another. There\'s no one central site: you choose a server to register. This ensures some decentralization and sovereignty of data. Fediverse (also called Fedi) has no built-in advertisements, no tricky algorithms, no one big corporation dictating the rules. Instead we have small cozy communities of like-minded people. Welcome!', 'activitypub' ) . '</p>' .
'<p>' . \__( 'For more informations please visit <a href="https://fediverse.party/" target="_blank">fediverse.party</a>', 'activitypub' ) . '</p>' .
'<p>' . \__( 'For more information please visit <a href="https://fediverse.party/" target="_blank">fediverse.party</a>', 'activitypub' ) . '</p>' .
'<p><h2>' . \__( 'ActivityPub', 'activitypub' ) . '</h2></p>' .
'<p>' . \__( 'ActivityPub is a decentralized social networking protocol based on the ActivityStreams 2.0 data format. ActivityPub is an official W3C recommended standard published by the W3C Social Web Working Group. It provides a client to server API for creating, updating and deleting content, as well as a federated server to server API for delivering notifications and subscribing to content.', 'activitypub' ) . '</p>' .
'<p><h2>' . \__( 'WebFinger', 'activitypub' ) . '</h2></p>' .
'<p>' . \__( 'WebFinger is used to discover information about people or other entities on the Internet that are identified by a URI using standard Hypertext Transfer Protocol (HTTP) methods over a secure transport. A WebFinger resource returns a JavaScript Object Notation (JSON) object describing the entity that is queried. The JSON object is referred to as the JSON Resource Descriptor (JRD).', 'activitypub' ) . '</p>' .
'<p>' . \__( 'For a person, the type of information that might be discoverable via WebFinger includes a personal profile address, identity service, telephone number, or preferred avatar. For other entities on the Internet, a WebFinger resource might return JRDs containing link relations that enable a client to discover, for example, that a printer can print in color on A4 paper, the physical location of a server, or other static information.', 'activitypub' ) . '</p>' .
'<p>' . \__( 'On Mastodon [and other Plattforms], user profiles can be hosted either locally on the same website as yours, or remotely on a completely different website. The same username may be used on a different domain. Therefore, a Mastodon user\'s full mention consists of both the username and the domain, in the form <code>@username@domain</code>. In practical terms, <code>@user@example.com</code> is not the same as <code>@user@example.org</code>. If the domain is not included, Mastodon will try to find a local user named <code>@username</code>. However, in order to deliver to someone over ActivityPub, the <code>@username@domain</code> mention is not enough mentions must be translated to an HTTPS URI first, so that the remote actor\'s inbox and outbox can be found. (This paragraph is copied from the <a href="https://docs.joinmastodon.org/spec/webfinger/" target="_blank">Mastodon Documentation</a>)', 'activitypub' ) . '</p>' .
'<p>' . \__( 'For more informations please visit <a href="https://webfinger.net/" target="_blank">webfinger.net</a>', 'activitypub' ) . '</p>' .
'<p>' . \__( 'For more information please visit <a href="https://webfinger.net/" target="_blank">webfinger.net</a>', 'activitypub' ) . '</p>' .
'<p><h2>' . \__( 'NodeInfo', 'activitypub' ) . '</h2></p>' .
'<p>' . \__( 'NodeInfo is an effort to create a standardized way of exposing metadata about a server running one of the distributed social networks. The two key goals are being able to get better insights into the user base of distributed social networking and the ability to build tools that allow users to choose the best fitting software and server for their needs.', 'activitypub' ) . '</p>' .
'<p>' . \__( 'For more informations please visit <a href="http://nodeinfo.diaspora.software/" target="_blank">nodeinfo.diaspora.software</a>', 'activitypub' ) . '</p>',
'<p>' . \__( 'For more information please visit <a href="http://nodeinfo.diaspora.software/" target="_blank">nodeinfo.diaspora.software</a>', 'activitypub' ) . '</p>',
)
);

View File

@ -1,4 +1,10 @@
<?php
/**
* Application model file.
*
* @package Activitypub
*/
namespace Activitypub\Model;
use WP_Query;
@ -8,6 +14,9 @@ use Activitypub\Collection\Users;
use function Activitypub\get_rest_url_by_path;
/**
* Application class.
*/
class Application extends Actor {
/**
* The User-ID
@ -17,7 +26,7 @@ class Application extends Actor {
protected $_id = Users::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
/**
* If the User is discoverable.
* Whether the Application is discoverable.
*
* @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
*
@ -28,7 +37,7 @@ class Application extends Actor {
protected $discoverable = false;
/**
* If the User is indexable.
* Whether the Application is indexable.
*
* @context http://joinmastodon.org/ns#indexable
*
@ -39,18 +48,33 @@ class Application extends Actor {
/**
* The WebFinger Resource.
*
* @var string<url>
* @var string
*/
protected $webfinger;
/**
* Returns the type of the object.
*
* @return string The type of the object.
*/
public function get_type() {
return 'Application';
}
/**
* Returns whether the Application manually approves followers.
*
* @return true Whether the Application manually approves followers.
*/
public function get_manually_approves_followers() {
return true;
}
/**
* Returns the ID of the Application.
*
* @return string The ID of the Application.
*/
public function get_id() {
return get_rest_url_by_path( 'application' );
}
@ -73,10 +97,20 @@ class Application extends Actor {
return $this->get_url();
}
/**
* Get the Username.
*
* @return string The Username.
*/
public function get_name() {
return 'application';
}
/**
* Get the preferred username.
*
* @return string The preferred username.
*/
public function get_preferred_username() {
return $this->get_name();
}
@ -87,10 +121,10 @@ class Application extends Actor {
* @return array The User-Icon.
*/
public function get_icon() {
// try site icon first
// Try site icon first.
$icon_id = get_option( 'site_icon' );
// try custom logo second
// Try custom logo second.
if ( ! $icon_id ) {
$icon_id = get_theme_mod( 'custom_logo' );
}
@ -105,7 +139,7 @@ class Application extends Actor {
}
if ( ! $icon_url ) {
// fallback to default icon
// Fallback to default icon.
$icon_url = plugins_url( '/assets/img/wp-logo.png', ACTIVITYPUB_PLUGIN_FILE );
}
@ -131,6 +165,11 @@ class Application extends Actor {
return null;
}
/**
* Get the first published date.
*
* @return string The published date.
*/
public function get_published() {
$first_post = new WP_Query(
array(
@ -176,6 +215,11 @@ class Application extends Actor {
return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
}
/**
* Returns the public key.
*
* @return array The public key.
*/
public function get_public_key() {
return array(
'id' => $this->get_id() . '#main-key',
@ -185,9 +229,9 @@ class Application extends Actor {
}
/**
* Get the User-Description.
* Get the User description.
*
* @return string The User-Description.
* @return string The User description.
*/
public function get_summary() {
return \wpautop(
@ -198,6 +242,11 @@ class Application extends Actor {
);
}
/**
* Returns the canonical URL of the object.
*
* @return string|null The canonical URL of the object.
*/
public function get_canonical_url() {
return \home_url();
}

View File

@ -1,17 +1,27 @@
<?php
/**
* Blog model file.
*
* @package Activitypub
*/
namespace Activitypub\Model;
use WP_Query;
use WP_Error;
use Activitypub\Signature;
use Activitypub\Activity\Actor;
use Activitypub\Collection\Users;
use Activitypub\Collection\Extra_Fields;
use function Activitypub\esc_hashtag;
use function Activitypub\is_single_user;
use function Activitypub\is_user_disabled;
use function Activitypub\is_blog_public;
use function Activitypub\get_rest_url_by_path;
/**
* Blog class.
*/
class Blog extends Actor {
/**
* The Featured-Posts.
@ -55,12 +65,12 @@ class Blog extends Actor {
/**
* The WebFinger Resource.
*
* @var string<url>
* @var string
*/
protected $webfinger;
/**
* If the User is discoverable.
* Whether the User is discoverable.
*
* @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
*
@ -71,7 +81,7 @@ class Blog extends Actor {
protected $discoverable;
/**
* Restrict posting to mods
* Restrict posting to mods.
*
* @see https://join-lemmy.org/docs/contributors/05-federation.html
*
@ -79,18 +89,28 @@ class Blog extends Actor {
*/
protected $posting_restricted_to_mods;
/**
* Whether the User manually approves followers.
*
* @return false
*/
public function get_manually_approves_followers() {
return false;
}
/**
* Whether the User is discoverable.
*
* @return boolean
*/
public function get_discoverable() {
return true;
}
/**
* Get the User-ID.
* Get the User ID.
*
* @return string The User-ID.
* @return string The User ID.
*/
public function get_id() {
return $this->get_url();
@ -112,9 +132,9 @@ class Blog extends Actor {
}
/**
* Get the User-Name.
* Get the Username.
*
* @return string The User-Name.
* @return string The Username.
*/
public function get_name() {
return \wp_strip_all_tags(
@ -127,23 +147,29 @@ class Blog extends Actor {
}
/**
* Get the User-Description.
* Get the User description.
*
* @return string The User-Description.
* @return string The User description.
*/
public function get_summary() {
$summary = \get_option( 'activitypub_blog_description', null );
if ( ! $summary ) {
$summary = \get_bloginfo( 'description' );
}
return \wpautop(
\wp_kses(
\get_bloginfo( 'description' ),
$summary,
'default'
)
);
}
/**
* Get the User-Url.
* Get the User url.
*
* @return string The User-Url.
* @return string The User url.
*/
public function get_url() {
return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() );
@ -164,12 +190,12 @@ class Blog extends Actor {
* @return string The auto-generated Username.
*/
public static function get_default_username() {
// check if domain host has a subdomain
// Check if domain host has a subdomain.
$host = \wp_parse_url( \get_home_url(), \PHP_URL_HOST );
$host = \preg_replace( '/^www\./i', '', $host );
/**
* Filter the default blog username.
* Filters the default blog username.
*
* @param string $host The default username.
*/
@ -177,12 +203,12 @@ class Blog extends Actor {
}
/**
* Get the preferred User-Name.
* Get the preferred Username.
*
* @return string The User-Name.
* @return string The Username.
*/
public function get_preferred_username() {
$username = \get_option( 'activitypub_blog_user_identifier' );
$username = \get_option( 'activitypub_blog_identifier' );
if ( $username ) {
return $username;
@ -192,15 +218,15 @@ class Blog extends Actor {
}
/**
* Get the User-Icon.
* Get the User icon.
*
* @return array The User-Icon.
* @return array The User icon.
*/
public function get_icon() {
// try site icon first
// Try site_logo, falling back to site_icon, first.
$icon_id = get_option( 'site_icon' );
// try custom logo second
// Try custom logo second.
if ( ! $icon_id ) {
$icon_id = get_theme_mod( 'custom_logo' );
}
@ -215,7 +241,7 @@ class Blog extends Actor {
}
if ( ! $icon_url ) {
// fallback to default icon
// Fallback to default icon.
$icon_url = plugins_url( '/assets/img/wp-logo.png', ACTIVITYPUB_PLUGIN_FILE );
}
@ -231,16 +257,32 @@ class Blog extends Actor {
* @return array|null The User-Header-Image.
*/
public function get_image() {
if ( \has_header_image() ) {
$header_image = get_option( 'activitypub_header_image' );
$image_url = null;
if ( $header_image ) {
$image_url = \wp_get_attachment_url( $header_image );
}
if ( ! $image_url && \has_header_image() ) {
$image_url = \get_header_image();
}
if ( $image_url ) {
return array(
'type' => 'Image',
'url' => esc_url( \get_header_image() ),
'url' => esc_url( $image_url ),
);
}
return null;
}
/**
* Get the published date.
*
* @return string The published date.
*/
public function get_published() {
$first_post = new WP_Query(
array(
@ -259,10 +301,20 @@ class Blog extends Actor {
return \gmdate( 'Y-m-d\TH:i:s\Z', $time );
}
/**
* Get the canonical URL.
*
* @return string|null The canonical URL.
*/
public function get_canonical_url() {
return \home_url();
}
/**
* Get the Moderators endpoint.
*
* @return string|null The Moderators endpoint.
*/
public function get_moderators() {
if ( is_single_user() || 'Group' !== $this->get_type() ) {
return null;
@ -271,6 +323,11 @@ class Blog extends Actor {
return get_rest_url_by_path( 'collections/moderators' );
}
/**
* Get attributedTo value.
*
* @return string|null The attributedTo value.
*/
public function get_attributed_to() {
if ( is_single_user() || 'Group' !== $this->get_type() ) {
return null;
@ -279,6 +336,11 @@ class Blog extends Actor {
return get_rest_url_by_path( 'collections/moderators' );
}
/**
* Get the public key information.
*
* @return array The public key.
*/
public function get_public_key() {
return array(
'id' => $this->get_id() . '#main-key',
@ -287,6 +349,11 @@ class Blog extends Actor {
);
}
/**
* Returns whether posting is restricted to mods.
*
* @return bool|null True if posting is restricted to mods, null if not applicable.
*/
public function get_posting_restricted_to_mods() {
if ( 'Group' === $this->get_type() ) {
return true;
@ -331,6 +398,11 @@ class Blog extends Actor {
return get_rest_url_by_path( sprintf( 'actors/%d/following', $this->get__id() ) );
}
/**
* Returns endpoints.
*
* @return array|null The endpoints.
*/
public function get_endpoints() {
$endpoints = null;
@ -361,45 +433,101 @@ class Blog extends Actor {
return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) );
}
/**
* Returns whether the site is indexable.
*
* @return bool Whether the site is indexable.
*/
public function get_indexable() {
if ( \get_option( 'blog_public', 1 ) ) {
if ( is_blog_public() ) {
return true;
} else {
return false;
}
}
/**
* Update the Username.
*
* @param mixed $value The new value.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_name( $value ) {
return \update_option( 'blogname', $value );
}
/**
* Update the User description.
*
* @param mixed $value The new value.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_summary( $value ) {
return \update_option( 'blogdescription', $value );
}
/**
* Update the User icon.
*
* @param mixed $value The new value.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_icon( $value ) {
if ( ! wp_attachment_is_image( $value ) ) {
return false;
}
return \update_option( 'site_icon', $value );
}
/**
* Update the User-Header-Image.
*
* @param mixed $value The new value.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_header( $value ) {
if ( ! wp_attachment_is_image( $value ) ) {
return false;
}
return \update_option( 'activitypub_header_image', $value );
}
/**
* Get the User - Hashtags.
*
* @see https://docs.joinmastodon.org/spec/activitypub/#Hashtag
*
* @return array The User - Hashtags.
*/
public function get_tag() {
$hashtags = array();
$args = array(
'orderby' => 'count',
'order' => 'DESC',
'number' => 10,
);
$tags = get_tags( $args );
foreach ( $tags as $tag ) {
$hashtags[] = array(
'type' => 'Hashtag',
'href' => \get_tag_link( $tag->term_id ),
'name' => esc_hashtag( $tag->name ),
);
}
return $hashtags;
}
/**
* Extend the User-Output with Attachments.
*
* @return array The extended User-Output.
*/
public function get_attachment() {
$array = array();
$array[] = array(
'type' => 'PropertyValue',
'name' => \__( 'Blog', 'activitypub' ),
'value' => \html_entity_decode(
sprintf(
'<a rel="me" title="%s" target="_blank" href="%s">%s</a>',
\esc_attr( \home_url( '/' ) ),
\esc_url( \home_url( '/' ) ),
\wp_parse_url( \home_url( '/' ), \PHP_URL_HOST )
),
\ENT_QUOTES,
'UTF-8'
),
);
// Add support for FEP-fb2a, for more information see FEDERATION.md
$array[] = array(
'type' => 'Link',
'name' => \__( 'Blog', 'activitypub' ),
'href' => \esc_url( \home_url( '/' ) ),
'rel' => array( 'me' ),
);
return $array;
$extra_fields = Extra_Fields::get_actor_fields( $this->_id );
return Extra_Fields::fields_to_attachments( $extra_fields );
}
}

View File

@ -1,13 +1,18 @@
<?php
/**
* Follower class file.
*
* @package Activitypub
*/
namespace Activitypub\Model;
use WP_Error;
use WP_Query;
use Activitypub\Activity\Actor;
use Activitypub\Collection\Followers;
/**
* ActivityPub Follower Class
* ActivityPub Follower Class.
*
* This Object represents a single Follower.
* There is no direct reference to a WordPress User here.
@ -19,9 +24,9 @@ use Activitypub\Collection\Followers;
*/
class Follower extends Actor {
/**
* The complete Remote-Profile of the Follower
* The complete Remote-Profile of the Follower.
*
* @var array
* @var int
*/
protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
@ -31,13 +36,13 @@ class Follower extends Actor {
* @return mixed
*/
public function get_errors() {
return get_post_meta( $this->_id, 'activitypub_errors' );
return get_post_meta( $this->_id, 'activitypub_errors', false );
}
/**
* Get the Summary.
*
* @return int The Summary.
* @return string The Summary.
*/
public function get_summary() {
if ( isset( $this->summary ) ) {
@ -51,7 +56,7 @@ class Follower extends Actor {
* Getter for URL attribute.
*
* Falls back to ID, if no URL is set. This is relevant for
* Plattforms like Lemmy, where the ID is the URL.
* Platforms like Lemmy, where the ID is the URL.
*
* @return string The URL.
*/
@ -65,8 +70,6 @@ class Follower extends Actor {
/**
* Reset (delete) all errors.
*
* @return void
*/
public function reset_errors() {
delete_post_meta( $this->_id, 'activitypub_errors' );
@ -103,21 +106,19 @@ class Follower extends Actor {
}
/**
* Update the current Follower-Object.
*
* @return void
* Update the current Follower object.
*/
public function update() {
$this->save();
}
/**
* Validate the current Follower-Object.
* Validate the current Follower object.
*
* @return boolean True if the verification was successful.
*/
public function is_valid() {
// the minimum required attributes
// The minimum required attributes.
$required_attributes = array(
'id',
'preferredUsername',
@ -136,9 +137,9 @@ class Follower extends Actor {
}
/**
* Save the current Follower-Object.
* Save the current Follower object.
*
* @return int|WP_Error The Post-ID or an WP_Error.
* @return int|WP_Error The post ID or an WP_Error.
*/
public function save() {
if ( ! $this->is_valid() ) {
@ -148,7 +149,7 @@ class Follower extends Actor {
if ( ! $this->get__id() ) {
global $wpdb;
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
$post_id = $wpdb->get_var(
$wpdb->prepare(
"SELECT ID FROM $wpdb->posts WHERE guid=%s",
@ -177,8 +178,7 @@ class Follower extends Actor {
);
if ( ! empty( $post_id ) ) {
// If this is an update, prevent the "followed" date from being
// overwritten by the current date.
// If this is an update, prevent the "followed" date from being overwritten by the current date.
$post = get_post( $post_id );
$args['post_date'] = $post->post_date;
$args['post_date_gmt'] = $post->post_date_gmt;
@ -191,23 +191,22 @@ class Follower extends Actor {
}
/**
* Upsert the current Follower-Object.
* Upsert the current Follower object.
*
* @return int|WP_Error The Post-ID or an WP_Error.
* @return int|WP_Error The post ID or an WP_Error.
*/
public function upsert() {
return $this->save();
}
/**
* Delete the current Follower-Object.
* Delete the current Follower object.
*
* Beware that this os deleting a Follower for ALL users!!!
*
* To delete only the User connection (unfollow)
* @see \Activitypub\Rest\Followers::remove_follower()
*
* @return void
* @see \Activitypub\Rest\Followers::remove_follower()
*/
public function delete() {
wp_delete_post( $this->_id );
@ -215,8 +214,6 @@ class Follower extends Actor {
/**
* Update the post meta.
*
* @return void
*/
protected function get_post_meta_input() {
$meta_input = array();
@ -278,7 +275,7 @@ class Follower extends Actor {
}
/**
* Get the Icon URL (Avatar)
* Get the Icon URL (Avatar).
*
* @return string The URL to the Avatar.
*/
@ -297,7 +294,7 @@ class Follower extends Actor {
}
/**
* Get the Icon URL (Avatar)
* Get the Icon URL (Avatar).
*
* @return string The URL to the Avatar.
*/
@ -333,9 +330,8 @@ class Follower extends Actor {
/**
* Convert a Custom-Post-Type input to an Activitypub\Model\Follower.
*
* @return string The JSON string.
*
* @return array Activitypub\Model\Follower
* @param \WP_Post $post The post object.
* @return \Activitypub\Activity\Base_Object|WP_Error
*/
public static function init_from_cpt( $post ) {
$actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true );
@ -370,10 +366,10 @@ class Follower extends Actor {
if ( $path ) {
if ( \strpos( $name, '@' ) !== false ) {
// expected: https://example.com/@user (default URL pattern)
// Expected: https://example.com/@user (default URL pattern).
$name = \preg_replace( '|^/@?|', '', $path );
} else {
// expected: https://example.com/users/user (default ID pattern)
// Expected: https://example.com/users/user (default ID pattern).
$parts = \explode( '/', $path );
$name = \array_pop( $parts );
}
@ -383,7 +379,7 @@ class Follower extends Actor {
\strpos( $name, 'acct' ) === 0 ||
\strpos( $name, '@' ) === 0
) {
// expected: user@example.com or acct:user@example (WebFinger)
// Expected: user@example.com or acct:user@example (WebFinger).
$name = \ltrim( $name, '@' );
$name = \ltrim( $name, 'acct:' );
$parts = \explode( '@', $name );

View File

@ -1,136 +0,0 @@
<?php
namespace Activitypub\Model;
use Activitypub\Collection\Users;
use Activitypub\Transformer\Factory;
/**
* ActivityPub Post Class
*
* @author Matthias Pfefferle
*/
class Post {
/**
* The \Activitypub\Activity\Base_Object object.
*
* @var \Activitypub\Activity\Base_Object
*/
protected $object;
/**
* The WordPress Post Object.
*
* @var WP_Post
*/
private $post;
/**
* Constructor
*
* @param WP_Post $post
* @param int $post_author
*/
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
public function __construct( $post, $post_author = null ) {
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Transformer\Factory::get_transformer' );
$transformer = Factory::get_transformer( $post );
if ( ! \is_wp_error( $transformer ) ) {
$this->post = $post;
$this->object = $transformer->to_object();
}
}
/**
* Returns the User ID.
*
* @return int the User ID.
*/
public function get_user_id() {
return apply_filters( 'activitypub_post_user_id', $this->post->post_author, $this->post );
}
/**
* Converts this Object into an Array.
*
* @return array the array representation of a Post.
*/
public function to_array() {
return \apply_filters( 'activitypub_post', $this->object->to_array(), $this->post );
}
/**
* Returns the Actor of this Object.
*
* @return string The URL of the Actor.
*/
public function get_actor() {
$user = Users::get_by_id( $this->get_user_id() );
return $user->get_url();
}
/**
* Converts this Object into a JSON String
*
* @return string
*/
public function to_json() {
return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT );
}
/**
* Returns the URL of an Activity Object
*
* @return string
*/
public function get_url() {
return $this->object->get_url();
}
/**
* Returns the ID of an Activity Object
*
* @return string
*/
public function get_id() {
return $this->object->get_id();
}
/**
* Returns a list of Image Attachments
*
* @return array
*/
public function get_attachments() {
return $this->object->get_attachment();
}
/**
* Returns a list of Tags, used in the Post
*
* @return array
*/
public function get_tags() {
return $this->object->get_tag();
}
/**
* Returns the as2 object-type for a given post
*
* @return string the object-type
*/
public function get_object_type() {
return $this->object->get_type();
}
/**
* Returns the content for the ActivityPub Item.
*
* @return string the content
*/
public function get_content() {
return $this->object->get_content();
}
}

View File

@ -1,18 +1,24 @@
<?php
/**
* User model file.
*
* @package Activitypub
*/
namespace Activitypub\Model;
use WP_Query;
use WP_Error;
use Activitypub\Migration;
use Activitypub\Signature;
use Activitypub\Model\Blog;
use Activitypub\Activity\Actor;
use Activitypub\Collection\Users;
use Activitypub\Collection\Extra_Fields;
use function Activitypub\is_blog_public;
use function Activitypub\is_user_disabled;
use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_actor_extra_fields;
/**
* User class.
*/
class User extends Actor {
/**
* The local User-ID (WP_User).
@ -36,7 +42,7 @@ class User extends Actor {
protected $featured;
/**
* If the User is discoverable.
* Whether the User is discoverable.
*
* @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
*
@ -47,7 +53,7 @@ class User extends Actor {
protected $discoverable = true;
/**
* If the User is indexable.
* Whether the User is indexable.
*
* @context http://joinmastodon.org/ns#indexable
*
@ -58,14 +64,26 @@ class User extends Actor {
/**
* The WebFinger Resource.
*
* @var string<url>
* @var string
*/
protected $webfinger;
/**
* The type of the object.
*
* @return string The type of the object.
*/
public function get_type() {
return 'Person';
}
/**
* Generate a User object from a WP_User.
*
* @param int $user_id The user ID.
*
* @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(
@ -82,30 +100,30 @@ class User extends Actor {
}
/**
* Get the User-ID.
* Get the user ID.
*
* @return string The User-ID.
* @return string The user ID.
*/
public function get_id() {
return $this->get_url();
}
/**
* Get the User-Name.
* Get the Username.
*
* @return string The User-Name.
* @return string The Username.
*/
public function get_name() {
return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) );
}
/**
* Get the User-Description.
* Get the User description.
*
* @return string The User-Description.
* @return string The User description.
*/
public function get_summary() {
$description = get_user_meta( $this->_id, 'activitypub_user_description', true );
$description = get_user_option( 'activitypub_description', $this->_id );
if ( empty( $description ) ) {
$description = get_user_meta( $this->_id, 'description', true );
}
@ -113,28 +131,46 @@ class User extends Actor {
}
/**
* Get the User-Url.
* Get the User url.
*
* @return string The User-Url.
* @return string The User url.
*/
public function get_url() {
return \esc_url( \get_author_posts_url( $this->_id ) );
}
/**
* Returns the User-URL with @-Prefix for the username.
* Returns the User URL with @-Prefix for the username.
*
* @return string The User-URL with @-Prefix for the username.
* @return string The User URL with @-Prefix for the username.
*/
public function get_alternate_url() {
return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() );
}
/**
* Get the preferred username.
*
* @return string The preferred username.
*/
public function get_preferred_username() {
return \esc_attr( \get_the_author_meta( 'login', $this->_id ) );
}
/**
* Get the User icon.
*
* @return array The User icon.
*/
public function get_icon() {
$icon = \get_user_option( 'activitypub_icon', $this->_id );
if ( wp_attachment_is_image( $icon ) ) {
return array(
'type' => 'Image',
'url' => esc_url( wp_get_attachment_url( $icon ) ),
);
}
$icon = \esc_url(
\get_avatar_url(
$this->_id,
@ -148,22 +184,47 @@ class User extends Actor {
);
}
/**
* Returns the header image.
*
* @return array|null The header image.
*/
public function get_image() {
if ( \has_header_image() ) {
$image = \esc_url( \get_header_image() );
$header_image = get_user_option( 'activitypub_header_image', $this->_id );
$image_url = null;
if ( ! $header_image && \has_header_image() ) {
$image_url = \get_header_image();
}
if ( $header_image ) {
$image_url = \wp_get_attachment_url( $header_image );
}
if ( $image_url ) {
return array(
'type' => 'Image',
'url' => $image,
'url' => esc_url( $image_url ),
);
}
return null;
}
/**
* Returns the date the user was created.
*
* @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 ) ) );
}
/**
* Returns the public key.
*
* @return array The public key.
*/
public function get_public_key() {
return array(
'id' => $this->get_id() . '#main-key',
@ -217,6 +278,11 @@ class User extends Actor {
return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) );
}
/**
* Returns the endpoints.
*
* @return array|null The endpoints.
*/
public function get_endpoints() {
$endpoints = null;
@ -235,74 +301,8 @@ class User extends Actor {
* @return array The extended User-Output.
*/
public function get_attachment() {
$extra_fields = get_actor_extra_fields( $this->_id );
$attachments = array();
foreach ( $extra_fields as $post ) {
$content = \get_the_content( null, false, $post );
$content = \make_clickable( $content );
$content = \do_blocks( $content );
$content = \wptexturize( $content );
$content = \wp_filter_content_tags( $content );
// replace script and style elements
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
$content = \strip_shortcodes( $content );
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
$attachments[] = array(
'type' => 'PropertyValue',
'name' => \get_the_title( $post ),
'value' => \html_entity_decode(
$content,
\ENT_QUOTES,
'UTF-8'
),
);
$link_added = false;
// Add support for FEP-fb2a, for more information see FEDERATION.md
if ( \class_exists( '\WP_HTML_Tag_Processor' ) ) {
$tags = new \WP_HTML_Tag_Processor( $content );
$tags->next_tag();
if ( 'P' === $tags->get_tag() ) {
$tags->next_tag();
}
if ( 'A' === $tags->get_tag() ) {
$tags->set_bookmark( 'link' );
if ( ! $tags->next_tag() ) {
$tags->seek( 'link' );
$attachment = array(
'type' => 'Link',
'name' => \get_the_title( $post ),
'href' => \esc_url( $tags->get_attribute( 'href' ) ),
'rel' => explode( ' ', $tags->get_attribute( 'rel' ) ),
);
$link_added = true;
}
}
}
if ( ! $link_added ) {
$attachment = array(
'type' => 'Note',
'name' => \get_the_title( $post ),
'content' => \html_entity_decode(
$content,
\ENT_QUOTES,
'UTF-8'
),
);
}
$attachments[] = $attachment;
}
return $attachments;
$extra_fields = Extra_Fields::get_actor_fields( $this->_id );
return Extra_Fields::fields_to_attachments( $extra_fields );
}
/**
@ -314,23 +314,93 @@ class User extends Actor {
return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
}
/**
* Returns the canonical URL.
*
* @return string The canonical URL.
*/
public function get_canonical_url() {
return $this->get_url();
}
/**
* Returns the streams.
*
* @return null The streams.
*/
public function get_streams() {
return null;
}
/**
* Returns the tag.
*
* @return array The tag.
*/
public function get_tag() {
return array();
}
/**
* Returns the indexable state.
*
* @return bool Whether the user is indexable.
*/
public function get_indexable() {
if ( \get_option( 'blog_public', 1 ) ) {
if ( is_blog_public() ) {
return true;
} else {
return false;
}
}
/**
* Update the username.
*
* @param string $value The new value.
* @return int|WP_Error The updated user ID or WP_Error on failure.
*/
public function update_name( $value ) {
$userdata = array(
'ID' => $this->_id,
'display_name' => $value,
);
return \wp_update_user( $userdata );
}
/**
* Update the User description.
*
* @param string $value The new value.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_summary( $value ) {
return \update_user_option( $this->_id, 'activitypub_description', $value );
}
/**
* Update the User icon.
*
* @param int $value The new value. Should be an attachment ID.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_icon( $value ) {
if ( ! wp_attachment_is_image( $value ) ) {
return false;
}
return update_user_option( $this->_id, 'activitypub_icon', $value );
}
/**
* Update the User-Header-Image.
*
* @param int $value The new value. Should be an attachment ID.
* @return bool True if the attribute was updated, false otherwise.
*/
public function update_header( $value ) {
if ( ! wp_attachment_is_image( $value ) ) {
return false;
}
return \update_user_option( $this->_id, 'activitypub_header_image', $value );
}
}

View File

@ -1,18 +1,22 @@
<?php
/**
* ActivityPub Actors REST-Class
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_Error;
use WP_REST_Server;
use WP_REST_Request;
use WP_REST_Response;
use Activitypub\Webfinger;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Users as User_Collection;
use function Activitypub\is_activitypub_request;
/**
* ActivityPub Actors REST-Class
* ActivityPub Actors REST-Class.
*
* @author Matthias Pfefferle
*
@ -20,14 +24,14 @@ use function Activitypub\is_activitypub_request;
*/
class Actors {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
* Register routes.
*/
public static function register_routes() {
\register_rest_route(
@ -65,9 +69,9 @@ class Actors {
/**
* Handle GET request
*
* @param WP_REST_Request $request
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function get( $request ) {
$user_id = $request->get_param( 'user_id' );
@ -77,14 +81,17 @@ class Actors {
return $user;
}
// redirect to canonical URL if it is not an ActivityPub request
$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 triggerd prior to the ActivityPub profile being created and sent to the client
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_users_pre' );
@ -92,17 +99,18 @@ class Actors {
$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
* Endpoint for remote follow UI/Block.
*
* @param WP_REST_Request $request The request object.
*
* @return void|string The URL to the remote follow page
* @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' );
@ -123,15 +131,18 @@ class Actors {
$url = str_replace( '{uri}', $resource, $template );
return new WP_REST_Response(
array( 'url' => $url, 'template' => $template ),
array(
'url' => $url,
'template' => $template,
),
200
);
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters,
*/
public static function request_parameters() {
$params = array();

View File

@ -1,4 +1,10 @@
<?php
/**
* Collections REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_REST_Server;
@ -6,14 +12,17 @@ use WP_REST_Response;
use Activitypub\Activity\Actor;
use Activitypub\Activity\Base_Object;
use Activitypub\Collection\Users as User_Collection;
use Activitypub\Collection\Replies;
use Activitypub\Transformer\Factory;
use WP_Error;
use function Activitypub\esc_hashtag;
use function Activitypub\is_single_user;
use function Activitypub\get_rest_url_by_path;
/**
* ActivityPub Collections REST-Class
* ActivityPub Collections REST-Class.
*
* @author Matthias Pfefferle
*
@ -22,14 +31,14 @@ use function Activitypub\get_rest_url_by_path;
*/
class Collection {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
* Register routes.
*/
public static function register_routes() {
\register_rest_route(
@ -69,14 +78,81 @@ class Collection {
),
)
);
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/(?P<type>[\w\-\.]+)s/(?P<id>[\w\-\.]+)/replies',
array(
array(
'methods' => WP_REST_Server::READABLE,
'callback' => array( self::class, 'replies_get' ),
'args' => self::request_parameters_for_replies(),
'permission_callback' => '__return_true',
),
)
);
}
/**
* The endpoint for replies collections.
*
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function replies_get( $request ) {
$type = $request->get_param( 'type' );
// Get the WordPress object of that "owns" the requested replies.
switch ( $type ) {
case 'comment':
$wp_object = \get_comment( $request->get_param( 'id' ) );
break;
case 'post':
default:
$wp_object = \get_post( $request->get_param( 'id' ) );
break;
}
if ( ! isset( $wp_object ) || is_wp_error( $wp_object ) ) {
return new WP_Error(
'activitypub_replies_collection_does_not_exist',
\sprintf(
// translators: %s: The type (post, comment, etc.) for which no replies collection exists.
\__( 'No reply collection exists for the type %s.', 'activitypub' ),
$type
)
);
}
$page = intval( $request->get_param( 'page' ) );
// If the request parameter page is present get the CollectionPage otherwise the replies collection.
if ( isset( $page ) ) {
$response = Replies::get_collection_page( $wp_object, $page );
} else {
$response = Replies::get_collection( $wp_object );
}
if ( is_wp_error( $response ) ) {
return $response;
}
// Add ActivityPub Context.
$response = array_merge(
array( '@context' => Base_Object::JSON_LD_CONTEXT ),
$response
);
return new WP_REST_Response( $response, 200 );
}
/**
* The Featured Tags endpoint
*
* @param WP_REST_Request $request The request object.
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response 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' );
@ -126,9 +202,9 @@ class Collection {
/**
* Featured posts endpoint
*
* @param WP_REST_Request $request The request object.
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response 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' );
@ -184,13 +260,11 @@ class Collection {
}
/**
* Moderators endpoint
*
* @param WP_REST_Request $request The request object.
* Moderators endpoint.
*
* @return WP_REST_Response The response object.
*/
public static function moderators_get( $request ) {
public static function moderators_get() {
$response = array(
'@context' => Actor::JSON_LD_CONTEXT,
'id' => get_rest_url_by_path( 'collections/moderators' ),
@ -211,9 +285,9 @@ class Collection {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function request_parameters() {
$params = array();
@ -225,4 +299,26 @@ class Collection {
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;
}
}

View File

@ -1,4 +1,10 @@
<?php
/**
* Comment REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_Error;
@ -9,7 +15,7 @@ use Activitypub\Comment as Comment_Utils;
use Activitypub\Webfinger as Webfinger_Utils;
/**
* ActivityPub Followers REST-Class
* ActivityPub Followers REST-Class.
*
* @author Matthias Pfefferle
*
@ -17,14 +23,14 @@ use Activitypub\Webfinger as Webfinger_Utils;
*/
class Comment {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
* Register routes.
*/
public static function register_routes() {
\register_rest_route(
@ -47,11 +53,11 @@ class Comment {
}
/**
* Endpoint for remote follow UI/Block
* Endpoint for remote follow UI/Block.
*
* @param WP_REST_Request $request The request object.
*
* @return void|string The URL to the remote follow page
* @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' );
@ -75,20 +81,19 @@ class Comment {
return $template;
}
$comment_meta = \get_comment_meta( $comment_id );
$resource = Comment_Utils::get_source_id( $comment_id );
if ( ! empty( $comment_meta['source_id'][0] ) ) {
$resource = $comment_meta['source_id'][0];
} elseif ( ! empty( $comment_meta['source_url'][0] ) ) {
$resource = $comment_meta['source_url'][0];
} else {
if ( ! $resource ) {
$resource = Comment_Utils::generate_id( $comment );
}
$url = str_replace( '{uri}', $resource, $template );
return new WP_REST_Response(
array( 'url' => $url, 'template' => $template ),
array(
'url' => $url,
'template' => $template,
),
200
);
}

View File

@ -1,7 +1,12 @@
<?php
/**
* Followers REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_Error;
use stdClass;
use WP_REST_Server;
use WP_REST_Response;
@ -12,7 +17,7 @@ use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_masked_wp_version;
/**
* ActivityPub Followers REST-Class
* ActivityPub Followers REST-Class.
*
* @author Matthias Pfefferle
*
@ -20,14 +25,14 @@ use function Activitypub\get_masked_wp_version;
*/
class Followers {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
* Register routes.
*/
public static function register_routes() {
\register_rest_route(
@ -47,9 +52,9 @@ class Followers {
/**
* Handle GET request
*
* @param WP_REST_Request $request
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function get( $request ) {
$user_id = $request->get_param( 'user_id' );
@ -64,36 +69,34 @@ class Followers {
$page = (int) $request->get_param( 'page' );
$context = $request->get_param( 'context' );
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* 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->totalItems = $data['total']; // phpcs:ignore
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/followers', $user->get__id() ) ); // phpcs:ignore
$json->first = \add_query_arg( 'page', 1, $json->partOf );
$json->last = \add_query_arg( 'page', \ceil( $json->totalItems / $per_page ), $json->partOf );
$json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore
$json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / $per_page ), $json->partOf ); // phpcs:ignore
if ( $page && ( ( \ceil ( $json->totalItems / $per_page ) ) > $page ) ) { // phpcs:ignore
$json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore
if ( $page && ( ( \ceil( $json->totalItems / $per_page ) ) > $page ) ) {
$json->next = \add_query_arg( 'page', $page + 1, $json->partOf );
}
if ( $page && ( $page > 1 ) ) { // phpcs:ignore
$json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore
if ( $page && ( $page > 1 ) ) {
$json->prev = \add_query_arg( 'page', $page - 1, $json->partOf );
}
// phpcs:ignore
$json->orderedItems = array_map(
function ( $item ) use ( $context ) {
if ( 'full' === $context ) {
@ -103,6 +106,7 @@ class Followers {
},
$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' ) );
@ -111,9 +115,9 @@ class Followers {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function request_parameters() {
$params = array();

View File

@ -1,4 +1,10 @@
<?php
/**
* ActivityPub Following REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_REST_Response;
@ -9,7 +15,7 @@ use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_masked_wp_version;
/**
* ActivityPub Following REST-Class
* ActivityPub Following REST-Class.
*
* @author Matthias Pfefferle
*
@ -17,7 +23,7 @@ use function Activitypub\get_masked_wp_version;
*/
class Following {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
@ -46,9 +52,9 @@ class Following {
/**
* Handle GET request
*
* @param WP_REST_Request $request
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response
* @return WP_REST_Response|\WP_Error The response object or WP_Error.
*/
public static function get( $request ) {
$user_id = $request->get_param( 'user_id' );
@ -58,8 +64,8 @@ class Following {
return $user;
}
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_following_pre' );
@ -67,19 +73,25 @@ class Following {
$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() ) );
$json->partOf = get_rest_url_by_path( sprintf( 'actors/%d/following', $user->get__id() ) ); // phpcs:ignore
/**
* 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 );
$items = apply_filters( 'activitypub_rest_following', array(), $user ); // phpcs:ignore
$json->totalItems = is_countable( $items ) ? count( $items ) : 0; // phpcs:ignore
$json->orderedItems = $items; // phpcs:ignore
$json->first = $json->partOf; // phpcs:ignore
$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' ) );
@ -88,9 +100,9 @@ class Following {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function request_parameters() {
$params = array();
@ -111,22 +123,22 @@ class Following {
* Add the Blog Authors to the following list of the Blog Actor
* if Blog not in single mode.
*
* @param array $array The array of following urls.
* @param User $user The user object.
* @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( $array, $user ) {
public static function default_following( $follow_list, $user ) {
if ( 0 !== $user->get__id() || is_single_user() ) {
return $array;
return $follow_list;
}
$users = User_Collection::get_collection();
foreach ( $users as $user ) {
$array[] = $user->get_url();
$follow_list[] = $user->get_url();
}
return $array;
return $follow_list;
}
}

View File

@ -1,21 +1,25 @@
<?php
/**
* Inbox REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_Error;
use WP_REST_Server;
use WP_REST_Response;
use Activitypub\Activity\Activity;
use Activitypub\Collection\Users as User_Collection;
use function Activitypub\get_context;
use function Activitypub\object_to_uri;
use function Activitypub\url_to_authorid;
use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_masked_wp_version;
use function Activitypub\extract_recipients_from_activity;
/**
* ActivityPub Inbox REST-Class
* ActivityPub Inbox REST-Class.
*
* @author Matthias Pfefferle
*
@ -23,14 +27,14 @@ use function Activitypub\extract_recipients_from_activity;
*/
class Inbox {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
* Register routes.
*/
public static function register_routes() {
\register_rest_route(
@ -67,10 +71,10 @@ class Inbox {
}
/**
* Renders the user-inbox
* Renders the user-inbox.
*
* @param WP_REST_Request $request
* @return WP_REST_Response
* @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' );
@ -80,29 +84,33 @@ class Inbox {
return $user;
}
$page = $request->get_param( 'page', 0 );
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* 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() ) ); // phpcs:ignore
$json->totalItems = 0; // phpcs:ignore
$json->orderedItems = array(); // phpcs:ignore
$json->first = $json->partOf; // phpcs:ignore
$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 output
/**
* Filter the ActivityPub inbox array.
*
* @param array $json The ActivityPub inbox array.
*/
$json = \apply_filters( 'activitypub_rest_inbox_array', $json );
/*
* Action triggerd after the ActivityPub profile has been created and sent to the client
/**
* Action triggered after the ActivityPub profile has been created and sent to the client.
*/
\do_action( 'activitypub_inbox_post' );
@ -113,11 +121,11 @@ class Inbox {
}
/**
* Handles user-inbox requests
* Handles user-inbox requests.
*
* @param WP_REST_Request $request
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response
* @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' );
@ -132,7 +140,23 @@ class Inbox {
$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 );
@ -142,9 +166,9 @@ class Inbox {
}
/**
* The shared inbox
* The shared inbox.
*
* @param WP_REST_Request $request
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response
*/
@ -154,7 +178,23 @@ class Inbox {
$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 );
@ -164,9 +204,9 @@ class Inbox {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function user_inbox_get_parameters() {
$params = array();
@ -184,17 +224,13 @@ class Inbox {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function user_inbox_post_parameters() {
$params = array();
$params['page'] = array(
'type' => 'integer',
);
$params['user_id'] = array(
'required' => true,
'type' => 'string',
@ -207,70 +243,42 @@ class Inbox {
$params['actor'] = array(
'required' => true,
'sanitize_callback' => function ( $param, $request, $key ) {
return object_to_uri( $param );
},
'sanitize_callback' => '\Activitypub\object_to_uri',
);
$params['type'] = array(
'required' => true,
//'type' => 'enum',
//'enum' => array( 'Create' ),
//'sanitize_callback' => function ( $param, $request, $key ) {
// return \strtolower( $param );
//},
);
$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
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function shared_inbox_post_parameters() {
$params = array();
$params['page'] = array(
'type' => 'integer',
);
$params['id'] = array(
'required' => true,
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
);
$params['actor'] = array(
'required' => true,
//'type' => array( 'object', 'string' ),
'sanitize_callback' => function ( $param, $request, $key ) {
return object_to_uri( $param );
},
);
$params['type'] = array(
'required' => true,
//'type' => 'enum',
//'enum' => array( 'Create' ),
//'sanitize_callback' => function ( $param, $request, $key ) {
// return \strtolower( $param );
//},
);
$params['object'] = array(
'required' => true,
//'type' => 'object',
);
$params = self::user_inbox_post_parameters();
$params['to'] = array(
'required' => false,
'sanitize_callback' => function ( $param, $request, $key ) {
'sanitize_callback' => function ( $param ) {
if ( \is_string( $param ) ) {
$param = array( $param );
}
@ -280,7 +288,7 @@ class Inbox {
);
$params['cc'] = array(
'sanitize_callback' => function ( $param, $request, $key ) {
'sanitize_callback' => function ( $param ) {
if ( \is_string( $param ) ) {
$param = array( $param );
}
@ -290,7 +298,7 @@ class Inbox {
);
$params['bcc'] = array(
'sanitize_callback' => function ( $param, $request, $key ) {
'sanitize_callback' => function ( $param ) {
if ( \is_string( $param ) ) {
$param = array( $param );
}
@ -303,11 +311,11 @@ class Inbox {
}
/**
* Get local user recipients
* Get local user recipients.
*
* @param array $data
* @param array $data The data array.
*
* @return array The list of local users
* @return array The list of local users.
*/
public static function get_recipients( $data ) {
$recipients = extract_recipients_from_activity( $data );

View File

@ -0,0 +1,118 @@
<?php
/**
* ActivityPub Interaction REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_REST_Response;
use Activitypub\Http;
/**
* Interaction class.
*/
class Interaction {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
}
/**
* Register routes
*/
public static function register_routes() {
\register_rest_route(
ACTIVITYPUB_REST_NAMESPACE,
'/interactions',
array(
array(
'methods' => \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 ),
)
);
}
}

View File

@ -1,4 +1,10 @@
<?php
/**
* NodeInfo REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_REST_Response;
@ -9,7 +15,7 @@ use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_masked_wp_version;
/**
* ActivityPub NodeInfo REST-Class
* ActivityPub NodeInfo REST-Class.
*
* @author Matthias Pfefferle
*
@ -17,7 +23,7 @@ use function Activitypub\get_masked_wp_version;
*/
class Nodeinfo {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
@ -65,15 +71,13 @@ class Nodeinfo {
}
/**
* Render NodeInfo file
* Render NodeInfo file.
*
* @param WP_REST_Request $request
*
* @return WP_REST_Response
* @return WP_REST_Response The JSON profile of the NodeInfo.
*/
public static function nodeinfo( $request ) {
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
public static function nodeinfo() {
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_nodeinfo_pre' );
@ -116,15 +120,13 @@ class Nodeinfo {
}
/**
* Render NodeInfo file
* Render NodeInfo file.
*
* @param WP_REST_Request $request
*
* @return WP_REST_Response
* @return WP_REST_Response The JSON profile of the NodeInfo.
*/
public static function nodeinfo2( $request ) {
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
public static function nodeinfo2() {
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_nodeinfo2_pre' );
@ -163,13 +165,11 @@ class Nodeinfo {
}
/**
* Render NodeInfo discovery file
*
* @param WP_REST_Request $request
* Render NodeInfo discovery file.
*
* @return WP_REST_Response
*/
public static function discovery( $request ) {
public static function discovery() {
$discovery = array();
$discovery['links'] = array(
array(

View File

@ -1,8 +1,13 @@
<?php
/**
* Outbox REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use stdClass;
use WP_Error;
use WP_REST_Server;
use WP_REST_Response;
use Activitypub\Activity\Activity;
@ -14,7 +19,7 @@ use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_masked_wp_version;
/**
* ActivityPub Outbox REST-Class
* ActivityPub Outbox REST-Class.
*
* @author Matthias Pfefferle
*
@ -22,7 +27,7 @@ use function Activitypub\get_masked_wp_version;
*/
class Outbox {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
self::register_routes();
@ -49,8 +54,8 @@ class Outbox {
/**
* Renders the user-outbox
*
* @param WP_REST_Request $request
* @return WP_REST_Response
* @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' );
@ -64,41 +69,43 @@ class Outbox {
$page = $request->get_param( 'page', 1 );
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* 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 ) ); // phpcs:ignore
$json->totalItems = 0; // phpcs:ignore
$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 ); // phpcs:ignore
$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 ); // phpcs:ignore
$json->totalItems += \intval( $count_posts->publish );
}
}
$json->first = \add_query_arg( 'page', 1, $json->partOf ); // phpcs:ignore
$json->last = \add_query_arg( 'page', \ceil ( $json->totalItems / 10 ), $json->partOf ); // phpcs:ignore
$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 ) ) { // phpcs:ignore
$json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore
if ( $page && ( ( \ceil( $json->totalItems / 10 ) ) > $page ) ) {
$json->next = \add_query_arg( 'page', $page + 1, $json->partOf );
}
if ( $page && ( $page > 1 ) ) { // phpcs:ignore
$json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore
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(
@ -121,15 +128,19 @@ class Outbox {
$activity = new Activity();
$activity->set_type( 'Create' );
$activity->set_object( $post );
$json->orderedItems[] = $activity->to_array( false ); // phpcs:ignore
$json->orderedItems[] = $activity->to_array( false ); // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase
}
}
// filter output
/**
* Filter the ActivityPub outbox array.
*
* @param array $json The ActivityPub outbox array.
*/
$json = \apply_filters( 'activitypub_rest_outbox_array', $json );
/*
* Action triggerd after the ActivityPub profile has been created and sent to the client
/**
* Action triggered after the ActivityPub profile has been created and sent to the client
*/
\do_action( 'activitypub_outbox_post' );
@ -140,9 +151,9 @@ class Outbox {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
* @return array List of parameters.
*/
public static function request_parameters() {
$params = array();

View File

@ -1,14 +1,19 @@
<?php
/**
* Server REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use stdClass;
use WP_Error;
use WP_REST_Response;
use Activitypub\Signature;
use Activitypub\Model\Application;
/**
* ActivityPub Server REST-Class
* ActivityPub Server REST-Class.
*
* @author Django Doucet
*
@ -16,11 +21,12 @@ use Activitypub\Model\Application;
*/
class Server {
/**
* Initialize the class, registering WordPress hooks
* 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 );
}
@ -65,10 +71,10 @@ class Server {
* @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.
* @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 Request used to generate the response.
*
* @return mixed|WP_Error The response, error, or modified response.
*/
@ -77,9 +83,13 @@ class Server {
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
// 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' ) ||
@ -90,13 +100,13 @@ class Server {
}
/**
* Filter to defer signature verification
* Filter to defer signature verification.
*
* Skip signature verification for debugging purposes or to reduce load for
* certain Activity-Types, like "Delete".
*
* @param bool $defer Whether to defer signature verification.
* @param WP_REST_Request $request The request used to generate the response.
* @param \WP_REST_Request $request The request used to generate the response.
*
* @return bool Whether to defer signature verification.
*/
@ -107,9 +117,9 @@ class Server {
}
if (
// POST-Requests are always signed
// POST-Requests are always signed.
'GET' !== $request->get_method() ||
// GET-Requests only require a signature in secure mode
// GET-Requests only require a signature in secure mode.
( 'GET' === $request->get_method() && ACTIVITYPUB_AUTHORIZED_FETCH )
) {
$verified_request = Signature::verify_http_signature( $request );
@ -124,4 +134,51 @@ class Server {
return $response;
}
/**
* Callback function to validate incoming ActivityPub requests
*
* @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.
*
* @return mixed|WP_Error The response, error, or modified response.
*/
public static function validate_activitypub_requests( $response, $handler, $request ) {
if ( 'HEAD' === $request->get_method() ) {
return $response;
}
$route = $request->get_route();
if (
\is_wp_error( $response ) ||
! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE )
) {
return $response;
}
$params = $request->get_json_params();
// Type is required for ActivityPub requests, so it fail later in the process.
if ( ! isset( $params['type'] ) ) {
return $response;
}
if (
ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS &&
in_array( $params['type'], array( 'Create', 'Like', 'Announce' ), true )
) {
return new WP_Error(
'activitypub_server_does_not_accept_incoming_interactions',
\__( 'This server does not accept incoming interactions.', 'activitypub' ),
// We have to use a 2XX status code here, because otherwise the response will be
// treated as an error and Mastodon might block this WordPress instance.
array( 'status' => 202 )
);
}
return $response;
}
}

View File

@ -1,12 +1,16 @@
<?php
/**
* WebFinger REST-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Rest;
use WP_Error;
use WP_REST_Response;
use Activitypub\Collection\Users as User_Collection;
/**
* ActivityPub WebFinger REST-Class
* ActivityPub WebFinger REST-Class.
*
* @author Matthias Pfefferle
*
@ -15,8 +19,6 @@ use Activitypub\Collection\Users as User_Collection;
class Webfinger {
/**
* Initialize the class, registering WordPress hooks.
*
* @return void
*/
public static function init() {
self::register_routes();
@ -24,8 +26,6 @@ class Webfinger {
/**
* Register routes.
*
* @return void
*/
public static function register_routes() {
\register_rest_route(
@ -45,13 +45,13 @@ class Webfinger {
/**
* WebFinger endpoint.
*
* @param WP_REST_Request $request The request object.
* @param \WP_REST_Request $request The request object.
*
* @return WP_REST_Response The response object.
*/
public static function webfinger( $request ) {
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client.
*/
\do_action( 'activitypub_rest_webfinger_pre' );
@ -80,7 +80,7 @@ class Webfinger {
}
/**
* The supported parameters
* The supported parameters.
*
* @return array list of parameters
*/
@ -99,47 +99,17 @@ class Webfinger {
/**
* Get the WebFinger profile.
*
* @param string $resource the WebFinger resource.
* @param string $webfinger the WebFinger resource.
*
* @return array the WebFinger profile.
* @return array|\WP_Error The WebFinger profile or WP_Error if not found.
*/
public static function get_profile( $resource ) {
$user = User_Collection::get_by_resource( $resource );
if ( \is_wp_error( $user ) ) {
return $user;
}
$aliases = array(
$user->get_url(),
$user->get_alternate_url(),
);
$aliases = array_unique( $aliases );
$profile = array(
'subject' => sprintf( 'acct:%s', $user->get_webfinger() ),
'aliases' => array_values( array_unique( $aliases ) ),
'links' => array(
array(
'rel' => 'self',
'type' => 'application/activity+json',
'href' => $user->get_url(),
),
array(
'rel' => 'http://webfinger.net/rel/profile-page',
'type' => 'text/html',
'href' => $user->get_url(),
),
),
);
if ( 'Person' !== $user->get_type() ) {
$profile['links'][0]['properties'] = array(
'https://www.w3.org/ns/activitystreams#type' => $user->get_type(),
);
}
return $profile;
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 );
}
}

View File

@ -1,4 +1,10 @@
<?php
/**
* Followers Table-Class file.
*
* @package Activitypub
*/
namespace Activitypub\Table;
use WP_List_Table;
@ -11,9 +17,20 @@ if ( ! \class_exists( '\WP_List_Table' ) ) {
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
}
/**
* Followers Table-Class.
*/
class Followers extends WP_List_Table {
/**
* User ID.
*
* @var int
*/
private $user_id;
/**
* Constructor.
*/
public function __construct() {
if ( get_current_screen()->id === 'settings_page_activitypub' ) {
$this->user_id = Users::BLOG_USER_ID;
@ -30,6 +47,11 @@ class Followers extends WP_List_Table {
);
}
/**
* Get columns.
*
* @return array
*/
public function get_columns() {
return array(
'cb' => '<input type="checkbox" />',
@ -42,16 +64,22 @@ class Followers extends WP_List_Table {
);
}
/**
* Returns sortable columns.
*
* @return array
*/
public function get_sortable_columns() {
$sortable_columns = array(
return array(
'post_title' => array( 'post_title', true ),
'modified' => array( 'modified', false ),
'published' => array( 'published', false ),
);
return $sortable_columns;
}
/**
* Prepare items.
*/
public function prepare_items() {
$columns = $this->get_columns();
$hidden = array();
@ -64,26 +92,22 @@ class Followers extends WP_List_Table {
$args = array();
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
// phpcs:disable WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['orderby'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$args['orderby'] = sanitize_text_field( wp_unslash( $_GET['orderby'] ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['order'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$args['order'] = sanitize_text_field( wp_unslash( $_GET['order'] ) );
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ( isset( $_GET['s'] ) && isset( $_REQUEST['_wpnonce'] ) ) {
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) );
if ( wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$args['s'] = sanitize_text_field( wp_unslash( $_GET['s'] ) );
}
}
// phpcs:enable WordPress.Security.NonceVerification.Recommended
$followers_with_count = FollowerCollection::get_followers_with_count( $this->user_id, $per_page, $page_num, $args );
$followers = $followers_with_count['followers'];
@ -113,12 +137,24 @@ class Followers extends WP_List_Table {
}
}
/**
* Returns bulk actions.
*
* @return array
*/
public function get_bulk_actions() {
return array(
'delete' => __( 'Delete', 'activitypub' ),
);
}
/**
* Column default.
*
* @param array $item Item.
* @param string $column_name Column name.
* @return string
*/
public function column_default( $item, $column_name ) {
if ( ! array_key_exists( $column_name, $item ) ) {
return __( 'None', 'activitypub' );
@ -126,6 +162,12 @@ class Followers extends WP_List_Table {
return $item[ $column_name ];
}
/**
* Column avatar.
*
* @param array $item Item.
* @return string
*/
public function column_avatar( $item ) {
return sprintf(
'<img src="%s" width="25px;" />',
@ -133,45 +175,63 @@ class Followers extends WP_List_Table {
);
}
/**
* Column url.
*
* @param array $item Item.
* @return string
*/
public function column_url( $item ) {
return sprintf(
'<a href="%s" target="_blank">%s</a>',
$item['url'],
esc_url( $item['url'] ),
$item['url']
);
}
/**
* Column cb.
*
* @param array $item Item.
* @return string
*/
public function column_cb( $item ) {
return sprintf( '<input type="checkbox" name="followers[]" value="%s" />', esc_attr( $item['identifier'] ) );
}
/**
* Process action.
*/
public function process_action() {
if ( ! isset( $_REQUEST['followers'] ) || ! isset( $_REQUEST['_wpnonce'] ) ) {
return false;
return;
}
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) );
if ( ! wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
return false;
return;
}
if ( ! current_user_can( 'edit_user', $this->user_id ) ) {
return false;
return;
}
$followers = $_REQUEST['followers']; // phpcs:ignore
$followers = $_REQUEST['followers']; // phpcs:ignore WordPress.Security.ValidatedSanitizedInput
switch ( $this->current_action() ) {
case 'delete':
if ( $this->current_action() === 'delete' ) {
if ( ! is_array( $followers ) ) {
$followers = array( $followers );
}
foreach ( $followers as $follower ) {
FollowerCollection::remove_follower( $this->user_id, $follower );
}
break;
}
}
/**
* Returns user count.
*
* @return int
*/
public function get_user_count() {
return FollowerCollection::count_followers( $this->user_id );
}

View File

@ -1,10 +1,14 @@
<?php
/**
* Attachment Transformer Class file.
*
* @package Activitypub
*/
namespace Activitypub\Transformer;
use Activitypub\Transformer\Post;
/**
* WordPress Attachment Transformer
* WordPress Attachment Transformer.
*
* The Attachment Transformer is responsible for transforming a WP_Post object into different other
* Object-Types.
@ -22,6 +26,7 @@ class Attachment extends Post {
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 = '';
switch ( $media_type ) {
case 'audio':

View File

@ -1,15 +1,21 @@
<?php
/**
* Base Transformer Class file.
*
* @package Activitypub
*/
namespace Activitypub\Transformer;
use WP_Error;
use WP_Post;
use WP_Comment;
use Activitypub\Activity\Activity;
use Activitypub\Activity\Base_Object;
use Activitypub\Collection\Replies;
/**
* WordPress Base Transformer
* WordPress Base Transformer.
*
* Transformers are responsible for transforming a WordPress objects into different ActivityPub
* Object-Types or Activities.
@ -29,18 +35,18 @@ abstract class Base {
*
* This helps to chain the output of the Transformer.
*
* @param WP_Post|WP_Comment $wp_object The WordPress object
* @param WP_Post|WP_Comment $wp_object The WordPress object.
*
* @return Base
*/
public static function transform( $object ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
return new static( $object );
public static function transform( $wp_object ) {
return new static( $wp_object );
}
/**
* Base constructor.
*
* @param WP_Post|WP_Comment $wp_object The WordPress object
* @param WP_Post|WP_Comment $wp_object The WordPress object.
*/
public function __construct( $wp_object ) {
$this->wp_object = $wp_object;
@ -49,9 +55,9 @@ abstract class Base {
/**
* Transform all properties with available get(ter) functions.
*
* @param Base_Object|object $object
* @param Base_Object|object $activitypub_object The ActivityPub Object.
*
* @return Base_Object|object $object
* @return Base_Object|object
*/
protected function transform_object_properties( $activitypub_object ) {
$vars = $activitypub_object->get_object_var_keys();
@ -75,13 +81,12 @@ abstract class Base {
/**
* Transform the WordPress Object into an ActivityPub Object.
*
* @return Activitypub\Activity\Base_Object
* @return Base_Object|object The ActivityPub Object.
*/
public function to_object() {
$activitypub_object = new Base_Object();
$activitypub_object = $this->transform_object_properties( $activitypub_object );
return $activitypub_object;
return $this->transform_object_properties( $activitypub_object );
}
/**
@ -89,7 +94,7 @@ abstract class Base {
*
* @param string $type The Activity-Type.
*
* @return \Activitypub\Activity\Activity The Activity.
* @return Activity The Activity.
*/
public function to_activity( $type ) {
$object = $this->to_object();
@ -100,7 +105,7 @@ abstract class Base {
// Pre-fill the Activity with data (for example cc and to).
$activity->set_object( $object );
// Use simple Object (only ID-URI) for Like and Announce
// Use simple Object (only ID-URI) for Like and Announce.
if ( in_array( $type, array( 'Like', 'Announce' ), true ) ) {
$activity->set_object( $object->get_id() );
}
@ -108,17 +113,27 @@ abstract class Base {
return $activity;
}
/**
* Get the ID of the WordPress Object.
*/
abstract protected function get_id();
/**
* Get the replies Collection.
*/
public function get_replies() {
return Replies::get_collection( $this->wp_object );
}
/**
* Returns the ID of the WordPress Object.
*
* @return int The ID of the WordPress Object
*/
abstract public function get_wp_user_id();
/**
* Change the User-ID of the WordPress Post.
*
* @return int The User-ID of the WordPress Post
* @param int $user_id The new user ID.
*/
abstract public function change_wp_user_id( $user_id );
}

View File

@ -1,21 +1,23 @@
<?php
namespace Activitypub\Transformer;
/**
* WordPress Comment Transformer file.
*
* @package Activitypub
*/
use WP_Comment;
use WP_Comment_Query;
namespace Activitypub\Transformer;
use Activitypub\Webfinger;
use Activitypub\Comment as Comment_Utils;
use Activitypub\Model\Blog;
use Activitypub\Collection\Users;
use Activitypub\Transformer\Base;
use function Activitypub\is_single_user;
use function Activitypub\get_rest_url_by_path;
use function Activitypub\get_comment_ancestors;
/**
* WordPress Comment Transformer
* WordPress Comment Transformer.
*
* The Comment Transformer is responsible for transforming a WP_Comment object into different
* Object-Types.
@ -37,18 +39,18 @@ class Comment extends Base {
/**
* Change the User-ID of the WordPress Comment.
*
* @return int 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;
}
/**
* Transforms the WP_Comment object to an ActivityPub Object
* Transforms the WP_Comment object to an ActivityPub Object.
*
* @see \Activitypub\Activity\Base_Object
*
* @return \Activitypub\Activity\Base_Object The ActivityPub Object
* @return \Activitypub\Activity\Base_Object The ActivityPub Object.
*/
public function to_object() {
$comment = $this->wp_object;
@ -106,41 +108,49 @@ class Comment extends Base {
* @return string The content.
*/
protected function get_content() {
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$comment = $this->wp_object;
$content = $comment->comment_content;
/**
* Filter the content of the comment.
*
* @param string $content The content of the comment.
* @param \WP_Comment $comment The comment object.
* @param array $args The arguments.
*
* @return string The filtered content of the comment.
*/
$content = \apply_filters( 'comment_text', $content, $comment, array() );
$content = \preg_replace( '/[\n\r\t]/', '', $content );
$content = \trim( $content );
$content = \apply_filters( 'activitypub_the_content', $content, $comment );
return $content;
/**
* Filter the content of the comment.
*
* @param string $content The content of the comment.
* @param \WP_Comment $comment The comment object.
*
* @return string The filtered content of the comment.
*/
return \apply_filters( 'activitypub_the_content', $content, $comment );
}
/**
* Returns the in-reply-to for the ActivityPub Item.
*
* @return int The URL of the in-reply-to.
* @return false|string|null The URL of the in-reply-to.
*/
protected function get_in_reply_to() {
$comment = $this->wp_object;
$parent_comment = null;
$in_reply_to = null;
if ( $comment->comment_parent ) {
$parent_comment = \get_comment( $comment->comment_parent );
}
if ( $parent_comment ) {
$comment_meta = \get_comment_meta( $parent_comment->comment_ID );
if ( ! empty( $comment_meta['source_id'][0] ) ) {
$in_reply_to = $comment_meta['source_id'][0];
} elseif ( ! empty( $comment_meta['source_url'][0] ) ) {
$in_reply_to = $comment_meta['source_url'][0];
} elseif ( ! empty( $parent_comment->user_id ) ) {
$in_reply_to = Comment_Utils::get_source_id( $parent_comment->comment_ID );
if ( ! $in_reply_to && ! empty( $parent_comment->user_id ) ) {
$in_reply_to = Comment_Utils::generate_id( $parent_comment );
}
} else {
@ -216,6 +226,15 @@ class Comment extends Base {
protected function get_mentions() {
\add_filter( 'activitypub_extract_mentions', array( $this, 'extract_reply_context' ) );
/**
* Filter the mentions in the comment.
*
* @param array $mentions The list of mentions.
* @param string $content The content of the comment.
* @param \WP_Comment $comment The comment object.
*
* @return array The filtered list of mentions.
*/
return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_object->comment_content, $this->wp_object );
}
@ -227,7 +246,7 @@ class Comment extends Base {
protected function get_comment_ancestors() {
$ancestors = get_comment_ancestors( $this->wp_object );
// Now that we have the full tree of ancestors, only return the ones received from the fediverse
// Now that we have the full tree of ancestors, only return the ones received from the fediverse.
return array_filter(
$ancestors,
function ( $comment_id ) {
@ -240,12 +259,12 @@ 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 The already mentioned ActivityPub users.
*
* @return array The list of all Repliers.
*/
public function extract_reply_context( $mentions ) {
// Check if `$this->wp_object` is a WP_Comment
// Check if `$this->wp_object` is a WP_Comment.
if ( 'WP_Comment' !== get_class( $this->wp_object ) ) {
return $mentions;
}
@ -283,7 +302,7 @@ class Comment extends Base {
*
* @param string $lang The locale of the comment.
* @param int $comment_id The comment ID.
* @param WP_Post $post The comment object.
* @param \WP_Post $post The comment object.
*
* @return string The filtered locale of the comment.
*/

View File

@ -1,26 +1,31 @@
<?php
/**
* Transformer Factory Class file.
*
* @package Activitypub
*/
namespace Activitypub\Transformer;
use WP_Error;
use Activitypub\Transformer\Base;
use Activitypub\Transformer\Post;
use Activitypub\Transformer\Comment;
use Activitypub\Transformer\Attachment;
/**
* Transformer Factory
* Transformer Factory.
*/
class Factory {
/**
* @param mixed $object The object to transform
* @return \Activitypub\Transformer|\WP_Error The transformer to use, or an error.
* Get the transformer for a given object.
*
* @param mixed $data The object to transform.
*
* @return Base|WP_Error The transformer to use, or an error.
*/
public static function get_transformer( $object ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
if ( ! \is_object( $object ) ) {
public static function get_transformer( $data ) {
if ( ! \is_object( $data ) ) {
return new WP_Error( 'invalid_object', __( 'Invalid object', 'activitypub' ) );
}
$class = \get_class( $object );
$class = \get_class( $data );
/**
* Filter the transformer for a given object.
@ -46,12 +51,12 @@ class Factory {
* }, 10, 3 );
*
* @param Base $transformer The transformer to use.
* @param mixed $object The object to transform.
* @param mixed $data The object to transform.
* @param string $object_class The class of the object to transform.
*
* @return mixed The transformer to use.
*/
$transformer = \apply_filters( 'activitypub_transformer', null, $object, $class );
$transformer = \apply_filters( 'activitypub_transformer', null, $data, $class );
if ( $transformer ) {
if (
@ -64,15 +69,15 @@ class Factory {
return $transformer;
}
// use default transformer
// Use default transformer.
switch ( $class ) {
case 'WP_Post':
if ( 'attachment' === $object->post_type ) {
return new Attachment( $object );
if ( 'attachment' === $data->post_type ) {
return new Attachment( $data );
}
return new Post( $object );
return new Post( $data );
case 'WP_Comment':
return new Comment( $object );
return new Comment( $data );
default:
return null;
}

View File

@ -1,20 +1,26 @@
<?php
/**
* WordPress Post Transformer Class file.
*
* @package Activitypub
*/
namespace Activitypub\Transformer;
use WP_Post;
use Activitypub\Shortcodes;
use Activitypub\Model\Blog;
use Activitypub\Transformer\Base;
use Activitypub\Collection\Users;
use function Activitypub\esc_hashtag;
use function Activitypub\is_single_user;
use function Activitypub\get_enclosures;
use function Activitypub\get_rest_url_by_path;
use function Activitypub\site_supports_blocks;
use function Activitypub\generate_post_summary;
use function Activitypub\get_content_warning;
/**
* WordPress Post Transformer
* WordPress Post Transformer.
*
* The Post Transformer is responsible for transforming a WP_Post object into different other
* Object-Types.
@ -24,6 +30,13 @@ use function Activitypub\site_supports_blocks;
* - Activitypub\Activity\Base_Object
*/
class Post extends Base {
/**
* The User as Actor Object.
*
* @var \Activitypub\Activity\Actor
*/
private $actor_object = null;
/**
* Returns the ID of the WordPress Post.
*
@ -36,7 +49,9 @@ class Post extends Base {
/**
* Change the User-ID of the WordPress Post.
*
* @return int 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;
@ -55,39 +70,51 @@ class Post extends Base {
$post = $this->wp_object;
$object = parent::to_object();
$published = \strtotime( $post->post_date_gmt );
$object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) );
$updated = \strtotime( $post->post_modified_gmt );
if ( $updated > $published ) {
$object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) );
$content_warning = get_content_warning( $post );
if ( ! empty( $content_warning ) ) {
$object->set_sensitive( true );
$object->set_summary( $content_warning );
$object->set_summary_map( null );
}
$object->set_content_map(
array(
$this->get_locale() => $this->get_content(),
)
);
$path = sprintf( 'actors/%d/followers', intval( $post->post_author ) );
$object->set_to(
array(
'https://www.w3.org/ns/activitystreams#Public',
get_rest_url_by_path( $path ),
)
);
return $object;
}
/**
* Returns the User-Object of the Author of the Post.
*
* If `single_user` mode is enabled, the Blog-User is returned.
*
* @return \Activitypub\Activity\Actor The User-Object.
*/
protected function get_actor_object() {
if ( $this->actor_object ) {
return $this->actor_object;
}
$blog_user = new Blog();
$this->actor_object = $blog_user;
if ( is_single_user() ) {
return $blog_user;
}
$user = Users::get_by_id( $this->wp_object->post_author );
if ( $user && ! is_wp_error( $user ) ) {
$this->actor_object = $user;
return $user;
}
return $blog_user;
}
/**
* Returns the ID of the Post.
*
* @return string The Posts ID.
*/
public function get_id() {
protected function get_id() {
return $this->get_url();
}
@ -99,13 +126,21 @@ class Post extends Base {
public function get_url() {
$post = $this->wp_object;
if ( 'trash' === get_post_status( $post ) ) {
switch ( \get_post_status( $post ) ) {
case 'trash':
$permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true );
} elseif ( 'draft' === get_post_status( $post ) && get_sample_permalink( $post->ID ) ) {
$sample = get_sample_permalink( $post->ID );
$permalink = str_replace( array( '%pagename%', '%postname%' ), $sample[1], $sample[0] );
} else {
break;
case 'draft':
// Get_sample_permalink is in wp-admin, not always loaded.
if ( ! \function_exists( '\get_sample_permalink' ) ) {
require_once ABSPATH . 'wp-admin/includes/post.php';
}
$sample = \get_sample_permalink( $post->ID );
$permalink = \str_replace( array( '%pagename%', '%postname%' ), $sample[1], $sample[0] );
break;
default:
$permalink = \get_permalink( $post );
break;
}
return \esc_url( $permalink );
@ -119,19 +154,7 @@ class Post extends Base {
* @return string The User-URL.
*/
protected function get_attributed_to() {
$blog_user = new Blog();
if ( is_single_user() ) {
return $blog_user->get_url();
}
$user = Users::get_by_id( $this->wp_object->post_author );
if ( $user && ! is_wp_error( $user ) ) {
return $user->get_url();
}
return $blog_user->get_url();
return $this->get_actor_object()->get_url();
}
/**
@ -161,7 +184,7 @@ class Post extends Base {
);
$id = $this->wp_object->ID;
// list post thumbnail first if this post has one
// List post thumbnail first if this post has one.
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
$media['image'][] = array( 'id' => \get_post_thumbnail_id( $id ) );
}
@ -179,155 +202,27 @@ class Post extends Base {
$media = \array_intersect_key( $media, $unique_ids );
$media = \array_slice( $media, 0, $max_media );
return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media ) );
}
/**
* Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
* Filter the attachment IDs for a post.
*
* @param array $media The media array grouped by type.
* @param int $max_media The maximum number of attachments to return.
* @param WP_Post $this->wp_object The post object.
*
* @return array The attachments.
* @return array The filtered attachment IDs.
*/
protected function get_block_attachments( $media, $max_media ) {
// max media can't be negative or zero
if ( $max_media <= 0 ) {
return array();
}
$media = \apply_filters( 'activitypub_attachment_ids', $media, $this->wp_object );
$blocks = \parse_blocks( $this->wp_object->post_content );
$media = self::get_media_from_blocks( $blocks, $media );
return $media;
}
$attachments = \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media ) );
/**
* Get image attachments from the classic editor.
* This is imperfect as the contained images aren't necessarily the
* same as the attachments.
* Filter the attachments for a post.
*
* @param int $max_images The maximum number of images to return.
* @param array $attachments The attachments.
* @param WP_Post $this->wp_object The post object.
*
* @return array The attachment IDs.
* @return array The filtered attachments.
*/
protected function get_classic_editor_image_attachments( $max_images ) {
// max images can't be negative or zero
if ( $max_images <= 0 ) {
return array();
}
$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;
}
/**
* 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 ) {
// if someone calls that function directly, bail
if ( ! \class_exists( '\WP_HTML_Tag_Processor' ) ) {
return array();
}
// max images can't be negative or zero
if ( $max_images <= 0 ) {
return array();
}
$images = array();
$base = \wp_get_upload_dir()['baseurl'];
$content = \get_post_field( 'post_content', $this->wp_object );
$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' );
// If the img source is in our uploads dir, get the
// associated ID. Note: if there's a -500x500
// type suffix, we remove it, but we try the original
// first in case the original image is actually called
// that. Likewise, we try adding the -scaled suffix for
// the case that this is a small version of an image
// that was big enough to get scaled down on upload:
// https://make.wordpress.org/core/2019/10/09/introducing-handling-of-big-images-in-wordpress-5-3/
if ( null !== $src && \str_starts_with( $src, $base ) ) {
$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 );
if ( $count > 0 ) {
$img_id = \attachment_url_to_postid( $src );
}
}
if ( 0 === $img_id ) {
$src = preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src );
$img_id = \attachment_url_to_postid( $src );
}
if ( 0 !== $img_id ) {
$images[] = array(
'id' => $img_id,
'alt' => $tags->get_attribute( 'alt' ),
);
}
}
}
return $images;
}
/**
* Get post images from the classic editor.
* Note that audio/video attachments are only supported in the block editor.
*
* @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;
return \apply_filters( 'activitypub_attachments', $attachments, $this->wp_object );
}
/**
@ -345,7 +240,7 @@ class Post extends Base {
}
foreach ( $enclosures as $enclosure ) {
// check if URL is an attachment
// Check if URL is an attachment.
$attachment_id = \attachment_url_to_postid( $enclosure['url'] );
if ( $attachment_id ) {
$enclosure['id'] = $attachment_id;
@ -372,17 +267,36 @@ class Post extends Base {
return $media;
}
/**
* Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
*
* @param array $media The media array grouped by type.
* @param int $max_media The maximum number of attachments to return.
*
* @return array The attachments.
*/
protected function get_block_attachments( $media, $max_media ) {
// Max media can't be negative or zero.
if ( $max_media <= 0 ) {
return array();
}
$blocks = \parse_blocks( $this->wp_object->post_content );
return self::get_media_from_blocks( $blocks, $media );
}
/**
* Recursively get media IDs from blocks.
* @param array $blocks The blocks to search for media IDs
* @param array $media The media IDs to append new IDs to
* @param int $max_media The maximum number of media to return.
*
* @param array $blocks The blocks to search for media IDs.
* @param array $media The media IDs to append new IDs to.
*
* @return array The image IDs.
*/
protected static function get_media_from_blocks( $blocks, $media ) {
foreach ( $blocks as $block ) {
// recurse into inner blocks
// Recurse into inner blocks.
if ( ! empty( $block['innerBlocks'] ) ) {
$media = self::get_media_from_blocks( $block['innerBlocks'], $media );
}
@ -443,22 +357,160 @@ class Post extends Base {
return $media;
}
/**
* Get post images from the classic editor.
* Note that audio/video attachments are only supported in the block editor.
*
* @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 ) {
// If someone calls that function directly, bail.
if ( ! \class_exists( '\WP_HTML_Tag_Processor' ) ) {
return array();
}
// Max images can't be negative or zero.
if ( $max_images <= 0 ) {
return array();
}
$images = array();
$base = \wp_get_upload_dir()['baseurl'];
$content = \get_post_field( 'post_content', $this->wp_object );
$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' );
/*
* If the img source is in our uploads dir, get the
* associated ID. Note: if there's a -500x500
* type suffix, we remove it, but we try the original
* first in case the original image is actually called
* that. Likewise, we try adding the -scaled suffix for
* the case that this is a small version of an image
* that was big enough to get scaled down on upload:
* https://make.wordpress.org/core/2019/10/09/introducing-handling-of-big-images-in-wordpress-5-3/
*/
if ( null !== $src && \str_starts_with( $src, $base ) ) {
$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 );
if ( $count > 0 ) {
$img_id = \attachment_url_to_postid( $src );
}
}
if ( 0 === $img_id ) {
$src = preg_replace( '/(\.[a-zA-Z]+)$/', '-scaled$1', $src );
$img_id = \attachment_url_to_postid( $src );
}
if ( 0 !== $img_id ) {
$images[] = array(
'id' => $img_id,
'alt' => $tags->get_attribute( 'alt' ),
);
}
}
}
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();
}
$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;
}
/**
* 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.
*
* @return array The filtered media IDs.
*/
protected static function filter_media_by_object_type( $media, $type, $wp_object ) {
/**
* Filter the object type for media attachments.
*
* @param string $type The object type.
* @param WP_Post $wp_object The post object.
*
* @return string The filtered object type.
*/
$type = \apply_filters( 'filter_media_by_object_type', \strtolower( $type ), $wp_object );
if ( ! empty( $media[ $type ] ) ) {
return $media[ $type ];
}
return array_filter( array_merge( array(), ...array_values( $media ) ) );
return array_filter( array_merge( ...array_values( $media ) ) );
}
/**
@ -477,7 +529,7 @@ class Post extends Base {
$attachment = array();
$mime_type = \get_post_mime_type( $id );
$mime_type_parts = \explode( '/', $mime_type );
// switching on image/audio/video
// Switching on image/audio/video.
switch ( $mime_type_parts[0] ) {
case 'image':
$image_size = 'large';
@ -525,7 +577,7 @@ class Post extends Base {
'name' => \esc_attr( \get_the_title( $id ) ),
);
$meta = wp_get_attachment_metadata( $id );
// height and width for videos
// Height and width for videos.
if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) {
$attachment['width'] = \esc_attr( $meta['width'] );
$attachment['height'] = \esc_attr( $meta['height'] );
@ -534,6 +586,14 @@ class Post extends Base {
break;
}
/**
* Filter the attachment for a post.
*
* @param array $attachment The attachment.
* @param int $id The attachment ID.
*
* @return array The filtered attachment.
*/
return \apply_filters( 'activitypub_attachment', $attachment, $id );
}
@ -589,7 +649,7 @@ class Post extends Base {
}
// Default to Article.
$object_type = 'Note';
$object_type = 'Article';
$post_format = 'standard';
if ( \get_theme_support( 'post-formats' ) ) {
@ -613,7 +673,7 @@ class Post extends Base {
$object_type = 'Page';
break;
default:
$object_type = 'Note';
$object_type = 'Article';
break;
}
@ -640,7 +700,11 @@ class Post extends Base {
return $cc;
}
/**
* Returns the Audience for the Post.
*
* @return string|null The audience.
*/
public function get_audience() {
if ( is_single_user() ) {
return null;
@ -705,24 +769,7 @@ class Post extends Base {
return \__( '(This post is being modified)', 'activitypub' );
}
$content = \get_post_field( 'post_content', $this->wp_object->ID );
$content = \html_entity_decode( $content );
$content = \wp_strip_all_tags( $content );
$content = \trim( $content );
$content = \preg_replace( '/\R+/m', "\n\n", $content );
$content = \preg_replace( '/[\r\t]/', '', $content );
$excerpt_more = \apply_filters( 'activitypub_excerpt_more', '[...]' );
$length = 500;
$length = $length - strlen( $excerpt_more );
if ( \strlen( $content ) > $length ) {
$content = \wordwrap( $content, $length, '</activitypub-summary>' );
$content = \explode( '</activitypub-summary>', $content, 2 );
$content = $content[0];
}
return $content . ' ' . $excerpt_more;
return generate_post_summary( $this->wp_object );
}
/**
@ -759,6 +806,8 @@ class Post extends Base {
* @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' );
@ -819,7 +868,8 @@ class Post extends Base {
$template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
break;
default:
$template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
$content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
$template = empty( $content ) ? ACTIVITYPUB_CUSTOM_POST_CONTENT : $content;
break;
}
@ -838,7 +888,21 @@ class Post extends Base {
* @return array The list of @-Mentions.
*/
protected function get_mentions() {
return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_object->post_content, $this->wp_object );
/**
* 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
);
}
/**
@ -862,6 +926,108 @@ class Post extends Base {
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'];
}
}
return null;
}
/**
* Returns the recipient of the post.
*
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-to
*
* @return array The recipient URLs of the post.
*/
public function get_to() {
return array(
'https://www.w3.org/ns/activitystreams#Public',
$this->get_actor_object()->get_followers(),
);
}
/**
* Returns the published date of the post.
*
* @return string The published date of the post.
*/
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() {
return array(
$this->get_locale() => $this->get_content(),
);
}
/**
* Returns the name map for the post.
*
* @return array The name map for the post.
*/
public function get_name_map() {
if ( ! $this->get_name() ) {
return null;
}
return array(
$this->get_locale() => $this->get_name(),
);
}
/**
* 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.
*
@ -870,8 +1036,8 @@ class Post extends Base {
* @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
* @param string $block_content The block content (html).
* @param object $block The block object.
*
* @return string A block level link
*/

View File

@ -1,34 +1,48 @@
<?php
/**
* BuddyPress integration class file.
*
* @package Activitypub
*/
namespace Activitypub\Integration;
/**
* Compatibility with the BuddyPress plugin
* Compatibility with the BuddyPress plugin.
*
* @see https://buddypress.org/
*/
class Buddypress {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'activitypub_json_author_array', array( self::class, 'add_user_metadata' ), 11, 2 );
}
public static function add_user_metadata( $object, $author_id ) {
$object->url = bp_core_get_user_domain( $author_id ); //add BP member profile URL as user URL
/**
* Add BuddyPress user metadata to the author array.
*
* @param object $author The author object.
* @param int $author_id The author ID.
*
* @return object The author object.
*/
public static function add_user_metadata( $author, $author_id ) {
$author->url = bp_core_get_user_domain( $author_id ); // Add BP member profile URL as user URL.
// add BuddyPress' cover_image instead of WordPress' header_image
// Add BuddyPress' cover_image instead of WordPress' header_image.
$cover_image_url = bp_attachments_get_attachment( 'url', array( 'item_id' => $author_id ) );
if ( $cover_image_url ) {
$object->image = array(
$author->image = array(
'type' => 'Image',
'url' => $cover_image_url,
);
}
// change profile URL to BuddyPress' profile URL
$object->attachment['profile_url'] = array(
// Change profile URL to BuddyPress' profile URL.
$author->attachment['profile_url'] = array(
'type' => 'PropertyValue',
'name' => \__( 'Profile', 'activitypub' ),
'value' => \html_entity_decode(
@ -43,16 +57,16 @@ class Buddypress {
),
);
// replace blog URL on multisite
// Replace blog URL on multisite.
if ( is_multisite() ) {
$user_blogs = get_blogs_of_user( $author_id ); //get sites of user to send as AP metadata
$user_blogs = get_blogs_of_user( $author_id ); // Get sites of user to send as AP metadata.
if ( ! empty( $user_blogs ) ) {
unset( $object->attachment['blog_url'] );
unset( $author->attachment['blog_url'] );
foreach ( $user_blogs as $blog ) {
if ( 1 !== $blog->userblog_id ) {
$object->attachment[] = array(
$author->attachment[] = array(
'type' => 'PropertyValue',
'name' => $blog->blogname,
'value' => \html_entity_decode(
@ -71,6 +85,6 @@ class Buddypress {
}
}
return $object;
return $author;
}
}

View File

@ -1,4 +1,10 @@
<?php
/**
* Enable Mastodon Apps integration class file.
*
* @package Activitypub
*/
namespace Activitypub\Integration;
use DateTime;
@ -6,46 +12,176 @@ use Activitypub\Webfinger as Webfinger_Util;
use Activitypub\Http;
use Activitypub\Collection\Users;
use Activitypub\Collection\Followers;
use Activitypub\Integration\Nodeinfo;
use Activitypub\Collection\Extra_Fields;
use Enable_Mastodon_Apps\Mastodon_API;
use Enable_Mastodon_Apps\Entity\Account;
use Enable_Mastodon_Apps\Entity\Status;
use Enable_Mastodon_Apps\Entity\Media_Attachment;
use function Activitypub\get_remote_metadata_by_actor;
use function Activitypub\is_user_type_disabled;
/**
* Class Enable_Mastodon_Apps
* Class Enable_Mastodon_Apps.
*
* This class is used to enable Mastodon Apps to work with ActivityPub
* This class is used to enable Mastodon Apps to work with ActivityPub.
*
* @see https://github.com/akirk/enable-mastodon-apps
*/
class Enable_Mastodon_Apps {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'mastodon_api_account_followers', array( self::class, 'api_account_followers' ), 10, 2 );
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_add_followers' ), 20, 2 );
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_external' ), 15, 2 );
\add_filter( 'mastodon_api_account', array( self::class, 'api_account_internal' ), 9, 2 );
\add_filter( 'mastodon_api_search', array( self::class, 'api_search' ), 40, 2 );
\add_filter( 'mastodon_api_search', array( self::class, 'api_search_by_url' ), 40, 2 );
\add_filter( 'mastodon_api_get_posts_query_args', array( self::class, 'api_get_posts_query_args' ) );
\add_filter( 'mastodon_api_statuses', array( self::class, 'api_statuses_external' ), 10, 2 );
\add_filter( 'mastodon_api_status_context', array( self::class, 'api_get_replies' ), 10, 23 );
\add_action( 'mastodon_api_update_credentials', array( self::class, 'api_update_credentials' ), 10, 2 );
}
/**
* Add followers to Mastodon API
* Map user to blog if user is disabled.
*
* @param array $followers An array of followers
* @param string $user_id The user id
* @param WP_REST_Request $request The request object
* @param int $user_id The user id.
*
* @return int The user id.
*/
public static function maybe_map_user_to_blog( $user_id ) {
if (
is_user_type_disabled( 'user' ) &&
! is_user_type_disabled( 'blog' ) &&
// Check if the blog user is permissible for this user.
user_can( $user_id, 'activitypub' )
) {
return Users::BLOG_USER_ID;
}
return $user_id;
}
/**
* Update profile data for Mastodon API.
*
* @param array $data The data to act on.
* @param int $user_id The user id.
* @return array The possibly-filtered data (data that's saved gets unset from the array).
*/
public static function api_update_credentials( $data, $user_id ) {
if ( empty( $user_id ) ) {
return $data;
}
$user_id = self::maybe_map_user_to_blog( $user_id );
$user = Users::get_by_id( $user_id );
if ( ! $user || is_wp_error( $user ) ) {
return $data;
}
// User::update_icon and other update_* methods check data validity, so we don't need to do it here.
if ( isset( $data['avatar'] ) && $user->update_icon( $data['avatar'] ) ) {
// Unset the avatar so it doesn't get saved again by other plugins.
// Ditto for all other fields below.
unset( $data['avatar'] );
}
if ( isset( $data['header'] ) && $user->update_header( $data['header'] ) ) {
unset( $data['header'] );
}
if ( isset( $data['display_name'] ) && $user->update_name( $data['display_name'] ) ) {
unset( $data['display_name'] );
}
if ( isset( $data['note'] ) && $user->update_summary( $data['note'] ) ) {
unset( $data['note'] );
}
if ( isset( $data['fields_attributes'] ) ) {
self::set_extra_fields( $user_id, $data['fields_attributes'] );
unset( $data['fields_attributes'] );
}
return $data;
}
/**
* Get extra fields for Mastodon API.
*
* @param int $user_id The user id to act on.
* @return array The extra fields.
*/
private static function get_extra_fields( $user_id ) {
$ret = array();
$fields = Extra_Fields::get_actor_fields( $user_id );
foreach ( $fields as $field ) {
$ret[] = array(
'name' => $field->post_title,
'value' => Extra_Fields::get_formatted_content( $field ),
);
}
return $ret;
}
/**
* Set extra fields for Mastodon API.
*
* @param int $user_id The user id to act on.
* @param array $fields The fields to set. It is assumed to be the entire set of desired fields.
*/
private static function set_extra_fields( $user_id, $fields ) {
// The Mastodon API submits a simple hash for every field.
// We can reasonably assume a similar order for our operations below.
$ids = wp_list_pluck( Extra_Fields::get_actor_fields( $user_id ), 'ID' );
$is_blog = Users::BLOG_USER_ID === $user_id;
$post_type = $is_blog ? Extra_Fields::BLOG_POST_TYPE : Extra_Fields::USER_POST_TYPE;
foreach ( $fields as $i => $field ) {
$post_id = $ids[ $i ] ?? null;
$has_post = $post_id && \get_post( $post_id );
$args = array(
'post_title' => $field['name'],
'post_content' => Extra_Fields::make_paragraph_block( $field['value'] ),
);
if ( $has_post ) {
$args['ID'] = $ids[ $i ];
\wp_update_post( $args );
} else {
$args['post_type'] = $post_type;
$args['post_status'] = 'publish';
if ( ! $is_blog ) {
$args['post_author'] = $user_id;
}
\wp_insert_post( $args );
}
}
// Delete any remaining fields.
if ( \count( $fields ) < \count( $ids ) ) {
$to_delete = \array_slice( $ids, \count( $fields ) );
foreach ( $to_delete as $id ) {
\wp_delete_post( $id, true );
}
}
}
/**
* Add followers to Mastodon API.
*
* @param array $followers An array of followers.
* @param string $user_id The user id.
*
* @return array The filtered followers
*/
public static function api_account_followers( $followers, $user_id ) {
$user_id = self::maybe_map_user_to_blog( $user_id );
$activitypub_followers = Followers::get_followers( $user_id, 40 );
$mastodon_followers = array_map(
function ( $item ) {
@ -63,7 +199,6 @@ class Enable_Mastodon_Apps {
$account->acct = $acct;
$account->display_name = $item->get_name();
$account->url = $item->get_url();
$account->uri = $item->get_id();
$account->avatar = $item->get_icon_url();
$account->avatar_static = $item->get_icon_url();
$account->created_at = new DateTime( $item->get_published() );
@ -77,72 +212,26 @@ class Enable_Mastodon_Apps {
$account->bot = false;
$account->locked = false;
$account->group = false;
$account->discoversable = false;
$account->indexable = false;
$account->hide_collections = false;
$account->discoverable = false;
$account->noindex = false;
$account->fields = array();
$account->emojis = array();
$account->roles = array();
return $account;
},
$activitypub_followers
);
$followers = array_merge( $mastodon_followers, $followers );
return $followers;
}
/**
* Add followers count to Mastodon API
*
* @param Enable_Mastodon_Apps\Entity\Account $account The account
* @param int $user_id The user id
*
* @return Enable_Mastodon_Apps\Entity\Account The filtered Account
*/
public static function api_account_add_followers( $account, $user_id ) {
if ( ! $account instanceof Account ) {
return $account;
}
$user = Users::get_by_various( $user_id );
if ( ! $user || is_wp_error( $user ) ) {
return $account;
}
$header = $user->get_image();
if ( $header ) {
$account->header = $header['url'];
$account->header_static = $header['url'];
}
foreach ( $user->get_attachment() as $attachment ) {
if ( 'PropertyValue' === $attachment['type'] ) {
$account->fields[] = array(
'name' => $attachment['name'],
'value' => $attachment['value'],
);
}
}
$account->acct = $user->get_preferred_username();
$account->note = $user->get_summary();
$account->followers_count = Followers::count_followers( $user->get__id() );
return $account;
return array_merge( $mastodon_followers, $followers );
}
/**
* Resolve external accounts for Mastodon API
*
* @param Enable_Mastodon_Apps\Entity\Account $user_data The user data
* @param string $user_id The user id
* @param Account $user_data The user data.
* @param string $user_id The user id.
*
* @return Enable_Mastodon_Apps\Entity\Account The filtered Account
* @return Account The filtered Account.
*/
public static function api_account_external( $user_data, $user_id ) {
if ( $user_data || ( is_numeric( $user_id ) && $user_id ) ) {
@ -170,6 +259,78 @@ class Enable_Mastodon_Apps {
return $user_data;
}
/**
* Resolve internal accounts for Mastodon API
*
* @param Account $user_data The user data.
* @param string $user_id The user id.
*
* @return Account The filtered Account.
*/
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 );
if ( ! $user || is_wp_error( $user ) ) {
return $user_data;
}
// Convert user to account.
$account = new Account();
// Even if we have a blog user, maintain the provided user_id so as not to confuse clients.
$account->id = (int) $user_id;
$account->username = $user->get_preferred_username();
$account->acct = $account->username;
$account->display_name = $user->get_name();
$account->note = $user->get_summary();
$account->source['note'] = wp_strip_all_tags( $account->note, true );
$account->url = $user->get_url();
$icon = $user->get_icon();
$account->avatar = $icon['url'];
$account->avatar_static = $account->avatar;
$header = $user->get_image();
if ( $header ) {
$account->header = $header['url'];
$account->header_static = $account->header;
}
$account->created_at = new DateTime( $user->get_published() );
$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
$query_args = array(
'post_type' => $post_types,
'posts_per_page' => 1,
);
if ( $user_id > 0 ) {
$query_args['author'] = $user_id;
}
$posts = \get_posts( $query_args );
$account->last_status_at = ! empty( $posts ) ? new DateTime( $posts[0]->post_date_gmt ) : $account->created_at;
$account->fields = self::get_extra_fields( $user_id_to_use );
// Now do it in source['fields'] with stripped tags.
$account->source['fields'] = \array_map(
function ( $field ) {
$field['value'] = \wp_strip_all_tags( $field['value'], true );
return $field;
},
$account->fields
);
$account->followers_count = Followers::count_followers( $user->get__id() );
return $account;
}
/**
* Get account for actor.
*
* @param string $uri The URI.
*
* @return Account|null The account.
*/
private static function get_account_for_actor( $uri ) {
if ( ! is_string( $uri ) ) {
return null;
@ -217,6 +378,14 @@ class Enable_Mastodon_Apps {
return $account;
}
/**
* Search by URL for Mastodon API.
*
* @param array $search_data The search data.
* @param object $request The request object.
*
* @return array The filtered search data.
*/
public static function api_search_by_url( $search_data, $request ) {
$p = \wp_parse_url( $request->get_param( 'q' ) );
if ( ! $p || ! isset( $p['host'] ) ) {
@ -241,6 +410,14 @@ class Enable_Mastodon_Apps {
return $search_data;
}
/**
* Search for Mastodon API.
*
* @param array $search_data The search data.
* @param object $request The request object.
*
* @return array The filtered search data.
*/
public static function api_search( $search_data, $request ) {
$user_id = \get_current_user_id();
if ( ! $user_id ) {
@ -288,6 +465,13 @@ class Enable_Mastodon_Apps {
return $search_data;
}
/**
* Get posts query args for Mastodon API.
*
* @param array $args The query arguments.
*
* @return array The filtered args.
*/
public static function api_get_posts_query_args( $args ) {
if ( isset( $args['author'] ) && is_string( $args['author'] ) ) {
$uri = Webfinger_Util::resolve( $args['author'] );
@ -300,6 +484,14 @@ class Enable_Mastodon_Apps {
return $args;
}
/**
* Convert an activity to a status.
*
* @param array $item The activity.
* @param Account $account The account.
*
* @return Status|null The status.
*/
private static function activity_to_status( $item, $account ) {
if ( isset( $item['object'] ) ) {
$object = $item['object'];
@ -372,6 +564,14 @@ class Enable_Mastodon_Apps {
return $status;
}
/**
* Get posts for Mastodon API.
*
* @param array $statuses The statuses.
* @param array $args The arguments.
*
* @return array The filtered statuses.
*/
public static function api_statuses_external( $statuses, $args ) {
if ( ! isset( $args['activitypub'] ) ) {
return $statuses;
@ -434,6 +634,15 @@ class Enable_Mastodon_Apps {
return array_slice( $activitypub_statuses, 0, $limit );
}
/**
* Get replies for Mastodon API.
*
* @param array $context The context.
* @param int $post_id The post id.
* @param string $url The URL.
*
* @return array The filtered context.
*/
public static function api_get_replies( $context, $post_id, $url ) {
$meta = Http::get_remote_object( $url, true );
if ( is_wp_error( $meta ) || ! isset( $meta['replies']['first']['next'] ) ) {

View File

@ -1,21 +1,40 @@
<?php
/**
* Jetpack integration file.
*
* @package Activitypub
*/
namespace Activitypub\Integration;
/**
* Jetpack integration class.
*/
class Jetpack {
/**
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'jetpack_sync_post_meta_whitelist', [ __CLASS__, 'add_sync_meta' ] );
\add_filter( 'jetpack_sync_post_meta_whitelist', array( self::class, 'add_sync_meta' ) );
}
public static function add_sync_meta( $whitelist ) {
if ( ! is_array( $whitelist ) ) {
return $whitelist;
/**
* Add ActivityPub meta keys to the Jetpack sync allow list.
*
* @param array $allow_list The Jetpack sync allow list.
*
* @return array The Jetpack sync allow list with ActivityPub meta keys.
*/
public static function add_sync_meta( $allow_list ) {
if ( ! is_array( $allow_list ) ) {
return $allow_list;
}
$activitypub_meta_keys = [
$activitypub_meta_keys = array(
'activitypub_user_id',
'activitypub_inbox',
'activitypub_actor_json',
];
return \array_merge( $whitelist, $activitypub_meta_keys );
);
return \array_merge( $allow_list, $activitypub_meta_keys );
}
}

View File

@ -1,4 +1,10 @@
<?php
/**
* NodeInfo integration file.
*
* @package Activitypub
*/
namespace Activitypub\Integration;
use function Activitypub\get_total_users;
@ -6,28 +12,28 @@ use function Activitypub\get_active_users;
use function Activitypub\get_rest_url_by_path;
/**
* Compatibility with the NodeInfo plugin
* Compatibility with the NodeInfo plugin.
*
* @see https://wordpress.org/plugins/nodeinfo/
*/
class Nodeinfo {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'nodeinfo_data', array( self::class, 'add_nodeinfo_data' ), 10, 2 );
\add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_data' ), 10 );
\add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_data' ) );
\add_filter( 'wellknown_nodeinfo_data', array( self::class, 'add_wellknown_nodeinfo_data' ), 10, 2 );
}
/**
* Extend NodeInfo data
* Extend NodeInfo data.
*
* @param array $nodeinfo NodeInfo data
* @param string The NodeInfo Version
* @param array $nodeinfo NodeInfo data.
* @param string $version The NodeInfo Version.
*
* @return array The extended array
* @return array The extended array.
*/
public static function add_nodeinfo_data( $nodeinfo, $version ) {
if ( $version >= '2.0' ) {
@ -47,11 +53,11 @@ class Nodeinfo {
}
/**
* Extend NodeInfo2 data
* Extend NodeInfo2 data.
*
* @param array $nodeinfo NodeInfo2 data
* @param array $nodeinfo NodeInfo2 data.
*
* @return array The extended array
* @return array The extended array.
*/
public static function add_nodeinfo2_data( $nodeinfo ) {
$nodeinfo['protocols'][] = 'activitypub';
@ -66,11 +72,11 @@ class Nodeinfo {
}
/**
* Extend the well-known nodeinfo data
* Extend the well-known nodeinfo data.
*
* @param array $data The well-known nodeinfo data
* @param array $data The well-known nodeinfo data.
*
* @return array The extended array
* @return array The extended array.
*/
public static function add_wellknown_nodeinfo_data( $data ) {
$data['links'][] = array(

View File

@ -1,4 +1,10 @@
<?php
/**
* Opengraph integration file.
*
* @package Activitypub
*/
namespace Activitypub\Integration;
use Activitypub\Model\Blog;
@ -8,7 +14,7 @@ use function Activitypub\is_single_user;
use function Activitypub\is_user_type_disabled;
/**
* Compatibility with the OpenGraph plugin
* Compatibility with the OpenGraph plugin.
*
* @see https://wordpress.org/plugins/opengraph/
* @see https://codeberg.org/fediverse/fep/src/branch/main/fep/XXXX/fep-XXXX.md
@ -16,7 +22,7 @@ use function Activitypub\is_user_type_disabled;
*/
class Opengraph {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
if ( ! function_exists( 'opengraph_metadata' ) ) {
@ -48,27 +54,27 @@ class Opengraph {
* @return array the updated metadata.
*/
public static function add_metadata( $metadata ) {
// Always show Blog-User if the Blog is in single user mode
// Always show Blog-User if the Blog is in single user mode.
if ( is_single_user() ) {
$user = new Blog();
// add WebFinger resource
// Add WebFinger resource.
$metadata['fediverse:creator'] = $user->get_webfinger();
return $metadata;
}
if ( \is_author() ) {
// Use the Author of the Archive-Page
// Use the Author of the Archive-Page.
$user_id = \get_queried_object_id();
} elseif ( \is_singular() ) {
// Use the Author of the Post
// Use the Author of the Post.
$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
// Use the Blog-User for any other page, if the Blog-User is not disabled.
$user_id = Users::BLOG_USER_ID;
} else {
// Do not add any metadata otherwise
// Do not add any metadata otherwise.
return $metadata;
}
@ -78,7 +84,7 @@ class Opengraph {
return $metadata;
}
// add WebFinger resource
// Add WebFinger resource.
$metadata['fediverse:creator'] = $user->get_webfinger();
return $metadata;

View File

@ -0,0 +1,68 @@
<?php
/**
* Seriously Simple Podcasting integration file.
*
* @package Activitypub
*/
namespace Activitypub\Integration;
use Activitypub\Transformer\Post;
use function Activitypub\generate_post_summary;
/**
* Compatibility with the Seriously Simple Podcasting plugin.
*
* This is a transformer for the Seriously Simple Podcasting plugin,
* that extends the default transformer for WordPress posts.
*
* @see https://wordpress.org/plugins/seriously-simple-podcasting/
*/
class Seriously_Simple_Podcasting extends Post {
/**
* Gets the attachment for a podcast episode.
*
* This method is overridden to add the audio file as an attachment.
*
* @return array The attachments array.
*/
public function get_attachment() {
$post = $this->wp_object;
$attachments = parent::get_attachment();
$attachment = array(
'type' => \esc_attr( \get_post_meta( $post->ID, 'episode_type', true ) ),
'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 ) ),
);
$attachment = array_filter( $attachment );
array_unshift( $attachments, $attachment );
return $attachments;
}
/**
* Gets the object type for a podcast episode.
*
* Always returns 'Note' for the best possible compatibility with ActivityPub.
*
* @return string The object type.
*/
public function get_type() {
return 'Note';
}
/**
* Returns the content for the ActivityPub Item.
*
* The content will be generated based on the user settings.
*
* @return string The content.
*/
public function get_content() {
return generate_post_summary( $this->wp_object );
}
}

View File

@ -0,0 +1,82 @@
<?php
/**
* Stream Connector integration file.
*
* @package Activitypub
*/
namespace Activitypub\Integration;
/**
* Stream Connector for ActivityPub.
*
* This class is a Stream Connector for the Stream plugin.
*
* @see https://wordpress.org/plugins/stream/
*/
class Stream_Connector extends \WP_Stream\Connector {
/**
* Connector slug.
*
* @var string
*/
public $name = 'activitypub';
/**
* Actions registered for this connector.
*
* @var array
*/
public $actions = array(
'activitypub_notification_follow',
);
/**
* Return translated connector label.
*
* @return string
*/
public function get_label() {
return __( 'ActivityPub', 'activitypub' );
}
/**
* Return translated context labels.
*
* @return array
*/
public function get_context_labels() {
return array();
}
/**
* Return translated action labels.
*
* @return array
*/
public function get_action_labels() {
return array();
}
/**
* Callback for activitypub_notification_follow.
*
* @param \Activitypub\Notification $notification The notification object.
*/
public function callback_activitypub_notification_follow( $notification ) {
$this->log(
sprintf(
// translators: %s is a URL.
__( 'New Follower: %s', 'activitypub' ),
$notification->actor
),
array(
'notification' => \wp_json_encode( $notification, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ),
),
null,
'notification',
$notification->type,
$notification->target
);
}
}

View File

@ -1,9 +1,16 @@
<?php
/**
* WebFinger integration file.
*
* @package Activitypub
*/
namespace Activitypub\Integration;
use Activitypub\Rest\Webfinger as Webfinger_Rest;
use Activitypub\Collection\Users as User_Collection;
use function Activitypub\get_rest_url_by_path;
/**
* Compatibility with the WebFinger plugin
*
@ -11,7 +18,7 @@ use Activitypub\Collection\Users as User_Collection;
*/
class Webfinger {
/**
* Initialize the class, registering WordPress hooks
* Initialize the class, registering WordPress hooks.
*/
public static function init() {
\add_filter( 'webfinger_user_data', array( self::class, 'add_user_discovery' ), 1, 3 );
@ -19,51 +26,89 @@ class Webfinger {
}
/**
* Add WebFinger discovery links
* Add WebFinger discovery links.
*
* @param array $array the jrd array
* @param string $resource the WebFinger resource
* @param WP_User $user the WordPress user
* @param array $jrd The jrd array.
* @param string $uri The WebFinger resource.
* @param \WP_User $user The WordPress user.
*
* @return array the jrd array
* @return array The jrd array.
*/
public static function add_user_discovery( $array, $resource, $user ) {
public static function add_user_discovery( $jrd, $uri, $user ) {
$user = User_Collection::get_by_id( $user->ID );
if ( ! $user || is_wp_error( $user ) ) {
return $array;
return $jrd;
}
$array['subject'] = sprintf( 'acct:%s', $user->get_webfinger() );
$jrd['subject'] = sprintf( 'acct:%s', $user->get_webfinger() );
$array['aliases'][] = $user->get_url();
$array['aliases'][] = $user->get_alternate_url();
$jrd['aliases'][] = $user->get_url();
$jrd['aliases'][] = $user->get_alternate_url();
$array['links'][] = array(
$jrd['links'][] = array(
'rel' => 'self',
'type' => 'application/activity+json',
'href' => $user->get_url(),
);
return $array;
$jrd['links'][] = array(
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
'template' => get_rest_url_by_path( 'interactions?uri={uri}' ),
);
return $jrd;
}
/**
* Add WebFinger discovery links
* Add WebFinger discovery links.
*
* @param array $array the jrd array
* @param string $resource the WebFinger resource
* @param WP_User $user the WordPress user
* @param array $jrd The jrd array.
* @param string $uri The WebFinger resource.
*
* @return array the jrd array
* @return array|\WP_Error The jrd array or WP_Error.
*/
public static function add_pseudo_user_discovery( $array, $resource ) {
$user = Webfinger_Rest::get_profile( $resource );
if ( ! $user || is_wp_error( $user ) ) {
return $array;
}
public static function add_pseudo_user_discovery( $jrd, $uri ) {
$user = User_Collection::get_by_resource( $uri );
if ( \is_wp_error( $user ) ) {
return $user;
}
$aliases = array(
$user->get_url(),
$user->get_alternate_url(),
);
$aliases = array_unique( $aliases );
$profile = array(
'subject' => sprintf( 'acct:%s', $user->get_webfinger() ),
'aliases' => array_values( array_unique( $aliases ) ),
'links' => array(
array(
'rel' => 'self',
'type' => 'application/activity+json',
'href' => $user->get_url(),
),
array(
'rel' => 'http://webfinger.net/rel/profile-page',
'type' => 'text/html',
'href' => $user->get_url(),
),
array(
'rel' => 'http://ostatus.org/schema/1.0/subscribe',
'template' => get_rest_url_by_path( 'interactions?uri={uri}' ),
),
),
);
if ( 'Person' !== $user->get_type() ) {
$profile['links'][0]['properties'] = array(
'https://www.w3.org/ns/activitystreams#type' => $user->get_type(),
);
}
return $profile;
}
}

View File

@ -0,0 +1,155 @@
<?php
/**
* Load the ActivityPub integrations.
*
* @package Activitypub
*/
namespace Activitypub\Integration;
/**
* Initialize the ActivityPub integrations.
*/
function plugin_init() {
/**
* Adds WebFinger (plugin) support.
*
* This class handles the compatibility with the WebFinger plugin
* and coordinates the internal WebFinger implementation.
*
* @see https://wordpress.org/plugins/webfinger/
*/
require_once __DIR__ . '/class-webfinger.php';
Webfinger::init();
/**
* Adds NodeInfo (plugin) support.
*
* This class handles the compatibility with the NodeInfo plugin
* and coordinates the internal NodeInfo implementation.
*
* @see https://wordpress.org/plugins/nodeinfo/
*/
require_once __DIR__ . '/class-nodeinfo.php';
Nodeinfo::init();
/**
* Adds Enable Mastodon Apps support.
*
* This class handles the compatibility with the Enable Mastodon Apps plugin.
*
* @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();
}
/**
* Adds OpenGraph support.
*
* This class handles the compatibility with the OpenGraph plugin.
*
* @see https://wordpress.org/plugins/opengraph/
*/
if ( '1' === \get_option( 'activitypub_use_opengraph', '1' ) ) {
require_once __DIR__ . '/class-opengraph.php';
Opengraph::init();
}
/**
* Adds Jetpack support.
*
* This class handles the compatibility with Jetpack.
*
* @see https://jetpack.com/
*/
if ( \defined( 'JETPACK__VERSION' ) && ! \defined( 'IS_WPCOM' ) ) {
require_once __DIR__ . '/class-jetpack.php';
Jetpack::init();
}
/**
* Adds Seriously Simple Podcasting support.
*
* This class handles the compatibility with Seriously Simple Podcasting.
*
* @see https://wordpress.org/plugins/seriously-simple-podcasting/
*/
if ( \defined( 'SSP_VERSION' ) ) {
add_filter(
'activitypub_transformer',
function ( $transformer, $data, $object_class ) {
if (
'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;
},
10,
3
);
}
}
\add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' );
/**
* Register the Stream Connector for ActivityPub.
*
* @param array $classes The Stream connectors.
*
* @return array The Stream connectors with the ActivityPub connector.
*/
function register_stream_connector( $classes ) {
require plugin_dir_path( __FILE__ ) . '/class-stream-connector.php';
$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() ) {
$classes[] = $class;
}
return $classes;
}
add_filter( 'wp_stream_connectors', __NAMESPACE__ . '\register_stream_connector' );
// Excluded ActivityPub post types from the Stream.
add_filter(
'wp_stream_posts_exclude_post_types',
function ( $post_types ) {
$post_types[] = 'ap_follower';
$post_types[] = 'ap_extrafield';
$post_types[] = 'ap_extrafield_blog';
return $post_types;
}
);
/**
* Load the BuddyPress integration.
*
* Only load code that needs BuddyPress to run once BP is loaded and initialized.
*
* @see https://buddypress.org/
*/
add_action(
'bp_include',
function () {
require_once __DIR__ . '/class-buddypress.php';
Buddypress::init();
},
0
);

View File

@ -3,7 +3,7 @@ Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nur
Tags: OStatus, fediverse, activitypub, activitystream
Requires at least: 5.5
Tested up to: 6.6
Stable tag: 2.6.1
Stable tag: 3.3.3
Requires PHP: 7.0
License: MIT
License URI: http://opensource.org/licenses/MIT
@ -20,7 +20,7 @@ An example: I give you my Mastodon profile name: `@pfefferle@mastodon.social`. Y
Once you follow Jane's `@jane@example.com` profile, any blog post she crafts on `example.com` will land in your Home feed. Simultaneously, by following the blog-wide profile `@example.com@example.com`, you'll receive updates from all authors.
**Note**: if no one follows your author or blog instance, your posts remain unseen. The simplest method to verify the plugin's operation is by following your profile. If you possess a Mastodon profile, initiate by following your new one.
**Note**: If no one follows your author or blog instance, your posts remain unseen. The simplest method to verify the plugin's operation is by following your profile. If you possess a Mastodon profile, initiate by following your new one.
The plugin works with the following tested federated platforms, but there may be more that it works with as well:
@ -48,7 +48,7 @@ So whats the process?
1. On your blog, publish a new post.
1. From Mastodon, check to see if the new post appears in your Home feed.
Please note that it may take up to 15 minutes or so for the new post to show up in your federated feed. This is because the messages are sent to the federated platforms using a delayed cron. This avoids breaking the publishing process for those cases where users might have lots of followers. So please dont assume that just because you didnt see it show up right away that something is broken. Give it some time. In most cases, it will show up within a few minutes, and youll know everything is working as expected.
**Note**: It may take up to 15 minutes or so for the new post to show up in your federated feed. This is because the messages are sent to the federated platforms using a delayed cron. This avoids breaking the publishing process for those cases where users might have lots of followers. So please dont assume that just because you didnt see it show up right away that something is broken. Give it some time. In most cases, it will show up within a few minutes, and youll know everything is working as expected.
== Frequently Asked Questions ==
@ -105,6 +105,12 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
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? =
If you are using a reverse proxy with Apache to run your host you may encounter that you are unable to have followers join the blog. This will occur because the proxy system rewrites the host headers to be the internal DNS name of your server, which the plugin then uses to attempt to sign the replies. The remote site attempting to follow your users is expecting the public DNS name on the replies. In these cases you will need to use the 'ProxyPreserveHost On' directive to ensure the external host name is passed to your internal host.
If you are using SSL between the proxy and internal host you may also need to `SSLProxyCheckPeerName off` if your internal host can not answer with the correct SSL name. This may present a security issue in some environments.
= Constants =
The plugin uses PHP Constants to enable, disable or change its default behaviour. Please use them with caution and only if you know what you are doing.
@ -115,6 +121,7 @@ The plugin uses PHP Constants to enable, disable or change its default behaviour
* `ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS` - Change the number of attachments, that should be federated. Default: `3`.
* `ACTIVITYPUB_HASHTAGS_REGEXP` - Change the default regex to detect hashtext in a text. Default: `(?:(?<=\s)|(?<=<p>)|(?<=<br>)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))`.
* `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: `<strong>[ap_title]</strong>\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]`.
* `ACTIVITYPUB_AUTHORIZED_FETCH` - Enable AUTHORIZED_FETCH. Default: `false`.
* `ACTIVITYPUB_DISABLE_REWRITES` - Disable auto generation of `mod_rewrite` rules. Default: `false`.
@ -131,62 +138,95 @@ The followers of a user can be found in the menu under "Users" -> "Followers" or
For reasons of data protection, it is not possible to see the followers of other users.
== Screenshots ==
1. The "Follow me"-Block in the Block-Editor
2. The "Followers"-Block in the Block-Editor
3. The "Federated Reply"-Block in the Block-Editor
4. A "Federated Reply" in a Post
5. A Blog-Profile on Mastodon
== Changelog ==
= 2.6.1 =
= 3.3.3 =
* Fixed: Extra Fields will generate wrong entries
* Fixed: Sanitization callback
* Improved: A lot of PHPCS cleanups
* Improved: Prepare multi-lang support
= 2.6.0 =
= 3.3.2 =
* Added: Support for FEP-fb2a
* Added: CRUD support for Extra Fields
* Improved: Remote-Follow UI and UX
* Improved: Open Graph `fediverse:creator` implementation
* Fixed: Compatibility issues with fed.brid.gy
* Fixed: Remote-Reply endpoint
* Fixed: WebFinger Error Codes (thanks to the FediTest project)
* Fixed: Fatal Error when wp_schedule_single_event third argument is being passed as a string
* Fixed: Keep priority of Icons
* Fixed: Fatal error if remote-object is `WP_Error`
* Improved: Adopt WordPress PHP Coding Standards
= 2.5.0 =
= 3.3.1 =
* Added: WebFinger cors header
* Added: WebFinger Content-Type
* Added: The Fediverse creator of a post to OpenGraph
* Improved: Try to lookup local users first for Enable Mastodon Apps
* Improved: Send also Announces for deletes
* Improved: Load time by adding `count_total=false` to `WP_User_Query`
* Fixed: Several WebFinger issues
* Fixed: Redirect issue for Application user
* Fixed: Accessibilty issues with missing screen-reader-text on User overview page
* Fixed: PHP Warnings
* Fixed: PHPCS issues
= 2.4.0 =
= 3.3.0 =
* Added: A core/embed block filter to transform iframes to links
* Added: Basic support of incoming `Announce`s
* Added: Improve attachment handling
* Added: Notifications: Introduce general class and use it for new follows
* Added: Always fall back to `get_by_username` if one of the above fail
* Added: Notification support for Jetpack
* Added: EMA: Support for fetching external statuses without replies
* Added: EMA: Remote context
* Added: EMA: Allow searching for URLs
* Added: EMA: Ensuring numeric ids is now done in EMA directly
* Added: Podcast support
* Added: Follower count to "At a Glance" dashboard widget
* Improved: Use `Note` as default Object-Type, instead of `Article`
* Improved: Improve `AUTHORIZED_FETCH`
* Improved: Only send Mentions to comments in the direct hierarchy
* Improved: Improve transformer
* Improved: Improve Lemmy compatibility
* Improved: Updated JS dependencies
* Fixed: EMA: Add missing static keyword and try to lookup if the id is 0
* Fixed: Blog-wide account when WordPress is in subdirectory
* Fixed: Funkwhale URLs
* Fixed: Prevent infinite loops in `get_comment_ancestors`
* Fixed: Better Content-Negotiation handling
* 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
See full Changelog on [GitHub](https://github.com/Automattic/wordpress-activitypub/blob/master/CHANGELOG.md).
= 3.2.5 =
* Fixed: Enable Mastodon Apps check
* Fixed: Fediverse replies were not threaded properly
= 3.2.4 =
* Improved: Inbox validation
= 3.2.3 =
* 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
= 3.2.2 =
* Fixed: Extra-Fields check
= 3.2.1 =
* Fixed: Use `Excerpt` for Podcast Episodes
= 3.2.0 =
* 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
= 3.1.0 =
* 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
See full Changelog on [GitHub](https://github.com/Automattic/wordpress-activitypub/blob/trunk/CHANGELOG.md).
== Upgrade Notice ==

View File

@ -1,5 +1,20 @@
<?php
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
/**
* Admin header template.
*
* @package Activitypub
*/
/* @var array $args Template arguments. */
$args = wp_parse_args(
$args,
array(
'welcome' => '',
'settings' => '',
'blog-profile' => '',
'followers' => '',
)
);
?>
<div class="activitypub-settings-header">
<div class="activitypub-settings-title-section">
@ -7,17 +22,21 @@
</div>
<nav class="activitypub-settings-tabs-wrapper" aria-label="<?php \esc_attr_e( 'Secondary menu', 'activitypub' ); ?>">
<a href="<?php echo \esc_url_raw( admin_url( 'options-general.php?page=activitypub' ) ); ?>" class="activitypub-settings-tab <?php echo \esc_attr( $args['welcome'] ); ?>">
<a href="<?php echo \esc_url( admin_url( 'options-general.php?page=activitypub' ) ); ?>" class="activitypub-settings-tab <?php echo \esc_attr( $args['welcome'] ); ?>">
<?php \esc_html_e( 'Welcome', 'activitypub' ); ?>
</a>
<a href="<?php echo \esc_url_raw( admin_url( 'options-general.php?page=activitypub&tab=settings' ) ); ?>" class="activitypub-settings-tab <?php echo \esc_attr( $args['settings'] ); ?>">
<a href="<?php echo \esc_url( admin_url( 'options-general.php?page=activitypub&tab=settings' ) ); ?>" class="activitypub-settings-tab <?php echo \esc_attr( $args['settings'] ); ?>">
<?php \esc_html_e( 'Settings', 'activitypub' ); ?>
</a>
<?php if ( ! \Activitypub\is_user_disabled( \Activitypub\Collection\Users::BLOG_USER_ID ) ) : ?>
<a href="<?php echo \esc_url_raw( admin_url( 'options-general.php?page=activitypub&tab=followers' ) ); ?>" class="activitypub-settings-tab <?php echo \esc_attr( $args['followers'] ); ?>">
<a href="<?php echo \esc_url( admin_url( 'options-general.php?page=activitypub&tab=blog-profile' ) ); ?>" class="activitypub-settings-tab <?php echo \esc_attr( $args['blog-profile'] ); ?>">
<?php \esc_html_e( 'Blog-Profile', 'activitypub' ); ?>
</a>
<a href="<?php echo \esc_url( admin_url( 'options-general.php?page=activitypub&tab=followers' ) ); ?>" class="activitypub-settings-tab <?php echo \esc_attr( $args['followers'] ); ?>">
<?php \esc_html_e( 'Followers', 'activitypub' ); ?>
</a>

View File

@ -1,13 +1,18 @@
<?php
/**
* ActivityPub Blog Followers List template.
*
* @package Activitypub
*/
\load_template(
__DIR__ . '/admin-header.php',
true,
array(
'settings' => '',
'welcome' => '',
'followers' => 'active',
)
);
$table = new \Activitypub\Table\Followers();
$follower_count = $table->get_user_count();
// translators: The follower count.

View File

@ -1,15 +1,21 @@
<?php
/**
* ActivityPub Blog JSON template.
*
* @package Activitypub
*/
$user = new \Activitypub\Model\Blog();
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* Action triggered prior to the ActivityPub profile being created and sent to the client
*/
\do_action( 'activitypub_json_author_pre', $user->get__id() );
\header( 'Content-Type: application/activity+json' );
echo $user->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
/*
* Action triggerd after the ActivityPub profile has been created and sent to the client
/**
* Action triggered after the ActivityPub profile has been created and sent to the client
*/
\do_action( 'activitypub_json_author_post', $user->get__id() );

View File

@ -0,0 +1,190 @@
<?php
/**
* ActivityPub Blog Settings template.
*
* @package Activitypub
*/
\load_template(
__DIR__ . '/admin-header.php',
true,
array(
'blog-profile' => 'active',
)
);
?>
<div class="activitypub-settings activitypub-settings-page hide-if-no-js">
<form method="post" action="options.php">
<?php \settings_fields( 'activitypub_blog' ); ?>
<div class="box">
<h3><?php \esc_html_e( 'Blog-Profile', 'activitypub' ); ?></h3>
<table class="form-table">
<tbody>
<tr>
<th scope="row">
<?php \esc_html_e( 'Manage Avatar', 'activitypub' ); ?>
</th>
<td>
<?php if ( \has_site_icon() ) : ?>
<p><img src="<?php echo esc_url( get_site_icon_url( '50' ) ); ?>" /></p>
<?php endif; ?>
<p class="description">
<?php
echo \wp_kses(
\sprintf(
// translators: %s is a URL.
\__( 'The ActivityPub plugin uses the WordPress Site Icon as Avatar for the Blog-Profile, you can change the Site Icon in the "<a href="%s">General Settings</a>" of WordPress.', 'activitypub' ),
\esc_url( \admin_url( 'options-general.php' ) )
),
'default'
);
?>
</p>
</td>
</tr>
<tr>
<th>
<?php \esc_html_e( 'Manage Header Image', 'activitypub' ); ?>
</th>
<td>
<?php
$classes_for_upload_button = 'button upload-button button-add-media button-add-header-image';
$classes_for_update_button = 'button';
$classes_for_wrapper = '';
if ( (int) get_option( 'activitypub_header_image', 0 ) ) :
$classes_for_wrapper .= ' has-header-image';
$classes_for_button = $classes_for_update_button;
$classes_for_button_on_change = $classes_for_upload_button;
else :
$classes_for_wrapper .= ' hidden';
$classes_for_button = $classes_for_upload_button;
$classes_for_button_on_change = $classes_for_update_button;
endif;
?>
<div id="activitypub-header-image-preview-wrapper" class='<?php echo esc_attr( $classes_for_wrapper ); ?>'>
<img id='activitypub-header-image-preview' src='<?php echo esc_url( wp_get_attachment_url( get_option( 'activitypub_header_image' ) ) ); ?>' style="max-width: 100%;" />
</div>
<button
type="button"
id="activitypub-choose-from-library-button"
type="button"
class="<?php echo esc_attr( $classes_for_button ); ?>"
data-alt-classes="<?php echo esc_attr( $classes_for_button_on_change ); ?>"
data-choose-text="<?php esc_attr_e( 'Choose a Header Image', 'activitypub' ); ?>"
data-update-text="<?php esc_attr_e( 'Change Header Icon', 'activitypub' ); ?>"
data-update="<?php esc_attr_e( 'Set as Header Image', 'activitypub' ); ?>"
data-state="<?php echo esc_attr( (int) get_option( 'activitypub_header_image', 0 ) ); ?>">
<?php if ( (int) get_option( 'activitypub_header_image', 0 ) ) : ?>
<?php esc_html_e( 'Change Header Image', 'activitypub' ); ?>
<?php else : ?>
<?php esc_html_e( 'Choose a Header Image', 'activitypub' ); ?>
<?php endif; ?>
</button>
<button
id="activitypub-remove-header-image"
type="button"
<?php echo (int) get_option( 'activitypub_header_image', 0 ) ? 'class="button button-secondary reset"' : 'class="button button-secondary reset hidden"'; ?>>
<?php esc_html_e( 'Remove Header Image', 'activitypub' ); ?>
</button>
<input type='hidden' name='activitypub_header_image' id='activitypub_header_image' value='<?php echo esc_attr( get_option( 'activitypub_header_image' ) ); ?>'>
</td>
</tr>
<tr>
<th scope="row">
<?php \esc_html_e( 'Change profile ID', 'activitypub' ); ?>
</th>
<td>
<label for="activitypub_blog_identifier">
<input class="blog-user-identifier" name="activitypub_blog_identifier" id="activitypub_blog_identifier" type="text" value="<?php echo esc_attr( \get_option( 'activitypub_blog_identifier', \Activitypub\Model\Blog::get_default_username() ) ); ?>" />
@<?php echo esc_html( \wp_parse_url( \home_url(), PHP_URL_HOST ) ); ?>
</label>
<p class="description">
<?php \esc_html_e( 'This profile name will federate all posts written on your blog, regardless of the author who posted it.', 'activitypub' ); ?>
</p>
<p>
<strong>
<?php \esc_html_e( 'Please avoid using an existing authors name as the blog profile ID. Fediverse platforms might use caching and this could break the functionality completely.', 'activitypub' ); ?>
</strong>
</p>
</td>
</tr>
<tr>
<th scope="row">
<?php \esc_html_e( 'Change Description', 'activitypub' ); ?>
</th>
<td>
<label for="activitypub_blog_description">
<textarea
class="blog-user-description large-text"
rows="5"
name="activitypub_blog_description"
id="activitypub_blog_description"
placeholder="<?php echo esc_attr( \get_bloginfo( 'description' ) ); ?>"
><?php echo \esc_textarea( \get_option( 'activitypub_blog_description' ) ); ?></textarea>
</label>
<p class="description">
<?php \esc_html_e( 'By default the ActivityPub plugin uses the WordPress tagline as a description for the blog profile.', 'activitypub' ); ?>
</p>
</td>
</tr>
<tr scope="row">
<th>
<label><?php \esc_html_e( 'Extra Fields', 'activitypub' ); ?></label>
</th>
<td>
<p class="description">
<?php
\esc_html_e( 'Your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' );
?>
</p>
<table class="widefat striped activitypub-extra-fields" role="presentation" style="margin: 15px 0;">
<?php
$extra_fields = \Activitypub\Collection\Extra_Fields::get_actor_fields( \Activitypub\Collection\Users::BLOG_USER_ID );
if ( empty( $extra_fields ) ) :
?>
<tr>
<td colspan="3">
<?php \esc_html_e( 'No extra fields found.', 'activitypub' ); ?>
</td>
</tr>
<?php
endif;
foreach ( $extra_fields as $extra_field ) :
?>
<tr>
<td><?php echo \esc_html( $extra_field->post_title ); ?></td>
<td><?php echo \wp_kses_post( \get_the_excerpt( $extra_field ) ); ?></td>
<td>
<a href="<?php echo \esc_url( \get_edit_post_link( $extra_field->ID ) ); ?>" class="button">
<?php \esc_html_e( 'Edit', 'activitypub' ); ?>
</a>
</td>
</tr>
<?php endforeach; ?>
</table>
<p>
<a href="<?php echo esc_url( admin_url( '/post-new.php?post_type=ap_extrafield_blog' ) ); ?>" class="button">
<?php esc_html_e( 'Add new', 'activitypub' ); ?>
</a>
<a href="<?php echo esc_url( admin_url( '/edit.php?post_type=ap_extrafield_blog' ) ); ?>">
<?php esc_html_e( 'Manage all', 'activitypub' ); ?>
</a>
</p>
</td>
</tr>
</tbody>
</table>
</div>
<?php \do_settings_sections( 'activitypub_blog_profile' ); ?>
<?php \submit_button(); ?>
</form>
</div>

View File

@ -1,5 +1,11 @@
<?php
$comment = \get_comment( \get_query_var( 'c', null ) ); // phpcs:ignore
/**
* ActivityPub Comment JSON template.
*
* @package Activitypub
*/
$comment = \get_comment( \get_query_var( 'c', null ) ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$transformer = \Activitypub\Transformer\Factory::get_transformer( $comment );
if ( \is_wp_error( $transformer ) ) {
@ -9,15 +15,15 @@ if ( \is_wp_error( $transformer ) ) {
);
}
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* 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 triggerd after the ActivityPub profile has been created and sent to the client
/**
* Action triggered after the ActivityPub profile has been created and sent to the client
*/
\do_action( 'activitypub_json_comment_post' );

View File

@ -1,6 +1,11 @@
<?php
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$post = \get_post();
/**
* ActivityPub Post JSON template.
*
* @package Activitypub
*/
$post = \get_post(); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
$transformer = \Activitypub\Transformer\Factory::get_transformer( $post );
if ( \is_wp_error( $transformer ) ) {
@ -11,15 +16,15 @@ if ( \is_wp_error( $transformer ) ) {
}
/*
* Action triggerd prior to the ActivityPub profile being created and sent to the client
/**
* 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 triggerd after the ActivityPub profile has been created and sent to the client
/**
* Action triggered after the ActivityPub profile has been created and sent to the client
*/
\do_action( 'activitypub_json_post_post' );

Some files were not shown because too many files have changed in this diff Show More