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
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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 ) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
<?php
/**
* Tests for the "Report this job" link and contact form prefill.
*
* @package jobswp
*/

declare( strict_types = 1 );

use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\TestCase;

/**
* Tests for `Jobs_Dot_WP_Contact_Form::get_report_subject()` and the shortcode prefill.
*/
#[Group( 'jobswp' )]
class Report_Job_Test extends TestCase {

/**
* IDs of posts created during a test, deleted again on teardown.
*
* @var int[]
*/
private array $post_ids = array();

/**
* Global state saved in setUp and restored on teardown.
*
* @var array<string, mixed>
*/
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<string, mixed> $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( '&amp;', $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() );
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -83,11 +83,19 @@

<aside class="job-sidebar">
<div class="job-sidebar__card">
<?php if ( $apply_url ) : ?>
<?php $can_report = class_exists( 'Jobs_Dot_WP_Contact_Form' ); ?>
<?php if ( $apply_url || $can_report ) : ?>
<div class="job-sidebar__apply">
<a href="<?php echo esc_attr( $apply_url ); ?>" class="btn btn-primary" rel="nofollow ugc noopener" <?php echo 'web' === $howtoapply_method ? 'target="_blank"' : ''; ?>>
<?php echo esc_html( $apply_text ); ?>
</a>
<?php if ( $apply_url ) : ?>
<a href="<?php echo esc_attr( $apply_url ); ?>" class="btn btn-primary" rel="nofollow ugc noopener" <?php echo 'web' === $howtoapply_method ? 'target="_blank"' : ''; ?>>
<?php echo esc_html( $apply_text ); ?>
</a>
<?php endif; ?>
<?php if ( $can_report ) : ?>
<p class="job-sidebar__report">
<a href="<?php echo esc_url( Jobs_Dot_WP_Contact_Form::get_report_url( get_the_ID() ) ); ?>" rel="nofollow"><?php esc_html_e( 'Report this job', 'jobswp' ); ?></a>
</p>
<?php endif; ?>
</div>
<?php endif; ?>
<h3><?php esc_html_e( 'Job Details', 'jobswp' ); ?></h3>
Expand Down
20 changes: 20 additions & 0 deletions jobs.wordpress.net/public_html/wp-content/themes/jobswp/style.css
Original file line number Diff line number Diff line change
Expand Up @@ -2106,3 +2106,23 @@ input[type="submit"].submit-job {
width: 100%;
margin-top: 0;
}

.job-sidebar__report {
margin: var(--space-10) 0 0;
font-size: var(--font-size-sm);
text-align: center;
}

.job-sidebar__report:first-child {
margin-top: 0;
}

.job-sidebar__report a {
color: var(--color-charcoal-4);
}

.job-sidebar__report a:hover,
.job-sidebar__report a:focus {
color: var(--color-charcoal-1);
text-decoration: underline;
}
Loading