Skip to content

Commit

Permalink
Merge pull request #2001 from Automattic/trunk
Browse files Browse the repository at this point in the history
Alpha release Dec 12
  • Loading branch information
leogermani authored Dec 12, 2024
2 parents 6ae74bd + 036517b commit 1dc269e
Show file tree
Hide file tree
Showing 5 changed files with 108 additions and 226 deletions.
266 changes: 45 additions & 221 deletions includes/class-newspack-blocks.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,32 +20,6 @@ class Newspack_Blocks {
'tiers-based' => 'newspack-blocks-donate-tiers-based',
];

/**
* Regex pattern we can use to search for and remove custom SQL statements.
* Custom statements added by this class are wrapped by `newspack-blocks` comments.
*/
const SQL_PATTERN = '/\/\* newspack-blocks \*\/(.|\n)*\/\* \/newspack-blocks \*\//';

/**
* Class property to store user IDs and CAP guest author names for building
* custom SQL statements. In order to allow a single WP_Query to filter by
* both WP users and CAP guest authors (a taxonomy), we need to directly
* modify the JOIN and WHERE clauses in the SQL query.
*
* If this property is false, then the custom statements will be stripped
* from all SQL clauses. If it's an array with `authors` and `coauthors`
* keys, the custom statements will be added to the SQL query.
*
* Example array:
* [
* 'authors' => [], // Array of numeric WP user IDs.
* 'coauthors' => [], // Array of CAP guest author name slugs.
* ]
*
* @var boolean|array
*/
protected static $filter_clauses = false;

/**
* Add hooks and filters.
*/
Expand All @@ -55,8 +29,6 @@ public static function init() {
add_post_type_support( 'page', 'newspack_blocks' );
add_action( 'jetpack_register_gutenberg_extensions', [ __CLASS__, 'disable_jetpack_donate' ], 99 );
add_filter( 'the_content', [ __CLASS__, 'hide_post_content_when_iframe_block_is_fullscreen' ] );
add_filter( 'posts_clauses', [ __CLASS__, 'filter_posts_clauses_when_co_authors' ], 999, 2 );
add_filter( 'posts_groupby', [ __CLASS__, 'group_by_post_id_filter' ], 999 );

/**
* Disable NextGEN's `C_NextGen_Shortcode_Manager`.
Expand Down Expand Up @@ -595,9 +567,6 @@ private static function get_specific_posts_from_blocks( $blocks, $block_name ) {
* @return array
*/
public static function build_articles_query( $attributes, $block_name ) {
// Reset author/CAP guest author SQL statements by default.
self::$filter_clauses = false;

global $newspack_blocks_post_id;
if ( ! $newspack_blocks_post_id ) {
$newspack_blocks_post_id = array();
Expand Down Expand Up @@ -633,6 +602,7 @@ public static function build_articles_query( $attributes, $block_name ) {
'ignore_sticky_posts' => true,
'has_password' => false,
'is_newspack_query' => true,
'tax_query' => [], // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
);
if ( $specific_mode && $specific_posts ) {
$args['posts_per_page'] = count( $specific_posts );
Expand Down Expand Up @@ -698,80 +668,59 @@ public static function build_articles_query( $attributes, $block_name ) {
}
}

$is_co_authors_plus_active = class_exists( 'CoAuthors_Plus' );
$co_authors_guest_authors = class_exists( 'CoAuthors_Guest_Authors' ) ? new CoAuthors_Guest_Authors() : null;

if ( $authors && count( $authors ) ) {
$co_authors_names = [];

if ( $is_co_authors_plus_active ) {
foreach ( $authors as $index => $author_id ) {
// If the given ID is a guest author.
$co_author = $co_authors_guest_authors ? $co_authors_guest_authors->get_guest_author_by( 'id', $author_id ) : null;
if ( $co_author ) {
if ( ! empty( $co_author->linked_account ) ) {
$linked_account = get_user_by( 'login', $co_author->linked_account );
if ( $linked_account ) {
$authors[] = $linked_account->ID;
global $coauthors_plus;
$is_co_authors_plus_active = is_object( $coauthors_plus ) && method_exists( $coauthors_plus, 'get_coauthor_by' );

if ( ! $is_co_authors_plus_active ) {
$args['author__in'] = $authors;
} else {
/**
* When CoAuthors Plus is active, we ignore the 'author__in' parameter and search only by the author taxonomy.
*
* If CAP has been activated recently, the author taxonomy may not have been populated yet. You'll need to run
* wp co-authors-plus create-author-terms-for-posts to make sure all posts have the author terms in place.
*/
$authors_term_ids = [];
foreach ( $authors as $author_id ) {
$co_author = $coauthors_plus->get_coauthor_by( 'id', $author_id );
if ( is_object( $co_author ) ) {
$term = $coauthors_plus->get_author_term( $co_author );
if ( $term ) {
$authors_term_ids[] = $term->term_id;
}

// If it's a guest author, also check the linked author.
if ( 'guest-author' === $co_author->type && ! empty( $co_author->wp_user ) && $co_author->wp_user instanceof \WP_User ) {
$term = $coauthors_plus->get_author_term( $co_author->wp_user );
if ( $term ) {
$authors_term_ids[] = $term->term_id;
}
}
$co_authors_names[] = $co_author->user_nicename;
unset( $authors[ $index ] );
} else {
$authors_controller = new WP_REST_Newspack_Authors_Controller();
$author_data = get_userdata( $author_id );
if ( $author_data ) {
$linked_guest_author = $authors_controller->get_linked_guest_author( $author_data->user_login );
// If the given ID is linked to a guest author.
if ( $linked_guest_author ) {
$guest_author_name = sanitize_title( $linked_guest_author->post_title );
if ( ! in_array( $guest_author_name, $co_authors_names, true ) ) {
$co_authors_names[] = $guest_author_name;
$co_authors_names[] = $linked_guest_author->post_name;
unset( $authors[ $index ] );

// If it's a regular wp user, check and include any linked guest authors.
if ( 'wpuser' === $co_author->type ) {
$authors_controller = new WP_REST_Newspack_Authors_Controller();
$linked_guest_author_post = $authors_controller->get_linked_guest_author( $co_author->user_login );
if ( $linked_guest_author_post ) {
$linked_guest_author_object = $coauthors_plus->get_coauthor_by( 'id', $author_id );
if ( is_object( $linked_guest_author_object ) ) {
$term = $coauthors_plus->get_author_term( $linked_guest_author_object );
if ( $term ) {
$authors_term_ids[] = $term->term_id;
}
}
} else {
$co_authors_names[] = $author_data->user_login;
$co_authors_names[] = $author_data->user_nicename;
$co_authors_names[] = 'cap-' . $author_data->user_nicename;
$co_authors_names[] = $author_data->user_email;
}
}
}
}
}

// Reset numeric indexes.
$authors = array_values( $authors );

if ( empty( $authors ) && count( $co_authors_names ) ) {
// We are only looking for Guest Authors posts. So we need to only search by taxonomy.
$args['tax_query'] = [ // phpcs:ignore WordPress.DB.SlowDBQuery.slow_db_query_tax_query
'relation' => 'OR',
[
'field' => 'slug',
'taxonomy' => 'author',
'terms' => $co_authors_names,
],
[
'field' => 'name',
if ( count( $authors_term_ids ) ) {
$args['tax_query'][] = [
'taxonomy' => 'author',
'terms' => $co_authors_names,
],
];
} elseif ( empty( $co_authors_names ) && count( $authors ) ) {
// Simple search by author. Co-Authors plus is not active.
$args['author__in'] = $authors;
} else {
// The query contains both WP users and CAP guest authors. We need to filter the SQL query.
// That's because author__in and tax_query would be combined with AND, not OR.
self::$filter_clauses = [
'authors' => $authors,
'coauthors' => $co_authors_names,
];

// Also, in these cases, never offload the query to Elastic Search.
$args['newspack_no_es_query'] = true;
'field' => 'term_id',
'terms' => $authors_term_ids,
];
}
}
}
}
Expand Down Expand Up @@ -1236,131 +1185,6 @@ public static function remove_excerpt_more_filter() {
remove_filter( 'excerpt_more', [ __CLASS__, 'more_excerpt' ], 999 );
}

/**
* Filter posts by authors and co-authors. If the query is filtering posts
* by both WP users and CAP guest authors, the SQL clauses must be modified
* directly so that the filtering can happen with a single SQL query.
*
* @param string[] $clauses Associative array of the clauses for the query.
* @param WP_Query $query The WP_Query instance (passed by reference).
*/
public static function filter_posts_clauses_when_co_authors( $clauses, $query ) {
// Remove any lingering custom SQL statements.
$clauses['join'] = preg_replace( self::SQL_PATTERN, '', $clauses['join'] );
$clauses['where'] = preg_replace( self::SQL_PATTERN, '', $clauses['where'] );
$is_newspack_query = isset( $query->query_vars['is_newspack_query'] ) && $query->query_vars['is_newspack_query'];

// If the query isn't coming from this plugin, or $filter_clauses lacks expected data.
if (
! $is_newspack_query ||
! self::$filter_clauses ||
! isset( self::$filter_clauses['authors'] ) ||
! isset( self::$filter_clauses['coauthors'] )
) {
return $clauses;
}

global $wpdb;

$authors_ids = self::$filter_clauses['authors'];
$co_authors_names = self::$filter_clauses['coauthors'];

// co-author tax query.
$tax_query = [
'relation' => 'OR',
[
'taxonomy' => 'author',
'field' => 'name',
'terms' => $co_authors_names,
],
[
'taxonomy' => 'author',
'field' => 'slug',
'terms' => $co_authors_names,
],
];

// Generate the tax query SQL.
$tax_query = new WP_Tax_Query( $tax_query );
$tax_query = $tax_query->get_sql( $wpdb->posts, 'ID' );

// Generate the author query SQL.
$csv = implode( ',', wp_parse_id_list( (array) $authors_ids ) );
$author_names = array_reduce(
$authors_ids,
function( $acc, $author_id ) {
$author_data = get_userdata( $author_id );
if ( $author_data ) {
$acc[] = $author_data->user_login;
}
return $acc;
},
[]
);

// If getting only WP users, we don't want to get posts attributed to CAP guest authors not linked to the given WP users.
$exclude = new WP_Tax_Query(
[
'relation' => 'OR',
[
'taxonomy' => 'author',
'operator' => 'NOT EXISTS',
],
[
'field' => 'name',
'taxonomy' => 'author',
'terms' => $author_names,
],
]
);
$exclude = $exclude->get_sql( $wpdb->posts, 'ID' );
$exclude = $exclude['where'];
$authors = " ( {$wpdb->posts}.post_author IN ( $csv ) $exclude ) ";

/**
* Make sure the authors are set, the tax query is valid (doesn't contain 0 = 1).
*
* Since we have two clauses (one searching on name, and one on slug), it's ok to have a "0 = 1" clause for
* one of them, but not for both.
*
* The reason we might have invalid queries is because we do a broad search with many possibles term slugs and names.
* There is not one consistent way terms are created, so the slug/name can have different values. We try to search for all of them, and
* if none of the options we are searching for exist as a term, it will create an invalid query.
*/
if ( substr_count( $tax_query['where'], ' 0 = 1' ) <= 1 ) {
// Append to the current join parts. The JOIN statment only needs to exist in the clause once.
if ( false === strpos( $clauses['join'], $tax_query['join'] ) ) {
$clauses['join'] .= '/* newspack-blocks */ ' . $tax_query['join'] . ' /* /newspack-blocks */';
}

$clauses['where'] .= sprintf(
// The tax query SQL comes prepended with AND.
'%s AND ( %s ( 1=1 %s ) ) %s',
'/* newspack-blocks */',
empty( $authors_ids ) ? '' : $authors . ' OR',
$tax_query['where'],
'/* /newspack-blocks */'
);
}
return $clauses;
}

/**
* Group by post ID filter, used when we join taxonomies while getting posts.
*
* @param string $groupby The GROUP BY clause of the query.
* @return string The filtered GROUP BY clause.
*/
public static function group_by_post_id_filter( $groupby ) {
global $wpdb;

if ( self::$filter_clauses ) {
return "{$wpdb->posts}.ID ";
}

return $groupby;
}

/**
* Utility to get the link for the given post ID. If the post has an external URL meta value, use that.
* Otherwise, use the permalink. But if the post type doesn't have a public singular view, don't link.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -435,7 +435,7 @@ function( $acc, $profile ) use ( $author_id ) {
$handle = $is_website ? get_post_meta( $author_id, 'cap-website', true ) : get_the_author_meta( $profile, $author_id );

if ( $handle ) {
$url = 'twitter' === $profile ? esc_url( 'https://twitter.com/' . $handle ) : esc_url( $handle );
$url = 'twitter' === $profile ? esc_url( 'https://x.com/' . $handle ) : esc_url( $handle );
$acc[ $profile ] = [ 'url' => $url ];

if ( class_exists( 'Newspack_SVG_Icons' ) ) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ public function register_routes() {
* @return WP_REST_Response
*/
public function get_items( $request ) {
$page = $request->get_param( 'page' ) ?? 1;
$page = (int) $request->get_param( 'page' ) ?? 1;
$exclude_ids = $request->get_param( 'exclude_ids' ) ?? [];
$next_page = $page + 1;
$attributes = wp_parse_args(
Expand Down
10 changes: 7 additions & 3 deletions tests/test-homepage-posts-block.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ public function test_hpb_build_articles_query() {
'posts_per_page' => 5,
'post_status' => [ 'publish' ],
'post_type' => [ 'post' ],
'tax_query' => [],
],
'description' => 'Default attributes',
],
Expand All @@ -32,9 +33,9 @@ public function test_hpb_build_articles_query() {
'authors' => [ 1 ],
],
'resulting_query_partial' => [
'posts_per_page' => 1,
'post_type' => 'some-type',
'newspack_no_es_query' => true,
'posts_per_page' => 1,
'post_type' => 'some-type',
'author__in' => [ 1 ],
],
'description' => 'With custom post type and author',
'ignore_tax_query' => true,
Expand Down Expand Up @@ -62,6 +63,9 @@ public function test_hpb_wp_query() {
$cap_author = self::create_guest_author();
$post_id = self::create_post( $cap_author['term_id'] );

global $coauthors_plus;
$coauthors_plus = new CoAuthors_Plus_Mock(); // phpcs:ignore

// Create another post.
self::create_post();

Expand Down
Loading

0 comments on commit 1dc269e

Please sign in to comment.