diff --git a/assets/js/ajax-filters.js b/assets/js/ajax-filters.js index f76d056ae..80bebf0b0 100644 --- a/assets/js/ajax-filters.js +++ b/assets/js/ajax-filters.js @@ -316,6 +316,7 @@ jQuery( document ).ready( function( $ ) { var remote_position = $target.data( 'remote_position' ); var job_types = $target.data( 'job_types' ); var post_status = $target.data( 'post_status' ); + var author = $target.data( 'author' ); var index = $( 'div.job_listings' ).index( this ); var categories, keywords, location; @@ -387,6 +388,7 @@ jQuery( document ).ready( function( $ ) { featured: featured, filled: filled, remote_position: remote_position, + author: author, show_pagination: $target.data( 'show_pagination' ), form_data: $form.serialize(), }; @@ -416,6 +418,7 @@ jQuery( document ).ready( function( $ ) { featured: featured, filled: filled, remote_position: remote_position, + author: author, show_pagination: $target.data( 'show_pagination' ), }; } diff --git a/includes/class-wp-job-manager-ajax.php b/includes/class-wp-job-manager-ajax.php index da75bd5ab..8c0be6742 100644 --- a/includes/class-wp-job-manager-ajax.php +++ b/includes/class-wp-job-manager-ajax.php @@ -136,6 +136,7 @@ public function get_listings() { $remote_position = isset( $_REQUEST['remote_position'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['remote_position'] ) ) : null; $show_pagination = isset( $_REQUEST['show_pagination'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['show_pagination'] ) ) : null; $featured_first = isset( $_REQUEST['featured_first'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['featured_first'] ) ) : null; + $author = ( isset( $_REQUEST['author'] ) && ! is_array( $_REQUEST['author'] ) ) ? sanitize_text_field( wp_unslash( $_REQUEST['author'] ) ) : ''; // phpcs:enable WordPress.Security.NonceVerification.Recommended if ( is_array( $search_categories ) ) { @@ -176,6 +177,7 @@ public function get_listings() { 'orderby' => $orderby, 'order' => $order, 'featured_first' => $featured_first, + 'author' => $author, 'offset' => ( $page - 1 ) * $per_page, 'posts_per_page' => max( 1, $per_page ), // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page -- Known slow query. ]; @@ -245,6 +247,7 @@ public function get_listings() { 'search_location' => $search_location, 'search_categories' => $search_categories, 'search_keywords' => $search_keywords, + 'author' => $author, ] ); diff --git a/includes/class-wp-job-manager-post-types.php b/includes/class-wp-job-manager-post-types.php index 31f9235e0..14bfeb1f4 100644 --- a/includes/class-wp-job-manager-post-types.php +++ b/includes/class-wp-job-manager-post-types.php @@ -744,6 +744,17 @@ public function job_feed() { $input_job_categories = false; } + if ( isset( $_GET['author'] ) && ! is_array( $_GET['author'] ) ) { + $sanitized_author = sanitize_text_field( wp_unslash( $_GET['author'] ) ); + $input_author = empty( $sanitized_author ) ? false : array_values( array_filter( array_map( 'intval', explode( ',', $sanitized_author ) ), fn( $v ) => $v > 0 ) ); + // Fails-closed: author was supplied but yielded no valid IDs → force empty results. + if ( false !== $input_author && empty( $input_author ) ) { + $input_author = [ 0 ]; + } + } else { + $input_author = false; + } + $job_manager_keyword = isset( $_GET['search_keywords'] ) ? sanitize_text_field( wp_unslash( $_GET['search_keywords'] ) ) : ''; $input_featured = isset( $_GET['featured'] ) ? sanitize_text_field( wp_unslash( $_GET['featured'] ) ) : null; // phpcs:enable WordPress.Security.NonceVerification.Recommended @@ -816,6 +827,10 @@ public function job_feed() { ]; } + if ( ! empty( $input_author ) ) { + $query_args['author__in'] = $input_author; + } + if ( ! empty( $job_manager_keyword ) ) { $query_args['s'] = $job_manager_keyword; add_filter( 'posts_search', 'get_job_listings_keyword_search', 10, 2 ); diff --git a/includes/class-wp-job-manager-shortcodes.php b/includes/class-wp-job-manager-shortcodes.php index 1b912cb59..150c34921 100644 --- a/includes/class-wp-job-manager-shortcodes.php +++ b/includes/class-wp-job-manager-shortcodes.php @@ -215,6 +215,7 @@ public function output_jobs( $atts ) { 'filled' => null, // True to show only filled, false to hide filled, leave null to show both/use the settings. 'remote_position' => null, // True to show only remote, false to hide remote, leave null to show both. 'featured_first' => false, // True to show featured first, false to show in default order. + 'author' => 0, // Limit listings to a specific author by user ID. 0 shows all. // Default values for filters. 'location' => '', @@ -237,6 +238,7 @@ public function output_jobs( $atts ) { $atts['show_more'] = $this->string_to_bool( $atts['show_more'] ); $atts['show_pagination'] = $this->string_to_bool( $atts['show_pagination'] ); $atts['featured_first'] = $this->string_to_bool( $atts['featured_first'] ); + $atts['author'] = sanitize_text_field( $atts['author'] ); if ( ! is_null( $atts['featured'] ) ) { $atts['featured'] = ( is_bool( $atts['featured'] ) && $atts['featured'] ) || in_array( $atts['featured'], [ 1, '1', 'true', 'yes' ], true ); @@ -349,6 +351,7 @@ public function output_jobs( $atts ) { 'filled' => $atts['filled'], 'remote_position' => $atts['remote_position'], 'featured_first' => $atts['featured_first'], + 'author' => $atts['author'], ] ) ); @@ -392,6 +395,9 @@ public function output_jobs( $atts ) { if ( ! empty( $atts['post_status'] ) ) { $data_attributes['post_status'] = implode( ',', $atts['post_status'] ); } + if ( ! empty( $atts['author'] ) ) { + $data_attributes['author'] = $atts['author']; + } $data_attributes['post_id'] = isset( $GLOBALS['post'] ) ? $GLOBALS['post']->ID : 0; diff --git a/tests/php/tests/test_class.wp-job-manager-functions.php b/tests/php/tests/test_class.wp-job-manager-functions.php index b739e1a01..26fbc0e9d 100644 --- a/tests/php/tests/test_class.wp-job-manager-functions.php +++ b/tests/php/tests/test_class.wp-job-manager-functions.php @@ -980,4 +980,134 @@ public function test_renew_job_listing() { WP_Job_Manager_Helper_Renewals::renew_job_listing( get_post( $job_listing_id ) ); $this->assertFalse( WP_Job_Manager_Helper_Renewals::job_can_be_renewed( $job_listing ) ); } + + /** + * @since 2.5.0 + * @covers ::get_job_listings + */ + public function test_get_job_listings_author_single_id() { + $user_a = $this->factory->user->create(); + $user_b = $this->factory->user->create(); + + $jobs_a = $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_a ] ); + $jobs_b = $this->factory->job_listing->create_many( 3, [ 'post_author' => $user_b ] ); + + $result = get_job_listings( [ 'author' => (string) $user_a ] ); + + $this->assertEqualSets( $jobs_a, wp_list_pluck( $result->posts, 'ID' ) ); + } + + /** + * @since 2.5.0 + * @covers ::get_job_listings + */ + public function test_get_job_listings_author_multiple_ids() { + $user_a = $this->factory->user->create(); + $user_b = $this->factory->user->create(); + $user_c = $this->factory->user->create(); + + $jobs_a = $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_a ] ); + $jobs_b = $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_b ] ); + $jobs_c = $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_c ] ); + + $result = get_job_listings( [ 'author' => $user_a . ',' . $user_b ] ); + + $this->assertEqualSets( array_merge( $jobs_a, $jobs_b ), wp_list_pluck( $result->posts, 'ID' ) ); + } + + /** + * @since 2.5.0 + * @covers ::get_job_listings + */ + public function test_get_job_listings_author_empty_string_shows_no_filter() { + $user_a = $this->factory->user->create(); + $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_a ] ); + + $result = get_job_listings( [ 'author' => '' ] ); + + // Empty string means no filter — all listings are returned. + $this->assertGreaterThanOrEqual( 2, $result->found_posts ); + } + + /** + * Non-numeric input should return zero results (fails-closed). + * + * @since 2.5.0 + * @covers ::get_job_listings + */ + public function test_get_job_listings_author_non_numeric_returns_no_results() { + $user_a = $this->factory->user->create(); + $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_a ] ); + + $result = get_job_listings( [ 'author' => 'abc' ] ); + + $this->assertSame( 0, $result->found_posts ); + } + + /** + * Negative IDs should not be converted to positive via absint and must return zero results. + * + * @since 2.5.0 + * @covers ::get_job_listings + */ + public function test_get_job_listings_author_negative_id_returns_no_results() { + $user_a = $this->factory->user->create(); + $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_a ] ); + + $result = get_job_listings( [ 'author' => '-5' ] ); + + $this->assertSame( 0, $result->found_posts ); + } + + /** + * @since 2.5.0 + * @covers ::get_job_listings + */ + public function test_get_job_listings_author_zero_returns_no_results() { + $user_a = $this->factory->user->create(); + $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_a ] ); + + $result = get_job_listings( [ 'author' => '0' ] ); + + $this->assertSame( 0, $result->found_posts ); + } + + /** + * Mixed valid and invalid IDs: only valid IDs should be used. + * + * @since 2.5.0 + * @covers ::get_job_listings + */ + public function test_get_job_listings_author_mixed_valid_and_invalid() { + $user_a = $this->factory->user->create(); + $user_b = $this->factory->user->create(); + + $jobs_a = $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_a ] ); + $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_b ] ); + + // 'abc' parses to 0 and should be dropped; only $user_a's listings are returned. + $result = get_job_listings( [ 'author' => $user_a . ',abc' ] ); + + $this->assertEqualSets( $jobs_a, wp_list_pluck( $result->posts, 'ID' ) ); + } + + /** + * Array input with valid user IDs should work. + * + * @since 2.5.0 + * @covers ::get_job_listings + */ + public function test_get_job_listings_author_array_input() { + $user_a = $this->factory->user->create(); + $user_b = $this->factory->user->create(); + $user_c = $this->factory->user->create(); + + $jobs_a = $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_a ] ); + $jobs_b = $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_b ] ); + $this->factory->job_listing->create_many( 2, [ 'post_author' => $user_c ] ); + + $result = get_job_listings( [ 'author' => [ $user_a, $user_b ] ] ); + + $this->assertEqualSets( array_merge( $jobs_a, $jobs_b ), wp_list_pluck( $result->posts, 'ID' ) ); + } } diff --git a/wp-job-manager-functions.php b/wp-job-manager-functions.php index ff0f591ae..a5ecfba1b 100644 --- a/wp-job-manager-functions.php +++ b/wp-job-manager-functions.php @@ -12,7 +12,11 @@ * Queries job listings with certain criteria and returns them. * * @since 1.0.5 - * @param string|array|object $args Arguments used to retrieve job listings. + * @param string|array|object $args { + * Arguments used to retrieve job listings. + * + * @type int|string|int[] $author Optional. User ID, comma-separated user IDs, or array of user IDs to filter listings by author. Default 0 (no filter). + * } * @return WP_Query */ function get_job_listings( $args = [] ) { @@ -193,6 +197,15 @@ function get_job_listings( $args = [] ) { ]; } + if ( isset( $args['author'] ) && ( is_array( $args['author'] ) || '' !== $args['author'] ) ) { + $raw_author = $args['author']; + $author_ids = is_array( $raw_author ) + ? array_values( array_filter( array_map( 'intval', $raw_author ), fn( $v ) => $v > 0 ) ) + : array_values( array_filter( array_map( 'intval', explode( ',', (string) $raw_author ) ), fn( $v ) => $v > 0 ) ); + // Fails-closed: if author was supplied but yielded no valid IDs, return zero results. + $query_args['author__in'] = ! empty( $author_ids ) ? $author_ids : [ 0 ]; + } + $job_manager_keyword = sanitize_text_field( $args['search_keywords'] ); if ( ! empty( $job_manager_keyword ) && strlen( $job_manager_keyword ) >= apply_filters( 'job_manager_get_listings_keyword_length_threshold', 2 ) ) { @@ -629,6 +642,7 @@ function job_manager_get_filtered_links( $args = [] ) { 'search_location' => $args['search_location'], 'job_categories' => implode( ',', $job_categories ), 'search_keywords' => $args['search_keywords'], + 'author' => ! empty( $args['author'] ) ? $args['author'] : '', ] ) ),