modified file plugins
This commit is contained in:
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
< | ||||