Compare commits
45 commits
main
...
add/manual
Author | SHA1 | Date | |
---|---|---|---|
03b6a8e598 | |||
616667b0ba | |||
33b28b1635 | |||
a3a918c54c | |||
54c18b2ac9 | |||
de3335bcda | |||
|
e915b7af8b | ||
|
a8078ce72b | ||
|
a9c65f55d6 | ||
|
3cda64a255 | ||
|
f2b231ccac | ||
|
b744dc551d | ||
|
a3ea9955d9 | ||
|
45e6ace890 | ||
|
5ce8f28852 | ||
|
f4cab6647d | ||
|
6f1a9a1f7f | ||
|
39c9288987 | ||
|
a47c9cd7ae | ||
|
97e2bbfe7a | ||
|
ae26609e5e | ||
|
a2e5fc2021 | ||
|
9f8bad3e8d | ||
|
77c508059b | ||
|
431c4a2676 | ||
|
e5fe4f20b7 | ||
|
93b2f1ee7d | ||
|
ef96008cb3 | ||
|
a59408d6b8 | ||
|
24c534961a | ||
|
db846729db | ||
|
cf541b41b4 | ||
|
8c93d36d95 | ||
|
15179f2c5a | ||
|
eecdb63da3 | ||
|
5d417d9f5c | ||
|
ca9e71ffc1 | ||
|
c3a18d72dd | ||
|
c3d5b4bb1a | ||
|
9bd8659e97 | ||
|
6e7f82bf42 | ||
|
4a72329bbe | ||
|
cffff80b54 | ||
|
1f5ef3ea2c | ||
|
e529b19b8b |
94 changed files with 4445 additions and 2032 deletions
|
@ -14,6 +14,7 @@ The WordPress plugin largely follows ActivityPub's server-to-server specificatio
|
||||||
- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)
|
- [FEP-f1d5: NodeInfo in Fediverse Software](https://codeberg.org/fediverse/fep/src/branch/main/fep/f1d5/fep-f1d5.md)
|
||||||
- [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md)
|
- [FEP-67ff: FEDERATION.md](https://codeberg.org/fediverse/fep/src/branch/main/fep/67ff/fep-67ff.md)
|
||||||
- [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md)
|
- [FEP-5feb: Search indexing consent for actors](https://codeberg.org/fediverse/fep/src/branch/main/fep/5feb/fep-5feb.md)
|
||||||
|
- [FEP-2677: Identifying the Application Actor](https://codeberg.org/fediverse/fep/src/branch/main/fep/2677/fep-2677.md)
|
||||||
|
|
||||||
Partially supported FEPs
|
Partially supported FEPs
|
||||||
|
|
||||||
|
|
27
README.md
27
README.md
|
@ -1,9 +1,9 @@
|
||||||
# ActivityPub #
|
# ActivityPub #
|
||||||
**Contributors:** [automattic](https://profiles.wordpress.org/automattic/), [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [mattwiebe](https://profiles.wordpress.org/mattwiebe/), [akirk](https://profiles.wordpress.org/akirk/), [jeherve](https://profiles.wordpress.org/jeherve/), [nuriapena](https://profiles.wordpress.org/nuriapena/), [cavalierlife](https://profiles.wordpress.org/cavalierlife/)
|
**Contributors:** [automattic](https://profiles.wordpress.org/automattic/), [pfefferle](https://profiles.wordpress.org/pfefferle/), [mediaformat](https://profiles.wordpress.org/mediaformat/), [mattwiebe](https://profiles.wordpress.org/mattwiebe/), [akirk](https://profiles.wordpress.org/akirk/), [jeherve](https://profiles.wordpress.org/jeherve/), [nuriapena](https://profiles.wordpress.org/nuriapena/), [cavalierlife](https://profiles.wordpress.org/cavalierlife/)
|
||||||
**Tags:** OStatus, fediverse, activitypub, activitystream
|
**Tags:** OStatus, fediverse, activitypub, activitystream
|
||||||
**Requires at least:** 4.7
|
**Requires at least:** 5.5
|
||||||
**Tested up to:** 6.4
|
**Tested up to:** 6.4
|
||||||
**Stable tag:** 1.2.0
|
**Stable tag:** 1.3.0
|
||||||
**Requires PHP:** 5.6
|
**Requires PHP:** 5.6
|
||||||
**License:** MIT
|
**License:** MIT
|
||||||
**License URI:** http://opensource.org/licenses/MIT
|
**License URI:** http://opensource.org/licenses/MIT
|
||||||
|
@ -68,10 +68,10 @@ Implemented:
|
||||||
* share posts
|
* share posts
|
||||||
* receive comments/reactions
|
* receive comments/reactions
|
||||||
* signature verification
|
* signature verification
|
||||||
|
* threaded comments support
|
||||||
|
|
||||||
To implement:
|
To implement:
|
||||||
|
|
||||||
* threaded comments support
|
|
||||||
* replace shortcodes with blocks for layout
|
* replace shortcodes with blocks for layout
|
||||||
|
|
||||||
### What is "ActivityPub for WordPress" ###
|
### What is "ActivityPub for WordPress" ###
|
||||||
|
@ -105,6 +105,27 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
|
||||||
|
|
||||||
Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub).
|
Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub).
|
||||||
|
|
||||||
|
### 2.0.0 ###
|
||||||
|
|
||||||
|
* Removed: Deprecated Classes
|
||||||
|
* Fixed: Normalize attributes that can have mixed value types
|
||||||
|
* Added: URL support for WebFinger
|
||||||
|
* Added: Make Post-Template filterable
|
||||||
|
* Added: CSS class for ActivityPub comments to allow custom designs
|
||||||
|
* Added: FEP-2677: Identifying the Application Actor
|
||||||
|
* Added: Basic Comment Federation
|
||||||
|
* Added: Profile Update Activities
|
||||||
|
* Improved: WebFinger endpoints
|
||||||
|
|
||||||
|
### 1.3.0 ###
|
||||||
|
|
||||||
|
* Added: Threaded-Comments support
|
||||||
|
* Improved: alt text for avatars in Follow Me/Followers blocks
|
||||||
|
* Improved: `Delete`, `Update` and `Follow` Activities
|
||||||
|
* Improved: better/more effective handling of `Delete` Activities
|
||||||
|
* Improved: allow `<p />` and `<br />` for Comments
|
||||||
|
* Fixed: removed default limit of WP_Query to send updates to all Inboxes and not only to the first 10
|
||||||
|
|
||||||
### 1.2.0 ###
|
### 1.2.0 ###
|
||||||
|
|
||||||
* Add: Search and order followerer lists
|
* Add: Search and order followerer lists
|
||||||
|
|
|
@ -3,7 +3,7 @@
|
||||||
* Plugin Name: ActivityPub
|
* Plugin Name: ActivityPub
|
||||||
* Plugin URI: https://github.com/pfefferle/wordpress-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.
|
* Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.
|
||||||
* Version: 1.2.0
|
* Version: 1.3.0
|
||||||
* Author: Matthias Pfefferle & Automattic
|
* Author: Matthias Pfefferle & Automattic
|
||||||
* Author URI: https://automattic.com/
|
* Author URI: https://automattic.com/
|
||||||
* License: MIT
|
* License: MIT
|
||||||
|
@ -33,15 +33,13 @@ require_once __DIR__ . '/includes/functions.php';
|
||||||
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "<strong>[ap_title]</strong>\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" );
|
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "<strong>[ap_title]</strong>\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" );
|
||||||
\defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false );
|
\defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false );
|
||||||
\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false );
|
\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false );
|
||||||
\defined( 'ACTIVITYPUB_DEFAULT_TRANSFORMER' ) || \define( 'ACTIVITYPUB_DEFAULT_TRANSFORMER', 'activitypub/default' );
|
\defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) || \define( 'ACTIVITYPUB_SHARED_INBOX_FEATURE', false );
|
||||||
|
|
||||||
|
|
||||||
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||||
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||||
\define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) );
|
\define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) );
|
||||||
\define( 'ACTIVITYPUB_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
\define( 'ACTIVITYPUB_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize REST routes.
|
* Initialize REST routes.
|
||||||
*/
|
*/
|
||||||
|
@ -69,7 +67,7 @@ function plugin_init() {
|
||||||
\add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) );
|
\add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) );
|
||||||
\add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) );
|
\add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) );
|
||||||
\add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) );
|
\add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) );
|
||||||
\add_action( 'init', array( __NAMESPACE__ . '\Collection\Followers', 'init' ) );
|
\add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) );
|
||||||
\add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) );
|
\add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) );
|
||||||
\add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) );
|
\add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) );
|
||||||
\add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) );
|
\add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) );
|
||||||
|
@ -181,6 +179,14 @@ add_action(
|
||||||
0
|
0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
add_action(
|
||||||
|
'wp_ajax_activitypub_handle_follow_request',
|
||||||
|
function () {
|
||||||
|
$wp_list_table = new \Activitypub\Table\Follow_Requests();
|
||||||
|
$wp_list_table->ajax_response();
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* `get_plugin_data` wrapper
|
* `get_plugin_data` wrapper
|
||||||
*
|
*
|
||||||
|
|
|
@ -197,3 +197,36 @@ input.blog-user-identifier {
|
||||||
border-bottom: none;
|
border-bottom: none;
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.activitypub-settings-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: .25em .4em;
|
||||||
|
font-size: 85%;
|
||||||
|
font-weight: 700;
|
||||||
|
line-height: 1.25;
|
||||||
|
text-align: center;
|
||||||
|
white-space: nowrap;
|
||||||
|
vertical-align: baseline;
|
||||||
|
border-radius: .25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activitypub-settings-label-success {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #28a745;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activitypub-settings-label-warning {
|
||||||
|
color: #212529;
|
||||||
|
background-color: #ffc107;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activitypub-settings-label-danger {
|
||||||
|
color: #fff;
|
||||||
|
background-color: #dc3545;
|
||||||
|
}
|
||||||
|
|
||||||
|
.activitypub-settings-action-buttons {
|
||||||
|
display: flex;
|
||||||
|
gap: 5px;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
}
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
jQuery( function( $ ) {
|
jQuery( function( $ ) {
|
||||||
|
const { __ } = wp.i18n;
|
||||||
// Accordion handling in various areas.
|
// Accordion handling in various areas.
|
||||||
$( '.activitypub-settings-accordion' ).on( 'click', '.activitypub-settings-accordion-trigger', function() {
|
$( '.activitypub-settings-accordion' ).on( 'click', '.activitypub-settings-accordion-trigger', function() {
|
||||||
var isExpanded = ( 'true' === $( this ).attr( 'aria-expanded' ) );
|
var isExpanded = ( 'true' === $( this ).attr( 'aria-expanded' ) );
|
||||||
|
@ -17,4 +18,51 @@ jQuery( function( $ ) {
|
||||||
$( '.activate-now' ).removeClass( 'thickbox open-plugin-details-modal' );
|
$( '.activate-now' ).removeClass( 'thickbox open-plugin-details-modal' );
|
||||||
}, 1200 );
|
}, 1200 );
|
||||||
} );
|
} );
|
||||||
|
|
||||||
|
$( '.activitypub-settings-action-buttons' ).on( 'click', '.button', function() {
|
||||||
|
var button = $ (this );
|
||||||
|
var actionValue = button.data('action');
|
||||||
|
window.console.log( actionValue );
|
||||||
|
$.ajax({
|
||||||
|
type: 'POST',
|
||||||
|
url: actionValue,
|
||||||
|
success: function ( response ) {
|
||||||
|
var statusText = button.closest( 'td' ).siblings( '.column-status' ).children( 'span' ).first();
|
||||||
|
if ( 'deleted' === response ) {
|
||||||
|
button.closest( 'tr' ).remove();
|
||||||
|
}
|
||||||
|
if ( 'approved' === response ) {
|
||||||
|
button.parent().find( '[data-action*="follow_action=reject"] ').attr( 'type', 'button' );
|
||||||
|
button.parent().find( '[data-action*="follow_action=delete"]' ).attr( 'type', 'hidden' );
|
||||||
|
statusText.text( __( 'Approved', 'activitypub' ) );
|
||||||
|
statusText.removeClass( 'activitypub-settings-label-danger' );
|
||||||
|
statusText.removeClass( 'activitypub-settings-label-warning' );
|
||||||
|
statusText.addClass( 'activitypub-settings-label-success' );
|
||||||
|
}
|
||||||
|
if ( 'rejected' === response ) {
|
||||||
|
// TODO: clarify this behavior together with Mobilizon and others.
|
||||||
|
button.closest( 'tr' ).remove();
|
||||||
|
// statusText.text( __( 'Rejected', 'activitypub' ) );
|
||||||
|
// statusText.removeClass( 'activitypub-settings-label-success' );
|
||||||
|
// statusText.removeClass( 'activitypub-settings-label-warning' );
|
||||||
|
// statusText.addClass( 'activitypub-settings-label-danger' );
|
||||||
|
// button.parent().find( '[data-action*="follow_action=approve"]' ).attr( 'type', 'button' );
|
||||||
|
// button.parent().find( '[data-action*="follow_action=delete"]' ).attr( 'type', 'button' );
|
||||||
|
}
|
||||||
|
button.attr( 'type', 'hidden' );
|
||||||
|
// Check if table is completely empty.
|
||||||
|
var tbody = button.closest( 'tbody' );
|
||||||
|
if ( 0 == tbody.find( 'tr' ).length ) {
|
||||||
|
var text = __( 'No items found.', 'core' );
|
||||||
|
var newRow = $('<tr>').append($('<td>', { class: 'colspanchange', colspan: 7, text: text }));
|
||||||
|
tbody.append(newRow);
|
||||||
|
tbody.append("Some appended text.");
|
||||||
|
}
|
||||||
|
},
|
||||||
|
error: function ( error ) {
|
||||||
|
// TODO: Handle the error
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} );
|
||||||
|
|
||||||
} );
|
} );
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
<?php return array('dependencies' => array('wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '2a185b1c488886051601');
|
<?php return array( 'dependencies' => array( 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives' ), 'version' => '6aeec6336fd28aa836a7' );
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
||||||
<?php return array('dependencies' => array('wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '17a158ceced1355cc8ea');
|
<?php return array( 'dependencies' => array( 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives' ), 'version' => '5b48281e37700a970a66' );
|
||||||
|
|
File diff suppressed because one or more lines are too long
|
@ -1 +1 @@
|
||||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => '1cbd9cbfcbd7fc813429');
|
<?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' => '59d9702e06860a6d13e4' );
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
(()=>{var e={184:(e,t)=>{var a;!function(){"use strict";var n={}.hasOwnProperty;function l(){for(var e=[],t=0;t<arguments.length;t++){var a=arguments[t];if(a){var r=typeof a;if("string"===r||"number"===r)e.push(a);else if(Array.isArray(a)){if(a.length){var o=l.apply(null,a);o&&e.push(o)}}else if("object"===r){if(a.toString!==Object.prototype.toString&&!a.toString.toString().includes("[native code]")){e.push(a.toString());continue}for(var i in a)n.call(a,i)&&a[i]&&e.push(i)}}}return e.join(" ")}e.exports?(l.default=l,e.exports=l):void 0===(a=function(){return l}.apply(t,[]))||(e.exports=a)}()}},t={};function a(n){var l=t[n];if(void 0!==l)return l.exports;var r=t[n]={exports:{}};return e[n](r,r.exports,a),r.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 n in t)a.o(t,n)&&!a.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.element,n=window.wp.primitives,l=(0,t.createElement)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,t.createElement)(n.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"}));function r(){return r=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},r.apply(this,arguments)}const o=window.wp.components,i=window.wp.blockEditor,c=window.wp.i18n,s=window.React,p=window.wp.apiFetch;var u=a.n(p);const v=window.wp.url;var m=a(184),b=a.n(m);function w(e){let{active:a,children:n,page:l,pageClick:r,className:o}=e;const i=b()("wp-block activitypub-pager",o,{current:a});return(0,t.createElement)("a",{className:i,onClick:e=>{e.preventDefault(),!a&&r(l)}},n)}const d={outlined:"outlined",minimal:"minimal"};function f(e){let{compact:a,nextLabel:n,page:l,pageClick:r,perPage:o,prevLabel:i,total:c,variant:s=d.outlined}=e;const p=((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,n)=>e>=1&&e<=t&&n.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(l,Math.ceil(c/o)),u=b()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":a});return(0,t.createElement)("nav",{className:u},i&&(0,t.createElement)(w,{key:"prev",page:l-1,pageClick:r,active:1===l,"aria-label":i,className:"wp-block-query-pagination-previous block-editor-block-list__block"},i),!a&&(0,t.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},p.map((e=>(0,t.createElement)(w,{key:e,page:e,pageClick:r,active:e===l,className:"page-numbers"},e)))),n&&(0,t.createElement)(w,{key:"next",page:l+1,pageClick:r,active:l===Math.ceil(c/o),"aria-label":n,className:"wp-block-query-pagination-next block-editor-block-list__block"},n))}const{namespace:g}=window._activityPubOptions;function y(e){let{selectedUser:a,per_page:n,order:l,title:o,page:i,setPage:p,className:m="",followLinks:b=!0,followerData:w=!1}=e;const d="site"===a?0:a,[y,k]=(0,s.useState)([]),[E,_]=(0,s.useState)(0),[x,C]=(0,s.useState)(0),[S,O]=function(){const[e,t]=(0,s.useState)(1);return[e,t]}(),N=i||S,P=p||O,L=(0,t.createInterpolateElement)(/* translators: arrow for previous followers link */
|
(()=>{var e={184:(e,t)=>{var a;!function(){"use strict";var n={}.hasOwnProperty;function l(){for(var e=[],t=0;t<arguments.length;t++){var a=arguments[t];if(a){var r=typeof a;if("string"===r||"number"===r)e.push(a);else if(Array.isArray(a)){if(a.length){var o=l.apply(null,a);o&&e.push(o)}}else if("object"===r){if(a.toString!==Object.prototype.toString&&!a.toString.toString().includes("[native code]")){e.push(a.toString());continue}for(var i in a)n.call(a,i)&&a[i]&&e.push(i)}}}return e.join(" ")}e.exports?(l.default=l,e.exports=l):void 0===(a=function(){return l}.apply(t,[]))||(e.exports=a)}()}},t={};function a(n){var l=t[n];if(void 0!==l)return l.exports;var r=t[n]={exports:{}};return e[n](r,r.exports,a),r.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 n in t)a.o(t,n)&&!a.o(e,n)&&Object.defineProperty(e,n,{enumerable:!0,get:t[n]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.element,n=window.wp.primitives,l=(0,t.createElement)(n.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24"},(0,t.createElement)(n.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"}));function r(){return r=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var n in a)Object.prototype.hasOwnProperty.call(a,n)&&(e[n]=a[n])}return e},r.apply(this,arguments)}const o=window.wp.components,i=window.wp.blockEditor,c=window.wp.i18n,s=window.React,p=window.wp.apiFetch;var u=a.n(p);const v=window.wp.url;var m=a(184),b=a.n(m);function w(e){let{active:a,children:n,page:l,pageClick:r,className:o}=e;const i=b()("wp-block activitypub-pager",o,{current:a});return(0,t.createElement)("a",{className:i,onClick:e=>{e.preventDefault(),!a&&r(l)}},n)}const d={outlined:"outlined",minimal:"minimal"};function f(e){let{compact:a,nextLabel:n,page:l,pageClick:r,perPage:o,prevLabel:i,total:c,variant:s=d.outlined}=e;const p=((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,n)=>e>=1&&e<=t&&n.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(l,Math.ceil(c/o)),u=b()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":a});return(0,t.createElement)("nav",{className:u},i&&(0,t.createElement)(w,{key:"prev",page:l-1,pageClick:r,active:1===l,"aria-label":i,className:"wp-block-query-pagination-previous block-editor-block-list__block"},i),!a&&(0,t.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},p.map((e=>(0,t.createElement)(w,{key:e,page:e,pageClick:r,active:e===l,className:"page-numbers"},e)))),n&&(0,t.createElement)(w,{key:"next",page:l+1,pageClick:r,active:l===Math.ceil(c/o),"aria-label":n,className:"wp-block-query-pagination-next block-editor-block-list__block"},n))}const{namespace:g}=window._activityPubOptions;function y(e){let{selectedUser:a,per_page:n,order:l,title:o,page:i,setPage:p,className:m="",followLinks:b=!0,followerData:w=!1}=e;const d="site"===a?0:a,[y,k]=(0,s.useState)([]),[E,_]=(0,s.useState)(0),[x,C]=(0,s.useState)(0),[S,O]=function(){const[e,t]=(0,s.useState)(1);return[e,t]}(),N=i||S,P=p||O,L=(0,t.createInterpolateElement)(/* translators: arrow for previous followers link */
|
||||||
(0,c.__)("<span>←</span> Less","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),j=(0,t.createInterpolateElement)(/* translators: arrow for next followers link */
|
(0,c.__)("<span>←</span> Less","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),j=(0,t.createInterpolateElement)(/* translators: arrow for next followers link */
|
||||||
(0,c.__)("More <span>→</span>","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),M=(e,t)=>{k(e),C(t),_(Math.ceil(t/n))};return(0,s.useEffect)((()=>{if(w&&1===N)return M(w.followers,w.total);const e=function(e,t,a,n){const l=`/${g}/users/${e}/followers`,r={per_page:t,order:a,page:n,context:"full"};return(0,v.addQueryArgs)(l,r)}(d,n,l,N);u()({path:e}).then((e=>M(e.orderedItems,e.totalItems))).catch((()=>{}))}),[d,n,l,N,w]),(0,t.createElement)("div",{className:"activitypub-follower-block "+m},(0,t.createElement)("h3",null,o),(0,t.createElement)("ul",null,y&&y.map((e=>(0,t.createElement)("li",{key:e.url},(0,t.createElement)(h,r({},e,{followLinks:b})))))),E>1&&(0,t.createElement)(f,{page:N,perPage:n,total:x,pageClick:P,nextLabel:j,prevLabel:L,compact:"is-style-compact"===m}))}function h(e){let{name:a,icon:n,url:l,preferredUsername:i,followLinks:c=!0}=e;const s=`@${i}`,p={};return c||(p.onClick=e=>e.preventDefault()),(0,t.createElement)(o.ExternalLink,r({className:"activitypub-link",href:l,title:s},p),(0,t.createElement)("img",{width:"40",height:"40",src:n.url,class:"avatar activitypub-avatar"}),(0,t.createElement)("span",{class:"activitypub-actor"},(0,t.createElement)("strong",{className:"activitypub-name"},a),(0,t.createElement)("span",{class:"sep"},"/"),(0,t.createElement)("span",{class:"activitypub-handle"},s)))}const k=window.wp.data,E=window._activityPubOptions?.enabled;(0,e.registerBlockType)("activitypub/followers",{edit:function(e){let{attributes:a,setAttributes:n}=e;const{order:l,per_page:s,selectedUser:p,title:u}=a,v=(0,i.useBlockProps)(),[m,b]=(0,t.useState)(1),w=[{label:(0,c.__)("New to old","activitypub"),value:"desc"},{label:(0,c.__)("Old to new","activitypub"),value:"asc"}],d=function(){const e=E?.users?(0,k.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,t.useMemo)((()=>{if(!e)return[];const t=E?.site?[{label:(0,c.__)("Whole Site","activitypub"),value:"site"}]:[];return e.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),t)}),[e])}(),f=e=>t=>{b(1),n({[e]:t})};return(0,t.useEffect)((()=>{d.length&&(d.find((e=>{let{value:t}=e;return t===p}))||n({selectedUser:d[0].value}))}),[p,d]),(0,t.createElement)("div",v,(0,t.createElement)(i.InspectorControls,{key:"setting"},(0,t.createElement)(o.PanelBody,{title:(0,c.__)("Followers Options","activitypub")},(0,t.createElement)(o.TextControl,{label:(0,c.__)("Title","activitypub"),help:(0,c.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:u,onChange:e=>n({title:e})}),d.length>1&&(0,t.createElement)(o.SelectControl,{label:(0,c.__)("Select User","activitypub"),value:p,options:d,onChange:f("selectedUser")}),(0,t.createElement)(o.SelectControl,{label:(0,c.__)("Sort","activitypub"),value:l,options:w,onChange:f("order")}),(0,t.createElement)(o.RangeControl,{label:(0,c.__)("Number of Followers","activitypub"),value:s,onChange:f("per_page"),min:1,max:10}))),(0,t.createElement)(y,r({},a,{page:m,setPage:b,followLinks:!1})))},save:()=>null,icon:l})})()})();
|
(0,c.__)("More <span>→</span>","activitypub"),{span:(0,t.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),M=(e,t)=>{k(e),C(t),_(Math.ceil(t/n))};return(0,s.useEffect)((()=>{if(w&&1===N)return M(w.followers,w.total);const e=function(e,t,a,n){const l=`/${g}/users/${e}/followers`,r={per_page:t,order:a,page:n,context:"full"};return(0,v.addQueryArgs)(l,r)}(d,n,l,N);u()({path:e}).then((e=>M(e.orderedItems,e.totalItems))).catch((()=>{}))}),[d,n,l,N,w]),(0,t.createElement)("div",{className:"activitypub-follower-block "+m},(0,t.createElement)("h3",null,o),(0,t.createElement)("ul",null,y&&y.map((e=>(0,t.createElement)("li",{key:e.url},(0,t.createElement)(h,r({},e,{followLinks:b})))))),E>1&&(0,t.createElement)(f,{page:N,perPage:n,total:x,pageClick:P,nextLabel:j,prevLabel:L,compact:"is-style-compact"===m}))}function h(e){let{name:a,icon:n,url:l,preferredUsername:i,followLinks:c=!0}=e;const s=`@${i}`,p={};return c||(p.onClick=e=>e.preventDefault()),(0,t.createElement)(o.ExternalLink,r({className:"activitypub-link",href:l,title:s},p),(0,t.createElement)("img",{width:"40",height:"40",src:n.url,class:"avatar activitypub-avatar",alt:a}),(0,t.createElement)("span",{class:"activitypub-actor"},(0,t.createElement)("strong",{className:"activitypub-name"},a),(0,t.createElement)("span",{class:"sep"},"/"),(0,t.createElement)("span",{class:"activitypub-handle"},s)))}const k=window.wp.data,E=window._activityPubOptions?.enabled;(0,e.registerBlockType)("activitypub/followers",{edit:function(e){let{attributes:a,setAttributes:n}=e;const{order:l,per_page:s,selectedUser:p,title:u}=a,v=(0,i.useBlockProps)(),[m,b]=(0,t.useState)(1),w=[{label:(0,c.__)("New to old","activitypub"),value:"desc"},{label:(0,c.__)("Old to new","activitypub"),value:"asc"}],d=function(){const e=E?.users?(0,k.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,t.useMemo)((()=>{if(!e)return[];const t=E?.site?[{label:(0,c.__)("Whole Site","activitypub"),value:"site"}]:[];return e.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),t)}),[e])}(),f=e=>t=>{b(1),n({[e]:t})};return(0,t.useEffect)((()=>{d.length&&(d.find((e=>{let{value:t}=e;return t===p}))||n({selectedUser:d[0].value}))}),[p,d]),(0,t.createElement)("div",v,(0,t.createElement)(i.InspectorControls,{key:"setting"},(0,t.createElement)(o.PanelBody,{title:(0,c.__)("Followers Options","activitypub")},(0,t.createElement)(o.TextControl,{label:(0,c.__)("Title","activitypub"),help:(0,c.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:u,onChange:e=>n({title:e})}),d.length>1&&(0,t.createElement)(o.SelectControl,{label:(0,c.__)("Select User","activitypub"),value:p,options:d,onChange:f("selectedUser")}),(0,t.createElement)(o.SelectControl,{label:(0,c.__)("Sort","activitypub"),value:l,options:w,onChange:f("order")}),(0,t.createElement)(o.RangeControl,{label:(0,c.__)("Number of Followers","activitypub"),value:s,onChange:f("per_page"),min:1,max:10}))),(0,t.createElement)(y,r({},a,{page:m,setPage:b,followLinks:!1})))},save:()=>null,icon:l})})()})();
|
|
@ -1 +1 @@
|
||||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => 'f0e21057f7ec615290d6');
|
<?php return array( 'dependencies' => array( 'react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url' ), 'version' => '04e51e7562fe28b0b2c3' );
|
||||||
|
|
|
@ -1,3 +1,3 @@
|
||||||
(()=>{var e,t={189:(e,t,a)=>{"use strict";const r=window.wp.element;function n(){return n=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var r in a)Object.prototype.hasOwnProperty.call(a,r)&&(e[r]=a[r])}return e},n.apply(this,arguments)}const l=window.React,o=window.wp.apiFetch;var i=a.n(o);const c=window.wp.url,s=window.wp.i18n;var p=a(184),u=a.n(p);function m(e){let{active:t,children:a,page:n,pageClick:l,className:o}=e;const i=u()("wp-block activitypub-pager",o,{current:t});return(0,r.createElement)("a",{className:i,onClick:e=>{e.preventDefault(),!t&&l(n)}},a)}const v={outlined:"outlined",minimal:"minimal"};function f(e){let{compact:t,nextLabel:a,page:n,pageClick:l,perPage:o,prevLabel:i,total:c,variant:s=v.outlined}=e;const p=((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})(n,Math.ceil(c/o)),f=u()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":t});return(0,r.createElement)("nav",{className:f},i&&(0,r.createElement)(m,{key:"prev",page:n-1,pageClick:l,active:1===n,"aria-label":i,className:"wp-block-query-pagination-previous block-editor-block-list__block"},i),!t&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},p.map((e=>(0,r.createElement)(m,{key:e,page:e,pageClick:l,active:e===n,className:"page-numbers"},e)))),a&&(0,r.createElement)(m,{key:"next",page:n+1,pageClick:l,active:n===Math.ceil(c/o),"aria-label":a,className:"wp-block-query-pagination-next block-editor-block-list__block"},a))}const b=window.wp.components,{namespace:d}=window._activityPubOptions;function w(e){let{selectedUser:t,per_page:a,order:o,title:p,page:u,setPage:m,className:v="",followLinks:b=!0,followerData:w=!1}=e;const y="site"===t?0:t,[k,h]=(0,l.useState)([]),[E,O]=(0,l.useState)(0),[x,_]=(0,l.useState)(0),[N,j]=function(){const[e,t]=(0,l.useState)(1);return[e,t]}(),S=u||N,C=m||j,L=(0,r.createInterpolateElement)(/* translators: arrow for previous followers link */
|
(()=>{var e,t={189:(e,t,a)=>{"use strict";const r=window.wp.element;function n(){return n=Object.assign?Object.assign.bind():function(e){for(var t=1;t<arguments.length;t++){var a=arguments[t];for(var r in a)Object.prototype.hasOwnProperty.call(a,r)&&(e[r]=a[r])}return e},n.apply(this,arguments)}const l=window.React,o=window.wp.apiFetch;var i=a.n(o);const c=window.wp.url,s=window.wp.i18n;var p=a(184),u=a.n(p);function m(e){let{active:t,children:a,page:n,pageClick:l,className:o}=e;const i=u()("wp-block activitypub-pager",o,{current:t});return(0,r.createElement)("a",{className:i,onClick:e=>{e.preventDefault(),!t&&l(n)}},a)}const v={outlined:"outlined",minimal:"minimal"};function f(e){let{compact:t,nextLabel:a,page:n,pageClick:l,perPage:o,prevLabel:i,total:c,variant:s=v.outlined}=e;const p=((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})(n,Math.ceil(c/o)),f=u()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${s}`,{"is-compact":t});return(0,r.createElement)("nav",{className:f},i&&(0,r.createElement)(m,{key:"prev",page:n-1,pageClick:l,active:1===n,"aria-label":i,className:"wp-block-query-pagination-previous block-editor-block-list__block"},i),!t&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},p.map((e=>(0,r.createElement)(m,{key:e,page:e,pageClick:l,active:e===n,className:"page-numbers"},e)))),a&&(0,r.createElement)(m,{key:"next",page:n+1,pageClick:l,active:n===Math.ceil(c/o),"aria-label":a,className:"wp-block-query-pagination-next block-editor-block-list__block"},a))}const b=window.wp.components,{namespace:d}=window._activityPubOptions;function w(e){let{selectedUser:t,per_page:a,order:o,title:p,page:u,setPage:m,className:v="",followLinks:b=!0,followerData:w=!1}=e;const y="site"===t?0:t,[k,h]=(0,l.useState)([]),[E,O]=(0,l.useState)(0),[x,_]=(0,l.useState)(0),[N,j]=function(){const[e,t]=(0,l.useState)(1);return[e,t]}(),S=u||N,C=m||j,L=(0,r.createInterpolateElement)(/* translators: arrow for previous followers link */
|
||||||
(0,s.__)("<span>←</span> Less","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),q=(0,r.createInterpolateElement)(/* translators: arrow for next followers link */
|
(0,s.__)("<span>←</span> Less","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),q=(0,r.createInterpolateElement)(/* translators: arrow for next followers link */
|
||||||
(0,s.__)("More <span>→</span>","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),P=(e,t)=>{h(e),_(t),O(Math.ceil(t/a))};return(0,l.useEffect)((()=>{if(w&&1===S)return P(w.followers,w.total);const e=function(e,t,a,r){const n=`/${d}/users/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,c.addQueryArgs)(n,l)}(y,a,o,S);i()({path:e}).then((e=>P(e.orderedItems,e.totalItems))).catch((()=>{}))}),[y,a,o,S,w]),(0,r.createElement)("div",{className:"activitypub-follower-block "+v},(0,r.createElement)("h3",null,p),(0,r.createElement)("ul",null,k&&k.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(g,n({},e,{followLinks:b})))))),E>1&&(0,r.createElement)(f,{page:S,perPage:a,total:x,pageClick:C,nextLabel:q,prevLabel:L,compact:"is-style-compact"===v}))}function g(e){let{name:t,icon:a,url:l,preferredUsername:o,followLinks:i=!0}=e;const c=`@${o}`,s={};return i||(s.onClick=e=>e.preventDefault()),(0,r.createElement)(b.ExternalLink,n({className:"activitypub-link",href:l,title:c},s),(0,r.createElement)("img",{width:"40",height:"40",src:a.url,class:"avatar activitypub-avatar"}),(0,r.createElement)("span",{class:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},t),(0,r.createElement)("span",{class:"sep"},"/"),(0,r.createElement)("span",{class:"activitypub-handle"},c)))}const y=window.wp.domReady;a.n(y)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,r.render)((0,r.createElement)(w,t),e)}))}))},184:(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];if(a){var l=typeof a;if("string"===l||"number"===l)e.push(a);else if(Array.isArray(a)){if(a.length){var o=n.apply(null,a);o&&e.push(o)}}else if("object"===l){if(a.toString!==Object.prototype.toString&&!a.toString.toString().includes("[native code]")){e.push(a.toString());continue}for(var i in a)r.call(a,i)&&a[i]&&e.push(i)}}}return e.join(" ")}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={638:0,962: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,[962],(()=>r(189)));n=r.O(n)})();
|
(0,s.__)("More <span>→</span>","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),P=(e,t)=>{h(e),_(t),O(Math.ceil(t/a))};return(0,l.useEffect)((()=>{if(w&&1===S)return P(w.followers,w.total);const e=function(e,t,a,r){const n=`/${d}/users/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,c.addQueryArgs)(n,l)}(y,a,o,S);i()({path:e}).then((e=>P(e.orderedItems,e.totalItems))).catch((()=>{}))}),[y,a,o,S,w]),(0,r.createElement)("div",{className:"activitypub-follower-block "+v},(0,r.createElement)("h3",null,p),(0,r.createElement)("ul",null,k&&k.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(g,n({},e,{followLinks:b})))))),E>1&&(0,r.createElement)(f,{page:S,perPage:a,total:x,pageClick:C,nextLabel:q,prevLabel:L,compact:"is-style-compact"===v}))}function g(e){let{name:t,icon:a,url:l,preferredUsername:o,followLinks:i=!0}=e;const c=`@${o}`,s={};return i||(s.onClick=e=>e.preventDefault()),(0,r.createElement)(b.ExternalLink,n({className:"activitypub-link",href:l,title:c},s),(0,r.createElement)("img",{width:"40",height:"40",src:a.url,class:"avatar activitypub-avatar",alt:t}),(0,r.createElement)("span",{class:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},t),(0,r.createElement)("span",{class:"sep"},"/"),(0,r.createElement)("span",{class:"activitypub-handle"},c)))}const y=window.wp.domReady;a.n(y)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,r.render)((0,r.createElement)(w,t),e)}))}))},184:(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];if(a){var l=typeof a;if("string"===l||"number"===l)e.push(a);else if(Array.isArray(a)){if(a.length){var o=n.apply(null,a);o&&e.push(o)}}else if("object"===l){if(a.toString!==Object.prototype.toString&&!a.toString.toString().includes("[native code]")){e.push(a.toString());continue}for(var i in a)r.call(a,i)&&a[i]&&e.push(i)}}}return e.join(" ")}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={638:0,962: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,[962],(()=>r(189)));n=r.O(n)})();
|
|
@ -15,7 +15,8 @@
|
||||||
"yoast/phpunit-polyfills": "^2.0",
|
"yoast/phpunit-polyfills": "^2.0",
|
||||||
"dealerdirect/phpcodesniffer-composer-installer": "^1.0.0",
|
"dealerdirect/phpcodesniffer-composer-installer": "^1.0.0",
|
||||||
"sirbrillig/phpcs-variable-analysis": "^2.11",
|
"sirbrillig/phpcs-variable-analysis": "^2.11",
|
||||||
"phpcsstandards/phpcsextra": "^1.1.0"
|
"phpcsstandards/phpcsextra": "^1.1.0",
|
||||||
|
"dms/phpunit-arraysubset-asserts": "^0.5.0"
|
||||||
},
|
},
|
||||||
"config": {
|
"config": {
|
||||||
"allow-plugins": true
|
"allow-plugins": true
|
||||||
|
|
|
@ -194,6 +194,12 @@ class Activity extends Base_Object {
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function set_object( $object ) {
|
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 );
|
$this->set( 'object', $object );
|
||||||
|
|
||||||
if ( ! is_object( $object ) ) {
|
if ( ! is_object( $object ) ) {
|
||||||
|
|
|
@ -254,19 +254,6 @@ class Base_Object {
|
||||||
*/
|
*/
|
||||||
protected $published;
|
protected $published;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The date and time describing the actual or expected starting time
|
* The date and time describing the actual or expected starting time
|
||||||
* of the object.
|
* of the object.
|
||||||
|
@ -437,6 +424,19 @@ class Base_Object {
|
||||||
*/
|
*/
|
||||||
protected $source;
|
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
|
* Magic function to implement getter and setter
|
||||||
*
|
*
|
||||||
|
@ -585,7 +585,7 @@ class Base_Object {
|
||||||
|
|
||||||
foreach ( $array as $key => $value ) {
|
foreach ( $array as $key => $value ) {
|
||||||
$key = camel_to_snake_case( $key );
|
$key = camel_to_snake_case( $key );
|
||||||
$object->set( $key, $value );
|
call_user_func( array( $object, 'set_' . $key ), $value );
|
||||||
}
|
}
|
||||||
|
|
||||||
return $object;
|
return $object;
|
||||||
|
@ -611,7 +611,7 @@ class Base_Object {
|
||||||
foreach ( $array as $key => $value ) {
|
foreach ( $array as $key => $value ) {
|
||||||
if ( $value ) {
|
if ( $value ) {
|
||||||
$key = camel_to_snake_case( $key );
|
$key = camel_to_snake_case( $key );
|
||||||
$this->set( $key, $value );
|
call_user_func( array( $this, 'set_' . $key ), $value );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -672,7 +672,24 @@ class Base_Object {
|
||||||
*/
|
*/
|
||||||
public function to_json() {
|
public function to_json() {
|
||||||
$array = $this->to_array();
|
$array = $this->to_array();
|
||||||
|
$options = \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT;
|
||||||
|
|
||||||
return \wp_json_encode( $array, \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 ) );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Inspired by the PHP ActivityPub Library by @Landrok
|
|
||||||
*
|
|
||||||
* @link https://github.com/landrok/activitypub
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Activitypub\Activity;
|
|
||||||
|
|
||||||
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 Note extends Base_Object {
|
|
||||||
protected $type = 'Event';
|
|
||||||
}
|
|
|
@ -1,23 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Inspired by the PHP ActivityPub Library by @Landrok
|
|
||||||
*
|
|
||||||
* @link https://github.com/landrok/activitypub
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Activitypub\Activity;
|
|
||||||
|
|
||||||
use Activitypub\Activity\Base_Object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Note is an implementation of one of the
|
|
||||||
* Activity Streams Note object type
|
|
||||||
*
|
|
||||||
* The Object is the primary base type for the Activity Streams
|
|
||||||
* vocabulary.
|
|
||||||
*
|
|
||||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-note
|
|
||||||
*/
|
|
||||||
class Note extends Base_Object {
|
|
||||||
protected $type = 'Note';
|
|
||||||
}
|
|
|
@ -2,11 +2,15 @@
|
||||||
namespace Activitypub;
|
namespace Activitypub;
|
||||||
|
|
||||||
use WP_Post;
|
use WP_Post;
|
||||||
|
use WP_Comment;
|
||||||
use Activitypub\Activity\Activity;
|
use Activitypub\Activity\Activity;
|
||||||
use Activitypub\Collection\Users;
|
use Activitypub\Collection\Users;
|
||||||
use Activitypub\Collection\Followers;
|
use Activitypub\Collection\Followers;
|
||||||
use Activitypub\Transformer\Transformers_Manager;
|
use Activitypub\Transformer\Factory;
|
||||||
|
use Activitypub\Transformer\Post;
|
||||||
|
use Activitypub\Transformer\Comment;
|
||||||
|
|
||||||
|
use function Activitypub\get_rest_url_by_path;
|
||||||
use function Activitypub\is_single_user;
|
use function Activitypub\is_single_user;
|
||||||
use function Activitypub\is_user_disabled;
|
use function Activitypub\is_user_disabled;
|
||||||
use function Activitypub\safe_remote_post;
|
use function Activitypub\safe_remote_post;
|
||||||
|
@ -25,103 +29,156 @@ class Activity_Dispatcher {
|
||||||
public static function init() {
|
public static function init() {
|
||||||
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 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_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.
|
* Send Activities to followers and mentioned users or `Announce` (boost) a blog post.
|
||||||
*
|
*
|
||||||
* @param WP_Post $wp_post The ActivityPub Post.
|
* @param mixed $wp_object The ActivityPub Post.
|
||||||
* @param string $type The Activity-Type.
|
* @param string $type The Activity-Type.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function send_activity_or_announce( WP_Post $wp_post, $type ) {
|
public static function send_activity_or_announce( $wp_object, $type ) {
|
||||||
// check if a migration is needed before sending new posts
|
// check if a migration is needed before sending new post
|
||||||
Migration::maybe_migrate();
|
Migration::maybe_migrate();
|
||||||
|
|
||||||
if ( is_user_type_disabled( 'blog' ) ) {
|
if ( is_user_type_disabled( 'blog' ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$wp_post->post_author = Users::BLOG_USER_ID;
|
|
||||||
|
|
||||||
if ( is_single_user() ) {
|
if ( is_single_user() ) {
|
||||||
self::send_activity( $wp_post, $type );
|
self::send_activity( $wp_object, $type, Users::BLOG_USER_ID );
|
||||||
|
self::send_announce( $wp_object, $type, Users::APPLICATION_USER_ID );
|
||||||
} else {
|
} else {
|
||||||
self::send_announce( $wp_post, $type );
|
self::send_announce( $wp_object, $type, Users::BLOG_USER_ID );
|
||||||
|
self::send_announce( $wp_object, $type, Users::APPLICATION_USER_ID );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Activities to followers and mentioned users.
|
* Send Activities to followers and mentioned users.
|
||||||
*
|
*
|
||||||
* @param WP_Post $wp_post The ActivityPub Post.
|
* @param mixed $wp_object The ActivityPub Post.
|
||||||
* @param string $type The Activity-Type.
|
* @param string $type The Activity-Type.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function send_activity( WP_Post $wp_post, $type ) {
|
public static function send_activity( $wp_object, $type, $user_id = null ) {
|
||||||
if ( is_user_disabled( $wp_post->post_author ) ) {
|
$transformer = Factory::get_transformer( $wp_object );
|
||||||
|
|
||||||
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$transformer = Transformers_Manager::instance()->get_transformer( $wp_post );
|
$activity = $transformer->to_activity( $type );
|
||||||
$object = $transformer->to_object();
|
|
||||||
|
|
||||||
$activity = new Activity();
|
self::send_activity_to_inboxes( $activity, $user_id );
|
||||||
$activity->set_type( $type );
|
|
||||||
$activity->set_object( $object );
|
|
||||||
|
|
||||||
$follower_inboxes = Followers::get_inboxes( $wp_post->post_author );
|
|
||||||
$mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() );
|
|
||||||
|
|
||||||
$inboxes = array_merge( $follower_inboxes, $mentioned_inboxes );
|
|
||||||
$inboxes = array_unique( $inboxes );
|
|
||||||
|
|
||||||
$json = $activity->to_json();
|
|
||||||
|
|
||||||
foreach ( $inboxes as $inbox ) {
|
|
||||||
safe_remote_post( $inbox, $json, $wp_post->post_author );
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Announces to followers and mentioned users.
|
* Send Announces to followers and mentioned users.
|
||||||
*
|
*
|
||||||
* @param WP_Post $wp_post The ActivityPub Post.
|
* @param mixed $wp_object The ActivityPub Post.
|
||||||
* @param string $type The Activity-Type.
|
* @param string $type The Activity-Type.
|
||||||
*
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public static function send_announce( WP_Post $wp_post, $type ) {
|
public static function send_announce( $wp_object, $type, $user_id = null ) {
|
||||||
if ( ! in_array( $type, array( 'Create', 'Update' ), true ) ) {
|
if ( ! in_array( $type, array( 'Create', 'Update' ), true ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
|
$transformer = Factory::get_transformer( $wp_object );
|
||||||
|
|
||||||
|
if ( null !== $user_id && Users::APPLICATION_USER_ID !== $user_id ) {
|
||||||
|
$transformer->change_wp_user_id( $user_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $user_id ) {
|
||||||
|
$user_id = $transformer->get_wp_user_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_user_disabled( $user_id ) ) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$transformer = Transformers_Manager::instance()->get_transformer( $wp_post );
|
$activity = $transformer->to_activity( 'Announce' );
|
||||||
$object = $transformer->to_object();
|
|
||||||
|
|
||||||
|
// TODO: properly fix this for the instance-to-instance federation with Mobilizon.
|
||||||
|
// Error:
|
||||||
|
// Failed to map identity from signature (payload actor mismatch)
|
||||||
|
// key_id=http://wp.lan/wp-json/activitypub/1.0/application, actor=http://wp.lan/@blog
|
||||||
|
// Of course, the announce must be sent as the Application actor because he also signed it!
|
||||||
|
if ( Users::APPLICATION_USER_ID === $user_id ) {
|
||||||
|
$activity->set_actor( get_rest_url_by_path( 'application' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
self::send_activity_to_inboxes( $activity, $user_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 = new Activity();
|
||||||
$activity->set_type( 'Announce' );
|
$activity->set_id( $user->get_url() . '#update' );
|
||||||
// to pre-fill attributes like "published" and "id"
|
$activity->set_type( 'Update' );
|
||||||
$activity->set_object( $object );
|
$activity->set_actor( $user->get_url() );
|
||||||
// send only the id
|
$activity->set_object( $user->get_url() );
|
||||||
$activity->set_object( $object->get_id() );
|
$activity->set_to( 'https://www.w3.org/ns/activitystreams#Public' );
|
||||||
|
|
||||||
$follower_inboxes = Followers::get_inboxes( $wp_post->post_author );
|
// send the update
|
||||||
$mentioned_inboxes = Mention::get_inboxes( $activity->get_cc() );
|
self::send_activity_to_inboxes( $activity, $user_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send an Activity to all followers and mentioned users.
|
||||||
|
*
|
||||||
|
* @param Activity $activity The ActivityPub Activity.
|
||||||
|
* @param int $user_id The user ID.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private static function send_activity_to_inboxes( $activity, $user_id ) {
|
||||||
|
$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_merge( $follower_inboxes, $mentioned_inboxes );
|
||||||
$inboxes = array_unique( $inboxes );
|
$inboxes = array_unique( $inboxes );
|
||||||
|
|
||||||
|
if ( empty( $inboxes ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
$json = $activity->to_json();
|
$json = $activity->to_json();
|
||||||
|
|
||||||
foreach ( $inboxes as $inbox ) {
|
foreach ( $inboxes as $inbox ) {
|
||||||
safe_remote_post( $inbox, $json, $wp_post->post_author );
|
safe_remote_post( $inbox, $json, $user_id );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,15 @@
|
||||||
<?php
|
<?php
|
||||||
namespace Activitypub;
|
namespace Activitypub;
|
||||||
|
|
||||||
|
use Exception;
|
||||||
use Activitypub\Signature;
|
use Activitypub\Signature;
|
||||||
use Activitypub\Collection\Users;
|
use Activitypub\Collection\Users;
|
||||||
|
use Activitypub\Collection\Followers;
|
||||||
|
|
||||||
|
use function Activitypub\sanitize_url;
|
||||||
|
|
||||||
|
use function Activitypub\is_comment;
|
||||||
|
use function Activitypub\is_activitypub_request;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub Class
|
* ActivityPub Class
|
||||||
|
@ -15,14 +22,15 @@ class Activitypub {
|
||||||
*/
|
*/
|
||||||
public static function init() {
|
public static function init() {
|
||||||
\add_filter( 'template_include', array( self::class, 'render_json_template' ), 99 );
|
\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( 'query_vars', array( self::class, 'add_query_vars' ) );
|
||||||
\add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
|
\add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
|
||||||
\add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 3 );
|
\add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 3 );
|
||||||
|
|
||||||
// Add support for ActivityPub to custom post types
|
// Add support for ActivityPub to custom post types
|
||||||
$transformer_mapping = \get_option( 'activitypub_transformer_mapping', array( 'post' => 'activitypub/default', 'page' => 'activitypub/default' ) ) ? \get_option( 'activitypub_transformer_mapping', array( 'post' => 'activitypub/default', 'page' => 'activitypub/default' ) ) : array();
|
$post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) : array();
|
||||||
|
|
||||||
foreach ( array_keys( $transformer_mapping ) as $post_type ) {
|
foreach ( $post_types as $post_type ) {
|
||||||
\add_post_type_support( $post_type, 'activitypub' );
|
\add_post_type_support( $post_type, 'activitypub' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -34,6 +42,11 @@ class Activitypub {
|
||||||
\add_action( 'after_setup_theme', array( self::class, 'theme_compat' ), 99 );
|
\add_action( 'after_setup_theme', array( self::class, 'theme_compat' ), 99 );
|
||||||
|
|
||||||
\add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) );
|
\add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) );
|
||||||
|
|
||||||
|
\add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 );
|
||||||
|
|
||||||
|
// register several post_types
|
||||||
|
self::register_post_types();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -54,7 +67,6 @@ class Activitypub {
|
||||||
*/
|
*/
|
||||||
public static function deactivate() {
|
public static function deactivate() {
|
||||||
self::flush_rewrite_rules();
|
self::flush_rewrite_rules();
|
||||||
|
|
||||||
Scheduler::deregister_schedules();
|
Scheduler::deregister_schedules();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -92,6 +104,8 @@ class Activitypub {
|
||||||
|
|
||||||
if ( \is_author() ) {
|
if ( \is_author() ) {
|
||||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/author-json.php';
|
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/author-json.php';
|
||||||
|
} elseif ( is_comment() ) {
|
||||||
|
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/comment-json.php';
|
||||||
} elseif ( \is_singular() ) {
|
} elseif ( \is_singular() ) {
|
||||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
|
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
|
||||||
} elseif ( \is_home() ) {
|
} elseif ( \is_home() ) {
|
||||||
|
@ -109,11 +123,43 @@ class Activitypub {
|
||||||
return $json_template;
|
return $json_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() && $comment->user_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
wp_safe_redirect( get_comment_link( $comment ) );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add the 'activitypub' query variable so WordPress won't mangle it.
|
* Add the 'activitypub' query variable so WordPress won't mangle it.
|
||||||
*/
|
*/
|
||||||
public static function add_query_vars( $vars ) {
|
public static function add_query_vars( $vars ) {
|
||||||
$vars[] = 'activitypub';
|
$vars[] = 'activitypub';
|
||||||
|
$vars[] = 'c';
|
||||||
|
$vars[] = 'p';
|
||||||
|
|
||||||
return $vars;
|
return $vars;
|
||||||
}
|
}
|
||||||
|
@ -189,10 +235,18 @@ class Activitypub {
|
||||||
* @return string $url
|
* @return string $url
|
||||||
*/
|
*/
|
||||||
public static function remote_comment_link( $comment_link, $comment ) {
|
public static function remote_comment_link( $comment_link, $comment ) {
|
||||||
$remote_comment_link = get_comment_meta( $comment->comment_ID, 'source_url', true );
|
if ( ! $comment || is_admin() ) {
|
||||||
if ( $remote_comment_link ) {
|
return $comment_link;
|
||||||
$comment_link = esc_url( $remote_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;
|
return $comment_link;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -328,4 +382,98 @@ class Activitypub {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 );
|
||||||
|
},
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
do_action( 'activitypub_after_register_post_type' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,6 @@ namespace Activitypub;
|
||||||
|
|
||||||
use WP_User_Query;
|
use WP_User_Query;
|
||||||
use Activitypub\Model\Blog_User;
|
use Activitypub\Model\Blog_User;
|
||||||
use Activitypub\Base\Transformer\Base as Transformer_Base;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub Admin Class
|
* ActivityPub Admin Class
|
||||||
|
@ -19,19 +18,11 @@ class Admin {
|
||||||
\add_action( 'admin_init', array( self::class, 'register_settings' ) );
|
\add_action( 'admin_init', array( self::class, 'register_settings' ) );
|
||||||
\add_action( 'personal_options_update', array( self::class, 'save_user_description' ) );
|
\add_action( 'personal_options_update', array( self::class, 'save_user_description' ) );
|
||||||
\add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
|
\add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
|
||||||
|
\add_action( 'admin_notices', array( self::class, 'admin_notices' ) );
|
||||||
|
|
||||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||||
\add_action( 'show_user_profile', array( self::class, 'add_profile' ) );
|
\add_action( 'show_user_profile', array( self::class, 'add_profile' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
add_filter(
|
|
||||||
'activitypub/transformers/is_transformer_enabled',
|
|
||||||
function( $should_register, Transformer_Base $transformer_instance ) {
|
|
||||||
return ! Options::is_transformer_disabled( $transformer_instance->get_name() );
|
|
||||||
},
|
|
||||||
10,
|
|
||||||
2
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -56,6 +47,37 @@ class Admin {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 without setting a permalink structure. This will prevent ActivityPub from working. Please set a permalink structure.', 'activitypub' );
|
||||||
|
self::show_admin_notice( $admin_notice, 'error' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
* Load settings page
|
||||||
*/
|
*/
|
||||||
|
@ -73,7 +95,7 @@ class Admin {
|
||||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' );
|
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' );
|
||||||
break;
|
break;
|
||||||
case 'followers':
|
case 'followers':
|
||||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/blog-user-followers-list.php' );
|
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/admin-followers-list.php' );
|
||||||
break;
|
break;
|
||||||
case 'welcome':
|
case 'welcome':
|
||||||
default:
|
default:
|
||||||
|
@ -164,47 +186,14 @@ class Admin {
|
||||||
'default' => '0',
|
'default' => '0',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
\register_setting(
|
||||||
/**
|
|
||||||
* Flexible activation of post_types together with mapping ActivityPub transformers.
|
|
||||||
*
|
|
||||||
* If a post-type is not mapped to any ActivtiyPub transformer it means it is not activated
|
|
||||||
* for ActivityPub federation.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
*/
|
|
||||||
register_setting(
|
|
||||||
'activitypub',
|
'activitypub',
|
||||||
'activitypub_transformer_mapping',
|
'activitypub_support_post_types',
|
||||||
array(
|
array(
|
||||||
'type' => 'array',
|
|
||||||
'default' => array(
|
|
||||||
'post' => 'note',
|
|
||||||
),
|
|
||||||
'show_in_rest' => array(
|
|
||||||
'schema' => array(
|
|
||||||
'type' => 'array',
|
|
||||||
'items' => array(
|
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
),
|
'description' => \esc_html__( 'Enable ActivityPub support for post types', 'activitypub' ),
|
||||||
),
|
'show_in_rest' => true,
|
||||||
),
|
'default' => array( 'post', 'pages' ),
|
||||||
'sanitize_callback' => function ( $value ) {
|
|
||||||
// Check if $value is an array
|
|
||||||
if ( ! is_array( $value ) ) {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
$value_keys = array_keys( $value );
|
|
||||||
|
|
||||||
$all_public_post_types = \get_post_types( array( 'public' => true ), 'names' );
|
|
||||||
|
|
||||||
// Unset the keys that are missing in $keysToCheck
|
|
||||||
foreach ( array_diff( $value_keys, $all_public_post_types ) as $missing_key ) {
|
|
||||||
unset( $value[ $missing_key ] );
|
|
||||||
}
|
|
||||||
// var_dump($value);
|
|
||||||
return $value;
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
\register_setting(
|
\register_setting(
|
||||||
|
@ -311,8 +300,8 @@ class Admin {
|
||||||
|
|
||||||
public static function enqueue_scripts( $hook_suffix ) {
|
public static function enqueue_scripts( $hook_suffix ) {
|
||||||
if ( false !== strpos( $hook_suffix, 'activitypub' ) ) {
|
if ( false !== strpos( $hook_suffix, 'activitypub' ) ) {
|
||||||
wp_enqueue_style( 'activitypub-admin-styles', plugins_url( 'assets/css/activitypub-admin.css', ACTIVITYPUB_PLUGIN_FILE ), array(), '1.0.0' );
|
wp_enqueue_style( 'activitypub-admin-styles', plugins_url( 'assets/css/activitypub-admin.css', ACTIVITYPUB_PLUGIN_FILE ), array(), '1.0.1' );
|
||||||
wp_enqueue_script( 'activitypub-admin-styles', plugins_url( 'assets/js/activitypub-admin.js', ACTIVITYPUB_PLUGIN_FILE ), array( 'jquery' ), '1.0.0', false );
|
wp_enqueue_script( 'activitypub-admin-styles', plugins_url( 'assets/js/activitypub-admin.js', ACTIVITYPUB_PLUGIN_FILE ), array( 'jquery' ), '1.0.1', false );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,7 +3,9 @@ namespace Activitypub;
|
||||||
|
|
||||||
use Activitypub\Collection\Followers;
|
use Activitypub\Collection\Followers;
|
||||||
use Activitypub\Collection\Users as User_Collection;
|
use Activitypub\Collection\Users as User_Collection;
|
||||||
use Activitypub\is_user_type_disabled;
|
|
||||||
|
use function Activitypub\object_to_uri;
|
||||||
|
use function Activitypub\is_user_type_disabled;
|
||||||
|
|
||||||
class Blocks {
|
class Blocks {
|
||||||
public static function init() {
|
public static function init() {
|
||||||
|
@ -140,7 +142,7 @@ class Blocks {
|
||||||
|
|
||||||
return sprintf(
|
return sprintf(
|
||||||
$template,
|
$template,
|
||||||
esc_url( $data['url'] ),
|
esc_url( object_to_uri( $data['url'] ) ),
|
||||||
esc_attr( $data['name'] ),
|
esc_attr( $data['name'] ),
|
||||||
esc_attr( $data['icon']['url'] ),
|
esc_attr( $data['icon']['url'] ),
|
||||||
esc_html( $data['name'] ),
|
esc_html( $data['name'] ),
|
||||||
|
|
33
includes/class-handler.php
Normal file
33
includes/class-handler.php
Normal file
|
@ -0,0 +1,33 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub;
|
||||||
|
|
||||||
|
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() {
|
||||||
|
Create::init();
|
||||||
|
Delete::init();
|
||||||
|
Follow::init();
|
||||||
|
Undo::init();
|
||||||
|
Update::init();
|
||||||
|
|
||||||
|
do_action( 'activitypub_register_handlers' );
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,8 +12,8 @@ class Hashtag {
|
||||||
*/
|
*/
|
||||||
public static function init() {
|
public static function init() {
|
||||||
if ( '1' === \get_option( 'activitypub_use_hashtags', '1' ) ) {
|
if ( '1' === \get_option( 'activitypub_use_hashtags', '1' ) ) {
|
||||||
\add_filter( 'wp_insert_post', array( self::class, 'insert_post' ), 10, 2 );
|
\add_action( 'wp_insert_post', array( self::class, 'insert_post' ), 10, 2 );
|
||||||
\add_filter( 'the_content', array( self::class, 'the_content' ), 10, 2 );
|
\add_filter( 'the_content', array( self::class, 'the_content' ), 10, 1 );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -67,7 +67,7 @@ class Hashtag {
|
||||||
$tag = strtolower( $m[2] );
|
$tag = strtolower( $m[2] );
|
||||||
if ( '/' === $m[1] ) {
|
if ( '/' === $m[1] ) {
|
||||||
// Closing tag.
|
// Closing tag.
|
||||||
$i = array_search( $tag, $tag_stack );
|
$i = array_search( $tag, $tag_stack, true );
|
||||||
// We can only remove the tag from the stack if it is in the stack.
|
// We can only remove the tag from the stack if it is in the stack.
|
||||||
if ( false !== $i ) {
|
if ( false !== $i ) {
|
||||||
$tag_stack = array_slice( $tag_stack, 0, $i );
|
$tag_stack = array_slice( $tag_stack, 0, $i );
|
||||||
|
|
|
@ -39,11 +39,6 @@ class Health_Check {
|
||||||
'test' => array( self::class, 'test_webfinger' ),
|
'test' => array( self::class, 'test_webfinger' ),
|
||||||
);
|
);
|
||||||
|
|
||||||
$tests['direct']['activitypub_test_system_cron'] = array(
|
|
||||||
'label' => __( 'System Cron Test', 'activitypub' ),
|
|
||||||
'test' => array( self::class, 'test_system_cron' ),
|
|
||||||
);
|
|
||||||
|
|
||||||
return $tests;
|
return $tests;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -20,7 +20,7 @@ class Http {
|
||||||
* @return array|WP_Error The POST Response or an WP_ERROR
|
* @return array|WP_Error The POST Response or an WP_ERROR
|
||||||
*/
|
*/
|
||||||
public static function post( $url, $body, $user_id ) {
|
public static function post( $url, $body, $user_id ) {
|
||||||
do_action( 'activitypub_pre_http_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' );
|
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||||
$digest = Signature::generate_digest( $body );
|
$digest = Signature::generate_digest( $body );
|
||||||
|
@ -70,7 +70,7 @@ class Http {
|
||||||
* @return array|WP_Error The GET Response or an WP_ERROR
|
* @return array|WP_Error The GET Response or an WP_ERROR
|
||||||
*/
|
*/
|
||||||
public static function get( $url ) {
|
public static function get( $url ) {
|
||||||
do_action( 'activitypub_pre_http_get', $url );
|
\do_action( 'activitypub_pre_http_get', $url );
|
||||||
|
|
||||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||||
$signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date );
|
$signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date );
|
||||||
|
@ -108,4 +108,24 @@ class Http {
|
||||||
|
|
||||||
return $response;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,8 @@ class Mention {
|
||||||
* Initialize the class, registering WordPress hooks
|
* Initialize the class, registering WordPress hooks
|
||||||
*/
|
*/
|
||||||
public static function init() {
|
public static function init() {
|
||||||
\add_filter( 'the_content', array( self::class, 'the_content' ), 99, 2 );
|
\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 );
|
\add_filter( 'activitypub_extract_mentions', array( self::class, 'extract_mentions' ), 99, 2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -4,7 +4,6 @@ namespace Activitypub;
|
||||||
use Activitypub\Activitypub;
|
use Activitypub\Activitypub;
|
||||||
use Activitypub\Model\Blog_User;
|
use Activitypub\Model\Blog_User;
|
||||||
use Activitypub\Collection\Followers;
|
use Activitypub\Collection\Followers;
|
||||||
use Activitypub\Admin;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub Migration Class
|
* ActivityPub Migration Class
|
||||||
|
@ -115,8 +114,8 @@ class Migration {
|
||||||
if ( version_compare( $version_from_db, '1.0.0', '<' ) ) {
|
if ( version_compare( $version_from_db, '1.0.0', '<' ) ) {
|
||||||
self::migrate_from_0_17();
|
self::migrate_from_0_17();
|
||||||
}
|
}
|
||||||
if ( version_compare( $version_from_db, 'version_number_transformer_management_placeholder', '<' ) ) {
|
if ( version_compare( $version_from_db, '1.3.0', '<' ) ) {
|
||||||
self::migrate_from_version_number_transformer_management_placeholder();
|
self::migrate_from_1_2_0();
|
||||||
}
|
}
|
||||||
|
|
||||||
update_option( 'activitypub_db_version', self::get_target_version() );
|
update_option( 'activitypub_db_version', self::get_target_version() );
|
||||||
|
@ -124,21 +123,6 @@ class Migration {
|
||||||
self::unlock();
|
self::unlock();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Updates the supported post type settings to the mapped transformer setting.
|
|
||||||
* TODO: Test this
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private static function migrate_from_version_number_transformer_management_placeholder() {
|
|
||||||
$supported_post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) );
|
|
||||||
Admin::register_settings();
|
|
||||||
$transformer_mapping = array();
|
|
||||||
foreach ( $supported_post_types as $supported_post_type ) {
|
|
||||||
$transformer_mapping[ $supported_post_type ] = ACTIVITYPUB_DEFAULT_TRANSFORMER;
|
|
||||||
}
|
|
||||||
update_option( 'activitypub_transformer_mapping', $transformer_mapping );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Updates the DB-schema of the followers-list
|
* Updates the DB-schema of the followers-list
|
||||||
*
|
*
|
||||||
|
@ -195,4 +179,22 @@ class Migration {
|
||||||
\update_option( 'activitypub_custom_post_content', $content );
|
\update_option( 'activitypub_custom_post_content', $content );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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' );
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,6 +4,9 @@ namespace Activitypub;
|
||||||
|
|
||||||
use Activitypub\Collection\Users;
|
use Activitypub\Collection\Users;
|
||||||
use Activitypub\Collection\Followers;
|
use Activitypub\Collection\Followers;
|
||||||
|
use Activitypub\Transformer\Post;
|
||||||
|
|
||||||
|
use function Activitypub\is_user_type_disabled;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub Scheduler Class
|
* ActivityPub Scheduler Class
|
||||||
|
@ -11,16 +14,69 @@ use Activitypub\Collection\Followers;
|
||||||
* @author Matthias Pfefferle
|
* @author Matthias Pfefferle
|
||||||
*/
|
*/
|
||||||
class Scheduler {
|
class Scheduler {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the class, registering WordPress hooks
|
* Initialize the class, registering WordPress hooks
|
||||||
*/
|
*/
|
||||||
public static function init() {
|
public static function init() {
|
||||||
|
// Post transitions
|
||||||
\add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
|
\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 );
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// 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_update_followers', array( self::class, 'update_followers' ) );
|
||||||
\add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
|
\add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
|
||||||
|
|
||||||
|
// Migration
|
||||||
\add_action( 'admin_init', array( self::class, 'schedule_migration' ) );
|
\add_action( 'admin_init', array( self::class, 'schedule_migration' ) );
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -57,6 +113,8 @@ class Scheduler {
|
||||||
* @param WP_Post $post Post object.
|
* @param WP_Post $post Post object.
|
||||||
*/
|
*/
|
||||||
public static function schedule_post_activity( $new_status, $old_status, $post ) {
|
public static function schedule_post_activity( $new_status, $old_status, $post ) {
|
||||||
|
$post = get_post( $post );
|
||||||
|
|
||||||
// Do not send activities if post is password protected.
|
// Do not send activities if post is password protected.
|
||||||
if ( \post_password_required( $post ) ) {
|
if ( \post_password_required( $post ) ) {
|
||||||
return;
|
return;
|
||||||
|
@ -98,6 +156,57 @@ class Scheduler {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 approved comments.
|
||||||
|
if ( ! $comment->user_id ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
'approved' === $new_status &&
|
||||||
|
'approved' !== $old_status
|
||||||
|
) {
|
||||||
|
$type = 'Create';
|
||||||
|
} elseif ( 'approved' === $new_status ) {
|
||||||
|
$type = 'Update';
|
||||||
|
} elseif (
|
||||||
|
'trash' === $new_status ||
|
||||||
|
'spam' === $new_status
|
||||||
|
) {
|
||||||
|
$type = 'Delete';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $type ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
\wp_schedule_single_event(
|
||||||
|
\time(),
|
||||||
|
'activitypub_send_activity',
|
||||||
|
array( $comment, $type )
|
||||||
|
);
|
||||||
|
|
||||||
|
\wp_schedule_single_event(
|
||||||
|
\time(),
|
||||||
|
sprintf(
|
||||||
|
'activitypub_send_%s_activity',
|
||||||
|
\strtolower( $type )
|
||||||
|
),
|
||||||
|
array( $comment )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Update followers
|
* Update followers
|
||||||
*
|
*
|
||||||
|
@ -113,7 +222,7 @@ class Scheduler {
|
||||||
$followers = Followers::get_outdated_followers( $number );
|
$followers = Followers::get_outdated_followers( $number );
|
||||||
|
|
||||||
foreach ( $followers as $follower ) {
|
foreach ( $followers as $follower ) {
|
||||||
$meta = get_remote_metadata_by_actor( $follower->get_url(), false );
|
$meta = get_remote_metadata_by_actor( $follower->get_id(), false );
|
||||||
|
|
||||||
if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
|
if ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
|
||||||
Followers::add_error( $follower->get__id(), $meta );
|
Followers::add_error( $follower->get__id(), $meta );
|
||||||
|
@ -165,4 +274,68 @@ class Scheduler {
|
||||||
\wp_schedule_single_event( \time(), 'activitypub_schedule_migration' );
|
\wp_schedule_single_event( \time(), 'activitypub_schedule_migration' );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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, 'publish_posts' ) ) {
|
||||||
|
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, 'publish_posts' ) ) {
|
||||||
|
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 )
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -110,8 +110,13 @@ class Shortcodes {
|
||||||
|
|
||||||
$excerpt = \get_post_field( 'post_excerpt', $item );
|
$excerpt = \get_post_field( 'post_excerpt', $item );
|
||||||
|
|
||||||
if ( '' === $excerpt ) {
|
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 );
|
$content = \get_post_field( 'post_content', $item );
|
||||||
|
|
||||||
// An empty string will make wp_trim_excerpt do stuff we do not want.
|
// An empty string will make wp_trim_excerpt do stuff we do not want.
|
||||||
|
@ -127,8 +132,7 @@ class Shortcodes {
|
||||||
// Strip out any remaining tags.
|
// Strip out any remaining tags.
|
||||||
$excerpt = \wp_strip_all_tags( $excerpt );
|
$excerpt = \wp_strip_all_tags( $excerpt );
|
||||||
|
|
||||||
/** This filter is documented in wp-includes/formatting.php */
|
$excerpt_more = \apply_filters( 'activitypub_excerpt_more', ' […]' );
|
||||||
$excerpt_more = \apply_filters( 'excerpt_more', ' [...]' );
|
|
||||||
$excerpt_more_len = strlen( $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:
|
// We now have a excerpt, but we need to check it's length, it may be longer than we want for two reasons:
|
||||||
|
@ -208,6 +212,15 @@ class Shortcodes {
|
||||||
$tag
|
$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 );
|
$content = \get_post_field( 'post_content', $item );
|
||||||
|
|
||||||
if ( 'yes' === $atts['apply_filters'] ) {
|
if ( 'yes' === $atts['apply_filters'] ) {
|
||||||
|
@ -222,6 +235,7 @@ class Shortcodes {
|
||||||
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
|
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
|
||||||
$content = strip_shortcodes( $content );
|
$content = strip_shortcodes( $content );
|
||||||
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
|
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
|
||||||
|
}
|
||||||
|
|
||||||
add_shortcode( 'ap_content', array( 'Activitypub\Shortcodes', 'content' ) );
|
add_shortcode( 'ap_content', array( 'Activitypub\Shortcodes', 'content' ) );
|
||||||
|
|
||||||
|
|
|
@ -259,6 +259,10 @@ class Signature {
|
||||||
$headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0];
|
$headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: Fix the signtature verification. Posts from mobilizon fail at the moment.
|
||||||
|
// The next line is temporaty. It must be removed in production and releases.
|
||||||
|
return true;
|
||||||
|
|
||||||
if ( ! isset( $headers['signature'] ) ) {
|
if ( ! isset( $headers['signature'] ) ) {
|
||||||
return new WP_Error( 'activitypub_signature', __( 'Request not signed', 'activitypub' ), array( 'status' => 401 ) );
|
return new WP_Error( 'activitypub_signature', __( 'Request not signed', 'activitypub' ), array( 'status' => 401 ) );
|
||||||
}
|
}
|
||||||
|
@ -292,6 +296,7 @@ class Signature {
|
||||||
if ( is_array( $headers['digest'] ) ) {
|
if ( is_array( $headers['digest'] ) ) {
|
||||||
$headers['digest'] = $headers['digest'][0];
|
$headers['digest'] = $headers['digest'][0];
|
||||||
}
|
}
|
||||||
|
$hashalg = 'sha256';
|
||||||
$digest = explode( '=', $headers['digest'], 2 );
|
$digest = explode( '=', $headers['digest'], 2 );
|
||||||
if ( 'SHA-256' === $digest[0] ) {
|
if ( 'SHA-256' === $digest[0] ) {
|
||||||
$hashalg = 'sha256';
|
$hashalg = 'sha256';
|
||||||
|
|
|
@ -15,7 +15,7 @@ class Webfinger {
|
||||||
/**
|
/**
|
||||||
* Returns a users WebFinger "resource"
|
* Returns a users WebFinger "resource"
|
||||||
*
|
*
|
||||||
* @param int $user_id
|
* @param int $user_id The WordPress user id
|
||||||
*
|
*
|
||||||
* @return string The user-resource
|
* @return string The user-resource
|
||||||
*/
|
*/
|
||||||
|
@ -36,68 +36,65 @@ class Webfinger {
|
||||||
/**
|
/**
|
||||||
* Resolve a WebFinger resource
|
* Resolve a WebFinger resource
|
||||||
*
|
*
|
||||||
* @param string $resource The WebFinger resource
|
* @param string $uri The WebFinger Resource
|
||||||
*
|
*
|
||||||
* @return string|WP_Error The URL or WP_Error
|
* @return string|WP_Error The URL or WP_Error
|
||||||
*/
|
*/
|
||||||
public static function resolve( $resource ) {
|
public static function resolve( $uri ) {
|
||||||
if ( ! $resource ) {
|
$data = self::get_data( $uri );
|
||||||
return null;
|
|
||||||
|
if ( \is_wp_error( $data ) ) {
|
||||||
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( ! preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $resource, $m ) ) {
|
foreach ( $data['links'] as $link ) {
|
||||||
return null;
|
if (
|
||||||
}
|
'self' === $link['rel'] &&
|
||||||
|
'application/activity+json' === $link['type']
|
||||||
$transient_key = 'activitypub_resolve_' . ltrim( $resource, '@' );
|
) {
|
||||||
|
|
||||||
$link = \get_transient( $transient_key );
|
|
||||||
if ( $link ) {
|
|
||||||
return $link;
|
|
||||||
}
|
|
||||||
|
|
||||||
$url = \add_query_arg( 'resource', 'acct:' . ltrim( $resource, '@' ), 'https://' . $m[2] . '/.well-known/webfinger' );
|
|
||||||
if ( ! \wp_http_validate_url( $url ) ) {
|
|
||||||
$response = new WP_Error( 'invalid_webfinger_url', null, $url );
|
|
||||||
\set_transient( $transient_key, $response, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
|
||||||
return $response;
|
|
||||||
}
|
|
||||||
|
|
||||||
// try to access author URL
|
|
||||||
$response = \wp_remote_get(
|
|
||||||
$url,
|
|
||||||
array(
|
|
||||||
'headers' => array( 'Accept' => 'application/jrd+json' ),
|
|
||||||
'redirection' => 2,
|
|
||||||
'timeout' => 2,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( \is_wp_error( $response ) ) {
|
|
||||||
$link = new WP_Error( 'webfinger_url_not_accessible', null, $url );
|
|
||||||
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
|
||||||
return $link;
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = \wp_remote_retrieve_body( $response );
|
|
||||||
$body = \json_decode( $body, true );
|
|
||||||
|
|
||||||
if ( empty( $body['links'] ) ) {
|
|
||||||
$link = new WP_Error( 'webfinger_url_invalid_response', null, $url );
|
|
||||||
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
|
||||||
return $link;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ( $body['links'] as $link ) {
|
|
||||||
if ( 'self' === $link['rel'] && 'application/activity+json' === $link['type'] ) {
|
|
||||||
\set_transient( $transient_key, $link['href'], WEEK_IN_SECONDS );
|
|
||||||
return $link['href'];
|
return $link['href'];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$link = new WP_Error( 'webfinger_url_no_activitypub', null, $body );
|
return new WP_Error( 'webfinger_url_no_activitypub', null, $data );
|
||||||
\set_transient( $transient_key, $link, HOUR_IN_SECONDS ); // Cache the error for a shorter period.
|
}
|
||||||
return $link;
|
|
||||||
|
/**
|
||||||
|
* Transform a URI to an acct <identifier>@<host>
|
||||||
|
*
|
||||||
|
* @param string $uri The URI (acct:, mailto:, http:, https:)
|
||||||
|
*
|
||||||
|
* @return string|WP_Error Error or acct URI
|
||||||
|
*/
|
||||||
|
public static function uri_to_acct( $uri ) {
|
||||||
|
$data = self::get_data( $uri );
|
||||||
|
|
||||||
|
if ( is_wp_error( $data ) ) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if subject is an acct URI
|
||||||
|
if (
|
||||||
|
isset( $data['subject'] ) &&
|
||||||
|
\str_starts_with( $data['subject'], 'acct:' )
|
||||||
|
) {
|
||||||
|
return $data['subject'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// search for an acct URI in the aliases
|
||||||
|
if ( isset( $data['aliases'] ) ) {
|
||||||
|
foreach ( $data['aliases'] as $alias ) {
|
||||||
|
if ( \str_starts_with( $alias, 'acct:' ) ) {
|
||||||
|
return $alias;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_Error(
|
||||||
|
'webfinger_url_no_acct',
|
||||||
|
__( 'No acct URI found.', 'activitypub' ),
|
||||||
|
$data
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -137,7 +134,7 @@ class Webfinger {
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( empty( $host ) ) {
|
if ( empty( $host ) ) {
|
||||||
return new WP_Error( 'invalid_identifier', __( 'Invalid Identifier', 'activitypub' ) );
|
return new WP_Error( 'webfinger_invalid_identifier', __( 'Invalid Identifier', 'activitypub' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
return array( $identifier, $host );
|
return array( $identifier, $host );
|
||||||
|
@ -146,55 +143,70 @@ class Webfinger {
|
||||||
/**
|
/**
|
||||||
* Get the WebFinger data for a given URI
|
* Get the WebFinger data for a given URI
|
||||||
*
|
*
|
||||||
* @param string $identifier The Identifier: <identifier>@<host>
|
* @param string $uri The Identifier: <identifier>@<host> or URI
|
||||||
* @param string $host The Host: <identifier>@<host>
|
|
||||||
*
|
*
|
||||||
* @return WP_Error|array Error reaction or array with
|
* @return WP_Error|array Error reaction or array with
|
||||||
* identifier and host as values
|
* identifier and host as values
|
||||||
*/
|
*/
|
||||||
public static function get_data( $identifier, $host ) {
|
public static function get_data( $uri ) {
|
||||||
$webfinger_url = 'https://' . $host . '/.well-known/webfinger?resource=' . rawurlencode( $identifier );
|
|
||||||
|
|
||||||
$response = wp_safe_remote_get(
|
|
||||||
$webfinger_url,
|
|
||||||
array(
|
|
||||||
'headers' => array( 'Accept' => 'application/jrd+json' ),
|
|
||||||
'redirection' => 0,
|
|
||||||
'timeout' => 2,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( is_wp_error( $response ) ) {
|
|
||||||
return new WP_Error( 'webfinger_url_not_accessible', null, $webfinger_url );
|
|
||||||
}
|
|
||||||
|
|
||||||
$body = wp_remote_retrieve_body( $response );
|
|
||||||
|
|
||||||
return json_decode( $body, true );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Undocumented function
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function get_remote_follow_endpoint( $uri ) {
|
|
||||||
$identifier_and_host = self::get_identifier_and_host( $uri );
|
$identifier_and_host = self::get_identifier_and_host( $uri );
|
||||||
|
|
||||||
if ( is_wp_error( $identifier_and_host ) ) {
|
if ( is_wp_error( $identifier_and_host ) ) {
|
||||||
return $identifier_and_host;
|
return $identifier_and_host;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$transient_key = self::generate_cache_key( $uri );
|
||||||
|
|
||||||
list( $identifier, $host ) = $identifier_and_host;
|
list( $identifier, $host ) = $identifier_and_host;
|
||||||
|
|
||||||
$data = self::get_data( $identifier, $host );
|
$data = \get_transient( $transient_key );
|
||||||
|
if ( $data ) {
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
$webfinger_url = 'https://' . $host . '/.well-known/webfinger?resource=' . rawurlencode( $identifier );
|
||||||
|
|
||||||
|
$response = wp_safe_remote_get(
|
||||||
|
$webfinger_url,
|
||||||
|
array(
|
||||||
|
'headers' => array( 'Accept' => 'application/jrd+json' ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
return new WP_Error(
|
||||||
|
'webfinger_url_not_accessible',
|
||||||
|
__( 'The WebFinger Resource is not accessible.', 'activitypub' ),
|
||||||
|
$webfinger_url
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$body = wp_remote_retrieve_body( $response );
|
||||||
|
$data = json_decode( $body, true );
|
||||||
|
|
||||||
|
\set_transient( $transient_key, $data, WEEK_IN_SECONDS );
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the Remote-Follow endpoint for a given URI
|
||||||
|
*
|
||||||
|
* @return string|WP_Error Error or the Remote-Follow endpoint URI.
|
||||||
|
*/
|
||||||
|
public static function get_remote_follow_endpoint( $uri ) {
|
||||||
|
$data = self::get_data( $uri );
|
||||||
|
|
||||||
if ( is_wp_error( $data ) ) {
|
if ( is_wp_error( $data ) ) {
|
||||||
return $data;
|
return $data;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( empty( $data['links'] ) ) {
|
if ( empty( $data['links'] ) ) {
|
||||||
return new WP_Error( 'webfinger_url_invalid_response', null, $data );
|
return new WP_Error(
|
||||||
|
'webfinger_missing_links',
|
||||||
|
__( 'No valid Link elements found.', 'activitypub' ),
|
||||||
|
$data
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach ( $data['links'] as $link ) {
|
foreach ( $data['links'] as $link ) {
|
||||||
|
@ -203,6 +215,27 @@ class Webfinger {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return new WP_Error( 'webfinger_remote_follow_endpoint_invalid', $data, array( 'status' => 417 ) );
|
return new WP_Error(
|
||||||
|
'webfinger_missing_remote_follow_endpoint',
|
||||||
|
__( 'No valid Remote-Follow endpoint found.', 'activitypub' ),
|
||||||
|
$data
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a cache key for a given URI
|
||||||
|
*
|
||||||
|
* @param string $uri A WebFinger Resource URI
|
||||||
|
*
|
||||||
|
* @return string The cache key
|
||||||
|
*/
|
||||||
|
public static function generate_cache_key( $uri ) {
|
||||||
|
$uri = ltrim( $uri, '@' );
|
||||||
|
|
||||||
|
if ( filter_var( $uri, FILTER_VALIDATE_EMAIL ) ) {
|
||||||
|
$uri = 'acct:' . $uri;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'webfinger_' . md5( $uri );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
53
includes/collection/class-follow-requests.php
Normal file
53
includes/collection/class-follow-requests.php
Normal file
|
@ -0,0 +1,53 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Collection;
|
||||||
|
|
||||||
|
use WP_Error;
|
||||||
|
use WP_Query;
|
||||||
|
use Activitypub\Http;
|
||||||
|
use Activitypub\Webfinger;
|
||||||
|
use Activitypub\Activity\Base_Object;
|
||||||
|
use Activitypub\Model\Follower;
|
||||||
|
|
||||||
|
use function Activitypub\is_tombstone;
|
||||||
|
use function Activitypub\get_remote_metadata_by_actor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ActivityPub Follow Requests Collection
|
||||||
|
*
|
||||||
|
* @author André Menrath
|
||||||
|
*/
|
||||||
|
class Follow_Requests {
|
||||||
|
/**
|
||||||
|
* Get a follow request together with information from the follower.
|
||||||
|
*
|
||||||
|
* @param int $user_id The ID of the WordPress User
|
||||||
|
* @param int $follower_id The follower ID
|
||||||
|
*
|
||||||
|
* @return \Activitypub\Model\Follow|null The Follower object or null
|
||||||
|
*/
|
||||||
|
public static function get_follow_requests_for_user( $user_id, $per_page, $page, $args ) {
|
||||||
|
$order = isset($args['order']) && strtolower($args['order']) === 'asc' ? 'ASC' : 'DESC';
|
||||||
|
$orderby = isset($args['orderby']) ? sanitize_text_field($args['orderby']) : 'published';
|
||||||
|
|
||||||
|
global $wpdb;
|
||||||
|
$follow_requests = $wpdb->get_results(
|
||||||
|
$wpdb->prepare(
|
||||||
|
"SELECT SQL_CALC_FOUND_ROWS follow_request.ID AS id, follow_request.post_date AS published, follow_request.guid, follow_request.post_status AS 'status', follower.post_title AS 'post_title', follower.guid AS follower_guid, follower.id AS follower_id, follower.post_modified AS follower_modified
|
||||||
|
FROM {$wpdb->posts} AS follow_request
|
||||||
|
LEFT JOIN {$wpdb->posts} AS follower ON follow_request.post_parent = follower.ID
|
||||||
|
LEFT JOIN {$wpdb->postmeta} AS meta ON follow_request.ID = meta.post_id
|
||||||
|
WHERE follow_request.post_type = 'ap_follow_request'
|
||||||
|
AND meta.meta_key = 'activitypub_user_id'
|
||||||
|
AND meta.meta_value = %s
|
||||||
|
ORDER BY {$orderby} {$order}
|
||||||
|
LIMIT %d OFFSET %d",
|
||||||
|
$user_id,
|
||||||
|
$per_page,
|
||||||
|
0
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$total_items = $wpdb->get_var("SELECT FOUND_ROWS()");
|
||||||
|
|
||||||
|
return compact( 'follow_requests', 'total_items' );
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,14 +2,10 @@
|
||||||
namespace Activitypub\Collection;
|
namespace Activitypub\Collection;
|
||||||
|
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
use Exception;
|
|
||||||
use WP_Query;
|
use WP_Query;
|
||||||
use Activitypub\Http;
|
use Activitypub\Http;
|
||||||
use Activitypub\Webfinger;
|
use Activitypub\Webfinger;
|
||||||
use Activitypub\Model\Follower;
|
use Activitypub\Model\Follower;
|
||||||
use Activitypub\Collection\Users;
|
|
||||||
use Activitypub\Activity\Activity;
|
|
||||||
use Activitypub\Activity\Base_Object;
|
|
||||||
|
|
||||||
use function Activitypub\is_tombstone;
|
use function Activitypub\is_tombstone;
|
||||||
use function Activitypub\get_remote_metadata_by_actor;
|
use function Activitypub\get_remote_metadata_by_actor;
|
||||||
|
@ -24,136 +20,6 @@ class Followers {
|
||||||
const POST_TYPE = 'ap_follower';
|
const POST_TYPE = 'ap_follower';
|
||||||
const CACHE_KEY_INBOXES = 'follower_inboxes_%s';
|
const CACHE_KEY_INBOXES = 'follower_inboxes_%s';
|
||||||
|
|
||||||
/**
|
|
||||||
* Register WordPress hooks/actions and register Taxonomy
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function init() {
|
|
||||||
// register "followers" post_type
|
|
||||||
self::register_post_type();
|
|
||||||
|
|
||||||
\add_action( 'activitypub_inbox_follow', array( self::class, 'handle_follow_request' ), 10, 2 );
|
|
||||||
\add_action( 'activitypub_inbox_undo', array( self::class, 'handle_undo_request' ), 10, 2 );
|
|
||||||
|
|
||||||
\add_action( 'activitypub_followers_post_follow', array( self::class, 'send_follow_response' ), 10, 4 );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register the "Followers" Taxonomy
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
private static function register_post_type() {
|
|
||||||
register_post_type(
|
|
||||||
self::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(
|
|
||||||
self::POST_TYPE,
|
|
||||||
'activitypub_inbox',
|
|
||||||
array(
|
|
||||||
'type' => 'string',
|
|
||||||
'single' => true,
|
|
||||||
'sanitize_callback' => array( self::class, 'sanitize_url' ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
register_post_meta(
|
|
||||||
self::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(
|
|
||||||
self::POST_TYPE,
|
|
||||||
'activitypub_user_id',
|
|
||||||
array(
|
|
||||||
'type' => 'string',
|
|
||||||
'single' => false,
|
|
||||||
'sanitize_callback' => function( $value ) {
|
|
||||||
return esc_sql( $value );
|
|
||||||
},
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
register_post_meta(
|
|
||||||
self::POST_TYPE,
|
|
||||||
'activitypub_actor_json',
|
|
||||||
array(
|
|
||||||
'type' => 'string',
|
|
||||||
'single' => true,
|
|
||||||
'sanitize_callback' => function( $value ) {
|
|
||||||
return sanitize_text_field( $value );
|
|
||||||
},
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
do_action( 'activitypub_after_register_post_type' );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function sanitize_url( $value ) {
|
|
||||||
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return esc_url_raw( $value );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle the "Follow" Request
|
|
||||||
*
|
|
||||||
* @param array $object The JSON "Follow" Activity
|
|
||||||
* @param int $user_id The ID of the ID of the WordPress User
|
|
||||||
*
|
|
||||||
* @return void
|
|
||||||
*/
|
|
||||||
public static function handle_follow_request( $object, $user_id ) {
|
|
||||||
// save follower
|
|
||||||
$follower = self::add_follower( $user_id, $object['actor'] );
|
|
||||||
|
|
||||||
do_action( 'activitypub_followers_post_follow', $object['actor'], $object, $user_id, $follower );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Handle "Unfollow" requests
|
|
||||||
*
|
|
||||||
* @param array $object The JSON "Undo" Activity
|
|
||||||
* @param int $user_id The ID of the ID of the WordPress User
|
|
||||||
*/
|
|
||||||
public static function handle_undo_request( $object, $user_id ) {
|
|
||||||
if (
|
|
||||||
isset( $object['object'] ) &&
|
|
||||||
isset( $object['object']['type'] ) &&
|
|
||||||
'Follow' === $object['object']['type']
|
|
||||||
) {
|
|
||||||
self::remove_follower( $user_id, $object['actor'] );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add new Follower
|
* Add new Follower
|
||||||
*
|
*
|
||||||
|
@ -176,17 +42,21 @@ class Followers {
|
||||||
$follower = new Follower();
|
$follower = new Follower();
|
||||||
$follower->from_array( $meta );
|
$follower->from_array( $meta );
|
||||||
|
|
||||||
$id = $follower->upsert();
|
// Save the follower to the internal post type or update if it's ActivityPub ID is already known.
|
||||||
|
$follower_id = $follower->upsert();
|
||||||
|
|
||||||
if ( is_wp_error( $id ) ) {
|
if ( is_wp_error( $follower_id ) ) {
|
||||||
return $id;
|
return $follower_id;
|
||||||
}
|
}
|
||||||
|
|
||||||
$post_meta = get_post_meta( $id, 'activitypub_user_id' );
|
$post_meta = get_post_meta( $follower_id, 'activitypub_user_id' );
|
||||||
|
|
||||||
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
|
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
|
||||||
|
// Check if the follow relationship is already present.
|
||||||
if ( is_array( $post_meta ) && ! in_array( $user_id, $post_meta ) ) {
|
if ( is_array( $post_meta ) && ! in_array( $user_id, $post_meta ) ) {
|
||||||
add_post_meta( $id, 'activitypub_user_id', $user_id );
|
// Actually save the follow relationship
|
||||||
|
add_post_meta( $follower_id, 'activitypub_user_id', $user_id );
|
||||||
|
// Reset the cached inboxes for the followed user
|
||||||
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
|
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -214,16 +84,17 @@ class Followers {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get a Follower
|
* Get a Follower.
|
||||||
*
|
*
|
||||||
* @param int $user_id The ID of the WordPress User
|
* @param int $user_id The ID of the WordPress User
|
||||||
* @param string $actor The Actor URL
|
* @param string $actor The Actor URL
|
||||||
*
|
*
|
||||||
* @return \Activitypub\Model\Follower The Follower object
|
* @return \Activitypub\Model\Follower|null The Follower object or null
|
||||||
*/
|
*/
|
||||||
public static function get_follower( $user_id, $actor ) {
|
public static function get_follower( $user_id, $actor ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
$post_id = $wpdb->get_var(
|
$post_id = $wpdb->get_var(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
"SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s",
|
"SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s",
|
||||||
|
@ -244,51 +115,29 @@ class Followers {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send Accept response
|
* Get a Follower by Actor indepenent from the User.
|
||||||
*
|
*
|
||||||
* @param string $actor The Actor URL
|
* @param string $actor The Actor URL.
|
||||||
* @param array $object The Activity object
|
|
||||||
* @param int $user_id The ID of the WordPress User
|
|
||||||
* @param Activitypub\Model\Follower $follower The Follower object
|
|
||||||
*
|
*
|
||||||
* @return void
|
* @return \Activitypub\Model\Follower|null The Follower object or null
|
||||||
*/
|
*/
|
||||||
public static function send_follow_response( $actor, $object, $user_id, $follower ) {
|
public static function get_follower_by_actor( $actor ) {
|
||||||
if ( is_wp_error( $follower ) ) {
|
global $wpdb;
|
||||||
// it is not even possible to send a "Reject" because
|
|
||||||
// we can not get the Remote-Inbox
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// only send minimal data
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
$object = array_intersect_key(
|
$post_id = $wpdb->get_var(
|
||||||
$object,
|
$wpdb->prepare(
|
||||||
array_flip(
|
"SELECT ID FROM $wpdb->posts WHERE guid=%s",
|
||||||
array(
|
esc_sql( $actor )
|
||||||
'id',
|
|
||||||
'type',
|
|
||||||
'actor',
|
|
||||||
'object',
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$user = Users::get_by_id( $user_id );
|
if ( $post_id ) {
|
||||||
|
$post = get_post( $post_id );
|
||||||
|
return Follower::init_from_cpt( $post );
|
||||||
|
}
|
||||||
|
|
||||||
// get inbox
|
return null;
|
||||||
$inbox = $follower->get_shared_inbox();
|
|
||||||
|
|
||||||
// send "Accept" activity
|
|
||||||
$activity = new Activity();
|
|
||||||
$activity->set_type( 'Accept' );
|
|
||||||
$activity->set_object( $object );
|
|
||||||
$activity->set_actor( $user->get_id() );
|
|
||||||
$activity->set_to( $actor );
|
|
||||||
$activity->set_id( $user->get_id() . '#follow-' . \preg_replace( '~^https?://~', '', $actor ) . '-' . \time() );
|
|
||||||
|
|
||||||
$activity = $activity->to_json();
|
|
||||||
|
|
||||||
Http::post( $inbox, $activity, $user_id );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -354,6 +203,7 @@ class Followers {
|
||||||
*/
|
*/
|
||||||
public static function get_all_followers() {
|
public static function get_all_followers() {
|
||||||
$args = array(
|
$args = array(
|
||||||
|
'nopaging' => true,
|
||||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||||
'meta_query' => array(
|
'meta_query' => array(
|
||||||
'relation' => 'AND',
|
'relation' => 'AND',
|
||||||
|
@ -413,15 +263,16 @@ class Followers {
|
||||||
*/
|
*/
|
||||||
public static function get_inboxes( $user_id ) {
|
public static function get_inboxes( $user_id ) {
|
||||||
$cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id );
|
$cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id );
|
||||||
$inboxes = wp_cache_get( $cache_key, 'activitypub' );
|
// $inboxes = wp_cache_get( $cache_key, 'activitypub' );
|
||||||
|
|
||||||
if ( $inboxes ) {
|
// if ( $inboxes ) {
|
||||||
return $inboxes;
|
// return $inboxes;
|
||||||
}
|
// }
|
||||||
|
|
||||||
// get all Followers of a ID of the WordPress User
|
// get all Followers of a ID of the WordPress User
|
||||||
$posts = new WP_Query(
|
$follower_query = new WP_Query(
|
||||||
array(
|
array(
|
||||||
|
'nopaging' => true,
|
||||||
'post_type' => self::POST_TYPE,
|
'post_type' => self::POST_TYPE,
|
||||||
'fields' => 'ids',
|
'fields' => 'ids',
|
||||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||||
|
@ -444,21 +295,44 @@ class Followers {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
$posts = $posts->get_posts();
|
$follower_ids = $follower_query->get_posts();
|
||||||
|
|
||||||
if ( ! $posts ) {
|
if ( ! $follower_ids ) {
|
||||||
return array();
|
return array();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$user = Users::get_by_id( $user_id );
|
||||||
|
|
||||||
|
if ( $user->get_manually_approved_followers() ) {
|
||||||
|
$accepted_follow_requests_query = new WP_Query(
|
||||||
|
array(
|
||||||
|
'nopaging' => true,
|
||||||
|
'post_type' => 'ap_follow_request',
|
||||||
|
'fields' => 'id=>parent',
|
||||||
|
'post_status' => 'publish',
|
||||||
|
'post_parent__in' => $follower_ids,
|
||||||
|
// phpcs:ign ore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => 'activitypub_user_id',
|
||||||
|
'value' => $user_id,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$accepted_follow_requests = $accepted_follow_requests_query->get_posts();
|
||||||
|
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||||
$results = $wpdb->get_col(
|
$results = $wpdb->get_col(
|
||||||
$wpdb->prepare(
|
$wpdb->prepare(
|
||||||
"SELECT DISTINCT meta_value FROM {$wpdb->postmeta}
|
"SELECT DISTINCT meta_value FROM {$wpdb->postmeta}
|
||||||
WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ")
|
WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $follower_ids ), '%d' ) ) . ")
|
||||||
AND meta_key = 'activitypub_inbox'
|
AND meta_key = 'activitypub_inbox'
|
||||||
AND meta_value IS NOT NULL",
|
AND meta_value IS NOT NULL",
|
||||||
$posts
|
$follower_ids
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
239
includes/collection/class-interactions.php
Normal file
239
includes/collection/class-interactions.php
Normal file
|
@ -0,0 +1,239 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Collection;
|
||||||
|
|
||||||
|
use WP_Error;
|
||||||
|
use WP_Comment_Query;
|
||||||
|
|
||||||
|
use function Activitypub\url_to_commentid;
|
||||||
|
use function Activitypub\object_id_to_comment;
|
||||||
|
use function Activitypub\get_remote_metadata_by_actor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ActivityPub Interactions Collection
|
||||||
|
*/
|
||||||
|
class Interactions {
|
||||||
|
/**
|
||||||
|
* Add a comment to a post
|
||||||
|
*
|
||||||
|
* @param array $activity The activity-object
|
||||||
|
*
|
||||||
|
* @return array|false The commentdata or false on failure
|
||||||
|
*/
|
||||||
|
public static function add_comment( $activity ) {
|
||||||
|
if (
|
||||||
|
! isset( $activity['object'] ) ||
|
||||||
|
! isset( $activity['object']['id'] )
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! isset( $activity['object']['inReplyTo'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$in_reply_to = \esc_url_raw( $activity['object']['inReplyTo'] );
|
||||||
|
$comment_post_id = \url_to_postid( $in_reply_to );
|
||||||
|
$parent_comment_id = url_to_commentid( $in_reply_to );
|
||||||
|
|
||||||
|
// save only replys and reactions
|
||||||
|
if ( ! $comment_post_id && $parent_comment_id ) {
|
||||||
|
$parent_comment = get_comment( $parent_comment_id );
|
||||||
|
$comment_post_id = $parent_comment->comment_post_ID;
|
||||||
|
}
|
||||||
|
|
||||||
|
// not a reply to a post or comment
|
||||||
|
if ( ! $comment_post_id ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$meta = get_remote_metadata_by_actor( $activity['actor'] );
|
||||||
|
|
||||||
|
if ( ! $meta || \is_wp_error( $meta ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$commentdata = array(
|
||||||
|
'comment_post_ID' => $comment_post_id,
|
||||||
|
'comment_author' => \esc_attr( $meta['name'] ),
|
||||||
|
'comment_author_url' => \esc_url_raw( $meta['url'] ),
|
||||||
|
'comment_content' => \addslashes( $activity['object']['content'] ),
|
||||||
|
'comment_type' => 'comment',
|
||||||
|
'comment_author_email' => '',
|
||||||
|
'comment_parent' => $parent_comment_id ? $parent_comment_id : 0,
|
||||||
|
'comment_meta' => array(
|
||||||
|
'source_id' => \esc_url_raw( $activity['object']['id'] ),
|
||||||
|
'protocol' => 'activitypub',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( isset( $meta['icon']['url'] ) ) {
|
||||||
|
$commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( isset( $activity['object']['url'] ) ) {
|
||||||
|
$commentdata['comment_meta']['source_url'] = \esc_url_raw( $activity['object']['url'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable flood control
|
||||||
|
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
|
||||||
|
// do not require email for AP entries
|
||||||
|
\add_filter( 'pre_option_require_name_email', '__return_false' );
|
||||||
|
// No nonce possible for this submission route
|
||||||
|
\add_filter(
|
||||||
|
'akismet_comment_nonce',
|
||||||
|
function () {
|
||||||
|
return 'inactive';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
\add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
|
||||||
|
|
||||||
|
$comment = \wp_new_comment( $commentdata, true );
|
||||||
|
|
||||||
|
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
|
||||||
|
\remove_filter( 'pre_option_require_name_email', '__return_false' );
|
||||||
|
// re-add flood control
|
||||||
|
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
|
||||||
|
|
||||||
|
return $comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update a comment
|
||||||
|
*
|
||||||
|
* @param array $activity The activity-object
|
||||||
|
*
|
||||||
|
* @return array|false The commentdata or false on failure
|
||||||
|
*/
|
||||||
|
public static function update_comment( $activity ) {
|
||||||
|
$meta = get_remote_metadata_by_actor( $activity['actor'] );
|
||||||
|
|
||||||
|
//Determine comment_ID
|
||||||
|
$comment = object_id_to_comment( \esc_url_raw( $activity['object']['id'] ) );
|
||||||
|
$commentdata = \get_comment( $comment, ARRAY_A );
|
||||||
|
|
||||||
|
if ( ! $commentdata ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
//found a local comment id
|
||||||
|
$commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] );
|
||||||
|
$commentdata['comment_content'] = \addslashes( $activity['object']['content'] );
|
||||||
|
if ( isset( $meta['icon']['url'] ) ) {
|
||||||
|
$commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
// disable flood control
|
||||||
|
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
|
||||||
|
// do not require email for AP entries
|
||||||
|
\add_filter( 'pre_option_require_name_email', '__return_false' );
|
||||||
|
// No nonce possible for this submission route
|
||||||
|
\add_filter(
|
||||||
|
'akismet_comment_nonce',
|
||||||
|
function () {
|
||||||
|
return 'inactive';
|
||||||
|
}
|
||||||
|
);
|
||||||
|
\add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
|
||||||
|
|
||||||
|
$comment = \wp_update_comment( $commentdata, true );
|
||||||
|
|
||||||
|
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
|
||||||
|
\remove_filter( 'pre_option_require_name_email', '__return_false' );
|
||||||
|
// re-add flood control
|
||||||
|
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
|
||||||
|
|
||||||
|
return $comment;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get interaction(s) for a given URL/ID.
|
||||||
|
*
|
||||||
|
* @param strin $url The URL/ID to get interactions for.
|
||||||
|
*
|
||||||
|
* @return array The interactions as WP_Comment objects.
|
||||||
|
*/
|
||||||
|
public static function get_interaction_by_id( $url ) {
|
||||||
|
$args = array(
|
||||||
|
'nopaging' => true,
|
||||||
|
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||||
|
'meta_query' => array(
|
||||||
|
'relation' => 'AND',
|
||||||
|
array(
|
||||||
|
'key' => 'protocol',
|
||||||
|
'value' => 'activitypub',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'relation' => 'OR',
|
||||||
|
array(
|
||||||
|
'key' => 'source_url',
|
||||||
|
'value' => $url,
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'key' => 'source_id',
|
||||||
|
'value' => $url,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$query = new WP_Comment_Query( $args );
|
||||||
|
return $query->comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get interaction(s) for a given actor.
|
||||||
|
*
|
||||||
|
* @param string $actor The Actor-URL.
|
||||||
|
*
|
||||||
|
* @return array The interactions as WP_Comment objects.
|
||||||
|
*/
|
||||||
|
public static function get_interactions_by_actor( $actor ) {
|
||||||
|
$meta = get_remote_metadata_by_actor( $actor );
|
||||||
|
|
||||||
|
// get URL, because $actor seems to be the ID
|
||||||
|
if ( $meta && ! is_wp_error( $meta ) && isset( $meta['url'] ) ) {
|
||||||
|
$actor = $meta['url'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'nopaging' => true,
|
||||||
|
'author_url' => $actor,
|
||||||
|
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => 'protocol',
|
||||||
|
'value' => 'activitypub',
|
||||||
|
'compare' => '=',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
$comment_query = new WP_Comment_Query( $args );
|
||||||
|
return $comment_query->comments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds line breaks to the list of allowed comment tags.
|
||||||
|
*
|
||||||
|
* @param array $allowed_tags Allowed HTML tags.
|
||||||
|
* @param string $context Context.
|
||||||
|
*
|
||||||
|
* @return array Filtered tag list.
|
||||||
|
*/
|
||||||
|
public static function allowed_comment_html( $allowed_tags, $context = '' ) {
|
||||||
|
if ( 'pre_comment_content' !== $context ) {
|
||||||
|
// Do nothing.
|
||||||
|
return $allowed_tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add `p` and `br` to the list of allowed tags.
|
||||||
|
if ( ! array_key_exists( 'br', $allowed_tags ) ) {
|
||||||
|
$allowed_tags['br'] = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! array_key_exists( 'p', $allowed_tags ) ) {
|
||||||
|
$allowed_tags['p'] = array();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $allowed_tags;
|
||||||
|
}
|
||||||
|
}
|
|
@ -7,6 +7,8 @@ use Activitypub\Model\User;
|
||||||
use Activitypub\Model\Blog_User;
|
use Activitypub\Model\Blog_User;
|
||||||
use Activitypub\Model\Application_User;
|
use Activitypub\Model\Application_User;
|
||||||
|
|
||||||
|
use function Activitypub\get_rest_url_by_path;
|
||||||
|
use function Activitypub\url_to_authorid;
|
||||||
use function Activitypub\is_user_disabled;
|
use function Activitypub\is_user_disabled;
|
||||||
|
|
||||||
class Users {
|
class Users {
|
||||||
|
@ -103,6 +105,8 @@ class Users {
|
||||||
return self::get_by_id( $user->results[0] );
|
return self::get_by_id( $user->results[0] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$username = str_replace( array( '*', '%' ), '', $username );
|
||||||
|
|
||||||
// check for login or nicename.
|
// check for login or nicename.
|
||||||
$user = new WP_User_Query(
|
$user = new WP_User_Query(
|
||||||
array(
|
array(
|
||||||
|
@ -133,21 +137,64 @@ class Users {
|
||||||
* @return \Acitvitypub\Model\User The User.
|
* @return \Acitvitypub\Model\User The User.
|
||||||
*/
|
*/
|
||||||
public static function get_by_resource( $resource ) {
|
public static function get_by_resource( $resource ) {
|
||||||
if ( \strpos( $resource, '@' ) === false ) {
|
$scheme = 'acct';
|
||||||
return new WP_Error(
|
$match = array();
|
||||||
'activitypub_unsupported_resource',
|
// try to extract the scheme and the host
|
||||||
\__( 'Resource is invalid', 'activitypub' ),
|
if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $resource, $match ) ) {
|
||||||
array( 'status' => 400 )
|
// extract the scheme
|
||||||
);
|
$scheme = esc_attr( $match[1] );
|
||||||
}
|
}
|
||||||
|
|
||||||
$resource = \str_replace( 'acct:', '', $resource );
|
switch ( $scheme ) {
|
||||||
|
// check for http(s) URIs
|
||||||
|
case 'http':
|
||||||
|
case 'https':
|
||||||
|
$url_parts = wp_parse_url( $resource );
|
||||||
|
|
||||||
$resource_identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) );
|
// check for http(s)://blog.example.com/@username
|
||||||
$resource_host = self::normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) );
|
if (
|
||||||
|
isset( $url_parts['path'] ) &&
|
||||||
|
str_starts_with( $url_parts['path'], '/@' )
|
||||||
|
) {
|
||||||
|
$identifier = str_replace( '/@', '', $url_parts['path'] );
|
||||||
|
$identifier = untrailingslashit( $identifier );
|
||||||
|
|
||||||
|
return self::get_by_username( $identifier );
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for http(s)://blog.example.com/author/username
|
||||||
|
$user_id = url_to_authorid( $resource );
|
||||||
|
|
||||||
|
if ( $user_id ) {
|
||||||
|
return self::get_by_id( $user_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for http(s)://blog.example.com/
|
||||||
|
if (
|
||||||
|
self::normalize_url( site_url() ) === self::normalize_url( $resource ) ||
|
||||||
|
self::normalize_url( home_url() ) === self::normalize_url( $resource )
|
||||||
|
) {
|
||||||
|
return self::get_by_id( self::BLOG_USER_ID );
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for application actor
|
||||||
|
if ( self::normalize_url( get_rest_url_by_path( 'application' ) ) === self::normalize_url( $resource ) ) {
|
||||||
|
return self::get_by_id( self::APPLICATION_USER_ID );
|
||||||
|
}
|
||||||
|
|
||||||
|
return new WP_Error(
|
||||||
|
'activitypub_no_user_found',
|
||||||
|
\__( 'User not found', 'activitypub' ),
|
||||||
|
array( 'status' => 404 )
|
||||||
|
);
|
||||||
|
// check for acct URIs
|
||||||
|
case 'acct':
|
||||||
|
$resource = \str_replace( 'acct:', '', $resource );
|
||||||
|
$identifier = \substr( $resource, 0, \strrpos( $resource, '@' ) );
|
||||||
|
$host = self::normalize_host( \substr( \strrchr( $resource, '@' ), 1 ) );
|
||||||
$blog_host = self::normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) );
|
$blog_host = self::normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) );
|
||||||
|
|
||||||
if ( $blog_host !== $resource_host ) {
|
if ( $blog_host !== $host ) {
|
||||||
return new WP_Error(
|
return new WP_Error(
|
||||||
'activitypub_wrong_host',
|
'activitypub_wrong_host',
|
||||||
\__( 'Resource host does not match blog host', 'activitypub' ),
|
\__( 'Resource host does not match blog host', 'activitypub' ),
|
||||||
|
@ -155,7 +202,19 @@ class Users {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return self::get_by_username( $resource_identifier );
|
// prepare wildcards https://github.com/mastodon/mastodon/issues/22213
|
||||||
|
if ( in_array( $identifier, array( '_', '*', '' ), true ) ) {
|
||||||
|
return self::get_by_id( self::BLOG_USER_ID );
|
||||||
|
}
|
||||||
|
|
||||||
|
return self::get_by_username( $identifier );
|
||||||
|
default:
|
||||||
|
return new WP_Error(
|
||||||
|
'activitypub_wrong_scheme',
|
||||||
|
\__( 'Wrong scheme', 'activitypub' ),
|
||||||
|
array( 'status' => 404 )
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -168,7 +227,12 @@ class Users {
|
||||||
public static function get_by_various( $id ) {
|
public static function get_by_various( $id ) {
|
||||||
if ( is_numeric( $id ) ) {
|
if ( is_numeric( $id ) ) {
|
||||||
return self::get_by_id( $id );
|
return self::get_by_id( $id );
|
||||||
} elseif ( filter_var( $id, FILTER_VALIDATE_URL ) ) {
|
} elseif (
|
||||||
|
// is URL
|
||||||
|
filter_var( $id, FILTER_VALIDATE_URL ) ||
|
||||||
|
// is acct
|
||||||
|
str_starts_with( $id, 'acct:' )
|
||||||
|
) {
|
||||||
return self::get_by_resource( $id );
|
return self::get_by_resource( $id );
|
||||||
} else {
|
} else {
|
||||||
return self::get_by_username( $id );
|
return self::get_by_username( $id );
|
||||||
|
@ -176,7 +240,7 @@ class Users {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Normalize the host.
|
* Normalize a host.
|
||||||
*
|
*
|
||||||
* @param string $host The host.
|
* @param string $host The host.
|
||||||
*
|
*
|
||||||
|
@ -186,6 +250,22 @@ class Users {
|
||||||
return \str_replace( 'www.', '', $host );
|
return \str_replace( 'www.', '', $host );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalize a URL.
|
||||||
|
*
|
||||||
|
* @param string $url The URL.
|
||||||
|
*
|
||||||
|
* @return string The normalized URL.
|
||||||
|
*/
|
||||||
|
public static function normalize_url( $url ) {
|
||||||
|
$url = \untrailingslashit( $url );
|
||||||
|
$url = \str_replace( 'https://', '', $url );
|
||||||
|
$url = \str_replace( 'http://', '', $url );
|
||||||
|
$url = \str_replace( 'www.', '', $url );
|
||||||
|
|
||||||
|
return $url;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the User collection.
|
* Get the User collection.
|
||||||
*
|
*
|
||||||
|
|
|
@ -47,3 +47,53 @@ if ( ! function_exists( 'is_countable' ) ) {
|
||||||
return is_array( $value ) || $value instanceof \Countable;
|
return is_array( $value ) || $value instanceof \Countable;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Polyfill for `array_is_list()` function added in PHP 7.3.
|
||||||
|
*
|
||||||
|
* @param array $array The array to check.
|
||||||
|
*
|
||||||
|
* @return bool True if `$array` is a list, otherwise false.
|
||||||
|
*/
|
||||||
|
if ( ! function_exists( 'array_is_list' ) ) {
|
||||||
|
function array_is_list( $array ) {
|
||||||
|
if ( ! is_array( $array ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( array_values( $array ) === $array ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
$next_key = -1;
|
||||||
|
|
||||||
|
foreach ( $array as $k => $v ) {
|
||||||
|
if ( ++$next_key !== $k ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! function_exists( 'str_contains' ) ) {
|
||||||
|
/**
|
||||||
|
* Polyfill for `str_contains()` function added in PHP 8.0.
|
||||||
|
*
|
||||||
|
* Performs a case-sensitive check indicating if needle is
|
||||||
|
* contained in haystack.
|
||||||
|
*
|
||||||
|
* @param string $haystack The string to search in.
|
||||||
|
* @param string $needle The substring to search for in the `$haystack`.
|
||||||
|
*
|
||||||
|
* @return bool True if `$needle` is in `$haystack`, otherwise false.
|
||||||
|
*/
|
||||||
|
function str_contains( $haystack, $needle ) {
|
||||||
|
if ( '' === $needle ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false !== strpos( $haystack, $needle );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -2,7 +2,9 @@
|
||||||
namespace Activitypub;
|
namespace Activitypub;
|
||||||
|
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
|
use WP_Comment_Query;
|
||||||
use Activitypub\Http;
|
use Activitypub\Http;
|
||||||
|
use Activitypub\Webfinger;
|
||||||
use Activitypub\Activity\Activity;
|
use Activitypub\Activity\Activity;
|
||||||
use Activitypub\Collection\Followers;
|
use Activitypub\Collection\Followers;
|
||||||
use Activitypub\Collection\Users;
|
use Activitypub\Collection\Users;
|
||||||
|
@ -167,6 +169,27 @@ function url_to_authorid( $url ) {
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Verify if url is a wp_ap_comment,
|
||||||
|
* Or if it is a previously received remote comment
|
||||||
|
*
|
||||||
|
* @return int comment_id
|
||||||
|
*/
|
||||||
|
function is_comment() {
|
||||||
|
$comment_id = get_query_var( 'c', null );
|
||||||
|
|
||||||
|
if ( ! is_null( $comment_id ) ) {
|
||||||
|
$comment = \get_comment( $comment_id );
|
||||||
|
|
||||||
|
// Only return local origin comments
|
||||||
|
if ( $comment && $comment->user_id ) {
|
||||||
|
return $comment_id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check for Tombstone Objects
|
* Check for Tombstone Objects
|
||||||
*
|
*
|
||||||
|
@ -486,6 +509,81 @@ function is_blog_public() {
|
||||||
return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
|
return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanitize a URL
|
||||||
|
*
|
||||||
|
* @param string $value The URL to sanitize
|
||||||
|
*
|
||||||
|
* @return string|null The sanitized URL or null if invalid
|
||||||
|
*/
|
||||||
|
function sanitize_url( $value ) {
|
||||||
|
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return esc_url_raw( $value );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract recipient URLs from Activity object
|
||||||
|
*
|
||||||
|
* @param array $data
|
||||||
|
*
|
||||||
|
* @return array The list of user URLs
|
||||||
|
*/
|
||||||
|
function extract_recipients_from_activity( $data ) {
|
||||||
|
$recipient_items = array();
|
||||||
|
|
||||||
|
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
|
||||||
|
if ( array_key_exists( $i, $data ) ) {
|
||||||
|
if ( is_array( $data[ $i ] ) ) {
|
||||||
|
$recipient = $data[ $i ];
|
||||||
|
} else {
|
||||||
|
$recipient = array( $data[ $i ] );
|
||||||
|
}
|
||||||
|
$recipient_items = array_merge( $recipient_items, $recipient );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
|
||||||
|
if ( is_array( $data['object'][ $i ] ) ) {
|
||||||
|
$recipient = $data['object'][ $i ];
|
||||||
|
} else {
|
||||||
|
$recipient = array( $data['object'][ $i ] );
|
||||||
|
}
|
||||||
|
$recipient_items = array_merge( $recipient_items, $recipient );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$recipients = array();
|
||||||
|
|
||||||
|
// flatten array
|
||||||
|
foreach ( $recipient_items as $recipient ) {
|
||||||
|
if ( is_array( $recipient ) ) {
|
||||||
|
// check if recipient is an object
|
||||||
|
if ( array_key_exists( 'id', $recipient ) ) {
|
||||||
|
$recipients[] = $recipient['id'];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$recipients[] = $recipient;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return array_unique( $recipients );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if passed Activity is Public
|
||||||
|
*
|
||||||
|
* @param array $data The Activity object as array
|
||||||
|
*
|
||||||
|
* @return boolean True if public, false if not
|
||||||
|
*/
|
||||||
|
function is_activity_public( $data ) {
|
||||||
|
$recipients = extract_recipients_from_activity( $data );
|
||||||
|
|
||||||
|
return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get active users based on a given duration
|
* Get active users based on a given duration
|
||||||
*
|
*
|
||||||
|
@ -503,7 +601,7 @@ function get_active_users( $duration = 1 ) {
|
||||||
global $wpdb;
|
global $wpdb;
|
||||||
$query = "SELECT COUNT( DISTINCT post_author ) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date <= DATE_SUB( NOW(), INTERVAL %d MONTH )";
|
$query = "SELECT COUNT( DISTINCT post_author ) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date <= DATE_SUB( NOW(), INTERVAL %d MONTH )";
|
||||||
$query = $wpdb->prepare( $query, $duration );
|
$query = $wpdb->prepare( $query, $duration );
|
||||||
$count = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
$count = $wpdb->get_var( $query ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||||
|
|
||||||
set_transient( $transient_key, $count, DAY_IN_SECONDS );
|
set_transient( $transient_key, $count, DAY_IN_SECONDS );
|
||||||
}
|
}
|
||||||
|
@ -557,3 +655,122 @@ function get_total_users() {
|
||||||
|
|
||||||
return $users + 1;
|
return $users + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Examine a comment ID and look up an existing comment it represents.
|
||||||
|
*
|
||||||
|
* @param string $id ActivityPub object ID (usually a URL) to check.
|
||||||
|
*
|
||||||
|
* @return int|boolean Comment ID, or false on failure.
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
function url_to_commentid( $url ) {
|
||||||
|
if ( ! $url || ! filter_var( $url, FILTER_VALIDATE_URL ) ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for local comment
|
||||||
|
if ( \wp_parse_url( \site_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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the URI of an ActivityPub object
|
||||||
|
*
|
||||||
|
* @param array $object The ActivityPub object
|
||||||
|
*
|
||||||
|
* @return string The URI of the ActivityPub object
|
||||||
|
*/
|
||||||
|
function object_to_uri( $object ) {
|
||||||
|
// check if it is already simple
|
||||||
|
if ( ! $object || is_string( $object ) ) {
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it is a list, then take first item
|
||||||
|
// this plugin does not support collections
|
||||||
|
if ( array_is_list( $object ) ) {
|
||||||
|
$object = $object[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if it is simplified now
|
||||||
|
if ( is_string( $object ) ) {
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
// return part of Object that makes most sense
|
||||||
|
switch ( $object['type'] ) {
|
||||||
|
case 'Link':
|
||||||
|
$object = $object['href'];
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$object = $object['id'];
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
66
includes/handler/class-create.php
Normal file
66
includes/handler/class-create.php
Normal file
|
@ -0,0 +1,66 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Handler;
|
||||||
|
|
||||||
|
use WP_Error;
|
||||||
|
use Activitypub\Collection\Interactions;
|
||||||
|
|
||||||
|
use function Activitypub\is_activity_public;
|
||||||
|
use function Activitypub\object_id_to_comment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Create requests
|
||||||
|
*/
|
||||||
|
class Create {
|
||||||
|
/**
|
||||||
|
* Initialize the class, registering WordPress hooks
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
\add_action(
|
||||||
|
'activitypub_inbox_create',
|
||||||
|
array( self::class, 'handle_create' ),
|
||||||
|
10,
|
||||||
|
3
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles "Create" requests
|
||||||
|
*
|
||||||
|
* @param array $array The activity-object
|
||||||
|
* @param int $user_id The id of the local blog-user
|
||||||
|
* @param Activitypub\Activity $object The activity object
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function handle_create( $array, $user_id, $object = null ) {
|
||||||
|
if (
|
||||||
|
! isset( $array['object'] ) ||
|
||||||
|
! isset( $array['object']['id'] )
|
||||||
|
) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if Activity is public or not
|
||||||
|
if ( ! is_activity_public( $array ) ) {
|
||||||
|
// @todo maybe send email
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$check_dupe = object_id_to_comment( $array['object']['id'] );
|
||||||
|
|
||||||
|
// if comment exists, call update action
|
||||||
|
if ( $check_dupe ) {
|
||||||
|
\do_action( 'activitypub_inbox_update', $array, $user_id, $object );
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$state = Interactions::add_comment( $array );
|
||||||
|
$reaction = null;
|
||||||
|
|
||||||
|
if ( $state && ! \is_wp_error( $reaction ) ) {
|
||||||
|
$reaction = \get_comment( $state );
|
||||||
|
}
|
||||||
|
|
||||||
|
\do_action( 'activitypub_handled_create', $array, $user_id, $state, $reaction );
|
||||||
|
}
|
||||||
|
}
|
178
includes/handler/class-delete.php
Normal file
178
includes/handler/class-delete.php
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Handler;
|
||||||
|
|
||||||
|
use WP_Error;
|
||||||
|
use WP_REST_Request;
|
||||||
|
use Activitypub\Http;
|
||||||
|
use Activitypub\Collection\Followers;
|
||||||
|
use Activitypub\Collection\Interactions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles Delete requests.
|
||||||
|
*/
|
||||||
|
class Delete {
|
||||||
|
/**
|
||||||
|
* Initialize the class, registering WordPress hooks
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
\add_action(
|
||||||
|
'activitypub_inbox_delete',
|
||||||
|
array( self::class, 'handle_delete' )
|
||||||
|
);
|
||||||
|
|
||||||
|
// defer signature verification for `Delete` requests.
|
||||||
|
\add_filter(
|
||||||
|
'activitypub_defer_signature_verification',
|
||||||
|
array( self::class, 'defer_signature_verification' ),
|
||||||
|
10,
|
||||||
|
2
|
||||||
|
);
|
||||||
|
|
||||||
|
// side effect
|
||||||
|
\add_action(
|
||||||
|
'activitypub_delete_actor_interactions',
|
||||||
|
array( self::class, 'delete_interactions' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles "Delete" requests.
|
||||||
|
*
|
||||||
|
* @param array $activity The delete activity.
|
||||||
|
* @param int $user_id The ID of the user performing the delete activity.
|
||||||
|
*/
|
||||||
|
public static function handle_delete( $activity ) {
|
||||||
|
$object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : '';
|
||||||
|
|
||||||
|
switch ( $object_type ) {
|
||||||
|
// Actor Types
|
||||||
|
// @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
||||||
|
case 'Person':
|
||||||
|
case 'Group':
|
||||||
|
case 'Organization':
|
||||||
|
case 'Service':
|
||||||
|
case 'Application':
|
||||||
|
self::maybe_delete_follower( $activity );
|
||||||
|
break;
|
||||||
|
// Object and Link Types
|
||||||
|
// @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||||
|
case 'Note':
|
||||||
|
case 'Article':
|
||||||
|
case 'Image':
|
||||||
|
case 'Audio':
|
||||||
|
case 'Video':
|
||||||
|
case 'Event':
|
||||||
|
case 'Document':
|
||||||
|
self::maybe_delete_interaction( $activity );
|
||||||
|
break;
|
||||||
|
// Tombstone Type
|
||||||
|
// @see: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
|
||||||
|
case 'Tombstone':
|
||||||
|
self::maybe_delete_interaction( $activity );
|
||||||
|
break;
|
||||||
|
// Minimal Activity
|
||||||
|
// @see https://www.w3.org/TR/activitystreams-core/#example-1
|
||||||
|
default:
|
||||||
|
// ignore non Minimal Activities.
|
||||||
|
if ( ! is_string( $activity['object'] ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if Object is an Actor.
|
||||||
|
if ( $activity['actor'] === $activity['object'] ) {
|
||||||
|
self::maybe_delete_follower( $activity );
|
||||||
|
self::maybe_delete_interactions( $activity );
|
||||||
|
} else { // assume a interaction otherwise.
|
||||||
|
self::maybe_delete_interaction( $activity );
|
||||||
|
}
|
||||||
|
// maybe handle Delete Activity for other Object Types.
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Follower if Actor-URL is a Tombstone.
|
||||||
|
*
|
||||||
|
* @param array $activity The delete activity.
|
||||||
|
*/
|
||||||
|
public static function maybe_delete_follower( $activity ) {
|
||||||
|
$follower = Followers::get_follower_by_actor( $activity['actor'] );
|
||||||
|
|
||||||
|
// verify if Actor is deleted.
|
||||||
|
if ( $follower && Http::is_tombstone( $activity['actor'] ) ) {
|
||||||
|
$follower->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete Reactions if Actor-URL is a Tombstone.
|
||||||
|
*
|
||||||
|
* @param array $activity The delete activity.
|
||||||
|
*/
|
||||||
|
public static function maybe_delete_interactions( $activity ) {
|
||||||
|
// verify if Actor is deleted.
|
||||||
|
if ( Http::is_tombstone( $activity['actor'] ) ) {
|
||||||
|
\wp_schedule_single_event(
|
||||||
|
\time(),
|
||||||
|
'activitypub_delete_actor_interactions',
|
||||||
|
array( $activity['actor'] )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete comments from an Actor.
|
||||||
|
*
|
||||||
|
* @param array $comments The comments to delete.
|
||||||
|
*/
|
||||||
|
public static function delete_interactions( $actor ) {
|
||||||
|
$comments = Interactions::get_interactions_by_actor( $actor );
|
||||||
|
|
||||||
|
if ( is_array( $comments ) ) {
|
||||||
|
foreach ( $comments as $comment ) {
|
||||||
|
wp_delete_comment( $comment->comment_ID );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete a Reaction if URL is a Tombstone.
|
||||||
|
*
|
||||||
|
* @param array $activity The delete activity.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function maybe_delete_interaction( $activity ) {
|
||||||
|
if ( is_array( $activity['object'] ) ) {
|
||||||
|
$id = $activity['object']['id'];
|
||||||
|
} else {
|
||||||
|
$id = $activity['object'];
|
||||||
|
}
|
||||||
|
|
||||||
|
$comments = Interactions::get_interaction_by_id( $id );
|
||||||
|
|
||||||
|
if ( $comments && Http::is_tombstone( $id ) ) {
|
||||||
|
foreach ( $comments as $comment ) {
|
||||||
|
wp_delete_comment( $comment->comment_ID, true );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defer signature verification for `Delete` requests.
|
||||||
|
*
|
||||||
|
* @param bool $defer Whether to defer signature verification.
|
||||||
|
* @param WP_REST_Request $request The request object.
|
||||||
|
*
|
||||||
|
* @return bool Whether to defer signature verification.
|
||||||
|
*/
|
||||||
|
public static function defer_signature_verification( $defer, $request ) {
|
||||||
|
$json = $request->get_json_params();
|
||||||
|
|
||||||
|
if ( isset( $json['type'] ) && 'Delete' === $json['type'] ) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
92
includes/handler/class-follow.php
Normal file
92
includes/handler/class-follow.php
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Handler;
|
||||||
|
|
||||||
|
use Activitypub\Http;
|
||||||
|
use Activitypub\Activity\Activity;
|
||||||
|
use Activitypub\Collection\Users;
|
||||||
|
use Activitypub\Collection\Followers;
|
||||||
|
use Activitypub\Model\Follow_Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Follow requests
|
||||||
|
*/
|
||||||
|
class Follow {
|
||||||
|
/**
|
||||||
|
* Initialize the class, registering WordPress hooks
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
\add_action(
|
||||||
|
'activitypub_inbox_follow',
|
||||||
|
array( self::class, 'handle_follow' )
|
||||||
|
);
|
||||||
|
|
||||||
|
\add_action(
|
||||||
|
'activitypub_send_follow_response',
|
||||||
|
array( self::class, 'send_follow_response' ),
|
||||||
|
10,
|
||||||
|
4
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle "Follow" requests
|
||||||
|
*
|
||||||
|
* @param array $activity The activity object
|
||||||
|
* @param int $user_id The user ID
|
||||||
|
*/
|
||||||
|
public static function handle_follow( $activity ) {
|
||||||
|
$user = Users::get_by_resource( $activity['object'] );
|
||||||
|
|
||||||
|
if ( ! $user || is_wp_error( $user ) ) {
|
||||||
|
// If we can not find a user,
|
||||||
|
// we can not initiate a follow process
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$user_id = $user->get__id();
|
||||||
|
|
||||||
|
// save follower
|
||||||
|
$follower = Followers::add_follower(
|
||||||
|
$user_id,
|
||||||
|
$activity['actor']
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( \is_wp_error( $follower ) ) {
|
||||||
|
// it is not even possible to send a "Reject" or "Accept" because
|
||||||
|
// we can not get the Remote-Inbox
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// save follow request by this follower
|
||||||
|
$follow_request = Follow_Request::save( $follower, $user_id, $activity['id'] );
|
||||||
|
|
||||||
|
if ( ! $user->get_manually_approves_followers() ) {
|
||||||
|
$follow_request->accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send Follow response
|
||||||
|
*
|
||||||
|
* @param Activitypub\Model\User $user The Target Users ActivityPub object
|
||||||
|
* @param Activitypub\Model\Follower $follower The Followers ActivityPub object
|
||||||
|
* @param array|object $object The ActivityPub follow object
|
||||||
|
* @param string $type The reponse object type: 'Accept' or 'Reject'
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function send_follow_response( $user, $follower, $object, $type ) {
|
||||||
|
// send activity
|
||||||
|
$activity = new Activity();
|
||||||
|
$activity->set_type( $type );
|
||||||
|
$activity->set_object( $object );
|
||||||
|
$activity->set_actor( $user->get_id() );
|
||||||
|
$activity->set_to( $follower->get_id() );
|
||||||
|
$activity->set_id( $user->get_id() . '#accept-' . \preg_replace( '~^https?://~', '', $follower->get_id() ) . '-' . \time() );
|
||||||
|
|
||||||
|
$activity = $activity->to_json();
|
||||||
|
|
||||||
|
Http::post( $follower->get_shared_inbox(), $activity, $user->get__id() );
|
||||||
|
}
|
||||||
|
}
|
46
includes/handler/class-undo.php
Normal file
46
includes/handler/class-undo.php
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Handler;
|
||||||
|
|
||||||
|
use Activitypub\Collection\Users;
|
||||||
|
use Activitypub\Model\Follow_Request;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Undo requests
|
||||||
|
*/
|
||||||
|
class Undo {
|
||||||
|
/**
|
||||||
|
* Initialize the class, registering WordPress hooks
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
\add_action(
|
||||||
|
'activitypub_inbox_undo',
|
||||||
|
array( self::class, 'handle_undo' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle "Unfollow" requests
|
||||||
|
*
|
||||||
|
* @param array $activity The JSON "Undo" Activity
|
||||||
|
* @param int $user_id The ID of the ID of the WordPress User
|
||||||
|
*/
|
||||||
|
public static function handle_undo( $activity ) {
|
||||||
|
if (
|
||||||
|
isset( $activity['object']['type'] ) &&
|
||||||
|
'Follow' === $activity['object']['type'] &&
|
||||||
|
isset( $activity['object']['object'] ) &&
|
||||||
|
filter_var( $activity['object']['object'], FILTER_VALIDATE_URL )
|
||||||
|
) {
|
||||||
|
$user = Users::get_by_resource( $activity['object']['object'] );
|
||||||
|
|
||||||
|
if ( ! $user || is_wp_error( $user ) ) {
|
||||||
|
// If we can not find a user,
|
||||||
|
// we can not initiate a follow process
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$follow_request = Follow_Request::get_from_array( $activity['object'] );
|
||||||
|
$follow_request->delete();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
92
includes/handler/class-update.php
Normal file
92
includes/handler/class-update.php
Normal file
|
@ -0,0 +1,92 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Handler;
|
||||||
|
|
||||||
|
use WP_Error;
|
||||||
|
use Activitypub\Collection\Interactions;
|
||||||
|
|
||||||
|
use function Activitypub\get_remote_metadata_by_actor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle Update requests.
|
||||||
|
*/
|
||||||
|
class Update {
|
||||||
|
/**
|
||||||
|
* Initialize the class, registering WordPress hooks
|
||||||
|
*/
|
||||||
|
public static function init() {
|
||||||
|
\add_action(
|
||||||
|
'activitypub_inbox_update',
|
||||||
|
array( self::class, 'handle_update' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle "Update" requests
|
||||||
|
*
|
||||||
|
* @param array $array The activity-object
|
||||||
|
* @param int $user_id The id of the local blog-user
|
||||||
|
*/
|
||||||
|
public static function handle_update( $array ) {
|
||||||
|
$object_type = isset( $array['object']['type'] ) ? $array['object']['type'] : '';
|
||||||
|
|
||||||
|
switch ( $object_type ) {
|
||||||
|
// Actor Types
|
||||||
|
// @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
||||||
|
case 'Person':
|
||||||
|
case 'Group':
|
||||||
|
case 'Organization':
|
||||||
|
case 'Service':
|
||||||
|
case 'Application':
|
||||||
|
self::update_actor( $array );
|
||||||
|
break;
|
||||||
|
// Object and Link Types
|
||||||
|
// @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||||
|
case 'Note':
|
||||||
|
case 'Article':
|
||||||
|
case 'Image':
|
||||||
|
case 'Audio':
|
||||||
|
case 'Video':
|
||||||
|
case 'Event':
|
||||||
|
case 'Document':
|
||||||
|
self::update_interaction( $array );
|
||||||
|
break;
|
||||||
|
// Minimal Activity
|
||||||
|
// @see https://www.w3.org/TR/activitystreams-core/#example-1
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an Interaction
|
||||||
|
*
|
||||||
|
* @param array $activity The activity-object
|
||||||
|
* @param int $user_id The id of the local blog-user
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function update_interaction( $activity ) {
|
||||||
|
$state = Interactions::update_comment( $activity );
|
||||||
|
$reaction = null;
|
||||||
|
|
||||||
|
if ( $state && ! \is_wp_error( $reaction ) ) {
|
||||||
|
$reaction = \get_comment( $state );
|
||||||
|
}
|
||||||
|
|
||||||
|
\do_action( 'activitypub_handled_update', $activity, null, $state, $reaction );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update an Actor
|
||||||
|
*
|
||||||
|
* @param array $activity The activity-object
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public static function update_actor( $activity ) {
|
||||||
|
// update cache
|
||||||
|
get_remote_metadata_by_actor( $activity['actor'], false );
|
||||||
|
|
||||||
|
// @todo maybe also update all interactions
|
||||||
|
}
|
||||||
|
}
|
|
@ -12,7 +12,7 @@
|
||||||
'<dt><code>[ap_content apply_filters="yes"]</code></dt>' .
|
'<dt><code>[ap_content apply_filters="yes"]</code></dt>' .
|
||||||
'<dd>' . \wp_kses( __( 'The post\'s content. With <code>apply_filters</code> you can decide if filters (<code>apply_filters( \'the_content\', $content )</code>) should be applied or not (default is <code>yes</code>). The values can be <code>yes</code> or <code>no</code>. <code>apply_filters</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
'<dd>' . \wp_kses( __( 'The post\'s content. With <code>apply_filters</code> you can decide if filters (<code>apply_filters( \'the_content\', $content )</code>) should be applied or not (default is <code>yes</code>). The values can be <code>yes</code> or <code>no</code>. <code>apply_filters</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||||
'<dt><code>[ap_excerpt length="400"]</code></dt>' .
|
'<dt><code>[ap_excerpt length="400"]</code></dt>' .
|
||||||
'<dd>' . \wp_kses( __( 'The post\'s excerpt (default 400 chars). <code>length</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
'<dd>' . \wp_kses( __( 'The post\'s excerpt (uses <code>the_excerpt</code> if that is set). If no excerpt is provided, will truncate at <code>length</code> (optional, default = 400).', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||||
'<dt><code>[ap_permalink type="url"]</code></dt>' .
|
'<dt><code>[ap_permalink type="url"]</code></dt>' .
|
||||||
'<dd>' . \wp_kses( __( 'The post\'s permalink. <code>type</code> can be either: <code>url</code> or <code>html</code> (an <a /> tag). <code>type</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
'<dd>' . \wp_kses( __( 'The post\'s permalink. <code>type</code> can be either: <code>url</code> or <code>html</code> (an <a /> tag). <code>type</code> attribute is optional.', 'activitypub' ), array( 'code' => array() ) ) . '</dd>' .
|
||||||
'<dt><code>[ap_shortlink type="url"]</code></dt>' .
|
'<dt><code>[ap_shortlink type="url"]</code></dt>' .
|
||||||
|
|
|
@ -29,6 +29,13 @@ class Application_User extends Blog_User {
|
||||||
*/
|
*/
|
||||||
protected $discoverable = false;
|
protected $discoverable = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The instance actor by default must manually approve all followers.
|
||||||
|
*
|
||||||
|
* @var boolean
|
||||||
|
*/
|
||||||
|
protected $manually_approves_followers = true;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the User-Url.
|
* Get the User-Url.
|
||||||
*
|
*
|
||||||
|
@ -38,12 +45,21 @@ class Application_User extends Blog_User {
|
||||||
return get_rest_url_by_path( 'application' );
|
return get_rest_url_by_path( 'application' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the User-URL with @-Prefix for the username.
|
||||||
|
*
|
||||||
|
* @return string The User-URL with @-Prefix for the username.
|
||||||
|
*/
|
||||||
|
public function get_alternate_url() {
|
||||||
|
return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() );
|
||||||
|
}
|
||||||
|
|
||||||
public function get_name() {
|
public function get_name() {
|
||||||
return 'application';
|
return 'application';
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_preferred_username() {
|
public function get_preferred_username() {
|
||||||
return $this::get_name();
|
return $this->get_name();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_followers() {
|
public function get_followers() {
|
||||||
|
@ -69,4 +85,8 @@ class Application_User extends Blog_User {
|
||||||
public function get_indexable() {
|
public function get_indexable() {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function get_type() {
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -100,12 +100,12 @@ class Blog_User extends User {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the User-URL with @-Prefix for the username.
|
* Get blog's homepage URL.
|
||||||
*
|
*
|
||||||
* @return string The User-URL with @-Prefix for the username.
|
* @return string The User-Url.
|
||||||
*/
|
*/
|
||||||
public function get_at_url() {
|
public function get_alternate_url() {
|
||||||
return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() );
|
return \esc_url( \trailingslashit( get_home_url() ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
215
includes/model/class-follow-request.php
Normal file
215
includes/model/class-follow-request.php
Normal file
|
@ -0,0 +1,215 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Model;
|
||||||
|
|
||||||
|
use WP_Error;
|
||||||
|
|
||||||
|
use Activitypub\Activity\Activity;
|
||||||
|
use Activitypub\Activity\Base_Object;
|
||||||
|
use Activitypub\Collection\Users;
|
||||||
|
use Activitypub\Model\Follower;
|
||||||
|
use Activitypub\Http;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* ActivityPub Follow Class
|
||||||
|
*
|
||||||
|
* This Object represents a Follow object.
|
||||||
|
* There is no direct reference to a WordPress User here.
|
||||||
|
*
|
||||||
|
* @author André Menrath
|
||||||
|
*
|
||||||
|
* @see https://www.w3.org/TR/activitypub/#follow-activity-inbox
|
||||||
|
*/
|
||||||
|
class Follow_Request extends Base_Object {
|
||||||
|
const FOLLOW_REQUEST_POST_TYPE = 'ap_follow_request';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stores theinternal WordPress post id of the post of type ap_follow_request
|
||||||
|
*
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $_id;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The id/URI of the follower
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $actor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The internal WordPress post id of the follower
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $_actor;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $id
|
||||||
|
* @return Follow_Request $follow_request
|
||||||
|
*/
|
||||||
|
public static function get_from_array( $array ) {
|
||||||
|
$follow_request = self::init_from_array( $array );
|
||||||
|
if ( ! self::is_valid( $follow_request ) ) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ( ! $follow_request->get__id() ) {
|
||||||
|
$follow_request->set__id( self::get_follow_request_id_by_uri( $follow_request->get_id() ) );
|
||||||
|
}
|
||||||
|
return $follow_request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieve the WordPress post id of the follow request by its id (URI)
|
||||||
|
*/
|
||||||
|
public static function get_follow_request_id_by_uri( $uri ) {
|
||||||
|
global $wpdb;
|
||||||
|
return $wpdb->get_var( $wpdb->prepare( "SELECT ID FROM $wpdb->posts WHERE guid=%s", esc_sql( $uri ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the follow request is valid which means it fits to the already stored data.
|
||||||
|
*
|
||||||
|
* @param Follow_Request $follow_request The follow request to be checked.
|
||||||
|
* @return bool Whether the follow request is valid.
|
||||||
|
*/
|
||||||
|
public static function is_valid( $follow_request ) {
|
||||||
|
if ( self::class != get_class( $follow_request ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ( 'Follow' != $follow_request->get_type() ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = self::get_follow_request_id_by_uri( $follow_request->get_id() );
|
||||||
|
if ( ! $id || is_wp_error( $id ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if ( self::FOLLOW_REQUEST_POST_TYPE != get_post_type( $id) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$post = get_post( $id );
|
||||||
|
$follower = get_post_parent( $id );
|
||||||
|
if ( $follower->guid != $follow_request->get_actor() ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $id
|
||||||
|
* @return Follow_Request $follow_request
|
||||||
|
*/
|
||||||
|
public static function from_wp_id( $id ) {
|
||||||
|
if ( self::FOLLOW_REQUEST_POST_TYPE != get_post_type( $id ) ){
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
$post = get_post( $id );
|
||||||
|
|
||||||
|
$follow_request = new static;
|
||||||
|
$follow_request->set_id( $post->guid );
|
||||||
|
$follow_request->set__id( $post->ID );
|
||||||
|
$follow_request->set_type( 'Follow' );
|
||||||
|
|
||||||
|
return $follow_request;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the current Follower-Object.
|
||||||
|
*
|
||||||
|
* @param Follower $follower
|
||||||
|
*
|
||||||
|
* @return Follow_Request|WP_Error The Follow_Request or an WP_Error.
|
||||||
|
*/
|
||||||
|
public static function save( $follower, $user_id, $activity_id ) {
|
||||||
|
$follower_id = $follower->get__id();
|
||||||
|
$meta_input = array(
|
||||||
|
'activitypub_user_id' => $user_id,
|
||||||
|
);
|
||||||
|
|
||||||
|
$args = array(
|
||||||
|
'guid' => $activity_id,
|
||||||
|
'post_author' => 0,
|
||||||
|
'post_type' => self::FOLLOW_REQUEST_POST_TYPE,
|
||||||
|
'post_status' => 'pending',
|
||||||
|
'post_parent' => $follower_id,
|
||||||
|
'meta_input' => $meta_input,
|
||||||
|
'mime_type' => 'text/plain'
|
||||||
|
);
|
||||||
|
|
||||||
|
$post_id = wp_insert_post( $args );
|
||||||
|
|
||||||
|
|
||||||
|
return self::from_wp_id( $post_id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the user is allowed to handle this follow request.
|
||||||
|
*
|
||||||
|
* Usually needed for the ajax functions.
|
||||||
|
* @return bool Whether the user is allowed.
|
||||||
|
*/
|
||||||
|
public function can_handle_follow_request() {
|
||||||
|
$target_actor = get_post_meta( $this->get__id(), 'activitypub_user_id');
|
||||||
|
if ( get_current_user_id() == $target_actor || current_user_can( 'manage_options' )) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reject the follow request
|
||||||
|
*/
|
||||||
|
public function reject() {
|
||||||
|
wp_update_post(
|
||||||
|
array(
|
||||||
|
'ID' => $this->get__id(),
|
||||||
|
'post_status' => 'rejected',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->send_response( 'Reject' );
|
||||||
|
$this->delete();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Approve the follow request
|
||||||
|
*/
|
||||||
|
public function approve() {
|
||||||
|
wp_update_post(
|
||||||
|
array(
|
||||||
|
'ID' => $this->get__id(),
|
||||||
|
'post_status' => 'approved',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->send_response( 'Accept' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the follow request
|
||||||
|
*
|
||||||
|
* This should only be called after it has been rejected.
|
||||||
|
*/
|
||||||
|
public function delete() {
|
||||||
|
wp_delete_post( $this->get__id() );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prepere the sending of the follow request response and hand it over to the sending handler.
|
||||||
|
*/
|
||||||
|
public function send_response( $type ) {
|
||||||
|
$user_id = get_post_meta( $this->get__id(), 'activitypub_user_id')[0];
|
||||||
|
$user = Users::get_by_id( $user_id );
|
||||||
|
|
||||||
|
$follower_id = wp_get_post_parent_id( $this->get__id() );
|
||||||
|
$follower = Follower::init_from_cpt( get_post( $follower_id ) );
|
||||||
|
|
||||||
|
$actor = $follower->get_id();
|
||||||
|
|
||||||
|
$object = array(
|
||||||
|
'id' => $this->get_id(),
|
||||||
|
'type' => $this->get_type(),
|
||||||
|
'actor' => $actor,
|
||||||
|
'object' => $user,
|
||||||
|
);
|
||||||
|
|
||||||
|
do_action( 'activitypub_send_follow_response', $user, $follower, $object, $type);
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,7 @@ use Activitypub\Collection\Followers;
|
||||||
*/
|
*/
|
||||||
class Follower extends Actor {
|
class Follower extends Actor {
|
||||||
/**
|
/**
|
||||||
* The complete Remote-Profile of the Follower
|
* The complete Remote-Profile of the Follower.
|
||||||
*
|
*
|
||||||
* @var array
|
* @var array
|
||||||
*/
|
*/
|
||||||
|
@ -34,6 +34,13 @@ class Follower extends Actor {
|
||||||
return get_post_meta( $this->_id, 'activitypub_errors' );
|
return get_post_meta( $this->_id, 'activitypub_errors' );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Getter function for the internal WordPress id.
|
||||||
|
*/
|
||||||
|
public function get__id() {
|
||||||
|
return $this->_id;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the Summary.
|
* Get the Summary.
|
||||||
*
|
*
|
||||||
|
@ -304,9 +311,9 @@ class Follower extends Actor {
|
||||||
/**
|
/**
|
||||||
* Convert a Custom-Post-Type input to an Activitypub\Model\Follower.
|
* Convert a Custom-Post-Type input to an Activitypub\Model\Follower.
|
||||||
*
|
*
|
||||||
* @return string The JSON string.
|
* @param WP_Post The JSON string.
|
||||||
*
|
*
|
||||||
* @return array Activitypub\Model\Follower
|
* @return \Activitypub\Model\Follower $object
|
||||||
*/
|
*/
|
||||||
public static function init_from_cpt( $post ) {
|
public static function init_from_cpt( $post ) {
|
||||||
$actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true );
|
$actor_json = get_post_meta( $post->ID, 'activitypub_actor_json', true );
|
||||||
|
|
|
@ -1,132 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Activitypub\Model;
|
|
||||||
|
|
||||||
use Activitypub\Transformer\Post as Transformer_Post;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ActivityPub Post Class
|
|
||||||
*
|
|
||||||
* @author Matthias Pfefferle
|
|
||||||
*/
|
|
||||||
class Post {
|
|
||||||
/**
|
|
||||||
* The \Activitypub\Activity\Base_Object object.
|
|
||||||
*
|
|
||||||
* @var \Activitypub\Activity\Base_Object
|
|
||||||
*/
|
|
||||||
protected $object;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The WordPress Post Object.
|
|
||||||
*
|
|
||||||
* @var WP_Post
|
|
||||||
*/
|
|
||||||
private $post;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Constructor
|
|
||||||
*
|
|
||||||
* @param WP_Post $post
|
|
||||||
* @param int $post_author
|
|
||||||
*/
|
|
||||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
|
||||||
public function __construct( $post, $post_author = null ) {
|
|
||||||
_deprecated_function( __CLASS__, '1.0.0', '\Activitypub\Transformer\Post' );
|
|
||||||
|
|
||||||
$this->post = $post;
|
|
||||||
$transformer = new Transformer_Post();
|
|
||||||
$this->object = $transformer->set_wp_post( $post )->to_object();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the User ID.
|
|
||||||
*
|
|
||||||
* @return int the User ID.
|
|
||||||
*/
|
|
||||||
public function get_user_id() {
|
|
||||||
return apply_filters( 'activitypub_post_user_id', $this->post->post_author, $this->post );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts this Object into an Array.
|
|
||||||
*
|
|
||||||
* @return array the array representation of a Post.
|
|
||||||
*/
|
|
||||||
public function to_array() {
|
|
||||||
return \apply_filters( 'activitypub_post', $this->object->to_array(), $this->post );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the Actor of this Object.
|
|
||||||
*
|
|
||||||
* @return string The URL of the Actor.
|
|
||||||
*/
|
|
||||||
public function get_actor() {
|
|
||||||
$user = User_Factory::get_by_id( $this->get_user_id() );
|
|
||||||
|
|
||||||
return $user->get_url();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts this Object into a JSON String
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function to_json() {
|
|
||||||
return \wp_json_encode( $this->to_array(), \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the URL of an Activity Object
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function get_url() {
|
|
||||||
return $this->object->get_url();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the ID of an Activity Object
|
|
||||||
*
|
|
||||||
* @return string
|
|
||||||
*/
|
|
||||||
public function get_id() {
|
|
||||||
return $this->object->get_id();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of Image Attachments
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function get_attachments() {
|
|
||||||
return $this->object->get_attachment();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of Tags, used in the Post
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function get_tags() {
|
|
||||||
return $this->object->get_tag();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the as2 object-type for a given post
|
|
||||||
*
|
|
||||||
* @return string the object-type
|
|
||||||
*/
|
|
||||||
public function get_object_type() {
|
|
||||||
return $this->object->get_type();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the content for the ActivityPub Item.
|
|
||||||
*
|
|
||||||
* @return string the content
|
|
||||||
*/
|
|
||||||
public function get_content() {
|
|
||||||
return $this->object->get_content();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -135,8 +135,8 @@ class User extends Actor {
|
||||||
*
|
*
|
||||||
* @return string The User-URL with @-Prefix for the username.
|
* @return string The User-URL with @-Prefix for the username.
|
||||||
*/
|
*/
|
||||||
public function get_at_url() {
|
public function get_alternate_url() {
|
||||||
return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_username() );
|
return \esc_url( \trailingslashit( get_home_url() ) . '@' . $this->get_preferred_username() );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function get_preferred_username() {
|
public function get_preferred_username() {
|
||||||
|
@ -226,6 +226,18 @@ class User extends Actor {
|
||||||
return get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $this->get__id() ) );
|
return get_rest_url_by_path( sprintf( 'users/%d/collections/featured', $this->get__id() ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function get_endpoints() {
|
||||||
|
$endpoints = null;
|
||||||
|
|
||||||
|
if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) {
|
||||||
|
$endpoints = array(
|
||||||
|
'sharedInbox' => get_rest_url_by_path( 'inbox' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $endpoints;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Extend the User-Output with Attachments.
|
* Extend the User-Output with Attachments.
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,34 +0,0 @@
|
||||||
<?php
|
|
||||||
namespace Activitypub\Peer;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ActivityPub Followers DB-Class
|
|
||||||
*
|
|
||||||
* @author Matthias Pfefferle
|
|
||||||
*/
|
|
||||||
class Followers {
|
|
||||||
|
|
||||||
public static function get_followers( $author_id ) {
|
|
||||||
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::get_followers' );
|
|
||||||
|
|
||||||
return \Activitypub\Collection\Followers::get_followers( $author_id );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function count_followers( $author_id ) {
|
|
||||||
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::count_followers' );
|
|
||||||
|
|
||||||
return \Activitypub\Collection\Followers::count_followers( $author_id );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function add_follower( $actor, $author_id ) {
|
|
||||||
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::add_follower' );
|
|
||||||
|
|
||||||
return \Activitypub\Collection\Followers::add_follower( $author_id, $actor );
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function remove_follower( $actor, $author_id ) {
|
|
||||||
_deprecated_function( __METHOD__, '1.0.0', '\Activitypub\Collection\Followers::remove_follower' );
|
|
||||||
|
|
||||||
return \Activitypub\Collection\Followers::remove_follower( $author_id, $actor );
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -4,7 +4,7 @@ namespace Activitypub\Rest;
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
use WP_REST_Server;
|
use WP_REST_Server;
|
||||||
use WP_REST_Response;
|
use WP_REST_Response;
|
||||||
use Activitypub\Transformer\Transformers_Manager;
|
use Activitypub\Transformer\Post;
|
||||||
use Activitypub\Activity\Activity;
|
use Activitypub\Activity\Activity;
|
||||||
use Activitypub\Collection\Users as User_Collection;
|
use Activitypub\Collection\Users as User_Collection;
|
||||||
|
|
||||||
|
@ -168,7 +168,7 @@ class Collection {
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ( $posts as $post ) {
|
foreach ( $posts as $post ) {
|
||||||
$response['orderedItems'][] = Transformers_Manager::instance()->get_transformer( $post )->to_object()->to_array();
|
$response['orderedItems'][] = Post::transform( $post )->to_object()->to_array();
|
||||||
}
|
}
|
||||||
|
|
||||||
$rest_response = new WP_REST_Response( $response, 200 );
|
$rest_response = new WP_REST_Response( $response, 200 );
|
||||||
|
|
|
@ -8,9 +8,11 @@ use Activitypub\Activity\Activity;
|
||||||
use Activitypub\Collection\Users as User_Collection;
|
use Activitypub\Collection\Users as User_Collection;
|
||||||
|
|
||||||
use function Activitypub\get_context;
|
use function Activitypub\get_context;
|
||||||
|
use function Activitypub\object_to_uri;
|
||||||
use function Activitypub\url_to_authorid;
|
use function Activitypub\url_to_authorid;
|
||||||
use function Activitypub\get_rest_url_by_path;
|
use function Activitypub\get_rest_url_by_path;
|
||||||
use function Activitypub\get_remote_metadata_by_actor;
|
use function Activitypub\get_remote_metadata_by_actor;
|
||||||
|
use function Activitypub\extract_recipients_from_activity;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ActivityPub Inbox REST-Class
|
* ActivityPub Inbox REST-Class
|
||||||
|
@ -25,8 +27,6 @@ class Inbox {
|
||||||
*/
|
*/
|
||||||
public static function init() {
|
public static function init() {
|
||||||
self::register_routes();
|
self::register_routes();
|
||||||
|
|
||||||
\add_action( 'activitypub_inbox_create', array( self::class, 'handle_create' ), 10, 2 );
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -94,11 +94,8 @@ class Inbox {
|
||||||
$json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' );
|
$json->generator = 'http://wordpress.org/?v=' . \get_bloginfo_rss( 'version' );
|
||||||
$json->type = 'OrderedCollectionPage';
|
$json->type = 'OrderedCollectionPage';
|
||||||
$json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) ); // phpcs:ignore
|
$json->partOf = get_rest_url_by_path( sprintf( 'users/%d/inbox', $user->get__id() ) ); // phpcs:ignore
|
||||||
|
|
||||||
$json->totalItems = 0; // phpcs:ignore
|
$json->totalItems = 0; // phpcs:ignore
|
||||||
|
|
||||||
$json->orderedItems = array(); // phpcs:ignore
|
$json->orderedItems = array(); // phpcs:ignore
|
||||||
|
|
||||||
$json->first = $json->partOf; // phpcs:ignore
|
$json->first = $json->partOf; // phpcs:ignore
|
||||||
|
|
||||||
// filter output
|
// filter output
|
||||||
|
@ -131,11 +128,12 @@ class Inbox {
|
||||||
}
|
}
|
||||||
|
|
||||||
$data = $request->get_json_params();
|
$data = $request->get_json_params();
|
||||||
|
$activity = Activity::init_from_array( $data );
|
||||||
$type = $request->get_param( 'type' );
|
$type = $request->get_param( 'type' );
|
||||||
$type = \strtolower( $type );
|
$type = \strtolower( $type );
|
||||||
|
|
||||||
\do_action( 'activitypub_inbox', $data, $user->get__id(), $type );
|
\do_action( 'activitypub_inbox', $data, $user->get__id(), $type, $activity );
|
||||||
\do_action( "activitypub_inbox_{$type}", $data, $user->get__id() );
|
\do_action( "activitypub_inbox_{$type}", $data, $user->get__id(), $activity );
|
||||||
|
|
||||||
$rest_response = new WP_REST_Response( array(), 202 );
|
$rest_response = new WP_REST_Response( array(), 202 );
|
||||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||||
|
@ -152,38 +150,12 @@ class Inbox {
|
||||||
*/
|
*/
|
||||||
public static function shared_inbox_post( $request ) {
|
public static function shared_inbox_post( $request ) {
|
||||||
$data = $request->get_json_params();
|
$data = $request->get_json_params();
|
||||||
|
$activity = Activity::init_from_array( $data );
|
||||||
$type = $request->get_param( 'type' );
|
$type = $request->get_param( 'type' );
|
||||||
$users = self::extract_recipients( $data );
|
|
||||||
|
|
||||||
if ( ! $users ) {
|
|
||||||
return new WP_Error(
|
|
||||||
'rest_invalid_param',
|
|
||||||
\__( 'No recipients found', 'activitypub' ),
|
|
||||||
array(
|
|
||||||
'status' => 400,
|
|
||||||
'params' => array(
|
|
||||||
'to' => \__( 'Please check/validate "to" field', 'activitypub' ),
|
|
||||||
'bto' => \__( 'Please check/validate "bto" field', 'activitypub' ),
|
|
||||||
'cc' => \__( 'Please check/validate "cc" field', 'activitypub' ),
|
|
||||||
'bcc' => \__( 'Please check/validate "bcc" field', 'activitypub' ),
|
|
||||||
'audience' => \__( 'Please check/validate "audience" field', 'activitypub' ),
|
|
||||||
),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ( $users as $user ) {
|
|
||||||
$user = User_Collection::get_by_various( $user );
|
|
||||||
|
|
||||||
if ( is_wp_error( $user ) ) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$type = \strtolower( $type );
|
$type = \strtolower( $type );
|
||||||
|
|
||||||
\do_action( 'activitypub_inbox', $data, $user->ID, $type );
|
\do_action( 'activitypub_inbox', $data, null, $type, $activity );
|
||||||
\do_action( "activitypub_inbox_{$type}", $data, $user->ID );
|
\do_action( "activitypub_inbox_{$type}", $data, null, $activity );
|
||||||
}
|
|
||||||
|
|
||||||
$rest_response = new WP_REST_Response( array(), 202 );
|
$rest_response = new WP_REST_Response( array(), 202 );
|
||||||
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
$rest_response->header( 'Content-Type', 'application/activity+json; charset=' . get_option( 'blog_charset' ) );
|
||||||
|
@ -236,14 +208,7 @@ class Inbox {
|
||||||
$params['actor'] = array(
|
$params['actor'] = array(
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'sanitize_callback' => function ( $param, $request, $key ) {
|
'sanitize_callback' => function ( $param, $request, $key ) {
|
||||||
if ( \is_array( $param ) ) {
|
return object_to_uri( $param );
|
||||||
if ( isset( $param['id'] ) ) {
|
|
||||||
$param = $param['id'];
|
|
||||||
} else {
|
|
||||||
$param = $param['url'];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return \esc_url_raw( $param );
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -285,10 +250,7 @@ class Inbox {
|
||||||
'required' => true,
|
'required' => true,
|
||||||
//'type' => array( 'object', 'string' ),
|
//'type' => array( 'object', 'string' ),
|
||||||
'sanitize_callback' => function ( $param, $request, $key ) {
|
'sanitize_callback' => function ( $param, $request, $key ) {
|
||||||
if ( ! \is_string( $param ) ) {
|
return object_to_uri( $param );
|
||||||
$param = $param['id'];
|
|
||||||
}
|
|
||||||
return \esc_url_raw( $param );
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -340,121 +302,6 @@ class Inbox {
|
||||||
return $params;
|
return $params;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Handles "Create" requests
|
|
||||||
*
|
|
||||||
* @param array $object The activity-object
|
|
||||||
* @param int $user_id The id of the local blog-user
|
|
||||||
*/
|
|
||||||
public static function handle_create( $object, $user_id ) {
|
|
||||||
$meta = get_remote_metadata_by_actor( $object['actor'] );
|
|
||||||
|
|
||||||
if ( ! isset( $object['object']['inReplyTo'] ) ) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// check if Activity is public or not
|
|
||||||
if ( ! self::is_activity_public( $object ) ) {
|
|
||||||
// @todo maybe send email
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$comment_post_id = \url_to_postid( $object['object']['inReplyTo'] );
|
|
||||||
|
|
||||||
// save only replys and reactions
|
|
||||||
if ( ! $comment_post_id ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$commentdata = array(
|
|
||||||
'comment_post_ID' => $comment_post_id,
|
|
||||||
'comment_author' => \esc_attr( $meta['name'] ),
|
|
||||||
'comment_author_url' => \esc_url_raw( $object['actor'] ),
|
|
||||||
'comment_content' => addslashes( \wp_kses( $object['object']['content'], 'pre_comment_content' ) ),
|
|
||||||
'comment_type' => 'comment',
|
|
||||||
'comment_author_email' => '',
|
|
||||||
'comment_parent' => 0,
|
|
||||||
'comment_meta' => array(
|
|
||||||
'source_url' => \esc_url_raw( $object['object']['url'] ),
|
|
||||||
'avatar_url' => \esc_url_raw( $meta['icon']['url'] ),
|
|
||||||
'protocol' => 'activitypub',
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
// disable flood control
|
|
||||||
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
|
|
||||||
|
|
||||||
// do not require email for AP entries
|
|
||||||
\add_filter( 'pre_option_require_name_email', '__return_false' );
|
|
||||||
|
|
||||||
// No nonce possible for this submission route
|
|
||||||
\add_filter(
|
|
||||||
'akismet_comment_nonce',
|
|
||||||
function() {
|
|
||||||
return 'inactive';
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
\add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
|
|
||||||
|
|
||||||
$state = \wp_new_comment( $commentdata, true );
|
|
||||||
|
|
||||||
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ) );
|
|
||||||
\remove_filter( 'pre_option_require_name_email', '__return_false' );
|
|
||||||
|
|
||||||
// re-add flood control
|
|
||||||
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
|
|
||||||
|
|
||||||
do_action( 'activitypub_handled_create', $object, $user_id, $state, $commentdata );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Extract recipient URLs from Activity object
|
|
||||||
*
|
|
||||||
* @param array $data
|
|
||||||
*
|
|
||||||
* @return array The list of user URLs
|
|
||||||
*/
|
|
||||||
public static function extract_recipients( $data ) {
|
|
||||||
$recipient_items = array();
|
|
||||||
|
|
||||||
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
|
|
||||||
if ( array_key_exists( $i, $data ) ) {
|
|
||||||
if ( is_array( $data[ $i ] ) ) {
|
|
||||||
$recipient = $data[ $i ];
|
|
||||||
} else {
|
|
||||||
$recipient = array( $data[ $i ] );
|
|
||||||
}
|
|
||||||
$recipient_items = array_merge( $recipient_items, $recipient );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
|
|
||||||
if ( is_array( $data['object'][ $i ] ) ) {
|
|
||||||
$recipient = $data['object'][ $i ];
|
|
||||||
} else {
|
|
||||||
$recipient = array( $data['object'][ $i ] );
|
|
||||||
}
|
|
||||||
$recipient_items = array_merge( $recipient_items, $recipient );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$recipients = array();
|
|
||||||
|
|
||||||
// flatten array
|
|
||||||
foreach ( $recipient_items as $recipient ) {
|
|
||||||
if ( is_array( $recipient ) ) {
|
|
||||||
// check if recipient is an object
|
|
||||||
if ( array_key_exists( 'id', $recipient ) ) {
|
|
||||||
$recipients[] = $recipient['id'];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
$recipients[] = $recipient;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return array_unique( $recipients );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get local user recipients
|
* Get local user recipients
|
||||||
*
|
*
|
||||||
|
@ -463,7 +310,7 @@ class Inbox {
|
||||||
* @return array The list of local users
|
* @return array The list of local users
|
||||||
*/
|
*/
|
||||||
public static function get_recipients( $data ) {
|
public static function get_recipients( $data ) {
|
||||||
$recipients = self::extract_recipients( $data );
|
$recipients = extract_recipients_from_activity( $data );
|
||||||
$users = array();
|
$users = array();
|
||||||
|
|
||||||
foreach ( $recipients as $recipient ) {
|
foreach ( $recipients as $recipient ) {
|
||||||
|
@ -478,41 +325,4 @@ class Inbox {
|
||||||
|
|
||||||
return $users;
|
return $users;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if passed Activity is Public
|
|
||||||
*
|
|
||||||
* @param array $data
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
public static function is_activity_public( $data ) {
|
|
||||||
$recipients = self::extract_recipients( $data );
|
|
||||||
|
|
||||||
return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds line breaks to the list of allowed comment tags.
|
|
||||||
*
|
|
||||||
* @param array $allowedtags Allowed HTML tags.
|
|
||||||
* @param string $context Context.
|
|
||||||
* @return array Filtered tag list.
|
|
||||||
*/
|
|
||||||
public static function allowed_comment_html( $allowedtags, $context = '' ) {
|
|
||||||
if ( 'pre_comment_content' !== $context ) {
|
|
||||||
// Do nothing.
|
|
||||||
return $allowedtags;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add `p` and `br` to the list of allowed tags.
|
|
||||||
if ( ! array_key_exists( 'br', $allowedtags ) ) {
|
|
||||||
$allowedtags['br'] = array();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( ! array_key_exists( 'p', $allowedtags ) ) {
|
|
||||||
$allowedtags['p'] = array();
|
|
||||||
}
|
|
||||||
|
|
||||||
return $allowedtags;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,6 +105,12 @@ class Nodeinfo {
|
||||||
'outbound' => array(),
|
'outbound' => array(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$nodeinfo['metadata'] = array(
|
||||||
|
'nodeName' => \get_bloginfo( 'name' ),
|
||||||
|
'nodeDescription' => \get_bloginfo( 'description' ),
|
||||||
|
'nodeIcon' => \get_site_icon_url(),
|
||||||
|
);
|
||||||
|
|
||||||
return new WP_REST_Response( $nodeinfo, 200 );
|
return new WP_REST_Response( $nodeinfo, 200 );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -169,6 +175,10 @@ class Nodeinfo {
|
||||||
'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
'rel' => 'http://nodeinfo.diaspora.software/ns/schema/2.0',
|
||||||
'href' => get_rest_url_by_path( 'nodeinfo' ),
|
'href' => get_rest_url_by_path( 'nodeinfo' ),
|
||||||
),
|
),
|
||||||
|
array(
|
||||||
|
'rel' => 'https://www.w3.org/ns/activitystreams#Application',
|
||||||
|
'href' => get_rest_url_by_path( 'application' ),
|
||||||
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
return new \WP_REST_Response( $discovery, 200 );
|
return new \WP_REST_Response( $discovery, 200 );
|
||||||
|
|
|
@ -5,7 +5,7 @@ use stdClass;
|
||||||
use WP_Error;
|
use WP_Error;
|
||||||
use WP_REST_Server;
|
use WP_REST_Server;
|
||||||
use WP_REST_Response;
|
use WP_REST_Response;
|
||||||
use Activitypub\Transformer\Transformers_Manager;
|
use Activitypub\Transformer\Post;
|
||||||
use Activitypub\Activity\Activity;
|
use Activitypub\Activity\Activity;
|
||||||
use Activitypub\Collection\Users as User_Collection;
|
use Activitypub\Collection\Users as User_Collection;
|
||||||
|
|
||||||
|
@ -59,7 +59,7 @@ class Outbox {
|
||||||
return $user;
|
return $user;
|
||||||
}
|
}
|
||||||
|
|
||||||
$post_types = array_keys( \get_option( 'activitypub_transformer_mapping', array( 'post' => 'activitypub/default', 'page' => 'activitypub/default' ) ) );
|
$post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) );
|
||||||
|
|
||||||
$page = $request->get_param( 'page', 1 );
|
$page = $request->get_param( 'page', 1 );
|
||||||
|
|
||||||
|
@ -105,8 +105,7 @@ class Outbox {
|
||||||
);
|
);
|
||||||
|
|
||||||
foreach ( $posts as $post ) {
|
foreach ( $posts as $post ) {
|
||||||
$transformer = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( $post );
|
$post = Post::transform( $post )->to_object();
|
||||||
$post = $transformer->to_object();
|
|
||||||
$activity = new Activity();
|
$activity = new Activity();
|
||||||
$activity->set_type( 'Create' );
|
$activity->set_type( 'Create' );
|
||||||
$activity->set_context( null );
|
$activity->set_context( null );
|
||||||
|
|
|
@ -110,14 +110,20 @@ class Server {
|
||||||
if ( 'GET' !== $request->get_method() ) {
|
if ( 'GET' !== $request->get_method() ) {
|
||||||
$verified_request = Signature::verify_http_signature( $request );
|
$verified_request = Signature::verify_http_signature( $request );
|
||||||
if ( \is_wp_error( $verified_request ) ) {
|
if ( \is_wp_error( $verified_request ) ) {
|
||||||
return new WP_Error( 'activitypub_signature_verification', $verified_request->get_error_message(), array( 'status' => 401 ) );
|
return new WP_Error(
|
||||||
|
'activitypub_signature_verification',
|
||||||
|
$verified_request->get_error_message(),
|
||||||
|
array( 'status' => 401 )
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} elseif ( 'GET' === $request->get_method() ) { // GET-Requests are only signed in secure mode
|
} elseif ( 'GET' === $request->get_method() && ACTIVITYPUB_AUTHORIZED_FETCH ) { // GET-Requests are only signed in secure mode
|
||||||
if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
|
|
||||||
$verified_request = Signature::verify_http_signature( $request );
|
$verified_request = Signature::verify_http_signature( $request );
|
||||||
if ( \is_wp_error( $verified_request ) ) {
|
if ( \is_wp_error( $verified_request ) ) {
|
||||||
return new WP_Error( 'activitypub_signature_verification', $verified_request->get_error_message(), array( 'status' => 401 ) );
|
return new WP_Error(
|
||||||
}
|
'activitypub_signature_verification',
|
||||||
|
$verified_request->get_error_message(),
|
||||||
|
array( 'status' => 401 )
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -50,14 +50,13 @@ class Users {
|
||||||
array(
|
array(
|
||||||
'methods' => WP_REST_Server::READABLE,
|
'methods' => WP_REST_Server::READABLE,
|
||||||
'callback' => array( self::class, 'remote_follow_get' ),
|
'callback' => array( self::class, 'remote_follow_get' ),
|
||||||
|
'permission_callback' => '__return_true',
|
||||||
'args' => array(
|
'args' => array(
|
||||||
'resource' => array(
|
'resource' => array(
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'sanitize_callback' => 'sanitize_text_field',
|
'sanitize_callback' => 'sanitize_text_field',
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
'permission_callback' => '__return_true',
|
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -72,7 +72,7 @@ class Webfinger {
|
||||||
$params['resource'] = array(
|
$params['resource'] = array(
|
||||||
'required' => true,
|
'required' => true,
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'pattern' => '^acct:(.+)@(.+)$',
|
'pattern' => '^(acct:)|^(https?://)(.+)$',
|
||||||
);
|
);
|
||||||
|
|
||||||
return $params;
|
return $params;
|
||||||
|
@ -94,10 +94,13 @@ class Webfinger {
|
||||||
|
|
||||||
$aliases = array(
|
$aliases = array(
|
||||||
$user->get_url(),
|
$user->get_url(),
|
||||||
|
$user->get_alternate_url(),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$aliases = array_unique( $aliases );
|
||||||
|
|
||||||
$profile = array(
|
$profile = array(
|
||||||
'subject' => $resource,
|
'subject' => sprintf( 'acct:%s', $user->get_resource() ),
|
||||||
'aliases' => array_values( array_unique( $aliases ) ),
|
'aliases' => array_values( array_unique( $aliases ) ),
|
||||||
'links' => array(
|
'links' => array(
|
||||||
array(
|
array(
|
||||||
|
@ -113,6 +116,12 @@ class Webfinger {
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if ( 'Person' !== $user->get_type() ) {
|
||||||
|
$profile['links'][0]['properties'] = array(
|
||||||
|
'https://www.w3.org/ns/activitystreams#type' => $user->get_type(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return $profile;
|
return $profile;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
290
includes/table/class-follow-requests.php
Normal file
290
includes/table/class-follow-requests.php
Normal file
|
@ -0,0 +1,290 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Table;
|
||||||
|
|
||||||
|
use WP_List_Table;
|
||||||
|
use Activitypub\Collection\Users;
|
||||||
|
use Activitypub\Collection\Followers as FollowerCollection;
|
||||||
|
use Activitypub\Collection\Follow_Requests as FollowerRequestCollection;
|
||||||
|
use Activitypub\Model\Follow_Request;
|
||||||
|
|
||||||
|
use function Activitypub\object_to_uri;
|
||||||
|
|
||||||
|
if ( ! \class_exists( '\WP_List_Table' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Table that shows all follow requests for a user and allows handling those requests.
|
||||||
|
*/
|
||||||
|
class Follow_Requests extends WP_List_Table {
|
||||||
|
private $user_id;
|
||||||
|
public $follow_requests_count = 0;
|
||||||
|
|
||||||
|
public function __construct( $user_id = null ) {
|
||||||
|
|
||||||
|
if ( $user_id ) {
|
||||||
|
$this->user_id = $user_id;
|
||||||
|
} else {
|
||||||
|
$this->user_id = \get_current_user_id();
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::__construct(
|
||||||
|
array(
|
||||||
|
'singular' => \__( 'Follower', 'activitypub' ),
|
||||||
|
'plural' => \__( 'Followers', 'activitypub' ),
|
||||||
|
'ajax' => true,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_columns() {
|
||||||
|
return array(
|
||||||
|
'cb' => '<input type="button">',
|
||||||
|
'action' => \__( 'Action', 'activitypub' ),
|
||||||
|
'name' => \__( 'Name', 'activitypub' ),
|
||||||
|
'url' => \__( 'URL', 'activitypub' ),
|
||||||
|
'status' => \__( 'Status', 'activitypub' ),
|
||||||
|
'published' => \__( 'Created', 'activitypub' ),
|
||||||
|
'modified' => \__( 'Last updated', 'activitypub' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_sortable_columns() {
|
||||||
|
$sortable_columns = array(
|
||||||
|
'status' => array( 'status', false),
|
||||||
|
'name' => array( 'name', true ),
|
||||||
|
'modified' => array( 'modified', false ),
|
||||||
|
'published' => array( 'published', false ),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $sortable_columns;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function prepare_items() {
|
||||||
|
$columns = $this->get_columns();
|
||||||
|
$hidden = array();
|
||||||
|
|
||||||
|
$this->process_action();
|
||||||
|
$this->_column_headers = array( $columns, $hidden, $this->get_sortable_columns() );
|
||||||
|
|
||||||
|
$page_num = $this->get_pagenum();
|
||||||
|
$per_page = 20;
|
||||||
|
|
||||||
|
$args = array();
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||||
|
if ( isset( $_GET['orderby'] ) ) {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||||
|
$args['orderby'] = sanitize_text_field( wp_unslash( $_GET['orderby'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||||
|
if ( isset( $_GET['order'] ) ) {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||||
|
$args['order'] = sanitize_text_field( wp_unslash( $_GET['order'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||||
|
if ( isset( $_GET['s'] ) && isset( $_REQUEST['_wpnonce'] ) ) {
|
||||||
|
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) );
|
||||||
|
if ( wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
|
||||||
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||||
|
$args['s'] = sanitize_text_field( wp_unslash( $_GET['s'] ) );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$follow_requests_with_count = FollowerRequestCollection::get_follow_requests_for_user( $this->user_id, $per_page, $page_num, $args );
|
||||||
|
|
||||||
|
$follow_requests = $follow_requests_with_count['follow_requests'];
|
||||||
|
$counter = $follow_requests_with_count['total_items'];
|
||||||
|
$this->follow_requests_count = $counter;
|
||||||
|
|
||||||
|
$this->items = array();
|
||||||
|
$this->set_pagination_args(
|
||||||
|
array(
|
||||||
|
'total_items' => $counter,
|
||||||
|
'total_pages' => ceil( $counter / $per_page ),
|
||||||
|
'per_page' => $per_page,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
foreach ( $follow_requests as $follow_request ) {
|
||||||
|
$item = array(
|
||||||
|
'status' => esc_attr( $follow_request->status ),
|
||||||
|
'name' => esc_attr( $follow_request->post_title ),
|
||||||
|
'url' => esc_attr( $follow_request->follower_guid ),
|
||||||
|
'guid' => esc_attr( $follow_request->guid ),
|
||||||
|
'id' => esc_attr( $follow_request->id ),
|
||||||
|
'published' => esc_attr( $follow_request->published ),
|
||||||
|
'modified' => esc_attr( $follow_request->follower_modified ),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->items[] = $item;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function get_bulk_actions() {
|
||||||
|
return array(
|
||||||
|
'reject' => __( 'Reject', 'activitypub' ),
|
||||||
|
'approve' => __( 'Approve', 'activitypub' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_default( $item, $column_name ) {
|
||||||
|
if ( ! array_key_exists( $column_name, $item ) ) {
|
||||||
|
return __( 'None', 'activitypub' );
|
||||||
|
}
|
||||||
|
return $item[ $column_name ];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_status( $item ) {
|
||||||
|
$status = $item['status'];
|
||||||
|
switch ( $status ) {
|
||||||
|
case 'approved':
|
||||||
|
$color = 'success';
|
||||||
|
break;
|
||||||
|
case 'pending':
|
||||||
|
$color = 'warning';
|
||||||
|
break;
|
||||||
|
case 'rejected':
|
||||||
|
$color = 'danger';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$color = 'warning';
|
||||||
|
}
|
||||||
|
return sprintf(
|
||||||
|
'<span class="activitypub-settings-label activitypub-settings-label-%s">%s</span>',
|
||||||
|
$color,
|
||||||
|
ucfirst( $status )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_avatar( $item ) {
|
||||||
|
return sprintf(
|
||||||
|
'<img src="%s" width="25px;" />',
|
||||||
|
$item['icon']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_url( $item ) {
|
||||||
|
return sprintf(
|
||||||
|
'<a href="%s" target="_blank">%s</a>',
|
||||||
|
$item['url'],
|
||||||
|
$item['url']
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_cb( $item ) {
|
||||||
|
return sprintf( '<input type="checkbox" name="follow_requests[]" value="%s" />', esc_attr( $item['id'] ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function ajax_response() {
|
||||||
|
$follow_action = $_REQUEST['follow_action'];
|
||||||
|
$id = $_REQUEST['follow_request'];
|
||||||
|
wp_verify_nonce( $_REQUEST['_wpnonce'], "activitypub_{$follow_action}_follow_request" );
|
||||||
|
$follow_request = Follow_Request::from_wp_id( $id );
|
||||||
|
if ( $follow_request->can_handle_follow_request() ) {
|
||||||
|
switch ( $follow_action ) {
|
||||||
|
case 'approve':
|
||||||
|
$follow_request->approve();
|
||||||
|
wp_die( 'approved' );
|
||||||
|
case 'reject':
|
||||||
|
$follow_request->reject();
|
||||||
|
wp_die( 'rejected' );
|
||||||
|
case 'delete':
|
||||||
|
$follow_request->delete();
|
||||||
|
wp_die( 'deleted' );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static function display_follow_request_action_button( $id, $follow_action, $display = true ) {
|
||||||
|
$url = add_query_arg(
|
||||||
|
array(
|
||||||
|
'follow_request' => $id,
|
||||||
|
'action' => 'activitypub_handle_follow_request',
|
||||||
|
'follow_action' => $follow_action,
|
||||||
|
'_wpnonce' => wp_create_nonce( "activitypub_{$follow_action}_follow_request" ),
|
||||||
|
),
|
||||||
|
admin_url( 'admin-ajax.php' )
|
||||||
|
);
|
||||||
|
if ( $display ) {
|
||||||
|
$type = 'button';
|
||||||
|
} else {
|
||||||
|
$type = 'hidden';
|
||||||
|
}
|
||||||
|
printf(
|
||||||
|
'<input type="%s" class="button" value="%s" data-action="%s">',
|
||||||
|
esc_attr( $type ),
|
||||||
|
esc_attr__( ucfirst( $follow_action ), 'activitypub' ),
|
||||||
|
esc_url( $url )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function column_action($item) {
|
||||||
|
$status = $item['status'];
|
||||||
|
|
||||||
|
printf('<div class="activitypub-settings-action-buttons">');
|
||||||
|
|
||||||
|
// TODO this can be written smarter, but at least it is readable.
|
||||||
|
if ( 'pending' === $status ) {
|
||||||
|
self::display_follow_request_action_button( $item['id'], 'approve');
|
||||||
|
self::display_follow_request_action_button( $item['id'], 'reject');
|
||||||
|
self::display_follow_request_action_button( $item['id'], 'delete', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'approved' === $status ) {
|
||||||
|
self::display_follow_request_action_button( $item['id'], 'approve', false);
|
||||||
|
self::display_follow_request_action_button( $item['id'], 'reject');
|
||||||
|
self::display_follow_request_action_button( $item['id'], 'delete', false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( 'rejected' === $status ) {
|
||||||
|
self::display_follow_request_action_button( $item['id'], 'approve', false); // TODO: Clarify with Mobilizon
|
||||||
|
self::display_follow_request_action_button( $item['id'], 'reject', false);
|
||||||
|
self::display_follow_request_action_button( $item['id'], 'delete');
|
||||||
|
}
|
||||||
|
|
||||||
|
printf('</div>');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function process_action() {
|
||||||
|
if ( ! isset( $_REQUEST['follow_requests'] ) || ! isset( $_REQUEST['_wpnonce'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_wpnonce'] ) );
|
||||||
|
if ( ! wp_verify_nonce( $nonce, 'bulk-' . $this->_args['plural'] ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! current_user_can( 'edit_user', $this->user_id ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$follow_requests = $_REQUEST['follow_requests']; // phpcs:ignore
|
||||||
|
|
||||||
|
switch ( $this->current_action() ) {
|
||||||
|
case 'reject':
|
||||||
|
if ( ! is_array( $follow_requests ) ) {
|
||||||
|
$follow_requests = array( $follow_requests );
|
||||||
|
}
|
||||||
|
foreach ( $follow_requests as $follow_request ) {
|
||||||
|
Follow_Request::from_wp_id( $follow_request )->reject();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'approve':
|
||||||
|
if ( ! is_array( $follow_requests ) ) {
|
||||||
|
$follow_requests = array( $follow_requests );
|
||||||
|
}
|
||||||
|
foreach ( $follow_requests as $follow_request ) {
|
||||||
|
Follow_Request::from_wp_id( $follow_request )->approve();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function follow_requests_count() {
|
||||||
|
return $this->follow_requests_count;
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,8 @@ use WP_List_Table;
|
||||||
use Activitypub\Collection\Users;
|
use Activitypub\Collection\Users;
|
||||||
use Activitypub\Collection\Followers as FollowerCollection;
|
use Activitypub\Collection\Followers as FollowerCollection;
|
||||||
|
|
||||||
|
use function Activitypub\object_to_uri;
|
||||||
|
|
||||||
if ( ! \class_exists( '\WP_List_Table' ) ) {
|
if ( ! \class_exists( '\WP_List_Table' ) ) {
|
||||||
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
require_once ABSPATH . 'wp-admin/includes/class-wp-list-table.php';
|
||||||
}
|
}
|
||||||
|
@ -101,7 +103,7 @@ class Followers extends WP_List_Table {
|
||||||
'icon' => esc_attr( $follower->get_icon_url() ),
|
'icon' => esc_attr( $follower->get_icon_url() ),
|
||||||
'post_title' => esc_attr( $follower->get_name() ),
|
'post_title' => esc_attr( $follower->get_name() ),
|
||||||
'username' => esc_attr( $follower->get_preferred_username() ),
|
'username' => esc_attr( $follower->get_preferred_username() ),
|
||||||
'url' => esc_attr( $follower->get_url() ),
|
'url' => esc_attr( object_to_uri( $follower->get_url() ) ),
|
||||||
'identifier' => esc_attr( $follower->get_id() ),
|
'identifier' => esc_attr( $follower->get_id() ),
|
||||||
'published' => esc_attr( $follower->get_published() ),
|
'published' => esc_attr( $follower->get_published() ),
|
||||||
'modified' => esc_attr( $follower->get_updated() ),
|
'modified' => esc_attr( $follower->get_updated() ),
|
||||||
|
|
49
includes/transformer/class-attachment.php
Normal file
49
includes/transformer/class-attachment.php
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Transformer;
|
||||||
|
|
||||||
|
use Activitypub\Transformer\Post;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WordPress Attachment Transformer
|
||||||
|
*
|
||||||
|
* The Attachment Transformer is responsible for transforming a WP_Post object into different other
|
||||||
|
* Object-Types.
|
||||||
|
*
|
||||||
|
* Currently supported are:
|
||||||
|
*
|
||||||
|
* - Activitypub\Activity\Base_Object
|
||||||
|
*/
|
||||||
|
class Attachment extends Post {
|
||||||
|
/**
|
||||||
|
* Generates all Media Attachments for a Post.
|
||||||
|
*
|
||||||
|
* @return array The Attachments.
|
||||||
|
*/
|
||||||
|
protected function get_attachment() {
|
||||||
|
$mime_type = get_post_mime_type( $this->object->ID );
|
||||||
|
$media_type = preg_replace( '/(\/[a-zA-Z]+)/i', '', $mime_type );
|
||||||
|
|
||||||
|
switch ( $media_type ) {
|
||||||
|
case 'audio':
|
||||||
|
case 'video':
|
||||||
|
$type = 'Document';
|
||||||
|
break;
|
||||||
|
case 'image':
|
||||||
|
$type = 'Image';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
$attachment = array(
|
||||||
|
'type' => $type,
|
||||||
|
'url' => wp_get_attachment_url( $this->object->ID ),
|
||||||
|
'mediaType' => $mime_type,
|
||||||
|
);
|
||||||
|
|
||||||
|
$alt = \get_post_meta( $this->object->ID, '_wp_attachment_image_alt', true );
|
||||||
|
if ( $alt ) {
|
||||||
|
$attachment['name'] = $alt;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $attachment;
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,619 +1,105 @@
|
||||||
<?php
|
<?php
|
||||||
/**
|
|
||||||
* Inspired by the PHP ActivityPub Library by @Landrok
|
|
||||||
*
|
|
||||||
* @link https://github.com/landrok/activitypub
|
|
||||||
*/
|
|
||||||
namespace Activitypub\Transformer;
|
namespace Activitypub\Transformer;
|
||||||
|
|
||||||
use WP_Post;
|
use Activitypub\Activity\Activity;
|
||||||
use Activitypub\Collection\Users;
|
|
||||||
use Activitypub\Model\Blog_User;
|
|
||||||
use Activitypub\Activity\Base_Object;
|
use Activitypub\Activity\Base_Object;
|
||||||
use Activitypub\Shortcodes;
|
|
||||||
|
|
||||||
use function Activitypub\esc_hashtag;
|
|
||||||
use function Activitypub\is_single_user;
|
|
||||||
use function Activitypub\get_rest_url_by_path;
|
|
||||||
use function Activitypub\site_supports_blocks;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Base class to implement WordPress to ActivityPub transformers.
|
* WordPress Base Transformer
|
||||||
|
*
|
||||||
|
* Transformers are responsible for transforming a WordPress objects into different ActivityPub
|
||||||
|
* Object-Types or Activities.
|
||||||
*/
|
*/
|
||||||
abstract class Base {
|
abstract class Base {
|
||||||
/**
|
/**
|
||||||
* The WP_Post object.
|
* The WP_Post object.
|
||||||
*
|
*
|
||||||
* @var WP_Post
|
* @var
|
||||||
*/
|
*/
|
||||||
protected $wp_post;
|
protected $object;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Assign WP_Post Object to a specific transformer instance.
|
* Static function to Transform a WordPress Object.
|
||||||
*
|
*
|
||||||
* This helps to chain the output of the Transformer.
|
* This helps to chain the output of the Transformer.
|
||||||
*
|
*
|
||||||
* @param WP_Post $wp_post The WP_Post object.
|
* @param stdClass $object The WP_Post object
|
||||||
|
*
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function set_wp_post( WP_Post $wp_post ) {
|
public static function transform( $object ) {
|
||||||
$post_type = get_post_type( $wp_post );
|
return new static( $object );
|
||||||
if ( ! $this->supports_post_type( $post_type ) ) {
|
|
||||||
_doing_it_wrong(
|
|
||||||
__METHOD__,
|
|
||||||
/* translators: %s: Block name. */
|
|
||||||
sprintf( 'The Transformer "%s" does not support the post type "%s".', esc_html( $this->get_label() ), esc_html( $post_type ) ),
|
|
||||||
'version_number_transformer_management_placeholder'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
$this->wp_post = $wp_post;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the supported WP post types that the transformer can use as an input.
|
* Base constructor.
|
||||||
*
|
*
|
||||||
* By default all post types are supported.
|
* @param stdClass $object
|
||||||
* You may very likely wish to override this function.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @return string[] An array containing all the supported post types.
|
|
||||||
*/
|
*/
|
||||||
public function get_supported_post_types() {
|
public function __construct( $object ) {
|
||||||
return \get_post_types( array( 'public' => true ), 'names' );
|
$this->object = $object;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the name of the plugin that registered the transformer.
|
* Transform the WordPress Object into an ActivityPub Object.
|
||||||
*
|
*
|
||||||
* @see Forked from the WordPress elementor plugin.
|
* @return Activitypub\Activity\Base_Object
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @return string Plugin name
|
|
||||||
*/
|
|
||||||
private function get_plugin_name_from_transformer_instance( $transformer ) {
|
|
||||||
$class_reflection = new \ReflectionClass( $transformer );
|
|
||||||
|
|
||||||
$plugin_basename = plugin_basename( $class_reflection->getFileName() );
|
|
||||||
|
|
||||||
$plugin_directory = strtok( $plugin_basename, '/' );
|
|
||||||
|
|
||||||
$plugins_data = get_plugins( '/' . $plugin_directory );
|
|
||||||
$plugin_data = array_shift( $plugins_data );
|
|
||||||
|
|
||||||
if ( isset( $plugin_data['Name'] ) ) {
|
|
||||||
return $plugin_data['Name'];
|
|
||||||
} else {
|
|
||||||
return esc_html__( 'Unknown', 'activitypub' );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return whether the transformer supports a post type.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @return string post_type Post type name.
|
|
||||||
*/
|
|
||||||
final public function supports_post_type( $post_type ) {
|
|
||||||
return in_array( $post_type, $this->get_supported_post_types(), true );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the name used for registering the transformer with the ActivityPub plugin.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @return string name
|
|
||||||
*/
|
|
||||||
abstract public function get_name();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the display name for the ActivityPub transformer.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @return string display name
|
|
||||||
*/
|
|
||||||
abstract public function get_label();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the ActivityStreams 2.0 Object-Type for a Post.
|
|
||||||
*
|
|
||||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
|
|
||||||
*
|
|
||||||
* @return string The Object-Type.
|
|
||||||
*/
|
|
||||||
abstract protected function get_object_type();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the content for the ActivityPub Item.
|
|
||||||
*
|
|
||||||
* The content will be generated based on the user settings.
|
|
||||||
*
|
|
||||||
* @return string The content.
|
|
||||||
*/
|
|
||||||
protected function get_content() {
|
|
||||||
global $post;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Provides an action hook so plugins can add their own hooks/filters before AP content is generated.
|
|
||||||
*
|
|
||||||
* Example: if a plugin adds a filter to `the_content` to add a button to the end of posts, it can also remove that filter here.
|
|
||||||
*
|
|
||||||
* @param WP_Post $post The post object.
|
|
||||||
*/
|
|
||||||
do_action( 'activitypub_before_get_content', $post );
|
|
||||||
|
|
||||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
|
||||||
$post = $this->wp_post;
|
|
||||||
$content = $this->get_post_content_template();
|
|
||||||
|
|
||||||
// Register our shortcodes just in time.
|
|
||||||
Shortcodes::register();
|
|
||||||
// Fill in the shortcodes.
|
|
||||||
setup_postdata( $post );
|
|
||||||
$content = do_shortcode( $content );
|
|
||||||
wp_reset_postdata();
|
|
||||||
|
|
||||||
$content = \wpautop( $content );
|
|
||||||
$content = \preg_replace( '/[\n\r\t]/', '', $content );
|
|
||||||
$content = \trim( $content );
|
|
||||||
|
|
||||||
$content = \apply_filters( 'activitypub_the_content', $content, $post );
|
|
||||||
|
|
||||||
// Don't need these any more, should never appear in a post.
|
|
||||||
Shortcodes::unregister();
|
|
||||||
|
|
||||||
return $content;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the template to use to generate the content of the activitypub item.
|
|
||||||
*
|
|
||||||
* @return string The Template.
|
|
||||||
*/
|
|
||||||
protected function get_post_content_template() {
|
|
||||||
if ( 'excerpt' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
|
|
||||||
return "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( 'title' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
|
|
||||||
return "[ap_title]\n\n[ap_permalink type=\"html\"]";
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( 'content' === \get_option( 'activitypub_post_content_type', 'content' ) ) {
|
|
||||||
return "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
|
|
||||||
}
|
|
||||||
|
|
||||||
return \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the ID of the Post.
|
|
||||||
*
|
|
||||||
* @return string The Posts ID.
|
|
||||||
*/
|
|
||||||
public function get_id() {
|
|
||||||
return $this->get_url();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the URL of the Post.
|
|
||||||
*
|
|
||||||
* @return string The Posts URL.
|
|
||||||
*/
|
|
||||||
public function get_url() {
|
|
||||||
$post = $this->wp_post;
|
|
||||||
|
|
||||||
if ( 'trash' === get_post_status( $post ) ) {
|
|
||||||
$permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true );
|
|
||||||
} else {
|
|
||||||
$permalink = \get_permalink( $post );
|
|
||||||
}
|
|
||||||
|
|
||||||
return \esc_url( $permalink );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the User-URL of the Author of the Post.
|
|
||||||
*
|
|
||||||
* If `single_user` mode is enabled, the URL of the Blog-User is returned.
|
|
||||||
*
|
|
||||||
* @return string The User-URL.
|
|
||||||
*/
|
|
||||||
protected function get_attributed_to() {
|
|
||||||
if ( is_single_user() ) {
|
|
||||||
$user = new Blog_User();
|
|
||||||
return $user->get_url();
|
|
||||||
}
|
|
||||||
|
|
||||||
return Users::get_by_id( $this->wp_post->post_author )->get_url();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Generates all Media Attachments for a Post.
|
|
||||||
*
|
|
||||||
* @return array The Attachments.
|
|
||||||
*/
|
|
||||||
protected function get_attachments() {
|
|
||||||
// Once upon a time we only supported images, but we now support audio/video as well.
|
|
||||||
// We maintain the image-centric naming for backwards compatibility.
|
|
||||||
$max_media = intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) );
|
|
||||||
|
|
||||||
if ( site_supports_blocks() && \has_blocks( $this->wp_post->post_content ) ) {
|
|
||||||
return $this->get_block_attachments( $max_media );
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->get_classic_editor_images( $max_media );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
|
|
||||||
*
|
|
||||||
* @param int $max_media The maximum number of attachments to return.
|
|
||||||
*
|
|
||||||
* @return array The attachments.
|
|
||||||
*/
|
|
||||||
protected function get_block_attachments( $max_media ) {
|
|
||||||
// max media can't be negative or zero
|
|
||||||
if ( $max_media <= 0 ) {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
$id = $this->wp_post->ID;
|
|
||||||
|
|
||||||
$media_ids = array();
|
|
||||||
|
|
||||||
// list post thumbnail first if this post has one
|
|
||||||
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
|
|
||||||
$media_ids[] = \get_post_thumbnail_id( $id );
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $max_media > 0 ) {
|
|
||||||
$blocks = \parse_blocks( $this->wp_post->post_content );
|
|
||||||
$media_ids = self::get_media_ids_from_blocks( $blocks, $media_ids, $max_media );
|
|
||||||
}
|
|
||||||
|
|
||||||
return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media_ids ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get image attachments from the classic editor.
|
|
||||||
* Note that audio/video attachments are only supported in the block editor.
|
|
||||||
*
|
|
||||||
* @param int $max_images The maximum number of images to return.
|
|
||||||
*
|
|
||||||
* @return array The attachments.
|
|
||||||
*/
|
|
||||||
protected function get_classic_editor_images( $max_images ) {
|
|
||||||
// max images can't be negative or zero
|
|
||||||
if ( $max_images <= 0 ) {
|
|
||||||
return array();
|
|
||||||
}
|
|
||||||
|
|
||||||
$id = $this->wp_post->ID;
|
|
||||||
|
|
||||||
$image_ids = array();
|
|
||||||
|
|
||||||
// list post thumbnail first if this post has one
|
|
||||||
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
|
|
||||||
$image_ids[] = \get_post_thumbnail_id( $id );
|
|
||||||
--$max_images;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $max_images > 0 ) {
|
|
||||||
$query = new \WP_Query(
|
|
||||||
array(
|
|
||||||
'post_parent' => $id,
|
|
||||||
'post_status' => 'inherit',
|
|
||||||
'post_type' => 'attachment',
|
|
||||||
'post_mime_type' => 'image',
|
|
||||||
'order' => 'ASC',
|
|
||||||
'orderby' => 'menu_order ID',
|
|
||||||
'posts_per_page' => $max_images,
|
|
||||||
)
|
|
||||||
);
|
|
||||||
foreach ( $query->get_posts() as $attachment ) {
|
|
||||||
if ( ! \in_array( $attachment->ID, $image_ids, true ) ) {
|
|
||||||
$image_ids[] = $attachment->ID;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
$image_ids = \array_unique( $image_ids );
|
|
||||||
|
|
||||||
return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $image_ids ) );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Recursively get media IDs from blocks.
|
|
||||||
*
|
|
||||||
* @param array $blocks The blocks to search for media IDs
|
|
||||||
* @param array $media_ids The media IDs to append new IDs to
|
|
||||||
* @param int $max_media The maximum number of media to return.
|
|
||||||
*
|
|
||||||
* @return array The image IDs.
|
|
||||||
*/
|
|
||||||
protected static function get_media_ids_from_blocks( $blocks, $media_ids, $max_media ) {
|
|
||||||
|
|
||||||
foreach ( $blocks as $block ) {
|
|
||||||
// recurse into inner blocks
|
|
||||||
if ( ! empty( $block['innerBlocks'] ) ) {
|
|
||||||
$media_ids = self::get_media_ids_from_blocks( $block['innerBlocks'], $media_ids, $max_media );
|
|
||||||
}
|
|
||||||
|
|
||||||
switch ( $block['blockName'] ) {
|
|
||||||
case 'core/image':
|
|
||||||
case 'core/cover':
|
|
||||||
case 'core/audio':
|
|
||||||
case 'core/video':
|
|
||||||
case 'videopress/video':
|
|
||||||
if ( ! empty( $block['attrs']['id'] ) ) {
|
|
||||||
$media_ids[] = $block['attrs']['id'];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'jetpack/slideshow':
|
|
||||||
case 'jetpack/tiled-gallery':
|
|
||||||
if ( ! empty( $block['attrs']['ids'] ) ) {
|
|
||||||
$media_ids = array_merge( $media_ids, $block['attrs']['ids'] );
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case 'jetpack/image-compare':
|
|
||||||
if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
|
|
||||||
$media_ids[] = $block['attrs']['beforeImageId'];
|
|
||||||
}
|
|
||||||
if ( ! empty( $block['attrs']['afterImageId'] ) ) {
|
|
||||||
$media_ids[] = $block['attrs']['afterImageId'];
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// depupe
|
|
||||||
$media_ids = \array_unique( $media_ids );
|
|
||||||
|
|
||||||
// stop doing unneeded work
|
|
||||||
if ( count( $media_ids ) >= $max_media ) {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// still need to slice it because one gallery could knock us over the limit
|
|
||||||
return array_slice( $media_ids, 0, $max_media );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Converts a WordPress Attachment to an ActivityPub Attachment.
|
|
||||||
*
|
|
||||||
* @param int $id The Attachment ID.
|
|
||||||
*
|
|
||||||
* @return array The ActivityPub Attachment.
|
|
||||||
*/
|
|
||||||
public static function wp_attachment_to_activity_attachment( $id ) {
|
|
||||||
$attachment = array();
|
|
||||||
$mime_type = \get_post_mime_type( $id );
|
|
||||||
$mime_type_parts = \explode( '/', $mime_type );
|
|
||||||
// switching on image/audio/video
|
|
||||||
switch ( $mime_type_parts[0] ) {
|
|
||||||
case 'image':
|
|
||||||
$image_size = 'full';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter the image URL returned for each post.
|
|
||||||
*
|
|
||||||
* @param array|false $thumbnail The image URL, or false if no image is available.
|
|
||||||
* @param int $id The attachment ID.
|
|
||||||
* @param string $image_size The image size to retrieve. Set to 'full' by default.
|
|
||||||
*/
|
|
||||||
$thumbnail = apply_filters(
|
|
||||||
'activitypub_get_image',
|
|
||||||
self::get_image( $id, $image_size ),
|
|
||||||
$id,
|
|
||||||
$image_size
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( $thumbnail ) {
|
|
||||||
$alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
|
|
||||||
$image = array(
|
|
||||||
'type' => 'Image',
|
|
||||||
'url' => $thumbnail[0],
|
|
||||||
'mediaType' => $mime_type,
|
|
||||||
);
|
|
||||||
|
|
||||||
if ( $alt ) {
|
|
||||||
$image['name'] = $alt;
|
|
||||||
}
|
|
||||||
$attachment = $image;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'audio':
|
|
||||||
case 'video':
|
|
||||||
$attachment = array(
|
|
||||||
'type' => 'Document',
|
|
||||||
'mediaType' => $mime_type,
|
|
||||||
'url' => \wp_get_attachment_url( $id ),
|
|
||||||
'name' => \get_the_title( $id ),
|
|
||||||
);
|
|
||||||
$meta = wp_get_attachment_metadata( $id );
|
|
||||||
// height and width for videos
|
|
||||||
if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) {
|
|
||||||
$attachment['width'] = $meta['width'];
|
|
||||||
$attachment['height'] = $meta['height'];
|
|
||||||
}
|
|
||||||
// @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail?
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
return \apply_filters( 'activitypub_attachment', $attachment, $id );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return details about an image attachment.
|
|
||||||
*
|
|
||||||
* @param int $id The attachment ID.
|
|
||||||
* @param string $image_size The image size to retrieve. Set to 'full' by default.
|
|
||||||
*
|
|
||||||
* @return array|false Array of image data, or boolean false if no image is available.
|
|
||||||
*/
|
|
||||||
protected static function get_image( $id, $image_size = 'full' ) {
|
|
||||||
/**
|
|
||||||
* Hook into the image retrieval process. Before image retrieval.
|
|
||||||
*
|
|
||||||
* @param int $id The attachment ID.
|
|
||||||
* @param string $image_size The image size to retrieve. Set to 'full' by default.
|
|
||||||
*/
|
|
||||||
do_action( 'activitypub_get_image_pre', $id, $image_size );
|
|
||||||
|
|
||||||
$image = \wp_get_attachment_image_src( $id, $image_size );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Hook into the image retrieval process. After image retrieval.
|
|
||||||
*
|
|
||||||
* @param int $id The attachment ID.
|
|
||||||
* @param string $image_size The image size to retrieve. Set to 'full' by default.
|
|
||||||
*/
|
|
||||||
do_action( 'activitypub_get_image_post', $id, $image_size );
|
|
||||||
|
|
||||||
return $image;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Helper function to get the @-Mentions from the post content.
|
|
||||||
*
|
|
||||||
* @return array The list of @-Mentions.
|
|
||||||
*/
|
|
||||||
protected function get_mentions() {
|
|
||||||
return apply_filters( 'activitypub_extract_mentions', array(), $this->wp_post->post_content, $this->wp_post );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of Mentions, used in the Post.
|
|
||||||
*
|
|
||||||
* @see https://docs.joinmastodon.org/spec/activitypub/#Mention
|
|
||||||
*
|
|
||||||
* @return array The list of Mentions.
|
|
||||||
*/
|
|
||||||
protected function get_cc() {
|
|
||||||
$cc = array();
|
|
||||||
|
|
||||||
$mentions = $this->get_mentions();
|
|
||||||
if ( $mentions ) {
|
|
||||||
foreach ( $mentions as $url ) {
|
|
||||||
$cc[] = $url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $cc;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a list of Tags, used in the Post.
|
|
||||||
*
|
|
||||||
* This includes Hash-Tags and Mentions.
|
|
||||||
*
|
|
||||||
* @return array The list of Tags.
|
|
||||||
*/
|
|
||||||
protected function get_tags() {
|
|
||||||
$tags = array();
|
|
||||||
|
|
||||||
$post_tags = \get_the_tags( $this->wp_post->ID );
|
|
||||||
if ( $post_tags ) {
|
|
||||||
foreach ( $post_tags as $post_tag ) {
|
|
||||||
$tag = array(
|
|
||||||
'type' => 'Hashtag',
|
|
||||||
'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
|
|
||||||
'name' => esc_hashtag( $post_tag->name ),
|
|
||||||
);
|
|
||||||
$tags[] = $tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$mentions = $this->get_mentions();
|
|
||||||
if ( $mentions ) {
|
|
||||||
foreach ( $mentions as $mention => $url ) {
|
|
||||||
$tag = array(
|
|
||||||
'type' => 'Mention',
|
|
||||||
'href' => \esc_url( $url ),
|
|
||||||
'name' => \esc_html( $mention ),
|
|
||||||
);
|
|
||||||
$tags[] = $tag;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return $tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns the locale of the post.
|
|
||||||
*
|
|
||||||
* @return string The locale of the post.
|
|
||||||
*/
|
|
||||||
public function get_locale() {
|
|
||||||
$post_id = $this->wp_post->ID;
|
|
||||||
$lang = \strtolower( \strtok( \get_locale(), '_-' ) );
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Filter the locale of the post.
|
|
||||||
*
|
|
||||||
* @param string $lang The locale of the post.
|
|
||||||
* @param int $post_id The post ID.
|
|
||||||
* @param WP_Post $post The post object.
|
|
||||||
*
|
|
||||||
* @return string The filtered locale of the post.
|
|
||||||
*/
|
|
||||||
return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->wp_post );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the contentMap
|
|
||||||
*
|
|
||||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-contentmap
|
|
||||||
*
|
|
||||||
* @return array the contenmap
|
|
||||||
*/
|
|
||||||
protected function get_content_map() {
|
|
||||||
return array(
|
|
||||||
$this->get_locale() => $this->get_content(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transforms the WP_Post object to an ActivityPub Object
|
|
||||||
*
|
|
||||||
* @see \Activitypub\Activity\Base_Object
|
|
||||||
*
|
|
||||||
* @return \Activitypub\Activity\Base_Object The ActivityPub Object
|
|
||||||
*/
|
*/
|
||||||
public function to_object() {
|
public function to_object() {
|
||||||
$wp_post = $this->wp_post;
|
|
||||||
$object = new Base_Object();
|
$object = new Base_Object();
|
||||||
|
|
||||||
$object->set_id( $this->get_id() );
|
$vars = $object->get_object_var_keys();
|
||||||
$object->set_url( $this->get_url() );
|
|
||||||
$object->set_type( $this->get_object_type() );
|
|
||||||
|
|
||||||
$published = \strtotime( $wp_post->post_date_gmt );
|
foreach ( $vars as $var ) {
|
||||||
|
$getter = 'get_' . $var;
|
||||||
|
|
||||||
$object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) );
|
if ( method_exists( $this, $getter ) ) {
|
||||||
|
$value = call_user_func( array( $this, $getter ) );
|
||||||
|
|
||||||
$updated = \strtotime( $wp_post->post_modified_gmt );
|
if ( isset( $value ) ) {
|
||||||
|
$setter = 'set_' . $var;
|
||||||
|
|
||||||
if ( $updated > $published ) {
|
call_user_func( array( $object, $setter ), $value );
|
||||||
$object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) );
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$object->set_attributed_to( $this->get_attributed_to() );
|
|
||||||
$object->set_content( $this->get_content() );
|
|
||||||
$object->set_content_map( $this->get_content_map );
|
|
||||||
$path = sprintf( 'users/%d/followers', intval( $wp_post->post_author ) );
|
|
||||||
|
|
||||||
$object->set_to(
|
|
||||||
array(
|
|
||||||
'https://www.w3.org/ns/activitystreams#Public',
|
|
||||||
get_rest_url_by_path( $path ),
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$object->set_cc( $this->get_cc() );
|
|
||||||
$object->set_attachment( $this->get_attachments() );
|
|
||||||
$object->set_tag( $this->get_tags() );
|
|
||||||
|
|
||||||
return $object;
|
return $object;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the ActivityPub Object to an Activity
|
||||||
|
*
|
||||||
|
* @param string $type The Activity-Type.
|
||||||
|
*
|
||||||
|
* @return \Activitypub\Activity\Activity The Activity.
|
||||||
|
*/
|
||||||
|
public function to_activity( $type ) {
|
||||||
|
$object = $this->to_object();
|
||||||
|
|
||||||
|
$activity = new Activity();
|
||||||
|
$activity->set_type( $type );
|
||||||
|
$activity->set_object( $object );
|
||||||
|
|
||||||
|
// Use simple Object (only ID-URI) for Like and Announce
|
||||||
|
if ( in_array( $type, array( 'Like', 'Announce' ), true ) ) {
|
||||||
|
$activity->set_object( $object->get_id() );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $activity;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of the WordPress Object.
|
||||||
|
*
|
||||||
|
* @return int The ID of the WordPress Object
|
||||||
|
*/
|
||||||
|
abstract public function get_wp_user_id();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the User-ID of the WordPress Post.
|
||||||
|
*
|
||||||
|
* @return int The User-ID of the WordPress Post
|
||||||
|
*/
|
||||||
|
abstract public function change_wp_user_id( $user_id );
|
||||||
}
|
}
|
||||||
|
|
277
includes/transformer/class-comment.php
Normal file
277
includes/transformer/class-comment.php
Normal file
|
@ -0,0 +1,277 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Transformer;
|
||||||
|
|
||||||
|
use WP_Comment;
|
||||||
|
use WP_Comment_Query;
|
||||||
|
use Activitypub\Hashtag;
|
||||||
|
use Activitypub\Webfinger;
|
||||||
|
use Activitypub\Model\Blog_User;
|
||||||
|
use Activitypub\Collection\Users;
|
||||||
|
use Activitypub\Transformer\Base;
|
||||||
|
use Activitypub\Activity\Base_Object;
|
||||||
|
|
||||||
|
use function Activitypub\esc_hashtag;
|
||||||
|
use function Activitypub\is_single_user;
|
||||||
|
use function Activitypub\get_rest_url_by_path;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* WordPress Comment Transformer
|
||||||
|
*
|
||||||
|
* The Comment Transformer is responsible for transforming a WP_Comment object into different
|
||||||
|
* Object-Types.
|
||||||
|
*
|
||||||
|
* Currently supported are:
|
||||||
|
*
|
||||||
|
* - Activitypub\Activity\Base_Object
|
||||||
|
*/
|
||||||
|
class Comment extends Base {
|
||||||
|
/**
|
||||||
|
* Returns the User-ID of the WordPress Comment.
|
||||||
|
*
|
||||||
|
* @return int The User-ID of the WordPress Comment
|
||||||
|
*/
|
||||||
|
public function get_wp_user_id() {
|
||||||
|
return $this->object->user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Change the User-ID of the WordPress Comment.
|
||||||
|
*
|
||||||
|
* @return int The User-ID of the WordPress Comment
|
||||||
|
*/
|
||||||
|
public function change_wp_user_id( $user_id ) {
|
||||||
|
$this->object->user_id = $user_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the WP_Comment object to an ActivityPub Object
|
||||||
|
*
|
||||||
|
* @see \Activitypub\Activity\Base_Object
|
||||||
|
*
|
||||||
|
* @return \Activitypub\Activity\Base_Object The ActivityPub Object
|
||||||
|
*/
|
||||||
|
public function to_object() {
|
||||||
|
$comment = $this->object;
|
||||||
|
$object = parent::to_object();
|
||||||
|
|
||||||
|
$object->set_url( \get_comment_link( $comment->comment_ID ) );
|
||||||
|
$object->set_type( 'Note' );
|
||||||
|
|
||||||
|
$published = \strtotime( $comment->comment_date_gmt );
|
||||||
|
$object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) );
|
||||||
|
|
||||||
|
$updated = \get_comment_meta( $comment->comment_ID, 'activitypub_last_modified', true );
|
||||||
|
if ( $updated > $published ) {
|
||||||
|
$object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', \strtotime( $updated ) ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$object->set_content_map(
|
||||||
|
array(
|
||||||
|
$this->get_locale() => $this->get_content(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$path = sprintf( 'users/%d/followers', intval( $comment->comment_author ) );
|
||||||
|
|
||||||
|
$object->set_to(
|
||||||
|
array(
|
||||||
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
get_rest_url_by_path( $path ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the User-URL of the Author of the Post.
|
||||||
|
*
|
||||||
|
* If `single_user` mode is enabled, the URL of the Blog-User is returned.
|
||||||
|
*
|
||||||
|
* @return string The User-URL.
|
||||||
|
*/
|
||||||
|
protected function get_attributed_to() {
|
||||||
|
if ( is_single_user() ) {
|
||||||
|
$user = new Blog_User();
|
||||||
|
return $user->get_url();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Users::get_by_id( $this->object->user_id )->get_url();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content for the ActivityPub Item.
|
||||||
|
*
|
||||||
|
* The content will be generated based on the user settings.
|
||||||
|
*
|
||||||
|
* @return string The content.
|
||||||
|
*/
|
||||||
|
protected function get_content() {
|
||||||
|
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||||
|
$comment = $this->object;
|
||||||
|
$content = $comment->comment_content;
|
||||||
|
|
||||||
|
$content = \wpautop( $content );
|
||||||
|
$content = \preg_replace( '/[\n\r\t]/', '', $content );
|
||||||
|
$content = \trim( $content );
|
||||||
|
$content = \apply_filters( 'the_content', $content, $comment );
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the in-reply-to for the ActivityPub Item.
|
||||||
|
*
|
||||||
|
* @return int The URL of the in-reply-to.
|
||||||
|
*/
|
||||||
|
protected function get_in_reply_to() {
|
||||||
|
$comment = $this->object;
|
||||||
|
|
||||||
|
$parent_comment = \get_comment( $comment->comment_parent );
|
||||||
|
|
||||||
|
if ( $parent_comment ) {
|
||||||
|
$comment_meta = \get_comment_meta( $parent_comment->comment_ID );
|
||||||
|
|
||||||
|
if ( ! empty( $comment_meta['source_id'][0] ) ) {
|
||||||
|
$in_reply_to = $comment_meta['source_id'][0];
|
||||||
|
} elseif ( ! empty( $comment_meta['source_url'][0] ) ) {
|
||||||
|
$in_reply_to = $comment_meta['source_url'][0];
|
||||||
|
} else {
|
||||||
|
$in_reply_to = $this->generate_id( $parent_comment );
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
$in_reply_to = \get_permalink( $comment->comment_post_ID );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $in_reply_to;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of the ActivityPub Object.
|
||||||
|
*
|
||||||
|
* @see https://www.w3.org/TR/activitypub/#obj-id
|
||||||
|
* @see https://github.com/tootsuite/mastodon/issues/13879
|
||||||
|
*
|
||||||
|
* @return string ActivityPub URI for comment
|
||||||
|
*/
|
||||||
|
protected function get_id() {
|
||||||
|
$comment = $this->object;
|
||||||
|
return $this->generate_id( $comment );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates an ActivityPub URI for a comment
|
||||||
|
*
|
||||||
|
* @param WP_Comment|int $comment A comment object or comment ID
|
||||||
|
*
|
||||||
|
* @return string ActivityPub URI for comment
|
||||||
|
*/
|
||||||
|
protected function generate_id( $comment ) {
|
||||||
|
$comment = get_comment( $comment );
|
||||||
|
|
||||||
|
return \add_query_arg(
|
||||||
|
array(
|
||||||
|
'c' => $comment->comment_ID,
|
||||||
|
),
|
||||||
|
\trailingslashit( site_url() )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of Mentions, used in the Comment.
|
||||||
|
*
|
||||||
|
* @see https://docs.joinmastodon.org/spec/activitypub/#Mention
|
||||||
|
*
|
||||||
|
* @return array The list of Mentions.
|
||||||
|
*/
|
||||||
|
protected function get_cc() {
|
||||||
|
$cc = array();
|
||||||
|
|
||||||
|
$mentions = $this->get_mentions();
|
||||||
|
if ( $mentions ) {
|
||||||
|
foreach ( $mentions as $mention => $url ) {
|
||||||
|
$cc[] = $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$comment_query = new WP_Comment_Query(
|
||||||
|
array(
|
||||||
|
'post_id' => $this->object->comment_post_ID,
|
||||||
|
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||||
|
'meta_query' => array(
|
||||||
|
array(
|
||||||
|
'key' => 'source_id',
|
||||||
|
'compare' => 'EXISTS',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $comment_query->comments ) {
|
||||||
|
foreach ( $comment_query->comments as $comment ) {
|
||||||
|
if ( empty( $comment->comment_author_url ) ) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$cc[] = \esc_url( $comment->comment_author_url );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$cc = \array_unique( $cc );
|
||||||
|
|
||||||
|
return $cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of Tags, used in the Comment.
|
||||||
|
*
|
||||||
|
* This includes Hash-Tags and Mentions.
|
||||||
|
*
|
||||||
|
* @return array The list of Tags.
|
||||||
|
*/
|
||||||
|
protected function get_tag() {
|
||||||
|
$tags = array();
|
||||||
|
|
||||||
|
$mentions = $this->get_mentions();
|
||||||
|
if ( $mentions ) {
|
||||||
|
foreach ( $mentions as $mention => $url ) {
|
||||||
|
$tag = array(
|
||||||
|
'type' => 'Mention',
|
||||||
|
'href' => \esc_url( $url ),
|
||||||
|
'name' => \esc_html( $mention ),
|
||||||
|
);
|
||||||
|
$tags[] = $tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return \array_unique( $tags, SORT_REGULAR );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get the @-Mentions from the comment content.
|
||||||
|
*
|
||||||
|
* @return array The list of @-Mentions.
|
||||||
|
*/
|
||||||
|
protected function get_mentions() {
|
||||||
|
return apply_filters( 'activitypub_extract_mentions', array(), $this->object->comment_content, $this->object );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the locale of the post.
|
||||||
|
*
|
||||||
|
* @return string The locale of the post.
|
||||||
|
*/
|
||||||
|
public function get_locale() {
|
||||||
|
$comment_id = $this->object->ID;
|
||||||
|
$lang = \strtolower( \strtok( \get_locale(), '_-' ) );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the locale of the comment.
|
||||||
|
*
|
||||||
|
* @param string $lang The locale of the comment.
|
||||||
|
* @param int $comment_id The comment ID.
|
||||||
|
* @param WP_Post $post The comment object.
|
||||||
|
*
|
||||||
|
* @return string The filtered locale of the comment.
|
||||||
|
*/
|
||||||
|
return apply_filters( 'activitypub_comment_locale', $lang, $comment_id, $this->object );
|
||||||
|
}
|
||||||
|
}
|
61
includes/transformer/class-factory.php
Normal file
61
includes/transformer/class-factory.php
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
<?php
|
||||||
|
namespace Activitypub\Transformer;
|
||||||
|
|
||||||
|
use Activitypub\Transformer\Post;
|
||||||
|
use Activitypub\Transformer\Comment;
|
||||||
|
use Activitypub\Transformer\Attachment;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transformer Factory
|
||||||
|
*/
|
||||||
|
class Factory {
|
||||||
|
public static function get_transformer( $object ) {
|
||||||
|
/**
|
||||||
|
* Filter the transformer for a given object.
|
||||||
|
*
|
||||||
|
* Add your own transformer based on the object class or the object type.
|
||||||
|
*
|
||||||
|
* Example usage:
|
||||||
|
*
|
||||||
|
* // Filter be object class
|
||||||
|
* add_filter( 'activitypub_transformer', function( $transformer, $object, $object_class ) {
|
||||||
|
* if ( $object_class === 'WP_Post' ) {
|
||||||
|
* return new My_Post_Transformer( $object );
|
||||||
|
* }
|
||||||
|
* return $transformer;
|
||||||
|
* }, 10, 3 );
|
||||||
|
*
|
||||||
|
* // Filter be object type
|
||||||
|
* add_filter( 'activitypub_transformer', function( $transformer, $object, $object_class ) {
|
||||||
|
* if ( $object->post_type === 'event' ) {
|
||||||
|
* return new My_Event_Transformer( $object );
|
||||||
|
* }
|
||||||
|
* return $transformer;
|
||||||
|
* }, 10, 3 );
|
||||||
|
*
|
||||||
|
* @param Activitypub\Transformer\Base $transformer The transformer to use.
|
||||||
|
* @param mixed $object The object to transform.
|
||||||
|
* @param string $object_class The class of the object to transform.
|
||||||
|
*
|
||||||
|
* @return mixed The transformer to use.
|
||||||
|
*/
|
||||||
|
$transformer = apply_filters( 'activitypub_transformer', null, $object, get_class( $object ) );
|
||||||
|
|
||||||
|
if ( $transformer ) {
|
||||||
|
return $transformer;
|
||||||
|
}
|
||||||
|
|
||||||
|
// use default transformer
|
||||||
|
switch ( get_class( $object ) ) {
|
||||||
|
case 'WP_Post':
|
||||||
|
if ( 'attachment' === $object->post_type ) {
|
||||||
|
return new Attachment( $object );
|
||||||
|
}
|
||||||
|
return new Post( $object );
|
||||||
|
case 'WP_Comment':
|
||||||
|
return new Comment( $object );
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -2,11 +2,11 @@
|
||||||
namespace Activitypub\Transformer;
|
namespace Activitypub\Transformer;
|
||||||
|
|
||||||
use WP_Post;
|
use WP_Post;
|
||||||
use Activitypub\Collection\Users;
|
|
||||||
use Activitypub\Model\Blog_User;
|
|
||||||
use Activitypub\Activity\Base_Object;
|
|
||||||
use Activitypub\Shortcodes;
|
use Activitypub\Shortcodes;
|
||||||
|
use Activitypub\Model\Blog_User;
|
||||||
use Activitypub\Transformer\Base;
|
use Activitypub\Transformer\Base;
|
||||||
|
use Activitypub\Collection\Users;
|
||||||
|
use Activitypub\Activity\Base_Object;
|
||||||
|
|
||||||
use function Activitypub\esc_hashtag;
|
use function Activitypub\esc_hashtag;
|
||||||
use function Activitypub\is_single_user;
|
use function Activitypub\is_single_user;
|
||||||
|
@ -15,29 +15,358 @@ use function Activitypub\site_supports_blocks;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* WordPress Post Transformer
|
* WordPress Post Transformer
|
||||||
* The Post Transformer is responsible for transforming a WP_Post object into different othe
|
*
|
||||||
|
* The Post Transformer is responsible for transforming a WP_Post object into different other
|
||||||
* Object-Types.
|
* Object-Types.
|
||||||
*
|
*
|
||||||
* Currently supported are:
|
* Currently supported are:
|
||||||
|
*
|
||||||
* - Activitypub\Activity\Base_Object
|
* - Activitypub\Activity\Base_Object
|
||||||
*/
|
*/
|
||||||
class Post extends Base {
|
class Post extends Base {
|
||||||
/**
|
/**
|
||||||
* Getter function for the name of the transformer.
|
* Returns the ID of the WordPress Post.
|
||||||
*
|
*
|
||||||
* @return string name
|
* @return int The ID of the WordPress Post
|
||||||
*/
|
*/
|
||||||
public function get_name() {
|
public function get_wp_user_id() {
|
||||||
return 'activitypub/default';
|
return $this->object->post_author;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Getter function for the display name (label/title) of the transformer.
|
* Change the User-ID of the WordPress Post.
|
||||||
*
|
*
|
||||||
* @return string name
|
* @return int The User-ID of the WordPress Post
|
||||||
*/
|
*/
|
||||||
public function get_label() {
|
public function change_wp_user_id( $user_id ) {
|
||||||
return 'Built-In';
|
$this->object->post_author = $user_id;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Transforms the WP_Post object to an ActivityPub Object
|
||||||
|
*
|
||||||
|
* @see \Activitypub\Activity\Base_Object
|
||||||
|
*
|
||||||
|
* @return \Activitypub\Activity\Base_Object The ActivityPub Object
|
||||||
|
*/
|
||||||
|
public function to_object() {
|
||||||
|
$post = $this->object;
|
||||||
|
$object = parent::to_object();
|
||||||
|
|
||||||
|
$published = \strtotime( $post->post_date_gmt );
|
||||||
|
|
||||||
|
$object->set_published( \gmdate( 'Y-m-d\TH:i:s\Z', $published ) );
|
||||||
|
|
||||||
|
$updated = \strtotime( $post->post_modified_gmt );
|
||||||
|
|
||||||
|
if ( $updated > $published ) {
|
||||||
|
$object->set_updated( \gmdate( 'Y-m-d\TH:i:s\Z', $updated ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$object->set_content_map(
|
||||||
|
array(
|
||||||
|
$this->get_locale() => $this->get_content(),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$path = sprintf( 'users/%d/followers', intval( $post->post_author ) );
|
||||||
|
|
||||||
|
$object->set_to(
|
||||||
|
array(
|
||||||
|
'https://www.w3.org/ns/activitystreams#Public',
|
||||||
|
get_rest_url_by_path( $path ),
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
return $object;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the ID of the Post.
|
||||||
|
*
|
||||||
|
* @return string The Posts ID.
|
||||||
|
*/
|
||||||
|
public function get_id() {
|
||||||
|
return $this->get_url();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the URL of the Post.
|
||||||
|
*
|
||||||
|
* @return string The Posts URL.
|
||||||
|
*/
|
||||||
|
public function get_url() {
|
||||||
|
$post = $this->object;
|
||||||
|
|
||||||
|
if ( 'trash' === get_post_status( $post ) ) {
|
||||||
|
$permalink = \get_post_meta( $post->ID, 'activitypub_canonical_url', true );
|
||||||
|
} else {
|
||||||
|
$permalink = \get_permalink( $post );
|
||||||
|
}
|
||||||
|
|
||||||
|
return \esc_url( $permalink );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the User-URL of the Author of the Post.
|
||||||
|
*
|
||||||
|
* If `single_user` mode is enabled, the URL of the Blog-User is returned.
|
||||||
|
*
|
||||||
|
* @return string The User-URL.
|
||||||
|
*/
|
||||||
|
protected function get_attributed_to() {
|
||||||
|
if ( is_single_user() ) {
|
||||||
|
$user = new Blog_User();
|
||||||
|
return $user->get_url();
|
||||||
|
}
|
||||||
|
|
||||||
|
return Users::get_by_id( $this->object->post_author )->get_url();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generates all Media Attachments for a Post.
|
||||||
|
*
|
||||||
|
* @return array The Attachments.
|
||||||
|
*/
|
||||||
|
protected function get_attachment() {
|
||||||
|
// Once upon a time we only supported images, but we now support audio/video as well.
|
||||||
|
// We maintain the image-centric naming for backwards compatibility.
|
||||||
|
$max_media = intval( \apply_filters( 'activitypub_max_image_attachments', \get_option( 'activitypub_max_image_attachments', ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS ) ) );
|
||||||
|
|
||||||
|
if ( site_supports_blocks() && \has_blocks( $this->object->post_content ) ) {
|
||||||
|
return $this->get_block_attachments( $max_media );
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->get_classic_editor_images( $max_media );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get media attachments from blocks. They will be formatted as ActivityPub attachments, not as WP attachments.
|
||||||
|
*
|
||||||
|
* @param int $max_media The maximum number of attachments to return.
|
||||||
|
*
|
||||||
|
* @return array The attachments.
|
||||||
|
*/
|
||||||
|
protected function get_block_attachments( $max_media ) {
|
||||||
|
// max media can't be negative or zero
|
||||||
|
if ( $max_media <= 0 ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $this->object->ID;
|
||||||
|
|
||||||
|
$media_ids = array();
|
||||||
|
|
||||||
|
// list post thumbnail first if this post has one
|
||||||
|
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
|
||||||
|
$media_ids[] = \get_post_thumbnail_id( $id );
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $max_media > 0 ) {
|
||||||
|
$blocks = \parse_blocks( $this->object->post_content );
|
||||||
|
$media_ids = self::get_media_ids_from_blocks( $blocks, $media_ids, $max_media );
|
||||||
|
}
|
||||||
|
|
||||||
|
return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $media_ids ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get image attachments from the classic editor.
|
||||||
|
* Note that audio/video attachments are only supported in the block editor.
|
||||||
|
*
|
||||||
|
* @param int $max_images The maximum number of images to return.
|
||||||
|
*
|
||||||
|
* @return array The attachments.
|
||||||
|
*/
|
||||||
|
protected function get_classic_editor_images( $max_images ) {
|
||||||
|
// max images can't be negative or zero
|
||||||
|
if ( $max_images <= 0 ) {
|
||||||
|
return array();
|
||||||
|
}
|
||||||
|
|
||||||
|
$id = $this->object->ID;
|
||||||
|
|
||||||
|
$image_ids = array();
|
||||||
|
|
||||||
|
// list post thumbnail first if this post has one
|
||||||
|
if ( \function_exists( 'has_post_thumbnail' ) && \has_post_thumbnail( $id ) ) {
|
||||||
|
$image_ids[] = \get_post_thumbnail_id( $id );
|
||||||
|
--$max_images;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( $max_images > 0 ) {
|
||||||
|
$query = new \WP_Query(
|
||||||
|
array(
|
||||||
|
'post_parent' => $id,
|
||||||
|
'post_status' => 'inherit',
|
||||||
|
'post_type' => 'attachment',
|
||||||
|
'post_mime_type' => 'image',
|
||||||
|
'order' => 'ASC',
|
||||||
|
'orderby' => 'menu_order ID',
|
||||||
|
'posts_per_page' => $max_images,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
foreach ( $query->get_posts() as $attachment ) {
|
||||||
|
if ( ! \in_array( $attachment->ID, $image_ids, true ) ) {
|
||||||
|
$image_ids[] = $attachment->ID;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$image_ids = \array_unique( $image_ids );
|
||||||
|
|
||||||
|
return \array_filter( \array_map( array( self::class, 'wp_attachment_to_activity_attachment' ), $image_ids ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Recursively get media IDs from blocks.
|
||||||
|
* @param array $blocks The blocks to search for media IDs
|
||||||
|
* @param array $media_ids The media IDs to append new IDs to
|
||||||
|
* @param int $max_media The maximum number of media to return.
|
||||||
|
*
|
||||||
|
* @return array The image IDs.
|
||||||
|
*/
|
||||||
|
protected static function get_media_ids_from_blocks( $blocks, $media_ids, $max_media ) {
|
||||||
|
|
||||||
|
foreach ( $blocks as $block ) {
|
||||||
|
// recurse into inner blocks
|
||||||
|
if ( ! empty( $block['innerBlocks'] ) ) {
|
||||||
|
$media_ids = self::get_media_ids_from_blocks( $block['innerBlocks'], $media_ids, $max_media );
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ( $block['blockName'] ) {
|
||||||
|
case 'core/image':
|
||||||
|
case 'core/cover':
|
||||||
|
case 'core/audio':
|
||||||
|
case 'core/video':
|
||||||
|
case 'videopress/video':
|
||||||
|
if ( ! empty( $block['attrs']['id'] ) ) {
|
||||||
|
$media_ids[] = $block['attrs']['id'];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'jetpack/slideshow':
|
||||||
|
case 'jetpack/tiled-gallery':
|
||||||
|
if ( ! empty( $block['attrs']['ids'] ) ) {
|
||||||
|
$media_ids = array_merge( $media_ids, $block['attrs']['ids'] );
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case 'jetpack/image-compare':
|
||||||
|
if ( ! empty( $block['attrs']['beforeImageId'] ) ) {
|
||||||
|
$media_ids[] = $block['attrs']['beforeImageId'];
|
||||||
|
}
|
||||||
|
if ( ! empty( $block['attrs']['afterImageId'] ) ) {
|
||||||
|
$media_ids[] = $block['attrs']['afterImageId'];
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// depupe
|
||||||
|
$media_ids = \array_unique( $media_ids );
|
||||||
|
|
||||||
|
// stop doing unneeded work
|
||||||
|
if ( count( $media_ids ) >= $max_media ) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// still need to slice it because one gallery could knock us over the limit
|
||||||
|
return array_slice( $media_ids, 0, $max_media );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a WordPress Attachment to an ActivityPub Attachment.
|
||||||
|
*
|
||||||
|
* @param int $id The Attachment ID.
|
||||||
|
*
|
||||||
|
* @return array The ActivityPub Attachment.
|
||||||
|
*/
|
||||||
|
public static function wp_attachment_to_activity_attachment( $id ) {
|
||||||
|
$attachment = array();
|
||||||
|
$mime_type = \get_post_mime_type( $id );
|
||||||
|
$mime_type_parts = \explode( '/', $mime_type );
|
||||||
|
// switching on image/audio/video
|
||||||
|
switch ( $mime_type_parts[0] ) {
|
||||||
|
case 'image':
|
||||||
|
$image_size = 'full';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the image URL returned for each post.
|
||||||
|
*
|
||||||
|
* @param array|false $thumbnail The image URL, or false if no image is available.
|
||||||
|
* @param int $id The attachment ID.
|
||||||
|
* @param string $image_size The image size to retrieve. Set to 'full' by default.
|
||||||
|
*/
|
||||||
|
$thumbnail = apply_filters(
|
||||||
|
'activitypub_get_image',
|
||||||
|
self::get_wordpress_attachment( $id, $image_size ),
|
||||||
|
$id,
|
||||||
|
$image_size
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $thumbnail ) {
|
||||||
|
$alt = \get_post_meta( $id, '_wp_attachment_image_alt', true );
|
||||||
|
$image = array(
|
||||||
|
'type' => 'Image',
|
||||||
|
'url' => $thumbnail[0],
|
||||||
|
'mediaType' => $mime_type,
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( $alt ) {
|
||||||
|
$image['name'] = $alt;
|
||||||
|
}
|
||||||
|
$attachment = $image;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case 'audio':
|
||||||
|
case 'video':
|
||||||
|
$attachment = array(
|
||||||
|
'type' => 'Document',
|
||||||
|
'mediaType' => $mime_type,
|
||||||
|
'url' => \wp_get_attachment_url( $id ),
|
||||||
|
'name' => \get_the_title( $id ),
|
||||||
|
);
|
||||||
|
$meta = wp_get_attachment_metadata( $id );
|
||||||
|
// height and width for videos
|
||||||
|
if ( isset( $meta['width'] ) && isset( $meta['height'] ) ) {
|
||||||
|
$attachment['width'] = $meta['width'];
|
||||||
|
$attachment['height'] = $meta['height'];
|
||||||
|
}
|
||||||
|
// @todo: add `icon` support for audio/video attachments. Maybe use post thumbnail?
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return \apply_filters( 'activitypub_attachment', $attachment, $id );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return details about an image attachment.
|
||||||
|
*
|
||||||
|
* @param int $id The attachment ID.
|
||||||
|
* @param string $image_size The image size to retrieve. Set to 'full' by default.
|
||||||
|
*
|
||||||
|
* @return array|false Array of image data, or boolean false if no image is available.
|
||||||
|
*/
|
||||||
|
protected static function get_wordpress_attachment( $id, $image_size = 'full' ) {
|
||||||
|
/**
|
||||||
|
* Hook into the image retrieval process. Before image retrieval.
|
||||||
|
*
|
||||||
|
* @param int $id The attachment ID.
|
||||||
|
* @param string $image_size The image size to retrieve. Set to 'full' by default.
|
||||||
|
*/
|
||||||
|
do_action( 'activitypub_get_image_pre', $id, $image_size );
|
||||||
|
|
||||||
|
$image = \wp_get_attachment_image_src( $id, $image_size );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook into the image retrieval process. After image retrieval.
|
||||||
|
*
|
||||||
|
* @param int $id The attachment ID.
|
||||||
|
* @param string $image_size The image size to retrieve. Set to 'full' by default.
|
||||||
|
*/
|
||||||
|
do_action( 'activitypub_get_image_post', $id, $image_size );
|
||||||
|
|
||||||
|
return $image;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -48,17 +377,17 @@ class Post extends Base {
|
||||||
*
|
*
|
||||||
* @return string The Object-Type.
|
* @return string The Object-Type.
|
||||||
*/
|
*/
|
||||||
protected function get_object_type() {
|
protected function get_type() {
|
||||||
if ( 'wordpress-post-format' !== \get_option( 'activitypub_object_type', 'note' ) ) {
|
if ( 'wordpress-post-format' !== \get_option( 'activitypub_object_type', 'note' ) ) {
|
||||||
return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) );
|
return \ucfirst( \get_option( 'activitypub_object_type', 'note' ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
// Default to Article.
|
// Default to Article.
|
||||||
$object_type = 'Article';
|
$object_type = 'Article';
|
||||||
$post_type = \get_post_type( $this->wp_post );
|
$post_type = \get_post_type( $this->object );
|
||||||
switch ( $post_type ) {
|
switch ( $post_type ) {
|
||||||
case 'post':
|
case 'post':
|
||||||
$post_format = \get_post_format( $this->wp_post );
|
$post_format = \get_post_format( $this->object );
|
||||||
switch ( $post_format ) {
|
switch ( $post_format ) {
|
||||||
case 'aside':
|
case 'aside':
|
||||||
case 'status':
|
case 'status':
|
||||||
|
@ -106,4 +435,159 @@ class Post extends Base {
|
||||||
|
|
||||||
return $object_type;
|
return $object_type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of Mentions, used in the Post.
|
||||||
|
*
|
||||||
|
* @see https://docs.joinmastodon.org/spec/activitypub/#Mention
|
||||||
|
*
|
||||||
|
* @return array The list of Mentions.
|
||||||
|
*/
|
||||||
|
protected function get_cc() {
|
||||||
|
$cc = array();
|
||||||
|
|
||||||
|
$mentions = $this->get_mentions();
|
||||||
|
if ( $mentions ) {
|
||||||
|
foreach ( $mentions as $url ) {
|
||||||
|
$cc[] = $url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $cc;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of Tags, used in the Post.
|
||||||
|
*
|
||||||
|
* This includes Hash-Tags and Mentions.
|
||||||
|
*
|
||||||
|
* @return array The list of Tags.
|
||||||
|
*/
|
||||||
|
protected function get_tag() {
|
||||||
|
$tags = array();
|
||||||
|
|
||||||
|
$post_tags = \get_the_tags( $this->object->ID );
|
||||||
|
if ( $post_tags ) {
|
||||||
|
foreach ( $post_tags as $post_tag ) {
|
||||||
|
$tag = array(
|
||||||
|
'type' => 'Hashtag',
|
||||||
|
'href' => \esc_url( \get_tag_link( $post_tag->term_id ) ),
|
||||||
|
'name' => esc_hashtag( $post_tag->name ),
|
||||||
|
);
|
||||||
|
$tags[] = $tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$mentions = $this->get_mentions();
|
||||||
|
if ( $mentions ) {
|
||||||
|
foreach ( $mentions as $mention => $url ) {
|
||||||
|
$tag = array(
|
||||||
|
'type' => 'Mention',
|
||||||
|
'href' => \esc_url( $url ),
|
||||||
|
'name' => \esc_html( $mention ),
|
||||||
|
);
|
||||||
|
$tags[] = $tag;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the content for the ActivityPub Item.
|
||||||
|
*
|
||||||
|
* The content will be generated based on the user settings.
|
||||||
|
*
|
||||||
|
* @return string The content.
|
||||||
|
*/
|
||||||
|
protected function get_content() {
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides an action hook so plugins can add their own hooks/filters before AP content is generated.
|
||||||
|
*
|
||||||
|
* Example: if a plugin adds a filter to `the_content` to add a button to the end of posts, it can also remove that filter here.
|
||||||
|
*
|
||||||
|
* @param WP_Post $post The post object.
|
||||||
|
*/
|
||||||
|
do_action( 'activitypub_before_get_content', $post );
|
||||||
|
|
||||||
|
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||||
|
$post = $this->object;
|
||||||
|
$content = $this->get_post_content_template();
|
||||||
|
|
||||||
|
// Register our shortcodes just in time.
|
||||||
|
Shortcodes::register();
|
||||||
|
// Fill in the shortcodes.
|
||||||
|
setup_postdata( $post );
|
||||||
|
$content = do_shortcode( $content );
|
||||||
|
wp_reset_postdata();
|
||||||
|
|
||||||
|
$content = \wpautop( $content );
|
||||||
|
$content = \preg_replace( '/[\n\r\t]/', '', $content );
|
||||||
|
$content = \trim( $content );
|
||||||
|
|
||||||
|
$content = \apply_filters( 'activitypub_the_content', $content, $post );
|
||||||
|
|
||||||
|
// Don't need these any more, should never appear in a post.
|
||||||
|
Shortcodes::unregister();
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the template to use to generate the content of the activitypub item.
|
||||||
|
*
|
||||||
|
* @return string The Template.
|
||||||
|
*/
|
||||||
|
protected function get_post_content_template() {
|
||||||
|
$type = \get_option( 'activitypub_post_content_type', 'content' );
|
||||||
|
|
||||||
|
switch ( $type ) {
|
||||||
|
case 'excerpt':
|
||||||
|
$template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
|
||||||
|
break;
|
||||||
|
case 'title':
|
||||||
|
$template = "[ap_title]\n\n[ap_permalink type=\"html\"]";
|
||||||
|
break;
|
||||||
|
case 'content':
|
||||||
|
$template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
$template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return apply_filters( 'activitypub_object_content_template', $template, $this->object );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to get the @-Mentions from the post content.
|
||||||
|
*
|
||||||
|
* @return array The list of @-Mentions.
|
||||||
|
*/
|
||||||
|
protected function get_mentions() {
|
||||||
|
return apply_filters( 'activitypub_extract_mentions', array(), $this->object->post_content, $this->object );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the locale of the post.
|
||||||
|
*
|
||||||
|
* @return string The locale of the post.
|
||||||
|
*/
|
||||||
|
public function get_locale() {
|
||||||
|
$post_id = $this->object->ID;
|
||||||
|
$lang = \strtolower( \strtok( \get_locale(), '_-' ) );
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filter the locale of the post.
|
||||||
|
*
|
||||||
|
* @param string $lang The locale of the post.
|
||||||
|
* @param int $post_id The post ID.
|
||||||
|
* @param WP_Post $post The post object.
|
||||||
|
*
|
||||||
|
* @return string The filtered locale of the post.
|
||||||
|
*/
|
||||||
|
return apply_filters( 'activitypub_post_locale', $lang, $post_id, $this->object );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,292 +0,0 @@
|
||||||
<?php
|
|
||||||
/**
|
|
||||||
* Inspired by the way elementor handles addons.
|
|
||||||
*
|
|
||||||
* @link https://github.com/elementor/elementor/
|
|
||||||
* @package Activitypub
|
|
||||||
*/
|
|
||||||
|
|
||||||
namespace Activitypub\Transformer;
|
|
||||||
|
|
||||||
use WP_Post;
|
|
||||||
use WP_Comment;
|
|
||||||
|
|
||||||
use function Activitypub\camel_to_snake_case;
|
|
||||||
use function Activitypub\snake_to_camel_case;
|
|
||||||
|
|
||||||
if ( ! defined( 'ABSPATH' ) ) {
|
|
||||||
exit; // Exit if accessed directly.
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* ActivityPub transformers manager.
|
|
||||||
*
|
|
||||||
* ActivityPub transformers manager handler class is responsible for registering and
|
|
||||||
* initializing all the supported WP-Pobject to ActivityPub transformers.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
*/
|
|
||||||
class Transformers_Manager {
|
|
||||||
const DEFAULT_TRANSFORMER_MAPPING = array(
|
|
||||||
'post' => ACTIVITYPUB_DEFAULT_TRANSFORMER,
|
|
||||||
'page' => ACTIVITYPUB_DEFAULT_TRANSFORMER,
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transformers.
|
|
||||||
*
|
|
||||||
* Holds the list of all the ActivityPub transformers. Default is `null`.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @access private
|
|
||||||
*
|
|
||||||
* @var \ActivityPub\Transformer\Base[]
|
|
||||||
*/
|
|
||||||
private $transformers = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transformer_Manager instance.
|
|
||||||
*
|
|
||||||
* Holds the transformer instance.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @access protected
|
|
||||||
*
|
|
||||||
* @var Transformer_Manager
|
|
||||||
*/
|
|
||||||
protected static $_instances = [];
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Instance.
|
|
||||||
*
|
|
||||||
* Ensures only one instance of the transformer manager class is loaded or can be loaded.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @access public
|
|
||||||
* @static
|
|
||||||
*
|
|
||||||
* @return Transformer_Manager An instance of the class.
|
|
||||||
*/
|
|
||||||
public static function instance() {
|
|
||||||
$class_name = static::class_name();
|
|
||||||
|
|
||||||
if ( empty( static::$_instances[ $class_name ] ) ) {
|
|
||||||
static::$_instances[ $class_name ] = new static();
|
|
||||||
}
|
|
||||||
|
|
||||||
return static::$_instances[ $class_name ];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Class name.
|
|
||||||
*
|
|
||||||
* Retrieve the name of the class.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @access public
|
|
||||||
* @static
|
|
||||||
*/
|
|
||||||
public static function class_name() {
|
|
||||||
return get_called_class();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Transformers manager constructor.
|
|
||||||
*
|
|
||||||
* Initializing ActivityPub transformers manager.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @access public
|
|
||||||
*/
|
|
||||||
public function __construct() {
|
|
||||||
$this->require_files();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Require files.
|
|
||||||
*
|
|
||||||
* Require ActivityPub transformer base class.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
private function require_files() {
|
|
||||||
require ACTIVITYPUB_PLUGIN_DIR . 'includes/transformer/class-base.php';
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if a transformer is registered.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
*
|
|
||||||
* @param string $name Transformer name including namespace.
|
|
||||||
* @return bool True if the block type is registered, false otherwise.
|
|
||||||
*/
|
|
||||||
public function is_registered( $name ) {
|
|
||||||
return isset( $this->transformers[ $name ] );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Register a transformer.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @access public
|
|
||||||
*
|
|
||||||
* @param \ActivityPub\Transformer\Base $transformer_instance ActivityPub Transformer.
|
|
||||||
*
|
|
||||||
* @return bool True if the ActivityPub transformer was registered.
|
|
||||||
*/
|
|
||||||
public function register( \ActivityPub\Transformer\Base $transformer_instance ) {
|
|
||||||
|
|
||||||
if ( ! $transformer_instance instanceof \ActivityPub\Transformer\Base ) {
|
|
||||||
_doing_it_wrong(
|
|
||||||
__METHOD__,
|
|
||||||
\esc_html__( 'ActivityPub transformer instance must be a of \ActivityPub\Transformer_Base class.' ),
|
|
||||||
'version_number_transformer_management_placeholder'
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$transformer_name = $transformer_instance->get_name();
|
|
||||||
if ( preg_match( '/[A-Z]+/', $transformer_name ) ) {
|
|
||||||
_doing_it_wrong(
|
|
||||||
__METHOD__,
|
|
||||||
\esc_html__( 'ActivityPub transformer names must not contain uppercase characters.' ),
|
|
||||||
'version_number_transformer_management_placeholder'
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
$name_matcher = '/^[a-z0-9-]+\/[a-z0-9-]+$/';
|
|
||||||
if ( ! preg_match( $name_matcher, $transformer_name ) ) {
|
|
||||||
_doing_it_wrong(
|
|
||||||
__METHOD__,
|
|
||||||
\esc_html__( 'ActivityPub transformer names must contain a namespace prefix. Example: my-plugin/my-custom-transformer' ),
|
|
||||||
'version_number_transformer_management_placeholder'
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( $this->is_registered( $transformer_name ) ) {
|
|
||||||
_doing_it_wrong(
|
|
||||||
__METHOD__,
|
|
||||||
/* translators: %s: Block name. */
|
|
||||||
sprintf( 'ActivityPub transformer with name "%s" is already registered.', esc_html( $transformer_name ) ),
|
|
||||||
'version_number_transformer_management_placeholder'
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Should the ActivityPub transformer be registered.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
*
|
|
||||||
* @param bool $should_register Should the ActivityPub transformer be registered. Default is `true`.
|
|
||||||
* @param \ActivityPub\Transformer\Base $transformer_instance Widget instance.
|
|
||||||
*/
|
|
||||||
// TODO: does not implementing this slow down the website? -> compare with gutenberg block registration.
|
|
||||||
// $should_register = apply_filters( 'activitypub/transformers/is_transformer_enabled', true, $transformer_instance );
|
|
||||||
|
|
||||||
// if ( ! $should_register ) {
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
|
|
||||||
$this->transformers[ $transformer_name ] = $transformer_instance;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Init transformers.
|
|
||||||
*
|
|
||||||
* Initialize ActivityPub transformer manager.
|
|
||||||
* Include the builtin transformers by default and add third party ones.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @access private
|
|
||||||
*/
|
|
||||||
private function init_transformers() {
|
|
||||||
$builtin_transformers = [
|
|
||||||
'post',
|
|
||||||
];
|
|
||||||
|
|
||||||
$this->transformers = [];
|
|
||||||
|
|
||||||
foreach ( $builtin_transformers as $transformer_name ) {
|
|
||||||
include ACTIVITYPUB_PLUGIN_DIR . 'includes/transformer/class-' . $transformer_name . '.php';
|
|
||||||
|
|
||||||
$class_name = ucfirst( $transformer_name );
|
|
||||||
|
|
||||||
$class_name = '\Activitypub\Transformer\\' . $class_name;
|
|
||||||
|
|
||||||
$this->register( new $class_name() );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Let other transformers register.
|
|
||||||
*
|
|
||||||
* Fires after the built-in Activitypub transformers are registered.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
*
|
|
||||||
* @param Transformers_Manager $this The widgets manager.
|
|
||||||
*/
|
|
||||||
do_action( 'activitypub_transformers_register', $this );
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get available ActivityPub transformers.
|
|
||||||
*
|
|
||||||
* Retrieve the registered transformers list. If given a transformer name
|
|
||||||
* it returns the given transformer if it is registered.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @access public
|
|
||||||
*
|
|
||||||
* @param string $transformer_name Optional. Transformer name. Default is null.
|
|
||||||
*
|
|
||||||
* @return Base|Base[]|null Registered transformers.
|
|
||||||
*/
|
|
||||||
public function get_transformers( $transformer_name = null ) {
|
|
||||||
if ( is_null( $this->transformers ) ) {
|
|
||||||
$this->init_transformers();
|
|
||||||
}
|
|
||||||
|
|
||||||
if ( null !== $transformer_name ) {
|
|
||||||
return isset( $this->transformers[ $transformer_name ] ) ? $this->transformers[ $transformer_name ] : null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $this->transformers;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the mapped ActivityPub transformer.
|
|
||||||
*
|
|
||||||
* Returns a new instance of the needed WordPress to ActivityPub transformer.
|
|
||||||
*
|
|
||||||
* @since version_number_transformer_management_placeholder
|
|
||||||
* @access public
|
|
||||||
*
|
|
||||||
* @param WP_Post|WP_Comment $object The WordPress Post/Comment.
|
|
||||||
*
|
|
||||||
* @return \ActivityPub\Transformer\Base|null Registered transformers.
|
|
||||||
*/
|
|
||||||
public function get_transformer( $object ) {
|
|
||||||
switch ( get_class( $object ) ) {
|
|
||||||
case 'WP_Post':
|
|
||||||
$post_type = get_post_type( $object );
|
|
||||||
$transformer_mapping = \get_option( 'activitypub_transformer_mapping', self::DEFAULT_TRANSFORMER_MAPPING );
|
|
||||||
$transformer_name = $transformer_mapping[ $post_type ];
|
|
||||||
$transformer_class = $this->get_transformers( $transformer_name );
|
|
||||||
$transformer_instance = new $transformer_class();
|
|
||||||
$transformer_instance->set_wp_post( $object );
|
|
||||||
return $transformer_instance;
|
|
||||||
case 'WP_Comment':
|
|
||||||
return new Comment( $object );
|
|
||||||
default:
|
|
||||||
return apply_filters( 'activitypub_transformer', null, $object, get_class( $object ) );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -3,6 +3,7 @@ namespace Activitypub\Integration;
|
||||||
|
|
||||||
use function Activitypub\get_total_users;
|
use function Activitypub\get_total_users;
|
||||||
use function Activitypub\get_active_users;
|
use function Activitypub\get_active_users;
|
||||||
|
use function Activitypub\get_rest_url_by_path;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Compatibility with the NodeInfo plugin
|
* Compatibility with the NodeInfo plugin
|
||||||
|
@ -14,8 +15,10 @@ class Nodeinfo {
|
||||||
* Initialize the class, registering WordPress hooks
|
* Initialize the class, registering WordPress hooks
|
||||||
*/
|
*/
|
||||||
public static function init() {
|
public static function init() {
|
||||||
\add_filter( 'nodeinfo_data', array( self::class, 'add_nodeinfo_discovery' ), 10, 2 );
|
\add_filter( 'nodeinfo_data', array( self::class, 'add_nodeinfo_data' ), 10, 2 );
|
||||||
\add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_discovery' ), 10 );
|
\add_filter( 'nodeinfo2_data', array( self::class, 'add_nodeinfo2_data' ), 10 );
|
||||||
|
|
||||||
|
\add_filter( 'wellknown_nodeinfo_data', array( self::class, 'add_wellknown_nodeinfo_data' ), 10, 2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -26,7 +29,7 @@ class Nodeinfo {
|
||||||
*
|
*
|
||||||
* @return array The extended array
|
* @return array The extended array
|
||||||
*/
|
*/
|
||||||
public static function add_nodeinfo_discovery( $nodeinfo, $version ) {
|
public static function add_nodeinfo_data( $nodeinfo, $version ) {
|
||||||
if ( $version >= '2.0' ) {
|
if ( $version >= '2.0' ) {
|
||||||
$nodeinfo['protocols'][] = 'activitypub';
|
$nodeinfo['protocols'][] = 'activitypub';
|
||||||
} else {
|
} else {
|
||||||
|
@ -50,7 +53,7 @@ class Nodeinfo {
|
||||||
*
|
*
|
||||||
* @return array The extended array
|
* @return array The extended array
|
||||||
*/
|
*/
|
||||||
public static function add_nodeinfo2_discovery( $nodeinfo ) {
|
public static function add_nodeinfo2_data( $nodeinfo ) {
|
||||||
$nodeinfo['protocols'][] = 'activitypub';
|
$nodeinfo['protocols'][] = 'activitypub';
|
||||||
|
|
||||||
$nodeinfo['usage']['users'] = array(
|
$nodeinfo['usage']['users'] = array(
|
||||||
|
@ -61,4 +64,20 @@ class Nodeinfo {
|
||||||
|
|
||||||
return $nodeinfo;
|
return $nodeinfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extend the well-known nodeinfo data
|
||||||
|
*
|
||||||
|
* @param array $data The well-known nodeinfo data
|
||||||
|
*
|
||||||
|
* @return array The extended array
|
||||||
|
*/
|
||||||
|
public static function add_wellknown_nodeinfo_data( $data ) {
|
||||||
|
$data['links'][] = array(
|
||||||
|
'rel' => 'https://www.w3.org/ns/activitystreams#Application',
|
||||||
|
'href' => get_rest_url_by_path( 'application' ),
|
||||||
|
);
|
||||||
|
|
||||||
|
return $data;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,8 +14,8 @@ class Webfinger {
|
||||||
* Initialize the class, registering WordPress hooks
|
* Initialize the class, registering WordPress hooks
|
||||||
*/
|
*/
|
||||||
public static function init() {
|
public static function init() {
|
||||||
\add_filter( 'webfinger_user_data', array( self::class, 'add_user_discovery' ), 10, 3 );
|
\add_filter( 'webfinger_user_data', array( self::class, 'add_user_discovery' ), 1, 3 );
|
||||||
\add_filter( 'webfinger_data', array( self::class, 'add_pseudo_user_discovery' ), 99, 2 );
|
\add_filter( 'webfinger_data', array( self::class, 'add_pseudo_user_discovery' ), 1, 2 );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -34,6 +34,11 @@ class Webfinger {
|
||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$array['subject'] = sprintf( 'acct:%s', $user->get_resource() );
|
||||||
|
|
||||||
|
$array['aliases'][] = $user->get_url();
|
||||||
|
$array['aliases'][] = $user->get_alternate_url();
|
||||||
|
|
||||||
$array['links'][] = array(
|
$array['links'][] = array(
|
||||||
'rel' => 'self',
|
'rel' => 'self',
|
||||||
'type' => 'application/activity+json',
|
'type' => 'application/activity+json',
|
||||||
|
@ -53,10 +58,12 @@ class Webfinger {
|
||||||
* @return array the jrd array
|
* @return array the jrd array
|
||||||
*/
|
*/
|
||||||
public static function add_pseudo_user_discovery( $array, $resource ) {
|
public static function add_pseudo_user_discovery( $array, $resource ) {
|
||||||
if ( $array ) {
|
$user = Webfinger_Rest::get_profile( $resource );
|
||||||
|
|
||||||
|
if ( ! $user || is_wp_error( $user ) ) {
|
||||||
return $array;
|
return $array;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Webfinger_Rest::get_profile( $resource );
|
return $user;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
27
readme.txt
27
readme.txt
|
@ -1,9 +1,9 @@
|
||||||
=== ActivityPub ===
|
=== ActivityPub ===
|
||||||
Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nuriapena, cavalierlife
|
Contributors: automattic, pfefferle, mediaformat, mattwiebe, akirk, jeherve, nuriapena, cavalierlife
|
||||||
Tags: OStatus, fediverse, activitypub, activitystream
|
Tags: OStatus, fediverse, activitypub, activitystream
|
||||||
Requires at least: 4.7
|
Requires at least: 5.5
|
||||||
Tested up to: 6.4
|
Tested up to: 6.4
|
||||||
Stable tag: 1.2.0
|
Stable tag: 1.3.0
|
||||||
Requires PHP: 5.6
|
Requires PHP: 5.6
|
||||||
License: MIT
|
License: MIT
|
||||||
License URI: http://opensource.org/licenses/MIT
|
License URI: http://opensource.org/licenses/MIT
|
||||||
|
@ -68,10 +68,10 @@ Implemented:
|
||||||
* share posts
|
* share posts
|
||||||
* receive comments/reactions
|
* receive comments/reactions
|
||||||
* signature verification
|
* signature verification
|
||||||
|
* threaded comments support
|
||||||
|
|
||||||
To implement:
|
To implement:
|
||||||
|
|
||||||
* threaded comments support
|
|
||||||
* replace shortcodes with blocks for layout
|
* replace shortcodes with blocks for layout
|
||||||
|
|
||||||
= What is "ActivityPub for WordPress" =
|
= What is "ActivityPub for WordPress" =
|
||||||
|
@ -105,6 +105,27 @@ Where 'blog' is the path to the subdirectory at which your blog resides.
|
||||||
|
|
||||||
Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub).
|
Project maintained on GitHub at [automattic/wordpress-activitypub](https://github.com/automattic/wordpress-activitypub).
|
||||||
|
|
||||||
|
= 2.0.0 =
|
||||||
|
|
||||||
|
* Removed: Deprecated Classes
|
||||||
|
* Fixed: Normalize attributes that can have mixed value types
|
||||||
|
* Added: URL support for WebFinger
|
||||||
|
* Added: Make Post-Template filterable
|
||||||
|
* Added: CSS class for ActivityPub comments to allow custom designs
|
||||||
|
* Added: FEP-2677: Identifying the Application Actor
|
||||||
|
* Added: Basic Comment Federation
|
||||||
|
* Added: Profile Update Activities
|
||||||
|
* Improved: WebFinger endpoints
|
||||||
|
|
||||||
|
= 1.3.0 =
|
||||||
|
|
||||||
|
* Added: Threaded-Comments support
|
||||||
|
* Improved: alt text for avatars in Follow Me/Followers blocks
|
||||||
|
* Improved: `Delete`, `Update` and `Follow` Activities
|
||||||
|
* Improved: better/more effective handling of `Delete` Activities
|
||||||
|
* Improved: allow `<p />` and `<br />` for Comments
|
||||||
|
* Fixed: removed default limit of WP_Query to send updates to all Inboxes and not only to the first 10
|
||||||
|
|
||||||
= 1.2.0 =
|
= 1.2.0 =
|
||||||
|
|
||||||
* Add: Search and order followerer lists
|
* Add: Search and order followerer lists
|
||||||
|
|
|
@ -37,7 +37,7 @@ function Profile( { profile, popupStyles, userId } ) {
|
||||||
const { avatar, name, resource } = profile;
|
const { avatar, name, resource } = profile;
|
||||||
return (
|
return (
|
||||||
<div className="activitypub-profile">
|
<div className="activitypub-profile">
|
||||||
<img className="activitypub-profile__avatar" src={ avatar } />
|
<img className="activitypub-profile__avatar" src={ avatar } alt={ name } />
|
||||||
<div className="activitypub-profile__content">
|
<div className="activitypub-profile__content">
|
||||||
<div className="activitypub-profile__name">{ name }</div>
|
<div className="activitypub-profile__name">{ name }</div>
|
||||||
<div className="activitypub-profile__handle" title={ resource }>{ resource }</div>
|
<div className="activitypub-profile__handle" title={ resource }>{ resource }</div>
|
||||||
|
|
|
@ -106,7 +106,13 @@ function Follower( { name, icon, url, preferredUsername, followLinks = true } )
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<ExternalLink className="activitypub-link" href={ url } title={ handle } { ...extraProps }>
|
<ExternalLink className="activitypub-link" href={ url } title={ handle } { ...extraProps }>
|
||||||
<img width="40" height="40" src={ icon.url } class="avatar activitypub-avatar" />
|
<img
|
||||||
|
width="40"
|
||||||
|
height="40"
|
||||||
|
src={ icon.url }
|
||||||
|
class="avatar activitypub-avatar"
|
||||||
|
alt={ name }
|
||||||
|
/>
|
||||||
<span class="activitypub-actor">
|
<span class="activitypub-actor">
|
||||||
<strong className="activitypub-name">{ name }</strong>
|
<strong className="activitypub-name">{ name }</strong>
|
||||||
<span class="sep">/</span>
|
<span class="sep">/</span>
|
||||||
|
|
57
templates/admin-followers-list.php
Normal file
57
templates/admin-followers-list.php
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
use Activitypub\Collection\Users;
|
||||||
|
|
||||||
|
\load_template(
|
||||||
|
__DIR__ . '/admin-header.php',
|
||||||
|
true,
|
||||||
|
array(
|
||||||
|
'settings' => '',
|
||||||
|
'welcome' => '',
|
||||||
|
'followers' => 'active',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Draw the follow table for the blog user if it is activated.
|
||||||
|
if ( ! \Activitypub\is_user_disabled( \Activitypub\Collection\Users::BLOG_USER_ID ) ) :
|
||||||
|
$table = new \Activitypub\Table\Followers();
|
||||||
|
$follower_count = $table->get_user_count();
|
||||||
|
// translators: The follower count.
|
||||||
|
$followers_template = _n( 'Your blog profile currently has %s follower.', 'Your blog profile currently has %s followers.', $follower_count, 'activitypub' );
|
||||||
|
?>
|
||||||
|
<div class="wrap activitypub-followers-page">
|
||||||
|
<p><?php \printf( \esc_html( $followers_template ), \esc_attr( $follower_count ) ); ?></p>
|
||||||
|
|
||||||
|
<form method="get">
|
||||||
|
<input type="hidden" name="page" value="activitypub" />
|
||||||
|
<input type="hidden" name="tab" value="followers" />
|
||||||
|
<?php
|
||||||
|
$table->prepare_items();
|
||||||
|
$table->search_box( 'Search', 'search' );
|
||||||
|
$table->display();
|
||||||
|
?>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php endif;
|
||||||
|
|
||||||
|
// Draw the the follow table for the application user with reject and accept options.
|
||||||
|
$table = new \Activitypub\Table\Follow_Requests( Users::APPLICATION_USER_ID );
|
||||||
|
$table->prepare_items();
|
||||||
|
$follow_requests_count = $table->follow_requests_count;
|
||||||
|
// translators: The follower count.
|
||||||
|
$followers_template = _n( 'Your WordPress site currently has %s follow request.', 'Your WordPress site currently has %s follow requests.', $follow_requests_count, 'activitypub' );
|
||||||
|
?>
|
||||||
|
<div class="wrap activitypub-followers-page">
|
||||||
|
<p><?php \printf( \esc_html( $followers_template ), \esc_attr( $follow_requests_count ) ); ?></p>
|
||||||
|
|
||||||
|
<form method="get">
|
||||||
|
<input type="hidden" name="page" value="activitypub" />
|
||||||
|
<input type="hidden" name="tab" value="followers" />
|
||||||
|
<?php
|
||||||
|
$table->search_box( 'Search', 'search' );
|
||||||
|
$table->display();
|
||||||
|
|
||||||
|
?>
|
||||||
|
</form>
|
||||||
|
</div>
|
|
@ -15,13 +15,10 @@
|
||||||
<?php \esc_html_e( 'Settings', 'activitypub' ); ?>
|
<?php \esc_html_e( 'Settings', 'activitypub' ); ?>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<?php if ( ! \Activitypub\is_user_disabled( \Activitypub\Collection\Users::BLOG_USER_ID ) ) : ?>
|
|
||||||
|
|
||||||
<a href="<?php echo \esc_url_raw( admin_url( 'options-general.php?page=activitypub&tab=followers' ) ); ?>" class="activitypub-settings-tab <?php echo \esc_attr( $args['followers'] ); ?>">
|
<a href="<?php echo \esc_url_raw( admin_url( 'options-general.php?page=activitypub&tab=followers' ) ); ?>" class="activitypub-settings-tab <?php echo \esc_attr( $args['followers'] ); ?>">
|
||||||
<?php \esc_html_e( 'Followers', 'activitypub' ); ?>
|
<?php \esc_html_e( 'Followers', 'activitypub' ); ?>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<?php endif; ?>
|
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
<hr class="wp-header-end">
|
<hr class="wp-header-end">
|
||||||
|
|
|
@ -10,23 +10,8 @@ $user->set_context(
|
||||||
*/
|
*/
|
||||||
\do_action( 'activitypub_json_author_pre', $user->get__id() );
|
\do_action( 'activitypub_json_author_pre', $user->get__id() );
|
||||||
|
|
||||||
$options = 0;
|
|
||||||
// JSON_PRETTY_PRINT added in PHP 5.4
|
|
||||||
if ( \get_query_var( 'pretty' ) ) {
|
|
||||||
$options |= \JSON_PRETTY_PRINT; // phpcs:ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
$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_author_options', $options, $user->get__id() );
|
|
||||||
|
|
||||||
\header( 'Content-Type: application/activity+json' );
|
\header( 'Content-Type: application/activity+json' );
|
||||||
echo \wp_json_encode( $user->to_array(), $options );
|
echo $user->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Action triggerd after the ActivityPub profile has been created and sent to the client
|
* Action triggerd after the ActivityPub profile has been created and sent to the client
|
||||||
|
|
|
@ -10,23 +10,8 @@ $user->set_context(
|
||||||
*/
|
*/
|
||||||
\do_action( 'activitypub_json_author_pre', $user->get__id() );
|
\do_action( 'activitypub_json_author_pre', $user->get__id() );
|
||||||
|
|
||||||
$options = 0;
|
|
||||||
// JSON_PRETTY_PRINT added in PHP 5.4
|
|
||||||
if ( \get_query_var( 'pretty' ) ) {
|
|
||||||
$options |= \JSON_PRETTY_PRINT; // phpcs:ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
$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_author_options', $options, $user->get__id() );
|
|
||||||
|
|
||||||
\header( 'Content-Type: application/activity+json' );
|
\header( 'Content-Type: application/activity+json' );
|
||||||
echo \wp_json_encode( $user->to_array(), $options );
|
echo $user->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Action triggerd after the ActivityPub profile has been created and sent to the client
|
* Action triggerd after the ActivityPub profile has been created and sent to the client
|
||||||
|
|
|
@ -1,28 +0,0 @@
|
||||||
<?php
|
|
||||||
\load_template(
|
|
||||||
__DIR__ . '/admin-header.php',
|
|
||||||
true,
|
|
||||||
array(
|
|
||||||
'settings' => '',
|
|
||||||
'welcome' => '',
|
|
||||||
'followers' => 'active',
|
|
||||||
)
|
|
||||||
);
|
|
||||||
$table = new \Activitypub\Table\Followers();
|
|
||||||
$follower_count = $table->get_user_count();
|
|
||||||
// translators: The follower count.
|
|
||||||
$followers_template = _n( 'Your blog profile currently has %s follower.', 'Your blog profile currently has %s followers.', $follower_count, 'activitypub' );
|
|
||||||
?>
|
|
||||||
<div class="wrap activitypub-followers-page">
|
|
||||||
<p><?php \printf( \esc_html( $followers_template ), \esc_attr( $follower_count ) ); ?></p>
|
|
||||||
|
|
||||||
<form method="get">
|
|
||||||
<input type="hidden" name="page" value="activitypub" />
|
|
||||||
<input type="hidden" name="tab" value="followers" />
|
|
||||||
<?php
|
|
||||||
$table->prepare_items();
|
|
||||||
$table->search_box( 'Search', 'search' );
|
|
||||||
$table->display();
|
|
||||||
?>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
36
templates/comment-json.php
Normal file
36
templates/comment-json.php
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
<?php
|
||||||
|
$comment = \get_comment( \get_query_var( 'c', null ) ); // phpcs:ignore
|
||||||
|
|
||||||
|
$object = \Activitypub\Transformer\Factory::get_transformer( $comment );
|
||||||
|
$json = \array_merge( array( '@context' => \Activitypub\get_context() ), $object->to_object()->to_array() );
|
||||||
|
|
||||||
|
// filter output
|
||||||
|
$json = \apply_filters( 'activitypub_json_comment_array', $json );
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||||
|
*/
|
||||||
|
\do_action( 'activitypub_json_comment_pre' );
|
||||||
|
|
||||||
|
$options = 0;
|
||||||
|
// JSON_PRETTY_PRINT added in PHP 5.4
|
||||||
|
if ( \get_query_var( 'pretty' ) ) {
|
||||||
|
$options |= \JSON_PRETTY_PRINT; // phpcs:ignore
|
||||||
|
}
|
||||||
|
|
||||||
|
$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_comment_options', $options );
|
||||||
|
|
||||||
|
\header( 'Content-Type: application/activity+json' );
|
||||||
|
echo \wp_json_encode( $json, $options );
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Action triggerd after the ActivityPub profile has been created and sent to the client
|
||||||
|
*/
|
||||||
|
\do_action( 'activitypub_json_comment_comment' );
|
|
@ -2,35 +2,16 @@
|
||||||
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
// phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited
|
||||||
$post = \get_post();
|
$post = \get_post();
|
||||||
|
|
||||||
$transformer = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( $post );
|
$post_object = \Activitypub\Transformer\Factory::get_transformer( $post )->to_object();
|
||||||
|
$post_object->set_context( \Activitypub\get_context() );
|
||||||
$json = \array_merge( array( '@context' => \Activitypub\get_context() ), $transformer->to_object()->to_array() );
|
|
||||||
|
|
||||||
// filter output
|
|
||||||
$json = \apply_filters( 'activitypub_json_post_array', $json );
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
* Action triggerd prior to the ActivityPub profile being created and sent to the client
|
||||||
*/
|
*/
|
||||||
\do_action( 'activitypub_json_post_pre' );
|
\do_action( 'activitypub_json_post_pre' );
|
||||||
|
|
||||||
$options = 0;
|
|
||||||
// JSON_PRETTY_PRINT added in PHP 5.4
|
|
||||||
if ( \get_query_var( 'pretty' ) ) {
|
|
||||||
$options |= \JSON_PRETTY_PRINT; // phpcs:ignore
|
|
||||||
}
|
|
||||||
|
|
||||||
$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_post_options', $options );
|
|
||||||
|
|
||||||
\header( 'Content-Type: application/activity+json' );
|
\header( 'Content-Type: application/activity+json' );
|
||||||
echo \wp_json_encode( $json, $options );
|
echo $post_object->to_json(); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Action triggerd after the ActivityPub profile has been created and sent to the client
|
* Action triggerd after the ActivityPub profile has been created and sent to the client
|
||||||
|
|
|
@ -93,7 +93,7 @@
|
||||||
<?php \esc_html_e( 'Excerpt', 'activitypub' ); ?>
|
<?php \esc_html_e( 'Excerpt', 'activitypub' ); ?>
|
||||||
-
|
-
|
||||||
<span class="description">
|
<span class="description">
|
||||||
<?php \esc_html_e( 'A content summary, shortened to 400 characters and without markup.', 'activitypub' ); ?>
|
<?php \esc_html_e( 'A content summary without markup (truncated if no excerpt is provided).', 'activitypub' ); ?>
|
||||||
</span>
|
</span>
|
||||||
</label>
|
</label>
|
||||||
</p>
|
</p>
|
||||||
|
@ -125,7 +125,7 @@
|
||||||
<ul>
|
<ul>
|
||||||
<li><code>[ap_title]</code> - <?php \esc_html_e( 'The post\'s title.', 'activitypub' ); ?></li>
|
<li><code>[ap_title]</code> - <?php \esc_html_e( 'The post\'s title.', 'activitypub' ); ?></li>
|
||||||
<li><code>[ap_content]</code> - <?php \esc_html_e( 'The post\'s content.', 'activitypub' ); ?></li>
|
<li><code>[ap_content]</code> - <?php \esc_html_e( 'The post\'s content.', 'activitypub' ); ?></li>
|
||||||
<li><code>[ap_excerpt]</code> - <?php \esc_html_e( 'The post\'s excerpt (default 400 chars).', 'activitypub' ); ?></li>
|
<li><code>[ap_excerpt]</code> - <?php \esc_html_e( 'The post\'s excerpt (may be truncated).', 'activitypub' ); ?></li>
|
||||||
<li><code>[ap_permalink]</code> - <?php \esc_html_e( 'The post\'s permalink.', 'activitypub' ); ?></li>
|
<li><code>[ap_permalink]</code> - <?php \esc_html_e( 'The post\'s permalink.', 'activitypub' ); ?></li>
|
||||||
<li><code>[ap_shortlink]</code> - <?php echo \wp_kses( \__( 'The post\'s shortlink. I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>.', 'activitypub' ), 'default' ); ?></li>
|
<li><code>[ap_shortlink]</code> - <?php echo \wp_kses( \__( 'The post\'s shortlink. I can recommend <a href="https://wordpress.org/plugins/hum/" target="_blank">Hum</a>.', 'activitypub' ), 'default' ); ?></li>
|
||||||
<li><code>[ap_hashtags]</code> - <?php \esc_html_e( 'The post\'s tags as hashtags.', 'activitypub' ); ?></li>
|
<li><code>[ap_hashtags]</code> - <?php \esc_html_e( 'The post\'s tags as hashtags.', 'activitypub' ); ?></li>
|
||||||
|
@ -201,6 +201,26 @@
|
||||||
</p>
|
</p>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr>
|
||||||
|
<th scope="row"><?php \esc_html_e( 'Supported post types', 'activitypub' ); ?></th>
|
||||||
|
<td>
|
||||||
|
<fieldset>
|
||||||
|
<?php \esc_html_e( 'Enable ActivityPub support for the following post types:', 'activitypub' ); ?>
|
||||||
|
|
||||||
|
<?php $post_types = \get_post_types( array( 'public' => true ), 'objects' ); ?>
|
||||||
|
<?php $support_post_types = \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post', 'page' ) ) : array(); ?>
|
||||||
|
<ul>
|
||||||
|
<?php // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited ?>
|
||||||
|
<?php foreach ( $post_types as $post_type ) { ?>
|
||||||
|
<li>
|
||||||
|
<input type="checkbox" id="activitypub_support_post_type_<?php echo \esc_attr( $post_type->name ); ?>" name="activitypub_support_post_types[]" value="<?php echo \esc_attr( $post_type->name ); ?>" <?php echo \checked( \in_array( $post_type->name, $support_post_types, true ) ); ?> />
|
||||||
|
<label for="activitypub_support_post_type_<?php echo \esc_attr( $post_type->name ); ?>"><?php echo \esc_html( $post_type->label ); ?></label>
|
||||||
|
</li>
|
||||||
|
<?php } ?>
|
||||||
|
</ul>
|
||||||
|
</fieldset>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="row">
|
<th scope="row">
|
||||||
<?php \esc_html_e( 'Hashtags (beta)', 'activitypub' ); ?>
|
<?php \esc_html_e( 'Hashtags (beta)', 'activitypub' ); ?>
|
||||||
|
@ -217,93 +237,6 @@
|
||||||
<?php \do_settings_fields( 'activitypub', 'activity' ); ?>
|
<?php \do_settings_fields( 'activitypub', 'activity' ); ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- OUR FORK HERE -->
|
|
||||||
<div class="box">
|
|
||||||
<h3><?php \esc_html_e( 'Enable ActivityPub support for post type', 'activitypub' ); ?></h3>
|
|
||||||
|
|
||||||
<table class="form-table">
|
|
||||||
<tbody>
|
|
||||||
<tr>
|
|
||||||
<th scope="row">
|
|
||||||
<?php \esc_html_e( 'Mapping', 'activitypub' ); ?>
|
|
||||||
</th>
|
|
||||||
<td>
|
|
||||||
<?php \esc_html_e( 'Enable ActivityPub support for a certain post type by selecting one of the available ActivityPub transformers.', 'activitypub' ); ?>
|
|
||||||
|
|
||||||
<?php $all_public_post_types = \get_post_types( array( 'public' => true ), 'objects' );
|
|
||||||
$transformer_mapping = \get_option( 'activitypub_transformer_mapping', array( 'default' => 'note' ) );
|
|
||||||
|
|
||||||
$all_public_post_type_names = array_map(function ($object) {
|
|
||||||
return $object->name;
|
|
||||||
}, $all_public_post_types);
|
|
||||||
|
|
||||||
$transformer_manager = \Activitypub\Transformer\Transformers_Manager::instance();
|
|
||||||
$transformers = $transformer_manager->get_transformers();
|
|
||||||
|
|
||||||
?>
|
|
||||||
|
|
||||||
|
|
||||||
<script>
|
|
||||||
// TODO Probably we should use checkboxes and not select and make this less buggy and insert the js at the right place.
|
|
||||||
document.addEventListener('DOMContentLoaded', function () {
|
|
||||||
var radioGroups = {};
|
|
||||||
|
|
||||||
var radioButtons = document.querySelectorAll('input[type="radio"]');
|
|
||||||
|
|
||||||
radioButtons.forEach(function (radioButton) {
|
|
||||||
radioButton.addEventListener('click', function () {
|
|
||||||
var name = this.name;
|
|
||||||
|
|
||||||
if (!radioGroups[name]) {
|
|
||||||
radioGroups[name] = this;
|
|
||||||
} else {
|
|
||||||
radioGroups[name].checked = false;
|
|
||||||
radioGroups[name] = this;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th></th>
|
|
||||||
<?php
|
|
||||||
// Generate column headers based on transformer objects
|
|
||||||
foreach ($transformers as $transformer) {
|
|
||||||
echo '<th>' . htmlspecialchars($transformer->get_label()) . '</th>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
<?php
|
|
||||||
// Generate rows based on post types and transformers
|
|
||||||
foreach ($all_public_post_types as $post_type) {
|
|
||||||
echo '<tr>';
|
|
||||||
echo '<td><strong>' . htmlspecialchars($post_type->label) . '</strong></td>';
|
|
||||||
// Generate radio inputs for each transformer, considering support for the post type
|
|
||||||
foreach ($transformers as $transformer) {
|
|
||||||
$disabled_attribute = $transformer->supports_post_type( $post_type->name ) ? '' : ' disabled';
|
|
||||||
$is_selected = ( is_array( $transformer_mapping ) && isset( $transformer_mapping[ $post_type->name ] ) && $transformer_mapping[ $post_type->name ] === $transformer->get_name() ) ? ' checked ' : '';
|
|
||||||
echo '<td><input type="radio" name="activitypub_transformer_mapping[' . $post_type->name . ']" value="' . $transformer->get_name() . '"' . $is_selected . $disabled_attribute . '></td>';
|
|
||||||
}
|
|
||||||
|
|
||||||
echo '</tr>';
|
|
||||||
}
|
|
||||||
?>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
|
|
||||||
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
<!-- OUR FORK ENDS HERE -->
|
|
||||||
|
|
||||||
<div class="box">
|
<div class="box">
|
||||||
<h3><?php \esc_html_e( 'Server', 'activitypub' ); ?></h3>
|
<h3><?php \esc_html_e( 'Server', 'activitypub' ); ?></h3>
|
||||||
<table class="form-table">
|
<table class="form-table">
|
||||||
|
|
|
@ -1,4 +1,6 @@
|
||||||
<?php
|
<?php
|
||||||
|
use DMS\PHPUnitExtensions\ArraySubset\Assert;
|
||||||
|
|
||||||
class Test_Activitypub_Activity extends WP_UnitTestCase {
|
class Test_Activitypub_Activity extends WP_UnitTestCase {
|
||||||
public function test_activity_mentions() {
|
public function test_activity_mentions() {
|
||||||
$post = \wp_insert_post(
|
$post = \wp_insert_post(
|
||||||
|
@ -17,8 +19,7 @@ class Test_Activitypub_Activity extends WP_UnitTestCase {
|
||||||
10
|
10
|
||||||
);
|
);
|
||||||
|
|
||||||
$wp_post = get_post( $post );
|
$activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object();
|
||||||
$activitypub_post = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( $post )->to_object();
|
|
||||||
|
|
||||||
$activitypub_activity = new \Activitypub\Activity\Activity();
|
$activitypub_activity = new \Activitypub\Activity\Activity();
|
||||||
$activitypub_activity->set_type( 'Create' );
|
$activitypub_activity->set_type( 'Create' );
|
||||||
|
@ -43,4 +44,21 @@ class Test_Activitypub_Activity extends WP_UnitTestCase {
|
||||||
$this->assertEquals( 'Hello world!', $object->get_content() );
|
$this->assertEquals( 'Hello world!', $object->get_content() );
|
||||||
$this->assertEquals( $test_array, $object->to_array() );
|
$this->assertEquals( $test_array, $object->to_array() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_activity_object() {
|
||||||
|
$test_array = array(
|
||||||
|
'id' => 'https://example.com/post/123',
|
||||||
|
'type' => 'Create',
|
||||||
|
'object' => array(
|
||||||
|
'id' => 'https://example.com/post/123/activity',
|
||||||
|
'type' => 'Note',
|
||||||
|
'content' => 'Hello world!',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
$activity = \Activitypub\Activity\Activity::init_from_array( $test_array );
|
||||||
|
|
||||||
|
$this->assertEquals( 'Hello world!', $activity->get_object()->get_content() );
|
||||||
|
Assert::assertArraySubset( $test_array, $activity->to_array() );
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
70
tests/test-class-activitypub-create-handler.php
Normal file
70
tests/test-class-activitypub-create-handler.php
Normal file
|
@ -0,0 +1,70 @@
|
||||||
|
<?php
|
||||||
|
class Test_Activitypub_Create_Handler extends WP_UnitTestCase {
|
||||||
|
public $user_id;
|
||||||
|
public $user_url;
|
||||||
|
public $post_id;
|
||||||
|
public $post_permalink;
|
||||||
|
|
||||||
|
public function set_up() {
|
||||||
|
$this->user_id = 1;
|
||||||
|
$authordata = \get_userdata( $this->user_id );
|
||||||
|
$this->user_url = $authordata->user_url;
|
||||||
|
|
||||||
|
$this->post_id = \wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_author' => $this->user_id,
|
||||||
|
'post_content' => 'test',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->post_permalink = \get_permalink( $this->post_id );
|
||||||
|
|
||||||
|
\add_filter( 'pre_get_remote_metadata_by_actor', array( '\Test_Activitypub_Create_Handler', 'get_remote_metadata_by_actor' ), 0, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_remote_metadata_by_actor( $value, $actor ) {
|
||||||
|
return array(
|
||||||
|
'name' => 'Example User',
|
||||||
|
'icon' => array(
|
||||||
|
'url' => 'https://example.com/icon',
|
||||||
|
),
|
||||||
|
'url' => $actor,
|
||||||
|
'id' => 'http://example.org/users/example',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create_test_object( $id = 'https://example.com/123' ) {
|
||||||
|
return array(
|
||||||
|
'actor' => $this->user_url,
|
||||||
|
'id' => 'https://example.com/id/' . microtime( true ),
|
||||||
|
'to' => [ $this->user_url ],
|
||||||
|
'cc' => [ 'https://www.w3.org/ns/activitystreams#Public' ],
|
||||||
|
'object' => array(
|
||||||
|
'id' => $id,
|
||||||
|
'url' => 'https://example.com/example',
|
||||||
|
'inReplyTo' => $this->post_permalink,
|
||||||
|
'content' => 'example',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_handle_create_object_unset_rejected() {
|
||||||
|
$object = $this->create_test_object();
|
||||||
|
unset( $object['object'] );
|
||||||
|
$converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
|
||||||
|
$this->assertNull( $converted );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_handle_create_non_public_rejected() {
|
||||||
|
$object = $this->create_test_object();
|
||||||
|
$object['cc'] = [];
|
||||||
|
$converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
|
||||||
|
$this->assertNull( $converted );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_handle_create_no_id_rejected() {
|
||||||
|
$object = $this->create_test_object();
|
||||||
|
unset( $object['object']['id'] );
|
||||||
|
$converted = Activitypub\Handler\Create::handle_create( $object, $this->user_id );
|
||||||
|
$this->assertNull( $converted );
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,5 +1,5 @@
|
||||||
<?php
|
<?php
|
||||||
class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
|
class Test_Activitypub_Followers extends WP_UnitTestCase {
|
||||||
public static $users = array(
|
public static $users = array(
|
||||||
'username@example.org' => array(
|
'username@example.org' => array(
|
||||||
'id' => 'https://example.org/users/username',
|
'id' => 'https://example.org/users/username',
|
||||||
|
@ -329,6 +329,82 @@ class Test_Db_Activitypub_Followers extends WP_UnitTestCase {
|
||||||
$this->assertEquals( $name, $follower->get_name() );
|
$this->assertEquals( $name, $follower->get_name() );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_get_inboxes() {
|
||||||
|
for ( $i = 0; $i < 30; $i++ ) {
|
||||||
|
$meta = array(
|
||||||
|
'id' => 'https://example.org/users/' . $i,
|
||||||
|
'url' => 'https://example.org/users/' . $i,
|
||||||
|
'inbox' => 'https://example.org/users/' . $i . '/inbox',
|
||||||
|
'name' => 'user' . $i,
|
||||||
|
'preferredUsername' => 'user' . $i,
|
||||||
|
'publicKey' => 'https://example.org/users/' . $i . '#main-key',
|
||||||
|
'publicKeyPem' => $i,
|
||||||
|
);
|
||||||
|
|
||||||
|
$follower = new \Activitypub\Model\Follower();
|
||||||
|
$follower->from_array( $meta );
|
||||||
|
|
||||||
|
$id = $follower->upsert();
|
||||||
|
|
||||||
|
add_post_meta( $id, 'activitypub_user_id', 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$inboxes = \Activitypub\Collection\Followers::get_inboxes( 1 );
|
||||||
|
|
||||||
|
$this->assertCount( 30, $inboxes );
|
||||||
|
|
||||||
|
wp_cache_delete( sprintf( \Activitypub\Collection\Followers::CACHE_KEY_INBOXES, 1 ), 'activitypub' );
|
||||||
|
|
||||||
|
for ( $j = 0; $j < 5; $j++ ) {
|
||||||
|
$k = $j + 100;
|
||||||
|
$meta = array(
|
||||||
|
'id' => 'https://example.org/users/' . $k,
|
||||||
|
'url' => 'https://example.org/users/' . $k,
|
||||||
|
'inbox' => 'https://example.org/users/' . $j . '/inbox',
|
||||||
|
'name' => 'user' . $k,
|
||||||
|
'preferredUsername' => 'user' . $k,
|
||||||
|
'publicKey' => 'https://example.org/users/' . $k . '#main-key',
|
||||||
|
'publicKeyPem' => $k,
|
||||||
|
);
|
||||||
|
|
||||||
|
$follower = new \Activitypub\Model\Follower();
|
||||||
|
$follower->from_array( $meta );
|
||||||
|
|
||||||
|
$id = $follower->upsert();
|
||||||
|
|
||||||
|
add_post_meta( $id, 'activitypub_user_id', 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$inboxes2 = \Activitypub\Collection\Followers::get_inboxes( 1 );
|
||||||
|
|
||||||
|
$this->assertCount( 30, $inboxes2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_all_followers() {
|
||||||
|
for ( $i = 0; $i < 30; $i++ ) {
|
||||||
|
$meta = array(
|
||||||
|
'id' => 'https://example.org/users/' . $i,
|
||||||
|
'url' => 'https://example.org/users/' . $i,
|
||||||
|
'inbox' => 'https://example.org/users/' . $i . '/inbox',
|
||||||
|
'name' => 'user' . $i,
|
||||||
|
'preferredUsername' => 'user' . $i,
|
||||||
|
'publicKey' => 'https://example.org/users/' . $i . '#main-key',
|
||||||
|
'publicKeyPem' => $i,
|
||||||
|
);
|
||||||
|
|
||||||
|
$follower = new \Activitypub\Model\Follower();
|
||||||
|
$follower->from_array( $meta );
|
||||||
|
|
||||||
|
$id = $follower->upsert();
|
||||||
|
|
||||||
|
add_post_meta( $id, 'activitypub_user_id', 1 );
|
||||||
|
}
|
||||||
|
|
||||||
|
$followers = \Activitypub\Collection\Followers::get_all_followers();
|
||||||
|
|
||||||
|
$this->assertCount( 30, $followers );
|
||||||
|
}
|
||||||
|
|
||||||
public static function http_request_host_is_external( $in, $host ) {
|
public static function http_request_host_is_external( $in, $host ) {
|
||||||
if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) {
|
if ( in_array( $host, array( 'example.com', 'example.org' ), true ) ) {
|
||||||
return true;
|
return true;
|
174
tests/test-class-activitypub-interactions.php
Normal file
174
tests/test-class-activitypub-interactions.php
Normal file
|
@ -0,0 +1,174 @@
|
||||||
|
<?php
|
||||||
|
class Test_Activitypub_Interactions extends WP_UnitTestCase {
|
||||||
|
public $user_id;
|
||||||
|
public $user_url;
|
||||||
|
public $post_id;
|
||||||
|
public $post_permalink;
|
||||||
|
|
||||||
|
public function set_up() {
|
||||||
|
$this->user_id = 1;
|
||||||
|
$authordata = \get_userdata( $this->user_id );
|
||||||
|
$this->user_url = $authordata->user_url;
|
||||||
|
|
||||||
|
$this->post_id = \wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_author' => $this->user_id,
|
||||||
|
'post_content' => 'test',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
$this->post_permalink = \get_permalink( $this->post_id );
|
||||||
|
|
||||||
|
\add_filter( 'pre_get_remote_metadata_by_actor', array( '\Test_Activitypub_Interactions', 'get_remote_metadata_by_actor' ), 0, 2 );
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function get_remote_metadata_by_actor( $value, $actor ) {
|
||||||
|
return array(
|
||||||
|
'name' => 'Example User',
|
||||||
|
'icon' => array(
|
||||||
|
'url' => 'https://example.com/icon',
|
||||||
|
),
|
||||||
|
'url' => $actor,
|
||||||
|
'id' => 'http://example.org/users/example',
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create_test_object( $id = 'https://example.com/123' ) {
|
||||||
|
return array(
|
||||||
|
'actor' => $this->user_url,
|
||||||
|
'id' => 'https://example.com/id/' . microtime( true ),
|
||||||
|
'to' => [ $this->user_url ],
|
||||||
|
'cc' => [ 'https://www.w3.org/ns/activitystreams#Public' ],
|
||||||
|
'object' => array(
|
||||||
|
'id' => $id,
|
||||||
|
'url' => 'https://example.com/example',
|
||||||
|
'inReplyTo' => $this->post_permalink,
|
||||||
|
'content' => 'example',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function create_test_rich_object( $id = 'https://example.com/123' ) {
|
||||||
|
return array(
|
||||||
|
'actor' => $this->user_url,
|
||||||
|
'id' => 'https://example.com/id/' . microtime( true ),
|
||||||
|
'to' => [ $this->user_url ],
|
||||||
|
'cc' => [ 'https://www.w3.org/ns/activitystreams#Public' ],
|
||||||
|
'object' => array(
|
||||||
|
'id' => $id,
|
||||||
|
'url' => 'https://example.com/example',
|
||||||
|
'inReplyTo' => $this->post_permalink,
|
||||||
|
'content' => 'Hello<br />example<p>example</p><img src="https://example.com/image.jpg" />',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_handle_create_basic() {
|
||||||
|
$comment_id = Activitypub\Collection\Interactions::add_comment( $this->create_test_object() );
|
||||||
|
$comment = get_comment( $comment_id, ARRAY_A );
|
||||||
|
|
||||||
|
$this->assertIsArray( $comment );
|
||||||
|
$this->assertEquals( $this->post_id, $comment['comment_post_ID'] );
|
||||||
|
$this->assertEquals( 'Example User', $comment['comment_author'] );
|
||||||
|
$this->assertEquals( $this->user_url, $comment['comment_author_url'] );
|
||||||
|
$this->assertEquals( 'example', $comment['comment_content'] );
|
||||||
|
$this->assertEquals( 'comment', $comment['comment_type'] );
|
||||||
|
$this->assertEquals( '', $comment['comment_author_email'] );
|
||||||
|
$this->assertEquals( 0, $comment['comment_parent'] );
|
||||||
|
$this->assertEquals( 'https://example.com/123', get_comment_meta( $comment_id, 'source_id', true ) );
|
||||||
|
$this->assertEquals( 'https://example.com/example', get_comment_meta( $comment_id, 'source_url', true ) );
|
||||||
|
$this->assertEquals( 'https://example.com/icon', get_comment_meta( $comment_id, 'avatar_url', true ) );
|
||||||
|
$this->assertEquals( 'activitypub', get_comment_meta( $comment_id, 'protocol', true ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_handle_create_rich() {
|
||||||
|
$comment_id = Activitypub\Collection\Interactions::add_comment( $this->create_test_rich_object() );
|
||||||
|
$comment = get_comment( $comment_id, ARRAY_A );
|
||||||
|
|
||||||
|
$this->assertEquals( 'Hello<br />example<p>example</p>', $comment['comment_content'] );
|
||||||
|
|
||||||
|
$commentarray = array(
|
||||||
|
'comment_post_ID' => $this->post_id,
|
||||||
|
'comment_author' => 'Example User',
|
||||||
|
'comment_author_url' => $this->user_url,
|
||||||
|
'comment_content' => 'Hello<br />example<p>example</p>',
|
||||||
|
'comment_type' => 'comment',
|
||||||
|
'comment_author_email' => '',
|
||||||
|
'comment_parent' => 0,
|
||||||
|
'comment_meta' => array(
|
||||||
|
'source_id' => 'https://example.com/123',
|
||||||
|
'source_url' => 'https://example.com/example',
|
||||||
|
'protocol' => 'activitypub',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
\remove_action( 'check_comment_flood', 'check_comment_flood_db', 10 );
|
||||||
|
$comment_id = wp_new_comment( $commentarray );
|
||||||
|
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
|
||||||
|
$comment = get_comment( $comment_id, ARRAY_A );
|
||||||
|
|
||||||
|
$this->assertEquals( 'Helloexampleexample', $comment['comment_content'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_convert_object_to_comment_not_reply_rejected() {
|
||||||
|
$object = $this->create_test_object();
|
||||||
|
unset( $object['object']['inReplyTo'] );
|
||||||
|
$converted = Activitypub\Collection\Interactions::add_comment( $object );
|
||||||
|
$this->assertFalse( $converted );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_convert_object_to_comment_already_exists_rejected() {
|
||||||
|
$object = $this->create_test_object( 'https://example.com/test_convert_object_to_comment_already_exists_rejected' );
|
||||||
|
Activitypub\Collection\Interactions::add_comment( $object );
|
||||||
|
$converted = Activitypub\Collection\Interactions::add_comment( $object );
|
||||||
|
$this->assertEquals( $converted->get_error_code(), 'comment_duplicate' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_convert_object_to_comment_reply_to_comment() {
|
||||||
|
$id = 'https://example.com/test_convert_object_to_comment_reply_to_comment';
|
||||||
|
$object = $this->create_test_object( $id );
|
||||||
|
Activitypub\Collection\Interactions::add_comment( $object );
|
||||||
|
$comment = \Activitypub\object_id_to_comment( $id );
|
||||||
|
|
||||||
|
$object['object']['inReplyTo'] = $id;
|
||||||
|
$object['object']['id'] = 'https://example.com/234';
|
||||||
|
$id = Activitypub\Collection\Interactions::add_comment( $object );
|
||||||
|
$converted = get_comment( $id, ARRAY_A );
|
||||||
|
|
||||||
|
$this->assertIsArray( $converted );
|
||||||
|
$this->assertEquals( $this->post_id, $converted['comment_post_ID'] );
|
||||||
|
$this->assertEquals( $comment->comment_ID, $converted['comment_parent'] );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_convert_object_to_comment_reply_to_non_existent_comment_rejected() {
|
||||||
|
$object = $this->create_test_object();
|
||||||
|
$object['object']['inReplyTo'] = 'https://example.com/not_found';
|
||||||
|
$converted = Activitypub\Collection\Interactions::add_comment( $object );
|
||||||
|
$this->assertFalse( $converted );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_handle_create_basic2() {
|
||||||
|
$id = 'https://example.com/test_handle_create_basic';
|
||||||
|
$object = $this->create_test_object( $id );
|
||||||
|
Activitypub\Collection\Interactions::add_comment( $object );
|
||||||
|
$comment = \Activitypub\object_id_to_comment( $id );
|
||||||
|
$this->assertInstanceOf( WP_Comment::class, $comment );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_get_interaction_by_id() {
|
||||||
|
$id = 'https://example.com/test_get_interaction_by_id';
|
||||||
|
$url = 'https://example.com/test_get_interaction_by_url';
|
||||||
|
$object = $this->create_test_object( $id );
|
||||||
|
$object['object']['url'] = $url;
|
||||||
|
|
||||||
|
Activitypub\Collection\Interactions::add_comment( $object );
|
||||||
|
$comment = \Activitypub\object_id_to_comment( $id );
|
||||||
|
$interactions = Activitypub\Collection\Interactions::get_interaction_by_id( $id );
|
||||||
|
$this->assertIsArray( $interactions );
|
||||||
|
$this->assertEquals( $comment->comment_ID, $interactions[0]->comment_ID );
|
||||||
|
|
||||||
|
$comment = \Activitypub\object_id_to_comment( $id );
|
||||||
|
$interactions = Activitypub\Collection\Interactions::get_interaction_by_id( $url );
|
||||||
|
$this->assertIsArray( $interactions );
|
||||||
|
$this->assertEquals( $comment->comment_ID, $interactions[0]->comment_ID );
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,13 +10,13 @@ class Test_Activitypub_Post extends WP_UnitTestCase {
|
||||||
|
|
||||||
$permalink = \get_permalink( $post );
|
$permalink = \get_permalink( $post );
|
||||||
|
|
||||||
$activitypub_post = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( get_post( $post ) )->to_object();
|
$activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object();
|
||||||
|
|
||||||
$this->assertEquals( $permalink, $activitypub_post->get_id() );
|
$this->assertEquals( $permalink, $activitypub_post->get_id() );
|
||||||
|
|
||||||
\wp_trash_post( $post );
|
\wp_trash_post( $post );
|
||||||
|
|
||||||
$activitypub_post = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( get_post( $post ) )->to_object();
|
$activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object();
|
||||||
|
|
||||||
$this->assertEquals( $permalink, $activitypub_post->get_id() );
|
$this->assertEquals( $permalink, $activitypub_post->get_id() );
|
||||||
|
|
||||||
|
|
|
@ -5,7 +5,7 @@ class Test_Activitypub_Rest_Inbox extends WP_UnitTestCase {
|
||||||
*/
|
*/
|
||||||
public function test_is_activity_public( $data, $check ) {
|
public function test_is_activity_public( $data, $check ) {
|
||||||
|
|
||||||
$this->assertEquals( $check, Activitypub\Rest\Inbox::is_activity_public( $data ) );
|
$this->assertEquals( $check, Activitypub\is_activity_public( $data ) );
|
||||||
}
|
}
|
||||||
|
|
||||||
public function the_data_provider() {
|
public function the_data_provider() {
|
||||||
|
|
|
@ -10,7 +10,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase {
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
$remote_actor = \get_author_posts_url( 2 );
|
$remote_actor = \get_author_posts_url( 2 );
|
||||||
$activitypub_post = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( get_post( $post ) )->to_object();
|
$activitypub_post = \Activitypub\Transformer\Post::transform( get_post( $post ) )->to_object();
|
||||||
$activitypub_activity = new Activitypub\Activity\Activity( 'Create' );
|
$activitypub_activity = new Activitypub\Activity\Activity( 'Create' );
|
||||||
$activitypub_activity->set_type( 'Create' );
|
$activitypub_activity->set_type( 'Create' );
|
||||||
$activitypub_activity->set_object( $activitypub_post );
|
$activitypub_activity->set_object( $activitypub_post );
|
||||||
|
@ -82,7 +82,7 @@ class Test_Activitypub_Signature_Verification extends WP_UnitTestCase {
|
||||||
);
|
);
|
||||||
$remote_actor = \get_author_posts_url( 2 );
|
$remote_actor = \get_author_posts_url( 2 );
|
||||||
$remote_actor_inbox = Activitypub\get_rest_url_by_path( '/inbox' );
|
$remote_actor_inbox = Activitypub\get_rest_url_by_path( '/inbox' );
|
||||||
$activitypub_post = \Activitypub\Transformer\Transformers_Manager::instance()->get_transformer( get_post( $post ) )->to_object();
|
$activitypub_post = \Activitypub\Transformer\Post::transform( \get_post( $post ) )->to_object();
|
||||||
$activitypub_activity = new Activitypub\Activity\Activity();
|
$activitypub_activity = new Activitypub\Activity\Activity();
|
||||||
$activitypub_activity->set_type( 'Create' );
|
$activitypub_activity->set_type( 'Create' );
|
||||||
$activitypub_activity->set_object( $activitypub_post );
|
$activitypub_activity->set_object( $activitypub_post );
|
||||||
|
|
|
@ -62,4 +62,34 @@ class Test_Activitypub_Shortcodes extends WP_UnitTestCase {
|
||||||
$this->assertEquals( '', $content );
|
$this->assertEquals( '', $content );
|
||||||
Shortcodes::unregister();
|
Shortcodes::unregister();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function test_excerpt() {
|
||||||
|
Shortcodes::register();
|
||||||
|
global $post;
|
||||||
|
|
||||||
|
$post_id = -97; // negative ID, to avoid clash with a valid post
|
||||||
|
$post = new stdClass();
|
||||||
|
$post->ID = $post_id;
|
||||||
|
$post->post_author = 1;
|
||||||
|
$post->post_date = current_time( 'mysql' );
|
||||||
|
$post->post_date_gmt = current_time( 'mysql', 1 );
|
||||||
|
$post->post_title = 'Some title or other';
|
||||||
|
$post->post_content = '<script>test</script>Lorem ipsum dolor sit amet, consectetur.<script type="javascript">{"asdf": "qwerty"}</script><style></style>';
|
||||||
|
$post->post_status = 'publish';
|
||||||
|
$post->comment_status = 'closed';
|
||||||
|
$post->ping_status = 'closed';
|
||||||
|
$post->post_name = 'fake-page-' . rand( 1, 99999 ); // append random number to avoid clash
|
||||||
|
$post->post_type = 'page';
|
||||||
|
$post->filter = 'raw'; // important!
|
||||||
|
|
||||||
|
$content = '[ap_excerpt length="25"]';
|
||||||
|
|
||||||
|
// Fill in the shortcodes.
|
||||||
|
setup_postdata( $post );
|
||||||
|
$content = do_shortcode( $content );
|
||||||
|
wp_reset_postdata();
|
||||||
|
|
||||||
|
$this->assertEquals( "<p>Lorem ipsum […]</p>\n", $content );
|
||||||
|
Shortcodes::unregister();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
43
tests/test-class-activitypub-users-collection.php
Normal file
43
tests/test-class-activitypub-users-collection.php
Normal file
|
@ -0,0 +1,43 @@
|
||||||
|
<?php
|
||||||
|
class Test_Activitypub_Users_Collection extends WP_UnitTestCase {
|
||||||
|
|
||||||
|
public function set_up() {
|
||||||
|
parent::set_up();
|
||||||
|
|
||||||
|
add_option( 'activitypub_blog_user_identifier', 'blog' );
|
||||||
|
add_user_meta( 1, 'activitypub_user_identifier', 'admin' );
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* @dataProvider the_resource_provider
|
||||||
|
*/
|
||||||
|
public function test_get_by_various( $resource, $expected ) {
|
||||||
|
$user = Activitypub\Collection\Users::get_by_resource( $resource );
|
||||||
|
|
||||||
|
$this->assertInstanceOf( $expected, $user );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function the_resource_provider() {
|
||||||
|
return array(
|
||||||
|
array( 'http://example.org/?author=1', 'Activitypub\Model\User' ),
|
||||||
|
array( 'https://example.org/?author=1', 'Activitypub\Model\User' ),
|
||||||
|
array( 'http://example.org/?author=7', 'WP_Error' ),
|
||||||
|
array( 'acct:admin@example.org', 'Activitypub\Model\User' ),
|
||||||
|
array( 'acct:blog@example.org', 'Activitypub\Model\Blog_User' ),
|
||||||
|
array( 'acct:*@example.org', 'Activitypub\Model\Blog_User' ),
|
||||||
|
array( 'acct:_@example.org', 'Activitypub\Model\Blog_User' ),
|
||||||
|
array( 'acct:aksd@example.org', 'WP_Error' ),
|
||||||
|
array( 'admin@example.org', 'Activitypub\Model\User' ),
|
||||||
|
array( 'acct:application@example.org', 'Activitypub\Model\Application_User' ),
|
||||||
|
array( 'http://example.org/@admin', 'Activitypub\Model\User' ),
|
||||||
|
array( 'http://example.org/@blog', 'Activitypub\Model\Blog_User' ),
|
||||||
|
array( 'https://example.org/@blog', 'Activitypub\Model\Blog_User' ),
|
||||||
|
array( 'http://example.org/@blog/', 'Activitypub\Model\Blog_User' ),
|
||||||
|
array( 'http://example.org/', 'Activitypub\Model\Blog_User' ),
|
||||||
|
array( 'http://example.org', 'Activitypub\Model\Blog_User' ),
|
||||||
|
array( 'https://example.org/', 'Activitypub\Model\Blog_User' ),
|
||||||
|
array( 'https://example.org', 'Activitypub\Model\Blog_User' ),
|
||||||
|
array( 'http://example.org/@blog/s', 'WP_Error' ),
|
||||||
|
array( 'http://example.org/@blogs/', 'WP_Error' ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
21
tests/test-class-activitypub-webfinger.php
Normal file
21
tests/test-class-activitypub-webfinger.php
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
<?php
|
||||||
|
class Test_Activitypub_Webfinger extends WP_UnitTestCase {
|
||||||
|
/**
|
||||||
|
* @dataProvider the_cache_key_provider
|
||||||
|
*/
|
||||||
|
public function test_generate_cache_key( $uri, $hash ) {
|
||||||
|
$cache_key = Activitypub\Webfinger::generate_cache_key( $uri );
|
||||||
|
|
||||||
|
$this->assertEquals( $cache_key, 'webfinger_' . $hash );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function the_cache_key_provider() {
|
||||||
|
return array(
|
||||||
|
array( 'http://example.org/?author=1', md5( 'http://example.org/?author=1' ) ),
|
||||||
|
array( '@author@example.org', md5( 'acct:author@example.org' ) ),
|
||||||
|
array( 'author@example.org', md5( 'acct:author@example.org' ) ),
|
||||||
|
array( 'acct:author@example.org', md5( 'acct:author@example.org' ) ),
|
||||||
|
array( 'https://example.org', md5( 'https://example.org' ) ),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
18
tests/test-class-admin.php
Normal file
18
tests/test-class-admin.php
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
<?php
|
||||||
|
class Test_Admin extends WP_UnitTestCase {
|
||||||
|
public function test_no_permalink_structure_has_errors() {
|
||||||
|
\add_option( 'permalink_structure', '' );
|
||||||
|
\do_action( 'admin_notices' );
|
||||||
|
$this->expectOutputRegex( "/notice-error/" );
|
||||||
|
|
||||||
|
\delete_option( 'permalink_structure' );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_has_permalink_structure_no_errors() {
|
||||||
|
\add_option( 'permalink_structure', '/archives/%post_id%' );
|
||||||
|
\do_action( 'admin_notices' );
|
||||||
|
$this->expectOutputRegex( "/^((?!notice-error).)*$/s" );
|
||||||
|
|
||||||
|
\delete_option( 'permalink_structure' );
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,9 +1,149 @@
|
||||||
<?php
|
<?php
|
||||||
class Test_Functions extends ActivityPub_TestCase_Cache_HTTP {
|
class Test_Functions extends ActivityPub_TestCase_Cache_HTTP {
|
||||||
|
public $user_id;
|
||||||
|
public $post_id;
|
||||||
|
|
||||||
public function test_get_remote_metadata_by_actor() {
|
public function test_get_remote_metadata_by_actor() {
|
||||||
$metadata = \ActivityPub\get_remote_metadata_by_actor( 'pfefferle@notiz.blog' );
|
$metadata = \ActivityPub\get_remote_metadata_by_actor( 'pfefferle@notiz.blog' );
|
||||||
$this->assertEquals( 'https://notiz.blog/author/matthias-pfefferle/', $metadata['url'] );
|
$this->assertEquals( 'https://notiz.blog/author/matthias-pfefferle/', $metadata['url'] );
|
||||||
$this->assertEquals( 'pfefferle', $metadata['preferredUsername'] );
|
$this->assertEquals( 'pfefferle', $metadata['preferredUsername'] );
|
||||||
$this->assertEquals( 'Matthias Pfefferle', $metadata['name'] );
|
$this->assertEquals( 'Matthias Pfefferle', $metadata['name'] );
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function set_up() {
|
||||||
|
$this->post_id = \wp_insert_post(
|
||||||
|
array(
|
||||||
|
'post_author' => $this->user_id,
|
||||||
|
'post_content' => 'test',
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_object_id_to_comment_basic() {
|
||||||
|
$single_comment_source_id = 'https://example.com/single';
|
||||||
|
$content = 'example';
|
||||||
|
$comment_id = \wp_new_comment(
|
||||||
|
array(
|
||||||
|
'comment_post_ID' => $this->post_id,
|
||||||
|
'comment_author' => 'Example User',
|
||||||
|
'comment_author_url' => 'https://example.com/user',
|
||||||
|
'comment_content' => $content,
|
||||||
|
'comment_type' => '',
|
||||||
|
'comment_author_email' => '',
|
||||||
|
'comment_parent' => 0,
|
||||||
|
'comment_meta' => array(
|
||||||
|
'source_id' => $single_comment_source_id,
|
||||||
|
'source_url' => 'https://example.com/123',
|
||||||
|
'avatar_url' => 'https://example.com/icon',
|
||||||
|
'protocol' => 'activitypub',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
$query_result = \Activitypub\object_id_to_comment( $single_comment_source_id );
|
||||||
|
$this->assertInstanceOf( WP_Comment::class, $query_result );
|
||||||
|
$this->assertEquals( $comment_id, $query_result->comment_ID );
|
||||||
|
$this->assertEquals( $content, $query_result->comment_content );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_object_id_to_comment_none() {
|
||||||
|
$single_comment_source_id = 'https://example.com/none';
|
||||||
|
$query_result = \Activitypub\object_id_to_comment( $single_comment_source_id );
|
||||||
|
$this->assertFalse( $query_result );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_object_id_to_comment_duplicate() {
|
||||||
|
$duplicate_comment_source_id = 'https://example.com/duplicate';
|
||||||
|
for ( $i = 0; $i < 2; ++$i ) {
|
||||||
|
\wp_new_comment(
|
||||||
|
array(
|
||||||
|
'comment_post_ID' => $this->post_id,
|
||||||
|
'comment_author' => 'Example User',
|
||||||
|
'comment_author_url' => 'https://example.com/user',
|
||||||
|
'comment_content' => 'example',
|
||||||
|
'comment_type' => '',
|
||||||
|
'comment_author_email' => '',
|
||||||
|
'comment_parent' => 0,
|
||||||
|
'comment_meta' => array(
|
||||||
|
'source_id' => $duplicate_comment_source_id,
|
||||||
|
'source_url' => 'https://example.com/123',
|
||||||
|
'avatar_url' => 'https://example.com/icon',
|
||||||
|
'protocol' => 'activitypub',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
true
|
||||||
|
);
|
||||||
|
}
|
||||||
|
$query_result = \Activitypub\object_id_to_comment( $duplicate_comment_source_id );
|
||||||
|
$this->assertFalse( $query_result );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider object_to_uri_provider
|
||||||
|
*/
|
||||||
|
public function test_object_to_uri( $input, $output ) {
|
||||||
|
$this->assertEquals( $output, \Activitypub\object_to_uri( $input ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
public function object_to_uri_provider() {
|
||||||
|
return array(
|
||||||
|
array( null, null ),
|
||||||
|
array( 'https://example.com', 'https://example.com' ),
|
||||||
|
array( array( 'https://example.com' ), 'https://example.com' ),
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'https://example.com',
|
||||||
|
'https://example.org',
|
||||||
|
),
|
||||||
|
'https://example.com',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'type' => 'Link',
|
||||||
|
'href' => 'https://example.com',
|
||||||
|
),
|
||||||
|
'https://example.com',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'type' => 'Link',
|
||||||
|
'href' => 'https://example.com',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'type' => 'Link',
|
||||||
|
'href' => 'https://example.org',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'https://example.com',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'type' => 'Actor',
|
||||||
|
'id' => 'https://example.com',
|
||||||
|
),
|
||||||
|
'https://example.com',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'type' => 'Actor',
|
||||||
|
'id' => 'https://example.com',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
'type' => 'Actor',
|
||||||
|
'id' => 'https://example.org',
|
||||||
|
),
|
||||||
|
),
|
||||||
|
'https://example.com',
|
||||||
|
),
|
||||||
|
array(
|
||||||
|
array(
|
||||||
|
'type' => 'Activity',
|
||||||
|
'id' => 'https://example.com',
|
||||||
|
),
|
||||||
|
'https://example.com',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue