wordpress-activitypub/includes/functions.php

1309 lines
32 KiB
PHP
Raw Normal View History

2018-09-24 18:47:15 +00:00
<?php
/**
* Functions file.
*
* @package Activitypub
*/
namespace Activitypub;
use WP_Error;
2023-07-03 16:18:03 +00:00
use Activitypub\Activity\Activity;
use Activitypub\Collection\Followers;
use Activitypub\Collection\Users;
2023-07-03 16:18:03 +00:00
/**
* Returns the ActivityPub default JSON-context.
*
* @return array The activitypub context.
*/
function get_context() {
Add/event objects (#629) * remove redundant property definitions * Add redused context for actors. * Add classes to construct Moblizon compatible events * Bind the context to the activitypub object - change the propertyname which stores the json-ld context from context to _context, because context is already reserved in the ActivityStreams vocabulary. - cleanup currently unused code * fix phpcs * Remove PostalAddress object: it's enough (at least atm) to directly write the array in transformers. * Remove _context property from ActivityPub objects in favour of getter function get_json_ld_context() * fix unit tests: ActivityPub Activity objects have a custom getter for the JsonLD context * fix phpcs * fix unit-tests to also support php5.6 * fix phpcs * add param include_json_ld_context to to_array function This allows to not set the @context in the resulting array. * propagate the param include_json_ld_context to nested calls of to_array. * fix phpcs * Nested AcitivityPub objects: never build context in inner items in to_array function * fix: param of set_address may also be an array * fix typo in comment * always prefix json-ld context with json-ld and move event class to sub-namespace * fix usage of reserved object keyword seems it should not be used as a namespace either * Merge commit 'b2271cda6b857f879e0abd4f3c6683642d725267' into add/event-objects * Fix calling non-static function as static Co-authored-by: Matthias Pfefferle <pfefferle@users.noreply.github.com> * Partly fix Json-LD contexts in collections * Update includes/activity/class-base-object.php * this is implicit We already set the correct user with `$transformer->change_wp_user_id( $user_id );` so the Actor will be generated properly. We can change the behaviour, but we should not use both. * this change prevents the Activity to re-use Object vars this should stay as is, because it pre-fills the Activity with data (for example cc and to) and it will no longer be done with your change. It is on purpose that it first sets the object and then replaces it with the URI. See: https://github.com/Automattic/wordpress-activitypub/blob/master/includes/activity/class-activity.php#L195 * add `$include_json_ld_context` support to `to_json` * disable some more contexts * remove whitespace * Add php-comment for 7ed17c042a2651e08afc790adbdfc5ccf46c2708 * Fix JSON-LD context for ActivityPub objects: child classes may override it. * coding standards * call folder/namespace `Extended_Object` to be consistent with folder names in singular * fix: unnessesary nesting of extended-objects * remove license I hope this is fine, to have the complete plugin under the MIT @Menrath ?!? --------- Co-authored-by: Matthias Pfefferle <pfefferle@users.noreply.github.com>
2024-01-18 15:35:52 +00:00
$context = Activity::JSON_LD_CONTEXT;
2018-09-30 20:51:22 +00:00
2019-09-27 08:12:59 +00:00
return \apply_filters( 'activitypub_json_context', $context );
}
2018-12-07 23:02:18 +00:00
/**
* Send a POST request to a remote server.
*
* @param string $url The URL endpoint.
* @param string $body The Post Body.
* @param int $user_id The WordPress user ID.
*
* @return array|WP_Error The POST Response or an WP_Error.
*/
function safe_remote_post( $url, $body, $user_id ) {
2023-07-03 16:18:03 +00:00
return Http::post( $url, $body, $user_id );
}
/**
* Send a GET request to a remote server.
*
* @param string $url The URL endpoint.
*
* @return array|WP_Error The GET Response or an WP_Error.
*/
function safe_remote_get( $url ) {
2023-07-03 16:18:03 +00:00
return Http::get( $url );
2020-02-21 10:05:17 +00:00
}
/**
* Returns a users WebFinger "resource".
*
* @param int $user_id The user ID.
*
* @return string The User resource.
*/
function get_webfinger_resource( $user_id ) {
2023-04-24 18:46:51 +00:00
return Webfinger::get_user_resource( $user_id );
}
/**
* Requests the Meta-Data from the Actors profile.
*
2023-05-31 12:03:46 +00:00
* @param string $actor The Actor URL.
* @param bool $cached Optional. Whether the result should be cached. Default true.
*
2023-10-24 12:54:03 +00:00
* @return array|WP_Error The Actor profile as array or WP_Error on failure.
*/
2023-05-31 12:03:46 +00:00
function get_remote_metadata_by_actor( $actor, $cached = true ) {
2022-12-02 11:46:42 +00:00
$pre = apply_filters( 'pre_get_remote_metadata_by_actor', false, $actor );
if ( $pre ) {
return $pre;
}
2024-04-23 06:47:35 +00:00
if ( is_array( $actor ) ) {
if ( array_key_exists( 'id', $actor ) ) {
$actor = $actor['id'];
} elseif ( array_key_exists( 'url', $actor ) ) {
$actor = $actor['url'];
} else {
2024-07-26 08:26:47 +00:00
return new WP_Error(
'activitypub_no_valid_actor_identifier',
\__( 'The "actor" identifier is not valid', 'activitypub' ),
array(
'status' => 404,
'actor' => $actor,
)
2024-07-26 08:26:47 +00:00
);
2024-04-23 06:47:35 +00:00
}
}
2022-12-09 10:59:24 +00:00
if ( preg_match( '/^@?' . ACTIVITYPUB_USERNAME_REGEXP . '$/i', $actor ) ) {
2022-12-09 18:05:43 +00:00
$actor = Webfinger::resolve( $actor );
2022-11-09 14:08:32 +00:00
}
if ( ! $actor ) {
2024-07-26 08:26:47 +00:00
return new WP_Error(
'activitypub_no_valid_actor_identifier',
\__( 'The "actor" identifier is not valid', 'activitypub' ),
array(
'status' => 404,
'actor' => $actor,
)
2024-07-26 08:26:47 +00:00
);
2022-11-09 14:08:32 +00:00
}
if ( is_wp_error( $actor ) ) {
return $actor;
}
2023-06-14 13:02:45 +00:00
$transient_key = 'activitypub_' . $actor;
// Only check the cache if needed.
2023-05-31 12:03:46 +00:00
if ( $cached ) {
$metadata = \get_transient( $transient_key );
2023-05-31 12:03:46 +00:00
if ( $metadata ) {
return $metadata;
}
}
2019-09-27 08:12:59 +00:00
if ( ! \wp_http_validate_url( $actor ) ) {
2024-07-26 08:26:47 +00:00
$metadata = new WP_Error(
'activitypub_no_valid_actor_url',
\__( 'The "actor" is no valid URL', 'activitypub' ),
array(
'status' => 400,
'actor' => $actor,
)
2024-07-26 08:26:47 +00:00
);
return $metadata;
}
$response = Http::get( $actor );
2019-09-27 08:12:59 +00:00
if ( \is_wp_error( $response ) ) {
return $response;
}
2019-09-27 08:12:59 +00:00
$metadata = \wp_remote_retrieve_body( $response );
$metadata = \json_decode( $metadata, true );
if ( ! $metadata ) {
2024-07-26 08:26:47 +00:00
$metadata = new WP_Error(
'activitypub_invalid_json',
\__( 'No valid JSON data', 'activitypub' ),
array(
'status' => 400,
'actor' => $actor,
)
2024-07-26 08:26:47 +00:00
);
2022-12-09 12:39:48 +00:00
return $metadata;
}
\set_transient( $transient_key, $metadata, WEEK_IN_SECONDS );
return $metadata;
}
2023-05-10 13:36:45 +00:00
/**
* Returns the followers of a given user.
*
* @param int $user_id The user ID.
2023-05-10 13:36:45 +00:00
*
* @return array The followers.
*/
function get_followers( $user_id ) {
2023-07-03 16:18:03 +00:00
return Followers::get_followers( $user_id );
}
2023-05-10 13:36:45 +00:00
/**
* Count the number of followers for a given user.
*
* @param int $user_id The user ID.
2023-05-10 13:36:45 +00:00
*
* @return int The number of followers.
*/
function count_followers( $user_id ) {
2023-07-03 16:18:03 +00:00
return Followers::count_followers( $user_id );
}
2019-11-18 19:57:00 +00:00
/**
* Examine a url and try to determine the author ID it represents.
*
* Checks are supposedly from the hosted site blog.
*
* @param string $url Permalink to check.
*
* @return int User ID, or 0 on failure.
*/
function url_to_authorid( $url ) {
global $wp_rewrite;
// Check if url hase the same host.
if ( \wp_parse_url( \home_url(), \PHP_URL_HOST ) !== \wp_parse_url( $url, \PHP_URL_HOST ) ) {
2019-11-18 19:57:00 +00:00
return 0;
}
// First, check to see if there is a 'author=N' to match against.
if ( \preg_match( '/[?&]author=(\d+)/i', $url, $values ) ) {
2020-05-12 18:30:06 +00:00
$id = \absint( $values[1] );
2019-11-18 19:57:00 +00:00
if ( $id ) {
return $id;
}
}
// Check to see if we are using rewrite rules.
2019-11-18 19:57:00 +00:00
$rewrite = $wp_rewrite->wp_rewrite_rules();
// Not using rewrite rules, and 'author=N' method failed, so we're out of options.
2019-11-18 19:57:00 +00:00
if ( empty( $rewrite ) ) {
return 0;
}
// Generate rewrite rule for the author url.
2019-11-18 19:57:00 +00:00
$author_rewrite = $wp_rewrite->get_author_permastruct();
$author_regexp = \str_replace( '%author%', '', $author_rewrite );
2019-11-18 19:57:00 +00:00
// Match the rewrite rule with the passed url.
if ( \preg_match( '/https?:\/\/(.+)' . \preg_quote( $author_regexp, '/' ) . '([^\/]+)/i', $url, $match ) ) {
2020-05-12 18:30:06 +00:00
$user = \get_user_by( 'slug', $match[2] );
2019-11-18 19:57:00 +00:00
if ( $user ) {
return $user->ID;
}
}
return 0;
}
Comment Federation (#550) * Comments 1 * Delete FUNDING.yml * Add basic BuddyPress support fix #122 thanks and props @skysarwer * change URL to `bp_core_get_user_domain` * fix "Follow" issue fix #133 * fix #135 * version bump * Create phpunit.yml * Update composer.json * Update composer.json * Update phpunit.yml * Update composer.json * Create phpcs.yml * Update phpcs.xml * Update composer.json * phpcs fixes * fix typo * Comments update * webfinger_extract remove extra param * coding standards * Replies Collection, settings, other fixes * Create stale.yml * move stale file * code standards cleanup * Migrate / Update script * bugfix * add settings link to plugin page * fix code standards * fix cs * fix PHPCS * PHPCS fixes * change background image for wp.org * fix docker * fix webfinger for email identifiers fix #152 * version bump * update composer file to fix unit testing * allow plugins * fix dependencies * Migrate tools * code cleanup * regression fix * Fix announce, clarified language * update included filename * code cleanup * Improve migration UX * Add comments view, warnings to migrate page * style fix * more style fixes * Fix send_delete_activity * replace ap_comment_id to reuse replytocom var * Comments class missing attributes * Post class fix attributes * move js file to assets/js * Separate file for Comment processing hooks * fix file path * associate comments to back compat post * Fix js assets enqueue * change regex matching potential hashtags Matches any string starting with '#' and consisting of any number and combination of [A-Za-z0-9_] that is directly followed by whitespace or punctuation. Groups everything after '#' for access in functions using this regex. This fixes #183 (incomplete links on hashtags containing special characters) by not matching these at all. * also detect hashtags at the start of a paragraph * restrict html tags after which to detect a hashtag Hashtags should not be detected after just any html tag - for example not after an opening a or div. To still allow detection at the start of a line, allow specifically p and br to directly precede a hashtag. * fix pagination * Add Custom Post Type support to outbox API * remove comment_type * fix comparison * remove trailing spaces * fix phpcs issues * fix phpcs issues * run phpcs also on pull_requests * fix phpcs issues * support threaded comments from ActivityPub * refactor support for threaded comments from ActivityPub * remove debugging log line * add first unit tests for class inbox * fix code smells * make filter function static * attempt to resolve backwards compatibility issues * update js to new file * delete old js * Remove migrate code * update post meta canonical * remove type and mention meta from comment filters * extract mentions from comment_content * phpcbf * remove extra curly bracket * Remove migrate code * remove version_check() * Update enqueue scripts * Remove remote comments from preprocessing * Reply to comments from Dashboard * rename function, inserts users into reply text * Update dispatch comments * update comment model * fix comment model replies property * fix preprocess_comment cap check * Add webfinger filter to comments * Add comment edit datetime * cleanup * fix var name * cleanup * phpcbf * better actual translation support * Separate comment reply script * migrate dispatch, migrate comment model to transform * ignore WP_Comment type for now * Adds new helpers for resolving inReplyTo url * Update activitypub_send_comment_activity to include type * remove redundant id check * reinclude user_id in saved ap_object meta * update post field meta * Fix comment updated datetime * front-end reply inserts @mentions * enqueue reply script on front end * use const instead of dirname * some simplifications * move some functions * fixes * some more fixes * fix namespace * fix unittests * fix testcase * fixed typo * fix tests * fix tests * fix PHPCS * move functions to transformer class * fix warnings * Link remote comments on frontend * Link to comment source as row action * Init Comments class * remove dead dispatch action * re-add extract mentions filter * Restore and tweak Comment transform * Schedule comments activities for non-admin users * lint * remove context property * rename get_id method to generate_id * fix locale * move functions * PHPDoc * this is never used * remove some edit methods * remove replies for now * remove JS calls * remove reply_recipients * never used * remove other query-vars * otherwise to_json would not work properly * small changes * use `c` for comment IDs * remove comments.php for now maybe re-add it later * wp_insert_post is an action * also parse comment_text * remove duplicate functions * add Base transformer * remove invalid test * update to new query var * update dispatcher to support comments and posts * fix transition * remove unused functions for now * schedule_comment_activity seems to ignore create and update * fix wrong use of functions! * not every platforms sends an URL * check source-id first * remove hashtags for now * fallback to ID * fix typo * move to_activity to Base class * remove unused function * add support for announce and like * also ping inboxes of other commenters in the thread * restructure WebFinger class * some small improvements * simplified to_object class props @Menrath for the feedback and the idea! * fix unit tests * make transformer filterable /cc @Menrath * use transformer factory, so that transformer can be overwritten * phpcs fixes * fix attachments * fix comment transformer * remove comments for now * update readme/changelog * simplify and unify json_encodes --------- Co-authored-by: Django Doucet <mediaformat.ux@gmail.com> Co-authored-by: Andreas <andreas@bocops.de> Co-authored-by: Eana Hufwe <eana@1a23.com> Co-authored-by: Matthew Exon <git.mexon@spamgourmet.com> Co-authored-by: Django Doucet <django.doucet@webdevstudios.com>
2023-12-22 09:12:26 +00:00
/**
* Verify that url is a wp_ap_comment or a previously received remote comment.
Comment Federation (#550) * Comments 1 * Delete FUNDING.yml * Add basic BuddyPress support fix #122 thanks and props @skysarwer * change URL to `bp_core_get_user_domain` * fix "Follow" issue fix #133 * fix #135 * version bump * Create phpunit.yml * Update composer.json * Update composer.json * Update phpunit.yml * Update composer.json * Create phpcs.yml * Update phpcs.xml * Update composer.json * phpcs fixes * fix typo * Comments update * webfinger_extract remove extra param * coding standards * Replies Collection, settings, other fixes * Create stale.yml * move stale file * code standards cleanup * Migrate / Update script * bugfix * add settings link to plugin page * fix code standards * fix cs * fix PHPCS * PHPCS fixes * change background image for wp.org * fix docker * fix webfinger for email identifiers fix #152 * version bump * update composer file to fix unit testing * allow plugins * fix dependencies * Migrate tools * code cleanup * regression fix * Fix announce, clarified language * update included filename * code cleanup * Improve migration UX * Add comments view, warnings to migrate page * style fix * more style fixes * Fix send_delete_activity * replace ap_comment_id to reuse replytocom var * Comments class missing attributes * Post class fix attributes * move js file to assets/js * Separate file for Comment processing hooks * fix file path * associate comments to back compat post * Fix js assets enqueue * change regex matching potential hashtags Matches any string starting with '#' and consisting of any number and combination of [A-Za-z0-9_] that is directly followed by whitespace or punctuation. Groups everything after '#' for access in functions using this regex. This fixes #183 (incomplete links on hashtags containing special characters) by not matching these at all. * also detect hashtags at the start of a paragraph * restrict html tags after which to detect a hashtag Hashtags should not be detected after just any html tag - for example not after an opening a or div. To still allow detection at the start of a line, allow specifically p and br to directly precede a hashtag. * fix pagination * Add Custom Post Type support to outbox API * remove comment_type * fix comparison * remove trailing spaces * fix phpcs issues * fix phpcs issues * run phpcs also on pull_requests * fix phpcs issues * support threaded comments from ActivityPub * refactor support for threaded comments from ActivityPub * remove debugging log line * add first unit tests for class inbox * fix code smells * make filter function static * attempt to resolve backwards compatibility issues * update js to new file * delete old js * Remove migrate code * update post meta canonical * remove type and mention meta from comment filters * extract mentions from comment_content * phpcbf * remove extra curly bracket * Remove migrate code * remove version_check() * Update enqueue scripts * Remove remote comments from preprocessing * Reply to comments from Dashboard * rename function, inserts users into reply text * Update dispatch comments * update comment model * fix comment model replies property * fix preprocess_comment cap check * Add webfinger filter to comments * Add comment edit datetime * cleanup * fix var name * cleanup * phpcbf * better actual translation support * Separate comment reply script * migrate dispatch, migrate comment model to transform * ignore WP_Comment type for now * Adds new helpers for resolving inReplyTo url * Update activitypub_send_comment_activity to include type * remove redundant id check * reinclude user_id in saved ap_object meta * update post field meta * Fix comment updated datetime * front-end reply inserts @mentions * enqueue reply script on front end * use const instead of dirname * some simplifications * move some functions * fixes * some more fixes * fix namespace * fix unittests * fix testcase * fixed typo * fix tests * fix tests * fix PHPCS * move functions to transformer class * fix warnings * Link remote comments on frontend * Link to comment source as row action * Init Comments class * remove dead dispatch action * re-add extract mentions filter * Restore and tweak Comment transform * Schedule comments activities for non-admin users * lint * remove context property * rename get_id method to generate_id * fix locale * move functions * PHPDoc * this is never used * remove some edit methods * remove replies for now * remove JS calls * remove reply_recipients * never used * remove other query-vars * otherwise to_json would not work properly * small changes * use `c` for comment IDs * remove comments.php for now maybe re-add it later * wp_insert_post is an action * also parse comment_text * remove duplicate functions * add Base transformer * remove invalid test * update to new query var * update dispatcher to support comments and posts * fix transition * remove unused functions for now * schedule_comment_activity seems to ignore create and update * fix wrong use of functions! * not every platforms sends an URL * check source-id first * remove hashtags for now * fallback to ID * fix typo * move to_activity to Base class * remove unused function * add support for announce and like * also ping inboxes of other commenters in the thread * restructure WebFinger class * some small improvements * simplified to_object class props @Menrath for the feedback and the idea! * fix unit tests * make transformer filterable /cc @Menrath * use transformer factory, so that transformer can be overwritten * phpcs fixes * fix attachments * fix comment transformer * remove comments for now * update readme/changelog * simplify and unify json_encodes --------- Co-authored-by: Django Doucet <mediaformat.ux@gmail.com> Co-authored-by: Andreas <andreas@bocops.de> Co-authored-by: Eana Hufwe <eana@1a23.com> Co-authored-by: Matthew Exon <git.mexon@spamgourmet.com> Co-authored-by: Django Doucet <django.doucet@webdevstudios.com>
2023-12-22 09:12:26 +00:00
*
* @return int|bool Comment ID or false if not found.
Comment Federation (#550) * Comments 1 * Delete FUNDING.yml * Add basic BuddyPress support fix #122 thanks and props @skysarwer * change URL to `bp_core_get_user_domain` * fix "Follow" issue fix #133 * fix #135 * version bump * Create phpunit.yml * Update composer.json * Update composer.json * Update phpunit.yml * Update composer.json * Create phpcs.yml * Update phpcs.xml * Update composer.json * phpcs fixes * fix typo * Comments update * webfinger_extract remove extra param * coding standards * Replies Collection, settings, other fixes * Create stale.yml * move stale file * code standards cleanup * Migrate / Update script * bugfix * add settings link to plugin page * fix code standards * fix cs * fix PHPCS * PHPCS fixes * change background image for wp.org * fix docker * fix webfinger for email identifiers fix #152 * version bump * update composer file to fix unit testing * allow plugins * fix dependencies * Migrate tools * code cleanup * regression fix * Fix announce, clarified language * update included filename * code cleanup * Improve migration UX * Add comments view, warnings to migrate page * style fix * more style fixes * Fix send_delete_activity * replace ap_comment_id to reuse replytocom var * Comments class missing attributes * Post class fix attributes * move js file to assets/js * Separate file for Comment processing hooks * fix file path * associate comments to back compat post * Fix js assets enqueue * change regex matching potential hashtags Matches any string starting with '#' and consisting of any number and combination of [A-Za-z0-9_] that is directly followed by whitespace or punctuation. Groups everything after '#' for access in functions using this regex. This fixes #183 (incomplete links on hashtags containing special characters) by not matching these at all. * also detect hashtags at the start of a paragraph * restrict html tags after which to detect a hashtag Hashtags should not be detected after just any html tag - for example not after an opening a or div. To still allow detection at the start of a line, allow specifically p and br to directly precede a hashtag. * fix pagination * Add Custom Post Type support to outbox API * remove comment_type * fix comparison * remove trailing spaces * fix phpcs issues * fix phpcs issues * run phpcs also on pull_requests * fix phpcs issues * support threaded comments from ActivityPub * refactor support for threaded comments from ActivityPub * remove debugging log line * add first unit tests for class inbox * fix code smells * make filter function static * attempt to resolve backwards compatibility issues * update js to new file * delete old js * Remove migrate code * update post meta canonical * remove type and mention meta from comment filters * extract mentions from comment_content * phpcbf * remove extra curly bracket * Remove migrate code * remove version_check() * Update enqueue scripts * Remove remote comments from preprocessing * Reply to comments from Dashboard * rename function, inserts users into reply text * Update dispatch comments * update comment model * fix comment model replies property * fix preprocess_comment cap check * Add webfinger filter to comments * Add comment edit datetime * cleanup * fix var name * cleanup * phpcbf * better actual translation support * Separate comment reply script * migrate dispatch, migrate comment model to transform * ignore WP_Comment type for now * Adds new helpers for resolving inReplyTo url * Update activitypub_send_comment_activity to include type * remove redundant id check * reinclude user_id in saved ap_object meta * update post field meta * Fix comment updated datetime * front-end reply inserts @mentions * enqueue reply script on front end * use const instead of dirname * some simplifications * move some functions * fixes * some more fixes * fix namespace * fix unittests * fix testcase * fixed typo * fix tests * fix tests * fix PHPCS * move functions to transformer class * fix warnings * Link remote comments on frontend * Link to comment source as row action * Init Comments class * remove dead dispatch action * re-add extract mentions filter * Restore and tweak Comment transform * Schedule comments activities for non-admin users * lint * remove context property * rename get_id method to generate_id * fix locale * move functions * PHPDoc * this is never used * remove some edit methods * remove replies for now * remove JS calls * remove reply_recipients * never used * remove other query-vars * otherwise to_json would not work properly * small changes * use `c` for comment IDs * remove comments.php for now maybe re-add it later * wp_insert_post is an action * also parse comment_text * remove duplicate functions * add Base transformer * remove invalid test * update to new query var * update dispatcher to support comments and posts * fix transition * remove unused functions for now * schedule_comment_activity seems to ignore create and update * fix wrong use of functions! * not every platforms sends an URL * check source-id first * remove hashtags for now * fallback to ID * fix typo * move to_activity to Base class * remove unused function * add support for announce and like * also ping inboxes of other commenters in the thread * restructure WebFinger class * some small improvements * simplified to_object class props @Menrath for the feedback and the idea! * fix unit tests * make transformer filterable /cc @Menrath * use transformer factory, so that transformer can be overwritten * phpcs fixes * fix attachments * fix comment transformer * remove comments for now * update readme/changelog * simplify and unify json_encodes --------- Co-authored-by: Django Doucet <mediaformat.ux@gmail.com> Co-authored-by: Andreas <andreas@bocops.de> Co-authored-by: Eana Hufwe <eana@1a23.com> Co-authored-by: Matthew Exon <git.mexon@spamgourmet.com> Co-authored-by: Django Doucet <django.doucet@webdevstudios.com>
2023-12-22 09:12:26 +00:00
*/
function is_comment() {
$comment_id = get_query_var( 'c', null );
if ( ! is_null( $comment_id ) ) {
$comment = \get_comment( $comment_id );
if ( $comment ) {
Comment Federation (#550) * Comments 1 * Delete FUNDING.yml * Add basic BuddyPress support fix #122 thanks and props @skysarwer * change URL to `bp_core_get_user_domain` * fix "Follow" issue fix #133 * fix #135 * version bump * Create phpunit.yml * Update composer.json * Update composer.json * Update phpunit.yml * Update composer.json * Create phpcs.yml * Update phpcs.xml * Update composer.json * phpcs fixes * fix typo * Comments update * webfinger_extract remove extra param * coding standards * Replies Collection, settings, other fixes * Create stale.yml * move stale file * code standards cleanup * Migrate / Update script * bugfix * add settings link to plugin page * fix code standards * fix cs * fix PHPCS * PHPCS fixes * change background image for wp.org * fix docker * fix webfinger for email identifiers fix #152 * version bump * update composer file to fix unit testing * allow plugins * fix dependencies * Migrate tools * code cleanup * regression fix * Fix announce, clarified language * update included filename * code cleanup * Improve migration UX * Add comments view, warnings to migrate page * style fix * more style fixes * Fix send_delete_activity * replace ap_comment_id to reuse replytocom var * Comments class missing attributes * Post class fix attributes * move js file to assets/js * Separate file for Comment processing hooks * fix file path * associate comments to back compat post * Fix js assets enqueue * change regex matching potential hashtags Matches any string starting with '#' and consisting of any number and combination of [A-Za-z0-9_] that is directly followed by whitespace or punctuation. Groups everything after '#' for access in functions using this regex. This fixes #183 (incomplete links on hashtags containing special characters) by not matching these at all. * also detect hashtags at the start of a paragraph * restrict html tags after which to detect a hashtag Hashtags should not be detected after just any html tag - for example not after an opening a or div. To still allow detection at the start of a line, allow specifically p and br to directly precede a hashtag. * fix pagination * Add Custom Post Type support to outbox API * remove comment_type * fix comparison * remove trailing spaces * fix phpcs issues * fix phpcs issues * run phpcs also on pull_requests * fix phpcs issues * support threaded comments from ActivityPub * refactor support for threaded comments from ActivityPub * remove debugging log line * add first unit tests for class inbox * fix code smells * make filter function static * attempt to resolve backwards compatibility issues * update js to new file * delete old js * Remove migrate code * update post meta canonical * remove type and mention meta from comment filters * extract mentions from comment_content * phpcbf * remove extra curly bracket * Remove migrate code * remove version_check() * Update enqueue scripts * Remove remote comments from preprocessing * Reply to comments from Dashboard * rename function, inserts users into reply text * Update dispatch comments * update comment model * fix comment model replies property * fix preprocess_comment cap check * Add webfinger filter to comments * Add comment edit datetime * cleanup * fix var name * cleanup * phpcbf * better actual translation support * Separate comment reply script * migrate dispatch, migrate comment model to transform * ignore WP_Comment type for now * Adds new helpers for resolving inReplyTo url * Update activitypub_send_comment_activity to include type * remove redundant id check * reinclude user_id in saved ap_object meta * update post field meta * Fix comment updated datetime * front-end reply inserts @mentions * enqueue reply script on front end * use const instead of dirname * some simplifications * move some functions * fixes * some more fixes * fix namespace * fix unittests * fix testcase * fixed typo * fix tests * fix tests * fix PHPCS * move functions to transformer class * fix warnings * Link remote comments on frontend * Link to comment source as row action * Init Comments class * remove dead dispatch action * re-add extract mentions filter * Restore and tweak Comment transform * Schedule comments activities for non-admin users * lint * remove context property * rename get_id method to generate_id * fix locale * move functions * PHPDoc * this is never used * remove some edit methods * remove replies for now * remove JS calls * remove reply_recipients * never used * remove other query-vars * otherwise to_json would not work properly * small changes * use `c` for comment IDs * remove comments.php for now maybe re-add it later * wp_insert_post is an action * also parse comment_text * remove duplicate functions * add Base transformer * remove invalid test * update to new query var * update dispatcher to support comments and posts * fix transition * remove unused functions for now * schedule_comment_activity seems to ignore create and update * fix wrong use of functions! * not every platforms sends an URL * check source-id first * remove hashtags for now * fallback to ID * fix typo * move to_activity to Base class * remove unused function * add support for announce and like * also ping inboxes of other commenters in the thread * restructure WebFinger class * some small improvements * simplified to_object class props @Menrath for the feedback and the idea! * fix unit tests * make transformer filterable /cc @Menrath * use transformer factory, so that transformer can be overwritten * phpcs fixes * fix attachments * fix comment transformer * remove comments for now * update readme/changelog * simplify and unify json_encodes --------- Co-authored-by: Django Doucet <mediaformat.ux@gmail.com> Co-authored-by: Andreas <andreas@bocops.de> Co-authored-by: Eana Hufwe <eana@1a23.com> Co-authored-by: Matthew Exon <git.mexon@spamgourmet.com> Co-authored-by: Django Doucet <django.doucet@webdevstudios.com>
2023-12-22 09:12:26 +00:00
return $comment_id;
}
}
return false;
}
2023-05-02 12:39:25 +00:00
/**
* Check for Tombstone Objects.
2023-05-02 12:39:25 +00:00
*
* @see https://www.w3.org/TR/activitypub/#delete-activity-outbox
*
* @param WP_Error $wp_error A WP_Error-Response of an HTTP-Request.
2023-05-02 12:39:25 +00:00
*
* @return boolean True if HTTP-Code is 410 or 404.
2023-05-02 12:39:25 +00:00
*/
function is_tombstone( $wp_error ) {
if ( ! is_wp_error( $wp_error ) ) {
return false;
}
if ( in_array( (int) $wp_error->get_error_code(), array( 404, 410 ), true ) ) {
return true;
}
return false;
}
/**
* Get the REST URL relative to this plugin's namespace.
*
* @param string $path Optional. REST route path. Default ''.
2023-05-17 07:03:26 +00:00
*
* @return string REST URL relative to this plugin's namespace.
*/
function get_rest_url_by_path( $path = '' ) {
// We'll handle the leading slash.
$path = ltrim( $path, '/' );
2023-05-12 21:42:30 +00:00
$namespaced_path = sprintf( '/%s/%s', ACTIVITYPUB_REST_NAMESPACE, $path );
2023-05-12 23:25:49 +00:00
return \get_rest_url( null, $namespaced_path );
}
/**
* Convert a string from camelCase to snake_case.
*
* @param string $input The string to convert.
*
* @return string The converted string.
*/
function camel_to_snake_case( $input ) {
return strtolower( preg_replace( '/(?<!^)[A-Z]/', '_$0', $input ) );
}
2023-06-26 09:08:04 +00:00
/**
* Convert a string from snake_case to camelCase.
*
* @param string $input The string to convert.
2023-06-26 09:08:04 +00:00
*
* @return string The converted string.
*/
function snake_to_camel_case( $input ) {
return lcfirst( str_replace( '_', '', ucwords( $input, '_' ) ) );
2023-06-26 09:08:04 +00:00
}
/**
* Escapes a Tag, to be used as a hashtag.
*
* @param string $input The string to escape.
*
* @return string The escaped hashtag.
*/
function esc_hashtag( $input ) {
$hashtag = \wp_specialchars_decode( $input, ENT_QUOTES );
// Remove all characters that are not letters, numbers, or underscores.
$hashtag = \preg_replace( '/emoji-regex(*SKIP)(?!)|[^\p{L}\p{Nd}_]+/u', '_', $hashtag );
// Capitalize every letter that is preceded by an underscore.
$hashtag = preg_replace_callback(
'/_(.)/',
function ( $matches ) {
return strtoupper( $matches[1] );
},
$hashtag
);
// Add a hashtag to the beginning of the string.
$hashtag = ltrim( $hashtag, '#' );
$hashtag = '#' . $hashtag;
/**
* Allow defining your own custom hashtag generation rules.
*
* @param string $hashtag The hashtag to be returned.
* @param string $input The original string.
*/
$hashtag = apply_filters( 'activitypub_esc_hashtag', $hashtag, $input );
return esc_html( $hashtag );
}
/**
* Check if a request is for an ActivityPub request.
*
* @return bool False by default.
*/
function is_activitypub_request() {
global $wp_query;
/*
* ActivityPub requests are currently only made for
* author archives, singular posts, and the homepage.
*/
2023-07-05 16:13:46 +00:00
if ( ! \is_author() && ! \is_singular() && ! \is_home() && ! defined( '\REST_REQUEST' ) ) {
return false;
}
// Check if the current post type supports ActivityPub.
if ( \is_singular() ) {
$queried_object = \get_queried_object();
$post_type = \get_post_type( $queried_object );
if ( ! \post_type_supports( $post_type, 'activitypub' ) ) {
return false;
}
}
// Check if header already sent.
if ( ! \headers_sent() && ACTIVITYPUB_SEND_VARY_HEADER ) {
// Send Vary header for Accept header.
\header( 'Vary: Accept' );
}
// One can trigger an ActivityPub request by adding ?activitypub to the URL.
if ( isset( $wp_query->query_vars['activitypub'] ) ) {
return true;
}
/*
* The other (more common) option to make an ActivityPub request
* is to send an Accept header.
*/
if ( isset( $_SERVER['HTTP_ACCEPT'] ) ) {
2023-07-18 20:02:27 +00:00
$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
2023-08-11 07:22:46 +00:00
* - application/json
*/
2023-08-11 07:22:46 +00:00
if ( preg_match( '/(application\/(ld\+json|activity\+json|json))/i', $accept ) ) {
return true;
}
}
return false;
}
/**
2023-06-28 12:22:27 +00:00
* This function checks if a user is disabled for ActivityPub.
2023-06-21 15:10:52 +00:00
*
* @param int $user_id The user ID.
2023-06-21 15:10:52 +00:00
*
2023-06-28 12:22:27 +00:00
* @return boolean True if the user is disabled, false otherwise.
2023-06-21 15:10:52 +00:00
*/
2023-06-28 12:22:27 +00:00
function is_user_disabled( $user_id ) {
2023-07-11 06:58:50 +00:00
$return = false;
2023-06-21 15:10:52 +00:00
switch ( $user_id ) {
// if the user is the application user, it's always enabled.
2023-07-03 09:20:44 +00:00
case \Activitypub\Collection\Users::APPLICATION_USER_ID:
2023-07-11 06:58:50 +00:00
$return = false;
break;
2023-06-21 15:10:52 +00:00
// if the user is the blog user, it's only enabled in single-user mode.
2023-07-03 09:20:44 +00:00
case \Activitypub\Collection\Users::BLOG_USER_ID:
if ( is_user_type_disabled( 'blog' ) ) {
$return = true;
2023-07-11 06:58:50 +00:00
break;
2023-06-21 15:10:52 +00:00
}
2023-07-11 06:58:50 +00:00
$return = false;
break;
2023-06-21 15:10:52 +00:00
// if the user is any other user, it's enabled if it can publish posts.
default:
2023-07-11 07:09:37 +00:00
if ( ! \get_user_by( 'id', $user_id ) ) {
$return = true;
break;
}
if ( is_user_type_disabled( 'user' ) ) {
$return = true;
2023-07-11 06:58:50 +00:00
break;
2023-06-28 12:22:27 +00:00
}
if ( ! \user_can( $user_id, 'activitypub' ) ) {
2023-07-11 06:58:50 +00:00
$return = true;
break;
}
2023-07-11 06:58:50 +00:00
$return = false;
break;
2023-06-21 15:10:52 +00:00
}
2023-07-11 06:58:50 +00:00
/**
* Allow plugins to disable users for ActivityPub.
*
* @param boolean $return True if the user is disabled, false otherwise.
* @param int $user_id The User-ID.
*/
2023-07-11 06:58:50 +00:00
return apply_filters( 'activitypub_is_user_disabled', $return, $user_id );
2023-06-21 15:10:52 +00:00
}
/**
* Checks if a User-Type is disabled for ActivityPub.
*
* This function is used to check if the 'blog' or 'user'
* type is disabled for ActivityPub.
*
* @param string $type User type. 'blog' or 'user'.
*
* @return boolean True if the user type is disabled, false otherwise.
*/
function is_user_type_disabled( $type ) {
switch ( $type ) {
case 'blog':
if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
$return = false;
break;
}
}
if ( \defined( 'ACTIVITYPUB_DISABLE_BLOG_USER' ) ) {
$return = ACTIVITYPUB_DISABLE_BLOG_USER;
break;
}
if ( '1' !== \get_option( 'activitypub_enable_blog_user', '0' ) ) {
$return = true;
break;
}
$return = false;
break;
case 'user':
if ( \defined( 'ACTIVITYPUB_SINGLE_USER_MODE' ) ) {
if ( ACTIVITYPUB_SINGLE_USER_MODE ) {
$return = true;
break;
}
}
if ( \defined( 'ACTIVITYPUB_DISABLE_USER' ) ) {
$return = ACTIVITYPUB_DISABLE_USER;
break;
}
if ( '1' !== \get_option( 'activitypub_enable_users', '1' ) ) {
$return = true;
break;
}
$return = false;
break;
default:
2024-07-26 08:26:47 +00:00
$return = new WP_Error(
'activitypub_wrong_user_type',
__( 'Wrong user type', 'activitypub' ),
array( 'status' => 400 )
);
break;
}
/**
* Allow plugins to disable user types for ActivityPub.
*
* @param boolean $return True if the user type is disabled, false otherwise.
* @param string $type The User-Type.
*/
return apply_filters( 'activitypub_is_user_type_disabled', $return, $type );
}
2023-07-10 08:29:02 +00:00
/**
* Check if the blog is in single-user mode.
*
* @return boolean True if the blog is in single-user mode, false otherwise.
*/
function is_single_user() {
2023-09-26 19:04:51 +00:00
if (
false === is_user_type_disabled( 'blog' ) &&
true === is_user_type_disabled( 'user' )
2023-07-10 08:29:02 +00:00
) {
2023-09-26 19:04:51 +00:00
return true;
2023-07-10 08:29:02 +00:00
}
2023-09-26 19:04:51 +00:00
return false;
2023-07-10 08:29:02 +00:00
}
/**
* Check if a site supports the block editor.
*
* @return boolean True if the site supports the block editor, false otherwise.
*/
function site_supports_blocks() {
if ( \version_compare( \get_bloginfo( 'version' ), '5.9', '<' ) ) {
return false;
}
if ( ! \function_exists( 'register_block_type_from_metadata' ) ) {
return false;
}
/**
* Allow plugins to disable block editor support,
* thus disabling blocks registered by the ActivityPub plugin.
*
* @param boolean $supports_blocks True if the site supports the block editor, false otherwise.
*/
return apply_filters( 'activitypub_site_supports_blocks', true );
}
/**
* Check if data is valid JSON.
*
* @param string $data The data to check.
*
* @return boolean True if the data is JSON, false otherwise.
*/
function is_json( $data ) {
return \is_array( \json_decode( $data, true ) ) ? true : false;
}
/**
* Check whther a blog is public based on the `blog_public` option.
*
* @return bool True if public, false if not
*/
function is_blog_public() {
/**
* Filter whether the blog is public.
*
* @param bool $public Whether the blog is public.
*/
return (bool) apply_filters( 'activitypub_is_blog_public', \get_option( 'blog_public', 1 ) );
}
/**
* Sanitize a URL.
*
* @param string $value The URL to sanitize.
*
* @return string|null The sanitized URL or null if invalid.
*/
function sanitize_url( $value ) {
if ( filter_var( $value, FILTER_VALIDATE_URL ) === false ) {
return null;
}
return esc_url_raw( $value );
}
/**
* Extract recipient URLs from Activity object.
*
* @param array $data The Activity object as array.
*
* @return array The list of user URLs.
*/
function extract_recipients_from_activity( $data ) {
$recipient_items = array();
foreach ( array( 'to', 'bto', 'cc', 'bcc', 'audience' ) as $i ) {
if ( array_key_exists( $i, $data ) ) {
if ( is_array( $data[ $i ] ) ) {
$recipient = $data[ $i ];
} else {
$recipient = array( $data[ $i ] );
}
$recipient_items = array_merge( $recipient_items, $recipient );
}
if ( is_array( $data['object'] ) && array_key_exists( $i, $data['object'] ) ) {
if ( is_array( $data['object'][ $i ] ) ) {
$recipient = $data['object'][ $i ];
} else {
$recipient = array( $data['object'][ $i ] );
}
$recipient_items = array_merge( $recipient_items, $recipient );
}
}
$recipients = array();
// Flatten array.
foreach ( $recipient_items as $recipient ) {
if ( is_array( $recipient ) ) {
// Check if recipient is an object.
if ( array_key_exists( 'id', $recipient ) ) {
$recipients[] = $recipient['id'];
}
} else {
$recipients[] = $recipient;
}
}
return array_unique( $recipients );
}
/**
* Check if passed Activity is Public.
*
* @param array $data The Activity object as array.
*
* @return boolean True if public, false if not.
*/
function is_activity_public( $data ) {
$recipients = extract_recipients_from_activity( $data );
return in_array( 'https://www.w3.org/ns/activitystreams#Public', $recipients, true );
}
/**
* Get active users based on a given duration.
*
* @param int $duration Optional. The duration to check in month(s). Default 1.
*
* @return int The number of active users.
*/
function get_active_users( $duration = 1 ) {
$duration = intval( $duration );
$transient_key = sprintf( 'monthly_active_users_%d', $duration );
$count = get_transient( $transient_key );
if ( false === $count ) {
global $wpdb;
2024-09-29 21:12:31 +00:00
// phpcs:ignore WordPress.DB.DirectDatabaseQuery
2024-09-29 21:12:31 +00:00
$count = $wpdb->get_var(
$wpdb->prepare(
"SELECT COUNT( DISTINCT post_author ) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' AND post_date <= DATE_SUB( NOW(), INTERVAL %d MONTH )",
$duration
)
);
set_transient( $transient_key, $count, DAY_IN_SECONDS );
}
// If 0 authors where active.
if ( 0 === $count ) {
return 0;
}
// If single user mode.
if ( is_single_user() ) {
return 1;
}
// If blog user is disabled.
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
2024-09-09 11:34:33 +00:00
return (int) $count;
}
// Also count blog user.
2024-09-09 11:34:33 +00:00
return (int) $count + 1;
}
/**
* Get the total number of users.
*
* @return int The total number of users.
*/
function get_total_users() {
// If single user mode.
if ( is_single_user() ) {
return 1;
}
$users = \get_users(
array(
'capability__in' => array( 'activitypub' ),
)
);
if ( is_array( $users ) ) {
$users = count( $users );
} else {
$users = 1;
}
// If blog user is disabled.
if ( is_user_disabled( Users::BLOG_USER_ID ) ) {
2024-09-09 11:34:33 +00:00
return (int) $users;
}
2024-09-09 11:34:33 +00:00
return (int) $users + 1;
}
/**
* Examine a comment ID and look up an existing comment it represents.
*
* @param string $id ActivityPub object ID (usually a URL) to check.
*
* @return \WP_Comment|boolean Comment, or false on failure.
*/
function object_id_to_comment( $id ) {
return Comment::object_id_to_comment( $id );
}
/**
* Verify that URL is a local comment or a previously received remote comment.
* (For threading comments locally)
*
* @param string $url The URL to check.
*
* @return string|null Comment ID or null if not found
*/
function url_to_commentid( $url ) {
return Comment::url_to_commentid( $url );
}
/**
* Get the URI of an ActivityPub object.
*
* @param array|string $data The ActivityPub object.
*
* @return string The URI of the ActivityPub object
*/
function object_to_uri( $data ) {
// Check whether it is already simple.
if ( ! $data || is_string( $data ) ) {
return $data;
}
/*
* Check if it is a list, then take first item.
* This plugin does not support collections.
*/
if ( array_is_list( $data ) ) {
$data = $data[0];
}
// Check if it is simplified now.
if ( is_string( $data ) ) {
return $data;
}
$type = 'Object';
if ( isset( $data['type'] ) ) {
$type = $data['type'];
}
// Return part of Object that makes most sense.
switch ( $type ) {
case 'Link':
$data = $data['href'];
break;
default:
$data = $data['id'];
break;
}
return $data;
}
2024-01-30 20:29:09 +00:00
/**
* Check if a comment should be federated.
*
* We consider a comment should be federated if it is authored by a user that is
* not disabled for federation and if it is a reply directly to the post or to a
* federated comment.
*
* @param mixed $comment Comment object or ID.
*
* @return boolean True if the comment should be federated, false otherwise.
*/
function should_comment_be_federated( $comment ) {
return Comment::should_be_federated( $comment );
}
/**
* Check if a comment was federated.
*
* This function checks if a comment was federated via ActivityPub.
*
* @param mixed $comment Comment object or ID.
*
* @return boolean True if the comment was federated, false otherwise.
*/
function was_comment_sent( $comment ) {
return Comment::was_sent( $comment );
}
/**
* Check if a comment is federated.
*
* We consider a comment federated if comment was received via ActivityPub.
*
* Use this function to check if it is comment that was received via ActivityPub.
*
* @param mixed $comment Comment object or ID.
*
* @return boolean True if the comment is federated, false otherwise.
*/
function was_comment_received( $comment ) {
return Comment::was_received( $comment );
}
/**
* Check if a comment is local only.
*
* This function checks if a comment is local only and was not sent or received via ActivityPub.
*
* @param mixed $comment Comment object or ID.
*
* @return boolean True if the comment is local only, false otherwise.
*/
function is_local_comment( $comment ) {
return Comment::is_local( $comment );
}
/**
* Mark a WordPress object as federated.
*
* @param \WP_Comment|\WP_Post $wp_object The WordPress object.
* @param string $state The state of the object.
*/
2024-02-07 16:54:48 +00:00
function set_wp_object_state( $wp_object, $state ) {
$meta_key = 'activitypub_status';
if ( $wp_object instanceof \WP_Post ) {
2024-02-07 16:54:48 +00:00
\update_post_meta( $wp_object->ID, $meta_key, $state );
} elseif ( $wp_object instanceof \WP_Comment ) {
2024-02-07 16:54:48 +00:00
\update_comment_meta( $wp_object->comment_ID, $meta_key, $state );
} else {
/**
* Allow plugins to mark WordPress objects as federated.
*
* @param \WP_Comment|\WP_Post $wp_object The WordPress object.
* @param string $state The state of the object.
*/
\apply_filters( 'activitypub_mark_wp_object_as_federated', $wp_object );
}
}
/**
* Get the federation state of a WordPress object.
*
* @param \WP_Comment|\WP_Post $wp_object The WordPress object.
*
* @return string|false The state of the object or false if not found.
*/
function get_wp_object_state( $wp_object ) {
$meta_key = 'activitypub_status';
if ( $wp_object instanceof \WP_Post ) {
return \get_post_meta( $wp_object->ID, $meta_key, true );
} elseif ( $wp_object instanceof \WP_Comment ) {
return \get_comment_meta( $wp_object->comment_ID, $meta_key, true );
} else {
/**
* Allow plugins to get the federation state of a WordPress object.
*
* @param \WP_Comment|\WP_Post $wp_object The WordPress object.
*/
return \apply_filters( 'activitypub_get_wp_object_state', false, $wp_object );
}
}
2024-01-30 20:29:09 +00:00
/**
* Get the description of a post type.
*
* Set some default descriptions for the default post types.
*
* @param \WP_Post_Type $post_type The post type object.
2024-01-30 20:29:09 +00:00
*
* @return string The description of the post type.
*/
function get_post_type_description( $post_type ) {
$description = '';
switch ( $post_type->name ) {
case 'post':
$description = '';
break;
case 'page':
$description = '';
break;
case 'attachment':
$description = ' - ' . __( 'The attachments that you have uploaded to a post (images, videos, documents or other files).', 'activitypub' );
break;
default:
if ( ! empty( $post_type->description ) ) {
$description = ' - ' . $post_type->description;
}
}
/**
* Allow plugins to get the description of a post type.
*
* @param string $description The description of the post type.
* @param \WP_Post_Type $post_type The post type object.
*/
2024-01-30 20:29:09 +00:00
return apply_filters( 'activitypub_post_type_description', $description, $post_type->name, $post_type );
}
2024-04-05 20:49:38 +00:00
/**
* Get the masked WordPress version to only show the major and minor version.
*
* @return string The masked version.
*/
function get_masked_wp_version() {
// Only show the major and minor version.
2024-04-05 20:49:38 +00:00
$version = get_bloginfo( 'version' );
// Strip the RC or beta part.
2024-04-05 20:49:38 +00:00
$version = preg_replace( '/-.*$/', '', $version );
$version = explode( '.', $version );
$version = array_slice( $version, 0, 2 );
return implode( '.', $version );
}
/**
* Get the enclosures of a post.
*
* @param int $post_id The post ID.
*
* @return array The enclosures.
*/
function get_enclosures( $post_id ) {
$enclosures = get_post_meta( $post_id, 'enclosure', false );
if ( ! $enclosures ) {
return array();
}
$enclosures = array_map(
function ( $enclosure ) {
$attributes = explode( "\n", $enclosure );
if ( ! isset( $attributes[0] ) || ! \wp_http_validate_url( $attributes[0] ) ) {
return false;
}
return array(
'url' => $attributes[0],
'length' => isset( $attributes[1] ) ? trim( $attributes[1] ) : null,
'mediaType' => isset( $attributes[2] ) ? trim( $attributes[2] ) : null,
);
},
$enclosures
);
return array_filter( $enclosures );
}
/**
* Retrieves the IDs of the ancestors of a comment.
*
* Adaption of `get_post_ancestors` from WordPress core.
*
* @see https://developer.wordpress.org/reference/functions/get_post_ancestors/
*
* @param int|\WP_Comment $comment Comment ID or comment object.
*
* @return \WP_Comment[] Array of ancestor comments or empty array if there are none.
*/
function get_comment_ancestors( $comment ) {
$comment = \get_comment( $comment );
if ( ! $comment || empty( $comment->comment_parent ) || (int) $comment->comment_parent === (int) $comment->comment_ID ) {
return array();
}
$ancestors = array();
$id = (int) $comment->comment_parent;
$ancestors[] = $id;
while ( $id > 0 ) {
$ancestor = \get_comment( $id );
$parent_id = (int) $ancestor->comment_parent;
// Loop detection: If the ancestor has been seen before, break.
if ( empty( $parent_id ) || ( $parent_id === (int) $comment->comment_ID ) || in_array( $parent_id, $ancestors, true ) ) {
break;
}
$id = $parent_id;
$ancestors[] = $id;
}
return $ancestors;
}
/**
* Change the display of large numbers on the site.
*
* @author Jeremy Herve
*
* @see https://wordpress.org/support/topic/abbreviate-numbers-with-k/
*
* @param string $formatted Converted number in string format.
* @param float $number The number to convert based on locale.
* @param int $decimals Precision of the number of decimal places.
*
* @return string Converted number in string format.
*/
function custom_large_numbers( $formatted, $number, $decimals ) {
global $wp_locale;
$decimals = 0;
$decimal_point = '.';
$thousands_sep = ',';
if ( isset( $wp_locale ) ) {
$decimals = (int) $wp_locale->number_format['decimal_point'];
$decimal_point = $wp_locale->number_format['decimal_point'];
$thousands_sep = $wp_locale->number_format['thousands_sep'];
}
if ( $number < 1000 ) { // Any number less than a Thousand.
return \number_format( $number, $decimals, $decimal_point, $thousands_sep );
} elseif ( $number < 1000000 ) { // Any number less than a million.
return \number_format( $number / 1000, $decimals, $decimal_point, $thousands_sep ) . 'K';
} elseif ( $number < 1000000000 ) { // Any number less than a billion.
return \number_format( $number / 1000000, $decimals, $decimal_point, $thousands_sep ) . 'M';
} else { // At least a billion.
return \number_format( $number / 1000000000, $decimals, $decimal_point, $thousands_sep ) . 'B';
}
// Default fallback. We should not get here.
return $formatted;
}
/**
* Registers a ActivityPub comment type.
*
* @param string $comment_type Key for comment type.
* @param array $args Optional. Array of arguments for registering a comment type. Default empty array.
*
* @return array The registered Activitypub comment type.
*/
function register_comment_type( $comment_type, $args = array() ) {
global $activitypub_comment_types;
if ( ! is_array( $activitypub_comment_types ) ) {
$activitypub_comment_types = array();
}
// Sanitize comment type name.
$comment_type = sanitize_key( $comment_type );
$activitypub_comment_types[ $comment_type ] = $args;
/**
* Fires after a ActivityPub comment type is registered.
*
* @param string $comment_type Comment type.
* @param array $args Arguments used to register the comment type.
*/
do_action( 'activitypub_registered_comment_type', $comment_type, $args );
return $args;
}
/**
* Normalize a URL.
*
* @param string $url The URL.
*
* @return string The normalized URL.
*/
function normalize_url( $url ) {
$url = \untrailingslashit( $url );
$url = \str_replace( 'https://', '', $url );
$url = \str_replace( 'http://', '', $url );
$url = \str_replace( 'www.', '', $url );
return $url;
}
/**
* Normalize a host.
*
* @param string $host The host.
*
* @return string The normalized host.
*/
function normalize_host( $host ) {
return \str_replace( 'www.', '', $host );
}
/**
* Get the reply intent URI.
*
* @return string The reply intent URI.
*/
function get_reply_intent_uri() {
return sprintf(
'javascript:(()=>{window.open(\'%s\'+encodeURIComponent(window.location.href));})();',
esc_url( \admin_url( 'post-new.php?in_reply_to=' ) )
);
}
/**
* Replace content with links, mentions or hashtags by Regex callback and not affect protected tags.
*
* @param string $content The content that should be changed.
* @param string $regex The regex to use.
* @param callable $regex_callback Callback for replacement logic.
*
* @return string The content with links, mentions, hashtags, etc.
*/
function enrich_content_data( $content, $regex, $regex_callback ) {
// Small protection against execution timeouts: limit to 1 MB.
if ( mb_strlen( $content ) > MB_IN_BYTES ) {
return $content;
}
$tag_stack = array();
$protected_tags = array(
'pre',
'code',
'textarea',
'style',
'a',
);
$content_with_links = '';
$in_protected_tag = false;
foreach ( wp_html_split( $content ) as $chunk ) {
if ( preg_match( '#^<!--[\s\S]*-->$#i', $chunk, $m ) ) {
$content_with_links .= $chunk;
continue;
}
if ( preg_match( '#^<(/)?([a-z-]+)\b[^>]*>$#i', $chunk, $m ) ) {
$tag = strtolower( $m[2] );
if ( '/' === $m[1] ) {
// Closing tag.
$i = array_search( $tag, $tag_stack, true );
// We can only remove the tag from the stack if it is in the stack.
if ( false !== $i ) {
$tag_stack = array_slice( $tag_stack, 0, $i );
}
} else {
// 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( $regex, $regex_callback, $chunk );
}
return $content_with_links;
}
/**
* Generate a summary of a post.
*
* This function generates a summary of a post by extracting:
*
* 1. The post excerpt if it exists.
* 2. The first part of the post content if it contains the <!--more--> tag.
* 3. An excerpt of the post content if it is longer than the specified length.
*
* @param int|\WP_Post $post The post ID or post object.
* @param integer $length The maximum length of the summary.
* Default is 500. It will be ignored if the post excerpt
* and the content above the <!--more--> tag.
*
* @return string The generated post summary.
*/
function generate_post_summary( $post, $length = 500 ) {
$post = get_post( $post );
if ( ! $post ) {
return '';
}
$content = \sanitize_post_field( 'post_excerpt', $post->post_excerpt, $post->ID );
if ( $content ) {
/**
* Filters the post excerpt.
*
* @param string $content The post excerpt.
*/
return \apply_filters( 'the_excerpt', $content );
}
$content = \sanitize_post_field( 'post_content', $post->post_content, $post->ID );
$content_parts = \get_extended( $content );
/**
* Filters the excerpt more value.
*
* @param string $excerpt_more The excerpt more.
*/
$excerpt_more = \apply_filters( 'activitypub_excerpt_more', '[…]' );
$length = $length - strlen( $excerpt_more );
// Check for the <!--more--> tag.
if (
! empty( $content_parts['extended'] ) &&
! empty( $content_parts['main'] )
) {
$content = $content_parts['main'] . ' ' . $excerpt_more;
$length = null;
}
$content = \html_entity_decode( $content );
$content = \wp_strip_all_tags( $content );
$content = \trim( $content );
$content = \preg_replace( '/\R+/m', "\n\n", $content );
$content = \preg_replace( '/[\r\t]/', '', $content );
if ( $length && \strlen( $content ) > $length ) {
$content = \wordwrap( $content, $length, '</activitypub-summary>' );
$content = \explode( '</activitypub-summary>', $content, 2 );
$content = $content[0] . ' ' . $excerpt_more;
}
/*
Removed until this is merged: https://github.com/mastodon/mastodon/pull/28629
return \apply_filters( 'the_excerpt', $content );
*/
return $content;
}
/**
* Get the content warning of a post.
*
* @param int|\WP_Post $post_id The post ID or post object.
*
* @return string|false The content warning or false if not found.
*/
function get_content_warning( $post_id ) {
$post = get_post( $post_id );
if ( ! $post ) {
return false;
}
$warning = get_post_meta( $post->ID, 'activitypub_content_warning', true );
if ( empty( $warning ) ) {
return false;
}
return $warning;
}