diff --git a/jobs.wordpress.net/public_html/wp-content/plugins/jobswp/jobswp-contact-form.php b/jobs.wordpress.net/public_html/wp-content/plugins/jobswp/jobswp-contact-form.php index ef363d7502..59472205f2 100644 --- a/jobs.wordpress.net/public_html/wp-content/plugins/jobswp/jobswp-contact-form.php +++ b/jobs.wordpress.net/public_html/wp-content/plugins/jobswp/jobswp-contact-form.php @@ -36,6 +36,13 @@ class Jobs_Dot_WP_Contact_Form { */ const RATE_LIMIT_WINDOW = 600; + /** + * Query var carrying the ID of the job being reported. + * + * @var string + */ + const REPORT_QUERY_VAR = 'report_job'; + /** * Initializes plugin. */ @@ -45,6 +52,56 @@ public static function init() { add_action( 'jobswp_contact_form_fields', [ Jobs_Dot_WP_Captcha::class, 'output_recaptcha_field' ] ); } + /** + * Gets the URL of the feedback form prefilled to report a job. + * + * @param int $job_id Job post ID. + * @return string Feedback page URL. + */ + public static function get_report_url( $job_id ) { + return add_query_arg( self::REPORT_QUERY_VAR, absint( $job_id ), home_url( '/feedback/' ) ); + } + + /** + * Gets the prefilled subject for reporting a job. + * + * Built server-side from the job ID so links cannot prefill arbitrary text. + * + * @param int $job_id Job post ID. + * @return string Subject, or an empty string if the job is not a published job. + */ + public static function get_report_subject( $job_id ) { + $job = get_post( $job_id ); + + if ( ! $job || 'job' !== $job->post_type || 'publish' !== $job->post_status ) { + return ''; + } + + // Raw title (not get_the_title()) to avoid texturized entities. Stored titles are kses-encoded (&), so decode to plain text. + $title = sanitize_text_field( wp_specialchars_decode( $job->post_title, ENT_QUOTES ) ); + + // validate() limits the subject to 200 bytes, so truncate by bytes without splitting a multibyte character. + if ( strlen( $title ) > 150 ) { + if ( function_exists( 'mb_strcut' ) ) { + $title = mb_strcut( $title, 0, 150, 'UTF-8' ); + } else { + // Core does not polyfill mb_strcut(); drop any partial multibyte character left at the end. + $title = substr( $title, 0, 150 ); + while ( '' !== $title && 1 !== preg_match( '//u', $title ) ) { + $title = substr( $title, 0, -1 ); + } + } + $title .= '…'; + } + + return sprintf( + /* translators: 1: Job title, 2: Job ID */ + __( 'Reporting - %1$s (%2$d)', 'jobswp' ), + $title, + $job->ID + ); + } + /** * Processes POST submissions before output is sent. */ @@ -334,6 +391,13 @@ public static function shortcode() { $subject = isset( $fields['subject'] ) ? $fields['subject'] : ''; $message = isset( $fields['message'] ) ? $fields['message'] : ''; + // Flash values (a failed submission) take priority over the report prefill. + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only prefill of a form field. + if ( '' === $subject && isset( $_GET[ self::REPORT_QUERY_VAR ] ) ) { + // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only prefill of a form field. + $subject = self::get_report_subject( absint( wp_unslash( $_GET[ self::REPORT_QUERY_VAR ] ) ) ); + } + ob_start(); if ( $error ) { diff --git a/jobs.wordpress.net/public_html/wp-content/plugins/jobswp/tests/Report_Job_Test.php b/jobs.wordpress.net/public_html/wp-content/plugins/jobswp/tests/Report_Job_Test.php new file mode 100644 index 0000000000..8c223e5de1 --- /dev/null +++ b/jobs.wordpress.net/public_html/wp-content/plugins/jobswp/tests/Report_Job_Test.php @@ -0,0 +1,221 @@ + + */ + private array $saved_globals = array(); + + /** + * Saves the globals the shortcode test replaces. + * + * @return void + */ + protected function setUp(): void { + parent::setUp(); + + $this->saved_globals = array( + 'post' => $GLOBALS['post'] ?? null, + 'wp_query' => $GLOBALS['wp_query'] ?? null, + ); + } + + /** + * Removes created posts and restores request globals. + * + * @return void + */ + protected function tearDown(): void { + unset( $_GET[ Jobs_Dot_WP_Contact_Form::REPORT_QUERY_VAR ] ); + + foreach ( $this->post_ids as $post_id ) { + wp_delete_post( $post_id, true ); + } + $this->post_ids = array(); + + $GLOBALS['post'] = $this->saved_globals['post']; + $GLOBALS['wp_query'] = $this->saved_globals['wp_query']; + + parent::tearDown(); + } + + /** + * Creates a post and tracks it for cleanup. + * + * @param array $args Post arguments overriding the defaults. + * @return int Post ID. + */ + private function create_post( array $args = array() ): int { + $post_id = wp_insert_post( + array_merge( + array( + 'post_type' => 'job', + 'post_status' => 'publish', + 'post_title' => 'Senior WordPress Developer', + 'post_content' => 'Description.', + ), + $args + ), + true + ); + $this->assertIsInt( $post_id, 'The post could not be created.' ); + $this->post_ids[] = $post_id; + + return $post_id; + } + + /** + * A published job yields the expected subject. + * + * @return void + */ + public function test_published_job_subject(): void { + $id = $this->create_post(); + + $this->assertSame( + sprintf( 'Reporting - Senior WordPress Developer (%d)', $id ), + Jobs_Dot_WP_Contact_Form::get_report_subject( $id ) + ); + } + + /** + * A draft job yields no subject. + * + * @return void + */ + public function test_draft_job_has_no_subject(): void { + $id = $this->create_post( array( 'post_status' => 'draft' ) ); + + $this->assertSame( '', Jobs_Dot_WP_Contact_Form::get_report_subject( $id ) ); + } + + /** + * A non-job post yields no subject. + * + * @return void + */ + public function test_non_job_has_no_subject(): void { + $id = $this->create_post( array( 'post_type' => 'post' ) ); + + $this->assertSame( '', Jobs_Dot_WP_Contact_Form::get_report_subject( $id ) ); + } + + /** + * A nonexistent ID yields no subject. + * + * @return void + */ + public function test_nonexistent_job_has_no_subject(): void { + $this->assertSame( '', Jobs_Dot_WP_Contact_Form::get_report_subject( 999999 ) ); + } + + /** + * A very long title is truncated so the subject fits the form's 200 character limit. + * + * @return void + */ + public function test_long_title_fits_subject_limit(): void { + $id = $this->create_post( array( 'post_title' => str_repeat( 'A', 300 ) ) ); + $subject = Jobs_Dot_WP_Contact_Form::get_report_subject( $id ); + + $this->assertNotSame( '', $subject ); + $this->assertLessThanOrEqual( 200, mb_strlen( $subject ) ); + // The form validates with strlen() (bytes), so the limit must hold there too. + $this->assertLessThanOrEqual( 200, strlen( $subject ) ); + } + + /** + * A long multibyte title still fits the byte-based limit, without broken characters. + * + * @return void + */ + public function test_long_multibyte_title_fits_subject_limit(): void { + $id = $this->create_post( array( 'post_title' => str_repeat( 'ワードプレス', 50 ) ) ); + $subject = Jobs_Dot_WP_Contact_Form::get_report_subject( $id ); + + $this->assertStringStartsWith( 'Reporting - ワ', $subject ); + $this->assertLessThanOrEqual( 200, strlen( $subject ) ); + $this->assertTrue( mb_check_encoding( $subject, 'UTF-8' ) ); + } + + /** + * Apostrophes and ampersands stay plain text rather than becoming HTML entities. + * + * @return void + */ + public function test_title_stays_plain_text(): void { + $id = $this->create_post( array( 'post_title' => "Dev's Role & Support" ) ); + $subject = Jobs_Dot_WP_Contact_Form::get_report_subject( $id ); + + $this->assertSame( sprintf( "Reporting - Dev's Role & Support (%d)", $id ), $subject ); + $this->assertStringNotContainsString( '&#', $subject ); + $this->assertStringNotContainsString( '&', $subject ); + } + + /** + * The report URL points at the feedback page with the job ID. + * + * @return void + */ + public function test_report_url(): void { + $url = Jobs_Dot_WP_Contact_Form::get_report_url( 8 ); + + $this->assertStringContainsString( '/feedback/', $url ); + $this->assertStringContainsString( 'report_job=8', $url ); + } + + /** + * The contact form prefills its subject field from the report query var. + * + * @return void + */ + public function test_shortcode_prefills_subject(): void { + $job_id = $this->create_post( array( 'post_title' => "Dev's Role & Support" ) ); + $page_id = $this->create_post( + array( + 'post_type' => 'page', + 'post_title' => 'Feedback', + 'post_content' => '[jobswp-contact-form]', + ) + ); + + $query = new WP_Query( array( 'page_id' => $page_id ) ); + $query->get_posts(); + $GLOBALS['wp_query'] = $query; + $GLOBALS['post'] = get_post( $page_id ); + + $_GET[ Jobs_Dot_WP_Contact_Form::REPORT_QUERY_VAR ] = (string) $job_id; + + $html = Jobs_Dot_WP_Contact_Form::shortcode(); + + $expected = sprintf( "Reporting - Dev's Role & Support (%d)", $job_id ); + $this->assertStringContainsString( 'value="' . esc_attr( $expected ) . '"', $html ); + + $_GET[ Jobs_Dot_WP_Contact_Form::REPORT_QUERY_VAR ] = '999999'; + $this->assertStringNotContainsString( 'Reporting -', Jobs_Dot_WP_Contact_Form::shortcode() ); + } +} diff --git a/jobs.wordpress.net/public_html/wp-content/themes/jobswp/content-single.php b/jobs.wordpress.net/public_html/wp-content/themes/jobswp/content-single.php index 393ffb7950..45ab1fc5a6 100644 --- a/jobs.wordpress.net/public_html/wp-content/themes/jobswp/content-single.php +++ b/jobs.wordpress.net/public_html/wp-content/themes/jobswp/content-single.php @@ -83,11 +83,19 @@