From ecd1bb331319f042321fdc7a5c385f3d563f2d00 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 30 Mar 2026 19:55:10 +0200 Subject: [PATCH 1/4] Fix nested comment reply link broken by replytocom hijacking The Webmention plugin registered "replytocom" as a WordPress query var and hijacked the template_include filter to serve a custom comment template whenever that parameter was present. This conflicted with WordPress core's threaded comment reply functionality, which uses "?replytocom=ID" as the fallback URL for reply links. The template override prevented the normal comment form from loading, breaking the "Reply" UI and comment nesting. The fix introduces a new "webmention_comment" query parameter for webmention source URLs, leaving WordPress core's "replytocom" parameter untouched. The receiver maintains backward compatibility by accepting both parameter names when parsing incoming webmention targets. Fixes #487 --- includes/class-comment.php | 21 ++++++++++++++------- includes/class-receiver.php | 5 ++++- includes/class-sender.php | 2 +- templates/comment.php | 9 +++++++-- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/includes/class-comment.php b/includes/class-comment.php index ebfefa9c..ee00cc58 100644 --- a/includes/class-comment.php +++ b/includes/class-comment.php @@ -246,17 +246,24 @@ public static function register_comment_types() { } /** - * replace the template for all URLs with a "replytocom" query-param + * Replace the template for all URLs with a "webmention_comment" query-param. * - * @param string $template the template url + * Uses "webmention_comment" instead of "replytocom" to avoid conflicting with + * WordPress core's threaded/nested comment reply functionality. The "replytocom" + * parameter is used by WordPress core for reply links on threaded comments, and + * hijacking it breaks the "Reply" link UI and comment nesting. + * + * @see https://github.com/pfefferle/wordpress-webmention/issues/487 + * + * @param string $template The template url. * * @return string */ public static function comment_template_include( $template ) { global $wp_query; - // replace template - if ( isset( $wp_query->query['replytocom'] ) ) { + // Replace template for webmention comment source URLs. + if ( isset( $wp_query->query['webmention_comment'] ) ) { return apply_filters( 'webmention_comment_template', WEBMENTION_PLUGIN_DIR . 'templates/comment.php' ); } @@ -264,14 +271,14 @@ public static function comment_template_include( $template ) { } /** - * adds some query vars + * Adds some query vars. * - * @param array $vars + * @param array $vars The query vars. * * @return array */ public static function query_var( $vars ) { - $vars[] = 'replytocom'; + $vars[] = 'webmention_comment'; return $vars; } } diff --git a/includes/class-receiver.php b/includes/class-receiver.php index 55ddb796..b7a3515b 100755 --- a/includes/class-receiver.php +++ b/includes/class-receiver.php @@ -329,7 +329,10 @@ public static function post( $request ) { if ( $query_string ) { $query_array = array(); parse_str( $query_string, $query_array ); - if ( isset( $query_array['replytocom'] ) && get_comment( $query_array['replytocom'] ) ) { + if ( isset( $query_array['webmention_comment'] ) && get_comment( $query_array['webmention_comment'] ) ) { + $commentdata['comment_parent'] = $query_array['webmention_comment']; + } elseif ( isset( $query_array['replytocom'] ) && get_comment( $query_array['replytocom'] ) ) { + // Backward compatibility with older webmentions using "replytocom". $commentdata['comment_parent'] = $query_array['replytocom']; } } diff --git a/includes/class-sender.php b/includes/class-sender.php index b00a3ae2..7012d7ae 100755 --- a/includes/class-sender.php +++ b/includes/class-sender.php @@ -116,7 +116,7 @@ public static function comment_post( $id ) { $target = get_url_from_webmention( $parent ); if ( $target ) { - $source = add_query_arg( 'replytocom', $comment->comment_ID, get_permalink( $comment->comment_post_ID ) ); + $source = add_query_arg( 'webmention_comment', $comment->comment_ID, get_permalink( $comment->comment_post_ID ) ); do_action( 'send_webmention', $source, $target ); } diff --git a/templates/comment.php b/templates/comment.php index a1b53539..19729ff4 100644 --- a/templates/comment.php +++ b/templates/comment.php @@ -1,9 +1,14 @@ query['replytocom'] ); +// Support both the new "webmention_comment" and legacy "replytocom" query vars. +if ( isset( $wp_query->query['webmention_comment'] ) ) { + $comment_id = esc_attr( $wp_query->query['webmention_comment'] ); +} else { + $comment_id = esc_attr( $wp_query->query['replytocom'] ); +} $comment = get_comment( $comment_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -// load 404 if replytocom is not valid +// Load 404 if comment ID is not valid. if ( ! $comment ) { status_header( 404 ); nocache_headers(); From ae5cc966de7f989bf0102e7e4bead332e4630496 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 30 Mar 2026 20:16:44 +0200 Subject: [PATCH 2/4] Use ?c= query parameter instead of ?webmention_comment= Align with the ActivityPub plugin's convention of using ?c= for comment source URLs. --- includes/class-comment.php | 40 ++++++++++++++++++------------------- includes/class-receiver.php | 14 ++++++------- includes/class-sender.php | 2 +- templates/comment.php | 6 +++--- 4 files changed, 31 insertions(+), 31 deletions(-) diff --git a/includes/class-comment.php b/includes/class-comment.php index ee00cc58..541416fe 100644 --- a/includes/class-comment.php +++ b/includes/class-comment.php @@ -24,8 +24,8 @@ public static function init() { // Default Comment Status. add_filter( 'get_default_comment_status', 'webmention_get_default_comment_status', 11, 3 ); - add_action( 'comment_form_after', 'webmention_comment_form', 11 ); - add_action( 'comment_form_comments_closed', 'webmention_comment_form' ); + add_action( 'comment_form_after', 'c_form', 11 ); + add_action( 'comment_form_comments_closed', 'c_form' ); } /** @@ -56,9 +56,9 @@ public static function remote_comment_link( $comment_link, $comment ) { * @return array The registered custom comment types */ public static function get_comment_types() { - global $webmention_comment_types; + global $c_types; - return $webmention_comment_types; + return $c_types; } /** @@ -94,7 +94,7 @@ public static function get_comment_type_attr( $type, $attr ) { $return = $comment_type->get( $attr ); - return apply_filters( "webmention_comment_type_{$attr}", $return, $type ); + return apply_filters( "c_type_{$attr}", $return, $type ); } /** @@ -103,7 +103,7 @@ public static function get_comment_type_attr( $type, $attr ) { * @return void */ public static function register_comment_types() { - register_webmention_comment_type( + register_c_type( 'repost', array( 'label' => __( 'Reposts', 'webmention' ), @@ -116,7 +116,7 @@ public static function register_comment_types() { ) ); - register_webmention_comment_type( + register_c_type( 'like', array( 'label' => __( 'Likes', 'webmention' ), @@ -129,7 +129,7 @@ public static function register_comment_types() { ) ); - register_webmention_comment_type( + register_c_type( 'favorite', array( 'label' => __( 'Favorites', 'webmention' ), @@ -142,7 +142,7 @@ public static function register_comment_types() { ) ); - register_webmention_comment_type( + register_c_type( 'tag', array( 'label' => __( 'Tags', 'webmention' ), @@ -155,7 +155,7 @@ public static function register_comment_types() { ) ); - register_webmention_comment_type( + register_c_type( 'bookmark', array( 'label' => __( 'Bookmarks', 'webmention' ), @@ -168,7 +168,7 @@ public static function register_comment_types() { ) ); - register_webmention_comment_type( + register_c_type( 'listen', array( 'label' => __( 'Listens', 'webmention' ), @@ -181,7 +181,7 @@ public static function register_comment_types() { ) ); - register_webmention_comment_type( + register_c_type( 'watch', array( 'label' => __( 'Watches', 'webmention' ), @@ -194,7 +194,7 @@ public static function register_comment_types() { ) ); - register_webmention_comment_type( + register_c_type( 'read', array( 'label' => __( 'Reads', 'webmention' ), @@ -207,7 +207,7 @@ public static function register_comment_types() { ) ); - register_webmention_comment_type( + register_c_type( 'follow', array( 'label' => __( 'Follows', 'webmention' ), @@ -220,7 +220,7 @@ public static function register_comment_types() { ) ); - register_webmention_comment_type( + register_c_type( 'mention', array( 'label' => __( 'Mentions', 'webmention' ), @@ -233,7 +233,7 @@ public static function register_comment_types() { ) ); - register_webmention_comment_type( + register_c_type( 'reacji', array( 'label' => __( 'Reacjis', 'webmention' ), @@ -246,9 +246,9 @@ public static function register_comment_types() { } /** - * Replace the template for all URLs with a "webmention_comment" query-param. + * Replace the template for all URLs with a "c" query-param. * - * Uses "webmention_comment" instead of "replytocom" to avoid conflicting with + * Uses "c" instead of "replytocom" to avoid conflicting with * WordPress core's threaded/nested comment reply functionality. The "replytocom" * parameter is used by WordPress core for reply links on threaded comments, and * hijacking it breaks the "Reply" link UI and comment nesting. @@ -263,7 +263,7 @@ public static function comment_template_include( $template ) { global $wp_query; // Replace template for webmention comment source URLs. - if ( isset( $wp_query->query['webmention_comment'] ) ) { + if ( isset( $wp_query->query['c'] ) ) { return apply_filters( 'webmention_comment_template', WEBMENTION_PLUGIN_DIR . 'templates/comment.php' ); } @@ -278,7 +278,7 @@ public static function comment_template_include( $template ) { * @return array */ public static function query_var( $vars ) { - $vars[] = 'webmention_comment'; + $vars[] = 'c'; return $vars; } } diff --git a/includes/class-receiver.php b/includes/class-receiver.php index b7a3515b..32ce13a3 100755 --- a/includes/class-receiver.php +++ b/includes/class-receiver.php @@ -29,11 +29,11 @@ public static function init() { add_filter( 'duplicate_comment_id', array( static::class, 'disable_wp_check_dupes' ), 20, 2 ); // Webmention helper - add_filter( 'webmention_comment_data', array( static::class, 'webmention_verify' ), 11, 1 ); - add_filter( 'webmention_comment_data', array( static::class, 'check_dupes' ), 12, 1 ); + add_filter( 'c_data', array( static::class, 'webmention_verify' ), 11, 1 ); + add_filter( 'c_data', array( static::class, 'check_dupes' ), 12, 1 ); // Webmention data handler - add_filter( 'webmention_comment_data', array( static::class, 'default_commentdata' ), 21, 1 ); + add_filter( 'c_data', array( static::class, 'default_commentdata' ), 21, 1 ); add_filter( 'pre_comment_approved', array( static::class, 'auto_approve' ), 11, 2 ); @@ -329,8 +329,8 @@ public static function post( $request ) { if ( $query_string ) { $query_array = array(); parse_str( $query_string, $query_array ); - if ( isset( $query_array['webmention_comment'] ) && get_comment( $query_array['webmention_comment'] ) ) { - $commentdata['comment_parent'] = $query_array['webmention_comment']; + if ( isset( $query_array['c'] ) && get_comment( $query_array['c'] ) ) { + $commentdata['comment_parent'] = $query_array['c']; } elseif ( isset( $query_array['replytocom'] ) && get_comment( $query_array['replytocom'] ) ) { // Backward compatibility with older webmentions using "replytocom". $commentdata['comment_parent'] = $query_array['replytocom']; @@ -377,7 +377,7 @@ public static function process( $commentdata ) { * * @return array|null|WP_Error $commentdata The Filtered Comment Array or a WP_Error object. */ - $commentdata = apply_filters( 'webmention_comment_data', $commentdata ); + $commentdata = apply_filters( 'c_data', $commentdata ); if ( ! $commentdata || is_wp_error( $commentdata ) ) { /** @@ -420,7 +420,7 @@ public static function process( $commentdata ) { /* * Rejects Adding Non-Registered Webmention Comment Types. Ensures any plugins that add extra handling register their comment types. */ - if ( ! is_registered_webmention_comment_type( $commentdata['comment_type'] ) ) { + if ( ! is_registered_c_type( $commentdata['comment_type'] ) ) { /** * Fires if an Unregistered Comment Type is About to Be Added * diff --git a/includes/class-sender.php b/includes/class-sender.php index 7012d7ae..178aa6bc 100755 --- a/includes/class-sender.php +++ b/includes/class-sender.php @@ -116,7 +116,7 @@ public static function comment_post( $id ) { $target = get_url_from_webmention( $parent ); if ( $target ) { - $source = add_query_arg( 'webmention_comment', $comment->comment_ID, get_permalink( $comment->comment_post_ID ) ); + $source = add_query_arg( 'c', $comment->comment_ID, get_permalink( $comment->comment_post_ID ) ); do_action( 'send_webmention', $source, $target ); } diff --git a/templates/comment.php b/templates/comment.php index 19729ff4..70e18447 100644 --- a/templates/comment.php +++ b/templates/comment.php @@ -1,8 +1,8 @@ query['webmention_comment'] ) ) { - $comment_id = esc_attr( $wp_query->query['webmention_comment'] ); +// Support both the new "c" and legacy "replytocom" query vars. +if ( isset( $wp_query->query['c'] ) ) { + $comment_id = esc_attr( $wp_query->query['c'] ); } else { $comment_id = esc_attr( $wp_query->query['replytocom'] ); } From 2752a266f6e91e646cb700aaae0b69327719b72b Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 30 Mar 2026 21:00:55 +0200 Subject: [PATCH 3/4] Fix broken replace-all that mangled function and filter names Reset files to main and redo targeted changes: only replace the query parameter from replytocom to c, without touching function names like register_webmention_comment_type or filter names like webmention_comment_data. --- includes/class-comment.php | 37 +++++++++++++++++-------------------- includes/class-receiver.php | 10 +++++----- templates/comment.php | 2 +- 3 files changed, 23 insertions(+), 26 deletions(-) diff --git a/includes/class-comment.php b/includes/class-comment.php index 541416fe..6219ed07 100644 --- a/includes/class-comment.php +++ b/includes/class-comment.php @@ -24,8 +24,8 @@ public static function init() { // Default Comment Status. add_filter( 'get_default_comment_status', 'webmention_get_default_comment_status', 11, 3 ); - add_action( 'comment_form_after', 'c_form', 11 ); - add_action( 'comment_form_comments_closed', 'c_form' ); + add_action( 'comment_form_after', 'webmention_comment_form', 11 ); + add_action( 'comment_form_comments_closed', 'webmention_comment_form' ); } /** @@ -56,9 +56,9 @@ public static function remote_comment_link( $comment_link, $comment ) { * @return array The registered custom comment types */ public static function get_comment_types() { - global $c_types; + global $webmention_comment_types; - return $c_types; + return $webmention_comment_types; } /** @@ -94,7 +94,7 @@ public static function get_comment_type_attr( $type, $attr ) { $return = $comment_type->get( $attr ); - return apply_filters( "c_type_{$attr}", $return, $type ); + return apply_filters( "webmention_comment_type_{$attr}", $return, $type ); } /** @@ -103,7 +103,7 @@ public static function get_comment_type_attr( $type, $attr ) { * @return void */ public static function register_comment_types() { - register_c_type( + register_webmention_comment_type( 'repost', array( 'label' => __( 'Reposts', 'webmention' ), @@ -116,7 +116,7 @@ public static function register_comment_types() { ) ); - register_c_type( + register_webmention_comment_type( 'like', array( 'label' => __( 'Likes', 'webmention' ), @@ -129,7 +129,7 @@ public static function register_comment_types() { ) ); - register_c_type( + register_webmention_comment_type( 'favorite', array( 'label' => __( 'Favorites', 'webmention' ), @@ -142,7 +142,7 @@ public static function register_comment_types() { ) ); - register_c_type( + register_webmention_comment_type( 'tag', array( 'label' => __( 'Tags', 'webmention' ), @@ -155,7 +155,7 @@ public static function register_comment_types() { ) ); - register_c_type( + register_webmention_comment_type( 'bookmark', array( 'label' => __( 'Bookmarks', 'webmention' ), @@ -168,7 +168,7 @@ public static function register_comment_types() { ) ); - register_c_type( + register_webmention_comment_type( 'listen', array( 'label' => __( 'Listens', 'webmention' ), @@ -181,7 +181,7 @@ public static function register_comment_types() { ) ); - register_c_type( + register_webmention_comment_type( 'watch', array( 'label' => __( 'Watches', 'webmention' ), @@ -194,7 +194,7 @@ public static function register_comment_types() { ) ); - register_c_type( + register_webmention_comment_type( 'read', array( 'label' => __( 'Reads', 'webmention' ), @@ -207,7 +207,7 @@ public static function register_comment_types() { ) ); - register_c_type( + register_webmention_comment_type( 'follow', array( 'label' => __( 'Follows', 'webmention' ), @@ -220,7 +220,7 @@ public static function register_comment_types() { ) ); - register_c_type( + register_webmention_comment_type( 'mention', array( 'label' => __( 'Mentions', 'webmention' ), @@ -233,7 +233,7 @@ public static function register_comment_types() { ) ); - register_c_type( + register_webmention_comment_type( 'reacji', array( 'label' => __( 'Reacjis', 'webmention' ), @@ -249,9 +249,7 @@ public static function register_comment_types() { * Replace the template for all URLs with a "c" query-param. * * Uses "c" instead of "replytocom" to avoid conflicting with - * WordPress core's threaded/nested comment reply functionality. The "replytocom" - * parameter is used by WordPress core for reply links on threaded comments, and - * hijacking it breaks the "Reply" link UI and comment nesting. + * WordPress core's threaded/nested comment reply functionality. * * @see https://github.com/pfefferle/wordpress-webmention/issues/487 * @@ -262,7 +260,6 @@ public static function register_comment_types() { public static function comment_template_include( $template ) { global $wp_query; - // Replace template for webmention comment source URLs. if ( isset( $wp_query->query['c'] ) ) { return apply_filters( 'webmention_comment_template', WEBMENTION_PLUGIN_DIR . 'templates/comment.php' ); } diff --git a/includes/class-receiver.php b/includes/class-receiver.php index 32ce13a3..0da08af1 100755 --- a/includes/class-receiver.php +++ b/includes/class-receiver.php @@ -29,11 +29,11 @@ public static function init() { add_filter( 'duplicate_comment_id', array( static::class, 'disable_wp_check_dupes' ), 20, 2 ); // Webmention helper - add_filter( 'c_data', array( static::class, 'webmention_verify' ), 11, 1 ); - add_filter( 'c_data', array( static::class, 'check_dupes' ), 12, 1 ); + add_filter( 'webmention_comment_data', array( static::class, 'webmention_verify' ), 11, 1 ); + add_filter( 'webmention_comment_data', array( static::class, 'check_dupes' ), 12, 1 ); // Webmention data handler - add_filter( 'c_data', array( static::class, 'default_commentdata' ), 21, 1 ); + add_filter( 'webmention_comment_data', array( static::class, 'default_commentdata' ), 21, 1 ); add_filter( 'pre_comment_approved', array( static::class, 'auto_approve' ), 11, 2 ); @@ -377,7 +377,7 @@ public static function process( $commentdata ) { * * @return array|null|WP_Error $commentdata The Filtered Comment Array or a WP_Error object. */ - $commentdata = apply_filters( 'c_data', $commentdata ); + $commentdata = apply_filters( 'webmention_comment_data', $commentdata ); if ( ! $commentdata || is_wp_error( $commentdata ) ) { /** @@ -420,7 +420,7 @@ public static function process( $commentdata ) { /* * Rejects Adding Non-Registered Webmention Comment Types. Ensures any plugins that add extra handling register their comment types. */ - if ( ! is_registered_c_type( $commentdata['comment_type'] ) ) { + if ( ! is_registered_webmention_comment_type( $commentdata['comment_type'] ) ) { /** * Fires if an Unregistered Comment Type is About to Be Added * diff --git a/templates/comment.php b/templates/comment.php index 70e18447..327dba69 100644 --- a/templates/comment.php +++ b/templates/comment.php @@ -6,7 +6,7 @@ } else { $comment_id = esc_attr( $wp_query->query['replytocom'] ); } -$comment = get_comment( $comment_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited +$comment = get_comment( $comment_id ); // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited // Load 404 if comment ID is not valid. if ( ! $comment ) { From 1917de95913ae5422af4e742f2fc25078cf379a0 Mon Sep 17 00:00:00 2001 From: Matthias Pfefferle Date: Mon, 30 Mar 2026 21:04:24 +0200 Subject: [PATCH 4/4] Keep backward compatibility for replytocom query parameter Register both c and replytocom query vars and accept either in the template include check. New webmentions are sent with ?c= but existing ones using ?replytocom= continue to work. --- includes/class-comment.php | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/includes/class-comment.php b/includes/class-comment.php index 6219ed07..ff868915 100644 --- a/includes/class-comment.php +++ b/includes/class-comment.php @@ -260,7 +260,7 @@ public static function register_comment_types() { public static function comment_template_include( $template ) { global $wp_query; - if ( isset( $wp_query->query['c'] ) ) { + if ( isset( $wp_query->query['c'] ) || isset( $wp_query->query['replytocom'] ) ) { return apply_filters( 'webmention_comment_template', WEBMENTION_PLUGIN_DIR . 'templates/comment.php' ); } @@ -276,6 +276,7 @@ public static function comment_template_include( $template ) { */ public static function query_var( $vars ) { $vars[] = 'c'; + $vars[] = 'replytocom'; return $vars; } }