Compare commits
65 Commits
cbfad7b099
...
master
Author | SHA1 | Date | |
---|---|---|---|
e4b9b8235b | |||
f3c623d403 | |||
37e74c1bea | |||
8fefb19ab4 | |||
7ca941b591 | |||
cf022e2628 | |||
fc3d7ab181 | |||
81e02d9aea | |||
6b573f08f6 | |||
51f6d193dd | |||
5dc2981470 | |||
0bc27333c2 | |||
a212704ec2 | |||
c950632407 | |||
fd76ba0cbe | |||
ebd40ef928 | |||
eb9181b250 | |||
c53f9e0e50 | |||
d652fac5a4 | |||
fdfbf76539 | |||
19dfd317cc | |||
e3858f0710 | |||
9cbc2cb832 | |||
db85936315 | |||
dd95c943cb | |||
7dcace54d3 | |||
e13bab0b76 | |||
cd379e1d95 | |||
65c751c1d9 | |||
db5f4b72eb | |||
ef209dc569 | |||
e73c3de31d | |||
f970470c59 | |||
a35dc419bc | |||
627ec103fe | |||
c54fa007bd | |||
fb4b27bbc6 | |||
b964c1846c | |||
44c2f9f9a2 | |||
51937c2f57 | |||
39ec06fbc1 | |||
311bc308f5 | |||
3b4e169a1e | |||
19e351ef3b | |||
91db4aebe1 | |||
03c1118952 | |||
877a737c75 | |||
938cef2946 | |||
ec9d8a5834 | |||
5c4b728efa | |||
65d26d4d83 | |||
4e493c268e | |||
eeef5ad6e0 | |||
9179edb708 | |||
baa5aa7ed5 | |||
62f3186aef | |||
496ccfac3d | |||
7b83df998e | |||
7b5aaceef5 | |||
31de6df412 | |||
5de19fe451 | |||
1a790bdd29 | |||
9420356fcf | |||
fd49653431 | |||
8e79281642 |
158
htaccess
158
htaccess
@ -1,161 +1,3 @@
|
||||
# BEGIN W3TC Browser Cache
|
||||
<IfModule mod_mime.c>
|
||||
AddType text/css .css
|
||||
AddType text/x-component .htc
|
||||
AddType application/x-javascript .js
|
||||
AddType application/javascript .js2
|
||||
AddType text/javascript .js3
|
||||
AddType text/x-js .js4
|
||||
AddType video/asf .asf .asx .wax .wmv .wmx
|
||||
AddType video/avi .avi
|
||||
AddType image/avif .avif
|
||||
AddType image/avif-sequence .avifs
|
||||
AddType image/bmp .bmp
|
||||
AddType application/java .class
|
||||
AddType video/divx .divx
|
||||
AddType application/msword .doc .docx
|
||||
AddType application/vnd.ms-fontobject .eot
|
||||
AddType application/x-msdownload .exe
|
||||
AddType image/gif .gif
|
||||
AddType application/x-gzip .gz .gzip
|
||||
AddType image/x-icon .ico
|
||||
AddType image/jpeg .jpg .jpeg .jpe
|
||||
AddType image/webp .webp
|
||||
AddType application/json .json
|
||||
AddType application/vnd.ms-access .mdb
|
||||
AddType audio/midi .mid .midi
|
||||
AddType video/quicktime .mov .qt
|
||||
AddType audio/mpeg .mp3 .m4a
|
||||
AddType video/mp4 .mp4 .m4v
|
||||
AddType video/mpeg .mpeg .mpg .mpe
|
||||
AddType video/webm .webm
|
||||
AddType application/vnd.ms-project .mpp
|
||||
AddType application/x-font-otf .otf
|
||||
AddType application/vnd.ms-opentype ._otf
|
||||
AddType application/vnd.oasis.opendocument.database .odb
|
||||
AddType application/vnd.oasis.opendocument.chart .odc
|
||||
AddType application/vnd.oasis.opendocument.formula .odf
|
||||
AddType application/vnd.oasis.opendocument.graphics .odg
|
||||
AddType application/vnd.oasis.opendocument.presentation .odp
|
||||
AddType application/vnd.oasis.opendocument.spreadsheet .ods
|
||||
AddType application/vnd.oasis.opendocument.text .odt
|
||||
AddType audio/ogg .ogg
|
||||
AddType video/ogg .ogv
|
||||
AddType application/pdf .pdf
|
||||
AddType image/png .png
|
||||
AddType application/vnd.ms-powerpoint .pot .pps .ppt .pptx
|
||||
AddType audio/x-realaudio .ra .ram
|
||||
AddType image/svg+xml .svg .svgz
|
||||
AddType application/x-shockwave-flash .swf
|
||||
AddType application/x-tar .tar
|
||||
AddType image/tiff .tif .tiff
|
||||
AddType application/x-font-ttf .ttf .ttc
|
||||
AddType application/vnd.ms-opentype ._ttf
|
||||
AddType audio/wav .wav
|
||||
AddType audio/wma .wma
|
||||
AddType application/vnd.ms-write .wri
|
||||
AddType application/font-woff .woff
|
||||
AddType application/font-woff2 .woff2
|
||||
AddType application/vnd.ms-excel .xla .xls .xlsx .xlt .xlw
|
||||
AddType application/zip .zip
|
||||
</IfModule>
|
||||
<IfModule mod_expires.c>
|
||||
ExpiresActive On
|
||||
ExpiresByType text/css A31536000
|
||||
ExpiresByType text/x-component A31536000
|
||||
ExpiresByType application/x-javascript A31536000
|
||||
ExpiresByType application/javascript A31536000
|
||||
ExpiresByType text/javascript A31536000
|
||||
ExpiresByType text/x-js A31536000
|
||||
ExpiresByType video/asf A31536000
|
||||
ExpiresByType video/avi A31536000
|
||||
ExpiresByType image/avif A31536000
|
||||
ExpiresByType image/avif-sequence A31536000
|
||||
ExpiresByType image/bmp A31536000
|
||||
ExpiresByType application/java A31536000
|
||||
ExpiresByType video/divx A31536000
|
||||
ExpiresByType application/msword A31536000
|
||||
ExpiresByType application/vnd.ms-fontobject A31536000
|
||||
ExpiresByType application/x-msdownload A31536000
|
||||
ExpiresByType image/gif A31536000
|
||||
ExpiresByType application/x-gzip A31536000
|
||||
ExpiresByType image/x-icon A31536000
|
||||
ExpiresByType image/jpeg A31536000
|
||||
ExpiresByType image/webp A31536000
|
||||
ExpiresByType application/json A31536000
|
||||
ExpiresByType application/vnd.ms-access A31536000
|
||||
ExpiresByType audio/midi A31536000
|
||||
ExpiresByType video/quicktime A31536000
|
||||
ExpiresByType audio/mpeg A31536000
|
||||
ExpiresByType video/mp4 A31536000
|
||||
ExpiresByType video/mpeg A31536000
|
||||
ExpiresByType video/webm A31536000
|
||||
ExpiresByType application/vnd.ms-project A31536000
|
||||
ExpiresByType application/x-font-otf A31536000
|
||||
ExpiresByType application/vnd.ms-opentype A31536000
|
||||
ExpiresByType application/vnd.oasis.opendocument.database A31536000
|
||||
ExpiresByType application/vnd.oasis.opendocument.chart A31536000
|
||||
ExpiresByType application/vnd.oasis.opendocument.formula A31536000
|
||||
ExpiresByType application/vnd.oasis.opendocument.graphics A31536000
|
||||
ExpiresByType application/vnd.oasis.opendocument.presentation A31536000
|
||||
ExpiresByType application/vnd.oasis.opendocument.spreadsheet A31536000
|
||||
ExpiresByType application/vnd.oasis.opendocument.text A31536000
|
||||
ExpiresByType audio/ogg A31536000
|
||||
ExpiresByType video/ogg A31536000
|
||||
ExpiresByType application/pdf A31536000
|
||||
ExpiresByType image/png A31536000
|
||||
ExpiresByType application/vnd.ms-powerpoint A31536000
|
||||
ExpiresByType audio/x-realaudio A31536000
|
||||
ExpiresByType image/svg+xml A31536000
|
||||
ExpiresByType application/x-shockwave-flash A31536000
|
||||
ExpiresByType application/x-tar A31536000
|
||||
ExpiresByType image/tiff A31536000
|
||||
ExpiresByType application/x-font-ttf A31536000
|
||||
ExpiresByType application/vnd.ms-opentype A31536000
|
||||
ExpiresByType audio/wav A31536000
|
||||
ExpiresByType audio/wma A31536000
|
||||
ExpiresByType application/vnd.ms-write A31536000
|
||||
ExpiresByType application/font-woff A31536000
|
||||
ExpiresByType application/font-woff2 A31536000
|
||||
ExpiresByType application/vnd.ms-excel A31536000
|
||||
ExpiresByType application/zip A31536000
|
||||
</IfModule>
|
||||
<IfModule mod_deflate.c>
|
||||
<IfModule mod_filter.c>
|
||||
AddOutputFilterByType DEFLATE text/css text/x-component application/x-javascript application/javascript text/javascript text/x-js text/html text/richtext text/plain text/xsd text/xsl text/xml image/bmp application/java application/msword application/vnd.ms-fontobject application/x-msdownload image/x-icon application/json application/vnd.ms-access video/webm application/vnd.ms-project application/x-font-otf application/vnd.ms-opentype application/vnd.oasis.opendocument.database application/vnd.oasis.opendocument.chart application/vnd.oasis.opendocument.formula application/vnd.oasis.opendocument.graphics application/vnd.oasis.opendocument.presentation application/vnd.oasis.opendocument.spreadsheet application/vnd.oasis.opendocument.text audio/ogg application/pdf application/vnd.ms-powerpoint image/svg+xml application/x-shockwave-flash image/tiff application/x-font-ttf application/vnd.ms-opentype audio/wav application/vnd.ms-write application/font-woff application/font-woff2 application/vnd.ms-excel
|
||||
<IfModule mod_mime.c>
|
||||
# DEFLATE by extension
|
||||
AddOutputFilter DEFLATE js css htm html xml
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
</IfModule>
|
||||
<FilesMatch "\.(css|htc|less|js|js2|js3|js4|CSS|HTC|LESS|JS|JS2|JS3|JS4)$">
|
||||
FileETag MTime Size
|
||||
<IfModule mod_headers.c>
|
||||
Header set Pragma "public"
|
||||
Header append Cache-Control "public"
|
||||
Header unset Set-Cookie
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
<FilesMatch "\.(html|htm|rtf|rtx|txt|xsd|xsl|xml|HTML|HTM|RTF|RTX|TXT|XSD|XSL|XML)$">
|
||||
FileETag MTime Size
|
||||
<IfModule mod_headers.c>
|
||||
Header set Pragma "public"
|
||||
Header set Cache-Control "max-age=3600, public"
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
<FilesMatch "\.(asf|asx|wax|wmv|wmx|avi|avif|avifs|bmp|class|divx|doc|docx|eot|exe|gif|gz|gzip|ico|jpg|jpeg|jpe|webp|json|mdb|mid|midi|mov|qt|mp3|m4a|mp4|m4v|mpeg|mpg|mpe|webm|mpp|otf|_otf|odb|odc|odf|odg|odp|ods|odt|ogg|ogv|pdf|png|pot|pps|ppt|pptx|ra|ram|svg|svgz|swf|tar|tif|tiff|ttf|ttc|_ttf|wav|wma|wri|woff|woff2|xla|xls|xlsx|xlt|xlw|zip|ASF|ASX|WAX|WMV|WMX|AVI|AVIF|AVIFS|BMP|CLASS|DIVX|DOC|DOCX|EOT|EXE|GIF|GZ|GZIP|ICO|JPG|JPEG|JPE|WEBP|JSON|MDB|MID|MIDI|MOV|QT|MP3|M4A|MP4|M4V|MPEG|MPG|MPE|WEBM|MPP|OTF|_OTF|ODB|ODC|ODF|ODG|ODP|ODS|ODT|OGG|OGV|PDF|PNG|POT|PPS|PPT|PPTX|RA|RAM|SVG|SVGZ|SWF|TAR|TIF|TIFF|TTF|TTC|_TTF|WAV|WMA|WRI|WOFF|WOFF2|XLA|XLS|XLSX|XLT|XLW|ZIP)$">
|
||||
FileETag MTime Size
|
||||
<IfModule mod_headers.c>
|
||||
Header set Pragma "public"
|
||||
Header append Cache-Control "public"
|
||||
Header unset Set-Cookie
|
||||
</IfModule>
|
||||
</FilesMatch>
|
||||
<IfModule mod_headers.c>
|
||||
Header set Referrer-Policy "no-referrer-when-downgrade"
|
||||
</IfModule>
|
||||
# END W3TC Browser Cache
|
||||
# BEGIN WordPress
|
||||
# The directives (lines) between "BEGIN WordPress" and "END WordPress" are
|
||||
# dynamically generated, and should only be modified via WordPress filters.
|
||||
|
11
wp-content/jetpack-waf/bootstrap.php
Normal file
11
wp-content/jetpack-waf/bootstrap.php
Normal file
@ -0,0 +1,11 @@
|
||||
<?php
|
||||
define( 'DISABLE_JETPACK_WAF', false );
|
||||
if ( defined( 'DISABLE_JETPACK_WAF' ) && DISABLE_JETPACK_WAF ) return;
|
||||
define( 'JETPACK_WAF_MODE', 'silent' );
|
||||
define( 'JETPACK_WAF_SHARE_DATA', false );
|
||||
define( 'JETPACK_WAF_SHARE_DEBUG_DATA', false );
|
||||
define( 'JETPACK_WAF_DIR', '/app/data/wp-content/jetpack-waf' );
|
||||
define( 'JETPACK_WAF_WPCONFIG', '/app/data/wp-content/../wp-config.php' );
|
||||
define( 'JETPACK_WAF_ENTRYPOINT', 'rules/rules.php' );
|
||||
require_once '/app/data/wp-content/plugins/jetpack-protect/vendor/autoload.php';
|
||||
Automattic\Jetpack\Waf\Waf_Runner::initialize();
|
4
wp-content/jetpack-waf/rules/allow-ip.php
Normal file
4
wp-content/jetpack-waf/rules/allow-ip.php
Normal file
@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$waf_allow_list = array (
|
||||
);
|
||||
return $waf->is_ip_in_array( $waf_allow_list );
|
1
wp-content/jetpack-waf/rules/automatic-rules.php
Normal file
1
wp-content/jetpack-waf/rules/automatic-rules.php
Normal file
@ -0,0 +1 @@
|
||||
<?php
|
4
wp-content/jetpack-waf/rules/block-ip.php
Normal file
4
wp-content/jetpack-waf/rules/block-ip.php
Normal file
@ -0,0 +1,4 @@
|
||||
<?php
|
||||
$waf_block_list = array (
|
||||
);
|
||||
return $waf->is_ip_in_array( $waf_block_list );
|
1
wp-content/jetpack-waf/rules/rules.php
Normal file
1
wp-content/jetpack-waf/rules/rules.php
Normal file
@ -0,0 +1 @@
|
||||
<?php
|
@ -1,42 +0,0 @@
|
||||
.DS_Store
|
||||
.editorconfig
|
||||
.git
|
||||
.gitignore
|
||||
.github
|
||||
.travis.yml
|
||||
.codeclimate.yml
|
||||
.data
|
||||
.svnignore
|
||||
.wordpress-org
|
||||
.php_cs
|
||||
Gruntfile.js
|
||||
LINGUAS
|
||||
Makefile
|
||||
README.md
|
||||
readme.md
|
||||
CHANGELOG.md
|
||||
CODE_OF_CONDUCT.md
|
||||
FEDERATION.md
|
||||
SECURITY.md
|
||||
LICENSE.md
|
||||
_site
|
||||
_config.yml
|
||||
bin
|
||||
composer.json
|
||||
composer.lock
|
||||
docker-compose.yml
|
||||
docker-compose-test.yml
|
||||
Dockerfile
|
||||
gulpfile.js
|
||||
package.json
|
||||
node_modules
|
||||
npm-debug.log
|
||||
phpcs.xml
|
||||
package.json
|
||||
package-lock.json
|
||||
phpunit.xml
|
||||
phpunit.xml.dist
|
||||
tests
|
||||
node_modules
|
||||
vendor
|
||||
src
|
@ -1,68 +1,64 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: ActivityPub
|
||||
* Plugin URI: https://github.com/pfefferle/wordpress-activitypub/
|
||||
* Plugin URI: https://github.com/Automattic/wordpress-activitypub
|
||||
* Description: The ActivityPub protocol is a decentralized social networking protocol based upon the ActivityStreams 2.0 data format.
|
||||
* Version: 2.3.0
|
||||
* Version: 5.8.0
|
||||
* Author: Matthias Pfefferle & Automattic
|
||||
* Author URI: https://automattic.com/
|
||||
* License: MIT
|
||||
* License URI: http://opensource.org/licenses/MIT
|
||||
* Requires PHP: 5.6
|
||||
* Requires PHP: 7.2
|
||||
* Text Domain: activitypub
|
||||
* Domain Path: /languages
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use function Activitypub\is_blog_public;
|
||||
use function Activitypub\site_supports_blocks;
|
||||
use WP_CLI;
|
||||
|
||||
require_once __DIR__ . '/includes/compat.php';
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
|
||||
\define( 'ACTIVITYPUB_PLUGIN_VERSION', '2.3.0' );
|
||||
|
||||
/**
|
||||
* Initialize the plugin constants.
|
||||
*/
|
||||
\defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || \define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' );
|
||||
\defined( 'ACTIVITYPUB_EXCERPT_LENGTH' ) || \define( 'ACTIVITYPUB_EXCERPT_LENGTH', 400 );
|
||||
\defined( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS' ) || \define( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS', true );
|
||||
\defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || \define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 3 );
|
||||
\defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=<p>)|(?<=<br>)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' );
|
||||
\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' );
|
||||
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "<h2>[ap_title]</h2>\n\n[ap_content]\n\n[ap_hashtags]\n\n[ap_shortlink]" );
|
||||
\defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) || \define( 'ACTIVITYPUB_AUTHORIZED_FETCH', false );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS', false );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false );
|
||||
\defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) || \define( 'ACTIVITYPUB_SHARED_INBOX_FEATURE', false );
|
||||
\defined( 'ACTIVITYPUB_SEND_VARY_HEADER' ) || \define( 'ACTIVITYPUB_SEND_VARY_HEADER', false );
|
||||
\defined( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE' ) || \define( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE', 'note' );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_VERSION', '5.8.0' );
|
||||
|
||||
// Plugin related constants.
|
||||
\define( 'ACTIVITYPUB_PLUGIN_DIR', plugin_dir_path( __FILE__ ) );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_BASENAME', plugin_basename( __FILE__ ) );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_FILE', plugin_dir_path( __FILE__ ) . '/' . basename( __FILE__ ) );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_FILE', ACTIVITYPUB_PLUGIN_DIR . basename( __FILE__ ) );
|
||||
\define( 'ACTIVITYPUB_PLUGIN_URL', plugin_dir_url( __FILE__ ) );
|
||||
|
||||
require_once __DIR__ . '/includes/class-autoloader.php';
|
||||
require_once __DIR__ . '/includes/compat.php';
|
||||
require_once __DIR__ . '/includes/functions.php';
|
||||
require_once __DIR__ . '/includes/constants.php';
|
||||
require_once __DIR__ . '/integration/load.php';
|
||||
|
||||
Autoloader::register_path( __NAMESPACE__, __DIR__ . '/includes' );
|
||||
|
||||
/**
|
||||
* Initialize REST routes.
|
||||
*/
|
||||
function rest_init() {
|
||||
Rest\Users::init();
|
||||
Rest\Outbox::init();
|
||||
Rest\Inbox::init();
|
||||
Rest\Followers::init();
|
||||
Rest\Following::init();
|
||||
Rest\Webfinger::init();
|
||||
Rest\Comment::init();
|
||||
Rest\Server::init();
|
||||
Rest\Collection::init();
|
||||
Rest\Post::init();
|
||||
( new Rest\Actors_Controller() )->register_routes();
|
||||
( new Rest\Actors_Inbox_Controller() )->register_routes();
|
||||
( new Rest\Application_Controller() )->register_routes();
|
||||
( new Rest\Collections_Controller() )->register_routes();
|
||||
( new Rest\Comments_Controller() )->register_routes();
|
||||
( new Rest\Followers_Controller() )->register_routes();
|
||||
( new Rest\Following_Controller() )->register_routes();
|
||||
( new Rest\Inbox_Controller() )->register_routes();
|
||||
( new Rest\Interaction_Controller() )->register_routes();
|
||||
( new Rest\Moderators_Controller() )->register_routes();
|
||||
( new Rest\Outbox_Controller() )->register_routes();
|
||||
( new Rest\Replies_Controller() )->register_routes();
|
||||
( new Rest\URL_Validator_Controller() )->register_routes();
|
||||
( new Rest\Webfinger_Controller() )->register_routes();
|
||||
|
||||
// load NodeInfo endpoints only if blog is public
|
||||
// Load NodeInfo endpoints only if blog is public.
|
||||
if ( is_blog_public() ) {
|
||||
Rest\NodeInfo::init();
|
||||
( new Rest\Nodeinfo_Controller() )->register_routes();
|
||||
}
|
||||
}
|
||||
\add_action( 'rest_api_init', __NAMESPACE__ . '\rest_init' );
|
||||
@ -71,16 +67,18 @@ function rest_init() {
|
||||
* Initialize plugin.
|
||||
*/
|
||||
function plugin_init() {
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Activitypub', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Activity_Dispatcher', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Admin', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Health_Check', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Comment', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Dispatcher', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Handler', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Hashtag', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Link', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Mailer', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Mention', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Migration', 'init' ), 1 );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Move', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Options', 'init' ) );
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Scheduler', 'init' ) );
|
||||
|
||||
if ( site_supports_blocks() ) {
|
||||
\add_action( 'init', array( __NAMESPACE__ . '\Blocks', 'init' ) );
|
||||
@ -91,69 +89,30 @@ function plugin_init() {
|
||||
require_once $debug_file;
|
||||
Debug::init();
|
||||
}
|
||||
|
||||
require_once __DIR__ . '/integration/class-webfinger.php';
|
||||
Integration\Webfinger::init();
|
||||
|
||||
require_once __DIR__ . '/integration/class-nodeinfo.php';
|
||||
Integration\Nodeinfo::init();
|
||||
|
||||
require_once __DIR__ . '/integration/class-enable-mastodon-apps.php';
|
||||
Integration\Enable_Mastodon_Apps::init();
|
||||
}
|
||||
\add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_init' );
|
||||
|
||||
|
||||
/**
|
||||
* Class Autoloader
|
||||
* Initialize plugin admin.
|
||||
*/
|
||||
\spl_autoload_register(
|
||||
function ( $full_class ) {
|
||||
$base_dir = __DIR__ . '/includes/';
|
||||
$base = 'Activitypub\\';
|
||||
function plugin_admin_init() {
|
||||
// Menus are registered before `admin_init`, because of course they are.
|
||||
\add_action( 'admin_menu', array( __NAMESPACE__ . '\WP_Admin\Menu', 'admin_menu' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Admin', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Health_Check', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Settings', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Settings_Fields', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Welcome_Fields', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Advanced_Settings_Fields', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\Blog_Settings_Fields', 'init' ) );
|
||||
\add_action( 'admin_init', array( __NAMESPACE__ . '\WP_Admin\User_Settings_Fields', 'init' ) );
|
||||
|
||||
if ( strncmp( $full_class, $base, strlen( $base ) ) === 0 ) {
|
||||
$maybe_uppercase = str_replace( $base, '', $full_class );
|
||||
$class = strtolower( $maybe_uppercase );
|
||||
// All classes should be capitalized. If this is instead looking for a lowercase method, we ignore that.
|
||||
if ( $maybe_uppercase === $class ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( false !== strpos( $class, '\\' ) ) {
|
||||
$parts = explode( '\\', $class );
|
||||
$class = array_pop( $parts );
|
||||
$sub_dir = strtr( implode( '/', $parts ), '_', '-' );
|
||||
$base_dir = $base_dir . $sub_dir . '/';
|
||||
}
|
||||
|
||||
$filename = 'class-' . strtr( $class, '_', '-' );
|
||||
$file = $base_dir . $filename . '.php';
|
||||
|
||||
if ( file_exists( $file ) && is_readable( $file ) ) {
|
||||
require_once $file;
|
||||
} else {
|
||||
// translators: %s is the class name
|
||||
\wp_die( sprintf( esc_html__( 'Required class not found or not readable: %s', 'activitypub' ), esc_html( $full_class ) ) );
|
||||
}
|
||||
}
|
||||
if ( defined( 'WP_LOAD_IMPORTERS' ) && WP_LOAD_IMPORTERS ) {
|
||||
require_once __DIR__ . '/includes/wp-admin/import/load.php';
|
||||
\add_action( 'admin_init', __NAMESPACE__ . '\WP_Admin\Import\load' );
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Add plugin settings link
|
||||
*/
|
||||
function plugin_settings_link( $actions ) {
|
||||
$settings_link = array();
|
||||
$settings_link[] = \sprintf(
|
||||
'<a href="%1s">%2s</a>',
|
||||
\menu_page_url( 'activitypub', false ),
|
||||
\__( 'Settings', 'activitypub' )
|
||||
);
|
||||
|
||||
return \array_merge( $settings_link, $actions );
|
||||
}
|
||||
\add_filter( 'plugin_action_links_' . plugin_basename( __FILE__ ), __NAMESPACE__ . '\plugin_settings_link' );
|
||||
\add_action( 'plugins_loaded', __NAMESPACE__ . '\plugin_admin_init' );
|
||||
|
||||
\register_activation_hook(
|
||||
__FILE__,
|
||||
@ -163,6 +122,19 @@ function plugin_settings_link( $actions ) {
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Redirect to the welcome page after plugin activation.
|
||||
*
|
||||
* @param string $plugin The plugin basename.
|
||||
*/
|
||||
function activation_redirect( $plugin ) {
|
||||
if ( ACTIVITYPUB_PLUGIN_BASENAME === $plugin ) {
|
||||
\wp_safe_redirect( \admin_url( 'options-general.php?page=activitypub' ) );
|
||||
exit;
|
||||
}
|
||||
}
|
||||
\add_action( 'activated_plugin', __NAMESPACE__ . '\activation_redirect' );
|
||||
|
||||
\register_deactivation_hook(
|
||||
__FILE__,
|
||||
array(
|
||||
@ -179,24 +151,18 @@ function plugin_settings_link( $actions ) {
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Only load code that needs BuddyPress to run once BP is loaded and initialized.
|
||||
*/
|
||||
add_action(
|
||||
'bp_include',
|
||||
function () {
|
||||
require_once __DIR__ . '/integration/class-buddypress.php';
|
||||
Integration\Buddypress::init();
|
||||
},
|
||||
0
|
||||
);
|
||||
|
||||
/**
|
||||
* `get_plugin_data` wrapper
|
||||
* `get_plugin_data` wrapper.
|
||||
*
|
||||
* @return array The plugin metadata array
|
||||
* @deprecated 4.2.0 Use `get_plugin_data` instead.
|
||||
*
|
||||
* @param array $default_headers Optional. The default plugin headers. Default empty array.
|
||||
* @return array The plugin metadata array.
|
||||
*/
|
||||
function get_plugin_meta( $default_headers = array() ) {
|
||||
_deprecated_function( __FUNCTION__, '4.2.0', 'get_plugin_data' );
|
||||
|
||||
if ( ! $default_headers ) {
|
||||
$default_headers = array(
|
||||
'Name' => 'Plugin Name',
|
||||
@ -219,13 +185,22 @@ function get_plugin_meta( $default_headers = array() ) {
|
||||
|
||||
/**
|
||||
* Plugin Version Number used for caching.
|
||||
*
|
||||
* @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly.
|
||||
*/
|
||||
function get_plugin_version() {
|
||||
if ( \defined( 'ACTIVITYPUB_PLUGIN_VERSION' ) ) {
|
||||
return ACTIVITYPUB_PLUGIN_VERSION;
|
||||
}
|
||||
_deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' );
|
||||
|
||||
$meta = get_plugin_meta( array( 'Version' => 'Version' ) );
|
||||
|
||||
return $meta['Version'];
|
||||
return ACTIVITYPUB_PLUGIN_VERSION;
|
||||
}
|
||||
|
||||
// Check for CLI env, to add the CLI commands.
|
||||
if ( defined( 'WP_CLI' ) && WP_CLI ) {
|
||||
WP_CLI::add_command(
|
||||
'activitypub',
|
||||
'\Activitypub\Cli',
|
||||
array(
|
||||
'shortdesc' => 'ActivityPub related commands to manage plugin functionality and the federation of posts and comments.',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
.activitypub-settings {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .notice {
|
||||
max-width: 800px;
|
||||
margin: auto;
|
||||
margin: 0px auto 30px;
|
||||
margin: 0 auto 30px;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .update-nag {
|
||||
margin: 25px 20px 15px 22px;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .wrap {
|
||||
@ -20,6 +24,15 @@
|
||||
border-bottom: 1px solid #dcdcde;
|
||||
}
|
||||
|
||||
.activitypub-settings-header h1 {
|
||||
display: inline-block;
|
||||
font-weight: 600;
|
||||
margin: 0 0.8rem 1rem;
|
||||
font-size: 23px;
|
||||
padding: 9px 0 4px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.activitypub-settings-title-section {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@ -33,11 +46,10 @@
|
||||
}
|
||||
|
||||
.activitypub-settings-tabs-wrapper {
|
||||
display: -ms-inline-grid;
|
||||
-ms-grid-columns: auto auto auto;
|
||||
display: inline-flex;
|
||||
vertical-align: top;
|
||||
display: inline-grid;
|
||||
grid-template-columns: auto auto auto;
|
||||
flex-wrap: nowrap;
|
||||
gap: 0;
|
||||
}
|
||||
|
||||
.activitypub-settings-tab.active {
|
||||
@ -54,6 +66,20 @@
|
||||
transition: box-shadow .5s ease-in-out;
|
||||
}
|
||||
|
||||
.activitypub-settings .row {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.activitypub-settings .row > div {
|
||||
max-width: calc(100% - 24px);
|
||||
display: inline-flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.activitypub-settings .row .description {
|
||||
margin-top: 0;
|
||||
}
|
||||
|
||||
.wp-header-end {
|
||||
visibility: hidden;
|
||||
margin: -2px 0 0;
|
||||
@ -168,8 +194,7 @@ input.blog-user-identifier {
|
||||
background-size: cover;
|
||||
}
|
||||
|
||||
.activitypub-settings
|
||||
.logo {
|
||||
.activitypub-settings .logo {
|
||||
height: 80px;
|
||||
width: 80px;
|
||||
position: relative;
|
||||
@ -177,23 +202,71 @@ input.blog-user-identifier {
|
||||
left: 40px;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .box {
|
||||
border: 1px solid #c3c4c7;
|
||||
background-color: #fff;
|
||||
padding: 1em 1.5em;
|
||||
margin-bottom: 1.5em;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .activitypub-welcome-page .box label {
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .activitypub-welcome-page input {
|
||||
font-size: 20px;
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.settings_page_activitypub .plugin-recommendations {
|
||||
border-bottom: none;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
#dashboard_right_now li a.activitypub-followers::before {
|
||||
content: "\f307";
|
||||
font-family: dashicons;
|
||||
}
|
||||
|
||||
.repost .dashboard-comment-wrap,
|
||||
.like .dashboard-comment-wrap {
|
||||
padding-inline-start: 63px;
|
||||
}
|
||||
|
||||
.repost .dashboard-comment-wrap .comment-author,
|
||||
.like .dashboard-comment-wrap .comment-author {
|
||||
margin-block: 0;
|
||||
}
|
||||
|
||||
.activitypub-settings .welcome-tab-close {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
right: 0px;
|
||||
font-size: 13px;
|
||||
padding: 0 5px 0 20px;
|
||||
text-decoration: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.activitypub-settings .welcome-tab-close::before {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
left: 0;
|
||||
transition: all .1s ease-in-out;
|
||||
font: normal 16px/20px dashicons;
|
||||
content: '\f335';
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.activitypub-notice .count {
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
box-sizing: border-box;
|
||||
margin: 1px 0 -1px 2px;
|
||||
padding: 0 5px;
|
||||
min-width: 18px;
|
||||
height: 18px;
|
||||
border-radius: 9px;
|
||||
background-color: #dba617;
|
||||
color: #fff;
|
||||
font-size: 11px;
|
||||
line-height: 1.6;
|
||||
text-align: center;
|
||||
z-index: 26;
|
||||
}
|
||||
|
||||
.activitypub-notice .dashicons-warning {
|
||||
color: #dba617;
|
||||
}
|
||||
|
||||
.extra-fields-nav a + a {
|
||||
margin-left: 8px;
|
||||
}
|
||||
.rtl .extra-fields-nav a + a {
|
||||
margin-left: auto;
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
115
wp-content/plugins/activitypub/assets/css/activitypub-embed.css
Normal file
115
wp-content/plugins/activitypub/assets/css/activitypub-embed.css
Normal file
@ -0,0 +1,115 @@
|
||||
/**
|
||||
* ActivityPub embed styles.
|
||||
*/
|
||||
|
||||
.activitypub-embed {
|
||||
background: #fff;
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 12px;
|
||||
padding: 0;
|
||||
max-width: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
.activitypub-reply-block .activitypub-embed {
|
||||
margin: 1em 0;
|
||||
}
|
||||
|
||||
.activitypub-embed-header {
|
||||
padding: 15px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.activitypub-embed-header img {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.activitypub-embed-header-text {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.activitypub-embed-header-text h2 {
|
||||
color: #000;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.activitypub-embed-header-text .ap-account {
|
||||
color: #687684;
|
||||
font-size: 14px;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.activitypub-embed-content {
|
||||
padding: 0 15px 15px;
|
||||
}
|
||||
|
||||
.activitypub-embed-content .ap-title {
|
||||
font-size: 23px;
|
||||
font-weight: 600;
|
||||
margin: 0 0 10px;
|
||||
padding: 0;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.activitypub-embed-content .ap-subtitle {
|
||||
font-size: 15px;
|
||||
color: #000;
|
||||
margin: 0 0 15px;
|
||||
}
|
||||
|
||||
.activitypub-embed-content .ap-preview {
|
||||
border: 1px solid #e6e6e6;
|
||||
border-radius: 8px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.activitypub-embed-content .ap-preview img {
|
||||
width: 100%;
|
||||
height: auto;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.activitypub-embed-content .ap-preview-text {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
.activitypub-embed-meta {
|
||||
padding: 15px;
|
||||
border-top: 1px solid #e6e6e6;
|
||||
color: #687684;
|
||||
font-size: 13px;
|
||||
display: flex;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.activitypub-embed-meta .ap-stat {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
@media only screen and (max-width: 399px) {
|
||||
.activitypub-embed-meta span.ap-stat {
|
||||
display: none !important;
|
||||
}
|
||||
}
|
||||
|
||||
.activitypub-embed-meta a.ap-stat {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.activitypub-embed-meta strong {
|
||||
font-weight: 600;
|
||||
color: #000;
|
||||
}
|
||||
|
||||
.activitypub-embed-meta .ap-stat-label {
|
||||
color: #687684;
|
||||
}
|
@ -0,0 +1,271 @@
|
||||
/**
|
||||
* Handle the header image setting in
|
||||
*
|
||||
* This is based on site-icon.js
|
||||
*
|
||||
* @see wp-admin/js/site-icon.js
|
||||
*/
|
||||
|
||||
/* global jQuery, wp */
|
||||
|
||||
( function ( $ ) {
|
||||
var $chooseButton = $( '#activitypub-choose-from-library-button' ),
|
||||
$headerImagePreviewWrapper = $( '#activitypub-header-image-preview-wrapper' ),
|
||||
$headerImagePreview = $( '#activitypub-header-image-preview' ),
|
||||
$hiddenDataField = $( '#activitypub_header_image' ),
|
||||
$removeButton = $( '#activitypub-remove-header-image' ),
|
||||
frame,
|
||||
ImageCropperNoCustomizer;
|
||||
|
||||
/**
|
||||
* We register our own handler because the Core one invokes the Customizer, which fails the request unnecessarily
|
||||
* for users who don't have the 'customize' capability.
|
||||
* See https://github.com/Automattic/wordpress-activitypub/issues/846
|
||||
*/
|
||||
ImageCropperNoCustomizer = wp.media.controller.CustomizeImageCropper.extend( {
|
||||
doCrop: function( attachment ) {
|
||||
var cropDetails = attachment.get( 'cropDetails' ),
|
||||
control = this.get( 'control' ),
|
||||
ratio = cropDetails.width / cropDetails.height;
|
||||
|
||||
// Use crop measurements when flexible in both directions.
|
||||
if ( control.params.flex_width && control.params.flex_height ) {
|
||||
cropDetails.dst_width = cropDetails.width;
|
||||
cropDetails.dst_height = cropDetails.height;
|
||||
|
||||
// Constrain flexible side based on image ratio and size of the fixed side.
|
||||
} else {
|
||||
cropDetails.dst_width = control.params.flex_width ? control.params.height * ratio : control.params.width;
|
||||
cropDetails.dst_height = control.params.flex_height ? control.params.width / ratio : control.params.height;
|
||||
}
|
||||
|
||||
return wp.ajax.post( 'crop-image', {
|
||||
// where wp_customize: 'on' would be in Core, for no good reason I understand.
|
||||
nonce: attachment.get( 'nonces' ).edit,
|
||||
id: attachment.get( 'id' ),
|
||||
context: control.id,
|
||||
cropDetails: cropDetails
|
||||
} );
|
||||
}
|
||||
} );
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Calculate image selection options based on the attachment dimensions.
|
||||
*
|
||||
* @since 6.5.0
|
||||
*
|
||||
* @param {Object} attachment The attachment object representing the image.
|
||||
* @return {Object} The image selection options.
|
||||
*/
|
||||
function calculateImageSelectOptions( attachment ) {
|
||||
var realWidth = attachment.get( 'width' ),
|
||||
realHeight = attachment.get( 'height' ),
|
||||
xInit = 1500,
|
||||
yInit = 500,
|
||||
ratio = xInit / yInit,
|
||||
xImg = xInit,
|
||||
yImg = yInit,
|
||||
x1,
|
||||
y1,
|
||||
imgSelectOptions;
|
||||
|
||||
if ( realWidth / realHeight > ratio ) {
|
||||
yInit = realHeight;
|
||||
xInit = yInit * ratio;
|
||||
} else {
|
||||
xInit = realWidth;
|
||||
yInit = xInit / ratio;
|
||||
}
|
||||
|
||||
x1 = ( realWidth - xInit ) / 2;
|
||||
y1 = ( realHeight - yInit ) / 2;
|
||||
|
||||
imgSelectOptions = {
|
||||
aspectRatio: xInit + ':' + yInit,
|
||||
handles: true,
|
||||
keys: true,
|
||||
instance: true,
|
||||
persistent: true,
|
||||
imageWidth: realWidth,
|
||||
imageHeight: realHeight,
|
||||
minWidth: xImg > xInit ? xInit : xImg,
|
||||
minHeight: yImg > yInit ? yInit : yImg,
|
||||
x1: x1,
|
||||
y1: y1,
|
||||
x2: xInit + x1,
|
||||
y2: yInit + y1,
|
||||
};
|
||||
|
||||
return imgSelectOptions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initializes the media frame for selecting or cropping an image.
|
||||
*
|
||||
* @since 6.5.0
|
||||
*/
|
||||
$chooseButton.on( 'click', function () {
|
||||
var $el = $( this );
|
||||
var userId = $el.data( 'userId' );
|
||||
var mediaQuery = { type: 'image' };
|
||||
if ( userId ) {
|
||||
mediaQuery.author = userId;
|
||||
}
|
||||
|
||||
// Create the media frame.
|
||||
frame = wp.media( {
|
||||
button: {
|
||||
// Set the text of the button.
|
||||
text: $el.data( 'update' ),
|
||||
|
||||
// Don't close, we might need to crop.
|
||||
close: false,
|
||||
},
|
||||
states: [
|
||||
new wp.media.controller.Library( {
|
||||
title: $el.data( 'choose-text' ),
|
||||
library: wp.media.query( mediaQuery ),
|
||||
date: false,
|
||||
suggestedWidth: $el.data( 'width' ),
|
||||
suggestedHeight: $el.data( 'height' ),
|
||||
} ),
|
||||
new ImageCropperNoCustomizer( {
|
||||
control: {
|
||||
id: 'activitypub-header-image',
|
||||
params: {
|
||||
width: $el.data( 'width' ),
|
||||
height: $el.data( 'height' ),
|
||||
},
|
||||
},
|
||||
imgSelectOptions: calculateImageSelectOptions,
|
||||
} ),
|
||||
],
|
||||
} );
|
||||
|
||||
frame.on( 'cropped', function ( attachment ) {
|
||||
$hiddenDataField.val( attachment.id );
|
||||
switchToUpdate( attachment );
|
||||
frame.close();
|
||||
|
||||
// Start over with a frame that is so fresh and so clean clean.
|
||||
frame = null;
|
||||
} );
|
||||
|
||||
// When an image is selected, run a callback.
|
||||
frame.on( 'select', function () {
|
||||
// Grab the selected attachment.
|
||||
var attachment = frame.state().get( 'selection' ).first(),
|
||||
targetRatio = $el.data( 'width' ) / $el.data( 'height' ),
|
||||
currentRatio = attachment.attributes.width / attachment.attributes.height,
|
||||
alreadyCropped = false;
|
||||
|
||||
// Check if the image already has the correct aspect ratio (with a small tolerance).
|
||||
if ( Math.abs( currentRatio - targetRatio ) < 0.01 ) {
|
||||
// Check if this is the same image that was already selected.
|
||||
if ( attachment.id !== parseInt( $hiddenDataField.val(), 10 ) ) {
|
||||
// This is a new image with the correct aspect ratio.
|
||||
$hiddenDataField.val( attachment.id );
|
||||
}
|
||||
|
||||
alreadyCropped = true;
|
||||
}
|
||||
|
||||
if ( alreadyCropped ) {
|
||||
// Skip cropping for already cropped images.
|
||||
switchToUpdate( attachment.attributes );
|
||||
frame.close();
|
||||
} else {
|
||||
frame.setState( 'cropper' );
|
||||
}
|
||||
} );
|
||||
|
||||
frame.open();
|
||||
} );
|
||||
|
||||
/**
|
||||
* Update the UI when a header is selected.
|
||||
*
|
||||
* @since 6.5.0
|
||||
*
|
||||
* @param {array} attributes The attributes for the attachment.
|
||||
*/
|
||||
function switchToUpdate( attributes ) {
|
||||
var i18nAppAlternativeString, i18nBrowserAlternativeString;
|
||||
|
||||
if ( attributes.alt ) {
|
||||
i18nBrowserAlternativeString = wp.i18n.sprintf(
|
||||
/* translators: %s: The selected image alt text. */
|
||||
wp.i18n.__( 'Header Image preview: Current image: %s' ),
|
||||
attributes.alt
|
||||
);
|
||||
} else {
|
||||
i18nAppAlternativeString = wp.i18n.sprintf(
|
||||
/* translators: %s: The selected image filename. */
|
||||
wp.i18n.__(
|
||||
'Header Image preview: The current image has no alternative text. The file name is: %s'
|
||||
),
|
||||
attributes.filename
|
||||
);
|
||||
i18nBrowserAlternativeString = wp.i18n.sprintf(
|
||||
/* translators: %s: The selected image filename. */
|
||||
wp.i18n.__(
|
||||
'Header Image preview: The current image has no alternative text. The file name is: %s'
|
||||
),
|
||||
attributes.filename
|
||||
);
|
||||
}
|
||||
|
||||
// Set activitypub-header-image-preview src.
|
||||
$headerImagePreview.attr( {
|
||||
src: attributes.url,
|
||||
alt: i18nAppAlternativeString,
|
||||
} );
|
||||
|
||||
// Remove hidden class from header image preview div and remove button.
|
||||
$headerImagePreviewWrapper.removeClass( 'hidden' );
|
||||
$removeButton.removeClass( 'hidden' );
|
||||
|
||||
// If the choose button is not in the update state, swap the classes.
|
||||
if ( $chooseButton.attr( 'data-state' ) !== '1' ) {
|
||||
$chooseButton.attr( {
|
||||
class: $chooseButton.attr( 'data-alt-classes' ),
|
||||
'data-alt-classes': $chooseButton.attr( 'class' ),
|
||||
'data-state': '1',
|
||||
} );
|
||||
}
|
||||
|
||||
// Swap the text of the choose button.
|
||||
$chooseButton.text( $chooseButton.attr( 'data-update-text' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the click event of the remove button.
|
||||
*
|
||||
* @since 6.5.0
|
||||
*/
|
||||
$removeButton.on( 'click', function () {
|
||||
$hiddenDataField.val( 'false' );
|
||||
$( this ).toggleClass( 'hidden' );
|
||||
$headerImagePreviewWrapper.toggleClass( 'hidden' );
|
||||
$headerImagePreview.attr( {
|
||||
src: '',
|
||||
alt: '',
|
||||
} );
|
||||
|
||||
/**
|
||||
* Resets state to the button, for correct visual style and state.
|
||||
* Updates the text of the button.
|
||||
* Sets focus state to the button.
|
||||
*/
|
||||
$chooseButton
|
||||
.attr( {
|
||||
class: $chooseButton.attr( 'data-alt-classes' ),
|
||||
'data-alt-classes': $chooseButton.attr( 'class' ),
|
||||
'data-state': '',
|
||||
} )
|
||||
.text( $chooseButton.attr( 'data-choose-text' ) )
|
||||
.trigger( 'focus' );
|
||||
} );
|
||||
} )( jQuery );
|
@ -0,0 +1,8 @@
|
||||
{
|
||||
"name": "editor-plugin",
|
||||
"title": "Editor Plugin: not a block, but block.json is very useful.",
|
||||
"category": "widgets",
|
||||
"icon": "admin-comments",
|
||||
"keywords": [],
|
||||
"editorScript": "file:./plugin.js"
|
||||
}
|
@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-components', 'wp-core-data', 'wp-data', 'wp-editor', 'wp-element', 'wp-i18n', 'wp-plugins', 'wp-primitives', 'wp-url'), 'version' => '293b8e75ac7a589c5096');
|
File diff suppressed because one or more lines are too long
@ -36,8 +36,29 @@
|
||||
"selectedUser": {
|
||||
"type": "string",
|
||||
"default": "site"
|
||||
},
|
||||
"buttonOnly": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"buttonText": {
|
||||
"type": "string",
|
||||
"default": "Follow"
|
||||
},
|
||||
"buttonSize": {
|
||||
"type": "string",
|
||||
"default": "default",
|
||||
"enum": [
|
||||
"small",
|
||||
"default",
|
||||
"compact"
|
||||
]
|
||||
}
|
||||
},
|
||||
"usesContext": [
|
||||
"postType",
|
||||
"postId"
|
||||
],
|
||||
"editorScript": "file:./index.js",
|
||||
"viewScript": "file:./view.js",
|
||||
"style": [
|
||||
|
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '0708145714d72862bff0');
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '8f1a6f7e5f76d58a3204');
|
||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-right:1rem;padding-left:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-left:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white)}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow:not(:only-child){margin-right:1rem}
|
@ -1 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border:1px solid var(--wp--preset--color--black);border-radius:inherit 0;color:var(--wp--preset--color--black);flex:1;padding:6px 12px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white);margin-left:0;text-decoration:none}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-left:1rem;padding-right:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white);margin-left:1rem}
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-follow-me-block-wrapper{width:100%}.activitypub-follow-me-block-wrapper.has-background .activitypub-profile,.activitypub-follow-me-block-wrapper.has-border-color .activitypub-profile{padding-left:1rem;padding-right:1rem}.activitypub-follow-me-block-wrapper .activitypub-profile{align-items:center;display:flex;padding:1rem 0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__avatar{border-radius:50%;height:75px;margin-right:1rem;width:75px}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__content{flex:1;min-width:0}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__handle,.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{line-height:1.2;margin:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__name{font-size:1.25em}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white)}.activitypub-follow-me-block-wrapper .activitypub-profile .activitypub-profile__follow:not(:only-child){margin-left:1rem}
|
||||
|
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => 'cbc379fca374f5f88e22');
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '635ed3e6db3230ae865f');
|
||||
|
File diff suppressed because one or more lines are too long
@ -33,6 +33,10 @@
|
||||
]
|
||||
}
|
||||
},
|
||||
"usesContext": [
|
||||
"postType",
|
||||
"postId"
|
||||
],
|
||||
"styles": [
|
||||
{
|
||||
"name": "default",
|
||||
|
@ -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' => '536d43b3eaab93a5c9ef');
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-core-data', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => 'e98a40c18060cbb88187');
|
||||
|
@ -1,3 +1,4 @@
|
||||
(()=>{var e={942:(e,t)=>{var a;!function(){"use strict";var n={}.hasOwnProperty;function r(){for(var e="",t=0;t<arguments.length;t++){var a=arguments[t];a&&(e=o(e,l(a)))}return e}function l(e){if("string"==typeof e||"number"==typeof e)return e;if("object"!=typeof e)return"";if(Array.isArray(e))return r.apply(null,e);if(e.toString!==Object.prototype.toString&&!e.toString.toString().includes("[native code]"))return e.toString();var t="";for(var a in e)n.call(e,a)&&e[a]&&(t=o(t,a));return t}function o(e,t){return t?e?e+" "+t:e+t:e}e.exports?(r.default=r,e.exports=r):void 0===(a=function(){return r}.apply(t,[]))||(e.exports=a)}()}},t={};function a(n){var r=t[n];if(void 0!==r)return r.exports;var l=t[n]={exports:{}};return e[n](l,l.exports,a),l.exports}a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var 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.React,n=window.wp.primitives,r=(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"})),l=window.wp.components,o=window.wp.element,i=window.wp.blockEditor,c=window.wp.i18n,s=window.wp.apiFetch;var p=a.n(s);const u=window.wp.url;var v=a(942),m=a.n(v);function w({active:e,children:a,page:n,pageClick:r,className:l}){const o=m()("wp-block activitypub-pager",l,{current:e});return(0,t.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&r(n)}},a)}const b={outlined:"outlined",minimal:"minimal"};function d({compact:e,nextLabel:a,page:n,pageClick:r,perPage:l,prevLabel:o,total:i,variant:c=b.outlined}){const s=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,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})(n,Math.ceil(i/l)),p=m()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${c}`,{"is-compact":e});return(0,t.createElement)("nav",{className:p},o&&(0,t.createElement)(w,{key:"prev",page:n-1,pageClick:r,active:1===n,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,t.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},s.map((e=>(0,t.createElement)(w,{key:e,page:e,pageClick:r,active:e===n,className:"page-numbers"},e)))),a&&(0,t.createElement)(w,{key:"next",page:n+1,pageClick:r,active:n===Math.ceil(i/l),"aria-label":a,className:"wp-block-query-pagination-next block-editor-block-list__block"},a))}const{namespace:f}=window._activityPubOptions;function g({selectedUser:e,per_page:a,order:n,title:r,page:l,setPage:i,className:s="",followLinks:v=!0,followerData:m=!1}){const w="site"===e?0:e,[b,g]=(0,t.useState)([]),[k,h]=(0,t.useState)(0),[E,_]=(0,t.useState)(0),[x,C]=function(){const[e,a]=(0,t.useState)(1);return[e,a]}(),S=l||x,N=i||C,P=(0,o.createInterpolateElement)(/* translators: arrow for previous followers link */ /* 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"})}),L=(0,o.createInterpolateElement)(/* translators: arrow for next followers link */ /* 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"})}),O=(e,t)=>{g(e),_(t),h(Math.ceil(t/a))};return(0,t.useEffect)((()=>{if(m&&1===S)return O(m.followers,m.total);const e=function(e,t,a,n){const r=`/${f}/users/${e}/followers`,l={per_page:t,order:a,page:n,context:"full"};return(0,u.addQueryArgs)(r,l)}(w,a,n,S);p()({path:e}).then((e=>O(e.orderedItems,e.totalItems))).catch((()=>{}))}),[w,a,n,S,m]),(0,t.createElement)("div",{className:"activitypub-follower-block "+s},(0,t.createElement)("h3",null,r),(0,t.createElement)("ul",null,b&&b.map((e=>(0,t.createElement)("li",{key:e.url},(0,t.createElement)(y,{...e,followLinks:v}))))),k>1&&(0,t.createElement)(d,{page:S,perPage:a,total:E,pageClick:N,nextLabel:L,prevLabel:P,compact:"is-style-compact"===s}))}function y({name:e,icon:a,url:n,preferredUsername:r,followLinks:o=!0}){const i=`@${r}`,c={};return o||(c.onClick=e=>e.preventDefault()),(0,t.createElement)(l.ExternalLink,{className:"activitypub-link",href:n,title:i,...c},(0,t.createElement)("img",{width:"40",height:"40",src:a.url,class:"avatar activitypub-avatar",alt:e}),(0,t.createElement)("span",{class:"activitypub-actor"},(0,t.createElement)("strong",{className:"activitypub-name"},e),(0,t.createElement)("span",{class:"sep"},"/"),(0,t.createElement)("span",{class:"activitypub-handle"},i)))}const k=window.wp.data,h=window._activityPubOptions?.enabled;(0,e.registerBlockType)("activitypub/followers",{edit:function({attributes:e,setAttributes:a}){const{order:n,per_page:r,selectedUser:s,title:p}=e,u=(0,i.useBlockProps)(),[v,m]=(0,o.useState)(1),w=[{label:(0,c.__)("New to old","activitypub"),value:"desc"},{label:(0,c.__)("Old to new","activitypub"),value:"asc"}],b=function(){const e=h?.users?(0,k.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,o.useMemo)((()=>{if(!e)return[];const t=h?.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])}(),d=e=>t=>{m(1),a({[e]:t})};return(0,o.useEffect)((()=>{b.length&&(b.find((({value:e})=>e===s))||a({selectedUser:b[0].value}))}),[s,b]),(0,t.createElement)("div",{...u},(0,t.createElement)(i.InspectorControls,{key:"setting"},(0,t.createElement)(l.PanelBody,{title:(0,c.__)("Followers Options","activitypub")},(0,t.createElement)(l.TextControl,{label:(0,c.__)("Title","activitypub"),help:(0,c.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:p,onChange:e=>a({title:e})}),b.length>1&&(0,t.createElement)(l.SelectControl,{label:(0,c.__)("Select User","activitypub"),value:s,options:b,onChange:d("selectedUser")}),(0,t.createElement)(l.SelectControl,{label:(0,c.__)("Sort","activitypub"),value:n,options:w,onChange:d("order")}),(0,t.createElement)(l.RangeControl,{label:(0,c.__)("Number of Followers","activitypub"),value:r,onChange:d("per_page"),min:1,max:10}))),(0,t.createElement)(g,{...e,page:v,setPage:m,followLinks:!1}))},save:()=>null,icon:r})})()})();
|
||||
(()=>{var e={20:(e,t,a)=>{"use strict";var r=a(609),n=Symbol.for("react.element"),l=(Symbol.for("react.fragment"),Object.prototype.hasOwnProperty),o=r.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED.ReactCurrentOwner,i={key:!0,ref:!0,__self:!0,__source:!0};t.jsx=function(e,t,a){var r,c={},s=null,p=null;for(r in void 0!==a&&(s=""+a),void 0!==t.key&&(s=""+t.key),void 0!==t.ref&&(p=t.ref),t)l.call(t,r)&&!i.hasOwnProperty(r)&&(c[r]=t[r]);if(e&&e.defaultProps)for(r in t=e.defaultProps)void 0===c[r]&&(c[r]=t[r]);return{$$typeof:n,type:e,key:s,ref:p,props:c,_owner:o.current}}},848:(e,t,a)=>{"use strict";e.exports=a(20)},609:e=>{"use strict";e.exports=window.React},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t<arguments.length;t++){var a=arguments[t];a&&(e=o(e,l(a)))}return e}function l(e){if("string"==typeof e||"number"==typeof e)return e;if("object"!=typeof e)return"";if(Array.isArray(e))return n.apply(null,e);if(e.toString!==Object.prototype.toString&&!e.toString.toString().includes("[native code]"))return e.toString();var t="";for(var a in e)r.call(e,a)&&e[a]&&(t=o(t,a));return t}function o(e,t){return t?e?e+" "+t:e+t:e}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},t={};function a(r){var n=t[r];if(void 0!==n)return n.exports;var l=t[r]={exports:{}};return e[r](l,l.exports,a),l.exports}a.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return a.d(t,{a:t}),t},a.d=(e,t)=>{for(var r in t)a.o(t,r)&&!a.o(e,r)&&Object.defineProperty(e,r,{enumerable:!0,get:t[r]})},a.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{"use strict";const e=window.wp.blocks,t=window.wp.primitives;var r=a(848);const n=(0,r.jsx)(t.SVG,{xmlns:"http://www.w3.org/2000/svg",viewBox:"0 0 24 24",children:(0,r.jsx)(t.Path,{d:"M15.5 9.5a1 1 0 100-2 1 1 0 000 2zm0 1.5a2.5 2.5 0 100-5 2.5 2.5 0 000 5zm-2.25 6v-2a2.75 2.75 0 00-2.75-2.75h-4A2.75 2.75 0 003.75 15v2h1.5v-2c0-.69.56-1.25 1.25-1.25h4c.69 0 1.25.56 1.25 1.25v2h1.5zm7-2v2h-1.5v-2c0-.69-.56-1.25-1.25-1.25H15v-1.5h2.5A2.75 2.75 0 0120.25 15zM9.5 8.5a1 1 0 11-2 0 1 1 0 012 0zm1.5 0a2.5 2.5 0 11-5 0 2.5 2.5 0 015 0z",fillRule:"evenodd"})});var l=a(609);const o=window.wp.components,i=window.wp.element,c=window.wp.blockEditor,s=window.wp.data,p=window.wp.coreData,u=window.wp.i18n,m=window.wp.apiFetch;var v=a.n(m);const d=window.wp.url;var w=a(942),b=a.n(w);function f({active:e,children:t,page:a,pageClick:r,className:n}){const o=b()("wp-block activitypub-pager",n,{current:e});return(0,l.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&r(a)}},t)}function y({compact:e,nextLabel:t,page:a,pageClick:r,perPage:n,prevLabel:o,total:i,variant:c="outlined"}){const s=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(i/n)),p=b()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${c}`,{"is-compact":e});return(0,l.createElement)("nav",{className:p},o&&(0,l.createElement)(f,{key:"prev",page:a-1,pageClick:r,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,l.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},s.map((e=>(0,l.createElement)(f,{key:e,page:e,pageClick:r,active:e===a,className:"page-numbers"},e)))),t&&(0,l.createElement)(f,{key:"next",page:a+1,pageClick:r,active:a===Math.ceil(i/n),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}function g(){return window._activityPubOptions||{}}function h({selectedUser:e,per_page:t,order:a,title:r,page:n,setPage:o,className:c="",followLinks:s=!0,followerData:p=!1}){const m="site"===e?0:e,[w,b]=(0,l.useState)([]),[f,h]=(0,l.useState)(0),[k,E]=(0,l.useState)(0),[x,S]=function(){const[e,t]=(0,l.useState)(1);return[e,t]}(),N=n||x,C=o||S,O=(0,i.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */
|
||||
(0,u.__)("<span>←</span> Less","activitypub"),{span:(0,l.createElement)("span",{className:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),P=(0,i.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */
|
||||
(0,u.__)("More <span>→</span>","activitypub"),{span:(0,l.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),I=(e,a)=>{b(e),E(a),h(Math.ceil(a/t))};return(0,l.useEffect)((()=>{if(p&&1===N)return I(p.followers,p.total);const e=function(e,t,a,r){const{namespace:n}=g(),l=`/${n}/actors/${e}/followers`,o={per_page:t,order:a,page:r,context:"full"};return(0,d.addQueryArgs)(l,o)}(m,t,a,N);v()({path:e}).then((e=>I(e.orderedItems,e.totalItems))).catch((()=>{}))}),[m,t,a,N,p]),(0,l.createElement)("div",{className:"activitypub-follower-block "+c},(0,l.createElement)("h3",null,r),(0,l.createElement)("ul",null,w&&w.map((e=>(0,l.createElement)("li",{key:e.url},(0,l.createElement)(_,{...e,followLinks:s}))))),f>1&&(0,l.createElement)(y,{page:N,perPage:t,total:k,pageClick:C,nextLabel:P,prevLabel:O,compact:"is-style-compact"===c}))}function _({name:e,icon:t,url:a,preferredUsername:r,followLinks:n=!0}){const i=`@${r}`,c={};return n||(c.onClick=e=>e.preventDefault()),(0,l.createElement)(o.ExternalLink,{className:"activitypub-link",href:a,title:i,...c},(0,l.createElement)("img",{width:"40",height:"40",src:t.url,className:"avatar activitypub-avatar",alt:e}),(0,l.createElement)("span",{className:"activitypub-actor"},(0,l.createElement)("strong",{className:"activitypub-name"},e),(0,l.createElement)("span",{className:"sep"},"/"),(0,l.createElement)("span",{className:"activitypub-handle"},i)))}function k({name:e}){const{enabled:t}=g(),a=t?.site?"":(0,u.__)("It will be empty in other non-author contexts.","activitypub"),r=(0,u.sprintf)(/* translators: %1$s: block name, %2$s: extra information for non-author context */ /* translators: %1$s: block name, %2$s: extra information for non-author context */
|
||||
(0,u.__)("This <strong>%1$s</strong> block will adapt to the page it is on, displaying the user profile associated with a post author (in a loop) or a user archive. %2$s","activitypub"),e,a).trim();return(0,l.createElement)(o.Card,null,(0,l.createElement)(o.CardBody,null,(0,i.createInterpolateElement)(r,{strong:(0,l.createElement)("strong",null)})))}(0,e.registerBlockType)("activitypub/followers",{edit:function({attributes:e,setAttributes:t,context:{postType:a,postId:r}}){const{order:n,per_page:m,selectedUser:v,title:d}=e,w=(0,c.useBlockProps)(),[b,f]=(0,i.useState)(1),y=[{label:(0,u.__)("New to old","activitypub"),value:"desc"},{label:(0,u.__)("Old to new","activitypub"),value:"asc"}],_=function({withInherit:e=!1}){const{enabled:t}=g(),a=t?.users?(0,s.useSelect)((e=>e("core").getUsers({who:"authors"}))):[];return(0,i.useMemo)((()=>{if(!a)return[];const r=[];return t?.site&&r.push({label:(0,u.__)("Site","activitypub"),value:"site"}),e&&t?.users&&r.push({label:(0,u.__)("Dynamic User","activitypub"),value:"inherit"}),a.reduce(((e,t)=>(e.push({label:t.name,value:`${t.id}`}),e)),r)}),[a])}({withInherit:!0}),E=e=>a=>{f(1),t({[e]:a})},x=(0,s.useSelect)((e=>{const{getEditedEntityRecord:t}=e(p.store),n=t("postType",a,r)?.author;return null!=n?n:null}),[a,r]);return(0,i.useEffect)((()=>{_.length&&(_.find((({value:e})=>e===v))||t({selectedUser:_[0].value}))}),[v,_]),(0,l.createElement)("div",{...w},(0,l.createElement)(c.InspectorControls,{key:"setting"},(0,l.createElement)(o.PanelBody,{title:(0,u.__)("Followers Options","activitypub")},(0,l.createElement)(o.TextControl,{label:(0,u.__)("Title","activitypub"),help:(0,u.__)("Title to display above the list of followers. Blank for none.","activitypub"),value:d,onChange:e=>t({title:e})}),_.length>1&&(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Select User","activitypub"),value:v,options:_,onChange:E("selectedUser")}),(0,l.createElement)(o.SelectControl,{label:(0,u.__)("Sort","activitypub"),value:n,options:y,onChange:E("order")}),(0,l.createElement)(o.RangeControl,{label:(0,u.__)("Number of Followers","activitypub"),value:m,onChange:E("per_page"),min:1,max:10}))),"inherit"===v?x?(0,l.createElement)(h,{...e,page:b,setPage:f,followLinks:!1,selectedUser:x}):(0,l.createElement)(k,{name:(0,u.__)("Followers","activitypub")}):(0,l.createElement)(h,{...e,page:b,setPage:f,followLinks:!1}))},save:()=>null,icon:n})})()})();
|
@ -0,0 +1 @@
|
||||
.activitypub-follower-block.is-style-compact .activitypub-handle,.activitypub-follower-block.is-style-compact .sep{display:none}.activitypub-follower-block.is-style-with-lines ul li{border-bottom:.5px solid;margin-bottom:.5rem;padding-bottom:.5rem}.activitypub-follower-block.is-style-with-lines ul li:last-child{border-bottom:none}.activitypub-follower-block.is-style-with-lines .activitypub-handle,.activitypub-follower-block.is-style-with-lines .activitypub-name{text-decoration:none}.activitypub-follower-block.is-style-with-lines .activitypub-handle:hover,.activitypub-follower-block.is-style-with-lines .activitypub-name:hover{text-decoration:underline}.activitypub-follower-block ul{margin:0!important;padding:0!important}.activitypub-follower-block li{display:flex;margin-bottom:1rem}.activitypub-follower-block img{border-radius:50%;height:40px;margin-left:var(--wp--preset--spacing--20,.5rem);width:40px}.activitypub-follower-block .activitypub-link{align-items:center;color:inherit!important;display:flex;flex-flow:row nowrap;max-width:100%;text-decoration:none!important}.activitypub-follower-block .activitypub-handle,.activitypub-follower-block .activitypub-name{text-decoration:underline;text-decoration-thickness:.8px;text-underline-position:under}.activitypub-follower-block .activitypub-handle:hover,.activitypub-follower-block .activitypub-name:hover{text-decoration:none}.activitypub-follower-block .activitypub-name{font-size:var(--wp--preset--font-size--normal,16px)}.activitypub-follower-block .activitypub-actor{font-size:var(--wp--preset--font-size--small,13px);overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.activitypub-follower-block .sep{padding:0 .2rem}.activitypub-follower-block .wp-block-query-pagination{margin-top:1.5rem}.activitypub-follower-block .activitypub-pager{cursor:default}.activitypub-follower-block .activitypub-pager.current{opacity:.33}.activitypub-follower-block .page-numbers{padding:0 .2rem}.activitypub-follower-block .page-numbers.current{font-weight:700;opacity:1}
|
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '23bc54443801976420cd');
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-url'), 'version' => '34299fc181d49292ada0');
|
||||
|
@ -1,3 +1,3 @@
|
||||
(()=>{var e,t={250:(e,t,a)=>{"use strict";const r=window.React,n=window.wp.apiFetch;var l=a.n(n);const o=window.wp.url,i=window.wp.element,c=window.wp.i18n;var s=a(942),p=a.n(s);function u({active:e,children:t,page:a,pageClick:n,className:l}){const o=p()("wp-block activitypub-pager",l,{current:e});return(0,r.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&n(a)}},t)}const m={outlined:"outlined",minimal:"minimal"};function f({compact:e,nextLabel:t,page:a,pageClick:n,perPage:l,prevLabel:o,total:i,variant:c=m.outlined}){const s=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(i/l)),f=p()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${c}`,{"is-compact":e});return(0,r.createElement)("nav",{className:f},o&&(0,r.createElement)(u,{key:"prev",page:a-1,pageClick:n,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},s.map((e=>(0,r.createElement)(u,{key:e,page:e,pageClick:n,active:e===a,className:"page-numbers"},e)))),t&&(0,r.createElement)(u,{key:"next",page:a+1,pageClick:n,active:a===Math.ceil(i/l),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}const v=window.wp.components,{namespace:b}=window._activityPubOptions;function d({selectedUser:e,per_page:t,order:a,title:n,page:s,setPage:p,className:u="",followLinks:m=!0,followerData:v=!1}){const d="site"===e?0:e,[g,y]=(0,r.useState)([]),[k,h]=(0,r.useState)(0),[E,x]=(0,r.useState)(0),[_,O]=function(){const[e,t]=(0,r.useState)(1);return[e,t]}(),N=s||_,S=p||O,C=(0,i.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */
|
||||
(0,c.__)("<span>←</span> Less","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),L=(0,i.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */
|
||||
(0,c.__)("More <span>→</span>","activitypub"),{span:(0,r.createElement)("span",{class:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),q=(e,a)=>{y(e),x(a),h(Math.ceil(a/t))};return(0,r.useEffect)((()=>{if(v&&1===N)return q(v.followers,v.total);const e=function(e,t,a,r){const n=`/${b}/users/${e}/followers`,l={per_page:t,order:a,page:r,context:"full"};return(0,o.addQueryArgs)(n,l)}(d,t,a,N);l()({path:e}).then((e=>q(e.orderedItems,e.totalItems))).catch((()=>{}))}),[d,t,a,N,v]),(0,r.createElement)("div",{className:"activitypub-follower-block "+u},(0,r.createElement)("h3",null,n),(0,r.createElement)("ul",null,g&&g.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(w,{...e,followLinks:m}))))),k>1&&(0,r.createElement)(f,{page:N,perPage:t,total:E,pageClick:S,nextLabel:L,prevLabel:C,compact:"is-style-compact"===u}))}function w({name:e,icon:t,url:a,preferredUsername:n,followLinks:l=!0}){const o=`@${n}`,i={};return l||(i.onClick=e=>e.preventDefault()),(0,r.createElement)(v.ExternalLink,{className:"activitypub-link",href:a,title:o,...i},(0,r.createElement)("img",{width:"40",height:"40",src:t.url,class:"avatar activitypub-avatar",alt:e}),(0,r.createElement)("span",{class:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},e),(0,r.createElement)("span",{class:"sep"},"/"),(0,r.createElement)("span",{class:"activitypub-handle"},o)))}const g=window.wp.domReady;a.n(g)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,i.render)((0,r.createElement)(d,{...t}),e)}))}))},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t<arguments.length;t++){var a=arguments[t];a&&(e=o(e,l(a)))}return e}function l(e){if("string"==typeof e||"number"==typeof e)return e;if("object"!=typeof e)return"";if(Array.isArray(e))return n.apply(null,e);if(e.toString!==Object.prototype.toString&&!e.toString.toString().includes("[native code]"))return e.toString();var t="";for(var a in e)r.call(e,a)&&e[a]&&(t=o(t,a));return t}function o(e,t){return t?e?e+" "+t:e+t:e}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},a={};function r(e){var n=a[e];if(void 0!==n)return n.exports;var l=a[e]={exports:{}};return t[e](l,l.exports,r),l.exports}r.m=t,e=[],r.O=(t,a,n,l)=>{if(!a){var o=1/0;for(p=0;p<e.length;p++){for(var[a,n,l]=e[p],i=!0,c=0;c<a.length;c++)(!1&l||o>=l)&&Object.keys(r.O).every((e=>r.O[e](a[c])))?a.splice(c--,1):(i=!1,l<o&&(o=l));if(i){e.splice(p--,1);var s=n();void 0!==s&&(t=s)}}return t}l=l||0;for(var p=e.length;p>0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[a,n,l]},r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var a in t)r.o(t,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={996:0,528:0};r.O.j=t=>0===e[t];var t=(t,a)=>{var n,l,[o,i,c]=a,s=0;if(o.some((t=>0!==e[t]))){for(n in i)r.o(i,n)&&(r.m[n]=i[n]);if(c)var p=c(r)}for(t&&t(a);s<o.length;s++)l=o[s],r.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return r.O(p)},a=globalThis.webpackChunkwordpress_activitypub=globalThis.webpackChunkwordpress_activitypub||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var n=r.O(void 0,[528],(()=>r(250)));n=r.O(n)})();
|
||||
(()=>{var e,t={73:(e,t,a)=>{"use strict";const r=window.React,n=window.wp.apiFetch;var l=a.n(n);const o=window.wp.url,c=window.wp.element,i=window.wp.i18n;var s=a(942),p=a.n(s);function u({active:e,children:t,page:a,pageClick:n,className:l}){const o=p()("wp-block activitypub-pager",l,{current:e});return(0,r.createElement)("a",{className:o,onClick:t=>{t.preventDefault(),!e&&n(a)}},t)}function m({compact:e,nextLabel:t,page:a,pageClick:n,perPage:l,prevLabel:o,total:c,variant:i="outlined"}){const s=((e,t)=>{let a=[1,e-2,e-1,e,e+1,e+2,t];a.sort(((e,t)=>e-t)),a=a.filter(((e,a,r)=>e>=1&&e<=t&&r.lastIndexOf(e)===a));for(let e=a.length-2;e>=0;e--)a[e]===a[e+1]&&a.splice(e+1,1);return a})(a,Math.ceil(c/l)),m=p()("alignwide wp-block-query-pagination is-content-justification-space-between is-layout-flex wp-block-query-pagination-is-layout-flex",`is-${i}`,{"is-compact":e});return(0,r.createElement)("nav",{className:m},o&&(0,r.createElement)(u,{key:"prev",page:a-1,pageClick:n,active:1===a,"aria-label":o,className:"wp-block-query-pagination-previous block-editor-block-list__block"},o),!e&&(0,r.createElement)("div",{className:"block-editor-block-list__block wp-block wp-block-query-pagination-numbers"},s.map((e=>(0,r.createElement)(u,{key:e,page:e,pageClick:n,active:e===a,className:"page-numbers"},e)))),t&&(0,r.createElement)(u,{key:"next",page:a+1,pageClick:n,active:a===Math.ceil(c/l),"aria-label":t,className:"wp-block-query-pagination-next block-editor-block-list__block"},t))}const f=window.wp.components;function v({selectedUser:e,per_page:t,order:a,title:n,page:s,setPage:p,className:u="",followLinks:f=!0,followerData:v=!1}){const w="site"===e?0:e,[d,g]=(0,r.useState)([]),[y,k]=(0,r.useState)(0),[h,E]=(0,r.useState)(0),[N,x]=function(){const[e,t]=(0,r.useState)(1);return[e,t]}(),_=s||N,O=p||x,S=(0,c.createInterpolateElement)(/* translators: arrow for previous followers link */ /* translators: arrow for previous followers link */
|
||||
(0,i.__)("<span>←</span> Less","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-previous-arrow is-arrow-arrow","aria-hidden":"true"})}),C=(0,c.createInterpolateElement)(/* translators: arrow for next followers link */ /* translators: arrow for next followers link */
|
||||
(0,i.__)("More <span>→</span>","activitypub"),{span:(0,r.createElement)("span",{className:"wp-block-query-pagination-next-arrow is-arrow-arrow","aria-hidden":"true"})}),L=(e,a)=>{g(e),E(a),k(Math.ceil(a/t))};return(0,r.useEffect)((()=>{if(v&&1===_)return L(v.followers,v.total);const e=function(e,t,a,r){const{namespace:n}=window._activityPubOptions||{},l=`/${n}/actors/${e}/followers`,c={per_page:t,order:a,page:r,context:"full"};return(0,o.addQueryArgs)(l,c)}(w,t,a,_);l()({path:e}).then((e=>L(e.orderedItems,e.totalItems))).catch((()=>{}))}),[w,t,a,_,v]),(0,r.createElement)("div",{className:"activitypub-follower-block "+u},(0,r.createElement)("h3",null,n),(0,r.createElement)("ul",null,d&&d.map((e=>(0,r.createElement)("li",{key:e.url},(0,r.createElement)(b,{...e,followLinks:f}))))),y>1&&(0,r.createElement)(m,{page:_,perPage:t,total:h,pageClick:O,nextLabel:C,prevLabel:S,compact:"is-style-compact"===u}))}function b({name:e,icon:t,url:a,preferredUsername:n,followLinks:l=!0}){const o=`@${n}`,c={};return l||(c.onClick=e=>e.preventDefault()),(0,r.createElement)(f.ExternalLink,{className:"activitypub-link",href:a,title:o,...c},(0,r.createElement)("img",{width:"40",height:"40",src:t.url,className:"avatar activitypub-avatar",alt:e}),(0,r.createElement)("span",{className:"activitypub-actor"},(0,r.createElement)("strong",{className:"activitypub-name"},e),(0,r.createElement)("span",{className:"sep"},"/"),(0,r.createElement)("span",{className:"activitypub-handle"},o)))}const w=window.wp.domReady;a.n(w)()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-follower-block"),(e=>{const t=JSON.parse(e.dataset.attrs);(0,c.createRoot)(e).render((0,r.createElement)(v,{...t}))}))}))},942:(e,t)=>{var a;!function(){"use strict";var r={}.hasOwnProperty;function n(){for(var e="",t=0;t<arguments.length;t++){var a=arguments[t];a&&(e=o(e,l(a)))}return e}function l(e){if("string"==typeof e||"number"==typeof e)return e;if("object"!=typeof e)return"";if(Array.isArray(e))return n.apply(null,e);if(e.toString!==Object.prototype.toString&&!e.toString.toString().includes("[native code]"))return e.toString();var t="";for(var a in e)r.call(e,a)&&e[a]&&(t=o(t,a));return t}function o(e,t){return t?e?e+" "+t:e+t:e}e.exports?(n.default=n,e.exports=n):void 0===(a=function(){return n}.apply(t,[]))||(e.exports=a)}()}},a={};function r(e){var n=a[e];if(void 0!==n)return n.exports;var l=a[e]={exports:{}};return t[e](l,l.exports,r),l.exports}r.m=t,e=[],r.O=(t,a,n,l)=>{if(!a){var o=1/0;for(p=0;p<e.length;p++){a=e[p][0],n=e[p][1],l=e[p][2];for(var c=!0,i=0;i<a.length;i++)(!1&l||o>=l)&&Object.keys(r.O).every((e=>r.O[e](a[i])))?a.splice(i--,1):(c=!1,l<o&&(o=l));if(c){e.splice(p--,1);var s=n();void 0!==s&&(t=s)}}return t}l=l||0;for(var p=e.length;p>0&&e[p-1][2]>l;p--)e[p]=e[p-1];e[p]=[a,n,l]},r.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return r.d(t,{a:t}),t},r.d=(e,t)=>{for(var a in t)r.o(t,a)&&!r.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},r.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={996:0,528:0};r.O.j=t=>0===e[t];var t=(t,a)=>{var n,l,o=a[0],c=a[1],i=a[2],s=0;if(o.some((t=>0!==e[t]))){for(n in c)r.o(c,n)&&(r.m[n]=c[n]);if(i)var p=i(r)}for(t&&t(a);s<o.length;s++)l=o[s],r.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return r.O(p)},a=self.webpackChunkwordpress_activitypub=self.webpackChunkwordpress_activitypub||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var n=r.O(void 0,[528],(()=>r(73)));n=r.O(n)})();
|
37
wp-content/plugins/activitypub/build/reactions/block.json
Normal file
37
wp-content/plugins/activitypub/build/reactions/block.json
Normal file
@ -0,0 +1,37 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"name": "activitypub/reactions",
|
||||
"apiVersion": 2,
|
||||
"version": "1.0.0",
|
||||
"title": "Fediverse Reactions",
|
||||
"category": "widgets",
|
||||
"icon": "heart",
|
||||
"description": "Display Fediverse likes and reposts",
|
||||
"supports": {
|
||||
"html": false,
|
||||
"align": true,
|
||||
"layout": {
|
||||
"default": {
|
||||
"type": "constrained",
|
||||
"orientation": "vertical",
|
||||
"justifyContent": "center"
|
||||
}
|
||||
}
|
||||
},
|
||||
"attributes": {
|
||||
"title": {
|
||||
"type": "string",
|
||||
"default": "Fediverse reactions"
|
||||
}
|
||||
},
|
||||
"blockHooks": {
|
||||
"core/post-content": "after"
|
||||
},
|
||||
"textdomain": "activitypub",
|
||||
"editorScript": "file:./index.js",
|
||||
"style": [
|
||||
"file:./style-index.css",
|
||||
"wp-components"
|
||||
],
|
||||
"viewScript": "file:./view.js"
|
||||
}
|
@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-element', 'wp-i18n'), 'version' => '32631215c76c36b38e5e');
|
3
wp-content/plugins/activitypub/build/reactions/index.js
Normal file
3
wp-content/plugins/activitypub/build/reactions/index.js
Normal file
@ -0,0 +1,3 @@
|
||||
(()=>{"use strict";var e,t={373:(e,t,a)=>{const n=window.wp.blocks,r=window.React,l=window.wp.blockEditor,o=window.wp.element,s=window.wp.i18n,i=window.wp.components,c=window.wp.apiFetch;var u=a.n(c);function m(){return window._activityPubOptions||{}}const p=({reactions:e})=>{const{defaultAvatarUrl:t}=m(),[a,n]=(0,o.useState)(new Set),[l,s]=(0,o.useState)(new Map),i=(0,o.useRef)([]),c=()=>{i.current.forEach((e=>clearTimeout(e))),i.current=[]},u=(t,a)=>{c();const r=100,l=e.length;a&&s((e=>{const a=new Map(e);return a.set(t,"clockwise"),a}));const o=e=>{const o="right"===e,c=o?l-1:0,u=o?1:-1;for(let e=o?t:t-1;o?e<=c:e>=c;e+=u){const l=Math.abs(e-t),o=setTimeout((()=>{n((t=>{const n=new Set(t);return a?n.add(e):n.delete(e),n})),a&&e!==t&&s((t=>{const a=new Map(t),n=e-u,r=a.get(n);return a.set(e,"clockwise"===r?"counter":"clockwise"),a}))}),l*r);i.current.push(o)}};if(o("right"),o("left"),!a){const e=Math.max((l-t)*r,t*r),a=setTimeout((()=>{s(new Map)}),e+r);i.current.push(a)}};return(0,o.useEffect)((()=>()=>c()),[]),(0,r.createElement)("ul",{className:"reaction-avatars"},e.map(((e,n)=>{const o=l.get(n),s=["reaction-avatar",a.has(n)?"wave-active":"",o?`rotate-${o}`:""].filter(Boolean).join(" "),i=e.avatar||t;return(0,r.createElement)("li",{key:n},(0,r.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer",onMouseEnter:()=>u(n,!0),onMouseLeave:()=>u(n,!1)},(0,r.createElement)("img",{src:i,alt:e.name,className:s,width:"32",height:"32"})))})))},f=({reactions:e,type:t})=>(0,r.createElement)("ul",{className:"activitypub-reaction-list"},e.map(((e,t)=>(0,r.createElement)("li",{key:t},(0,r.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,r.createElement)("img",{src:e.avatar,alt:e.name,width:"32",height:"32"}),(0,r.createElement)("span",null,e.name)))))),h=({items:e,label:t})=>{const[a,n]=(0,o.useState)(!1),[l,s]=(0,o.useState)(null),[c,u]=(0,o.useState)(e.length),m=(0,o.useRef)(null);(0,o.useEffect)((()=>{if(!m.current)return;const t=()=>{const t=m.current;if(!t)return;const a=t.offsetWidth-(l?.offsetWidth||0)-12,n=Math.max(1,Math.floor((a-32)/22));u(Math.min(n,e.length))};t();const a=new ResizeObserver(t);return a.observe(m.current),()=>{a.disconnect()}}),[l,e.length]);const h=e.slice(0,c);return(0,r.createElement)("div",{className:"reaction-group",ref:m},(0,r.createElement)(p,{reactions:h}),(0,r.createElement)(i.Button,{ref:s,className:"reaction-label is-link",onClick:()=>n(!a),"aria-expanded":a},t),a&&l&&(0,r.createElement)(i.Popover,{anchor:l,onClose:()=>n(!1)},(0,r.createElement)(f,{reactions:e})))};function d({title:e="",postId:t=null,reactions:a=null,titleComponent:n=null}){const{namespace:l}=m(),[s,i]=(0,o.useState)(a),[c,p]=(0,o.useState)(!a);return(0,o.useEffect)((()=>{if(a)return i(a),void p(!1);t?(p(!0),u()({path:`/${l}/posts/${t}/reactions`}).then((e=>{i(e),p(!1)})).catch((()=>p(!1)))):p(!1)}),[t,a]),c?null:s&&Object.values(s).some((e=>e.items?.length>0))?(0,r.createElement)("div",{className:"activitypub-reactions"},n||e&&(0,r.createElement)("h6",null,e),Object.entries(s).map((([e,t])=>t.items?.length?(0,r.createElement)(h,{key:e,items:t.items,label:t.label}):null))):null}const v=e=>{const t=["#FF6B6B","#4ECDC4","#45B7D1","#96CEB4","#FFEEAD","#D4A5A5","#9B59B6","#3498DB","#E67E22"],a=(()=>{const e=["Bouncy","Cosmic","Dancing","Fluffy","Giggly","Hoppy","Jazzy","Magical","Nifty","Perky","Quirky","Sparkly","Twirly","Wiggly","Zippy"],t=["Badger","Capybara","Dolphin","Echidna","Flamingo","Giraffe","Hedgehog","Iguana","Jellyfish","Koala","Lemur","Manatee","Narwhal","Octopus","Penguin"];return`${e[Math.floor(Math.random()*e.length)]} ${t[Math.floor(Math.random()*t.length)]}`})(),n=t[Math.floor(Math.random()*t.length)],r=a.charAt(0),l=document.createElement("canvas");l.width=64,l.height=64;const o=l.getContext("2d");return o.fillStyle=n,o.beginPath(),o.arc(32,32,32,0,2*Math.PI),o.fill(),o.fillStyle="#FFFFFF",o.font="32px sans-serif",o.textAlign="center",o.textBaseline="middle",o.fillText(r,32,32),{name:a,url:"#",avatar:l.toDataURL()}},g=JSON.parse('{"UU":"activitypub/reactions"}');(0,n.registerBlockType)(g.UU,{edit:function({attributes:e,setAttributes:t,__unstableLayoutClassNames:a}){const n=(0,l.useBlockProps)({className:a}),[i]=(0,o.useState)({likes:{label:(0,s.sprintf)(/* translators: %d: Number of likes */ /* translators: %d: Number of likes */
|
||||
(0,s._x)("%d likes","number of likes","activitypub"),9),items:Array.from({length:9},((e,t)=>v()))},reposts:{label:(0,s.sprintf)(/* translators: %d: Number of reposts */ /* translators: %d: Number of reposts */
|
||||
(0,s._x)("%d reposts","number of reposts","activitypub"),6),items:Array.from({length:6},((e,t)=>v()))}}),c=(0,r.createElement)(l.RichText,{tagName:"h6",value:e.title,onChange:e=>t({title:e}),placeholder:(0,s.__)("Fediverse Reactions","activitypub"),disableLineBreaks:!0,allowedFormats:[]});return(0,r.createElement)("div",{...n},(0,r.createElement)(d,{titleComponent:c,reactions:i}))}})}},a={};function n(e){var r=a[e];if(void 0!==r)return r.exports;var l=a[e]={exports:{}};return t[e](l,l.exports,n),l.exports}n.m=t,e=[],n.O=(t,a,r,l)=>{if(!a){var o=1/0;for(u=0;u<e.length;u++){a=e[u][0],r=e[u][1],l=e[u][2];for(var s=!0,i=0;i<a.length;i++)(!1&l||o>=l)&&Object.keys(n.O).every((e=>n.O[e](a[i])))?a.splice(i--,1):(s=!1,l<o&&(o=l));if(s){e.splice(u--,1);var c=r();void 0!==c&&(t=c)}}return t}l=l||0;for(var u=e.length;u>0&&e[u-1][2]>l;u--)e[u]=e[u-1];e[u]=[a,r,l]},n.n=e=>{var t=e&&e.__esModule?()=>e.default:()=>e;return n.d(t,{a:t}),t},n.d=(e,t)=>{for(var a in t)n.o(t,a)&&!n.o(e,a)&&Object.defineProperty(e,a,{enumerable:!0,get:t[a]})},n.o=(e,t)=>Object.prototype.hasOwnProperty.call(e,t),(()=>{var e={608:0,104:0};n.O.j=t=>0===e[t];var t=(t,a)=>{var r,l,o=a[0],s=a[1],i=a[2],c=0;if(o.some((t=>0!==e[t]))){for(r in s)n.o(s,r)&&(n.m[r]=s[r]);if(i)var u=i(n)}for(t&&t(a);c<o.length;c++)l=o[c],n.o(e,l)&&e[l]&&e[l][0](),e[l]=0;return n.O(u)},a=self.webpackChunkwordpress_activitypub=self.webpackChunkwordpress_activitypub||[];a.forEach(t.bind(null,0)),a.push=t.bind(null,a.push.bind(a))})();var r=n.O(void 0,[104],(()=>n(373)));r=n.O(r)})();
|
@ -0,0 +1 @@
|
||||
.activitypub-reactions h6{border-top:1px solid;border-top-color:var(--wp--preset--color--contrast-2);display:inline-block;padding-top:.5em}.activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75em;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0;padding:0}.activitypub-reactions .reaction-avatars li{margin:0 0 0 -10px;padding:0}.activitypub-reactions .reaction-avatars li:last-child{margin-left:0}.activitypub-reactions .reaction-avatars li a{display:block;text-decoration:none}.activitypub-reactions .reaction-avatars .reaction-avatar{border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);height:32px;transition:transform .6s cubic-bezier(.34,1.56,.64,1);width:32px;will-change:transform}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active{transform:translateY(-5px)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-clockwise{transform:translateY(-5px) rotate(-30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-counter{transform:translateY(-5px) rotate(30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar:hover{position:relative;z-index:1}.activitypub-reactions .reaction-label.components-button{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#2271b1);flex:0 0 auto;height:auto;padding:0;text-decoration:none;white-space:nowrap}.activitypub-reactions .reaction-label.components-button:hover{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#135e96);text-decoration:underline}.activitypub-reactions .reaction-label.components-button:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-reaction-list{background-color:var(--wp--preset--color--background,var(--wp--preset--color--custom-background,var(--wp--preset--color--base)));list-style:none;margin:0;max-width:300px;padding:.25em .7em .25em 1.3em;width:-moz-max-content;width:max-content}.activitypub-reaction-list ul{margin:0;padding:0}.activitypub-reaction-list li{font-size:var(--wp--preset--font-size--small);margin:0;padding:0}.activitypub-reaction-list a{align-items:center;color:var(--wp--preset--color--contrast,var(--wp--preset--color--secondary));display:flex;font-size:var(--wp--preset--font-size--small,.75rem);gap:.5em;justify-content:flex-start;padding:.5em;text-decoration:none}.activitypub-reaction-list a:hover{text-decoration:underline}.activitypub-reaction-list a img{border-radius:50%;flex:none;height:24px;width:24px}
|
@ -0,0 +1 @@
|
||||
.activitypub-reactions h6{border-top:1px solid;border-top-color:var(--wp--preset--color--contrast-2);display:inline-block;padding-top:.5em}.activitypub-reactions .reaction-group{align-items:center;display:flex;gap:.75em;justify-content:flex-start;margin:.5em 0;position:relative;width:100%}@media(max-width:782px){.activitypub-reactions .reaction-group:has(.reaction-avatars:not(:empty)){justify-content:space-between}}.activitypub-reactions .reaction-avatars{align-items:center;display:flex;flex-direction:row;list-style:none;margin:0;padding:0}.activitypub-reactions .reaction-avatars li{margin:0 -10px 0 0;padding:0}.activitypub-reactions .reaction-avatars li:last-child{margin-right:0}.activitypub-reactions .reaction-avatars li a{display:block;text-decoration:none}.activitypub-reactions .reaction-avatars .reaction-avatar{border:.5px solid var(--wp--preset--color--contrast,hsla(0,0%,100%,.8));border-radius:50%;box-shadow:0 0 0 .5px hsla(0,0%,100%,.8),0 1px 3px rgba(0,0,0,.2);height:32px;transition:transform .6s cubic-bezier(.34,1.56,.64,1);width:32px;will-change:transform}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active{transform:translateY(-5px)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-clockwise{transform:translateY(-5px) rotate(30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar.wave-active.rotate-counter{transform:translateY(-5px) rotate(-30deg)}.activitypub-reactions .reaction-avatars .reaction-avatar:hover{position:relative;z-index:1}.activitypub-reactions .reaction-label.components-button{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#2271b1);flex:0 0 auto;height:auto;padding:0;text-decoration:none;white-space:nowrap}.activitypub-reactions .reaction-label.components-button:hover{color:var(--wp--preset--color--contrast,--wp--preset--color--secondary,#135e96);text-decoration:underline}.activitypub-reactions .reaction-label.components-button:focus:not(:disabled){box-shadow:none;outline:1px solid var(--wp--preset--color--contrast,#135e96);outline-offset:2px}.activitypub-reaction-list{background-color:var(--wp--preset--color--background,var(--wp--preset--color--custom-background,var(--wp--preset--color--base)));list-style:none;margin:0;max-width:300px;padding:.25em 1.3em .25em .7em;width:-moz-max-content;width:max-content}.activitypub-reaction-list ul{margin:0;padding:0}.activitypub-reaction-list li{font-size:var(--wp--preset--font-size--small);margin:0;padding:0}.activitypub-reaction-list a{align-items:center;color:var(--wp--preset--color--contrast,var(--wp--preset--color--secondary));display:flex;font-size:var(--wp--preset--font-size--small,.75rem);gap:.5em;justify-content:flex-start;padding:.5em;text-decoration:none}.activitypub-reaction-list a:hover{text-decoration:underline}.activitypub-reaction-list a img{border-radius:50%;flex:none;height:24px;width:24px}
|
@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-dom-ready', 'wp-element', 'wp-i18n'), 'version' => 'd5cb95d9bd6062974b3c');
|
1
wp-content/plugins/activitypub/build/reactions/view.js
Normal file
1
wp-content/plugins/activitypub/build/reactions/view.js
Normal file
@ -0,0 +1 @@
|
||||
(()=>{"use strict";var e={n:t=>{var n=t&&t.__esModule?()=>t.default:()=>t;return e.d(n,{a:n}),n},d:(t,n)=>{for(var a in n)e.o(n,a)&&!e.o(t,a)&&Object.defineProperty(t,a,{enumerable:!0,get:n[a]})},o:(e,t)=>Object.prototype.hasOwnProperty.call(e,t)};const t=window.React,n=window.wp.element,a=window.wp.domReady;var r=e.n(a);const c=window.wp.components,o=window.wp.apiFetch;var l=e.n(o);function s(){return window._activityPubOptions||{}}window.wp.i18n;const i=({reactions:e})=>{const{defaultAvatarUrl:a}=s(),[r,c]=(0,n.useState)(new Set),[o,l]=(0,n.useState)(new Map),i=(0,n.useRef)([]),u=()=>{i.current.forEach((e=>clearTimeout(e))),i.current=[]},m=(t,n)=>{u();const a=100,r=e.length;n&&l((e=>{const n=new Map(e);return n.set(t,"clockwise"),n}));const o=e=>{const o="right"===e,s=o?r-1:0,u=o?1:-1;for(let e=o?t:t-1;o?e<=s:e>=s;e+=u){const r=Math.abs(e-t),o=setTimeout((()=>{c((t=>{const a=new Set(t);return n?a.add(e):a.delete(e),a})),n&&e!==t&&l((t=>{const n=new Map(t),a=e-u,r=n.get(a);return n.set(e,"clockwise"===r?"counter":"clockwise"),n}))}),r*a);i.current.push(o)}};if(o("right"),o("left"),!n){const e=Math.max((r-t)*a,t*a),n=setTimeout((()=>{l(new Map)}),e+a);i.current.push(n)}};return(0,n.useEffect)((()=>()=>u()),[]),(0,t.createElement)("ul",{className:"reaction-avatars"},e.map(((e,n)=>{const c=o.get(n),l=["reaction-avatar",r.has(n)?"wave-active":"",c?`rotate-${c}`:""].filter(Boolean).join(" "),s=e.avatar||a;return(0,t.createElement)("li",{key:n},(0,t.createElement)("a",{href:e.url,target:"_blank",rel:"noopener noreferrer",onMouseEnter:()=>m(n,!0),onMouseLeave:()=>m(n,!1)},(0,t.createElement)("img",{src:s,alt:e.name,className:l,width:"32",height:"32"})))})))},u=({reactions:e,type:n})=>(0,t.createElement)("ul",{className:"activitypub-reaction-list"},e.map(((e,n)=>(0,t.createElement)("li",{key:n},(0,t.createElement)("a",{href:e.url,className:"reaction-item",target:"_blank",rel:"noopener noreferrer"},(0,t.createElement)("img",{src:e.avatar,alt:e.name,width:"32",height:"32"}),(0,t.createElement)("span",null,e.name)))))),m=({items:e,label:a})=>{const[r,o]=(0,n.useState)(!1),[l,s]=(0,n.useState)(null),[m,p]=(0,n.useState)(e.length),h=(0,n.useRef)(null);(0,n.useEffect)((()=>{if(!h.current)return;const t=()=>{const t=h.current;if(!t)return;const n=t.offsetWidth-(l?.offsetWidth||0)-12,a=Math.max(1,Math.floor((n-32)/22));p(Math.min(a,e.length))};t();const n=new ResizeObserver(t);return n.observe(h.current),()=>{n.disconnect()}}),[l,e.length]);const f=e.slice(0,m);return(0,t.createElement)("div",{className:"reaction-group",ref:h},(0,t.createElement)(i,{reactions:f}),(0,t.createElement)(c.Button,{ref:s,className:"reaction-label is-link",onClick:()=>o(!r),"aria-expanded":r},a),r&&l&&(0,t.createElement)(c.Popover,{anchor:l,onClose:()=>o(!1)},(0,t.createElement)(u,{reactions:e})))};function p({title:e="",postId:a=null,reactions:r=null,titleComponent:c=null}){const{namespace:o}=s(),[i,u]=(0,n.useState)(r),[p,h]=(0,n.useState)(!r);return(0,n.useEffect)((()=>{if(r)return u(r),void h(!1);a?(h(!0),l()({path:`/${o}/posts/${a}/reactions`}).then((e=>{u(e),h(!1)})).catch((()=>h(!1)))):h(!1)}),[a,r]),p?null:i&&Object.values(i).some((e=>e.items?.length>0))?(0,t.createElement)("div",{className:"activitypub-reactions"},c||e&&(0,t.createElement)("h6",null,e),Object.entries(i).map((([e,n])=>n.items?.length?(0,t.createElement)(m,{key:e,items:n.items,label:n.label}):null))):null}r()((()=>{[].forEach.call(document.querySelectorAll(".activitypub-reactions-block"),(e=>{const a=JSON.parse(e.dataset.attrs);(0,n.createRoot)(e).render((0,t.createElement)(p,{...a}))}))}))})();
|
@ -1 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '9aee45886ecf2680fbd4');
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-components', 'wp-compose', 'wp-dom-ready', 'wp-element', 'wp-i18n', 'wp-primitives'), 'version' => '7160b6399cd924e1c7be');
|
||||
|
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-left:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:0 50px 50px 0;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:50px 0 0 50px;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex}
|
@ -1 +1 @@
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--wp--preset--color--white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border:1px solid var(--wp--preset--color--black);border-radius:inherit 0;color:var(--wp--preset--color--black);flex:1;padding:6px 12px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);color:var(--wp--preset--color--white);margin-left:0;text-decoration:none}
|
||||
.activitypub__modal.components-modal__frame{background-color:#f7f7f7;color:#333}.activitypub__modal.components-modal__frame .components-modal__header-heading,.activitypub__modal.components-modal__frame h4{color:#333;letter-spacing:inherit;word-spacing:inherit}.activitypub__modal.components-modal__frame .components-modal__header .components-button:hover{color:var(--color-white)}.activitypub__dialog{max-width:40em}.activitypub__dialog h4{line-height:1;margin:0}.activitypub__dialog .activitypub-dialog__section{margin-bottom:2em}.activitypub__dialog .activitypub-dialog__remember{margin-top:1em}.activitypub__dialog .activitypub-dialog__description{font-size:var(--wp--preset--font-size--normal,.75rem);margin:.33em 0 1em}.activitypub__dialog .activitypub-dialog__button-group{align-items:flex-end;display:flex;justify-content:flex-end}.activitypub__dialog .activitypub-dialog__button-group svg{height:21px;margin-right:.5em;width:21px}.activitypub__dialog .activitypub-dialog__button-group input{background-color:var(--wp--preset--color--white);border-radius:50px 0 0 50px;border-width:1px;border:1px solid var(--wp--preset--color--black);color:var(--wp--preset--color--black);flex:1;font-size:16px;height:inherit;line-height:1;margin-right:0;padding:15px 23px}.activitypub__dialog .activitypub-dialog__button-group button{align-self:center;background-color:var(--wp--preset--color--black);border-radius:0 50px 50px 0;border-width:1px;color:var(--wp--preset--color--white);font-size:16px;height:inherit;line-height:1;margin-left:0;padding:15px 23px;text-decoration:none}.activitypub__dialog .activitypub-dialog__button-group button:hover{border:inherit}.activitypub-remote-profile-delete{align-self:center;color:inherit;font-size:inherit;height:inherit;padding:0 5px}.activitypub-remote-profile-delete:hover{background:inherit;border:inherit}.activitypub-remote-reply{display:flex}
|
||||
|
12
wp-content/plugins/activitypub/build/reply-intent/block.json
Normal file
12
wp-content/plugins/activitypub/build/reply-intent/block.json
Normal file
@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "reply-handler",
|
||||
"title": "Reply Handler: not a block, but block.json is very useful.",
|
||||
"category": "widgets",
|
||||
"icon": "admin-comments",
|
||||
"keywords": [
|
||||
"reply",
|
||||
"handler",
|
||||
"comments"
|
||||
],
|
||||
"editorScript": "file:./plugin.js"
|
||||
}
|
@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('wp-block-editor', 'wp-blocks', 'wp-data', 'wp-element', 'wp-plugins'), 'version' => 'f65a7269b5abb57d3e73');
|
@ -0,0 +1 @@
|
||||
(()=>{"use strict";const e=window.wp.plugins,t=window.wp.blocks,i=window.wp.data,n=window.wp.blockEditor,o=window.wp.element;let r=!1;(0,e.registerPlugin)("activitypub-reply-intent",{render:()=>((0,o.useEffect)((()=>{if(r)return;const e=new URLSearchParams(window.location.search).get("in_reply_to");e&&!r&&setTimeout((()=>{const o=(0,t.createBlock)("activitypub/reply",{url:e,embedPost:!0}),r=(0,i.dispatch)(n.store);r.insertBlock(o),r.insertAfterBlock(o.clientId)}),200),r=!0})),null)})})();
|
29
wp-content/plugins/activitypub/build/reply/block.json
Normal file
29
wp-content/plugins/activitypub/build/reply/block.json
Normal file
@ -0,0 +1,29 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 3,
|
||||
"name": "activitypub/reply",
|
||||
"version": "0.1.0",
|
||||
"title": "Federated Reply",
|
||||
"category": "widgets",
|
||||
"icon": "commentReplyLink",
|
||||
"description": "Respond to posts, notes, videos, and other content on the fediverse. Ensure the URL originates from a federated social network like Mastodon, as other URLs might not function as expected.",
|
||||
"supports": {
|
||||
"html": false,
|
||||
"inserter": true,
|
||||
"reusable": false,
|
||||
"lock": false
|
||||
},
|
||||
"textdomain": "activitypub",
|
||||
"editorScript": "file:./index.js",
|
||||
"editorStyle": "file:./style-index.css",
|
||||
"style": "file:./index.css",
|
||||
"attributes": {
|
||||
"url": {
|
||||
"type": "string"
|
||||
},
|
||||
"embedPost": {
|
||||
"type": "boolean",
|
||||
"default": null
|
||||
}
|
||||
}
|
||||
}
|
1
wp-content/plugins/activitypub/build/reply/index-rtl.css
Normal file
1
wp-content/plugins/activitypub/build/reply/index-rtl.css
Normal file
@ -0,0 +1 @@
|
||||
.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6;border-radius:8px;overflow:hidden}.activitypub-embed-content .ap-preview img{display:block;height:auto;width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta .ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684}.wp-block-activitypub-reply .components-spinner{height:12px;margin-bottom:0;margin-top:0;width:12px}
|
@ -0,0 +1 @@
|
||||
<?php return array('dependencies' => array('react', 'wp-api-fetch', 'wp-block-editor', 'wp-blocks', 'wp-components', 'wp-compose', 'wp-data', 'wp-element', 'wp-i18n', 'wp-primitives', 'wp-url'), 'version' => 'fcd855ff6f64b21029be');
|
1
wp-content/plugins/activitypub/build/reply/index.css
Normal file
1
wp-content/plugins/activitypub/build/reply/index.css
Normal file
@ -0,0 +1 @@
|
||||
.activitypub-embed{background:#fff;border:1px solid #e6e6e6;border-radius:12px;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Helvetica,Arial,sans-serif;max-width:100%;padding:0}.activitypub-reply-block .activitypub-embed{margin:1em 0}.activitypub-embed-header{align-items:center;display:flex;gap:10px;padding:15px}.activitypub-embed-header img{border-radius:50%;height:48px;width:48px}.activitypub-embed-header-text{flex-grow:1}.activitypub-embed-header-text h2{color:#000;font-size:15px;font-weight:600;margin:0;padding:0}.activitypub-embed-header-text .ap-account{color:#687684;font-size:14px;text-decoration:none}.activitypub-embed-content{padding:0 15px 15px}.activitypub-embed-content .ap-title{color:#000;font-size:23px;font-weight:600;margin:0 0 10px;padding:0}.activitypub-embed-content .ap-subtitle{color:#000;font-size:15px;margin:0 0 15px}.activitypub-embed-content .ap-preview{border:1px solid #e6e6e6;border-radius:8px;overflow:hidden}.activitypub-embed-content .ap-preview img{display:block;height:auto;width:100%}.activitypub-embed-content .ap-preview-text{padding:15px}.activitypub-embed-meta{border-top:1px solid #e6e6e6;color:#687684;display:flex;font-size:13px;gap:15px;padding:15px}.activitypub-embed-meta .ap-stat{align-items:center;display:flex;gap:5px}@media only screen and (max-width:399px){.activitypub-embed-meta .ap-stat{display:none!important}}.activitypub-embed-meta a.ap-stat{color:inherit;text-decoration:none}.activitypub-embed-meta strong{color:#000;font-weight:600}.activitypub-embed-meta .ap-stat-label{color:#687684}.wp-block-activitypub-reply .components-spinner{height:12px;margin-bottom:0;margin-top:0;width:12px}
|
1
wp-content/plugins/activitypub/build/reply/index.js
Normal file
1
wp-content/plugins/activitypub/build/reply/index.js
Normal file
File diff suppressed because one or more lines are too long
@ -0,0 +1 @@
|
||||
.activitypub-embed-container{margin-top:1em;min-height:100px;pointer-events:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.activitypub-embed-loading{align-items:center;display:flex;justify-content:center}.activitypub-embed-container .wp-block-embed{pointer-events:none!important}.activitypub-embed-preview,.activitypub-embed-preview iframe{pointer-events:none}.activitypub-reply-display{margin:1em 0}.activitypub-reply-display p{margin:0}.activitypub-reply-display a{color:#2271b1;text-decoration:none}.activitypub-reply-display a:hover{color:#135e96;text-decoration:underline}
|
@ -0,0 +1 @@
|
||||
.activitypub-embed-container{margin-top:1em;min-height:100px;pointer-events:none;position:relative;-webkit-user-select:none;-moz-user-select:none;user-select:none}.activitypub-embed-loading{align-items:center;display:flex;justify-content:center}.activitypub-embed-container .wp-block-embed{pointer-events:none!important}.activitypub-embed-preview,.activitypub-embed-preview iframe{pointer-events:none}.activitypub-reply-display{margin:1em 0}.activitypub-reply-display p{margin:0}.activitypub-reply-display a{color:#2271b1;text-decoration:none}.activitypub-reply-display a:hover{color:#135e96;text-decoration:underline}
|
@ -3,11 +3,14 @@
|
||||
* Inspired by the PHP ActivityPub Library by @Landrok
|
||||
*
|
||||
* @link https://github.com/landrok/activitypub
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity;
|
||||
|
||||
use Activitypub\Activity\Base_Object;
|
||||
use Activitypub\Activity\Extended_Object\Event;
|
||||
use Activitypub\Activity\Extended_Object\Place;
|
||||
|
||||
/**
|
||||
* \Activitypub\Activity\Activity implements the common
|
||||
@ -22,6 +25,45 @@ class Activity extends Base_Object {
|
||||
);
|
||||
|
||||
/**
|
||||
* The default types for Activities.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const TYPES = array(
|
||||
'Accept',
|
||||
'Add',
|
||||
'Announce',
|
||||
'Arrive',
|
||||
'Block',
|
||||
'Create',
|
||||
'Delete',
|
||||
'Dislike',
|
||||
'Follow',
|
||||
'Flag',
|
||||
'Ignore',
|
||||
'Invite',
|
||||
'Join',
|
||||
'Leave',
|
||||
'Like',
|
||||
'Listen',
|
||||
'Move',
|
||||
'Offer',
|
||||
'Read',
|
||||
'Reject',
|
||||
'Remove',
|
||||
'TentativeAccept',
|
||||
'TentativeReject',
|
||||
'Travel',
|
||||
'Undo',
|
||||
'Update',
|
||||
'View',
|
||||
);
|
||||
|
||||
/**
|
||||
* The type of the object.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Activity';
|
||||
@ -33,10 +75,7 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object-term
|
||||
*
|
||||
* @var string
|
||||
* | Base_Object
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|Base_Object|null
|
||||
*/
|
||||
protected $object;
|
||||
|
||||
@ -48,11 +87,7 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-actor
|
||||
*
|
||||
* @var string
|
||||
* | \ActivityPhp\Type\Extended\AbstractActor
|
||||
* | array<Actor>
|
||||
* | array<Link>
|
||||
* | Link
|
||||
* @var string|array
|
||||
*/
|
||||
protected $actor;
|
||||
|
||||
@ -67,11 +102,7 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-target
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | array<ObjectType>
|
||||
* | Link
|
||||
* | array<Link>
|
||||
* @var string|array
|
||||
*/
|
||||
protected $target;
|
||||
|
||||
@ -83,13 +114,22 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-result
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|Base_Object
|
||||
*/
|
||||
protected $result;
|
||||
|
||||
/**
|
||||
* Identifies a Collection containing objects considered to be responses
|
||||
* to this object.
|
||||
* WordPress has a strong core system of approving replies. We only include
|
||||
* approved replies here.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $replies;
|
||||
|
||||
/**
|
||||
* An indirect object of the activity from which the
|
||||
* activity is directed.
|
||||
@ -100,10 +140,7 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-origin
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|array
|
||||
*/
|
||||
protected $origin;
|
||||
|
||||
@ -113,10 +150,7 @@ class Activity extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-instrument
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|array
|
||||
*/
|
||||
protected $instrument;
|
||||
|
||||
@ -124,29 +158,67 @@ class Activity extends Base_Object {
|
||||
* Set the object and copy Object properties to the Activity.
|
||||
*
|
||||
* Any to, bto, cc, bcc, and audience properties specified on the object
|
||||
* MUST be copied over to the new Create activity by the server.
|
||||
* MUST be copied over to the new "Create" activity by the server.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#object-without-create
|
||||
*
|
||||
* @param string|Base_Objectr|Link|null $object
|
||||
*
|
||||
* @return void
|
||||
* @param array|string|Base_Object|Activity|Actor|null $data Activity object.
|
||||
*/
|
||||
public function set_object( $object ) {
|
||||
// convert array to object
|
||||
if ( is_array( $object ) ) {
|
||||
$object = self::init_from_array( $object );
|
||||
public function set_object( $data ) {
|
||||
$object = $data;
|
||||
|
||||
// Convert array to appropriate object type.
|
||||
if ( is_array( $data ) ) {
|
||||
$type = $data['type'] ?? null;
|
||||
|
||||
if ( in_array( $type, self::TYPES, true ) ) {
|
||||
$object = self::init_from_array( $data );
|
||||
} elseif ( in_array( $type, Actor::TYPES, true ) ) {
|
||||
$object = Actor::init_from_array( $data );
|
||||
} elseif ( in_array( $type, Base_Object::TYPES, true ) ) {
|
||||
switch ( $type ) {
|
||||
case 'Event':
|
||||
$object = Event::init_from_array( $data );
|
||||
break;
|
||||
case 'Place':
|
||||
$object = Place::init_from_array( $data );
|
||||
break;
|
||||
default:
|
||||
$object = Base_Object::init_from_array( $data );
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
$object = Generic_Object::init_from_array( $data );
|
||||
}
|
||||
}
|
||||
|
||||
// set object
|
||||
$this->set( 'object', $object );
|
||||
$this->pre_fill_activity_from_object();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fills the Activity with the specified activity object.
|
||||
*/
|
||||
public function pre_fill_activity_from_object() {
|
||||
$object = $this->get_object();
|
||||
|
||||
// Check if `$data` is a URL and use it to generate an ID then.
|
||||
if ( is_string( $object ) && filter_var( $object, FILTER_VALIDATE_URL ) && ! $this->get_id() ) {
|
||||
$this->set( 'id', $object . '#activity-' . strtolower( $this->get_type() ) . '-' . time() );
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if `$data` is an object and copy some properties otherwise do nothing.
|
||||
if ( ! is_object( $object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
|
||||
$this->set( $i, $object->get( $i ) );
|
||||
$value = $object->get( $i );
|
||||
if ( $value && ! $this->get( $i ) ) {
|
||||
$this->set( $i, $value );
|
||||
}
|
||||
}
|
||||
|
||||
if ( $object->get_published() && ! $this->get_published() ) {
|
||||
@ -161,8 +233,20 @@ class Activity extends Base_Object {
|
||||
$this->set( 'actor', $object->get_attributed_to() );
|
||||
}
|
||||
|
||||
if ( $this->get_type() !== 'Announce' && $object->get_in_reply_to() && ! $this->get_in_reply_to() ) {
|
||||
$this->set( 'in_reply_to', $object->get_in_reply_to() );
|
||||
}
|
||||
|
||||
if ( $object->get_id() && ! $this->get_id() ) {
|
||||
$this->set( 'id', $object->get_id() . '#activity' );
|
||||
$id = strtok( $object->get_id(), '#' );
|
||||
if ( $object->get_updated() ) {
|
||||
$updated = $object->get_updated();
|
||||
} elseif ( $object->get_published() ) {
|
||||
$updated = $object->get_published();
|
||||
} else {
|
||||
$updated = time();
|
||||
}
|
||||
$this->set( 'id', $id . '#activity-' . strtolower( $this->get_type() ) . '-' . $updated );
|
||||
}
|
||||
}
|
||||
|
||||
@ -175,7 +259,7 @@ class Activity extends Base_Object {
|
||||
if ( $this->object instanceof Base_Object ) {
|
||||
$class = get_class( $this->object );
|
||||
if ( $class && $class::JSON_LD_CONTEXT ) {
|
||||
// Without php 5.6 support this could be just: 'return $this->object::JSON_LD_CONTEXT;'
|
||||
// Without php 5.6 support this could be just: 'return $this->object::JSON_LD_CONTEXT;'.
|
||||
return $class::JSON_LD_CONTEXT;
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,8 @@
|
||||
* Inspired by the PHP ActivityPub Library by @Landrok
|
||||
*
|
||||
* @link https://github.com/landrok/activitypub
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity;
|
||||
@ -22,34 +24,61 @@ class Actor extends Base_Object {
|
||||
'https://w3id.org/security/v1',
|
||||
'https://purl.archive.org/socialweb/webfinger',
|
||||
array(
|
||||
'schema' => 'http://schema.org#',
|
||||
'toot' => 'http://joinmastodon.org/ns#',
|
||||
'webfinger' => 'https://webfinger.net/#',
|
||||
'lemmy' => 'https://join-lemmy.org/ns#',
|
||||
'schema' => 'http://schema.org#',
|
||||
'toot' => 'http://joinmastodon.org/ns#',
|
||||
'lemmy' => 'https://join-lemmy.org/ns#',
|
||||
'manuallyApprovesFollowers' => 'as:manuallyApprovesFollowers',
|
||||
'PropertyValue' => 'schema:PropertyValue',
|
||||
'value' => 'schema:value',
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'featured' => array(
|
||||
'@id' => 'toot:featured',
|
||||
'PropertyValue' => 'schema:PropertyValue',
|
||||
'value' => 'schema:value',
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'featured' => array(
|
||||
'@id' => 'toot:featured',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'featuredTags' => array(
|
||||
'@id' => 'toot:featuredTags',
|
||||
'featuredTags' => array(
|
||||
'@id' => 'toot:featuredTags',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'moderators' => array(
|
||||
'@id' => 'lemmy:moderators',
|
||||
'moderators' => array(
|
||||
'@id' => 'lemmy:moderators',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods',
|
||||
'discoverable' => 'toot:discoverable',
|
||||
'indexable' => 'toot:indexable',
|
||||
'resource' => 'webfinger:resource',
|
||||
'alsoKnownAs' => array(
|
||||
'@id' => 'as:alsoKnownAs',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'movedTo' => array(
|
||||
'@id' => 'as:movedTo',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'attributionDomains' => array(
|
||||
'@id' => 'toot:attributionDomains',
|
||||
'@type' => '@id',
|
||||
),
|
||||
'postingRestrictedToMods' => 'lemmy:postingRestrictedToMods',
|
||||
'discoverable' => 'toot:discoverable',
|
||||
'indexable' => 'toot:indexable',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* The default types for Actors.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const TYPES = array(
|
||||
'Application',
|
||||
'Group',
|
||||
'Organization',
|
||||
'Person',
|
||||
'Service',
|
||||
);
|
||||
|
||||
/**
|
||||
* The type of the object.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
@ -60,8 +89,7 @@ class Actor extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#inbox
|
||||
*
|
||||
* @var string
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $inbox;
|
||||
|
||||
@ -71,8 +99,7 @@ class Actor extends Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#outbox
|
||||
*
|
||||
* @var string
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $outbox;
|
||||
|
||||
@ -171,4 +198,28 @@ class Actor extends Base_Object {
|
||||
* @var boolean
|
||||
*/
|
||||
protected $manually_approves_followers = false;
|
||||
|
||||
/**
|
||||
* Domains allowed to use `fediverse:creator` for this actor in
|
||||
* published articles.
|
||||
*
|
||||
* @see https://blog.joinmastodon.org/2024/07/highlighting-journalism-on-mastodon/
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $attribution_domains = null;
|
||||
|
||||
/**
|
||||
* The target of the actor.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
protected $moved_to;
|
||||
|
||||
/**
|
||||
* The alsoKnownAs of the actor.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $also_known_as;
|
||||
}
|
||||
|
@ -3,17 +3,12 @@
|
||||
* Inspired by the PHP ActivityPub Library by @Landrok
|
||||
*
|
||||
* @link https://github.com/landrok/activitypub
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity;
|
||||
|
||||
use WP_Error;
|
||||
use ReflectionClass;
|
||||
use DateTime;
|
||||
|
||||
use function Activitypub\camel_to_snake_case;
|
||||
use function Activitypub\snake_to_camel_case;
|
||||
|
||||
/**
|
||||
* Base_Object is an implementation of one of the
|
||||
* Activity Streams Core Types.
|
||||
@ -25,25 +20,89 @@ use function Activitypub\snake_to_camel_case;
|
||||
* 'Base_' for this reason.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-core/#object
|
||||
*
|
||||
* @method string|null get_actor() Gets one or more entities that performed or are expected to perform the activity.
|
||||
* @method string|null get_attributed_to() Gets the entity attributed as the original author.
|
||||
* @method array|null get_attachment() Gets the attachment property of the object.
|
||||
* @method array|null get_cc() Gets the secondary recipients of the object.
|
||||
* @method string|null get_content() Gets the content property of the object.
|
||||
* @method array|null get_icon() Gets the icon property of the object.
|
||||
* @method string|null get_id() Gets the object's unique global identifier.
|
||||
* @method array|null get_image() Gets the image property of the object.
|
||||
* @method array|string|null get_in_reply_to() Gets the objects this object is in reply to.
|
||||
* @method string|null get_name() Gets the natural language name of the object.
|
||||
* @method Base_Object|string|null get_object() Gets the direct object of the activity.
|
||||
* @method string|null get_published() Gets the date and time the object was published in ISO 8601 format.
|
||||
* @method string|null get_summary() Gets the natural language summary of the object.
|
||||
* @method array|null get_tag() Gets the tag property of the object.
|
||||
* @method array|string|null get_to() Gets the primary recipients of the object.
|
||||
* @method string get_type() Gets the type of the object.
|
||||
* @method string|null get_updated() Gets the date and time the object was updated in ISO 8601 format.
|
||||
* @method string|null get_url() Gets the URL of the object.
|
||||
*
|
||||
* @method string|array add_cc( string|array $cc ) Adds one or more entities to the secondary audience of the object.
|
||||
* @method string|array add_to( string|array $to ) Adds one or more entities to the primary audience of the object.
|
||||
*
|
||||
* @method Base_Object set_actor( string|array $actor ) Sets one or more entities that performed the activity.
|
||||
* @method Base_Object set_attachment( array $attachment ) Sets the attachment property of the object.
|
||||
* @method Base_Object set_attributed_to( string $attributed_to ) Sets the entity attributed as the original author.
|
||||
* @method Base_Object set_cc( array|string $cc ) Sets the secondary recipients of the object.
|
||||
* @method Base_Object set_content( string $content ) Sets the content property of the object.
|
||||
* @method Base_Object set_content_map( array $content_map ) Sets the content property of the object.
|
||||
* @method Base_Object set_icon( array $icon ) Sets the icon property of the object.
|
||||
* @method Base_Object set_id( string $id ) Sets the object's unique global identifier.
|
||||
* @method Base_Object set_image( array $image ) Sets the image property of the object.
|
||||
* @method Base_Object set_name( string $name ) Sets the natural language name of the object.
|
||||
* @method Base_Object set_origin( string $origin ) Sets the origin property of the object.
|
||||
* @method Base_Object set_published( string $published ) Sets the date and time the object was published in ISO 8601 format.
|
||||
* @method Base_Object set_sensitive( bool $sensitive ) Sets the sensitive property of the object.
|
||||
* @method Base_Object set_summary( string $summary ) Sets the natural language summary of the object.
|
||||
* @method Base_Object set_summary_map( array|null $summary_map ) Sets the summary property of the object.
|
||||
* @method Base_Object set_target( string $target ) Sets the target property of the object.
|
||||
* @method Base_Object set_to( array|string $to ) Sets the primary recipients of the object.
|
||||
* @method Base_Object set_type( string $type ) Sets the type of the object.
|
||||
* @method Base_Object set_updated( string $updated ) Sets the date and time the object was updated in ISO 8601 format.
|
||||
* @method Base_Object set_url( string $url ) Sets the URL of the object.
|
||||
*/
|
||||
class Base_Object {
|
||||
class Base_Object extends Generic_Object {
|
||||
/**
|
||||
* The JSON-LD context for the object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
array(
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'Hashtag' => 'as:Hashtag',
|
||||
'sensitive' => 'as:sensitive',
|
||||
),
|
||||
);
|
||||
|
||||
/**
|
||||
* The object's unique global identifier
|
||||
* The default types for Objects.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#obj-id
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||
*
|
||||
* @var string
|
||||
* @var array
|
||||
*/
|
||||
protected $id;
|
||||
const TYPES = array(
|
||||
'Article',
|
||||
'Audio',
|
||||
'Document',
|
||||
'Event',
|
||||
'Image',
|
||||
'Note',
|
||||
'Page',
|
||||
'Place',
|
||||
'Profile',
|
||||
'Relationship',
|
||||
'Tombstone',
|
||||
'Video',
|
||||
);
|
||||
|
||||
/**
|
||||
* The type of the object.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type = 'Object';
|
||||
@ -56,12 +115,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attachment
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $attachment;
|
||||
|
||||
@ -72,12 +126,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-attributedto
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $attributed_to;
|
||||
|
||||
@ -87,12 +136,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-audience
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $audience;
|
||||
|
||||
@ -122,10 +166,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-context
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $context;
|
||||
|
||||
@ -186,12 +227,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-icon
|
||||
*
|
||||
* @var string
|
||||
* | Image
|
||||
* | Link
|
||||
* | array<Image>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $icon;
|
||||
|
||||
@ -202,12 +238,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-image-term
|
||||
*
|
||||
* @var string
|
||||
* | Image
|
||||
* | Link
|
||||
* | array<Image>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
@ -217,12 +248,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-inreplyto
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $in_reply_to;
|
||||
|
||||
@ -232,12 +258,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-location
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $location;
|
||||
|
||||
@ -246,10 +267,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-preview
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $preview;
|
||||
|
||||
@ -281,10 +299,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $summary;
|
||||
|
||||
@ -294,7 +309,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-summary
|
||||
*
|
||||
* @var array<string>|null
|
||||
* @var string[]|null
|
||||
*/
|
||||
protected $summary_map;
|
||||
|
||||
@ -307,12 +322,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tag
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $tag;
|
||||
|
||||
@ -328,11 +338,7 @@ class Base_Object {
|
||||
/**
|
||||
* One or more links to representations of the object.
|
||||
*
|
||||
* @var string
|
||||
* | array<string>
|
||||
* | Link
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|null
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
@ -342,12 +348,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-to
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $to;
|
||||
|
||||
@ -357,12 +358,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bto
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $bto;
|
||||
|
||||
@ -372,12 +368,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-cc
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $cc;
|
||||
|
||||
@ -387,12 +378,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-bcc
|
||||
*
|
||||
* @var string
|
||||
* | ObjectType
|
||||
* | Link
|
||||
* | array<ObjectType>
|
||||
* | array<Link>
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $bcc;
|
||||
|
||||
@ -428,7 +414,7 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#source-property
|
||||
*
|
||||
* @var ObjectType
|
||||
* @var array
|
||||
*/
|
||||
protected $source;
|
||||
|
||||
@ -438,58 +424,40 @@ class Base_Object {
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-replies
|
||||
*
|
||||
* @var string
|
||||
* | Collection
|
||||
* | Link
|
||||
* | null
|
||||
* @var string|array|null
|
||||
*/
|
||||
protected $replies;
|
||||
|
||||
/**
|
||||
* Magic function to implement getter and setter
|
||||
* A Collection containing objects considered to be likes for
|
||||
* this object.
|
||||
*
|
||||
* @param string $method The method name.
|
||||
* @param string $params The method params.
|
||||
* @see https://www.w3.org/TR/activitypub/#likes
|
||||
*
|
||||
* @return void
|
||||
* @var array
|
||||
*/
|
||||
public function __call( $method, $params ) {
|
||||
$var = \strtolower( \substr( $method, 4 ) );
|
||||
|
||||
if ( \strncasecmp( $method, 'get', 3 ) === 0 ) {
|
||||
if ( ! $this->has( $var ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
return $this->$var;
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
|
||||
return $this->set( $var, $params[0] );
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'add', 3 ) === 0 ) {
|
||||
$this->add( $var, $params[0] );
|
||||
}
|
||||
}
|
||||
protected $likes;
|
||||
|
||||
/**
|
||||
* Magic function, to transform the object to string.
|
||||
* A Collection containing objects considered to be shares for
|
||||
* this object.
|
||||
*
|
||||
* @return string The object id.
|
||||
* @see https://www.w3.org/TR/activitypub/#shares
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public function __toString() {
|
||||
return $this->to_string();
|
||||
}
|
||||
protected $shares;
|
||||
|
||||
/**
|
||||
* Function to transform the object to string.
|
||||
* Used to mark an object as containing sensitive content.
|
||||
* Mastodon displays a content warning, requiring users to click
|
||||
* through to view the content.
|
||||
*
|
||||
* @return string The object id.
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#sensitive
|
||||
*
|
||||
* @var boolean
|
||||
*/
|
||||
public function to_string() {
|
||||
return $this->get_id();
|
||||
}
|
||||
protected $sensitive;
|
||||
|
||||
/**
|
||||
* Generic getter.
|
||||
@ -500,21 +468,10 @@ class Base_Object {
|
||||
*/
|
||||
public function get( $key ) {
|
||||
if ( ! $this->has( $key ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
return new \WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
return call_user_func( array( $this, 'get_' . $key ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the object has a key
|
||||
*
|
||||
* @param string $key The key to check.
|
||||
*
|
||||
* @return boolean True if the object has the key.
|
||||
*/
|
||||
public function has( $key ) {
|
||||
return property_exists( $this, $key );
|
||||
return parent::get( $key );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -527,12 +484,10 @@ class Base_Object {
|
||||
*/
|
||||
public function set( $key, $value ) {
|
||||
if ( ! $this->has( $key ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
return new \WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$this->$key = $value;
|
||||
|
||||
return $this;
|
||||
return parent::set( $key, $value );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -545,170 +500,9 @@ class Base_Object {
|
||||
*/
|
||||
public function add( $key, $value ) {
|
||||
if ( ! $this->has( $key ) ) {
|
||||
return new WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
return new \WP_Error( 'invalid_key', __( 'Invalid key', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
if ( ! isset( $this->$key ) ) {
|
||||
$this->$key = array();
|
||||
}
|
||||
|
||||
$attributes = $this->$key;
|
||||
$attributes[] = $value;
|
||||
|
||||
$this->$key = $attributes;
|
||||
|
||||
return $this->$key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array.
|
||||
*
|
||||
* @return string The JSON string.
|
||||
*
|
||||
* @return \Activitypub\Activity\Base_Object An Object built from the JSON string.
|
||||
*/
|
||||
public static function init_from_json( $json ) {
|
||||
$array = \json_decode( $json, true );
|
||||
|
||||
if ( ! is_array( $array ) ) {
|
||||
$array = array();
|
||||
}
|
||||
|
||||
return self::init_from_array( $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array.
|
||||
*
|
||||
* @return string The object array.
|
||||
*
|
||||
* @return \Activitypub\Activity\Base_Object An Object built from the JSON string.
|
||||
*/
|
||||
public static function init_from_array( $array ) {
|
||||
if ( ! is_array( $array ) ) {
|
||||
return new WP_Error( 'invalid_array', __( 'Invalid array', 'activitypub' ), array( 'status' => 404 ) );
|
||||
}
|
||||
|
||||
$object = new static();
|
||||
|
||||
foreach ( $array as $key => $value ) {
|
||||
$key = camel_to_snake_case( $key );
|
||||
call_user_func( array( $object, 'set_' . $key ), $value );
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array and pre-fill the object.
|
||||
*
|
||||
* @param string $json The JSON string.
|
||||
*/
|
||||
public function from_json( $json ) {
|
||||
$array = \json_decode( $json, true );
|
||||
|
||||
$this->from_array( $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array and pre-fill the object.
|
||||
*
|
||||
* @param array $array The array.
|
||||
*/
|
||||
public function from_array( $array ) {
|
||||
foreach ( $array as $key => $value ) {
|
||||
if ( $value ) {
|
||||
$key = camel_to_snake_case( $key );
|
||||
call_user_func( array( $this, 'set_' . $key ), $value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Object to an array.
|
||||
*
|
||||
* It tries to get the object attributes if they exist
|
||||
* and falls back to the getters. Empty values are ignored.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return array An array built from the Object.
|
||||
*/
|
||||
public function to_array( $include_json_ld_context = true ) {
|
||||
$array = array();
|
||||
$vars = get_object_vars( $this );
|
||||
|
||||
foreach ( $vars as $key => $value ) {
|
||||
// ignotre all _prefixed keys.
|
||||
if ( '_' === substr( $key, 0, 1 ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if value is empty, try to get it from a getter.
|
||||
if ( ! $value ) {
|
||||
$value = call_user_func( array( $this, 'get_' . $key ) );
|
||||
}
|
||||
|
||||
if ( is_object( $value ) ) {
|
||||
$value = $value->to_array( false );
|
||||
}
|
||||
|
||||
// if value is still empty, ignore it for the array and continue.
|
||||
if ( isset( $value ) ) {
|
||||
$array[ snake_to_camel_case( $key ) ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $include_json_ld_context ) {
|
||||
// Get JsonLD context and move it to '@context' at the top.
|
||||
$array = array_merge( array( '@context' => $this->get_json_ld_context() ), $array );
|
||||
}
|
||||
|
||||
$class = new ReflectionClass( $this );
|
||||
$class = strtolower( $class->getShortName() );
|
||||
|
||||
$array = \apply_filters( 'activitypub_activity_object_array', $array, $class, $this->id, $this );
|
||||
$array = \apply_filters( "activitypub_activity_{$class}_object_array", $array, $this->id, $this );
|
||||
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Object to JSON.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return string The JSON string.
|
||||
*/
|
||||
public function to_json( $include_json_ld_context = true ) {
|
||||
$array = $this->to_array( $include_json_ld_context );
|
||||
$options = \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT;
|
||||
|
||||
/*
|
||||
* Options to be passed to json_encode()
|
||||
*
|
||||
* @param int $options The current options flags
|
||||
*/
|
||||
$options = \apply_filters( 'activitypub_json_encode_options', $options );
|
||||
|
||||
return \wp_json_encode( $array, $options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the keys of the object vars.
|
||||
*
|
||||
* @return array The keys of the object vars.
|
||||
*/
|
||||
public function get_object_var_keys() {
|
||||
return \array_keys( \get_object_vars( $this ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON-LD context of this object.
|
||||
*
|
||||
* @return array $context A compacted JSON-LD context for the ActivityPub object.
|
||||
*/
|
||||
public function get_json_ld_context() {
|
||||
return static::JSON_LD_CONTEXT;
|
||||
return parent::add( $key, $value );
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,325 @@
|
||||
<?php
|
||||
/**
|
||||
* Generic Object.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Activity;
|
||||
|
||||
use function Activitypub\camel_to_snake_case;
|
||||
use function Activitypub\snake_to_camel_case;
|
||||
|
||||
/**
|
||||
* Generic Object.
|
||||
*
|
||||
* This class is used to create Generic Objects.
|
||||
* It is used to create objects that might be unknown by the plugin but
|
||||
* conform to the ActivityStreams vocabulary.
|
||||
*
|
||||
* @since 5.3.0
|
||||
*/
|
||||
#[\AllowDynamicProperties]
|
||||
class Generic_Object {
|
||||
/**
|
||||
* The JSON-LD context for the object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://www.w3.org/ns/activitystreams',
|
||||
);
|
||||
|
||||
/**
|
||||
* The object's unique global identifier
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/#obj-id
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* Magic function, to transform the object to string.
|
||||
*
|
||||
* @return string The object id.
|
||||
*/
|
||||
public function __toString() {
|
||||
return $this->to_string();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to transform the object to string.
|
||||
*
|
||||
* @return string The object id.
|
||||
*/
|
||||
public function to_string() {
|
||||
return $this->get_id();
|
||||
}
|
||||
|
||||
/**
|
||||
* Magic function to implement getter and setter.
|
||||
*
|
||||
* @param string $method The method name.
|
||||
* @param string $params The method params.
|
||||
*/
|
||||
public function __call( $method, $params ) {
|
||||
$var = \strtolower( \substr( $method, 4 ) );
|
||||
|
||||
if ( \strncasecmp( $method, 'get', 3 ) === 0 ) {
|
||||
if ( ! $this->has( $var ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $this->$var;
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'set', 3 ) === 0 ) {
|
||||
return $this->set( $var, $params[0] );
|
||||
}
|
||||
|
||||
if ( \strncasecmp( $method, 'add', 3 ) === 0 ) {
|
||||
return $this->add( $var, $params[0] );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic getter.
|
||||
*
|
||||
* @param string $key The key to get.
|
||||
*
|
||||
* @return mixed The value.
|
||||
*/
|
||||
public function get( $key ) {
|
||||
return call_user_func( array( $this, 'get_' . $key ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic setter.
|
||||
*
|
||||
* @param string $key The key to set.
|
||||
* @param string $value The value to set.
|
||||
*
|
||||
* @return mixed The value.
|
||||
*/
|
||||
public function set( $key, $value ) {
|
||||
$this->$key = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generic adder.
|
||||
*
|
||||
* @param string $key The key to set.
|
||||
* @param mixed $value The value to add.
|
||||
*
|
||||
* @return mixed The value.
|
||||
*/
|
||||
public function add( $key, $value ) {
|
||||
if ( empty( $value ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $this->$key ) ) {
|
||||
$this->$key = array();
|
||||
}
|
||||
|
||||
if ( is_string( $this->$key ) ) {
|
||||
$this->$key = array( $this->$key );
|
||||
}
|
||||
|
||||
$attributes = $this->$key;
|
||||
|
||||
if ( is_array( $value ) ) {
|
||||
$attributes = array_merge( $attributes, $value );
|
||||
} else {
|
||||
$attributes[] = $value;
|
||||
}
|
||||
|
||||
$this->$key = array_unique( $attributes );
|
||||
|
||||
return $this->$key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the object has a key
|
||||
*
|
||||
* @param string $key The key to check.
|
||||
*
|
||||
* @return boolean True if the object has the key.
|
||||
*/
|
||||
public function has( $key ) {
|
||||
return property_exists( $this, $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array.
|
||||
*
|
||||
* @param string $json The JSON string.
|
||||
*
|
||||
* @return Generic_Object|\WP_Error An Object built from the JSON string or WP_Error when it's not a JSON string.
|
||||
*/
|
||||
public static function init_from_json( $json ) {
|
||||
$array = \json_decode( $json, true );
|
||||
|
||||
if ( ! is_array( $array ) ) {
|
||||
return new \WP_Error( 'invalid_json', __( 'Invalid JSON', 'activitypub' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
return self::init_from_array( $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert input array to a Base_Object.
|
||||
*
|
||||
* @param array $data The object array.
|
||||
*
|
||||
* @return Generic_Object|\WP_Error An Object built from the input array or WP_Error when it's not an array.
|
||||
*/
|
||||
public static function init_from_array( $data ) {
|
||||
if ( ! is_array( $data ) ) {
|
||||
return new \WP_Error( 'invalid_array', __( 'Invalid array', 'activitypub' ), array( 'status' => 400 ) );
|
||||
}
|
||||
|
||||
$object = new static();
|
||||
$object->from_array( $data );
|
||||
|
||||
return $object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array and pre-fill the object.
|
||||
*
|
||||
* @param array $data The array.
|
||||
*/
|
||||
public function from_array( $data ) {
|
||||
foreach ( $data as $key => $value ) {
|
||||
if ( null !== $value ) {
|
||||
$key = camel_to_snake_case( $key );
|
||||
call_user_func( array( $this, 'set_' . $key ), $value );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert JSON input to an array and pre-fill the object.
|
||||
*
|
||||
* @param string $json The JSON string.
|
||||
*/
|
||||
public function from_json( $json ) {
|
||||
$array = \json_decode( $json, true );
|
||||
|
||||
$this->from_array( $array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Object to an array.
|
||||
*
|
||||
* It tries to get the object attributes if they exist
|
||||
* and falls back to the getters. Empty values are ignored.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return array An array built from the Object.
|
||||
*/
|
||||
public function to_array( $include_json_ld_context = true ) {
|
||||
$array = array();
|
||||
$vars = get_object_vars( $this );
|
||||
|
||||
foreach ( $vars as $key => $value ) {
|
||||
if ( \is_wp_error( $value ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Ignore all _prefixed keys.
|
||||
if ( '_' === substr( $key, 0, 1 ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// If value is empty, try to get it from a getter.
|
||||
if ( ! $value ) {
|
||||
$value = call_user_func( array( $this, 'get_' . $key ) );
|
||||
}
|
||||
|
||||
if ( is_object( $value ) ) {
|
||||
$value = $value->to_array( false );
|
||||
}
|
||||
|
||||
// If value is still empty, ignore it for the array and continue.
|
||||
if ( isset( $value ) ) {
|
||||
$array[ snake_to_camel_case( $key ) ] = $value;
|
||||
}
|
||||
}
|
||||
|
||||
if ( $include_json_ld_context ) {
|
||||
// Get JsonLD context and move it to '@context' at the top.
|
||||
$array = array_merge( array( '@context' => $this->get_json_ld_context() ), $array );
|
||||
}
|
||||
|
||||
$class = new \ReflectionClass( $this );
|
||||
$class = strtolower( $class->getShortName() );
|
||||
|
||||
/**
|
||||
* Filter the array of the ActivityPub object.
|
||||
*
|
||||
* @param array $array The array of the ActivityPub object.
|
||||
* @param string $class The class of the ActivityPub object.
|
||||
* @param string $id The ID of the ActivityPub object.
|
||||
* @param Generic_Object $object The ActivityPub object.
|
||||
*
|
||||
* @return array The filtered array of the ActivityPub object.
|
||||
*/
|
||||
$array = \apply_filters( 'activitypub_activity_object_array', $array, $class, $this->id, $this );
|
||||
|
||||
/**
|
||||
* Filter the array of the ActivityPub object by class.
|
||||
*
|
||||
* @param array $array The array of the ActivityPub object.
|
||||
* @param string $id The ID of the ActivityPub object.
|
||||
* @param Generic_Object $object The ActivityPub object.
|
||||
*
|
||||
* @return array The filtered array of the ActivityPub object.
|
||||
*/
|
||||
return \apply_filters( "activitypub_activity_{$class}_object_array", $array, $this->id, $this );
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert Object to JSON.
|
||||
*
|
||||
* @param bool $include_json_ld_context Whether to include the JSON-LD context. Default true.
|
||||
*
|
||||
* @return string The JSON string.
|
||||
*/
|
||||
public function to_json( $include_json_ld_context = true ) {
|
||||
$array = $this->to_array( $include_json_ld_context );
|
||||
$options = \JSON_HEX_TAG | \JSON_HEX_AMP | \JSON_HEX_QUOT | \JSON_UNESCAPED_SLASHES;
|
||||
|
||||
/**
|
||||
* Options to be passed to json_encode().
|
||||
*
|
||||
* @param int $options The current options flags.
|
||||
*/
|
||||
$options = \apply_filters( 'activitypub_json_encode_options', $options );
|
||||
|
||||
return \wp_json_encode( $array, $options );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the keys of the object vars.
|
||||
*
|
||||
* @return array The keys of the object vars.
|
||||
*/
|
||||
public function get_object_var_keys() {
|
||||
return \array_keys( \get_object_vars( $this ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the JSON-LD context of this object.
|
||||
*
|
||||
* @return array $context A compacted JSON-LD context for the ActivityPub object.
|
||||
*/
|
||||
public function get_json_ld_context() {
|
||||
return static::JSON_LD_CONTEXT;
|
||||
}
|
||||
}
|
@ -19,8 +19,8 @@ use Activitypub\Activity\Base_Object;
|
||||
class Event extends Base_Object {
|
||||
// Human friendly minimal context for full Mobilizon compatible ActivityPub events.
|
||||
const JSON_LD_CONTEXT = array(
|
||||
'https://schema.org/', // The base context is schema.org, cause it is used a lot.
|
||||
'https://www.w3.org/ns/activitystreams', // The ActivityStreams context overrides everyting also defined in schema.org.
|
||||
'https://schema.org/', // The base context is schema.org, because it is used a lot.
|
||||
'https://www.w3.org/ns/activitystreams', // The ActivityStreams context overrides everything also defined in schema.org.
|
||||
array( // The keys here override/extend the context even more.
|
||||
'pt' => 'https://joinpeertube.org/ns#',
|
||||
'mz' => 'https://joinmobilizon.org/ns#',
|
||||
@ -50,7 +50,8 @@ class Event extends Base_Object {
|
||||
);
|
||||
|
||||
/**
|
||||
* Mobilizon compatible values for repliesModertaionOption.
|
||||
* Mobilizon compatible values for repliesModerationOption.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const REPLIES_MODERATION_OPTION_TYPES = array( 'allow_all', 'closed' );
|
||||
@ -58,10 +59,11 @@ class Event extends Base_Object {
|
||||
/**
|
||||
* Mobilizon compatible values for joinModeTypes.
|
||||
*/
|
||||
const JOIN_MODE_TYPES = array( 'free', 'restricted', 'external' ); // and 'invite', but not used by mobilizon atm
|
||||
const JOIN_MODE_TYPES = array( 'free', 'restricted', 'external' ); // and 'invite', but not used by mobilizon atm.
|
||||
|
||||
/**
|
||||
* Allowed values for ical VEVENT STATUS.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const ICAL_EVENT_STATUS_TYPES = array( 'TENTATIVE', 'CONFIRMED', 'CANCELLED' );
|
||||
@ -70,6 +72,7 @@ class Event extends Base_Object {
|
||||
* Default event categories.
|
||||
*
|
||||
* These values currently reflect the default set as proposed by Mobilizon to maximize interoperability.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
const DEFAULT_EVENT_CATEGORIES = array(
|
||||
@ -106,8 +109,7 @@ class Event extends Base_Object {
|
||||
);
|
||||
|
||||
/**
|
||||
* Event is an implementation of one of the
|
||||
* Activity Streams
|
||||
* Event is an implementation of one of the Activity Streams.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
@ -115,11 +117,13 @@ class Event extends Base_Object {
|
||||
|
||||
/**
|
||||
* The Title of the event.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The events contacts
|
||||
* The events contacts.
|
||||
*
|
||||
* @context {
|
||||
* '@id' => 'mz:contacts',
|
||||
@ -142,12 +146,16 @@ class Event extends Base_Object {
|
||||
protected $comments_enabled;
|
||||
|
||||
/**
|
||||
* Timezone of the event.
|
||||
*
|
||||
* @context https://joinmobilizon.org/ns#timezone
|
||||
* @var string
|
||||
*/
|
||||
protected $timezone;
|
||||
|
||||
/**
|
||||
* Moderation option for replies.
|
||||
*
|
||||
* @context https://joinmobilizon.org/ns#repliesModerationOption
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#repliesmoderation
|
||||
* @var string
|
||||
@ -155,6 +163,8 @@ class Event extends Base_Object {
|
||||
protected $replies_moderation_option;
|
||||
|
||||
/**
|
||||
* Whether anonymous participation is enabled.
|
||||
*
|
||||
* @context https://joinmobilizon.org/ns#anonymousParticipationEnabled
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#anonymousparticipationenabled
|
||||
* @var bool
|
||||
@ -162,26 +172,34 @@ class Event extends Base_Object {
|
||||
protected $anonymous_participation_enabled;
|
||||
|
||||
/**
|
||||
* The event's category.
|
||||
*
|
||||
* @context https://schema.org/category
|
||||
* @var enum
|
||||
* @var string
|
||||
*/
|
||||
protected $category;
|
||||
|
||||
/**
|
||||
* Language of the event.
|
||||
*
|
||||
* @context https://schema.org/inLanguage
|
||||
* @var
|
||||
* @var string
|
||||
*/
|
||||
protected $in_language;
|
||||
|
||||
/**
|
||||
* Whether the event is online.
|
||||
*
|
||||
* @context https://joinmobilizon.org/ns#isOnline
|
||||
* @var bool
|
||||
*/
|
||||
protected $is_online;
|
||||
|
||||
/**
|
||||
* The event's status.
|
||||
*
|
||||
* @context https://www.w3.org/2002/12/cal/ical#status
|
||||
* @var enum
|
||||
* @var string
|
||||
*/
|
||||
protected $status;
|
||||
|
||||
@ -196,25 +214,33 @@ class Event extends Base_Object {
|
||||
protected $actor;
|
||||
|
||||
/**
|
||||
* The external participation URL.
|
||||
*
|
||||
* @context https://joinmobilizon.org/ns#externalParticipationUrl
|
||||
* @var string
|
||||
*/
|
||||
protected $external_participation_url;
|
||||
|
||||
/**
|
||||
* Indicator of how new members may be able to join.
|
||||
*
|
||||
* @context https://joinmobilizon.org/ns#joinMode
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#joinmode
|
||||
* @var
|
||||
* @var string
|
||||
*/
|
||||
protected $join_mode;
|
||||
|
||||
/**
|
||||
* The participant count of the event.
|
||||
*
|
||||
* @context https://joinmobilizon.org/ns#participantCount
|
||||
* @var int
|
||||
*/
|
||||
protected $participant_count;
|
||||
|
||||
/**
|
||||
* How many places there can be for an event.
|
||||
*
|
||||
* @context https://schema.org/maximumAttendeeCapacity
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#maximumattendeecapacity
|
||||
* @var int
|
||||
@ -222,6 +248,8 @@ class Event extends Base_Object {
|
||||
protected $maximum_attendee_capacity;
|
||||
|
||||
/**
|
||||
* The number of attendee places for an event that remain unallocated.
|
||||
*
|
||||
* @context https://schema.org/remainingAttendeeCapacity
|
||||
* @see https://docs.joinmobilizon.org/contribute/activity_pub/#remainignattendeecapacity
|
||||
* @var int
|
||||
@ -234,6 +262,7 @@ class Event extends Base_Object {
|
||||
* The passed timezone is only set when it is a valid one, otherwise the site's timezone is used.
|
||||
*
|
||||
* @param string $timezone The timezone string to be set, e.g. 'Europe/Berlin'.
|
||||
* @return Event
|
||||
*/
|
||||
public function set_timezone( $timezone ) {
|
||||
if ( in_array( $timezone, timezone_identifiers_list(), true ) ) {
|
||||
@ -246,14 +275,16 @@ class Event extends Base_Object {
|
||||
}
|
||||
|
||||
/**
|
||||
* Custom setter for repliesModerationOption which also directy sets commentsEnabled accordingly.
|
||||
* Custom setter for repliesModerationOption which also directly sets commentsEnabled accordingly.
|
||||
*
|
||||
* @param string $type
|
||||
* @param string $type The type of the replies moderation option.
|
||||
*
|
||||
* @return Event
|
||||
*/
|
||||
public function set_replies_moderation_option( $type ) {
|
||||
if ( in_array( $type, self::REPLIES_MODERATION_OPTION_TYPES, true ) ) {
|
||||
$this->replies_moderation_option = $type;
|
||||
$this->comments_enabled = ( 'allow_all' === $type ) ? true : false;
|
||||
$this->comments_enabled = ( 'allow_all' === $type ) ? true : false;
|
||||
} else {
|
||||
_doing_it_wrong(
|
||||
__METHOD__,
|
||||
@ -268,11 +299,13 @@ class Event extends Base_Object {
|
||||
/**
|
||||
* Custom setter for commentsEnabled which also directly sets repliesModerationOption accordingly.
|
||||
*
|
||||
* @param bool $comments_enabled
|
||||
* @param bool $comments_enabled Whether comments are enabled.
|
||||
*
|
||||
* @return Event
|
||||
*/
|
||||
public function set_comments_enabled( $comments_enabled ) {
|
||||
if ( is_bool( $comments_enabled ) ) {
|
||||
$this->comments_enabled = $comments_enabled;
|
||||
$this->comments_enabled = $comments_enabled;
|
||||
$this->replies_moderation_option = $comments_enabled ? 'allow_all' : 'closed';
|
||||
} else {
|
||||
_doing_it_wrong(
|
||||
@ -288,7 +321,9 @@ class Event extends Base_Object {
|
||||
/**
|
||||
* Custom setter for the ical status that checks whether the status is an ical event status.
|
||||
*
|
||||
* @param string $status
|
||||
* @param string $status The status of the event.
|
||||
*
|
||||
* @return Event
|
||||
*/
|
||||
public function set_status( $status ) {
|
||||
if ( in_array( $status, self::ICAL_EVENT_STATUS_TYPES, true ) ) {
|
||||
@ -307,13 +342,15 @@ class Event extends Base_Object {
|
||||
/**
|
||||
* Custom setter for the event category.
|
||||
*
|
||||
* Falls back to Mobilizons default category.
|
||||
* Falls back to Mobilizon's default category.
|
||||
*
|
||||
* @param string $category
|
||||
* @param bool $mobilizon_compatibilty Whether the category must be compatibly with Mobilizon.
|
||||
* @param string $category The category of the event.
|
||||
* @param bool $mobilizon_compatibility Optional. Whether the category must be compatibly with Mobilizon. Default true.
|
||||
*
|
||||
* @return Event
|
||||
*/
|
||||
public function set_category( $category, $mobilizon_compatibilty = true ) {
|
||||
if ( $mobilizon_compatibilty ) {
|
||||
public function set_category( $category, $mobilizon_compatibility = true ) {
|
||||
if ( $mobilizon_compatibility ) {
|
||||
$this->category = in_array( $category, self::DEFAULT_EVENT_CATEGORIES, true ) ? $category : 'MEETING';
|
||||
} else {
|
||||
$this->category = $category;
|
||||
@ -327,12 +364,14 @@ class Event extends Base_Object {
|
||||
*
|
||||
* Automatically sets the joinMode to true if called.
|
||||
*
|
||||
* @param string $url
|
||||
* @param string $url The URL for external participation.
|
||||
*
|
||||
* @return Event
|
||||
*/
|
||||
public function set_external_participation_url( $url ) {
|
||||
if ( preg_match( '/^https?:\/\/.*/i', $url ) ) {
|
||||
$this->external_participation_url = $url;
|
||||
$this->join_mode = 'external';
|
||||
$this->join_mode = 'external';
|
||||
}
|
||||
|
||||
return $this;
|
||||
|
@ -38,8 +38,8 @@ class Place extends Base_Object {
|
||||
protected $accuracy;
|
||||
|
||||
/**
|
||||
* Indicates the altitude of a place. The measurement units is indicated using the units property.
|
||||
* If units is not specified, the default is assumed to be "m" indicating meters.
|
||||
* Indicates the altitude of a place. The measurement unit is indicated using the unit's property.
|
||||
* If unit is not specified, the default is assumed to be "m" indicating meters.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-altitude
|
||||
* @var float xsd:float
|
||||
@ -63,22 +63,34 @@ class Place extends Base_Object {
|
||||
protected $longitude;
|
||||
|
||||
/**
|
||||
* The radius from the given latitude and longitude for a Place.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-radius
|
||||
* @var float
|
||||
*/
|
||||
protected $radius;
|
||||
|
||||
/**
|
||||
* Specifies the measurement units for the `radius` and `altitude` properties.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-units
|
||||
* @var string
|
||||
*/
|
||||
protected $units;
|
||||
|
||||
/**
|
||||
* @var Postal_Address|string
|
||||
* The address of the place.
|
||||
*
|
||||
* @see https://schema.org/PostalAddress
|
||||
* @var array|string
|
||||
*/
|
||||
protected $address;
|
||||
|
||||
/**
|
||||
* Set the address of the place.
|
||||
*
|
||||
* @param array|string $address The address of the place.
|
||||
*/
|
||||
public function set_address( $address ) {
|
||||
if ( is_string( $address ) || is_array( $address ) ) {
|
||||
$this->address = $address;
|
||||
|
@ -1,225 +0,0 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Post;
|
||||
use WP_Comment;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Transformer\Factory;
|
||||
use Activitypub\Transformer\Post;
|
||||
use Activitypub\Transformer\Comment;
|
||||
|
||||
use function Activitypub\is_single_user;
|
||||
use function Activitypub\is_user_disabled;
|
||||
use function Activitypub\safe_remote_post;
|
||||
use function Activitypub\set_wp_object_state;
|
||||
|
||||
/**
|
||||
* ActivityPub Activity_Dispatcher Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/
|
||||
*/
|
||||
class Activity_Dispatcher {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_send_post', array( self::class, 'send_post' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_comment', array( self::class, 'send_comment' ), 10, 2 );
|
||||
|
||||
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_activity', array( self::class, 'send_activity_or_announce' ), 10, 2 );
|
||||
\add_action( 'activitypub_send_update_profile_activity', array( self::class, 'send_profile_update' ), 10, 1 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Activities to followers and mentioned users or `Announce` (boost) a blog post.
|
||||
*
|
||||
* @param mixed $wp_object The ActivityPub Post.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_activity_or_announce( $wp_object, $type ) {
|
||||
if ( is_user_type_disabled( 'blog' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_single_user() ) {
|
||||
self::send_activity( $wp_object, $type, Users::BLOG_USER_ID );
|
||||
} else {
|
||||
self::send_announce( $wp_object, $type );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Activities to followers and mentioned users.
|
||||
*
|
||||
* @param mixed $wp_object The ActivityPub Post.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_activity( $wp_object, $type, $user_id = null ) {
|
||||
$transformer = Factory::get_transformer( $wp_object );
|
||||
|
||||
if ( null !== $user_id ) {
|
||||
$transformer->change_wp_user_id( $user_id );
|
||||
}
|
||||
|
||||
$user_id = $transformer->get_wp_user_id();
|
||||
|
||||
if ( is_user_disabled( $user_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$activity = $transformer->to_activity( $type );
|
||||
|
||||
self::send_activity_to_followers( $activity, $user_id, $wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Announces to followers and mentioned users.
|
||||
*
|
||||
* @param mixed $wp_object The ActivityPub Post.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_announce( $wp_object, $type ) {
|
||||
if ( ! in_array( $type, array( 'Create', 'Update' ), true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$transformer = Factory::get_transformer( $wp_object );
|
||||
$transformer->change_wp_user_id( Users::BLOG_USER_ID );
|
||||
|
||||
$user_id = $transformer->get_wp_user_id();
|
||||
$activity = $transformer->to_activity( 'Announce' );
|
||||
|
||||
self::send_activity_to_followers( $activity, $user_id, $wp_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "Update" Activity when a user updates their profile.
|
||||
*
|
||||
* @param int $user_id The user ID to send an update for.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_profile_update( $user_id ) {
|
||||
$user = Users::get_by_various( $user_id );
|
||||
|
||||
// bail if that's not a good user
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// build the update
|
||||
$activity = new Activity();
|
||||
$activity->set_id( $user->get_url() . '#update' );
|
||||
$activity->set_type( 'Update' );
|
||||
$activity->set_actor( $user->get_url() );
|
||||
$activity->set_object( $user->get_url() );
|
||||
$activity->set_to( 'https://www.w3.org/ns/activitystreams#Public' );
|
||||
|
||||
// send the update
|
||||
self::send_activity_to_followers( $activity, $user_id, $user );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an Activity to all followers and mentioned users.
|
||||
*
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
* @param int $user_id The user ID.
|
||||
* @param WP_User|WP_Post|WP_Comment $wp_object The WordPress object.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function send_activity_to_followers( $activity, $user_id, $wp_object ) {
|
||||
// check if the Activity should be send to the followers
|
||||
if ( ! apply_filters( 'activitypub_send_activity_to_followers', true, $activity, $user_id, $wp_object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$follower_inboxes = Followers::get_inboxes( $user_id );
|
||||
|
||||
$mentioned_inboxes = array();
|
||||
$cc = $activity->get_cc();
|
||||
if ( $cc ) {
|
||||
$mentioned_inboxes = Mention::get_inboxes( $cc );
|
||||
}
|
||||
|
||||
$inboxes = array_merge( $follower_inboxes, $mentioned_inboxes );
|
||||
$inboxes = array_unique( $inboxes );
|
||||
|
||||
if ( empty( $inboxes ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$json = $activity->to_json();
|
||||
|
||||
foreach ( $inboxes as $inbox ) {
|
||||
safe_remote_post( $inbox, $json, $user_id );
|
||||
}
|
||||
|
||||
set_wp_object_state( $wp_object, 'federated' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "Create" or "Update" Activity for a WordPress Post.
|
||||
*
|
||||
* @param int $id The WordPress Post ID.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_post( $id, $type ) {
|
||||
$post = get_post( $id );
|
||||
|
||||
if ( ! $post ) {
|
||||
return;
|
||||
}
|
||||
|
||||
do_action( 'activitypub_send_activity', $post, $type );
|
||||
do_action(
|
||||
sprintf(
|
||||
'activitypub_send_%s_activity',
|
||||
\strtolower( $type )
|
||||
),
|
||||
$post
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a "Create" or "Update" Activity for a WordPress Comment.
|
||||
*
|
||||
* @param int $id The WordPress Comment ID.
|
||||
* @param string $type The Activity-Type.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function send_comment( $id, $type ) {
|
||||
$comment = get_comment( $id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return;
|
||||
}
|
||||
|
||||
do_action( 'activitypub_send_activity', $comment, $type );
|
||||
do_action(
|
||||
sprintf(
|
||||
'activitypub_send_%s_activity',
|
||||
\strtolower( $type )
|
||||
),
|
||||
$comment
|
||||
);
|
||||
}
|
||||
}
|
@ -1,19 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub Class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Exception;
|
||||
use Activitypub\Signature;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Outbox;
|
||||
use Activitypub\Collection\Followers;
|
||||
|
||||
use function Activitypub\is_comment;
|
||||
use function Activitypub\sanitize_url;
|
||||
use function Activitypub\is_local_comment;
|
||||
use function Activitypub\is_activitypub_request;
|
||||
use function Activitypub\should_comment_be_federated;
|
||||
use Activitypub\Collection\Extra_Fields;
|
||||
|
||||
/**
|
||||
* ActivityPub Class
|
||||
* ActivityPub Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
@ -22,13 +23,15 @@ class Activitypub {
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'template_include', array( self::class, 'render_json_template' ), 99 );
|
||||
\add_filter( 'template_include', array( self::class, 'render_activitypub_template' ), 99 );
|
||||
\add_action( 'template_redirect', array( self::class, 'template_redirect' ) );
|
||||
\add_filter( 'redirect_canonical', array( self::class, 'redirect_canonical' ), 10, 2 );
|
||||
\add_filter( 'redirect_canonical', array( self::class, 'no_trailing_redirect' ), 10, 2 );
|
||||
\add_filter( 'query_vars', array( self::class, 'add_query_vars' ) );
|
||||
\add_filter( 'pre_get_avatar_data', array( self::class, 'pre_get_avatar_data' ), 11, 2 );
|
||||
|
||||
// Add support for ActivityPub to custom post types
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) ) ? \get_option( 'activitypub_support_post_types', array( 'post' ) ) : array();
|
||||
// Add support for ActivityPub to custom post types.
|
||||
$post_types = \get_option( 'activitypub_support_post_types', array( 'post' ) );
|
||||
|
||||
foreach ( $post_types as $post_type ) {
|
||||
\add_post_type_support( $post_type, 'activitypub' );
|
||||
@ -42,39 +45,84 @@ class Activitypub {
|
||||
|
||||
\add_action( 'user_register', array( self::class, 'user_register' ) );
|
||||
|
||||
\add_action( 'in_plugin_update_message-' . ACTIVITYPUB_PLUGIN_BASENAME, array( self::class, 'plugin_update_message' ) );
|
||||
\add_filter( 'activitypub_get_actor_extra_fields', array( Extra_Fields::class, 'default_actor_extra_fields' ), 10, 2 );
|
||||
|
||||
// register several post_types
|
||||
\add_action( 'updated_postmeta', array( self::class, 'updated_postmeta' ), 10, 4 );
|
||||
\add_action( 'added_post_meta', array( self::class, 'updated_postmeta' ), 10, 4 );
|
||||
|
||||
\add_action( 'init', array( self::class, 'register_user_meta' ), 11 );
|
||||
|
||||
// Register several post_types.
|
||||
self::register_post_types();
|
||||
|
||||
self::register_oembed_providers();
|
||||
Embed::init();
|
||||
}
|
||||
|
||||
/**
|
||||
* Activation Hook
|
||||
*
|
||||
* @return void
|
||||
* Activation Hook.
|
||||
*/
|
||||
public static function activate() {
|
||||
self::flush_rewrite_rules();
|
||||
Scheduler::register_schedules();
|
||||
|
||||
\add_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ), 10, 3 );
|
||||
Migration::update_comment_counts();
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivation Hook
|
||||
*
|
||||
* @return void
|
||||
* Deactivation Hook.
|
||||
*/
|
||||
public static function deactivate() {
|
||||
self::flush_rewrite_rules();
|
||||
Scheduler::deregister_schedules();
|
||||
|
||||
\remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) );
|
||||
Migration::update_comment_counts( 2000 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Uninstall Hook
|
||||
*
|
||||
* @return void
|
||||
* Uninstall Hook.
|
||||
*/
|
||||
public static function uninstall() {
|
||||
Scheduler::deregister_schedules();
|
||||
|
||||
\remove_filter( 'pre_wp_update_comment_count_now', array( Comment::class, 'pre_wp_update_comment_count_now' ) );
|
||||
Migration::update_comment_counts( 2000 );
|
||||
|
||||
\delete_option( 'activitypub_actor_mode' );
|
||||
\delete_option( 'activitypub_allow_likes' );
|
||||
\delete_option( 'activitypub_allow_replies' );
|
||||
\delete_option( 'activitypub_attribution_domains' );
|
||||
\delete_option( 'activitypub_authorized_fetch' );
|
||||
\delete_option( 'activitypub_application_user_private_key' );
|
||||
\delete_option( 'activitypub_application_user_public_key' );
|
||||
\delete_option( 'activitypub_blog_user_also_known_as' );
|
||||
\delete_option( 'activitypub_blog_user_mailer_new_dm' );
|
||||
\delete_option( 'activitypub_blog_user_mailer_new_follower' );
|
||||
\delete_option( 'activitypub_blog_user_mailer_new_mention' );
|
||||
\delete_option( 'activitypub_blog_user_moved_to' );
|
||||
\delete_option( 'activitypub_blog_user_private_key' );
|
||||
\delete_option( 'activitypub_blog_user_public_key' );
|
||||
\delete_option( 'activitypub_blog_description' );
|
||||
\delete_option( 'activitypub_blog_identifier' );
|
||||
\delete_option( 'activitypub_custom_post_content' );
|
||||
\delete_option( 'activitypub_db_version' );
|
||||
\delete_option( 'activitypub_default_extra_fields' );
|
||||
\delete_option( 'activitypub_enable_blog_user' );
|
||||
\delete_option( 'activitypub_enable_users' );
|
||||
\delete_option( 'activitypub_header_image' );
|
||||
\delete_option( 'activitypub_last_post_with_permalink_as_id' );
|
||||
\delete_option( 'activitypub_max_image_attachments' );
|
||||
\delete_option( 'activitypub_migration_lock' );
|
||||
\delete_option( 'activitypub_object_type' );
|
||||
\delete_option( 'activitypub_outbox_purge_days' );
|
||||
\delete_option( 'activitypub_shared_inbox' );
|
||||
\delete_option( 'activitypub_support_post_types' );
|
||||
\delete_option( 'activitypub_use_hashtags' );
|
||||
\delete_option( 'activitypub_use_opengraph' );
|
||||
\delete_option( 'activitypub_use_permalink_as_id_for_blog' );
|
||||
\delete_option( 'activitypub_vary_header' );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -84,46 +132,144 @@ class Activitypub {
|
||||
*
|
||||
* @return string The new path to the JSON template.
|
||||
*/
|
||||
public static function render_json_template( $template ) {
|
||||
if ( defined( 'REST_REQUEST' ) && REST_REQUEST ) {
|
||||
public static function render_activitypub_template( $template ) {
|
||||
if ( \wp_is_serving_rest_request() || \wp_doing_ajax() ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
self::add_headers();
|
||||
|
||||
if ( ! is_activitypub_request() ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
$json_template = false;
|
||||
$activitypub_template = false;
|
||||
$activitypub_object = Query::get_instance()->get_activitypub_object();
|
||||
|
||||
// check if user can publish posts
|
||||
if ( \is_author() && is_wp_error( Users::get_by_id( \get_the_author_meta( 'ID' ) ) ) ) {
|
||||
return $template;
|
||||
if ( $activitypub_object ) {
|
||||
if ( \get_query_var( 'preview' ) ) {
|
||||
\define( 'ACTIVITYPUB_PREVIEW', true );
|
||||
|
||||
/**
|
||||
* Filter the template used for the ActivityPub preview.
|
||||
*
|
||||
* @param string $activitypub_template Absolute path to the template file.
|
||||
*/
|
||||
$activitypub_template = apply_filters( 'activitypub_preview_template', ACTIVITYPUB_PLUGIN_DIR . '/templates/post-preview.php' );
|
||||
} else {
|
||||
$activitypub_template = ACTIVITYPUB_PLUGIN_DIR . 'templates/activitypub-json.php';
|
||||
}
|
||||
}
|
||||
|
||||
// check if blog-user is enabled
|
||||
if ( \is_home() && is_wp_error( Users::get_by_id( Users::BLOG_USER_ID ) ) ) {
|
||||
return $template;
|
||||
}
|
||||
|
||||
if ( \is_author() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/author-json.php';
|
||||
} elseif ( is_comment() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/comment-json.php';
|
||||
} elseif ( \is_singular() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/post-json.php';
|
||||
} elseif ( \is_home() ) {
|
||||
$json_template = ACTIVITYPUB_PLUGIN_DIR . '/templates/blog-json.php';
|
||||
}
|
||||
|
||||
if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
|
||||
/*
|
||||
* Check if the request is authorized.
|
||||
*
|
||||
* @see https://www.w3.org/wiki/SocialCG/ActivityPub/Primer/Authentication_Authorization#Authorized_fetch
|
||||
* @see https://swicg.github.io/activitypub-http-signature/#authorized-fetch
|
||||
*/
|
||||
if ( $activitypub_template && use_authorized_fetch() ) {
|
||||
$verification = Signature::verify_http_signature( $_SERVER );
|
||||
if ( \is_wp_error( $verification ) ) {
|
||||
// fallback as template_loader can't return http headers
|
||||
header( 'HTTP/1.1 401 Unauthorized' );
|
||||
|
||||
// Fallback as template_loader can't return http headers.
|
||||
return $template;
|
||||
}
|
||||
}
|
||||
|
||||
return $json_template;
|
||||
if ( $activitypub_template ) {
|
||||
\set_query_var( 'is_404', false );
|
||||
|
||||
// Check if header already sent.
|
||||
if ( ! \headers_sent() ) {
|
||||
// Send 200 status header.
|
||||
\status_header( 200 );
|
||||
}
|
||||
|
||||
return $activitypub_template;
|
||||
}
|
||||
|
||||
return $template;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the 'self' link to the header.
|
||||
*/
|
||||
public static function add_headers() {
|
||||
$id = Query::get_instance()->get_activitypub_object_id();
|
||||
|
||||
if ( ! $id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! headers_sent() ) {
|
||||
\header( 'Link: <' . esc_url( $id ) . '>; title="ActivityPub (JSON)"; rel="alternate"; type="application/activity+json"', false );
|
||||
|
||||
if ( \get_option( 'activitypub_vary_header' ) ) {
|
||||
// Send Vary header for Accept header.
|
||||
\header( 'Vary: Accept', false );
|
||||
}
|
||||
}
|
||||
|
||||
add_action(
|
||||
'wp_head',
|
||||
function () use ( $id ) {
|
||||
echo PHP_EOL . '<link rel="alternate" title="ActivityPub (JSON)" type="application/activity+json" href="' . esc_url( $id ) . '" />' . PHP_EOL;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove trailing slash from ActivityPub @username requests.
|
||||
*
|
||||
* @param string $redirect_url The URL to redirect to.
|
||||
* @param string $requested_url The requested URL.
|
||||
*
|
||||
* @return string $redirect_url The possibly-unslashed redirect URL.
|
||||
*/
|
||||
public static function no_trailing_redirect( $redirect_url, $requested_url ) {
|
||||
if ( get_query_var( 'actor' ) ) {
|
||||
return $requested_url;
|
||||
}
|
||||
|
||||
return $redirect_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add support for `p` and `author` query vars.
|
||||
*
|
||||
* @param string $redirect_url The URL to redirect to.
|
||||
* @param string $requested_url The requested URL.
|
||||
*
|
||||
* @return string $redirect_url
|
||||
*/
|
||||
public static function redirect_canonical( $redirect_url, $requested_url ) {
|
||||
if ( ! is_activitypub_request() ) {
|
||||
return $redirect_url;
|
||||
}
|
||||
|
||||
$query = \wp_parse_url( $requested_url, PHP_URL_QUERY );
|
||||
|
||||
if ( ! $query ) {
|
||||
return $redirect_url;
|
||||
}
|
||||
|
||||
$query_params = \wp_parse_args( $query );
|
||||
unset( $query_params['activitypub'] );
|
||||
|
||||
if ( 1 !== count( $query_params ) ) {
|
||||
return $redirect_url;
|
||||
}
|
||||
|
||||
if ( isset( $query_params['p'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( isset( $query_params['author'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $requested_url;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -132,36 +278,64 @@ class Activitypub {
|
||||
* @return void
|
||||
*/
|
||||
public static function template_redirect() {
|
||||
global $wp_query;
|
||||
|
||||
$comment_id = get_query_var( 'c', null );
|
||||
|
||||
// check if it seems to be a comment
|
||||
if ( ! $comment_id ) {
|
||||
return;
|
||||
// Check if it seems to be a comment.
|
||||
if ( $comment_id ) {
|
||||
$comment = get_comment( $comment_id );
|
||||
|
||||
// Load a 404-page if `c` is set but not valid.
|
||||
if ( ! $comment ) {
|
||||
$wp_query->set_404();
|
||||
return;
|
||||
}
|
||||
|
||||
// Stop if it's not an ActivityPub comment.
|
||||
if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_safe_redirect( get_comment_link( $comment ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
$comment = get_comment( $comment_id );
|
||||
$actor = get_query_var( 'actor', null );
|
||||
if ( $actor ) {
|
||||
$actor = Actors::get_by_username( $actor );
|
||||
if ( ! $actor || \is_wp_error( $actor ) ) {
|
||||
$wp_query->set_404();
|
||||
return;
|
||||
}
|
||||
|
||||
// load a 404 page if `c` is set but not valid
|
||||
if ( ! $comment ) {
|
||||
global $wp_query;
|
||||
$wp_query->set_404();
|
||||
return;
|
||||
if ( is_activitypub_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $actor->get__id() > 0 ) {
|
||||
$redirect_url = $actor->get_url();
|
||||
} else {
|
||||
$redirect_url = get_bloginfo( 'url' );
|
||||
}
|
||||
|
||||
wp_safe_redirect( $redirect_url, 301 );
|
||||
exit;
|
||||
}
|
||||
|
||||
// stop if it's not an ActivityPub comment
|
||||
if ( is_activitypub_request() && ! is_local_comment( $comment ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
wp_safe_redirect( get_comment_link( $comment ) );
|
||||
exit;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the 'activitypub' query variable so WordPress won't mangle it.
|
||||
*
|
||||
* @param array $vars The query variables.
|
||||
*
|
||||
* @return array The query variables.
|
||||
*/
|
||||
public static function add_query_vars( $vars ) {
|
||||
$vars[] = 'activitypub';
|
||||
$vars[] = 'preview';
|
||||
$vars[] = 'author';
|
||||
$vars[] = 'actor';
|
||||
$vars[] = 'c';
|
||||
$vars[] = 'p';
|
||||
|
||||
@ -200,7 +374,7 @@ class Activitypub {
|
||||
}
|
||||
|
||||
// Check if comment has an avatar.
|
||||
$avatar = self::get_avatar_url( $id_or_email->comment_ID );
|
||||
$avatar = \get_comment_meta( $id_or_email->comment_ID, 'avatar_url', true );
|
||||
|
||||
if ( $avatar ) {
|
||||
if ( empty( $args['class'] ) ) {
|
||||
@ -218,53 +392,37 @@ class Activitypub {
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to retrieve Avatar URL if stored in meta.
|
||||
*
|
||||
* @param int|WP_Comment $comment
|
||||
*
|
||||
* @return string $url
|
||||
*/
|
||||
public static function get_avatar_url( $comment ) {
|
||||
if ( \is_numeric( $comment ) ) {
|
||||
$comment = \get_comment( $comment );
|
||||
}
|
||||
return \get_comment_meta( $comment->comment_ID, 'avatar_url', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Store permalink in meta, to send delete Activity.
|
||||
*
|
||||
* @param string $post_id The Post ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function trash_post( $post_id ) {
|
||||
\add_post_meta(
|
||||
$post_id,
|
||||
'activitypub_canonical_url',
|
||||
'_activitypub_canonical_url',
|
||||
\get_permalink( $post_id ),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete permalink from meta
|
||||
* Delete permalink from meta.
|
||||
*
|
||||
* @param string $post_id The Post ID
|
||||
*
|
||||
* @return void
|
||||
* @param string $post_id The Post ID.
|
||||
*/
|
||||
public static function untrash_post( $post_id ) {
|
||||
\delete_post_meta( $post_id, 'activitypub_canonical_url' );
|
||||
\delete_post_meta( $post_id, '_activitypub_canonical_url' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add rewrite rules
|
||||
* Add rewrite rules.
|
||||
*/
|
||||
public static function add_rewrite_rules() {
|
||||
// If another system needs to take precedence over the ActivityPub rewrite rules,
|
||||
// they can define their own and will manually call the appropriate functions as required.
|
||||
/*
|
||||
* If another system needs to take precedence over the ActivityPub rewrite rules,
|
||||
* they can define their own and will manually call the appropriate functions as required.
|
||||
*/
|
||||
if ( ACTIVITYPUB_DISABLE_REWRITES ) {
|
||||
return;
|
||||
}
|
||||
@ -280,27 +438,17 @@ class Activitypub {
|
||||
if ( ! \class_exists( 'Nodeinfo_Endpoint' ) && true === (bool) \get_option( 'blog_public', 1 ) ) {
|
||||
\add_rewrite_rule(
|
||||
'^.well-known/nodeinfo',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo/discovery',
|
||||
'top'
|
||||
);
|
||||
\add_rewrite_rule(
|
||||
'^.well-known/x-nodeinfo2',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo2',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/nodeinfo',
|
||||
'top'
|
||||
);
|
||||
}
|
||||
|
||||
\add_rewrite_rule(
|
||||
'^@([\w\-\.]+)',
|
||||
'index.php?rest_route=/' . ACTIVITYPUB_REST_NAMESPACE . '/users/$matches[1]',
|
||||
'top'
|
||||
);
|
||||
|
||||
\add_rewrite_rule( '^@([\w\-\.]+)\/?$', 'index.php?actor=$matches[1]', 'top' );
|
||||
\add_rewrite_endpoint( 'activitypub', EP_AUTHORS | EP_PERMALINK | EP_PAGES );
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush rewrite rules;
|
||||
* Flush rewrite rules.
|
||||
*/
|
||||
public static function flush_rewrite_rules() {
|
||||
self::add_rewrite_rules();
|
||||
@ -308,37 +456,10 @@ class Activitypub {
|
||||
}
|
||||
|
||||
/**
|
||||
* Theme compatibility stuff
|
||||
*
|
||||
* @return void
|
||||
* Theme compatibility stuff.
|
||||
*/
|
||||
public static function theme_compat() {
|
||||
$site_icon = get_theme_support( 'custom-logo' );
|
||||
|
||||
if ( ! $site_icon ) {
|
||||
// custom logo support
|
||||
add_theme_support(
|
||||
'custom-logo',
|
||||
array(
|
||||
'height' => 80,
|
||||
'width' => 80,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$custom_header = get_theme_support( 'custom-header' );
|
||||
|
||||
if ( ! $custom_header ) {
|
||||
// This theme supports a custom header
|
||||
$custom_header_args = array(
|
||||
'width' => 1250,
|
||||
'height' => 600,
|
||||
'header-text' => true,
|
||||
);
|
||||
add_theme_support( 'custom-header', $custom_header_args );
|
||||
}
|
||||
|
||||
// We assume that you want to use Post-Formats when enabling the setting
|
||||
// We assume that you want to use Post-Formats when enabling the setting.
|
||||
if ( 'wordpress-post-format' === \get_option( 'activitypub_object_type', ACTIVITYPUB_DEFAULT_OBJECT_TYPE ) ) {
|
||||
if ( ! get_theme_support( 'post-formats' ) ) {
|
||||
// Add support for the Aside, Gallery Post Formats...
|
||||
@ -357,35 +478,7 @@ class Activitypub {
|
||||
}
|
||||
|
||||
/**
|
||||
* Display plugin upgrade notice to users
|
||||
*
|
||||
* @param array $data The plugin data
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function plugin_update_message( $data ) {
|
||||
if ( ! isset( $data['upgrade_notice'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
printf(
|
||||
'<div class="update-message">%s</div>',
|
||||
wp_kses(
|
||||
wpautop( $data['upgrade_notice '] ),
|
||||
array(
|
||||
'p' => array(),
|
||||
'a' => array( 'href', 'title' ),
|
||||
'strong' => array(),
|
||||
'em' => array(),
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the "Followers" Taxonomy
|
||||
*
|
||||
* @return void
|
||||
* Register Custom Post Types.
|
||||
*/
|
||||
private static function register_post_types() {
|
||||
\register_post_type(
|
||||
@ -407,7 +500,7 @@ class Activitypub {
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_inbox',
|
||||
'_activitypub_inbox',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
@ -417,7 +510,7 @@ class Activitypub {
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_errors',
|
||||
'_activitypub_errors',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => false,
|
||||
@ -433,7 +526,7 @@ class Activitypub {
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_user_id',
|
||||
'_activitypub_user_id',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => false,
|
||||
@ -445,7 +538,7 @@ class Activitypub {
|
||||
|
||||
\register_post_meta(
|
||||
Followers::POST_TYPE,
|
||||
'activitypub_actor_json',
|
||||
'_activitypub_actor_json',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
@ -455,14 +548,168 @@ class Activitypub {
|
||||
)
|
||||
);
|
||||
|
||||
// Register Outbox Post-Type.
|
||||
register_post_type(
|
||||
Outbox::POST_TYPE,
|
||||
array(
|
||||
'labels' => array(
|
||||
'name' => _x( 'Outbox', 'post_type plural name', 'activitypub' ),
|
||||
'singular_name' => _x( 'Outbox Item', 'post_type single name', 'activitypub' ),
|
||||
),
|
||||
'capabilities' => array(
|
||||
'create_posts' => false,
|
||||
),
|
||||
'map_meta_cap' => true,
|
||||
'public' => false,
|
||||
'show_in_rest' => true,
|
||||
'rewrite' => false,
|
||||
'query_var' => false,
|
||||
'supports' => array( 'title', 'editor', 'author', 'custom-fields' ),
|
||||
'delete_with_user' => true,
|
||||
'can_export' => true,
|
||||
'exclude_from_search' => true,
|
||||
)
|
||||
);
|
||||
|
||||
/**
|
||||
* Register Activity Type meta for Outbox items.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#activity-types
|
||||
*/
|
||||
\register_post_meta(
|
||||
Outbox::POST_TYPE,
|
||||
'_activitypub_activity_type',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => 'The type of the activity',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
$value = ucfirst( strtolower( $value ) );
|
||||
$schema = array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'Accept', 'Add', 'Announce', 'Arrive', 'Block', 'Create', 'Delete', 'Dislike', 'Flag', 'Follow', 'Ignore', 'Invite', 'Join', 'Leave', 'Like', 'Listen', 'Move', 'Offer', 'Question', 'Reject', 'Read', 'Remove', 'TentativeReject', 'TentativeAccept', 'Travel', 'Undo', 'Update', 'View' ),
|
||||
'default' => 'Announce',
|
||||
);
|
||||
|
||||
if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
|
||||
return $schema['default'];
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Outbox::POST_TYPE,
|
||||
'_activitypub_activity_actor',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
$schema = array(
|
||||
'type' => 'string',
|
||||
'enum' => array( 'application', 'blog', 'user' ),
|
||||
'default' => 'user',
|
||||
);
|
||||
|
||||
if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
|
||||
return $schema['default'];
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Outbox::POST_TYPE,
|
||||
'_activitypub_outbox_offset',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'single' => true,
|
||||
'description' => 'Keeps track of the followers offset when processing outbox items.',
|
||||
'sanitize_callback' => 'absint',
|
||||
'default' => 0,
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Outbox::POST_TYPE,
|
||||
'_activitypub_object_id',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
'description' => 'The ID (ActivityPub URI) of the object that the outbox item is about.',
|
||||
'sanitize_callback' => 'sanitize_url',
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
Outbox::POST_TYPE,
|
||||
'activitypub_content_visibility',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
$schema = array(
|
||||
'type' => 'string',
|
||||
'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
|
||||
'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
|
||||
);
|
||||
|
||||
if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
|
||||
return $schema['default'];
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
// Both User and Blog Extra Fields types have the same args.
|
||||
$args = array(
|
||||
'labels' => array(
|
||||
'name' => _x( 'Extra fields', 'post_type plural name', 'activitypub' ),
|
||||
'singular_name' => _x( 'Extra field', 'post_type single name', 'activitypub' ),
|
||||
'add_new' => __( 'Add new', 'activitypub' ),
|
||||
'add_new_item' => __( 'Add new extra field', 'activitypub' ),
|
||||
'new_item' => __( 'New extra field', 'activitypub' ),
|
||||
'edit_item' => __( 'Edit extra field', 'activitypub' ),
|
||||
'view_item' => __( 'View extra field', 'activitypub' ),
|
||||
'all_items' => __( 'All extra fields', 'activitypub' ),
|
||||
),
|
||||
'public' => false,
|
||||
'hierarchical' => false,
|
||||
'query_var' => false,
|
||||
'has_archive' => false,
|
||||
'publicly_queryable' => false,
|
||||
'show_in_menu' => false,
|
||||
'delete_with_user' => true,
|
||||
'can_export' => true,
|
||||
'exclude_from_search' => true,
|
||||
'show_in_rest' => true,
|
||||
'map_meta_cap' => true,
|
||||
'show_ui' => true,
|
||||
'supports' => array( 'title', 'editor', 'page-attributes' ),
|
||||
);
|
||||
|
||||
\register_post_type( Extra_Fields::USER_POST_TYPE, $args );
|
||||
\register_post_type( Extra_Fields::BLOG_POST_TYPE, $args );
|
||||
|
||||
/**
|
||||
* Fires after ActivityPub custom post types have been registered.
|
||||
*/
|
||||
\do_action( 'activitypub_after_register_post_type' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the 'activitypub' query variable so WordPress won't mangle it.
|
||||
* Add the 'activitypub' capability to users who can publish posts.
|
||||
*
|
||||
* @param int $user_id User ID.
|
||||
* @param array $userdata The raw array of data passed to wp_insert_user().
|
||||
* @param int $user_id User ID.
|
||||
*/
|
||||
public static function user_register( $user_id ) {
|
||||
if ( \user_can( $user_id, 'publish_posts' ) ) {
|
||||
@ -470,4 +717,182 @@ class Activitypub {
|
||||
$user->add_cap( 'activitypub' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete `activitypub_content_visibility` when updated to an empty value.
|
||||
*
|
||||
* @param int $meta_id ID of updated metadata entry.
|
||||
* @param int $object_id Post ID.
|
||||
* @param string $meta_key Metadata key.
|
||||
* @param mixed $meta_value Metadata value. This will be a PHP-serialized string representation of the value
|
||||
* if the value is an array, an object, or itself a PHP-serialized string.
|
||||
*/
|
||||
public static function updated_postmeta( $meta_id, $object_id, $meta_key, $meta_value ) {
|
||||
if ( 'activitypub_content_visibility' === $meta_key && empty( $meta_value ) ) {
|
||||
\delete_post_meta( $object_id, 'activitypub_content_visibility' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register some Mastodon oEmbed providers.
|
||||
*/
|
||||
public static function register_oembed_providers() {
|
||||
\wp_oembed_add_provider( '#https?://mastodon\.social/(@.+)/([0-9]+)#i', 'https://mastodon.social/api/oembed', true );
|
||||
\wp_oembed_add_provider( '#https?://mastodon\.online/(@.+)/([0-9]+)#i', 'https://mastodon.online/api/oembed', true );
|
||||
\wp_oembed_add_provider( '#https?://mastodon\.cloud/(@.+)/([0-9]+)#i', 'https://mastodon.cloud/api/oembed', true );
|
||||
\wp_oembed_add_provider( '#https?://mstdn\.social/(@.+)/([0-9]+)#i', 'https://mstdn.social/api/oembed', true );
|
||||
\wp_oembed_add_provider( '#https?://mastodon\.world/(@.+)/([0-9]+)#i', 'https://mastodon.world/api/oembed', true );
|
||||
\wp_oembed_add_provider( '#https?://mas\.to/(@.+)/([0-9]+)#i', 'https://mas.to/api/oembed', true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register user meta.
|
||||
*/
|
||||
public static function register_user_meta() {
|
||||
$blog_prefix = $GLOBALS['wpdb']->get_blog_prefix();
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_also_known_as',
|
||||
array(
|
||||
'type' => 'array',
|
||||
'description' => 'An array of URLs that the user is known by.',
|
||||
'single' => true,
|
||||
'default' => array(),
|
||||
'sanitize_callback' => array( Sanitize::class, 'url_list' ),
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_old_host_data',
|
||||
array(
|
||||
'description' => 'Actor object for the user on the old host.',
|
||||
'single' => true,
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_moved_to',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => 'The new URL of the user.',
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'sanitize_url',
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_description',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => 'The user’s description.',
|
||||
'single' => true,
|
||||
'default' => '',
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
return wp_kses( $value, 'user_description' );
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_icon',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'The attachment ID for user’s profile image.',
|
||||
'single' => true,
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_header_image',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'The attachment ID for the user’s header image.',
|
||||
'single' => true,
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_mailer_new_dm',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Send a notification when someone sends this user a direct message.',
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
\add_filter( 'get_user_option_activitypub_mailer_new_dm', array( self::class, 'user_options_default' ) );
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_mailer_new_follower',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Send a notification when someone starts to follow this user.',
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
\add_filter( 'get_user_option_activitypub_mailer_new_follower', array( self::class, 'user_options_default' ) );
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
$blog_prefix . 'activitypub_mailer_new_mention',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Send a notification when someone mentions this user.',
|
||||
'single' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
\add_filter( 'get_user_option_activitypub_mailer_new_mention', array( self::class, 'user_options_default' ) );
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
'activitypub_show_welcome_tab',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Whether to show the welcome tab.',
|
||||
'single' => true,
|
||||
'default' => 1,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
|
||||
\register_meta(
|
||||
'user',
|
||||
'activitypub_show_advanced_tab',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => 'Whether to show the advanced tab.',
|
||||
'single' => true,
|
||||
'default' => 0,
|
||||
'sanitize_callback' => 'absint',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set default values for user options.
|
||||
*
|
||||
* @param bool|string $value Option value.
|
||||
* @return bool|string
|
||||
*/
|
||||
public static function user_options_default( $value ) {
|
||||
if ( false === $value ) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
|
@ -1,472 +0,0 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_User_Query;
|
||||
use Activitypub\Model\Blog_User;
|
||||
|
||||
use function Activitypub\is_user_disabled;
|
||||
use function Activitypub\was_comment_received;
|
||||
use function Activitypub\is_comment_federatable;
|
||||
|
||||
/**
|
||||
* ActivityPub Admin Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Admin {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'admin_menu', array( self::class, 'admin_menu' ) );
|
||||
\add_action( 'admin_init', array( self::class, 'register_settings' ) );
|
||||
\add_action( 'load-comment.php', array( self::class, 'edit_comment' ) );
|
||||
\add_action( 'personal_options_update', array( self::class, 'save_user_description' ) );
|
||||
\add_action( 'admin_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
|
||||
\add_action( 'admin_notices', array( self::class, 'admin_notices' ) );
|
||||
|
||||
\add_filter( 'comment_row_actions', array( self::class, 'comment_row_actions' ), 10, 2 );
|
||||
\add_filter( 'manage_edit-comments_columns', array( static::class, 'manage_comment_columns' ) );
|
||||
\add_filter( 'manage_comments_custom_column', array( static::class, 'manage_comments_custom_column' ), 9, 2 );
|
||||
|
||||
\add_filter( 'manage_users_columns', array( self::class, 'manage_users_columns' ), 10, 1 );
|
||||
\add_filter( 'manage_users_custom_column', array( self::class, 'manage_users_custom_column' ), 10, 3 );
|
||||
\add_filter( 'bulk_actions-users', array( self::class, 'user_bulk_options' ) );
|
||||
\add_filter( 'handle_bulk_actions-users', array( self::class, 'handle_bulk_request' ), 10, 3 );
|
||||
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
\add_action( 'show_user_profile', array( self::class, 'add_profile' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add admin menu entry
|
||||
*/
|
||||
public static function admin_menu() {
|
||||
$settings_page = \add_options_page(
|
||||
'Welcome',
|
||||
'ActivityPub',
|
||||
'manage_options',
|
||||
'activitypub',
|
||||
array( self::class, 'settings_page' )
|
||||
);
|
||||
|
||||
\add_action( 'load-' . $settings_page, array( self::class, 'add_settings_help_tab' ) );
|
||||
|
||||
// user has to be able to publish posts
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
$followers_list_page = \add_users_page( \__( 'Followers', 'activitypub' ), \__( 'Followers', 'activitypub' ), 'read', 'activitypub-followers-list', array( self::class, 'followers_list_page' ) );
|
||||
|
||||
\add_action( 'load-' . $followers_list_page, array( self::class, 'add_followers_list_help_tab' ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display admin menu notices about configuration problems or conflicts.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function admin_notices() {
|
||||
$permalink_structure = \get_option( 'permalink_structure' );
|
||||
if ( empty( $permalink_structure ) ) {
|
||||
$admin_notice = \__( 'You are using the ActivityPub plugin with a permalink structure of "plain". This will prevent ActivityPub from working. Please go to "Settings" / "Permalinks" and choose a permalink structure other than "plain".', 'activitypub' );
|
||||
self::show_admin_notice( $admin_notice, 'error' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display one admin menu notice about configuration problems or conflicts.
|
||||
*
|
||||
* @param string $admin_notice The notice to display.
|
||||
* @param string $level The level of the notice (error, warning, success, info).
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function show_admin_notice( $admin_notice, $level ) {
|
||||
?>
|
||||
|
||||
<div class="notice notice-<?php echo esc_attr( $level ); ?>">
|
||||
<p><?php echo wp_kses( $admin_notice, 'data' ); ?></p>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Load settings page
|
||||
*/
|
||||
public static function settings_page() {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( empty( $_GET['tab'] ) ) {
|
||||
$tab = 'welcome';
|
||||
} else {
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
$tab = sanitize_key( $_GET['tab'] );
|
||||
}
|
||||
|
||||
switch ( $tab ) {
|
||||
case 'settings':
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/settings.php' );
|
||||
break;
|
||||
case 'followers':
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/blog-user-followers-list.php' );
|
||||
break;
|
||||
case 'welcome':
|
||||
default:
|
||||
wp_enqueue_script( 'plugin-install' );
|
||||
add_thickbox();
|
||||
wp_enqueue_script( 'updates' );
|
||||
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/welcome.php' );
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Load user settings page
|
||||
*/
|
||||
public static function followers_list_page() {
|
||||
// user has to be able to publish posts
|
||||
if ( ! is_user_disabled( get_current_user_id() ) ) {
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/user-followers-list.php' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Register ActivityPub settings
|
||||
*/
|
||||
public static function register_settings() {
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_post_content_type',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'Use title and link, summary, full or custom content', 'activitypub' ),
|
||||
'show_in_rest' => array(
|
||||
'schema' => array(
|
||||
'enum' => array(
|
||||
'title',
|
||||
'excerpt',
|
||||
'content',
|
||||
),
|
||||
),
|
||||
),
|
||||
'default' => 'content',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_custom_post_content',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'Define your own custom post template', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => ACTIVITYPUB_CUSTOM_POST_CONTENT,
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_max_image_attachments',
|
||||
array(
|
||||
'type' => 'integer',
|
||||
'description' => \__( 'Number of images to attach to posts.', 'activitypub' ),
|
||||
'default' => ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS,
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_object_type',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \__( 'The Activity-Object-Type', 'activitypub' ),
|
||||
'show_in_rest' => array(
|
||||
'schema' => array(
|
||||
'enum' => array(
|
||||
'note',
|
||||
'wordpress-post-format',
|
||||
),
|
||||
),
|
||||
),
|
||||
'default' => 'note',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_use_hashtags',
|
||||
array(
|
||||
'type' => 'boolean',
|
||||
'description' => \__( 'Add hashtags in the content as native tags and replace the #tag with the tag-link', 'activitypub' ),
|
||||
'default' => '0',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_support_post_types',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \esc_html__( 'Enable ActivityPub support for post types', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => array( 'post' ),
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_blog_user_identifier',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'description' => \esc_html__( 'The Identifier of the Blog-User', 'activitypub' ),
|
||||
'show_in_rest' => true,
|
||||
'default' => Blog_User::get_default_username(),
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
// hack to allow dots in the username
|
||||
$parts = explode( '.', $value );
|
||||
$sanitized = array();
|
||||
|
||||
foreach ( $parts as $part ) {
|
||||
$sanitized[] = \sanitize_title( $part );
|
||||
}
|
||||
|
||||
$sanitized = implode( '.', $sanitized );
|
||||
|
||||
// check for login or nicename.
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'search' => $sanitized,
|
||||
'search_columns' => array( 'user_login', 'user_nicename' ),
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
add_settings_error(
|
||||
'activitypub_blog_user_identifier',
|
||||
'activitypub_blog_user_identifier',
|
||||
\esc_html__( 'You cannot use an existing author\'s name for the blog profile ID.', 'activitypub' ),
|
||||
'error'
|
||||
);
|
||||
|
||||
return Blog_User::get_default_username();
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
},
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_enable_users',
|
||||
array(
|
||||
'type' => 'boolean',
|
||||
'description' => \__( 'Every Author on this Blog (with the publish_posts capability) gets his own ActivityPub enabled Profile.', 'activitypub' ),
|
||||
'default' => '1',
|
||||
)
|
||||
);
|
||||
\register_setting(
|
||||
'activitypub',
|
||||
'activitypub_enable_blog_user',
|
||||
array(
|
||||
'type' => 'boolean',
|
||||
'description' => \__( 'Your Blog becomes an ActivityPub compatible Profile.', 'activitypub' ),
|
||||
'default' => '0',
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function add_settings_help_tab() {
|
||||
require_once ACTIVITYPUB_PLUGIN_DIR . 'includes/help.php';
|
||||
}
|
||||
|
||||
public static function add_followers_list_help_tab() {
|
||||
// todo
|
||||
}
|
||||
|
||||
public static function add_profile( $user ) {
|
||||
$description = get_user_meta( $user->ID, 'activitypub_user_description', true );
|
||||
|
||||
\load_template(
|
||||
ACTIVITYPUB_PLUGIN_DIR . 'templates/user-settings.php',
|
||||
true,
|
||||
array(
|
||||
'description' => $description,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
public static function save_user_description( $user_id ) {
|
||||
if ( ! isset( $_REQUEST['_apnonce'] ) ) {
|
||||
return false;
|
||||
}
|
||||
$nonce = sanitize_text_field( wp_unslash( $_REQUEST['_apnonce'] ) );
|
||||
if (
|
||||
! wp_verify_nonce( $nonce, 'activitypub-user-description' ) ||
|
||||
! current_user_can( 'edit_user', $user_id )
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
$description = ! empty( $_POST['activitypub-user-description'] ) ? sanitize_text_field( wp_unslash( $_POST['activitypub-user-description'] ) ) : false;
|
||||
if ( $description ) {
|
||||
update_user_meta( $user_id, 'activitypub_user_description', $description );
|
||||
}
|
||||
}
|
||||
|
||||
public static function enqueue_scripts( $hook_suffix ) {
|
||||
if ( false !== strpos( $hook_suffix, 'activitypub' ) ) {
|
||||
wp_enqueue_style( 'activitypub-admin-styles', plugins_url( 'assets/css/activitypub-admin.css', ACTIVITYPUB_PLUGIN_FILE ), array(), '1.0.0' );
|
||||
wp_enqueue_script( 'activitypub-admin-styles', plugins_url( 'assets/js/activitypub-admin.js', ACTIVITYPUB_PLUGIN_FILE ), array( 'jquery' ), '1.0.0', false );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook into the edit_comment functionality
|
||||
*
|
||||
* * Disable the edit_comment capability for federated comments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function edit_comment() {
|
||||
// Disable the edit_comment capability for federated comments.
|
||||
\add_filter(
|
||||
'user_has_cap',
|
||||
function ( $allcaps, $caps, $arg ) {
|
||||
if ( 'edit_comment' !== $arg[0] ) {
|
||||
return $allcaps;
|
||||
}
|
||||
|
||||
if ( was_comment_received( $arg[2] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $allcaps;
|
||||
},
|
||||
1,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
public static function comment_row_actions( $actions, $comment ) {
|
||||
if ( was_comment_received( $comment ) ) {
|
||||
unset( $actions['edit'] );
|
||||
unset( $actions['quickedit'] );
|
||||
}
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a column "activitypub"
|
||||
*
|
||||
* This column shows if the user has the capability to use ActivityPub.
|
||||
*
|
||||
* @param array $columns The columns.
|
||||
*
|
||||
* @return array The columns extended by the activitypub.
|
||||
*/
|
||||
public static function manage_users_columns( $columns ) {
|
||||
$columns['activitypub'] = __( 'ActivityPub', 'activitypub' );
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "comment-type" and "protocol" as column in WP-Admin
|
||||
*
|
||||
* @param array $columns the list of column names
|
||||
*/
|
||||
public static function manage_comment_columns( $columns ) {
|
||||
$columns['comment_type'] = esc_attr__( 'Comment-Type', 'activitypub' );
|
||||
$columns['comment_protocol'] = esc_attr__( 'Protocol', 'activitypub' );
|
||||
|
||||
return $columns;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add "comment-type" and "protocol" as column in WP-Admin
|
||||
*
|
||||
* @param array $column The column to implement
|
||||
* @param int $comment_id The comment id
|
||||
*/
|
||||
public static function manage_comments_custom_column( $column, $comment_id ) {
|
||||
if ( 'comment_type' === $column && ! defined( 'WEBMENTION_PLUGIN_DIR' ) ) {
|
||||
echo esc_attr( ucfirst( get_comment_type( $comment_id ) ) );
|
||||
} elseif ( 'comment_protocol' === $column ) {
|
||||
$protocol = get_comment_meta( $comment_id, 'protocol', true );
|
||||
|
||||
if ( $protocol ) {
|
||||
echo esc_attr( ucfirst( str_replace( 'activitypub', 'ActivityPub', $protocol ) ) );
|
||||
} else {
|
||||
esc_attr_e( 'Local', 'activitypub' );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the results for the activitypub column.
|
||||
*
|
||||
* @param string $output Custom column output. Default empty.
|
||||
* @param string $column_name Column name.
|
||||
* @param int $user_id ID of the currently-listed user.
|
||||
*
|
||||
* @return string The column contents.
|
||||
*/
|
||||
public static function manage_users_custom_column( $output, $column_name, $user_id ) {
|
||||
if ( 'activitypub' !== $column_name ) {
|
||||
return $output;
|
||||
}
|
||||
|
||||
if ( \user_can( $user_id, 'activitypub' ) ) {
|
||||
return '✓';
|
||||
} else {
|
||||
return '✗';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add options to the Bulk dropdown on the users page
|
||||
*
|
||||
* @param array $actions The existing bulk options.
|
||||
*
|
||||
* @return array The extended bulk options.
|
||||
*/
|
||||
public static function user_bulk_options( $actions ) {
|
||||
$actions['add_activitypub_cap'] = __( 'Enable for ActivityPub', 'activitypub' );
|
||||
$actions['remove_activitypub_cap'] = __( 'Disable for ActivityPub', 'activitypub' );
|
||||
|
||||
return $actions;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle bulk activitypub requests
|
||||
*
|
||||
* * `add_activitypub_cap` - Add the activitypub capability to the selected users.
|
||||
* * `remove_activitypub_cap` - Remove the activitypub capability from the selected users.
|
||||
*
|
||||
* @param string $sendback The URL to send the user back to.
|
||||
* @param string $action The requested action.
|
||||
* @param array $users The selected users.
|
||||
*
|
||||
* @return string The URL to send the user back to.
|
||||
*/
|
||||
public static function handle_bulk_request( $sendback, $action, $users ) {
|
||||
if (
|
||||
'remove_activitypub_cap' !== $action &&
|
||||
'add_activitypub_cap' !== $action
|
||||
) {
|
||||
return $sendback;
|
||||
}
|
||||
|
||||
foreach ( $users as $user_id ) {
|
||||
$user = new \WP_User( $user_id );
|
||||
if (
|
||||
'add_activitypub_cap' === $action &&
|
||||
user_can( $user_id, 'publish_posts' )
|
||||
) {
|
||||
$user->add_cap( 'activitypub' );
|
||||
} elseif ( 'remove_activitypub_cap' === $action ) {
|
||||
$user->remove_cap( 'activitypub' );
|
||||
}
|
||||
}
|
||||
|
||||
return $sendback;
|
||||
}
|
||||
}
|
106
wp-content/plugins/activitypub/includes/class-autoloader.php
Normal file
106
wp-content/plugins/activitypub/includes/class-autoloader.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
/**
|
||||
* Autoloader for Activitypub.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* An Autoloader that respects WordPress's filename standards.
|
||||
*/
|
||||
class Autoloader {
|
||||
|
||||
/**
|
||||
* Namespace separator.
|
||||
*/
|
||||
const NS_SEPARATOR = '\\';
|
||||
|
||||
/**
|
||||
* The prefix to compare classes against.
|
||||
*
|
||||
* @var string
|
||||
* @access protected
|
||||
*/
|
||||
protected $prefix;
|
||||
|
||||
/**
|
||||
* Length of the prefix string.
|
||||
*
|
||||
* @var int
|
||||
* @access protected
|
||||
*/
|
||||
protected $prefix_length;
|
||||
|
||||
/**
|
||||
* Path to the file to be loaded.
|
||||
*
|
||||
* @var string
|
||||
* @access protected
|
||||
*/
|
||||
protected $path;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* @param string $prefix Namespace prefix all classes have in common.
|
||||
* @param string $path Path to the files to be loaded.
|
||||
*/
|
||||
public function __construct( $prefix, $path ) {
|
||||
$this->prefix = $prefix;
|
||||
$this->prefix_length = \strlen( $prefix );
|
||||
$this->path = \rtrim( $path . '/' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers Autoloader's autoload function.
|
||||
*
|
||||
* @throws \Exception When autoload_function cannot be registered.
|
||||
*
|
||||
* @param string $prefix Namespace prefix all classes have in common.
|
||||
* @param string $path Path to the files to be loaded.
|
||||
*/
|
||||
public static function register_path( $prefix, $path ) {
|
||||
$loader = new self( $prefix, $path );
|
||||
\spl_autoload_register( array( $loader, 'load' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a class if its namespace starts with `$this->prefix`.
|
||||
*
|
||||
* @param string $class_name The class to be loaded.
|
||||
*/
|
||||
public function load( $class_name ) {
|
||||
if ( \strpos( $class_name, $this->prefix . self::NS_SEPARATOR ) !== 0 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Strip prefix from the start (ala PSR-4).
|
||||
$class_name = \substr( $class_name, $this->prefix_length + 1 );
|
||||
$class_name = \strtolower( $class_name );
|
||||
$dir = '';
|
||||
|
||||
$last_ns_pos = \strripos( $class_name, self::NS_SEPARATOR );
|
||||
if ( false !== $last_ns_pos ) {
|
||||
$namespace = \substr( $class_name, 0, $last_ns_pos );
|
||||
$namespace = \str_replace( '_', '-', $namespace );
|
||||
$class_name = \substr( $class_name, $last_ns_pos + 1 );
|
||||
$dir = \str_replace( self::NS_SEPARATOR, DIRECTORY_SEPARATOR, $namespace ) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
$path = $this->path . $dir . 'class-' . \str_replace( '_', '-', $class_name ) . '.php';
|
||||
|
||||
if ( ! \file_exists( $path ) ) {
|
||||
$path = $this->path . $dir . 'interface-' . \str_replace( '_', '-', $class_name ) . '.php';
|
||||
}
|
||||
|
||||
if ( ! \file_exists( $path ) ) {
|
||||
$path = $this->path . $dir . 'trait-' . \str_replace( '_', '-', $class_name ) . '.php';
|
||||
}
|
||||
|
||||
if ( \file_exists( $path ) ) {
|
||||
require_once $path;
|
||||
}
|
||||
}
|
||||
}
|
@ -1,36 +1,136 @@
|
||||
<?php
|
||||
/**
|
||||
* Blocks file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Users as User_Collection;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
|
||||
/**
|
||||
* Block class.
|
||||
*/
|
||||
class Blocks {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
// this is already being called on the init hook, so just add it.
|
||||
// This is already being called on the init hook, so just add it.
|
||||
self::register_blocks();
|
||||
\add_action( 'wp_enqueue_scripts', array( self::class, 'add_data' ) );
|
||||
\add_action( 'enqueue_block_editor_assets', array( self::class, 'add_data' ) );
|
||||
|
||||
\add_action( 'wp_head', array( self::class, 'inject_activitypub_options' ), 11 );
|
||||
\add_action( 'admin_print_scripts', array( self::class, 'inject_activitypub_options' ) );
|
||||
\add_action( 'load-post-new.php', array( self::class, 'handle_in_reply_to_get_param' ) );
|
||||
// Add editor plugin.
|
||||
\add_action( 'enqueue_block_editor_assets', array( self::class, 'enqueue_editor_assets' ) );
|
||||
\add_action( 'init', array( self::class, 'register_postmeta' ), 11 );
|
||||
|
||||
\add_filter( 'activitypub_import_mastodon_post_data', array( self::class, 'filter_import_mastodon_post_data' ), 10, 2 );
|
||||
}
|
||||
|
||||
public static function add_data() {
|
||||
$context = is_admin() ? 'editor' : 'view';
|
||||
$followers_handle = 'activitypub-followers-' . $context . '-script';
|
||||
$follow_me_handle = 'activitypub-follow-me-' . $context . '-script';
|
||||
/**
|
||||
* Register post meta for content warnings.
|
||||
*/
|
||||
public static function register_postmeta() {
|
||||
$ap_post_types = \get_post_types_by_support( 'activitypub' );
|
||||
foreach ( $ap_post_types as $post_type ) {
|
||||
\register_post_meta(
|
||||
$post_type,
|
||||
'activitypub_content_warning',
|
||||
array(
|
||||
'show_in_rest' => true,
|
||||
'single' => true,
|
||||
'type' => 'string',
|
||||
'sanitize_callback' => function ( $warning ) {
|
||||
if ( $warning ) {
|
||||
return \sanitize_text_field( $warning );
|
||||
}
|
||||
|
||||
return null;
|
||||
},
|
||||
)
|
||||
);
|
||||
|
||||
\register_post_meta(
|
||||
$post_type,
|
||||
'activitypub_content_visibility',
|
||||
array(
|
||||
'type' => 'string',
|
||||
'single' => true,
|
||||
'show_in_rest' => true,
|
||||
'sanitize_callback' => function ( $value ) {
|
||||
$schema = array(
|
||||
'type' => 'string',
|
||||
'enum' => array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE, ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL ),
|
||||
'default' => ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC,
|
||||
);
|
||||
|
||||
if ( is_wp_error( rest_validate_enum( $value, $schema, '' ) ) ) {
|
||||
return $schema['default'];
|
||||
}
|
||||
|
||||
return $value;
|
||||
},
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the block editor assets.
|
||||
*/
|
||||
public static function enqueue_editor_assets() {
|
||||
// Check for our supported post types.
|
||||
$current_screen = \get_current_screen();
|
||||
$ap_post_types = \get_post_types_by_support( 'activitypub' );
|
||||
if ( ! $current_screen || ! in_array( $current_screen->post_type, $ap_post_types, true ) ) {
|
||||
return;
|
||||
}
|
||||
$asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/editor-plugin/plugin.asset.php';
|
||||
$plugin_url = plugins_url( 'build/editor-plugin/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
|
||||
wp_enqueue_script( 'activitypub-block-editor', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Enqueue the reply handle script if the in_reply_to GET param is set.
|
||||
*/
|
||||
public static function handle_in_reply_to_get_param() {
|
||||
// Only load the script if the in_reply_to GET param is set, action happens there, not here.
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
if ( ! isset( $_GET['in_reply_to'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$asset_data = include ACTIVITYPUB_PLUGIN_DIR . 'build/reply-intent/plugin.asset.php';
|
||||
$plugin_url = plugins_url( 'build/reply-intent/plugin.js', ACTIVITYPUB_PLUGIN_FILE );
|
||||
wp_enqueue_script( 'activitypub-reply-intent', $plugin_url, $asset_data['dependencies'], $asset_data['version'], true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Output ActivityPub options as a script tag.
|
||||
*/
|
||||
public static function inject_activitypub_options() {
|
||||
$data = array(
|
||||
'namespace' => ACTIVITYPUB_REST_NAMESPACE,
|
||||
'enabled' => array(
|
||||
'site' => ! is_user_type_disabled( 'blog' ),
|
||||
'namespace' => ACTIVITYPUB_REST_NAMESPACE,
|
||||
'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
|
||||
'enabled' => array(
|
||||
'site' => ! is_user_type_disabled( 'blog' ),
|
||||
'users' => ! is_user_type_disabled( 'user' ),
|
||||
),
|
||||
);
|
||||
$js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
|
||||
\wp_add_inline_script( $followers_handle, $js, 'before' );
|
||||
\wp_add_inline_script( $follow_me_handle, $js, 'before' );
|
||||
|
||||
printf(
|
||||
"\n<script>var _activityPubOptions = %s;</script>",
|
||||
wp_json_encode( $data )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the blocks.
|
||||
*/
|
||||
public static function register_blocks() {
|
||||
\register_block_type_from_metadata(
|
||||
ACTIVITYPUB_PLUGIN_DIR . '/build/followers',
|
||||
@ -44,49 +144,135 @@ class Blocks {
|
||||
'render_callback' => array( self::class, 'render_follow_me_block' ),
|
||||
)
|
||||
);
|
||||
\register_block_type_from_metadata(
|
||||
ACTIVITYPUB_PLUGIN_DIR . '/build/reply',
|
||||
array(
|
||||
'render_callback' => array( self::class, 'render_reply_block' ),
|
||||
)
|
||||
);
|
||||
|
||||
\register_block_type_from_metadata(
|
||||
ACTIVITYPUB_PLUGIN_DIR . '/build/reactions',
|
||||
array(
|
||||
'render_callback' => array( self::class, 'render_post_reactions_block' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the post reactions block.
|
||||
*
|
||||
* @param array $attrs The block attributes.
|
||||
*
|
||||
* @return string The HTML to render.
|
||||
*/
|
||||
public static function render_post_reactions_block( $attrs ) {
|
||||
if ( ! isset( $attrs['postId'] ) ) {
|
||||
$attrs['postId'] = get_the_ID();
|
||||
}
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'class' => 'activitypub-reactions-block',
|
||||
'data-attrs' => wp_json_encode( $attrs ),
|
||||
)
|
||||
);
|
||||
|
||||
return sprintf(
|
||||
'<div %s></div>',
|
||||
$wrapper_attributes
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user ID from a user string.
|
||||
*
|
||||
* @param string $user_string The user string. Can be a user ID, 'site', or 'inherit'.
|
||||
* @return int|null The user ID, or null if the 'inherit' string is not supported in this context.
|
||||
*/
|
||||
private static function get_user_id( $user_string ) {
|
||||
if ( is_numeric( $user_string ) ) {
|
||||
return absint( $user_string );
|
||||
}
|
||||
// any other non-numeric falls back to 0, including the `site` string used in the UI
|
||||
return 0;
|
||||
|
||||
// If the user string is 'site', return the Blog User ID.
|
||||
if ( 'site' === $user_string ) {
|
||||
return Actors::BLOG_USER_ID;
|
||||
}
|
||||
|
||||
// The only other value should be 'inherit', which means to use the query context to determine the User.
|
||||
if ( 'inherit' !== $user_string ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// For a homepage/front page, if the Blog User is active, use it.
|
||||
if ( ( is_front_page() || is_home() ) && ! is_user_type_disabled( 'blog' ) ) {
|
||||
return Actors::BLOG_USER_ID;
|
||||
}
|
||||
|
||||
// If we're in a loop, use the post author.
|
||||
$author_id = get_the_author_meta( 'ID' );
|
||||
if ( $author_id ) {
|
||||
return $author_id;
|
||||
}
|
||||
|
||||
// For other pages, the queried object will clue us in.
|
||||
$queried_object = get_queried_object();
|
||||
if ( ! $queried_object ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// If we're on a user archive page, use that user's ID.
|
||||
if ( is_a( $queried_object, 'WP_User' ) ) {
|
||||
return $queried_object->ID;
|
||||
}
|
||||
|
||||
// For a single post, use the post author's ID.
|
||||
if ( is_a( $queried_object, 'WP_Post' ) ) {
|
||||
return get_the_author_meta( 'ID' );
|
||||
}
|
||||
|
||||
// We won't properly account for some conditions, like tag archives.
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter an array by a list of keys.
|
||||
* @param array $array The array to filter.
|
||||
*
|
||||
* @param array $data The array to filter.
|
||||
* @param array $keys The keys to keep.
|
||||
* @return array The filtered array.
|
||||
*/
|
||||
protected static function filter_array_by_keys( $array, $keys ) {
|
||||
return array_intersect_key( $array, array_flip( $keys ) );
|
||||
protected static function filter_array_by_keys( $data, $keys ) {
|
||||
return array_intersect_key( $data, array_flip( $keys ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the follow me block.
|
||||
*
|
||||
* @param array $attrs The block attributes.
|
||||
* @return string The HTML to render.
|
||||
*/
|
||||
public static function render_follow_me_block( $attrs ) {
|
||||
$user_id = self::get_user_id( $attrs['selectedUser'] );
|
||||
$user = User_Collection::get_by_id( $user_id );
|
||||
if ( ! is_wp_error( $user ) ) {
|
||||
$attrs['profileData'] = self::filter_array_by_keys(
|
||||
$user->to_array(),
|
||||
array( 'icon', 'name', 'webfinger' )
|
||||
);
|
||||
$user = Actors::get_by_id( $user_id );
|
||||
if ( is_wp_error( $user ) ) {
|
||||
if ( 'inherit' === $attrs['selectedUser'] ) {
|
||||
// If the user is 'inherit' and we couldn't determine the user, don't render anything.
|
||||
return '<!-- Follow Me block: `inherit` mode does not display on this type of page -->';
|
||||
} else {
|
||||
// If the user is a specific ID and we couldn't find it, render an error message.
|
||||
return '<!-- Follow Me block: user not found -->';
|
||||
}
|
||||
}
|
||||
|
||||
// add `@` prefix if it's missing
|
||||
if ( '@' !== substr( $attrs['profileData']['webfinger'], 0, 1 ) ) {
|
||||
$attrs['profileData']['webfinger'] = '@' . $attrs['profileData']['webfinger'];
|
||||
}
|
||||
$attrs['profileData'] = self::filter_array_by_keys(
|
||||
$user->to_array(),
|
||||
array( 'icon', 'name', 'webfinger' )
|
||||
);
|
||||
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'aria-label' => __( 'Follow me on the Fediverse', 'activitypub' ),
|
||||
'class' => 'activitypub-follow-me-block-wrapper',
|
||||
'data-attrs' => wp_json_encode( $attrs ),
|
||||
)
|
||||
@ -95,12 +281,28 @@ class Blocks {
|
||||
return '<div ' . $wrapper_attributes . '></div>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the follower block.
|
||||
*
|
||||
* @param array $attrs The block attributes.
|
||||
*
|
||||
* @return string The HTML to render.
|
||||
*/
|
||||
public static function render_follower_block( $attrs ) {
|
||||
$followee_user_id = self::get_user_id( $attrs['selectedUser'] );
|
||||
$per_page = absint( $attrs['per_page'] );
|
||||
if ( is_null( $followee_user_id ) ) {
|
||||
return '<!-- Followers block: `inherit` mode does not display on this type of page -->';
|
||||
}
|
||||
|
||||
$user = Actors::get_by_id( $followee_user_id );
|
||||
if ( is_wp_error( $user ) ) {
|
||||
return '<!-- Followers block: `' . $followee_user_id . '` not an active ActivityPub user -->';
|
||||
}
|
||||
|
||||
$per_page = absint( $attrs['per_page'] );
|
||||
$follower_data = Followers::get_followers_with_count( $followee_user_id, $per_page );
|
||||
|
||||
$attrs['followerData']['total'] = $follower_data['total'];
|
||||
$attrs['followerData']['total'] = $follower_data['total'];
|
||||
$attrs['followerData']['followers'] = array_map(
|
||||
function ( $follower ) {
|
||||
return self::filter_array_by_keys(
|
||||
@ -110,7 +312,7 @@ class Blocks {
|
||||
},
|
||||
$follower_data['followers']
|
||||
);
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
$wrapper_attributes = get_block_wrapper_attributes(
|
||||
array(
|
||||
'aria-label' => __( 'Fediverse Followers', 'activitypub' ),
|
||||
'class' => 'activitypub-follower-block',
|
||||
@ -131,9 +333,65 @@ class Blocks {
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the reply block.
|
||||
*
|
||||
* @param array $attrs The block attributes.
|
||||
*
|
||||
* @return string The HTML to render.
|
||||
*/
|
||||
public static function render_reply_block( $attrs ) {
|
||||
// Return early if no URL is provided.
|
||||
if ( empty( $attrs['url'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$show_embed = isset( $attrs['embedPost'] ) && $attrs['embedPost'];
|
||||
|
||||
$wrapper_attrs = get_block_wrapper_attributes(
|
||||
array(
|
||||
'aria-label' => __( 'Reply', 'activitypub' ),
|
||||
'class' => 'activitypub-reply-block',
|
||||
'data-in-reply-to' => $attrs['url'],
|
||||
)
|
||||
);
|
||||
|
||||
$html = '<div ' . $wrapper_attrs . '>';
|
||||
|
||||
// Try to get and append the embed if requested.
|
||||
if ( $show_embed ) {
|
||||
$embed = wp_oembed_get( $attrs['url'] );
|
||||
if ( $embed ) {
|
||||
$html .= $embed;
|
||||
}
|
||||
}
|
||||
|
||||
// Only show the link if we're not showing the embed.
|
||||
if ( ! $show_embed ) {
|
||||
$html .= sprintf(
|
||||
'<p><a title="%2$s" aria-label="%2$s" href="%1$s" class="u-in-reply-to" target="_blank">%3$s</a></p>',
|
||||
esc_url( $attrs['url'] ),
|
||||
esc_attr__( 'This post is a response to the referenced content.', 'activitypub' ),
|
||||
// translators: %s is the URL of the post being replied to.
|
||||
sprintf( __( '↬%s', 'activitypub' ), \str_replace( array( 'https://', 'http://' ), '', esc_url( $attrs['url'] ) ) )
|
||||
);
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a follower.
|
||||
*
|
||||
* @param \Activitypub\Model\Follower $follower The follower to render.
|
||||
*
|
||||
* @return string The HTML to render.
|
||||
*/
|
||||
public static function render_follower( $follower ) {
|
||||
$external_svg = '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="components-external-link__icon css-rvs7bx esh4a730" aria-hidden="true" focusable="false"><path d="M18.2 17c0 .7-.6 1.2-1.2 1.2H7c-.7 0-1.2-.6-1.2-1.2V7c0-.7.6-1.2 1.2-1.2h3.2V4.2H7C5.5 4.2 4.2 5.5 4.2 7v10c0 1.5 1.2 2.8 2.8 2.8h10c1.5 0 2.8-1.2 2.8-2.8v-3.6h-1.5V17zM14.9 3v1.5h3.7l-6.4 6.4 1.1 1.1 6.4-6.4v3.7h1.5V3h-6.3z"></path></svg>';
|
||||
$template =
|
||||
$template =
|
||||
'<a href="%s" title="%s" class="components-external-link activitypub-link" target="_blank" rel="external noreferrer noopener">
|
||||
<img width="40" height="40" src="%s" class="avatar activitypub-avatar" />
|
||||
<span class="activitypub-actor">
|
||||
@ -156,4 +414,33 @@ class Blocks {
|
||||
$external_svg
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts content to blocks before saving to the database.
|
||||
*
|
||||
* @param array $data The post data to be inserted.
|
||||
* @param object $post The Mastodon Create activity.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function filter_import_mastodon_post_data( $data, $post ) {
|
||||
// Convert paragraphs to blocks.
|
||||
\preg_match_all( '#<p>.*?</p>#is', $data['post_content'], $matches );
|
||||
$blocks = \array_map(
|
||||
function ( $paragraph ) {
|
||||
return '<!-- wp:paragraph -->' . PHP_EOL . $paragraph . PHP_EOL . '<!-- /wp:paragraph -->' . PHP_EOL;
|
||||
},
|
||||
$matches[0] ?? array()
|
||||
);
|
||||
|
||||
$data['post_content'] = \rtrim( \implode( PHP_EOL, $blocks ), PHP_EOL );
|
||||
|
||||
// Add reply block if it's a reply.
|
||||
if ( null !== $post->object->inReplyTo ) {
|
||||
$reply_block = \sprintf( '<!-- wp:activitypub/reply {"url":"%1$s","embedPost":true} /-->' . PHP_EOL, \esc_url( $post->object->inReplyTo ) );
|
||||
$data['post_content'] = $reply_block . $data['post_content'];
|
||||
}
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
230
wp-content/plugins/activitypub/includes/class-cli.php
Normal file
230
wp-content/plugins/activitypub/includes/class-cli.php
Normal file
@ -0,0 +1,230 @@
|
||||
<?php
|
||||
/**
|
||||
* WP-CLI file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Collection\Outbox;
|
||||
|
||||
/**
|
||||
* WP-CLI commands.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
class Cli extends \WP_CLI_Command {
|
||||
|
||||
/**
|
||||
* Remove the entire blog from the Fediverse.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* $ wp activitypub self-destruct
|
||||
*
|
||||
* @param array|null $args The arguments.
|
||||
* @param array|null $assoc_args The associative arguments.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function self_destruct( $args, $assoc_args ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
\WP_CLI::warning( 'Self-Destructing is not implemented yet.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete or Update a Post, Page, Custom Post Type or Attachment.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <action>
|
||||
* : The action to perform. Either `delete` or `update`.
|
||||
* ---
|
||||
* options:
|
||||
* - delete
|
||||
* - update
|
||||
* ---
|
||||
*
|
||||
* <id>
|
||||
* : The id of the Post, Page, Custom Post Type or Attachment.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* $ wp activitypub post delete 1
|
||||
*
|
||||
* @synopsis <action> <id>
|
||||
*
|
||||
* @param array $args The arguments.
|
||||
*/
|
||||
public function post( $args ) {
|
||||
$post = get_post( $args[1] );
|
||||
|
||||
if ( ! $post ) {
|
||||
\WP_CLI::error( 'Post not found.' );
|
||||
}
|
||||
|
||||
switch ( $args[0] ) {
|
||||
case 'delete':
|
||||
\WP_CLI::confirm( 'Do you really want to delete the (Custom) Post with the ID: ' . $args[1] );
|
||||
add_to_outbox( $post, 'Delete', $post->post_author );
|
||||
\WP_CLI::success( '"Delete" activity is queued.' );
|
||||
break;
|
||||
case 'update':
|
||||
add_to_outbox( $post, 'Update', $post->post_author );
|
||||
\WP_CLI::success( '"Update" activity is queued.' );
|
||||
break;
|
||||
default:
|
||||
\WP_CLI::error( 'Unknown action.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete or Update a Comment.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <action>
|
||||
* : The action to perform. Either `delete` or `update`.
|
||||
* ---
|
||||
* options:
|
||||
* - delete
|
||||
* - update
|
||||
* ---
|
||||
*
|
||||
* <id>
|
||||
* : The id of the Comment.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* $ wp activitypub comment delete 1
|
||||
*
|
||||
* @synopsis <action> <id>
|
||||
*
|
||||
* @param array $args The arguments.
|
||||
*/
|
||||
public function comment( $args ) {
|
||||
$comment = get_comment( $args[1] );
|
||||
|
||||
if ( ! $comment ) {
|
||||
\WP_CLI::error( 'Comment not found.' );
|
||||
}
|
||||
|
||||
if ( was_comment_received( $comment ) ) {
|
||||
\WP_CLI::error( 'This comment was received via ActivityPub and cannot be deleted or updated.' );
|
||||
}
|
||||
|
||||
switch ( $args[0] ) {
|
||||
case 'delete':
|
||||
\WP_CLI::confirm( 'Do you really want to delete the Comment with the ID: ' . $args[1] );
|
||||
add_to_outbox( $comment, 'Delete', $comment->user_id );
|
||||
\WP_CLI::success( '"Delete" activity is queued.' );
|
||||
break;
|
||||
case 'update':
|
||||
add_to_outbox( $comment, 'Update', $comment->user_id );
|
||||
\WP_CLI::success( '"Update" activity is queued.' );
|
||||
break;
|
||||
default:
|
||||
\WP_CLI::error( 'Unknown action.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Undo an activity that was sent to the Fediverse.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <outbox_item_id>
|
||||
* The ID or URL of the outbox item to undo.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* $ wp activitypub undo 123
|
||||
* $ wp activitypub undo "https://example.com/?post_type=ap_outbox&p=123"
|
||||
*
|
||||
* @synopsis <outbox_item_id>
|
||||
*
|
||||
* @param array $args The arguments.
|
||||
*/
|
||||
public function undo( $args ) {
|
||||
$outbox_item_id = $args[0];
|
||||
if ( ! is_numeric( $outbox_item_id ) ) {
|
||||
$outbox_item_id = url_to_postid( $outbox_item_id );
|
||||
}
|
||||
|
||||
$outbox_item_id = get_post( $outbox_item_id );
|
||||
if ( ! $outbox_item_id ) {
|
||||
\WP_CLI::error( 'Activity not found.' );
|
||||
}
|
||||
|
||||
$undo_id = Outbox::undo( $outbox_item_id );
|
||||
if ( ! $undo_id ) {
|
||||
\WP_CLI::error( 'Failed to undo activity.' );
|
||||
}
|
||||
\WP_CLI::success( 'Undo activity scheduled.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-Schedule an activity that was sent to the Fediverse before.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <outbox_item_id>
|
||||
* The ID or URL of the outbox item to reschedule.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* $ wp activitypub reschedule 123
|
||||
* $ wp activitypub reschedule "https://example.com/?post_type=ap_outbox&p=123"
|
||||
*
|
||||
* @synopsis <outbox_item_id>
|
||||
*
|
||||
* @param array $args The arguments.
|
||||
*/
|
||||
public function reschedule( $args ) {
|
||||
$outbox_item_id = $args[0];
|
||||
if ( ! is_numeric( $outbox_item_id ) ) {
|
||||
$outbox_item_id = url_to_postid( $outbox_item_id );
|
||||
}
|
||||
|
||||
$outbox_item_id = get_post( $outbox_item_id );
|
||||
if ( ! $outbox_item_id ) {
|
||||
\WP_CLI::error( 'Activity not found.' );
|
||||
}
|
||||
|
||||
Outbox::reschedule( $outbox_item_id );
|
||||
|
||||
\WP_CLI::success( 'Rescheduled activity.' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the blog to a new URL.
|
||||
*
|
||||
* ## OPTIONS
|
||||
*
|
||||
* <from>
|
||||
* The current URL of the blog.
|
||||
*
|
||||
* <to>
|
||||
* The new URL of the blog.
|
||||
*
|
||||
* ## EXAMPLES
|
||||
*
|
||||
* $ wp activitypub move https://example.com/ https://newsite.com/
|
||||
*
|
||||
* @synopsis <from> <to>
|
||||
*
|
||||
* @param array $args The arguments.
|
||||
*/
|
||||
public function move( $args ) {
|
||||
$from = $args[0];
|
||||
$to = $args[1];
|
||||
|
||||
$outbox_item_id = Move::account( $from, $to );
|
||||
|
||||
if ( is_wp_error( $outbox_item_id ) ) {
|
||||
\WP_CLI::error( $outbox_item_id->get_error_message() );
|
||||
} else {
|
||||
\WP_CLI::success( 'Move Scheduled.' );
|
||||
}
|
||||
}
|
||||
}
|
@ -1,28 +1,38 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub Comment Class
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
use WP_Comment_Query;
|
||||
|
||||
use function Activitypub\is_user_disabled;
|
||||
use function Activitypub\is_single_user;
|
||||
|
||||
/**
|
||||
* ActivityPub Comment Class
|
||||
* ActivityPub Comment Class.
|
||||
*
|
||||
* This class is a helper/utils class that provides a collection of static
|
||||
* methods that are used to handle comments.
|
||||
*/
|
||||
class Comment {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_comment_types();
|
||||
|
||||
\add_filter( 'comment_reply_link', array( self::class, 'comment_reply_link' ), 10, 3 );
|
||||
\add_filter( 'comment_class', array( self::class, 'comment_class' ), 10, 3 );
|
||||
\add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 3 );
|
||||
\add_filter( 'get_comment_link', array( self::class, 'remote_comment_link' ), 11, 2 );
|
||||
\add_action( 'wp_enqueue_scripts', array( self::class, 'enqueue_scripts' ) );
|
||||
\add_action( 'pre_get_comments', array( static::class, 'comment_query' ) );
|
||||
\add_filter( 'pre_comment_approved', array( static::class, 'pre_comment_approved' ), 10, 2 );
|
||||
\add_filter( 'get_avatar_comment_types', array( static::class, 'get_avatar_comment_types' ), 99 );
|
||||
\add_action( 'update_option_activitypub_allow_likes', array( self::class, 'maybe_update_comment_counts' ), 10, 2 );
|
||||
\add_action( 'update_option_activitypub_allow_reposts', array( self::class, 'maybe_update_comment_counts' ), 10, 2 );
|
||||
\add_filter( 'pre_wp_update_comment_count_now', array( static::class, 'pre_wp_update_comment_count_now' ), 10, 3 );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -31,16 +41,15 @@ class Comment {
|
||||
* We don't want to show the comment reply link for federated comments
|
||||
* if the user is disabled for federation.
|
||||
*
|
||||
* @param string $link The HTML markup for the comment reply link.
|
||||
* @param array $args An array of arguments overriding the defaults.
|
||||
* @param WP_Comment $comment The object of the comment being replied.
|
||||
* @param string $link The HTML markup for the comment reply link.
|
||||
* @param array $args An array of arguments overriding the defaults.
|
||||
* @param \WP_Comment $comment The object of the comment being replied.
|
||||
*
|
||||
* @return string The filtered HTML markup for the comment reply link.
|
||||
*/
|
||||
public static function comment_reply_link( $link, $args, $comment ) {
|
||||
if ( self::are_comments_allowed( $comment ) ) {
|
||||
$user_id = get_current_user_id();
|
||||
if ( $user_id && self::was_received( $comment ) && \user_can( $user_id, 'activitypub' ) ) {
|
||||
if ( \current_user_can( 'activitypub' ) && self::was_received( $comment ) ) {
|
||||
return self::create_fediverse_reply_link( $link, $args );
|
||||
}
|
||||
|
||||
@ -49,19 +58,27 @@ class Comment {
|
||||
|
||||
$attrs = array(
|
||||
'selectedComment' => self::generate_id( $comment ),
|
||||
'commentId' => $comment->comment_ID,
|
||||
'commentId' => $comment->comment_ID,
|
||||
);
|
||||
|
||||
$div = sprintf(
|
||||
'<div class="activitypub-remote-reply" data-attrs="%s"></div>',
|
||||
'<div class="reply activitypub-remote-reply" data-attrs="%s"></div>',
|
||||
esc_attr( wp_json_encode( $attrs ) )
|
||||
);
|
||||
|
||||
/**
|
||||
* Filters the HTML markup for the ActivityPub remote comment reply container.
|
||||
*
|
||||
* @param string $div The HTML markup for the remote reply container. Default is a div
|
||||
* with class 'activitypub-remote-reply' and data attributes for
|
||||
* the selected comment ID and internal comment ID.
|
||||
*/
|
||||
return apply_filters( 'activitypub_comment_reply_link', $div );
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a link to reply to a federated comment.
|
||||
*
|
||||
* This function adds a title attribute to the reply link to inform the user
|
||||
* that the comment was received from the fediverse and the reply will be sent
|
||||
* to the original author.
|
||||
@ -73,7 +90,7 @@ class Comment {
|
||||
*/
|
||||
private static function create_fediverse_reply_link( $link, $args ) {
|
||||
$str_to_replace = sprintf( '>%s<', $args['reply_text'] );
|
||||
$replace_with = sprintf(
|
||||
$replace_with = sprintf(
|
||||
' title="%s">%s<',
|
||||
esc_attr__( 'This comment was received from the fediverse and your reply will be sent to the original author', 'activitypub' ),
|
||||
esc_html__( 'Reply with federation', 'activitypub' )
|
||||
@ -104,17 +121,11 @@ class Comment {
|
||||
}
|
||||
|
||||
if ( is_single_user() && \user_can( $current_user, 'publish_posts' ) ) {
|
||||
// On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user
|
||||
$current_user = Users::BLOG_USER_ID;
|
||||
// On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user.
|
||||
$current_user = Actors::BLOG_USER_ID;
|
||||
}
|
||||
|
||||
$is_user_disabled = is_user_disabled( $current_user );
|
||||
|
||||
if ( $is_user_disabled ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return user_can_activitypub( $current_user );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -200,7 +211,7 @@ class Comment {
|
||||
* @return boolean True if the comment should be federated, false otherwise.
|
||||
*/
|
||||
public static function should_be_federated( $comment ) {
|
||||
// we should not federate federated comments
|
||||
// We should not federate federated comments.
|
||||
if ( self::was_received( $comment ) ) {
|
||||
return false;
|
||||
}
|
||||
@ -208,29 +219,27 @@ class Comment {
|
||||
$comment = \get_comment( $comment );
|
||||
$user_id = $comment->user_id;
|
||||
|
||||
// comments without user can't be federated
|
||||
// Comments without user can't be federated.
|
||||
if ( ! $user_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( is_single_user() && \user_can( $user_id, 'publish_posts' ) ) {
|
||||
// On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user
|
||||
$user_id = Users::BLOG_USER_ID;
|
||||
if ( is_single_user() && \user_can( $user_id, 'activitypub' ) ) {
|
||||
// On a single user site, comments by users with the `publish_posts` capability will be federated as the blog user.
|
||||
$user_id = Actors::BLOG_USER_ID;
|
||||
}
|
||||
|
||||
$is_user_disabled = is_user_disabled( $user_id );
|
||||
|
||||
// user is disabled for federation
|
||||
if ( $is_user_disabled ) {
|
||||
// User is not allowed to federate comments.
|
||||
if ( ! user_can_activitypub( $user_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// it is a comment to the post and can be federated
|
||||
// It is a comment to the post and can be federated.
|
||||
if ( empty( $comment->comment_parent ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// check if parent comment is federated
|
||||
// Check if parent comment is federated.
|
||||
$parent_comment = \get_comment( $comment->comment_parent );
|
||||
|
||||
return ! self::is_local( $parent_comment );
|
||||
@ -241,13 +250,15 @@ class Comment {
|
||||
*
|
||||
* @param string $id ActivityPub object ID (usually a URL) to check.
|
||||
*
|
||||
* @return int|boolean Comment ID, or false on failure.
|
||||
* @return \WP_Comment|false Comment object, or false on failure.
|
||||
*/
|
||||
public static function object_id_to_comment( $id ) {
|
||||
$comment_query = new WP_Comment_Query(
|
||||
array(
|
||||
'meta_key' => 'source_id', // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
'meta_value' => $id, // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_value
|
||||
'orderby' => 'comment_date',
|
||||
'order' => 'DESC',
|
||||
)
|
||||
);
|
||||
|
||||
@ -255,28 +266,24 @@ class Comment {
|
||||
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)
|
||||
* remote comment (For threading comments locally).
|
||||
*
|
||||
* @param string $url The URL to check.
|
||||
*
|
||||
* @return int comment_ID or null if not found
|
||||
* @return string|null Comment ID or null if not found.
|
||||
*/
|
||||
public static function url_to_commentid( $url ) {
|
||||
if ( ! $url || ! filter_var( $url, \FILTER_VALIDATE_URL ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// check for local comment
|
||||
if ( \wp_parse_url( \site_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) {
|
||||
// Check for local comment.
|
||||
if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) === \wp_parse_url( $url, \PHP_URL_HOST ) ) {
|
||||
$query = \wp_parse_url( $url, \PHP_URL_QUERY );
|
||||
|
||||
if ( $query ) {
|
||||
@ -327,7 +334,7 @@ class Comment {
|
||||
* @return string[] An array of classes.
|
||||
*/
|
||||
public static function comment_class( $classes, $css_class, $comment_id ) {
|
||||
// check if ActivityPub comment
|
||||
// Check if ActivityPub comment.
|
||||
if ( 'activitypub' === get_comment_meta( $comment_id, 'protocol', true ) ) {
|
||||
$classes[] = 'activitypub-comment';
|
||||
}
|
||||
@ -335,11 +342,51 @@ class Comment {
|
||||
return $classes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the public comment id via the WordPress comments meta.
|
||||
*
|
||||
* @param int $wp_comment_id The internal WordPress comment ID.
|
||||
* @param bool $fallback Whether the code should fall back to `source_url` if `source_id` is not set.
|
||||
*
|
||||
* @return string|null The ActivityPub id/url of the comment.
|
||||
*/
|
||||
public static function get_source_id( $wp_comment_id, $fallback = true ) {
|
||||
$comment_meta = \get_comment_meta( $wp_comment_id );
|
||||
|
||||
if ( ! empty( $comment_meta['source_id'][0] ) ) {
|
||||
return $comment_meta['source_id'][0];
|
||||
} elseif ( ! empty( $comment_meta['source_url'][0] ) && $fallback ) {
|
||||
return $comment_meta['source_url'][0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the public comment url via the WordPress comments meta.
|
||||
*
|
||||
* @param int $wp_comment_id The internal WordPress comment ID.
|
||||
* @param bool $fallback Whether the code should fall back to `source_id` if `source_url` is not set.
|
||||
*
|
||||
* @return string|null The ActivityPub id/url of the comment.
|
||||
*/
|
||||
public static function get_source_url( $wp_comment_id, $fallback = true ) {
|
||||
$comment_meta = \get_comment_meta( $wp_comment_id );
|
||||
|
||||
if ( ! empty( $comment_meta['source_url'][0] ) ) {
|
||||
return $comment_meta['source_url'][0];
|
||||
} elseif ( ! empty( $comment_meta['source_id'][0] ) && $fallback ) {
|
||||
return $comment_meta['source_id'][0];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Link remote comments to source url.
|
||||
*
|
||||
* @param string $comment_link
|
||||
* @param object|WP_Comment $comment
|
||||
* @param string $comment_link The comment link.
|
||||
* @param object|\WP_Comment $comment The comment object.
|
||||
*
|
||||
* @return string $url
|
||||
*/
|
||||
@ -348,43 +395,31 @@ class Comment {
|
||||
return $comment_link;
|
||||
}
|
||||
|
||||
$comment_meta = \get_comment_meta( $comment->comment_ID );
|
||||
$public_comment_link = self::get_source_url( $comment->comment_ID );
|
||||
|
||||
if ( ! empty( $comment_meta['source_url'][0] ) ) {
|
||||
return $comment_meta['source_url'][0];
|
||||
} elseif ( ! empty( $comment_meta['source_id'][0] ) ) {
|
||||
return $comment_meta['source_id'][0];
|
||||
}
|
||||
|
||||
return $comment_link;
|
||||
return $public_comment_link ?? $comment_link;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Generates an ActivityPub URI for a comment
|
||||
*
|
||||
* @param WP_Comment|int $comment A comment object or comment ID
|
||||
* @param \WP_Comment|int $comment A comment object or comment ID.
|
||||
*
|
||||
* @return string ActivityPub URI for comment
|
||||
*/
|
||||
public static function generate_id( $comment ) {
|
||||
$comment = \get_comment( $comment );
|
||||
$comment_meta = \get_comment_meta( $comment->comment_ID );
|
||||
$comment = \get_comment( $comment );
|
||||
|
||||
// show external comment ID if it exists
|
||||
if ( ! empty( $comment_meta['source_id'][0] ) ) {
|
||||
return $comment_meta['source_id'][0];
|
||||
} elseif ( ! empty( $comment_meta['source_url'][0] ) ) {
|
||||
return $comment_meta['source_url'][0];
|
||||
// Show external comment ID if it exists.
|
||||
$public_comment_link = self::get_source_id( $comment->comment_ID );
|
||||
|
||||
if ( $public_comment_link ) {
|
||||
return $public_comment_link;
|
||||
}
|
||||
|
||||
// generate URI based on comment ID
|
||||
return \add_query_arg(
|
||||
array(
|
||||
'c' => $comment->comment_ID,
|
||||
),
|
||||
\trailingslashit( site_url() )
|
||||
);
|
||||
// Generate URI based on comment ID.
|
||||
return \add_query_arg( 'c', $comment->comment_ID, \trailingslashit( \home_url() ) );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -397,7 +432,8 @@ class Comment {
|
||||
private static function post_has_remote_comments( $post_id ) {
|
||||
$comments = \get_comments(
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'post_id' => $post_id,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
@ -421,28 +457,29 @@ class Comment {
|
||||
*/
|
||||
public static function enqueue_scripts() {
|
||||
if ( ! \is_singular() || \is_user_logged_in() ) {
|
||||
// only on single pages, only for logged out users
|
||||
// Only on single pages, only for logged-out users.
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! \post_type_supports( \get_post_type(), 'activitypub' ) ) {
|
||||
// post type does not support ActivityPub
|
||||
// Post type does not support ActivityPub.
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! \comments_open() || ! \get_comments_number() ) {
|
||||
// no comments, no need to load the script
|
||||
// No comments, no need to load the script.
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! self::post_has_remote_comments( \get_the_ID() ) ) {
|
||||
// no remote comments, no need to load the script
|
||||
// No remote comments, no need to load the script.
|
||||
return;
|
||||
}
|
||||
|
||||
$handle = 'activitypub-remote-reply';
|
||||
$data = array(
|
||||
'namespace' => ACTIVITYPUB_REST_NAMESPACE,
|
||||
'namespace' => ACTIVITYPUB_REST_NAMESPACE,
|
||||
'defaultAvatarUrl' => ACTIVITYPUB_PLUGIN_URL . 'assets/img/mp.jpg',
|
||||
);
|
||||
$js = sprintf( 'var _activityPubOptions = %s;', wp_json_encode( $data ) );
|
||||
$asset_file = ACTIVITYPUB_PLUGIN_DIR . 'build/remote-reply/index.asset.php';
|
||||
@ -458,13 +495,323 @@ class Comment {
|
||||
true
|
||||
);
|
||||
\wp_add_inline_script( $handle, $js, 'before' );
|
||||
\wp_set_script_translations( $handle, 'activitypub' );
|
||||
|
||||
\wp_enqueue_style(
|
||||
$handle,
|
||||
\plugins_url( 'build/remote-reply/style-index.css', __DIR__ ),
|
||||
[ 'wp-components' ],
|
||||
array( 'wp-components' ),
|
||||
$assets['version']
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the comment type by activity type.
|
||||
*
|
||||
* @param string $activity_type The activity type.
|
||||
*
|
||||
* @return array|null The comment type.
|
||||
*/
|
||||
public static function get_comment_type_by_activity_type( $activity_type ) {
|
||||
$activity_type = \strtolower( $activity_type );
|
||||
$activity_type = \sanitize_key( $activity_type );
|
||||
$comment_types = self::get_comment_types();
|
||||
|
||||
foreach ( $comment_types as $comment_type ) {
|
||||
if ( in_array( $activity_type, $comment_type['activity_types'], true ) ) {
|
||||
return $comment_type;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the registered custom comment types.
|
||||
*
|
||||
* @return array The registered custom comment types
|
||||
*/
|
||||
public static function get_comment_types() {
|
||||
global $activitypub_comment_types;
|
||||
|
||||
return $activitypub_comment_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this a registered comment type.
|
||||
*
|
||||
* @param string $slug The slug of the type.
|
||||
*
|
||||
* @return boolean True if registered.
|
||||
*/
|
||||
public static function is_registered_comment_type( $slug ) {
|
||||
$slug = \strtolower( $slug );
|
||||
$slug = \sanitize_key( $slug );
|
||||
|
||||
$comment_types = self::get_comment_types();
|
||||
|
||||
return isset( $comment_types[ $slug ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the registered custom comment type slugs.
|
||||
*
|
||||
* @return array The registered custom comment type slugs.
|
||||
*/
|
||||
public static function get_comment_type_slugs() {
|
||||
return array_keys( self::get_comment_types() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the registered custom comment type slugs.
|
||||
*
|
||||
* @deprecated 4.5.0 Use get_comment_type_slugs instead.
|
||||
*
|
||||
* @return array The registered custom comment type slugs.
|
||||
*/
|
||||
public static function get_comment_type_names() {
|
||||
_deprecated_function( __METHOD__, '4.5.0', 'get_comment_type_slugs' );
|
||||
|
||||
return self::get_comment_type_slugs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the custom comment type.
|
||||
*
|
||||
* Check if the type is registered, if not, check if it is a custom type.
|
||||
*
|
||||
* It looks for the array key in the registered types and returns the array.
|
||||
* If it is not found, it looks for the type in the custom types and returns the array.
|
||||
*
|
||||
* @param string $type The comment type.
|
||||
*
|
||||
* @return array The comment type.
|
||||
*/
|
||||
public static function get_comment_type( $type ) {
|
||||
$type = strtolower( $type );
|
||||
$type = sanitize_key( $type );
|
||||
|
||||
$comment_types = self::get_comment_types();
|
||||
$type_array = array();
|
||||
|
||||
// Check array keys.
|
||||
if ( in_array( $type, array_keys( $comment_types ), true ) ) {
|
||||
$type_array = $comment_types[ $type ];
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the comment type.
|
||||
*
|
||||
* @param array $type_array The comment type.
|
||||
*/
|
||||
return apply_filters( "activitypub_comment_type_{$type}", $type_array );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a comment type attribute.
|
||||
*
|
||||
* @param string $type The comment type.
|
||||
* @param string $attr The attribute to get.
|
||||
*
|
||||
* @return mixed The value of the attribute.
|
||||
*/
|
||||
public static function get_comment_type_attr( $type, $attr ) {
|
||||
$type_array = self::get_comment_type( $type );
|
||||
|
||||
if ( $type_array && isset( $type_array[ $attr ] ) ) {
|
||||
$value = $type_array[ $attr ];
|
||||
} else {
|
||||
$value = '';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the comment type attribute.
|
||||
*
|
||||
* @param mixed $value The value of the attribute.
|
||||
* @param string $type The comment type.
|
||||
*/
|
||||
return apply_filters( "activitypub_comment_type_{$attr}", $value, $type );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the comment types used by the ActivityPub plugin.
|
||||
*/
|
||||
public static function register_comment_types() {
|
||||
register_comment_type(
|
||||
'repost',
|
||||
array(
|
||||
'label' => __( 'Reposts', 'activitypub' ),
|
||||
'singular' => __( 'Repost', 'activitypub' ),
|
||||
'description' => __( 'A repost on the indieweb is a post that is purely a 100% re-publication of another (typically someone else\'s) post.', 'activitypub' ),
|
||||
'icon' => '♻️',
|
||||
'class' => 'p-repost',
|
||||
'type' => 'repost',
|
||||
'collection' => 'reposts',
|
||||
'activity_types' => array( 'announce' ),
|
||||
'excerpt' => html_entity_decode( \__( '… reposted this!', 'activitypub' ) ),
|
||||
/* translators: %d: Number of reposts */
|
||||
'count_single' => _x( '%d repost', 'number of reposts', 'activitypub' ),
|
||||
/* translators: %d: Number of reposts */
|
||||
'count_plural' => _x( '%d reposts', 'number of reposts', 'activitypub' ),
|
||||
)
|
||||
);
|
||||
|
||||
register_comment_type(
|
||||
'like',
|
||||
array(
|
||||
'label' => __( 'Likes', 'activitypub' ),
|
||||
'singular' => __( 'Like', 'activitypub' ),
|
||||
'description' => __( 'A like is a popular webaction button and in some cases post type on various silos such as Facebook and Instagram.', 'activitypub' ),
|
||||
'icon' => '👍',
|
||||
'class' => 'p-like',
|
||||
'type' => 'like',
|
||||
'collection' => 'likes',
|
||||
'activity_types' => array( 'like' ),
|
||||
'excerpt' => html_entity_decode( \__( '… liked this!', 'activitypub' ) ),
|
||||
/* translators: %d: Number of likes */
|
||||
'count_single' => _x( '%d like', 'number of likes', 'activitypub' ),
|
||||
/* translators: %d: Number of likes */
|
||||
'count_plural' => _x( '%d likes', 'number of likes', 'activitypub' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show avatars on Activities if set.
|
||||
*
|
||||
* @param array $types List of avatar enabled comment types.
|
||||
*
|
||||
* @return array show avatars on Activities
|
||||
*/
|
||||
public static function get_avatar_comment_types( $types ) {
|
||||
$comment_types = self::get_comment_type_slugs();
|
||||
$types = array_merge( $types, $comment_types );
|
||||
|
||||
return array_unique( $types );
|
||||
}
|
||||
|
||||
/**
|
||||
* Excludes likes and reposts from comment queries.
|
||||
*
|
||||
* @author Jan Boddez
|
||||
*
|
||||
* @see https://github.com/janboddez/indieblocks/blob/a2d59de358031056a649ee47a1332ce9e39d4ce2/includes/functions.php#L423-L432
|
||||
*
|
||||
* @param WP_Comment_Query $query Comment count.
|
||||
*/
|
||||
public static function comment_query( $query ) {
|
||||
if ( ! $query instanceof WP_Comment_Query ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not exclude likes and reposts on ActivityPub requests.
|
||||
if ( defined( 'ACTIVITYPUB_REQUEST' ) && ACTIVITYPUB_REQUEST ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not exclude likes and reposts on REST requests.
|
||||
if ( \wp_is_serving_rest_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not exclude likes and reposts on admin pages or on non-singular pages.
|
||||
if ( is_admin() || ! is_singular() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Do not exclude likes and reposts if the query is for comments.
|
||||
if ( ! empty( $query->query_vars['type__in'] ) || ! empty( $query->query_vars['type'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Exclude likes and reposts by the ActivityPub plugin.
|
||||
$query->query_vars['type__not_in'] = self::get_comment_type_slugs();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the comment status before it is set.
|
||||
*
|
||||
* @param string $approved The approved comment status.
|
||||
* @param array $commentdata The comment data.
|
||||
*
|
||||
* @return boolean `true` if the comment is approved, `false` otherwise.
|
||||
*/
|
||||
public static function pre_comment_approved( $approved, $commentdata ) {
|
||||
if ( $approved || \is_wp_error( $approved ) ) {
|
||||
return $approved;
|
||||
}
|
||||
|
||||
if ( '1' !== \get_option( 'comment_previously_approved' ) ) {
|
||||
return $approved;
|
||||
}
|
||||
|
||||
if (
|
||||
empty( $commentdata['comment_meta']['protocol'] ) ||
|
||||
'activitypub' !== $commentdata['comment_meta']['protocol']
|
||||
) {
|
||||
return $approved;
|
||||
}
|
||||
|
||||
global $wpdb;
|
||||
|
||||
$author = $commentdata['comment_author'];
|
||||
$author_url = $commentdata['comment_author_url'];
|
||||
// phpcs:ignore
|
||||
$ok_to_comment = $wpdb->get_var( $wpdb->prepare( "SELECT comment_approved FROM $wpdb->comments WHERE comment_author = %s AND comment_author_url = %s and comment_approved = '1' LIMIT 1", $author, $author_url ) );
|
||||
|
||||
if ( 1 === (int) $ok_to_comment ) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
return $approved;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update comment counts when interaction settings are disabled.
|
||||
*
|
||||
* Triggers a recount when likes or reposts are disabled to ensure accurate comment counts.
|
||||
*
|
||||
* @param mixed $old_value The old option value.
|
||||
* @param mixed $value The new option value.
|
||||
*/
|
||||
public static function maybe_update_comment_counts( $old_value, $value ) {
|
||||
if ( '1' === $old_value && '1' !== $value ) {
|
||||
Migration::update_comment_counts();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the comment count to exclude ActivityPub comment types.
|
||||
*
|
||||
* @param int|null $new_count The new comment count. Default null.
|
||||
* @param int $old_count The old comment count.
|
||||
* @param int $post_id Post ID.
|
||||
*
|
||||
* @return int|null The updated comment count, or null to use the default query.
|
||||
*/
|
||||
public static function pre_wp_update_comment_count_now( $new_count, $old_count, $post_id ) {
|
||||
if ( null === $new_count ) {
|
||||
$excluded_types = array_filter( self::get_comment_type_slugs(), array( self::class, 'is_comment_type_enabled' ) );
|
||||
|
||||
if ( ! empty( $excluded_types ) ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB
|
||||
$new_count = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM $wpdb->comments WHERE comment_post_ID = %d AND comment_approved = '1' AND comment_type NOT IN ('" . implode( "','", $excluded_types ) . "')", $post_id ) );
|
||||
}
|
||||
}
|
||||
|
||||
return $new_count;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a comment type is enabled.
|
||||
*
|
||||
* @param string $comment_type The comment type.
|
||||
* @return bool True if the comment type is enabled.
|
||||
*/
|
||||
public static function is_comment_type_enabled( $comment_type ) {
|
||||
return '1' === get_option( "activitypub_allow_{$comment_type}s", '1' );
|
||||
}
|
||||
}
|
||||
|
@ -1,37 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* Debug Class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_DEBUG;
|
||||
use WP_DEBUG_LOG;
|
||||
|
||||
/**
|
||||
* ActivityPub Debug Class
|
||||
* ActivityPub Debug Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Debug {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( WP_DEBUG_LOG ) {
|
||||
\add_action( 'activitypub_safe_remote_post_response', array( self::class, 'log_remote_post_responses' ), 10, 4 );
|
||||
if ( \WP_DEBUG && \WP_DEBUG_LOG ) {
|
||||
\add_action( 'activitypub_safe_remote_post_response', array( self::class, 'log_remote_post_responses' ), 10, 2 );
|
||||
\add_action( 'activitypub_inbox', array( self::class, 'log_inbox' ), 10, 3 );
|
||||
|
||||
\add_action( 'activitypub_sent_to_inbox', array( self::class, 'log_sent_to_inbox' ), 10, 2 );
|
||||
}
|
||||
}
|
||||
|
||||
// phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter.FoundAfterLastUsed, VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
|
||||
public static function log_remote_post_responses( $response, $url, $body, $user_id ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r
|
||||
\error_log( "Request to: {$url} with response: " . \print_r( $response, true ) );
|
||||
/**
|
||||
* Log the responses of remote post requests.
|
||||
*
|
||||
* @param array $response The response from the remote server.
|
||||
* @param string $url The URL of the remote server.
|
||||
*/
|
||||
public static function log_remote_post_responses( $response, $url ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
|
||||
\error_log( "[OUTBOX] Request to: {$url} with Response: " . \print_r( $response, true ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the inbox requests.
|
||||
*
|
||||
* @param array $data The Activity array.
|
||||
* @param int $user_id The ID of the local blog user.
|
||||
* @param string $type The type of the request.
|
||||
*/
|
||||
public static function log_inbox( $data, $user_id, $type ) {
|
||||
$type = strtolower( $type );
|
||||
|
||||
if ( 'delete' !== $type ) {
|
||||
$actor = $data['actor'] ?? '';
|
||||
$url = object_to_uri( $actor );
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
|
||||
\error_log( "[INBOX] Request From: {$url} with Activity: " . \print_r( $data, true ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Log the sent to follower action.
|
||||
*
|
||||
* @param array $result The result of the remote post request.
|
||||
* @param string $inbox The inbox URL.
|
||||
*/
|
||||
public static function log_sent_to_inbox( $result, $inbox ) {
|
||||
if ( \is_wp_error( $result ) ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
|
||||
\error_log( "[DISPATCHER] Failed Request to: {$inbox} with Result: " . \print_r( $result, true ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Write a log entry.
|
||||
*
|
||||
* @param mixed $log The log entry.
|
||||
*/
|
||||
public static function write_log( $log ) {
|
||||
if ( \is_array( $log ) || \is_object( $log ) ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log, WordPress.PHP.DevelopmentFunctions.error_log_print_r
|
||||
\error_log( \print_r( $log, true ) );
|
||||
} else {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
\error_log( $log );
|
||||
}
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions
|
||||
\error_log( \print_r( $log, true ) );
|
||||
}
|
||||
}
|
||||
|
466
wp-content/plugins/activitypub/includes/class-dispatcher.php
Normal file
466
wp-content/plugins/activitypub/includes/class-dispatcher.php
Normal file
@ -0,0 +1,466 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub Dispatcher Class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Outbox;
|
||||
|
||||
/**
|
||||
* ActivityPub Dispatcher Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitypub/
|
||||
*/
|
||||
class Dispatcher {
|
||||
|
||||
/**
|
||||
* Batch size.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public static $batch_size = ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE;
|
||||
|
||||
/**
|
||||
* Callback for the async batch processing.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public static $callback = array( self::class, 'send_to_followers' );
|
||||
|
||||
/**
|
||||
* Error codes that qualify for a retry.
|
||||
*
|
||||
* @see https://github.com/tfredrich/RestApiTutorial.com/blob/fd08b0f67f07450521d143b123cd6e1846cb2e3b/content/advanced/responses/retries.md
|
||||
* @var int[]
|
||||
*/
|
||||
public static $retry_error_codes = array( 408, 429, 500, 502, 503, 504 );
|
||||
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_process_outbox', array( self::class, 'process_outbox' ) );
|
||||
|
||||
// Default filters to add Inboxes to sent to.
|
||||
\add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_by_mentioned_actors' ), 10, 3 );
|
||||
\add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_replied_urls' ), 10, 3 );
|
||||
\add_filter( 'activitypub_additional_inboxes', array( self::class, 'add_inboxes_of_relays' ), 10, 3 );
|
||||
|
||||
// Fallback for `activitypub_send_to_inboxes` filter.
|
||||
\add_filter(
|
||||
'activitypub_additional_inboxes',
|
||||
function ( $inboxes, $actor_id, $activity ) {
|
||||
/**
|
||||
* Filters the list of interactees inboxes to send the Activity to.
|
||||
*
|
||||
* @param array $inboxes The list of inboxes to send to.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*
|
||||
* @deprecated 5.2.0 Use `activitypub_additional_inboxes` instead.
|
||||
* @deprecated 5.4.0 Use `activitypub_additional_inboxes` instead.
|
||||
*/
|
||||
$inboxes = \apply_filters_deprecated( 'activitypub_send_to_inboxes', array( $inboxes, $actor_id, $activity ), '5.2.0', 'activitypub_additional_inboxes' );
|
||||
$inboxes = \apply_filters_deprecated( 'activitypub_interactees_inboxes', array( $inboxes, $actor_id, $activity ), '5.4.0', 'activitypub_additional_inboxes' );
|
||||
|
||||
return $inboxes;
|
||||
},
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process the outbox.
|
||||
*
|
||||
* @param int $id The outbox ID.
|
||||
*/
|
||||
public static function process_outbox( $id ) {
|
||||
$outbox_item = \get_post( $id );
|
||||
|
||||
// If the activity is not a post, return.
|
||||
if ( ! $outbox_item ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$actor = Outbox::get_actor( $outbox_item );
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
// If the actor is not found, publish the post and don't try again.
|
||||
\wp_publish_post( $outbox_item );
|
||||
return;
|
||||
}
|
||||
|
||||
$activity = Outbox::get_activity( $outbox_item );
|
||||
|
||||
// Send to mentioned and replied-to users. Everyone other than followers.
|
||||
self::send_to_additional_inboxes( $activity, $actor->get__id(), $outbox_item );
|
||||
|
||||
if ( self::should_send_to_followers( $activity, $actor, $outbox_item ) ) {
|
||||
Scheduler::async_batch(
|
||||
self::$callback,
|
||||
$outbox_item->ID,
|
||||
self::$batch_size,
|
||||
\get_post_meta( $outbox_item->ID, '_activitypub_outbox_offset', true ) ?: 0 // phpcs:ignore
|
||||
);
|
||||
} else {
|
||||
// No followers to process for this update. We're done.
|
||||
\wp_publish_post( $outbox_item );
|
||||
\delete_post_meta( $outbox_item->ID, '_activitypub_outbox_offset' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously runs batch processing routines.
|
||||
*
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $batch_size Optional. The batch size. Default ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE.
|
||||
* @param int $offset Optional. The offset. Default 0.
|
||||
*
|
||||
* @return array|void The next batch of followers to process, or void if done.
|
||||
*/
|
||||
public static function send_to_followers( $outbox_item_id, $batch_size = ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE, $offset = 0 ) {
|
||||
$json = Outbox::get_activity( $outbox_item_id )->to_json();
|
||||
$actor = Outbox::get_actor( \get_post( $outbox_item_id ) );
|
||||
$inboxes = Followers::get_inboxes_for_activity( $json, $actor->get__id(), $batch_size, $offset );
|
||||
|
||||
$retries = self::send_to_inboxes( $inboxes, $outbox_item_id );
|
||||
|
||||
// Retry failed inboxes.
|
||||
if ( ! empty( $retries ) ) {
|
||||
self::schedule_retry( $retries, $outbox_item_id );
|
||||
}
|
||||
|
||||
if ( is_countable( $inboxes ) && count( $inboxes ) < $batch_size ) {
|
||||
\delete_post_meta( $outbox_item_id, '_activitypub_outbox_offset' );
|
||||
|
||||
/**
|
||||
* Fires when the followers are complete.
|
||||
*
|
||||
* @param array $inboxes The inboxes.
|
||||
* @param string $json The ActivityPub Activity JSON
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $batch_size The batch size.
|
||||
* @param int $offset The offset.
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_processing_complete', $inboxes, $json, $actor->get__id(), $outbox_item_id, $batch_size, $offset );
|
||||
|
||||
// No more followers to process for this update.
|
||||
\wp_publish_post( $outbox_item_id );
|
||||
} else {
|
||||
\update_post_meta( $outbox_item_id, '_activitypub_outbox_offset', $offset + $batch_size );
|
||||
|
||||
/**
|
||||
* Fires when the batch of followers is complete.
|
||||
*
|
||||
* @param array $inboxes The inboxes.
|
||||
* @param string $json The ActivityPub Activity JSON
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $batch_size The batch size.
|
||||
* @param int $offset The offset.
|
||||
*/
|
||||
\do_action( 'activitypub_outbox_processing_batch_complete', $inboxes, $json, $actor->get__id(), $outbox_item_id, $batch_size, $offset );
|
||||
|
||||
return array( $outbox_item_id, $batch_size, $offset + $batch_size );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retry sending to followers.
|
||||
*
|
||||
* @param string $transient_key The key to retrieve retry inboxes.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $attempt The attempt number.
|
||||
*/
|
||||
public static function retry_send_to_followers( $transient_key, $outbox_item_id, $attempt = 1 ) {
|
||||
$inboxes = \get_transient( $transient_key );
|
||||
if ( false === $inboxes ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Delete the transient as we no longer need it.
|
||||
\delete_transient( $transient_key );
|
||||
|
||||
$retries = self::send_to_inboxes( $inboxes, $outbox_item_id );
|
||||
|
||||
// Retry failed inboxes.
|
||||
if ( ++$attempt < 3 && ! empty( $retries ) ) {
|
||||
self::schedule_retry( $retries, $outbox_item_id, $attempt );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send to inboxes.
|
||||
*
|
||||
* @param array $inboxes The inboxes to notify.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @return array The failed inboxes.
|
||||
*/
|
||||
private static function send_to_inboxes( $inboxes, $outbox_item_id ) {
|
||||
$json = Outbox::get_activity( $outbox_item_id )->to_json();
|
||||
$actor = Outbox::get_actor( \get_post( $outbox_item_id ) );
|
||||
$retries = array();
|
||||
|
||||
/**
|
||||
* Fires before sending an Activity to inboxes.
|
||||
*
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param array $inboxes The inboxes to send to.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
*/
|
||||
\do_action( 'activitypub_pre_send_to_inboxes', $json, $inboxes, $outbox_item_id );
|
||||
|
||||
foreach ( $inboxes as $inbox ) {
|
||||
$result = safe_remote_post( $inbox, $json, $actor->get__id() );
|
||||
|
||||
if ( is_wp_error( $result ) && in_array( $result->get_error_code(), self::$retry_error_codes, true ) ) {
|
||||
$retries[] = $inbox;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after an Activity has been sent to an inbox.
|
||||
*
|
||||
* @param array $result The result of the remote post request.
|
||||
* @param string $inbox The inbox URL.
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
*/
|
||||
\do_action( 'activitypub_sent_to_inbox', $result, $inbox, $json, $actor->get__id(), $outbox_item_id );
|
||||
}
|
||||
|
||||
return $retries;
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule a retry.
|
||||
*
|
||||
* @param array $retries The inboxes to retry.
|
||||
* @param int $outbox_item_id The Outbox item ID.
|
||||
* @param int $attempt Optional. The attempt number. Default 1.
|
||||
*/
|
||||
private static function schedule_retry( $retries, $outbox_item_id, $attempt = 1 ) {
|
||||
$transient_key = 'activitypub_retry_' . \wp_generate_password( 12, false );
|
||||
\set_transient( $transient_key, $retries, WEEK_IN_SECONDS );
|
||||
|
||||
\wp_schedule_single_event(
|
||||
\time() + ( $attempt * $attempt * HOUR_IN_SECONDS ),
|
||||
'activitypub_async_batch',
|
||||
array(
|
||||
array( self::class, 'retry_send_to_followers' ),
|
||||
$transient_key,
|
||||
$outbox_item_id,
|
||||
$attempt,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an Activity to a custom list of inboxes, like mentioned users or replied-to posts.
|
||||
*
|
||||
* For all custom implementations, please use the `activitypub_additional_inboxes` filter.
|
||||
*
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param \WP_Post $outbox_item The WordPress object.
|
||||
*/
|
||||
private static function send_to_additional_inboxes( $activity, $actor_id, $outbox_item = null ) {
|
||||
/**
|
||||
* Filters the list of inboxes to send the Activity to.
|
||||
*
|
||||
* @param array $inboxes The list of inboxes to send to.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*/
|
||||
$inboxes = apply_filters( 'activitypub_additional_inboxes', array(), $actor_id, $activity );
|
||||
$inboxes = array_unique( $inboxes );
|
||||
|
||||
$retries = self::send_to_inboxes( $inboxes, $outbox_item->ID );
|
||||
|
||||
// Retry failed inboxes.
|
||||
if ( ! empty( $retries ) ) {
|
||||
self::schedule_retry( $retries, $outbox_item->ID );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Default filter to add Inboxes of Mentioned Actors
|
||||
*
|
||||
* @param array $inboxes The list of Inboxes.
|
||||
* @param int $actor_id The WordPress Actor-ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*
|
||||
* @return array The filtered Inboxes.
|
||||
*/
|
||||
public static function add_inboxes_by_mentioned_actors( $inboxes, $actor_id, $activity ) {
|
||||
$cc = $activity->get_cc() ?? array();
|
||||
$to = $activity->get_to() ?? array();
|
||||
|
||||
$audience = array_merge( $cc, $to );
|
||||
|
||||
// Remove "public placeholder" and "same domain" from the audience.
|
||||
$audience = array_filter(
|
||||
$audience,
|
||||
function ( $actor ) {
|
||||
return 'https://www.w3.org/ns/activitystreams#Public' !== $actor && ! is_same_domain( $actor );
|
||||
}
|
||||
);
|
||||
|
||||
if ( $audience ) {
|
||||
$mentioned_inboxes = Mention::get_inboxes( $audience );
|
||||
|
||||
return array_merge( $inboxes, $mentioned_inboxes );
|
||||
}
|
||||
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Default filter to add Inboxes of Posts that are set as `in-reply-to`
|
||||
*
|
||||
* @param array $inboxes The list of Inboxes.
|
||||
* @param int $actor_id The WordPress Actor-ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*
|
||||
* @return array The filtered Inboxes
|
||||
*/
|
||||
public static function add_inboxes_of_replied_urls( $inboxes, $actor_id, $activity ) {
|
||||
$in_reply_to = $activity->get_in_reply_to();
|
||||
|
||||
if ( ! $in_reply_to ) {
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
if ( ! is_array( $in_reply_to ) ) {
|
||||
$in_reply_to = array( $in_reply_to );
|
||||
}
|
||||
|
||||
foreach ( $in_reply_to as $url ) {
|
||||
// No need to self-notify.
|
||||
if ( is_same_domain( $url ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$object = Http::get_remote_object( $url );
|
||||
|
||||
if (
|
||||
! $object ||
|
||||
\is_wp_error( $object ) ||
|
||||
empty( $object['attributedTo'] )
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$actor = object_to_uri( $object['attributedTo'] );
|
||||
$actor = Http::get_remote_object( $actor );
|
||||
|
||||
if ( ! $actor || \is_wp_error( $actor ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( ! empty( $actor['endpoints']['sharedInbox'] ) ) {
|
||||
$inboxes[] = $actor['endpoints']['sharedInbox'];
|
||||
} elseif ( ! empty( $actor['inbox'] ) ) {
|
||||
$inboxes[] = $actor['inbox'];
|
||||
}
|
||||
}
|
||||
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Blog Actor inboxes to Updates so the Blog User's followers are notified of edits.
|
||||
*
|
||||
* @deprecated 5.2.0 Use {@see Followers::maybe_add_inboxes_of_blog_user} instead.
|
||||
*
|
||||
* @param array $inboxes The list of Inboxes.
|
||||
* @param int $actor_id The WordPress Actor-ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*
|
||||
* @return array The filtered Inboxes.
|
||||
*/
|
||||
public static function maybe_add_inboxes_of_blog_user( $inboxes, $actor_id, $activity ) { // phpcs:ignore
|
||||
_deprecated_function( __METHOD__, '5.2.0', 'Followers::maybe_add_inboxes_of_blog_user' );
|
||||
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if passed Activity is public.
|
||||
*
|
||||
* @param Activity $activity The Activity object.
|
||||
* @param \Activitypub\Model\User|\Activitypub\Model\Blog $actor The Actor object.
|
||||
* @param \WP_Post $outbox_item The Outbox item.
|
||||
*
|
||||
* @return boolean True if public, false if not.
|
||||
*/
|
||||
protected static function should_send_to_followers( $activity, $actor, $outbox_item ) {
|
||||
// Check if follower endpoint is set.
|
||||
$cc = $activity->get_cc() ?? array();
|
||||
$to = $activity->get_to() ?? array();
|
||||
|
||||
$audience = array_merge( $cc, $to );
|
||||
|
||||
$send = (
|
||||
// Check if activity is public.
|
||||
in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) ||
|
||||
// ...or check if follower endpoint is set.
|
||||
in_array( $actor->get_followers(), $audience, true )
|
||||
);
|
||||
|
||||
if ( $send ) {
|
||||
$followers = Followers::get_inboxes_for_activity( $activity->to_json(), $actor->get__id() );
|
||||
|
||||
// Only send if there are followers to send to.
|
||||
$send = ! is_countable( $followers ) || 0 < count( $followers );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters whether to send an Activity to followers.
|
||||
*
|
||||
* @param bool $send_activity_to_followers Whether to send the Activity to followers.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param \WP_Post $outbox_item The WordPress object.
|
||||
*/
|
||||
return apply_filters( 'activitypub_send_activity_to_followers', $send, $activity, $actor->get__id(), $outbox_item );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Inboxes of Relays.
|
||||
*
|
||||
* @param array $inboxes The list of Inboxes.
|
||||
* @param int $actor_id The Actor-ID.
|
||||
* @param Activity $activity The ActivityPub Activity.
|
||||
*
|
||||
* @return array The filtered Inboxes.
|
||||
*/
|
||||
public static function add_inboxes_of_relays( $inboxes, $actor_id, $activity ) {
|
||||
// Check if follower endpoint is set.
|
||||
$cc = $activity->get_cc() ?? array();
|
||||
$to = $activity->get_to() ?? array();
|
||||
|
||||
$audience = array_merge( $cc, $to );
|
||||
|
||||
// Check if activity is public.
|
||||
if ( ! in_array( 'https://www.w3.org/ns/activitystreams#Public', $audience, true ) ) {
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
$relays = \get_option( 'activitypub_relays', array() );
|
||||
|
||||
if ( empty( $relays ) ) {
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
return array_merge( $inboxes, $relays );
|
||||
}
|
||||
}
|
263
wp-content/plugins/activitypub/includes/class-embed.php
Normal file
263
wp-content/plugins/activitypub/includes/class-embed.php
Normal file
@ -0,0 +1,263 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub Embed Handler.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* Class to handle embedding ActivityPub content
|
||||
*/
|
||||
class Embed {
|
||||
|
||||
/**
|
||||
* Initialize the embed handler
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'pre_oembed_result', array( self::class, 'maybe_use_activitypub_embed' ), 10, 3 );
|
||||
\add_filter( 'oembed_dataparse', array( self::class, 'handle_filtered_oembed_result' ), 11, 3 );
|
||||
\add_filter( 'oembed_request_post_id', array( self::class, 'register_fallback_hook' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ActivityPub embed HTML for a URL.
|
||||
*
|
||||
* @param string $url The URL to get the embed for.
|
||||
* @param boolean $inline_css Whether to inline CSS. Default true.
|
||||
*
|
||||
* @return string|false The embed HTML or false if not found.
|
||||
*/
|
||||
public static function get_html( $url, $inline_css = true ) {
|
||||
// Try to get ActivityPub representation.
|
||||
$object = Http::get_remote_object( $url );
|
||||
if ( is_wp_error( $object ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::get_html_for_object( $object, $inline_css );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an ActivityPub embed HTML for an ActivityPub object.
|
||||
*
|
||||
* @param array $activity_object The ActivityPub object to build the embed for.
|
||||
* @param boolean $inline_css Whether to inline CSS. Default true.
|
||||
*
|
||||
* @return string The embed HTML.
|
||||
*/
|
||||
public static function get_html_for_object( $activity_object, $inline_css = true ) {
|
||||
$author_name = $activity_object['attributedTo'] ?? '';
|
||||
$avatar_url = $activity_object['icon']['url'] ?? '';
|
||||
$author_url = $author_name;
|
||||
|
||||
// If we don't have an avatar URL, but we have an author URL, try to fetch it.
|
||||
if ( ! $avatar_url && $author_url ) {
|
||||
$author = Http::get_remote_object( $author_url );
|
||||
if ( ! is_wp_error( $author ) ) {
|
||||
$avatar_url = $author['icon']['url'] ?? '';
|
||||
$author_name = $author['name'] ?? $author_name;
|
||||
}
|
||||
}
|
||||
|
||||
// Create Webfinger where not found.
|
||||
if ( empty( $author['webfinger'] ) ) {
|
||||
if ( ! empty( $author['preferredUsername'] ) && ! empty( $author['url'] ) ) {
|
||||
// Construct webfinger-style identifier from username and domain.
|
||||
$domain = wp_parse_url( $author['url'], PHP_URL_HOST );
|
||||
$author['webfinger'] = '@' . $author['preferredUsername'] . '@' . $domain;
|
||||
} else {
|
||||
// Fallback to URL.
|
||||
$author['webfinger'] = $author_url;
|
||||
}
|
||||
}
|
||||
|
||||
$title = $activity_object['name'] ?? '';
|
||||
$content = $activity_object['content'] ?? '';
|
||||
$published = isset( $activity_object['published'] ) ? gmdate( get_option( 'date_format' ) . ', ' . get_option( 'time_format' ), strtotime( $activity_object['published'] ) ) : '';
|
||||
$boosts = isset( $activity_object['shares']['totalItems'] ) ? (int) $activity_object['shares']['totalItems'] : null;
|
||||
$favorites = isset( $activity_object['likes']['totalItems'] ) ? (int) $activity_object['likes']['totalItems'] : null;
|
||||
|
||||
$image = '';
|
||||
if ( isset( $activity_object['image']['url'] ) ) {
|
||||
$image = $activity_object['image']['url'];
|
||||
} elseif ( isset( $activity_object['attachment'] ) ) {
|
||||
foreach ( $activity_object['attachment'] as $attachment ) {
|
||||
if ( isset( $attachment['type'] ) && in_array( $attachment['type'], array( 'Image', 'Document' ), true ) ) {
|
||||
$image = $attachment['url'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ob_start();
|
||||
load_template(
|
||||
ACTIVITYPUB_PLUGIN_DIR . 'templates/reply-embed.php',
|
||||
false,
|
||||
array(
|
||||
'author_name' => $author_name,
|
||||
'author_url' => $author_url,
|
||||
'avatar_url' => $avatar_url,
|
||||
'published' => $published,
|
||||
'title' => $title,
|
||||
'content' => $content,
|
||||
'image' => $image,
|
||||
'boosts' => $boosts,
|
||||
'favorites' => $favorites,
|
||||
'url' => $activity_object['id'],
|
||||
'webfinger' => $author['webfinger'],
|
||||
)
|
||||
);
|
||||
|
||||
if ( $inline_css ) {
|
||||
// Grab the CSS.
|
||||
$css = \file_get_contents( ACTIVITYPUB_PLUGIN_DIR . 'assets/css/activitypub-embed.css' ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
// We embed CSS directly because this may be in an iframe.
|
||||
printf( '<style>%s</style>', $css ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
|
||||
}
|
||||
|
||||
// A little light whitespace cleanup.
|
||||
return preg_replace( '/\s+/', ' ', ob_get_clean() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a real oEmbed result exists for the given URL.
|
||||
*
|
||||
* @param string $url The URL to check.
|
||||
* @param array $args Additional arguments passed to wp_oembed_get().
|
||||
* @return bool True if a real oEmbed result exists, false otherwise.
|
||||
*/
|
||||
public static function has_real_oembed( $url, $args = array() ) {
|
||||
// Temporarily remove our filter to avoid infinite loops.
|
||||
\remove_filter( 'pre_oembed_result', array( self::class, 'maybe_use_activitypub_embed' ), 10, 3 );
|
||||
|
||||
// Try to get a "real" oEmbed result. If found, it'll be cached to avoid unnecessary HTTP requests in `wp_oembed_get`.
|
||||
$oembed_result = \wp_oembed_get( $url, $args );
|
||||
|
||||
// Add our filter back.
|
||||
\add_filter( 'pre_oembed_result', array( self::class, 'maybe_use_activitypub_embed' ), 10, 3 );
|
||||
|
||||
return false !== $oembed_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the oembed result to handle ActivityPub content when no oEmbed is found.
|
||||
* Implementation is a bit weird because there's no way to filter on a false result, we have to use `pre_oembed_result`.
|
||||
*
|
||||
* @param null|string $result The UNSANITIZED (and potentially unsafe) HTML that should be used to embed.
|
||||
* @param string $url The URL to the content that should be attempted to be embedded.
|
||||
* @param array $args Additional arguments passed to wp_oembed_get().
|
||||
* @return null|string Return null to allow normal oEmbed processing, or string for ActivityPub embed.
|
||||
*/
|
||||
public static function maybe_use_activitypub_embed( $result, $url, $args ) {
|
||||
// If we already have a result, return it.
|
||||
if ( null !== $result ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
// If we found a real oEmbed, return null to allow normal processing.
|
||||
if ( self::has_real_oembed( $url, $args ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// No oEmbed found, try to get ActivityPub representation.
|
||||
$html = get_embed_html( $url );
|
||||
|
||||
// If we couldn't get an ActivityPub embed either, return null to allow normal processing.
|
||||
if ( ! $html ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Return the ActivityPub embed HTML.
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle cases where WordPress has filtered out the oEmbed result for security reasons,
|
||||
* but we can provide a safe ActivityPub-specific markup.
|
||||
*
|
||||
* This runs after wp_filter_oembed_result has potentially nullified the result.
|
||||
*
|
||||
* @param string|false $html The returned oEmbed HTML.
|
||||
* @param object $data A data object result from an oEmbed provider.
|
||||
* @param string $url The URL of the content to be embedded.
|
||||
* @return string|false The filtered oEmbed HTML or our ActivityPub embed.
|
||||
*/
|
||||
public static function handle_filtered_oembed_result( $html, $data, $url ) {
|
||||
// If we already have valid HTML, return it.
|
||||
if ( $html ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// If this isn't a rich or video type, we can't help.
|
||||
if ( ! isset( $data->type ) || ! \in_array( $data->type, array( 'rich', 'video' ), true ) ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// If there's no HTML in the data, we can't help.
|
||||
if ( empty( $data->html ) || ! \is_string( $data->html ) ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Try to get ActivityPub representation.
|
||||
$activitypub_html = get_embed_html( $url );
|
||||
if ( ! $activitypub_html ) {
|
||||
return $html;
|
||||
}
|
||||
|
||||
// Return our safer ActivityPub embed HTML.
|
||||
return $activitypub_html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the fallback hook for oEmbed requests.
|
||||
*
|
||||
* Avoids filtering every single API request.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @return int The post ID.
|
||||
*/
|
||||
public static function register_fallback_hook( $post_id ) {
|
||||
\add_filter( 'rest_request_after_callbacks', array( self::class, 'oembed_fediverse_fallback' ), 10, 3 );
|
||||
|
||||
return $post_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fallback for oEmbed requests to the Fediverse.
|
||||
*
|
||||
* @param \WP_REST_Response|\WP_Error $response Result to send to the client.
|
||||
* @param array $handler Route handler used for the request.
|
||||
* @param \WP_REST_Request $request Request used to generate the response.
|
||||
*
|
||||
* @return \WP_REST_Response|\WP_Error The response to send to the client.
|
||||
*/
|
||||
public static function oembed_fediverse_fallback( $response, $handler, $request ) {
|
||||
if ( is_wp_error( $response ) && 'oembed_invalid_url' === $response->get_error_code() ) {
|
||||
$url = $request->get_param( 'url' );
|
||||
$html = get_embed_html( $url );
|
||||
|
||||
if ( $html ) {
|
||||
$args = $request->get_params();
|
||||
$data = (object) array(
|
||||
'provider_name' => 'Embed Handler',
|
||||
'html' => $html,
|
||||
'scripts' => array(),
|
||||
);
|
||||
|
||||
/** This filter is documented in wp-includes/class-wp-oembed.php */
|
||||
$data->html = apply_filters( 'oembed_result', $data->html, $url, $args );
|
||||
|
||||
/** This filter is documented in wp-includes/class-wp-oembed-controller.php */
|
||||
$ttl = apply_filters( 'rest_oembed_ttl', DAY_IN_SECONDS, $url, $args );
|
||||
|
||||
set_transient( 'oembed_' . md5( serialize( $args ) ), $data, $ttl ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
|
||||
|
||||
$response = new \WP_REST_Response( $data );
|
||||
}
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
@ -1,9 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Handler class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Handler\Announce;
|
||||
use Activitypub\Handler\Create;
|
||||
use Activitypub\Handler\Delete;
|
||||
use Activitypub\Handler\Follow;
|
||||
use Activitypub\Handler\Like;
|
||||
use Activitypub\Handler\Move;
|
||||
use Activitypub\Handler\Undo;
|
||||
use Activitypub\Handler\Update;
|
||||
|
||||
@ -12,7 +21,7 @@ use Activitypub\Handler\Update;
|
||||
*/
|
||||
class Handler {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
self::register_handlers();
|
||||
@ -22,12 +31,20 @@ class Handler {
|
||||
* Register handlers.
|
||||
*/
|
||||
public static function register_handlers() {
|
||||
Announce::init();
|
||||
Create::init();
|
||||
Delete::init();
|
||||
Follow::init();
|
||||
Undo::init();
|
||||
Update::init();
|
||||
Like::init();
|
||||
Move::init();
|
||||
|
||||
/**
|
||||
* Register additional handlers.
|
||||
*
|
||||
* @since 1.3.0
|
||||
*/
|
||||
do_action( 'activitypub_register_handlers' );
|
||||
}
|
||||
}
|
||||
|
@ -1,117 +1,108 @@
|
||||
<?php
|
||||
/**
|
||||
* Hashtag Class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub Hashtag Class
|
||||
* ActivityPub Hashtag Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Hashtag {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
if ( '1' === \get_option( 'activitypub_use_hashtags', '1' ) ) {
|
||||
\add_action( 'wp_insert_post', array( self::class, 'insert_post' ), 10, 2 );
|
||||
\add_filter( 'the_content', array( self::class, 'the_content' ), 10, 1 );
|
||||
\add_filter( 'the_content', array( self::class, 'the_content' ) );
|
||||
\add_filter( 'activitypub_activity_object_array', array( self::class, 'filter_activity_object' ), 99 );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to save #tags as real WordPress tags
|
||||
* Filter only the activity object and replace summery it with URLs.
|
||||
*
|
||||
* @param int $id the rev-id
|
||||
* @param WP_Post $post the post
|
||||
* @param array $activity The activity object array.
|
||||
*
|
||||
* @return
|
||||
* @return array The filtered activity object array.
|
||||
*/
|
||||
public static function insert_post( $id, $post ) {
|
||||
if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $post->post_content, $match ) ) {
|
||||
$tags = \implode( ', ', $match[1] );
|
||||
|
||||
\wp_add_post_tags( $post->post_parent, $tags );
|
||||
public static function filter_activity_object( $activity ) {
|
||||
/* phpcs:ignore Squiz.PHP.CommentedOutCode.Found
|
||||
Only changed it for Person and Group as long is not merged: https://github.com/mastodon/mastodon/pull/28629
|
||||
*/
|
||||
if ( ! empty( $activity['summary'] ) && in_array( $activity['type'], array( 'Person', 'Group' ), true ) ) {
|
||||
$activity['summary'] = self::the_content( $activity['summary'] );
|
||||
}
|
||||
|
||||
return $id;
|
||||
if ( ! empty( $activity['content'] ) ) {
|
||||
$activity['content'] = self::the_content( $activity['content'] );
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to replace the #tags in the content with links
|
||||
* Filter to save #tags as real WordPress tags.
|
||||
*
|
||||
* @param string $the_content the post-content
|
||||
* @param int $post_id Post ID.
|
||||
* @param \WP_Post $post Post object.
|
||||
*/
|
||||
public static function insert_post( $post_id, $post ) {
|
||||
// Check if the post supports ActivityPub.
|
||||
if ( ! \post_type_supports( \get_post_type( $post ), 'activitypub' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the (custom) post supports tags.
|
||||
$taxonomies = \get_object_taxonomies( $post );
|
||||
if ( ! in_array( 'post_tag', $taxonomies, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$tags = array();
|
||||
|
||||
// Skip hashtags in HTML attributes, like hex colors.
|
||||
$content = wp_strip_all_tags( $post->post_content . "\n" . $post->post_excerpt );
|
||||
|
||||
if ( \preg_match_all( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', $content, $match ) ) {
|
||||
$tags = array_unique( $match[1] );
|
||||
}
|
||||
|
||||
\wp_add_post_tags( $post->ID, \implode( ', ', $tags ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to replace the #tags in the content with links.
|
||||
*
|
||||
* @return string the filtered post-content
|
||||
* @param string $the_content The post content.
|
||||
*
|
||||
* @return string The filtered post content.
|
||||
*/
|
||||
public static function the_content( $the_content ) {
|
||||
// small protection against execution timeouts: limit to 1 MB
|
||||
if ( mb_strlen( $the_content ) > MB_IN_BYTES ) {
|
||||
return $the_content;
|
||||
}
|
||||
$tag_stack = array();
|
||||
$protected_tags = array(
|
||||
'pre',
|
||||
'code',
|
||||
'textarea',
|
||||
'style',
|
||||
'a',
|
||||
);
|
||||
$content_with_links = '';
|
||||
$in_protected_tag = false;
|
||||
foreach ( wp_html_split( $the_content ) as $chunk ) {
|
||||
if ( preg_match( '#^<!--[\s\S]*-->$#i', $chunk, $m ) ) {
|
||||
$content_with_links .= $chunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( preg_match( '#^<(/)?([a-z-]+)\b[^>]*>$#i', $chunk, $m ) ) {
|
||||
$tag = strtolower( $m[2] );
|
||||
if ( '/' === $m[1] ) {
|
||||
// Closing tag.
|
||||
$i = array_search( $tag, $tag_stack, true );
|
||||
// We can only remove the tag from the stack if it is in the stack.
|
||||
if ( false !== $i ) {
|
||||
$tag_stack = array_slice( $tag_stack, 0, $i );
|
||||
}
|
||||
} else {
|
||||
// Opening tag, add it to the stack.
|
||||
$tag_stack[] = $tag;
|
||||
}
|
||||
|
||||
// If we're in a protected tag, the tag_stack contains at least one protected tag string.
|
||||
// The protected tag state can only change when we encounter a start or end tag.
|
||||
$in_protected_tag = array_intersect( $tag_stack, $protected_tags );
|
||||
|
||||
// Never inspect tags.
|
||||
$content_with_links .= $chunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $in_protected_tag ) {
|
||||
// Don't inspect a chunk inside an inspected tag.
|
||||
$content_with_links .= $chunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only reachable when there is no protected tag in the stack.
|
||||
$content_with_links .= \preg_replace_callback( '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', array( '\Activitypub\Hashtag', 'replace_with_links' ), $chunk );
|
||||
}
|
||||
|
||||
return $content_with_links;
|
||||
return enrich_content_data( $the_content, '/' . ACTIVITYPUB_HASHTAGS_REGEXP . '/i', array( self::class, 'replace_with_links' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback for preg_replace to build the term links
|
||||
* A callback for preg_replace to build the term links.
|
||||
*
|
||||
* @param array $result the preg_match results
|
||||
* @param array $result The preg_match results.
|
||||
* @return string the final string
|
||||
*/
|
||||
public static function replace_with_links( $result ) {
|
||||
$tag = $result[1];
|
||||
$tag = $result[1];
|
||||
$tag_object = \get_term_by( 'name', $tag, 'post_tag' );
|
||||
if ( ! $tag_object ) {
|
||||
$tag_object = \get_term_by( 'name', $tag, 'category' );
|
||||
}
|
||||
|
||||
if ( $tag_object ) {
|
||||
$link = \get_term_link( $tag_object, 'post_tag' );
|
||||
return \sprintf( '<a rel="tag" class="hashtag u-tag u-category" href="%s">#%s</a>', $link, $tag );
|
||||
return \sprintf( '<a rel="tag" class="hashtag u-tag u-category" href="%s">#%s</a>', esc_url( $link ), $tag );
|
||||
}
|
||||
|
||||
return '#' . $tag;
|
||||
|
@ -1,365 +0,0 @@
|
||||
<?php
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Webfinger;
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
use function Activitypub\get_plugin_version;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
use function Activitypub\get_webfinger_resource;
|
||||
|
||||
/**
|
||||
* ActivityPub Health_Check Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Health_Check {
|
||||
|
||||
/**
|
||||
* Initialize health checks
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'site_status_tests', array( self::class, 'add_tests' ) );
|
||||
\add_filter( 'debug_information', array( self::class, 'debug_information' ) );
|
||||
}
|
||||
|
||||
public static function add_tests( $tests ) {
|
||||
if ( ! is_user_type_disabled( 'user' ) ) {
|
||||
$tests['direct']['activitypub_test_author_url'] = array(
|
||||
'label' => \__( 'Author URL test', 'activitypub' ),
|
||||
'test' => array( self::class, 'test_author_url' ),
|
||||
);
|
||||
}
|
||||
|
||||
$tests['direct']['activitypub_test_webfinger'] = array(
|
||||
'label' => __( 'WebFinger Test', 'activitypub' ),
|
||||
'test' => array( self::class, 'test_webfinger' ),
|
||||
);
|
||||
|
||||
return $tests;
|
||||
}
|
||||
|
||||
/**
|
||||
* Author URL tests
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function test_author_url() {
|
||||
$result = array(
|
||||
'label' => \__( 'Author URL accessible', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Your author URL is accessible and supports the required "Accept" header.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_author_url',
|
||||
);
|
||||
|
||||
$check = self::is_author_url_accessible();
|
||||
|
||||
if ( true === $check ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['status'] = 'critical';
|
||||
$result['label'] = \__( 'Author URL is not accessible', 'activitypub' );
|
||||
$result['badge']['color'] = 'red';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
$check->get_error_message()
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* System Cron tests
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function test_system_cron() {
|
||||
$result = array(
|
||||
'label' => \__( 'System Task Scheduler configured', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\esc_html__( 'You seem to use the System Task Scheduler to process WP_Cron tasks.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_system_cron',
|
||||
);
|
||||
|
||||
if ( defined( 'DISABLE_WP_CRON' ) && DISABLE_WP_CRON ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['status'] = 'recommended';
|
||||
$result['label'] = \__( 'System Task Scheduler not configured', 'activitypub' );
|
||||
$result['badge']['color'] = 'orange';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Enhance your WordPress site’s performance and mitigate potential heavy loads caused by plugins like ActivityPub by setting up a system cron job to run WP Cron. This ensures scheduled tasks are executed consistently and reduces the reliance on website traffic for trigger events.', 'activitypub' )
|
||||
);
|
||||
$result['actions'] .= sprintf(
|
||||
'<p><a href="%s" target="_blank" rel="noopener">%s<span class="screen-reader-text"> %s</span><span aria-hidden="true" class="dashicons dashicons-external"></span></a></p>',
|
||||
__( 'https://developer.wordpress.org/plugins/cron/hooking-wp-cron-into-the-system-task-scheduler/', 'activitypub' ),
|
||||
__( 'Learn how to hook the WP-Cron into the System Task Scheduler.', 'activitypub' ),
|
||||
/* translators: Hidden accessibility text. */
|
||||
__( '(opens in a new tab)', 'activitypub' )
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* WebFinger tests
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function test_webfinger() {
|
||||
$result = array(
|
||||
'label' => \__( 'WebFinger endpoint', 'activitypub' ),
|
||||
'status' => 'good',
|
||||
'badge' => array(
|
||||
'label' => \__( 'ActivityPub', 'activitypub' ),
|
||||
'color' => 'green',
|
||||
),
|
||||
'description' => \sprintf(
|
||||
'<p>%s</p>',
|
||||
\__( 'Your WebFinger endpoint is accessible and returns the correct information.', 'activitypub' )
|
||||
),
|
||||
'actions' => '',
|
||||
'test' => 'test_webfinger',
|
||||
);
|
||||
|
||||
$check = self::is_webfinger_endpoint_accessible();
|
||||
|
||||
if ( true === $check ) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$result['status'] = 'critical';
|
||||
$result['label'] = \__( 'WebFinger endpoint is not accessible', 'activitypub' );
|
||||
$result['badge']['color'] = 'red';
|
||||
$result['description'] = \sprintf(
|
||||
'<p>%s</p>',
|
||||
$check->get_error_message()
|
||||
);
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `author_posts_url` is accessible and that request returns correct JSON
|
||||
*
|
||||
* @return boolean|WP_Error
|
||||
*/
|
||||
public static function is_author_url_accessible() {
|
||||
$user = \wp_get_current_user();
|
||||
$author_url = \get_author_posts_url( $user->ID );
|
||||
$reference_author_url = self::get_author_posts_url( $user->ID, $user->user_nicename );
|
||||
|
||||
// check for "author" in URL
|
||||
if ( $author_url !== $reference_author_url ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your author URL <code>%s</code> was replaced, this is often done by plugins.',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// try to access author URL
|
||||
$response = \wp_remote_get(
|
||||
$author_url,
|
||||
array(
|
||||
'headers' => array( 'Accept' => 'application/activity+json' ),
|
||||
'redirection' => 0,
|
||||
)
|
||||
);
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your author URL <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure. If the setup seems fine, maybe check if a plugin might restrict the access.',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$response_code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
// check for redirects
|
||||
if ( \in_array( $response_code, array( 301, 302, 307, 308 ), true ) ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your author URL <code>%s</code> is redirecting to another page, this is often done by SEO plugins like "Yoast SEO".',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// check if response is JSON
|
||||
$body = \wp_remote_retrieve_body( $response );
|
||||
|
||||
if ( ! \is_string( $body ) || ! \is_array( \json_decode( $body, true ) ) ) {
|
||||
return new WP_Error(
|
||||
'author_url_not_accessible',
|
||||
\sprintf(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your author URL <code>%s</code> does not return valid JSON for <code>application/activity+json</code>. Please check if your hosting supports alternate <code>Accept</code> headers.',
|
||||
'activitypub'
|
||||
),
|
||||
$author_url
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if WebFinger endpoint is accessible and profile request returns correct JSON
|
||||
*
|
||||
* @return boolean|WP_Error
|
||||
*/
|
||||
public static function is_webfinger_endpoint_accessible() {
|
||||
$user = Users::get_by_id( Users::APPLICATION_USER_ID );
|
||||
$resource = $user->get_webfinger();
|
||||
|
||||
$url = Webfinger::resolve( $resource );
|
||||
if ( \is_wp_error( $url ) ) {
|
||||
$allowed = array( 'code' => array() );
|
||||
$not_accessible = wp_kses(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your WebFinger endpoint <code>%s</code> is not accessible. Please check your WordPress setup or permalink structure.',
|
||||
'activitypub'
|
||||
),
|
||||
$allowed
|
||||
);
|
||||
$invalid_response = wp_kses(
|
||||
// translators: %s: Author URL
|
||||
\__(
|
||||
'Your WebFinger endpoint <code>%s</code> does not return valid JSON for <code>application/jrd+json</code>.',
|
||||
'activitypub'
|
||||
),
|
||||
$allowed
|
||||
);
|
||||
|
||||
$health_messages = array(
|
||||
'webfinger_url_not_accessible' => \sprintf(
|
||||
$not_accessible,
|
||||
$url->get_error_data()
|
||||
),
|
||||
'webfinger_url_invalid_response' => \sprintf(
|
||||
// translators: %s: Author URL
|
||||
$invalid_response,
|
||||
$url->get_error_data()
|
||||
),
|
||||
);
|
||||
$message = null;
|
||||
if ( isset( $health_messages[ $url->get_error_code() ] ) ) {
|
||||
$message = $health_messages[ $url->get_error_code() ];
|
||||
}
|
||||
return new WP_Error(
|
||||
$url->get_error_code(),
|
||||
$message,
|
||||
$url->get_error_data()
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve the URL to the author page for the user with the ID provided.
|
||||
*
|
||||
* @global WP_Rewrite $wp_rewrite WordPress rewrite component.
|
||||
*
|
||||
* @param int $author_id Author ID.
|
||||
* @param string $author_nicename Optional. The author's nicename (slug). Default empty.
|
||||
*
|
||||
* @return string The URL to the author's page.
|
||||
*/
|
||||
public static function get_author_posts_url( $author_id, $author_nicename = '' ) {
|
||||
global $wp_rewrite;
|
||||
$auth_id = (int) $author_id;
|
||||
$link = $wp_rewrite->get_author_permastruct();
|
||||
|
||||
if ( empty( $link ) ) {
|
||||
$file = home_url( '/' );
|
||||
$link = $file . '?author=' . $auth_id;
|
||||
} else {
|
||||
if ( '' === $author_nicename ) {
|
||||
$user = get_userdata( $author_id );
|
||||
if ( ! empty( $user->user_nicename ) ) {
|
||||
$author_nicename = $user->user_nicename;
|
||||
}
|
||||
}
|
||||
$link = str_replace( '%author%', $author_nicename, $link );
|
||||
$link = home_url( user_trailingslashit( $link ) );
|
||||
}
|
||||
|
||||
return $link;
|
||||
}
|
||||
|
||||
/**
|
||||
* Static function for generating site debug data when required.
|
||||
*
|
||||
* @param array $info The debug information to be added to the core information page.
|
||||
* @return array The filtered information
|
||||
*/
|
||||
public static function debug_information( $info ) {
|
||||
$info['activitypub'] = array(
|
||||
'label' => __( 'ActivityPub', 'activitypub' ),
|
||||
'fields' => array(
|
||||
'webfinger' => array(
|
||||
'label' => __( 'WebFinger Resource', 'activitypub' ),
|
||||
'value' => Webfinger::get_user_resource( wp_get_current_user()->ID ),
|
||||
'private' => true,
|
||||
),
|
||||
'author_url' => array(
|
||||
'label' => __( 'Author URL', 'activitypub' ),
|
||||
'value' => get_author_posts_url( wp_get_current_user()->ID ),
|
||||
'private' => true,
|
||||
),
|
||||
'plugin_version' => array(
|
||||
'label' => __( 'Plugin Version', 'activitypub' ),
|
||||
'value' => get_plugin_version(),
|
||||
'private' => true,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
return $info;
|
||||
}
|
||||
}
|
@ -1,10 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub HTTP Class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Collection\Users;
|
||||
|
||||
use function Activitypub\get_masked_wp_version;
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
/**
|
||||
* ActivityPub HTTP Class
|
||||
@ -15,87 +19,142 @@ class Http {
|
||||
/**
|
||||
* Send a POST Request with the needed HTTP Headers
|
||||
*
|
||||
* @param string $url The URL endpoint
|
||||
* @param string $body The Post Body
|
||||
* @param int $user_id The WordPress User-ID
|
||||
* @param string $url The URL endpoint.
|
||||
* @param string $body The Post Body.
|
||||
* @param int $user_id The WordPress User-ID.
|
||||
*
|
||||
* @return array|WP_Error The POST Response or an WP_ERROR
|
||||
* @return array|WP_Error The POST Response or an WP_Error.
|
||||
*/
|
||||
public static function post( $url, $body, $user_id ) {
|
||||
/**
|
||||
* Fires before an HTTP POST request is made.
|
||||
*
|
||||
* @param string $url The URL endpoint.
|
||||
* @param string $body The POST body.
|
||||
* @param int $user_id The WordPress User ID.
|
||||
*/
|
||||
\do_action( 'activitypub_pre_http_post', $url, $body, $user_id );
|
||||
|
||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||
$digest = Signature::generate_digest( $body );
|
||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||
$digest = Signature::generate_digest( $body );
|
||||
$signature = Signature::generate_signature( $user_id, 'post', $url, $date, $digest );
|
||||
|
||||
$wp_version = get_masked_wp_version();
|
||||
|
||||
/**
|
||||
* Filter the HTTP headers user agent.
|
||||
* Filters the HTTP headers user agent string.
|
||||
*
|
||||
* @param string $user_agent The user agent string.
|
||||
*/
|
||||
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
|
||||
$args = array(
|
||||
'timeout' => 100,
|
||||
$args = array(
|
||||
'timeout' => 100,
|
||||
'limit_response_size' => 1048576,
|
||||
'redirection' => 3,
|
||||
'user-agent' => "$user_agent; ActivityPub",
|
||||
'headers' => array(
|
||||
'Accept' => 'application/activity+json',
|
||||
'redirection' => 3,
|
||||
'user-agent' => "$user_agent; ActivityPub",
|
||||
'headers' => array(
|
||||
'Accept' => 'application/activity+json',
|
||||
'Content-Type' => 'application/activity+json',
|
||||
'Digest' => $digest,
|
||||
'Signature' => $signature,
|
||||
'Date' => $date,
|
||||
'Digest' => $digest,
|
||||
'Signature' => $signature,
|
||||
'Date' => $date,
|
||||
),
|
||||
'body' => $body,
|
||||
'body' => $body,
|
||||
);
|
||||
|
||||
$response = \wp_safe_remote_post( $url, $args );
|
||||
$code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( $code >= 400 ) {
|
||||
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
|
||||
$response = new WP_Error(
|
||||
$code,
|
||||
__( 'Failed HTTP Request', 'activitypub' ),
|
||||
array(
|
||||
'status' => $code,
|
||||
'response' => $response,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to save the response of the remote POST request.
|
||||
*
|
||||
* @param array|WP_Error $response The response of the remote POST request.
|
||||
* @param string $url The URL endpoint.
|
||||
* @param string $body The Post Body.
|
||||
* @param int $user_id The WordPress User-ID.
|
||||
*/
|
||||
\do_action( 'activitypub_safe_remote_post_response', $response, $url, $body, $user_id );
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a GET Request with the needed HTTP Headers
|
||||
* Send a GET Request with the needed HTTP Headers.
|
||||
*
|
||||
* @param string $url The URL endpoint
|
||||
* @param int $user_id The WordPress User-ID
|
||||
* @param string $url The URL endpoint.
|
||||
* @param bool|int $cached Optional. Whether the result should be cached, or its duration. Default false.
|
||||
*
|
||||
* @return array|WP_Error The GET Response or an WP_ERROR
|
||||
* @return array|WP_Error The GET Response or a WP_Error.
|
||||
*/
|
||||
public static function get( $url ) {
|
||||
public static function get( $url, $cached = false ) {
|
||||
/**
|
||||
* Fires before an HTTP GET request is made.
|
||||
*
|
||||
* @param string $url The URL endpoint.
|
||||
*/
|
||||
\do_action( 'activitypub_pre_http_get', $url );
|
||||
|
||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||
$signature = Signature::generate_signature( Users::APPLICATION_USER_ID, 'get', $url, $date );
|
||||
if ( $cached ) {
|
||||
$transient_key = self::generate_cache_key( $url );
|
||||
|
||||
$response = \get_transient( $transient_key );
|
||||
|
||||
if ( $response ) {
|
||||
/**
|
||||
* Action to save the response of the remote GET request.
|
||||
*
|
||||
* @param array|WP_Error $response The response of the remote GET request.
|
||||
* @param string $url The URL endpoint.
|
||||
*/
|
||||
\do_action( 'activitypub_safe_remote_get_response', $response, $url );
|
||||
|
||||
return $response;
|
||||
}
|
||||
}
|
||||
|
||||
$date = \gmdate( 'D, d M Y H:i:s T' );
|
||||
$signature = Signature::generate_signature( Actors::APPLICATION_USER_ID, 'get', $url, $date );
|
||||
|
||||
$wp_version = get_masked_wp_version();
|
||||
|
||||
/**
|
||||
* Filter the HTTP headers user agent.
|
||||
* Filters the HTTP headers user agent string.
|
||||
*
|
||||
* This filter allows developers to modify the user agent string that is
|
||||
* sent with HTTP requests.
|
||||
*
|
||||
* @param string $user_agent The user agent string.
|
||||
*/
|
||||
$user_agent = \apply_filters( 'http_headers_useragent', 'WordPress/' . $wp_version . '; ' . \get_bloginfo( 'url' ) );
|
||||
|
||||
/**
|
||||
* Filters the timeout duration for remote GET requests in ActivityPub.
|
||||
*
|
||||
* @param int $timeout The timeout value in seconds. Default 100 seconds.
|
||||
*/
|
||||
$timeout = \apply_filters( 'activitypub_remote_get_timeout', 100 );
|
||||
|
||||
$args = array(
|
||||
'timeout' => apply_filters( 'activitypub_remote_get_timeout', 100 ),
|
||||
'timeout' => $timeout,
|
||||
'limit_response_size' => 1048576,
|
||||
'redirection' => 3,
|
||||
'user-agent' => "$user_agent; ActivityPub",
|
||||
'headers' => array(
|
||||
'Accept' => 'application/activity+json',
|
||||
'redirection' => 3,
|
||||
'user-agent' => "$user_agent; ActivityPub",
|
||||
'headers' => array(
|
||||
'Accept' => 'application/activity+json',
|
||||
'Content-Type' => 'application/activity+json',
|
||||
'Signature' => $signature,
|
||||
'Date' => $date,
|
||||
'Signature' => $signature,
|
||||
'Date' => $date,
|
||||
),
|
||||
);
|
||||
|
||||
@ -106,8 +165,22 @@ class Http {
|
||||
$response = new WP_Error( $code, __( 'Failed HTTP Request', 'activitypub' ), array( 'status' => $code ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Action to save the response of the remote GET request.
|
||||
*
|
||||
* @param array|WP_Error $response The response of the remote GET request.
|
||||
* @param string $url The URL endpoint.
|
||||
*/
|
||||
\do_action( 'activitypub_safe_remote_get_response', $response, $url );
|
||||
|
||||
if ( $cached ) {
|
||||
$cache_duration = $cached;
|
||||
if ( ! is_int( $cache_duration ) ) {
|
||||
$cache_duration = HOUR_IN_SECONDS;
|
||||
}
|
||||
\set_transient( $transient_key, $response, $cache_duration );
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
@ -119,15 +192,114 @@ class Http {
|
||||
* @return bool True if the URL is a tombstone.
|
||||
*/
|
||||
public static function is_tombstone( $url ) {
|
||||
/**
|
||||
* Fires before checking if the URL is a tombstone.
|
||||
*
|
||||
* @param string $url The URL to check.
|
||||
*/
|
||||
\do_action( 'activitypub_pre_http_is_tombstone', $url );
|
||||
|
||||
$response = \wp_safe_remote_get( $url );
|
||||
$response = \wp_safe_remote_get( $url, array( 'headers' => array( 'Accept' => 'application/activity+json' ) ) );
|
||||
$code = \wp_remote_retrieve_response_code( $response );
|
||||
|
||||
if ( in_array( (int) $code, array( 404, 410 ), true ) ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$data = \wp_remote_retrieve_body( $response );
|
||||
$data = \json_decode( $data, true );
|
||||
if ( $data && isset( $data['type'] ) && 'Tombstone' === $data['type'] ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cache key for the URL.
|
||||
*
|
||||
* @param string $url The URL to generate the cache key for.
|
||||
*
|
||||
* @return string The cache key.
|
||||
*/
|
||||
public static function generate_cache_key( $url ) {
|
||||
return 'activitypub_http_' . \md5( $url );
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests the Data from the Object-URL or Object-Array.
|
||||
*
|
||||
* @param array|string $url_or_object The Object or the Object URL.
|
||||
* @param bool $cached Optional. Whether the result should be cached. Default true.
|
||||
*
|
||||
* @return array|WP_Error The Object data as array or WP_Error on failure.
|
||||
*/
|
||||
public static function get_remote_object( $url_or_object, $cached = true ) {
|
||||
$url = object_to_uri( $url_or_object );
|
||||
|
||||
if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $url ) ) {
|
||||
$url = Webfinger::resolve( $url );
|
||||
}
|
||||
|
||||
if ( ! $url ) {
|
||||
return new WP_Error(
|
||||
'activitypub_no_valid_actor_identifier',
|
||||
\__( 'The "actor" identifier is not valid', 'activitypub' ),
|
||||
array(
|
||||
'status' => 404,
|
||||
'object' => $url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( is_wp_error( $url ) ) {
|
||||
return $url;
|
||||
}
|
||||
|
||||
$transient_key = self::generate_cache_key( $url );
|
||||
|
||||
// Only check the cache if needed.
|
||||
if ( $cached ) {
|
||||
$data = \get_transient( $transient_key );
|
||||
|
||||
if ( $data ) {
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! \wp_http_validate_url( $url ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_no_valid_object_url',
|
||||
\__( 'The "object" is/has no valid URL', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'object' => $url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
$response = self::get( $url );
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$data = \wp_remote_retrieve_body( $response );
|
||||
$data = \json_decode( $data, true );
|
||||
|
||||
if ( ! $data ) {
|
||||
return new WP_Error(
|
||||
'activitypub_invalid_json',
|
||||
\__( 'No valid JSON data', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'object' => $url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
\set_transient( $transient_key, $data, WEEK_IN_SECONDS );
|
||||
|
||||
return $data;
|
||||
}
|
||||
}
|
||||
|
132
wp-content/plugins/activitypub/includes/class-link.php
Normal file
132
wp-content/plugins/activitypub/includes/class-link.php
Normal file
@ -0,0 +1,132 @@
|
||||
<?php
|
||||
/**
|
||||
* Link class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* ActivityPub Summery Links Class.
|
||||
*/
|
||||
class Link {
|
||||
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'activitypub_extra_field_content', array( self::class, 'the_content' ) );
|
||||
\add_filter( 'activitypub_activity_object_array', array( self::class, 'filter_activity_object' ), 99 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter only the activity object and replace the summary with URLs.
|
||||
*
|
||||
* @param array $activity The activity object array.
|
||||
*
|
||||
* @return array Rhe activity object array.
|
||||
*/
|
||||
public static function filter_activity_object( $activity ) {
|
||||
/* phpcs:ignore Squiz.PHP.CommentedOutCode.Found
|
||||
Only changed it for Person and Group as long is not merged: https://github.com/mastodon/mastodon/pull/28629
|
||||
*/
|
||||
if ( ! empty( $activity['summary'] ) && in_array( $activity['type'], array( 'Person', 'Group' ), true ) ) {
|
||||
$activity['summary'] = self::the_content( $activity['summary'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $activity['content'] ) ) {
|
||||
$activity['content'] = self::the_content( $activity['content'] );
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to replace the URLS in the content with links
|
||||
*
|
||||
* @param string $the_content The post content.
|
||||
*
|
||||
* @return string the filtered post content.
|
||||
*/
|
||||
public static function the_content( $the_content ) {
|
||||
return enrich_content_data( $the_content, '/' . ACTIVITYPUB_URL_REGEXP . '/i', array( self::class, 'replace_with_links' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback for preg_replace to build the links.
|
||||
*
|
||||
* Link shortening https://docs.joinmastodon.org/api/guidelines/#links
|
||||
*
|
||||
* @param array $result The preg_match results.
|
||||
*
|
||||
* @return string The final string.
|
||||
*/
|
||||
public static function replace_with_links( $result ) {
|
||||
if ( 'www.' === substr( $result[0], 0, 4 ) ) {
|
||||
$result[0] = 'https://' . $result[0];
|
||||
}
|
||||
$parsed_url = \wp_parse_url( html_entity_decode( $result[0] ) );
|
||||
if ( ! $parsed_url || empty( $parsed_url['host'] ) ) {
|
||||
return $result[0];
|
||||
}
|
||||
|
||||
if ( empty( $parsed_url['scheme'] ) ) {
|
||||
$invisible_prefix = 'https://';
|
||||
} else {
|
||||
$invisible_prefix = $parsed_url['scheme'] . '://';
|
||||
}
|
||||
if ( ! empty( $parsed_url['user'] ) ) {
|
||||
$invisible_prefix .= $parsed_url['user'];
|
||||
}
|
||||
if ( ! empty( $parsed_url['pass'] ) ) {
|
||||
$invisible_prefix .= ':' . $parsed_url['pass'];
|
||||
}
|
||||
if ( ! empty( $parsed_url['user'] ) ) {
|
||||
$invisible_prefix .= '@';
|
||||
}
|
||||
|
||||
$text_url = $parsed_url['host'];
|
||||
if ( 'www.' === substr( $text_url, 0, 4 ) ) {
|
||||
$text_url = substr( $text_url, 4 );
|
||||
$invisible_prefix .= 'www.';
|
||||
}
|
||||
if ( ! empty( $parsed_url['port'] ) ) {
|
||||
$text_url .= ':' . $parsed_url['port'];
|
||||
}
|
||||
if ( ! empty( $parsed_url['path'] ) ) {
|
||||
$text_url .= $parsed_url['path'];
|
||||
}
|
||||
if ( ! empty( $parsed_url['query'] ) ) {
|
||||
$text_url .= '?' . $parsed_url['query'];
|
||||
}
|
||||
if ( ! empty( $parsed_url['fragment'] ) ) {
|
||||
$text_url .= '#' . $parsed_url['fragment'];
|
||||
}
|
||||
|
||||
$display = \substr( $text_url, 0, 30 );
|
||||
$invisible_suffix = \substr( $text_url, 30 );
|
||||
|
||||
$display_class = '';
|
||||
if ( $invisible_suffix ) {
|
||||
$display_class .= 'ellipsis';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the rel attribute for ActivityPub links.
|
||||
*
|
||||
* @param string $rel The rel attribute string. Default 'nofollow noopener noreferrer'.
|
||||
*/
|
||||
$rel = apply_filters( 'activitypub_link_rel', 'nofollow noopener noreferrer' );
|
||||
|
||||
return \sprintf(
|
||||
'<a href="%s" target="_blank" rel="%s" translate="no"><span class="invisible">%s</span><span class="%s">%s</span><span class="invisible">%s</span></a>',
|
||||
esc_url( $result[0] ),
|
||||
$rel,
|
||||
esc_html( $invisible_prefix ),
|
||||
$display_class,
|
||||
esc_html( $display ),
|
||||
esc_html( $invisible_suffix )
|
||||
);
|
||||
}
|
||||
}
|
337
wp-content/plugins/activitypub/includes/class-mailer.php
Normal file
337
wp-content/plugins/activitypub/includes/class-mailer.php
Normal file
@ -0,0 +1,337 @@
|
||||
<?php
|
||||
/**
|
||||
* Mailer Class.
|
||||
*
|
||||
* @package ActivityPub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
/**
|
||||
* Mailer Class.
|
||||
*/
|
||||
class Mailer {
|
||||
/**
|
||||
* Initialize the Mailer.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'comment_notification_subject', array( self::class, 'comment_notification_subject' ), 10, 2 );
|
||||
\add_filter( 'comment_notification_text', array( self::class, 'comment_notification_text' ), 10, 2 );
|
||||
|
||||
\add_action( 'activitypub_inbox_follow', array( self::class, 'new_follower' ), 10, 2 );
|
||||
\add_action( 'activitypub_inbox_create', array( self::class, 'direct_message' ), 10, 2 );
|
||||
\add_action( 'activitypub_inbox_create', array( self::class, 'mention' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the subject line for Like and Announce notifications.
|
||||
*
|
||||
* @param string $subject The default subject line.
|
||||
* @param int|string $comment_id The comment ID.
|
||||
*
|
||||
* @return string The filtered subject line.
|
||||
*/
|
||||
public static function comment_notification_subject( $subject, $comment_id ) {
|
||||
$comment = \get_comment( $comment_id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return $subject;
|
||||
}
|
||||
|
||||
$type = \get_comment_meta( $comment->comment_ID, 'protocol', true );
|
||||
|
||||
if ( 'activitypub' !== $type ) {
|
||||
return $subject;
|
||||
}
|
||||
|
||||
$singular = Comment::get_comment_type_attr( $comment->comment_type, 'singular' );
|
||||
|
||||
if ( ! $singular ) {
|
||||
return $subject;
|
||||
}
|
||||
|
||||
$post = \get_post( $comment->comment_post_ID );
|
||||
|
||||
/* translators: 1: Blog name, 2: Like or Repost, 3: Post title */
|
||||
return \sprintf( \esc_html__( '[%1$s] %2$s: %3$s', 'activitypub' ), \esc_html( get_option( 'blogname' ) ), \esc_html( $singular ), \esc_html( $post->post_title ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the notification text for Like and Announce notifications.
|
||||
*
|
||||
* @param string $message The default notification text.
|
||||
* @param int|string $comment_id The comment ID.
|
||||
*
|
||||
* @return string The filtered notification text.
|
||||
*/
|
||||
public static function comment_notification_text( $message, $comment_id ) {
|
||||
$comment = \get_comment( $comment_id );
|
||||
|
||||
if ( ! $comment ) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
$type = \get_comment_meta( $comment->comment_ID, 'protocol', true );
|
||||
|
||||
if ( 'activitypub' !== $type ) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
$comment_type = Comment::get_comment_type( $comment->comment_type );
|
||||
|
||||
if ( ! $comment_type ) {
|
||||
return $message;
|
||||
}
|
||||
|
||||
$post = \get_post( $comment->comment_post_ID );
|
||||
$comment_author_domain = \gethostbyaddr( $comment->comment_author_IP );
|
||||
|
||||
/* translators: 1: Comment type, 2: Post title */
|
||||
$notify_message = \sprintf( html_entity_decode( esc_html__( 'New %1$s on your post “%2$s”.', 'activitypub' ) ), \esc_html( $comment_type['singular'] ), \esc_html( $post->post_title ) ) . "\r\n\r\n";
|
||||
/* translators: 1: Website name, 2: Website IP address, 3: Website hostname. */
|
||||
$notify_message .= \sprintf( \esc_html__( 'From: %1$s (IP address: %2$s, %3$s)', 'activitypub' ), \esc_html( $comment->comment_author ), \esc_html( $comment->comment_author_IP ), \esc_html( $comment_author_domain ) ) . "\r\n";
|
||||
/* translators: Reaction author URL. */
|
||||
$notify_message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $comment->comment_author_url ) ) . "\r\n\r\n";
|
||||
/* translators: Comment type label */
|
||||
$notify_message .= \sprintf( \esc_html__( 'You can see all %s on this post here:', 'activitypub' ), \esc_html( $comment_type['label'] ) ) . "\r\n";
|
||||
$notify_message .= \get_permalink( $comment->comment_post_ID ) . '#' . \esc_attr( $comment_type['type'] ) . "\r\n\r\n";
|
||||
|
||||
return $notify_message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a notification email for every new follower.
|
||||
*
|
||||
* @param array $activity The activity object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
*/
|
||||
public static function new_follower( $activity, $user_id ) {
|
||||
if ( $user_id > Actors::BLOG_USER_ID ) {
|
||||
if ( ! \get_user_option( 'activitypub_mailer_new_follower', $user_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_userdata( $user_id )->user_email;
|
||||
$admin_url = '/users.php?page=activitypub-followers-list';
|
||||
} else {
|
||||
if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_follower', '1' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_option( 'admin_email' );
|
||||
$admin_url = '/options-general.php?page=activitypub&tab=followers';
|
||||
}
|
||||
|
||||
$actor = get_remote_metadata_by_actor( $activity['actor'] );
|
||||
if ( ! $actor || \is_wp_error( $actor ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( empty( $actor['webfinger'] ) ) {
|
||||
$actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST );
|
||||
}
|
||||
|
||||
$template_args = array_merge(
|
||||
$actor,
|
||||
array(
|
||||
'admin_url' => $admin_url,
|
||||
'user_id' => $user_id,
|
||||
'stats' => array(
|
||||
'outbox' => null,
|
||||
'followers' => null,
|
||||
'following' => null,
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $template_args['stats'] as $field => $value ) {
|
||||
if ( empty( $actor[ $field ] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$result = Http::get( $actor[ $field ], true );
|
||||
if ( 200 === \wp_remote_retrieve_response_code( $result ) ) {
|
||||
$body = \json_decode( \wp_remote_retrieve_body( $result ), true );
|
||||
if ( isset( $body['totalItems'] ) ) {
|
||||
$template_args['stats'][ $field ] = $body['totalItems'];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* translators: 1: Blog name, 2: Follower name */
|
||||
$subject = \sprintf( \__( '[%1$s] New Follower: %2$s', 'activitypub' ), \get_option( 'blogname' ), $actor['name'] );
|
||||
|
||||
\ob_start();
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-follower.php', false, $template_args );
|
||||
$html_message = \ob_get_clean();
|
||||
|
||||
$alt_function = function ( $mailer ) use ( $actor, $admin_url ) {
|
||||
/* translators: 1: Follower name */
|
||||
$message = \sprintf( \__( 'New Follower: %1$s.', 'activitypub' ), $actor['name'] ) . "\r\n\r\n";
|
||||
/* translators: Follower URL */
|
||||
$message .= \sprintf( \__( 'URL: %s', 'activitypub' ), \esc_url( $actor['url'] ) ) . "\r\n\r\n";
|
||||
$message .= \__( 'You can see all followers here:', 'activitypub' ) . "\r\n";
|
||||
$message .= \esc_url( \admin_url( $admin_url ) ) . "\r\n\r\n";
|
||||
$mailer->{'AltBody'} = $message;
|
||||
};
|
||||
\add_action( 'phpmailer_init', $alt_function );
|
||||
|
||||
\wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
|
||||
|
||||
\remove_action( 'phpmailer_init', $alt_function );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a direct message.
|
||||
*
|
||||
* @param array $activity The activity object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
*/
|
||||
public static function direct_message( $activity, $user_id ) {
|
||||
if (
|
||||
is_activity_public( $activity ) ||
|
||||
// Only accept messages that have the user in the "to" field.
|
||||
empty( $activity['to'] ) ||
|
||||
! in_array( Actors::get_by_id( $user_id )->get_id(), (array) $activity['to'], true )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $user_id > Actors::BLOG_USER_ID ) {
|
||||
if ( ! \get_user_option( 'activitypub_mailer_new_dm', $user_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_userdata( $user_id )->user_email;
|
||||
} else {
|
||||
if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_dm', '1' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_option( 'admin_email' );
|
||||
}
|
||||
|
||||
$actor = get_remote_metadata_by_actor( $activity['actor'] );
|
||||
|
||||
if ( ! $actor || \is_wp_error( $actor ) || empty( $activity['object']['content'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( empty( $actor['webfinger'] ) ) {
|
||||
$actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST );
|
||||
}
|
||||
|
||||
$template_args = array(
|
||||
'activity' => $activity,
|
||||
'actor' => $actor,
|
||||
'user_id' => $user_id,
|
||||
);
|
||||
|
||||
/* translators: 1: Blog name, 2 Actor name */
|
||||
$subject = \sprintf( \esc_html__( '[%1$s] Direct Message from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
|
||||
|
||||
\ob_start();
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-dm.php', false, $template_args );
|
||||
$html_message = \ob_get_clean();
|
||||
|
||||
$alt_function = function ( $mailer ) use ( $actor, $activity ) {
|
||||
$content = \html_entity_decode(
|
||||
\wp_strip_all_tags(
|
||||
str_replace( '</p>', PHP_EOL . PHP_EOL, $activity['object']['content'] )
|
||||
),
|
||||
ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
|
||||
);
|
||||
|
||||
/* translators: Actor name */
|
||||
$message = \sprintf( \esc_html__( 'New Direct Message: %s', 'activitypub' ), $content ) . "\r\n\r\n";
|
||||
/* translators: Actor name */
|
||||
$message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
|
||||
/* translators: Message URL */
|
||||
$message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $activity['object']['id'] ) ) . "\r\n\r\n";
|
||||
|
||||
$mailer->{'AltBody'} = $message;
|
||||
};
|
||||
\add_action( 'phpmailer_init', $alt_function );
|
||||
|
||||
\wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
|
||||
|
||||
\remove_action( 'phpmailer_init', $alt_function );
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a mention notification.
|
||||
*
|
||||
* @param array $activity The activity object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
*/
|
||||
public static function mention( $activity, $user_id ) {
|
||||
if (
|
||||
// Only accept messages that have the user in the "cc" field.
|
||||
empty( $activity['cc'] ) ||
|
||||
! in_array( Actors::get_by_id( $user_id )->get_id(), (array) $activity['cc'], true )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $user_id > Actors::BLOG_USER_ID ) {
|
||||
if ( ! \get_user_option( 'activitypub_mailer_new_mention', $user_id ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_userdata( $user_id )->user_email;
|
||||
} else {
|
||||
if ( '1' !== \get_option( 'activitypub_blog_user_mailer_new_mention', '1' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$email = \get_option( 'admin_email' );
|
||||
}
|
||||
|
||||
$actor = get_remote_metadata_by_actor( $activity['actor'] );
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( empty( $actor['webfinger'] ) ) {
|
||||
$actor['webfinger'] = '@' . ( $actor['preferredUsername'] ?? $actor['name'] ) . '@' . \wp_parse_url( $actor['url'], PHP_URL_HOST );
|
||||
}
|
||||
|
||||
$template_args = array(
|
||||
'activity' => $activity,
|
||||
'actor' => $actor,
|
||||
'user_id' => $user_id,
|
||||
);
|
||||
|
||||
/* translators: 1: Blog name, 2 Actor name */
|
||||
$subject = \sprintf( \esc_html__( '[%1$s] Mention from: %2$s', 'activitypub' ), \esc_html( \get_option( 'blogname' ) ), \esc_html( $actor['name'] ) );
|
||||
|
||||
\ob_start();
|
||||
\load_template( ACTIVITYPUB_PLUGIN_DIR . 'templates/emails/new-mention.php', false, $template_args );
|
||||
$html_message = \ob_get_clean();
|
||||
|
||||
$alt_function = function ( $mailer ) use ( $actor, $activity ) {
|
||||
$content = \html_entity_decode(
|
||||
\wp_strip_all_tags(
|
||||
str_replace( '</p>', PHP_EOL . PHP_EOL, $activity['object']['content'] )
|
||||
),
|
||||
ENT_QUOTES | ENT_SUBSTITUTE | ENT_HTML401
|
||||
);
|
||||
|
||||
/* translators: Message content */
|
||||
$message = \sprintf( \esc_html__( 'New Mention: %s', 'activitypub' ), $content ) . "\r\n\r\n";
|
||||
/* translators: Actor name */
|
||||
$message .= \sprintf( \esc_html__( 'From: %s', 'activitypub' ), \esc_html( $actor['name'] ) ) . "\r\n";
|
||||
/* translators: Message URL */
|
||||
$message .= \sprintf( \esc_html__( 'URL: %s', 'activitypub' ), \esc_url( $activity['object']['id'] ) ) . "\r\n\r\n";
|
||||
|
||||
$mailer->{'AltBody'} = $message;
|
||||
};
|
||||
\add_action( 'phpmailer_init', $alt_function );
|
||||
|
||||
\wp_mail( $email, $subject, $html_message, array( 'Content-type: text/html' ) );
|
||||
|
||||
\remove_action( 'phpmailer_init', $alt_function );
|
||||
}
|
||||
}
|
@ -1,99 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* Mention class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Webfinger;
|
||||
|
||||
/**
|
||||
* ActivityPub Mention Class
|
||||
* ActivityPub Mention Class.
|
||||
*
|
||||
* @author Alex Kirk
|
||||
*/
|
||||
class Mention {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'the_content', array( self::class, 'the_content' ), 99, 1 );
|
||||
\add_filter( 'comment_text', array( self::class, 'the_content' ), 10, 1 );
|
||||
\add_filter( 'activitypub_extra_field_content', array( self::class, 'the_content' ), 10, 1 );
|
||||
\add_filter( 'activitypub_extract_mentions', array( self::class, 'extract_mentions' ), 99, 2 );
|
||||
\add_filter( 'activitypub_activity_object_array', array( self::class, 'filter_activity_object' ), 99 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to replace the mentions in the content with links
|
||||
* Filter only the activity object and replace summery it with URLs
|
||||
* add tag to user.
|
||||
*
|
||||
* @param string $the_content the post-content
|
||||
* @param array $object_array Array of activity.
|
||||
*
|
||||
* @return string the filtered post-content
|
||||
* @return array The activity object array.
|
||||
*/
|
||||
public static function filter_activity_object( $object_array ) {
|
||||
if ( ! empty( $object_array['summary'] ) ) {
|
||||
$object_array['summary'] = self::the_content( $object_array['summary'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $object_array['content'] ) ) {
|
||||
$object_array['content'] = self::the_content( $object_array['content'] );
|
||||
}
|
||||
|
||||
return $object_array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to replace the mentions in the content with links.
|
||||
*
|
||||
* @param string $the_content The post content.
|
||||
*
|
||||
* @return string The filtered post-content.
|
||||
*/
|
||||
public static function the_content( $the_content ) {
|
||||
// small protection against execution timeouts: limit to 1 MB
|
||||
if ( mb_strlen( $the_content ) > MB_IN_BYTES ) {
|
||||
return $the_content;
|
||||
}
|
||||
$tag_stack = array();
|
||||
$protected_tags = array(
|
||||
'pre',
|
||||
'code',
|
||||
'textarea',
|
||||
'style',
|
||||
'a',
|
||||
);
|
||||
$content_with_links = '';
|
||||
$in_protected_tag = false;
|
||||
foreach ( wp_html_split( $the_content ) as $chunk ) {
|
||||
if ( preg_match( '#^<!--[\s\S]*-->$#i', $chunk, $m ) ) {
|
||||
$content_with_links .= $chunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( preg_match( '#^<(/)?([a-z-]+)\b[^>]*>$#i', $chunk, $m ) ) {
|
||||
$tag = strtolower( $m[2] );
|
||||
if ( '/' === $m[1] ) {
|
||||
// Closing tag.
|
||||
$i = array_search( $tag, $tag_stack );
|
||||
// We can only remove the tag from the stack if it is in the stack.
|
||||
if ( false !== $i ) {
|
||||
$tag_stack = array_slice( $tag_stack, 0, $i );
|
||||
}
|
||||
} else {
|
||||
// Opening tag, add it to the stack.
|
||||
$tag_stack[] = $tag;
|
||||
}
|
||||
|
||||
// If we're in a protected tag, the tag_stack contains at least one protected tag string.
|
||||
// The protected tag state can only change when we encounter a start or end tag.
|
||||
$in_protected_tag = array_intersect( $tag_stack, $protected_tags );
|
||||
|
||||
// Never inspect tags.
|
||||
$content_with_links .= $chunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $in_protected_tag ) {
|
||||
// Don't inspect a chunk inside an inspected tag.
|
||||
$content_with_links .= $chunk;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only reachable when there is no protected tag in the stack.
|
||||
$content_with_links .= \preg_replace_callback( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( self::class, 'replace_with_links' ), $chunk );
|
||||
}
|
||||
|
||||
return $content_with_links;
|
||||
return enrich_content_data( $the_content, '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/', array( self::class, 'replace_with_links' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* A callback for preg_replace to build the user links
|
||||
* A callback for preg_replace to build the user links.
|
||||
*
|
||||
* @param array $result the preg_match results
|
||||
* @param array $result The preg_match results.
|
||||
*
|
||||
* @return string the final string
|
||||
* @return string The final string.
|
||||
*/
|
||||
public static function replace_with_links( $result ) {
|
||||
$metadata = get_remote_metadata_by_actor( $result[0] );
|
||||
|
||||
if ( ! empty( $metadata ) && ! is_wp_error( $metadata ) && ! empty( $metadata['url'] ) ) {
|
||||
if (
|
||||
! empty( $metadata ) &&
|
||||
! is_wp_error( $metadata ) &&
|
||||
( ! empty( $metadata['id'] ) || ! empty( $metadata['url'] ) )
|
||||
) {
|
||||
$username = ltrim( $result[0], '@' );
|
||||
if ( ! empty( $metadata['name'] ) ) {
|
||||
$username = $metadata['name'];
|
||||
@ -102,24 +80,20 @@ class Mention {
|
||||
$username = $metadata['preferredUsername'];
|
||||
}
|
||||
|
||||
$url = isset( $metadata['url'] ) ? $metadata['url'] : $metadata['id'];
|
||||
$url = isset( $metadata['url'] ) ? object_to_uri( $metadata['url'] ) : object_to_uri( $metadata['id'] );
|
||||
|
||||
if ( \is_array( $url ) ) {
|
||||
$url = $url[0];
|
||||
}
|
||||
|
||||
return \sprintf( '<a rel="mention" class="u-url mention" href="%s">@<span>%s</span></a>', esc_url( $url ), esc_html( $username ) );
|
||||
return \sprintf( '<a rel="mention" class="u-url mention" href="%1$s">@%2$s</a>', esc_url( $url ), esc_html( $username ) );
|
||||
}
|
||||
|
||||
return $result[0];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Inboxes for the mentioned Actors
|
||||
* Get the Inboxes for the mentioned Actors.
|
||||
*
|
||||
* @param array $mentioned The list of Actors that were mentioned
|
||||
* @param array $mentioned The list of Actors that were mentioned.
|
||||
*
|
||||
* @return array The list of Inboxes
|
||||
* @return array The list of Inboxes.
|
||||
*/
|
||||
public static function get_inboxes( $mentioned ) {
|
||||
$inboxes = array();
|
||||
@ -136,11 +110,11 @@ class Mention {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the inbox from the Remote-Profile of a mentioned Actor
|
||||
* Get the inbox from the Remote-Profile of a mentioned Actor.
|
||||
*
|
||||
* @param string $actor The Actor-URL
|
||||
* @param string $actor The Actor URL.
|
||||
*
|
||||
* @return string The Inbox-URL
|
||||
* @return string|WP_Error The Inbox-URL or WP_Error if not found.
|
||||
*/
|
||||
public static function get_inbox_by_mentioned_actor( $actor ) {
|
||||
$metadata = get_remote_metadata_by_actor( $actor );
|
||||
@ -149,7 +123,7 @@ class Mention {
|
||||
return $metadata;
|
||||
}
|
||||
|
||||
if ( isset( $metadata['endpoints'] ) && isset( $metadata['endpoints']['sharedInbox'] ) ) {
|
||||
if ( isset( $metadata['endpoints']['sharedInbox'] ) ) {
|
||||
return $metadata['endpoints']['sharedInbox'];
|
||||
}
|
||||
|
||||
@ -163,10 +137,10 @@ class Mention {
|
||||
/**
|
||||
* Extract the mentions from the post_content.
|
||||
*
|
||||
* @param array $mentions The already found mentions.
|
||||
* @param array $mentions The already found mentions.
|
||||
* @param string $post_content The post content.
|
||||
*
|
||||
* @return mixed The discovered mentions.
|
||||
* @return array The discovered mentions.
|
||||
*/
|
||||
public static function extract_mentions( $mentions, $post_content ) {
|
||||
\preg_match_all( '/@' . ACTIVITYPUB_USERNAME_REGEXP . '/i', $post_content, $matches );
|
||||
@ -176,6 +150,6 @@ class Mention {
|
||||
$mentions[ $match ] = $link;
|
||||
}
|
||||
}
|
||||
return $mentions;
|
||||
return \array_unique( $mentions );
|
||||
}
|
||||
}
|
||||
|
@ -1,9 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* Migration class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Activitypub;
|
||||
use Activitypub\Model\Blog_User;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Extra_Fields;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Outbox;
|
||||
use Activitypub\Transformer\Factory;
|
||||
|
||||
/**
|
||||
* ActivityPub Migration Class
|
||||
@ -12,10 +20,12 @@ use Activitypub\Collection\Followers;
|
||||
*/
|
||||
class Migration {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_migrate', array( self::class, 'async_migration' ) );
|
||||
\add_action( 'activitypub_upgrade', array( self::class, 'async_upgrade' ), 10, 99 );
|
||||
\add_action( 'activitypub_update_comment_counts', array( self::class, 'update_comment_counts' ), 10, 2 );
|
||||
|
||||
self::maybe_migrate();
|
||||
}
|
||||
@ -26,10 +36,14 @@ class Migration {
|
||||
* This is the version that the database structure will be updated to.
|
||||
* It is the same as the plugin version.
|
||||
*
|
||||
* @deprecated 4.2.0 Use constant ACTIVITYPUB_PLUGIN_VERSION directly.
|
||||
*
|
||||
* @return string The target version.
|
||||
*/
|
||||
public static function get_target_version() {
|
||||
return get_plugin_version();
|
||||
_deprecated_function( __FUNCTION__, '4.2.0', 'ACTIVITYPUB_PLUGIN_VERSION' );
|
||||
|
||||
return ACTIVITYPUB_PLUGIN_VERSION;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -44,16 +58,23 @@ class Migration {
|
||||
/**
|
||||
* Locks the database migration process to prevent simultaneous migrations.
|
||||
*
|
||||
* @return void
|
||||
* @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
|
||||
*/
|
||||
public static function lock() {
|
||||
\update_option( 'activitypub_migration_lock', \time() );
|
||||
global $wpdb;
|
||||
|
||||
// Try to lock.
|
||||
$lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_migration_lock', \time() ) ); // phpcs:ignore WordPress.DB
|
||||
|
||||
if ( ! $lock_result ) {
|
||||
$lock_result = \get_option( 'activitypub_migration_lock' );
|
||||
}
|
||||
|
||||
return $lock_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks the database migration process.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function unlock() {
|
||||
\delete_option( 'activitypub_migration_lock' );
|
||||
@ -87,9 +108,9 @@ class Migration {
|
||||
* @return bool True if the database structure is up to date, false otherwise.
|
||||
*/
|
||||
public static function is_latest_version() {
|
||||
return (bool) version_compare(
|
||||
return (bool) \version_compare(
|
||||
self::get_version(),
|
||||
self::get_target_version(),
|
||||
ACTIVITYPUB_PLUGIN_VERSION,
|
||||
'=='
|
||||
);
|
||||
}
|
||||
@ -110,30 +131,91 @@ class Migration {
|
||||
|
||||
$version_from_db = self::get_version();
|
||||
|
||||
// check for inital migration
|
||||
// Check for initial migration.
|
||||
if ( ! $version_from_db ) {
|
||||
self::add_default_settings();
|
||||
$version_from_db = self::get_target_version();
|
||||
$version_from_db = ACTIVITYPUB_PLUGIN_VERSION;
|
||||
}
|
||||
|
||||
// schedule the async migration
|
||||
// Schedule the async migration.
|
||||
if ( ! \wp_next_scheduled( 'activitypub_migrate', $version_from_db ) ) {
|
||||
\wp_schedule_single_event( \time(), 'activitypub_migrate', $version_from_db );
|
||||
\wp_schedule_single_event( \time(), 'activitypub_migrate', array( $version_from_db ) );
|
||||
}
|
||||
if ( version_compare( $version_from_db, '0.17.0', '<' ) ) {
|
||||
if ( \version_compare( $version_from_db, '0.17.0', '<' ) ) {
|
||||
self::migrate_from_0_16();
|
||||
}
|
||||
if ( version_compare( $version_from_db, '1.3.0', '<' ) ) {
|
||||
if ( \version_compare( $version_from_db, '1.3.0', '<' ) ) {
|
||||
self::migrate_from_1_2_0();
|
||||
}
|
||||
if ( version_compare( $version_from_db, '2.1.0', '<' ) ) {
|
||||
if ( \version_compare( $version_from_db, '2.1.0', '<' ) ) {
|
||||
self::migrate_from_2_0_0();
|
||||
}
|
||||
if ( version_compare( $version_from_db, '2.3.0', '<' ) ) {
|
||||
if ( \version_compare( $version_from_db, '2.3.0', '<' ) ) {
|
||||
self::migrate_from_2_2_0();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '3.0.0', '<' ) ) {
|
||||
self::migrate_from_2_6_0();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.0.0', '<' ) ) {
|
||||
self::migrate_to_4_0_0();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.1.0', '<' ) ) {
|
||||
self::migrate_to_4_1_0();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.5.0', '<' ) ) {
|
||||
\wp_schedule_single_event( \time() + MINUTE_IN_SECONDS, 'activitypub_update_comment_counts' );
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.7.1', '<' ) ) {
|
||||
self::migrate_to_4_7_1();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.7.2', '<' ) ) {
|
||||
self::migrate_to_4_7_2();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '4.7.3', '<' ) ) {
|
||||
add_action( 'init', 'flush_rewrite_rules', 20 );
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '5.0.0', '<' ) ) {
|
||||
Scheduler::register_schedules();
|
||||
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'create_post_outbox_items' ) );
|
||||
\wp_schedule_single_event( \time() + 15, 'activitypub_upgrade', array( 'create_comment_outbox_items' ) );
|
||||
add_action( 'init', 'flush_rewrite_rules', 20 );
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '5.2.0', '<' ) ) {
|
||||
Scheduler::register_schedules();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '5.4.0', '<' ) ) {
|
||||
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_actor_json_slashing' ) );
|
||||
\wp_schedule_single_event( \time(), 'activitypub_upgrade', array( 'update_comment_author_emails' ) );
|
||||
\add_action( 'init', 'flush_rewrite_rules', 20 );
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '5.7.0', '<' ) ) {
|
||||
self::delete_mastodon_api_orphaned_extra_fields();
|
||||
}
|
||||
if ( \version_compare( $version_from_db, '5.8.0', '<' ) ) {
|
||||
self::update_notification_options();
|
||||
}
|
||||
|
||||
update_option( 'activitypub_db_version', self::get_target_version() );
|
||||
/*
|
||||
* Add new update routines above this comment. ^
|
||||
*
|
||||
* Use 'unreleased' as the version number for new migrations and add tests for the callback directly.
|
||||
* The release script will automatically replace it with the actual version number.
|
||||
* Example:
|
||||
*
|
||||
* if ( \version_compare( $version_from_db, 'unreleased', '<' ) ) {
|
||||
* // Update routine.
|
||||
* }
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fires when the system has to be migrated.
|
||||
*
|
||||
* @param string $version_from_db The version from which to migrate.
|
||||
* @param string $target_version The target version to migrate to.
|
||||
*/
|
||||
\do_action( 'activitypub_migrate', $version_from_db, ACTIVITYPUB_PLUGIN_VERSION );
|
||||
|
||||
\update_option( 'activitypub_db_version', ACTIVITYPUB_PLUGIN_VERSION );
|
||||
|
||||
self::unlock();
|
||||
}
|
||||
@ -144,22 +226,54 @@ class Migration {
|
||||
* @param string $version_from_db The version from which to migrate.
|
||||
*/
|
||||
public static function async_migration( $version_from_db ) {
|
||||
if ( version_compare( $version_from_db, '1.0.0', '<' ) ) {
|
||||
if ( \version_compare( $version_from_db, '1.0.0', '<' ) ) {
|
||||
self::migrate_from_0_17();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the custom template to use shortcodes instead of the deprecated templates.
|
||||
* Asynchronously runs upgrade routines.
|
||||
*
|
||||
* @return void
|
||||
* @param callable $callback Callable upgrade routine. Must be a method of this class.
|
||||
* @params mixed ...$args Optional. Parameters that get passed to the callback.
|
||||
*/
|
||||
public static function async_upgrade( $callback ) {
|
||||
$args = \func_get_args();
|
||||
|
||||
// Bail if the existing lock is still valid.
|
||||
if ( self::is_locked() ) {
|
||||
\wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_upgrade', $args );
|
||||
return;
|
||||
}
|
||||
|
||||
self::lock();
|
||||
|
||||
$callback = array_shift( $args ); // Remove $callback from arguments.
|
||||
$next = \call_user_func_array( array( self::class, $callback ), $args );
|
||||
|
||||
self::unlock();
|
||||
|
||||
if ( ! empty( $next ) ) {
|
||||
// Schedule the next run, adding the result to the arguments.
|
||||
\wp_schedule_single_event(
|
||||
\time() + 30,
|
||||
'activitypub_upgrade',
|
||||
\array_merge( array( $callback ), \array_values( $next ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the custom template to use shortcodes instead of the deprecated templates.
|
||||
*/
|
||||
private static function migrate_from_0_16() {
|
||||
// Get the custom template.
|
||||
$old_content = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
|
||||
|
||||
// If the old content exists but is a blank string, we're going to need a flag to updated it even
|
||||
// after setting it to the default contents.
|
||||
/*
|
||||
* If the old content exists but is a blank string, we're going to need a flag to updated it even
|
||||
* after setting it to the default contents.
|
||||
*/
|
||||
$need_update = false;
|
||||
|
||||
// If the old contents is blank, use the defaults.
|
||||
@ -187,12 +301,10 @@ class Migration {
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the DB-schema of the followers-list
|
||||
*
|
||||
* @return void
|
||||
* Updates the DB-schema of the followers-list.
|
||||
*/
|
||||
public static function migrate_from_0_17() {
|
||||
// migrate followers
|
||||
// Migrate followers.
|
||||
foreach ( get_users( array( 'fields' => 'ID' ) ) as $user_id ) {
|
||||
$followers = get_user_meta( $user_id, 'activitypub_followers', true );
|
||||
|
||||
@ -207,9 +319,7 @@ class Migration {
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear the cache after updating to 1.3.0
|
||||
*
|
||||
* @return void
|
||||
* Clear the cache after updating to 1.3.0.
|
||||
*/
|
||||
private static function migrate_from_1_2_0() {
|
||||
$user_ids = \get_users(
|
||||
@ -225,9 +335,7 @@ class Migration {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unschedule Hooks after updating to 2.0.0
|
||||
*
|
||||
* @return void
|
||||
* Unschedule Hooks after updating to 2.0.0.
|
||||
*/
|
||||
private static function migrate_from_2_0_0() {
|
||||
wp_clear_scheduled_hook( 'activitypub_send_post_activity' );
|
||||
@ -246,42 +354,591 @@ class Migration {
|
||||
|
||||
/**
|
||||
* Add the ActivityPub capability to all users that can publish posts
|
||||
* Delete old meta to store followers
|
||||
*
|
||||
* @return void
|
||||
* Delete old meta to store followers.
|
||||
*/
|
||||
private static function migrate_from_2_2_0() {
|
||||
// add the ActivityPub capability to all users that can publish posts
|
||||
// Add the ActivityPub capability to all users that can publish posts.
|
||||
self::add_activitypub_capability();
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the defaults needed for the plugin to work
|
||||
* Rename DB fields.
|
||||
*/
|
||||
private static function migrate_from_2_6_0() {
|
||||
wp_cache_flush();
|
||||
|
||||
self::update_usermeta_key( 'activitypub_user_description', 'activitypub_description' );
|
||||
|
||||
self::update_options_key( 'activitypub_blog_user_description', 'activitypub_blog_description' );
|
||||
self::update_options_key( 'activitypub_blog_user_identifier', 'activitypub_blog_identifier' );
|
||||
}
|
||||
|
||||
/**
|
||||
* * Update actor-mode settings.
|
||||
* * Get the ID of the latest blog post and save it to the options table.
|
||||
*/
|
||||
private static function migrate_to_4_0_0() {
|
||||
$latest_post_id = 0;
|
||||
|
||||
// Get the ID of the latest blog post and save it to the options table.
|
||||
$latest_post = get_posts(
|
||||
array(
|
||||
'numberposts' => 1,
|
||||
'orderby' => 'ID',
|
||||
'order' => 'DESC',
|
||||
'post_type' => 'any',
|
||||
'post_status' => 'publish',
|
||||
)
|
||||
);
|
||||
|
||||
if ( $latest_post ) {
|
||||
$latest_post_id = $latest_post[0]->ID;
|
||||
}
|
||||
|
||||
\update_option( 'activitypub_last_post_with_permalink_as_id', $latest_post_id );
|
||||
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$followers = Followers::get_followers( $user->ID );
|
||||
|
||||
if ( $followers ) {
|
||||
\update_user_option( $user->ID, 'activitypub_use_permalink_as_id', '1' );
|
||||
}
|
||||
}
|
||||
|
||||
$followers = Followers::get_followers( Actors::BLOG_USER_ID );
|
||||
|
||||
if ( $followers ) {
|
||||
\update_option( 'activitypub_use_permalink_as_id_for_blog', '1' );
|
||||
}
|
||||
|
||||
self::migrate_actor_mode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Upate to 4.1.0
|
||||
*
|
||||
* * Add the ActivityPub capability to all users that can publish posts
|
||||
* * Migrate the `activitypub_post_content_type` to only use `activitypub_custom_post_content`.
|
||||
*/
|
||||
public static function migrate_to_4_1_0() {
|
||||
$content_type = \get_option( 'activitypub_post_content_type' );
|
||||
|
||||
switch ( $content_type ) {
|
||||
case 'excerpt':
|
||||
$template = "[ap_excerpt]\n\n[ap_permalink type=\"html\"]";
|
||||
break;
|
||||
case 'title':
|
||||
$template = "[ap_title type=\"html\"]\n\n[ap_permalink type=\"html\"]";
|
||||
break;
|
||||
case 'content':
|
||||
$template = "[ap_content]\n\n[ap_permalink type=\"html\"]\n\n[ap_hashtags]";
|
||||
break;
|
||||
case 'custom':
|
||||
$template = \get_option( 'activitypub_custom_post_content', ACTIVITYPUB_CUSTOM_POST_CONTENT );
|
||||
break;
|
||||
default:
|
||||
$template = ACTIVITYPUB_CUSTOM_POST_CONTENT;
|
||||
break;
|
||||
}
|
||||
|
||||
\update_option( 'activitypub_custom_post_content', $template );
|
||||
|
||||
\delete_option( 'activitypub_post_content_type' );
|
||||
|
||||
$object_type = \get_option( 'activitypub_object_type', false );
|
||||
if ( ! $object_type ) {
|
||||
\update_option( 'activitypub_object_type', 'note' );
|
||||
}
|
||||
|
||||
// Clean up empty visibility meta.
|
||||
global $wpdb;
|
||||
$wpdb->query( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
"DELETE FROM $wpdb->postmeta
|
||||
WHERE meta_key = 'activitypub_content_visibility'
|
||||
AND (meta_value IS NULL OR meta_value = '')"
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates post meta keys to be prefixed with an underscore.
|
||||
*/
|
||||
public static function migrate_to_4_7_1() {
|
||||
global $wpdb;
|
||||
|
||||
$meta_keys = array(
|
||||
'activitypub_actor_json',
|
||||
'activitypub_canonical_url',
|
||||
'activitypub_errors',
|
||||
'activitypub_inbox',
|
||||
'activitypub_user_id',
|
||||
);
|
||||
|
||||
foreach ( $meta_keys as $meta_key ) {
|
||||
// phpcs:ignore WordPress.DB
|
||||
$wpdb->update( $wpdb->postmeta, array( 'meta_key' => '_' . $meta_key ), array( 'meta_key' => $meta_key ) );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears the post cache for Followers, we should have done this in 4.7.1 when we renamed those keys.
|
||||
*/
|
||||
public static function migrate_to_4_7_2() {
|
||||
global $wpdb;
|
||||
// phpcs:ignore WordPress.DB
|
||||
$followers = $wpdb->get_col(
|
||||
$wpdb->prepare( "SELECT ID FROM {$wpdb->posts} WHERE post_type = %s", Followers::POST_TYPE )
|
||||
);
|
||||
foreach ( $followers as $id ) {
|
||||
clean_post_cache( $id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update comment counts for posts in batches.
|
||||
*
|
||||
* @return void
|
||||
* @see Comment::pre_wp_update_comment_count_now()
|
||||
* @param int $batch_size Optional. Number of posts to process per batch. Default 100.
|
||||
* @param int $offset Optional. Number of posts to skip. Default 0.
|
||||
*/
|
||||
public static function update_comment_counts( $batch_size = 100, $offset = 0 ) {
|
||||
global $wpdb;
|
||||
|
||||
// Bail if the existing lock is still valid.
|
||||
if ( self::is_locked() ) {
|
||||
\wp_schedule_single_event(
|
||||
time() + ( 5 * MINUTE_IN_SECONDS ),
|
||||
'activitypub_update_comment_counts',
|
||||
array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset,
|
||||
)
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
self::lock();
|
||||
|
||||
Comment::register_comment_types();
|
||||
$comment_types = Comment::get_comment_type_slugs();
|
||||
$type_inclusion = "AND comment_type IN ('" . implode( "','", $comment_types ) . "')";
|
||||
|
||||
// Get and process this batch.
|
||||
$post_ids = $wpdb->get_col( // phpcs:ignore WordPress.DB
|
||||
$wpdb->prepare(
|
||||
// phpcs:ignore WordPress.DB.PreparedSQL.InterpolatedNotPrepared
|
||||
"SELECT DISTINCT comment_post_ID FROM {$wpdb->comments} WHERE comment_approved = '1' {$type_inclusion} ORDER BY comment_post_ID LIMIT %d OFFSET %d",
|
||||
$batch_size,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
\wp_update_comment_count_now( $post_id );
|
||||
}
|
||||
|
||||
if ( count( $post_ids ) === $batch_size ) {
|
||||
// Schedule next batch.
|
||||
\wp_schedule_single_event(
|
||||
time() + MINUTE_IN_SECONDS,
|
||||
'activitypub_update_comment_counts',
|
||||
array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset + $batch_size,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
self::unlock();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create outbox items for posts in batches.
|
||||
*
|
||||
* @param int $batch_size Optional. Number of posts to process per batch. Default 50.
|
||||
* @param int $offset Optional. Number of posts to skip. Default 0.
|
||||
* @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
|
||||
*/
|
||||
public static function create_post_outbox_items( $batch_size = 50, $offset = 0 ) {
|
||||
$posts = \get_posts(
|
||||
array(
|
||||
// our own `ap_outbox` will be excluded from `any` by virtue of its `exclude_from_search` arg.
|
||||
'post_type' => 'any',
|
||||
'posts_per_page' => $batch_size,
|
||||
'offset' => $offset,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'activitypub_status',
|
||||
'value' => 'federated',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
// Avoid multiple queries for post meta.
|
||||
\update_postmeta_cache( \wp_list_pluck( $posts, 'ID' ) );
|
||||
|
||||
foreach ( $posts as $post ) {
|
||||
$visibility = \get_post_meta( $post->ID, 'activitypub_content_visibility', true );
|
||||
|
||||
self::add_to_outbox( $post, 'Create', $post->post_author, $visibility );
|
||||
|
||||
// Add Update activity when the post has been modified.
|
||||
if ( $post->post_modified !== $post->post_date ) {
|
||||
self::add_to_outbox( $post, 'Update', $post->post_author, $visibility );
|
||||
}
|
||||
}
|
||||
|
||||
if ( count( $posts ) === $batch_size ) {
|
||||
return array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset + $batch_size,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create outbox items for comments in batches.
|
||||
*
|
||||
* @param int $batch_size Optional. Number of posts to process per batch. Default 50.
|
||||
* @param int $offset Optional. Number of posts to skip. Default 0.
|
||||
* @return array|null Array with batch size and offset if there are more posts to process, null otherwise.
|
||||
*/
|
||||
public static function create_comment_outbox_items( $batch_size = 50, $offset = 0 ) {
|
||||
$comments = \get_comments(
|
||||
array(
|
||||
'author__not_in' => array( 0 ), // Limit to comments by registered users.
|
||||
'number' => $batch_size,
|
||||
'offset' => $offset,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'activitypub_status',
|
||||
'value' => 'federated',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $comments as $comment ) {
|
||||
self::add_to_outbox( $comment, 'Create', $comment->user_id );
|
||||
}
|
||||
|
||||
if ( count( $comments ) === $batch_size ) {
|
||||
return array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset + $batch_size,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update _activitypub_actor_json meta values to ensure they are properly slashed.
|
||||
*
|
||||
* @param int $batch_size Optional. Number of meta values to process per batch. Default 100.
|
||||
* @param int $offset Optional. Number of meta values to skip. Default 0.
|
||||
* @return array|null Array with batch size and offset if there are more meta values to process, null otherwise.
|
||||
*/
|
||||
public static function update_actor_json_slashing( $batch_size = 100, $offset = 0 ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$meta_values = $wpdb->get_results(
|
||||
$wpdb->prepare(
|
||||
"SELECT post_id, meta_value FROM {$wpdb->postmeta} WHERE meta_key = '_activitypub_actor_json' LIMIT %d OFFSET %d",
|
||||
$batch_size,
|
||||
$offset
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $meta_values as $meta ) {
|
||||
$json = \json_decode( $meta->meta_value, true );
|
||||
|
||||
// If json_decode fails, try adding slashes.
|
||||
if ( null === $json && \json_last_error() !== JSON_ERROR_NONE ) {
|
||||
$escaped_value = \preg_replace( '#\\\\(?!["\\\\/bfnrtu])#', '\\\\\\\\', $meta->meta_value );
|
||||
$json = \json_decode( $escaped_value, true );
|
||||
|
||||
// Update the meta if json_decode succeeds with slashes.
|
||||
if ( null !== $json && \json_last_error() === JSON_ERROR_NONE ) {
|
||||
\update_post_meta( $meta->post_id, '_activitypub_actor_json', \wp_slash( $escaped_value ) );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( \count( $meta_values ) === $batch_size ) {
|
||||
return array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset + $batch_size,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update comment author emails with webfinger addresses for ActivityPub comments.
|
||||
*
|
||||
* @param int $batch_size Optional. Number of comments to process per batch. Default 50.
|
||||
* @param int $offset Optional. Number of comments to skip. Default 0.
|
||||
* @return array|null Array with batch size and offset if there are more comments to process, null otherwise.
|
||||
*/
|
||||
public static function update_comment_author_emails( $batch_size = 50, $offset = 0 ) {
|
||||
$comments = \get_comments(
|
||||
array(
|
||||
'number' => $batch_size,
|
||||
'offset' => $offset,
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'protocol',
|
||||
'value' => 'activitypub',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $comments as $comment ) {
|
||||
$comment_author_url = $comment->comment_author_url;
|
||||
if ( empty( $comment_author_url ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$webfinger = Webfinger::uri_to_acct( $comment_author_url );
|
||||
if ( \is_wp_error( $webfinger ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
\wp_update_comment(
|
||||
array(
|
||||
'comment_ID' => $comment->comment_ID,
|
||||
'comment_author_email' => \str_replace( 'acct:', '', $webfinger ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( count( $comments ) === $batch_size ) {
|
||||
return array(
|
||||
'batch_size' => $batch_size,
|
||||
'offset' => $offset + $batch_size,
|
||||
);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the defaults needed for the plugin to work.
|
||||
*
|
||||
* Add the ActivityPub capability to all users that can publish posts.
|
||||
*/
|
||||
public static function add_default_settings() {
|
||||
self::add_activitypub_capability();
|
||||
self::add_default_extra_field();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ActivityPub capability to all users that can publish posts
|
||||
* Add an activity to the outbox without federating it.
|
||||
*
|
||||
* @return void
|
||||
* @param \WP_Post|\WP_Comment $comment The comment or post object.
|
||||
* @param string $activity_type The type of activity.
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $visibility Optional. The visibility of the content. Default 'public'.
|
||||
*/
|
||||
private static function add_to_outbox( $comment, $activity_type, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) {
|
||||
$transformer = Factory::get_transformer( $comment );
|
||||
if ( ! $transformer || \is_wp_error( $transformer ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$activity = $transformer->to_activity( $activity_type );
|
||||
if ( ! $activity || \is_wp_error( $activity ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If the user is disabled, fall back to the blog user when available.
|
||||
if ( ! user_can_activitypub( $user_id ) ) {
|
||||
if ( user_can_activitypub( Actors::BLOG_USER_ID ) ) {
|
||||
$user_id = Actors::BLOG_USER_ID;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
$post_id = Outbox::add( $activity, $user_id, $visibility );
|
||||
|
||||
// Immediately set to publish, no federation needed.
|
||||
\wp_publish_post( $post_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the ActivityPub capability to all users that can publish posts.
|
||||
*/
|
||||
private static function add_activitypub_capability() {
|
||||
// get all WP_User objects that can publish posts
|
||||
// Get all WP_User objects that can publish posts.
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'publish_posts' ),
|
||||
)
|
||||
);
|
||||
|
||||
// add ActivityPub capability to all users that can publish posts
|
||||
// Add ActivityPub capability to all users that can publish posts.
|
||||
foreach ( $users as $user ) {
|
||||
$user->add_cap( 'activitypub' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a default extra field for the user.
|
||||
*/
|
||||
private static function add_default_extra_field() {
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
|
||||
$title = \__( 'Powered by', 'activitypub' );
|
||||
$content = 'WordPress';
|
||||
|
||||
// Add a default extra field for each user.
|
||||
foreach ( $users as $user ) {
|
||||
\wp_insert_post(
|
||||
array(
|
||||
'post_type' => Extra_Fields::USER_POST_TYPE,
|
||||
'post_author' => $user->ID,
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $title,
|
||||
'post_content' => $content,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
\wp_insert_post(
|
||||
array(
|
||||
'post_type' => Extra_Fields::BLOG_POST_TYPE,
|
||||
'post_author' => 0,
|
||||
'post_status' => 'publish',
|
||||
'post_title' => $title,
|
||||
'post_content' => $content,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename meta keys.
|
||||
*
|
||||
* @param string $old_key The old comment meta key.
|
||||
* @param string $new_key The new comment meta key.
|
||||
*/
|
||||
private static function update_usermeta_key( $old_key, $new_key ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$wpdb->usermeta,
|
||||
array( 'meta_key' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
array( 'meta_key' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
array( '%s' ),
|
||||
array( '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename option keys.
|
||||
*
|
||||
* @param string $old_key The old option key.
|
||||
* @param string $new_key The new option key.
|
||||
*/
|
||||
private static function update_options_key( $old_key, $new_key ) {
|
||||
global $wpdb;
|
||||
|
||||
$wpdb->update( // phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$wpdb->options,
|
||||
array( 'option_name' => $new_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
array( 'option_name' => $old_key ), // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_key
|
||||
array( '%s' ),
|
||||
array( '%s' )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrate the actor mode settings.
|
||||
*/
|
||||
public static function migrate_actor_mode() {
|
||||
$blog_profile = \get_option( 'activitypub_enable_blog_user', '0' );
|
||||
$author_profiles = \get_option( 'activitypub_enable_users', '1' );
|
||||
|
||||
if (
|
||||
'1' === $blog_profile &&
|
||||
'1' === $author_profiles
|
||||
) {
|
||||
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_AND_BLOG_MODE );
|
||||
} elseif (
|
||||
'1' === $blog_profile &&
|
||||
'1' !== $author_profiles
|
||||
) {
|
||||
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_BLOG_MODE );
|
||||
} elseif (
|
||||
'1' !== $blog_profile &&
|
||||
'1' === $author_profiles
|
||||
) {
|
||||
\update_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes user extra fields where the author is the blog user.
|
||||
*
|
||||
* These extra fields were created when the Enable Mastodon Apps integration passed
|
||||
* an author_url instead of a user_id to the mastodon_api_account filter. This caused
|
||||
* Extra_Fields::default_actor_extra_fields() to run but fail to cache the fact it ran
|
||||
* for non-existent users. The result is a number of user extra fields with no author.
|
||||
*
|
||||
* @ticket https://github.com/Automattic/wordpress-activitypub/pull/1554
|
||||
*/
|
||||
public static function delete_mastodon_api_orphaned_extra_fields() {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$wpdb->delete(
|
||||
$wpdb->posts,
|
||||
array(
|
||||
'post_type' => Extra_Fields::USER_POST_TYPE,
|
||||
'post_author' => Actors::BLOG_USER_ID,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update notification options.
|
||||
*/
|
||||
public static function update_notification_options() {
|
||||
$new_dm = \get_option( 'activitypub_mailer_new_dm', '1' );
|
||||
$new_follower = \get_option( 'activitypub_mailer_new_follower', '1' );
|
||||
|
||||
// Add the blog user notification options.
|
||||
\add_option( 'activitypub_blog_user_mailer_new_dm', $new_dm );
|
||||
\add_option( 'activitypub_blog_user_mailer_new_follower', $new_follower );
|
||||
\add_option( 'activitypub_blog_user_mailer_new_mention', '1' );
|
||||
|
||||
// Add the actor notification options.
|
||||
foreach ( Actors::get_collection() as $actor ) {
|
||||
\update_user_option( $actor->get__id(), 'activitypub_mailer_new_dm', $new_dm );
|
||||
\update_user_option( $actor->get__id(), 'activitypub_mailer_new_follower', $new_follower );
|
||||
\update_user_option( $actor->get__id(), 'activitypub_mailer_new_mention', '1' );
|
||||
}
|
||||
|
||||
// Delete the old notification options.
|
||||
\delete_option( 'activitypub_mailer_new_dm' );
|
||||
\delete_option( 'activitypub_mailer_new_follower' );
|
||||
}
|
||||
}
|
||||
|
313
wp-content/plugins/activitypub/includes/class-move.php
Normal file
313
wp-content/plugins/activitypub/includes/class-move.php
Normal file
@ -0,0 +1,313 @@
|
||||
<?php
|
||||
/**
|
||||
* Move class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Activity\Actor;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Model\User;
|
||||
|
||||
/**
|
||||
* ActivityPub (Account) Move Class
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Move {
|
||||
|
||||
/**
|
||||
* Initialize the Move class.
|
||||
*/
|
||||
public static function init() {
|
||||
/**
|
||||
* Filter to enable automatically moving Fediverse accounts when the domain changes.
|
||||
*
|
||||
* @param bool $domain_moves_enabled Whether domain moves are enabled.
|
||||
*/
|
||||
$domain_moves_enabled = apply_filters( 'activitypub_enable_primary_domain_moves', false );
|
||||
|
||||
if ( $domain_moves_enabled ) {
|
||||
// Add the filter to change the domain.
|
||||
\add_filter( 'update_option_home', array( self::class, 'change_domain' ), 10, 2 );
|
||||
|
||||
if ( get_option( 'activitypub_old_host' ) ) {
|
||||
\add_action( 'activitypub_construct_model_actor', array( self::class, 'maybe_initiate_old_user' ) );
|
||||
\add_action( 'activitypub_pre_send_to_inboxes', array( self::class, 'pre_send_to_inboxes' ) );
|
||||
|
||||
if ( ! is_user_type_disabled( 'blog' ) ) {
|
||||
\add_filter( 'activitypub_pre_get_by_username', array( self::class, 'old_blog_username' ), 10, 2 );
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an ActivityPub account from one location to another.
|
||||
*
|
||||
* @param string $from The current account URL.
|
||||
* @param string $to The new account URL.
|
||||
*
|
||||
* @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
|
||||
*/
|
||||
public static function account( $from, $to ) {
|
||||
if ( is_same_domain( $from ) && is_same_domain( $to ) ) {
|
||||
return self::internally( $from, $to );
|
||||
}
|
||||
|
||||
return self::externally( $from, $to );
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an ActivityPub Actor from one location (internal) to another (external).
|
||||
*
|
||||
* This helps migrating local profiles to a new external profile:
|
||||
*
|
||||
* `Move::externally( 'https://example.com/?author=123', 'https://mastodon.example/users/foo' );`
|
||||
*
|
||||
* @param string $from The current account URL.
|
||||
* @param string $to The new account URL.
|
||||
*
|
||||
* @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
|
||||
*/
|
||||
public static function externally( $from, $to ) {
|
||||
$user = Actors::get_by_various( $from );
|
||||
|
||||
if ( \is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Update the movedTo property.
|
||||
if ( $user->get__id() > 0 ) {
|
||||
\update_user_option( $user->get__id(), 'activitypub_moved_to', $to );
|
||||
} else {
|
||||
\update_option( 'activitypub_blog_user_moved_to', $to );
|
||||
}
|
||||
|
||||
$response = Http::get_remote_object( $to );
|
||||
|
||||
if ( \is_wp_error( $response ) ) {
|
||||
return $response;
|
||||
}
|
||||
|
||||
$target_actor = new Actor();
|
||||
$target_actor->from_array( $response );
|
||||
|
||||
// Check if the `Move` Activity is valid.
|
||||
$also_known_as = $target_actor->get_also_known_as() ?? array();
|
||||
if ( ! in_array( $from, $also_known_as, true ) ) {
|
||||
return new \WP_Error( 'invalid_target', __( 'Invalid target', 'activitypub' ) );
|
||||
}
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->set_type( 'Move' );
|
||||
$activity->set_actor( $user->get_id() );
|
||||
$activity->set_origin( $user->get_id() );
|
||||
$activity->set_object( $user->get_id() );
|
||||
$activity->set_target( $target_actor->get_id() );
|
||||
|
||||
// Add to outbox.
|
||||
return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC );
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal Move.
|
||||
*
|
||||
* Move an ActivityPub Actor from one location (internal) to another (internal).
|
||||
*
|
||||
* This helps migrating abandoned profiles to `Move` to other profiles:
|
||||
*
|
||||
* `Move::internally( 'https://example.com/?author=123', 'https://example.com/?author=321' );`
|
||||
*
|
||||
* ... or to change Actor-IDs like:
|
||||
*
|
||||
* `Move::internally( 'https://example.com/author/foo', 'https://example.com/?author=123' );`
|
||||
*
|
||||
* @param string $from The current account URL.
|
||||
* @param string $to The new account URL.
|
||||
*
|
||||
* @return int|bool|\WP_Error The ID of the outbox item or false or WP_Error on failure.
|
||||
*/
|
||||
public static function internally( $from, $to ) {
|
||||
$user = Actors::get_by_various( $from );
|
||||
|
||||
if ( \is_wp_error( $user ) ) {
|
||||
return $user;
|
||||
}
|
||||
|
||||
// Add the old account URL to alsoKnownAs.
|
||||
if ( $user->get__id() > 0 ) {
|
||||
self::update_user_also_known_as( $user->get__id(), $from );
|
||||
\update_user_option( $user->get__id(), 'activitypub_moved_to', $to );
|
||||
} else {
|
||||
self::update_blog_also_known_as( $from );
|
||||
\update_option( 'activitypub_blog_user_moved_to', $to );
|
||||
}
|
||||
|
||||
// check if `$from` is a URL or an ID.
|
||||
if ( \filter_var( $from, FILTER_VALIDATE_URL ) ) {
|
||||
$actor = $from;
|
||||
} else {
|
||||
$actor = $user->get_id();
|
||||
}
|
||||
|
||||
$activity = new Activity();
|
||||
$activity->set_type( 'Move' );
|
||||
$activity->set_actor( $actor );
|
||||
$activity->set_origin( $actor );
|
||||
$activity->set_object( $actor );
|
||||
$activity->set_target( $to );
|
||||
|
||||
return add_to_outbox( $activity, null, $user->get__id(), ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the alsoKnownAs property of a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
* @param string $from The current account URL.
|
||||
*/
|
||||
private static function update_user_also_known_as( $user_id, $from ) {
|
||||
// phpcs:ignore Universal.Operators.DisallowShortTernary.Found
|
||||
$also_known_as = \get_user_option( 'activitypub_also_known_as', $user_id ) ?: array();
|
||||
$also_known_as[] = $from;
|
||||
|
||||
\update_user_option( $user_id, 'activitypub_also_known_as', $also_known_as );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the alsoKnownAs property of the blog.
|
||||
*
|
||||
* @param string $from The current account URL.
|
||||
*/
|
||||
private static function update_blog_also_known_as( $from ) {
|
||||
$also_known_as = \get_option( 'activitypub_blog_user_also_known_as', array() );
|
||||
$also_known_as[] = $from;
|
||||
|
||||
\update_option( 'activitypub_blog_user_also_known_as', $also_known_as );
|
||||
}
|
||||
|
||||
/**
|
||||
* Change domain for all ActivityPub Actors.
|
||||
*
|
||||
* This method handles domain migration according to the ActivityPub Data Portability spec.
|
||||
* It stores the old host and calls Move::internally for each available profile.
|
||||
* It also caches the JSON representation of the old Actor for future lookups.
|
||||
*
|
||||
* @param string $from The old domain.
|
||||
* @param string $to The new domain.
|
||||
*
|
||||
* @return array Array of results from Move::internally calls.
|
||||
*/
|
||||
public static function change_domain( $from, $to ) {
|
||||
// Get all actors that need to be migrated.
|
||||
$actors = Actors::get_all();
|
||||
|
||||
$results = array();
|
||||
$to_host = \wp_parse_url( $to, \PHP_URL_HOST );
|
||||
$from_host = \wp_parse_url( $from, \PHP_URL_HOST );
|
||||
|
||||
// Store the old host for future reference.
|
||||
\update_option( 'activitypub_old_host', $from_host );
|
||||
|
||||
// Process each actor.
|
||||
foreach ( $actors as $actor ) {
|
||||
$actor_id = $actor->get_id();
|
||||
|
||||
// Replace the new host with the old host in the actor ID.
|
||||
$old_actor_id = str_replace( $to_host, $from_host, $actor_id );
|
||||
|
||||
// Call Move::internally for this actor.
|
||||
$result = self::internally( $old_actor_id, $actor_id );
|
||||
|
||||
if ( \is_wp_error( $result ) ) {
|
||||
// Log the error and continue with the next actor.
|
||||
Debug::write_log( 'Error moving actor: ' . $actor_id . ' - ' . $result->get_error_message() );
|
||||
continue;
|
||||
}
|
||||
|
||||
$json = str_replace( $to_host, $from_host, $actor->to_json() );
|
||||
|
||||
// Save the current actor data after migration.
|
||||
if ( $actor instanceof Blog ) {
|
||||
\update_option( 'activitypub_blog_user_old_host_data', $json, false );
|
||||
} else {
|
||||
\update_user_option( $actor->get__id(), 'activitypub_old_host_data', $json, false );
|
||||
}
|
||||
|
||||
$results[] = array(
|
||||
'actor' => $actor_id,
|
||||
'result' => $result,
|
||||
);
|
||||
}
|
||||
|
||||
return $results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe initiate old user.
|
||||
*
|
||||
* This method checks if the current request domain matches the old host.
|
||||
* If it does, it retrieves the cached data for the user and populates the instance.
|
||||
*
|
||||
* @param Blog|User $instance The Blog or User instance to populate.
|
||||
*/
|
||||
public static function maybe_initiate_old_user( $instance ) {
|
||||
if ( ! Query::get_instance()->is_old_host_request() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( $instance instanceof Blog ) {
|
||||
$cached_data = \get_option( 'activitypub_blog_user_old_host_data' );
|
||||
} elseif ( $instance instanceof User ) {
|
||||
$cached_data = \get_user_option( 'activitypub_old_host_data', $instance->get__id() );
|
||||
}
|
||||
|
||||
if ( ! empty( $cached_data ) ) {
|
||||
$instance->from_json( $cached_data );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-send to inboxes.
|
||||
*
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
*/
|
||||
public static function pre_send_to_inboxes( $json ) {
|
||||
$json = json_decode( $json, true );
|
||||
|
||||
if ( 'Move' !== $json['type'] ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( is_same_domain( $json['object'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
Query::get_instance()->set_old_host_request();
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter to return the old blog username.
|
||||
*
|
||||
* @param null $pre The pre-existing value.
|
||||
* @param string $username The username to check.
|
||||
*
|
||||
* @return Blog|null The old blog instance or null.
|
||||
*/
|
||||
public static function old_blog_username( $pre, $username ) {
|
||||
$old_host = \get_option( 'activitypub_old_host' );
|
||||
|
||||
// Special case for Blog Actor on old host.
|
||||
if ( $old_host === $username && Query::get_instance()->is_old_host_request() ) {
|
||||
// Return a new Blog instance which will load the cached data in its constructor.
|
||||
$pre = new Blog();
|
||||
}
|
||||
|
||||
return $pre;
|
||||
}
|
||||
}
|
@ -0,0 +1,77 @@
|
||||
<?php
|
||||
/**
|
||||
* Notification file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* Notification class.
|
||||
*/
|
||||
class Notification {
|
||||
/**
|
||||
* The type of the notification.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $type;
|
||||
|
||||
/**
|
||||
* The actor URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $actor;
|
||||
|
||||
/**
|
||||
* The Activity object.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
public $object;
|
||||
|
||||
/**
|
||||
* The WordPress User-Id.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $target;
|
||||
|
||||
/**
|
||||
* Notification constructor.
|
||||
*
|
||||
* @param string $type The type of the notification.
|
||||
* @param string $actor The actor URL.
|
||||
* @param array $activity The Activity object.
|
||||
* @param int $target The WordPress User-Id.
|
||||
*/
|
||||
public function __construct( $type, $actor, $activity, $target ) {
|
||||
$this->type = $type;
|
||||
$this->actor = $actor;
|
||||
$this->object = $activity;
|
||||
$this->target = $target;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send the notification.
|
||||
*/
|
||||
public function send() {
|
||||
$type = \strtolower( $this->type );
|
||||
|
||||
/**
|
||||
* Action to send ActivityPub notifications.
|
||||
*
|
||||
* @param Notification $instance The notification object.
|
||||
*/
|
||||
do_action( 'activitypub_notification', $this );
|
||||
|
||||
/**
|
||||
* Type-specific action to send ActivityPub notifications.
|
||||
*
|
||||
* @param Notification $instance The notification object.
|
||||
*/
|
||||
do_action( "activitypub_notification_{$type}", $this );
|
||||
}
|
||||
}
|
124
wp-content/plugins/activitypub/includes/class-options.php
Normal file
124
wp-content/plugins/activitypub/includes/class-options.php
Normal file
@ -0,0 +1,124 @@
|
||||
<?php
|
||||
/**
|
||||
* Options file.
|
||||
*
|
||||
* @package ActivityPub
|
||||
*/
|
||||
|
||||
namespace ActivityPub;
|
||||
|
||||
/**
|
||||
* Options class.
|
||||
*
|
||||
* @package ActivityPub
|
||||
*/
|
||||
class Options {
|
||||
|
||||
/**
|
||||
* Initialize the options.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_filter( 'pre_option_activitypub_actor_mode', array( self::class, 'pre_option_activitypub_actor_mode' ) );
|
||||
\add_filter( 'pre_option_activitypub_authorized_fetch', array( self::class, 'pre_option_activitypub_authorized_fetch' ) );
|
||||
\add_filter( 'pre_option_activitypub_shared_inbox', array( self::class, 'pre_option_activitypub_shared_inbox' ) );
|
||||
\add_filter( 'pre_option_activitypub_vary_header', array( self::class, 'pre_option_activitypub_vary_header' ) );
|
||||
|
||||
\add_filter( 'pre_option_activitypub_allow_likes', array( self::class, 'maybe_disable_interactions' ) );
|
||||
\add_filter( 'pre_option_activitypub_allow_replies', array( self::class, 'maybe_disable_interactions' ) );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Pre-get option filter for the Actor-Mode.
|
||||
*
|
||||
* @param string|false $pre The pre-get option value.
|
||||
*
|
||||
* @return string|false The actor mode or false if it should not be filtered.
|
||||
*/
|
||||
public static function pre_option_activitypub_actor_mode( $pre ) {
|
||||
if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) && ACTIVITYPUB_SINGLE_USER_MODE ) {
|
||||
return ACTIVITYPUB_BLOG_MODE;
|
||||
}
|
||||
|
||||
if ( \defined( 'ACTIVITYPUB_DISABLE_USER' ) && ACTIVITYPUB_DISABLE_USER ) {
|
||||
return ACTIVITYPUB_BLOG_MODE;
|
||||
}
|
||||
|
||||
if ( \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) && ACTIVITYPUB_DISABLE_BLOG_USER ) {
|
||||
return ACTIVITYPUB_ACTOR_MODE;
|
||||
}
|
||||
|
||||
return $pre;
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-get option filter for the Authorized Fetch.
|
||||
*
|
||||
* @param string $pre The pre-get option value.
|
||||
*
|
||||
* @return string If the constant is defined, return the value, otherwise return the pre-get option value.
|
||||
*/
|
||||
public static function pre_option_activitypub_authorized_fetch( $pre ) {
|
||||
if ( ! \defined( 'ACTIVITYPUB_AUTHORIZED_FETCH' ) ) {
|
||||
return $pre;
|
||||
}
|
||||
|
||||
if ( ACTIVITYPUB_AUTHORIZED_FETCH ) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-get option filter for the Shared Inbox.
|
||||
*
|
||||
* @param string $pre The pre-get option value.
|
||||
*
|
||||
* @return string If the constant is defined, return the value, otherwise return the pre-get option value.
|
||||
*/
|
||||
public static function pre_option_activitypub_shared_inbox( $pre ) {
|
||||
if ( ! \defined( 'ACTIVITYPUB_SHARED_INBOX_FEATURE' ) ) {
|
||||
return $pre;
|
||||
}
|
||||
|
||||
if ( ACTIVITYPUB_SHARED_INBOX_FEATURE ) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-get option filter for the Vary Header.
|
||||
*
|
||||
* @param string $pre The pre-get option value.
|
||||
*
|
||||
* @return string If the constant is defined, return the value, otherwise return the pre-get option value.
|
||||
*/
|
||||
public static function pre_option_activitypub_vary_header( $pre ) {
|
||||
if ( ! \defined( 'ACTIVITYPUB_SEND_VARY_HEADER' ) ) {
|
||||
return $pre;
|
||||
}
|
||||
|
||||
if ( ACTIVITYPUB_SEND_VARY_HEADER ) {
|
||||
return '1';
|
||||
}
|
||||
|
||||
return '0';
|
||||
}
|
||||
|
||||
/**
|
||||
* Disallow interactions if the constant is set.
|
||||
*
|
||||
* @param bool $pre_option The value of the option.
|
||||
* @return bool|string The value of the option.
|
||||
*/
|
||||
public static function maybe_disable_interactions( $pre_option ) {
|
||||
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
|
||||
return '0';
|
||||
}
|
||||
|
||||
return $pre_option;
|
||||
}
|
||||
}
|
351
wp-content/plugins/activitypub/includes/class-query.php
Normal file
351
wp-content/plugins/activitypub/includes/class-query.php
Normal file
@ -0,0 +1,351 @@
|
||||
<?php
|
||||
/**
|
||||
* Query class.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Outbox;
|
||||
use Activitypub\Transformer\Factory;
|
||||
|
||||
/**
|
||||
* Singleton class to handle and store the ActivityPub query.
|
||||
*/
|
||||
class Query {
|
||||
|
||||
/**
|
||||
* The singleton instance.
|
||||
*
|
||||
* @var Query
|
||||
*/
|
||||
private static $instance;
|
||||
|
||||
/**
|
||||
* The ActivityPub object.
|
||||
*
|
||||
* @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-object
|
||||
*
|
||||
* @var object
|
||||
*/
|
||||
private $activitypub_object;
|
||||
|
||||
/**
|
||||
* The ActivityPub object ID.
|
||||
*
|
||||
* @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-id
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $activitypub_object_id;
|
||||
|
||||
/**
|
||||
* Whether the current request is an ActivityPub request.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_activitypub_request;
|
||||
|
||||
/**
|
||||
* Whether the current request is from the old host.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private $is_old_host_request;
|
||||
|
||||
/**
|
||||
* The constructor.
|
||||
*/
|
||||
private function __construct() {
|
||||
// Do nothing.
|
||||
}
|
||||
|
||||
/**
|
||||
* The destructor.
|
||||
*/
|
||||
public function __destruct() {
|
||||
self::$instance = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the singleton instance.
|
||||
*
|
||||
* @return Query The singleton instance.
|
||||
*/
|
||||
public static function get_instance() {
|
||||
if ( ! isset( self::$instance ) ) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub object.
|
||||
*
|
||||
* @return object The ActivityPub object.
|
||||
*/
|
||||
public function get_activitypub_object() {
|
||||
if ( $this->activitypub_object ) {
|
||||
return $this->activitypub_object;
|
||||
}
|
||||
|
||||
if ( $this->prepare_activitypub_data() ) {
|
||||
return $this->activitypub_object;
|
||||
}
|
||||
|
||||
$queried_object = $this->get_queried_object();
|
||||
$transformer = Factory::get_transformer( $queried_object );
|
||||
|
||||
if ( $transformer && ! \is_wp_error( $transformer ) ) {
|
||||
$this->activitypub_object = $transformer->to_object();
|
||||
}
|
||||
|
||||
return $this->activitypub_object;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub object ID.
|
||||
*
|
||||
* @return string The ActivityPub object ID.
|
||||
*/
|
||||
public function get_activitypub_object_id() {
|
||||
if ( $this->activitypub_object_id ) {
|
||||
return $this->activitypub_object_id;
|
||||
}
|
||||
|
||||
if ( $this->prepare_activitypub_data() ) {
|
||||
return $this->activitypub_object_id;
|
||||
}
|
||||
|
||||
$queried_object = $this->get_queried_object();
|
||||
$transformer = Factory::get_transformer( $queried_object );
|
||||
|
||||
if ( $transformer && ! \is_wp_error( $transformer ) ) {
|
||||
$this->activitypub_object_id = $transformer->to_id();
|
||||
}
|
||||
|
||||
return $this->activitypub_object_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Prepare and set both ActivityPub object and ID for Outbox activities and virtual objects.
|
||||
*
|
||||
* @return bool True if an object was found and set, false otherwise.
|
||||
*/
|
||||
private function prepare_activitypub_data() {
|
||||
$queried_object = $this->get_queried_object();
|
||||
|
||||
// Check for Outbox Activity.
|
||||
if (
|
||||
$queried_object instanceof \WP_Post &&
|
||||
Outbox::POST_TYPE === $queried_object->post_type
|
||||
) {
|
||||
$activitypub_object = Outbox::maybe_get_activity( $queried_object );
|
||||
|
||||
// Check if the Outbox Activity is public.
|
||||
if ( ! \is_wp_error( $activitypub_object ) ) {
|
||||
$this->activitypub_object = $activitypub_object;
|
||||
$this->activitypub_object_id = $this->activitypub_object->get_id();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $queried_object ) {
|
||||
// If the object is not a valid ActivityPub object, try to get a virtual object.
|
||||
$activitypub_object = $this->maybe_get_virtual_object();
|
||||
|
||||
if ( $activitypub_object ) {
|
||||
$this->activitypub_object = $activitypub_object;
|
||||
$this->activitypub_object_id = $this->activitypub_object->get_id();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the queried object.
|
||||
*
|
||||
* This adds support for Comments by `?c=123` IDs and Users by `?author=123` and `@username` IDs.
|
||||
*
|
||||
* @return \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null The queried object.
|
||||
*/
|
||||
public function get_queried_object() {
|
||||
$queried_object = \get_queried_object();
|
||||
|
||||
// Check Comment by ID.
|
||||
if ( ! $queried_object ) {
|
||||
$comment_id = \get_query_var( 'c' );
|
||||
if ( $comment_id ) {
|
||||
$queried_object = \get_comment( $comment_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Check Post by ID (works for custom post types).
|
||||
if ( ! $queried_object ) {
|
||||
$post_id = \get_query_var( 'p' );
|
||||
if ( $post_id ) {
|
||||
$queried_object = \get_post( $post_id );
|
||||
}
|
||||
}
|
||||
|
||||
// Try to get Author by ID.
|
||||
if ( ! $queried_object ) {
|
||||
$url = $this->get_request_url();
|
||||
$author_id = url_to_authorid( $url );
|
||||
if ( $author_id ) {
|
||||
$queried_object = \get_user_by( 'id', $author_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the queried object.
|
||||
*
|
||||
* @param \WP_Term|\WP_Post_Type|\WP_Post|\WP_User|\WP_Comment|null $queried_object The queried object.
|
||||
*/
|
||||
return apply_filters( 'activitypub_queried_object', $queried_object );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the virtual object.
|
||||
*
|
||||
* Virtual objects are objects that are not stored in the database, but are created on the fly.
|
||||
* The plugins currently supports two virtual objects: The Blog-Actor and the Application-Actor.
|
||||
*
|
||||
* @see \Activitypub\Model\Blog
|
||||
* @see \Activitypub\Model\Application
|
||||
*
|
||||
* @return object|null The virtual object.
|
||||
*/
|
||||
protected function maybe_get_virtual_object() {
|
||||
$url = $this->get_request_url();
|
||||
|
||||
if ( ! $url ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$author_id = url_to_authorid( $url );
|
||||
|
||||
if ( ! is_numeric( $author_id ) ) {
|
||||
$author_id = $url;
|
||||
}
|
||||
|
||||
$user = Actors::get_by_various( $author_id );
|
||||
|
||||
if ( \is_wp_error( $user ) || ! $user ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the request URL.
|
||||
*
|
||||
* @return string|null The request URL.
|
||||
*/
|
||||
protected function get_request_url() {
|
||||
if ( ! isset( $_SERVER['REQUEST_URI'] ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
|
||||
$url = \wp_unslash( $_SERVER['REQUEST_URI'] );
|
||||
$url = \WP_Http::make_absolute_url( $url, \home_url() );
|
||||
$url = \sanitize_url( $url );
|
||||
|
||||
return $url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current request is an ActivityPub request.
|
||||
*
|
||||
* @return bool True if the request is an ActivityPub request, false otherwise.
|
||||
*/
|
||||
public function is_activitypub_request() {
|
||||
if ( isset( $this->is_activitypub_request ) ) {
|
||||
return $this->is_activitypub_request;
|
||||
}
|
||||
|
||||
global $wp_query;
|
||||
|
||||
// One can trigger an ActivityPub request by adding `?activitypub` to the URL.
|
||||
if (
|
||||
isset( $wp_query->query_vars['activitypub'] ) ||
|
||||
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
||||
isset( $_GET['activitypub'] )
|
||||
) {
|
||||
\defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
|
||||
$this->is_activitypub_request = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* The other (more common) option to make an ActivityPub request
|
||||
* is to send an Accept header.
|
||||
*/
|
||||
if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
|
||||
$accept = \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_ACCEPT'] ) );
|
||||
|
||||
/*
|
||||
* $accept can be a single value, or a comma separated list of values.
|
||||
* We want to support both scenarios,
|
||||
* and return true when the header includes at least one of the following:
|
||||
* - application/activity+json
|
||||
* - application/ld+json
|
||||
* - application/json
|
||||
*/
|
||||
if ( \preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
|
||||
\defined( 'ACTIVITYPUB_REQUEST' ) || \define( 'ACTIVITYPUB_REQUEST', true );
|
||||
$this->is_activitypub_request = true;
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
$this->is_activitypub_request = false;
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the current request is from the old host.
|
||||
*
|
||||
* @return bool True if the request is from the old host, false otherwise.
|
||||
*/
|
||||
public function is_old_host_request() {
|
||||
if ( isset( $this->is_old_host_request ) ) {
|
||||
return $this->is_old_host_request;
|
||||
}
|
||||
|
||||
$old_host = \get_option( 'activitypub_old_host' );
|
||||
|
||||
if ( ! $old_host ) {
|
||||
$this->is_old_host_request = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
$request_host = isset( $_SERVER['HTTP_HOST'] ) ? \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_HOST'] ) ) : '';
|
||||
$referer_host = isset( $_SERVER['HTTP_REFERER'] ) ? \wp_parse_url( \sanitize_text_field( \wp_unslash( $_SERVER['HTTP_REFERER'] ) ), PHP_URL_HOST ) : '';
|
||||
|
||||
// Check if the domain matches either the request domain or referer.
|
||||
$check = $old_host === $request_host || $old_host === $referer_host;
|
||||
$this->is_old_host_request = $check;
|
||||
|
||||
return $check;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fake an old host request.
|
||||
*
|
||||
* @param bool $state Optional. The state to set. Default true.
|
||||
*/
|
||||
public function set_old_host_request( $state = true ) {
|
||||
$this->is_old_host_request = $state;
|
||||
}
|
||||
}
|
122
wp-content/plugins/activitypub/includes/class-sanitize.php
Normal file
122
wp-content/plugins/activitypub/includes/class-sanitize.php
Normal file
@ -0,0 +1,122 @@
|
||||
<?php
|
||||
/**
|
||||
* Sanitization file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Model\Blog;
|
||||
|
||||
/**
|
||||
* Sanitization class.
|
||||
*/
|
||||
class Sanitize {
|
||||
/**
|
||||
* Sanitize a list of URLs.
|
||||
*
|
||||
* @param string|array $value The value to sanitize.
|
||||
* @return array The sanitized list of URLs.
|
||||
*/
|
||||
public static function url_list( $value ) {
|
||||
if ( ! \is_array( $value ) ) {
|
||||
$value = \explode( PHP_EOL, $value );
|
||||
}
|
||||
|
||||
$value = \array_filter( $value );
|
||||
$value = \array_map( 'trim', $value );
|
||||
$value = \array_map( 'sanitize_url', $value );
|
||||
$value = \array_unique( $value );
|
||||
|
||||
return \array_values( $value );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a list of hosts.
|
||||
*
|
||||
* @param string $value The value to sanitize.
|
||||
* @return string The sanitized list of hosts.
|
||||
*/
|
||||
public static function host_list( $value ) {
|
||||
$value = \explode( PHP_EOL, $value );
|
||||
$value = \array_map(
|
||||
function ( $host ) {
|
||||
$host = \trim( $host );
|
||||
$host = \strtolower( $host );
|
||||
$host = \set_url_scheme( $host );
|
||||
$host = \sanitize_url( $host, array( 'http', 'https' ) );
|
||||
|
||||
// Remove protocol.
|
||||
if ( \str_contains( $host, 'http' ) ) {
|
||||
$host = \wp_parse_url( $host, PHP_URL_HOST );
|
||||
}
|
||||
|
||||
return \filter_var( $host, FILTER_VALIDATE_DOMAIN );
|
||||
},
|
||||
$value
|
||||
);
|
||||
|
||||
return \implode( PHP_EOL, \array_filter( $value ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a blog identifier.
|
||||
*
|
||||
* @param string $value The value to sanitize.
|
||||
* @return string The sanitized blog identifier.
|
||||
*/
|
||||
public static function blog_identifier( $value ) {
|
||||
// Hack to allow dots in the username.
|
||||
$parts = \explode( '.', $value );
|
||||
$sanitized = \array_map( 'sanitize_title', $parts );
|
||||
$sanitized = \implode( '.', $sanitized );
|
||||
|
||||
// Check for login or nicename.
|
||||
$user = new \WP_User_Query(
|
||||
array(
|
||||
'search' => $sanitized,
|
||||
'search_columns' => array( 'user_login', 'user_nicename' ),
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->get_results() ) {
|
||||
\add_settings_error(
|
||||
'activitypub_blog_identifier',
|
||||
'activitypub_blog_identifier',
|
||||
\esc_html__( 'You cannot use an existing author’s name for the blog profile ID.', 'activitypub' )
|
||||
);
|
||||
|
||||
return Blog::get_default_username();
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the sanitized value of a constant.
|
||||
*
|
||||
* @param mixed $value The constant value.
|
||||
*
|
||||
* @return string The sanitized value.
|
||||
*/
|
||||
public static function constant_value( $value ) {
|
||||
if ( is_bool( $value ) ) {
|
||||
return $value ? 'true' : 'false';
|
||||
}
|
||||
|
||||
if ( is_string( $value ) ) {
|
||||
return esc_attr( $value );
|
||||
}
|
||||
|
||||
if ( is_array( $value ) ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_print_r
|
||||
return print_r( $value, true );
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
}
|
@ -1,90 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Scheduler class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use Activitypub\Transformer\Post;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Activity\Base_Object;
|
||||
use Activitypub\Scheduler\Post;
|
||||
use Activitypub\Scheduler\Actor;
|
||||
use Activitypub\Scheduler\Comment;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Outbox;
|
||||
use Activitypub\Collection\Followers;
|
||||
|
||||
use function Activitypub\was_comment_sent;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
use function Activitypub\should_comment_be_federated;
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
use Activitypub\Transformer\Factory;
|
||||
|
||||
/**
|
||||
* ActivityPub Scheduler Class
|
||||
* Scheduler class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Scheduler {
|
||||
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Allowed batch callbacks.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private static $batch_callbacks = array();
|
||||
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
// Post transitions
|
||||
\add_action( 'transition_post_status', array( self::class, 'schedule_post_activity' ), 33, 3 );
|
||||
\add_action(
|
||||
'edit_attachment',
|
||||
function ( $post_id ) {
|
||||
self::schedule_post_activity( 'publish', 'publish', $post_id );
|
||||
}
|
||||
);
|
||||
\add_action(
|
||||
'add_attachment',
|
||||
function ( $post_id ) {
|
||||
self::schedule_post_activity( 'publish', '', $post_id );
|
||||
}
|
||||
);
|
||||
\add_action(
|
||||
'delete_attachment',
|
||||
function ( $post_id ) {
|
||||
self::schedule_post_activity( 'trash', '', $post_id );
|
||||
}
|
||||
self::register_schedulers();
|
||||
|
||||
self::$batch_callbacks = array(
|
||||
Dispatcher::$callback,
|
||||
array( Dispatcher::class, 'retry_send_to_followers' ),
|
||||
);
|
||||
|
||||
if ( ! ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS ) {
|
||||
// Comment transitions
|
||||
\add_action( 'transition_comment_status', array( self::class, 'schedule_comment_activity' ), 20, 3 );
|
||||
\add_action(
|
||||
'edit_comment',
|
||||
function ( $comment_id ) {
|
||||
self::schedule_comment_activity( 'approved', 'approved', $comment_id );
|
||||
}
|
||||
);
|
||||
\add_action(
|
||||
'wp_insert_comment',
|
||||
function ( $comment_id ) {
|
||||
self::schedule_comment_activity( 'approved', '', $comment_id );
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// Follower Cleanups
|
||||
// Follower Cleanups.
|
||||
\add_action( 'activitypub_update_followers', array( self::class, 'update_followers' ) );
|
||||
\add_action( 'activitypub_cleanup_followers', array( self::class, 'cleanup_followers' ) );
|
||||
|
||||
// profile updates for blog options
|
||||
if ( ! is_user_type_disabled( 'blog' ) ) {
|
||||
\add_action( 'update_option_site_icon', array( self::class, 'blog_user_update' ) );
|
||||
\add_action( 'update_option_blogdescription', array( self::class, 'blog_user_update' ) );
|
||||
\add_action( 'update_option_blogname', array( self::class, 'blog_user_update' ) );
|
||||
\add_filter( 'pre_set_theme_mod_custom_logo', array( self::class, 'blog_user_update' ) );
|
||||
\add_filter( 'pre_set_theme_mod_header_image', array( self::class, 'blog_user_update' ) );
|
||||
}
|
||||
// Event callbacks.
|
||||
\add_action( 'activitypub_async_batch', array( self::class, 'async_batch' ), 10, 99 );
|
||||
\add_action( 'activitypub_reprocess_outbox', array( self::class, 'reprocess_outbox' ) );
|
||||
\add_action( 'activitypub_outbox_purge', array( self::class, 'purge_outbox' ) );
|
||||
|
||||
// profile updates for user options
|
||||
if ( ! is_user_type_disabled( 'user' ) ) {
|
||||
\add_action( 'wp_update_user', array( self::class, 'user_update' ) );
|
||||
\add_action( 'updated_user_meta', array( self::class, 'user_meta_update' ), 10, 3 );
|
||||
// @todo figure out a feasible way of updating the header image since it's not unique to any user.
|
||||
}
|
||||
\add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_outbox_activity_for_federation' ) );
|
||||
\add_action( 'post_activitypub_add_to_outbox', array( self::class, 'schedule_announce_activity' ), 10, 4 );
|
||||
|
||||
\add_action( 'update_option_activitypub_outbox_purge_days', array( self::class, 'handle_outbox_purge_days_update' ), 10, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register handlers.
|
||||
*/
|
||||
public static function register_schedulers() {
|
||||
Post::init();
|
||||
Actor::init();
|
||||
Comment::init();
|
||||
|
||||
/**
|
||||
* Register additional schedulers.
|
||||
*
|
||||
* @since 5.0.0
|
||||
*/
|
||||
do_action( 'activitypub_register_schedulers' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule all ActivityPub schedules.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_schedules() {
|
||||
if ( ! \wp_next_scheduled( 'activitypub_update_followers' ) ) {
|
||||
@ -94,119 +84,30 @@ class Scheduler {
|
||||
if ( ! \wp_next_scheduled( 'activitypub_cleanup_followers' ) ) {
|
||||
\wp_schedule_event( time(), 'daily', 'activitypub_cleanup_followers' );
|
||||
}
|
||||
|
||||
if ( ! \wp_next_scheduled( 'activitypub_reprocess_outbox' ) ) {
|
||||
\wp_schedule_event( time(), 'hourly', 'activitypub_reprocess_outbox' );
|
||||
}
|
||||
|
||||
if ( ! wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'activitypub_outbox_purge' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unscedule all ActivityPub schedules.
|
||||
* Un-schedule all ActivityPub schedules.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function deregister_schedules() {
|
||||
wp_unschedule_hook( 'activitypub_update_followers' );
|
||||
wp_unschedule_hook( 'activitypub_cleanup_followers' );
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Schedule Activities.
|
||||
*
|
||||
* @param string $new_status New post status.
|
||||
* @param string $old_status Old post status.
|
||||
* @param WP_Post $post Post object.
|
||||
*/
|
||||
public static function schedule_post_activity( $new_status, $old_status, $post ) {
|
||||
$post = get_post( $post );
|
||||
|
||||
// Do not send activities if post is password protected.
|
||||
if ( \post_password_required( $post ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if post-type supports ActivityPub.
|
||||
$post_types = \get_post_types_by_support( 'activitypub' );
|
||||
if ( ! \in_array( $post->post_type, $post_types, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = false;
|
||||
|
||||
if ( 'publish' === $new_status && 'publish' !== $old_status ) {
|
||||
$type = 'Create';
|
||||
} elseif ( 'publish' === $new_status ) {
|
||||
$type = 'Update';
|
||||
} elseif ( 'trash' === $new_status ) {
|
||||
$type = 'Delete';
|
||||
}
|
||||
|
||||
if ( empty( $type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hook = 'activitypub_send_post';
|
||||
$args = array( $post->ID, $type );
|
||||
|
||||
if ( false === wp_next_scheduled( $hook, $args ) ) {
|
||||
set_wp_object_state( $post, 'federate' );
|
||||
\wp_schedule_single_event( \time(), $hook, $args );
|
||||
}
|
||||
wp_unschedule_hook( 'activitypub_reprocess_outbox' );
|
||||
wp_unschedule_hook( 'activitypub_outbox_purge' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Schedule Comment Activities
|
||||
*
|
||||
* transition_comment_status()
|
||||
*
|
||||
* @param string $new_status New comment status.
|
||||
* @param string $old_status Old comment status.
|
||||
* @param WP_Comment $comment Comment object.
|
||||
*/
|
||||
public static function schedule_comment_activity( $new_status, $old_status, $comment ) {
|
||||
$comment = get_comment( $comment );
|
||||
|
||||
// federate only comments that are written by a registered user.
|
||||
if ( ! $comment->user_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = false;
|
||||
|
||||
if (
|
||||
'approved' === $new_status &&
|
||||
'approved' !== $old_status
|
||||
) {
|
||||
$type = 'Create';
|
||||
} elseif ( 'approved' === $new_status ) {
|
||||
$type = 'Update';
|
||||
\update_comment_meta( $comment->comment_ID, 'activitypub_comment_modified', time(), true );
|
||||
} elseif (
|
||||
'trash' === $new_status ||
|
||||
'spam' === $new_status
|
||||
) {
|
||||
$type = 'Delete';
|
||||
}
|
||||
|
||||
if ( empty( $type ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if comment should be federated or not
|
||||
if ( ! should_comment_be_federated( $comment ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$hook = 'activitypub_send_comment';
|
||||
$args = array( $comment->comment_ID, $type );
|
||||
|
||||
if ( false === wp_next_scheduled( $hook, $args ) ) {
|
||||
set_wp_object_state( $comment, 'federate' );
|
||||
\wp_schedule_single_event( \time(), $hook, $args );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update followers
|
||||
*
|
||||
* @return void
|
||||
* Update followers.
|
||||
*/
|
||||
public static function update_followers() {
|
||||
$number = 5;
|
||||
@ -215,6 +116,11 @@ class Scheduler {
|
||||
$number = 50;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the number of followers to update.
|
||||
*
|
||||
* @param int $number The number of followers to update.
|
||||
*/
|
||||
$number = apply_filters( 'activitypub_update_followers_number', $number );
|
||||
$followers = Followers::get_outdated_followers( $number );
|
||||
|
||||
@ -231,9 +137,7 @@ class Scheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Cleanup followers
|
||||
*
|
||||
* @return void
|
||||
* Cleanup followers.
|
||||
*/
|
||||
public static function cleanup_followers() {
|
||||
$number = 5;
|
||||
@ -242,6 +146,11 @@ class Scheduler {
|
||||
$number = 50;
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter the number of followers to clean up.
|
||||
*
|
||||
* @param int $number The number of followers to clean up.
|
||||
*/
|
||||
$number = apply_filters( 'activitypub_update_followers_number', $number );
|
||||
$followers = Followers::get_faulty_followers( $number );
|
||||
|
||||
@ -253,6 +162,11 @@ class Scheduler {
|
||||
} elseif ( empty( $meta ) || ! is_array( $meta ) || is_wp_error( $meta ) ) {
|
||||
if ( $follower->count_errors() >= 5 ) {
|
||||
$follower->delete();
|
||||
\wp_schedule_single_event(
|
||||
\time(),
|
||||
'activitypub_delete_actor_interactions',
|
||||
array( $follower->get_id() )
|
||||
);
|
||||
} else {
|
||||
Followers::add_error( $follower->get__id(), $meta );
|
||||
}
|
||||
@ -263,66 +177,268 @@ class Scheduler {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a profile update when relevant user meta is updated.
|
||||
* Schedule the outbox item for federation.
|
||||
*
|
||||
* @param int $meta_id Meta ID being updated.
|
||||
* @param int $user_id User ID being updated.
|
||||
* @param string $meta_key Meta key being updated.
|
||||
*
|
||||
* @return void
|
||||
* @param int $id The ID of the outbox item.
|
||||
* @param int $offset The offset to add to the scheduled time.
|
||||
*/
|
||||
public static function user_meta_update( $meta_id, $user_id, $meta_key ) {
|
||||
// don't bother if the user can't publish
|
||||
if ( ! \user_can( $user_id, 'activitypub' ) ) {
|
||||
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 );
|
||||
public static function schedule_outbox_activity_for_federation( $id, $offset = 0 ) {
|
||||
$hook = 'activitypub_process_outbox';
|
||||
$args = array( $id );
|
||||
|
||||
if ( false === wp_next_scheduled( $hook, $args ) ) {
|
||||
\wp_schedule_single_event(
|
||||
\time() + $offset,
|
||||
$hook,
|
||||
$args
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a profile update when a user is updated.
|
||||
*
|
||||
* @param int $user_id User ID being updated.
|
||||
*
|
||||
* @return void
|
||||
* Reprocess the outbox.
|
||||
*/
|
||||
public static function user_update( $user_id ) {
|
||||
// don't bother if the user can't publish
|
||||
if ( ! \user_can( $user_id, 'activitypub' ) ) {
|
||||
public static function reprocess_outbox() {
|
||||
// Bail if there is a pending batch.
|
||||
if ( self::next_scheduled_hook( 'activitypub_async_batch' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::schedule_profile_update( $user_id );
|
||||
}
|
||||
// Bail if there is a batch in progress.
|
||||
$key = \md5( \serialize( Dispatcher::$callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
|
||||
if ( self::is_locked( $key ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 )
|
||||
$ids = \get_posts(
|
||||
array(
|
||||
'post_type' => Outbox::POST_TYPE,
|
||||
'post_status' => 'pending',
|
||||
'posts_per_page' => 10,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $ids as $id ) {
|
||||
self::schedule_outbox_activity_for_federation( $id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge outbox items based on a schedule.
|
||||
*/
|
||||
public static function purge_outbox() {
|
||||
$total_posts = (int) wp_count_posts( Outbox::POST_TYPE )->publish;
|
||||
if ( $total_posts <= 20 ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$days = (int) get_option( 'activitypub_outbox_purge_days', 180 );
|
||||
$timezone = new \DateTimeZone( 'UTC' );
|
||||
$date = new \DateTime( 'now', $timezone );
|
||||
|
||||
$date->sub( \DateInterval::createFromDateString( "$days days" ) );
|
||||
|
||||
$post_ids = get_posts(
|
||||
array(
|
||||
'post_type' => Outbox::POST_TYPE,
|
||||
'post_status' => 'any',
|
||||
'fields' => 'ids',
|
||||
'numberposts' => -1,
|
||||
'date_query' => array(
|
||||
array(
|
||||
'before' => $date->format( 'Y-m-d' ),
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $post_ids as $post_id ) {
|
||||
\wp_delete_post( $post_id, true );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update schedules when outbox purge days settings change.
|
||||
*
|
||||
* @param int $old_value The old value.
|
||||
* @param int $value The new value.
|
||||
*/
|
||||
public static function handle_outbox_purge_days_update( $old_value, $value ) {
|
||||
if ( 0 === (int) $value ) {
|
||||
wp_clear_scheduled_hook( 'activitypub_outbox_purge' );
|
||||
} elseif ( ! wp_next_scheduled( 'activitypub_outbox_purge' ) ) {
|
||||
wp_schedule_event( time(), 'daily', 'activitypub_outbox_purge' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Asynchronously runs batch processing routines.
|
||||
*
|
||||
* The batching part is optional and only comes into play if the callback returns anything.
|
||||
* Beyond that it's a helper to run a callback asynchronously with locking to prevent simultaneous processing.
|
||||
*
|
||||
* @param callable $callback Callable processing routine.
|
||||
* @params mixed ...$args Optional. Parameters that get passed to the callback.
|
||||
*/
|
||||
public static function async_batch( $callback ) {
|
||||
if ( ! in_array( $callback, self::$batch_callbacks, true ) || ! \is_callable( $callback ) ) {
|
||||
_doing_it_wrong( __METHOD__, 'The first argument must be a valid callback.', '5.2.0' );
|
||||
return;
|
||||
}
|
||||
|
||||
$args = \func_get_args(); // phpcs:ignore PHPCompatibility.FunctionUse.ArgumentFunctionsReportCurrentValue
|
||||
$key = \md5( \serialize( $callback ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize
|
||||
|
||||
// Bail if the existing lock is still valid.
|
||||
if ( self::is_locked( $key ) ) {
|
||||
\wp_schedule_single_event( time() + MINUTE_IN_SECONDS, 'activitypub_async_batch', $args );
|
||||
return;
|
||||
}
|
||||
|
||||
self::lock( $key );
|
||||
|
||||
$callback = array_shift( $args ); // Remove $callback from arguments.
|
||||
$next = \call_user_func_array( $callback, $args );
|
||||
|
||||
self::unlock( $key );
|
||||
|
||||
if ( ! empty( $next ) ) {
|
||||
// Schedule the next run, adding the result to the arguments.
|
||||
\wp_schedule_single_event(
|
||||
\time() + 30,
|
||||
'activitypub_async_batch',
|
||||
\array_merge( array( $callback ), \array_values( $next ) )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Locks the async batch process for individual callbacks to prevent simultaneous processing.
|
||||
*
|
||||
* @param string $key Serialized callback name.
|
||||
* @return bool|int True if the lock was successful, timestamp of existing lock otherwise.
|
||||
*/
|
||||
public static function lock( $key ) {
|
||||
global $wpdb;
|
||||
|
||||
// Try to lock.
|
||||
$lock_result = (bool) $wpdb->query( $wpdb->prepare( "INSERT IGNORE INTO `$wpdb->options` ( `option_name`, `option_value`, `autoload` ) VALUES (%s, %s, 'no') /* LOCK */", 'activitypub_async_batch_' . $key, \time() ) ); // phpcs:ignore WordPress.DB
|
||||
|
||||
if ( ! $lock_result ) {
|
||||
$lock_result = \get_option( 'activitypub_async_batch_' . $key );
|
||||
}
|
||||
|
||||
return $lock_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unlocks processing for the async batch callback.
|
||||
*
|
||||
* @param string $key Serialized callback name.
|
||||
*/
|
||||
public static function unlock( $key ) {
|
||||
\delete_option( 'activitypub_async_batch_' . $key );
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the async batch callback is locked.
|
||||
*
|
||||
* @param string $key Serialized callback name.
|
||||
* @return boolean
|
||||
*/
|
||||
public static function is_locked( $key ) {
|
||||
$lock = \get_option( 'activitypub_async_batch_' . $key );
|
||||
|
||||
if ( ! $lock ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$lock = (int) $lock;
|
||||
|
||||
if ( $lock < \time() - 1800 ) {
|
||||
self::unlock( $key );
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the next scheduled hook.
|
||||
*
|
||||
* @param string $hook The hook name.
|
||||
* @return int|bool The timestamp of the next scheduled hook, or false if none found.
|
||||
*/
|
||||
private static function next_scheduled_hook( $hook ) {
|
||||
$crons = _get_cron_array();
|
||||
if ( empty( $crons ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Get next event.
|
||||
$next = false;
|
||||
foreach ( $crons as $timestamp => $cron ) {
|
||||
if ( isset( $cron[ $hook ] ) ) {
|
||||
$next = $timestamp;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $next;
|
||||
}
|
||||
|
||||
/**
|
||||
* Send announces.
|
||||
*
|
||||
* @param int $outbox_activity_id The outbox activity ID.
|
||||
* @param \Activitypub\Activity\Activity $activity The activity object.
|
||||
* @param int $actor_id The actor ID.
|
||||
* @param int $content_visibility The content visibility.
|
||||
*/
|
||||
public static function schedule_announce_activity( $outbox_activity_id, $activity, $actor_id, $content_visibility ) {
|
||||
// Only if we're in both Blog and User modes.
|
||||
if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only if this isn't the Blog Actor.
|
||||
if ( Actors::BLOG_USER_ID === $actor_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only if the content is public or quiet public.
|
||||
if ( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC !== $content_visibility ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only if the activity is a Create.
|
||||
if ( 'Create' !== $activity->get_type() ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! is_object( $activity->get_object() ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if the object is an article, image, audio, video, event, or document and ignore profile updates and other activities.
|
||||
if ( ! in_array( $activity->get_object()->get_type(), Base_Object::TYPES, true ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$announce = new Activity();
|
||||
$announce->set_type( 'Announce' );
|
||||
$announce->set_actor( Actors::get_by_id( Actors::BLOG_USER_ID )->get_id() );
|
||||
$announce->set_object( $activity );
|
||||
|
||||
$outbox_activity_id = Outbox::add( $announce, Actors::BLOG_USER_ID );
|
||||
|
||||
if ( ! $outbox_activity_id ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Schedule the outbox item for federation.
|
||||
self::schedule_outbox_activity_for_federation( $outbox_activity_id, 120 );
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,18 @@
|
||||
<?php
|
||||
/**
|
||||
* Shortcodes class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use function Activitypub\esc_hashtag;
|
||||
|
||||
/**
|
||||
* Shortcodes class.
|
||||
*/
|
||||
class Shortcodes {
|
||||
/**
|
||||
* Register the shortcodes
|
||||
* Register the shortcodes.
|
||||
*/
|
||||
public static function register() {
|
||||
foreach ( get_class_methods( self::class ) as $shortcode ) {
|
||||
@ -16,7 +23,7 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Unregister the shortcodes
|
||||
* Unregister the shortcodes.
|
||||
*/
|
||||
public static function unregister() {
|
||||
foreach ( get_class_methods( self::class ) as $shortcode ) {
|
||||
@ -27,15 +34,11 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_hashtags' shortcode
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
* @param string $tag The tag/name of the Shortcode.
|
||||
* Generates output for the 'ap_hashtags' shortcode.
|
||||
*
|
||||
* @return string The post tags as hashtags.
|
||||
*/
|
||||
public static function hashtags( $atts, $content, $tag ) {
|
||||
public static function hashtags() {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
@ -51,6 +54,11 @@ class Shortcodes {
|
||||
$hash_tags = array();
|
||||
|
||||
foreach ( $tags as $tag ) {
|
||||
// Tag can be empty.
|
||||
if ( ! $tag ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$hash_tags[] = \sprintf(
|
||||
'<a rel="tag" class="hashtag u-tag u-category" href="%s">%s</a>',
|
||||
\esc_url( \get_tag_link( $tag ) ),
|
||||
@ -77,7 +85,23 @@ class Shortcodes {
|
||||
return '';
|
||||
}
|
||||
|
||||
return \wp_strip_all_tags( \get_the_title( $item->ID ), true );
|
||||
$title = \wp_strip_all_tags( \get_the_title( $item->ID ), true );
|
||||
|
||||
if ( ! $title ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$atts = shortcode_atts(
|
||||
array( 'type' => 'plain' ),
|
||||
$atts,
|
||||
$tag
|
||||
);
|
||||
|
||||
if ( 'html' !== $atts['type'] ) {
|
||||
return $title;
|
||||
}
|
||||
|
||||
return sprintf( '<h2>%s</h2>', $title );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -108,87 +132,14 @@ class Shortcodes {
|
||||
$excerpt_length = ACTIVITYPUB_EXCERPT_LENGTH;
|
||||
}
|
||||
|
||||
$excerpt = \get_post_field( 'post_excerpt', $item );
|
||||
|
||||
if ( 'attachment' === $item->post_type ) {
|
||||
// get title of attachment with fallback to alt text.
|
||||
$content = wp_get_attachment_caption( $item->ID );
|
||||
if ( empty( $content ) ) {
|
||||
$content = get_post_meta( $item->ID, '_wp_attachment_image_alt', true );
|
||||
}
|
||||
} elseif ( '' === $excerpt ) {
|
||||
$content = \get_post_field( 'post_content', $item );
|
||||
|
||||
// An empty string will make wp_trim_excerpt do stuff we do not want.
|
||||
if ( '' !== $content ) {
|
||||
$excerpt = \strip_shortcodes( $content );
|
||||
|
||||
/** This filter is documented in wp-includes/post-template.php */
|
||||
$excerpt = \apply_filters( 'the_content', $excerpt );
|
||||
$excerpt = \str_replace( ']]>', ']]>', $excerpt );
|
||||
}
|
||||
}
|
||||
|
||||
// Strip out any remaining tags.
|
||||
$excerpt = \wp_strip_all_tags( $excerpt );
|
||||
|
||||
$excerpt_more = \apply_filters( 'activitypub_excerpt_more', ' […]' );
|
||||
$excerpt_more_len = strlen( $excerpt_more );
|
||||
|
||||
// We now have a excerpt, but we need to check it's length, it may be longer than we want for two reasons:
|
||||
//
|
||||
// * The user has entered a manual excerpt which is longer that what we want.
|
||||
// * No manual excerpt exists so we've used the content which might be longer than we want.
|
||||
//
|
||||
// Either way, let's trim it up if we need too. Also, don't forget to take into account the more indicator
|
||||
// as part of the total length.
|
||||
//
|
||||
|
||||
// Setup a variable to hold the current excerpts length.
|
||||
$current_excerpt_length = strlen( $excerpt );
|
||||
|
||||
// Setup a variable to keep track of our target length.
|
||||
$target_excerpt_length = $excerpt_length - $excerpt_more_len;
|
||||
|
||||
// Setup a variable to keep track of the current max length.
|
||||
$current_excerpt_max = $target_excerpt_length;
|
||||
|
||||
// This is a loop since we can't calculate word break the string after 'the_excpert' filter has run (we would break
|
||||
// all kinds of html tags), so we have to cut the excerpt down a bit at a time until we hit our target length.
|
||||
while ( $current_excerpt_length > $target_excerpt_length && $current_excerpt_max > 0 ) {
|
||||
// Trim the excerpt based on wordwrap() positioning.
|
||||
// Note: we're using <br> as the linebreak just in case there are any newlines existing in the excerpt from the user.
|
||||
// There won't be any <br> left after we've run wp_strip_all_tags() in the code above, so they're
|
||||
// safe to use here. It won't be included in the final excerpt as the substr() will trim it off.
|
||||
$excerpt = substr( $excerpt, 0, strpos( wordwrap( $excerpt, $current_excerpt_max, '<br>' ), '<br>' ) );
|
||||
|
||||
// If something went wrong, or we're in a language that wordwrap() doesn't understand,
|
||||
// just chop it off and don't worry about breaking in the middle of a word.
|
||||
if ( strlen( $excerpt ) > $excerpt_length - $excerpt_more_len ) {
|
||||
$excerpt = substr( $excerpt, 0, $current_excerpt_max );
|
||||
}
|
||||
|
||||
// Add in the more indicator.
|
||||
$excerpt = $excerpt . $excerpt_more;
|
||||
|
||||
// Run it through the excerpt filter which will add some html tags back in.
|
||||
$excerpt_filtered = apply_filters( 'the_excerpt', $excerpt );
|
||||
|
||||
// Now set the current excerpt length to this new filtered length.
|
||||
$current_excerpt_length = strlen( $excerpt_filtered );
|
||||
|
||||
// Check to see if we're over the target length.
|
||||
if ( $current_excerpt_length > $target_excerpt_length ) {
|
||||
// If so, remove 20 characters from the current max and run the loop again.
|
||||
$current_excerpt_max = $current_excerpt_max - 20;
|
||||
}
|
||||
}
|
||||
$excerpt = generate_post_summary( $item, $excerpt_length );
|
||||
|
||||
/** This filter is documented in wp-includes/post-template.php */
|
||||
return \apply_filters( 'the_excerpt', $excerpt );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_content' Shortcode
|
||||
* Generates output for the 'ap_content' Shortcode.
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
@ -203,7 +154,7 @@ class Shortcodes {
|
||||
return '';
|
||||
}
|
||||
|
||||
// prevent inception
|
||||
// Prevent inception.
|
||||
remove_shortcode( 'ap_content' );
|
||||
|
||||
$atts = shortcode_atts(
|
||||
@ -215,35 +166,40 @@ class Shortcodes {
|
||||
$content = '';
|
||||
|
||||
if ( 'attachment' === $item->post_type ) {
|
||||
// get title of attachment with fallback to alt text.
|
||||
// Get title of attachment with fallback to alt text.
|
||||
$content = wp_get_attachment_caption( $item->ID );
|
||||
if ( empty( $content ) ) {
|
||||
$content = get_post_meta( $item->ID, '_wp_attachment_image_alt', true );
|
||||
}
|
||||
} else {
|
||||
$content = \get_post_field( 'post_content', $item );
|
||||
|
||||
if ( 'yes' === $atts['apply_filters'] ) {
|
||||
$content = \apply_filters( 'the_content', $content );
|
||||
} else {
|
||||
$content = do_blocks( $content );
|
||||
$content = wptexturize( $content );
|
||||
$content = wp_filter_content_tags( $content );
|
||||
}
|
||||
|
||||
// replace script and style elements
|
||||
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
|
||||
$content = strip_shortcodes( $content );
|
||||
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
|
||||
}
|
||||
|
||||
if ( empty( $content ) ) {
|
||||
$content = \get_post_field( 'post_content', $item );
|
||||
}
|
||||
|
||||
if ( 'yes' === $atts['apply_filters'] ) {
|
||||
/** This filter is documented in wp-includes/post-template.php */
|
||||
$content = \apply_filters( 'the_content', $content );
|
||||
} else {
|
||||
if ( site_supports_blocks() ) {
|
||||
$content = \do_blocks( $content );
|
||||
}
|
||||
$content = \wptexturize( $content );
|
||||
$content = \wp_filter_content_tags( $content );
|
||||
}
|
||||
|
||||
// Replace script and style elements.
|
||||
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
|
||||
$content = \strip_shortcodes( $content );
|
||||
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
|
||||
|
||||
add_shortcode( 'ap_content', array( 'Activitypub\Shortcodes', 'content' ) );
|
||||
|
||||
return $content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_permalink' Shortcode
|
||||
* Generates output for the 'ap_permalink' Shortcode.
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
@ -266,7 +222,7 @@ class Shortcodes {
|
||||
$tag
|
||||
);
|
||||
|
||||
if ( 'url' === $atts['type'] ) {
|
||||
if ( 'html' !== $atts['type'] ) {
|
||||
return \esc_url( \get_permalink( $item->ID ) );
|
||||
}
|
||||
|
||||
@ -277,7 +233,7 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_shortlink' Shortcode
|
||||
* Generates output for the 'ap_shortlink' Shortcode.
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
@ -300,7 +256,7 @@ class Shortcodes {
|
||||
$tag
|
||||
);
|
||||
|
||||
if ( 'url' === $atts['type'] ) {
|
||||
if ( 'html' !== $atts['type'] ) {
|
||||
return \esc_url( \wp_get_shortlink( $item->ID ) );
|
||||
}
|
||||
|
||||
@ -311,7 +267,7 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_image' Shortcode
|
||||
* Generates output for the 'ap_image' Shortcode.
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
@ -354,15 +310,11 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_hashcats' Shortcode
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
* @param string $tag The tag/name of the Shortcode.
|
||||
* Generates output for the 'ap_hashcats' Shortcode.
|
||||
*
|
||||
* @return string The post categories as hashtags.
|
||||
*/
|
||||
public static function hashcats( $atts, $content, $tag ) {
|
||||
public static function hashcats() {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
@ -389,15 +341,11 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_author' Shortcode
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
* @param string $tag The tag/name of the Shortcode.
|
||||
* Generates output for the 'ap_author' Shortcode.
|
||||
*
|
||||
* @return string The author name.
|
||||
*/
|
||||
public static function author( $atts, $content, $tag ) {
|
||||
public static function author() {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
@ -405,7 +353,7 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
$author_id = \get_post_field( 'post_author', $item->ID );
|
||||
$name = \get_the_author_meta( 'display_name', $author_id );
|
||||
$name = \get_the_author_meta( 'display_name', $author_id );
|
||||
|
||||
if ( ! $name ) {
|
||||
return '';
|
||||
@ -415,15 +363,11 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_authorurl' Shortcode
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
* @param string $tag The tag/name of the Shortcode.
|
||||
* Generates output for the 'ap_authorurl' Shortcode.
|
||||
*
|
||||
* @return string The author URL.
|
||||
*/
|
||||
public static function authorurl( $atts, $content, $tag ) {
|
||||
public static function authorurl() {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
@ -431,7 +375,7 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
$author_id = \get_post_field( 'post_author', $item->ID );
|
||||
$url = \get_the_author_meta( 'user_url', $author_id );
|
||||
$url = \get_the_author_meta( 'user_url', $author_id );
|
||||
|
||||
if ( ! $url ) {
|
||||
return '';
|
||||
@ -441,63 +385,46 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_blogurl' Shortcode
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
* @param string $tag The tag/name of the Shortcode.
|
||||
* Generates output for the 'ap_blogurl' Shortcode.
|
||||
*
|
||||
* @return string The site URL.
|
||||
*/
|
||||
public static function blogurl( $atts, $content, $tag ) {
|
||||
public static function blogurl() {
|
||||
return \esc_url( \get_bloginfo( 'url' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_blogname' Shortcode
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
* @param string $tag The tag/name of the Shortcode.
|
||||
* Generates output for the 'ap_blogname' Shortcode.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function blogname( $atts, $content, $tag ) {
|
||||
public static function blogname() {
|
||||
return \wp_strip_all_tags( \get_bloginfo( 'name' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_blogdesc' Shortcode
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
* @param string $tag The tag/name of the Shortcode.
|
||||
* Generates output for the 'ap_blogdesc' Shortcode.
|
||||
*
|
||||
* @return string The site description.
|
||||
*/
|
||||
public static function blogdesc( $atts, $content, $tag ) {
|
||||
public static function blogdesc() {
|
||||
return \wp_strip_all_tags( \get_bloginfo( 'description' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_date' Shortcode
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
* @param string $tag The tag/name of the Shortcode.
|
||||
* Generates output for the 'ap_date' Shortcode.
|
||||
*
|
||||
* @return string The post date.
|
||||
*/
|
||||
public static function date( $atts, $content, $tag ) {
|
||||
public static function date() {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$datetime = \get_post_datetime( $item );
|
||||
$datetime = \get_post_datetime( $item );
|
||||
$dateformat = \get_option( 'date_format' );
|
||||
$timeformat = \get_option( 'time_format' );
|
||||
|
||||
$date = $datetime->format( $dateformat );
|
||||
|
||||
@ -509,23 +436,18 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_time' Shortcode
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
* @param string $tag The tag/name of the Shortcode.
|
||||
* Generates output for the 'ap_time' Shortcode.
|
||||
*
|
||||
* @return string The post time.
|
||||
*/
|
||||
public static function time( $atts, $content, $tag ) {
|
||||
public static function time() {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$datetime = \get_post_datetime( $item );
|
||||
$dateformat = \get_option( 'date_format' );
|
||||
$datetime = \get_post_datetime( $item );
|
||||
$timeformat = \get_option( 'time_format' );
|
||||
|
||||
$date = $datetime->format( $timeformat );
|
||||
@ -538,22 +460,18 @@ class Shortcodes {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates output for the 'ap_datetime' Shortcode
|
||||
*
|
||||
* @param array $atts The Shortcode attributes.
|
||||
* @param string $content The ActivityPub post-content.
|
||||
* @param string $tag The tag/name of the Shortcode.
|
||||
* Generates output for the 'ap_datetime' Shortcode.
|
||||
*
|
||||
* @return string The post date/time.
|
||||
*/
|
||||
public static function datetime( $atts, $content, $tag ) {
|
||||
public static function datetime() {
|
||||
$item = self::get_item();
|
||||
|
||||
if ( ! $item ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$datetime = \get_post_datetime( $item );
|
||||
$datetime = \get_post_datetime( $item );
|
||||
$dateformat = \get_option( 'date_format' );
|
||||
$timeformat = \get_option( 'time_format' );
|
||||
|
||||
@ -572,7 +490,7 @@ class Shortcodes {
|
||||
* Checks if item (WP_Post) is "public", a supported post type
|
||||
* and not password protected.
|
||||
*
|
||||
* @return null|WP_Post The WordPress item.
|
||||
* @return null|\WP_Post The WordPress item.
|
||||
*/
|
||||
protected static function get_item() {
|
||||
$post = \get_post();
|
||||
|
@ -1,14 +1,20 @@
|
||||
<?php
|
||||
/**
|
||||
* Signature class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use DateTime;
|
||||
use DateTimeZone;
|
||||
use WP_REST_Request;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
/**
|
||||
* ActivityPub Signature Class
|
||||
* ActivityPub Signature Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
* @author Django Doucet
|
||||
@ -19,7 +25,7 @@ class Signature {
|
||||
* Return the public key for a given user.
|
||||
*
|
||||
* @param int $user_id The WordPress User ID.
|
||||
* @param bool $force Force the generation of a new key pair.
|
||||
* @param bool $force Optional. Force the generation of a new key pair. Default false.
|
||||
*
|
||||
* @return mixed The public key.
|
||||
*/
|
||||
@ -37,7 +43,7 @@ class Signature {
|
||||
* Return the private key for a given user.
|
||||
*
|
||||
* @param int $user_id The WordPress User ID.
|
||||
* @param bool $force Force the generation of a new key pair.
|
||||
* @param bool $force Optional. Force the generation of a new key pair. Default false.
|
||||
*
|
||||
* @return mixed The private key.
|
||||
*/
|
||||
@ -60,7 +66,7 @@ class Signature {
|
||||
*/
|
||||
public static function get_keypair_for( $user_id ) {
|
||||
$option_key = self::get_signature_options_key_for( $user_id );
|
||||
$key_pair = \get_option( $option_key );
|
||||
$key_pair = \get_option( $option_key );
|
||||
|
||||
if ( ! $key_pair ) {
|
||||
$key_pair = self::generate_key_pair_for( $user_id );
|
||||
@ -78,7 +84,7 @@ class Signature {
|
||||
*/
|
||||
protected static function generate_key_pair_for( $user_id ) {
|
||||
$option_key = self::get_signature_options_key_for( $user_id );
|
||||
$key_pair = self::check_legacy_key_pair_for( $user_id );
|
||||
$key_pair = self::check_legacy_key_pair_for( $user_id );
|
||||
|
||||
if ( $key_pair ) {
|
||||
\add_option( $option_key, $key_pair );
|
||||
@ -87,19 +93,21 @@ class Signature {
|
||||
}
|
||||
|
||||
$config = array(
|
||||
'digest_alg' => 'sha512',
|
||||
'digest_alg' => 'sha512',
|
||||
'private_key_bits' => 2048,
|
||||
'private_key_type' => \OPENSSL_KEYTYPE_RSA,
|
||||
);
|
||||
|
||||
$key = \openssl_pkey_new( $config );
|
||||
$key = \openssl_pkey_new( $config );
|
||||
$priv_key = null;
|
||||
$detail = array();
|
||||
if ( $key ) {
|
||||
\openssl_pkey_export( $key, $priv_key );
|
||||
|
||||
\openssl_pkey_export( $key, $priv_key );
|
||||
$detail = \openssl_pkey_get_details( $key );
|
||||
}
|
||||
|
||||
$detail = \openssl_pkey_get_details( $key );
|
||||
|
||||
// check if keys are valid
|
||||
// Check if keys are valid.
|
||||
if (
|
||||
empty( $priv_key ) || ! is_string( $priv_key ) ||
|
||||
! isset( $detail['key'] ) || ! is_string( $detail['key'] )
|
||||
@ -115,7 +123,7 @@ class Signature {
|
||||
'public_key' => $detail['key'],
|
||||
);
|
||||
|
||||
// persist keys
|
||||
// Persist keys.
|
||||
\add_option( $option_key, $key_pair );
|
||||
|
||||
return $key_pair;
|
||||
@ -133,7 +141,7 @@ class Signature {
|
||||
|
||||
if ( $user_id > 0 ) {
|
||||
$user = \get_userdata( $user_id );
|
||||
// sanatize username because it could include spaces and special chars
|
||||
// Sanitize username because it could include spaces and special chars.
|
||||
$id = sanitize_title( $user->user_login );
|
||||
}
|
||||
|
||||
@ -150,15 +158,15 @@ class Signature {
|
||||
protected static function check_legacy_key_pair_for( $user_id ) {
|
||||
switch ( $user_id ) {
|
||||
case 0:
|
||||
$public_key = \get_option( 'activitypub_blog_user_public_key' );
|
||||
$public_key = \get_option( 'activitypub_blog_user_public_key' );
|
||||
$private_key = \get_option( 'activitypub_blog_user_private_key' );
|
||||
break;
|
||||
case -1:
|
||||
$public_key = \get_option( 'activitypub_application_user_public_key' );
|
||||
$public_key = \get_option( 'activitypub_application_user_public_key' );
|
||||
$private_key = \get_option( 'activitypub_application_user_private_key' );
|
||||
break;
|
||||
default:
|
||||
$public_key = \get_user_meta( $user_id, 'magic_sig_public_key', true );
|
||||
$public_key = \get_user_meta( $user_id, 'magic_sig_public_key', true );
|
||||
$private_key = \get_user_meta( $user_id, 'magic_sig_private_key', true );
|
||||
break;
|
||||
}
|
||||
@ -174,18 +182,18 @@ class Signature {
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the Signature for a HTTP Request
|
||||
* Generates the Signature for an HTTP Request.
|
||||
*
|
||||
* @param int $user_id The WordPress User ID.
|
||||
* @param string $http_method The HTTP method.
|
||||
* @param string $url The URL to send the request to.
|
||||
* @param string $date The date the request is sent.
|
||||
* @param string $digest The digest of the request body.
|
||||
* @param string $digest Optional. The digest of the request body. Default null.
|
||||
*
|
||||
* @return string The signature.
|
||||
*/
|
||||
public static function generate_signature( $user_id, $http_method, $url, $date, $digest = null ) {
|
||||
$user = Users::get_by_id( $user_id );
|
||||
$user = Actors::get_by_id( $user_id );
|
||||
$key = self::get_private_key_for( $user->get__id() );
|
||||
|
||||
$url_parts = \wp_parse_url( $url );
|
||||
@ -193,12 +201,12 @@ class Signature {
|
||||
$host = $url_parts['host'];
|
||||
$path = '/';
|
||||
|
||||
// add path
|
||||
// Add path.
|
||||
if ( ! empty( $url_parts['path'] ) ) {
|
||||
$path = $url_parts['path'];
|
||||
}
|
||||
|
||||
// add query
|
||||
// Add query.
|
||||
if ( ! empty( $url_parts['query'] ) ) {
|
||||
$path .= '?' . $url_parts['query'];
|
||||
}
|
||||
@ -213,9 +221,9 @@ class Signature {
|
||||
|
||||
$signature = null;
|
||||
\openssl_sign( $signed_string, $signature, $key, \OPENSSL_ALGO_SHA256 );
|
||||
$signature = \base64_encode( $signature ); // phpcs:ignore
|
||||
$signature = \base64_encode( $signature ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
|
||||
$key_id = $user->get_url() . '#main-key';
|
||||
$key_id = $user->get_id() . '#main-key';
|
||||
|
||||
if ( ! empty( $digest ) ) {
|
||||
return \sprintf( 'keyId="%s",algorithm="rsa-sha256",headers="(request-target) host date digest",signature="%s"', $key_id, $signature );
|
||||
@ -229,18 +237,18 @@ class Signature {
|
||||
*
|
||||
* @param WP_REST_Request|array $request The request object or $_SERVER array.
|
||||
*
|
||||
* @return mixed A boolean or WP_Error.
|
||||
* @return bool|WP_Error A boolean or WP_Error.
|
||||
*/
|
||||
public static function verify_http_signature( $request ) {
|
||||
if ( is_object( $request ) ) { // REST Request object
|
||||
// check if route starts with "index.php"
|
||||
if ( is_object( $request ) ) { // REST Request object.
|
||||
// Check if route starts with "index.php".
|
||||
if ( str_starts_with( $request->get_route(), '/index.php' ) || ! rest_get_url_prefix() ) {
|
||||
$route = $request->get_route();
|
||||
} else {
|
||||
$route = '/' . rest_get_url_prefix() . '/' . ltrim( $request->get_route(), '/' );
|
||||
}
|
||||
|
||||
// fix route for subdirectory installs
|
||||
// Fix route for subdirectory installs.
|
||||
$path = \wp_parse_url( \get_home_url(), PHP_URL_PATH );
|
||||
|
||||
if ( \is_string( $path ) ) {
|
||||
@ -251,32 +259,23 @@ class Signature {
|
||||
$route = '/' . $path . $route;
|
||||
}
|
||||
|
||||
$headers = $request->get_headers();
|
||||
$headers = $request->get_headers();
|
||||
$headers['(request-target)'][0] = strtolower( $request->get_method() ) . ' ' . $route;
|
||||
} else {
|
||||
$request = self::format_server_request( $request );
|
||||
$headers = $request['headers']; // $_SERVER array
|
||||
$request = self::format_server_request( $request );
|
||||
$headers = $request['headers']; // $_SERVER array
|
||||
$headers['(request-target)'][0] = strtolower( $headers['request_method'][0] ) . ' ' . $headers['request_uri'][0];
|
||||
}
|
||||
|
||||
if ( ! isset( $headers['signature'] ) ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Request not signed', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
if ( array_key_exists( 'signature', $headers ) ) {
|
||||
$signature_block = self::parse_signature_header( $headers['signature'][0] );
|
||||
} elseif ( array_key_exists( 'authorization', $headers ) ) {
|
||||
$signature_block = self::parse_signature_header( $headers['authorization'][0] );
|
||||
}
|
||||
|
||||
if ( ! isset( $signature_block ) || ! $signature_block ) {
|
||||
} else {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Incompatible request signature. keyId and signature are required', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
|
||||
$signed_headers = $signature_block['headers'];
|
||||
if ( ! $signed_headers ) {
|
||||
$signed_headers = array( 'date' );
|
||||
}
|
||||
|
||||
$signed_data = self::get_signed_data( $signed_headers, $signature_block, $headers );
|
||||
if ( ! $signed_data ) {
|
||||
@ -313,7 +312,6 @@ class Signature {
|
||||
}
|
||||
|
||||
$verified = \openssl_verify( $signed_data, $signature_block['signature'], $public_key, $algorithm ) > 0;
|
||||
|
||||
if ( ! $verified ) {
|
||||
return new WP_Error( 'activitypub_signature', __( 'Invalid signature', 'activitypub' ), array( 'status' => 401 ) );
|
||||
}
|
||||
@ -321,14 +319,14 @@ class Signature {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get public key from key_id
|
||||
* Get public key from key_id.
|
||||
*
|
||||
* @param string $key_id The URL to the public key.
|
||||
*
|
||||
* @return WP_Error|string The public key or WP_Error.
|
||||
* @return resource|WP_Error The public key resource or WP_Error.
|
||||
*/
|
||||
public static function get_remote_key( $key_id ) { // phpcs:ignore
|
||||
$actor = get_remote_metadata_by_actor( strip_fragment_from_url( $key_id ) ); // phpcs:ignore
|
||||
public static function get_remote_key( $key_id ) {
|
||||
$actor = get_remote_metadata_by_actor( strip_fragment_from_url( $key_id ) );
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_no_remote_profile_found',
|
||||
@ -336,9 +334,14 @@ class Signature {
|
||||
array( 'status' => 401 )
|
||||
);
|
||||
}
|
||||
|
||||
if ( isset( $actor['publicKey']['publicKeyPem'] ) ) {
|
||||
return \rtrim( $actor['publicKey']['publicKeyPem'] ); // phpcs:ignore
|
||||
$key_resource = \openssl_pkey_get_public( \rtrim( $actor['publicKey']['publicKeyPem'] ) );
|
||||
if ( $key_resource ) {
|
||||
return $key_resource;
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_no_remote_key_found',
|
||||
__( 'No Public-Key found', 'activitypub' ),
|
||||
@ -347,9 +350,9 @@ class Signature {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the signature algorithm from the signature header
|
||||
* Gets the signature algorithm from the signature header.
|
||||
*
|
||||
* @param array $signature_block
|
||||
* @param array $signature_block The signature block.
|
||||
*
|
||||
* @return string The signature algorithm.
|
||||
*/
|
||||
@ -357,7 +360,7 @@ class Signature {
|
||||
if ( $signature_block['algorithm'] ) {
|
||||
switch ( $signature_block['algorithm'] ) {
|
||||
case 'rsa-sha-512':
|
||||
return 'sha512'; //hs2019 https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12
|
||||
return 'sha512'; // hs2019 https://datatracker.ietf.org/doc/html/draft-cavage-http-signatures-12.
|
||||
default:
|
||||
return 'sha256';
|
||||
}
|
||||
@ -366,23 +369,23 @@ class Signature {
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses the Signature header
|
||||
* Parses the Signature header.
|
||||
*
|
||||
* @param string $signature The signature header.
|
||||
*
|
||||
* @return array signature parts
|
||||
* @return array Signature parts.
|
||||
*/
|
||||
public static function parse_signature_header( $signature ) {
|
||||
$parsed_header = array();
|
||||
$matches = array();
|
||||
$parsed_header = array();
|
||||
$matches = array();
|
||||
|
||||
if ( \preg_match( '/keyId="(.*?)"/ism', $signature, $matches ) ) {
|
||||
$parsed_header['keyId'] = trim( $matches[1] );
|
||||
}
|
||||
if ( \preg_match( '/created=([0-9]*)/ism', $signature, $matches ) ) {
|
||||
if ( \preg_match( '/created=["|\']*([0-9]*)["|\']*/ism', $signature, $matches ) ) {
|
||||
$parsed_header['(created)'] = trim( $matches[1] );
|
||||
}
|
||||
if ( \preg_match( '/expires=([0-9]*)/ism', $signature, $matches ) ) {
|
||||
if ( \preg_match( '/expires=["|\']*([0-9]*)["|\']*/ism', $signature, $matches ) ) {
|
||||
$parsed_header['(expires)'] = trim( $matches[1] );
|
||||
}
|
||||
if ( \preg_match( '/algorithm="(.*?)"/ism', $signature, $matches ) ) {
|
||||
@ -392,10 +395,10 @@ class Signature {
|
||||
$parsed_header['headers'] = \explode( ' ', trim( $matches[1] ) );
|
||||
}
|
||||
if ( \preg_match( '/signature="(.*?)"/ism', $signature, $matches ) ) {
|
||||
$parsed_header['signature'] = \base64_decode( preg_replace( '/\s+/', '', trim( $matches[1] ) ) ); // phpcs:ignore
|
||||
$parsed_header['signature'] = \base64_decode( preg_replace( '/\s+/', '', trim( $matches[1] ) ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
|
||||
}
|
||||
|
||||
if ( ( $parsed_header['signature'] ) && ( $parsed_header['algorithm'] ) && ( ! $parsed_header['headers'] ) ) {
|
||||
if ( empty( $parsed_header['headers'] ) ) {
|
||||
$parsed_header['headers'] = array( 'date' );
|
||||
}
|
||||
|
||||
@ -403,16 +406,17 @@ class Signature {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the header data from the included pseudo headers
|
||||
* Gets the header data from the included pseudo headers.
|
||||
*
|
||||
* @param array $signed_headers The signed headers.
|
||||
* @param array $signature_block (pseudo-headers)
|
||||
* @param array $headers (http headers)
|
||||
* @param array $signature_block The signature block.
|
||||
* @param array $headers The HTTP headers.
|
||||
*
|
||||
* @return string signed headers for comparison
|
||||
*/
|
||||
public static function get_signed_data( $signed_headers, $signature_block, $headers ) {
|
||||
$signed_data = '';
|
||||
|
||||
// This also verifies time-based values by returning false if any of these are out of range.
|
||||
foreach ( $signed_headers as $header ) {
|
||||
if ( 'host' === $header ) {
|
||||
@ -431,52 +435,69 @@ class Signature {
|
||||
}
|
||||
if ( '(created)' === $header ) {
|
||||
if ( ! empty( $signature_block['(created)'] ) && \intval( $signature_block['(created)'] ) > \time() ) {
|
||||
// created in future
|
||||
// Created in the future.
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! array_key_exists( '(created)', $headers ) ) {
|
||||
$signed_data .= $header . ': ' . $signature_block['(created)'] . "\n";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ( '(expires)' === $header ) {
|
||||
if ( ! empty( $signature_block['(expires)'] ) && \intval( $signature_block['(expires)'] ) < \time() ) {
|
||||
// expired in past
|
||||
// Expired in the past.
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! array_key_exists( '(expires)', $headers ) ) {
|
||||
$signed_data .= $header . ': ' . $signature_block['(expires)'] . "\n";
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if ( 'date' === $header ) {
|
||||
// allow a bit of leeway for misconfigured clocks.
|
||||
if ( empty( $headers[ $header ][0] ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Allow a bit of leeway for misconfigured clocks.
|
||||
$d = new DateTime( $headers[ $header ][0] );
|
||||
$d->setTimeZone( new DateTimeZone( 'UTC' ) );
|
||||
$c = $d->format( 'U' );
|
||||
|
||||
$dplus = time() + ( 3 * HOUR_IN_SECONDS );
|
||||
$dplus = time() + ( 3 * HOUR_IN_SECONDS );
|
||||
$dminus = time() - ( 3 * HOUR_IN_SECONDS );
|
||||
|
||||
if ( $c > $dplus || $c < $dminus ) {
|
||||
// time out of range
|
||||
// Time out of range.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
$signed_data .= $header . ': ' . $headers[ $header ][0] . "\n";
|
||||
|
||||
if ( ! empty( $headers[ $header ][0] ) ) {
|
||||
$signed_data .= $header . ': ' . $headers[ $header ][0] . "\n";
|
||||
}
|
||||
}
|
||||
return \rtrim( $signed_data, "\n" );
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates the digest for a HTTP Request
|
||||
* Generates the digest for an HTTP Request.
|
||||
*
|
||||
* @param string $body The body of the request.
|
||||
*
|
||||
* @return string The digest.
|
||||
*/
|
||||
public static function generate_digest( $body ) {
|
||||
$digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore
|
||||
$digest = \base64_encode( \hash( 'sha256', $body, true ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
|
||||
return "SHA-256=$digest";
|
||||
}
|
||||
|
||||
/**
|
||||
* Formats the $_SERVER to resemble the WP_REST_REQUEST array,
|
||||
* for use with verify_http_signature()
|
||||
* for use with verify_http_signature().
|
||||
*
|
||||
* @param array $_SERVER The $_SERVER array.
|
||||
* @param array $server The $_SERVER array.
|
||||
*
|
||||
* @return array $request The formatted request array.
|
||||
*/
|
||||
@ -487,7 +508,7 @@ class Signature {
|
||||
if ( 'REQUEST_URI' === $req_param ) {
|
||||
$request['headers']['route'][] = $param_val;
|
||||
} else {
|
||||
$header_key = str_replace(
|
||||
$header_key = str_replace(
|
||||
'http_',
|
||||
'',
|
||||
$req_param
|
||||
|
@ -1,11 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* WebFinger class file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
|
||||
/**
|
||||
* ActivityPub WebFinger Class
|
||||
* ActivityPub WebFinger Class.
|
||||
*
|
||||
* @author Matthias Pfefferle
|
||||
*
|
||||
@ -13,14 +19,14 @@ use Activitypub\Collection\Users;
|
||||
*/
|
||||
class Webfinger {
|
||||
/**
|
||||
* Returns a users WebFinger "resource"
|
||||
* Returns a users WebFinger "resource".
|
||||
*
|
||||
* @param int $user_id The WordPress user id
|
||||
* @param int $user_id The WordPress user id.
|
||||
*
|
||||
* @return string The user-resource
|
||||
* @return string The user-resource.
|
||||
*/
|
||||
public static function get_user_resource( $user_id ) {
|
||||
$user = Users::get_by_id( $user_id );
|
||||
$user = Actors::get_by_id( $user_id );
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
return '';
|
||||
}
|
||||
@ -29,11 +35,11 @@ class Webfinger {
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolve a WebFinger resource
|
||||
* Resolve a WebFinger resource.
|
||||
*
|
||||
* @param string $uri The WebFinger Resource
|
||||
* @param string $uri The WebFinger Resource.
|
||||
*
|
||||
* @return string|WP_Error The URL or WP_Error
|
||||
* @return string|WP_Error The URL or WP_Error.
|
||||
*/
|
||||
public static function resolve( $uri ) {
|
||||
$data = self::get_data( $uri );
|
||||
@ -46,13 +52,17 @@ class Webfinger {
|
||||
return new WP_Error(
|
||||
'webfinger_missing_links',
|
||||
__( 'No valid Link elements found.', 'activitypub' ),
|
||||
$data
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
foreach ( $data['links'] as $link ) {
|
||||
if (
|
||||
'self' === $link['rel'] &&
|
||||
isset( $link['type'] ) &&
|
||||
(
|
||||
'application/activity+json' === $link['type'] ||
|
||||
'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' === $link['type']
|
||||
@ -65,16 +75,21 @@ class Webfinger {
|
||||
return new WP_Error(
|
||||
'webfinger_url_no_activitypub',
|
||||
__( 'The Site supports WebFinger but not ActivityPub', 'activitypub' ),
|
||||
$data
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a URI to an acct <identifier>@<host>
|
||||
* Transform a URI to an acct <identifier>@<host>.
|
||||
*
|
||||
* @param string $uri The URI (acct:, mailto:, http:, https:)
|
||||
* @see https://swicg.github.io/activitypub-webfinger/#reverse-discovery
|
||||
*
|
||||
* @return string|WP_Error Error or acct URI
|
||||
* @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 );
|
||||
@ -83,7 +98,7 @@ class Webfinger {
|
||||
return $data;
|
||||
}
|
||||
|
||||
// check if subject is an acct URI
|
||||
// Check if subject is an acct URI.
|
||||
if (
|
||||
isset( $data['subject'] ) &&
|
||||
\str_starts_with( $data['subject'], 'acct:' )
|
||||
@ -91,7 +106,7 @@ class Webfinger {
|
||||
return $data['subject'];
|
||||
}
|
||||
|
||||
// search for an acct URI in the aliases
|
||||
// Search for an acct URI in the aliases.
|
||||
if ( isset( $data['aliases'] ) ) {
|
||||
foreach ( $data['aliases'] as $alias ) {
|
||||
if ( \str_starts_with( $alias, 'acct:' ) ) {
|
||||
@ -103,7 +118,10 @@ class Webfinger {
|
||||
return new WP_Error(
|
||||
'webfinger_url_no_acct',
|
||||
__( 'No acct URI found.', 'activitypub' ),
|
||||
$data
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -111,21 +129,31 @@ class Webfinger {
|
||||
* Convert a URI string to an identifier and its host.
|
||||
* Automatically adds acct: if it's missing.
|
||||
*
|
||||
* @param string $url The URI (acct:, mailto:, http:, https:)
|
||||
* @param string $url The URI (acct:, mailto:, http:, https:).
|
||||
*
|
||||
* @return WP_Error|array Error reaction or array with
|
||||
* identifier and host as values
|
||||
* @return WP_Error|array Error reaction or array with identifier and host as values.
|
||||
*/
|
||||
public static function get_identifier_and_host( $url ) {
|
||||
// remove leading @
|
||||
if ( ! $url ) {
|
||||
return new WP_Error(
|
||||
'webfinger_invalid_identifier',
|
||||
__( 'Invalid Identifier', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// Remove leading @.
|
||||
$url = ltrim( $url, '@' );
|
||||
|
||||
if ( ! preg_match( '/^([a-zA-Z+]+):/', $url, $match ) ) {
|
||||
$identifier = 'acct:' . $url;
|
||||
$scheme = 'acct';
|
||||
$scheme = 'acct';
|
||||
} else {
|
||||
$identifier = $url;
|
||||
$scheme = $match[1];
|
||||
$scheme = $match[1];
|
||||
}
|
||||
|
||||
$host = null;
|
||||
@ -144,19 +172,25 @@ class Webfinger {
|
||||
}
|
||||
|
||||
if ( empty( $host ) ) {
|
||||
return new WP_Error( 'webfinger_invalid_identifier', __( 'Invalid Identifier', 'activitypub' ) );
|
||||
return new WP_Error(
|
||||
'webfinger_invalid_identifier',
|
||||
__( 'Invalid Identifier', 'activitypub' ),
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return array( $identifier, $host );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the WebFinger data for a given URI
|
||||
* Get the WebFinger data for a given URI.
|
||||
*
|
||||
* @param string $uri The Identifier: <identifier>@<host> or URI
|
||||
* @param string $uri The Identifier: <identifier>@<host> or URI.
|
||||
*
|
||||
* @return WP_Error|array Error reaction or array with
|
||||
* identifier and host as values
|
||||
* @return WP_Error|array Error reaction or array with identifier and host as values.
|
||||
*/
|
||||
public static function get_data( $uri ) {
|
||||
$identifier_and_host = self::get_identifier_and_host( $uri );
|
||||
@ -174,7 +208,11 @@ class Webfinger {
|
||||
return $data;
|
||||
}
|
||||
|
||||
$webfinger_url = 'https://' . $host . '/.well-known/webfinger?resource=' . rawurlencode( $identifier );
|
||||
$webfinger_url = sprintf(
|
||||
'https://%s/.well-known/webfinger?resource=%s',
|
||||
$host,
|
||||
rawurlencode( $identifier )
|
||||
);
|
||||
|
||||
$response = wp_safe_remote_get(
|
||||
$webfinger_url,
|
||||
@ -187,7 +225,10 @@ class Webfinger {
|
||||
return new WP_Error(
|
||||
'webfinger_url_not_accessible',
|
||||
__( 'The WebFinger Resource is not accessible.', 'activitypub' ),
|
||||
$webfinger_url
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $webfinger_url,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -200,7 +241,9 @@ class Webfinger {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Remote-Follow endpoint for a given URI
|
||||
* Get the Remote-Follow endpoint for a given URI.
|
||||
*
|
||||
* @param string $uri The WebFinger Resource URI.
|
||||
*
|
||||
* @return string|WP_Error Error or the Remote-Follow endpoint URI.
|
||||
*/
|
||||
@ -215,7 +258,10 @@ class Webfinger {
|
||||
return new WP_Error(
|
||||
'webfinger_missing_links',
|
||||
__( 'No valid Link elements found.', 'activitypub' ),
|
||||
$data
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@ -228,16 +274,19 @@ class Webfinger {
|
||||
return new WP_Error(
|
||||
'webfinger_missing_remote_follow_endpoint',
|
||||
__( 'No valid Remote-Follow endpoint found.', 'activitypub' ),
|
||||
$data
|
||||
array(
|
||||
'status' => 400,
|
||||
'data' => $data,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a cache key for a given URI
|
||||
* Generate a cache key for a given URI.
|
||||
*
|
||||
* @param string $uri A WebFinger Resource URI
|
||||
* @param string $uri A WebFinger Resource URI.
|
||||
*
|
||||
* @return string The cache key
|
||||
* @return string The cache key.
|
||||
*/
|
||||
public static function generate_cache_key( $uri ) {
|
||||
$uri = ltrim( $uri, '@' );
|
||||
|
@ -0,0 +1,378 @@
|
||||
<?php
|
||||
/**
|
||||
* Actors collection file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use WP_Error;
|
||||
use WP_User_Query;
|
||||
use Activitypub\Model\User;
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Model\Application;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\normalize_url;
|
||||
use function Activitypub\normalize_host;
|
||||
use function Activitypub\url_to_authorid;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
use function Activitypub\user_can_activitypub;
|
||||
|
||||
/**
|
||||
* Actors collection.
|
||||
*/
|
||||
class Actors {
|
||||
/**
|
||||
* The ID of the Blog User.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const BLOG_USER_ID = 0;
|
||||
|
||||
/**
|
||||
* The ID of the Application User.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const APPLICATION_USER_ID = -1;
|
||||
|
||||
/**
|
||||
* Get the Actor by ID.
|
||||
*
|
||||
* @param int $user_id The User-ID.
|
||||
*
|
||||
* @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_id( $user_id ) {
|
||||
if ( is_numeric( $user_id ) ) {
|
||||
$user_id = (int) $user_id;
|
||||
}
|
||||
|
||||
if ( ! user_can_activitypub( $user_id ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'Actor not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
switch ( $user_id ) {
|
||||
case self::BLOG_USER_ID:
|
||||
return new Blog();
|
||||
case self::APPLICATION_USER_ID:
|
||||
return new Application();
|
||||
default:
|
||||
return User::from_wp_user( $user_id );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Actor by username.
|
||||
*
|
||||
* @param string $username Name of the Actor.
|
||||
*
|
||||
* @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_username( $username ) {
|
||||
/**
|
||||
* Filter the username before we do anything else.
|
||||
*
|
||||
* @param null $pre The pre-existing value.
|
||||
* @param string $username The username.
|
||||
*/
|
||||
$pre = apply_filters( 'activitypub_pre_get_by_username', null, $username );
|
||||
if ( null !== $pre ) {
|
||||
return $pre;
|
||||
}
|
||||
|
||||
// Check for blog user.
|
||||
if ( Blog::get_default_username() === $username ) {
|
||||
return new Blog();
|
||||
}
|
||||
|
||||
if ( get_option( 'activitypub_blog_identifier' ) === $username ) {
|
||||
return new Blog();
|
||||
}
|
||||
|
||||
// Check for application user.
|
||||
if ( 'application' === $username ) {
|
||||
return new Application();
|
||||
}
|
||||
|
||||
// Check for 'activitypub_username' meta.
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'count_total' => false,
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => '_activitypub_user_identifier',
|
||||
'value' => $username,
|
||||
'compare' => 'LIKE',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
$actor = self::get_by_id( $user->results[0] );
|
||||
if ( ! \is_wp_error( $actor ) ) {
|
||||
return $actor;
|
||||
}
|
||||
}
|
||||
|
||||
$username = str_replace( array( '*', '%' ), '', $username );
|
||||
|
||||
// Check for login or nicename.
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'count_total' => false,
|
||||
'search' => $username,
|
||||
'search_columns' => array( 'user_login', 'user_nicename' ),
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
$actor = self::get_by_id( $user->results[0] );
|
||||
if ( ! \is_wp_error( $actor ) ) {
|
||||
return $actor;
|
||||
}
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'Actor not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Actor by resource.
|
||||
*
|
||||
* @param string $uri The Actor resource.
|
||||
*
|
||||
* @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_resource( $uri ) {
|
||||
$uri = object_to_uri( $uri );
|
||||
|
||||
if ( ! $uri ) {
|
||||
return new WP_Error(
|
||||
'activitypub_no_uri',
|
||||
\__( 'No URI provided', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
$scheme = 'acct';
|
||||
$match = array();
|
||||
// Try to extract the scheme and the host.
|
||||
if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $uri, $match ) ) {
|
||||
// Extract the scheme.
|
||||
$scheme = \esc_attr( $match[1] );
|
||||
}
|
||||
|
||||
// @todo: handle old domain URIs here before we serve a new domain below when we shouldn't.
|
||||
// Although maybe passing through to ::get_by_username() is enough?
|
||||
|
||||
switch ( $scheme ) {
|
||||
// Check for http(s) URIs.
|
||||
case 'http':
|
||||
case 'https':
|
||||
$resource_path = \wp_parse_url( $uri, PHP_URL_PATH );
|
||||
|
||||
if ( $resource_path ) {
|
||||
$blog_path = \wp_parse_url( \home_url(), PHP_URL_PATH );
|
||||
|
||||
if ( $blog_path ) {
|
||||
$resource_path = \str_replace( $blog_path, '', $resource_path );
|
||||
}
|
||||
|
||||
$resource_path = \trim( $resource_path, '/' );
|
||||
|
||||
// Check for http(s)://blog.example.com/@username.
|
||||
if ( str_starts_with( $resource_path, '@' ) ) {
|
||||
$identifier = \str_replace( '@', '', $resource_path );
|
||||
$identifier = \trim( $identifier, '/' );
|
||||
|
||||
return self::get_by_username( $identifier );
|
||||
}
|
||||
}
|
||||
|
||||
// Check for http(s)://blog.example.com/author/username.
|
||||
$user_id = url_to_authorid( $uri );
|
||||
|
||||
if ( \is_int( $user_id ) ) {
|
||||
return self::get_by_id( $user_id );
|
||||
}
|
||||
|
||||
// Check for http(s)://blog.example.com/.
|
||||
$normalized_uri = normalize_url( $uri );
|
||||
|
||||
if (
|
||||
normalize_url( site_url() ) === $normalized_uri ||
|
||||
normalize_url( home_url() ) === $normalized_uri
|
||||
) {
|
||||
return self::get_by_id( self::BLOG_USER_ID );
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_no_user_found',
|
||||
\__( 'Actor not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
// Check for acct URIs.
|
||||
case 'acct':
|
||||
$uri = \str_replace( 'acct:', '', $uri );
|
||||
$identifier = \substr( $uri, 0, \strrpos( $uri, '@' ) );
|
||||
$host = normalize_host( \substr( \strrchr( $uri, '@' ), 1 ) );
|
||||
$blog_host = normalize_host( \wp_parse_url( \home_url( '/' ), \PHP_URL_HOST ) );
|
||||
|
||||
if ( $blog_host !== $host && get_option( 'activitypub_old_host' ) !== $host ) {
|
||||
return new WP_Error(
|
||||
'activitypub_wrong_host',
|
||||
\__( 'Resource host does not match blog host', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// Prepare wildcards https://github.com/mastodon/mastodon/issues/22213.
|
||||
if ( in_array( $identifier, array( '_', '*', '' ), true ) ) {
|
||||
return self::get_by_id( self::BLOG_USER_ID );
|
||||
}
|
||||
|
||||
return self::get_by_username( $identifier );
|
||||
default:
|
||||
return new WP_Error(
|
||||
'activitypub_wrong_scheme',
|
||||
\__( 'Wrong scheme', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Actor by resource.
|
||||
*
|
||||
* @param string $id The Actor resource.
|
||||
*
|
||||
* @return User|Blog|Application|WP_Error The Actor or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_various( $id ) {
|
||||
$user = null;
|
||||
|
||||
if ( is_numeric( $id ) ) {
|
||||
$user = self::get_by_id( $id );
|
||||
} elseif (
|
||||
// Is URL.
|
||||
filter_var( $id, FILTER_VALIDATE_URL ) ||
|
||||
// Is acct.
|
||||
str_starts_with( $id, 'acct:' ) ||
|
||||
// Is email.
|
||||
filter_var( $id, FILTER_VALIDATE_EMAIL )
|
||||
) {
|
||||
$user = self::get_by_resource( $id );
|
||||
} else {
|
||||
$user = self::get_by_username( $id );
|
||||
}
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Actor collection.
|
||||
*
|
||||
* @return array The Actor collection.
|
||||
*/
|
||||
public static function get_collection() {
|
||||
if ( is_user_type_disabled( 'user' ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
|
||||
$return = array();
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$actor = User::from_wp_user( $user->ID );
|
||||
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$return[] = $actor;
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all active Actors including the Blog Actor.
|
||||
*
|
||||
* @return array The actor collection.
|
||||
*/
|
||||
public static function get_all() {
|
||||
$return = array();
|
||||
|
||||
if ( ! is_user_type_disabled( 'user' ) ) {
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$actor = User::from_wp_user( $user->ID );
|
||||
|
||||
if ( \is_wp_error( $actor ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$return[] = $actor;
|
||||
}
|
||||
}
|
||||
|
||||
// Also include the blog actor if active.
|
||||
if ( ! is_user_type_disabled( 'blog' ) ) {
|
||||
$blog_actor = self::get_by_id( self::BLOG_USER_ID );
|
||||
if ( ! \is_wp_error( $blog_actor ) ) {
|
||||
$return[] = $blog_actor;
|
||||
}
|
||||
}
|
||||
|
||||
return $return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the actor type based on the user ID.
|
||||
*
|
||||
* @param int $user_id The user ID to check.
|
||||
* @return string The user type.
|
||||
*/
|
||||
public static function get_type_by_id( $user_id ) {
|
||||
$user_id = (int) $user_id;
|
||||
|
||||
if ( self::APPLICATION_USER_ID === $user_id ) {
|
||||
return 'application';
|
||||
}
|
||||
|
||||
if ( self::BLOG_USER_ID === $user_id ) {
|
||||
return 'blog';
|
||||
}
|
||||
|
||||
return 'user';
|
||||
}
|
||||
}
|
@ -0,0 +1,300 @@
|
||||
<?php
|
||||
/**
|
||||
* Extra Fields collection file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use Activitypub\Link;
|
||||
|
||||
use function Activitypub\site_supports_blocks;
|
||||
|
||||
/**
|
||||
* Extra Fields collection.
|
||||
*/
|
||||
class Extra_Fields {
|
||||
|
||||
const USER_POST_TYPE = 'ap_extrafield';
|
||||
const BLOG_POST_TYPE = 'ap_extrafield_blog';
|
||||
|
||||
/**
|
||||
* Get the extra fields for a user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
*
|
||||
* @return \WP_Post[] The extra fields.
|
||||
*/
|
||||
public static function get_actor_fields( $user_id ) {
|
||||
$is_blog = self::is_blog( $user_id );
|
||||
$post_type = $is_blog ? self::BLOG_POST_TYPE : self::USER_POST_TYPE;
|
||||
$args = array(
|
||||
'post_type' => $post_type,
|
||||
'nopaging' => true,
|
||||
'orderby' => 'menu_order',
|
||||
'order' => 'ASC',
|
||||
);
|
||||
if ( ! $is_blog ) {
|
||||
$args['author'] = $user_id;
|
||||
}
|
||||
|
||||
$query = new \WP_Query( $args );
|
||||
$fields = $query->posts ?? array();
|
||||
|
||||
/**
|
||||
* Filters the extra fields for an ActivityPub actor.
|
||||
*
|
||||
* This filter allows developers to modify or add custom fields to an actor's
|
||||
* profile.
|
||||
*
|
||||
* @param \WP_Post[] $fields Array of WP_Post objects representing the extra fields.
|
||||
* @param int $user_id The ID of the user whose fields are being retrieved.
|
||||
*/
|
||||
return apply_filters( 'activitypub_get_actor_extra_fields', $fields, $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get formatted content for an extra field.
|
||||
*
|
||||
* @param \WP_Post $post The post.
|
||||
*
|
||||
* @return string The formatted content.
|
||||
*/
|
||||
public static function get_formatted_content( $post ) {
|
||||
$content = \get_the_content( null, false, $post );
|
||||
$content = Link::the_content( $content );
|
||||
if ( site_supports_blocks() ) {
|
||||
$content = \do_blocks( $content );
|
||||
}
|
||||
$content = \wptexturize( $content );
|
||||
$content = \wp_filter_content_tags( $content );
|
||||
|
||||
// Replace script and style elements.
|
||||
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
|
||||
$content = \strip_shortcodes( $content );
|
||||
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
|
||||
|
||||
/**
|
||||
* Filters the content of an extra field.
|
||||
*
|
||||
* @param string $content The content.
|
||||
* @param \WP_Post $post The post.
|
||||
*/
|
||||
return \apply_filters( 'activitypub_extra_field_content', $content, $post );
|
||||
}
|
||||
|
||||
/**
|
||||
* Transforms the Extra Fields (Custom Post Types) to ActivityPub Actor-Attachments.
|
||||
*
|
||||
* @param \WP_Post[] $fields The extra fields.
|
||||
*
|
||||
* @return array ActivityPub attachments.
|
||||
*/
|
||||
public static function fields_to_attachments( $fields ) {
|
||||
$attachments = array();
|
||||
\add_filter( 'activitypub_link_rel', array( self::class, 'add_rel_me' ) );
|
||||
|
||||
foreach ( $fields as $post ) {
|
||||
$content = self::get_formatted_content( $post );
|
||||
$attachments[] = array(
|
||||
'type' => 'PropertyValue',
|
||||
'name' => \get_the_title( $post ),
|
||||
'value' => \html_entity_decode(
|
||||
$content,
|
||||
\ENT_QUOTES,
|
||||
'UTF-8'
|
||||
),
|
||||
);
|
||||
|
||||
$attachment = false;
|
||||
|
||||
// Add support for FEP-fb2a, for more information see FEDERATION.md.
|
||||
$link_content = \trim( \strip_tags( $content, '<a>' ) );
|
||||
if (
|
||||
\stripos( $link_content, '<a' ) === 0 &&
|
||||
\stripos( $link_content, '<a', 3 ) === false &&
|
||||
\stripos( $link_content, '</a>', \strlen( $link_content ) - 4 ) !== false &&
|
||||
\class_exists( '\WP_HTML_Tag_Processor' )
|
||||
) {
|
||||
$tags = new \WP_HTML_Tag_Processor( $link_content );
|
||||
$tags->next_tag( 'A' );
|
||||
|
||||
if ( 'A' === $tags->get_tag() ) {
|
||||
$attachment = array(
|
||||
'type' => 'Link',
|
||||
'name' => \get_the_title( $post ),
|
||||
'href' => \esc_url( $tags->get_attribute( 'href' ) ),
|
||||
);
|
||||
|
||||
$rel = $tags->get_attribute( 'rel' );
|
||||
|
||||
if ( $rel && \is_string( $rel ) ) {
|
||||
$attachment['rel'] = \explode( ' ', $rel );
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! $attachment ) {
|
||||
$attachment = array(
|
||||
'type' => 'Note',
|
||||
'name' => \get_the_title( $post ),
|
||||
'content' => \html_entity_decode(
|
||||
$content,
|
||||
\ENT_QUOTES,
|
||||
'UTF-8'
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
$attachments[] = $attachment;
|
||||
}
|
||||
|
||||
\remove_filter( 'activitypub_link_rel', array( self::class, 'add_rel_me' ) );
|
||||
|
||||
return $attachments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a post type is an extra fields post type.
|
||||
*
|
||||
* @param string $post_type The post type.
|
||||
*
|
||||
* @return bool True if the post type is an extra fields post type, otherwise false.
|
||||
*/
|
||||
public static function is_extra_fields_post_type( $post_type ) {
|
||||
return \in_array( $post_type, array( self::USER_POST_TYPE, self::BLOG_POST_TYPE ), true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a post type is the `ap_extrafield` post type.
|
||||
*
|
||||
* @param string $post_type The post type.
|
||||
*
|
||||
* @return bool True if the post type is `ap_extrafield`, otherwise false.
|
||||
*/
|
||||
public static function is_extra_field_post_type( $post_type ) {
|
||||
return self::USER_POST_TYPE === $post_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a post type is the `ap_extrafield_blog` post type.
|
||||
*
|
||||
* @param string $post_type The post type.
|
||||
*
|
||||
* @return bool True if the post type is `ap_extrafield_blog`, otherwise false.
|
||||
*/
|
||||
public static function is_extra_field_blog_post_type( $post_type ) {
|
||||
return self::BLOG_POST_TYPE === $post_type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add default extra fields to an actor.
|
||||
*
|
||||
* @param array $extra_fields The extra fields.
|
||||
* @param int $user_id The User-ID.
|
||||
*
|
||||
* @return array The extra fields.
|
||||
*/
|
||||
public static function default_actor_extra_fields( $extra_fields, $user_id ) {
|
||||
// We'll only take action when there are none yet.
|
||||
if ( ! empty( $extra_fields ) ) {
|
||||
return $extra_fields;
|
||||
}
|
||||
|
||||
$is_blog = self::is_blog( $user_id );
|
||||
$already_migrated = $is_blog
|
||||
? \get_option( 'activitypub_default_extra_fields' )
|
||||
: \get_user_meta( $user_id, 'activitypub_default_extra_fields', true );
|
||||
|
||||
if ( $already_migrated ) {
|
||||
return $extra_fields;
|
||||
}
|
||||
|
||||
\add_filter(
|
||||
'activitypub_link_rel',
|
||||
function ( $rel ) {
|
||||
$rel .= ' me';
|
||||
|
||||
return $rel;
|
||||
}
|
||||
);
|
||||
|
||||
$defaults = array(
|
||||
\__( 'Blog', 'activitypub' ) => \home_url( '/' ),
|
||||
);
|
||||
|
||||
if ( ! $is_blog ) {
|
||||
$author_url = \get_the_author_meta( 'user_url', $user_id );
|
||||
$author_posts_url = \get_author_posts_url( $user_id );
|
||||
|
||||
$defaults[ \__( 'Profile', 'activitypub' ) ] = $author_posts_url;
|
||||
if ( $author_url !== $author_posts_url ) {
|
||||
$defaults[ \__( 'Homepage', 'activitypub' ) ] = $author_url;
|
||||
}
|
||||
}
|
||||
|
||||
$post_type = $is_blog ? self::BLOG_POST_TYPE : self::USER_POST_TYPE;
|
||||
$menu_order = 10;
|
||||
|
||||
foreach ( $defaults as $title => $url ) {
|
||||
if ( ! $url ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$extra_field = array(
|
||||
'post_type' => $post_type,
|
||||
'post_title' => $title,
|
||||
'post_status' => 'publish',
|
||||
'post_author' => $user_id,
|
||||
'post_content' => self::make_paragraph_block( Link::the_content( $url ) ),
|
||||
'comment_status' => 'closed',
|
||||
'menu_order' => $menu_order,
|
||||
);
|
||||
|
||||
$menu_order += 10;
|
||||
$extra_field_id = wp_insert_post( $extra_field );
|
||||
$extra_fields[] = get_post( $extra_field_id );
|
||||
}
|
||||
|
||||
$is_blog
|
||||
? \update_option( 'activitypub_default_extra_fields', true )
|
||||
: \update_user_meta( $user_id, 'activitypub_default_extra_fields', true );
|
||||
|
||||
return $extra_fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a paragraph block.
|
||||
*
|
||||
* @param string $content The content.
|
||||
*
|
||||
* @return string The paragraph block.
|
||||
*/
|
||||
public static function make_paragraph_block( $content ) {
|
||||
if ( ! site_supports_blocks() ) {
|
||||
return $content;
|
||||
}
|
||||
return '<!-- wp:paragraph --><p>' . $content . '</p><!-- /wp:paragraph -->';
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the 'me' rel to the link.
|
||||
*
|
||||
* @param string $rel The rel attribute.
|
||||
* @return string The modified rel attribute.
|
||||
*/
|
||||
public static function add_rel_me( $rel ) {
|
||||
return $rel . ' me';
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the user is the blog user.
|
||||
*
|
||||
* @param int $user_id The user ID.
|
||||
* @return bool True if the user is the blog user, otherwise false.
|
||||
*/
|
||||
private static function is_blog( $user_id ) {
|
||||
return Actors::BLOG_USER_ID === $user_id;
|
||||
}
|
||||
}
|
@ -1,32 +1,36 @@
|
||||
<?php
|
||||
/**
|
||||
* Followers collection file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use Activitypub\Model\Follower;
|
||||
use WP_Error;
|
||||
use WP_Query;
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Webfinger;
|
||||
use Activitypub\Model\Follower;
|
||||
|
||||
use function Activitypub\is_tombstone;
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
|
||||
/**
|
||||
* ActivityPub Followers Collection
|
||||
* ActivityPub Followers Collection.
|
||||
*
|
||||
* @author Matt Wiebe
|
||||
* @author Matthias Pfefferle
|
||||
*/
|
||||
class Followers {
|
||||
const POST_TYPE = 'ap_follower';
|
||||
const POST_TYPE = 'ap_follower';
|
||||
const CACHE_KEY_INBOXES = 'follower_inboxes_%s';
|
||||
|
||||
/**
|
||||
* Add new Follower
|
||||
* Add new Follower.
|
||||
*
|
||||
* @param int $user_id The ID of the WordPress User
|
||||
* @param string $actor The Actor URL
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param string $actor The Actor URL.
|
||||
*
|
||||
* @return array|WP_Error The Follower (WP_Post array) or an WP_Error
|
||||
* @return Follower|WP_Error The Follower (WP_Post array) or an WP_Error.
|
||||
*/
|
||||
public static function add_follower( $user_id, $actor ) {
|
||||
$meta = get_remote_metadata_by_actor( $actor );
|
||||
@ -48,11 +52,11 @@ class Followers {
|
||||
return $id;
|
||||
}
|
||||
|
||||
$post_meta = get_post_meta( $id, 'activitypub_user_id' );
|
||||
$post_meta = get_post_meta( $id, '_activitypub_user_id', false );
|
||||
|
||||
// phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
|
||||
if ( is_array( $post_meta ) && ! in_array( $user_id, $post_meta ) ) {
|
||||
add_post_meta( $id, 'activitypub_user_id', $user_id );
|
||||
add_post_meta( $id, '_activitypub_user_id', $user_id );
|
||||
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
|
||||
}
|
||||
|
||||
@ -60,12 +64,12 @@ class Followers {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a Follower
|
||||
* Remove a Follower.
|
||||
*
|
||||
* @param int $user_id The ID of the WordPress User
|
||||
* @param string $actor The Actor URL
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param string $actor The Actor URL.
|
||||
*
|
||||
* @return bool|WP_Error True on success, false or WP_Error on failure.
|
||||
* @return bool True on success, false on failure.
|
||||
*/
|
||||
public static function remove_follower( $user_id, $actor ) {
|
||||
wp_cache_delete( sprintf( self::CACHE_KEY_INBOXES, $user_id ), 'activitypub' );
|
||||
@ -76,24 +80,33 @@ class Followers {
|
||||
return false;
|
||||
}
|
||||
|
||||
return delete_post_meta( $follower->get__id(), 'activitypub_user_id', $user_id );
|
||||
/**
|
||||
* Fires before a Follower is removed.
|
||||
*
|
||||
* @param Follower $follower The Follower object.
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param string $actor The Actor URL.
|
||||
*/
|
||||
do_action( 'activitypub_followers_pre_remove_follower', $follower, $user_id, $actor );
|
||||
|
||||
return delete_post_meta( $follower->get__id(), '_activitypub_user_id', $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Follower.
|
||||
*
|
||||
* @param int $user_id The ID of the WordPress User
|
||||
* @param string $actor The Actor URL
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param string $actor The Actor URL.
|
||||
*
|
||||
* @return \Activitypub\Model\Follower|null The Follower object or null
|
||||
* @return Follower|false|null The Follower object or null
|
||||
*/
|
||||
public static function get_follower( $user_id, $actor ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$post_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = 'activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s",
|
||||
"SELECT DISTINCT p.ID FROM $wpdb->posts p INNER JOIN $wpdb->postmeta pm ON p.ID = pm.post_id WHERE p.post_type = %s AND pm.meta_key = '_activitypub_user_id' AND pm.meta_value = %d AND p.guid = %s",
|
||||
array(
|
||||
esc_sql( self::POST_TYPE ),
|
||||
esc_sql( $user_id ),
|
||||
@ -111,16 +124,16 @@ class Followers {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a Follower by Actor indepenent from the User.
|
||||
* Get a Follower by Actor independent of the User.
|
||||
*
|
||||
* @param string $actor The Actor URL.
|
||||
*
|
||||
* @return \Activitypub\Model\Follower|null The Follower object or null
|
||||
* @return Follower|false|null The Follower object or false on failure.
|
||||
*/
|
||||
public static function get_follower_by_actor( $actor ) {
|
||||
global $wpdb;
|
||||
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery, WordPress.DB.DirectDatabaseQuery.NoCaching
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
|
||||
$post_id = $wpdb->get_var(
|
||||
$wpdb->prepare(
|
||||
"SELECT ID FROM $wpdb->posts WHERE guid=%s",
|
||||
@ -137,13 +150,13 @@ class Followers {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Followers of a given user
|
||||
* Get the Followers of a given user.
|
||||
*
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param int $number Maximum number of results to return.
|
||||
* @param int $page Page number.
|
||||
* @param array $args The WP_Query arguments.
|
||||
* @return array List of `Follower` objects.
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param int $number Maximum number of results to return.
|
||||
* @param int $page Page number.
|
||||
* @param array $args The WP_Query arguments.
|
||||
* @return Follower[] List of `Follower` objects.
|
||||
*/
|
||||
public static function get_followers( $user_id, $number = -1, $page = null, $args = array() ) {
|
||||
$data = self::get_followers_with_count( $user_id, $number, $page, $args );
|
||||
@ -153,14 +166,17 @@ class Followers {
|
||||
/**
|
||||
* Get the Followers of a given user, along with a total count for pagination purposes.
|
||||
*
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param int $number Maximum number of results to return.
|
||||
* @param int $page Page number.
|
||||
* @param array $args The WP_Query arguments.
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param int $number Maximum number of results to return.
|
||||
* @param int $page Page number.
|
||||
* @param array $args The WP_Query arguments.
|
||||
*
|
||||
* @return array
|
||||
* followers List of `Follower` objects.
|
||||
* total Total number of followers.
|
||||
* @return array {
|
||||
* Data about the followers.
|
||||
*
|
||||
* @type Follower[] $followers List of `Follower` objects.
|
||||
* @type int $total Total number of followers.
|
||||
* }
|
||||
*/
|
||||
public static function get_followers_with_count( $user_id, $number = -1, $page = null, $args = array() ) {
|
||||
$defaults = array(
|
||||
@ -172,30 +188,25 @@ class Followers {
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
array(
|
||||
'key' => 'activitypub_user_id',
|
||||
'key' => '_activitypub_user_id',
|
||||
'value' => $user_id,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
$query = new WP_Query( $args );
|
||||
$total = $query->found_posts;
|
||||
$followers = array_map(
|
||||
function ( $post ) {
|
||||
return Follower::init_from_cpt( $post );
|
||||
},
|
||||
$query->get_posts()
|
||||
);
|
||||
$args = wp_parse_args( $args, $defaults );
|
||||
$query = new WP_Query( $args );
|
||||
$total = $query->found_posts;
|
||||
$followers = array_map( array( Follower::class, 'init_from_cpt' ), $query->get_posts() );
|
||||
$followers = array_filter( $followers );
|
||||
|
||||
return compact( 'followers', 'total' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Followers
|
||||
* Get all Followers.
|
||||
*
|
||||
* @param array $args The WP_Query arguments.
|
||||
*
|
||||
* @return array The Term list of Followers.
|
||||
* @return Follower[] The Term list of Followers.
|
||||
*/
|
||||
public static function get_all_followers() {
|
||||
$args = array(
|
||||
@ -204,11 +215,11 @@ class Followers {
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'key' => '_activitypub_actor_json',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
@ -219,7 +230,7 @@ class Followers {
|
||||
/**
|
||||
* Count the total number of followers
|
||||
*
|
||||
* @param int $user_id The ID of the WordPress User
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
*
|
||||
* @return int The number of Followers
|
||||
*/
|
||||
@ -232,15 +243,15 @@ class Followers {
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'activitypub_user_id',
|
||||
'key' => '_activitypub_user_id',
|
||||
'value' => $user_id,
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'key' => '_activitypub_actor_json',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
),
|
||||
@ -251,21 +262,21 @@ class Followers {
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all Inboxes fo a Users Followers
|
||||
* Returns all Inboxes for an Actor's Followers.
|
||||
*
|
||||
* @param int $user_id The ID of the WordPress User
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
*
|
||||
* @return array The list of Inboxes
|
||||
* @return array The list of Inboxes.
|
||||
*/
|
||||
public static function get_inboxes( $user_id ) {
|
||||
$cache_key = sprintf( self::CACHE_KEY_INBOXES, $user_id );
|
||||
$inboxes = wp_cache_get( $cache_key, 'activitypub' );
|
||||
$inboxes = wp_cache_get( $cache_key, 'activitypub' );
|
||||
|
||||
if ( $inboxes ) {
|
||||
return $inboxes;
|
||||
}
|
||||
|
||||
// get all Followers of a ID of the WordPress User
|
||||
// Get all Followers of an ID of the WordPress User.
|
||||
$posts = new WP_Query(
|
||||
array(
|
||||
'nopaging' => true,
|
||||
@ -275,15 +286,15 @@ class Followers {
|
||||
'meta_query' => array(
|
||||
'relation' => 'AND',
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_user_id',
|
||||
'key' => '_activitypub_user_id',
|
||||
'value' => $user_id,
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'value' => '',
|
||||
'compare' => '!=',
|
||||
),
|
||||
@ -303,7 +314,7 @@ class Followers {
|
||||
$wpdb->prepare(
|
||||
"SELECT DISTINCT meta_value FROM {$wpdb->postmeta}
|
||||
WHERE post_id IN (" . implode( ', ', array_fill( 0, count( $posts ), '%d' ) ) . ")
|
||||
AND meta_key = 'activitypub_inbox'
|
||||
AND meta_key = '_activitypub_inbox'
|
||||
AND meta_value IS NOT NULL",
|
||||
$posts
|
||||
)
|
||||
@ -316,13 +327,62 @@ class Followers {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Followers that have not been updated for a given time
|
||||
* Get all Inboxes for a given Activity.
|
||||
*
|
||||
* @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT.
|
||||
* @param int $number Limits the result.
|
||||
* @param int $older_than The time in seconds.
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param int $actor_id The WordPress Actor ID.
|
||||
* @param int $batch_size Optional. The batch size. Default 50.
|
||||
* @param int $offset Optional. The offset. Default 0.
|
||||
*
|
||||
* @return mixed The Term list of Followers, the format depends on $output.
|
||||
* @return array The list of Inboxes.
|
||||
*/
|
||||
public static function get_inboxes_for_activity( $json, $actor_id, $batch_size = 50, $offset = 0 ) {
|
||||
$inboxes = self::get_inboxes( $actor_id );
|
||||
|
||||
if ( self::maybe_add_inboxes_of_blog_user( $json, $actor_id ) ) {
|
||||
$inboxes = array_fill_keys( $inboxes, 1 );
|
||||
foreach ( self::get_inboxes( Actors::BLOG_USER_ID ) as $inbox ) {
|
||||
$inboxes[ $inbox ] = 1;
|
||||
}
|
||||
$inboxes = array_keys( $inboxes );
|
||||
}
|
||||
|
||||
return array_slice( $inboxes, $offset, $batch_size );
|
||||
}
|
||||
|
||||
/**
|
||||
* Maybe add Inboxes of the Blog User.
|
||||
*
|
||||
* @param string $json The ActivityPub Activity JSON.
|
||||
* @param int $actor_id The WordPress Actor ID.
|
||||
* @return bool True if the Inboxes of the Blog User should be added, false otherwise.
|
||||
*/
|
||||
public static function maybe_add_inboxes_of_blog_user( $json, $actor_id ) {
|
||||
// Only if we're in both Blog and User modes.
|
||||
if ( ACTIVITYPUB_ACTOR_AND_BLOG_MODE !== \get_option( 'activitypub_actor_mode', ACTIVITYPUB_ACTOR_MODE ) ) {
|
||||
return false;
|
||||
}
|
||||
// Only if this isn't the Blog Actor.
|
||||
if ( Actors::BLOG_USER_ID === $actor_id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$activity = json_decode( $json, true );
|
||||
// Only if this is an Update or Delete. Create handles its own "Announce" in dual user mode.
|
||||
if ( ! in_array( $activity['type'] ?? null, array( 'Update', 'Delete' ), true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Followers that have not been updated for a given time.
|
||||
*
|
||||
* @param int $number Optional. Limits the result. Default 50.
|
||||
* @param int $older_than Optional. The time in seconds. Default 86400 (1 day).
|
||||
*
|
||||
* @return Follower[] The Term list of Followers.
|
||||
*/
|
||||
public static function get_outdated_followers( $number = 50, $older_than = 86400 ) {
|
||||
$args = array(
|
||||
@ -330,7 +390,7 @@ class Followers {
|
||||
'posts_per_page' => $number,
|
||||
'orderby' => 'modified',
|
||||
'order' => 'ASC',
|
||||
'post_status' => 'any', // 'any' includes 'trash
|
||||
'post_status' => 'any', // 'any' includes 'trash'.
|
||||
'date_query' => array(
|
||||
array(
|
||||
'column' => 'post_modified_gmt',
|
||||
@ -340,22 +400,17 @@ class Followers {
|
||||
);
|
||||
|
||||
$posts = new WP_Query( $args );
|
||||
$items = array();
|
||||
$items = array_map( array( Follower::class, 'init_from_cpt' ), $posts->get_posts() );
|
||||
|
||||
foreach ( $posts->get_posts() as $follower ) {
|
||||
$items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore
|
||||
}
|
||||
|
||||
return $items;
|
||||
return array_filter( $items );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all Followers that had errors
|
||||
* Get all Followers that had errors.
|
||||
*
|
||||
* @param enum $output The output format, supported ARRAY_N, OBJECT and ACTIVITYPUB_OBJECT
|
||||
* @param integer $number The number of Followers to return.
|
||||
* @param int $number Optional. The number of Followers to return. Default 20.
|
||||
*
|
||||
* @return mixed The Term list of Followers, the format depends on $output.
|
||||
* @return Follower[] The Term list of Followers.
|
||||
*/
|
||||
public static function get_faulty_followers( $number = 20 ) {
|
||||
$args = array(
|
||||
@ -365,24 +420,24 @@ class Followers {
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'activitypub_errors',
|
||||
'key' => '_activitypub_errors',
|
||||
'compare' => 'EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'key' => '_activitypub_actor_json',
|
||||
'compare' => 'NOT EXISTS',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_inbox',
|
||||
'key' => '_activitypub_inbox',
|
||||
'value' => '',
|
||||
'compare' => '=',
|
||||
),
|
||||
array(
|
||||
'key' => 'activitypub_actor_json',
|
||||
'key' => '_activitypub_actor_json',
|
||||
'value' => '',
|
||||
'compare' => '=',
|
||||
),
|
||||
@ -390,21 +445,16 @@ class Followers {
|
||||
);
|
||||
|
||||
$posts = new WP_Query( $args );
|
||||
$items = array();
|
||||
$items = array_map( array( Follower::class, 'init_from_cpt' ), $posts->get_posts() );
|
||||
|
||||
foreach ( $posts->get_posts() as $follower ) {
|
||||
$items[] = Follower::init_from_cpt( $follower ); // phpcs:ignore
|
||||
}
|
||||
|
||||
return $items;
|
||||
return array_filter( $items );
|
||||
}
|
||||
|
||||
/**
|
||||
* This function is used to store errors that occur when
|
||||
* sending an ActivityPub message to a Follower.
|
||||
*
|
||||
* The error will be stored in the
|
||||
* post meta.
|
||||
* The error will be stored in post meta.
|
||||
*
|
||||
* @param int $post_id The ID of the WordPress Custom-Post-Type.
|
||||
* @param mixed $error The error message. Can be a string or a WP_Error.
|
||||
@ -425,7 +475,7 @@ class Followers {
|
||||
|
||||
return add_post_meta(
|
||||
$post_id,
|
||||
'activitypub_errors',
|
||||
'_activitypub_errors',
|
||||
$error_message
|
||||
);
|
||||
}
|
||||
|
@ -1,113 +1,75 @@
|
||||
<?php
|
||||
/**
|
||||
* Interactions collection file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Webfinger;
|
||||
use WP_Comment_Query;
|
||||
use Activitypub\Comment;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\is_post_disabled;
|
||||
use function Activitypub\url_to_commentid;
|
||||
use function Activitypub\object_id_to_comment;
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
|
||||
/**
|
||||
* ActivityPub Interactions Collection
|
||||
* ActivityPub Interactions Collection.
|
||||
*/
|
||||
class Interactions {
|
||||
const INSERT = 'insert';
|
||||
const UPDATE = 'update';
|
||||
|
||||
/**
|
||||
* Add a comment to a post
|
||||
* Add a comment to a post.
|
||||
*
|
||||
* @param array $activity The activity-object
|
||||
* @param array $activity The activity-object.
|
||||
*
|
||||
* @return array|false The commentdata or false on failure
|
||||
* @return int|false|\WP_Error The comment ID or false or WP_Error on failure.
|
||||
*/
|
||||
public static function add_comment( $activity ) {
|
||||
if (
|
||||
! isset( $activity['object'] ) ||
|
||||
! isset( $activity['object']['id'] )
|
||||
) {
|
||||
$commentdata = self::activity_to_comment( $activity );
|
||||
|
||||
if ( ! $commentdata || ! isset( $activity['object']['inReplyTo'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( ! isset( $activity['object']['inReplyTo'] ) ) {
|
||||
return false;
|
||||
}
|
||||
$in_reply_to = object_to_uri( $activity['object']['inReplyTo'] );
|
||||
$in_reply_to = \esc_url_raw( $in_reply_to );
|
||||
$comment_post_id = \url_to_postid( $in_reply_to );
|
||||
$parent_comment_id = url_to_commentid( $in_reply_to );
|
||||
|
||||
$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
|
||||
// Save only replies and reactions.
|
||||
if ( ! $comment_post_id && $parent_comment_id ) {
|
||||
$parent_comment = get_comment( $parent_comment_id );
|
||||
$comment_post_id = $parent_comment->comment_post_ID;
|
||||
}
|
||||
|
||||
// not a reply to a post or comment
|
||||
if ( ! $comment_post_id ) {
|
||||
if ( is_post_disabled( $comment_post_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$meta = get_remote_metadata_by_actor( $activity['actor'] );
|
||||
$commentdata['comment_post_ID'] = $comment_post_id;
|
||||
$commentdata['comment_parent'] = $parent_comment_id ? $parent_comment_id : 0;
|
||||
|
||||
if ( ! $meta || \is_wp_error( $meta ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$commentdata = array(
|
||||
'comment_post_ID' => $comment_post_id,
|
||||
'comment_author' => isset( $meta['name'] ) ? \esc_attr( $meta['name'] ) : \esc_attr( $meta['preferredUsername'] ),
|
||||
'comment_author_url' => \esc_url_raw( $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;
|
||||
return self::persist( $commentdata, self::INSERT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a comment
|
||||
* Update a comment.
|
||||
*
|
||||
* @param array $activity The activity-object
|
||||
* @param array $activity The activity object.
|
||||
*
|
||||
* @return array|string|int|\WP_Error|false The commentdata or false on failure
|
||||
* @return array|string|int|\WP_Error|false The comment data or false on failure.
|
||||
*/
|
||||
public static function update_comment( $activity ) {
|
||||
$meta = get_remote_metadata_by_actor( $activity['actor'] );
|
||||
|
||||
//Determine comment_ID
|
||||
// Determine comment_ID.
|
||||
$comment = object_id_to_comment( \esc_url_raw( $activity['object']['id'] ) );
|
||||
$commentdata = \get_comment( $comment, ARRAY_A );
|
||||
|
||||
@ -115,44 +77,62 @@ class Interactions {
|
||||
return false;
|
||||
}
|
||||
|
||||
//found a local comment id
|
||||
$commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] );
|
||||
// Found a local comment id.
|
||||
$commentdata['comment_author'] = \esc_attr( $meta['name'] ? $meta['name'] : $meta['preferredUsername'] );
|
||||
$commentdata['comment_content'] = \addslashes( $activity['object']['content'] );
|
||||
if ( isset( $meta['icon']['url'] ) ) {
|
||||
$commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $meta['icon']['url'] );
|
||||
|
||||
return self::persist( $commentdata, self::UPDATE );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds an incoming Like, Announce, ... as a comment to a post.
|
||||
*
|
||||
* @param array $activity Activity array.
|
||||
*
|
||||
* @return array|false Comment data or `false` on failure.
|
||||
*/
|
||||
public static function add_reaction( $activity ) {
|
||||
$commentdata = self::activity_to_comment( $activity );
|
||||
|
||||
if ( ! $commentdata ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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 );
|
||||
$url = object_to_uri( $activity['object'] );
|
||||
$comment_post_id = \url_to_postid( $url );
|
||||
$parent_comment_id = url_to_commentid( $url );
|
||||
|
||||
$state = \wp_update_comment( $commentdata, true );
|
||||
|
||||
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10 );
|
||||
\remove_filter( 'pre_option_require_name_email', '__return_false' );
|
||||
// re-add flood control
|
||||
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
|
||||
|
||||
if ( 1 === $state ) {
|
||||
return $commentdata;
|
||||
} else {
|
||||
return $state; // Either `false` or a `WP_Error` instance or `0` or `1`!
|
||||
if ( ! $comment_post_id && $parent_comment_id ) {
|
||||
$parent_comment = \get_comment( $parent_comment_id );
|
||||
$comment_post_id = $parent_comment->comment_post_ID;
|
||||
}
|
||||
|
||||
if ( ! $comment_post_id || is_post_disabled( $comment_post_id ) ) {
|
||||
// Not a reply to a post or comment.
|
||||
return false;
|
||||
}
|
||||
|
||||
$comment_type = Comment::get_comment_type_by_activity_type( $activity['type'] );
|
||||
|
||||
if ( ! $comment_type ) {
|
||||
// Not a valid comment type.
|
||||
return false;
|
||||
}
|
||||
|
||||
$comment_content = $comment_type['excerpt'];
|
||||
|
||||
$commentdata['comment_post_ID'] = $comment_post_id;
|
||||
$commentdata['comment_content'] = \esc_html( $comment_content );
|
||||
$commentdata['comment_type'] = \esc_attr( $comment_type['type'] );
|
||||
$commentdata['comment_meta']['source_id'] = \esc_url_raw( $activity['id'] );
|
||||
|
||||
return self::persist( $commentdata, self::INSERT );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get interaction(s) for a given URL/ID.
|
||||
*
|
||||
* @param strin $url The URL/ID to get interactions for.
|
||||
* @param string $url The URL/ID to get interactions for.
|
||||
*
|
||||
* @return array The interactions as WP_Comment objects.
|
||||
*/
|
||||
@ -194,9 +174,9 @@ class Interactions {
|
||||
public static function get_interactions_by_actor( $actor ) {
|
||||
$meta = get_remote_metadata_by_actor( $actor );
|
||||
|
||||
// get URL, because $actor seems to be the ID
|
||||
// Get URL, because $actor seems to be the ID.
|
||||
if ( $meta && ! is_wp_error( $meta ) && isset( $meta['url'] ) ) {
|
||||
$actor = $meta['url'];
|
||||
$actor = object_to_uri( $meta['url'] );
|
||||
}
|
||||
|
||||
$args = array(
|
||||
@ -207,19 +187,18 @@ class Interactions {
|
||||
array(
|
||||
'key' => 'protocol',
|
||||
'value' => 'activitypub',
|
||||
'compare' => '=',
|
||||
),
|
||||
),
|
||||
);
|
||||
$comment_query = new WP_Comment_Query( $args );
|
||||
return $comment_query->comments;
|
||||
|
||||
return get_comments( $args );
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds line breaks to the list of allowed comment tags.
|
||||
*
|
||||
* @param array $allowed_tags Allowed HTML tags.
|
||||
* @param string $context Context.
|
||||
* @param string $context Optional. Context. Default empty.
|
||||
*
|
||||
* @return array Filtered tag list.
|
||||
*/
|
||||
@ -240,4 +219,131 @@ class Interactions {
|
||||
|
||||
return $allowed_tags;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an Activity to a WP_Comment
|
||||
*
|
||||
* @param array $activity The Activity array.
|
||||
*
|
||||
* @return array|false The comment data or false on failure.
|
||||
*/
|
||||
public static function activity_to_comment( $activity ) {
|
||||
$comment_content = null;
|
||||
$actor = object_to_uri( $activity['actor'] ?? null );
|
||||
$actor = get_remote_metadata_by_actor( $actor );
|
||||
|
||||
// Check Actor-Meta.
|
||||
if ( ! $actor || is_wp_error( $actor ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check Actor-Name.
|
||||
if ( isset( $actor['name'] ) ) {
|
||||
$comment_author = $actor['name'];
|
||||
} elseif ( isset( $actor['preferredUsername'] ) ) {
|
||||
$comment_author = $actor['preferredUsername'];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
|
||||
$url = object_to_uri( $actor['url'] ?? $actor['id'] );
|
||||
|
||||
if ( ! $url ) {
|
||||
$url = object_to_uri( $actor['id'] );
|
||||
}
|
||||
|
||||
if ( isset( $activity['object']['content'] ) ) {
|
||||
$comment_content = \addslashes( $activity['object']['content'] );
|
||||
}
|
||||
|
||||
$webfinger = Webfinger::uri_to_acct( $url );
|
||||
if ( is_wp_error( $webfinger ) ) {
|
||||
$webfinger = '';
|
||||
} else {
|
||||
$webfinger = str_replace( 'acct:', '', $webfinger );
|
||||
}
|
||||
|
||||
$commentdata = array(
|
||||
'comment_author' => \esc_attr( $comment_author ),
|
||||
'comment_author_url' => \esc_url_raw( $url ),
|
||||
'comment_content' => $comment_content,
|
||||
'comment_type' => 'comment',
|
||||
'comment_author_email' => $webfinger,
|
||||
'comment_meta' => array(
|
||||
'source_id' => \esc_url_raw( object_to_uri( $activity['object'] ) ),
|
||||
'protocol' => 'activitypub',
|
||||
),
|
||||
);
|
||||
|
||||
if ( isset( $actor['icon']['url'] ) ) {
|
||||
$commentdata['comment_meta']['avatar_url'] = \esc_url_raw( $actor['icon']['url'] );
|
||||
}
|
||||
|
||||
if ( isset( $activity['object']['url'] ) ) {
|
||||
$commentdata['comment_meta']['source_url'] = \esc_url_raw( object_to_uri( $activity['object']['url'] ) );
|
||||
}
|
||||
|
||||
return $commentdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Persist a comment.
|
||||
*
|
||||
* @param array $commentdata The commentdata array.
|
||||
* @param string $action Optional. Either 'insert' or 'update'. Default 'insert'.
|
||||
*
|
||||
* @return array|string|int|\WP_Error|false The comment data or false on failure
|
||||
*/
|
||||
public static function persist( $commentdata, $action = self::INSERT ) {
|
||||
// Disable flood control.
|
||||
\remove_action( 'check_comment_flood', 'check_comment_flood_db' );
|
||||
// Do not require email for AP entries.
|
||||
\add_filter( 'pre_option_require_name_email', '__return_false' );
|
||||
// No nonce possible for this submission route.
|
||||
\add_filter(
|
||||
'akismet_comment_nonce',
|
||||
function () {
|
||||
return 'inactive';
|
||||
}
|
||||
);
|
||||
\add_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ), 10, 2 );
|
||||
|
||||
if ( self::INSERT === $action ) {
|
||||
$state = \wp_new_comment( $commentdata, true );
|
||||
} else {
|
||||
$state = \wp_update_comment( $commentdata, true );
|
||||
}
|
||||
|
||||
\remove_filter( 'wp_kses_allowed_html', array( self::class, 'allowed_comment_html' ) );
|
||||
\remove_filter( 'pre_option_require_name_email', '__return_false' );
|
||||
// Restore flood control.
|
||||
\add_action( 'check_comment_flood', 'check_comment_flood_db', 10, 4 );
|
||||
|
||||
if ( 1 === $state ) {
|
||||
return $commentdata;
|
||||
} else {
|
||||
return $state; // Either WP_Comment, false, a WP_Error, 0, or 1!
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of interactions by type for a given ID.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
* @param string $type The type of interaction to count.
|
||||
*
|
||||
* @return int The total number of interactions.
|
||||
*/
|
||||
public static function count_by_type( $post_id, $type ) {
|
||||
return \get_comments(
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'status' => 'approve',
|
||||
'type' => $type,
|
||||
'count' => true,
|
||||
'paging' => false,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,351 @@
|
||||
<?php
|
||||
/**
|
||||
* Outbox collection file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use Activitypub\Dispatcher;
|
||||
use Activitypub\Scheduler;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Activity\Base_Object;
|
||||
|
||||
use function Activitypub\add_to_outbox;
|
||||
|
||||
/**
|
||||
* ActivityPub Outbox Collection
|
||||
*
|
||||
* @link https://www.w3.org/TR/activitypub/#outbox
|
||||
*/
|
||||
class Outbox {
|
||||
const POST_TYPE = 'ap_outbox';
|
||||
|
||||
/**
|
||||
* Add an Item to the outbox.
|
||||
*
|
||||
* @param Activity $activity Full Activity object that will be added to the outbox.
|
||||
* @param int $user_id The real or imaginary user ID of the actor that published the activity that will be added to the outbox.
|
||||
* @param string $visibility Optional. The visibility of the content. Default: `ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC`. See `constants.php` for possible values: `ACTIVITYPUB_CONTENT_VISIBILITY_*`.
|
||||
*
|
||||
* @return false|int|\WP_Error The added item or an error.
|
||||
*/
|
||||
public static function add( Activity $activity, $user_id, $visibility = ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC ) {
|
||||
$actor_type = Actors::get_type_by_id( $user_id );
|
||||
$object_id = self::get_object_id( $activity );
|
||||
$title = self::get_object_title( $activity->get_object() );
|
||||
|
||||
if ( ! $activity->get_actor() ) {
|
||||
$activity->set_actor( Actors::get_by_id( $user_id )->get_id() );
|
||||
}
|
||||
|
||||
$outbox_item = array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_title' => sprintf(
|
||||
/* translators: 1. Activity type, 2. Object Title or Excerpt */
|
||||
__( '[%1$s] %2$s', 'activitypub' ),
|
||||
$activity->get_type(),
|
||||
\wp_trim_words( $title, 5 )
|
||||
),
|
||||
'post_content' => wp_slash( $activity->to_json() ),
|
||||
// ensure that user ID is not below 0.
|
||||
'post_author' => \max( $user_id, 0 ),
|
||||
'post_status' => 'pending',
|
||||
'meta_input' => array(
|
||||
'_activitypub_object_id' => $object_id,
|
||||
'_activitypub_activity_type' => $activity->get_type(),
|
||||
'_activitypub_activity_actor' => $actor_type,
|
||||
'activitypub_content_visibility' => $visibility,
|
||||
),
|
||||
);
|
||||
|
||||
$has_kses = false !== \has_filter( 'content_save_pre', 'wp_filter_post_kses' );
|
||||
if ( $has_kses ) {
|
||||
// Prevent KSES from corrupting JSON in post_content.
|
||||
\kses_remove_filters();
|
||||
}
|
||||
|
||||
$id = \wp_insert_post( $outbox_item, true );
|
||||
|
||||
// Update the activity ID if the post was inserted successfully.
|
||||
if ( $id && ! \is_wp_error( $id ) ) {
|
||||
$activity->set_id( \get_the_guid( $id ) );
|
||||
|
||||
\wp_update_post(
|
||||
array(
|
||||
'ID' => $id,
|
||||
'post_content' => \wp_slash( $activity->to_json() ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
if ( $has_kses ) {
|
||||
\kses_init_filters();
|
||||
}
|
||||
|
||||
if ( \is_wp_error( $id ) ) {
|
||||
return $id;
|
||||
}
|
||||
|
||||
if ( ! $id ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
self::invalidate_existing_items( $object_id, $activity->get_type(), $id );
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Invalidate existing outbox items with the same activity type and object ID
|
||||
* by setting their status to 'publish'.
|
||||
*
|
||||
* @param string $object_id The ID of the activity object.
|
||||
* @param string $activity_type The type of the activity.
|
||||
* @param int $current_id The ID of the current outbox item to exclude.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private static function invalidate_existing_items( $object_id, $activity_type, $current_id ) {
|
||||
// Do not invalidate items for Announce activities.
|
||||
if ( 'Announce' === $activity_type ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$meta_query = array(
|
||||
array(
|
||||
'key' => '_activitypub_object_id',
|
||||
'value' => $object_id,
|
||||
),
|
||||
);
|
||||
|
||||
// For non-Delete activities, only invalidate items of the same type.
|
||||
if ( 'Delete' !== $activity_type ) {
|
||||
$meta_query[] = array(
|
||||
'key' => '_activitypub_activity_type',
|
||||
'value' => $activity_type,
|
||||
);
|
||||
}
|
||||
|
||||
$existing_items = get_posts(
|
||||
array(
|
||||
'post_type' => self::POST_TYPE,
|
||||
'post_status' => 'pending',
|
||||
'exclude' => array( $current_id ),
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => $meta_query,
|
||||
'fields' => 'ids',
|
||||
)
|
||||
);
|
||||
|
||||
foreach ( $existing_items as $existing_item_id ) {
|
||||
$event_args = array(
|
||||
Dispatcher::$callback,
|
||||
$existing_item_id,
|
||||
Dispatcher::$batch_size,
|
||||
\get_post_meta( $existing_item_id, '_activitypub_outbox_offset', true ) ?: 0, // phpcs:ignore
|
||||
);
|
||||
|
||||
$timestamp = \wp_next_scheduled( 'activitypub_async_batch', $event_args );
|
||||
\wp_unschedule_event( $timestamp, 'activitypub_async_batch', $event_args );
|
||||
|
||||
$timestamp = \wp_next_scheduled( 'activitypub_process_outbox', array( $existing_item_id ) );
|
||||
\wp_unschedule_event( $timestamp, 'activitypub_process_outbox', array( $existing_item_id ) );
|
||||
|
||||
\wp_publish_post( $existing_item_id );
|
||||
\delete_post_meta( $existing_item_id, '_activitypub_outbox_offset' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates an Undo activity.
|
||||
*
|
||||
* @param int|\WP_Post $outbox_item The Outbox post or post ID.
|
||||
*
|
||||
* @return int|bool The ID of the outbox item or false on failure.
|
||||
*/
|
||||
public static function undo( $outbox_item ) {
|
||||
$outbox_item = get_post( $outbox_item );
|
||||
$activity = self::get_activity( $outbox_item );
|
||||
|
||||
$type = 'Undo';
|
||||
if ( 'Create' === $activity->get_type() ) {
|
||||
$type = 'Delete';
|
||||
} elseif ( 'Add' === $activity->get_type() ) {
|
||||
$type = 'Remove';
|
||||
}
|
||||
|
||||
return add_to_outbox( $activity, $type, $outbox_item->post_author );
|
||||
}
|
||||
|
||||
/**
|
||||
* Reschedule an activity.
|
||||
*
|
||||
* @param int|\WP_Post $outbox_item The Outbox post or post ID.
|
||||
*
|
||||
* @return bool True if the activity was rescheduled, false otherwise.
|
||||
*/
|
||||
public static function reschedule( $outbox_item ) {
|
||||
$outbox_item = get_post( $outbox_item );
|
||||
|
||||
$outbox_item->post_status = 'pending';
|
||||
$outbox_item->post_date = current_time( 'mysql' );
|
||||
|
||||
wp_update_post( $outbox_item );
|
||||
|
||||
Scheduler::schedule_outbox_activity_for_federation( $outbox_item->ID );
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Activity object from the Outbox item.
|
||||
*
|
||||
* @param int|\WP_Post $outbox_item The Outbox post or post ID.
|
||||
* @return Activity|\WP_Error The Activity object or WP_Error.
|
||||
*/
|
||||
public static function get_activity( $outbox_item ) {
|
||||
$outbox_item = get_post( $outbox_item );
|
||||
$actor = self::get_actor( $outbox_item );
|
||||
if ( is_wp_error( $actor ) ) {
|
||||
return $actor;
|
||||
}
|
||||
|
||||
$activity_object = \json_decode( $outbox_item->post_content, true );
|
||||
$type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
|
||||
|
||||
if ( $activity_object['type'] === $type ) {
|
||||
$activity = Activity::init_from_array( $activity_object );
|
||||
if ( ! $activity->get_actor() ) {
|
||||
$activity->set_actor( $actor->get_id() );
|
||||
}
|
||||
} else {
|
||||
$activity = new Activity();
|
||||
$activity->set_type( $type );
|
||||
$activity->set_id( $outbox_item->guid );
|
||||
$activity->set_actor( $actor->get_id() );
|
||||
// Pre-fill the Activity with data (for example cc and to).
|
||||
$activity->set_object( $activity_object );
|
||||
}
|
||||
|
||||
if ( 'Update' === $type ) {
|
||||
$activity->set_updated( gmdate( ACTIVITYPUB_DATE_TIME_RFC3339, strtotime( $outbox_item->post_modified ) ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters the Activity object before it is returned.
|
||||
*
|
||||
* @param Activity $activity The Activity object.
|
||||
* @param \WP_Post $outbox_item The outbox item post object.
|
||||
*/
|
||||
return apply_filters( 'activitypub_get_outbox_activity', $activity, $outbox_item );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Actor object from the Outbox item.
|
||||
*
|
||||
* @param \WP_Post $outbox_item The Outbox post.
|
||||
*
|
||||
* @return \Activitypub\Model\User|\Activitypub\Model\Blog|\WP_Error The Actor object or WP_Error.
|
||||
*/
|
||||
public static function get_actor( $outbox_item ) {
|
||||
$actor_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_actor', true );
|
||||
|
||||
switch ( $actor_type ) {
|
||||
case 'blog':
|
||||
$actor_id = Actors::BLOG_USER_ID;
|
||||
break;
|
||||
case 'application':
|
||||
$actor_id = Actors::APPLICATION_USER_ID;
|
||||
break;
|
||||
case 'user':
|
||||
default:
|
||||
$actor_id = $outbox_item->post_author;
|
||||
break;
|
||||
}
|
||||
|
||||
return Actors::get_by_id( $actor_id );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Activity object from the Outbox item.
|
||||
*
|
||||
* @param \WP_Post $outbox_item The Outbox post.
|
||||
*
|
||||
* @return Activity|\WP_Error The Activity object or WP_Error.
|
||||
*/
|
||||
public static function maybe_get_activity( $outbox_item ) {
|
||||
if ( ! $outbox_item instanceof \WP_Post ) {
|
||||
return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' );
|
||||
}
|
||||
|
||||
if ( 'ap_outbox' !== $outbox_item->post_type ) {
|
||||
return new \WP_Error( 'invalid_outbox_item', 'Invalid Outbox item.' );
|
||||
}
|
||||
|
||||
// Check if Outbox Activity is public.
|
||||
$visibility = \get_post_meta( $outbox_item->ID, 'activitypub_content_visibility', true );
|
||||
|
||||
if ( ! in_array( $visibility, array( ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC, ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC ), true ) ) {
|
||||
return new \WP_Error( 'private_outbox_item', 'Not a public Outbox item.' );
|
||||
}
|
||||
|
||||
$activity_types = \apply_filters( 'rest_activitypub_outbox_activity_types', array( 'Announce', 'Create', 'Like', 'Update' ) );
|
||||
$activity_type = \get_post_meta( $outbox_item->ID, '_activitypub_activity_type', true );
|
||||
|
||||
if ( ! in_array( $activity_type, $activity_types, true ) ) {
|
||||
return new \WP_Error( 'private_outbox_item', 'Not public Outbox item type.' );
|
||||
}
|
||||
|
||||
return self::get_activity( $outbox_item );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the object ID of an activity.
|
||||
*
|
||||
* @param Activity|Base_Object|string $data The activity object.
|
||||
*
|
||||
* @return string The object ID.
|
||||
*/
|
||||
private static function get_object_id( $data ) {
|
||||
$object = $data->get_object();
|
||||
|
||||
if ( is_object( $object ) ) {
|
||||
return self::get_object_id( $object );
|
||||
}
|
||||
|
||||
if ( is_string( $object ) ) {
|
||||
return $object;
|
||||
}
|
||||
|
||||
return $data->get_id() ?? $data->get_actor();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the title of an activity recursively.
|
||||
*
|
||||
* @param Base_Object $activity_object The activity object.
|
||||
*
|
||||
* @return string The title.
|
||||
*/
|
||||
private static function get_object_title( $activity_object ) {
|
||||
if ( ! $activity_object ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if ( is_string( $activity_object ) ) {
|
||||
$post_id = url_to_postid( $activity_object );
|
||||
|
||||
return $post_id ? get_the_title( $post_id ) : '';
|
||||
}
|
||||
|
||||
$title = $activity_object->get_name() ?? $activity_object->get_content();
|
||||
|
||||
if ( ! $title && $activity_object->get_object() instanceof Base_Object ) {
|
||||
$title = $activity_object->get_object()->get_name() ?? $activity_object->get_object()->get_content();
|
||||
}
|
||||
|
||||
return $title;
|
||||
}
|
||||
}
|
@ -0,0 +1,226 @@
|
||||
<?php
|
||||
/**
|
||||
* Replies collection file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use WP_Post;
|
||||
use WP_Comment;
|
||||
use WP_Error;
|
||||
|
||||
use Activitypub\Comment;
|
||||
use Activitypub\Model\Blog;
|
||||
use Activitypub\Transformer\Post as PostTransformer;
|
||||
use Activitypub\Transformer\Comment as CommentTransformer;
|
||||
|
||||
use function Activitypub\is_post_disabled;
|
||||
use function Activitypub\is_local_comment;
|
||||
use function Activitypub\get_rest_url_by_path;
|
||||
use function Activitypub\is_user_type_disabled;
|
||||
|
||||
/**
|
||||
* Class containing code for getting replies Collections and CollectionPages of posts and comments.
|
||||
*/
|
||||
class Replies {
|
||||
/**
|
||||
* Build base arguments for fetching the comments of either a WordPress post or comment.
|
||||
*
|
||||
* @param WP_Post|WP_Comment|WP_Error $wp_object The post or comment to fetch replies for on success.
|
||||
*/
|
||||
private static function build_args( $wp_object ) {
|
||||
$args = array(
|
||||
'status' => 'approve',
|
||||
'orderby' => 'comment_date_gmt',
|
||||
'order' => 'ASC',
|
||||
'type' => 'comment',
|
||||
);
|
||||
|
||||
if ( $wp_object instanceof WP_Post ) {
|
||||
$args['parent'] = 0; // TODO: maybe this is unnecessary.
|
||||
$args['post_id'] = $wp_object->ID;
|
||||
} elseif ( $wp_object instanceof WP_Comment ) {
|
||||
$args['parent'] = $wp_object->comment_ID;
|
||||
} else {
|
||||
return new WP_Error();
|
||||
}
|
||||
|
||||
return $args;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the replies collections ID.
|
||||
*
|
||||
* @param WP_Post|WP_Comment $wp_object The post or comment to fetch replies for.
|
||||
*
|
||||
* @return string|WP_Error The rest URL of the replies collection or WP_Error if the object is not a post or comment.
|
||||
*/
|
||||
private static function get_id( $wp_object ) {
|
||||
if ( $wp_object instanceof WP_Post ) {
|
||||
return get_rest_url_by_path( sprintf( 'posts/%d/replies', $wp_object->ID ) );
|
||||
} elseif ( $wp_object instanceof WP_Comment ) {
|
||||
return get_rest_url_by_path( sprintf( 'comments/%d/replies', $wp_object->comment_ID ) );
|
||||
} else {
|
||||
return new WP_Error( 'unsupported_object', 'The object is not a post or comment.' );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Replies collection.
|
||||
*
|
||||
* @param WP_Post|WP_Comment $wp_object The post or comment to fetch replies for.
|
||||
*
|
||||
* @return array|\WP_Error|null An associative array containing the replies collection without JSON-LD context on success.
|
||||
*/
|
||||
public static function get_collection( $wp_object ) {
|
||||
$id = self::get_id( $wp_object );
|
||||
|
||||
if ( is_wp_error( $id ) ) {
|
||||
return \wp_is_serving_rest_request() ? $id : null;
|
||||
}
|
||||
|
||||
$replies = array(
|
||||
'id' => $id,
|
||||
'type' => 'Collection',
|
||||
);
|
||||
|
||||
$replies['first'] = self::get_collection_page( $wp_object, 1, $replies['id'] );
|
||||
|
||||
return $replies;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a replies collection page as an associative array.
|
||||
*
|
||||
* @link https://www.w3.org/TR/activitystreams-vocabulary/#dfn-collectionpage
|
||||
*
|
||||
* @param WP_Post|WP_Comment $wp_object The post of comment the replies are for.
|
||||
* @param int $page The current pagination page.
|
||||
* @param string $part_of Optional. The collection id/url the returned CollectionPage belongs to. Default null.
|
||||
*
|
||||
* @return array|WP_Error|null A CollectionPage as an associative array on success, WP_Error or null on failure.
|
||||
*/
|
||||
public static function get_collection_page( $wp_object, $page, $part_of = null ) {
|
||||
// Build initial arguments for fetching approved comments.
|
||||
$args = self::build_args( $wp_object );
|
||||
if ( is_wp_error( $args ) ) {
|
||||
return \wp_is_serving_rest_request() ? $args : null;
|
||||
}
|
||||
|
||||
// Retrieve the partOf if not already given.
|
||||
$part_of = $part_of ?? self::get_id( $wp_object );
|
||||
|
||||
// If the collection page does not exist.
|
||||
if ( is_wp_error( $part_of ) ) {
|
||||
return \wp_is_serving_rest_request() ? $part_of : null;
|
||||
}
|
||||
|
||||
// Get to total replies count.
|
||||
$total_replies = \get_comments( array_merge( $args, array( 'count' => true ) ) );
|
||||
|
||||
// If set to zero, we get errors below. You need at least one comment per page, here.
|
||||
$args['number'] = max( (int) \get_option( 'comments_per_page' ), 1 );
|
||||
$args['offset'] = intval( $page - 1 ) * $args['number'];
|
||||
|
||||
// Get the ActivityPub ID's of the comments, without local-only comments.
|
||||
$comment_ids = self::get_reply_ids( \get_comments( $args ) );
|
||||
|
||||
// Build the associative CollectionPage array.
|
||||
$collection_page = array(
|
||||
'id' => \add_query_arg( 'page', $page, $part_of ),
|
||||
'type' => 'CollectionPage',
|
||||
'partOf' => $part_of,
|
||||
'items' => $comment_ids,
|
||||
);
|
||||
|
||||
if ( ( $total_replies / $args['number'] ) > $page ) {
|
||||
$collection_page['next'] = \add_query_arg( 'page', $page + 1, $part_of );
|
||||
}
|
||||
|
||||
if ( $page > 1 ) {
|
||||
$collection_page['prev'] = \add_query_arg( 'page', $page - 1, $part_of );
|
||||
}
|
||||
|
||||
return $collection_page;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the context collection for a post.
|
||||
*
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return array|false The context for the post or false if the post is not found or disabled.
|
||||
*/
|
||||
public static function get_context_collection( $post_id ) {
|
||||
$post = \get_post( $post_id );
|
||||
|
||||
if ( ! $post || is_post_disabled( $post_id ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$comments = \get_comments(
|
||||
array(
|
||||
'post_id' => $post_id,
|
||||
'type' => 'comment',
|
||||
'status' => 'approve',
|
||||
'orderby' => 'comment_date_gmt',
|
||||
'order' => 'ASC',
|
||||
)
|
||||
);
|
||||
$ids = self::get_reply_ids( $comments, true );
|
||||
$post_uri = ( new PostTransformer( $post ) )->to_id();
|
||||
\array_unshift( $ids, $post_uri );
|
||||
|
||||
$author = Actors::get_by_id( $post->post_author );
|
||||
if ( is_wp_error( $author ) ) {
|
||||
if ( is_user_type_disabled( 'blog' ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$author = new Blog();
|
||||
}
|
||||
|
||||
return array(
|
||||
'type' => 'OrderedCollection',
|
||||
'url' => \get_permalink( $post_id ),
|
||||
'attributedTo' => $author->get_id(),
|
||||
'totalItems' => count( $ids ),
|
||||
'items' => $ids,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the ActivityPub ID's from a list of comments.
|
||||
*
|
||||
* It takes only federated/non-local comments into account, others also do not have an
|
||||
* ActivityPub ID available.
|
||||
*
|
||||
* @param WP_Comment[] $comments The comments to retrieve the ActivityPub ids from.
|
||||
* @param boolean $include_blog_comments Optional. Include blog comments in the returned array. Default false.
|
||||
*
|
||||
* @return string[] A list of the ActivityPub ID's.
|
||||
*/
|
||||
private static function get_reply_ids( $comments, $include_blog_comments = false ) {
|
||||
$comment_ids = array();
|
||||
|
||||
foreach ( $comments as $comment ) {
|
||||
if ( is_local_comment( $comment ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$public_comment_id = Comment::get_source_id( $comment->comment_ID );
|
||||
if ( $public_comment_id ) {
|
||||
$comment_ids[] = $public_comment_id;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ( $include_blog_comments ) {
|
||||
$comment_ids[] = ( new CommentTransformer( $comment ) )->to_id();
|
||||
}
|
||||
}
|
||||
|
||||
return \array_unique( $comment_ids );
|
||||
}
|
||||
}
|
@ -1,63 +1,29 @@
|
||||
<?php
|
||||
/**
|
||||
* Users collection file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Collection;
|
||||
|
||||
use WP_Error;
|
||||
use WP_User_Query;
|
||||
use Activitypub\Model\User;
|
||||
use Activitypub\Model\Blog_User;
|
||||
use Activitypub\Model\Application_User;
|
||||
|
||||
use function Activitypub\url_to_authorid;
|
||||
use function Activitypub\is_user_disabled;
|
||||
|
||||
class Users {
|
||||
/**
|
||||
* Users collection.
|
||||
*
|
||||
* @deprecated version 4.2.0
|
||||
*/
|
||||
class Users extends Actors {
|
||||
/**
|
||||
* The ID of the Blog User
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const BLOG_USER_ID = 0;
|
||||
|
||||
/**
|
||||
* The ID of the Application User
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
const APPLICATION_USER_ID = -1;
|
||||
|
||||
/**
|
||||
* Get the User by ID
|
||||
* Get the User by ID.
|
||||
*
|
||||
* @param int $user_id The User-ID.
|
||||
*
|
||||
* @return \Acitvitypub\Model\User The User.
|
||||
* @return User|Blog|Application|WP_Error The User or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_id( $user_id ) {
|
||||
if ( is_string( $user_id ) || is_numeric( $user_id ) ) {
|
||||
$user_id = (int) $user_id;
|
||||
}
|
||||
_deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_id' );
|
||||
|
||||
if ( is_user_disabled( $user_id ) ) {
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
if ( self::BLOG_USER_ID === $user_id ) {
|
||||
return Blog_User::from_wp_user( $user_id );
|
||||
} elseif ( self::APPLICATION_USER_ID === $user_id ) {
|
||||
return Application_User::from_wp_user( $user_id );
|
||||
} elseif ( $user_id > 0 ) {
|
||||
return User::from_wp_user( $user_id );
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
return parent::get_by_id( $user_id );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -65,199 +31,38 @@ class Users {
|
||||
*
|
||||
* @param string $username The User-Name.
|
||||
*
|
||||
* @return \Acitvitypub\Model\User The User.
|
||||
* @return User|Blog|Application|WP_Error The User or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_username( $username ) {
|
||||
// check for blog user.
|
||||
if ( Blog_User::get_default_username() === $username ) {
|
||||
return self::get_by_id( self::BLOG_USER_ID );
|
||||
}
|
||||
_deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_username' );
|
||||
|
||||
if ( get_option( 'activitypub_blog_user_identifier' ) === $username ) {
|
||||
return self::get_by_id( self::BLOG_USER_ID );
|
||||
}
|
||||
|
||||
// check for application user.
|
||||
if ( 'application' === $username ) {
|
||||
return self::get_by_id( self::APPLICATION_USER_ID );
|
||||
}
|
||||
|
||||
// check for 'activitypub_username' meta
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
// phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_meta_query
|
||||
'meta_query' => array(
|
||||
'relation' => 'OR',
|
||||
array(
|
||||
'key' => 'activitypub_user_identifier',
|
||||
'value' => $username,
|
||||
'compare' => 'LIKE',
|
||||
),
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
return self::get_by_id( $user->results[0] );
|
||||
}
|
||||
|
||||
$username = str_replace( array( '*', '%' ), '', $username );
|
||||
|
||||
// check for login or nicename.
|
||||
$user = new WP_User_Query(
|
||||
array(
|
||||
'search' => $username,
|
||||
'search_columns' => array( 'user_login', 'user_nicename' ),
|
||||
'number' => 1,
|
||||
'hide_empty' => true,
|
||||
'fields' => 'ID',
|
||||
)
|
||||
);
|
||||
|
||||
if ( $user->results ) {
|
||||
return self::get_by_id( $user->results[0] );
|
||||
}
|
||||
|
||||
return new WP_Error(
|
||||
'activitypub_user_not_found',
|
||||
\__( 'User not found', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
return parent::get_by_username( $username );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User by resource.
|
||||
*
|
||||
* @param string $resource The User-Resource.
|
||||
* @param string $uri The User-Resource.
|
||||
*
|
||||
* @return \Acitvitypub\Model\User The User.
|
||||
* @return User|WP_Error The User or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_resource( $resource ) {
|
||||
$scheme = 'acct';
|
||||
$match = array();
|
||||
// try to extract the scheme and the host
|
||||
if ( preg_match( '/^([a-zA-Z^:]+):(.*)$/i', $resource, $match ) ) {
|
||||
// extract the scheme
|
||||
$scheme = esc_attr( $match[1] );
|
||||
}
|
||||
public static function get_by_resource( $uri ) {
|
||||
_deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_resource' );
|
||||
|
||||
switch ( $scheme ) {
|
||||
// check for http(s) URIs
|
||||
case 'http':
|
||||
case 'https':
|
||||
$url_parts = wp_parse_url( $resource );
|
||||
|
||||
// check for http(s)://blog.example.com/@username
|
||||
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 );
|
||||
}
|
||||
|
||||
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 ) );
|
||||
|
||||
if ( $blog_host !== $host ) {
|
||||
return new WP_Error(
|
||||
'activitypub_wrong_host',
|
||||
\__( 'Resource host does not match blog host', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
|
||||
// prepare wildcards https://github.com/mastodon/mastodon/issues/22213
|
||||
if ( in_array( $identifier, array( '_', '*', '' ), true ) ) {
|
||||
return self::get_by_id( self::BLOG_USER_ID );
|
||||
}
|
||||
|
||||
return self::get_by_username( $identifier );
|
||||
default:
|
||||
return new WP_Error(
|
||||
'activitypub_wrong_scheme',
|
||||
\__( 'Wrong scheme', 'activitypub' ),
|
||||
array( 'status' => 404 )
|
||||
);
|
||||
}
|
||||
return parent::get_by_resource( $uri );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the User by resource.
|
||||
*
|
||||
* @param string $resource The User-Resource.
|
||||
* @param string $id The User-Resource.
|
||||
*
|
||||
* @return \Acitvitypub\Model\User The User.
|
||||
* @return User|Blog|Application|WP_Error The User or WP_Error if user not found.
|
||||
*/
|
||||
public static function get_by_various( $id ) {
|
||||
if ( is_numeric( $id ) ) {
|
||||
return self::get_by_id( $id );
|
||||
} elseif (
|
||||
// is URL
|
||||
filter_var( $id, FILTER_VALIDATE_URL ) ||
|
||||
// is acct
|
||||
str_starts_with( $id, 'acct:' )
|
||||
) {
|
||||
return self::get_by_resource( $id );
|
||||
} else {
|
||||
return self::get_by_username( $id );
|
||||
}
|
||||
}
|
||||
_deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_by_various' );
|
||||
|
||||
/**
|
||||
* Normalize a host.
|
||||
*
|
||||
* @param string $host The host.
|
||||
*
|
||||
* @return string The normalized host.
|
||||
*/
|
||||
public static function normalize_host( $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;
|
||||
return parent::get_by_various( $id );
|
||||
}
|
||||
|
||||
/**
|
||||
@ -266,18 +71,8 @@ class Users {
|
||||
* @return array The User collection.
|
||||
*/
|
||||
public static function get_collection() {
|
||||
$users = \get_users(
|
||||
array(
|
||||
'capability__in' => array( 'activitypub' ),
|
||||
)
|
||||
);
|
||||
_deprecated_function( __METHOD__, '4.2.0', 'Activitypub\Collection\Actors::get_collection' );
|
||||
|
||||
$return = array();
|
||||
|
||||
foreach ( $users as $user ) {
|
||||
$return[] = User::from_wp_user( $user->ID );
|
||||
}
|
||||
|
||||
return $return;
|
||||
return parent::get_collection();
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
<?php
|
||||
/**
|
||||
* ActivityPub implementation for WordPress/PHP functions either missing from older WordPress/PHP versions or not included by default.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
if ( ! function_exists( 'str_starts_with' ) ) {
|
||||
@ -23,19 +25,6 @@ if ( ! function_exists( 'str_starts_with' ) ) {
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_self_link' ) ) {
|
||||
/**
|
||||
* Returns the link for the currently displayed feed.
|
||||
*
|
||||
* @return string Correct link for the atom:self element.
|
||||
*/
|
||||
function get_self_link() {
|
||||
$host = wp_parse_url( home_url() );
|
||||
$path = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
||||
return esc_url( apply_filters( 'self_link', set_url_scheme( 'http://' . $host['host'] . $path ) ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'is_countable' ) ) {
|
||||
/**
|
||||
* Polyfill for `is_countable()` function added in PHP 7.3.
|
||||
@ -56,18 +45,28 @@ if ( ! function_exists( 'is_countable' ) ) {
|
||||
* @return bool True if `$array` is a list, otherwise false.
|
||||
*/
|
||||
if ( ! function_exists( 'array_is_list' ) ) {
|
||||
function array_is_list( $array ) {
|
||||
if ( ! is_array( $array ) ) {
|
||||
/**
|
||||
* Check if an array is a list.
|
||||
*
|
||||
* An array is considered a list if its keys are a range of numbers
|
||||
* starting from 0 and ending at count( $array ) - 1.
|
||||
*
|
||||
* @param array $input The array to check.
|
||||
*
|
||||
* @return bool True if `$input` is a list, otherwise false.
|
||||
*/
|
||||
function array_is_list( $input ) {
|
||||
if ( ! is_array( $input ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( array_values( $array ) === $array ) {
|
||||
if ( array_values( $input ) === $input ) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$next_key = -1;
|
||||
|
||||
foreach ( $array as $k => $v ) {
|
||||
foreach ( $input as $k => $v ) {
|
||||
if ( ++$next_key !== $k ) {
|
||||
return false;
|
||||
}
|
||||
@ -97,3 +96,16 @@ if ( ! function_exists( 'str_contains' ) ) {
|
||||
return false !== strpos( $haystack, $needle );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_is_serving_rest_request' ) ) {
|
||||
/**
|
||||
* Polyfill for `wp_is_serving_rest_request()` function added in WordPress 6.5.
|
||||
*
|
||||
* @see https://developer.wordpress.org/reference/functions/wp_is_serving_rest_request/
|
||||
*
|
||||
* @return bool True if it's a WordPress REST API request, false otherwise.
|
||||
*/
|
||||
function wp_is_serving_rest_request() {
|
||||
return defined( 'REST_REQUEST' ) && REST_REQUEST;
|
||||
}
|
||||
}
|
||||
|
76
wp-content/plugins/activitypub/includes/constants.php
Normal file
76
wp-content/plugins/activitypub/includes/constants.php
Normal file
@ -0,0 +1,76 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin constants.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
// The following constants can be defined in your wp-config.php file to override the default values.
|
||||
|
||||
\defined( 'ACTIVITYPUB_REST_NAMESPACE' ) || \define( 'ACTIVITYPUB_REST_NAMESPACE', 'activitypub/1.0' );
|
||||
\defined( 'ACTIVITYPUB_EXCERPT_LENGTH' ) || \define( 'ACTIVITYPUB_EXCERPT_LENGTH', 400 );
|
||||
\defined( 'ACTIVITYPUB_NOTE_LENGTH' ) || \define( 'ACTIVITYPUB_NOTE_LENGTH', 400 );
|
||||
\defined( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS' ) || \define( 'ACTIVITYPUB_SHOW_PLUGIN_RECOMMENDATIONS', true );
|
||||
\defined( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS' ) || \define( 'ACTIVITYPUB_MAX_IMAGE_ATTACHMENTS', 3 );
|
||||
\defined( 'ACTIVITYPUB_HASHTAGS_REGEXP' ) || \define( 'ACTIVITYPUB_HASHTAGS_REGEXP', '(?:(?<=\s)|(?<=<p>)|(?<=<br>)|^)#([A-Za-z0-9_]+)(?:(?=\s|[[:punct:]]|$))' );
|
||||
\defined( 'ACTIVITYPUB_USERNAME_REGEXP' ) || \define( 'ACTIVITYPUB_USERNAME_REGEXP', '(?:([A-Za-z0-9\._-]+)@((?:[A-Za-z0-9_-]+\.)+[A-Za-z]+))' );
|
||||
\defined( 'ACTIVITYPUB_URL_REGEXP' ) || \define( 'ACTIVITYPUB_URL_REGEXP', '(https?:|www\.)\S+[\w\/]' );
|
||||
\defined( 'ACTIVITYPUB_CUSTOM_POST_CONTENT' ) || \define( 'ACTIVITYPUB_CUSTOM_POST_CONTENT', "[ap_title type=\"html\"]\n\n[ap_content]\n\n[ap_hashtags]" );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_REWRITES' ) || \define( 'ACTIVITYPUB_DISABLE_REWRITES', false );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS', false );
|
||||
\defined( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS' ) || \define( 'ACTIVITYPUB_DISABLE_OUTGOING_INTERACTIONS', false );
|
||||
\defined( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE' ) || \define( 'ACTIVITYPUB_DEFAULT_OBJECT_TYPE', 'wordpress-post-format' );
|
||||
\defined( 'ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE' ) || \define( 'ACTIVITYPUB_OUTBOX_PROCESSING_BATCH_SIZE', 100 );
|
||||
|
||||
// The following constants are invariable and define values used throughout the plugin.
|
||||
|
||||
/*
|
||||
* Mastodon HTML sanitizer.
|
||||
*
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#sanitization
|
||||
*/
|
||||
\define(
|
||||
'ACTIVITYPUB_MASTODON_HTML_SANITIZER',
|
||||
array(
|
||||
'p' => array(),
|
||||
'span' => array( 'class' => true ),
|
||||
'br' => array(),
|
||||
'a' => array(
|
||||
'href' => true,
|
||||
'rel' => true,
|
||||
'class' => true,
|
||||
),
|
||||
'del' => array(),
|
||||
'pre' => array(),
|
||||
'code' => array(),
|
||||
'em' => array(),
|
||||
'strong' => array(),
|
||||
'b' => array(),
|
||||
'i' => array(),
|
||||
'u' => array(),
|
||||
'ul' => array(),
|
||||
'ol' => array(
|
||||
'start' => true,
|
||||
'reversed' => true,
|
||||
),
|
||||
'li' => array( 'value' => true ),
|
||||
'blockquote' => array(),
|
||||
'h1' => array(),
|
||||
'h2' => array(),
|
||||
'h3' => array(),
|
||||
'h4' => array(),
|
||||
)
|
||||
);
|
||||
|
||||
\define( 'ACTIVITYPUB_DATE_TIME_RFC3339', 'Y-m-d\TH:i:s\Z' );
|
||||
|
||||
// Define Actor-Modes for the plugin.
|
||||
\define( 'ACTIVITYPUB_ACTOR_MODE', 'actor' );
|
||||
\define( 'ACTIVITYPUB_BLOG_MODE', 'blog' );
|
||||
\define( 'ACTIVITYPUB_ACTOR_AND_BLOG_MODE', 'actor_blog' );
|
||||
|
||||
// Post visibility constants.
|
||||
\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PUBLIC', '' );
|
||||
\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_QUIET_PUBLIC', 'quiet_public' );
|
||||
\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE', 'private' );
|
||||
\define( 'ACTIVITYPUB_CONTENT_VISIBILITY_LOCAL', 'local' );
|
@ -1,17 +1,79 @@
|
||||
<?php
|
||||
/**
|
||||
* Debugging functions.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub;
|
||||
|
||||
/**
|
||||
* Allow localhost URLs if WP_DEBUG is true.
|
||||
*
|
||||
* @param array $r Array of HTTP request args.
|
||||
* @param string $url The request URL.
|
||||
* @param array $parsed_args An array of HTTP request arguments.
|
||||
*
|
||||
* @return array Array or string of HTTP request arguments.
|
||||
*/
|
||||
function allow_localhost( $r, $url ) {
|
||||
$r['reject_unsafe_urls'] = false;
|
||||
function allow_localhost( $parsed_args ) {
|
||||
$parsed_args['reject_unsafe_urls'] = false;
|
||||
|
||||
return $r;
|
||||
return $parsed_args;
|
||||
}
|
||||
add_filter( 'http_request_args', '\Activitypub\allow_localhost', 10, 2 );
|
||||
\add_filter( 'http_request_args', '\Activitypub\allow_localhost' );
|
||||
|
||||
/**
|
||||
* Debug the outbox post type.
|
||||
*
|
||||
* @param array $args The arguments for the post type.
|
||||
* @param string $post_type The post type.
|
||||
*
|
||||
* @return array The arguments for the post type.
|
||||
*/
|
||||
function debug_outbox_post_type( $args, $post_type ) {
|
||||
if ( 'ap_outbox' !== $post_type ) {
|
||||
return $args;
|
||||
}
|
||||
|
||||
$args['show_ui'] = true;
|
||||
$args['menu_icon'] = 'dashicons-upload';
|
||||
|
||||
return $args;
|
||||
}
|
||||
\add_filter( 'register_post_type_args', '\Activitypub\debug_outbox_post_type', 10, 2 );
|
||||
|
||||
/**
|
||||
* Debug the outbox post type column.
|
||||
*
|
||||
* @param array $columns The columns.
|
||||
* @param string $post_type The post type.
|
||||
*
|
||||
* @return array The updated columns.
|
||||
*/
|
||||
function debug_outbox_post_type_column( $columns, $post_type ) {
|
||||
if ( 'ap_outbox' !== $post_type ) {
|
||||
return $columns;
|
||||
}
|
||||
|
||||
$columns['ap_outbox_meta'] = 'Meta';
|
||||
|
||||
return $columns;
|
||||
}
|
||||
\add_filter( 'manage_posts_columns', '\Activitypub\debug_outbox_post_type_column', 10, 2 );
|
||||
|
||||
/**
|
||||
* Debug the outbox post type meta.
|
||||
*
|
||||
* @param string $column_name The column name.
|
||||
* @param int $post_id The post ID.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
function manage_posts_custom_column( $column_name, $post_id ) {
|
||||
if ( 'ap_outbox_meta' === $column_name ) {
|
||||
$meta = \get_post_meta( $post_id );
|
||||
foreach ( $meta as $key => $value ) {
|
||||
echo \esc_attr( $key ) . ': ' . \esc_html( $value[0] ) . '<br>';
|
||||
}
|
||||
}
|
||||
}
|
||||
\add_action( 'manage_posts_custom_column', '\Activitypub\manage_posts_custom_column', 10, 2 );
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,125 @@
|
||||
<?php
|
||||
/**
|
||||
* Announce handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Comment;
|
||||
use Activitypub\Collection\Interactions;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
use function Activitypub\is_activity_public;
|
||||
|
||||
/**
|
||||
* Handle Create requests.
|
||||
*/
|
||||
class Announce {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_announce',
|
||||
array( self::class, 'handle_announce' ),
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles "Announce" requests.
|
||||
*
|
||||
* @param array $announcement The activity-object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
* @param \Activitypub\Activity\Activity $activity The activity object.
|
||||
*/
|
||||
public static function handle_announce( $announcement, $user_id, $activity = null ) {
|
||||
// Check if Activity is public or not.
|
||||
if ( ! is_activity_public( $announcement ) ) {
|
||||
// @todo maybe send email
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if reposts are allowed.
|
||||
if ( ! Comment::is_comment_type_enabled( 'repost' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
self::maybe_save_announce( $announcement, $user_id );
|
||||
|
||||
if ( is_string( $announcement['object'] ) ) {
|
||||
$object = Http::get_remote_object( $announcement['object'] );
|
||||
} else {
|
||||
$object = $announcement['object'];
|
||||
}
|
||||
|
||||
if ( ! $object || is_wp_error( $object ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ( ! isset( $object['type'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$type = \strtolower( $object['type'] );
|
||||
|
||||
/**
|
||||
* Fires after an Announce has been received.
|
||||
*
|
||||
* @param array $object The object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
* @param string $type The type of the activity.
|
||||
* @param \Activitypub\Activity\Activity|null $activity The activity object.
|
||||
*/
|
||||
\do_action( 'activitypub_inbox', $object, $user_id, $type, $activity );
|
||||
|
||||
/**
|
||||
* Fires after an Announce of a specific type has been received.
|
||||
*
|
||||
* @param array $object The object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
* @param \Activitypub\Activity\Activity|null $activity The activity object.
|
||||
*/
|
||||
\do_action( "activitypub_inbox_{$type}", $object, $user_id, $activity );
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to save the Announce.
|
||||
*
|
||||
* @param array $activity The activity-object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
*/
|
||||
public static function maybe_save_announce( $activity, $user_id ) {
|
||||
$url = object_to_uri( $activity['object'] );
|
||||
|
||||
if ( empty( $url ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = Comment::object_id_to_comment( esc_url_raw( $url ) );
|
||||
if ( $exists ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$state = Interactions::add_reaction( $activity );
|
||||
$reaction = null;
|
||||
|
||||
if ( $state && ! is_wp_error( $state ) ) {
|
||||
$reaction = get_comment( $state );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after an Announce has been saved.
|
||||
*
|
||||
* @param array $activity The activity-object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
* @param mixed $state The state of the reaction.
|
||||
* @param mixed $reaction The reaction.
|
||||
*/
|
||||
do_action( 'activitypub_handled_announce', $activity, $user_id, $state, $reaction );
|
||||
}
|
||||
}
|
@ -1,18 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* Create handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Collection\Interactions;
|
||||
|
||||
use function Activitypub\is_self_ping;
|
||||
use function Activitypub\is_activity_reply;
|
||||
use function Activitypub\is_activity_public;
|
||||
use function Activitypub\object_id_to_comment;
|
||||
|
||||
/**
|
||||
* Handle Create requests
|
||||
* Handle Create requests.
|
||||
*/
|
||||
class Create {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
@ -21,50 +28,106 @@ class Create {
|
||||
10,
|
||||
3
|
||||
);
|
||||
|
||||
\add_filter(
|
||||
'activitypub_validate_object',
|
||||
array( self::class, 'validate_object' ),
|
||||
10,
|
||||
3
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles "Create" requests
|
||||
* Handles "Create" requests.
|
||||
*
|
||||
* @param array $array The activity-object
|
||||
* @param int $user_id The id of the local blog-user
|
||||
* @param Activitypub\Activity $object The activity object
|
||||
*
|
||||
* @return void
|
||||
* @param array $activity The activity-object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
* @param \Activitypub\Activity\Activity $activity_object Optional. The activity object. Default null.
|
||||
*/
|
||||
public static function handle_create( $array, $user_id, $object = null ) {
|
||||
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
|
||||
return;
|
||||
}
|
||||
|
||||
public static function handle_create( $activity, $user_id, $activity_object = null ) {
|
||||
// Check if Activity is public or not.
|
||||
if (
|
||||
! isset( $array['object'] ) ||
|
||||
! isset( $array['object']['id'] )
|
||||
! is_activity_public( $activity ) ||
|
||||
! is_activity_reply( $activity )
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if Activity is public or not
|
||||
if ( ! is_activity_public( $array ) ) {
|
||||
// @todo maybe send email
|
||||
return;
|
||||
}
|
||||
$check_dupe = object_id_to_comment( $activity['object']['id'] );
|
||||
|
||||
$check_dupe = object_id_to_comment( $array['object']['id'] );
|
||||
|
||||
// if comment exists, call update action
|
||||
// If comment exists, call update action.
|
||||
if ( $check_dupe ) {
|
||||
\do_action( 'activitypub_inbox_update', $array, $user_id, $object );
|
||||
/**
|
||||
* Fires when a Create activity is received for an existing comment.
|
||||
*
|
||||
* @param array $activity The activity-object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
* @param \Activitypub\Activity\Activity $activity_object The activity object.
|
||||
*/
|
||||
\do_action( 'activitypub_inbox_update', $activity, $user_id, $activity_object );
|
||||
return;
|
||||
}
|
||||
|
||||
$state = Interactions::add_comment( $array );
|
||||
if ( is_self_ping( $activity['object']['id'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$state = Interactions::add_comment( $activity );
|
||||
$reaction = null;
|
||||
|
||||
if ( $state && ! \is_wp_error( $reaction ) ) {
|
||||
if ( $state && ! \is_wp_error( $state ) ) {
|
||||
$reaction = \get_comment( $state );
|
||||
}
|
||||
|
||||
\do_action( 'activitypub_handled_create', $array, $user_id, $state, $reaction );
|
||||
/**
|
||||
* Fires after a Create activity has been handled.
|
||||
*
|
||||
* @param array $activity The activity-object.
|
||||
* @param int $user_id The id of the local blog-user.
|
||||
* @param \WP_Comment|\WP_Error $state The comment object or WP_Error.
|
||||
* @param \WP_Comment|\WP_Error|null $reaction The reaction object or WP_Error.
|
||||
*/
|
||||
\do_action( 'activitypub_handled_create', $activity, $user_id, $state, $reaction );
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate the object.
|
||||
*
|
||||
* @param bool $valid The validation state.
|
||||
* @param string $param The object parameter.
|
||||
* @param \WP_REST_Request $request The request object.
|
||||
*
|
||||
* @return bool The validation state: true if valid, false if not.
|
||||
*/
|
||||
public static function validate_object( $valid, $param, $request ) {
|
||||
$json_params = $request->get_json_params();
|
||||
|
||||
if ( empty( $json_params['type'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
'Create' !== $json_params['type'] ||
|
||||
is_wp_error( $request )
|
||||
) {
|
||||
return $valid;
|
||||
}
|
||||
|
||||
$object = $json_params['object'];
|
||||
|
||||
if ( ! is_array( $object ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$required = array(
|
||||
'id',
|
||||
'content',
|
||||
);
|
||||
|
||||
if ( array_intersect( $required, array_keys( $object ) ) !== $required ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return $valid;
|
||||
}
|
||||
}
|
||||
|
@ -1,52 +1,47 @@
|
||||
<?php
|
||||
/**
|
||||
* Delete handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Interactions;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handles Delete requests.
|
||||
*/
|
||||
class Delete {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_delete',
|
||||
array( self::class, 'handle_delete' )
|
||||
);
|
||||
|
||||
// defer signature verification for `Delete` requests.
|
||||
\add_filter(
|
||||
'activitypub_defer_signature_verification',
|
||||
array( self::class, 'defer_signature_verification' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
|
||||
// side effect
|
||||
\add_action(
|
||||
'activitypub_delete_actor_interactions',
|
||||
array( self::class, 'delete_interactions' )
|
||||
);
|
||||
\add_action( 'activitypub_inbox_delete', array( self::class, 'handle_delete' ) );
|
||||
\add_filter( 'activitypub_defer_signature_verification', array( self::class, 'defer_signature_verification' ), 10, 2 );
|
||||
\add_action( 'activitypub_delete_actor_interactions', array( self::class, 'delete_interactions' ) );
|
||||
\add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles "Delete" requests.
|
||||
*
|
||||
* @param array $activity The delete activity.
|
||||
* @param int $user_id The ID of the user performing the delete activity.
|
||||
*/
|
||||
public static function handle_delete( $activity ) {
|
||||
$object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : '';
|
||||
|
||||
switch ( $object_type ) {
|
||||
// Actor Types
|
||||
// @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
||||
/*
|
||||
* Actor Types.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
||||
*/
|
||||
case 'Person':
|
||||
case 'Group':
|
||||
case 'Organization':
|
||||
@ -54,8 +49,12 @@ class Delete {
|
||||
case 'Application':
|
||||
self::maybe_delete_follower( $activity );
|
||||
break;
|
||||
// Object and Link Types
|
||||
// @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||
|
||||
/*
|
||||
* Object and Link Types.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||
*/
|
||||
case 'Note':
|
||||
case 'Article':
|
||||
case 'Image':
|
||||
@ -65,26 +64,34 @@ class Delete {
|
||||
case 'Document':
|
||||
self::maybe_delete_interaction( $activity );
|
||||
break;
|
||||
// Tombstone Type
|
||||
// @see: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
|
||||
|
||||
/*
|
||||
* Tombstone Type.
|
||||
*
|
||||
* @see: https://www.w3.org/TR/activitystreams-vocabulary/#dfn-tombstone
|
||||
*/
|
||||
case 'Tombstone':
|
||||
self::maybe_delete_interaction( $activity );
|
||||
break;
|
||||
// Minimal Activity
|
||||
// @see https://www.w3.org/TR/activitystreams-core/#example-1
|
||||
|
||||
/*
|
||||
* Minimal Activity.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-core/#example-1
|
||||
*/
|
||||
default:
|
||||
// ignore non Minimal Activities.
|
||||
// Ignore non Minimal Activities.
|
||||
if ( ! is_string( $activity['object'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// check if Object is an Actor.
|
||||
// Check if Object is an Actor.
|
||||
if ( $activity['actor'] === $activity['object'] ) {
|
||||
self::maybe_delete_follower( $activity );
|
||||
} else { // assume a interaction otherwise.
|
||||
} else { // Assume an interaction otherwise.
|
||||
self::maybe_delete_interaction( $activity );
|
||||
}
|
||||
// maybe handle Delete Activity for other Object Types.
|
||||
// Maybe handle Delete Activity for other Object Types.
|
||||
break;
|
||||
}
|
||||
}
|
||||
@ -95,9 +102,10 @@ class Delete {
|
||||
* @param array $activity The delete activity.
|
||||
*/
|
||||
public static function maybe_delete_follower( $activity ) {
|
||||
/* @var \Activitypub\Model\Follower $follower Follower object. */
|
||||
$follower = Followers::get_follower_by_actor( $activity['actor'] );
|
||||
|
||||
// verify if Actor is deleted.
|
||||
// Verify that Actor is deleted.
|
||||
if ( $follower && Http::is_tombstone( $activity['actor'] ) ) {
|
||||
$follower->delete();
|
||||
self::maybe_delete_interactions( $activity );
|
||||
@ -110,7 +118,7 @@ class Delete {
|
||||
* @param array $activity The delete activity.
|
||||
*/
|
||||
public static function maybe_delete_interactions( $activity ) {
|
||||
// verify if Actor is deleted.
|
||||
// Verify that Actor is deleted.
|
||||
if ( Http::is_tombstone( $activity['actor'] ) ) {
|
||||
\wp_schedule_single_event(
|
||||
\time(),
|
||||
@ -123,15 +131,13 @@ class Delete {
|
||||
/**
|
||||
* Delete comments from an Actor.
|
||||
*
|
||||
* @param array $comments The comments to delete.
|
||||
* @param string $actor The URL of the actor whose comments to delete.
|
||||
*/
|
||||
public static function delete_interactions( $actor ) {
|
||||
$comments = Interactions::get_interactions_by_actor( $actor );
|
||||
|
||||
if ( is_array( $comments ) ) {
|
||||
foreach ( $comments as $comment ) {
|
||||
wp_delete_comment( $comment->comment_ID );
|
||||
}
|
||||
foreach ( $comments as $comment ) {
|
||||
wp_delete_comment( $comment, true );
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,8 +145,6 @@ class Delete {
|
||||
* Delete a Reaction if URL is a Tombstone.
|
||||
*
|
||||
* @param array $activity The delete activity.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function maybe_delete_interaction( $activity ) {
|
||||
if ( is_array( $activity['object'] ) ) {
|
||||
@ -175,4 +179,18 @@ class Delete {
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the object to the object ID.
|
||||
*
|
||||
* @param \Activitypub\Activity\Activity $activity The Activity object.
|
||||
* @return \Activitypub\Activity\Activity The filtered Activity object.
|
||||
*/
|
||||
public static function outbox_activity( $activity ) {
|
||||
if ( 'Delete' === $activity->get_type() ) {
|
||||
$activity->set_object( object_to_uri( $activity->get_object() ) );
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,25 @@
|
||||
<?php
|
||||
/**
|
||||
* Follow handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Notification;
|
||||
use Activitypub\Activity\Activity;
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Followers;
|
||||
|
||||
use function Activitypub\add_to_outbox;
|
||||
|
||||
/**
|
||||
* Handle Follow requests
|
||||
* Handle Follow requests.
|
||||
*/
|
||||
class Follow {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
@ -21,64 +29,70 @@ class Follow {
|
||||
|
||||
\add_action(
|
||||
'activitypub_followers_post_follow',
|
||||
array( self::class, 'send_follow_response' ),
|
||||
array( self::class, 'queue_accept' ),
|
||||
10,
|
||||
4
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "Follow" requests
|
||||
* Handle "Follow" requests.
|
||||
*
|
||||
* @param array $activity The activity object
|
||||
* @param int $user_id The user ID
|
||||
* @param array $activity The activity object.
|
||||
*/
|
||||
public static function handle_follow( $activity ) {
|
||||
$user = Users::get_by_resource( $activity['object'] );
|
||||
$user = Actors::get_by_resource( $activity['object'] );
|
||||
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
// If we can not find a user,
|
||||
// we can not initiate a follow process
|
||||
// If we can not find a user, we can not initiate a follow process.
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = $user->get__id();
|
||||
|
||||
// save follower
|
||||
// Save follower.
|
||||
$follower = Followers::add_follower(
|
||||
$user_id,
|
||||
$activity['actor']
|
||||
);
|
||||
|
||||
do_action(
|
||||
'activitypub_followers_post_follow',
|
||||
/**
|
||||
* Fires after a new follower has been added.
|
||||
*
|
||||
* @param string $actor The URL of the actor (follower) who initiated the follow.
|
||||
* @param array $activity The complete activity data of the follow request.
|
||||
* @param int $user_id The ID of the WordPress user being followed.
|
||||
* @param \Activitypub\Model\Follower|\WP_Error $follower The Follower object containing the new follower's data.
|
||||
*/
|
||||
do_action( 'activitypub_followers_post_follow', $activity['actor'], $activity, $user_id, $follower );
|
||||
|
||||
// Send notification.
|
||||
$notification = new Notification(
|
||||
'follow',
|
||||
$activity['actor'],
|
||||
$activity,
|
||||
$user_id,
|
||||
$follower
|
||||
$user_id
|
||||
);
|
||||
$notification->send();
|
||||
}
|
||||
|
||||
/**
|
||||
* Send Accept response
|
||||
* Send Accept response.
|
||||
*
|
||||
* @param string $actor The Actor URL
|
||||
* @param array $object The Activity object
|
||||
* @param int $user_id The ID of the WordPress User
|
||||
* @param Activitypub\Model\Follower $follower The Follower object
|
||||
*
|
||||
* @return void
|
||||
* @param string $actor The Actor URL.
|
||||
* @param array $activity_object The Activity object.
|
||||
* @param int $user_id The ID of the WordPress User.
|
||||
* @param \Activitypub\Model\Follower|\WP_Error $follower The Follower object.
|
||||
*/
|
||||
public static function send_follow_response( $actor, $object, $user_id, $follower ) {
|
||||
public static function queue_accept( $actor, $activity_object, $user_id, $follower ) {
|
||||
if ( \is_wp_error( $follower ) ) {
|
||||
// it is not even possible to send a "Reject" because
|
||||
// we can not get the Remote-Inbox
|
||||
// Impossible to send a "Reject" because we can not get the Remote-Inbox.
|
||||
return;
|
||||
}
|
||||
|
||||
// only send minimal data
|
||||
$object = array_intersect_key(
|
||||
$object,
|
||||
// Only send minimal data.
|
||||
$activity_object = array_intersect_key(
|
||||
$activity_object,
|
||||
array_flip(
|
||||
array(
|
||||
'id',
|
||||
@ -89,21 +103,12 @@ class Follow {
|
||||
)
|
||||
);
|
||||
|
||||
$user = Users::get_by_id( $user_id );
|
||||
|
||||
// get inbox
|
||||
$inbox = $follower->get_shared_inbox();
|
||||
|
||||
// send "Accept" activity
|
||||
$activity = new Activity();
|
||||
$activity->set_type( 'Accept' );
|
||||
$activity->set_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->set_actor( Actors::get_by_id( $user_id )->get_id() );
|
||||
$activity->set_object( $activity_object );
|
||||
$activity->set_to( array( $actor ) );
|
||||
|
||||
$activity = $activity->to_json();
|
||||
|
||||
Http::post( $inbox, $activity, $user_id );
|
||||
add_to_outbox( $activity, null, $user_id, ACTIVITYPUB_CONTENT_VISIBILITY_PRIVATE );
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,80 @@
|
||||
<?php
|
||||
/**
|
||||
* Like handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Comment;
|
||||
use Activitypub\Collection\Interactions;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handle Like requests.
|
||||
*/
|
||||
class Like {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_inbox_like', array( self::class, 'handle_like' ), 10, 2 );
|
||||
\add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles "Like" requests.
|
||||
*
|
||||
* @param array $like The Activity array.
|
||||
* @param int $user_id The ID of the local blog user.
|
||||
*/
|
||||
public static function handle_like( $like, $user_id ) {
|
||||
if ( ! Comment::is_comment_type_enabled( 'like' ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$url = object_to_uri( $like['object'] );
|
||||
|
||||
if ( empty( $url ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$exists = Comment::object_id_to_comment( esc_url_raw( $url ) );
|
||||
if ( $exists ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$state = Interactions::add_reaction( $like );
|
||||
$reaction = null;
|
||||
|
||||
if ( $state && ! is_wp_error( $state ) ) {
|
||||
$reaction = get_comment( $state );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after a Like has been handled.
|
||||
*
|
||||
* @param array $like The Activity array.
|
||||
* @param int $user_id The ID of the local blog user.
|
||||
* @param mixed $state The state of the reaction.
|
||||
* @param mixed $reaction The reaction object.
|
||||
*/
|
||||
do_action( 'activitypub_handled_like', $like, $user_id, $state, $reaction );
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the object to the object ID.
|
||||
*
|
||||
* @param \Activitypub\Activity\Activity $activity The Activity object.
|
||||
* @return \Activitypub\Activity\Activity The filtered Activity object.
|
||||
*/
|
||||
public static function outbox_activity( $activity ) {
|
||||
if ( 'Like' === $activity->get_type() ) {
|
||||
$activity->set_object( object_to_uri( $activity->get_object() ) );
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
}
|
213
wp-content/plugins/activitypub/includes/handler/class-move.php
Normal file
213
wp-content/plugins/activitypub/includes/handler/class-move.php
Normal file
@ -0,0 +1,213 @@
|
||||
<?php
|
||||
/**
|
||||
* Move handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Http;
|
||||
use Activitypub\Collection\Followers;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handle Move requests.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#dfn-move
|
||||
* @see https://docs.joinmastodon.org/user/moving/
|
||||
* @see https://docs.joinmastodon.org/spec/activitypub/#Move
|
||||
*/
|
||||
class Move {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action( 'activitypub_inbox_move', array( self::class, 'handle_move' ) );
|
||||
\add_filter( 'activitypub_get_outbox_activity', array( self::class, 'outbox_activity' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle Move requests.
|
||||
*
|
||||
* @param array $activity The JSON "Move" Activity.
|
||||
*/
|
||||
public static function handle_move( $activity ) {
|
||||
$target = self::extract_target( $activity );
|
||||
$origin = self::extract_origin( $activity );
|
||||
|
||||
if ( ! $target || ! $origin ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target_object = Http::get_remote_object( $target );
|
||||
$origin_object = Http::get_remote_object( $origin );
|
||||
|
||||
$verified = self::verify_move( $target_object, $origin_object );
|
||||
|
||||
if ( ! $verified ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$target_follower = Followers::get_follower_by_actor( $target );
|
||||
$origin_follower = Followers::get_follower_by_actor( $origin );
|
||||
|
||||
/*
|
||||
* If the new target is followed, but the origin is not,
|
||||
* everything is fine, so we can return.
|
||||
*/
|
||||
if ( $target_follower && ! $origin_follower ) {
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the new target is not followed, but the origin is,
|
||||
* update the origin follower to the new target.
|
||||
*/
|
||||
if ( ! $target_follower && $origin_follower ) {
|
||||
$origin_follower->from_array( $target_object );
|
||||
$origin_follower->set_id( $target );
|
||||
$origin_id = $origin_follower->upsert();
|
||||
|
||||
global $wpdb;
|
||||
// phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery
|
||||
$wpdb->update(
|
||||
$wpdb->posts,
|
||||
array( 'guid' => sanitize_url( $target ) ),
|
||||
array( 'ID' => sanitize_key( $origin_id ) )
|
||||
);
|
||||
|
||||
// Clear the cache.
|
||||
wp_cache_delete( $origin_id, 'posts' );
|
||||
return;
|
||||
}
|
||||
|
||||
/*
|
||||
* If the new target is followed, and the origin is followed,
|
||||
* move users and delete the origin follower.
|
||||
*/
|
||||
if ( $target_follower && $origin_follower ) {
|
||||
$origin_users = \get_post_meta( $origin_follower->get__id(), '_activitypub_user_id', false );
|
||||
$target_users = \get_post_meta( $target_follower->get__id(), '_activitypub_user_id', false );
|
||||
|
||||
// Get all user ids from $origin_users that are not in $target_users.
|
||||
$users = \array_diff( $origin_users, $target_users );
|
||||
|
||||
foreach ( $users as $user_id ) {
|
||||
\add_post_meta( $target_follower->get__id(), '_activitypub_user_id', $user_id );
|
||||
}
|
||||
|
||||
$origin_follower->delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert the object and origin to the correct format.
|
||||
*
|
||||
* @param \Activitypub\Activity\Activity $activity The Activity object.
|
||||
* @return \Activitypub\Activity\Activity The filtered Activity object.
|
||||
*/
|
||||
public static function outbox_activity( $activity ) {
|
||||
if ( 'Move' === $activity->get_type() ) {
|
||||
$activity->set_object( object_to_uri( $activity->get_object() ) );
|
||||
$activity->set_origin( $activity->get_actor() );
|
||||
$activity->set_target( $activity->get_object() );
|
||||
}
|
||||
|
||||
return $activity;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the target from the activity.
|
||||
*
|
||||
* The ActivityStreams spec define the `target` attribute as the
|
||||
* destination of the activity, but Mastodon uses the `object`
|
||||
* attribute to move profiles.
|
||||
*
|
||||
* @param array $activity The JSON "Move" Activity.
|
||||
*
|
||||
* @return string|null The target URI or null if not found.
|
||||
*/
|
||||
private static function extract_target( $activity ) {
|
||||
if ( ! empty( $activity['target'] ) ) {
|
||||
return object_to_uri( $activity['target'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $activity['object'] ) ) {
|
||||
return object_to_uri( $activity['object'] );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the origin from the activity.
|
||||
*
|
||||
* The ActivityStreams spec define the `origin` attribute as source
|
||||
* of the activity, but Mastodon uses the `actor` attribute as source
|
||||
* to move profiles.
|
||||
*
|
||||
* @param array $activity The JSON "Move" Activity.
|
||||
*
|
||||
* @return string|null The origin URI or null if not found.
|
||||
*/
|
||||
private static function extract_origin( $activity ) {
|
||||
if ( ! empty( $activity['origin'] ) ) {
|
||||
return object_to_uri( $activity['origin'] );
|
||||
}
|
||||
|
||||
if ( ! empty( $activity['actor'] ) ) {
|
||||
return object_to_uri( $activity['actor'] );
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify the move.
|
||||
*
|
||||
* @param array $target_object The target object.
|
||||
* @param array $origin_object The origin object.
|
||||
*
|
||||
* @return bool True if the move is verified, false otherwise.
|
||||
*/
|
||||
private static function verify_move( $target_object, $origin_object ) {
|
||||
// Check if both objects are valid.
|
||||
if ( \is_wp_error( $target_object ) || \is_wp_error( $origin_object ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if both objects are persons.
|
||||
if ( 'Person' !== $target_object['type'] || 'Person' !== $origin_object['type'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the target and origin are not the same.
|
||||
if ( $target_object['id'] === $origin_object['id'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the target has an alsoKnownAs property.
|
||||
if ( empty( $target_object['also_known_as'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the origin is in the alsoKnownAs property of the target.
|
||||
if ( ! in_array( $origin_object['id'], $target_object['also_known_as'], true ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the origin has a movedTo property.
|
||||
if ( empty( $origin_object['movedTo'] ) ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if the movedTo property of the origin is the target.
|
||||
if ( $origin_object['movedTo'] !== $target_object['id'] ) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
@ -1,47 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* Undo handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use Activitypub\Collection\Users;
|
||||
use Activitypub\Collection\Actors;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Comment;
|
||||
|
||||
use function Activitypub\object_to_uri;
|
||||
|
||||
/**
|
||||
* Handle Undo requests
|
||||
* Handle Undo requests.
|
||||
*/
|
||||
class Undo {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
'activitypub_inbox_undo',
|
||||
array( self::class, 'handle_undo' )
|
||||
array( self::class, 'handle_undo' ),
|
||||
10,
|
||||
2
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "Unfollow" requests
|
||||
* Handle "Unfollow" requests.
|
||||
*
|
||||
* @param array $activity The JSON "Undo" Activity
|
||||
* @param int $user_id The ID of the ID of the WordPress User
|
||||
* @param array $activity The JSON "Undo" Activity.
|
||||
* @param int|null $user_id The ID of the user who initiated the "Undo" activity.
|
||||
*/
|
||||
public static function handle_undo( $activity ) {
|
||||
public static function handle_undo( $activity, $user_id ) {
|
||||
if (
|
||||
isset( $activity['object']['type'] ) &&
|
||||
'Follow' === $activity['object']['type'] &&
|
||||
isset( $activity['object']['object'] ) &&
|
||||
filter_var( $activity['object']['object'], FILTER_VALIDATE_URL )
|
||||
! isset( $activity['object']['type'] ) ||
|
||||
! isset( $activity['object']['object'] )
|
||||
) {
|
||||
$user = Users::get_by_resource( $activity['object']['object'] );
|
||||
return;
|
||||
}
|
||||
|
||||
$type = $activity['object']['type'];
|
||||
$state = false;
|
||||
|
||||
// Handle "Unfollow" requests.
|
||||
if ( 'Follow' === $type ) {
|
||||
$id = object_to_uri( $activity['object']['object'] );
|
||||
$user = Actors::get_by_resource( $id );
|
||||
|
||||
if ( ! $user || is_wp_error( $user ) ) {
|
||||
// If we can not find a user,
|
||||
// we can not initiate a follow process
|
||||
// If we can not find a user, we can not initiate a follow process.
|
||||
return;
|
||||
}
|
||||
|
||||
$user_id = $user->get__id();
|
||||
$actor = object_to_uri( $activity['actor'] );
|
||||
|
||||
Followers::remove_follower( $user_id, $activity['actor'] );
|
||||
$state = Followers::remove_follower( $user_id, $actor );
|
||||
}
|
||||
|
||||
// Handle "Undo" requests for "Like" and "Create" activities.
|
||||
if ( in_array( $type, array( 'Like', 'Create', 'Announce' ), true ) ) {
|
||||
if ( ACTIVITYPUB_DISABLE_INCOMING_INTERACTIONS ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$object_id = object_to_uri( $activity['object'] );
|
||||
$comment = Comment::object_id_to_comment( esc_url_raw( $object_id ) );
|
||||
|
||||
if ( empty( $comment ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$state = wp_delete_comment( $comment, true );
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after an "Undo" activity has been handled.
|
||||
*
|
||||
* @param array $activity The JSON "Undo" Activity.
|
||||
* @param int|null $user_id The ID of the user who initiated the "Undo" activity otherwise null.
|
||||
* @param mixed $state The state of the "Undo" activity.
|
||||
*/
|
||||
do_action( 'activitypub_handled_undo', $activity, $user_id, $state );
|
||||
}
|
||||
}
|
||||
|
@ -1,7 +1,13 @@
|
||||
<?php
|
||||
/**
|
||||
* Update handler file.
|
||||
*
|
||||
* @package Activitypub
|
||||
*/
|
||||
|
||||
namespace Activitypub\Handler;
|
||||
|
||||
use WP_Error;
|
||||
use Activitypub\Collection\Followers;
|
||||
use Activitypub\Collection\Interactions;
|
||||
|
||||
use function Activitypub\get_remote_metadata_by_actor;
|
||||
@ -11,7 +17,7 @@ use function Activitypub\get_remote_metadata_by_actor;
|
||||
*/
|
||||
class Update {
|
||||
/**
|
||||
* Initialize the class, registering WordPress hooks
|
||||
* Initialize the class, registering WordPress hooks.
|
||||
*/
|
||||
public static function init() {
|
||||
\add_action(
|
||||
@ -21,26 +27,32 @@ class Update {
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle "Update" requests
|
||||
* Handle "Update" requests.
|
||||
*
|
||||
* @param array $array The activity-object
|
||||
* @param int $user_id The id of the local blog-user
|
||||
* @param array $activity The Activity object.
|
||||
*/
|
||||
public static function handle_update( $array ) {
|
||||
$object_type = isset( $array['object']['type'] ) ? $array['object']['type'] : '';
|
||||
public static function handle_update( $activity ) {
|
||||
$object_type = isset( $activity['object']['type'] ) ? $activity['object']['type'] : '';
|
||||
|
||||
switch ( $object_type ) {
|
||||
// Actor Types
|
||||
// @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
||||
/*
|
||||
* Actor Types.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#actor-types
|
||||
*/
|
||||
case 'Person':
|
||||
case 'Group':
|
||||
case 'Organization':
|
||||
case 'Service':
|
||||
case 'Application':
|
||||
self::update_actor( $array );
|
||||
self::update_actor( $activity );
|
||||
break;
|
||||
// Object and Link Types
|
||||
// @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||
|
||||
/*
|
||||
* Object and Link Types.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-vocabulary/#object-types
|
||||
*/
|
||||
case 'Note':
|
||||
case 'Article':
|
||||
case 'Image':
|
||||
@ -48,22 +60,23 @@ class Update {
|
||||
case 'Video':
|
||||
case 'Event':
|
||||
case 'Document':
|
||||
self::update_interaction( $array );
|
||||
self::update_interaction( $activity );
|
||||
break;
|
||||
// Minimal Activity
|
||||
// @see https://www.w3.org/TR/activitystreams-core/#example-1
|
||||
|
||||
/*
|
||||
* Minimal Activity.
|
||||
*
|
||||
* @see https://www.w3.org/TR/activitystreams-core/#example-1
|
||||
*/
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an Interaction
|
||||
* Update an Interaction.
|
||||
*
|
||||
* @param array $activity The activity-object
|
||||
* @param int $user_id The id of the local blog-user
|
||||
*
|
||||
* @return void
|
||||
* @param array $activity The Activity object.
|
||||
*/
|
||||
public static function update_interaction( $activity ) {
|
||||
$commentdata = Interactions::update_comment( $activity );
|
||||
@ -76,20 +89,37 @@ class Update {
|
||||
$state = $commentdata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fires after an Update activity has been handled.
|
||||
*
|
||||
* @param array $activity The complete Update activity data.
|
||||
* @param null $user Always null for Update activities.
|
||||
* @param int|array $state 1 if comment was updated successfully, error data otherwise.
|
||||
* @param \WP_Comment|null $reaction The updated comment object if successful, null otherwise.
|
||||
*/
|
||||
\do_action( 'activitypub_handled_update', $activity, null, $state, $reaction );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an Actor
|
||||
* Update an Actor.
|
||||
*
|
||||
* @param array $activity The activity-object
|
||||
*
|
||||
* @return void
|
||||
* @param array $activity The Activity object.
|
||||
*/
|
||||
public static function update_actor( $activity ) {
|
||||
// update cache
|
||||
get_remote_metadata_by_actor( $activity['actor'], false );
|
||||
// Update cache.
|
||||
$actor = get_remote_metadata_by_actor( $activity['actor'], false );
|
||||
|
||||
// @todo maybe also update all interactions
|
||||
if ( ! $actor || \is_wp_error( $actor ) || ! isset( $actor['id'] ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$follower = Followers::get_follower_by_actor( $actor['id'] );
|
||||
|
||||
if ( ! $follower ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$follower->from_array( $actor );
|
||||
$follower->upsert();
|
||||
}
|
||||
}
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user