Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions projects/packages/forms/changelog/fix-feedback-source-data
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: fixed

Forms: Store the feedback source info with more context
Copy link
Contributor

@edanzer edanzer Sep 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

(adding this as a comment on the changelog just to allow comment threading)

This is testing well! I did not test legacy theme or widget, but tested a range of scenarios with block themes.

  • 404 page. I added a form to my 404 site editor template.

    • Confirm bug: On trunk, when submitted, this shows the source as '/' (not accurate). There is no edit form button.
    • Confirm fix: On this PR, it shows the source as '404 Not Found' with the url being the actual url I'd entered (/asdf). It takes me to that url, which is of course a 404 page. But this seems correct to me.
    • Confirm edit button: On this PR, if I click the 'Edit Form' button, it correctly takes me to the 404 template in the site editor (nice).
  • Form in footer template pattern. I added a form to my footer template. Then created a "Page with No Form".

    • Confirm bug: On trunk if I submit the footer form on "Page with no form", it shows the source as '/' (not accurate). There is no edit form button.
    • Confirm fix: On this PR, if I submit the footer form, it shows the source a "Page with no form" with the correct URL.
    • Confirm edit button: If I click the Edit Form button, it surprisingly takes me to the footer template where I created the form. Actually quite impressed on that.
    • Confirm delete page behavior: If I delete "Page with no form", the source still says "Page with no form" and takes me to the accurate but now deleted url, so I get a 404. Seems like a very reasonable action.
  • Blog archive pagination: I set my home page to show blog posts, and then navigated to the third page of the blog archive (/page/3) and submitted the form that lives in the footer template pattern.

    • Confirm bug: On trunk, when submitted, this shows the source as '/' (not accurate). There is no edit form button.
    • Confirm fix: On this PR, it shows the source as the site title, and links to the correct page including pagination (/page/3).
    • Confirm edit button: On this PR, if I click the 'Edit Form' button, it correctly takes me to the footer template pattern when the form lives.
  • Page deletion. I have some other old form pages that I deleted. In the new PR, I now see (deleted) in the link, and the link just goes back to Jetpack Forms admin. That seems reasonable, but also wondering if we should just remove the link?

  • Tests pass. I also confirmed tests pass.


Only question: For deleted pages rather than link circularly back to forms admin, I wonder if we should just remove the link? Not a big deal though.


Verdict on merging: This touches sensitive logic and there are lot of possible permutations, so it's possible we'll find an issue that needs fixing. BUT there's a lot of clearly broken behavior now, and this fixes all of it so far. Given that, I'd say LGTM.

Original file line number Diff line number Diff line change
Expand Up @@ -408,6 +408,16 @@ public function get_item_schema() {
'readonly' => true,
);

$schema['properties']['edit_form_url'] = array(
'description' => __( 'The URL to edit the form.', 'jetpack-forms' ),
'type' => 'string',
'context' => array( 'view', 'edit', 'embed' ),
'arg_options' => array(
'sanitize_callback' => 'sanitize_text_field',
),
'readonly' => true,
);

