modified file plugins
This commit is contained in:
parent
cd379e1d95
commit
e13bab0b76
22
wp-content/upgrade-temp-backup/plugins/activitypub/LICENSE
Normal file
22
wp-content/upgrade-temp-backup/plugins/activitypub/LICENSE
Normal file
@ -0,0 +1,22 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2019 Matthias Pfefferle
|
||||
Copyright (c) 2023 Automattic
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
@ -0,0 +1,239 @@
|
||||
<?php
|
||||
/**
|
||||
* 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
|
||||
* Author: Matthias Pfefferle & Automattic
|
||||
* Author URI: https://automattic.com/
|
||||
* License: MIT
|
||||
* License URI: http://opensource.org/licenses/MIT
|
||||
* Requires PHP: 7.0
|
||||
* Text Domain: activitypub
|
||||
* Domain Path: /languages
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use function Activitypub\is_blog_public;
|
||||
use function Activitypub\site_supports_blocks;
|
||||
|
||||
require_once __DIR__ . '/includes/compat.php';
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
\define( 'ACTIVITYPUB_PLUGIN_VERSION', '2.6.1' );
|
||||
|
||||
/**
|
||||
* Initialize the plugin constants.
|
||||
*/
|
||||
\defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || \define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' );
|
||||
\defined( 'ACTIVITYPUB_EXCERPT_LENGTH' ) || \define( 'ACTIVITYPUB_EXCERPT_LENGTH', 400 );
|
||||
\defined( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS' ) || \define( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS', true );
|
||||
\defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || \define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 3 );
|
||||
\defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=<p>)|(?<=<br>)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' );
|
||||
\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' );
|
||||
\defined( 'ACTIVITYPUB_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 );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false );
|
||||
\defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) || \define( 'ACTIVITYPUB_SHARED_INBOX_FEATURE', false );
|
||||
\defined( 'ACTIVITYPUB_SEND_VARY_HEADER' ) || \define( 'ACTIVITYPUB_SEND_VARY_HEADER', false );
|
||||
\defined( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE' ) || \define( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE', 'note' );
|
||||
|
||||
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
/**
|
||||
* Initialize REST routes.
|
||||
*/
|
||||
function rest_init() {
|
||||
Rest\Actors::init();
|
||||
Rest\Outbox::init();
|
||||
Rest\Inbox::init();
|
||||
Rest\Followers::init();
|
||||
Rest\Following::init();
|
||||
Rest\Webfinger::init();
|
||||
Rest\Comment::init();
|
||||
Rest\Server::init();
|
||||
Rest\Collection::init();
|
||||
|
||||
// load NodeInfo endpoints only if blog is public
|
||||
if ( is_blog_public() ) {
|
||||
Rest\NodeInfo::init();
|
||||
}
|
||||
}
|
||||
\add_action( 'rest_api_init', __NAMESPACE__ . '\rest_init' );
|
||||
|
||||
/**
|
||||
* Initialize plugin.
|
||||
*/
|
||||
function plugin_init() {
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Health_Check', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Comment', 'init' ) );
|
||||
|
||||
if ( site_supports_blocks() ) {
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Blocks', 'init' ) );
|
||||
}
|
||||
|
||||
$debug_file = __DIR__ . '/includes/debug.php';
|
||||
if ( \WP_DEBUG && file_exists( $debug_file ) && is_readable( $debug_file ) ) {
|
||||
require_once $debug_file;
|
||||
Debug::init();
|
||||
}
|
||||
|
||||
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
|
||||
*/
|
||||
\spl_autoload_register(
|
||||
function ( $full_class ) {
|
||||
$base_dir = __DIR__ . '/includes/';
|
||||
$base = 'Activitypub\\';
|
||||
|
||||
if ( strncmp( $full_class, $base, strlen( $base ) ) === 0 ) {
|
||||
$maybe_uppercase = str_replace( $base, '', $full_class );
|
||||
$class = strtolower( $maybe_uppercase );
|
||||
// All classes should be capitalized. If this is instead looking for a lowercase method, we ignore that.
|
||||
if ( $maybe_uppercase === $class ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( false !== strpos( $class, '\\' ) ) {
|
||||
$parts = explode( '\\', $class );
|
||||
$class = array_pop( $parts );
|
||||
$sub_dir = strtr( implode( '/', $parts ), '_', '-' );
|
||||
$base_dir = $base_dir . $sub_dir . '/';
|
||||
}
|
||||
|
||||
$filename = 'class-' . strtr( $class, '_', '-' );
|
||||
$file = $base_dir . $filename . '.php';
|
||||
|
||||
if ( file_exists( $file ) && is_readable( $file ) ) {
|
||||
require_once $file;
|
||||
} else {
|
||||
// translators: %s is the class name
|
||||
\wp_die( sprintf( esc_html__( 'Required class not found or not readable: %s', 'activitypub' ), esc_html( $full_class ) ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Add plugin settings link
|
||||
*/
|
||||
function plugin_settings_link( $actions ) {
|
||||
$settings_link = array();
|
||||
$settings_link[] = \sprintf(
|
||||
'<a href="%1s">%2s</a>',
|
||||
\menu_page_url( 'activitypub', false ),
|
||||
\__( 'Settings', 'activitypub' )
|
||||
);
|
||||
|
||||
return \array_merge( $settings_link, $actions );
|
||||
}
|
||||
\add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), __NAMESPACE__ . '\plugin_settings_link' );
|
||||
|
||||
\register_activation_hook(
|
||||
__FILE__,
|
||||
array(
|
||||
__NAMESPACE__ . '\Activitypub',
|
||||
'activate',
|
||||
)
|
||||
);
|
||||
|
||||
\register_deactivation_hook(
|
||||
__FILE__,
|
||||
array(
|
||||
__NAMESPACE__ . '\Activitypub',
|
||||
'deactivate',
|
||||
)
|
||||
);
|
||||
|
||||
\register_uninstall_hook(
|
||||
__FILE__,
|
||||
array(
|
||||
__NAMESPACE__ . '\Activitypub',
|
||||
'uninstall',
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* 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
|
||||
);
|
||||
|
||||
/**
|
||||
* `get_plugin_data` wrapper
|
||||
*
|
||||
* @return array The plugin metadata array
|
||||
*/
|
||||
function get_plugin_meta( $default_headers = array() ) {
|
||||
if ( ! $default_headers ) {
|
||||
$default_headers = array(
|
||||
'Name' => 'Plugin Name',
|
||||
'PluginURI' => 'Plugin URI',
|
||||
'Version' => 'Version',
|
||||
'Description' => 'Description',
|
||||
'Author' => 'Author',
|
||||
'AuthorURI' => 'Author URI',
|
||||
'TextDomain' => 'Text Domain',
|
||||
'DomainPath' => 'Domain Path',
|
||||
'Network' => 'Network',
|
||||
'RequiresWP' => 'Requires at least',
|
||||
'RequiresPHP' => 'Requires PHP',
|
||||
'UpdateURI' => 'Update URI',
|
||||
);
|
||||
}
|
||||
|
||||
return \get_file_data( __FILE__, $default_headers, 'plugin' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Plugin Version Number used for caching.
|
||||
*/
|
||||
function get_plugin_version() {
|
||||
if ( \defined( 'ACTIVITYPUB_PLUGIN_VERSION' ) ) {
|
||||
return ACTIVITYPUB_PLUGIN_VERSION;
|
||||
}
|
||||
|
||||
$meta = get_plugin_meta( array( 'Version' => 'Version' ) );
|
||||
|
||||
return $meta['Version'];
|
||||
}
|
@ -0,0 +1,204 @@
|
||||
.activitypub-settings {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .notice {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
margin: 0px auto 30px;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .wrap {
|
||||
padding-left: 22px;
|
||||
}
|
||||
|
||||
.activitypub-settings-header {
|
||||
text-align: center;
|
||||
margin: 0 0 1rem;
|
||||
background: #fff;
|
||||
border-bottom: 1px solid #dcdcde;
|
||||
}
|
||||
|
||||
.activitypub-settings-title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
clear: both;
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.settings_page_activitypub #wpcontent {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.activitypub-settings-tabs-wrapper {
|
||||
display: -ms-inline-grid;
|
||||
-ms-grid-columns: auto auto auto;
|
||||
vertical-align: top;
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
}
|
||||
|
||||
.activitypub-settings-tab.active {
|
||||
box-shadow: inset 0 -3px #3582c4;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.activitypub-settings-tab {
|
||||
display: block;
|
||||
text-decoration: none;
|
||||
color: inherit;
|
||||
padding: .5rem 1rem 1rem;
|
||||
margin: 0 1rem;
|
||||
transition: box-shadow .5s ease-in-out;
|
||||
}
|
||||
|
||||
.wp-header-end {
|
||||
visibility: hidden;
|
||||
margin: -2px 0 0;
|
||||
}
|
||||
|
||||
summary {
|
||||
cursor: pointer;
|
||||
text-decoration: underline;
|
||||
color: #2271b1;
|
||||
}
|
||||
|
||||
.activitypub-settings-accordion {
|
||||
border: 1px solid #c3c4c7;
|
||||
}
|
||||
|
||||
.activitypub-settings-accordion-heading {
|
||||
margin: 0;
|
||||
border-top: 1px solid #c3c4c7;
|
||||
font-size: inherit;
|
||||
line-height: inherit;
|
||||
font-weight: 600;
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.activitypub-settings-accordion-heading:first-child {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.activitypub-settings-accordion-panel {
|
||||
margin: 0;
|
||||
padding: 1em 1.5em;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.activitypub-settings-accordion-trigger {
|
||||
background: #fff;
|
||||
border: 0;
|
||||
color: #2c3338;
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-weight: 400;
|
||||
margin: 0;
|
||||
padding: 1em 3.5em 1em 1.5em;
|
||||
min-height: 46px;
|
||||
position: relative;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
-webkit-user-select: auto;
|
||||
user-select: auto;
|
||||
}
|
||||
|
||||
.activitypub-settings-accordion-trigger {
|
||||
color: #2c3338;
|
||||
cursor: pointer;
|
||||
font-weight: 400;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.activitypub-settings-accordion-trigger .title {
|
||||
pointer-events: none;
|
||||
font-weight: 600;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.activitypub-settings-accordion-trigger .icon,
|
||||
.activitypub-settings-accordion-viewed .icon {
|
||||
border: solid #50575e medium;
|
||||
border-width: 0 2px 2px 0;
|
||||
height: .5rem;
|
||||
pointer-events: none;
|
||||
position: absolute;
|
||||
right: 1.5em;
|
||||
top: 50%;
|
||||
transform: translateY(-70%) rotate(45deg);
|
||||
width: .5rem;
|
||||
}
|
||||
|
||||
.activitypub-settings-accordion-trigger[aria-expanded="true"] .icon {
|
||||
transform: translateY(-30%) rotate(-135deg);
|
||||
}
|
||||
|
||||
.activitypub-settings-accordion-trigger:active,
|
||||
.activitypub-settings-accordion-trigger:hover {
|
||||
background: #f6f7f7;
|
||||
}
|
||||
|
||||
.activitypub-settings-accordion-trigger:focus {
|
||||
color: #1d2327;
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
outline-offset: -1px;
|
||||
outline: 2px solid #2271b1;
|
||||
background-color: #f6f7f7;
|
||||
}
|
||||
|
||||
.activitypub-settings
|
||||
input.blog-user-identifier {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.activitypub-settings
|
||||
.header-image {
|
||||
width: 100%;
|
||||
height: 80px;
|
||||
position: relative;
|
||||
display: block;
|
||||
margin-bottom: 40px;
|
||||
background-image: rgb(168,165,175);
|
||||
background-image: linear-gradient(180deg, red, yellow);
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.activitypub-settings
|
||||
.logo {
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
position: relative;
|
||||
top: 40px;
|
||||
left: 40px;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .box {
|
||||
border: 1px solid #c3c4c7;
|
||||
background-color: #fff;
|
||||
padding: 1em 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .activitypub-welcome-page .box label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .activitypub-welcome-page input {
|
||||
font-size: 20px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .plugin-recommendations {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#dashboard_right_now li a.activitypub-followers::before {
|
||||
content: "\f307";
|
||||
font-family: dashicons;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
After Width: | Height: | Size: 12 KiB |
@ -0,0 +1,21 @@
|
||||
jQuery( function( $ ) {
|
||||
// Accordion handling in various areas.
|
||||
$( '.activitypub-settings-accordion' ).on( 'click', '.activitypub-settings-accordion-trigger', function() {
|
||||
var isExpanded = ( 'true' === $( this ).attr( 'aria-expanded' ) );
|
||||
|
||||
if ( isExpanded ) {
|
||||
$( this ).attr( 'aria-expanded', 'false' );
|
||||
$( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', true );
|
||||
} else {
|
||||
$( this ).attr( 'aria-expanded', 'true' );
|
||||
$( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', false );
|
||||
}
|
||||
} );
|
||||
|
||||
$(document).on( 'wp-plugin-install-success', function( event, response ) {
|
||||
setTimeout( function() {
|
||||
$( '.activate-now' ).removeClass( 'thickbox open-plugin-details-modal' );
|
||||
}, 1200 );
|
||||
} );
|
||||
|
||||
} );
|
@ -0,0 +1,47 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"name": "activitypub/follow-me",
|
||||
"apiVersion": 3,
|
||||
"version": "1.0.0",
|
||||
"title": "Follow me on the Fediverse",
|
||||
"category": "widgets",
|
||||
"description": "Display your Fediverse profile so that visitors can follow you.",
|
||||
"textdomain": "activitypub",
|
||||
"icon": "groups",
|
||||
"supports": {
|
||||
"html": false,
|
||||
"color": {
|
||||
"gradients": true,
|
||||
"link": true,
|
||||
"__experimentalDefaultControls": {
|
||||
"background": true,
|
||||
"text": true,
|
||||
"link": true
|
||||
}
|
||||
},
|
||||
"__experimentalBorder": {
|
||||
"radius": true,
|
||||
"width": true,
|
||||
"color": true,
|
||||
"style": true
|
||||
},
|
||||
"typography": {
|
||||
"fontSize": true,
|
||||
"__experimentalDefaultControls": {
|
||||
"fontSize": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"selectedUser": {
|
||||
"type": "string",
|
||||
"default": "site"
|
||||
}
|
||||
},
|
||||
"editorScript": "file:./index.js",
|
||||
"viewScript": "file:./view.js",
|
||||
"style": [
|
||||
"file:./style-view.css",
|
||||
"wp-components"
|
||||
]
|
||||
}
|
@ -0,0 +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');
|
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-right:1rem;padding-left:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-left:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white);margin-right:1rem}
|
@ -0,0 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-left:1rem;padding-right:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white);margin-left:1rem}
|
@ -0,0 +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');
|
File diff suppressed because one or more lines are too long
@ -0,0 +1,57 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"name": "activitypub/followers",
|
||||
"apiVersion": 3,
|
||||
"version": "1.0.0",
|
||||
"title": "Fediverse Followers",
|
||||
"category": "widgets",
|
||||
"description": "Display your followers from the Fediverse on your website.",
|
||||
"textdomain": "activitypub",
|
||||
"icon": "groups",
|
||||
"supports": {
|
||||
"html": false
|
||||
},
|
||||
"attributes": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"default": "Fediverse Followers"
|
||||
},
|
||||
"selectedUser": {
|
||||
"type": "string",
|
||||
"default": "site"
|
||||
},
|
||||
"per_page": {
|
||||
"type": "number",
|
||||
"default": 10
|
||||
},
|
||||
"order": {
|
||||
"type": "string",
|
||||
"default": "desc",
|
||||
"enum": [
|
||||
"asc",
|
||||
"desc"
|
||||
]
|
||||
}
|
||||
},
|
||||
"styles": [
|
||||
{
|
||||
"name": "default",
|
||||
"label": "No Lines",
|
||||
"isDefault": true
|
||||
},
|
||||
{
|
||||
"name": "with-lines",
|
||||
"label": "Lines"
|
||||
},
|
||||
{
|
||||
"name": "compact",
|
||||
"label": "Compact"
|
||||
}
|
||||
],
|
||||
"editorScript": "file:./index.js",
|
||||
"viewScript": "file:./view.js",
|
||||
"style": [
|
||||
"file:./style-view.css",
|
||||
"wp-block-query-pagination"
|
||||
]
|
||||
}
|
@ -0,0 +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');
|
@ -0,0 +1,3 @@
|
||||
(()=>{var e={20:(e,t,a)=>{"use strict";var r=a(609),n=Symbol.for("react.element"),l=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),o=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,a){var r,c={},s=null,p=null;for(r in void 0!==a&&(s=""+a),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(p=t.ref),t)l.call(t,r)&&!i.hasOwnProperty(r)&&(c[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===c[r]&&(c[r]=t[r]);return{$$typeof:n,type:e,key:s,ref:p,props:c,_owner:o.current}}},848:(e,t,a)=>{"use strict";e.exports=a(20)},609:e=>{"use strict";e.exports=window.React},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t<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})})()})();
|
@ -0,0 +1 @@
|
||||
.activitypub-follower-block.is-style-compact .activitypub-handle,.activitypub-follower-block.is-style-compact .sep{display:none}.activitypub-follower-block.is-style-with-lines ul li{border-bottom:.5px solid;margin-bottom:.5rem;padding-bottom:.5rem}.activitypub-follower-block.is-style-with-lines ul li:last-child{border-bottom:none}.activitypub-follower-block.is-style-with-lines .activitypub-handle,.activitypub-follower-block.is-style-with-lines .activitypub-name{text-decoration:none}.activitypub-follower-block.is-style-with-lines .activitypub-handle:hover,.activitypub-follower-block.is-style-with-lines .activitypub-name:hover{text-decoration:underline}.activitypub-follower-block ul{margin:0!important;padding:0!important}.activitypub-follower-block li{display:flex;margin-bottom:1rem}.activitypub-follower-block img{border-radius:50%;height:40px;margin-left:var(--wp--preset--spacing--20,.5rem);width:40px}.activitypub-follower-block .activitypub-link{align-items:center;color:inherit!important;display:flex;flex-flow:row nowrap;max-width:100%;text-decoration:none!important}.activitypub-follower-block .activitypub-handle,.activitypub-follower-block .activitypub-name{text-decoration:underline;text-decoration-thickness:.8px;text-underline-position:under}.activitypub-follower-block .activitypub-handle:hover,.activitypub-follower-block .activitypub-name:hover{text-decoration:none}.activitypub-follower-block .activitypub-name{font-size:var(--wp--preset--font-size--normal,16px)}.activitypub-follower-block .activitypub-actor{font-size:var(--wp--preset--font-size--small,13px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follower-block .sep{padding:0 .2rem}.activitypub-follower-block .wp-block-query-pagination{margin-top:1.5rem}.activitypub-follower-block .activitypub-pager{cursor:default}.activitypub-follower-block .activitypub-pager.current{opacity:.33}.activitypub-follower-block .page-numbers{padding:0 .2rem}.activitypub-follower-block .page-numbers.current{font-weight:700;opacity:1}
|
@ -0,0 +1 @@
|
||||
.activitypub-follower-block.is-style-compact .activitypub-handle,.activitypub-follower-block.is-style-compact .sep{display:none}.activitypub-follower-block.is-style-with-lines ul li{border-bottom:.5px solid;margin-bottom:.5rem;padding-bottom:.5rem}.activitypub-follower-block.is-style-with-lines ul li:last-child{border-bottom:none}.activitypub-follower-block.is-style-with-lines .activitypub-handle,.activitypub-follower-block.is-style-with-lines .activitypub-name{text-decoration:none}.activitypub-follower-block.is-style-with-lines .activitypub-handle:hover,.activitypub-follower-block.is-style-with-lines .activitypub-name:hover{text-decoration:underline}.activitypub-follower-block ul{margin:0!important;padding:0!important}.activitypub-follower-block li{display:flex;margin-bottom:1rem}.activitypub-follower-block img{border-radius:50%;height:40px;margin-right:var(--wp--preset--spacing--20,.5rem);width:40px}.activitypub-follower-block .activitypub-link{align-items:center;color:inherit!important;display:flex;flex-flow:row nowrap;max-width:100%;text-decoration:none!important}.activitypub-follower-block .activitypub-handle,.activitypub-follower-block .activitypub-name{text-decoration:underline;text-decoration-thickness:.8px;text-underline-position:under}.activitypub-follower-block .activitypub-handle:hover,.activitypub-follower-block .activitypub-name:hover{text-decoration:none}.activitypub-follower-block .activitypub-name{font-size:var(--wp--preset--font-size--normal,16px)}.activitypub-follower-block .activitypub-actor{font-size:var(--wp--preset--font-size--small,13px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follower-block .sep{padding:0 .2rem}.activitypub-follower-block .wp-block-query-pagination{margin-top:1.5rem}.activitypub-follower-block .activitypub-pager{cursor:default}.activitypub-follower-block .activitypub-pager.current{opacity:.33}.activitypub-follower-block .page-numbers{padding:0 .2rem}.activitypub-follower-block .page-numbers.current{font-weight:700;opacity:1}
|
@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '111b88843c05346aadbf');
|
@ -0,0 +1,3 @@
|
||||
(()=>{var e,t={250:(e,t,a)=>{"use strict";const r=window.React,n=window.wp.apiFetch;var l=a.n(n);const o=window.wp.url,i=window.wp.element,c=window.wp.i18n;var s=a(942),p=a.n(s);function u({active:e,children:t,page:a,pageClick:n,className:l}){const o=p()("wp-block activitypub-pager",l,{current:e});return(0,r.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&n(a)}},t)}const m={outlined:"outlined",minimal:"minimal"};function f({compact:e,nextLabel:t,page:a,pageClick:n,perPage:l,prevLabel:o,total:i,variant:c=m.outlined}){const s=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(i/l)),f=p()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${c}`,{"is-compact":e});return(0,r.createElement)("nav",{className:f},o&&(0,r.createElement)(u,{key:"prev",page:a-1,pageClick:n,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},s.map((e=>(0,r.createElement)(u,{key:e,page:e,pageClick:n,active:e===a,className:"page-numbers"},e)))),t&&(0,r.createElement)(u,{key:"next",page:a+1,pageClick:n,active:a===Math.ceil(i/l),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}const v=window.wp.components,{namespace:b}=window._activityPubOptions;function d({selectedUser:e,per_page:t,order:a,title:n,page:s,setPage:p,className:u="",followLinks:m=!0,followerData:v=!1}){const d="site"===e?0:e,[g,y]=(0,r.useState)([]),[k,h]=(0,r.useState)(0),[E,N]=(0,r.useState)(0),[x,_]=function(){const[e,t]=(0,r.useState)(1);return[e,t]}(),O=s||x,S=p||_,C=(0,i.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */
|
||||
(0,c.__)("<span>←</span> Less","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),L=(0,i.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */
|
||||
(0,c.__)("More <span>→</span>","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),q=(e,a)=>{y(e),N(a),h(Math.ceil(a/t))};return(0,r.useEffect)((()=>{if(v&&1===O)return q(v.followers,v.total);const e=function(e,t,a,r){const n=`/${b}/actors/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,o.addQueryArgs)(n,l)}(d,t,a,O);l()({path:e}).then((e=>q(e.orderedItems,e.totalItems))).catch((()=>{}))}),[d,t,a,O,v]),(0,r.createElement)("div",{className:"activitypub-follower-block "+u},(0,r.createElement)("h3",null,n),(0,r.createElement)("ul",null,g&&g.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(w,{...e,followLinks:m}))))),k>1&&(0,r.createElement)(f,{page:O,perPage:t,total:E,pageClick:S,nextLabel:L,prevLabel:C,compact:"is-style-compact"===u}))}function w({name:e,icon:t,url:a,preferredUsername:n,followLinks:l=!0}){const o=`@${n}`,i={};return l||(i.onClick=e=>e.preventDefault()),(0,r.createElement)(v.ExternalLink,{className:"activitypub-link",href:a,title:o,...i},(0,r.createElement)("img",{width:"40",height:"40",src:t.url,className:"avatar activitypub-avatar",alt:e}),(0,r.createElement)("span",{className:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},e),(0,r.createElement)("span",{className:"sep"},"/"),(0,r.createElement)("span",{className:"activitypub-handle"},o)))}const g=window.wp.domReady;a.n(g)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,i.createRoot)(e).render((0,r.createElement)(d,{...t}))}))}))},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t<arguments.length;t++){var a=arguments[t];a&&(e=o(e,l(a)))}return e}function l(e){if("string"==typeof e||"number"==typeof e)return e;if("object"!=typeof e)return"";if(Array.isArray(e))return n.apply(null,e);if(e.toString!==Object.prototype.toString&&!e.toString.toString().includes("[native code]"))return e.toString();var t="";for(var a in e)r.call(e,a)&&e[a]&&(t=o(t,a));return t}function o(e,t){return t?e?e+" "+t:e+t:e}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},a={};function r(e){var n=a[e];if(void 0!==n)return n.exports;var l=a[e]={exports:{}};return t[e](l,l.exports,r),l.exports}r.m=t,e=[],r.O=(t,a,n,l)=>{if(!a){var o=1/0;for(p=0;p<e.length;p++){for(var[a,n,l]=e[p],i=!0,c=0;c<a.length;c++)(!1&l||o>=l)&&Object.keys(r.O).every((e=>r.O[e](a[c])))?a.splice(c--,1):(i=!1,l<o&&(o=l));if(i){e.splice(p--,1);var s=n();void 0!==s&&(t=s)}}return t}l=l||0;for(var p=e.length;p>0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[a,n,l]},r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var a in t)r.o(t,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={996:0,528:0};r.O.j=t=>0===e[t];var t=(t,a)=>{var n,l,[o,i,c]=a,s=0;if(o.some((t=>0!==e[t]))){for(n in i)r.o(i,n)&&(r.m[n]=i[n]);if(c)var p=c(r)}for(t&&t(a);s<o.length;s++)l=o[s],r.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return r.O(p)},a=globalThis.webpackChunkwordpress_activitypub=globalThis.webpackChunkwordpress_activitypub||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var n=r.O(void 0,[528],(()=>r(250)));n=r.O(n)})();
|
@ -0,0 +1,11 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"name": "activitypub/remote-reply",
|
||||
"apiVersion": 3,
|
||||
"version": "1.0.0",
|
||||
"title": "Reply on the Fediverse",
|
||||
"category": "widgets",
|
||||
"description": "",
|
||||
"textdomain": "activitypub",
|
||||
"viewScript": "file:./index.js"
|
||||
}
|
@ -0,0 +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');
|
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex}
|
@ -0,0 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex}
|
@ -0,0 +1,191 @@
|
||||
<?php
|
||||
/**
|
||||
* Inspired by the PHP ActivityPub Library by @Landrok
|
||||
*
|
||||
* @link https://github.com/landrok/activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity;
|
||||
|
||||
use Activitypub\Activity\Base_Object;
|
||||
|
||||
/**
|
||||
* \Activitypub\Activity\Activity implements the common
|
||||
* attributes of an Activity.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-core/#activities
|
||||
* @see https://www.w3.org/TR/activitystreams-core/#intransitiveactivities
|
||||
*/
|
||||
class Activity extends Base_Object {
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
);
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Activity';
|
||||
|
||||
/**
|
||||
* Describes the direct object of the activity.
|
||||
* For instance, in the activity "John added a movie to his
|
||||
* wishlist", the object of the activity is the movie added.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object-term
|
||||
*
|
||||
* @var string
|
||||
* | Base_Object
|
||||
* | Link
|
||||
* | null
|
||||
*/
|
||||
protected $object;
|
||||
|
||||
/**
|
||||
* Describes one or more entities that either performed or are
|
||||
* expected to perform the activity.
|
||||
* Any single activity can have multiple actors.
|
||||
* The actor MAY be specified using an indirect Link.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-actor
|
||||
*
|
||||
* @var string
|
||||
* | \ActivityPhp\Type\Extended\AbstractActor
|
||||
* | array<Actor>
|
||||
* | array<Link>
|
||||
* | Link
|
||||
*/
|
||||
protected $actor;
|
||||
|
||||
/**
|
||||
* The indirect object, or target, of the activity.
|
||||
* The precise meaning of the target is largely dependent on the
|
||||
* type of action being described but will often be the object of
|
||||
* the English preposition "to".
|
||||
* For instance, in the activity "John added a movie to his
|
||||
* wishlist", the target of the activity is John's wishlist.
|
||||
* An activity can have more than one target.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-target
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | array<ObjectType>
|
||||
* | Link
|
||||
* | array<Link>
|
||||
*/
|
||||
protected $target;
|
||||
|
||||
/**
|
||||
* Describes the result of the activity.
|
||||
* For instance, if a particular action results in the creation of
|
||||
* a new resource, the result property can be used to describe
|
||||
* that new resource.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-result
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* An indirect object of the activity from which the
|
||||
* activity is directed.
|
||||
* The precise meaning of the origin is the object of the English
|
||||
* preposition "from".
|
||||
* For instance, in the activity "John moved an item to List B
|
||||
* from List A", the origin of the activity is "List A".
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-origin
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
*/
|
||||
protected $origin;
|
||||
|
||||
/**
|
||||
* One or more objects used (or to be used) in the completion of an
|
||||
* Activity.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-instrument
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
*/
|
||||
protected $instrument;
|
||||
|
||||
/**
|
||||
* Set the object and copy Object properties to the Activity.
|
||||
*
|
||||
* Any to, bto, cc, bcc, and audience properties specified on the object
|
||||
* MUST be copied over to the new Create activity by the server.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#object-without-create
|
||||
*
|
||||
* @param string|Base_Objectr|Link|null $object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function set_object( $object ) {
|
||||
// convert array to object
|
||||
if ( is_array( $object ) ) {
|
||||
$object = self::init_from_array( $object );
|
||||
}
|
||||
|
||||
// set object
|
||||
$this->set( 'object', $object );
|
||||
|
||||
if ( ! is_object( $object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
|
||||
$this->set( $i, $object->get( $i ) );
|
||||
}
|
||||
|
||||
if ( $object->get_published() && ! $this->get_published() ) {
|
||||
$this->set( 'published', $object->get_published() );
|
||||
}
|
||||
|
||||
if ( $object->get_updated() && ! $this->get_updated() ) {
|
||||
$this->set( 'updated', $object->get_updated() );
|
||||
}
|
||||
|
||||
if ( $object->get_attributed_to() && ! $this->get_actor() ) {
|
||||
$this->set( 'actor', $object->get_attributed_to() );
|
||||
}
|
||||
|
||||
if ( $object->get_id() && ! $this->get_id() ) {
|
||||
$id = strtok( $object->get_id(), '#' );
|
||||
if ( $object->get_updated() ) {
|
||||
$updated = $object->get_updated();
|
||||
} else {
|
||||
$updated = $object->get_published();
|
||||
}
|
||||
$this->set( 'id', $id . '#activity-' . strtolower( $this->get_type() ) . '-' . $updated );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The context of an Activity is usually just the context of the object it contains.
|
||||
*
|
||||
* @return array $context A compacted JSON-LD context.
|
||||
*/
|
||||
public function get_json_ld_context() {
|
||||
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;'
|
||||
return $class::JSON_LD_CONTEXT;
|
||||
}
|
||||
}
|
||||
|
||||
return static::JSON_LD_CONTEXT;
|
||||
}
|
||||
}
|
@ -0,0 +1,174 @@
|
||||
<?php
|
||||
/**
|
||||
* Inspired by the PHP ActivityPub Library by @Landrok
|
||||
*
|
||||
* @link https://github.com/landrok/activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity;
|
||||
|
||||
/**
|
||||
* \Activitypub\Activity\Actor is an implementation of
|
||||
* one an Activity Streams Actor.
|
||||
*
|
||||
* Represents an individual actor.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
||||
*/
|
||||
class Actor extends Base_Object {
|
||||
// Reduced context for actors. TODO: still unused.
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
'https://w3id.org/security/v1',
|
||||
'https://purl.archive.org/socialweb/webfinger',
|
||||
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',
|
||||
'value' => 'schema:value',
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'featured' => array(
|
||||
'@id' => 'toot:featured',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'featuredTags' => array(
|
||||
'@id' => 'toot:featuredTags',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'moderators' => array(
|
||||
'@id' => 'lemmy:moderators',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods',
|
||||
'discoverable' => 'toot:discoverable',
|
||||
'indexable' => 'toot:indexable',
|
||||
'resource' => 'webfinger:resource',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* A reference to an ActivityStreams OrderedCollection comprised of
|
||||
* all the messages received by the actor.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#inbox
|
||||
*
|
||||
* @var string
|
||||
* | null
|
||||
*/
|
||||
protected $inbox;
|
||||
|
||||
/**
|
||||
* A reference to an ActivityStreams OrderedCollection comprised of
|
||||
* all the messages produced by the actor.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#outbox
|
||||
*
|
||||
* @var string
|
||||
* | null
|
||||
*/
|
||||
protected $outbox;
|
||||
|
||||
/**
|
||||
* A link to an ActivityStreams collection of the actors that this
|
||||
* actor is following.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#following
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $following;
|
||||
|
||||
/**
|
||||
* A link to an ActivityStreams collection of the actors that
|
||||
* follow this actor.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#followers
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $followers;
|
||||
|
||||
/**
|
||||
* A link to an ActivityStreams collection of objects this actor has
|
||||
* liked.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#liked
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $liked;
|
||||
|
||||
/**
|
||||
* A list of supplementary Collections which may be of interest.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#streams-property
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $streams = array();
|
||||
|
||||
/**
|
||||
* A short username which may be used to refer to the actor, with no
|
||||
* uniqueness guarantees.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#preferredUsername
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $preferred_username;
|
||||
|
||||
/**
|
||||
* A JSON object which maps additional typically server/domain-wide
|
||||
* endpoints which may be useful either for this actor or someone
|
||||
* referencing this actor. This mapping may be nested inside the
|
||||
* actor document as the value or may be a link to a JSON-LD
|
||||
* document with these properties.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#endpoints
|
||||
*
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $endpoints;
|
||||
|
||||
/**
|
||||
* It's not part of the ActivityPub protocol but it's a quite common
|
||||
* practice to handle an actor public key with a publicKey array:
|
||||
* [
|
||||
* 'id' => 'https://my-example.com/actor#main-key'
|
||||
* 'owner' => 'https://my-example.com/actor',
|
||||
* 'publicKeyPem' => '-----BEGIN PUBLIC KEY-----
|
||||
* MIIBI [...]
|
||||
* DQIDAQAB
|
||||
* -----END PUBLIC KEY-----'
|
||||
* ]
|
||||
*
|
||||
* @see https://www.w3.org/wiki/SocialCG/ActivityPub/Authentication_Authorization#Signing_requests_using_HTTP_Signatures
|
||||
*
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $public_key;
|
||||
|
||||
/**
|
||||
* It's not part of the ActivityPub protocol but it's a quite common
|
||||
* practice to lock an account. If anabled, new followers will not be
|
||||
* automatically accepted, but will instead require you to manually
|
||||
* approve them.
|
||||
*
|
||||
* WordPress does only support 'false' at the moment.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#as
|
||||
*
|
||||
* @context as:manuallyApprovesFollowers
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $manually_approves_followers = false;
|
||||
}
|
@ -0,0 +1,714 @@
|
||||
<?php
|
||||
/**
|
||||
* Inspired by the PHP ActivityPub Library by @Landrok
|
||||
*
|
||||
* @link https://github.com/landrok/activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity;
|
||||
|
||||
use WP_Error;
|
||||
use ReflectionClass;
|
||||
use DateTime;
|
||||
|
||||
use function Activitypub\camel_to_snake_case;
|
||||
use function Activitypub\snake_to_camel_case;
|
||||
|
||||
/**
|
||||
* Base_Object is an implementation of one of the
|
||||
* Activity Streams Core Types.
|
||||
*
|
||||
* The Object is the primary base type for the Activity Streams
|
||||
* vocabulary.
|
||||
*
|
||||
* Note: Object is a reserved keyword in PHP. It has been suffixed with
|
||||
* 'Base_' for this reason.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-core/#object
|
||||
*/
|
||||
class Base_Object {
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
array(
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* The object's unique global identifier
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#obj-id
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Object';
|
||||
|
||||
/**
|
||||
* A resource attached or related to an object that potentially
|
||||
* requires special handling.
|
||||
* The intent is to provide a model that is at least semantically
|
||||
* similar to attachments in email.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attachment
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $attachment;
|
||||
|
||||
/**
|
||||
* One or more entities to which this object is attributed.
|
||||
* The attributed entities might not be Actors. For instance, an
|
||||
* object might be attributed to the completion of another activity.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attributedto
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $attributed_to;
|
||||
|
||||
/**
|
||||
* One or more entities that represent the total population of
|
||||
* entities for which the object can considered to be relevant.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audience
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $audience;
|
||||
|
||||
/**
|
||||
* The content or textual representation of the Object encoded as a
|
||||
* JSON string. By default, the value of content is HTML.
|
||||
* The mediaType property can be used in the object to indicate a
|
||||
* different content type.
|
||||
*
|
||||
* The content MAY be expressed using multiple language-tagged
|
||||
* values.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-content
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $content;
|
||||
|
||||
/**
|
||||
* The context within which the object exists or an activity was
|
||||
* performed.
|
||||
* The notion of "context" used is intentionally vague.
|
||||
* The intended function is to serve as a means of grouping objects
|
||||
* and activities that share a common originating context or
|
||||
* purpose. An example could be all activities relating to a common
|
||||
* project or event.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
/**
|
||||
* The content MAY be expressed using multiple language-tagged
|
||||
* values.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-content
|
||||
*
|
||||
* @var array|null
|
||||
*/
|
||||
protected $content_map;
|
||||
|
||||
/**
|
||||
* A simple, human-readable, plain-text name for the object.
|
||||
* HTML markup MUST NOT be included.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-name
|
||||
*
|
||||
* @var string|null xsd:string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The name MAY be expressed using multiple language-tagged values.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-name
|
||||
*
|
||||
* @var array|null rdf:langString
|
||||
*/
|
||||
protected $name_map;
|
||||
|
||||
/**
|
||||
* The date and time describing the actual or expected ending time
|
||||
* of the object.
|
||||
* When used with an Activity object, for instance, the endTime
|
||||
* property specifies the moment the activity concluded or
|
||||
* is expected to conclude.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-endtime
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $end_time;
|
||||
|
||||
/**
|
||||
* The entity (e.g. an application) that generated the object.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $generator;
|
||||
|
||||
/**
|
||||
* An entity that describes an icon for this object.
|
||||
* The image should have an aspect ratio of one (horizontal)
|
||||
* to one (vertical) and should be suitable for presentation
|
||||
* at a small size.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
|
||||
*
|
||||
* @var string
|
||||
* | Image
|
||||
* | Link
|
||||
* | array<Image>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $icon;
|
||||
|
||||
/**
|
||||
* An entity that describes an image for this object.
|
||||
* Unlike the icon property, there are no aspect ratio
|
||||
* or display size limitations assumed.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image-term
|
||||
*
|
||||
* @var string
|
||||
* | Image
|
||||
* | Link
|
||||
* | array<Image>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* One or more entities for which this object is considered a
|
||||
* response.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-inreplyto
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $in_reply_to;
|
||||
|
||||
/**
|
||||
* One or more physical or logical locations associated with the
|
||||
* object.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-location
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $location;
|
||||
|
||||
/**
|
||||
* An entity that provides a preview of this object.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-preview
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
*/
|
||||
protected $preview;
|
||||
|
||||
/**
|
||||
* The date and time at which the object was published
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-published
|
||||
*
|
||||
* @var string|null xsd:dateTime
|
||||
*/
|
||||
protected $published;
|
||||
|
||||
/**
|
||||
* The date and time describing the actual or expected starting time
|
||||
* of the object.
|
||||
* When used with an Activity object, for instance, the startTime
|
||||
* property specifies the moment the activity began
|
||||
* or is scheduled to begin.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-starttime
|
||||
*
|
||||
* @var string|null xsd:dateTime
|
||||
*/
|
||||
protected $start_time;
|
||||
|
||||
/**
|
||||
* A natural language summarization of the object encoded as HTML.
|
||||
* Multiple language tagged summaries MAY be provided.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
*/
|
||||
protected $summary;
|
||||
|
||||
/**
|
||||
* The content MAY be expressed using multiple language-tagged
|
||||
* values.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary
|
||||
*
|
||||
* @var array<string>|null
|
||||
*/
|
||||
protected $summary_map;
|
||||
|
||||
/**
|
||||
* One or more "tags" that have been associated with an objects.
|
||||
* A tag can be any kind of Object.
|
||||
* The key difference between attachment and tag is that the former
|
||||
* implies association by inclusion, while the latter implies
|
||||
* associated by reference.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $tag;
|
||||
|
||||
/**
|
||||
* The date and time at which the object was updated
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-updated
|
||||
*
|
||||
* @var string|null xsd:dateTime
|
||||
*/
|
||||
protected $updated;
|
||||
|
||||
/**
|
||||
* One or more links to representations of the object.
|
||||
*
|
||||
* @var string
|
||||
* | array<string>
|
||||
* | Link
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* An entity considered to be part of the public primary audience
|
||||
* of an Object
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-to
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $to;
|
||||
|
||||
/**
|
||||
* An Object that is part of the private primary audience of this
|
||||
* Object.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bto
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $bto;
|
||||
|
||||
/**
|
||||
* An Object that is part of the public secondary audience of this
|
||||
* Object.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-cc
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $cc;
|
||||
|
||||
/**
|
||||
* One or more Objects that are part of the private secondary
|
||||
* audience of this Object.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bcc
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
*/
|
||||
protected $bcc;
|
||||
|
||||
/**
|
||||
* The MIME media type of the value of the content property.
|
||||
* If not specified, the content property is assumed to contain
|
||||
* text/html content.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-mediatype
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $media_type;
|
||||
|
||||
/**
|
||||
* When the object describes a time-bound resource, such as an audio
|
||||
* or video, a meeting, etc, the duration property indicates the
|
||||
* object's approximate duration.
|
||||
* The value MUST be expressed as an xsd:duration as defined by
|
||||
* xmlschema11-2, section 3.3.6 (e.g. a period of 5 seconds is
|
||||
* represented as "PT5S").
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-duration
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $duration;
|
||||
|
||||
/**
|
||||
* Intended to convey some sort of source from which the content
|
||||
* markup was derived, as a form of provenance, or to support
|
||||
* future editing by clients.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#source-property
|
||||
*
|
||||
* @var ObjectType
|
||||
*/
|
||||
protected $source;
|
||||
|
||||
/**
|
||||
* A Collection containing objects considered to be responses to
|
||||
* this object.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies
|
||||
*
|
||||
* @var string
|
||||
* | Collection
|
||||
* | Link
|
||||
* | null
|
||||
*/
|
||||
protected $replies;
|
||||
|
||||
/**
|
||||
* 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 ) );
|
||||
|
||||
if ( \strncasecmp( $method, 'get', 3 ) === 0 ) {
|
||||
if ( ! $this->has( $var ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
return $this->$var;
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
|
||||
return $this->set( $var, $params[0] );
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'add', 3 ) === 0 ) {
|
||||
$this->add( $var, $params[0] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic function, to transform the object to string.
|
||||
*
|
||||
* @return string The object id.
|
||||
*/
|
||||
public function __toString() {
|
||||
return $this->to_string();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to transform the object to string.
|
||||
*
|
||||
* @return string The object id.
|
||||
*/
|
||||
public function to_string() {
|
||||
return $this->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic getter.
|
||||
*
|
||||
* @param string $key The key to get.
|
||||
*
|
||||
* @return mixed The value.
|
||||
*/
|
||||
public function get( $key ) {
|
||||
if ( ! $this->has( $key ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
return call_user_func( array( $this, 'get_' . $key ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the object has a key
|
||||
*
|
||||
* @param string $key The key to check.
|
||||
*
|
||||
* @return boolean True if the object has the key.
|
||||
*/
|
||||
public function has( $key ) {
|
||||
return property_exists( $this, $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic setter.
|
||||
*
|
||||
* @param string $key The key to set.
|
||||
* @param string $value The value to set.
|
||||
*
|
||||
* @return mixed The value.
|
||||
*/
|
||||
public function set( $key, $value ) {
|
||||
if ( ! $this->has( $key ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$this->$key = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic adder.
|
||||
*
|
||||
* @param string $key The key to set.
|
||||
* @param mixed $value The value to add.
|
||||
*
|
||||
* @return mixed The value.
|
||||
*/
|
||||
public function add( $key, $value ) {
|
||||
if ( ! $this->has( $key ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
if ( ! isset( $this->$key ) ) {
|
||||
$this->$key = array();
|
||||
}
|
||||
|
||||
$attributes = $this->$key;
|
||||
$attributes[] = $value;
|
||||
|
||||
$this->$key = $attributes;
|
||||
|
||||
return $this->$key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array.
|
||||
*
|
||||
* @return string The JSON string.
|
||||
*
|
||||
* @return \Activitypub\Activity\Base_Object An Object built from the JSON string.
|
||||
*/
|
||||
public static function init_from_json( $json ) {
|
||||
$array = \json_decode( $json, true );
|
||||
|
||||
if ( ! is_array( $array ) ) {
|
||||
$array = array();
|
||||
}
|
||||
|
||||
return self::init_from_array( $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array.
|
||||
*
|
||||
* @return string The object array.
|
||||
*
|
||||
* @return \Activitypub\Activity\Base_Object An Object built from the JSON string.
|
||||
*/
|
||||
public static function init_from_array( $array ) {
|
||||
if ( ! is_array( $array ) ) {
|
||||
return new WP_Error( 'invalid_array', __( 'Invalid array', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$object = new static();
|
||||
|
||||
foreach ( $array as $key => $value ) {
|
||||
$key = camel_to_snake_case( $key );
|
||||
call_user_func( array( $object, 'set_' . $key ), $value );
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array and pre-fill the object.
|
||||
*
|
||||
* @param string $json The JSON string.
|
||||
*/
|
||||
public function from_json( $json ) {
|
||||
$array = \json_decode( $json, true );
|
||||
|
||||
$this->from_array( $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array and pre-fill the object.
|
||||
*
|
||||
* @param array $array The array.
|
||||
*/
|
||||
public function from_array( $array ) {
|
||||
foreach ( $array as $key => $value ) {
|
||||
if ( $value ) {
|
||||
$key = camel_to_snake_case( $key );
|
||||
call_user_func( array( $this, 'set_' . $key ), $value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Object to an array.
|
||||
*
|
||||
* It tries to get the object attributes if they exist
|
||||
* and falls back to the getters. Empty values are ignored.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return array An array built from the Object.
|
||||
*/
|
||||
public function to_array( $include_json_ld_context = true ) {
|
||||
$array = array();
|
||||
$vars = get_object_vars( $this );
|
||||
|
||||
foreach ( $vars as $key => $value ) {
|
||||
// ignotre all _prefixed keys.
|
||||
if ( '_' === substr( $key, 0, 1 ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if value is empty, try to get it from a getter.
|
||||
if ( ! $value ) {
|
||||
$value = call_user_func( array( $this, 'get_' . $key ) );
|
||||
}
|
||||
|
||||
if ( is_object( $value ) ) {
|
||||
$value = $value->to_array( false );
|
||||
}
|
||||
|
||||
// if value is still empty, ignore it for the array and continue.
|
||||
if ( isset( $value ) ) {
|
||||
$array[ snake_to_camel_case( $key ) ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $include_json_ld_context ) {
|
||||
// Get JsonLD context and move it to '@context' at the top.
|
||||
$array = array_merge( array( '@context' => $this->get_json_ld_context() ), $array );
|
||||
}
|
||||
|
||||
$class = new ReflectionClass( $this );
|
||||
$class = strtolower( $class->getShortName() );
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Object to JSON.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return string The JSON string.
|
||||
*/
|
||||
public function to_json( $include_json_ld_context = true ) {
|
||||
$array = $this->to_array( $include_json_ld_context );
|
||||
$options = \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT;
|
||||
|
||||
/*
|
||||
* Options to be passed to json_encode()
|
||||
*
|
||||
* @param int $options The current options flags
|
||||
*/
|
||||
$options = \apply_filters( 'activitypub_json_encode_options', $options );
|
||||
|
||||
return \wp_json_encode( $array, $options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the keys of the object vars.
|
||||
*
|
||||
* @return array The keys of the object vars.
|
||||
*/
|
||||
public function get_object_var_keys() {
|
||||
return \array_keys( \get_object_vars( $this ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON-LD context of this object.
|
||||
*
|
||||
* @return array $context A compacted JSON-LD context for the ActivityPub object.
|
||||
*/
|
||||
public function get_json_ld_context() {
|
||||
return static::JSON_LD_CONTEXT;
|
||||
}
|
||||
}
|
@ -0,0 +1,340 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub Object of type Event.
|
||||
*
|
||||
* @package activity-event-transformers
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity\Extended_Object;
|
||||
|
||||
use Activitypub\Activity\Base_Object;
|
||||
|
||||
/**
|
||||
* Event is an implementation of one of the Activity Streams Event object type.
|
||||
*
|
||||
* This class contains extra keys as used by Mobilizon to ensure compatibility.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event
|
||||
*/
|
||||
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.
|
||||
array( // The keys here override/extend the context even more.
|
||||
'pt' => 'https://joinpeertube.org/ns#',
|
||||
'mz' => 'https://joinmobilizon.org/ns#',
|
||||
'status' => 'http://www.w3.org/2002/12/cal/ical#status',
|
||||
'commentsEnabled' => 'pt:commentsEnabled',
|
||||
'isOnline' => 'mz:isOnline',
|
||||
'timezone' => 'mz:timezone',
|
||||
'participantCount' => 'mz:participantCount',
|
||||
'anonymousParticipationEnabled' => 'mz:anonymousParticipationEnabled',
|
||||
'joinMode' => array(
|
||||
'@id' => 'mz:joinMode',
|
||||
'@type' => 'mz:joinModeType',
|
||||
),
|
||||
'externalParticipationUrl' => array(
|
||||
'@id' => 'mz:externalParticipationUrl',
|
||||
'@type' => 'schema:URL',
|
||||
),
|
||||
'repliesModerationOption' => array(
|
||||
'@id' => 'mz:repliesModerationOption',
|
||||
'@type' => '@vocab',
|
||||
),
|
||||
'contacts' => array(
|
||||
'@id' => 'mz:contacts',
|
||||
'@type' => '@id',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* Mobilizon compatible values for repliesModertaionOption.
|
||||
* @var array
|
||||
*/
|
||||
const REPLIES_MODERATION_OPTION_TYPES = array( 'allow_all', 'closed' );
|
||||
|
||||
/**
|
||||
* Mobilizon compatible values for joinModeTypes.
|
||||
*/
|
||||
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' );
|
||||
|
||||
/**
|
||||
* Default event categories.
|
||||
*
|
||||
* These values currently reflect the default set as proposed by Mobilizon to maximize interoperability.
|
||||
* @var array
|
||||
*/
|
||||
const DEFAULT_EVENT_CATEGORIES = array(
|
||||
'ARTS',
|
||||
'BOOK_CLUBS',
|
||||
'BUSINESS',
|
||||
'CAUSES',
|
||||
'COMEDY',
|
||||
'CRAFTS',
|
||||
'FOOD_DRINK',
|
||||
'HEALTH',
|
||||
'MUSIC',
|
||||
'AUTO_BOAT_AIR',
|
||||
'COMMUNITY',
|
||||
'FAMILY_EDUCATION',
|
||||
'FASHION_BEAUTY',
|
||||
'FILM_MEDIA',
|
||||
'GAMES',
|
||||
'LANGUAGE_CULTURE',
|
||||
'LEARNING',
|
||||
'LGBTQ',
|
||||
'MOVEMENTS_POLITICS',
|
||||
'NETWORKING',
|
||||
'PARTY',
|
||||
'PERFORMING_VISUAL_ARTS',
|
||||
'PETS',
|
||||
'PHOTOGRAPHY',
|
||||
'OUTDOORS_ADVENTURE',
|
||||
'SPIRITUALITY_RELIGION_BELIEFS',
|
||||
'SCIENCE_TECH',
|
||||
'SPORTS',
|
||||
'THEATRE',
|
||||
'MEETING', // Default value.
|
||||
);
|
||||
|
||||
/**
|
||||
* Event is an implementation of one of the
|
||||
* Activity Streams
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Event';
|
||||
|
||||
/**
|
||||
* The Title of the event.
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The events contacts
|
||||
*
|
||||
* @context {
|
||||
* '@id' => 'mz:contacts',
|
||||
* '@type' => '@id',
|
||||
* }
|
||||
*
|
||||
* @var array Array of contacts (ActivityPub actor IDs).
|
||||
*/
|
||||
protected $contacts;
|
||||
|
||||
/**
|
||||
* Extension invented by PeerTube whether comments/replies are <enabled>
|
||||
* Mobilizon also implemented this as a fallback to their own
|
||||
* repliesModerationOption.
|
||||
*
|
||||
* @see https://docs.joinpeertube.org/api/activitypub#video
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/
|
||||
* @var bool|null
|
||||
*/
|
||||
protected $comments_enabled;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#timezone
|
||||
* @var string
|
||||
*/
|
||||
protected $timezone;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#repliesModerationOption
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#repliesmoderation
|
||||
* @var string
|
||||
*/
|
||||
protected $replies_moderation_option;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#anonymousParticipationEnabled
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#anonymousparticipationenabled
|
||||
* @var bool
|
||||
*/
|
||||
protected $anonymous_participation_enabled;
|
||||
|
||||
/**
|
||||
* @context https://schema.org/category
|
||||
* @var enum
|
||||
*/
|
||||
protected $category;
|
||||
|
||||
/**
|
||||
* @context https://schema.org/inLanguage
|
||||
* @var
|
||||
*/
|
||||
protected $in_language;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#isOnline
|
||||
* @var bool
|
||||
*/
|
||||
protected $is_online;
|
||||
|
||||
/**
|
||||
* @context https://www.w3.org/2002/12/cal/ical#status
|
||||
* @var enum
|
||||
*/
|
||||
protected $status;
|
||||
|
||||
/**
|
||||
* Which actor created the event.
|
||||
*
|
||||
* This field is needed due to the current group structure of Mobilizon.
|
||||
*
|
||||
* @todo this seems to not be a default property of an Object but needed by mobilizon.
|
||||
* @var string
|
||||
*/
|
||||
protected $actor;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#externalParticipationUrl
|
||||
* @var string
|
||||
*/
|
||||
protected $external_participation_url;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#joinMode
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#joinmode
|
||||
* @var
|
||||
*/
|
||||
protected $join_mode;
|
||||
|
||||
/**
|
||||
* @context https://joinmobilizon.org/ns#participantCount
|
||||
* @var int
|
||||
*/
|
||||
protected $participant_count;
|
||||
|
||||
/**
|
||||
* @context https://schema.org/maximumAttendeeCapacity
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#maximumattendeecapacity
|
||||
* @var int
|
||||
*/
|
||||
protected $maximum_attendee_capacity;
|
||||
|
||||
/**
|
||||
* @context https://schema.org/remainingAttendeeCapacity
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#remainignattendeecapacity
|
||||
* @var int
|
||||
*/
|
||||
protected $remaining_attendee_capacity;
|
||||
|
||||
/**
|
||||
* Setter for the timezone.
|
||||
*
|
||||
* 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'.
|
||||
*/
|
||||
public function set_timezone( $timezone ) {
|
||||
if ( in_array( $timezone, timezone_identifiers_list(), true ) ) {
|
||||
$this->timezone = $timezone;
|
||||
} else {
|
||||
$this->timezone = wp_timezone_string();
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom setter for repliesModerationOption which also directy sets commentsEnabled accordingly.
|
||||
*
|
||||
* @param string $type
|
||||
*/
|
||||
public function set_replies_moderation_option( $type ) {
|
||||
if ( in_array( $type, self::REPLIES_MODERATION_OPTION_TYPES, true ) ) {
|
||||
$this->replies_moderation_option = $type;
|
||||
$this->comments_enabled = ( 'allow_all' === $type ) ? true : false;
|
||||
} else {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
'The replies moderation option must be either allow_all or closed.',
|
||||
'<version_placeholder>'
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom setter for commentsEnabled which also directly sets repliesModerationOption accordingly.
|
||||
*
|
||||
* @param bool $comments_enabled
|
||||
*/
|
||||
public function set_comments_enabled( $comments_enabled ) {
|
||||
if ( is_bool( $comments_enabled ) ) {
|
||||
$this->comments_enabled = $comments_enabled;
|
||||
$this->replies_moderation_option = $comments_enabled ? 'allow_all' : 'closed';
|
||||
} else {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
'The commentsEnabled must be boolean.',
|
||||
'<version_placeholder>'
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom setter for the ical status that checks whether the status is an ical event status.
|
||||
*
|
||||
* @param string $status
|
||||
*/
|
||||
public function set_status( $status ) {
|
||||
if ( in_array( $status, self::ICAL_EVENT_STATUS_TYPES, true ) ) {
|
||||
$this->status = $status;
|
||||
} else {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
'The status of the event must be a VEVENT iCal status.',
|
||||
'<version_placeholder>'
|
||||
);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom setter for the event category.
|
||||
*
|
||||
* Falls back to Mobilizons default category.
|
||||
*
|
||||
* @param string $category
|
||||
* @param bool $mobilizon_compatibilty Whether the category must be compatibly with Mobilizon.
|
||||
*/
|
||||
public function set_category( $category, $mobilizon_compatibilty = true ) {
|
||||
if ( $mobilizon_compatibilty ) {
|
||||
$this->category = in_array( $category, self::DEFAULT_EVENT_CATEGORIES, true ) ? $category : 'MEETING';
|
||||
} else {
|
||||
$this->category = $category;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom setter for an external participation url.
|
||||
*
|
||||
* Automatically sets the joinMode to true if called.
|
||||
*
|
||||
* @param string $url
|
||||
*/
|
||||
public function set_external_participation_url( $url ) {
|
||||
if ( preg_match( '/^https?:\/\/.*/i', $url ) ) {
|
||||
$this->external_participation_url = $url;
|
||||
$this->join_mode = 'external';
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
}
|
@ -0,0 +1,93 @@
|
||||
<?php
|
||||
/**
|
||||
* Event is an implementation of one of the
|
||||
* Activity Streams Event object type
|
||||
*
|
||||
* @package activity-event-transformers
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity\Extended_Object;
|
||||
|
||||
use Activitypub\Activity\Base_Object;
|
||||
|
||||
/**
|
||||
* Event is an implementation of one of the
|
||||
* Activity Streams Event object type
|
||||
*
|
||||
* The Object is the primary base type for the Activity Streams
|
||||
* vocabulary.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-event
|
||||
*/
|
||||
class Place extends Base_Object {
|
||||
/**
|
||||
* Place is an implementation of one of the
|
||||
* Activity Streams
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Place';
|
||||
|
||||
/**
|
||||
* Indicates the accuracy of position coordinates on a Place objects.
|
||||
* Expressed in properties of percentage. e.g. "94.0" means "94.0% accurate".
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-accuracy
|
||||
* @var float xsd:float [>= 0.0f, <= 100.0f]
|
||||
*/
|
||||
protected $accuracy;
|
||||
|
||||
/**
|
||||
* Indicates the altitude of a place. The measurement units is indicated using the units property.
|
||||
* If units is not specified, the default is assumed to be "m" indicating meters.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-altitude
|
||||
* @var float xsd:float
|
||||
*/
|
||||
protected $altitude;
|
||||
|
||||
/**
|
||||
* The latitude of a place.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-latitude
|
||||
* @var float xsd:float
|
||||
*/
|
||||
protected $latitude;
|
||||
|
||||
/**
|
||||
* The longitude of a place.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-longitude
|
||||
* @var float xsd:float
|
||||
*/
|
||||
protected $longitude;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-radius
|
||||
* @var float
|
||||
*/
|
||||
protected $radius;
|
||||
|
||||
/**
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-units
|
||||
* @var string
|
||||
*/
|
||||
protected $units;
|
||||
|
||||
/**
|
||||
* @var Postal_Address|string
|
||||
*/
|
||||
protected $address;
|
||||
|
||||
public function set_address( $address ) {
|
||||
if ( is_string( $address ) || is_array( $address ) ) {
|
||||
$this->address = $address;
|
||||
} else {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
'The address must be either a string or an array like schema.org/PostalAddress.',
|
||||
'<version_placeholder>'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,238 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Post;
|
||||
use WP_Comment;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Users;
|
||||
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
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/
|
||||
*/
|
||||
class Activity_Dispatcher {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_send_post', array( self::class, 'send_post' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_comment', array( self::class, 'send_comment' ), 10, 2 );
|
||||
|
||||
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_update_profile_activity', array( self::class, 'send_profile_update' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Activities to followers and mentioned users or `Announce` (boost) a blog post.
|
||||
*
|
||||
* @param mixed $wp_object The ActivityPub Post.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_activity_or_announce( $wp_object, $type ) {
|
||||
if ( is_user_type_disabled( 'blog' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_single_user() ) {
|
||||
self::send_activity( $wp_object, $type, Users::BLOG_USER_ID );
|
||||
} else {
|
||||
self::send_announce( $wp_object, $type );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Activities to followers and mentioned users.
|
||||
*
|
||||
* @param mixed $wp_object The ActivityPub Post.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_activity( $wp_object, $type, $user_id = null ) {
|
||||
$transformer = Factory::get_transformer( $wp_object ); // Could potentially return a `\WP_Error` instance.
|
||||
|
||||
if ( \is_wp_error( $transformer ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( null !== $user_id ) {
|
||||
$transformer->change_wp_user_id( $user_id );
|
||||
}
|
||||
|
||||
$user_id = $transformer->get_wp_user_id();
|
||||
|
||||
if ( is_user_disabled( $user_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$activity = $transformer->to_activity( $type );
|
||||
|
||||
self::send_activity_to_followers( $activity, $user_id, $wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Announces to followers and mentioned users.
|
||||
*
|
||||
* @param mixed $wp_object The ActivityPub Post.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_announce( $wp_object, $type ) {
|
||||
if ( ! in_array( $type, array( 'Create', 'Update', 'Delete' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transformer = Factory::get_transformer( $wp_object );
|
||||
|
||||
if ( \is_wp_error( $transformer ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = Users::BLOG_USER_ID;
|
||||
$activity = $transformer->to_activity( $type );
|
||||
$user = Users::get_by_id( Users::BLOG_USER_ID );
|
||||
|
||||
$announce = new Activity();
|
||||
$announce->set_type( 'Announce' );
|
||||
$announce->set_object( $activity );
|
||||
$announce->set_actor( $user->get_id() );
|
||||
|
||||
self::send_activity_to_followers( $announce, $user_id, $wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "Update" Activity when a user updates their profile.
|
||||
*
|
||||
* @param int $user_id The user ID to send an update for.
|
||||
*
|
||||
* @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
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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
|
||||
self::send_activity_to_followers( $activity, $user_id, $user );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an Activity to all followers and mentioned users.
|
||||
*
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
* @param int $user_id The user ID.
|
||||
* @param WP_User|WP_Post|WP_Comment $wp_object The WordPress object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function send_activity_to_followers( $activity, $user_id, $wp_object ) {
|
||||
// check if the Activity should be send to the followers
|
||||
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 );
|
||||
$inboxes = array_unique( $inboxes );
|
||||
|
||||
if ( empty( $inboxes ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$json = $activity->to_json();
|
||||
|
||||
foreach ( $inboxes as $inbox ) {
|
||||
safe_remote_post( $inbox, $json, $user_id );
|
||||
}
|
||||
|
||||
set_wp_object_state( $wp_object, 'federated' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "Create" or "Update" Activity for a WordPress Post.
|
||||
*
|
||||
* @param int $id The WordPress Post ID.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_post( $id, $type ) {
|
||||
$post = get_post( $id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
do_action( 'activitypub_send_activity', $post, $type );
|
||||
do_action(
|
||||
sprintf(
|
||||
'activitypub_send_%s_activity',
|
||||
\strtolower( $type )
|
||||
),
|
||||
$post
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "Create" or "Update" Activity for a WordPress Comment.
|
||||
*
|
||||
* @param int $id The WordPress Comment ID.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_comment( $id, $type ) {
|
||||
$comment = get_comment( $id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return;
|
||||
}
|
||||
|
||||
do_action( 'activitypub_send_activity', $comment, $type );
|
||||
do_action(
|
||||
sprintf(
|
||||
'activitypub_send_%s_activity',
|
||||
\strtolower( $type )
|
||||
),
|
||||
$comment
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,561 @@
|
||||
<?php
|
||||
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;
|
||||
|
||||
/**
|
||||
* ActivityPub Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Activitypub {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'template_include', array( self::class, 'render_json_template' ), 99 );
|
||||
\add_action( 'template_redirect', array( self::class, 'template_redirect' ) );
|
||||
\add_filter( 'query_vars', array( self::class, 'add_query_vars' ) );
|
||||
\add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
|
||||
|
||||
// Add support for ActivityPub to custom post types
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post' ) ) : array();
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
\add_post_type_support( $post_type, 'activitypub' );
|
||||
}
|
||||
|
||||
\add_action( 'wp_trash_post', array( self::class, 'trash_post' ), 1 );
|
||||
\add_action( 'untrash_post', array( self::class, 'untrash_post' ), 1 );
|
||||
|
||||
\add_action( 'init', array( self::class, 'add_rewrite_rules' ), 11 );
|
||||
\add_action( 'init', array( self::class, 'theme_compat' ), 11 );
|
||||
|
||||
\add_action( 'user_register', array( self::class, 'user_register' ) );
|
||||
|
||||
\add_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 );
|
||||
|
||||
// register several post_types
|
||||
self::register_post_types();
|
||||
}
|
||||
|
||||
/**
|
||||
* Activation Hook
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function activate() {
|
||||
self::flush_rewrite_rules();
|
||||
Scheduler::register_schedules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivation Hook
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function deactivate() {
|
||||
self::flush_rewrite_rules();
|
||||
Scheduler::deregister_schedules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall Hook
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function uninstall() {
|
||||
Scheduler::deregister_schedules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a AS2 JSON version of an author, post or page.
|
||||
*
|
||||
* @param string $template The path to the template object.
|
||||
*
|
||||
* @return string The new path to the JSON template.
|
||||
*/
|
||||
public static function render_json_template( $template ) {
|
||||
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
if ( ! is_activitypub_request() ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
$json_template = false;
|
||||
|
||||
if ( \is_author() && ! is_user_disabled( \get_the_author_meta( 'ID' ) ) ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/author-json.php';
|
||||
} elseif ( is_comment() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/comment-json.php';
|
||||
} elseif ( \is_singular() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
|
||||
} elseif ( \is_home() && ! is_user_type_disabled( 'blog' ) ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php';
|
||||
}
|
||||
|
||||
/*
|
||||
* Check if the request is authorized.
|
||||
*
|
||||
* @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
|
||||
* @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
|
||||
*/
|
||||
if ( $json_template && ACTIVITYPUB_AUTHORIZED_FETCH ) {
|
||||
$verification = Signature::verify_http_signature( $_SERVER );
|
||||
if ( \is_wp_error( $verification ) ) {
|
||||
header( 'HTTP/1.1 401 Unauthorized' );
|
||||
|
||||
// fallback as template_loader can't return http headers
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $json_template ) {
|
||||
return $json_template;
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom redirects for ActivityPub requests.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function template_redirect() {
|
||||
$comment_id = get_query_var( 'c', null );
|
||||
|
||||
// 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
|
||||
if ( ! $comment ) {
|
||||
global $wp_query;
|
||||
$wp_query->set_404();
|
||||
return;
|
||||
}
|
||||
|
||||
// stop if it's not an ActivityPub comment
|
||||
if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_safe_redirect( get_comment_link( $comment ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the 'activitypub' query variable so WordPress won't mangle it.
|
||||
*/
|
||||
public static function add_query_vars( $vars ) {
|
||||
$vars[] = 'activitypub';
|
||||
$vars[] = 'c';
|
||||
$vars[] = 'p';
|
||||
|
||||
return $vars;
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the default avatar.
|
||||
*
|
||||
* @param array $args Arguments passed to get_avatar_data(), after processing.
|
||||
* @param int|string|object $id_or_email A user ID, email address, or comment object.
|
||||
*
|
||||
* @return array $args
|
||||
*/
|
||||
public static function pre_get_avatar_data( $args, $id_or_email ) {
|
||||
if (
|
||||
! $id_or_email instanceof \WP_Comment ||
|
||||
! isset( $id_or_email->comment_type ) ||
|
||||
$id_or_email->user_id
|
||||
) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$allowed_comment_types = \apply_filters( 'get_avatar_comment_types', array( 'comment' ) );
|
||||
if (
|
||||
! empty( $id_or_email->comment_type ) &&
|
||||
! \in_array(
|
||||
$id_or_email->comment_type,
|
||||
(array) $allowed_comment_types,
|
||||
true
|
||||
)
|
||||
) {
|
||||
$args['url'] = false;
|
||||
/** This filter is documented in wp-includes/link-template.php */
|
||||
return \apply_filters( 'get_avatar_data', $args, $id_or_email );
|
||||
}
|
||||
|
||||
// Check if comment has an avatar.
|
||||
$avatar = self::get_avatar_url( $id_or_email->comment_ID );
|
||||
|
||||
if ( $avatar ) {
|
||||
if ( empty( $args['class'] ) ) {
|
||||
$args['class'] = array();
|
||||
} elseif ( \is_string( $args['class'] ) ) {
|
||||
$args['class'] = \explode( ' ', $args['class'] );
|
||||
}
|
||||
|
||||
$args['url'] = $avatar;
|
||||
$args['class'][] = 'avatar-activitypub';
|
||||
$args['class'][] = 'u-photo';
|
||||
$args['class'] = \array_unique( $args['class'] );
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to retrieve Avatar URL if stored in meta.
|
||||
*
|
||||
* @param int|WP_Comment $comment
|
||||
*
|
||||
* @return string $url
|
||||
*/
|
||||
public static function get_avatar_url( $comment ) {
|
||||
if ( \is_numeric( $comment ) ) {
|
||||
$comment = \get_comment( $comment );
|
||||
}
|
||||
return \get_comment_meta( $comment->comment_ID, 'avatar_url', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Store permalink in meta, to send delete Activity.
|
||||
*
|
||||
* @param string $post_id The Post ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function trash_post( $post_id ) {
|
||||
\add_post_meta(
|
||||
$post_id,
|
||||
'activitypub_canonical_url',
|
||||
\get_permalink( $post_id ),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete permalink from meta
|
||||
*
|
||||
* @param string $post_id The Post ID
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function untrash_post( $post_id ) {
|
||||
\delete_post_meta( $post_id, 'activitypub_canonical_url' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add rewrite rules
|
||||
*/
|
||||
public static function add_rewrite_rules() {
|
||||
// If another system needs to take precedence over the ActivityPub rewrite rules,
|
||||
// they can define their own and will manually call the appropriate functions as required.
|
||||
if ( ACTIVITYPUB_DISABLE_REWRITES ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! \class_exists( 'Webfinger' ) ) {
|
||||
\add_rewrite_rule(
|
||||
'^.well-known/webfinger',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/webfinger',
|
||||
'top'
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! \class_exists( 'Nodeinfo_Endpoint' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
|
||||
\add_rewrite_rule(
|
||||
'^.well-known/nodeinfo',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo/discovery',
|
||||
'top'
|
||||
);
|
||||
\add_rewrite_rule(
|
||||
'^.well-known/x-nodeinfo2',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2',
|
||||
'top'
|
||||
);
|
||||
}
|
||||
|
||||
\add_rewrite_rule(
|
||||
'^@([\w\-\.]+)',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/actors/$matches[1]',
|
||||
'top'
|
||||
);
|
||||
|
||||
\add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush rewrite rules;
|
||||
*/
|
||||
public static function flush_rewrite_rules() {
|
||||
self::add_rewrite_rules();
|
||||
\flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme compatibility stuff
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
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
|
||||
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...
|
||||
add_theme_support(
|
||||
'post-formats',
|
||||
array(
|
||||
'gallery',
|
||||
'status',
|
||||
'image',
|
||||
'video',
|
||||
'audio',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display plugin upgrade notice to users
|
||||
*
|
||||
* @param array $data The plugin data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function plugin_update_message( $data ) {
|
||||
if ( ! isset( $data['upgrade_notice'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf(
|
||||
'<div class="update-message">%s</div>',
|
||||
wp_kses(
|
||||
wpautop( $data['upgrade_notice '] ),
|
||||
array(
|
||||
'p' => array(),
|
||||
'a' => array( 'href', 'title' ),
|
||||
'strong' => array(),
|
||||
'em' => array(),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the "Followers" Taxonomy
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function register_post_types() {
|
||||
\register_post_type(
|
||||
Followers::POST_TYPE,
|
||||
array(
|
||||
'labels' => array(
|
||||
'name' => _x( 'Followers', 'post_type plural name', 'activitypub' ),
|
||||
'singular_name' => _x( 'Follower', 'post_type single name', 'activitypub' ),
|
||||
),
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'rewrite' => false,
|
||||
'query_var' => false,
|
||||
'delete_with_user' => false,
|
||||
'can_export' => true,
|
||||
'supports' => array(),
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_inbox',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'sanitize_url',
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_errors',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => false,
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
if ( ! is_string( $value ) ) {
|
||||
throw new Exception( 'Error message is no valid string' );
|
||||
}
|
||||
|
||||
return esc_sql( $value );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_user_id',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => false,
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
return esc_sql( $value );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_actor_json',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
return sanitize_text_field( $value );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_type(
|
||||
'ap_extrafield',
|
||||
array(
|
||||
'labels' => array(
|
||||
'name' => _x( 'Extra fields', 'post_type plural name', 'activitypub' ),
|
||||
'singular_name' => _x( 'Extra field', 'post_type single name', 'activitypub' ),
|
||||
'add_new' => __( 'Add new', 'activitypub' ),
|
||||
'add_new_item' => __( 'Add new extra field', 'activitypub' ),
|
||||
'new_item' => __( 'New extra field', 'activitypub' ),
|
||||
'edit_item' => __( 'Edit extra field', 'activitypub' ),
|
||||
'view_item' => __( 'View extra field', 'activitypub' ),
|
||||
'all_items' => __( 'All extra fields', 'activitypub' ),
|
||||
),
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'query_var' => false,
|
||||
'has_archive' => false,
|
||||
'publicly_queryable' => false,
|
||||
'show_in_menu' => false,
|
||||
'delete_with_user' => true,
|
||||
'can_export' => true,
|
||||
'exclude_from_search' => true,
|
||||
'show_in_rest' => true,
|
||||
'map_meta_cap' => true,
|
||||
'show_ui' => true,
|
||||
'supports' => array( 'title', 'editor' ),
|
||||
)
|
||||
);
|
||||
|
||||
\do_action( 'activitypub_after_register_post_type' );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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' ) ) {
|
||||
$user = \get_user_by( 'id', $user_id );
|
||||
$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;
|
||||
}
|
||||
}
|
@ -0,0 +1,649 @@
|
||||
<?php
|
||||
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;
|
||||
|
||||
/**
|
||||
* ActivityPub Admin Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Admin {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'admin_menu', array( self::class, 'admin_menu' ) );
|
||||
\add_action( 'admin_init', array( self::class, 'register_settings' ) );
|
||||
\add_action( 'load-comment.php', array( self::class, 'edit_comment' ) );
|
||||
\add_action( 'load-post.php', array( self::class, 'edit_post' ) );
|
||||
\add_action( 'load-edit.php', array( self::class, 'list_posts' ) );
|
||||
\add_action( 'personal_options_update', array( self::class, 'save_user_description' ) );
|
||||
\add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
|
||||
\add_action( 'admin_notices', array( self::class, 'admin_notices' ) );
|
||||
|
||||
\add_filter( 'comment_row_actions', array( self::class, 'comment_row_actions' ), 10, 2 );
|
||||
\add_filter( 'manage_edit-comments_columns', array( static::class, 'manage_comment_columns' ) );
|
||||
\add_action( 'manage_comments_custom_column', array( static::class, 'manage_comments_custom_column' ), 9, 2 );
|
||||
|
||||
\add_filter( 'manage_posts_columns', array( static::class, 'manage_post_columns' ), 10, 2 );
|
||||
\add_action( 'manage_posts_custom_column', array( self::class, 'manage_posts_custom_column' ), 10, 2 );
|
||||
|
||||
\add_filter( 'manage_users_columns', array( self::class, 'manage_users_columns' ), 10, 1 );
|
||||
\add_action( 'manage_users_custom_column', array( self::class, 'manage_users_custom_column' ), 10, 3 );
|
||||
\add_filter( 'bulk_actions-users', array( self::class, 'user_bulk_options' ) );
|
||||
\add_filter( 'handle_bulk_actions-users', array( self::class, 'handle_bulk_request' ), 10, 3 );
|
||||
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
\add_action( 'show_user_profile', array( self::class, 'add_profile' ) );
|
||||
}
|
||||
|
||||
\add_filter( 'dashboard_glance_items', array( self::class, 'dashboard_glance_items' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add admin menu entry
|
||||
*/
|
||||
public static function admin_menu() {
|
||||
$settings_page = \add_options_page(
|
||||
'Welcome',
|
||||
'ActivityPub',
|
||||
'manage_options',
|
||||
'activitypub',
|
||||
array( self::class, 'settings_page' )
|
||||
);
|
||||
|
||||
\add_action( 'load-' . $settings_page, array( self::class, 'add_settings_help_tab' ) );
|
||||
|
||||
// user has to be able to publish posts
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
$followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) );
|
||||
|
||||
\add_action( 'load-' . $followers_list_page, array( self::class, 'add_followers_list_help_tab' ) );
|
||||
|
||||
\add_users_page( \__( 'Extra Fields', 'activitypub' ), \__( 'Extra Fields', 'activitypub' ), 'read', esc_url( admin_url( '/edit.php?post_type=ap_extrafield' ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display admin menu notices about configuration problems or conflicts.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function admin_notices() {
|
||||
$permalink_structure = \get_option( 'permalink_structure' );
|
||||
if ( empty( $permalink_structure ) ) {
|
||||
$admin_notice = \__( 'You are using the ActivityPub plugin with a permalink structure of "plain". This will prevent ActivityPub from working. Please go to "Settings" / "Permalinks" and choose a permalink structure other than "plain".', 'activitypub' );
|
||||
self::show_admin_notice( $admin_notice, 'error' );
|
||||
}
|
||||
|
||||
$current_screen = get_current_screen();
|
||||
|
||||
if ( isset( $current_screen->id ) && 'edit-ap_extrafield' === $current_screen->id ) {
|
||||
?>
|
||||
<div class="notice" style="margin: 0; background: none; border: none; box-shadow: none; padding: 15px 0 0 0; font-size: 14px;">
|
||||
<?php esc_html_e( 'These are extra fields that are used for your ActivityPub profile. You can use your homepage, social profiles, pronouns, age, anything you want.', 'activitypub' ); ?>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display one admin menu notice about configuration problems or conflicts.
|
||||
*
|
||||
* @param string $admin_notice The notice to display.
|
||||
* @param string $level The level of the notice (error, warning, success, info).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function show_admin_notice( $admin_notice, $level ) {
|
||||
?>
|
||||
|
||||
<div class="notice notice-<?php echo esc_attr( $level ); ?>">
|
||||
<p><?php echo wp_kses( $admin_notice, 'data' ); ?></p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings page
|
||||
*/
|
||||
public static function settings_page() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( empty( $_GET['tab'] ) ) {
|
||||
$tab = 'welcome';
|
||||
} else {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$tab = sanitize_key( $_GET['tab'] );
|
||||
}
|
||||
|
||||
switch ( $tab ) {
|
||||
case 'settings':
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' );
|
||||
break;
|
||||
case 'followers':
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/blog-user-followers-list.php' );
|
||||
break;
|
||||
case 'welcome':
|
||||
default:
|
||||
wp_enqueue_script( 'plugin-install' );
|
||||
add_thickbox();
|
||||
wp_enqueue_script( 'updates' );
|
||||
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/welcome.php' );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user settings page
|
||||
*/
|
||||
public static function followers_list_page() {
|
||||
// user has to be able to publish posts
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/user-followers-list.php' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register ActivityPub settings
|
||||
*/
|
||||
public static function register_settings() {
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_post_content_type',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'Use title and link, summary, full or custom content', 'activitypub' ),
|
||||
'show_in_rest' => array(
|
||||
'schema' => array(
|
||||
'enum' => array(
|
||||
'title',
|
||||
'excerpt',
|
||||
'content',
|
||||
),
|
||||
),
|
||||
),
|
||||
'default' => 'content',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_custom_post_content',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'Define your own custom post template', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => ACTIVITYPUB_CUSTOM_POST_CONTENT,
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_max_image_attachments',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => \__( 'Number of images to attach to posts.', 'activitypub' ),
|
||||
'default' => ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS,
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_object_type',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'The Activity-Object-Type', 'activitypub' ),
|
||||
'show_in_rest' => array(
|
||||
'schema' => array(
|
||||
'enum' => array(
|
||||
'note',
|
||||
'wordpress-post-format',
|
||||
),
|
||||
),
|
||||
),
|
||||
'default' => 'note',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_use_hashtags',
|
||||
array(
|
||||
'type' => 'boolean',
|
||||
'description' => \__( 'Add hashtags in the content as native tags and replace the #tag with the tag-link', 'activitypub' ),
|
||||
'default' => '0',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_support_post_types',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \esc_html__( 'Enable ActivityPub support for post types', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => array( 'post' ),
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_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',
|
||||
array(
|
||||
'type' => 'boolean',
|
||||
'description' => \__( 'Every Author on this Blog (with the publish_posts capability) gets his own ActivityPub enabled Profile.', 'activitypub' ),
|
||||
'default' => '1',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_enable_blog_user',
|
||||
array(
|
||||
'type' => 'boolean',
|
||||
'description' => \__( 'Your Blog becomes an ActivityPub compatible Profile.', 'activitypub' ),
|
||||
'default' => '0',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function add_settings_help_tab() {
|
||||
require_once ACTIVITYPUB_PLUGIN_DIR . 'includes/help.php';
|
||||
}
|
||||
|
||||
public static function add_followers_list_help_tab() {
|
||||
// todo
|
||||
}
|
||||
|
||||
public static function add_profile( $user ) {
|
||||
$description = get_user_meta( $user->ID, 'activitypub_user_description', true );
|
||||
|
||||
\load_template(
|
||||
ACTIVITYPUB_PLUGIN_DIR . 'templates/user-settings.php',
|
||||
true,
|
||||
array(
|
||||
'description' => $description,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function save_user_description( $user_id ) {
|
||||
if ( ! isset( $_REQUEST['_apnonce'] ) ) {
|
||||
return false;
|
||||
}
|
||||
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_apnonce'] ) );
|
||||
if (
|
||||
! wp_verify_nonce( $nonce, 'activitypub-user-description' ) ||
|
||||
! current_user_can( 'edit_user', $user_id )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
$description = ! empty( $_POST['activitypub-user-description'] ) ? sanitize_text_field( wp_unslash( $_POST['activitypub-user-description'] ) ) : false;
|
||||
if ( $description ) {
|
||||
update_user_meta( $user_id, 'activitypub_user_description', $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() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into the edit_comment functionality
|
||||
*
|
||||
* * Disable the edit_comment capability for federated comments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function edit_comment() {
|
||||
// Disable the edit_comment capability for federated comments.
|
||||
\add_filter(
|
||||
'user_has_cap',
|
||||
function ( $allcaps, $caps, $arg ) {
|
||||
if ( 'edit_comment' !== $arg[0] ) {
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
if ( was_comment_received( $arg[2] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $allcaps;
|
||||
},
|
||||
1,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
public static function edit_post() {
|
||||
// Disable the edit_post capability for federated posts.
|
||||
\add_filter(
|
||||
'user_has_cap',
|
||||
function ( $allcaps, $caps, $arg ) {
|
||||
if ( 'edit_post' !== $arg[0] ) {
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
$post = get_post( $arg[2] );
|
||||
|
||||
if ( 'ap_extrafield' !== $post->post_type ) {
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
if ( (int) get_current_user_id() !== (int) $post->post_author ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $allcaps;
|
||||
},
|
||||
1,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ActivityPub specific actions/filters to the post list view
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function list_posts() {
|
||||
// Show only the user's extra fields.
|
||||
\add_action(
|
||||
'pre_get_posts',
|
||||
function ( $query ) {
|
||||
if ( $query->get( 'post_type' ) === 'ap_extrafield' ) {
|
||||
$query->set( 'author', get_current_user_id() );
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
// Remove all views for the extra fields.
|
||||
$screen_id = get_current_screen()->id;
|
||||
|
||||
add_filter(
|
||||
"views_{$screen_id}",
|
||||
function ( $views ) {
|
||||
if ( 'ap_extrafield' === get_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() );
|
||||
}
|
||||
}
|
||||
|
||||
public static function comment_row_actions( $actions, $comment ) {
|
||||
if ( was_comment_received( $comment ) ) {
|
||||
unset( $actions['edit'] );
|
||||
unset( $actions['quickedit'] );
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a column "activitypub"
|
||||
*
|
||||
* This column shows if the user has the capability to use ActivityPub.
|
||||
*
|
||||
* @param array $columns The columns.
|
||||
*
|
||||
* @return array The columns extended by the activitypub.
|
||||
*/
|
||||
public static function manage_users_columns( $columns ) {
|
||||
$columns['activitypub'] = __( 'ActivityPub', 'activitypub' );
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "comment-type" and "protocol" as column in WP-Admin
|
||||
*
|
||||
* @param array $columns the list of column names
|
||||
*/
|
||||
public static function manage_comment_columns( $columns ) {
|
||||
$columns['comment_type'] = esc_attr__( 'Comment-Type', 'activitypub' );
|
||||
$columns['comment_protocol'] = esc_attr__( 'Protocol', 'activitypub' );
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "post_content" as column for Extra-Fields in WP-Admin
|
||||
*
|
||||
* @param array $columns Tthe list of column names.
|
||||
* @param string $post_type The post type.
|
||||
*/
|
||||
public static function manage_post_columns( $columns, $post_type ) {
|
||||
if ( 'ap_extrafield' === $post_type ) {
|
||||
$after_key = 'title';
|
||||
$index = array_search( $after_key, array_keys( $columns ), true );
|
||||
$columns = array_slice( $columns, 0, $index + 1 ) + array( 'extra_field_content' => esc_attr__( 'Content', 'activitypub' ) ) + $columns;
|
||||
}
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "comment-type" and "protocol" as column in WP-Admin
|
||||
*
|
||||
* @param array $column The column to implement
|
||||
* @param int $comment_id The comment id
|
||||
*/
|
||||
public static function manage_comments_custom_column( $column, $comment_id ) {
|
||||
if ( 'comment_type' === $column && ! defined( 'WEBMENTION_PLUGIN_DIR' ) ) {
|
||||
echo esc_attr( ucfirst( get_comment_type( $comment_id ) ) );
|
||||
} elseif ( 'comment_protocol' === $column ) {
|
||||
$protocol = get_comment_meta( $comment_id, 'protocol', true );
|
||||
|
||||
if ( $protocol ) {
|
||||
echo esc_attr( ucfirst( str_replace( 'activitypub', 'ActivityPub', $protocol ) ) );
|
||||
} else {
|
||||
esc_attr_e( 'Local', 'activitypub' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the results for the activitypub column.
|
||||
*
|
||||
* @param string $output Custom column output. Default empty.
|
||||
* @param string $column_name Column name.
|
||||
* @param int $user_id ID of the currently-listed user.
|
||||
*
|
||||
* @return string The column contents.
|
||||
*/
|
||||
public static function manage_users_custom_column( $output, $column_name, $user_id ) {
|
||||
if ( 'activitypub' !== $column_name ) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
if ( \user_can( $user_id, 'activitypub' ) ) {
|
||||
return '<span aria-hidden="true">✓</span><span class="screen-reader-text">' . esc_html__( 'ActivityPub enabled for this author', 'activitypub' ) . '</span>';
|
||||
} else {
|
||||
return '<span aria-hidden="true">✗</span><span class="screen-reader-text">' . esc_html__( 'ActivityPub disabled for this author', 'activitypub' ) . '</span>';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a column "extra_field_content" to the post list view
|
||||
*
|
||||
* @param string $column_name The column name.
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function manage_posts_custom_column( $column_name, $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
|
||||
if ( 'extra_field_content' === $column_name ) {
|
||||
$post = get_post( $post_id );
|
||||
if ( 'ap_extrafield' === $post->post_type ) {
|
||||
echo esc_attr( wp_strip_all_tags( $post->post_content ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add options to the Bulk dropdown on the users page
|
||||
*
|
||||
* @param array $actions The existing bulk options.
|
||||
*
|
||||
* @return array The extended bulk options.
|
||||
*/
|
||||
public static function user_bulk_options( $actions ) {
|
||||
$actions['add_activitypub_cap'] = __( 'Enable for ActivityPub', 'activitypub' );
|
||||
$actions['remove_activitypub_cap'] = __( 'Disable for ActivityPub', 'activitypub' );
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bulk activitypub requests
|
||||
*
|
||||
* * `add_activitypub_cap` - Add the activitypub capability to the selected users.
|
||||
* * `remove_activitypub_cap` - Remove the activitypub capability from the selected users.
|
||||
*
|
||||
* @param string $sendback The URL to send the user back to.
|
||||
* @param string $action The requested action.
|
||||
* @param array $users The selected users.
|
||||
*
|
||||
* @return string The URL to send the user back to.
|
||||
*/
|
||||
public static function handle_bulk_request( $sendback, $action, $users ) {
|
||||
if (
|
||||
'remove_activitypub_cap' !== $action &&
|
||||
'add_activitypub_cap' !== $action
|
||||
) {
|
||||
return $sendback;
|
||||
}
|
||||
|
||||
foreach ( $users as $user_id ) {
|
||||
$user = new \WP_User( $user_id );
|
||||
if (
|
||||
'add_activitypub_cap' === $action &&
|
||||
user_can( $user_id, 'publish_posts' )
|
||||
) {
|
||||
$user->add_cap( 'activitypub' );
|
||||
} elseif ( 'remove_activitypub_cap' === $action ) {
|
||||
$user->remove_cap( 'activitypub' );
|
||||
}
|
||||
}
|
||||
|
||||
return $sendback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add ActivityPub infos to the dashboard glance items
|
||||
*
|
||||
* @param array $items The existing glance items.
|
||||
*
|
||||
* @return array The extended glance items.
|
||||
*/
|
||||
public static function dashboard_glance_items( $items ) {
|
||||
\add_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers', 10, 3 );
|
||||
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
$follower_count = sprintf(
|
||||
// translators: %s: number of followers
|
||||
_n(
|
||||
'%s Follower',
|
||||
'%s Followers',
|
||||
count_followers( \get_current_user_id() ),
|
||||
'activitypub'
|
||||
),
|
||||
\number_format_i18n( count_followers( \get_current_user_id() ) )
|
||||
);
|
||||
$items['activitypub-followers-user'] = sprintf(
|
||||
'<a class="activitypub-followers" href="%1$s" title="%2$s">%3$s</a>',
|
||||
\esc_url( \admin_url( 'users.php?page=activitypub-followers-list' ) ),
|
||||
\esc_attr__( 'Your followers', 'activitypub' ),
|
||||
\esc_html( $follower_count )
|
||||
);
|
||||
}
|
||||
|
||||
if ( ! is_user_type_disabled( 'blog' ) && current_user_can( 'manage_options' ) ) {
|
||||
$follower_count = sprintf(
|
||||
// translators: %s: number of followers
|
||||
_n(
|
||||
'%s Follower (Blog)',
|
||||
'%s Followers (Blog)',
|
||||
count_followers( Users::BLOG_USER_ID ),
|
||||
'activitypub'
|
||||
),
|
||||
\number_format_i18n( count_followers( Users::BLOG_USER_ID ) )
|
||||
);
|
||||
$items['activitypub-followers-blog'] = sprintf(
|
||||
'<a class="activitypub-followers" href="%1$s" title="%2$s">%3$s</a>',
|
||||
\esc_url( \admin_url( 'options-general.php?page=activitypub&tab=followers' ) ),
|
||||
\esc_attr__( 'The Blog\'s followers', 'activitypub' ),
|
||||
\esc_html( $follower_count )
|
||||
);
|
||||
}
|
||||
|
||||
\remove_filter( 'number_format_i18n', '\Activitypub\custom_large_numbers', 10, 3 );
|
||||
|
||||
return $items;
|
||||
}
|
||||
}
|
@ -0,0 +1,159 @@
|
||||
<?php
|
||||
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;
|
||||
|
||||
class Blocks {
|
||||
public static function init() {
|
||||
// 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' ) );
|
||||
}
|
||||
|
||||
public static function add_data() {
|
||||
$context = is_admin() ? 'editor' : 'view';
|
||||
$followers_handle = 'activitypub-followers-' . $context . '-script';
|
||||
$follow_me_handle = 'activitypub-follow-me-' . $context . '-script';
|
||||
$data = array(
|
||||
'namespace' => ACTIVITYPUB_REST_NAMESPACE,
|
||||
'enabled' => array(
|
||||
'site' => ! is_user_type_disabled( 'blog' ),
|
||||
'users' => ! is_user_type_disabled( 'user' ),
|
||||
),
|
||||
);
|
||||
$js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
|
||||
\wp_add_inline_script( $followers_handle, $js, 'before' );
|
||||
\wp_add_inline_script( $follow_me_handle, $js, 'before' );
|
||||
}
|
||||
|
||||
public static function register_blocks() {
|
||||
\register_block_type_from_metadata(
|
||||
ACTIVITYPUB_PLUGIN_DIR . '/build/followers',
|
||||
array(
|
||||
'render_callback' => array( self::class, 'render_follower_block' ),
|
||||
)
|
||||
);
|
||||
\register_block_type_from_metadata(
|
||||
ACTIVITYPUB_PLUGIN_DIR . '/build/follow-me',
|
||||
array(
|
||||
'render_callback' => array( self::class, 'render_follow_me_block' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter an array by a list of keys.
|
||||
* @param array $array 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 ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ) ) {
|
||||
$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(
|
||||
'aria-label' => __( 'Follow me on the Fediverse', 'activitypub' ),
|
||||
'class' => 'activitypub-follow-me-block-wrapper',
|
||||
'data-attrs' => wp_json_encode( $attrs ),
|
||||
)
|
||||
);
|
||||
// todo: render more than an empty div?
|
||||
return '<div ' . $wrapper_attributes . '></div>';
|
||||
}
|
||||
|
||||
public static function render_follower_block( $attrs ) {
|
||||
$followee_user_id = self::get_user_id( $attrs['selectedUser'] );
|
||||
$per_page = absint( $attrs['per_page'] );
|
||||
$follower_data = Followers::get_followers_with_count( $followee_user_id, $per_page );
|
||||
|
||||
$attrs['followerData']['total'] = $follower_data['total'];
|
||||
$attrs['followerData']['followers'] = array_map(
|
||||
function ( $follower ) {
|
||||
return self::filter_array_by_keys(
|
||||
$follower->to_array(),
|
||||
array( 'icon', 'name', 'preferredUsername', 'url' )
|
||||
);
|
||||
},
|
||||
$follower_data['followers']
|
||||
);
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'aria-label' => __( 'Fediverse Followers', 'activitypub' ),
|
||||
'class' => 'activitypub-follower-block',
|
||||
'data-attrs' => wp_json_encode( $attrs ),
|
||||
)
|
||||
);
|
||||
|
||||
$html = '<div ' . $wrapper_attributes . '>';
|
||||
if ( $attrs['title'] ) {
|
||||
$html .= '<h3>' . esc_html( $attrs['title'] ) . '</h3>';
|
||||
}
|
||||
$html .= '<ul>';
|
||||
foreach ( $follower_data['followers'] as $follower ) {
|
||||
$html .= '<li>' . self::render_follower( $follower ) . '</li>';
|
||||
}
|
||||
// We are only pagination on the JS side. Could be revisited but we gotta ship!
|
||||
$html .= '</ul></div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
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 =
|
||||
'<a href="%s" title="%s" class="components-external-link activitypub-link" target="_blank" rel="external noreferrer noopener">
|
||||
<img width="40" height="40" src="%s" class="avatar activitypub-avatar" />
|
||||
<span class="activitypub-actor">
|
||||
<strong class="activitypub-name">%s</strong>
|
||||
<span class="sep">/</span>
|
||||
<span class="activitypub-handle">@%s</span>
|
||||
</span>
|
||||
%s
|
||||
</a>';
|
||||
|
||||
$data = $follower->to_array();
|
||||
|
||||
return sprintf(
|
||||
$template,
|
||||
esc_url( object_to_uri( $data['url'] ) ),
|
||||
esc_attr( $data['name'] ),
|
||||
esc_attr( $data['icon']['url'] ),
|
||||
esc_html( $data['name'] ),
|
||||
esc_html( $data['preferredUsername'] ),
|
||||
$external_svg
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,465 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
public static function init() {
|
||||
\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' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the comment reply link.
|
||||
*
|
||||
* We don't want to show the comment reply link for federated comments
|
||||
* if the user is disabled for federation.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @return string The filtered HTML markup for the comment reply link.
|
||||
*/
|
||||
public static function comment_reply_link( $link, $args, $comment ) {
|
||||
if ( self::are_comments_allowed( $comment ) ) {
|
||||
$user_id = get_current_user_id();
|
||||
if ( $user_id && self::was_received( $comment ) && \user_can( $user_id, 'activitypub' ) ) {
|
||||
return self::create_fediverse_reply_link( $link, $args );
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
$attrs = array(
|
||||
'selectedComment' => self::generate_id( $comment ),
|
||||
'commentId' => $comment->comment_ID,
|
||||
);
|
||||
|
||||
$div = sprintf(
|
||||
'<div class="activitypub-remote-reply" data-attrs="%s"></div>',
|
||||
esc_attr( wp_json_encode( $attrs ) )
|
||||
);
|
||||
|
||||
return apply_filters( 'activitypub_comment_reply_link', $div );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a link to reply to a federated comment.
|
||||
* This function adds a title attribute to the reply link to inform the user
|
||||
* that the comment was received from the fediverse and the reply will be sent
|
||||
* to the original author.
|
||||
*
|
||||
* @param string $link The HTML markup for the comment reply link.
|
||||
* @param array $args The args provided by the `comment_reply_link` filter.
|
||||
*
|
||||
* @return string The modified HTML markup for the comment reply link.
|
||||
*/
|
||||
private static function create_fediverse_reply_link( $link, $args ) {
|
||||
$str_to_replace = sprintf( '>%s<', $args['reply_text'] );
|
||||
$replace_with = sprintf(
|
||||
' title="%s">%s<',
|
||||
esc_attr__( 'This comment was received from the fediverse and your reply will be sent to the original author', 'activitypub' ),
|
||||
esc_html__( 'Reply with federation', 'activitypub' )
|
||||
);
|
||||
return str_replace( $str_to_replace, $replace_with, $link );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if it is allowed to comment to a comment.
|
||||
*
|
||||
* Checks if the comment is local only or if the user can comment federated comments.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the user can comment, false otherwise.
|
||||
*/
|
||||
public static function are_comments_allowed( $comment ) {
|
||||
$comment = \get_comment( $comment );
|
||||
|
||||
if ( ! self::was_received( $comment ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$current_user = get_current_user_id();
|
||||
|
||||
if ( ! $current_user ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( is_single_user() && \user_can( $current_user, 'publish_posts' ) ) {
|
||||
// On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user
|
||||
$current_user = Users::BLOG_USER_ID;
|
||||
}
|
||||
|
||||
$is_user_disabled = is_user_disabled( $current_user );
|
||||
|
||||
if ( $is_user_disabled ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment is federated.
|
||||
*
|
||||
* We consider a comment federated if comment was received via ActivityPub.
|
||||
*
|
||||
* Use this function to check if it is comment that was received via ActivityPub.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment is federated, false otherwise.
|
||||
*/
|
||||
public static function was_received( $comment ) {
|
||||
$comment = \get_comment( $comment );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$protocol = \get_comment_meta( $comment->comment_ID, 'protocol', true );
|
||||
|
||||
if ( 'activitypub' === $protocol ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment was federated.
|
||||
*
|
||||
* This function checks if a comment was federated via ActivityPub.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment was federated, false otherwise.
|
||||
*/
|
||||
public static function was_sent( $comment ) {
|
||||
$comment = \get_comment( $comment );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$status = \get_comment_meta( $comment->comment_ID, 'activitypub_status', true );
|
||||
|
||||
if ( $status ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment is local only.
|
||||
*
|
||||
* This function checks if a comment is local only and was not sent or received via ActivityPub.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment is local only, false otherwise.
|
||||
*/
|
||||
public static function is_local( $comment ) {
|
||||
if ( self::was_sent( $comment ) || self::was_received( $comment ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment should be federated.
|
||||
*
|
||||
* We consider a comment should be federated if it is authored by a user that is
|
||||
* not disabled for federation and if it is a reply directly to the post or to a
|
||||
* federated comment.
|
||||
*
|
||||
* Use this function to check if a comment should be federated.
|
||||
*
|
||||
* @param mixed $comment Comment object or ID.
|
||||
*
|
||||
* @return boolean True if the comment should be federated, false otherwise.
|
||||
*/
|
||||
public static function should_be_federated( $comment ) {
|
||||
// we should not federate federated comments
|
||||
if ( self::was_received( $comment ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$comment = \get_comment( $comment );
|
||||
$user_id = $comment->user_id;
|
||||
|
||||
// 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
|
||||
$user_id = Users::BLOG_USER_ID;
|
||||
}
|
||||
|
||||
$is_user_disabled = is_user_disabled( $user_id );
|
||||
|
||||
// user is disabled for federation
|
||||
if ( $is_user_disabled ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// it is a comment to the post and can be federated
|
||||
if ( empty( $comment->comment_parent ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if parent comment is federated
|
||||
$parent_comment = \get_comment( $comment->comment_parent );
|
||||
|
||||
return ! self::is_local( $parent_comment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Examine a comment ID and look up an existing comment it represents.
|
||||
*
|
||||
* @param string $id ActivityPub object ID (usually a URL) to check.
|
||||
*
|
||||
* @return \WP_Comment|false Comment object, or false on failure.
|
||||
*/
|
||||
public static function object_id_to_comment( $id ) {
|
||||
$comment_query = new WP_Comment_Query(
|
||||
array(
|
||||
'meta_key' => 'source_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! $comment_query->comments ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( count( $comment_query->comments ) > 1 ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $comment_query->comments[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify if URL is a local comment, or if it is 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
|
||||
*/
|
||||
public static function url_to_commentid( $url ) {
|
||||
if ( ! $url || ! filter_var( $url, \FILTER_VALIDATE_URL ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check for local comment
|
||||
if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) {
|
||||
$query = \wp_parse_url( $url, \PHP_URL_QUERY );
|
||||
|
||||
if ( $query ) {
|
||||
parse_str( $query, $params );
|
||||
|
||||
if ( ! empty( $params['c'] ) ) {
|
||||
$comment = \get_comment( $params['c'] );
|
||||
|
||||
if ( $comment ) {
|
||||
return $comment->comment_ID;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$args = array(
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'source_url',
|
||||
'value' => $url,
|
||||
),
|
||||
array(
|
||||
'key' => 'source_id',
|
||||
'value' => $url,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$query = new WP_Comment_Query();
|
||||
$comments = $query->query( $args );
|
||||
|
||||
if ( $comments && is_array( $comments ) ) {
|
||||
return $comments[0]->comment_ID;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the CSS classes to add an ActivityPub class.
|
||||
*
|
||||
* @param string[] $classes An array of comment classes.
|
||||
* @param string[] $css_class An array of additional classes added to the list.
|
||||
* @param string $comment_id The comment ID as a numeric string.
|
||||
*
|
||||
* @return string[] An array of classes.
|
||||
*/
|
||||
public static function comment_class( $classes, $css_class, $comment_id ) {
|
||||
// check if ActivityPub comment
|
||||
if ( 'activitypub' === get_comment_meta( $comment_id, 'protocol', true ) ) {
|
||||
$classes[] = 'activitypub-comment';
|
||||
}
|
||||
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link remote comments to source url.
|
||||
*
|
||||
* @param string $comment_link
|
||||
* @param object|WP_Comment $comment
|
||||
*
|
||||
* @return string $url
|
||||
*/
|
||||
public static function remote_comment_link( $comment_link, $comment ) {
|
||||
if ( ! $comment || is_admin() ) {
|
||||
return $comment_link;
|
||||
}
|
||||
|
||||
$comment_meta = \get_comment_meta( $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;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates an ActivityPub URI for a comment
|
||||
*
|
||||
* @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];
|
||||
}
|
||||
|
||||
// generate URI based on comment ID
|
||||
return \add_query_arg( 'c', $comment->comment_ID, \trailingslashit( \home_url() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a post has remote comments
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return bool True if the post has remote comments, false otherwise.
|
||||
*/
|
||||
private static function post_has_remote_comments( $post_id ) {
|
||||
$comments = \get_comments(
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'protocol',
|
||||
'value' => 'activitypub',
|
||||
'compare' => '=',
|
||||
),
|
||||
array(
|
||||
'key' => 'source_id',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return ! empty( $comments );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue scripts for remote comments
|
||||
*/
|
||||
public static function enqueue_scripts() {
|
||||
if ( ! \is_singular() || \is_user_logged_in() ) {
|
||||
// only on single pages, only for logged out users
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! \post_type_supports( \get_post_type(), 'activitypub' ) ) {
|
||||
// post type does not support ActivityPub
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! \comments_open() || ! \get_comments_number() ) {
|
||||
// no comments, no need to load the script
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! self::post_has_remote_comments( \get_the_ID() ) ) {
|
||||
// no remote comments, no need to load the script
|
||||
return;
|
||||
}
|
||||
|
||||
$handle = 'activitypub-remote-reply';
|
||||
$data = array(
|
||||
'namespace' => ACTIVITYPUB_REST_NAMESPACE,
|
||||
);
|
||||
$js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
|
||||
$asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/remote-reply/index.asset.php';
|
||||
|
||||
if ( \file_exists( $asset_file ) ) {
|
||||
$assets = require_once $asset_file;
|
||||
|
||||
\wp_enqueue_script(
|
||||
$handle,
|
||||
\plugins_url( 'build/remote-reply/index.js', __DIR__ ),
|
||||
$assets['dependencies'],
|
||||
$assets['version'],
|
||||
true
|
||||
);
|
||||
\wp_add_inline_script( $handle, $js, 'before' );
|
||||
|
||||
\wp_enqueue_style(
|
||||
$handle,
|
||||
\plugins_url( 'build/remote-reply/style-index.css', __DIR__ ),
|
||||
[ 'wp-components' ],
|
||||
$assets['version']
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_DEBUG;
|
||||
use WP_DEBUG_LOG;
|
||||
|
||||
/**
|
||||
* ActivityPub Debug Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Debug {
|
||||
/**
|
||||
* 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 );
|
||||
}
|
||||
}
|
||||
|
||||
// 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 ) );
|
||||
}
|
||||
|
||||
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
|
||||
\error_log( \print_r( $log, true ) );
|
||||
} else {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
\error_log( $log );
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,35 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Handler\Announce;
|
||||
use Activitypub\Handler\Create;
|
||||
use Activitypub\Handler\Delete;
|
||||
use Activitypub\Handler\Follow;
|
||||
use Activitypub\Handler\Undo;
|
||||
use Activitypub\Handler\Update;
|
||||
|
||||
/**
|
||||
* Handler class.
|
||||
*/
|
||||
class Handler {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_handlers();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register handlers.
|
||||
*/
|
||||
public static function register_handlers() {
|
||||
Announce::init();
|
||||
Create::init();
|
||||
Delete::init();
|
||||
Follow::init();
|
||||
Undo::init();
|
||||
Update::init();
|
||||
|
||||
do_action( 'activitypub_register_handlers' );
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub Hashtag Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Hashtag {
|
||||
/**
|
||||
* 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 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to save #tags as real WordPress tags
|
||||
*
|
||||
* @param int $id the rev-id
|
||||
* @param WP_Post $post the post
|
||||
*
|
||||
* @return
|
||||
*/
|
||||
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 );
|
||||
}
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to replace the #tags 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, 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback for preg_replace to build the term links
|
||||
*
|
||||
* @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 ) {
|
||||
$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 '#' . $tag;
|
||||
}
|
||||
}
|
@ -0,0 +1,365 @@
|
||||
<?php
|
||||
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
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Health_Check {
|
||||
|
||||
/**
|
||||
* Initialize health checks
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'site_status_tests', array( self::class, 'add_tests' ) );
|
||||
\add_filter( 'debug_information', array( self::class, 'debug_information' ) );
|
||||
}
|
||||
|
||||
public static function add_tests( $tests ) {
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
$tests['direct']['activitypub_test_author_url'] = array(
|
||||
'label' => \__( 'Author URL test', 'activitypub' ),
|
||||
'test' => array( self::class, 'test_author_url' ),
|
||||
);
|
||||
}
|
||||
|
||||
$tests['direct']['activitypub_test_webfinger'] = array(
|
||||
'label' => __( 'WebFinger Test', 'activitypub' ),
|
||||
'test' => array( self::class, 'test_webfinger' ),
|
||||
);
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Author URL tests
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function test_author_url() {
|
||||
$result = array(
|
||||
'label' => \__( 'Author URL accessible', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Your author URL is accessible and supports the required "Accept" header.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_author_url',
|
||||
);
|
||||
|
||||
$check = self::is_author_url_accessible();
|
||||
|
||||
if ( true === $check ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['status'] = 'critical';
|
||||
$result['label'] = \__( 'Author URL is not accessible', 'activitypub' );
|
||||
$result['badge']['color'] = 'red';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
$check->get_error_message()
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* System Cron tests
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function test_system_cron() {
|
||||
$result = array(
|
||||
'label' => \__( 'System Task Scheduler configured', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\esc_html__( 'You seem to use the System Task Scheduler to process WP_Cron tasks.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_system_cron',
|
||||
);
|
||||
|
||||
if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['status'] = 'recommended';
|
||||
$result['label'] = \__( 'System Task Scheduler not configured', 'activitypub' );
|
||||
$result['badge']['color'] = 'orange';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Enhance your WordPress site’s performance and mitigate potential heavy loads caused by plugins like ActivityPub by setting up a system cron job to run WP Cron. This ensures scheduled tasks are executed consistently and reduces the reliance on website traffic for trigger events.', 'activitypub' )
|
||||
);
|
||||
$result['actions'] .= sprintf(
|
||||
'<p><a href="%s" target="_blank" rel="noopener">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
|
||||
__( 'https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/', 'activitypub' ),
|
||||
__( 'Learn how to hook the WP-Cron into the System Task Scheduler.', 'activitypub' ),
|
||||
/* translators: Hidden accessibility text. */
|
||||
__( '(opens in a new tab)', 'activitypub' )
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebFinger tests
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function test_webfinger() {
|
||||
$result = array(
|
||||
'label' => \__( 'WebFinger endpoint', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Your WebFinger endpoint is accessible and returns the correct information.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_webfinger',
|
||||
);
|
||||
|
||||
$check = self::is_webfinger_endpoint_accessible();
|
||||
|
||||
if ( true === $check ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['status'] = 'critical';
|
||||
$result['label'] = \__( 'WebFinger endpoint is not accessible', 'activitypub' );
|
||||
$result['badge']['color'] = 'red';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
$check->get_error_message()
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `author_posts_url` is accessible and that request returns correct JSON
|
||||
*
|
||||
* @return boolean|WP_Error
|
||||
*/
|
||||
public static function is_author_url_accessible() {
|
||||
$user = \wp_get_current_user();
|
||||
$author_url = \get_author_posts_url( $user->ID );
|
||||
$reference_author_url = self::get_author_posts_url( $user->ID, $user->user_nicename );
|
||||
|
||||
// check for "author" in URL
|
||||
if ( $author_url !== $reference_author_url ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your author URL <code>%s</code> was replaced, this is often done by plugins.',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// try to access author URL
|
||||
$response = \wp_remote_get(
|
||||
$author_url,
|
||||
array(
|
||||
'headers' => array( 'Accept' => 'application/activity+json' ),
|
||||
'redirection' => 0,
|
||||
)
|
||||
);
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your author URL <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure. If the setup seems fine, maybe check if a plugin might restrict the access.',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$response_code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
// check for redirects
|
||||
if ( \in_array( $response_code, array( 301, 302, 307, 308 ), true ) ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your author URL <code>%s</code> is redirecting to another page, this is often done by SEO plugins like "Yoast SEO".',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// check if response is JSON
|
||||
$body = \wp_remote_retrieve_body( $response );
|
||||
|
||||
if ( ! \is_string( $body ) || ! \is_array( \json_decode( $body, true ) ) ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your author URL <code>%s</code> does not return valid JSON for <code>application/activity+json</code>. Please check if your hosting supports alternate <code>Accept</code> headers.',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WebFinger endpoint is accessible and profile request returns correct JSON
|
||||
*
|
||||
* @return boolean|WP_Error
|
||||
*/
|
||||
public static function is_webfinger_endpoint_accessible() {
|
||||
$user = Users::get_by_id( Users::APPLICATION_USER_ID );
|
||||
$resource = $user->get_webfinger();
|
||||
|
||||
$url = Webfinger::resolve( $resource );
|
||||
if ( \is_wp_error( $url ) ) {
|
||||
$allowed = array( 'code' => array() );
|
||||
$not_accessible = wp_kses(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your WebFinger endpoint <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure.',
|
||||
'activitypub'
|
||||
),
|
||||
$allowed
|
||||
);
|
||||
$invalid_response = wp_kses(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your WebFinger endpoint <code>%s</code> does not return valid JSON for <code>application/jrd+json</code>.',
|
||||
'activitypub'
|
||||
),
|
||||
$allowed
|
||||
);
|
||||
|
||||
$health_messages = array(
|
||||
'webfinger_url_not_accessible' => \sprintf(
|
||||
$not_accessible,
|
||||
$url->get_error_data()
|
||||
),
|
||||
'webfinger_url_invalid_response' => \sprintf(
|
||||
// translators: %s: Author URL
|
||||
$invalid_response,
|
||||
$url->get_error_data()
|
||||
),
|
||||
);
|
||||
$message = null;
|
||||
if ( isset( $health_messages[ $url->get_error_code() ] ) ) {
|
||||
$message = $health_messages[ $url->get_error_code() ];
|
||||
}
|
||||
return new WP_Error(
|
||||
$url->get_error_code(),
|
||||
$message,
|
||||
$url->get_error_data()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the URL to the author page for the user with the ID provided.
|
||||
*
|
||||
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
|
||||
*
|
||||
* @param int $author_id Author ID.
|
||||
* @param string $author_nicename Optional. The author's nicename (slug). Default empty.
|
||||
*
|
||||
* @return string The URL to the author's page.
|
||||
*/
|
||||
public static function get_author_posts_url( $author_id, $author_nicename = '' ) {
|
||||
global $wp_rewrite;
|
||||
$auth_id = (int) $author_id;
|
||||
$link = $wp_rewrite->get_author_permastruct();
|
||||
|
||||
if ( empty( $link ) ) {
|
||||
$file = home_url( '/' );
|
||||
$link = $file . '?author=' . $auth_id;
|
||||
} else {
|
||||
if ( '' === $author_nicename ) {
|
||||
$user = get_userdata( $author_id );
|
||||
if ( ! empty( $user->user_nicename ) ) {
|
||||
$author_nicename = $user->user_nicename;
|
||||
}
|
||||
}
|
||||
$link = str_replace( '%author%', $author_nicename, $link );
|
||||
$link = home_url( user_trailingslashit( $link ) );
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function for generating site debug data when required.
|
||||
*
|
||||
* @param array $info The debug information to be added to the core information page.
|
||||
* @return array The filtered information
|
||||
*/
|
||||
public static function debug_information( $info ) {
|
||||
$info['activitypub'] = array(
|
||||
'label' => __( 'ActivityPub', 'activitypub' ),
|
||||
'fields' => array(
|
||||
'webfinger' => array(
|
||||
'label' => __( 'WebFinger Resource', 'activitypub' ),
|
||||
'value' => Webfinger::get_user_resource( wp_get_current_user()->ID ),
|
||||
'private' => true,
|
||||
),
|
||||
'author_url' => array(
|
||||
'label' => __( 'Author URL', 'activitypub' ),
|
||||
'value' => get_author_posts_url( wp_get_current_user()->ID ),
|
||||
'private' => true,
|
||||
),
|
||||
'plugin_version' => array(
|
||||
'label' => __( 'Plugin Version', 'activitypub' ),
|
||||
'value' => get_plugin_version(),
|
||||
'private' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $info;
|
||||
}
|
||||
}
|
@ -0,0 +1,251 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
use function Activitypub\get_masked_wp_version;
|
||||
|
||||
/**
|
||||
* ActivityPub HTTP Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
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
|
||||
*
|
||||
* @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 );
|
||||
|
||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||
$digest = Signature::generate_digest( $body );
|
||||
$signature = Signature::generate_signature( $user_id, 'post', $url, $date, $digest );
|
||||
|
||||
$wp_version = get_masked_wp_version();
|
||||
|
||||
/**
|
||||
* Filter the HTTP headers user agent.
|
||||
*
|
||||
* @param string $user_agent The user agent string.
|
||||
*/
|
||||
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
|
||||
$args = array(
|
||||
'timeout' => 100,
|
||||
'limit_response_size' => 1048576,
|
||||
'redirection' => 3,
|
||||
'user-agent' => "$user_agent; ActivityPub",
|
||||
'headers' => array(
|
||||
'Accept' => 'application/activity+json',
|
||||
'Content-Type' => 'application/activity+json',
|
||||
'Digest' => $digest,
|
||||
'Signature' => $signature,
|
||||
'Date' => $date,
|
||||
),
|
||||
'body' => $body,
|
||||
);
|
||||
|
||||
$response = \wp_safe_remote_post( $url, $args );
|
||||
$code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( $code >= 400 ) {
|
||||
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
|
||||
}
|
||||
|
||||
\do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return array|WP_Error The GET Response or an WP_ERROR
|
||||
*/
|
||||
public static function get( $url, $cached = false ) {
|
||||
\do_action( 'activitypub_pre_http_get', $url );
|
||||
|
||||
if ( $cached ) {
|
||||
$transient_key = self::generate_cache_key( $url );
|
||||
|
||||
$response = \get_transient( $transient_key );
|
||||
|
||||
if ( $response ) {
|
||||
\do_action( 'activitypub_safe_remote_get_response', $response, $url );
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||
$signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date );
|
||||
|
||||
$wp_version = get_masked_wp_version();
|
||||
|
||||
/**
|
||||
* Filter the HTTP headers user agent.
|
||||
*
|
||||
* @param string $user_agent The user agent string.
|
||||
*/
|
||||
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
|
||||
|
||||
$args = array(
|
||||
'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ),
|
||||
'limit_response_size' => 1048576,
|
||||
'redirection' => 3,
|
||||
'user-agent' => "$user_agent; ActivityPub",
|
||||
'headers' => array(
|
||||
'Accept' => 'application/activity+json',
|
||||
'Content-Type' => 'application/activity+json',
|
||||
'Signature' => $signature,
|
||||
'Date' => $date,
|
||||
),
|
||||
);
|
||||
|
||||
$response = \wp_safe_remote_get( $url, $args );
|
||||
$code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( $code >= 400 ) {
|
||||
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
|
||||
}
|
||||
|
||||
\do_action( 'activitypub_safe_remote_get_response', $response, $url );
|
||||
|
||||
if ( $cached ) {
|
||||
$cache_duration = $cached;
|
||||
if ( ! is_int( $cache_duration ) ) {
|
||||
$cache_duration = HOUR_IN_SECONDS;
|
||||
}
|
||||
\set_transient( $transient_key, $response, $cache_duration );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check for URL for Tombstone.
|
||||
*
|
||||
* @param string $url The URL to check.
|
||||
*
|
||||
* @return bool True if the URL is a tombstone.
|
||||
*/
|
||||
public static function is_tombstone( $url ) {
|
||||
\do_action( 'activitypub_pre_http_is_tombstone', $url );
|
||||
|
||||
$response = \wp_safe_remote_get( $url );
|
||||
$code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( in_array( (int) $code, array( 404, 410 ), true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public static function generate_cache_key( $url ) {
|
||||
return 'activitypub_http_' . \md5( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return array|WP_Error The Object data as array or WP_Error on failure.
|
||||
*/
|
||||
public static function get_remote_object( $url_or_object, $cached = true ) {
|
||||
if ( is_array( $url_or_object ) ) {
|
||||
if ( array_key_exists( 'id', $url_or_object ) ) {
|
||||
$url = $url_or_object['id'];
|
||||
} elseif ( array_key_exists( 'url', $url_or_object ) ) {
|
||||
$url = $url_or_object['url'];
|
||||
} else {
|
||||
return new WP_Error(
|
||||
'activitypub_no_valid_actor_identifier',
|
||||
\__( 'The "actor" identifier is not valid', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
'object' => $url_or_object,
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$url = $url_or_object;
|
||||
}
|
||||
|
||||
if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $url ) ) {
|
||||
$url = Webfinger::resolve( $url );
|
||||
}
|
||||
|
||||
if ( ! $url ) {
|
||||
return new WP_Error(
|
||||
'activitypub_no_valid_actor_identifier',
|
||||
\__( 'The "actor" identifier is not valid', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
'object' => $url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( is_wp_error( $url ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$transient_key = self::generate_cache_key( $url );
|
||||
|
||||
// only check the cache if needed.
|
||||
if ( $cached ) {
|
||||
$data = \get_transient( $transient_key );
|
||||
|
||||
if ( $data ) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! \wp_http_validate_url( $url ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_no_valid_object_url',
|
||||
\__( 'The "object" is/has no valid URL', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'object' => $url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$response = self::get( $url );
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = \wp_remote_retrieve_body( $response );
|
||||
$data = \json_decode( $data, true );
|
||||
|
||||
if ( ! $data ) {
|
||||
return new WP_Error(
|
||||
'activitypub_invalid_json',
|
||||
\__( 'No valid JSON data', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'object' => $url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
\set_transient( $transient_key, $data, WEEK_IN_SECONDS );
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
@ -0,0 +1,183 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Webfinger;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* ActivityPub Mention Class
|
||||
*
|
||||
* @author Alex Kirk
|
||||
*/
|
||||
class Mention {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'the_content', array( self::class, 'the_content' ), 99, 1 );
|
||||
\add_filter( 'comment_text', array( self::class, 'the_content' ), 10, 1 );
|
||||
\add_filter( 'activitypub_extract_mentions', array( self::class, 'extract_mentions' ), 99, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback for preg_replace to build the user links
|
||||
*
|
||||
* @param array $result the preg_match results
|
||||
*
|
||||
* @return string the final string
|
||||
*/
|
||||
public static function replace_with_links( $result ) {
|
||||
$metadata = get_remote_metadata_by_actor( $result[0] );
|
||||
|
||||
if (
|
||||
! empty( $metadata ) &&
|
||||
! is_wp_error( $metadata ) &&
|
||||
( ! empty( $metadata['id'] ) || ! empty( $metadata['url'] ) )
|
||||
) {
|
||||
$username = ltrim( $result[0], '@' );
|
||||
if ( ! empty( $metadata['name'] ) ) {
|
||||
$username = $metadata['name'];
|
||||
}
|
||||
if ( ! empty( $metadata['preferredUsername'] ) ) {
|
||||
$username = $metadata['preferredUsername'];
|
||||
}
|
||||
|
||||
$url = isset( $metadata['url'] ) ? object_to_uri( $metadata['url'] ) : object_to_uri( $metadata['id'] );
|
||||
|
||||
return \sprintf( '<a rel="mention" class="u-url mention" href="%s">@<span>%s</span></a>', esc_url( $url ), esc_html( $username ) );
|
||||
}
|
||||
|
||||
return $result[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Inboxes for the mentioned Actors
|
||||
*
|
||||
* @param array $mentioned The list of Actors that were mentioned
|
||||
*
|
||||
* @return array The list of Inboxes
|
||||
*/
|
||||
public static function get_inboxes( $mentioned ) {
|
||||
$inboxes = array();
|
||||
|
||||
foreach ( $mentioned as $actor ) {
|
||||
$inbox = self::get_inbox_by_mentioned_actor( $actor );
|
||||
|
||||
if ( ! is_wp_error( $inbox ) && $inbox ) {
|
||||
$inboxes[] = $inbox;
|
||||
}
|
||||
}
|
||||
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inbox from the Remote-Profile of a mentioned Actor
|
||||
*
|
||||
* @param string $actor The Actor-URL
|
||||
*
|
||||
* @return string The Inbox-URL
|
||||
*/
|
||||
public static function get_inbox_by_mentioned_actor( $actor ) {
|
||||
$metadata = get_remote_metadata_by_actor( $actor );
|
||||
|
||||
if ( \is_wp_error( $metadata ) ) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if ( isset( $metadata['endpoints'] ) && isset( $metadata['endpoints']['sharedInbox'] ) ) {
|
||||
return $metadata['endpoints']['sharedInbox'];
|
||||
}
|
||||
|
||||
if ( \array_key_exists( 'inbox', $metadata ) ) {
|
||||
return $metadata['inbox'];
|
||||
}
|
||||
|
||||
return new WP_Error( 'activitypub_no_inbox', \__( 'No "Inbox" found', 'activitypub' ), $metadata );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the mentions from the post_content.
|
||||
*
|
||||
* @param array $mentions The already found mentions.
|
||||
* @param string $post_content The post content.
|
||||
*
|
||||
* @return mixed The discovered mentions.
|
||||
*/
|
||||
public static function extract_mentions( $mentions, $post_content ) {
|
||||
\preg_match_all( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/i', $post_content, $matches );
|
||||
foreach ( $matches[0] as $match ) {
|
||||
$link = Webfinger::resolve( $match );
|
||||
if ( ! is_wp_error( $link ) ) {
|
||||
$mentions[ $match ] = $link;
|
||||
}
|
||||
}
|
||||
return $mentions;
|
||||
}
|
||||
}
|
@ -0,0 +1,287 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Activitypub;
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Collection\Followers;
|
||||
|
||||
/**
|
||||
* ActivityPub Migration Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Migration {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) );
|
||||
|
||||
self::maybe_migrate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the target version.
|
||||
*
|
||||
* This is the version that the database structure will be updated to.
|
||||
* It is the same as the plugin version.
|
||||
*
|
||||
* @return string The target version.
|
||||
*/
|
||||
public static function get_target_version() {
|
||||
return get_plugin_version();
|
||||
}
|
||||
|
||||
/**
|
||||
* The current version of the database structure.
|
||||
*
|
||||
* @return string The current version.
|
||||
*/
|
||||
public static function get_version() {
|
||||
return get_option( 'activitypub_db_version', 0 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Locks the database migration process to prevent simultaneous migrations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function lock() {
|
||||
\update_option( 'activitypub_migration_lock', \time() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks the database migration process.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function unlock() {
|
||||
\delete_option( 'activitypub_migration_lock' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the database migration process is locked.
|
||||
*
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_locked() {
|
||||
$lock = \get_option( 'activitypub_migration_lock' );
|
||||
|
||||
if ( ! $lock ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lock = (int) $lock;
|
||||
|
||||
if ( $lock < \time() - 1800 ) {
|
||||
self::unlock();
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the database structure is up to date.
|
||||
*
|
||||
* @return bool True if the database structure is up to date, false otherwise.
|
||||
*/
|
||||
public static function is_latest_version() {
|
||||
return (bool) version_compare(
|
||||
self::get_version(),
|
||||
self::get_target_version(),
|
||||
'=='
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the database structure if necessary.
|
||||
*/
|
||||
public static function maybe_migrate() {
|
||||
if ( self::is_latest_version() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( self::is_locked() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::lock();
|
||||
|
||||
$version_from_db = self::get_version();
|
||||
|
||||
// check for inital migration
|
||||
if ( ! $version_from_db ) {
|
||||
self::add_default_settings();
|
||||
$version_from_db = self::get_target_version();
|
||||
}
|
||||
|
||||
// schedule the async migration
|
||||
if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) {
|
||||
\wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) );
|
||||
}
|
||||
if ( version_compare( $version_from_db, '0.17.0', '<' ) ) {
|
||||
self::migrate_from_0_16();
|
||||
}
|
||||
if ( version_compare( $version_from_db, '1.3.0', '<' ) ) {
|
||||
self::migrate_from_1_2_0();
|
||||
}
|
||||
if ( version_compare( $version_from_db, '2.1.0', '<' ) ) {
|
||||
self::migrate_from_2_0_0();
|
||||
}
|
||||
if ( version_compare( $version_from_db, '2.3.0', '<' ) ) {
|
||||
self::migrate_from_2_2_0();
|
||||
}
|
||||
|
||||
update_option( 'activitypub_db_version', self::get_target_version() );
|
||||
|
||||
self::unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously migrates the database structure.
|
||||
*
|
||||
* @param string $version_from_db The version from which to migrate.
|
||||
*/
|
||||
public static function async_migration( $version_from_db ) {
|
||||
if ( version_compare( $version_from_db, '1.0.0', '<' ) ) {
|
||||
self::migrate_from_0_17();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
$need_update = false;
|
||||
|
||||
// If the old contents is blank, use the defaults.
|
||||
if ( '' === $old_content ) {
|
||||
$old_content = ACTIVITYPUB_CUSTOM_POST_CONTENT;
|
||||
$need_update = true;
|
||||
}
|
||||
|
||||
// Set the new content to be the old content.
|
||||
$content = $old_content;
|
||||
|
||||
// Convert old templates to shortcodes.
|
||||
$content = \str_replace( '%title%', '[ap_title]', $content );
|
||||
$content = \str_replace( '%excerpt%', '[ap_excerpt]', $content );
|
||||
$content = \str_replace( '%content%', '[ap_content]', $content );
|
||||
$content = \str_replace( '%permalink%', '[ap_permalink type="html"]', $content );
|
||||
$content = \str_replace( '%shortlink%', '[ap_shortlink type="html"]', $content );
|
||||
$content = \str_replace( '%hashtags%', '[ap_hashtags]', $content );
|
||||
$content = \str_replace( '%tags%', '[ap_hashtags]', $content );
|
||||
|
||||
// Store the new template if required.
|
||||
if ( $content !== $old_content || $need_update ) {
|
||||
\update_option( 'activitypub_custom_post_content', $content );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the DB-schema of the followers-list
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function migrate_from_0_17() {
|
||||
// migrate followers
|
||||
foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
|
||||
$followers = get_user_meta( $user_id, 'activitypub_followers', true );
|
||||
|
||||
if ( $followers ) {
|
||||
foreach ( $followers as $actor ) {
|
||||
Followers::add_follower( $user_id, $actor );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Activitypub::flush_rewrite_rules();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache after updating to 1.3.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function migrate_from_1_2_0() {
|
||||
$user_ids = \get_users(
|
||||
array(
|
||||
'fields' => 'ID',
|
||||
'capability__in' => array( 'publish_posts' ),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $user_ids as $user_id ) {
|
||||
wp_cache_delete( sprintf( Followers::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unschedule Hooks after updating to 2.0.0
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function migrate_from_2_0_0() {
|
||||
wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
|
||||
wp_clear_scheduled_hook( 'activitypub_send_update_activity' );
|
||||
wp_clear_scheduled_hook( 'activitypub_send_delete_activity' );
|
||||
|
||||
wp_unschedule_hook( 'activitypub_send_post_activity' );
|
||||
wp_unschedule_hook( 'activitypub_send_update_activity' );
|
||||
wp_unschedule_hook( 'activitypub_send_delete_activity' );
|
||||
|
||||
$object_type = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
|
||||
if ( 'article' === $object_type ) {
|
||||
\update_option( 'activitypub_object_type', 'wordpress-post-format' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ActivityPub capability to all users that can publish posts
|
||||
* Delete old meta to store followers
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function migrate_from_2_2_0() {
|
||||
// add the ActivityPub capability to all users that can publish posts
|
||||
self::add_activitypub_capability();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the defaults needed for the plugin to work
|
||||
*
|
||||
* * Add the ActivityPub capability to all users that can publish posts
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function add_default_settings() {
|
||||
self::add_activitypub_capability();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ActivityPub capability to all users that can publish posts
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function add_activitypub_capability() {
|
||||
// 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
|
||||
foreach ( $users as $user ) {
|
||||
$user->add_cap( 'activitypub' );
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,58 @@
|
||||
<?php
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* Notification class.
|
||||
*/
|
||||
class Notification {
|
||||
/**
|
||||
* The type of the notification.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* The actor URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $actor;
|
||||
|
||||
/**
|
||||
* The Activity object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $object;
|
||||
|
||||
/**
|
||||
* The WordPress User-Id.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $target;
|
||||
|
||||
/**
|
||||
* Notification constructor.
|
||||
*
|
||||
* @param string $type The type of the notification.
|
||||
* @param string $actor The actor URL.
|
||||
* @param array $object The Activity object.
|
||||
* @param int $target The WordPress User-Id.
|
||||
*/
|
||||
public function __construct( $type, $actor, $object, $target ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
|
||||
$this->type = $type;
|
||||
$this->actor = $actor;
|
||||
$this->object = $object;
|
||||
$this->target = $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the notification.
|
||||
*/
|
||||
public function send() {
|
||||
do_action( 'activitypub_notification', $this );
|
||||
}
|
||||
}
|
@ -0,0 +1,348 @@
|
||||
<?php
|
||||
|
||||
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
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Scheduler {
|
||||
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
// Post transitions
|
||||
\add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
|
||||
\add_action(
|
||||
'edit_attachment',
|
||||
function ( $post_id ) {
|
||||
self::schedule_post_activity( 'publish', 'publish', $post_id );
|
||||
}
|
||||
);
|
||||
\add_action(
|
||||
'add_attachment',
|
||||
function ( $post_id ) {
|
||||
self::schedule_post_activity( 'publish', '', $post_id );
|
||||
}
|
||||
);
|
||||
\add_action(
|
||||
'delete_attachment',
|
||||
function ( $post_id ) {
|
||||
self::schedule_post_activity( 'trash', '', $post_id );
|
||||
}
|
||||
);
|
||||
|
||||
if ( ! ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS ) {
|
||||
// Comment transitions
|
||||
\add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 );
|
||||
\add_action(
|
||||
'edit_comment',
|
||||
function ( $comment_id ) {
|
||||
self::schedule_comment_activity( 'approved', 'approved', $comment_id );
|
||||
}
|
||||
);
|
||||
\add_action(
|
||||
'wp_insert_comment',
|
||||
function ( $comment_id ) {
|
||||
self::schedule_comment_activity( 'approved', '', $comment_id );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Follower Cleanups
|
||||
\add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
|
||||
\add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
|
||||
|
||||
// profile updates for blog options
|
||||
if ( ! is_user_type_disabled( 'blog' ) ) {
|
||||
\add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) );
|
||||
\add_action( 'update_option_blogdescription', array( self::class, 'blog_user_update' ) );
|
||||
\add_action( 'update_option_blogname', array( self::class, 'blog_user_update' ) );
|
||||
\add_filter( 'pre_set_theme_mod_custom_logo', array( self::class, 'blog_user_update' ) );
|
||||
\add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) );
|
||||
}
|
||||
|
||||
// profile updates for user options
|
||||
if ( ! is_user_type_disabled( 'user' ) ) {
|
||||
\add_action( 'wp_update_user', array( self::class, 'user_update' ) );
|
||||
\add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 );
|
||||
// @todo figure out a feasible way of updating the header image since it's not unique to any user.
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule all ActivityPub schedules.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_schedules() {
|
||||
if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) {
|
||||
\wp_schedule_event( time(), 'hourly', 'activitypub_update_followers' );
|
||||
}
|
||||
|
||||
if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) {
|
||||
\wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unscedule all ActivityPub schedules.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function deregister_schedules() {
|
||||
wp_unschedule_hook( 'activitypub_update_followers' );
|
||||
wp_unschedule_hook( 'activitypub_cleanup_followers' );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Schedule Activities.
|
||||
*
|
||||
* @param string $new_status New post status.
|
||||
* @param string $old_status Old post status.
|
||||
* @param WP_Post $post Post object.
|
||||
*/
|
||||
public static function schedule_post_activity( $new_status, $old_status, $post ) {
|
||||
$post = get_post( $post );
|
||||
|
||||
if ( 'ap_extrafield' === $post->post_type ) {
|
||||
self::schedule_profile_update( $post->post_author );
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not send activities if post is password protected.
|
||||
if ( \post_password_required( $post ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if post-type supports ActivityPub.
|
||||
$post_types = \get_post_types_by_support( 'activitypub' );
|
||||
if ( ! \in_array( $post->post_type, $post_types, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = false;
|
||||
|
||||
if (
|
||||
'publish' === $new_status &&
|
||||
'publish' !== $old_status
|
||||
) {
|
||||
$type = 'Create';
|
||||
} elseif (
|
||||
'publish' === $new_status ||
|
||||
( 'draft' === $new_status &&
|
||||
'draft' !== $old_status )
|
||||
) {
|
||||
$type = 'Update';
|
||||
} elseif ( 'trash' === $new_status ) {
|
||||
$type = 'Delete';
|
||||
}
|
||||
|
||||
if ( empty( $type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hook = 'activitypub_send_post';
|
||||
$args = array( $post->ID, $type );
|
||||
|
||||
if ( false === wp_next_scheduled( $hook, $args ) ) {
|
||||
set_wp_object_state( $post, 'federate' );
|
||||
\wp_schedule_single_event( \time(), $hook, $args );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule Comment Activities
|
||||
*
|
||||
* transition_comment_status()
|
||||
*
|
||||
* @param string $new_status New comment status.
|
||||
* @param string $old_status Old comment status.
|
||||
* @param WP_Comment $comment Comment object.
|
||||
*/
|
||||
public static function schedule_comment_activity( $new_status, $old_status, $comment ) {
|
||||
$comment = get_comment( $comment );
|
||||
|
||||
// federate only comments that are written by a registered user.
|
||||
if ( ! $comment->user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = false;
|
||||
|
||||
if (
|
||||
'approved' === $new_status &&
|
||||
'approved' !== $old_status
|
||||
) {
|
||||
$type = 'Create';
|
||||
} elseif ( 'approved' === $new_status ) {
|
||||
$type = 'Update';
|
||||
\update_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', time(), true );
|
||||
} elseif (
|
||||
'trash' === $new_status ||
|
||||
'spam' === $new_status
|
||||
) {
|
||||
$type = 'Delete';
|
||||
}
|
||||
|
||||
if ( empty( $type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if comment should be federated or not
|
||||
if ( ! should_comment_be_federated( $comment ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hook = 'activitypub_send_comment';
|
||||
$args = array( $comment->comment_ID, $type );
|
||||
|
||||
if ( false === wp_next_scheduled( $hook, $args ) ) {
|
||||
set_wp_object_state( $comment, 'federate' );
|
||||
\wp_schedule_single_event( \time(), $hook, $args );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update followers
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function update_followers() {
|
||||
$number = 5;
|
||||
|
||||
if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
|
||||
$number = 50;
|
||||
}
|
||||
|
||||
$number = apply_filters( 'activitypub_update_followers_number', $number );
|
||||
$followers = Followers::get_outdated_followers( $number );
|
||||
|
||||
foreach ( $followers as $follower ) {
|
||||
$meta = get_remote_metadata_by_actor( $follower->get_id(), false );
|
||||
|
||||
if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
|
||||
Followers::add_error( $follower->get__id(), $meta );
|
||||
} else {
|
||||
$follower->from_array( $meta );
|
||||
$follower->update();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup followers
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function cleanup_followers() {
|
||||
$number = 5;
|
||||
|
||||
if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
|
||||
$number = 50;
|
||||
}
|
||||
|
||||
$number = apply_filters( 'activitypub_update_followers_number', $number );
|
||||
$followers = Followers::get_faulty_followers( $number );
|
||||
|
||||
foreach ( $followers as $follower ) {
|
||||
$meta = get_remote_metadata_by_actor( $follower->get_url(), false );
|
||||
|
||||
if ( is_tombstone( $meta ) ) {
|
||||
$follower->delete();
|
||||
} elseif ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
|
||||
if ( $follower->count_errors() >= 5 ) {
|
||||
$follower->delete();
|
||||
\wp_schedule_single_event(
|
||||
\time(),
|
||||
'activitypub_delete_actor_interactions',
|
||||
array( $follower->get_id() )
|
||||
);
|
||||
} else {
|
||||
Followers::add_error( $follower->get__id(), $meta );
|
||||
}
|
||||
} else {
|
||||
$follower->reset_errors();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a profile update when relevant user meta is updated.
|
||||
*
|
||||
* @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
|
||||
if ( ! \user_can( $user_id, 'activitypub' ) ) {
|
||||
return;
|
||||
}
|
||||
// the user meta fields that affect a profile.
|
||||
$fields = array(
|
||||
'activitypub_user_description',
|
||||
'description',
|
||||
'user_url',
|
||||
'display_name',
|
||||
);
|
||||
if ( in_array( $meta_key, $fields, true ) ) {
|
||||
self::schedule_profile_update( $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a profile update when a user is updated.
|
||||
*
|
||||
* @param int $user_id User ID being updated.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function user_update( $user_id ) {
|
||||
// don't bother if the user can't publish
|
||||
if ( ! \user_can( $user_id, 'activitypub' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::schedule_profile_update( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme mods only have a dynamic filter so we fudge it like this.
|
||||
*
|
||||
* @param mixed $value
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public static function blog_user_update( $value = null ) {
|
||||
self::schedule_profile_update( 0 );
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a profile update to all followers. Gets hooked into all relevant options/meta etc.
|
||||
*
|
||||
* @param int $user_id The user ID to update (Could be 0 for Blog-User).
|
||||
*/
|
||||
public static function schedule_profile_update( $user_id ) {
|
||||
\wp_schedule_single_event(
|
||||
\time(),
|
||||
'activitypub_send_update_profile_activity',
|
||||
array( $user_id )
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,598 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use function Activitypub\esc_hashtag;
|
||||
|
||||
class Shortcodes {
|
||||
/**
|
||||
* Register the shortcodes
|
||||
*/
|
||||
public static function register() {
|
||||
foreach ( get_class_methods( self::class ) as $shortcode ) {
|
||||
if ( 'init' !== $shortcode ) {
|
||||
add_shortcode( 'ap_' . $shortcode, array( self::class, $shortcode ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the shortcodes
|
||||
*/
|
||||
public static function unregister() {
|
||||
foreach ( get_class_methods( self::class ) as $shortcode ) {
|
||||
if ( 'init' !== $shortcode ) {
|
||||
remove_shortcode( 'ap_' . $shortcode );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return string The post tags as hashtags.
|
||||
*/
|
||||
public static function hashtags( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$tags = \get_the_tags( $item->ID );
|
||||
|
||||
if ( ! $tags ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hash_tags = array();
|
||||
|
||||
foreach ( $tags as $tag ) {
|
||||
$hash_tags[] = \sprintf(
|
||||
'<a rel="tag" class="hashtag u-tag u-category" href="%s">%s</a>',
|
||||
\esc_url( \get_tag_link( $tag ) ),
|
||||
esc_hashtag( $tag->name )
|
||||
);
|
||||
}
|
||||
|
||||
return \implode( ' ', $hash_tags );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return \wp_strip_all_tags( \get_the_title( $item->ID ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_excerpt' 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 excerpt.
|
||||
*/
|
||||
public static function excerpt( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array( 'length' => ACTIVITYPUB_EXCERPT_LENGTH ),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
$excerpt_length = intval( $atts['length'] );
|
||||
|
||||
if ( 0 === $excerpt_length ) {
|
||||
$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( ']]>', ']]>', $excerpt );
|
||||
}
|
||||
}
|
||||
|
||||
// Strip out any remaining tags.
|
||||
$excerpt = \wp_strip_all_tags( $excerpt );
|
||||
|
||||
$excerpt_more = \apply_filters( 'activitypub_excerpt_more', ' […]' );
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
||||
return \apply_filters( 'the_excerpt', $excerpt );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_content' 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 content.
|
||||
*/
|
||||
public static function content( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
// prevent inception
|
||||
remove_shortcode( 'ap_content' );
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array( 'apply_filters' => 'yes' ),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
$content = '';
|
||||
|
||||
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 );
|
||||
}
|
||||
} else {
|
||||
$content = \get_post_field( 'post_content', $item );
|
||||
|
||||
if ( 'yes' === $atts['apply_filters'] ) {
|
||||
$content = \apply_filters( 'the_content', $content );
|
||||
} else {
|
||||
$content = do_blocks( $content );
|
||||
$content = wptexturize( $content );
|
||||
$content = wp_filter_content_tags( $content );
|
||||
}
|
||||
|
||||
// replace script and style elements
|
||||
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
|
||||
$content = strip_shortcodes( $content );
|
||||
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
|
||||
}
|
||||
|
||||
add_shortcode( 'ap_content', array( 'Activitypub\Shortcodes', 'content' ) );
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_permalink' 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 permalink.
|
||||
*/
|
||||
public static function permalink( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'type' => 'url',
|
||||
),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
if ( 'url' === $atts['type'] ) {
|
||||
return \esc_url( \get_permalink( $item->ID ) );
|
||||
}
|
||||
|
||||
return \sprintf(
|
||||
'<a href="%1$s" class="status-link unhandled-link">%1$s</a>',
|
||||
\esc_url( \get_permalink( $item->ID ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_shortlink' 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 shortlink.
|
||||
*/
|
||||
public static function shortlink( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'type' => 'url',
|
||||
),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
if ( 'url' === $atts['type'] ) {
|
||||
return \esc_url( \wp_get_shortlink( $item->ID ) );
|
||||
}
|
||||
|
||||
return \sprintf(
|
||||
'<a href="%1$s" class="status-link unhandled-link">%1$s</a>',
|
||||
\esc_url( \wp_get_shortlink( $item->ID ) )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_image' 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
|
||||
*/
|
||||
public static function image( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array(
|
||||
'type' => 'full',
|
||||
),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
$size = 'full';
|
||||
|
||||
if ( in_array(
|
||||
$atts['type'],
|
||||
array( 'thumbnail', 'medium', 'large', 'full' ),
|
||||
true
|
||||
) ) {
|
||||
$size = $atts['type'];
|
||||
}
|
||||
|
||||
$image = \get_the_post_thumbnail_url( $item->ID, $size );
|
||||
|
||||
if ( ! $image ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return \esc_url( $image );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return string The post categories as hashtags.
|
||||
*/
|
||||
public static function hashcats( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$categories = \get_the_category( $item->ID );
|
||||
|
||||
if ( ! $categories ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$hash_tags = array();
|
||||
|
||||
foreach ( $categories as $category ) {
|
||||
$hash_tags[] = \sprintf(
|
||||
'<a rel="tag" class="hashtag u-tag u-category" href="%s">%s</a>',
|
||||
\esc_url( \get_category_link( $category ) ),
|
||||
esc_hashtag( $category->name )
|
||||
);
|
||||
}
|
||||
|
||||
return \implode( ' ', $hash_tags );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return string The author name.
|
||||
*/
|
||||
public static function author( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$author_id = \get_post_field( 'post_author', $item->ID );
|
||||
$name = \get_the_author_meta( 'display_name', $author_id );
|
||||
|
||||
if ( ! $name ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return wp_strip_all_tags( $name );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return string The author URL.
|
||||
*/
|
||||
public static function authorurl( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$author_id = \get_post_field( 'post_author', $item->ID );
|
||||
$url = \get_the_author_meta( 'user_url', $author_id );
|
||||
|
||||
if ( ! $url ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return \esc_url( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return string The site URL.
|
||||
*/
|
||||
public static function blogurl( $atts, $content, $tag ) {
|
||||
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.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function blogname( $atts, $content, $tag ) {
|
||||
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.
|
||||
*
|
||||
* @return string The site description.
|
||||
*/
|
||||
public static function blogdesc( $atts, $content, $tag ) {
|
||||
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.
|
||||
*
|
||||
* @return string The post date.
|
||||
*/
|
||||
public static function date( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$datetime = \get_post_datetime( $item );
|
||||
$dateformat = \get_option( 'date_format' );
|
||||
$timeformat = \get_option( 'time_format' );
|
||||
|
||||
$date = $datetime->format( $dateformat );
|
||||
|
||||
if ( ! $date ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return string The post time.
|
||||
*/
|
||||
public static function time( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$datetime = \get_post_datetime( $item );
|
||||
$dateformat = \get_option( 'date_format' );
|
||||
$timeformat = \get_option( 'time_format' );
|
||||
|
||||
$date = $datetime->format( $timeformat );
|
||||
|
||||
if ( ! $date ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return string The post date/time.
|
||||
*/
|
||||
public static function datetime( $atts, $content, $tag ) {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$datetime = \get_post_datetime( $item );
|
||||
$dateformat = \get_option( 'date_format' );
|
||||
$timeformat = \get_option( 'time_format' );
|
||||
|
||||
$date = $datetime->format( $dateformat . ' @ ' . $timeformat );
|
||||
|
||||
if ( ! $date ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a WordPress item to federate.
|
||||
*
|
||||
* Checks if item (WP_Post) is "public", a supported post type
|
||||
* and not password protected.
|
||||
*
|
||||
* @return null|WP_Post The WordPress item.
|
||||
*/
|
||||
protected static function get_item() {
|
||||
$post = \get_post();
|
||||
|
||||
if ( ! $post ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( 'publish' !== \get_post_status( $post ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( \post_password_required( $post ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! \in_array( \get_post_type( $post ), \get_post_types_by_support( 'activitypub' ), true ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $post;
|
||||
}
|
||||
}
|
@ -0,0 +1,510 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use WP_REST_Request;
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
/**
|
||||
* ActivityPub Signature Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
* @author Django Doucet
|
||||
*/
|
||||
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.
|
||||
*
|
||||
* @return mixed The public key.
|
||||
*/
|
||||
public static function get_public_key_for( $user_id, $force = false ) {
|
||||
if ( $force ) {
|
||||
self::generate_key_pair_for( $user_id );
|
||||
}
|
||||
|
||||
$key_pair = self::get_keypair_for( $user_id );
|
||||
|
||||
return $key_pair['public_key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return mixed The private key.
|
||||
*/
|
||||
public static function get_private_key_for( $user_id, $force = false ) {
|
||||
if ( $force ) {
|
||||
self::generate_key_pair_for( $user_id );
|
||||
}
|
||||
|
||||
$key_pair = self::get_keypair_for( $user_id );
|
||||
|
||||
return $key_pair['private_key'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the key pair for a given user.
|
||||
*
|
||||
* @param int $user_id The WordPress User ID.
|
||||
*
|
||||
* @return array The key pair.
|
||||
*/
|
||||
public static function get_keypair_for( $user_id ) {
|
||||
$option_key = self::get_signature_options_key_for( $user_id );
|
||||
$key_pair = \get_option( $option_key );
|
||||
|
||||
if ( ! $key_pair ) {
|
||||
$key_pair = self::generate_key_pair_for( $user_id );
|
||||
}
|
||||
|
||||
return $key_pair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the pair keys
|
||||
*
|
||||
* @param int $user_id The WordPress User ID.
|
||||
*
|
||||
* @return array The key pair.
|
||||
*/
|
||||
protected static function generate_key_pair_for( $user_id ) {
|
||||
$option_key = self::get_signature_options_key_for( $user_id );
|
||||
$key_pair = self::check_legacy_key_pair_for( $user_id );
|
||||
|
||||
if ( $key_pair ) {
|
||||
\add_option( $option_key, $key_pair );
|
||||
|
||||
return $key_pair;
|
||||
}
|
||||
|
||||
$config = array(
|
||||
'digest_alg' => 'sha512',
|
||||
'private_key_bits' => 2048,
|
||||
'private_key_type' => \OPENSSL_KEYTYPE_RSA,
|
||||
);
|
||||
|
||||
$key = \openssl_pkey_new( $config );
|
||||
$priv_key = null;
|
||||
|
||||
\openssl_pkey_export( $key, $priv_key );
|
||||
|
||||
$detail = \openssl_pkey_get_details( $key );
|
||||
|
||||
// check if keys are valid
|
||||
if (
|
||||
empty( $priv_key ) || ! is_string( $priv_key ) ||
|
||||
! isset( $detail['key'] ) || ! is_string( $detail['key'] )
|
||||
) {
|
||||
return array(
|
||||
'private_key' => null,
|
||||
'public_key' => null,
|
||||
);
|
||||
}
|
||||
|
||||
$key_pair = array(
|
||||
'private_key' => $priv_key,
|
||||
'public_key' => $detail['key'],
|
||||
);
|
||||
|
||||
// persist keys
|
||||
\add_option( $option_key, $key_pair );
|
||||
|
||||
return $key_pair;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the option key for a given user.
|
||||
*
|
||||
* @param int $user_id The WordPress User ID.
|
||||
*
|
||||
* @return string The option key.
|
||||
*/
|
||||
protected static function get_signature_options_key_for( $user_id ) {
|
||||
$id = $user_id;
|
||||
|
||||
if ( $user_id > 0 ) {
|
||||
$user = \get_userdata( $user_id );
|
||||
// sanatize username because it could include spaces and special chars
|
||||
$id = sanitize_title( $user->user_login );
|
||||
}
|
||||
|
||||
return 'activitypub_keypair_for_' . $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if there is a legacy key pair
|
||||
*
|
||||
* @param int $user_id The WordPress User ID.
|
||||
*
|
||||
* @return array|bool The key pair or false.
|
||||
*/
|
||||
protected static function check_legacy_key_pair_for( $user_id ) {
|
||||
switch ( $user_id ) {
|
||||
case 0:
|
||||
$public_key = \get_option( 'activitypub_blog_user_public_key' );
|
||||
$private_key = \get_option( 'activitypub_blog_user_private_key' );
|
||||
break;
|
||||
case -1:
|
||||
$public_key = \get_option( 'activitypub_application_user_public_key' );
|
||||
$private_key = \get_option( 'activitypub_application_user_private_key' );
|
||||
break;
|
||||
default:
|
||||
$public_key = \get_user_meta( $user_id, 'magic_sig_public_key', true );
|
||||
$private_key = \get_user_meta( $user_id, 'magic_sig_private_key', true );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( ! empty( $public_key ) && is_string( $public_key ) && ! empty( $private_key ) && is_string( $private_key ) ) {
|
||||
return array(
|
||||
'private_key' => $private_key,
|
||||
'public_key' => $public_key,
|
||||
);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the Signature for a 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.
|
||||
*
|
||||
* @return string The signature.
|
||||
*/
|
||||
public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) {
|
||||
$user = Users::get_by_id( $user_id );
|
||||
$key = self::get_private_key_for( $user->get__id() );
|
||||
|
||||
$url_parts = \wp_parse_url( $url );
|
||||
|
||||
$host = $url_parts['host'];
|
||||
$path = '/';
|
||||
|
||||
// add path
|
||||
if ( ! empty( $url_parts['path'] ) ) {
|
||||
$path = $url_parts['path'];
|
||||
}
|
||||
|
||||
// add query
|
||||
if ( ! empty( $url_parts['query'] ) ) {
|
||||
$path .= '?' . $url_parts['query'];
|
||||
}
|
||||
|
||||
$http_method = \strtolower( $http_method );
|
||||
|
||||
if ( ! empty( $digest ) ) {
|
||||
$signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date\ndigest: $digest";
|
||||
} else {
|
||||
$signed_string = "(request-target): $http_method $path\nhost: $host\ndate: $date";
|
||||
}
|
||||
|
||||
$signature = null;
|
||||
\openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 );
|
||||
$signature = \base64_encode( $signature ); // phpcs:ignore
|
||||
|
||||
$key_id = $user->get_url() . '#main-key';
|
||||
|
||||
if ( ! empty( $digest ) ) {
|
||||
return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $key_id, $signature );
|
||||
} else {
|
||||
return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date",signature="%s"', $key_id, $signature );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verifies the http signatures
|
||||
*
|
||||
* @param WP_REST_Request|array $request The request object or $_SERVER array.
|
||||
*
|
||||
* @return mixed 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 ( 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
|
||||
$path = \wp_parse_url( \get_home_url(), PHP_URL_PATH );
|
||||
|
||||
if ( \is_string( $path ) ) {
|
||||
$path = trim( $path, '/' );
|
||||
}
|
||||
|
||||
if ( $path ) {
|
||||
$route = '/' . $path . $route;
|
||||
}
|
||||
|
||||
$headers = $request->get_headers();
|
||||
$headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' ' . $route;
|
||||
} else {
|
||||
$request = self::format_server_request( $request );
|
||||
$headers = $request['headers']; // $_SERVER array
|
||||
$headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0];
|
||||
}
|
||||
|
||||
if ( ! isset( $headers['signature'] ) ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Request not signed', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
if ( array_key_exists( 'signature', $headers ) ) {
|
||||
$signature_block = self::parse_signature_header( $headers['signature'][0] );
|
||||
} elseif ( array_key_exists( 'authorization', $headers ) ) {
|
||||
$signature_block = self::parse_signature_header( $headers['authorization'][0] );
|
||||
}
|
||||
|
||||
if ( ! isset( $signature_block ) || ! $signature_block ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Incompatible request signature. keyId and signature are required', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
$signed_headers = $signature_block['headers'];
|
||||
if ( ! $signed_headers ) {
|
||||
$signed_headers = array( 'date' );
|
||||
}
|
||||
|
||||
$signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers );
|
||||
if ( ! $signed_data ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Signed request date outside acceptable time window', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
$algorithm = self::get_signature_algorithm( $signature_block );
|
||||
if ( ! $algorithm ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Unsupported signature algorithm (only rsa-sha256 and hs2019 are supported)', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
if ( \in_array( 'digest', $signed_headers, true ) && isset( $body ) ) {
|
||||
if ( is_array( $headers['digest'] ) ) {
|
||||
$headers['digest'] = $headers['digest'][0];
|
||||
}
|
||||
$hashalg = 'sha256';
|
||||
$digest = explode( '=', $headers['digest'], 2 );
|
||||
if ( 'SHA-256' === $digest[0] ) {
|
||||
$hashalg = 'sha256';
|
||||
}
|
||||
if ( 'SHA-512' === $digest[0] ) {
|
||||
$hashalg = 'sha512';
|
||||
}
|
||||
|
||||
if ( \base64_encode( \hash( $hashalg, $body, true ) ) !== $digest[1] ) { // phpcs:ignore
|
||||
return new WP_Error( 'activitypub_signature', __( 'Invalid Digest header', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
}
|
||||
|
||||
$public_key = self::get_remote_key( $signature_block['keyId'] );
|
||||
|
||||
if ( \is_wp_error( $public_key ) ) {
|
||||
return $public_key;
|
||||
}
|
||||
|
||||
$verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0;
|
||||
|
||||
if ( ! $verified ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Invalid signature', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
return $verified;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_no_remote_profile_found',
|
||||
__( 'No Profile found or Profile not accessible', 'activitypub' ),
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
if ( isset( $actor['publicKey']['publicKeyPem'] ) ) {
|
||||
return \rtrim( $actor['publicKey']['publicKeyPem'] ); // phpcs:ignore
|
||||
}
|
||||
return new WP_Error(
|
||||
'activitypub_no_remote_key_found',
|
||||
__( 'No Public-Key found', 'activitypub' ),
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the signature algorithm from the signature header
|
||||
*
|
||||
* @param array $signature_block
|
||||
*
|
||||
* @return string The signature algorithm.
|
||||
*/
|
||||
public static function get_signature_algorithm( $signature_block ) {
|
||||
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
|
||||
default:
|
||||
return 'sha256';
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the Signature header
|
||||
*
|
||||
* @param string $signature The signature header.
|
||||
*
|
||||
* @return array signature parts
|
||||
*/
|
||||
public static function parse_signature_header( $signature ) {
|
||||
$parsed_header = array();
|
||||
$matches = array();
|
||||
|
||||
if ( \preg_match( '/keyId="(.*?)"/ism', $signature, $matches ) ) {
|
||||
$parsed_header['keyId'] = trim( $matches[1] );
|
||||
}
|
||||
if ( \preg_match( '/created=["|\']*([0-9]*)["|\']*/ism', $signature, $matches ) ) {
|
||||
$parsed_header['(created)'] = trim( $matches[1] );
|
||||
}
|
||||
if ( \preg_match( '/expires=["|\']*([0-9]*)["|\']*/ism', $signature, $matches ) ) {
|
||||
$parsed_header['(expires)'] = trim( $matches[1] );
|
||||
}
|
||||
if ( \preg_match( '/algorithm="(.*?)"/ism', $signature, $matches ) ) {
|
||||
$parsed_header['algorithm'] = trim( $matches[1] );
|
||||
}
|
||||
if ( \preg_match( '/headers="(.*?)"/ism', $signature, $matches ) ) {
|
||||
$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
|
||||
}
|
||||
|
||||
if ( ( $parsed_header['signature'] ) && ( $parsed_header['algorithm'] ) && ( ! $parsed_header['headers'] ) ) {
|
||||
$parsed_header['headers'] = array( 'date' );
|
||||
}
|
||||
|
||||
return $parsed_header;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
*
|
||||
* @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 ) {
|
||||
if ( isset( $headers['x_original_host'] ) ) {
|
||||
$signed_data .= $header . ': ' . $headers['x_original_host'][0] . "\n";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ( '(request-target)' === $header ) {
|
||||
$signed_data .= $header . ': ' . $headers[ $header ][0] . "\n";
|
||||
continue;
|
||||
}
|
||||
if ( str_contains( $header, '-' ) ) {
|
||||
$signed_data .= $header . ': ' . $headers[ str_replace( '-', '_', $header ) ][0] . "\n";
|
||||
continue;
|
||||
}
|
||||
if ( '(created)' === $header ) {
|
||||
if ( ! empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) {
|
||||
// created in future
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! array_key_exists( '(created)', $headers ) ) {
|
||||
$signed_data .= $header . ': ' . $signature_block['(created)'] . "\n";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ( '(expires)' === $header ) {
|
||||
if ( ! empty( $signature_block['(expires)'] ) && \intval( $signature_block['(expires)'] ) < \time() ) {
|
||||
// expired in past
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! array_key_exists( '(expires)', $headers ) ) {
|
||||
$signed_data .= $header . ': ' . $signature_block['(expires)'] . "\n";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ( 'date' === $header ) {
|
||||
// allow a bit of leeway for misconfigured clocks.
|
||||
$d = new DateTime( $headers[ $header ][0] );
|
||||
$d->setTimeZone( new DateTimeZone( 'UTC' ) );
|
||||
$c = $d->format( 'U' );
|
||||
|
||||
$dplus = time() + ( 3 * HOUR_IN_SECONDS );
|
||||
$dminus = time() - ( 3 * HOUR_IN_SECONDS );
|
||||
|
||||
if ( $c > $dplus || $c < $dminus ) {
|
||||
// time out of range
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$signed_data .= $header . ': ' . $headers[ $header ][0] . "\n";
|
||||
}
|
||||
return \rtrim( $signed_data, "\n" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the digest for a 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
|
||||
return "SHA-256=$digest";
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the $_SERVER to resemble the WP_REST_REQUEST array,
|
||||
* for use with verify_http_signature()
|
||||
*
|
||||
* @param array $_SERVER The $_SERVER array.
|
||||
*
|
||||
* @return array $request The formatted request array.
|
||||
*/
|
||||
public static function format_server_request( $server ) {
|
||||
$request = array();
|
||||
foreach ( $server as $param_key => $param_val ) {
|
||||
$req_param = strtolower( $param_key );
|
||||
if ( 'REQUEST_URI' === $req_param ) {
|
||||
$request['headers']['route'][] = $param_val;
|
||||
} else {
|
||||
$header_key = str_replace(
|
||||
'http_',
|
||||
'',
|
||||
$req_param
|
||||
);
|
||||
$request['headers'][ $header_key ][] = \wp_unslash( $param_val );
|
||||
}
|
||||
}
|
||||
return $request;
|
||||
}
|
||||
}
|
@ -0,0 +1,287 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
/**
|
||||
* ActivityPub WebFinger Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://webfinger.net/
|
||||
*/
|
||||
class Webfinger {
|
||||
/**
|
||||
* Returns a users WebFinger "resource"
|
||||
*
|
||||
* @param int $user_id The WordPress user id
|
||||
*
|
||||
* @return string The user-resource
|
||||
*/
|
||||
public static function get_user_resource( $user_id ) {
|
||||
$user = Users::get_by_id( $user_id );
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return $user->get_webfinger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a WebFinger resource
|
||||
*
|
||||
* @param string $uri The WebFinger Resource
|
||||
*
|
||||
* @return string|WP_Error The URL or WP_Error
|
||||
*/
|
||||
public static function resolve( $uri ) {
|
||||
$data = self::get_data( $uri );
|
||||
|
||||
if ( \is_wp_error( $data ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if ( ! is_array( $data ) || empty( $data['links'] ) ) {
|
||||
return new WP_Error(
|
||||
'webfinger_missing_links',
|
||||
__( 'No valid Link elements found.', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $data['links'] as $link ) {
|
||||
if (
|
||||
'self' === $link['rel'] &&
|
||||
(
|
||||
'application/activity+json' === $link['type'] ||
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' === $link['type']
|
||||
)
|
||||
) {
|
||||
return $link['href'];
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'webfinger_url_no_activitypub',
|
||||
__( 'The Site supports WebFinger but not ActivityPub', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a URI to an acct <identifier>@<host>
|
||||
*
|
||||
* @param string $uri The URI (acct:, mailto:, http:, https:)
|
||||
*
|
||||
* @return string|WP_Error Error or acct URI
|
||||
*/
|
||||
public static function uri_to_acct( $uri ) {
|
||||
$data = self::get_data( $uri );
|
||||
|
||||
if ( is_wp_error( $data ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// check if subject is an acct URI
|
||||
if (
|
||||
isset( $data['subject'] ) &&
|
||||
\str_starts_with( $data['subject'], 'acct:' )
|
||||
) {
|
||||
return $data['subject'];
|
||||
}
|
||||
|
||||
// search for an acct URI in the aliases
|
||||
if ( isset( $data['aliases'] ) ) {
|
||||
foreach ( $data['aliases'] as $alias ) {
|
||||
if ( \str_starts_with( $alias, 'acct:' ) ) {
|
||||
return $alias;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'webfinger_url_no_acct',
|
||||
__( 'No acct URI found.', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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:)
|
||||
*
|
||||
* @return WP_Error|array Error reaction or array with
|
||||
* identifier and host as values
|
||||
*/
|
||||
public static function get_identifier_and_host( $url ) {
|
||||
if ( ! $url ) {
|
||||
return new WP_Error(
|
||||
'webfinger_invalid_identifier',
|
||||
__( 'Invalid Identifier', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// remove leading @
|
||||
$url = ltrim( $url, '@' );
|
||||
|
||||
if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
|
||||
$identifier = 'acct:' . $url;
|
||||
$scheme = 'acct';
|
||||
} else {
|
||||
$identifier = $url;
|
||||
$scheme = $match[1];
|
||||
}
|
||||
|
||||
$host = null;
|
||||
|
||||
switch ( $scheme ) {
|
||||
case 'acct':
|
||||
case 'mailto':
|
||||
case 'xmpp':
|
||||
if ( strpos( $identifier, '@' ) !== false ) {
|
||||
$host = substr( $identifier, strpos( $identifier, '@' ) + 1 );
|
||||
}
|
||||
break;
|
||||
default:
|
||||
$host = wp_parse_url( $identifier, PHP_URL_HOST );
|
||||
break;
|
||||
}
|
||||
|
||||
if ( empty( $host ) ) {
|
||||
return new WP_Error(
|
||||
'webfinger_invalid_identifier',
|
||||
__( 'Invalid Identifier', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return array( $identifier, $host );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WebFinger data for a given URI
|
||||
*
|
||||
* @param string $uri The Identifier: <identifier>@<host> or URI
|
||||
*
|
||||
* @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 );
|
||||
|
||||
if ( is_wp_error( $identifier_and_host ) ) {
|
||||
return $identifier_and_host;
|
||||
}
|
||||
|
||||
$transient_key = self::generate_cache_key( $uri );
|
||||
|
||||
list( $identifier, $host ) = $identifier_and_host;
|
||||
|
||||
$data = \get_transient( $transient_key );
|
||||
if ( $data ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$webfinger_url = sprintf( 'https://%s/.well-known/webfinger?resource=%s', $host, rawurlencode( $identifier ) );
|
||||
|
||||
$response = wp_safe_remote_get(
|
||||
$webfinger_url,
|
||||
array(
|
||||
'headers' => array( 'Accept' => 'application/jrd+json' ),
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $response ) ) {
|
||||
return new WP_Error(
|
||||
'webfinger_url_not_accessible',
|
||||
__( 'The WebFinger Resource is not accessible.', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $webfinger_url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$body = wp_remote_retrieve_body( $response );
|
||||
$data = json_decode( $body, true );
|
||||
|
||||
\set_transient( $transient_key, $data, WEEK_IN_SECONDS );
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Remote-Follow endpoint for a given URI
|
||||
*
|
||||
* @return string|WP_Error Error or the Remote-Follow endpoint URI.
|
||||
*/
|
||||
public static function get_remote_follow_endpoint( $uri ) {
|
||||
$data = self::get_data( $uri );
|
||||
|
||||
if ( is_wp_error( $data ) ) {
|
||||
return $data;
|
||||
}
|
||||
|
||||
if ( empty( $data['links'] ) ) {
|
||||
return new WP_Error(
|
||||
'webfinger_missing_links',
|
||||
__( 'No valid Link elements found.', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $data['links'] as $link ) {
|
||||
if ( 'http://ostatus.org/schema/1.0/subscribe' === $link['rel'] ) {
|
||||
return $link['template'];
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'webfinger_missing_remote_follow_endpoint',
|
||||
__( 'No valid Remote-Follow endpoint found.', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cache key for a given URI
|
||||
*
|
||||
* @param string $uri A WebFinger Resource URI
|
||||
*
|
||||
* @return string The cache key
|
||||
*/
|
||||
public static function generate_cache_key( $uri ) {
|
||||
$uri = ltrim( $uri, '@' );
|
||||
|
||||
if ( filter_var( $uri, FILTER_VALIDATE_EMAIL ) ) {
|
||||
$uri = 'acct:' . $uri;
|
||||
}
|
||||
|
||||
return 'webfinger_' . md5( $uri );
|
||||
}
|
||||
}
|
@ -0,0 +1,432 @@
|
||||
<?php
|
||||
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
|
||||
*
|
||||
* @author Matt Wiebe
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Followers {
|
||||
const POST_TYPE = 'ap_follower';
|
||||
const CACHE_KEY_INBOXES = 'follower_inboxes_%s';
|
||||
|
||||
/**
|
||||
* Add new Follower
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public static function add_follower( $user_id, $actor ) {
|
||||
$meta = get_remote_metadata_by_actor( $actor );
|
||||
|
||||
if ( is_tombstone( $meta ) ) {
|
||||
return $meta;
|
||||
}
|
||||
|
||||
if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
|
||||
return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$follower = new Follower();
|
||||
$follower->from_array( $meta );
|
||||
|
||||
$id = $follower->upsert();
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$post_meta = get_post_meta( $id, 'activitypub_user_id' );
|
||||
|
||||
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
|
||||
if ( is_array( $post_meta ) && ! in_array( $user_id, $post_meta ) ) {
|
||||
add_post_meta( $id, 'activitypub_user_id', $user_id );
|
||||
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
|
||||
}
|
||||
|
||||
return $follower;
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a Follower
|
||||
*
|
||||
* @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.
|
||||
*/
|
||||
public static function remove_follower( $user_id, $actor ) {
|
||||
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
|
||||
|
||||
$follower = self::get_follower( $user_id, $actor );
|
||||
|
||||
if ( ! $follower ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return delete_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Follower.
|
||||
*
|
||||
* @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
|
||||
*/
|
||||
public static function get_follower( $user_id, $actor ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$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",
|
||||
array(
|
||||
esc_sql( self::POST_TYPE ),
|
||||
esc_sql( $user_id ),
|
||||
esc_sql( $actor ),
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
if ( $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
return Follower::init_from_cpt( $post );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Follower by Actor indepenent from the User.
|
||||
*
|
||||
* @param string $actor The Actor URL.
|
||||
*
|
||||
* @return \Activitypub\Model\Follower|null The Follower object or null
|
||||
*/
|
||||
public static function get_follower_by_actor( $actor ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$post_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT ID FROM $wpdb->posts WHERE guid=%s",
|
||||
esc_sql( $actor )
|
||||
)
|
||||
);
|
||||
|
||||
if ( $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
return Follower::init_from_cpt( $post );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* @param int $page Page number.
|
||||
* @param array $args The WP_Query arguments.
|
||||
* @return array List of `Follower` objects.
|
||||
*/
|
||||
public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) {
|
||||
$data = self::get_followers_with_count( $user_id, $number, $page, $args );
|
||||
return $data['followers'];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Followers of a given user, along with a total count for pagination purposes.
|
||||
*
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param int $number Maximum number of results to return.
|
||||
* @param int $page Page number.
|
||||
* @param array $args The WP_Query arguments.
|
||||
*
|
||||
* @return array
|
||||
* followers List of `Follower` objects.
|
||||
* total Total number of followers.
|
||||
*/
|
||||
public static function get_followers_with_count( $user_id, $number = -1, $page = null, $args = array() ) {
|
||||
$defaults = array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'posts_per_page' => $number,
|
||||
'paged' => $page,
|
||||
'orderby' => 'ID',
|
||||
'order' => 'DESC',
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'activitypub_user_id',
|
||||
'value' => $user_id,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
$query = new WP_Query( $args );
|
||||
$total = $query->found_posts;
|
||||
$followers = array_map(
|
||||
function ( $post ) {
|
||||
return Follower::init_from_cpt( $post );
|
||||
},
|
||||
$query->get_posts()
|
||||
);
|
||||
return compact( 'followers', 'total' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Followers
|
||||
*
|
||||
* @param array $args The WP_Query arguments.
|
||||
*
|
||||
* @return array The Term list of Followers.
|
||||
*/
|
||||
public static function get_all_followers() {
|
||||
$args = array(
|
||||
'nopaging' => true,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
);
|
||||
return self::get_followers( null, null, null, $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the total number of followers
|
||||
*
|
||||
* @param int $user_id The ID of the WordPress User
|
||||
*
|
||||
* @return int The number of Followers
|
||||
*/
|
||||
public static function count_followers( $user_id ) {
|
||||
$query = new WP_Query(
|
||||
array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'fields' => 'ids',
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'activitypub_user_id',
|
||||
'value' => $user_id,
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
return $query->found_posts;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all Inboxes fo a Users Followers
|
||||
*
|
||||
* @param int $user_id The ID of the WordPress User
|
||||
*
|
||||
* @return array The list of Inboxes
|
||||
*/
|
||||
public static function get_inboxes( $user_id ) {
|
||||
$cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id );
|
||||
$inboxes = wp_cache_get( $cache_key, 'activitypub' );
|
||||
|
||||
if ( $inboxes ) {
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
// get all Followers of a ID of the WordPress User
|
||||
$posts = new WP_Query(
|
||||
array(
|
||||
'nopaging' => true,
|
||||
'post_type' => self::POST_TYPE,
|
||||
'fields' => 'ids',
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_user_id',
|
||||
'value' => $user_id,
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'value' => '',
|
||||
'compare' => '!=',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$posts = $posts->get_posts();
|
||||
|
||||
if ( ! $posts ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$results = $wpdb->get_col(
|
||||
$wpdb->prepare(
|
||||
"SELECT DISTINCT meta_value FROM {$wpdb->postmeta}
|
||||
WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ")
|
||||
AND meta_key = 'activitypub_inbox'
|
||||
AND meta_value IS NOT NULL",
|
||||
$posts
|
||||
)
|
||||
);
|
||||
|
||||
$inboxes = array_filter( $results );
|
||||
wp_cache_set( $cache_key, $inboxes, 'activitypub' );
|
||||
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return mixed The Term list of Followers, the format depends on $output.
|
||||
*/
|
||||
public static function get_outdated_followers( $number = 50, $older_than = 86400 ) {
|
||||
$args = array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'posts_per_page' => $number,
|
||||
'orderby' => 'modified',
|
||||
'order' => 'ASC',
|
||||
'post_status' => 'any', // 'any' includes 'trash
|
||||
'date_query' => array(
|
||||
array(
|
||||
'column' => 'post_modified_gmt',
|
||||
'before' => gmdate( 'Y-m-d', \time() - $older_than ),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$posts = new WP_Query( $args );
|
||||
$items = array();
|
||||
|
||||
foreach ( $posts->get_posts() as $follower ) {
|
||||
$items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return mixed The Term list of Followers, the format depends on $output.
|
||||
*/
|
||||
public static function get_faulty_followers( $number = 20 ) {
|
||||
$args = array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'posts_per_page' => $number,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'activitypub_errors',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'value' => '',
|
||||
'compare' => '=',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'value' => '',
|
||||
'compare' => '=',
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$posts = new WP_Query( $args );
|
||||
$items = array();
|
||||
|
||||
foreach ( $posts->get_posts() as $follower ) {
|
||||
$items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @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.
|
||||
*
|
||||
* @return int|false The meta ID on success, false on failure.
|
||||
*/
|
||||
public static function add_error( $post_id, $error ) {
|
||||
if ( is_string( $error ) ) {
|
||||
$error_message = $error;
|
||||
} elseif ( is_wp_error( $error ) ) {
|
||||
$error_message = $error->get_error_message();
|
||||
} else {
|
||||
$error_message = __(
|
||||
'Unknown Error or misconfigured Error-Message',
|
||||
'activitypub'
|
||||
);
|
||||
}
|
||||
|
||||
return add_post_meta(
|
||||
$post_id,
|
||||
'activitypub_errors',
|
||||
$error_message
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,247 @@
|
||||
<?php
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use WP_Error;
|
||||
use WP_Comment_Query;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\url_to_commentid;
|
||||
use function Activitypub\object_id_to_comment;
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
|
||||
/**
|
||||
* ActivityPub Interactions Collection
|
||||
*/
|
||||
class Interactions {
|
||||
/**
|
||||
* Add a comment to a post
|
||||
*
|
||||
* @param array $activity The activity-object
|
||||
*
|
||||
* @return array|false The commentdata or false on failure
|
||||
*/
|
||||
public static function add_comment( $activity ) {
|
||||
if (
|
||||
! isset( $activity['object'] ) ||
|
||||
! isset( $activity['object']['id'] )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $activity['object']['inReplyTo'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$in_reply_to = \esc_url_raw( $activity['object']['inReplyTo'] );
|
||||
$comment_post_id = \url_to_postid( $in_reply_to );
|
||||
$parent_comment_id = url_to_commentid( $in_reply_to );
|
||||
|
||||
// save only replys 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
|
||||
if ( ! $comment_post_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$actor = object_to_uri( $activity['actor'] );
|
||||
$meta = get_remote_metadata_by_actor( $actor );
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a comment
|
||||
*
|
||||
* @param array $activity The activity-object
|
||||
*
|
||||
* @return array|string|int|\WP_Error|false The commentdata or false on failure
|
||||
*/
|
||||
public static function update_comment( $activity ) {
|
||||
$meta = get_remote_metadata_by_actor( $activity['actor'] );
|
||||
|
||||
//Determine comment_ID
|
||||
$comment = object_id_to_comment( \esc_url_raw( $activity['object']['id'] ) );
|
||||
$commentdata = \get_comment( $comment, ARRAY_A );
|
||||
|
||||
if ( ! $commentdata ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
//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'] );
|
||||
}
|
||||
|
||||
// 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 );
|
||||
|
||||
$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' );
|
||||
// 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`!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interaction(s) for a given URL/ID.
|
||||
*
|
||||
* @param strin $url The URL/ID to get interactions for.
|
||||
*
|
||||
* @return array The interactions as WP_Comment objects.
|
||||
*/
|
||||
public static function get_interaction_by_id( $url ) {
|
||||
$args = array(
|
||||
'nopaging' => true,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'protocol',
|
||||
'value' => 'activitypub',
|
||||
),
|
||||
array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'source_url',
|
||||
'value' => $url,
|
||||
),
|
||||
array(
|
||||
'key' => 'source_id',
|
||||
'value' => $url,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$query = new WP_Comment_Query( $args );
|
||||
return $query->comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interaction(s) for a given actor.
|
||||
*
|
||||
* @param string $actor The Actor-URL.
|
||||
*
|
||||
* @return array The interactions as WP_Comment objects.
|
||||
*/
|
||||
public static function get_interactions_by_actor( $actor ) {
|
||||
$meta = get_remote_metadata_by_actor( $actor );
|
||||
|
||||
// get URL, because $actor seems to be the ID
|
||||
if ( $meta && ! is_wp_error( $meta ) && isset( $meta['url'] ) ) {
|
||||
$actor = object_to_uri( $meta['url'] );
|
||||
}
|
||||
|
||||
$args = array(
|
||||
'nopaging' => true,
|
||||
'author_url' => $actor,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'protocol',
|
||||
'value' => 'activitypub',
|
||||
'compare' => '=',
|
||||
),
|
||||
),
|
||||
);
|
||||
$comment_query = new WP_Comment_Query( $args );
|
||||
return $comment_query->comments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds line breaks to the list of allowed comment tags.
|
||||
*
|
||||
* @param array $allowed_tags Allowed HTML tags.
|
||||
* @param string $context Context.
|
||||
*
|
||||
* @return array Filtered tag list.
|
||||
*/
|
||||
public static function allowed_comment_html( $allowed_tags, $context = '' ) {
|
||||
if ( 'pre_comment_content' !== $context ) {
|
||||
// Do nothing.
|
||||
return $allowed_tags;
|
||||
}
|
||||
|
||||
// Add `p` and `br` to the list of allowed tags.
|
||||
if ( ! array_key_exists( 'br', $allowed_tags ) ) {
|
||||
$allowed_tags['br'] = array();
|
||||
}
|
||||
|
||||
if ( ! array_key_exists( 'p', $allowed_tags ) ) {
|
||||
$allowed_tags['p'] = array();
|
||||
}
|
||||
|
||||
return $allowed_tags;
|
||||
}
|
||||
}
|
@ -0,0 +1,278 @@
|
||||
<?php
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use WP_Error;
|
||||
use WP_User_Query;
|
||||
use Activitypub\Model\User;
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Model\Application;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\normalize_url;
|
||||
use function Activitypub\normalize_host;
|
||||
use function Activitypub\url_to_authorid;
|
||||
use function Activitypub\is_user_disabled;
|
||||
|
||||
class Users {
|
||||
/**
|
||||
* The ID of the Blog User
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const BLOG_USER_ID = 0;
|
||||
|
||||
/**
|
||||
* The ID of the Application User
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const APPLICATION_USER_ID = -1;
|
||||
|
||||
/**
|
||||
* Get the User by ID
|
||||
*
|
||||
* @param int $user_id The User-ID.
|
||||
*
|
||||
* @return \Acitvitypub\Model\User The User.
|
||||
*/
|
||||
public static function get_by_id( $user_id ) {
|
||||
if ( is_string( $user_id ) || is_numeric( $user_id ) ) {
|
||||
$user_id = (int) $user_id;
|
||||
}
|
||||
|
||||
if ( is_user_disabled( $user_id ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
if ( self::BLOG_USER_ID === $user_id ) {
|
||||
return new Blog();
|
||||
} elseif ( self::APPLICATION_USER_ID === $user_id ) {
|
||||
return new Application();
|
||||
} elseif ( $user_id > 0 ) {
|
||||
return User::from_wp_user( $user_id );
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User by username.
|
||||
*
|
||||
* @param string $username The User-Name.
|
||||
*
|
||||
* @return \Acitvitypub\Model\User The User.
|
||||
*/
|
||||
public static function get_by_username( $username ) {
|
||||
// check for blog user.
|
||||
if ( Blog::get_default_username() === $username ) {
|
||||
return new Blog();
|
||||
}
|
||||
|
||||
if ( get_option( 'activitypub_blog_user_identifier' ) === $username ) {
|
||||
return new Blog();
|
||||
}
|
||||
|
||||
// check for application user.
|
||||
if ( 'application' === $username ) {
|
||||
return new Application();
|
||||
}
|
||||
|
||||
// check for 'activitypub_username' meta
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'count_total' => false,
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'activitypub_user_identifier',
|
||||
'value' => $username,
|
||||
'compare' => 'LIKE',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
return self::get_by_id( $user->results[0] );
|
||||
}
|
||||
|
||||
$username = str_replace( array( '*', '%' ), '', $username );
|
||||
|
||||
// check for login or nicename.
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'count_total' => false,
|
||||
'search' => $username,
|
||||
'search_columns' => array( 'user_login', 'user_nicename' ),
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
return self::get_by_id( $user->results[0] );
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User by resource.
|
||||
*
|
||||
* @param string $resource The User-Resource.
|
||||
*
|
||||
* @return \Acitvitypub\Model\User The User.
|
||||
*/
|
||||
public static function get_by_resource( $resource ) {
|
||||
$resource = object_to_uri( $resource );
|
||||
|
||||
$scheme = 'acct';
|
||||
$match = array();
|
||||
// try to extract the scheme and the host
|
||||
if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $resource, $match ) ) {
|
||||
// extract the scheme
|
||||
$scheme = \esc_attr( $match[1] );
|
||||
}
|
||||
|
||||
switch ( $scheme ) {
|
||||
// check for http(s) URIs
|
||||
case 'http':
|
||||
case 'https':
|
||||
$resource_path = \wp_parse_url( $resource, PHP_URL_PATH );
|
||||
|
||||
if ( $resource_path ) {
|
||||
$blog_path = \wp_parse_url( \home_url(), PHP_URL_PATH );
|
||||
|
||||
if ( $blog_path ) {
|
||||
$resource_path = \str_replace( $blog_path, '', $resource_path );
|
||||
}
|
||||
|
||||
$resource_path = \trim( $resource_path, '/' );
|
||||
|
||||
// check for http(s)://blog.example.com/@username
|
||||
if ( str_starts_with( $resource_path, '@' ) ) {
|
||||
$identifier = \str_replace( '@', '', $resource_path );
|
||||
$identifier = \trim( $identifier, '/' );
|
||||
|
||||
return self::get_by_username( $identifier );
|
||||
}
|
||||
}
|
||||
|
||||
// check for http(s)://blog.example.com/author/username
|
||||
$user_id = url_to_authorid( $resource );
|
||||
|
||||
if ( $user_id ) {
|
||||
return self::get_by_id( $user_id );
|
||||
}
|
||||
|
||||
// check for http(s)://blog.example.com/
|
||||
if (
|
||||
normalize_url( site_url() ) === normalize_url( $resource ) ||
|
||||
normalize_url( home_url() ) === normalize_url( $resource )
|
||||
) {
|
||||
return self::get_by_id( self::BLOG_USER_ID );
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_no_user_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
// check for acct URIs
|
||||
case 'acct':
|
||||
$resource = \str_replace( 'acct:', '', $resource );
|
||||
$identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) );
|
||||
$host = normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) );
|
||||
$blog_host = normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) );
|
||||
|
||||
if ( $blog_host !== $host ) {
|
||||
return new WP_Error(
|
||||
'activitypub_wrong_host',
|
||||
\__( 'Resource host does not match blog host', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// prepare wildcards https://github.com/mastodon/mastodon/issues/22213
|
||||
if ( in_array( $identifier, array( '_', '*', '' ), true ) ) {
|
||||
return self::get_by_id( self::BLOG_USER_ID );
|
||||
}
|
||||
|
||||
return self::get_by_username( $identifier );
|
||||
default:
|
||||
return new WP_Error(
|
||||
'activitypub_wrong_scheme',
|
||||
\__( 'Wrong scheme', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User by resource.
|
||||
*
|
||||
* @param string $resource The User-Resource.
|
||||
*
|
||||
* @return \Acitvitypub\Model\User The User.
|
||||
*/
|
||||
public static function get_by_various( $id ) {
|
||||
$user = null;
|
||||
|
||||
if ( is_numeric( $id ) ) {
|
||||
$user = self::get_by_id( $id );
|
||||
} elseif (
|
||||
// is URL
|
||||
filter_var( $id, FILTER_VALIDATE_URL ) ||
|
||||
// is acct
|
||||
str_starts_with( $id, 'acct:' ) ||
|
||||
// is email
|
||||
filter_var( $id, FILTER_VALIDATE_EMAIL )
|
||||
) {
|
||||
$user = self::get_by_resource( $id );
|
||||
}
|
||||
|
||||
if ( $user && ! is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
return self::get_by_username( $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User collection.
|
||||
*
|
||||
* @return array The User collection.
|
||||
*/
|
||||
public static function get_collection() {
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
|
||||
$return = array();
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$return[] = User::from_wp_user( $user->ID );
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub implementation for WordPress/PHP functions either missing from older WordPress/PHP versions or not included by default.
|
||||
*/
|
||||
|
||||
if ( ! function_exists( 'str_starts_with' ) ) {
|
||||
/**
|
||||
* Polyfill for `str_starts_with()` function added in PHP 8.0.
|
||||
*
|
||||
* Performs a case-sensitive check indicating if
|
||||
* the haystack begins with needle.
|
||||
*
|
||||
* @param string $haystack The string to search in.
|
||||
* @param string $needle The substring to search for in the `$haystack`.
|
||||
* @return bool True if `$haystack` starts with `$needle`, otherwise false.
|
||||
*/
|
||||
function str_starts_with( $haystack, $needle ) {
|
||||
if ( '' === $needle ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return 0 === strpos( $haystack, $needle );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_self_link' ) ) {
|
||||
/**
|
||||
* Returns the link for the currently displayed feed.
|
||||
*
|
||||
* @return string Correct link for the atom:self element.
|
||||
*/
|
||||
function get_self_link() {
|
||||
$host = wp_parse_url( home_url() );
|
||||
$path = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||
return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . $path ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'is_countable' ) ) {
|
||||
/**
|
||||
* Polyfill for `is_countable()` function added in PHP 7.3.
|
||||
*
|
||||
* @param mixed $value The value to check.
|
||||
* @return bool True if `$value` is countable, otherwise false.
|
||||
*/
|
||||
function is_countable( $value ) {
|
||||
return is_array( $value ) || $value instanceof \Countable;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Polyfill for `array_is_list()` function added in PHP 7.3.
|
||||
*
|
||||
* @param array $array The array to check.
|
||||
*
|
||||
* @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 ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( array_values( $array ) === $array ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$next_key = -1;
|
||||
|
||||
foreach ( $array as $k => $v ) {
|
||||
if ( ++$next_key !== $k ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'str_contains' ) ) {
|
||||
/**
|
||||
* Polyfill for `str_contains()` function added in PHP 8.0.
|
||||
*
|
||||
* Performs a case-sensitive check indicating if needle is
|
||||
* contained in haystack.
|
||||
*
|
||||
* @param string $haystack The string to search in.
|
||||
* @param string $needle The substring to search for in the `$haystack`.
|
||||
*
|
||||
* @return bool True if `$needle` is in `$haystack`, otherwise false.
|
||||
*/
|
||||
function str_contains( $haystack, $needle ) {
|
||||
if ( '' === $needle ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false !== strpos( $haystack, $needle );
|
||||
}
|
||||
}
|
@ -0,0 +1,17 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* Allow localhost URLs if WP_DEBUG is true.
|
||||
*
|
||||
* @param array $r Array of HTTP request args.
|
||||
* @param string $url The request URL.
|
||||
*
|
||||
* @return array Array or string of HTTP request arguments.
|
||||
*/
|
||||
function allow_localhost( $r, $url ) {
|
||||
$r['reject_unsafe_urls'] = false;
|
||||
|
||||
return $r;
|
||||
}
|
||||
add_filter( 'http_request_args', '\Activitypub\allow_localhost', 10, 2 );
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Http;
|
||||
|
||||
use function Activitypub\is_activity_public;
|
||||
|
||||
/**
|
||||
* Handle Create requests
|
||||
*/
|
||||
class Announce {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_announce',
|
||||
array( self::class, 'handle_announce' ),
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
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 ) ) {
|
||||
// @todo maybe send email
|
||||
return;
|
||||
}
|
||||
|
||||
// @todo save the `Announce`-Activity itself
|
||||
|
||||
if ( is_string( $array['object'] ) ) {
|
||||
$object = Http::get_remote_object( $array['object'] );
|
||||
} else {
|
||||
$object = $array['object'];
|
||||
}
|
||||
|
||||
if ( ! $object || is_wp_error( $object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $object['type'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = \strtolower( $object['type'] );
|
||||
|
||||
\do_action( 'activitypub_inbox', $object, $user_id, $type, $activity );
|
||||
\do_action( "activitypub_inbox_{$type}", $object, $user_id, $activity );
|
||||
}
|
||||
}
|
@ -0,0 +1,70 @@
|
||||
<?php
|
||||
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
|
||||
*/
|
||||
class Create {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_create',
|
||||
array( self::class, 'handle_create' ),
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function handle_create( $array, $user_id, $object = null ) {
|
||||
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
! isset( $array['object'] ) ||
|
||||
! isset( $array['object']['id'] )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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 ( $check_dupe ) {
|
||||
\do_action( 'activitypub_inbox_update', $array, $user_id, $object );
|
||||
return;
|
||||
}
|
||||
|
||||
$state = Interactions::add_comment( $array );
|
||||
$reaction = null;
|
||||
|
||||
if ( $state && ! \is_wp_error( $state ) ) {
|
||||
$reaction = \get_comment( $state );
|
||||
}
|
||||
|
||||
\do_action( 'activitypub_handled_create', $array, $user_id, $state, $reaction );
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Interactions;
|
||||
|
||||
/**
|
||||
* Handles Delete requests.
|
||||
*/
|
||||
class Delete {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_delete',
|
||||
array( self::class, 'handle_delete' )
|
||||
);
|
||||
|
||||
// defer signature verification for `Delete` requests.
|
||||
\add_filter(
|
||||
'activitypub_defer_signature_verification',
|
||||
array( self::class, 'defer_signature_verification' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
|
||||
// side effect
|
||||
\add_action(
|
||||
'activitypub_delete_actor_interactions',
|
||||
array( self::class, 'delete_interactions' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
case 'Person':
|
||||
case 'Group':
|
||||
case 'Organization':
|
||||
case 'Service':
|
||||
case 'Application':
|
||||
self::maybe_delete_follower( $activity );
|
||||
break;
|
||||
// Object and Link Types
|
||||
// @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||
case 'Note':
|
||||
case 'Article':
|
||||
case 'Image':
|
||||
case 'Audio':
|
||||
case 'Video':
|
||||
case 'Event':
|
||||
case 'Document':
|
||||
self::maybe_delete_interaction( $activity );
|
||||
break;
|
||||
// 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
|
||||
default:
|
||||
// ignore non Minimal Activities.
|
||||
if ( ! is_string( $activity['object'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if Object is an Actor.
|
||||
if ( $activity['actor'] === $activity['object'] ) {
|
||||
self::maybe_delete_follower( $activity );
|
||||
} else { // assume a interaction otherwise.
|
||||
self::maybe_delete_interaction( $activity );
|
||||
}
|
||||
// maybe handle Delete Activity for other Object Types.
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a Follower if Actor-URL is a Tombstone.
|
||||
*
|
||||
* @param array $activity The delete activity.
|
||||
*/
|
||||
public static function maybe_delete_follower( $activity ) {
|
||||
$follower = Followers::get_follower_by_actor( $activity['actor'] );
|
||||
|
||||
// verify if Actor is deleted.
|
||||
if ( $follower && Http::is_tombstone( $activity['actor'] ) ) {
|
||||
$follower->delete();
|
||||
self::maybe_delete_interactions( $activity );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete Reactions if Actor-URL is a Tombstone.
|
||||
*
|
||||
* @param array $activity The delete activity.
|
||||
*/
|
||||
public static function maybe_delete_interactions( $activity ) {
|
||||
// verify if Actor is deleted.
|
||||
if ( Http::is_tombstone( $activity['actor'] ) ) {
|
||||
\wp_schedule_single_event(
|
||||
\time(),
|
||||
'activitypub_delete_actor_interactions',
|
||||
array( $activity['actor'] )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete comments from an Actor.
|
||||
*
|
||||
* @param array $comments The comments to delete.
|
||||
*/
|
||||
public static function delete_interactions( $actor ) {
|
||||
$comments = Interactions::get_interactions_by_actor( $actor );
|
||||
|
||||
if ( is_array( $comments ) ) {
|
||||
foreach ( $comments as $comment ) {
|
||||
wp_delete_comment( $comment->comment_ID );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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'] ) ) {
|
||||
$id = $activity['object']['id'];
|
||||
} else {
|
||||
$id = $activity['object'];
|
||||
}
|
||||
|
||||
$comments = Interactions::get_interaction_by_id( $id );
|
||||
|
||||
if ( $comments && Http::is_tombstone( $id ) ) {
|
||||
foreach ( $comments as $comment ) {
|
||||
wp_delete_comment( $comment->comment_ID, true );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Defer signature verification for `Delete` requests.
|
||||
*
|
||||
* @param bool $defer Whether to defer signature verification.
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return bool Whether to defer signature verification.
|
||||
*/
|
||||
public static function defer_signature_verification( $defer, $request ) {
|
||||
$json = $request->get_json_params();
|
||||
|
||||
if ( isset( $json['type'] ) && 'Delete' === $json['type'] ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
@ -0,0 +1,119 @@
|
||||
<?php
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Notification;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Followers;
|
||||
|
||||
/**
|
||||
* Handle Follow requests
|
||||
*/
|
||||
class Follow {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_follow',
|
||||
array( self::class, 'handle_follow' )
|
||||
);
|
||||
|
||||
\add_action(
|
||||
'activitypub_followers_post_follow',
|
||||
array( self::class, 'send_follow_response' ),
|
||||
10,
|
||||
4
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "Follow" requests
|
||||
*
|
||||
* @param array $activity The activity object
|
||||
* @param int $user_id The user ID
|
||||
*/
|
||||
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
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = $user->get__id();
|
||||
|
||||
// save follower
|
||||
$follower = Followers::add_follower(
|
||||
$user_id,
|
||||
$activity['actor']
|
||||
);
|
||||
|
||||
do_action(
|
||||
'activitypub_followers_post_follow',
|
||||
$activity['actor'],
|
||||
$activity,
|
||||
$user_id,
|
||||
$follower
|
||||
);
|
||||
|
||||
// send notification
|
||||
$notification = new Notification(
|
||||
'follow',
|
||||
$activity['actor'],
|
||||
$activity,
|
||||
$user_id
|
||||
);
|
||||
$notification->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public static function send_follow_response( $actor, $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
|
||||
return;
|
||||
}
|
||||
|
||||
// only send minimal data
|
||||
$object = array_intersect_key(
|
||||
$object,
|
||||
array_flip(
|
||||
array(
|
||||
'id',
|
||||
'type',
|
||||
'actor',
|
||||
'object',
|
||||
)
|
||||
)
|
||||
);
|
||||
|
||||
$user = Users::get_by_id( $user_id );
|
||||
|
||||
// get inbox
|
||||
$inbox = $follower->get_shared_inbox();
|
||||
|
||||
// send "Accept" activity
|
||||
$activity = new Activity();
|
||||
$activity->set_type( 'Accept' );
|
||||
$activity->set_object( $object );
|
||||
$activity->set_actor( $user->get_id() );
|
||||
$activity->set_to( $actor );
|
||||
$activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() );
|
||||
|
||||
$activity = $activity->to_json();
|
||||
|
||||
Http::post( $inbox, $activity, $user_id );
|
||||
}
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Followers;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handle Undo requests
|
||||
*/
|
||||
class Undo {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_undo',
|
||||
array( self::class, 'handle_undo' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "Unfollow" requests
|
||||
*
|
||||
* @param array $activity The JSON "Undo" Activity
|
||||
* @param int $user_id The ID of the ID of the WordPress User
|
||||
*/
|
||||
public static function handle_undo( $activity ) {
|
||||
if (
|
||||
isset( $activity['object']['type'] ) &&
|
||||
'Follow' === $activity['object']['type'] &&
|
||||
isset( $activity['object']['object'] )
|
||||
) {
|
||||
$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
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = $user->get__id();
|
||||
$actor = object_to_uri( $activity['actor'] );
|
||||
|
||||
Followers::remove_follower( $user_id, $actor );
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Collection\Interactions;
|
||||
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
|
||||
/**
|
||||
* Handle Update requests.
|
||||
*/
|
||||
class Update {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_update',
|
||||
array( self::class, 'handle_update' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "Update" requests
|
||||
*
|
||||
* @param array $array The activity-object
|
||||
* @param int $user_id The id of the local blog-user
|
||||
*/
|
||||
public static function handle_update( $array ) {
|
||||
$object_type = isset( $array['object']['type'] ) ? $array['object']['type'] : '';
|
||||
|
||||
switch ( $object_type ) {
|
||||
// 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 );
|
||||
break;
|
||||
// Object and Link Types
|
||||
// @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||
case 'Note':
|
||||
case 'Article':
|
||||
case 'Image':
|
||||
case 'Audio':
|
||||
case 'Video':
|
||||
case 'Event':
|
||||
case 'Document':
|
||||
self::update_interaction( $array );
|
||||
break;
|
||||
// Minimal Activity
|
||||
// @see https://www.w3.org/TR/activitystreams-core/#example-1
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an Interaction
|
||||
*
|
||||
* @param array $activity The activity-object
|
||||
* @param int $user_id The id of the local blog-user
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function update_interaction( $activity ) {
|
||||
$commentdata = Interactions::update_comment( $activity );
|
||||
$reaction = null;
|
||||
|
||||
if ( ! empty( $commentdata['comment_ID'] ) ) {
|
||||
$state = 1;
|
||||
$reaction = \get_comment( $commentdata['comment_ID'] );
|
||||
} else {
|
||||
$state = $commentdata;
|
||||
}
|
||||
|
||||
\do_action( 'activitypub_handled_update', $activity, null, $state, $reaction );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an Actor
|
||||
*
|
||||
* @param array $activity The activity-object
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function update_actor( $activity ) {
|
||||
// update cache
|
||||
get_remote_metadata_by_actor( $activity['actor'], false );
|
||||
|
||||
// @todo maybe also update all interactions
|
||||
}
|
||||
}
|
@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
\get_current_screen()->add_help_tab(
|
||||
array(
|
||||
'id' => 'template-tags',
|
||||
'title' => \__( 'Template Tags', 'activitypub' ),
|
||||
'content' =>
|
||||
'<p>' . __( 'The following Template Tags are available:', 'activitypub' ) . '</p>' .
|
||||
'<dl>' .
|
||||
'<dt><code>[ap_title]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s title.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_content apply_filters="yes"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s content. With <code>apply_filters</code> you can decide if filters (<code>apply_filters( \'the_content\', $content )</code>) should be applied or not (default is <code>yes</code>). The values can be <code>yes</code> or <code>no</code>. <code>apply_filters</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_excerpt length="400"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s excerpt (uses <code>the_excerpt</code> if that is set). If no excerpt is provided, will truncate at <code>length</code> (optional, default = 400).', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_permalink type="url"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s permalink. <code>type</code> can be either: <code>url</code> or <code>html</code> (an <a /> tag). <code>type</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_shortlink type="url"]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s shortlink. <code>type</code> can be either <code>url</code> or <code>html</code> (an <a /> tag). I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>, to prettify the Shortlinks. <code>type</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_hashtags]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s tags as hashtags.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_hashcats]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s categories as hashtags.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_image type=full]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The URL for the post\'s featured image, defaults to full size. The type attribute can be any of the following: <code>thumbnail</code>, <code>medium</code>, <code>large</code>, <code>full</code>. <code>type</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_author]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The author\'s name.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_authorurl]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The URL to the author\'s profile page.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_date]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s date.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_time]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s time.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_datetime]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The post\'s date/time formated as "date @ time".', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_blogurl]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The URL to the site.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_blogname]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The name of the site.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'<dt><code>[ap_blogdesc]</code></dt>' .
|
||||
'<dd>' . \wp_kses( __( 'The description of the site.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||
'</dl>' .
|
||||
'<p>' . __( 'You may also use any Shortcode normally available to you on your site, however be aware that Shortcodes may significantly increase the size of your content depending on what they do.', 'activitypub' ) . '</p>' .
|
||||
'<p>' . __( 'Note: the old Template Tags are now deprecated and automatically converted to the new ones.', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \wp_kses( \__( '<a href="https://github.com/pfefferle/wordpress-activitypub/issues/new" target="_blank">Let me know</a> if you miss a Template Tag.', 'activitypub' ), 'activitypub' ) . '</p>',
|
||||
)
|
||||
);
|
||||
|
||||
\get_current_screen()->add_help_tab(
|
||||
array(
|
||||
'id' => 'glossary',
|
||||
'title' => \__( 'Glossary', 'activitypub' ),
|
||||
'content' =>
|
||||
'<p><h2>' . \__( 'Fediverse', 'activitypub' ) . '</h2></p>' .
|
||||
'<p>' . \__( 'The Fediverse is a new word made of two words: "federation" + "universe"', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'It is a federated social network running on free open software on a myriad of computers across the globe. Many independent servers are interconnected and allow people to interact with one another. There\'s no one central site: you choose a server to register. This ensures some decentralization and sovereignty of data. Fediverse (also called Fedi) has no built-in advertisements, no tricky algorithms, no one big corporation dictating the rules. Instead we have small cozy communities of like-minded people. Welcome!', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( 'For more informations 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><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>',
|
||||
)
|
||||
);
|
||||
|
||||
\get_current_screen()->set_help_sidebar(
|
||||
'<p><strong>' . \__( 'For more information:', 'activitypub' ) . '</strong></p>' .
|
||||
'<p>' . \__( '<a href="https://wordpress.org/support/plugin/activitypub/">Get support</a>', 'activitypub' ) . '</p>' .
|
||||
'<p>' . \__( '<a href="https://github.com/automattic/wordpress-activitypub/issues">Report an issue</a>', 'activitypub' ) . '</p>'
|
||||
);
|
@ -0,0 +1,204 @@
|
||||
<?php
|
||||
namespace Activitypub\Model;
|
||||
|
||||
use WP_Query;
|
||||
use Activitypub\Signature;
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
class Application extends Actor {
|
||||
/**
|
||||
* The User-ID
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $_id = Users::APPLICATION_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
|
||||
|
||||
/**
|
||||
* If the User is discoverable.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
|
||||
*
|
||||
* @context http://joinmastodon.org/ns#discoverable
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $discoverable = false;
|
||||
|
||||
/**
|
||||
* If the User is indexable.
|
||||
*
|
||||
* @context http://joinmastodon.org/ns#indexable
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $indexable = false;
|
||||
|
||||
/**
|
||||
* The WebFinger Resource.
|
||||
*
|
||||
* @var string<url>
|
||||
*/
|
||||
protected $webfinger;
|
||||
|
||||
public function get_type() {
|
||||
return 'Application';
|
||||
}
|
||||
|
||||
public function get_manually_approves_followers() {
|
||||
return true;
|
||||
}
|
||||
|
||||
public function get_id() {
|
||||
return get_rest_url_by_path( 'application' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Url.
|
||||
*
|
||||
* @return string The User-Url.
|
||||
*/
|
||||
public function get_url() {
|
||||
return $this->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the User-URL with @-Prefix for the username.
|
||||
*
|
||||
* @return string The User-URL with @-Prefix for the username.
|
||||
*/
|
||||
public function get_alternate_url() {
|
||||
return $this->get_url();
|
||||
}
|
||||
|
||||
public function get_name() {
|
||||
return 'application';
|
||||
}
|
||||
|
||||
public function get_preferred_username() {
|
||||
return $this->get_name();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Icon.
|
||||
*
|
||||
* @return array The User-Icon.
|
||||
*/
|
||||
public function get_icon() {
|
||||
// try site icon first
|
||||
$icon_id = get_option( 'site_icon' );
|
||||
|
||||
// try custom logo second
|
||||
if ( ! $icon_id ) {
|
||||
$icon_id = get_theme_mod( 'custom_logo' );
|
||||
}
|
||||
|
||||
$icon_url = false;
|
||||
|
||||
if ( $icon_id ) {
|
||||
$icon = wp_get_attachment_image_src( $icon_id, 'full' );
|
||||
if ( $icon ) {
|
||||
$icon_url = $icon[0];
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $icon_url ) {
|
||||
// fallback to default icon
|
||||
$icon_url = plugins_url( '/assets/img/wp-logo.png', ACTIVITYPUB_PLUGIN_FILE );
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'url' => esc_url( $icon_url ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Header-Image.
|
||||
*
|
||||
* @return array|null The User-Header-Image.
|
||||
*/
|
||||
public function get_header_image() {
|
||||
if ( \has_header_image() ) {
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'url' => esc_url( \get_header_image() ),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function get_published() {
|
||||
$first_post = new WP_Query(
|
||||
array(
|
||||
'orderby' => 'date',
|
||||
'order' => 'ASC',
|
||||
'number' => 1,
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! empty( $first_post->posts[0] ) ) {
|
||||
$time = \strtotime( $first_post->posts[0]->post_date_gmt );
|
||||
} else {
|
||||
$time = \time();
|
||||
}
|
||||
|
||||
return \gmdate( 'Y-m-d\TH:i:s\Z', $time );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Inbox-API-Endpoint.
|
||||
*
|
||||
* @return string The Inbox-Endpoint.
|
||||
*/
|
||||
public function get_inbox() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/inbox', $this->get__id() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Outbox-API-Endpoint.
|
||||
*
|
||||
* @return string The Outbox-Endpoint.
|
||||
*/
|
||||
public function get_outbox() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/outbox', $this->get__id() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a user@domain type of identifier for the user.
|
||||
*
|
||||
* @return string The Webfinger-Identifier.
|
||||
*/
|
||||
public function get_webfinger() {
|
||||
return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
|
||||
}
|
||||
|
||||
public function get_public_key() {
|
||||
return array(
|
||||
'id' => $this->get_id() . '#main-key',
|
||||
'owner' => $this->get_id(),
|
||||
'publicKeyPem' => Signature::get_public_key_for( Users::APPLICATION_USER_ID ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Description.
|
||||
*
|
||||
* @return string The User-Description.
|
||||
*/
|
||||
public function get_summary() {
|
||||
return \wpautop(
|
||||
\wp_kses(
|
||||
\get_bloginfo( 'description' ),
|
||||
'default'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function get_canonical_url() {
|
||||
return \home_url();
|
||||
}
|
||||
}
|
@ -0,0 +1,405 @@
|
||||
<?php
|
||||
namespace Activitypub\Model;
|
||||
|
||||
use WP_Query;
|
||||
use WP_Error;
|
||||
|
||||
use Activitypub\Signature;
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\is_user_disabled;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
class Blog extends Actor {
|
||||
/**
|
||||
* The Featured-Posts.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#featured
|
||||
*
|
||||
* @context {
|
||||
* "@id": "http://joinmastodon.org/ns#featured",
|
||||
* "@type": "@id"
|
||||
* }
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $featured;
|
||||
|
||||
/**
|
||||
* Moderators endpoint.
|
||||
*
|
||||
* @see https://join-lemmy.org/docs/contributors/05-federation.html
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $moderators;
|
||||
|
||||
/**
|
||||
* The User-ID
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $_id = Users::BLOG_USER_ID; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
|
||||
|
||||
/**
|
||||
* If the User is indexable.
|
||||
*
|
||||
* @context http://joinmastodon.org/ns#indexable
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $indexable;
|
||||
|
||||
/**
|
||||
* The WebFinger Resource.
|
||||
*
|
||||
* @var string<url>
|
||||
*/
|
||||
protected $webfinger;
|
||||
|
||||
/**
|
||||
* If the User is discoverable.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
|
||||
*
|
||||
* @context http://joinmastodon.org/ns#discoverable
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $discoverable;
|
||||
|
||||
/**
|
||||
* Restrict posting to mods
|
||||
*
|
||||
* @see https://join-lemmy.org/docs/contributors/05-federation.html
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $posting_restricted_to_mods;
|
||||
|
||||
public function get_manually_approves_followers() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public function get_discoverable() {
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-ID.
|
||||
*
|
||||
* @return string The User-ID.
|
||||
*/
|
||||
public function get_id() {
|
||||
return $this->get_url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of the object.
|
||||
*
|
||||
* If the Blog is in "single user" mode, return "Person" insted of "Group".
|
||||
*
|
||||
* @return string The type of the object.
|
||||
*/
|
||||
public function get_type() {
|
||||
if ( is_single_user() ) {
|
||||
return 'Person';
|
||||
} else {
|
||||
return 'Group';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Name.
|
||||
*
|
||||
* @return string The User-Name.
|
||||
*/
|
||||
public function get_name() {
|
||||
return \wp_strip_all_tags(
|
||||
\html_entity_decode(
|
||||
\get_bloginfo( 'name' ),
|
||||
\ENT_QUOTES,
|
||||
'UTF-8'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Description.
|
||||
*
|
||||
* @return string The User-Description.
|
||||
*/
|
||||
public function get_summary() {
|
||||
return \wpautop(
|
||||
\wp_kses(
|
||||
\get_bloginfo( 'description' ),
|
||||
'default'
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Url.
|
||||
*
|
||||
* @return string The User-Url.
|
||||
*/
|
||||
public function get_url() {
|
||||
return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get blog's homepage URL.
|
||||
*
|
||||
* @return string The User-Url.
|
||||
*/
|
||||
public function get_alternate_url() {
|
||||
return \esc_url( \trailingslashit( get_home_url() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a default Username.
|
||||
*
|
||||
* @return string The auto-generated Username.
|
||||
*/
|
||||
public static function get_default_username() {
|
||||
// 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.
|
||||
*
|
||||
* @param string $host The default username.
|
||||
*/
|
||||
return apply_filters( 'activitypub_default_blog_username', $host );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the preferred User-Name.
|
||||
*
|
||||
* @return string The User-Name.
|
||||
*/
|
||||
public function get_preferred_username() {
|
||||
$username = \get_option( 'activitypub_blog_user_identifier' );
|
||||
|
||||
if ( $username ) {
|
||||
return $username;
|
||||
}
|
||||
|
||||
return self::get_default_username();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Icon.
|
||||
*
|
||||
* @return array The User-Icon.
|
||||
*/
|
||||
public function get_icon() {
|
||||
// try site icon first
|
||||
$icon_id = get_option( 'site_icon' );
|
||||
|
||||
// try custom logo second
|
||||
if ( ! $icon_id ) {
|
||||
$icon_id = get_theme_mod( 'custom_logo' );
|
||||
}
|
||||
|
||||
$icon_url = false;
|
||||
|
||||
if ( $icon_id ) {
|
||||
$icon = wp_get_attachment_image_src( $icon_id, 'full' );
|
||||
if ( $icon ) {
|
||||
$icon_url = $icon[0];
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $icon_url ) {
|
||||
// fallback to default icon
|
||||
$icon_url = plugins_url( '/assets/img/wp-logo.png', ACTIVITYPUB_PLUGIN_FILE );
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'url' => esc_url( $icon_url ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Header-Image.
|
||||
*
|
||||
* @return array|null The User-Header-Image.
|
||||
*/
|
||||
public function get_image() {
|
||||
if ( \has_header_image() ) {
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'url' => esc_url( \get_header_image() ),
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function get_published() {
|
||||
$first_post = new WP_Query(
|
||||
array(
|
||||
'orderby' => 'date',
|
||||
'order' => 'ASC',
|
||||
'number' => 1,
|
||||
)
|
||||
);
|
||||
|
||||
if ( ! empty( $first_post->posts[0] ) ) {
|
||||
$time = \strtotime( $first_post->posts[0]->post_date_gmt );
|
||||
} else {
|
||||
$time = \time();
|
||||
}
|
||||
|
||||
return \gmdate( 'Y-m-d\TH:i:s\Z', $time );
|
||||
}
|
||||
|
||||
public function get_canonical_url() {
|
||||
return \home_url();
|
||||
}
|
||||
|
||||
public function get_moderators() {
|
||||
if ( is_single_user() || 'Group' !== $this->get_type() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_rest_url_by_path( 'collections/moderators' );
|
||||
}
|
||||
|
||||
public function get_attributed_to() {
|
||||
if ( is_single_user() || 'Group' !== $this->get_type() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return get_rest_url_by_path( 'collections/moderators' );
|
||||
}
|
||||
|
||||
public function get_public_key() {
|
||||
return array(
|
||||
'id' => $this->get_id() . '#main-key',
|
||||
'owner' => $this->get_id(),
|
||||
'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ),
|
||||
);
|
||||
}
|
||||
|
||||
public function get_posting_restricted_to_mods() {
|
||||
if ( 'Group' === $this->get_type() ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Inbox-API-Endpoint.
|
||||
*
|
||||
* @return string The Inbox-Endpoint.
|
||||
*/
|
||||
public function get_inbox() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/inbox', $this->get__id() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Outbox-API-Endpoint.
|
||||
*
|
||||
* @return string The Outbox-Endpoint.
|
||||
*/
|
||||
public function get_outbox() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/outbox', $this->get__id() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Followers-API-Endpoint.
|
||||
*
|
||||
* @return string The Followers-Endpoint.
|
||||
*/
|
||||
public function get_followers() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/followers', $this->get__id() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Following-API-Endpoint.
|
||||
*
|
||||
* @return string The Following-Endpoint.
|
||||
*/
|
||||
public function get_following() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/following', $this->get__id() ) );
|
||||
}
|
||||
|
||||
public function get_endpoints() {
|
||||
$endpoints = null;
|
||||
|
||||
if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) {
|
||||
$endpoints = array(
|
||||
'sharedInbox' => get_rest_url_by_path( 'inbox' ),
|
||||
);
|
||||
}
|
||||
|
||||
return $endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a user@domain type of identifier for the user.
|
||||
*
|
||||
* @return string The Webfinger-Identifier.
|
||||
*/
|
||||
public function get_webfinger() {
|
||||
return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Featured-API-Endpoint.
|
||||
*
|
||||
* @return string The Featured-Endpoint.
|
||||
*/
|
||||
public function get_featured() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) );
|
||||
}
|
||||
|
||||
public function get_indexable() {
|
||||
if ( \get_option( 'blog_public', 1 ) ) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,395 @@
|
||||
<?php
|
||||
namespace Activitypub\Model;
|
||||
|
||||
use WP_Error;
|
||||
use WP_Query;
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Collection\Followers;
|
||||
|
||||
/**
|
||||
* ActivityPub Follower Class
|
||||
*
|
||||
* This Object represents a single Follower.
|
||||
* There is no direct reference to a WordPress User here.
|
||||
*
|
||||
* @author Matt Wiebe
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#follow-activity-inbox
|
||||
*/
|
||||
class Follower extends Actor {
|
||||
/**
|
||||
* The complete Remote-Profile of the Follower
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
|
||||
|
||||
/**
|
||||
* Get the errors.
|
||||
*
|
||||
* @return mixed
|
||||
*/
|
||||
public function get_errors() {
|
||||
return get_post_meta( $this->_id, 'activitypub_errors' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Summary.
|
||||
*
|
||||
* @return int The Summary.
|
||||
*/
|
||||
public function get_summary() {
|
||||
if ( isset( $this->summary ) ) {
|
||||
return $this->summary;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return string The URL.
|
||||
*/
|
||||
public function get_url() {
|
||||
if ( $this->url ) {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset (delete) all errors.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function reset_errors() {
|
||||
delete_post_meta( $this->_id, 'activitypub_errors' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Count the errors.
|
||||
*
|
||||
* @return int The number of errors.
|
||||
*/
|
||||
public function count_errors() {
|
||||
$errors = $this->get_errors();
|
||||
|
||||
if ( is_array( $errors ) && ! empty( $errors ) ) {
|
||||
return count( $errors );
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the latest error message.
|
||||
*
|
||||
* @return string The error message.
|
||||
*/
|
||||
public function get_latest_error_message() {
|
||||
$errors = $this->get_errors();
|
||||
|
||||
if ( is_array( $errors ) && ! empty( $errors ) ) {
|
||||
return reset( $errors );
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the current Follower-Object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function update() {
|
||||
$this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the current Follower-Object.
|
||||
*
|
||||
* @return boolean True if the verification was successful.
|
||||
*/
|
||||
public function is_valid() {
|
||||
// the minimum required attributes
|
||||
$required_attributes = array(
|
||||
'id',
|
||||
'preferredUsername',
|
||||
'inbox',
|
||||
'publicKey',
|
||||
'publicKeyPem',
|
||||
);
|
||||
|
||||
foreach ( $required_attributes as $attribute ) {
|
||||
if ( ! $this->get( $attribute ) ) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the current Follower-Object.
|
||||
*
|
||||
* @return int|WP_Error The Post-ID or an WP_Error.
|
||||
*/
|
||||
public function save() {
|
||||
if ( ! $this->is_valid() ) {
|
||||
return new WP_Error( 'activitypub_invalid_follower', __( 'Invalid Follower', 'activitypub' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
if ( ! $this->get__id() ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
$post_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT ID FROM $wpdb->posts WHERE guid=%s",
|
||||
esc_sql( $this->get_id() )
|
||||
)
|
||||
);
|
||||
|
||||
if ( $post_id ) {
|
||||
$post = get_post( $post_id );
|
||||
$this->set__id( $post->ID );
|
||||
}
|
||||
}
|
||||
|
||||
$post_id = $this->get__id();
|
||||
|
||||
$args = array(
|
||||
'ID' => $post_id,
|
||||
'guid' => esc_url_raw( $this->get_id() ),
|
||||
'post_title' => wp_strip_all_tags( sanitize_text_field( $this->get_name() ) ),
|
||||
'post_author' => 0,
|
||||
'post_type' => Followers::POST_TYPE,
|
||||
'post_name' => esc_url_raw( $this->get_id() ),
|
||||
'post_excerpt' => sanitize_text_field( wp_kses( $this->get_summary(), 'user_description' ) ),
|
||||
'post_status' => 'publish',
|
||||
'meta_input' => $this->get_post_meta_input(),
|
||||
);
|
||||
|
||||
if ( ! empty( $post_id ) ) {
|
||||
// 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;
|
||||
}
|
||||
|
||||
$post_id = wp_insert_post( $args );
|
||||
$this->_id = $post_id;
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Upsert the current Follower-Object.
|
||||
*
|
||||
* @return int|WP_Error The Post-ID or an WP_Error.
|
||||
*/
|
||||
public function upsert() {
|
||||
return $this->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
public function delete() {
|
||||
wp_delete_post( $this->_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the post meta.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
protected function get_post_meta_input() {
|
||||
$meta_input = array();
|
||||
$meta_input['activitypub_inbox'] = $this->get_shared_inbox();
|
||||
$meta_input['activitypub_actor_json'] = $this->to_json();
|
||||
|
||||
return $meta_input;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the icon.
|
||||
*
|
||||
* Sets a fallback to better handle API and HTML outputs.
|
||||
*
|
||||
* @return array The icon.
|
||||
*/
|
||||
public function get_icon() {
|
||||
if ( isset( $this->icon['url'] ) ) {
|
||||
return $this->icon;
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'mediaType' => 'image/jpeg',
|
||||
'url' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Name.
|
||||
*
|
||||
* Tries to extract a name from the URL or ID if not set.
|
||||
*
|
||||
* @return string The name.
|
||||
*/
|
||||
public function get_name() {
|
||||
if ( $this->name ) {
|
||||
return $this->name;
|
||||
} elseif ( $this->preferred_username ) {
|
||||
return $this->preferred_username;
|
||||
}
|
||||
|
||||
return $this->extract_name_from_uri();
|
||||
}
|
||||
|
||||
/**
|
||||
* The preferred Username.
|
||||
*
|
||||
* Tries to extract a name from the URL or ID if not set.
|
||||
*
|
||||
* @return string The preferred Username.
|
||||
*/
|
||||
public function get_preferred_username() {
|
||||
if ( $this->preferred_username ) {
|
||||
return $this->preferred_username;
|
||||
}
|
||||
|
||||
return $this->extract_name_from_uri();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Icon URL (Avatar)
|
||||
*
|
||||
* @return string The URL to the Avatar.
|
||||
*/
|
||||
public function get_icon_url() {
|
||||
$icon = $this->get_icon();
|
||||
|
||||
if ( ! $icon ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( is_array( $icon ) ) {
|
||||
return $icon['url'];
|
||||
}
|
||||
|
||||
return $icon;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Icon URL (Avatar)
|
||||
*
|
||||
* @return string The URL to the Avatar.
|
||||
*/
|
||||
public function get_image_url() {
|
||||
$image = $this->get_image();
|
||||
|
||||
if ( ! $image ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( is_array( $image ) ) {
|
||||
return $image['url'];
|
||||
}
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared inbox, with a fallback to the inbox.
|
||||
*
|
||||
* @return string|null The URL to the shared inbox, the inbox or null.
|
||||
*/
|
||||
public function get_shared_inbox() {
|
||||
if ( ! empty( $this->get_endpoints()['sharedInbox'] ) ) {
|
||||
return $this->get_endpoints()['sharedInbox'];
|
||||
} elseif ( ! empty( $this->get_inbox() ) ) {
|
||||
return $this->get_inbox();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a Custom-Post-Type input to an Activitypub\Model\Follower.
|
||||
*
|
||||
* @return string The JSON string.
|
||||
*
|
||||
* @return array Activitypub\Model\Follower
|
||||
*/
|
||||
public static function init_from_cpt( $post ) {
|
||||
$actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true );
|
||||
$object = self::init_from_json( $actor_json );
|
||||
$object->set__id( $post->ID );
|
||||
$object->set_id( $post->guid );
|
||||
$object->set_name( $post->post_title );
|
||||
$object->set_summary( $post->post_excerpt );
|
||||
$object->set_published( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_date ) ) );
|
||||
$object->set_updated( gmdate( 'Y-m-d H:i:s', strtotime( $post->post_modified ) ) );
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Infer a shortname from the Actor ID or URL. Used only for fallbacks,
|
||||
* we will try to use what's supplied.
|
||||
*
|
||||
* @return string Hopefully the name of the Follower.
|
||||
*/
|
||||
protected function extract_name_from_uri() {
|
||||
// prefer the URL, but fall back to the ID.
|
||||
if ( $this->url ) {
|
||||
$name = $this->url;
|
||||
} else {
|
||||
$name = $this->id;
|
||||
}
|
||||
|
||||
if ( \filter_var( $name, FILTER_VALIDATE_URL ) ) {
|
||||
$name = \rtrim( $name, '/' );
|
||||
$path = \wp_parse_url( $name, PHP_URL_PATH );
|
||||
|
||||
if ( $path ) {
|
||||
if ( \strpos( $name, '@' ) !== false ) {
|
||||
// expected: https://example.com/@user (default URL pattern)
|
||||
$name = \preg_replace( '|^/@?|', '', $path );
|
||||
} else {
|
||||
// expected: https://example.com/users/user (default ID pattern)
|
||||
$parts = \explode( '/', $path );
|
||||
$name = \array_pop( $parts );
|
||||
}
|
||||
}
|
||||
} elseif (
|
||||
\is_email( $name ) ||
|
||||
\strpos( $name, 'acct' ) === 0 ||
|
||||
\strpos( $name, '@' ) === 0
|
||||
) {
|
||||
// expected: user@example.com or acct:user@example (WebFinger)
|
||||
$name = \ltrim( $name, '@' );
|
||||
$name = \ltrim( $name, 'acct:' );
|
||||
$parts = \explode( '@', $name );
|
||||
$name = $parts[0];
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
}
|
@ -0,0 +1,136 @@
|
||||
<?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();
|
||||
}
|
||||
}
|
@ -0,0 +1,336 @@
|
||||
<?php
|
||||
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 function Activitypub\is_user_disabled;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\get_actor_extra_fields;
|
||||
|
||||
class User extends Actor {
|
||||
/**
|
||||
* The local User-ID (WP_User).
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $_id; // phpcs:ignore PSR2.Classes.PropertyDeclaration.Underscore
|
||||
|
||||
/**
|
||||
* The Featured-Posts.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#featured
|
||||
*
|
||||
* @context {
|
||||
* "@id": "http://joinmastodon.org/ns#featured",
|
||||
* "@type": "@id"
|
||||
* }
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $featured;
|
||||
|
||||
/**
|
||||
* If the User is discoverable.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#discoverable
|
||||
*
|
||||
* @context http://joinmastodon.org/ns#discoverable
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $discoverable = true;
|
||||
|
||||
/**
|
||||
* If the User is indexable.
|
||||
*
|
||||
* @context http://joinmastodon.org/ns#indexable
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
protected $indexable;
|
||||
|
||||
/**
|
||||
* The WebFinger Resource.
|
||||
*
|
||||
* @var string<url>
|
||||
*/
|
||||
protected $webfinger;
|
||||
|
||||
public function get_type() {
|
||||
return 'Person';
|
||||
}
|
||||
|
||||
public static function from_wp_user( $user_id ) {
|
||||
if ( is_user_disabled( $user_id ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
$object = new static();
|
||||
$object->_id = $user_id;
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-ID.
|
||||
*
|
||||
* @return string The User-ID.
|
||||
*/
|
||||
public function get_id() {
|
||||
return $this->get_url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Name.
|
||||
*
|
||||
* @return string The User-Name.
|
||||
*/
|
||||
public function get_name() {
|
||||
return \esc_attr( \get_the_author_meta( 'display_name', $this->_id ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User-Description.
|
||||
*
|
||||
* @return string The User-Description.
|
||||
*/
|
||||
public function get_summary() {
|
||||
$description = get_user_meta( $this->_id, 'activitypub_user_description', true );
|
||||
if ( empty( $description ) ) {
|
||||
$description = get_user_meta( $this->_id, 'description', true );
|
||||
}
|
||||
return \wpautop( \wp_kses( $description, 'default' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get 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.
|
||||
*
|
||||
* @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() );
|
||||
}
|
||||
|
||||
public function get_preferred_username() {
|
||||
return \esc_attr( \get_the_author_meta( 'login', $this->_id ) );
|
||||
}
|
||||
|
||||
public function get_icon() {
|
||||
$icon = \esc_url(
|
||||
\get_avatar_url(
|
||||
$this->_id,
|
||||
array( 'size' => 120 )
|
||||
)
|
||||
);
|
||||
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'url' => $icon,
|
||||
);
|
||||
}
|
||||
|
||||
public function get_image() {
|
||||
if ( \has_header_image() ) {
|
||||
$image = \esc_url( \get_header_image() );
|
||||
return array(
|
||||
'type' => 'Image',
|
||||
'url' => $image,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
public function get_published() {
|
||||
return \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( \get_the_author_meta( 'registered', $this->_id ) ) );
|
||||
}
|
||||
|
||||
public function get_public_key() {
|
||||
return array(
|
||||
'id' => $this->get_id() . '#main-key',
|
||||
'owner' => $this->get_id(),
|
||||
'publicKeyPem' => Signature::get_public_key_for( $this->get__id() ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Inbox-API-Endpoint.
|
||||
*
|
||||
* @return string The Inbox-Endpoint.
|
||||
*/
|
||||
public function get_inbox() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/inbox', $this->get__id() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Outbox-API-Endpoint.
|
||||
*
|
||||
* @return string The Outbox-Endpoint.
|
||||
*/
|
||||
public function get_outbox() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/outbox', $this->get__id() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Followers-API-Endpoint.
|
||||
*
|
||||
* @return string The Followers-Endpoint.
|
||||
*/
|
||||
public function get_followers() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/followers', $this->get__id() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Following-API-Endpoint.
|
||||
*
|
||||
* @return string The Following-Endpoint.
|
||||
*/
|
||||
public function get_following() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/following', $this->get__id() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Featured-API-Endpoint.
|
||||
*
|
||||
* @return string The Featured-Endpoint.
|
||||
*/
|
||||
public function get_featured() {
|
||||
return get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $this->get__id() ) );
|
||||
}
|
||||
|
||||
public function get_endpoints() {
|
||||
$endpoints = null;
|
||||
|
||||
if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) {
|
||||
$endpoints = array(
|
||||
'sharedInbox' => get_rest_url_by_path( 'inbox' ),
|
||||
);
|
||||
}
|
||||
|
||||
return $endpoints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the User-Output with Attachments.
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a user@domain type of identifier for the user.
|
||||
*
|
||||
* @return string The Webfinger-Identifier.
|
||||
*/
|
||||
public function get_webfinger() {
|
||||
return $this->get_preferred_username() . '@' . \wp_parse_url( \home_url(), \PHP_URL_HOST );
|
||||
}
|
||||
|
||||
public function get_canonical_url() {
|
||||
return $this->get_url();
|
||||
}
|
||||
|
||||
public function get_streams() {
|
||||
return null;
|
||||
}
|
||||
|
||||
public function get_tag() {
|
||||
return array();
|
||||
}
|
||||
|
||||
public function get_indexable() {
|
||||
if ( \get_option( 'blog_public', 1 ) ) {
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
<?php
|
||||
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
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#followers
|
||||
*/
|
||||
class Actors {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_routes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)/remote-follow',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'remote_follow_get' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'resource' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET request
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
// redirect to canonical URL if it is not an ActivityPub request
|
||||
if ( ! is_activitypub_request() ) {
|
||||
header( 'Location: ' . $user->get_canonical_url(), true, 301 );
|
||||
exit;
|
||||
}
|
||||
|
||||
/*
|
||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_rest_users_pre' );
|
||||
|
||||
$json = $user->to_array();
|
||||
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Endpoint for remote follow UI/Block
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return void|string The URL to the remote follow page
|
||||
*/
|
||||
public static function remote_follow_get( WP_REST_Request $request ) {
|
||||
$resource = $request->get_param( 'resource' );
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$template = Webfinger::get_remote_follow_endpoint( $resource );
|
||||
|
||||
if ( is_wp_error( $template ) ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
$resource = $user->get_webfinger();
|
||||
$url = str_replace( '{uri}', $resource, $template );
|
||||
|
||||
return new WP_REST_Response(
|
||||
array( 'url' => $url, 'template' => $template ),
|
||||
200
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
@ -0,0 +1,228 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Activity\Base_Object;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
use Activitypub\Transformer\Factory;
|
||||
|
||||
use function Activitypub\esc_hashtag;
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
/**
|
||||
* ActivityPub Collections REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#featured
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#featuredTags
|
||||
*/
|
||||
class Collection {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_routes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)/collections/tags',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'tags_get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)/collections/featured',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'featured_get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/collections/moderators',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'moderators_get' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The Featured Tags endpoint
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response The response object.
|
||||
*/
|
||||
public static function tags_get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$number = 4;
|
||||
|
||||
$tags = \get_terms(
|
||||
array(
|
||||
'taxonomy' => 'post_tag',
|
||||
'orderby' => 'count',
|
||||
'order' => 'DESC',
|
||||
'number' => $number,
|
||||
)
|
||||
);
|
||||
|
||||
if ( is_wp_error( $tags ) ) {
|
||||
$tags = array();
|
||||
}
|
||||
|
||||
$response = array(
|
||||
'@context' => Base_Object::JSON_LD_CONTEXT,
|
||||
'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/tags', $user->get__id() ) ),
|
||||
'type' => 'Collection',
|
||||
'totalItems' => is_countable( $tags ) ? count( $tags ) : 0,
|
||||
'items' => array(),
|
||||
);
|
||||
|
||||
foreach ( $tags as $tag ) {
|
||||
$response['items'][] = array(
|
||||
'type' => 'Hashtag',
|
||||
'href' => \esc_url( \get_tag_link( $tag ) ),
|
||||
'name' => esc_hashtag( $tag->name ),
|
||||
);
|
||||
}
|
||||
|
||||
$rest_response = new WP_REST_Response( $response, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Featured posts endpoint
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response The response object.
|
||||
*/
|
||||
public static function featured_get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$sticky_posts = \get_option( 'sticky_posts' );
|
||||
|
||||
if ( ! is_single_user() && User_Collection::BLOG_USER_ID === $user->get__id() ) {
|
||||
$posts = array();
|
||||
} elseif ( $sticky_posts ) {
|
||||
$args = array(
|
||||
'post__in' => $sticky_posts,
|
||||
'ignore_sticky_posts' => 1,
|
||||
'orderby' => 'date',
|
||||
'order' => 'DESC',
|
||||
);
|
||||
|
||||
if ( $user->get__id() > 0 ) {
|
||||
$args['author'] = $user->get__id();
|
||||
}
|
||||
|
||||
$posts = \get_posts( $args );
|
||||
} else {
|
||||
$posts = array();
|
||||
}
|
||||
|
||||
$response = array(
|
||||
'@context' => Base_Object::JSON_LD_CONTEXT,
|
||||
'id' => get_rest_url_by_path( sprintf( 'actors/%d/collections/featured', $user_id ) ),
|
||||
'type' => 'OrderedCollection',
|
||||
'totalItems' => is_countable( $posts ) ? count( $posts ) : 0,
|
||||
'orderedItems' => array(),
|
||||
);
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$transformer = Factory::get_transformer( $post );
|
||||
|
||||
if ( \is_wp_error( $transformer ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$response['orderedItems'][] = $transformer->to_object()->to_array( false );
|
||||
}
|
||||
|
||||
$rest_response = new WP_REST_Response( $response, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Moderators endpoint
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response The response object.
|
||||
*/
|
||||
public static function moderators_get( $request ) {
|
||||
$response = array(
|
||||
'@context' => Actor::JSON_LD_CONTEXT,
|
||||
'id' => get_rest_url_by_path( 'collections/moderators' ),
|
||||
'type' => 'OrderedCollection',
|
||||
'orderedItems' => array(),
|
||||
);
|
||||
|
||||
$users = User_Collection::get_collection();
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$response['orderedItems'][] = $user->get_url();
|
||||
}
|
||||
|
||||
$rest_response = new WP_REST_Response( $response, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
@ -0,0 +1,95 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use WP_Error;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Comment as Comment_Utils;
|
||||
use Activitypub\Webfinger as Webfinger_Utils;
|
||||
|
||||
/**
|
||||
* ActivityPub Followers REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#followers
|
||||
*/
|
||||
class Comment {
|
||||
/**
|
||||
* 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,
|
||||
'/comments/(?P<comment_id>\d+)/remote-reply',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'remote_reply_get' ),
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => array(
|
||||
'resource' => array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'sanitize_text_field',
|
||||
),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Endpoint for remote follow UI/Block
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return void|string The URL to the remote follow page
|
||||
*/
|
||||
public static function remote_reply_get( WP_REST_Request $request ) {
|
||||
$resource = $request->get_param( 'resource' );
|
||||
$comment_id = $request->get_param( 'comment_id' );
|
||||
|
||||
$comment = get_comment( $comment_id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return new WP_Error( 'activitypub_comment_not_found', __( 'Comment not found', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$is_local = Comment_Utils::is_local( $comment );
|
||||
|
||||
if ( $is_local ) {
|
||||
return new WP_Error( 'activitypub_local_only_comment', __( 'Comment is local only', 'activitypub' ), array( 'status' => 403 ) );
|
||||
}
|
||||
|
||||
$template = Webfinger_Utils::get_remote_follow_endpoint( $resource );
|
||||
|
||||
if ( is_wp_error( $template ) ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
$comment_meta = \get_comment_meta( $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 {
|
||||
$resource = Comment_Utils::generate_id( $comment );
|
||||
}
|
||||
|
||||
$url = str_replace( '{uri}', $resource, $template );
|
||||
|
||||
return new WP_REST_Response(
|
||||
array( 'url' => $url, 'template' => $template ),
|
||||
200
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,150 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use WP_Error;
|
||||
use stdClass;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
use Activitypub\Collection\Followers as Follower_Collection;
|
||||
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\get_masked_wp_version;
|
||||
|
||||
/**
|
||||
* ActivityPub Followers REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#followers
|
||||
*/
|
||||
class Followers {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_routes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)/followers',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET request
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$order = $request->get_param( 'order' );
|
||||
$per_page = (int) $request->get_param( 'per_page' );
|
||||
$page = (int) $request->get_param( 'page' );
|
||||
$context = $request->get_param( 'context' );
|
||||
|
||||
/*
|
||||
* Action triggerd 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();
|
||||
|
||||
$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']; // 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 ); // 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 && ( $page > 1 ) ) { // phpcs:ignore
|
||||
$json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore
|
||||
}
|
||||
|
||||
// phpcs:ignore
|
||||
$json->orderedItems = array_map(
|
||||
function ( $item ) use ( $context ) {
|
||||
if ( 'full' === $context ) {
|
||||
return $item->to_array( false );
|
||||
}
|
||||
return $item->get_url();
|
||||
},
|
||||
$data['followers']
|
||||
);
|
||||
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'integer',
|
||||
'default' => 1,
|
||||
);
|
||||
|
||||
$params['per_page'] = array(
|
||||
'type' => 'integer',
|
||||
'default' => 20,
|
||||
);
|
||||
|
||||
$params['order'] = array(
|
||||
'type' => 'string',
|
||||
'default' => 'desc',
|
||||
'enum' => array( 'asc', 'desc' ),
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
$params['context'] = array(
|
||||
'type' => 'string',
|
||||
'default' => 'simple',
|
||||
'enum' => array( 'simple', 'full' ),
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\get_masked_wp_version;
|
||||
|
||||
/**
|
||||
* ActivityPub Following REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#following
|
||||
*/
|
||||
class Following {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_routes();
|
||||
|
||||
\add_filter( 'activitypub_rest_following', array( self::class, 'default_following' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)/following',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle GET request
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
/*
|
||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_rest_following_pre' );
|
||||
|
||||
$json = new \stdClass();
|
||||
|
||||
$json->{'@context'} = \Activitypub\get_context();
|
||||
|
||||
$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() ) ); // phpcs:ignore
|
||||
|
||||
$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
|
||||
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'integer',
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the Blog Authors to the following list of the Blog Actor
|
||||
* if Blog not in single mode.
|
||||
*
|
||||
* @param array $array The array of following urls.
|
||||
* @param User $user The user object.
|
||||
*
|
||||
* @return array The array of following urls.
|
||||
*/
|
||||
public static function default_following( $array, $user ) {
|
||||
if ( 0 !== $user->get__id() || is_single_user() ) {
|
||||
return $array;
|
||||
}
|
||||
|
||||
$users = User_Collection::get_collection();
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$array[] = $user->get_url();
|
||||
}
|
||||
|
||||
return $array;
|
||||
}
|
||||
}
|
@ -0,0 +1,328 @@
|
||||
<?php
|
||||
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
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#inbox
|
||||
*/
|
||||
class Inbox {
|
||||
/**
|
||||
* 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,
|
||||
'/inbox',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( self::class, 'shared_inbox_post' ),
|
||||
'args' => self::shared_inbox_post_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)/inbox',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::CREATABLE,
|
||||
'callback' => array( self::class, 'user_inbox_post' ),
|
||||
'args' => self::user_inbox_post_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'user_inbox_get' ),
|
||||
'args' => self::user_inbox_get_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the user-inbox
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function user_inbox_get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$page = $request->get_param( 'page', 0 );
|
||||
|
||||
/*
|
||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_rest_inbox_pre' );
|
||||
|
||||
$json = new \stdClass();
|
||||
|
||||
$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
|
||||
|
||||
// filter output
|
||||
$json = \apply_filters( 'activitypub_rest_inbox_array', $json );
|
||||
|
||||
/*
|
||||
* Action triggerd after the ActivityPub profile has been created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_inbox_post' );
|
||||
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles user-inbox requests
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function user_inbox_post( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$data = $request->get_json_params();
|
||||
$activity = Activity::init_from_array( $data );
|
||||
$type = $request->get_param( 'type' );
|
||||
$type = \strtolower( $type );
|
||||
|
||||
\do_action( 'activitypub_inbox', $data, $user->get__id(), $type, $activity );
|
||||
\do_action( "activitypub_inbox_{$type}", $data, $user->get__id(), $activity );
|
||||
|
||||
$rest_response = new WP_REST_Response( array(), 202 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The shared inbox
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function shared_inbox_post( $request ) {
|
||||
$data = $request->get_json_params();
|
||||
$activity = Activity::init_from_array( $data );
|
||||
$type = $request->get_param( 'type' );
|
||||
$type = \strtolower( $type );
|
||||
|
||||
\do_action( 'activitypub_inbox', $data, null, $type, $activity );
|
||||
\do_action( "activitypub_inbox_{$type}", $data, null, $activity );
|
||||
|
||||
$rest_response = new WP_REST_Response( array(), 202 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function user_inbox_get_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'integer',
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function user_inbox_post_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'integer',
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
$params['id'] = array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'esc_url_raw',
|
||||
);
|
||||
|
||||
$params['actor'] = array(
|
||||
'required' => true,
|
||||
'sanitize_callback' => 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,
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported 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['to'] = array(
|
||||
'required' => false,
|
||||
'sanitize_callback' => function ( $param, $request, $key ) {
|
||||
if ( \is_string( $param ) ) {
|
||||
$param = array( $param );
|
||||
}
|
||||
|
||||
return $param;
|
||||
},
|
||||
);
|
||||
|
||||
$params['cc'] = array(
|
||||
'sanitize_callback' => function ( $param, $request, $key ) {
|
||||
if ( \is_string( $param ) ) {
|
||||
$param = array( $param );
|
||||
}
|
||||
|
||||
return $param;
|
||||
},
|
||||
);
|
||||
|
||||
$params['bcc'] = array(
|
||||
'sanitize_callback' => function ( $param, $request, $key ) {
|
||||
if ( \is_string( $param ) ) {
|
||||
$param = array( $param );
|
||||
}
|
||||
|
||||
return $param;
|
||||
},
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get local user recipients
|
||||
*
|
||||
* @param array $data
|
||||
*
|
||||
* @return array The list of local users
|
||||
*/
|
||||
public static function get_recipients( $data ) {
|
||||
$recipients = extract_recipients_from_activity( $data );
|
||||
$users = array();
|
||||
|
||||
foreach ( $recipients as $recipient ) {
|
||||
$user_id = url_to_authorid( $recipient );
|
||||
|
||||
$user = get_user_by( 'id', $user_id );
|
||||
|
||||
if ( $user ) {
|
||||
$users[] = $user;
|
||||
}
|
||||
}
|
||||
|
||||
return $users;
|
||||
}
|
||||
}
|
@ -0,0 +1,187 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use WP_REST_Response;
|
||||
|
||||
use function Activitypub\get_total_users;
|
||||
use function Activitypub\get_active_users;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\get_masked_wp_version;
|
||||
|
||||
/**
|
||||
* ActivityPub NodeInfo REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see http://nodeinfo.diaspora.software/
|
||||
*/
|
||||
class Nodeinfo {
|
||||
/**
|
||||
* 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,
|
||||
'/nodeinfo/discovery',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'discovery' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/nodeinfo',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'nodeinfo' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/nodeinfo2',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'nodeinfo2' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render NodeInfo file
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function nodeinfo( $request ) {
|
||||
/*
|
||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_rest_nodeinfo_pre' );
|
||||
|
||||
$nodeinfo = array();
|
||||
|
||||
$nodeinfo['version'] = '2.0';
|
||||
$nodeinfo['software'] = array(
|
||||
'name' => 'wordpress',
|
||||
'version' => get_masked_wp_version(),
|
||||
);
|
||||
|
||||
$posts = \wp_count_posts();
|
||||
$comments = \wp_count_comments();
|
||||
|
||||
$nodeinfo['usage'] = array(
|
||||
'users' => array(
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users( '1 month ago' ),
|
||||
'activeHalfyear' => get_active_users( '6 month ago' ),
|
||||
),
|
||||
'localPosts' => (int) $posts->publish,
|
||||
'localComments' => (int) $comments->approved,
|
||||
);
|
||||
|
||||
$nodeinfo['openRegistrations'] = false;
|
||||
$nodeinfo['protocols'] = array( 'activitypub' );
|
||||
|
||||
$nodeinfo['services'] = array(
|
||||
'inbound' => array(),
|
||||
'outbound' => array(),
|
||||
);
|
||||
|
||||
$nodeinfo['metadata'] = array(
|
||||
'nodeName' => \get_bloginfo( 'name' ),
|
||||
'nodeDescription' => \get_bloginfo( 'description' ),
|
||||
'nodeIcon' => \get_site_icon_url(),
|
||||
);
|
||||
|
||||
return new WP_REST_Response( $nodeinfo, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render NodeInfo file
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function nodeinfo2( $request ) {
|
||||
/*
|
||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_rest_nodeinfo2_pre' );
|
||||
|
||||
$nodeinfo = array();
|
||||
|
||||
$nodeinfo['version'] = '2.0';
|
||||
$nodeinfo['server'] = array(
|
||||
'baseUrl' => \home_url( '/' ),
|
||||
'name' => \get_bloginfo( 'name' ),
|
||||
'software' => 'wordpress',
|
||||
'version' => get_masked_wp_version(),
|
||||
);
|
||||
|
||||
$posts = \wp_count_posts();
|
||||
$comments = \wp_count_comments();
|
||||
|
||||
$nodeinfo['usage'] = array(
|
||||
'users' => array(
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users( 1 ),
|
||||
'activeHalfyear' => get_active_users( 6 ),
|
||||
),
|
||||
'localPosts' => (int) $posts->publish,
|
||||
'localComments' => (int) $comments->approved,
|
||||
);
|
||||
|
||||
$nodeinfo['openRegistrations'] = false;
|
||||
$nodeinfo['protocols'] = array( 'activitypub' );
|
||||
|
||||
$nodeinfo['services'] = array(
|
||||
'inbound' => array(),
|
||||
'outbound' => array(),
|
||||
);
|
||||
|
||||
return new WP_REST_Response( $nodeinfo, 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render NodeInfo discovery file
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function discovery( $request ) {
|
||||
$discovery = array();
|
||||
$discovery['links'] = array(
|
||||
array(
|
||||
'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
||||
'href' => get_rest_url_by_path( 'nodeinfo' ),
|
||||
),
|
||||
array(
|
||||
'rel' => 'https://www.w3.org/ns/activitystreams#Application',
|
||||
'href' => get_rest_url_by_path( 'application' ),
|
||||
),
|
||||
);
|
||||
|
||||
return new \WP_REST_Response( $discovery, 200 );
|
||||
}
|
||||
}
|
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use stdClass;
|
||||
use WP_Error;
|
||||
use WP_REST_Server;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
use Activitypub\Transformer\Factory;
|
||||
|
||||
use function Activitypub\get_context;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\get_masked_wp_version;
|
||||
|
||||
/**
|
||||
* ActivityPub Outbox REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#outbox
|
||||
*/
|
||||
class Outbox {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_routes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/(users|actors)/(?P<user_id>[\w\-\.]+)/outbox',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'user_outbox_get' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the user-outbox
|
||||
*
|
||||
* @param WP_REST_Request $request
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function user_outbox_get( $request ) {
|
||||
$user_id = $request->get_param( 'user_id' );
|
||||
$user = User_Collection::get_by_various( $user_id );
|
||||
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
|
||||
|
||||
$page = $request->get_param( 'page', 1 );
|
||||
|
||||
/*
|
||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_rest_outbox_pre' );
|
||||
|
||||
$json = new stdClass();
|
||||
|
||||
$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
|
||||
|
||||
if ( $user_id > 0 ) {
|
||||
$count_posts = \count_user_posts( $user_id, $post_types, true );
|
||||
$json->totalItems = \intval( $count_posts ); // phpcs:ignore
|
||||
} else {
|
||||
foreach ( $post_types as $post_type ) {
|
||||
$count_posts = \wp_count_posts( $post_type );
|
||||
$json->totalItems += \intval( $count_posts->publish ); // phpcs:ignore
|
||||
}
|
||||
}
|
||||
|
||||
$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
|
||||
|
||||
if ( $page && ( ( \ceil ( $json->totalItems / 10 ) ) > $page ) ) { // phpcs:ignore
|
||||
$json->next = \add_query_arg( 'page', $page + 1, $json->partOf ); // phpcs:ignore
|
||||
}
|
||||
|
||||
if ( $page && ( $page > 1 ) ) { // phpcs:ignore
|
||||
$json->prev = \add_query_arg( 'page', $page - 1, $json->partOf ); // phpcs:ignore
|
||||
}
|
||||
|
||||
if ( $page ) {
|
||||
$posts = \get_posts(
|
||||
array(
|
||||
'posts_per_page' => 10,
|
||||
'author' => $user_id > 0 ? $user_id : null,
|
||||
'paged' => $page,
|
||||
'post_type' => $post_types,
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$transformer = Factory::get_transformer( $post );
|
||||
|
||||
if ( \is_wp_error( $transformer ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$post = $transformer->to_object();
|
||||
$activity = new Activity();
|
||||
$activity->set_type( 'Create' );
|
||||
$activity->set_object( $post );
|
||||
$json->orderedItems[] = $activity->to_array( false ); // phpcs:ignore
|
||||
}
|
||||
}
|
||||
|
||||
// filter output
|
||||
$json = \apply_filters( 'activitypub_rest_outbox_array', $json );
|
||||
|
||||
/*
|
||||
* Action triggerd after the ActivityPub profile has been created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_post' );
|
||||
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['page'] = array(
|
||||
'type' => 'integer',
|
||||
'default' => 1,
|
||||
);
|
||||
|
||||
$params['user_id'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
}
|
@ -0,0 +1,127 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use stdClass;
|
||||
use WP_Error;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Signature;
|
||||
use Activitypub\Model\Application;
|
||||
|
||||
/**
|
||||
* ActivityPub Server REST-Class
|
||||
*
|
||||
* @author Django Doucet
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#security-verification
|
||||
*/
|
||||
class Server {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_routes();
|
||||
|
||||
\add_filter( 'rest_request_before_callbacks', array( self::class, 'authorize_activitypub_requests' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/application',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'application_actor' ),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render Application actor profile
|
||||
*
|
||||
* @return WP_REST_Response The JSON profile of the Application Actor.
|
||||
*/
|
||||
public static function application_actor() {
|
||||
$user = new Application();
|
||||
|
||||
$json = $user->to_array();
|
||||
|
||||
$rest_response = new WP_REST_Response( $json, 200 );
|
||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||
|
||||
return $rest_response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback function to authorize each api requests
|
||||
*
|
||||
* @see WP_REST_Request
|
||||
*
|
||||
* @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
|
||||
* @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
|
||||
*
|
||||
* @param WP_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
|
||||
* Usually a WP_REST_Response or WP_Error.
|
||||
* @param array $handler Route handler used for the request.
|
||||
* @param WP_REST_Request $request Request used to generate the response.
|
||||
*
|
||||
* @return mixed|WP_Error The response, error, or modified response.
|
||||
*/
|
||||
public static function authorize_activitypub_requests( $response, $handler, $request ) {
|
||||
if ( 'HEAD' === $request->get_method() ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$route = $request->get_route();
|
||||
|
||||
// check if it is an activitypub request and exclude webfinger and nodeinfo endpoints
|
||||
if (
|
||||
! \str_starts_with( $route, '/' . ACTIVITYPUB_REST_NAMESPACE ) ||
|
||||
\str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'webfinger' ) ||
|
||||
\str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'nodeinfo' ) ||
|
||||
\str_starts_with( $route, '/' . \trailingslashit( ACTIVITYPUB_REST_NAMESPACE ) . 'application' )
|
||||
) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @return bool Whether to defer signature verification.
|
||||
*/
|
||||
$defer = \apply_filters( 'activitypub_defer_signature_verification', false, $request );
|
||||
|
||||
if ( $defer ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
if (
|
||||
// POST-Requests are always signed
|
||||
'GET' !== $request->get_method() ||
|
||||
// GET-Requests only require a signature in secure mode
|
||||
( 'GET' === $request->get_method() && ACTIVITYPUB_AUTHORIZED_FETCH )
|
||||
) {
|
||||
$verified_request = Signature::verify_http_signature( $request );
|
||||
if ( \is_wp_error( $verified_request ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_signature_verification',
|
||||
$verified_request->get_error_message(),
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
@ -0,0 +1,145 @@
|
||||
<?php
|
||||
namespace Activitypub\Rest;
|
||||
|
||||
use WP_Error;
|
||||
use WP_REST_Response;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
|
||||
/**
|
||||
* ActivityPub WebFinger REST-Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://webfinger.net/
|
||||
*/
|
||||
class Webfinger {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_routes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Register routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_routes() {
|
||||
\register_rest_route(
|
||||
ACTIVITYPUB_REST_NAMESPACE,
|
||||
'/webfinger',
|
||||
array(
|
||||
array(
|
||||
'methods' => \WP_REST_Server::READABLE,
|
||||
'callback' => array( self::class, 'webfinger' ),
|
||||
'args' => self::request_parameters(),
|
||||
'permission_callback' => '__return_true',
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* WebFinger endpoint.
|
||||
*
|
||||
* @param WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return WP_REST_Response The response object.
|
||||
*/
|
||||
public static function webfinger( $request ) {
|
||||
/*
|
||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||
*/
|
||||
\do_action( 'activitypub_rest_webfinger_pre' );
|
||||
|
||||
$code = 200;
|
||||
|
||||
$resource = $request->get_param( 'resource' );
|
||||
$response = self::get_profile( $resource );
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
$code = 400;
|
||||
$error_data = $response->get_error_data();
|
||||
|
||||
if ( isset( $error_data['status'] ) ) {
|
||||
$code = $error_data['status'];
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_REST_Response(
|
||||
$response,
|
||||
$code,
|
||||
array(
|
||||
'Access-Control-Allow-Origin' => '*',
|
||||
'Content-Type' => 'application/jrd+json; charset=' . get_option( 'blog_charset' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* The supported parameters
|
||||
*
|
||||
* @return array list of parameters
|
||||
*/
|
||||
public static function request_parameters() {
|
||||
$params = array();
|
||||
|
||||
$params['resource'] = array(
|
||||
'required' => true,
|
||||
'type' => 'string',
|
||||
'pattern' => '^(acct:)|^(https?://)(.+)$',
|
||||
);
|
||||
|
||||
return $params;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WebFinger profile.
|
||||
*
|
||||
* @param string $resource the WebFinger resource.
|
||||
*
|
||||
* @return array the WebFinger profile.
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
@ -0,0 +1,178 @@
|
||||
<?php
|
||||
namespace Activitypub\Table;
|
||||
|
||||
use WP_List_Table;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Followers as FollowerCollection;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
if ( ! \class_exists( '\WP_List_Table' ) ) {
|
||||
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||
}
|
||||
|
||||
class Followers extends WP_List_Table {
|
||||
private $user_id;
|
||||
|
||||
public function __construct() {
|
||||
if ( get_current_screen()->id === 'settings_page_activitypub' ) {
|
||||
$this->user_id = Users::BLOG_USER_ID;
|
||||
} else {
|
||||
$this->user_id = \get_current_user_id();
|
||||
}
|
||||
|
||||
parent::__construct(
|
||||
array(
|
||||
'singular' => \__( 'Follower', 'activitypub' ),
|
||||
'plural' => \__( 'Followers', 'activitypub' ),
|
||||
'ajax' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public function get_columns() {
|
||||
return array(
|
||||
'cb' => '<input type="checkbox" />',
|
||||
'avatar' => \__( 'Avatar', 'activitypub' ),
|
||||
'post_title' => \__( 'Name', 'activitypub' ),
|
||||
'username' => \__( 'Username', 'activitypub' ),
|
||||
'url' => \__( 'URL', 'activitypub' ),
|
||||
'published' => \__( 'Followed', 'activitypub' ),
|
||||
'modified' => \__( 'Last updated', 'activitypub' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function get_sortable_columns() {
|
||||
$sortable_columns = array(
|
||||
'post_title' => array( 'post_title', true ),
|
||||
'modified' => array( 'modified', false ),
|
||||
'published' => array( 'published', false ),
|
||||
);
|
||||
|
||||
return $sortable_columns;
|
||||
}
|
||||
|
||||
public function prepare_items() {
|
||||
$columns = $this->get_columns();
|
||||
$hidden = array();
|
||||
|
||||
$this->process_action();
|
||||
$this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() );
|
||||
|
||||
$page_num = $this->get_pagenum();
|
||||
$per_page = 20;
|
||||
|
||||
$args = array();
|
||||
|
||||
// phpcs:ignore 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'] ) );
|
||||
}
|
||||
}
|
||||
|
||||
$followers_with_count = FollowerCollection::get_followers_with_count( $this->user_id, $per_page, $page_num, $args );
|
||||
$followers = $followers_with_count['followers'];
|
||||
$counter = $followers_with_count['total'];
|
||||
|
||||
$this->items = array();
|
||||
$this->set_pagination_args(
|
||||
array(
|
||||
'total_items' => $counter,
|
||||
'total_pages' => ceil( $counter / $per_page ),
|
||||
'per_page' => $per_page,
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $followers as $follower ) {
|
||||
$item = array(
|
||||
'icon' => esc_attr( $follower->get_icon_url() ),
|
||||
'post_title' => esc_attr( $follower->get_name() ),
|
||||
'username' => esc_attr( $follower->get_preferred_username() ),
|
||||
'url' => esc_attr( object_to_uri( $follower->get_url() ) ),
|
||||
'identifier' => esc_attr( $follower->get_id() ),
|
||||
'published' => esc_attr( $follower->get_published() ),
|
||||
'modified' => esc_attr( $follower->get_updated() ),
|
||||
);
|
||||
|
||||
$this->items[] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
public function get_bulk_actions() {
|
||||
return array(
|
||||
'delete' => __( 'Delete', 'activitypub' ),
|
||||
);
|
||||
}
|
||||
|
||||
public function column_default( $item, $column_name ) {
|
||||
if ( ! array_key_exists( $column_name, $item ) ) {
|
||||
return __( 'None', 'activitypub' );
|
||||
}
|
||||
return $item[ $column_name ];
|
||||
}
|
||||
|
||||
public function column_avatar( $item ) {
|
||||
return sprintf(
|
||||
'<img src="%s" width="25px;" />',
|
||||
$item['icon']
|
||||
);
|
||||
}
|
||||
|
||||
public function column_url( $item ) {
|
||||
return sprintf(
|
||||
'<a href="%s" target="_blank">%s</a>',
|
||||
$item['url'],
|
||||
$item['url']
|
||||
);
|
||||
}
|
||||
|
||||
public function column_cb( $item ) {
|
||||
return sprintf( '<input type="checkbox" name="followers[]" value="%s" />', esc_attr( $item['identifier'] ) );
|
||||
}
|
||||
|
||||
public function process_action() {
|
||||
if ( ! isset( $_REQUEST['followers'] ) || ! isset( $_REQUEST['_wpnonce'] ) ) {
|
||||
return false;
|
||||
}
|
||||
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) );
|
||||
if ( ! wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! current_user_can( 'edit_user', $this->user_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$followers = $_REQUEST['followers']; // phpcs:ignore
|
||||
|
||||
switch ( $this->current_action() ) {
|
||||
case 'delete':
|
||||
if ( ! is_array( $followers ) ) {
|
||||
$followers = array( $followers );
|
||||
}
|
||||
foreach ( $followers as $follower ) {
|
||||
FollowerCollection::remove_follower( $this->user_id, $follower );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
public function get_user_count() {
|
||||
return FollowerCollection::count_followers( $this->user_id );
|
||||
}
|
||||
}
|
@ -0,0 +1,61 @@
|
||||
<?php
|
||||
namespace Activitypub\Transformer;
|
||||
|
||||
use Activitypub\Transformer\Post;
|
||||
|
||||
/**
|
||||
* WordPress Attachment Transformer
|
||||
*
|
||||
* The Attachment Transformer is responsible for transforming a WP_Post object into different other
|
||||
* Object-Types.
|
||||
*
|
||||
* Currently supported are:
|
||||
*
|
||||
* - Activitypub\Activity\Base_Object
|
||||
*/
|
||||
class Attachment extends Post {
|
||||
/**
|
||||
* Generates all Media Attachments for a Post.
|
||||
*
|
||||
* @return array The Attachments.
|
||||
*/
|
||||
protected function get_attachment() {
|
||||
$mime_type = get_post_mime_type( $this->wp_object->ID );
|
||||
$media_type = preg_replace( '/(\/[a-zA-Z]+)/i', '', $mime_type );
|
||||
|
||||
switch ( $media_type ) {
|
||||
case 'audio':
|
||||
case 'video':
|
||||
$type = 'Document';
|
||||
break;
|
||||
case 'image':
|
||||
$type = 'Image';
|
||||
break;
|
||||
}
|
||||
|
||||
$attachment = array(
|
||||
'type' => $type,
|
||||
'url' => wp_get_attachment_url( $this->wp_object->ID ),
|
||||
'mediaType' => $mime_type,
|
||||
);
|
||||
|
||||
$alt = \get_post_meta( $this->wp_object->ID, '_wp_attachment_image_alt', true );
|
||||
if ( $alt ) {
|
||||
$attachment['name'] = $alt;
|
||||
}
|
||||
|
||||
return $attachment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ActivityStreams 2.0 Object-Type for a Post based on the
|
||||
* settings and the Post-Type.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
|
||||
*
|
||||
* @return string The Object-Type.
|
||||
*/
|
||||
protected function get_type() {
|
||||
return 'Note';
|
||||
}
|
||||
}
|
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
namespace Activitypub\Transformer;
|
||||
|
||||
use WP_Error;
|
||||
use WP_Post;
|
||||
use WP_Comment;
|
||||
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Activity\Base_Object;
|
||||
|
||||
/**
|
||||
* WordPress Base Transformer
|
||||
*
|
||||
* Transformers are responsible for transforming a WordPress objects into different ActivityPub
|
||||
* Object-Types or Activities.
|
||||
*/
|
||||
abstract class Base {
|
||||
/**
|
||||
* The WP_Post or WP_Comment object.
|
||||
*
|
||||
* This is the source object of the transformer.
|
||||
*
|
||||
* @var WP_Post|WP_Comment
|
||||
*/
|
||||
protected $wp_object;
|
||||
|
||||
/**
|
||||
* Static function to Transform a WordPress Object.
|
||||
*
|
||||
* This helps to chain the output of the Transformer.
|
||||
*
|
||||
* @param WP_Post|WP_Comment $wp_object The WordPress object
|
||||
*
|
||||
* @return Base
|
||||
*/
|
||||
public static function transform( $object ) { // phpcs:ignore Universal.NamingConventions.NoReservedKeywordParameterNames.objectFound
|
||||
return new static( $object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Base constructor.
|
||||
*
|
||||
* @param WP_Post|WP_Comment $wp_object The WordPress object
|
||||
*/
|
||||
public function __construct( $wp_object ) {
|
||||
$this->wp_object = $wp_object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform all properties with available get(ter) functions.
|
||||
*
|
||||
* @param Base_Object|object $object
|
||||
*
|
||||
* @return Base_Object|object $object
|
||||
*/
|
||||
protected function transform_object_properties( $activitypub_object ) {
|
||||
$vars = $activitypub_object->get_object_var_keys();
|
||||
|
||||
foreach ( $vars as $var ) {
|
||||
$getter = 'get_' . $var;
|
||||
|
||||
if ( method_exists( $this, $getter ) ) {
|
||||
$value = call_user_func( array( $this, $getter ) );
|
||||
|
||||
if ( isset( $value ) ) {
|
||||
$setter = 'set_' . $var;
|
||||
|
||||
call_user_func( array( $activitypub_object, $setter ), $value );
|
||||
}
|
||||
}
|
||||
}
|
||||
return $activitypub_object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the WordPress Object into an ActivityPub Object.
|
||||
*
|
||||
* @return Activitypub\Activity\Base_Object
|
||||
*/
|
||||
public function to_object() {
|
||||
$activitypub_object = new Base_Object();
|
||||
$activitypub_object = $this->transform_object_properties( $activitypub_object );
|
||||
|
||||
return $activitypub_object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the ActivityPub Object to an Activity
|
||||
*
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return \Activitypub\Activity\Activity The Activity.
|
||||
*/
|
||||
public function to_activity( $type ) {
|
||||
$object = $this->to_object();
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->set_type( $type );
|
||||
|
||||
// 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
|
||||
if ( in_array( $type, array( 'Like', 'Announce' ), true ) ) {
|
||||
$activity->set_object( $object->get_id() );
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
abstract public function change_wp_user_id( $user_id );
|
||||
}
|
@ -0,0 +1,292 @@
|
||||
<?php
|
||||
namespace Activitypub\Transformer;
|
||||
|
||||
use WP_Comment;
|
||||
use WP_Comment_Query;
|
||||
|
||||
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
|
||||
*
|
||||
* The Comment Transformer is responsible for transforming a WP_Comment object into different
|
||||
* Object-Types.
|
||||
*
|
||||
* Currently supported are:
|
||||
*
|
||||
* - Activitypub\Activity\Base_Object
|
||||
*/
|
||||
class Comment extends Base {
|
||||
/**
|
||||
* Returns the User-ID of the WordPress Comment.
|
||||
*
|
||||
* @return int The User-ID of the WordPress Comment
|
||||
*/
|
||||
public function get_wp_user_id() {
|
||||
return $this->wp_object->user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the User-ID of the WordPress Comment.
|
||||
*
|
||||
* @return int The User-ID of the WordPress Comment
|
||||
*/
|
||||
public function change_wp_user_id( $user_id ) {
|
||||
$this->wp_object->user_id = $user_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the WP_Comment object to an ActivityPub Object
|
||||
*
|
||||
* @see \Activitypub\Activity\Base_Object
|
||||
*
|
||||
* @return \Activitypub\Activity\Base_Object The ActivityPub Object
|
||||
*/
|
||||
public function to_object() {
|
||||
$comment = $this->wp_object;
|
||||
$object = parent::to_object();
|
||||
|
||||
$object->set_url( $this->get_id() );
|
||||
$object->set_type( 'Note' );
|
||||
|
||||
$published = \strtotime( $comment->comment_date_gmt );
|
||||
$object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) );
|
||||
|
||||
$updated = \get_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', true );
|
||||
if ( $updated > $published ) {
|
||||
$object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) );
|
||||
}
|
||||
|
||||
$object->set_content_map(
|
||||
array(
|
||||
$this->get_locale() => $this->get_content(),
|
||||
)
|
||||
);
|
||||
$path = sprintf( 'actors/%d/followers', intval( $comment->comment_author ) );
|
||||
|
||||
$object->set_to(
|
||||
array(
|
||||
'https://www.w3.org/ns/activitystreams#Public',
|
||||
get_rest_url_by_path( $path ),
|
||||
)
|
||||
);
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the User-URL of the Author of the Post.
|
||||
*
|
||||
* If `single_user` mode is enabled, the URL of the Blog-User is returned.
|
||||
*
|
||||
* @return string The User-URL.
|
||||
*/
|
||||
protected function get_attributed_to() {
|
||||
if ( is_single_user() ) {
|
||||
$user = new Blog();
|
||||
return $user->get_url();
|
||||
}
|
||||
|
||||
return Users::get_by_id( $this->wp_object->user_id )->get_url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content for the ActivityPub Item.
|
||||
*
|
||||
* The content will be generated based on the user settings.
|
||||
*
|
||||
* @return string The content.
|
||||
*/
|
||||
protected function get_content() {
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
$comment = $this->wp_object;
|
||||
$content = $comment->comment_content;
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the in-reply-to for the ActivityPub Item.
|
||||
*
|
||||
* @return int 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::generate_id( $parent_comment );
|
||||
}
|
||||
} else {
|
||||
$in_reply_to = \get_permalink( $comment->comment_post_ID );
|
||||
}
|
||||
|
||||
return $in_reply_to;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ID of the ActivityPub Object.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#obj-id
|
||||
* @see https://github.com/tootsuite/mastodon/issues/13879
|
||||
*
|
||||
* @return string ActivityPub URI for comment
|
||||
*/
|
||||
protected function get_id() {
|
||||
$comment = $this->wp_object;
|
||||
return Comment_Utils::generate_id( $comment );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of Mentions, used in the Comment.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#Mention
|
||||
*
|
||||
* @return array The list of Mentions.
|
||||
*/
|
||||
protected function get_cc() {
|
||||
$cc = array();
|
||||
|
||||
$mentions = $this->get_mentions();
|
||||
if ( $mentions ) {
|
||||
foreach ( $mentions as $url ) {
|
||||
$cc[] = $url;
|
||||
}
|
||||
}
|
||||
|
||||
return array_unique( $cc );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of Tags, used in the Comment.
|
||||
*
|
||||
* This includes Hash-Tags and Mentions.
|
||||
*
|
||||
* @return array The list of Tags.
|
||||
*/
|
||||
protected function get_tag() {
|
||||
$tags = array();
|
||||
|
||||
$mentions = $this->get_mentions();
|
||||
if ( $mentions ) {
|
||||
foreach ( $mentions as $mention => $url ) {
|
||||
$tag = array(
|
||||
'type' => 'Mention',
|
||||
'href' => \esc_url( $url ),
|
||||
'name' => \esc_html( $mention ),
|
||||
);
|
||||
$tags[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
return \array_unique( $tags, SORT_REGULAR );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the @-Mentions from the comment content.
|
||||
*
|
||||
* @return array The list of @-Mentions.
|
||||
*/
|
||||
protected function get_mentions() {
|
||||
\add_filter( 'activitypub_extract_mentions', array( $this, 'extract_reply_context' ) );
|
||||
|
||||
return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_object->comment_content, $this->wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the ancestors of the comment, but only the ones that are ActivityPub comments.
|
||||
*
|
||||
* @return array The list of ancestors.
|
||||
*/
|
||||
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
|
||||
return array_filter(
|
||||
$ancestors,
|
||||
function ( $comment_id ) {
|
||||
return \get_comment_meta( $comment_id, 'protocol', true ) === 'activitypub';
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*
|
||||
* @return array The list of all Repliers.
|
||||
*/
|
||||
public function extract_reply_context( $mentions ) {
|
||||
// Check if `$this->wp_object` is a WP_Comment
|
||||
if ( 'WP_Comment' !== get_class( $this->wp_object ) ) {
|
||||
return $mentions;
|
||||
}
|
||||
|
||||
$ancestors = $this->get_comment_ancestors();
|
||||
if ( ! $ancestors ) {
|
||||
return $mentions;
|
||||
}
|
||||
|
||||
foreach ( $ancestors as $comment_id ) {
|
||||
$comment = \get_comment( $comment_id );
|
||||
if ( $comment && ! empty( $comment->comment_author_url ) ) {
|
||||
$acct = Webfinger::uri_to_acct( $comment->comment_author_url );
|
||||
if ( $acct && ! is_wp_error( $acct ) ) {
|
||||
$acct = str_replace( 'acct:', '@', $acct );
|
||||
$mentions[ $acct ] = $comment->comment_author_url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $mentions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale of the post.
|
||||
*
|
||||
* @return string The locale of the post.
|
||||
*/
|
||||
public function get_locale() {
|
||||
$comment_id = $this->wp_object->ID;
|
||||
$lang = \strtolower( \strtok( \get_locale(), '_-' ) );
|
||||
|
||||
/**
|
||||
* Filter the locale of the comment.
|
||||
*
|
||||
* @param string $lang The locale of the comment.
|
||||
* @param int $comment_id The comment ID.
|
||||
* @param WP_Post $post The comment object.
|
||||
*
|
||||
* @return string The filtered locale of the comment.
|
||||
*/
|
||||
return apply_filters( 'activitypub_comment_locale', $lang, $comment_id, $this->wp_object );
|
||||
}
|
||||
}
|
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
namespace Activitypub\Transformer;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Transformer\Base;
|
||||
use Activitypub\Transformer\Post;
|
||||
use Activitypub\Transformer\Comment;
|
||||
use Activitypub\Transformer\Attachment;
|
||||
|
||||
/**
|
||||
* Transformer Factory
|
||||
*/
|
||||
class Factory {
|
||||
/**
|
||||
* @param mixed $object The object to transform
|
||||
* @return \Activitypub\Transformer|\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 ) ) {
|
||||
return new WP_Error( 'invalid_object', __( 'Invalid object', 'activitypub' ) );
|
||||
}
|
||||
|
||||
$class = \get_class( $object );
|
||||
|
||||
/**
|
||||
* Filter the transformer for a given object.
|
||||
*
|
||||
* Add your own transformer based on the object class or the object type.
|
||||
*
|
||||
* Example usage:
|
||||
*
|
||||
* // Filter be object class
|
||||
* add_filter( 'activitypub_transformer', function( $transformer, $object, $object_class ) {
|
||||
* if ( $object_class === 'WP_Post' ) {
|
||||
* return new My_Post_Transformer( $object );
|
||||
* }
|
||||
* return $transformer;
|
||||
* }, 10, 3 );
|
||||
*
|
||||
* // Filter be object type
|
||||
* add_filter( 'activitypub_transformer', function( $transformer, $object, $object_class ) {
|
||||
* if ( $object->post_type === 'event' ) {
|
||||
* return new My_Event_Transformer( $object );
|
||||
* }
|
||||
* return $transformer;
|
||||
* }, 10, 3 );
|
||||
*
|
||||
* @param Base $transformer The transformer to use.
|
||||
* @param mixed $object 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 );
|
||||
|
||||
if ( $transformer ) {
|
||||
if (
|
||||
! \is_object( $transformer ) ||
|
||||
! $transformer instanceof Base
|
||||
) {
|
||||
return new WP_Error( 'invalid_transformer', __( 'Invalid transformer', 'activitypub' ) );
|
||||
}
|
||||
|
||||
return $transformer;
|
||||
}
|
||||
|
||||
// use default transformer
|
||||
switch ( $class ) {
|
||||
case 'WP_Post':
|
||||
if ( 'attachment' === $object->post_type ) {
|
||||
return new Attachment( $object );
|
||||
}
|
||||
return new Post( $object );
|
||||
case 'WP_Comment':
|
||||
return new Comment( $object );
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,881 @@
|
||||
<?php
|
||||
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;
|
||||
|
||||
/**
|
||||
* WordPress Post Transformer
|
||||
*
|
||||
* The Post Transformer is responsible for transforming a WP_Post object into different other
|
||||
* Object-Types.
|
||||
*
|
||||
* Currently supported are:
|
||||
*
|
||||
* - Activitypub\Activity\Base_Object
|
||||
*/
|
||||
class Post extends Base {
|
||||
/**
|
||||
* Returns the ID of the WordPress Post.
|
||||
*
|
||||
* @return int The ID of the WordPress Post
|
||||
*/
|
||||
public function get_wp_user_id() {
|
||||
return $this->wp_object->post_author;
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the User-ID of the WordPress Post.
|
||||
*
|
||||
* @return int The User-ID of the WordPress Post
|
||||
*/
|
||||
public function change_wp_user_id( $user_id ) {
|
||||
$this->wp_object->post_author = $user_id;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the WP_Post object to an ActivityPub Object
|
||||
*
|
||||
* @see \Activitypub\Activity\Base_Object
|
||||
*
|
||||
* @return \Activitypub\Activity\Base_Object The ActivityPub Object
|
||||
*/
|
||||
public function to_object() {
|
||||
$post = $this->wp_object;
|
||||
$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 ) );
|
||||
}
|
||||
|
||||
$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 ID of the Post.
|
||||
*
|
||||
* @return string The Posts ID.
|
||||
*/
|
||||
public function get_id() {
|
||||
return $this->get_url();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the Post.
|
||||
*
|
||||
* @return string The Posts URL.
|
||||
*/
|
||||
public function get_url() {
|
||||
$post = $this->wp_object;
|
||||
|
||||
if ( 'trash' === get_post_status( $post ) ) {
|
||||
$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 {
|
||||
$permalink = \get_permalink( $post );
|
||||
}
|
||||
|
||||
return \esc_url( $permalink );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the User-URL of the Author of the Post.
|
||||
*
|
||||
* If `single_user` mode is enabled, the URL of the Blog-User is returned.
|
||||
*
|
||||
* @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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates all Media Attachments for a Post.
|
||||
*
|
||||
* @return array The Attachments.
|
||||
*/
|
||||
protected function get_attachment() {
|
||||
// Remove attachments from drafts.
|
||||
if ( 'draft' === \get_post_status( $this->wp_object ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
// Once upon a time we only supported images, but we now support audio/video as well.
|
||||
// We maintain the image-centric naming for backwards compatibility.
|
||||
$max_media = \intval(
|
||||
\apply_filters(
|
||||
'activitypub_max_image_attachments',
|
||||
\get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS )
|
||||
)
|
||||
);
|
||||
|
||||
$media = array(
|
||||
'audio' => array(),
|
||||
'video' => array(),
|
||||
'image' => array(),
|
||||
);
|
||||
$id = $this->wp_object->ID;
|
||||
|
||||
// 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 ) );
|
||||
}
|
||||
|
||||
$media = $this->get_enclosures( $media );
|
||||
|
||||
if ( site_supports_blocks() && \has_blocks( $this->wp_object->post_content ) ) {
|
||||
$media = $this->get_block_attachments( $media, $max_media );
|
||||
} else {
|
||||
$media = $this->get_classic_editor_images( $media, $max_media );
|
||||
}
|
||||
|
||||
$media = self::filter_media_by_object_type( $media, \get_post_format( $this->wp_object ), $this->wp_object );
|
||||
$unique_ids = \array_unique( \array_column( $media, 'id' ) );
|
||||
$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.
|
||||
*
|
||||
* @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 );
|
||||
$media = self::get_media_from_blocks( $blocks, $media );
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get enclosures for a post.
|
||||
*
|
||||
* @param array $media The media array grouped by type.
|
||||
*
|
||||
* @return array The media array extended with enclosures.
|
||||
*/
|
||||
public function get_enclosures( $media ) {
|
||||
$enclosures = get_enclosures( $this->wp_object->ID );
|
||||
|
||||
if ( ! $enclosures ) {
|
||||
return $media;
|
||||
}
|
||||
|
||||
foreach ( $enclosures as $enclosure ) {
|
||||
// check if URL is an attachment
|
||||
$attachment_id = \attachment_url_to_postid( $enclosure['url'] );
|
||||
if ( $attachment_id ) {
|
||||
$enclosure['id'] = $attachment_id;
|
||||
$enclosure['url'] = \wp_get_attachment_url( $attachment_id );
|
||||
$enclosure['mediaType'] = \get_post_mime_type( $attachment_id );
|
||||
}
|
||||
|
||||
$mime_type = $enclosure['mediaType'];
|
||||
$mime_type_parts = \explode( '/', $mime_type );
|
||||
|
||||
switch ( $mime_type_parts[0] ) {
|
||||
case 'image':
|
||||
$media['image'][] = $enclosure;
|
||||
break;
|
||||
case 'audio':
|
||||
$media['audio'][] = $enclosure;
|
||||
break;
|
||||
case 'video':
|
||||
$media['video'][] = $enclosure;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $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.
|
||||
*
|
||||
* @return array The image IDs.
|
||||
*/
|
||||
protected static function get_media_from_blocks( $blocks, $media ) {
|
||||
foreach ( $blocks as $block ) {
|
||||
// recurse into inner blocks
|
||||
if ( ! empty( $block['innerBlocks'] ) ) {
|
||||
$media = self::get_media_from_blocks( $block['innerBlocks'], $media );
|
||||
}
|
||||
|
||||
switch ( $block['blockName'] ) {
|
||||
case 'core/image':
|
||||
case 'core/cover':
|
||||
if ( ! empty( $block['attrs']['id'] ) ) {
|
||||
$alt = '';
|
||||
$check = preg_match( '/<img.*?alt\s*=\s*([\"\'])(.*?)\1.*>/i', $block['innerHTML'], $match );
|
||||
|
||||
if ( $check ) {
|
||||
$alt = $match[2];
|
||||
}
|
||||
|
||||
$media['image'][] = array(
|
||||
'id' => $block['attrs']['id'],
|
||||
'alt' => $alt,
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'core/audio':
|
||||
if ( ! empty( $block['attrs']['id'] ) ) {
|
||||
$media['audio'][] = array( 'id' => $block['attrs']['id'] );
|
||||
}
|
||||
break;
|
||||
case 'core/video':
|
||||
case 'videopress/video':
|
||||
if ( ! empty( $block['attrs']['id'] ) ) {
|
||||
$media['video'][] = array( 'id' => $block['attrs']['id'] );
|
||||
}
|
||||
break;
|
||||
case 'jetpack/slideshow':
|
||||
case 'jetpack/tiled-gallery':
|
||||
if ( ! empty( $block['attrs']['ids'] ) ) {
|
||||
$media['image'] = array_merge(
|
||||
$media['image'],
|
||||
array_map(
|
||||
function ( $id ) {
|
||||
return array( 'id' => $id );
|
||||
},
|
||||
$block['attrs']['ids']
|
||||
)
|
||||
);
|
||||
}
|
||||
break;
|
||||
case 'jetpack/image-compare':
|
||||
if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
|
||||
$media['image'][] = array( 'id' => $block['attrs']['beforeImageId'] );
|
||||
}
|
||||
if ( ! empty( $block['attrs']['afterImageId'] ) ) {
|
||||
$media['image'][] = array( 'id' => $block['attrs']['afterImageId'] );
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter media IDs by object type.
|
||||
*
|
||||
* @param array $media The media array grouped by type.
|
||||
* @param string $type The object type.
|
||||
*
|
||||
* @return array The filtered media IDs.
|
||||
*/
|
||||
protected static function filter_media_by_object_type( $media, $type, $wp_object ) {
|
||||
$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 ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a WordPress Attachment to an ActivityPub Attachment.
|
||||
*
|
||||
* @param array $media The Attachment array.
|
||||
*
|
||||
* @return array The ActivityPub Attachment.
|
||||
*/
|
||||
public static function wp_attachment_to_activity_attachment( $media ) {
|
||||
if ( ! isset( $media['id'] ) ) {
|
||||
return $media;
|
||||
}
|
||||
|
||||
$id = $media['id'];
|
||||
$attachment = array();
|
||||
$mime_type = \get_post_mime_type( $id );
|
||||
$mime_type_parts = \explode( '/', $mime_type );
|
||||
// switching on image/audio/video
|
||||
switch ( $mime_type_parts[0] ) {
|
||||
case 'image':
|
||||
$image_size = 'large';
|
||||
|
||||
/**
|
||||
* Filter the image URL returned for each post.
|
||||
*
|
||||
* @param array|false $thumbnail The image URL, or false if no image is available.
|
||||
* @param int $id The attachment ID.
|
||||
* @param string $image_size The image size to retrieve. Set to 'large' by default.
|
||||
*/
|
||||
$thumbnail = apply_filters(
|
||||
'activitypub_get_image',
|
||||
self::get_wordpress_attachment( $id, $image_size ),
|
||||
$id,
|
||||
$image_size
|
||||
);
|
||||
|
||||
if ( $thumbnail ) {
|
||||
$image = array(
|
||||
'type' => 'Image',
|
||||
'url' => \esc_url( $thumbnail[0] ),
|
||||
'mediaType' => \esc_attr( $mime_type ),
|
||||
);
|
||||
|
||||
if ( ! empty( $media['alt'] ) ) {
|
||||
$image['name'] = \wp_strip_all_tags( \html_entity_decode( $media['alt'] ) );
|
||||
} else {
|
||||
$alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
|
||||
if ( $alt ) {
|
||||
$image['name'] = \wp_strip_all_tags( \html_entity_decode( $alt ) );
|
||||
}
|
||||
}
|
||||
|
||||
$attachment = $image;
|
||||
}
|
||||
break;
|
||||
|
||||
case 'audio':
|
||||
case 'video':
|
||||
$attachment = array(
|
||||
'type' => 'Document',
|
||||
'mediaType' => \esc_attr( $mime_type ),
|
||||
'url' => \esc_url( \wp_get_attachment_url( $id ) ),
|
||||
'name' => \esc_attr( \get_the_title( $id ) ),
|
||||
);
|
||||
$meta = wp_get_attachment_metadata( $id );
|
||||
// height and width for videos
|
||||
if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) {
|
||||
$attachment['width'] = \esc_attr( $meta['width'] );
|
||||
$attachment['height'] = \esc_attr( $meta['height'] );
|
||||
}
|
||||
// @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail?
|
||||
break;
|
||||
}
|
||||
|
||||
return \apply_filters( 'activitypub_attachment', $attachment, $id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return details about an image attachment.
|
||||
*
|
||||
* @param int $id The attachment ID.
|
||||
* @param string $image_size The image size to retrieve. Set to 'large' by default.
|
||||
*
|
||||
* @return array|false Array of image data, or boolean false if no image is available.
|
||||
*/
|
||||
protected static function get_wordpress_attachment( $id, $image_size = 'large' ) {
|
||||
/**
|
||||
* Hook into the image retrieval process. Before image retrieval.
|
||||
*
|
||||
* @param int $id The attachment ID.
|
||||
* @param string $image_size The image size to retrieve. Set to 'large' by default.
|
||||
*/
|
||||
do_action( 'activitypub_get_image_pre', $id, $image_size );
|
||||
|
||||
$image = \wp_get_attachment_image_src( $id, $image_size );
|
||||
|
||||
/**
|
||||
* Hook into the image retrieval process. After image retrieval.
|
||||
*
|
||||
* @param int $id The attachment ID.
|
||||
* @param string $image_size The image size to retrieve. Set to 'large' by default.
|
||||
*/
|
||||
do_action( 'activitypub_get_image_post', $id, $image_size );
|
||||
|
||||
return $image;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ActivityStreams 2.0 Object-Type for a Post based on the
|
||||
* settings and the Post-Type.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
|
||||
*
|
||||
* @return string The Object-Type.
|
||||
*/
|
||||
protected function get_type() {
|
||||
$post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
|
||||
|
||||
if ( 'wordpress-post-format' !== $post_format_setting ) {
|
||||
return \ucfirst( $post_format_setting );
|
||||
}
|
||||
|
||||
$has_title = post_type_supports( $this->wp_object->post_type, 'title' );
|
||||
|
||||
if ( ! $has_title ) {
|
||||
return 'Note';
|
||||
}
|
||||
|
||||
// Default to Article.
|
||||
$object_type = 'Note';
|
||||
$post_format = 'standard';
|
||||
|
||||
if ( \get_theme_support( 'post-formats' ) ) {
|
||||
$post_format = \get_post_format( $this->wp_object );
|
||||
}
|
||||
|
||||
$post_type = \get_post_type( $this->wp_object );
|
||||
switch ( $post_type ) {
|
||||
case 'post':
|
||||
switch ( $post_format ) {
|
||||
case 'standard':
|
||||
case '':
|
||||
$object_type = 'Article';
|
||||
break;
|
||||
default:
|
||||
$object_type = 'Note';
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'page':
|
||||
$object_type = 'Page';
|
||||
break;
|
||||
default:
|
||||
$object_type = 'Note';
|
||||
break;
|
||||
}
|
||||
|
||||
return $object_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of Mentions, used in the Post.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#Mention
|
||||
*
|
||||
* @return array The list of Mentions.
|
||||
*/
|
||||
protected function get_cc() {
|
||||
$cc = array();
|
||||
|
||||
$mentions = $this->get_mentions();
|
||||
if ( $mentions ) {
|
||||
foreach ( $mentions as $url ) {
|
||||
$cc[] = $url;
|
||||
}
|
||||
}
|
||||
|
||||
return $cc;
|
||||
}
|
||||
|
||||
|
||||
public function get_audience() {
|
||||
if ( is_single_user() ) {
|
||||
return null;
|
||||
} else {
|
||||
$blog = new Blog();
|
||||
return $blog->get_id();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of Tags, used in the Post.
|
||||
*
|
||||
* This includes Hash-Tags and Mentions.
|
||||
*
|
||||
* @return array The list of Tags.
|
||||
*/
|
||||
protected function get_tag() {
|
||||
$tags = array();
|
||||
|
||||
$post_tags = \get_the_tags( $this->wp_object->ID );
|
||||
if ( $post_tags ) {
|
||||
foreach ( $post_tags as $post_tag ) {
|
||||
$tag = array(
|
||||
'type' => 'Hashtag',
|
||||
'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
|
||||
'name' => esc_hashtag( $post_tag->name ),
|
||||
);
|
||||
$tags[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
$mentions = $this->get_mentions();
|
||||
if ( $mentions ) {
|
||||
foreach ( $mentions as $mention => $url ) {
|
||||
$tag = array(
|
||||
'type' => 'Mention',
|
||||
'href' => \esc_url( $url ),
|
||||
'name' => \esc_html( $mention ),
|
||||
);
|
||||
$tags[] = $tag;
|
||||
}
|
||||
}
|
||||
|
||||
return $tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the summary for the ActivityPub Item.
|
||||
*
|
||||
* The summary will be generated based on the user settings and only if the
|
||||
* object type is not set to `note`.
|
||||
*
|
||||
* @return string|null The summary or null if the object type is `note`.
|
||||
*/
|
||||
protected function get_summary() {
|
||||
if ( 'Note' === $this->get_type() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Remove Teaser from drafts.
|
||||
if ( 'draft' === \get_post_status( $this->wp_object ) ) {
|
||||
return \__( '(This post is being modified)', 'activitypub' );
|
||||
}
|
||||
|
||||
$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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title for the ActivityPub Item.
|
||||
*
|
||||
* The title will be generated based on the user settings and only if the
|
||||
* object type is not set to `note`.
|
||||
*
|
||||
* @return string|null The title or null if the object type is `note`.
|
||||
*/
|
||||
protected function get_name() {
|
||||
if ( 'Note' === $this->get_type() ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$title = \get_the_title( $this->wp_object->ID );
|
||||
|
||||
if ( $title ) {
|
||||
return \wp_strip_all_tags(
|
||||
\html_entity_decode(
|
||||
$title
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content for the ActivityPub Item.
|
||||
*
|
||||
* The content will be generated based on the user settings.
|
||||
*
|
||||
* @return string The content.
|
||||
*/
|
||||
protected function get_content() {
|
||||
// Remove Content from drafts.
|
||||
if ( 'draft' === \get_post_status( $this->wp_object ) ) {
|
||||
return \__( '(This post is being modified)', 'activitypub' );
|
||||
}
|
||||
|
||||
global $post;
|
||||
|
||||
/**
|
||||
* Provides an action hook so plugins can add their own hooks/filters before AP content is generated.
|
||||
*
|
||||
* Example: if a plugin adds a filter to `the_content` to add a button to the end of posts, it can also remove that filter here.
|
||||
*
|
||||
* @param WP_Post $post The post object.
|
||||
*/
|
||||
do_action( 'activitypub_before_get_content', $post );
|
||||
|
||||
add_filter( 'render_block_core/embed', array( self::class, 'revert_embed_links' ), 10, 2 );
|
||||
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
$post = $this->wp_object;
|
||||
$content = $this->get_post_content_template();
|
||||
|
||||
// Register our shortcodes just in time.
|
||||
Shortcodes::register();
|
||||
// Fill in the shortcodes.
|
||||
setup_postdata( $post );
|
||||
$content = do_shortcode( $content );
|
||||
wp_reset_postdata();
|
||||
|
||||
$content = \wpautop( $content );
|
||||
$content = \preg_replace( '/[\n\r\t]/', '', $content );
|
||||
$content = \trim( $content );
|
||||
|
||||
$content = \apply_filters( 'activitypub_the_content', $content, $post );
|
||||
|
||||
// Don't need these any more, should never appear in a post.
|
||||
Shortcodes::unregister();
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the template to use to generate the content of the activitypub item.
|
||||
*
|
||||
* @return string The Template.
|
||||
*/
|
||||
protected function get_post_content_template() {
|
||||
$type = \get_option( 'activitypub_post_content_type', 'content' );
|
||||
|
||||
switch ( $type ) {
|
||||
case 'excerpt':
|
||||
$template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
|
||||
break;
|
||||
case 'title':
|
||||
$template = "<h2>[ap_title]</h2>\n\n[ap_permalink type=\"html\"]";
|
||||
break;
|
||||
case 'content':
|
||||
$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 );
|
||||
break;
|
||||
}
|
||||
|
||||
$post_format_setting = \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE );
|
||||
|
||||
if ( 'wordpress-post-format' === $post_format_setting ) {
|
||||
$template = '[ap_content]';
|
||||
}
|
||||
|
||||
return apply_filters( 'activitypub_object_content_template', $template, $this->wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper function to get the @-Mentions from the post content.
|
||||
*
|
||||
* @return array The list of @-Mentions.
|
||||
*/
|
||||
protected function get_mentions() {
|
||||
return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_object->post_content, $this->wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the locale of the post.
|
||||
*
|
||||
* @return string The locale of the post.
|
||||
*/
|
||||
public function get_locale() {
|
||||
$post_id = $this->wp_object->ID;
|
||||
$lang = \strtolower( \strtok( \get_locale(), '_-' ) );
|
||||
|
||||
/**
|
||||
* Filter the locale of the post.
|
||||
*
|
||||
* @param string $lang The locale of the post.
|
||||
* @param int $post_id The post ID.
|
||||
* @param WP_Post $post The post object.
|
||||
*
|
||||
* @return string The filtered locale of the post.
|
||||
*/
|
||||
return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform Embed blocks to block level link.
|
||||
*
|
||||
* Remote servers will simply drop iframe elements, rendering incomplete content.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#security-sanitizing-content
|
||||
* @see https://www.w3.org/wiki/ActivityPub/Primer/HTML
|
||||
*
|
||||
* @param string $block_content The block content (html)
|
||||
* @param object $block The block object
|
||||
*
|
||||
* @return string A block level link
|
||||
*/
|
||||
public static function revert_embed_links( $block_content, $block ) {
|
||||
return '<p><a href="' . esc_url( $block['attrs']['url'] ) . '">' . $block['attrs']['url'] . '</a></p>';
|
||||
}
|
||||
}
|
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
/**
|
||||
* Compatibility with the BuddyPress plugin
|
||||
*
|
||||
* @see https://buddypress.org/
|
||||
*/
|
||||
class Buddypress {
|
||||
/**
|
||||
* 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' 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(
|
||||
'type' => 'Image',
|
||||
'url' => $cover_image_url,
|
||||
);
|
||||
}
|
||||
|
||||
// change profile URL to BuddyPress' profile URL
|
||||
$object->attachment['profile_url'] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => \__( 'Profile', 'activitypub' ),
|
||||
'value' => \html_entity_decode(
|
||||
sprintf(
|
||||
'<a rel="me" title="%s" target="_blank" href="%s">%s</a>',
|
||||
\esc_attr( bp_core_get_user_domain( $author_id ) ),
|
||||
\bp_core_get_user_domain( $author_id ),
|
||||
\wp_parse_url( \bp_core_get_user_domain( $author_id ), \PHP_URL_HOST )
|
||||
),
|
||||
\ENT_QUOTES,
|
||||
'UTF-8'
|
||||
),
|
||||
);
|
||||
|
||||
// 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
|
||||
|
||||
if ( ! empty( $user_blogs ) ) {
|
||||
unset( $object->attachment['blog_url'] );
|
||||
|
||||
foreach ( $user_blogs as $blog ) {
|
||||
if ( 1 !== $blog->userblog_id ) {
|
||||
$object->attachment[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => $blog->blogname,
|
||||
'value' => \html_entity_decode(
|
||||
sprintf(
|
||||
'<a rel="me" title="%s" target="_blank" href="%s">%s</a>',
|
||||
\esc_attr( $blog->siteurl ),
|
||||
$blog->siteurl,
|
||||
\wp_parse_url( $blog->siteurl, \PHP_URL_HOST )
|
||||
),
|
||||
\ENT_QUOTES,
|
||||
'UTF-8'
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
}
|
@ -0,0 +1,468 @@
|
||||
<?php
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use DateTime;
|
||||
use Activitypub\Webfinger as Webfinger_Util;
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Integration\Nodeinfo;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Class Enable_Mastodon_Apps
|
||||
*
|
||||
* 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
|
||||
*/
|
||||
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_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 followers to Mastodon API
|
||||
*
|
||||
* @param array $followers An array of followers
|
||||
* @param string $user_id The user id
|
||||
* @param WP_REST_Request $request The request object
|
||||
*
|
||||
* @return array The filtered followers
|
||||
*/
|
||||
public static function api_account_followers( $followers, $user_id ) {
|
||||
$activitypub_followers = Followers::get_followers( $user_id, 40 );
|
||||
$mastodon_followers = array_map(
|
||||
function ( $item ) {
|
||||
$acct = Webfinger_Util::uri_to_acct( $item->get_id() );
|
||||
|
||||
if ( $acct && ! is_wp_error( $acct ) ) {
|
||||
$acct = \str_replace( 'acct:', '', $acct );
|
||||
} else {
|
||||
$acct = $item->get_url();
|
||||
}
|
||||
|
||||
$account = new Account();
|
||||
$account->id = \strval( $item->get__id() );
|
||||
$account->username = $item->get_preferred_username();
|
||||
$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() );
|
||||
$account->last_status_at = new DateTime( $item->get_published() );
|
||||
$account->note = $item->get_summary();
|
||||
$account->header = $item->get_image_url();
|
||||
$account->header_static = $item->get_image_url();
|
||||
$account->followers_count = 0;
|
||||
$account->following_count = 0;
|
||||
$account->statuses_count = 0;
|
||||
$account->bot = false;
|
||||
$account->locked = false;
|
||||
$account->group = false;
|
||||
$account->discoversable = false;
|
||||
$account->indexable = false;
|
||||
$account->hide_collections = 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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve external accounts for Mastodon API
|
||||
*
|
||||
* @param Enable_Mastodon_Apps\Entity\Account $user_data The user data
|
||||
* @param string $user_id The user id
|
||||
*
|
||||
* @return Enable_Mastodon_Apps\Entity\Account The filtered Account
|
||||
*/
|
||||
public static function api_account_external( $user_data, $user_id ) {
|
||||
if ( $user_data || ( is_numeric( $user_id ) && $user_id ) ) {
|
||||
// Only augment.
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
$user = Users::get_by_various( $user_id );
|
||||
|
||||
if ( $user && ! is_wp_error( $user ) ) {
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
$uri = Webfinger_Util::resolve( $user_id );
|
||||
|
||||
if ( ! $uri || is_wp_error( $uri ) ) {
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
$account = self::get_account_for_actor( $uri );
|
||||
if ( $account ) {
|
||||
return $account;
|
||||
}
|
||||
|
||||
return $user_data;
|
||||
}
|
||||
|
||||
private static function get_account_for_actor( $uri ) {
|
||||
if ( ! is_string( $uri ) ) {
|
||||
return null;
|
||||
}
|
||||
$data = get_remote_metadata_by_actor( $uri );
|
||||
|
||||
if ( ! $data || is_wp_error( $data ) ) {
|
||||
return null;
|
||||
}
|
||||
$account = new Account();
|
||||
|
||||
$acct = Webfinger_Util::uri_to_acct( $uri );
|
||||
if ( str_starts_with( $acct, 'acct:' ) ) {
|
||||
$acct = substr( $acct, 5 );
|
||||
}
|
||||
|
||||
$account->id = $acct;
|
||||
$account->username = $acct;
|
||||
$account->acct = $acct;
|
||||
$account->display_name = $data['name'];
|
||||
$account->url = $uri;
|
||||
|
||||
if ( ! empty( $data['summary'] ) ) {
|
||||
$account->note = $data['summary'];
|
||||
}
|
||||
|
||||
if (
|
||||
isset( $data['icon']['type'] ) &&
|
||||
isset( $data['icon']['url'] ) &&
|
||||
'Image' === $data['icon']['type']
|
||||
) {
|
||||
$account->avatar = $data['icon']['url'];
|
||||
$account->avatar_static = $data['icon']['url'];
|
||||
}
|
||||
|
||||
if ( isset( $data['image'] ) ) {
|
||||
$account->header = $data['image']['url'];
|
||||
$account->header_static = $data['image']['url'];
|
||||
}
|
||||
if ( ! isset( $data['published'] ) ) {
|
||||
$data['published'] = 'now';
|
||||
}
|
||||
$account->created_at = new DateTime( $data['published'] );
|
||||
|
||||
return $account;
|
||||
}
|
||||
|
||||
public static function api_search_by_url( $search_data, $request ) {
|
||||
$p = \wp_parse_url( $request->get_param( 'q' ) );
|
||||
if ( ! $p || ! isset( $p['host'] ) ) {
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
$object = Http::get_remote_object( $request->get_param( 'q' ), true );
|
||||
if ( is_wp_error( $object ) || ! isset( $object['attributedTo'] ) ) {
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
$account = self::get_account_for_actor( $object['attributedTo'] );
|
||||
if ( ! $account ) {
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
$status = self::activity_to_status( $object, $account );
|
||||
if ( $status ) {
|
||||
$search_data['statuses'][] = $status;
|
||||
}
|
||||
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
public static function api_search( $search_data, $request ) {
|
||||
$user_id = \get_current_user_id();
|
||||
if ( ! $user_id ) {
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
$q = $request->get_param( 'q' );
|
||||
if ( ! $q ) {
|
||||
return $search_data;
|
||||
}
|
||||
$q = sanitize_text_field( wp_unslash( $q ) );
|
||||
|
||||
$followers = Followers::get_followers( $user_id, 40, null, array( 's' => $q ) );
|
||||
if ( ! $followers ) {
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
foreach ( $followers as $follower ) {
|
||||
$acct = Webfinger_Util::uri_to_acct( $follower->get_id() );
|
||||
|
||||
if ( $acct && ! is_wp_error( $acct ) ) {
|
||||
$acct = \str_replace( 'acct:', '', $acct );
|
||||
} else {
|
||||
$acct = $follower->get_url();
|
||||
}
|
||||
|
||||
$account = new Account();
|
||||
$account->id = \strval( $follower->get__id() );
|
||||
$account->username = $follower->get_preferred_username();
|
||||
$account->acct = $acct;
|
||||
$account->display_name = $follower->get_name();
|
||||
$account->url = $follower->get_url();
|
||||
$account->uri = $follower->get_id();
|
||||
$account->avatar = $follower->get_icon_url();
|
||||
$account->avatar_static = $follower->get_icon_url();
|
||||
$account->created_at = new DateTime( $follower->get_published() );
|
||||
$account->last_status_at = new DateTime( $follower->get_published() );
|
||||
$account->note = $follower->get_summary();
|
||||
$account->header = $follower->get_image_url();
|
||||
$account->header_static = $follower->get_image_url();
|
||||
|
||||
$search_data['accounts'][] = $account;
|
||||
}
|
||||
|
||||
return $search_data;
|
||||
}
|
||||
|
||||
public static function api_get_posts_query_args( $args ) {
|
||||
if ( isset( $args['author'] ) && is_string( $args['author'] ) ) {
|
||||
$uri = Webfinger_Util::resolve( $args['author'] );
|
||||
if ( $uri && ! is_wp_error( $uri ) ) {
|
||||
$args['activitypub'] = $uri;
|
||||
unset( $args['author'] );
|
||||
}
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
private static function activity_to_status( $item, $account ) {
|
||||
if ( isset( $item['object'] ) ) {
|
||||
$object = $item['object'];
|
||||
} else {
|
||||
$object = $item;
|
||||
}
|
||||
|
||||
if ( ! isset( $object['type'] ) || 'Note' !== $object['type'] ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$status = new Status();
|
||||
$status->id = $object['id'];
|
||||
$status->created_at = new DateTime( $object['published'] );
|
||||
$status->content = $object['content'];
|
||||
$status->account = $account;
|
||||
|
||||
if ( ! empty( $object['inReplyTo'] ) ) {
|
||||
$status->in_reply_to_id = $object['inReplyTo'];
|
||||
}
|
||||
|
||||
if ( ! empty( $object['visibility'] ) ) {
|
||||
$status->visibility = $object['visibility'];
|
||||
}
|
||||
if ( ! empty( $object['url'] ) ) {
|
||||
$status->url = $object['url'];
|
||||
$status->uri = $object['url'];
|
||||
} else {
|
||||
$status->uri = $object['id'];
|
||||
}
|
||||
|
||||
if ( ! empty( $object['attachment'] ) ) {
|
||||
$status->media_attachments = array_map(
|
||||
function ( $attachment ) {
|
||||
$default_attachment = array(
|
||||
'url' => null,
|
||||
'mediaType' => null,
|
||||
'name' => null,
|
||||
'width' => 0,
|
||||
'height' => 0,
|
||||
'blurhash' => null,
|
||||
);
|
||||
|
||||
$attachment = array_merge( $default_attachment, $attachment );
|
||||
|
||||
$media_attachment = new Media_Attachment();
|
||||
$media_attachment->id = $attachment['url'];
|
||||
$media_attachment->type = strtok( $attachment['mediaType'], '/' );
|
||||
$media_attachment->url = $attachment['url'];
|
||||
$media_attachment->preview_url = $attachment['url'];
|
||||
$media_attachment->description = $attachment['name'];
|
||||
if ( $attachment['blurhash'] ) {
|
||||
$media_attachment->blurhash = $attachment['blurhash'];
|
||||
}
|
||||
if ( $attachment['width'] > 0 && $attachment['height'] > 0 ) {
|
||||
$media_attachment->meta = array(
|
||||
'original' => array(
|
||||
'width' => $attachment['width'],
|
||||
'height' => $attachment['height'],
|
||||
'size' => $attachment['width'] . 'x' . $attachment['height'],
|
||||
'aspect' => $attachment['width'] / $attachment['height'],
|
||||
),
|
||||
);}
|
||||
return $media_attachment;
|
||||
},
|
||||
$object['attachment']
|
||||
);
|
||||
}
|
||||
|
||||
return $status;
|
||||
}
|
||||
|
||||
public static function api_statuses_external( $statuses, $args ) {
|
||||
if ( ! isset( $args['activitypub'] ) ) {
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
$data = get_remote_metadata_by_actor( $args['activitypub'] );
|
||||
|
||||
if ( ! $data || is_wp_error( $data ) || ! isset( $data['outbox'] ) ) {
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
$outbox = Http::get_remote_object( $data['outbox'], true );
|
||||
if ( is_wp_error( $outbox ) || ! isset( $outbox['first'] ) ) {
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
$account = self::get_account_for_actor( $args['activitypub'] );
|
||||
if ( ! $account ) {
|
||||
return $statuses;
|
||||
}
|
||||
$limit = 10;
|
||||
if ( isset( $args['posts_per_page'] ) ) {
|
||||
$limit = $args['posts_per_page'];
|
||||
}
|
||||
if ( $limit > 40 ) {
|
||||
$limit = 40;
|
||||
}
|
||||
$activitypub_statuses = array();
|
||||
$url = $outbox['first'];
|
||||
$tries = 0;
|
||||
while ( $url ) {
|
||||
if ( ++$tries > 3 ) {
|
||||
break;
|
||||
}
|
||||
|
||||
$posts = Http::get_remote_object( $url, true );
|
||||
if ( is_wp_error( $posts ) ) {
|
||||
return $statuses;
|
||||
}
|
||||
|
||||
$new_statuses = array_map(
|
||||
function ( $item ) use ( $account, $args ) {
|
||||
if ( $args['exclude_replies'] ) {
|
||||
if ( isset( $item['object']['inReplyTo'] ) && $item['object']['inReplyTo'] ) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return self::activity_to_status( $item, $account );
|
||||
},
|
||||
$posts['orderedItems']
|
||||
);
|
||||
$activitypub_statuses = array_merge( $activitypub_statuses, array_filter( $new_statuses ) );
|
||||
$url = $posts['next'];
|
||||
|
||||
if ( count( $activitypub_statuses ) >= $limit ) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return array_slice( $activitypub_statuses, 0, $limit );
|
||||
}
|
||||
|
||||
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'] ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
$replies_url = $meta['replies']['first']['next'];
|
||||
$replies = Http::get_remote_object( $replies_url, true );
|
||||
if ( is_wp_error( $replies ) || ! isset( $replies['items'] ) ) {
|
||||
return $context;
|
||||
}
|
||||
|
||||
foreach ( $replies['items'] as $url ) {
|
||||
$response = Http::get( $url, true );
|
||||
if ( is_wp_error( $response ) || wp_remote_retrieve_response_code( $response ) !== 200 ) {
|
||||
continue;
|
||||
}
|
||||
$status = json_decode( wp_remote_retrieve_body( $response ), true );
|
||||
if ( ! $status || is_wp_error( $status ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$account = self::get_account_for_actor( $status['attributedTo'] );
|
||||
$status = self::activity_to_status( $status, $account );
|
||||
if ( $status ) {
|
||||
$context['descendants'][ $status->id ] = $status;
|
||||
}
|
||||
}
|
||||
|
||||
return $context;
|
||||
}
|
||||
}
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
class Jetpack {
|
||||
|
||||
public static function init() {
|
||||
\add_filter( 'jetpack_sync_post_meta_whitelist', [ __CLASS__, 'add_sync_meta' ] );
|
||||
}
|
||||
|
||||
public static function add_sync_meta( $whitelist ) {
|
||||
if ( ! is_array( $whitelist ) ) {
|
||||
return $whitelist;
|
||||
}
|
||||
$activitypub_meta_keys = [
|
||||
'activitypub_user_id',
|
||||
'activitypub_inbox',
|
||||
'activitypub_actor_json',
|
||||
];
|
||||
return \array_merge( $whitelist, $activitypub_meta_keys );
|
||||
}
|
||||
}
|
@ -0,0 +1,83 @@
|
||||
<?php
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use function Activitypub\get_total_users;
|
||||
use function Activitypub\get_active_users;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
|
||||
/**
|
||||
* Compatibility with the NodeInfo plugin
|
||||
*
|
||||
* @see https://wordpress.org/plugins/nodeinfo/
|
||||
*/
|
||||
class Nodeinfo {
|
||||
/**
|
||||
* 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( 'wellknown_nodeinfo_data', array( self::class, 'add_wellknown_nodeinfo_data' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend NodeInfo data
|
||||
*
|
||||
* @param array $nodeinfo NodeInfo data
|
||||
* @param string The NodeInfo Version
|
||||
*
|
||||
* @return array The extended array
|
||||
*/
|
||||
public static function add_nodeinfo_data( $nodeinfo, $version ) {
|
||||
if ( $version >= '2.0' ) {
|
||||
$nodeinfo['protocols'][] = 'activitypub';
|
||||
} else {
|
||||
$nodeinfo['protocols']['inbound'][] = 'activitypub';
|
||||
$nodeinfo['protocols']['outbound'][] = 'activitypub';
|
||||
}
|
||||
|
||||
$nodeinfo['usage']['users'] = array(
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users( '1 month ago' ),
|
||||
'activeHalfyear' => get_active_users( '6 month ago' ),
|
||||
);
|
||||
|
||||
return $nodeinfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend NodeInfo2 data
|
||||
*
|
||||
* @param array $nodeinfo NodeInfo2 data
|
||||
*
|
||||
* @return array The extended array
|
||||
*/
|
||||
public static function add_nodeinfo2_data( $nodeinfo ) {
|
||||
$nodeinfo['protocols'][] = 'activitypub';
|
||||
|
||||
$nodeinfo['usage']['users'] = array(
|
||||
'total' => get_total_users(),
|
||||
'activeMonth' => get_active_users( '1 month ago' ),
|
||||
'activeHalfyear' => get_active_users( '6 month ago' ),
|
||||
);
|
||||
|
||||
return $nodeinfo;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extend the well-known nodeinfo data
|
||||
*
|
||||
* @param array $data The well-known nodeinfo data
|
||||
*
|
||||
* @return array The extended array
|
||||
*/
|
||||
public static function add_wellknown_nodeinfo_data( $data ) {
|
||||
$data['links'][] = array(
|
||||
'rel' => 'https://www.w3.org/ns/activitystreams#Application',
|
||||
'href' => get_rest_url_by_path( 'application' ),
|
||||
);
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
@ -0,0 +1,107 @@
|
||||
<?php
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
|
||||
/**
|
||||
* 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
|
||||
* @see https://github.com/mastodon/mastodon/pull/30398
|
||||
*/
|
||||
class Opengraph {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
if ( ! function_exists( 'opengraph_metadata' ) ) {
|
||||
\add_action( 'wp_head', array( self::class, 'add_meta_tags' ) );
|
||||
}
|
||||
|
||||
\add_filter( 'opengraph_metadata', array( self::class, 'add_metadata' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ActivityPub prefix to the OpenGraph prefixes.
|
||||
*
|
||||
* @param array $prefixes the current prefixes.
|
||||
*
|
||||
* @return array the updated prefixes.
|
||||
*/
|
||||
public static function add_prefixes( $prefixes ) {
|
||||
// @todo discuss namespace
|
||||
$prefixes['fediverse'] = 'https://codeberg.org/fediverse/fep/src/branch/main/fep/XXXX/fep-XXXX.md';
|
||||
|
||||
return $prefixes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ActivityPub metadata to the OpenGraph metadata.
|
||||
*
|
||||
* @param array $metadata the current metadata.
|
||||
*
|
||||
* @return array the updated metadata.
|
||||
*/
|
||||
public static function add_metadata( $metadata ) {
|
||||
// Always show Blog-User if the Blog is in single user mode
|
||||
if ( is_single_user() ) {
|
||||
$user = new Blog();
|
||||
|
||||
// add WebFinger resource
|
||||
$metadata['fediverse:creator'] = $user->get_webfinger();
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if ( \is_author() ) {
|
||||
// Use the Author of the Archive-Page
|
||||
$user_id = \get_queried_object_id();
|
||||
} elseif ( \is_singular() ) {
|
||||
// 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
|
||||
$user_id = Users::BLOG_USER_ID;
|
||||
} else {
|
||||
// Do not add any metadata otherwise
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
$user = Users::get_by_id( $user_id );
|
||||
|
||||
if ( ! $user || \is_wp_error( $user ) ) {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
// add WebFinger resource
|
||||
$metadata['fediverse:creator'] = $user->get_webfinger();
|
||||
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output Open Graph <meta> tags in the page header.
|
||||
*/
|
||||
public static function add_meta_tags() {
|
||||
$metadata = apply_filters( 'opengraph_metadata', array() );
|
||||
foreach ( $metadata as $key => $value ) {
|
||||
if ( empty( $key ) || empty( $value ) ) {
|
||||
continue;
|
||||
}
|
||||
$value = (array) $value;
|
||||
|
||||
foreach ( $value as $v ) {
|
||||
printf(
|
||||
'<meta property="%1$s" name="%1$s" content="%2$s" />' . PHP_EOL,
|
||||
esc_attr( $key ),
|
||||
esc_attr( $v )
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,69 @@
|
||||
<?php
|
||||
namespace Activitypub\Integration;
|
||||
|
||||
use Activitypub\Rest\Webfinger as Webfinger_Rest;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
|
||||
/**
|
||||
* Compatibility with the WebFinger plugin
|
||||
*
|
||||
* @see https://wordpress.org/plugins/webfinger/
|
||||
*/
|
||||
class Webfinger {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'webfinger_user_data', array( self::class, 'add_user_discovery' ), 1, 3 );
|
||||
\add_filter( 'webfinger_data', array( self::class, 'add_pseudo_user_discovery' ), 1, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add WebFinger discovery links
|
||||
*
|
||||
* @param array $array the jrd array
|
||||
* @param string $resource the WebFinger resource
|
||||
* @param WP_User $user the WordPress user
|
||||
*
|
||||
* @return array the jrd array
|
||||
*/
|
||||
public static function add_user_discovery( $array, $resource, $user ) {
|
||||
$user = User_Collection::get_by_id( $user->ID );
|
||||
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
return $array;
|
||||
}
|
||||
|
||||
$array['subject'] = sprintf( 'acct:%s', $user->get_webfinger() );
|
||||
|
||||
$array['aliases'][] = $user->get_url();
|
||||
$array['aliases'][] = $user->get_alternate_url();
|
||||
|
||||
$array['links'][] = array(
|
||||
'rel' => 'self',
|
||||
'type' => 'application/activity+json',
|
||||
'href' => $user->get_url(),
|
||||
);
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add WebFinger discovery links
|
||||
*
|
||||
* @param array $array the jrd array
|
||||
* @param string $resource the WebFinger resource
|
||||
* @param WP_User $user the WordPress user
|
||||
*
|
||||
* @return array the jrd array
|
||||
*/
|
||||
public static function add_pseudo_user_discovery( $array, $resource ) {
|
||||
$user = Webfinger_Rest::get_profile( $resource );
|
||||
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
return $array;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
}
|
233
wp-content/upgrade-temp-backup/plugins/activitypub/readme.txt
Normal file
233
wp-content/upgrade-temp-backup/plugins/activitypub/readme.txt
Normal file
@ -0,0 +1,233 @@
|
||||
=== ActivityPub ===
|
||||
Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nuriapena, cavalierlife
|
||||
Tags: OStatus, fediverse, activitypub, activitystream
|
||||
Requires at least: 5.5
|
||||
Tested up to: 6.6
|
||||
Stable tag: 2.6.1
|
||||
Requires PHP: 7.0
|
||||
License: MIT
|
||||
License URI: http://opensource.org/licenses/MIT
|
||||
|
||||
The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.
|
||||
|
||||
== Description ==
|
||||
|
||||
Enter the fediverse with **ActivityPub**, broadcasting your blog to a wider audience! Attract followers, deliver updates, and receive comments from a diverse user base of **ActivityPub**\-compliant platforms.
|
||||
|
||||
With the ActivityPub plugin installed, your WordPress blog itself function as a federated profile, along with profiles for each author. For instance, if your website is `example.com`, then the blog-wide profile can be found at `@example.com@example.com`, and authors like Jane and Bob would have their individual profiles at `@jane@example.com` and `@bobz@example.com`, respectively.
|
||||
|
||||
An example: I give you my Mastodon profile name: `@pfefferle@mastodon.social`. You search, see my profile, and hit follow. Now, any post I make appears in your Home feed. Similarly, with the ActivityPub plugin, you can find and follow Jane's profile at `@jane@example.com`.
|
||||
|
||||
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.
|
||||
|
||||
The plugin works with the following tested federated platforms, but there may be more that it works with as well:
|
||||
|
||||
* [Mastodon](https://joinmastodon.org/)
|
||||
* [Pleroma](https://pleroma.social/)/[Akkoma](https://akkoma.social/)
|
||||
* [friendica](https://friendi.ca/)
|
||||
* [Hubzilla](https://hubzilla.org/)
|
||||
* [Pixelfed](https://pixelfed.org/)
|
||||
* [Socialhome](https://socialhome.network/)
|
||||
* [Misskey](https://join.misskey.page/)
|
||||
* [Firefish](https://joinfirefish.org/) (rebrand of Calckey)
|
||||
|
||||
Some things to note:
|
||||
|
||||
1. The blog-wide profile is only compatible with sites with rewrite rules enabled. If your site does not have rewrite rules enabled, the author-specific profiles may still work.
|
||||
1. Many single-author blogs have chosen to turn off or redirect their author profile pages, usually via an SEO plugin like Yoast or Rank Math. This is usually done to avoid duplicate content with your blog’s home page. If your author page has been deactivated in this way, then ActivityPub author profiles won’t work for you. Instead, you can turn your author profile page back on, and then use the option in your SEO plugin to noindex the author page. This will still resolve duplicate content issues with search engines and will enable ActivityPub author profiles to work.
|
||||
1. Once ActivityPub is installed, *only new posts going forward* will be available in the fediverse. Likewise, even if you’ve been using ActivityPub for a while, anyone who follows your site will only see new posts you publish from that moment on. They will never see previously-published posts in their Home feed. This process is very similar to subscribing to a newsletter. If you subscribe to a newsletter, you will only receive future emails, but not the old archived ones. With ActivityPub, if someone follows your site, they will only receive new blog posts you publish from then on.
|
||||
|
||||
So what’s the process?
|
||||
|
||||
1. Install the ActivityPub plugin.
|
||||
1. Go to the plugin’s settings page and adjust the settings to your liking. Click the Save button when ready.
|
||||
1. Make sure your blog’s author profile page is active if you are using author profiles.
|
||||
1. Go to Mastodon or any other federated platform, and search for your profile, and follow it. Your new profile will be in the form of either `@your_username@example.com` or `@example.com@example.com`, so that is what you’ll search for.
|
||||
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 don’t assume that just because you didn’t 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 you’ll know everything is working as expected.
|
||||
|
||||
== Frequently Asked Questions ==
|
||||
|
||||
= tl;dr =
|
||||
|
||||
This plugin connects your WordPress blog to popular social platforms like Mastodon, making your posts more accessible to a wider audience. Once installed, your blog can be followed by users on these platforms, allowing them to receive your new posts in their feeds.
|
||||
|
||||
= What is the status of this plugin? =
|
||||
|
||||
Implemented:
|
||||
|
||||
* blog profile pages (JSON representation)
|
||||
* author profile pages (JSON representation)
|
||||
* custom links
|
||||
* functional inbox/outbox
|
||||
* follow (accept follows)
|
||||
* share posts
|
||||
* receive comments/reactions
|
||||
* signature verification
|
||||
* threaded comments support
|
||||
|
||||
To implement:
|
||||
|
||||
* replace shortcodes with blocks for layout
|
||||
|
||||
= What is "ActivityPub for WordPress" =
|
||||
|
||||
*ActivityPub for WordPress* extends WordPress with some Fediverse features, but it does not compete with platforms like Friendica or Mastodon. If you want to run a **decentralized social network**, please use [Mastodon](https://joinmastodon.org/) or [GNU social](https://gnusocial.network/).
|
||||
|
||||
= What if you are running your blog in a subdirectory? =
|
||||
|
||||
In order for webfinger to work, it must be mapped to the root directory of the URL on which your blog resides.
|
||||
|
||||
**Apache**
|
||||
|
||||
Add the following to the .htaccess file in the root directory:
|
||||
|
||||
RedirectMatch "^\/\.well-known/(webfinger|nodeinfo|x-nodeinfo2)(.*)$" /blog/.well-known/$1$2
|
||||
|
||||
Where 'blog' is the path to the subdirectory at which your blog resides.
|
||||
|
||||
**Nginx**
|
||||
|
||||
Add the following to the site.conf in sites-available:
|
||||
|
||||
location ~* /.well-known {
|
||||
allow all;
|
||||
try_files $uri $uri/ /blog/?$args;
|
||||
}
|
||||
|
||||
Where 'blog' is the path to the subdirectory at which your blog resides.
|
||||
|
||||
= What if you are running your blog in a subdirectory? =
|
||||
|
||||
If you are running your blog in a subdirectory, but have a different [wp_siteurl](https://wordpress.org/documentation/article/giving-wordpress-its-own-directory/), you don't need the redirect, because the index.php will take care of that.
|
||||
|
||||
= 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.
|
||||
|
||||
* `ACTIVITYPUB_REST_NAMESPACE` - Change the default Namespace of the REST endpoint. Default: `activitypub/1.0`.
|
||||
* `ACTIVITYPUB_EXCERPT_LENGTH` - Change the length of the Excerpt. Default: `400`.
|
||||
* `ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS` - show plugin recommendations in the ActivityPub settings. Default: `true`.
|
||||
* `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_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`.
|
||||
* `ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS` - Block incoming replies/comments/likes. Default: `false`.
|
||||
* `ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS` - Disable outgoing replies/comments/likes. Default: `false`.
|
||||
* `ACTIVITYPUB_SHARED_INBOX_FEATURE` - Enable the shared inbox. Default: `false`.
|
||||
* `ACTIVITYPUB_SEND_VARY_HEADER` - Enable to send the `Vary: Accept` header. Default: `false`.
|
||||
|
||||
= Where can you manage your followers? =
|
||||
|
||||
If you have activated the blog user, you will find the list of his followers in the settings under `/wp-admin/options-general.php?page=activitypub&tab=followers`.
|
||||
|
||||
The followers of a user can be found in the menu under "Users" -> "Followers" or under `wp-admin/users.php?page=activitypub-followers-list`.
|
||||
|
||||
For reasons of data protection, it is not possible to see the followers of other users.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 2.6.1 =
|
||||
|
||||
* Fixed: Extra Fields will generate wrong entries
|
||||
|
||||
= 2.6.0 =
|
||||
|
||||
* 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
|
||||
|
||||
= 2.5.0 =
|
||||
|
||||
* 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
|
||||
|
||||
= 2.4.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
|
||||
|
||||
See full Changelog on [GitHub](https://github.com/Automattic/wordpress-activitypub/blob/master/CHANGELOG.md).
|
||||
|
||||
== Upgrade Notice ==
|
||||
|
||||
= 1.0.0 =
|
||||
|
||||
For version 1.0.0 we have completely rebuilt the followers lists. There is a migration from the old format to the new, but it may take some time until the migration is complete. No data will be lost in the process, please give the migration some time.
|
||||
|
||||
== Installation ==
|
||||
|
||||
Follow the normal instructions for [installing WordPress plugins](https://wordpress.org/support/article/managing-plugins/).
|
||||
|
||||
= Automatic Plugin Installation =
|
||||
|
||||
To add a WordPress Plugin using the [built-in plugin installer](https://codex.wordpress.org/Administration_Screens#Add_New_Plugins):
|
||||
|
||||
1. Go to [Plugins](https://codex.wordpress.org/Administration_Screens#Plugins) > [Add New](https://codex.wordpress.org/Plugins_Add_New_Screen).
|
||||
1. Type "`activitypub`" into the **Search Plugins** box.
|
||||
1. Find the WordPress Plugin you wish to install.
|
||||
1. Click **Details** for more information about the Plugin and instructions you may wish to print or save to help setup the Plugin.
|
||||
1. Click **Install Now** to install the WordPress Plugin.
|
||||
1. The resulting installation screen will list the installation as successful or note any problems during the install.
|
||||
1. If successful, click **Activate Plugin** to activate it, or **Return to Plugin Installer** for further actions.
|
||||
|
||||
= Manual Plugin Installation =
|
||||
|
||||
There are a few cases when manually installing a WordPress Plugin is appropriate.
|
||||
|
||||
* If you wish to control the placement and the process of installing a WordPress Plugin.
|
||||
* If your server does not permit automatic installation of a WordPress Plugin.
|
||||
* If you want to try the [latest development version](https://github.com/pfefferle/wordpress-activitypub).
|
||||
|
||||
Installation of a WordPress Plugin manually requires FTP familiarity and the awareness that you may put your site at risk if you install a WordPress Plugin incompatible with the current version or from an unreliable source.
|
||||
|
||||
Backup your site completely before proceeding.
|
||||
|
||||
To install a WordPress Plugin manually:
|
||||
|
||||
* Download your WordPress Plugin to your desktop.
|
||||
* Download from [the WordPress directory](https://wordpress.org/plugins/activitypub/)
|
||||
* Download from [GitHub](https://github.com/pfefferle/wordpress-activitypub/releases)
|
||||
* If downloaded as a zip archive, extract the Plugin folder to your desktop.
|
||||
* With your FTP program, upload the Plugin folder to the `wp-content/plugins` folder in your WordPress directory online.
|
||||
* Go to [Plugins screen](https://codex.wordpress.org/Administration_Screens#Plugins) and find the newly uploaded Plugin in the list.
|
||||
* Click **Activate** to activate it.
|
@ -0,0 +1,27 @@
|
||||
<?php
|
||||
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
|
||||
?>
|
||||
<div class="activitypub-settings-header">
|
||||
<div class="activitypub-settings-title-section">
|
||||
<h1><?php \esc_html_e( 'ActivityPub', 'activitypub' ); ?></h1>
|
||||
</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'] ); ?>">
|
||||
<?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'] ); ?>">
|
||||
<?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'] ); ?>">
|
||||
<?php \esc_html_e( 'Followers', 'activitypub' ); ?>
|
||||
</a>
|
||||
|
||||
<?php endif; ?>
|
||||
</nav>
|
||||
</div>
|
||||
<hr class="wp-header-end">
|
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
$user = \Activitypub\Collection\Users::get_by_id( \get_the_author_meta( 'ID' ) );
|
||||
|
||||
/*
|
||||
* Action triggerd 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
|
||||
*/
|
||||
\do_action( 'activitypub_json_author_post', $user->get__id() );
|
@ -0,0 +1,15 @@
|
||||
<?php
|
||||
$user = new \Activitypub\Model\Blog();
|
||||
|
||||
/*
|
||||
* Action triggerd 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
|
||||
*/
|
||||
\do_action( 'activitypub_json_author_post', $user->get__id() );
|
@ -0,0 +1,28 @@
|
||||
<?php
|
||||
\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.
|
||||
$followers_template = _n( 'Your blog profile currently has %s follower.', 'Your blog profile currently has %s followers.', $follower_count, 'activitypub' );
|
||||
?>
|
||||
<div class="wrap activitypub-followers-page">
|
||||
<p><?php \printf( \esc_html( $followers_template ), \esc_attr( $follower_count ) ); ?></p>
|
||||
|
||||
<form method="get">
|
||||
<input type="hidden" name="page" value="activitypub" />
|
||||
<input type="hidden" name="tab" value="followers" />
|
||||
<?php
|
||||
$table->prepare_items();
|
||||
$table->search_box( 'Search', 'search' );
|
||||
$table->display();
|
||||
?>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,23 @@
|
||||
<?php
|
||||
$comment = \get_comment( \get_query_var( 'c', null ) ); // phpcs:ignore
|
||||
$transformer = \Activitypub\Transformer\Factory::get_transformer( $comment );
|
||||
|
||||
if ( \is_wp_error( $transformer ) ) {
|
||||
\wp_die(
|
||||
\esc_html( $transformer->get_error_message() ),
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
/*
|
||||
* Action triggerd 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
|
||||
*/
|
||||
\do_action( 'activitypub_json_comment_post' );
|
@ -0,0 +1,25 @@
|
||||
<?php
|
||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||
$post = \get_post();
|
||||
$transformer = \Activitypub\Transformer\Factory::get_transformer( $post );
|
||||
|
||||
if ( \is_wp_error( $transformer ) ) {
|
||||
\wp_die(
|
||||
esc_html( $transformer->get_error_message() ),
|
||||
404
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Action triggerd 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
|
||||
*/
|
||||
\do_action( 'activitypub_json_post_post' );
|
@ -0,0 +1,270 @@
|
||||
<?php
|
||||
\load_template(
|
||||
__DIR__ . '/admin-header.php',
|
||||
true,
|
||||
array(
|
||||
'settings' => 'active',
|
||||
'welcome' => '',
|
||||
'followers' => '',
|
||||
)
|
||||
);
|
||||
?>
|
||||
|
||||
<div class="activitypub-settings activitypub-settings-page hide-if-no-js">
|
||||
<form method="post" action="options.php">
|
||||
<?php \settings_fields( 'activitypub' ); ?>
|
||||
|
||||
<div class="box">
|
||||
<h3><?php \esc_html_e( 'Profiles', 'activitypub' ); ?></h3>
|
||||
<table class="form-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php \esc_html_e( 'Enable profiles by type', 'activitypub' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="activitypub_enable_users" id="activitypub_enable_users" value="1" <?php echo \checked( '1', \get_option( 'activitypub_enable_users', '1' ) ); ?> />
|
||||
<?php \esc_html_e( 'Enable authors', 'activitypub' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p class="description">
|
||||
<?php echo \wp_kses( \__( 'Every author on this blog (with the <code>activitypub</code> capability) gets their own ActivityPub profile.', 'activitypub' ), array( 'code' => array() ) ); ?>
|
||||
<?php // translators: %s is a URL. ?>
|
||||
<strong><?php echo \wp_kses( sprintf( \__( 'You can add/remove the capability in the <a href="%s">user settings.</a>', 'activitypub' ), admin_url( '/users.php' ) ), array( 'a' => array( 'href' => array() ) ) ); ?></strong>
|
||||
<?php echo \wp_kses( \__( 'Select all the users you want to update, choose the method from the drop-down list and click on the "Apply" button.', 'activitypub' ), array( 'code' => array() ) ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="checkbox" name="activitypub_enable_blog_user" id="activitypub_enable_blog_user" value="1" <?php echo \checked( '1', \get_option( 'activitypub_enable_blog_user', '0' ) ); ?> />
|
||||
<?php \esc_html_e( 'Enable blog', 'activitypub' ); ?>
|
||||
</label>
|
||||
</p>
|
||||
<p class="description">
|
||||
<?php \esc_html_e( 'Your blog becomes an ActivityPub profile.', 'activitypub' ); ?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php \esc_html_e( 'Change blog profile ID', 'activitypub' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<label for="activitypub_blog_user_identifier">
|
||||
<input class="blog-user-identifier" name="activitypub_blog_user_identifier" id="activitypub_blog_user_identifier" type="text" value="<?php echo esc_attr( \get_option( 'activitypub_blog_user_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 author’s name as the blog profile ID. Fediverse platforms might use caching and this could break the functionality completely.', 'activitypub' ); ?>
|
||||
</strong>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php \do_settings_fields( 'activitypub', 'user' ); ?>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3><?php \esc_html_e( 'Activities', 'activitypub' ); ?></h3>
|
||||
<table class="form-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php \esc_html_e( 'Activity-Object-Type', 'activitypub' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<p>
|
||||
<label for="activitypub_object_type_note">
|
||||
<input type="radio" name="activitypub_object_type" id="activitypub_object_type_note" value="note" <?php echo \checked( 'note', \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ) ); ?> />
|
||||
<?php \esc_html_e( 'Note (default)', 'activitypub' ); ?>
|
||||
-
|
||||
<span class="description">
|
||||
<?php \esc_html_e( 'Should work with most platforms.', 'activitypub' ); ?>
|
||||
</span>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label>
|
||||
<input type="radio" name="activitypub_object_type" id="activitypub_object_type" value="wordpress-post-format" <?php echo \checked( 'wordpress-post-format', \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ) ); ?> />
|
||||
<?php \esc_html_e( 'WordPress Post-Format', 'activitypub' ); ?>
|
||||
-
|
||||
<span class="description">
|
||||
<?php \esc_html_e( 'Maps the WordPress Post-Format to the ActivityPub Object Type.', 'activitypub' ); ?>
|
||||
</span>
|
||||
</label>
|
||||
</p>
|
||||
|
||||
</td>
|
||||
</tr>
|
||||
<?php // phpcs:ignore Squiz.ControlStructures.ControlSignature.NewlineAfterOpenBrace ?>
|
||||
<tr <?php if ( 'wordpress-post-format' === \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ) ) { echo 'style="display: none"'; } ?>>
|
||||
<th scope="row">
|
||||
<?php \esc_html_e( 'Post content', 'activitypub' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<p><strong><?php \esc_html_e( 'These settings only apply if you use the "Note" Object-Type setting above.', 'activitypub' ); ?></strong></p>
|
||||
<p>
|
||||
<label for="activitypub_post_content_type_title_link">
|
||||
<input type="radio" name="activitypub_post_content_type" id="activitypub_post_content_type_title_link" value="title" <?php echo \checked( 'title', \get_option( 'activitypub_post_content_type', 'content' ) ); ?> />
|
||||
<?php \esc_html_e( 'Title and link', 'activitypub' ); ?>
|
||||
-
|
||||
<span class="description">
|
||||
<?php \esc_html_e( 'Only the title and a link.', 'activitypub' ); ?>
|
||||
</span>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label for="activitypub_post_content_type_excerpt">
|
||||
<input type="radio" name="activitypub_post_content_type" id="activitypub_post_content_type_excerpt" value="excerpt" <?php echo \checked( 'excerpt', \get_option( 'activitypub_post_content_type', 'content' ) ); ?> />
|
||||
<?php \esc_html_e( 'Excerpt', 'activitypub' ); ?>
|
||||
-
|
||||
<span class="description">
|
||||
<?php \esc_html_e( 'A content summary without markup (truncated if no excerpt is provided).', 'activitypub' ); ?>
|
||||
</span>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label for="activitypub_post_content_type_content">
|
||||
<input type="radio" name="activitypub_post_content_type" id="activitypub_post_content_type_content" value="content" <?php echo \checked( 'content', \get_option( 'activitypub_post_content_type', 'content' ) ); ?> />
|
||||
<?php \esc_html_e( 'Content (default)', 'activitypub' ); ?>
|
||||
-
|
||||
<span class="description">
|
||||
<?php \esc_html_e( 'The full content.', 'activitypub' ); ?>
|
||||
</span>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<label for="activitypub_post_content_type_custom">
|
||||
<input type="radio" name="activitypub_post_content_type" id="activitypub_post_content_type_custom" value="custom" <?php echo \checked( 'custom', \get_option( 'activitypub_post_content_type', 'content' ) ); ?> />
|
||||
<?php \esc_html_e( 'Custom', 'activitypub' ); ?>
|
||||
-
|
||||
<span class="description">
|
||||
<?php \esc_html_e( 'Use the text area below, to customize your activities.', 'activitypub' ); ?>
|
||||
</span>
|
||||
</label>
|
||||
</p>
|
||||
<p>
|
||||
<textarea name="activitypub_custom_post_content" id="activitypub_custom_post_content" rows="10" cols="50" class="large-text" placeholder="<?php echo wp_kses( ACTIVITYPUB_CUSTOM_POST_CONTENT, 'post' ); ?>"><?php echo esc_textarea( wp_kses( \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT ), 'post' ) ); ?></textarea>
|
||||
<details>
|
||||
<summary><?php esc_html_e( 'See a list of ActivityPub Template Tags.', 'activitypub' ); ?></summary>
|
||||
<div class="description">
|
||||
<ul>
|
||||
<li><code>[ap_title]</code> - <?php \esc_html_e( 'The post\'s title.', 'activitypub' ); ?></li>
|
||||
<li><code>[ap_content]</code> - <?php \esc_html_e( 'The post\'s content.', 'activitypub' ); ?></li>
|
||||
<li><code>[ap_excerpt]</code> - <?php \esc_html_e( 'The post\'s excerpt (may be truncated).', 'activitypub' ); ?></li>
|
||||
<li><code>[ap_permalink]</code> - <?php \esc_html_e( 'The post\'s permalink.', 'activitypub' ); ?></li>
|
||||
<li><code>[ap_shortlink]</code> - <?php echo \wp_kses( \__( 'The post\'s shortlink. I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>.', 'activitypub' ), 'default' ); ?></li>
|
||||
<li><code>[ap_hashtags]</code> - <?php \esc_html_e( 'The post\'s tags as hashtags.', 'activitypub' ); ?></li>
|
||||
</ul>
|
||||
<p><?php \esc_html_e( 'You can find the full list with all possible attributes in the help section on the top-right of the screen.', 'activitypub' ); ?></p>
|
||||
</div>
|
||||
</details>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php \esc_html_e( 'Media attachments', 'activitypub' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<input value="<?php echo esc_attr( \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ); ?>" name="activitypub_max_image_attachments" id="activitypub_max_image_attachments" type="number" min="0" />
|
||||
<p class="description">
|
||||
<?php
|
||||
echo \wp_kses(
|
||||
\sprintf(
|
||||
// translators:
|
||||
\__( 'The number of media (images, audio, video) to attach to posts. Default: <code>%s</code>', 'activitypub' ),
|
||||
\esc_html( ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS )
|
||||
),
|
||||
'default'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
<p class="description">
|
||||
<em>
|
||||
<?php
|
||||
esc_html_e( 'Note: audio and video attachments are only supported from Block Editor.', 'activitypub' );
|
||||
?>
|
||||
</em>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row"><?php \esc_html_e( 'Supported post types', 'activitypub' ); ?></th>
|
||||
<td>
|
||||
<fieldset>
|
||||
<?php \esc_html_e( 'Automatically publish items of the selected post types to the fediverse:', 'activitypub' ); ?>
|
||||
|
||||
<?php $post_types = \get_post_types( array( 'public' => true ), 'objects' ); ?>
|
||||
<?php $support_post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post' ) ) : array(); ?>
|
||||
<ul>
|
||||
<?php // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited ?>
|
||||
<?php foreach ( $post_types as $post_type ) { ?>
|
||||
<li>
|
||||
<input type="checkbox" id="activitypub_support_post_type_<?php echo \esc_attr( $post_type->name ); ?>" name="activitypub_support_post_types[]" value="<?php echo \esc_attr( $post_type->name ); ?>" <?php echo \checked( \in_array( $post_type->name, $support_post_types, true ) ); ?> />
|
||||
<label for="activitypub_support_post_type_<?php echo \esc_attr( $post_type->name ); ?>"><?php echo \esc_html( $post_type->label ); ?></label>
|
||||
<span class="description">
|
||||
<?php echo \esc_html( \Activitypub\get_post_type_description( $post_type ) ); ?>
|
||||
</span>
|
||||
</li>
|
||||
<?php } ?>
|
||||
</ul>
|
||||
</fieldset>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php \esc_html_e( 'Hashtags', 'activitypub' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<p>
|
||||
<label><input type="checkbox" name="activitypub_use_hashtags" id="activitypub_use_hashtags" value="1" <?php echo \checked( '1', \get_option( 'activitypub_use_hashtags', '1' ) ); ?> /> <?php echo wp_kses( \__( 'Add hashtags in the content as native tags and replace the <code>#tag</code> with the tag link.', 'activitypub' ), 'default' ); ?></label>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php \do_settings_fields( 'activitypub', 'activity' ); ?>
|
||||
</div>
|
||||
|
||||
<div class="box">
|
||||
<h3><?php \esc_html_e( 'Server', 'activitypub' ); ?></h3>
|
||||
<table class="form-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<?php \esc_html_e( 'Blocklist', 'activitypub' ); ?>
|
||||
</th>
|
||||
<td>
|
||||
<p class="description">
|
||||
<?php
|
||||
echo \wp_kses(
|
||||
\sprintf(
|
||||
// translators: %s is a URL.
|
||||
\__( 'To block servers, add the host of the server to the "<a href="%s">Disallowed Comment Keys</a>" list.', 'activitypub' ),
|
||||
\esc_attr( \admin_url( 'options-discussion.php#disallowed_keys' ) )
|
||||
),
|
||||
'default'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php \do_settings_fields( 'activitypub', 'server' ); ?>
|
||||
</div>
|
||||
<?php \do_settings_sections( 'activitypub' ); ?>
|
||||
|
||||
<?php \submit_button(); ?>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,21 @@
|
||||
<?php
|
||||
|
||||
$follower_count = \Activitypub\Collection\Followers::count_followers( \get_current_user_id() );
|
||||
// translators: The follower count.
|
||||
$followers_template = _n( 'Your author profile currently has %s follower.', 'Your author profile currently has %s followers.', $follower_count, 'activitypub' );
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php \esc_html_e( 'Author Followers', 'activitypub' ); ?></h1>
|
||||
<p><?php \printf( \esc_html( $followers_template ), \esc_attr( $follower_count ) ); ?></p>
|
||||
|
||||
<?php $table = new \Activitypub\Table\Followers(); ?>
|
||||
|
||||
<form method="get">
|
||||
<input type="hidden" name="page" value="activitypub-followers-list" />
|
||||
<?php
|
||||
$table->prepare_items();
|
||||
$table->search_box( 'Search', 'search' );
|
||||
$table->display();
|
||||
?>
|
||||
</form>
|
||||
</div>
|
@ -0,0 +1,67 @@
|
||||
<?php
|
||||
// phpcs:disable VariableAnalysis.CodeAnalysis.VariableAnalysis.UndefinedVariable
|
||||
$user = \Activitypub\Collection\Users::get_by_id( \get_current_user_id() ); ?>
|
||||
<h2 id="activitypub"><?php \esc_html_e( 'ActivityPub', 'activitypub' ); ?></h2>
|
||||
|
||||
<table class="form-table">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th scope="row">
|
||||
<label><?php \esc_html_e( 'Profile URL', 'activitypub' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<p>
|
||||
<code><?php echo \esc_html( $user->get_webfinger() ); ?></code> or
|
||||
<code><?php echo \esc_url( $user->get_url() ); ?></code>
|
||||
</p>
|
||||
<?php // translators: the webfinger resource ?>
|
||||
<p class="description"><?php \printf( \esc_html__( 'Follow "@%s" by searching for it on Mastodon, Friendica, etc.', 'activitypub' ), \esc_html( $user->get_webfinger() ) ); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="activitypub-user-description-wrap">
|
||||
<th>
|
||||
<label for="activitypub-user-description"><?php \esc_html_e( 'Biography', 'activitypub' ); ?></label>
|
||||
</th>
|
||||
<td>
|
||||
<textarea name="activitypub-user-description" id="activitypub-user-description" rows="5" cols="30" placeholder="<?php echo \esc_html( get_user_meta( \get_current_user_id(), 'description', true ) ); ?>"><?php echo \esc_html( $args['description'] ); ?></textarea>
|
||||
<p class="description"><?php \esc_html_e( 'If you wish to use different biographical info for the fediverse, enter your alternate bio here.', 'activitypub' ); ?></p>
|
||||
</td>
|
||||
<?php wp_nonce_field( 'activitypub-user-description', '_apnonce' ); ?>
|
||||
</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\get_actor_extra_fields( \get_current_user_id() );
|
||||
|
||||
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 } ?>
|
||||
</table>
|
||||
|
||||
<p>
|
||||
<a href="<?php echo esc_url( admin_url( '/post-new.php?post_type=ap_extrafield' ) ); ?>" class="button">
|
||||
<?php esc_html_e( 'Add new', 'activitypub' ); ?>
|
||||
</a>
|
||||
<a href="<?php echo esc_url( admin_url( '/edit.php?post_type=ap_extrafield' ) ); ?>">
|
||||
<?php esc_html_e( 'Manage all', 'activitypub' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
@ -0,0 +1,162 @@
|
||||
<?php
|
||||
\load_template(
|
||||
__DIR__ . '/admin-header.php',
|
||||
true,
|
||||
array(
|
||||
'settings' => '',
|
||||
'welcome' => 'active',
|
||||
'followers' => '',
|
||||
)
|
||||
);
|
||||
?>
|
||||
|
||||
<div class="activitypub-settings activitypub-welcome-page hide-if-no-js">
|
||||
<div class="box">
|
||||
<h2><?php \esc_html_e( 'Welcome', 'activitypub' ); ?></h2>
|
||||
|
||||
<p><?php echo wp_kses( \__( 'Enter the fediverse with <strong>ActivityPub</strong>, broadcasting your blog to a wider audience. Attract followers, deliver updates, and receive comments from a diverse user base on <strong>Mastodon</strong>, <strong>Friendica</strong>, <strong>Pleroma</strong>, <strong>Pixelfed</strong>, and all <strong>ActivityPub</strong>-compliant platforms.', 'activitypub' ), array( 'strong' => array() ) ); ?></p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
if ( ! \Activitypub\is_user_disabled( \Activitypub\Collection\Users::BLOG_USER_ID ) ) :
|
||||
$blog_user = new \Activitypub\Model\Blog();
|
||||
?>
|
||||
<div class="box">
|
||||
<h3><?php \esc_html_e( 'Blog profile', 'activitypub' ); ?></h3>
|
||||
<p>
|
||||
<?php \esc_html_e( 'People can follow your blog by using:', 'activitypub' ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<label for="activitypub-blog-identifier"><?php \esc_html_e( 'Username', 'activitypub' ); ?></label>
|
||||
</p>
|
||||
<p>
|
||||
<input type="text" class="regular-text" id="activitypub-blog-identifier" value="<?php echo \esc_attr( $blog_user->get_webfinger() ); ?>" readonly />
|
||||
</p>
|
||||
<p>
|
||||
<label for="activitypub-blog-url"><?php \esc_html_e( 'Profile URL', 'activitypub' ); ?></label>
|
||||
</p>
|
||||
<p>
|
||||
<input type="text" class="regular-text" id="activitypub-blog-url" value="<?php echo \esc_attr( $blog_user->get_url() ); ?>" readonly />
|
||||
</p>
|
||||
<p>
|
||||
<?php \esc_html_e( 'This blog profile will federate all posts written on your blog, regardless of the author who posted it.', 'activitypub' ); ?>
|
||||
<p>
|
||||
<p>
|
||||
<a href="<?php echo \esc_url_raw( \admin_url( '/options-general.php?page=activitypub&tab=settings' ) ); ?>">
|
||||
<?php \esc_html_e( 'Customize the blog profile', 'activitypub' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<?php
|
||||
if ( ! \Activitypub\is_user_disabled( get_current_user_id() ) ) :
|
||||
$user = \Activitypub\Collection\Users::get_by_id( wp_get_current_user()->ID );
|
||||
?>
|
||||
<div class="box">
|
||||
<h3><?php \esc_html_e( 'Author profile', 'activitypub' ); ?></h3>
|
||||
<p>
|
||||
<?php \esc_html_e( 'People can follow you by using your author name:', 'activitypub' ); ?>
|
||||
</p>
|
||||
<p>
|
||||
<label for="activitypub-user-identifier"><?php \esc_html_e( 'Username', 'activitypub' ); ?></label>
|
||||
</p>
|
||||
<p>
|
||||
<input type="text" class="regular-text" id="activitypub-user-identifier" value="<?php echo \esc_attr( $user->get_webfinger() ); ?>" readonly />
|
||||
</p>
|
||||
<p>
|
||||
<label for="activitypub-user-url"><?php \esc_html_e( 'Profile URL', 'activitypub' ); ?></label>
|
||||
</p>
|
||||
<p>
|
||||
<input type="text" class="regular-text" id="activitypub-user-url" value="<?php echo \esc_attr( $user->get_url() ); ?>" readonly />
|
||||
</p>
|
||||
<p>
|
||||
<?php \esc_html_e( 'Authors who can not access this settings page will find their username on the "Edit Profile" page.', 'activitypub' ); ?>
|
||||
<p>
|
||||
<p>
|
||||
<a href="<?php echo \esc_url_raw( \admin_url( '/profile.php#activitypub' ) ); ?>">
|
||||
<?php \esc_html_e( 'Customize username on "Edit Profile" page.', 'activitypub' ); ?>
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<div class="box">
|
||||
<h3><?php \esc_html_e( 'Troubleshooting', 'activitypub' ); ?></h3>
|
||||
<p>
|
||||
<?php
|
||||
echo wp_kses(
|
||||
\sprintf(
|
||||
/* translators: the placeholder is the Site Health URL */
|
||||
\__(
|
||||
'If you have problems using this plugin, please check the <a href="%s">Site Health</a> page to ensure that your site is compatible and/or use the "Help" tab (in the top right of the settings pages).',
|
||||
'activitypub'
|
||||
),
|
||||
\esc_url_raw( admin_url( 'site-health.php' ) )
|
||||
),
|
||||
'default'
|
||||
);
|
||||
?>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<?php if ( ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS ) : ?>
|
||||
<div class="box plugin-recommendations">
|
||||
<h3><?php \esc_html_e( 'Recommended Plugins', 'activitypub' ); ?></h3>
|
||||
|
||||
<p><?php \esc_html_e( 'ActivityPub works as is and there is no need for you to install additional plugins, nevertheless there are some plugins that extends the functionality of ActivityPub.', 'activitypub' ); ?></p>
|
||||
</div>
|
||||
<div class="activitypub-settings-accordion">
|
||||
<?php if ( ! \defined( 'FRIENDS_VERSION' ) ) : ?>
|
||||
<h4 class="activitypub-settings-accordion-heading">
|
||||
<button aria-expanded="true" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-friends-plugin" type="button">
|
||||
<span class="title"><?php \esc_html_e( 'Following Others', 'activitypub' ); ?></span>
|
||||
<span class="icon"></span>
|
||||
</button>
|
||||
</h4>
|
||||
<div id="activitypub-settings-accordion-block-friends-plugin" class="activitypub-settings-accordion-panel plugin-card-friends">
|
||||
<p><?php \esc_html_e( 'To follow people on Mastodon or similar platforms using your own WordPress, you can use the Friends Plugin for WordPress which uses this plugin to receive posts and display them on your own WordPress, thus making your own WordPress a Fediverse instance of its own.', 'activitypub' ); ?></p>
|
||||
<p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=friends&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install the Friends Plugin', 'activitypub' ); ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( ! \class_exists( 'Hum' ) ) : ?>
|
||||
<h4 class="activitypub-settings-accordion-heading">
|
||||
<button aria-expanded="false" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-activitypub-hum-plugin" type="button">
|
||||
<span class="title"><?php \esc_html_e( 'Add a URL Shortener', 'activitypub' ); ?></span>
|
||||
<span class="icon"></span>
|
||||
</button>
|
||||
</h4>
|
||||
<div id="activitypub-settings-accordion-block-activitypub-hum-plugin" class="activitypub-settings-accordion-panel plugin-card-hum" hidden="hidden">
|
||||
<p><?php \esc_html_e( 'Hum is a personal URL shortener for WordPress, designed to provide short URLs to your personal content, both hosted on WordPress and elsewhere.', 'activitypub' ); ?></p>
|
||||
<p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=hum&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install the Hum Plugin', 'activitypub' ); ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( ! \class_exists( 'Webfinger' ) ) : ?>
|
||||
<h4 class="activitypub-settings-accordion-heading">
|
||||
<button aria-expanded="false" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-activitypub-webfinger-plugin" type="button">
|
||||
<span class="title"><?php \esc_html_e( 'Advanced WebFinger Support', 'activitypub' ); ?></span>
|
||||
<span class="icon"></span>
|
||||
</button>
|
||||
</h4>
|
||||
<div id="activitypub-settings-accordion-block-activitypub-webfinger-plugin" class="activitypub-settings-accordion-panel plugin-card-webfinger" hidden="hidden">
|
||||
<p><?php \esc_html_e( 'WebFinger is a protocol that allows for discovery of information about people and things identified by a URI. Information about a person might be discovered via an "acct:" URI, for example, which is a URI that looks like an email address.', 'activitypub' ); ?></p>
|
||||
<p><?php \esc_html_e( 'The ActivityPub plugin comes with basic WebFinger support, if you need more configuration options and compatibility with other Fediverse/IndieWeb plugins, please install the WebFinger plugin.', 'activitypub' ); ?></p>
|
||||
<p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=webfinger&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install the WebFinger Plugin', 'activitypub' ); ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
<?php if ( ! \function_exists( 'nodeinfo_init' ) ) : ?>
|
||||
<h4 class="activitypub-settings-accordion-heading">
|
||||
<button aria-expanded="false" class="activitypub-settings-accordion-trigger" aria-controls="activitypub-settings-accordion-block-activitypub-nodeinfo-plugin" type="button">
|
||||
<span class="title"><?php \esc_html_e( 'Provide Enhanced Information about Your Blog', 'activitypub' ); ?></span>
|
||||
<span class="icon"></span>
|
||||
</button>
|
||||
</h4>
|
||||
<div id="activitypub-settings-accordion-block-activitypub-nodeinfo-plugin" class="activitypub-settings-accordion-panel plugin-card-nodeinfo" hidden="hidden">
|
||||
<p><?php \esc_html_e( '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><?php \esc_html_e( 'The ActivityPub plugin comes with a simple NodeInfo endpoint. If you need more configuration options and compatibility with other Fediverse plugins, please install the NodeInfo plugin.', 'activitypub' ); ?></p>
|
||||
<p><a href="<?php echo \esc_url_raw( \admin_url( 'plugin-install.php?tab=plugin-information&plugin=nodeinfo&TB_iframe=true' ) ); ?>" class="thickbox open-plugin-details-modal button install-now" target="_blank"><?php \esc_html_e( 'Install the NodeInfo Plugin', 'activitypub' ); ?></a></p>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</div>
|
420
wp-content/upgrade-temp-backup/plugins/gitium/functions.php
Normal file
420
wp-content/upgrade-temp-backup/plugins/gitium/functions.php
Normal file
@ -0,0 +1,420 @@
|
||||
<?php
|
||||
/* Copyright 2014-2016 Presslabs SRL <ping@presslabs.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License, version 2, as
|
||||
published by the Free Software Foundation.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
function gitium_error_log( $message ) {
|
||||
if ( ! defined( 'WP_DEBUG' ) || ! WP_DEBUG ) { return; }
|
||||
error_log( "gitium_error_log: $message" );
|
||||
}
|
||||
|
||||
function wp_content_is_versioned() {
|
||||
return file_exists( WP_CONTENT_DIR . '/.git' );
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'gitium_enable_maintenance_mode' ) ) :
|
||||
function gitium_enable_maintenance_mode() {
|
||||
$file = ABSPATH . '/.maintenance';
|
||||
|
||||
if ( false === file_put_contents( $file, '<?php $upgrading = ' . time() .';' ) ) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
endif;
|
||||
|
||||
if ( ! function_exists( 'gitium_disable_maintenance_mode' ) ) :
|
||||
function gitium_disable_maintenance_mode() {
|
||||
return unlink( ABSPATH . '/.maintenance' );
|
||||
}
|
||||
endif;
|
||||
|
||||
function gitium_get_versions() {
|
||||
$versions = get_transient( 'gitium_versions' );
|
||||
if ( empty( $versions ) ) {
|
||||
$versions = gitium_update_versions();
|
||||
}
|
||||
return $versions;
|
||||
}
|
||||
|
||||
function _gitium_commit_changes( $message, $dir = '.' ) {
|
||||
global $git;
|
||||
|
||||
list( , $git_private_key ) = gitium_get_keypair();
|
||||
if (!$git_private_key)
|
||||
return false;
|
||||
$git->set_key( $git_private_key );
|
||||
|
||||
$git->add( $dir );
|
||||
gitium_update_versions();
|
||||
$current_user = wp_get_current_user();
|
||||
return $git->commit( $message, $current_user->display_name, $current_user->user_email );
|
||||
}
|
||||
|
||||
function _gitium_format_message( $name, $version = false, $prefix = '' ) {
|
||||
$commit_message = "`$name`";
|
||||
if ( $version ) {
|
||||
$commit_message .= " version $version";
|
||||
}
|
||||
if ( $prefix ) {
|
||||
$commit_message = "$prefix $commit_message";
|
||||
}
|
||||
return $commit_message;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function return the basic info about a path.
|
||||
*
|
||||
* base_path - means the path after wp-content dir (themes/plugins)
|
||||
* type - can be file/theme/plugin
|
||||
* name - the file name of the path, if it is a file, or the theme/plugin name
|
||||
* version - the theme/plugin version, othewise null
|
||||
*/
|
||||
/* Some examples:
|
||||
|
||||
with 'wp-content/themes/twentyten/style.css' will return:
|
||||
array(
|
||||
'base_path' => 'wp-content/themes/twentyten'
|
||||
'type' => 'theme'
|
||||
'name' => 'TwentyTen'
|
||||
'version' => '1.12'
|
||||
)
|
||||
|
||||
with 'wp-content/themes/twentyten/img/foo.png' will return:
|
||||
array(
|
||||
'base_path' => 'wp-content/themes/twentyten'
|
||||
'type' => 'theme'
|
||||
'name' => 'TwentyTen'
|
||||
'version' => '1.12'
|
||||
)
|
||||
|
||||
with 'wp-content/plugins/foo.php' will return:
|
||||
array(
|
||||
'base_path' => 'wp-content/plugins/foo.php'
|
||||
'type' => 'plugin'
|
||||
'name' => 'Foo'
|
||||
'varsion' => '2.0'
|
||||
)
|
||||
|
||||
with 'wp-content/plugins/autover/autover.php' will return:
|
||||
array(
|
||||
'base_path' => 'wp-content/plugins/autover'
|
||||
'type' => 'plugin'
|
||||
'name' => 'autover'
|
||||
'version' => '3.12'
|
||||
)
|
||||
|
||||
with 'wp-content/plugins/autover/' will return:
|
||||
array(
|
||||
'base_path' => 'wp-content/plugins/autover'
|
||||
'type' => 'plugin'
|
||||
'name' => 'autover'
|
||||
'version' => '3.12'
|
||||
)
|
||||
*/
|
||||
function _gitium_module_by_path( $path ) {
|
||||
$versions = gitium_get_versions();
|
||||
|
||||
// default values
|
||||
$module = array(
|
||||
'base_path' => $path,
|
||||
'type' => 'file',
|
||||
'name' => basename( $path ),
|
||||
'version' => null,
|
||||
);
|
||||
|
||||
// find the base_path
|
||||
$split_path = explode( '/', $path );
|
||||
if ( 2 < count( $split_path ) ) {
|
||||
$module['base_path'] = "{$split_path[0]}/{$split_path[1]}/{$split_path[2]}";
|
||||
}
|
||||
|
||||
// find other data for theme
|
||||
if ( array_key_exists( 'themes', $versions ) && 0 === strpos( $path, 'wp-content/themes/' ) ) {
|
||||
$module['type'] = 'theme';
|
||||
foreach ( $versions['themes'] as $theme => $data ) {
|
||||
if ( 0 === strpos( $path, "wp-content/themes/$theme" ) ) {
|
||||
$module['name'] = $data['name'];
|
||||
$module['version'] = $data['version'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// find other data for plugin
|
||||
if ( array_key_exists( 'plugins', $versions ) && 0 === strpos( $path, 'wp-content/plugins/' ) ) {
|
||||
$module['type'] = 'plugin';
|
||||
foreach ( $versions['plugins'] as $plugin => $data ) {
|
||||
if ( '.' === dirname( $plugin ) ) { // single file plugin
|
||||
if ( "wp-content/plugins/$plugin" === $path ) {
|
||||
$module['base_path'] = $path;
|
||||
$module['name'] = $data['name'];
|
||||
$module['version'] = $data['version'];
|
||||
break;
|
||||
}
|
||||
} else if ( 'wp-content/plugins/' . dirname( $plugin ) === $module['base_path'] ) {
|
||||
$module['name'] = $data['name'];
|
||||
$module['version'] = $data['version'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $module;
|
||||
}
|
||||
|
||||
function gitium_group_commit_modified_plugins_and_themes( $msg_append = '' ) {
|
||||
global $git;
|
||||
|
||||
$uncommited_changes = $git->get_local_changes();
|
||||
$commit_groups = array();
|
||||
$commits = array();
|
||||
|
||||
if ( ! empty( $msg_append ) ) {
|
||||
$msg_append = "($msg_append)";
|
||||
}
|
||||
foreach ( $uncommited_changes as $path => $action ) {
|
||||
$change = _gitium_module_by_path( $path );
|
||||
$change['action'] = $action;
|
||||
$commit_groups[ $change['base_path'] ] = $change;
|
||||
}
|
||||
|
||||
foreach ( $commit_groups as $base_path => $change ) {
|
||||
$commit_message = _gitium_format_message( $change['name'], $change['version'], "${change['action']} ${change['type']}" );
|
||||
$commit = _gitium_commit_changes( "$commit_message $msg_append", $base_path, false );
|
||||
if ( $commit ) {
|
||||
$commits[] = $commit;
|
||||
}
|
||||
}
|
||||
|
||||
return $commits;
|
||||
}
|
||||
|
||||
function gitium_commit_and_push_gitignore_file( $path = '' ) {
|
||||
global $git;
|
||||
|
||||
$current_user = wp_get_current_user();
|
||||
if ( ! empty( $path ) ) { $git->rm_cached( $path ); }
|
||||
$git->add( '.gitignore' );
|
||||
$commit = $git->commit( 'Update the `.gitignore` file', $current_user->display_name, $current_user->user_email );
|
||||
gitium_merge_and_push( $commit );
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'gitium_acquire_merge_lock' ) ) :
|
||||
function gitium_acquire_merge_lock() {
|
||||
$gitium_lock_path = apply_filters( 'gitium_lock_path', sys_get_temp_dir().'/.gitium-lock' );
|
||||
$gitium_lock_handle = fopen( $gitium_lock_path, 'w+' );
|
||||
|
||||
$lock_timeout = intval( ini_get( 'max_execution_time' ) ) > 10 ? intval( ini_get( 'max_execution_time' ) ) - 5 : 10;
|
||||
$lock_timeout_ms = 10;
|
||||
$lock_retries = 0;
|
||||
while ( ! flock( $gitium_lock_handle, LOCK_EX | LOCK_NB ) ) {
|
||||
usleep( $lock_timeout_ms * 1000 );
|
||||
$lock_retries++;
|
||||
if ( $lock_retries * $lock_timeout_ms > $lock_timeout * 1000 ) {
|
||||
return false; // timeout
|
||||
}
|
||||
}
|
||||
gitium_error_log( __FUNCTION__ );
|
||||
return array( $gitium_lock_path, $gitium_lock_handle );
|
||||
}
|
||||
endif;
|
||||
|
||||
if ( ! function_exists( 'gitium_release_merge_lock' ) ) :
|
||||
function gitium_release_merge_lock( $lock ) {
|
||||
list( $gitium_lock_path, $gitium_lock_handle ) = $lock;
|
||||
gitium_error_log( __FUNCTION__ );
|
||||
flock( $gitium_lock_handle, LOCK_UN );
|
||||
fclose( $gitium_lock_handle );
|
||||
}
|
||||
endif;
|
||||
|
||||
// Merges the commits with remote and pushes them back
|
||||
function gitium_merge_and_push( $commits ) {
|
||||
global $git;
|
||||
|
||||
$lock = gitium_acquire_merge_lock()
|
||||
or trigger_error( 'Timeout when gitium lock was acquired', E_USER_WARNING );
|
||||
|
||||
if ( ! $git->fetch_ref() ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$merge_status = $git->merge_with_accept_mine( $commits );
|
||||
|
||||
gitium_release_merge_lock( $lock );
|
||||
|
||||
return $git->push() && $merge_status;
|
||||
}
|
||||
|
||||
function gitium_check_after_event( $plugin, $event = 'activation' ) {
|
||||
global $git;
|
||||
|
||||
if ( 'gitium/gitium.php' == $plugin ) { return; } // do not hook on activation of this plugin
|
||||
|
||||
if ( $git->is_dirty() ) {
|
||||
$versions = gitium_update_versions();
|
||||
if ( isset( $versions['plugins'][ $plugin ] ) ) {
|
||||
$name = $versions['plugins'][ $plugin ]['name'];
|
||||
$version = $versions['plugins'][ $plugin ]['version'];
|
||||
} else {
|
||||
$name = $plugin;
|
||||
}
|
||||
gitium_auto_push( _gitium_format_message( $name, $version, "after $event of" ) );
|
||||
}
|
||||
}
|
||||
|
||||
function gitium_update_remote_tracking_branch() {
|
||||
global $git;
|
||||
$remote_branch = $git->get_remote_tracking_branch();
|
||||
set_transient( 'gitium_remote_tracking_branch', $remote_branch );
|
||||
|
||||
return $remote_branch;
|
||||
}
|
||||
|
||||
function _gitium_get_remote_tracking_branch( $update_transient = false ) {
|
||||
if ( ! $update_transient && ( false !== ( $remote_branch = get_transient( 'gitium_remote_tracking_branch' ) ) ) ) {
|
||||
return $remote_branch;
|
||||
} else {
|
||||
return gitium_update_remote_tracking_branch();
|
||||
}
|
||||
}
|
||||
|
||||
function gitium_update_is_status_working() {
|
||||
global $git;
|
||||
$is_status_working = $git->is_status_working();
|
||||
set_transient( 'gitium_is_status_working', $is_status_working );
|
||||
return $is_status_working;
|
||||
}
|
||||
|
||||
function _gitium_is_status_working( $update_transient = false ) {
|
||||
if ( ! $update_transient && ( false !== ( $is_status_working = get_transient( 'gitium_is_status_working' ) ) ) ) {
|
||||
return $is_status_working;
|
||||
} else {
|
||||
return gitium_update_is_status_working();
|
||||
}
|
||||
}
|
||||
|
||||
function _gitium_status( $update_transient = false ) {
|
||||
global $git;
|
||||
|
||||
if ( ! $update_transient && ( false !== ( $changes = get_transient( 'gitium_uncommited_changes' ) ) ) ) {
|
||||
return $changes;
|
||||
}
|
||||
|
||||
$git_version = get_transient( 'gitium_git_version' );
|
||||
if ( false === $git_version ) {
|
||||
set_transient( 'gitium_git_version', $git->get_version() );
|
||||
}
|
||||
|
||||
if ( $git->is_status_working() && $git->get_remote_tracking_branch() ) {
|
||||
if ( ! $git->fetch_ref() ) {
|
||||
set_transient( 'gitium_remote_disconnected', $git->get_last_error() );
|
||||
} else {
|
||||
delete_transient( 'gitium_remote_disconnected' );
|
||||
}
|
||||
$changes = $git->status();
|
||||
} else {
|
||||
delete_transient( 'gitium_remote_disconnected' );
|
||||
$changes = array();
|
||||
}
|
||||
|
||||
set_transient( 'gitium_uncommited_changes', $changes, 12 * 60 * 60 ); // cache changes for half-a-day
|
||||
return $changes;
|
||||
}
|
||||
|
||||
function _gitium_ssh_encode_buffer( $buffer ) {
|
||||
$len = strlen( $buffer );
|
||||
if ( ord( $buffer[0] ) & 0x80 ) {
|
||||
$len++;
|
||||
$buffer = "\x00" . $buffer;
|
||||
}
|
||||
return pack( 'Na*', $len, $buffer );
|
||||
}
|
||||
|
||||
function _gitium_generate_keypair() {
|
||||
$rsa_key = openssl_pkey_new(
|
||||
array(
|
||||
'private_key_bits' => 2048,
|
||||
'private_key_type' => OPENSSL_KEYTYPE_RSA,
|
||||
)
|
||||
);
|
||||
|
||||
try {
|
||||
$private_key = openssl_pkey_get_private( $rsa_key );
|
||||
$try = openssl_pkey_export( $private_key, $pem ); //Private Key
|
||||
if (!$try)
|
||||
return false;
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$key_info = openssl_pkey_get_details( $rsa_key );
|
||||
$buffer = pack( 'N', 7 ) . 'ssh-rsa' .
|
||||
_gitium_ssh_encode_buffer( $key_info['rsa']['e'] ) .
|
||||
_gitium_ssh_encode_buffer( $key_info['rsa']['n'] );
|
||||
$public_key = 'ssh-rsa ' . base64_encode( $buffer ) . ' gitium@' . parse_url( get_home_url(), PHP_URL_HOST );
|
||||
|
||||
return array( $public_key, $pem );
|
||||
}
|
||||
|
||||
function gitium_get_keypair( $generate_new_keypair = false ) {
|
||||
if ( $generate_new_keypair ) {
|
||||
$keypair = _gitium_generate_keypair();
|
||||
delete_option( 'gitium_keypair' );
|
||||
add_option( 'gitium_keypair', $keypair, '', false );
|
||||
}
|
||||
if ( false === ( $keypair = get_option( 'gitium_keypair', false ) ) ) {
|
||||
$keypair = _gitium_generate_keypair();
|
||||
add_option( 'gitium_keypair', $keypair, '', false );
|
||||
}
|
||||
return $keypair;
|
||||
}
|
||||
|
||||
function _gitium_generate_webhook_key() {
|
||||
return md5( str_shuffle( 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.()[]{}-_=+!@#%^&*~<>:;' ) );
|
||||
}
|
||||
|
||||
function gitium_get_webhook_key( $generate_new_webhook_key = false ) {
|
||||
if ( $generate_new_webhook_key ) {
|
||||
$key = _gitium_generate_webhook_key();
|
||||
delete_option( 'gitium_webhook_key' );
|
||||
add_option( 'gitium_webhook_key', $key, '', false );
|
||||
return $key;
|
||||
}
|
||||
if ( false === ( $key = get_option( 'gitium_webhook_key', false ) ) ) {
|
||||
$key = _gitium_generate_webhook_key();
|
||||
add_option( 'gitium_webhook_key', $key, '', false );
|
||||
}
|
||||
return $key;
|
||||
}
|
||||
|
||||
function gitium_get_webhook() {
|
||||
if ( defined( 'GIT_WEBHOOK_URL' ) && GIT_WEBHOOK_URL ) { return GIT_WEBHOOK_URL; }
|
||||
$key = gitium_get_webhook_key();
|
||||
$url = add_query_arg( 'key', $key, plugins_url( 'gitium-webhook.php', __FILE__ ) );
|
||||
return apply_filters( 'gitium_webhook_url', $url, $key );
|
||||
}
|
||||
|
||||
function gitium_admin_init() {
|
||||
global $git;
|
||||
|
||||
$git_version = get_transient( 'gitium_git_version' );
|
||||
if ( false === $git_version ) {
|
||||
set_transient( 'gitium_git_version', $git->get_version() );
|
||||
}
|
||||
}
|
||||
add_action( 'admin_init', 'gitium_admin_init' );
|
@ -0,0 +1,49 @@
|
||||
<?php
|
||||
/* Copyright 2014-2016 Presslabs SRL <ping@presslabs.com>
|
||||
|
||||
This program is free software; you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License, version 2, as
|
||||
published by the Free Software Foundation.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program; if not, write to the Free Software
|
||||
Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
|
||||
*/
|
||||
|
||||
header( 'Content-Type: text/html' );
|
||||
define( 'SHORTINIT', true );
|
||||
//$wordpress_loader = $_SERVER['DOCUMENT_ROOT'] . '/wp-load.php';
|
||||
$wordpress_loader = filter_input(INPUT_SERVER, 'DOCUMENT_ROOT', FILTER_SANITIZE_STRING) . '/wp-load.php';
|
||||
|
||||
require_once $wordpress_loader;
|
||||
require_once __DIR__ . '/functions.php';
|
||||
require_once __DIR__ . '/inc/class-git-wrapper.php';
|
||||
|
||||
$webhook_key = get_option( 'gitium_webhook_key', '' );
|
||||
$get_key = filter_input(INPUT_GET, 'key', FILTER_SANITIZE_STRING);
|
||||
if ( ! empty ( $webhook_key ) && isset( $get_key ) && $webhook_key == $get_key ) :
|
||||
( '1.7' <= substr( $git->get_version(), 0, 3 ) ) or wp_die( 'Gitium plugin require minimum `git version 1.7`!' );
|
||||
|
||||
list( $git_public_key, $git_private_key ) = gitium_get_keypair();
|
||||
if ( ! $git_public_key || ! $git_private_key )
|
||||
wp_die('Not ready.', 'Not ready.', array( 'response' => 403 ));
|
||||
else
|
||||
$git->set_key( $git_private_key );
|
||||
|
||||
$commits = array();
|
||||
$commitmsg = sprintf( 'Merged changes from %s on %s', $_SERVER['SERVER_NAME'], date( 'm.d.Y' ) );
|
||||
|
||||
if ( $git->is_dirty() && $git->add() > 0 ) {
|
||||
$commits[] = $git->commit( $commitmsg ) or trigger_error( 'Could not commit local changes!', E_USER_ERROR );
|
||||
}
|
||||
gitium_merge_and_push( $commits ) or trigger_error( 'Failed merge & push: ' . serialize( $git->get_last_error() ), E_USER_ERROR );
|
||||
|
||||
wp_die( $commitmsg , 'Pull done!', array( 'response' => 200 ) );
|
||||
else :
|
||||
wp_die( 'Cheating uh?', 'Cheating uh?', array( 'response' => 403 ) );
|
||||
endif;
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user