Skip to content
Merged
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
16 changes: 14 additions & 2 deletions includes/frontend/class-display.php
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,7 @@ public static function get_term_posts( $term ): \WP_Query {

// Get all the posts for the current term excluding posts located in its child terms.
$args = array(
'post_status' => 'publish',
'posts_per_page' => -1,
'no_found_rows' => true,
'ignore_sticky_posts' => true,
Expand All @@ -587,19 +588,30 @@ public static function get_term_posts( $term ): \WP_Query {
$query = null;
$cache_enabled = ! empty( self::get_cached_option( 'cache' ) );
$meta_key = null;
$version = Cache::get_content_version();

// Support caching to speed up retrieval. Cache::get_key() includes the current language.
if ( $cache_enabled ) {
$meta_key = Cache::get_key( $args );
$query = Cache::get( $term->term_id, $meta_key );
$cached = Cache::get( $term->term_id, $meta_key );
if ( is_array( $cached ) && ( $cached['version'] ?? 0 ) === $version ) {
$query = $cached['query'] ?? null;
}
}

if ( empty( $query ) ) {
$query = new \WP_Query( $args );

// Support caching to speed up retrieval.
if ( $cache_enabled ) {
Cache::set( $term->term_id, $meta_key, $query );
Cache::set(
$term->term_id,
$meta_key,
array(
'version' => $version,
'query' => $query,
)
);
}
}

Expand Down
6 changes: 3 additions & 3 deletions includes/frontend/class-media-handler.php
Original file line number Diff line number Diff line change
Expand Up @@ -600,8 +600,8 @@ protected static function get_thumbnail_from_content_scan( \WP_Post $post, array
);
}

// Skip content scanning for very large posts to prevent memory exhaustion.
if ( strlen( $post->post_content ) > 50000 ) { // 50KB limit.
// Skip oversized or locked content before scanning for images.
if ( strlen( $post->post_content ) > 50000 || post_password_required( $post ) ) { // 50KB limit.
$post_content = '';
} else {
/**
Expand Down Expand Up @@ -660,7 +660,7 @@ protected static function get_thumbnail_from_content_scan( \WP_Post $post, array
*/
protected static function get_thumbnail_from_first_child( \WP_Post $post, array $args ): array {
$dimensions = self::get_thumb_size( $args['size'] );
$postimage = self::get_first_image( $post->ID, $dimensions[0], $dimensions[1] );
$postimage = post_password_required( $post ) ? '' : self::get_first_image( $post->ID, $dimensions[0], $dimensions[1] );

if ( ! $postimage ) {
return array(
Expand Down
3 changes: 2 additions & 1 deletion includes/frontend/class-related.php
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ public static function get_related_articles_query( $args = array() ) {
'ignore_sticky_posts' => true,
'no_found_rows' => true,
'post_type' => 'wz_knowledgebase',
'post_status' => 'publish',
'posts_per_page' => $args['numberposts'],
'post__not_in' => $exclude,
'orderby' => 'date',
Expand Down Expand Up @@ -299,7 +300,7 @@ public static function get_related_articles( $args = array() ) {

$output .= '</a>';

if ( $args['show_excerpt'] ) {
if ( $args['show_excerpt'] && ! post_password_required( $post_id ) ) {
$excerpt = get_the_excerpt();
if ( empty( $excerpt ) ) {
$content = get_post_field( 'post_content', $post_id );
Expand Down
2 changes: 1 addition & 1 deletion includes/frontend/class-shortcodes.php
Original file line number Diff line number Diff line change
Expand Up @@ -266,7 +266,7 @@ public static function toc( $atts ): string {
);

$post = get_post();
if ( ! $post ) {
if ( ! $post || TOC::is_post_locked( $post ) ) {
return '';
}

Expand Down
20 changes: 20 additions & 0 deletions includes/frontend/class-toc.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,26 @@ public function __construct() {
}
}

/**
* Check whether a post's content is locked behind a password for the current visitor.
*
* Editors previewing the post in the block editor still see the TOC.
*
* @since 3.2.1
*
* @param \WP_Post $post Post object.
* @return bool True when the TOC must not be shown.
*/
public static function is_post_locked( \WP_Post $post ): bool {
if ( ! post_password_required( $post ) ) {
return false;
}

$is_editor_preview = ( is_admin() || ( defined( 'REST_REQUEST' ) && REST_REQUEST ) ) && current_user_can( 'edit_post', $post->ID );

return ! $is_editor_preview;
}

/**
* Filter callback: add anchor IDs to headings in article content.
*
Expand Down
100 changes: 67 additions & 33 deletions includes/rest/class-rest-controller.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
use WP_REST_Response;
use WP_REST_Server;
use WebberZone\Knowledge_Base\Frontend\Related;
use WebberZone\Knowledge_Base\Util\Cache;
use WebberZone\Knowledge_Base\Util\Hook_Registry;

if ( ! defined( 'WPINC' ) ) {
Expand Down Expand Up @@ -41,6 +42,16 @@ class REST_Controller {
*/
private const DEFAULT_TTL = 300;

/**
* Cache key format version.
*
* Changing it orphans entries cached by earlier releases, such as 3.2.0 responses
* that included password-protected content.
*
* @since 3.2.1
*/
private const CACHE_KEY_VERSION = 'p3';

/**
* Constructor.
*/
Expand Down Expand Up @@ -460,6 +471,7 @@ public function get_knowledgebase_posts( WP_REST_Request $request ) {

$query_args = array(
'post_type' => 'wz_knowledgebase',
'post_status' => 'publish',
'ignore_sticky_posts' => true,
'no_found_rows' => false,
'posts_per_page' => $per_page,
Expand Down Expand Up @@ -520,14 +532,16 @@ public function get_knowledgebase_posts( WP_REST_Request $request ) {
$response->header( 'X-WP-Total', (string) (int) $query->found_posts );
$response->header( 'X-WP-TotalPages', (string) (int) $query->max_num_pages );

$this->cache_set(
$cache_key,
array(
'body' => $posts,
'total' => (int) $query->found_posts,
'pages' => (int) $query->max_num_pages,
)
);
if ( ! $this->has_password_protected_post( $query->posts ) ) {
$this->cache_set(
$cache_key,
array(
'body' => $posts,
'total' => (int) $query->found_posts,
'pages' => (int) $query->max_num_pages,
)
);
}

return $response;
}
Expand Down Expand Up @@ -558,6 +572,11 @@ public function get_single_knowledgebase_post( WP_REST_Request $request ) {
);
}

// Password-protected responses depend on the requester's password cookie, so never cache them.
if ( '' !== $post->post_password ) {
return rest_ensure_response( $this->prepare_post_for_response( $post, true ) );
}

$cache_key = $this->get_cache_key(
'kb_single',
array(
Expand Down Expand Up @@ -643,29 +662,14 @@ public function get_related_articles( WP_REST_Request $request ) {
);
}

$cache_key = $this->get_cache_key(
'related',
array(
'post_id' => $post_id,
'limit' => $limit,
)
);

$cached = $this->cache_get( $cache_key );
if ( false !== $cached ) {
return rest_ensure_response( $cached );
}

// Render per visitor because protected thumbnails vary with the password cookie.
$related = Related::get_related_articles(
array(
'post_id' => $post_id,
'limit' => $limit,
'echo' => false,
'post' => $post,
'numberposts' => $limit,
)
);

$this->cache_set( $cache_key, $related );

return rest_ensure_response( $related );
}

Expand Down Expand Up @@ -742,7 +746,9 @@ public function get_search_results( WP_REST_Request $request ) {
$results[] = $this->prepare_post_for_response( $post );
}

$this->cache_set( $cache_key, $results );
if ( ! $this->has_password_protected_post( $wp_query->posts ) ) {
$this->cache_set( $cache_key, $results );
}

return rest_ensure_response( $results );
}
Expand Down Expand Up @@ -1013,6 +1019,9 @@ private function get_post_list_args() {
/**
* Prepare KB post data.
*
* Password-protected posts return an empty excerpt and content unless the
* requester has entered the post password using WordPress's password form.
*
* @param \WP_Post $post Post object.
* @param bool $include_content Include full content flag.
* @return array
Expand All @@ -1023,25 +1032,50 @@ private function prepare_post_for_response( $post, $include_content = false ) {
return array();
}

$is_protected = '' !== $post->post_password;
$is_locked = $is_protected && post_password_required( $post );

$data = array(
'id' => (int) $post->ID,
'title' => get_the_title( $post ),
'slug' => $post->post_name,
'excerpt' => wp_trim_words( wp_strip_all_tags( $post->post_content ), 55 ),
'excerpt' => $is_locked ? '' : wp_trim_words( wp_strip_all_tags( $post->post_content ), 55 ),
'permalink' => get_permalink( $post ),
'products' => $this->format_terms( get_the_terms( $post, 'wzkb_product' ) ),
'sections' => $this->format_terms( get_the_terms( $post, 'wzkb_category' ) ),
'date' => mysql2date( DATE_RFC3339, $post->post_date_gmt, false ),
'modified' => mysql2date( DATE_RFC3339, $post->post_modified_gmt, false ),
'protected' => $is_protected,
);

if ( $include_content ) {
$data['content'] = apply_filters( 'the_content', $post->post_content );
$data['content'] = $is_locked ? '' : apply_filters( 'the_content', $post->post_content );
}

return $data;
}

/**
* Check whether any post in a list is password protected.
*
* Responses containing such posts vary with the requester's password cookie
* and must not be cached.
*
* @since 3.2.1
*
* @param array $posts Post objects.
* @return bool
*/
private function has_password_protected_post( array $posts ): bool {
foreach ( $posts as $post ) {
if ( $post instanceof \WP_Post && '' !== $post->post_password ) {
return true;
}
}

return false;
}

/**
* Format terms list.
*
Expand Down Expand Up @@ -1100,8 +1134,9 @@ private function get_cache_key( $prefix, $context = array() ) {
}

return sprintf(
'%s_%d_%s',
'%s_%s_%d_%s',
$prefix,
self::CACHE_KEY_VERSION,
$this->get_cache_version(),
empty( $context ) ? 'static' : md5( wp_json_encode( $context ) )
);
Expand All @@ -1113,7 +1148,7 @@ private function get_cache_key( $prefix, $context = array() ) {
* @return int
*/
private function get_cache_version() {
return (int) get_option( 'wzkb_rest_cache_version', 1 );
return Cache::get_content_version();
}

/**
Expand All @@ -1122,8 +1157,7 @@ private function get_cache_version() {
* @return void
*/
public function bump_cache_version() {
$version = $this->get_cache_version();
update_option( 'wzkb_rest_cache_version', $version + 1, false );
Cache::bump_content_version();
}

/**
Expand Down
24 changes: 24 additions & 0 deletions includes/util/class-cache.php
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,30 @@ public static function get_key( $attr ) {
return $meta_key;
}

/**
* Get the content version used to invalidate cached Knowledge Base output.
*
* Stored as an option so it persists without an object cache. It is bumped
* whenever an article is saved, changes status or is deleted, and when
* sections or products change.
*
* @since 3.2.1
*
* @return int Content version.
*/
public static function get_content_version(): int {
return (int) get_option( 'wzkb_rest_cache_version', 1 );
}

/**
* Increment the content version, invalidating cached Knowledge Base output.
*
* @since 3.2.1
*/
public static function bump_content_version(): void {
update_option( 'wzkb_rest_cache_version', self::get_content_version() + 1, false );
}

/**
* Get the timestamp meta key for a cache entry.
*
Expand Down
Loading
Loading