$schema['properties']['subject'] = array(
'description' => __( 'The subject line of the form submission.', 'jetpack-forms' ),
'type' => 'string',
Expand Down Expand Up @@ -621,6 +631,9 @@ public function prepare_item_for_response( $item, $request ) {
if ( rest_is_field_included( 'entry_permalink', $fields ) ) {
$data['entry_permalink'] = $response->get_entry_permalink();
}
if ( rest_is_field_included( 'edit_form_url', $fields ) ) {
$data['edit_form_url'] = $response->get_edit_form_url();
}
if ( rest_is_field_included( 'subject', $fields ) ) {
$data['subject'] = $response->get_subject();
}
Expand Down
39 changes: 26 additions & 13 deletions projects/packages/forms/src/contact-form/class-contact-form.php
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ class Contact_Form extends Contact_Form_Shortcode {
*/
public $has_verified_jwt = false;

/**
* The source of the feedback entry.
*
* @var Feedback_Source
*/
private $source;

/**
* Construction function.
*
Expand Down Expand Up @@ -243,6 +250,7 @@ public static function get_instance_from_jwt( $jwt_token ) {
}

$form = new self( $data['attributes'], $data['content'], empty( $data['attributes']['id'] ) );
$form->source = Feedback_Source::from_serialized( $data['source'] );
$form->hash = $data['hash'];
$form->has_verified_jwt = true;
return $form;
Expand Down Expand Up @@ -354,17 +362,32 @@ public function get_attributes() {
* @return string The JWT token.
*/
public function get_jwt() {
$attributes = $this->attributes;
$attributes = $this->attributes;
$this->source = Feedback_Source::get_current( $attributes );
return JWT::encode(
array(
'attributes' => $attributes,
'content' => $this->content,
'hash' => $this->hash,
'source' => $this->source->serialize(),
),
self::get_secret()
);
}

/**
* Get the current source obejct. That is relevent to the form and there current request.
*
* @return Feedback_Source Return the current feedback source object.
*/
public function get_source() {
if ( ! $this->source ) {
$attributes = $this->attributes;
$this->source = Feedback_Source::get_current( $attributes );
}
return $this->source;
}

/**
* Get the count of forms.
*
Expand Down Expand Up @@ -829,10 +852,6 @@ class='" . esc_attr( $form_classes ) . "' $form_aria_label
$r .= "\t\t<input type='hidden' name='action' value='grunion-contact-form' />\n";
$r .= "\t\t<input type='hidden' name='contact-form-hash' value='" . esc_attr( $form->hash ) . "' />\n";

if ( $page && $page > 1 ) {
$r .= "\t\t<input type='hidden' name='page' value='$page' />\n";
}

if ( ! $has_submit_button_block ) {
$r .= "\t</p>\n";
}
Expand Down Expand Up @@ -1627,15 +1646,9 @@ public function get_field_ids() {
* Stores feedback. Sends email.
*/
public function process_submission() {
$page_number = 1;

// We skip the nonce verification for since nonce earlier in process_form_submission.
if ( isset( $_POST['page'] ) ) { // phpcs:Ignore WordPress.Security.NonceVerification.Missing
$page_number = absint( wp_unslash( $_POST['page'] ) ); // phpcs:Ignore WordPress.Security.NonceVerification.Missing
}

$response = Feedback::from_submission( $_POST, $this, $this->current_post, $page_number ); // phpcs:Ignore WordPress.Security.NonceVerification.Missing

$response = Feedback::from_submission( $_POST, $this ); // phpcs:Ignore WordPress.Security.NonceVerification.Missing
$response->set_source( $this->get_source() );
$plugin = Contact_Form_Plugin::init();

$id = $this->get_attribute( 'id' );
Expand Down
174 changes: 156 additions & 18 deletions projects/packages/forms/src/contact-form/class-feedback-source.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ class Feedback_Source {
/**
* The ID of the post or page that the feedback was created on.
*
* @var int
* @var string
*/
private $id = 0;
private $id = '';

/**
* The title of the post or page that the feedback was created on.
Expand All @@ -43,29 +43,62 @@ class Feedback_Source {
*/
private $page_number = 1;

/**
* The source type of the feedback entry.
* Possible values: single, widget, block_template, block_template_part
*
* @var string
*/
private $source_type = 'single';

/**
* The request URL of the feedback entry.
*
* @var string
*/
private $request_url = '';

/**
* Constructor for Feedback_Source.
*
* @param int $id The ID of the feedback entry.
* @param string $title The title of the feedback entry.
* @param int $page_number The page number of the feedback entry, default is 1.
* @param string|int $id The Source ID = post ID, widget ID, block template ID, or 0 for homepage or non-post/page.
* @param string $title The title of the feedback entry.
* @param int $page_number The page number of the feedback entry, default is 1.
* @param string $source_type The source type of the feedback entry, default is 'single'.
* @param string $request_url The request URL of the feedback entry.
*/
public function __construct( $id, $title, $page_number = 1 ) {
public function __construct( $id = 0, $title = '', $page_number = 1, $source_type = 'single', $request_url = '' ) {

if ( is_numeric( $id ) ) {
$this->id = $id > 0 ? $id : 0;
} else {
$this->id = $id;
}

$this->id = $id > 0 ? (int) $id : 0;
$this->title = $title;
$this->page_number = $page_number;
$this->permalink = $this->id === 0 ? home_url() : '';

if ( $id <= 0 ) {
return;
}
$this->permalink = empty( $request_url ) ? home_url() : $request_url;
$this->source_type = $source_type; // possible source types: single, widget, block_template, block_template_part
$this->request_url = $request_url;

$entry_post = get_post( $id );
if ( is_numeric( $id ) && ! empty( $id ) ) {
$entry_post = get_post( (int) $id );
if ( $entry_post && $entry_post->post_status === 'publish' ) {
$this->permalink = get_permalink( $entry_post );
$this->title = get_the_title( $entry_post );
} elseif ( $entry_post ) {
$this->permalink = '';

if ( $entry_post && $entry_post->post_status === 'publish' ) {
$this->permalink = get_permalink( $entry_post );
$this->title = get_the_title( $entry_post );
if ( $entry_post->post_status === 'trash' ) {
/* translators: %s is the post title */
$this->title = sprintf( __( '(trashed) %s', 'jetpack-forms' ), $this->title );
}
}
if ( empty( $entry_post ) ) {
/* translators: %s is the post title */
$this->title = sprintf( __( '(deleted) %s', 'jetpack-forms' ), $this->title );
$this->permalink = '';
}
}
}

Expand All @@ -83,11 +116,81 @@ public static function from_submission( $current_post, int $current_page_number
return new self( 0, '', $current_page_number );
}

$title = $current_post->post_title ?? '';
$title = $current_post->post_title ?? __( '(no title)', 'jetpack-forms' );

return new self( $id, $title, $current_page_number );
}

/**
* Get the title of the current page. That we can then use to display in the feedback entry.
*
* @return string The title of the current page. That we want to show to the user. To tell them where the feedback was left.
*/
private static function get_source_title() {
if ( is_front_page() ) {
return get_bloginfo( 'name' );
}
if ( is_home() ) {
return get_the_title( get_option( 'page_for_posts', true ) );
}
if ( is_singular() ) {
return get_the_title();
}
if ( is_archive() ) {
return get_the_archive_title();
}
if ( is_search() ) {
/* translators: %s is the search term */
return sprintf( __( 'Search results for: %s', 'jetpack-forms' ), get_search_query() );
}
if ( is_404() ) {
return __( '404 Not Found', 'jetpack-forms' );
}
return get_bloginfo( 'name' );
}

/**
* Creates a Feedback_Source instance for a block template.
*
* @param array $attributes Form Shortcode attributes.
*
* @return Feedback_Source Returns an instance of Feedback_Source.
*/
public static function get_current( $attributes ) {
global $wp, $page;
$current_url = home_url( add_query_arg( array(), $wp->request ) );
if ( isset( $attributes['widget'] ) && ! empty( $attributes['widget'] ) ) {
return new self( $attributes['widget'], self::get_source_title(), 1, 'widget', $current_url );
}

if ( isset( $attributes['block_template'] ) && ! empty( $attributes['block_template'] ) ) {
global $_wp_current_template_id;
return new self( $_wp_current_template_id, self::get_source_title(), $page, 'block_template', $current_url );
}

if ( isset( $attributes['block_template_part'] ) && ! empty( $attributes['block_template_part'] ) ) {
return new self( $attributes['block_template_part'], self::get_source_title(), $page, 'block_template_part', $current_url );
}

return new Feedback_Source( \get_the_ID(), \get_the_title(), $page, 'single', $current_url );
}

/**
* Creates a Feedback_Source instance from serialized data.
*
* @param array $data The serialized data.
* @return Feedback_Source Returns an instance of Feedback_Source.
*/
public static function from_serialized( $data ) {
$id = $data['source_id'] ?? 0;
$title = $data['entry_title'] ?? '';
$page_number = $data['entry_page'] ?? 1;
$source_type = $data['source_type'] ?? 'single';
$request_url = $data['request_url'] ?? '';

return new self( $id, $title, $page_number, $source_type, $request_url );
}

/**
* Get the permalink of the feedback entry.
*
Expand All @@ -100,6 +203,38 @@ public function get_permalink() {
return $this->permalink;
}

/**
* Get the edit URL of the form or page where the feedback was submitted from.
*
* @return string The edit URL of the form or page.
*/
public function get_edit_form_url() {

if ( current_user_can( 'edit_theme_options' ) ) {
if ( $this->source_type === 'block_template' && \wp_is_block_theme() ) {
return admin_url( 'site-editor.php?p=' . esc_attr( '/wp_template/' . addslashes( $this->id ) ) . '&canvas=edit' );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I remember this template editor linking being particularly difficult because sometimes you need theme namespace added, or sometimes it's self created template. Worth testing well!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So far it has been working well for me.
Should we test this across older versions of WP or Gutenberg?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Jetpack supports current stable WP minus one, so no need to test anything older. I don't think there have been changes in recent versions to these URLs either.

}

if ( $this->source_type === 'block_template_part' && \wp_is_block_theme() ) {
return admin_url( 'site-editor.php?p=' . esc_attr( '/wp_template_part/' . addslashes( $this->id ) ) . '&canvas=edit' );
}

if ( $this->source_type === 'widget' && current_theme_supports( 'widgets' ) ) {
return admin_url( 'widgets.php' );
}
}

if ( $this->id && is_numeric( $this->id ) && $this->id > 0 && current_user_can( 'edit_post', (int) $this->id ) ) {
$entry_post = get_post( (int) $this->id );
if ( $entry_post && $entry_post->post_status === 'trash' ) {
return ''; // No edit link is possible for trashed posts. They need to be restored first.
}
return \get_edit_post_link( (int) $this->id, 'url' );
}

return '';
}

/**
* Get the relative permalink of the feedback entry.
*
Expand Down Expand Up @@ -131,7 +266,7 @@ public function get_title() {
/**
* Get the post id of the feedback entry.
*
* @return int The ID of the feedback entry.
* @return int|string The ID of the feedback entry.
*/
public function get_id() {
return $this->id;
Expand All @@ -146,6 +281,9 @@ public function serialize() {
return array(
'entry_title' => $this->title,
'entry_page' => $this->page_number,
'source_id' => $this->id,
'source_type' => $this->source_type,
'request_url' => $this->request_url,
);
}
}
Loading
Loading