wordpress-activitypub/includes/class-shortcodes.php
Matthias Pfefferle b744dc551d
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 10:12:26 +01:00

598 lines
15 KiB
PHP

<?php
namespace Activitypub;
use function Activitypub\esc_hashtag;
class Shortcodes {
/**
* Register the shortcodes
*/
public static function register() {
foreach ( get_class_methods( self::class ) as $shortcode ) {
if ( 'init' !== $shortcode ) {
add_shortcode( 'ap_' . $shortcode, array( self::class, $shortcode ) );
}
}
}
/**
* Unregister the shortcodes
*/
public static function unregister() {
foreach ( get_class_methods( self::class ) as $shortcode ) {
if ( 'init' !== $shortcode ) {
remove_shortcode( 'ap_' . $shortcode );
}
}
}
/**
* Generates output for the 'ap_hashtags' shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The post tags as hashtags.
*/
public static function hashtags( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$tags = \get_the_tags( $item->ID );
if ( ! $tags ) {
return '';
}
$hash_tags = array();
foreach ( $tags as $tag ) {
$hash_tags[] = \sprintf(
'<a rel="tag" class="hashtag u-tag u-category" href="%s">%s</a>',
\esc_url( \get_tag_link( $tag ) ),
esc_hashtag( $tag->name )
);
}
return \implode( ' ', $hash_tags );
}
/**
* Generates output for the 'ap_title' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The post title.
*/
public static function title( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
return \wp_strip_all_tags( \get_the_title( $item->ID ), true );
}
/**
* Generates output for the 'ap_excerpt' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The post excerpt.
*/
public static function excerpt( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$atts = shortcode_atts(
array( 'length' => ACTIVITYPUB_EXCERPT_LENGTH ),
$atts,
$tag
);
$excerpt_length = intval( $atts['length'] );
if ( 0 === $excerpt_length ) {
$excerpt_length = ACTIVITYPUB_EXCERPT_LENGTH;
}
$excerpt = \get_post_field( 'post_excerpt', $item );
if ( 'attachment' === $item->post_type ) {
// get title of attachment with fallback to alt text.
$content = wp_get_attachment_caption( $item->ID );
if ( empty( $content ) ) {
$content = get_post_meta( $item->ID, '_wp_attachment_image_alt', true );
}
} elseif ( '' === $excerpt ) {
$content = \get_post_field( 'post_content', $item );
// An empty string will make wp_trim_excerpt do stuff we do not want.
if ( '' !== $content ) {
$excerpt = \strip_shortcodes( $content );
/** This filter is documented in wp-includes/post-template.php */
$excerpt = \apply_filters( 'the_content', $excerpt );
$excerpt = \str_replace( ']]>', ']]&gt;', $excerpt );
}
}
// Strip out any remaining tags.
$excerpt = \wp_strip_all_tags( $excerpt );
$excerpt_more = \apply_filters( 'activitypub_excerpt_more', ' [&hellip;]' );
$excerpt_more_len = strlen( $excerpt_more );
// We now have a excerpt, but we need to check it's length, it may be longer than we want for two reasons:
//
// * The user has entered a manual excerpt which is longer that what we want.
// * No manual excerpt exists so we've used the content which might be longer than we want.
//
// Either way, let's trim it up if we need too. Also, don't forget to take into account the more indicator
// as part of the total length.
//
// Setup a variable to hold the current excerpts length.
$current_excerpt_length = strlen( $excerpt );
// Setup a variable to keep track of our target length.
$target_excerpt_length = $excerpt_length - $excerpt_more_len;
// Setup a variable to keep track of the current max length.
$current_excerpt_max = $target_excerpt_length;
// This is a loop since we can't calculate word break the string after 'the_excpert' filter has run (we would break
// all kinds of html tags), so we have to cut the excerpt down a bit at a time until we hit our target length.
while ( $current_excerpt_length > $target_excerpt_length && $current_excerpt_max > 0 ) {
// Trim the excerpt based on wordwrap() positioning.
// Note: we're using <br> as the linebreak just in case there are any newlines existing in the excerpt from the user.
// There won't be any <br> left after we've run wp_strip_all_tags() in the code above, so they're
// safe to use here. It won't be included in the final excerpt as the substr() will trim it off.
$excerpt = substr( $excerpt, 0, strpos( wordwrap( $excerpt, $current_excerpt_max, '<br>' ), '<br>' ) );
// If something went wrong, or we're in a language that wordwrap() doesn't understand,
// just chop it off and don't worry about breaking in the middle of a word.
if ( strlen( $excerpt ) > $excerpt_length - $excerpt_more_len ) {
$excerpt = substr( $excerpt, 0, $current_excerpt_max );
}
// Add in the more indicator.
$excerpt = $excerpt . $excerpt_more;
// Run it through the excerpt filter which will add some html tags back in.
$excerpt_filtered = apply_filters( 'the_excerpt', $excerpt );
// Now set the current excerpt length to this new filtered length.
$current_excerpt_length = strlen( $excerpt_filtered );
// Check to see if we're over the target length.
if ( $current_excerpt_length > $target_excerpt_length ) {
// If so, remove 20 characters from the current max and run the loop again.
$current_excerpt_max = $current_excerpt_max - 20;
}
}
return \apply_filters( 'the_excerpt', $excerpt );
}
/**
* Generates output for the 'ap_content' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The post content.
*/
public static function content( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
// prevent inception
remove_shortcode( 'ap_content' );
$atts = shortcode_atts(
array( 'apply_filters' => 'yes' ),
$atts,
$tag
);
$content = '';
if ( 'attachment' === $item->post_type ) {
// get title of attachment with fallback to alt text.
$content = wp_get_attachment_caption( $item->ID );
if ( empty( $content ) ) {
$content = get_post_meta( $item->ID, '_wp_attachment_image_alt', true );
}
} else {
$content = \get_post_field( 'post_content', $item );
if ( 'yes' === $atts['apply_filters'] ) {
$content = \apply_filters( 'the_content', $content );
} else {
$content = do_blocks( $content );
$content = wptexturize( $content );
$content = wp_filter_content_tags( $content );
}
// replace script and style elements
$content = \preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', $content );
$content = strip_shortcodes( $content );
$content = \trim( \preg_replace( '/[\n\r\t]/', '', $content ) );
}
add_shortcode( 'ap_content', array( 'Activitypub\Shortcodes', 'content' ) );
return $content;
}
/**
* Generates output for the 'ap_permalink' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The post permalink.
*/
public static function permalink( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$atts = shortcode_atts(
array(
'type' => 'url',
),
$atts,
$tag
);
if ( 'url' === $atts['type'] ) {
return \esc_url( \get_permalink( $item->ID ) );
}
return \sprintf(
'<a href="%1$s">%1$s</a>',
\esc_url( \get_permalink( $item->ID ) )
);
}
/**
* Generates output for the 'ap_shortlink' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The post shortlink.
*/
public static function shortlink( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$atts = shortcode_atts(
array(
'type' => 'url',
),
$atts,
$tag
);
if ( 'url' === $atts['type'] ) {
return \esc_url( \wp_get_shortlink( $item->ID ) );
}
return \sprintf(
'<a href="%1$s">%1$s</a>',
\esc_url( \wp_get_shortlink( $item->ID ) )
);
}
/**
* Generates output for the 'ap_image' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string
*/
public static function image( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$atts = shortcode_atts(
array(
'type' => 'full',
),
$atts,
$tag
);
$size = 'full';
if ( in_array(
$atts['type'],
array( 'thumbnail', 'medium', 'large', 'full' ),
true
) ) {
$size = $atts['type'];
}
$image = \get_the_post_thumbnail_url( $item->ID, $size );
if ( ! $image ) {
return '';
}
return \esc_url( $image );
}
/**
* Generates output for the 'ap_hashcats' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The post categories as hashtags.
*/
public static function hashcats( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$categories = \get_the_category( $item->ID );
if ( ! $categories ) {
return '';
}
$hash_tags = array();
foreach ( $categories as $category ) {
$hash_tags[] = \sprintf(
'<a rel="tag" class="hashtag u-tag u-category" href="%s">%s</a>',
\esc_url( \get_category_link( $category ) ),
esc_hashtag( $category->name )
);
}
return \implode( ' ', $hash_tags );
}
/**
* Generates output for the 'ap_author' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The author name.
*/
public static function author( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$author_id = \get_post_field( 'post_author', $item->ID );
$name = \get_the_author_meta( 'display_name', $author_id );
if ( ! $name ) {
return '';
}
return wp_strip_all_tags( $name );
}
/**
* Generates output for the 'ap_authorurl' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The author URL.
*/
public static function authorurl( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$author_id = \get_post_field( 'post_author', $item->ID );
$url = \get_the_author_meta( 'user_url', $author_id );
if ( ! $url ) {
return '';
}
return \esc_url( $url );
}
/**
* Generates output for the 'ap_blogurl' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The site URL.
*/
public static function blogurl( $atts, $content, $tag ) {
return \esc_url( \get_bloginfo( 'url' ) );
}
/**
* Generates output for the 'ap_blogname' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string
*/
public static function blogname( $atts, $content, $tag ) {
return \wp_strip_all_tags( \get_bloginfo( 'name' ) );
}
/**
* Generates output for the 'ap_blogdesc' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The site description.
*/
public static function blogdesc( $atts, $content, $tag ) {
return \wp_strip_all_tags( \get_bloginfo( 'description' ) );
}
/**
* Generates output for the 'ap_date' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The post date.
*/
public static function date( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$datetime = \get_post_datetime( $item );
$dateformat = \get_option( 'date_format' );
$timeformat = \get_option( 'time_format' );
$date = $datetime->format( $dateformat );
if ( ! $date ) {
return '';
}
return $date;
}
/**
* Generates output for the 'ap_time' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The post time.
*/
public static function time( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$datetime = \get_post_datetime( $item );
$dateformat = \get_option( 'date_format' );
$timeformat = \get_option( 'time_format' );
$date = $datetime->format( $timeformat );
if ( ! $date ) {
return '';
}
return $date;
}
/**
* Generates output for the 'ap_datetime' Shortcode
*
* @param array $atts The Shortcode attributes.
* @param string $content The ActivityPub post-content.
* @param string $tag The tag/name of the Shortcode.
*
* @return string The post date/time.
*/
public static function datetime( $atts, $content, $tag ) {
$item = self::get_item();
if ( ! $item ) {
return '';
}
$datetime = \get_post_datetime( $item );
$dateformat = \get_option( 'date_format' );
$timeformat = \get_option( 'time_format' );
$date = $datetime->format( $dateformat . ' @ ' . $timeformat );
if ( ! $date ) {
return '';
}
return $date;
}
/**
* Get a WordPress item to federate.
*
* Checks if item (WP_Post) is "public", a supported post type
* and not password protected.
*
* @return null|WP_Post The WordPress item.
*/
protected static function get_item() {
$post = \get_post();
if ( ! $post ) {
return null;
}
if ( 'publish' !== \get_post_status( $post ) ) {
return null;
}
if ( \post_password_required( $post ) ) {
return null;
}
if ( ! \in_array( \get_post_type( $post ), \get_post_types_by_support( 'activitypub' ), true ) ) {
return null;
}
return $post;
}
}