PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 14.2.2
Jetpack – WP Security, Backup, Speed, & Growth v14.2.2
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / related-posts / jetpack-related-posts.php
jetpack / modules / related-posts Last commit date
rtl 11 years ago class.related-posts-customize.php 2 years ago jetpack-related-posts.php 1 year ago readme.md 3 years ago related-posts-customizer.js 6 years ago related-posts-rtl.css 2 years ago related-posts.css 2 years ago related-posts.js 1 year ago
jetpack-related-posts.php
2153 lines
1 <?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * The Jetpack_RelatedPosts class.
4 *
5 * @package automattic/jetpack
6 */
7
8 use Automattic\Jetpack\Assets;
9 use Automattic\Jetpack\Blocks;
10 use Automattic\Jetpack\Sync\Settings;
11
12 /**
13 * The Jetpack_RelatedPosts class.
14 */
15 class Jetpack_RelatedPosts {
16 const VERSION = '20240116';
17 const SHORTCODE = 'jetpack-related-posts';
18
19 /**
20 * Instance of the class.
21 *
22 * @var Jetpack_RelatedPosts
23 */
24 private static $instance = null;
25
26 /**
27 * Instance of the raw class (?).
28 *
29 * @var Jetpack_RelatedPosts
30 */
31 private static $instance_raw = null;
32
33 /**
34 * Creates and returns a static instance of Jetpack_RelatedPosts.
35 *
36 * @return Jetpack_RelatedPosts
37 */
38 public static function init() {
39 if ( ! self::$instance ) {
40 if ( class_exists( 'WPCOM_RelatedPosts' ) && method_exists( 'WPCOM_RelatedPosts', 'init' ) ) {
41 self::$instance = WPCOM_RelatedPosts::init();
42 } else {
43 self::$instance = new Jetpack_RelatedPosts();
44 }
45 }
46
47 return self::$instance;
48 }
49
50 /**
51 * Creates and returns a static instance of Jetpack_RelatedPosts_Raw.
52 *
53 * @return Jetpack_RelatedPosts
54 */
55 public static function init_raw() {
56 if ( ! self::$instance_raw ) {
57 if ( class_exists( 'WPCOM_RelatedPosts' ) && method_exists( 'WPCOM_RelatedPosts', 'init_raw' ) ) {
58 self::$instance_raw = WPCOM_RelatedPosts::init_raw();
59 } else {
60 self::$instance_raw = new Jetpack_RelatedPosts_Raw();
61 }
62 }
63
64 return self::$instance_raw;
65 }
66
67 /**
68 * Options.
69 *
70 * @var array $options
71 */
72 protected $options;
73
74 /**
75 * Allow feature toggle variable.
76 *
77 * @var bool
78 */
79 protected $allow_feature_toggle;
80
81 /**
82 * Blog character set.
83 *
84 * @var mixed
85 */
86 protected $blog_charset;
87
88 /**
89 * Convert character set.
90 *
91 * @var bool
92 */
93 protected $convert_charset;
94
95 /**
96 * Previous Post ID
97 *
98 * @var int
99 */
100 protected $previous_post_id;
101
102 /**
103 * Shortcode usage.
104 *
105 * @var bool
106 */
107 protected $found_shortcode = false;
108
109 /**
110 * Constructor for Jetpack_RelatedPosts.
111 *
112 * @uses get_option, add_action, apply_filters
113 */
114 public function __construct() {
115 $this->blog_charset = get_option( 'blog_charset' );
116 $this->convert_charset = ( function_exists( 'iconv' ) && ! preg_match( '/^utf\-?8$/i', $this->blog_charset ) );
117 add_action( 'admin_init', array( $this, 'action_admin_init' ) );
118 add_action( 'wp', array( $this, 'action_frontend_init' ) );
119
120 if ( ! class_exists( 'Jetpack_Media_Summary' ) ) {
121 require_once JETPACK__PLUGIN_DIR . '_inc/lib/class.media-summary.php';
122 }
123
124 // Add Related Posts to the REST API Post response.
125 add_action( 'rest_api_init', array( $this, 'rest_register_related_posts' ) );
126 }
127
128 /**
129 * Get the blog ID.
130 *
131 * @return Object current blog id.
132 */
133 protected function get_blog_id() {
134 return Jetpack_Options::get_option( 'id' );
135 }
136
137 /**
138 * =================
139 * ACTIONS & FILTERS
140 * =================
141 */
142
143 /**
144 * Add a checkbox field to Settings > Reading for enabling related posts.
145 *
146 * @action admin_init
147 * @uses add_settings_field, __, register_setting, add_action
148 */
149 public function action_admin_init() {
150
151 // Add the setting field [jetpack_relatedposts] and place it in Settings > Reading.
152 add_settings_field( 'jetpack_relatedposts', '<span id="jetpack_relatedposts">' . __( 'Related posts', 'jetpack' ) . '</span>', array( $this, 'print_setting_html' ), 'reading' );
153 register_setting( 'reading', 'jetpack_relatedposts', array( $this, 'parse_options' ) );
154 add_action( 'admin_head', array( $this, 'print_setting_head' ) );
155
156 if ( 'options-reading.php' === $GLOBALS['pagenow'] ) {
157 // Enqueue style for live preview on the reading settings page.
158 $this->enqueue_assets( false, true );
159 }
160 }
161
162 /**
163 * Load related posts assets if it's an eligible front end page or execute search and return JSON if it's an endpoint request.
164 *
165 * @global $_GET
166 * @action wp
167 * @uses add_shortcode, get_the_ID
168 */
169 public function action_frontend_init() {
170 // Add a shortcode handler that outputs nothing, this gets overridden later if we can display related content.
171 add_shortcode( self::SHORTCODE, array( $this, 'get_client_rendered_html_unsupported' ) );
172
173 if ( ! $this->enabled_for_request() ) {
174 return;
175 }
176
177 if ( isset( $_GET['relatedposts'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Reading and checking if we need to generate a list of excuded posts, does not update anything on the site.
178 $excludes = $this->parse_numeric_get_arg( 'relatedposts_exclude' );
179 $this->action_frontend_init_ajax( $excludes );
180 } else {
181 if ( isset( $_GET['relatedposts_hit'] ) && isset( $_GET['relatedposts_origin'] ) && isset( $_GET['relatedposts_position'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- checking if fields are set to setup tracking, nothing is changing on the site.
182 $this->previous_post_id = (int) $_GET['relatedposts_origin']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- fetching a previous post ID for tracking, nothing is changing on the site.
183 $this->log_click( $this->previous_post_id, get_the_ID(), sanitize_text_field( wp_unslash( $_GET['relatedposts_position'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- logging the click for tracking, nothing is changing on the site.
184 }
185
186 $this->action_frontend_init_page();
187 }
188 }
189
190 /**
191 * Render insertion point.
192 *
193 * @since 4.2.0
194 *
195 * @return string
196 */
197 public function get_headline() {
198 $options = $this->get_options();
199
200 if ( $options['show_headline'] ) {
201 $headline = sprintf(
202 /** This filter is already documented in modules/sharedaddy/sharing-service.php */
203 apply_filters( 'jetpack_sharing_headline_html', '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>', esc_html( $options['headline'] ), 'related-posts' ),
204 esc_html( $options['headline'] )
205 );
206 } else {
207 $headline = '';
208 }
209 return $headline;
210 }
211
212 /**
213 * Adds a target to the post content to load related posts into if a shortcode for it did not already exist.
214 * Will skip adding the target if the post content contains a Related Posts block, if the 'get_the_excerpt'
215 * hook is in the current filter list, or if the site is running an FSE/Site Editor theme.
216 *
217 * @filter the_content
218 *
219 * @param string $content Post content.
220 *
221 * @return string
222 */
223 public function filter_add_target_to_dom( $content ) {
224 // Do not output related posts for ActivityPub requests.
225 if (
226 function_exists( '\Activitypub\is_activitypub_request' )
227 && \Activitypub\is_activitypub_request()
228 ) {
229 return $content;
230 }
231
232 if ( has_block( 'jetpack/related-posts' ) || Blocks::is_fse_theme() ) {
233 return $content;
234 }
235
236 if ( ! $this->found_shortcode && ! doing_filter( 'get_the_excerpt' ) ) {
237 if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
238 $content .= "\n" . $this->get_server_rendered_html();
239 } else {
240 $content .= "\n" . $this->get_client_rendered_html();
241 }
242 }
243
244 return $content;
245 }
246
247 /**
248 * Render static markup based on the Gutenberg block code
249 *
250 * @return string Rendered related posts HTML.
251 */
252 public function get_server_rendered_html() {
253 $rp_settings = $this->get_options();
254 $block_rp_settings = array(
255 'displayThumbnails' => $rp_settings['show_thumbnails'],
256 'showHeadline' => $rp_settings['show_headline'],
257 'displayDate' => isset( $rp_settings['show_date'] ) ? (bool) $rp_settings['show_date'] : true,
258 'displayContext' => isset( $rp_settings['show_context'] ) && $rp_settings['show_context'],
259 'postLayout' => isset( $rp_settings['layout'] ) ? $rp_settings['layout'] : 'grid',
260 'postsToShow' => isset( $rp_settings['size'] ) ? $rp_settings['size'] : 3,
261 /** This filter is already documented in modules/related-posts/jetpack-related-posts.php */
262 'headline' => apply_filters( 'jetpack_relatedposts_filter_headline', $this->get_headline() ),
263 'isServerRendered' => true,
264 );
265
266 return $this->render_block( $block_rp_settings, '' );
267 }
268
269 /**
270 * Looks for our shortcode on the unfiltered content, this has to execute early.
271 *
272 * @filter the_content
273 * @param string $content - content of the post.
274 * @uses has_shortcode
275 * @return string $content
276 */
277 public function test_for_shortcode( $content ) {
278 $this->found_shortcode = has_shortcode( $content, self::SHORTCODE );
279
280 return $content;
281 }
282
283 /**
284 * Returns the HTML for the related posts section.
285 *
286 * @uses esc_html__, apply_filters
287 * @return string
288 */
289 public function get_client_rendered_html() {
290 if ( Settings::is_syncing() ) {
291 return '';
292 }
293
294 /**
295 * Filter the Related Posts headline.
296 *
297 * @module related-posts
298 *
299 * @since 3.0.0
300 *
301 * @param string $headline Related Posts heading.
302 */
303 $headline = apply_filters( 'jetpack_relatedposts_filter_headline', $this->get_headline() );
304
305 if ( $this->previous_post_id ) {
306 $exclude = "data-exclude='{$this->previous_post_id}'";
307 } else {
308 $exclude = '';
309 }
310
311 return <<<EOT
312 <div id='jp-relatedposts' class='jp-relatedposts' $exclude>
313 $headline
314 </div>
315 EOT;
316 }
317
318 /**
319 * Returns the HTML for the related posts section if it's running in the loop or other instances where we don't support related posts.
320 *
321 * @return string
322 */
323 public function get_client_rendered_html_unsupported() {
324 if ( Settings::is_syncing() ) {
325 return '';
326 }
327 return "\n\n<!-- Jetpack Related Posts is not supported in this context. -->\n\n";
328 }
329
330 /**
331 * ===============
332 * GUTENBERG BLOCK
333 * ===============
334 */
335
336 /**
337 * Echoes out items for the Gutenberg block
338 *
339 * @param array $related_post The post object.
340 * @param array $block_attributes The block attributes.
341 */
342 public function render_block_item( $related_post, $block_attributes ) {
343 $instance_id = 'related-posts-item-' . uniqid();
344 $label_id = $instance_id . '-label';
345 $title = $related_post['title'];
346 $url = $related_post['url'];
347 $rel = $related_post['rel'];
348 $img = '';
349 $list = '';
350
351 $item_markup = sprintf(
352 '<li id="%1$s" class="jp-related-posts-i2__post">',
353 esc_attr( $instance_id )
354 );
355
356 // Thumbnail
357 if ( ! empty( $block_attributes['show_thumbnails'] ) && ! empty( $related_post['img']['src'] ) ) {
358 $img = sprintf(
359 '<img loading="lazy" class="jp-related-posts-i2__post-img" src="%1$s" alt="%2$s" %3$s/>',
360 esc_url( $related_post['img']['src'] ),
361 esc_attr( $related_post['img']['alt_text'] ),
362 ( ! empty( $related_post['img']['srcset'] ) ? 'srcset="' . esc_attr( $related_post['img']['srcset'] ) . '"' : '' )
363 );
364 }
365
366 // Link
367 $item_markup .= sprintf(
368 '<a id="%1$s" href="%2$s" class="jp-related-posts-i2__post-link" %3$s>%4$s%5$s</a>',
369 esc_attr( $label_id ),
370 esc_url( $url ),
371 ( ! empty( $rel ) ? 'rel="' . esc_attr( $rel ) . '"' : '' ),
372 esc_html( $title ),
373 $img
374 );
375
376 // Date
377 if ( $block_attributes['show_date'] ) {
378 $list .= '<dt>' . __( 'Date', 'jetpack' ) . '</dt>';
379 $list .= '<dd class="jp-related-posts-i2__post-date">';
380 $list .= esc_html( $related_post['date'] );
381 $list .= '</dd>';
382 }
383
384 // Author
385 if ( $block_attributes['show_author'] ) {
386 $list .= '<dt>' . __( 'Author', 'jetpack' ) . '</dt>';
387 $list .= '<dd class="jp-related-posts-i2__post-author">';
388 $list .= esc_html( $related_post['author'] );
389 $list .= '</dd>';
390 }
391
392 // Context
393 if ( ( $block_attributes['show_context'] ) && ! empty( $related_post['block_context'] ) ) {
394 // translators: this is followed by the reason why the item is related to the current post
395 $list .= '<dt>' . __( 'In relation to', 'jetpack' ) . '</dt>';
396 $list .= '<dd class="jp-related-posts-i2__post-context">';
397
398 // Note: The original 'context' value is not used when rendering the block.
399 // It is still generated and available for the legacy rendering code path though.
400 // See './related-posts.js' for that usage.
401 $block_context = $related_post['block_context'];
402
403 if ( ! empty( $block_context['link'] ) ) {
404 $list .= sprintf(
405 '<a href="%1$s">%2$s</a>',
406 esc_url( $block_context['link'] ),
407 esc_html( $block_context['text'] )
408 );
409 } else {
410 $list .= esc_html( $block_context['text'] );
411 }
412
413 $list .= '</dd>';
414 }
415
416 // Metadata
417 if ( ! empty( $list ) ) {
418 $item_markup .= '<dl class="jp-related-posts-i2__post-defs">' . $list . '</dl>';
419 }
420
421 $item_markup .= '</li>';
422
423 return $item_markup;
424 }
425
426 /**
427 * Render the list of related posts.
428 *
429 * @param array $posts The posts to render into the list.
430 * @param array $block_attributes Block attributes.
431 * @return string
432 */
433 public function render_post_list( $posts, $block_attributes ) {
434 $markup = '';
435
436 foreach ( $posts as $post ) {
437 $markup .= $this->render_block_item( $post, $block_attributes );
438 }
439
440 return sprintf(
441 // role="list" is required for accessibility as VoiceOver ignores unstyled lists.
442 '<ul class="jp-related-posts-i2__list" role="list" data-post-count="%1$s">%2$s</ul>',
443 count( $posts ),
444 $markup
445 );
446 }
447
448 /**
449 * Render the related posts markup.
450 *
451 * @param array $attributes Block attributes.
452 * @param string $content String containing the related Posts block content.
453 * @param WP_Block $block The block object.
454 * @return string
455 */
456 public function render_block( $attributes, $content, $block = null ) {
457 if ( ! jetpack_is_frontend() ) {
458 return $content;
459 }
460
461 $wrapper_attributes = array();
462 $post_id = get_the_ID();
463 $block_attributes = array(
464 'headline' => isset( $attributes['headline'] ) ? $attributes['headline'] : null,
465 'show_thumbnails' => isset( $attributes['displayThumbnails'] ) && $attributes['displayThumbnails'],
466 'show_author' => isset( $attributes['displayAuthor'] ) ? (bool) $attributes['displayAuthor'] : false,
467 'show_headline' => isset( $attributes['displayHeadline'] ) ? (bool) $attributes['displayHeadline'] : false,
468 'show_date' => isset( $attributes['displayDate'] ) ? (bool) $attributes['displayDate'] : true,
469 'show_context' => isset( $attributes['displayContext'] ) && $attributes['displayContext'],
470 'layout' => isset( $attributes['postLayout'] ) && 'list' === $attributes['postLayout'] ? $attributes['postLayout'] : 'grid',
471 'size' => ! empty( $attributes['postsToShow'] ) ? absint( $attributes['postsToShow'] ) : 3,
472 );
473
474 $excludes = $this->parse_numeric_get_arg( 'relatedposts_origin' );
475
476 $related_posts = $this->get_for_post_id(
477 $post_id,
478 array(
479 'size' => $block_attributes['size'],
480 'exclude_post_ids' => $excludes,
481 )
482 );
483
484 if ( empty( $related_posts ) ) {
485 return '';
486 }
487
488 $list_markup = $this->render_post_list( $related_posts, $block_attributes );
489
490 if ( empty( $attributes['isServerRendered'] ) ) {
491 // The get_server_rendered_html() path won't register a block,
492 // so only apply block supports when not server rendered.
493 $wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports();
494 }
495
496 $headline_markup = '';
497
498 if ( isset( $block ) ) {
499 foreach ( $block->inner_blocks as $inner_block ) {
500 if ( 'core/heading' === $inner_block->name && ! empty( wp_strip_all_tags( $inner_block->inner_html ) ) ) {
501 $headline_markup = trim( $inner_block->inner_html );
502 break;
503 }
504 }
505 }
506
507 if ( empty( $headline_markup ) && $block_attributes['show_headline'] === true ) {
508 $headline = $block_attributes['headline'];
509 if ( strlen( trim( $headline ) ) !== 0 ) {
510 $headline_markup = sprintf(
511 '<h3 class="jp-relatedposts-headline">%1$s</h3>',
512 esc_html( $headline )
513 );
514 }
515 }
516
517 $display_markup = sprintf(
518 '<nav class="jp-relatedposts-i2%1$s"%2$s data-layout="%3$s" aria-label="%6$s">%4$s%5$s</nav>',
519 ! empty( $wrapper_attributes['class'] ) ? ' ' . esc_attr( $wrapper_attributes['class'] ) : '',
520 ! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : '',
521 esc_attr( $block_attributes['layout'] ),
522 $headline_markup,
523 $list_markup,
524 empty( $headline_markup ) ? esc_attr__( 'Related Posts', 'jetpack' ) : esc_attr( wp_strip_all_tags( $headline_markup ) )
525 );
526
527 /**
528 * Filter the output HTML of Related Posts.
529 *
530 * @module related-posts
531 *
532 * @since 10.7
533 *
534 * @param string $display_markup HTML output of Related Posts.
535 * @param int|false get_the_ID() Post ID of the post for which we are retrieving Related Posts.
536 * @param array $related_posts Array of related posts.
537 * @param array $block_attributes Array of Block attributes.
538 */
539 return (string) apply_filters( 'jetpack_related_posts_display_markup', $display_markup, $post_id, $related_posts, $block_attributes );
540 }
541
542 /**
543 * ========================
544 * PUBLIC UTILITY FUNCTIONS
545 * ========================
546 */
547
548 /**
549 * Parse a numeric GET variable to an array of values.
550 *
551 * @since 6.9.0
552 *
553 * @uses absint
554 *
555 * @param string $arg Name of the GET variable.
556 * @return array $result Parsed value(s)
557 */
558 public function parse_numeric_get_arg( $arg ) {
559 $result = array();
560
561 if ( isset( $_GET[ $arg ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- requests are used to generate a list of related posts we want to exclude.
562 if ( is_string( $_GET[ $arg ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
563 $result = explode( ',', sanitize_text_field( wp_unslash( $_GET[ $arg ] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
564 } elseif ( is_array( $_GET[ $arg ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
565 $args = array_map( 'sanitize_text_field', wp_unslash( $_GET[ $arg ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
566 $result = array_values( $args );
567 }
568
569 $result = array_unique( array_filter( array_map( 'absint', $result ) ) );
570 }
571
572 return $result;
573 }
574
575 /**
576 * Gets options set for Jetpack_RelatedPosts and merge with defaults.
577 *
578 * @uses Jetpack_Options::get_option, apply_filters
579 * @return array
580 */
581 public function get_options() {
582 if ( null === $this->options ) {
583 $this->options = Jetpack_Options::get_option( 'relatedposts', array() );
584 if ( ! is_array( $this->options ) ) {
585 $this->options = array();
586 }
587 if ( ! isset( $this->options['enabled'] ) ) {
588 $this->options['enabled'] = true;
589 }
590 if ( ! isset( $this->options['show_headline'] ) ) {
591 $this->options['show_headline'] = true;
592 }
593 if ( ! isset( $this->options['show_thumbnails'] ) ) {
594 $this->options['show_thumbnails'] = false;
595 }
596 if ( ! isset( $this->options['show_date'] ) ) {
597 $this->options['show_date'] = true;
598 }
599 if ( ! isset( $this->options['show_context'] ) ) {
600 $this->options['show_context'] = true;
601 }
602 if ( ! isset( $this->options['layout'] ) ) {
603 $this->options['layout'] = 'grid';
604 }
605 if ( ! isset( $this->options['headline'] ) ) {
606 $this->options['headline'] = esc_html__( 'Related', 'jetpack' );
607 }
608 if ( empty( $this->options['size'] ) || (int) $this->options['size'] < 1 ) {
609 $this->options['size'] = 3;
610 }
611
612 /**
613 * Filter Related Posts basic options.
614 *
615 * @module related-posts
616 *
617 * @since 2.8.0
618 *
619 * @param array $this->_options Array of basic Related Posts options.
620 */
621 $this->options = apply_filters( 'jetpack_relatedposts_filter_options', $this->options );
622 }
623
624 return $this->options;
625 }
626
627 /**
628 * Gets options.
629 *
630 * @param string $option_name - option we want to get.
631 */
632 public function get_option( $option_name ) {
633 $options = $this->get_options();
634
635 if ( isset( $options[ $option_name ] ) ) {
636 return $options[ $option_name ];
637 }
638
639 return false;
640 }
641
642 /**
643 * Parses input and returns normalized options array.
644 *
645 * @param array $input - input we're parsing.
646 * @uses self::get_options
647 * @return array
648 */
649 public function parse_options( $input ) {
650 $current = $this->get_options();
651
652 if ( ! is_array( $input ) ) {
653 $input = array();
654 }
655
656 if (
657 ! isset( $input['enabled'] )
658 || isset( $input['show_date'] )
659 || isset( $input['show_context'] )
660 || isset( $input['layout'] )
661 || isset( $input['headline'] )
662 ) {
663 $input['enabled'] = '1';
664 }
665
666 if ( '1' == $input['enabled'] ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- expecting string, but may return bools.
667 $current['enabled'] = true;
668 $current['show_headline'] = ( isset( $input['show_headline'] ) && '1' == $input['show_headline'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
669 $current['show_thumbnails'] = ( isset( $input['show_thumbnails'] ) && '1' == $input['show_thumbnails'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
670 $current['show_date'] = ( isset( $input['show_date'] ) && '1' == $input['show_date'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
671 $current['show_context'] = ( isset( $input['show_context'] ) && '1' == $input['show_context'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
672 $current['layout'] = isset( $input['layout'] ) && in_array( $input['layout'], array( 'grid', 'list' ), true ) ? $input['layout'] : 'grid';
673 $current['headline'] = isset( $input['headline'] ) ? $input['headline'] : esc_html__( 'Related', 'jetpack' );
674 } else {
675 $current['enabled'] = false;
676 }
677
678 if ( isset( $input['size'] ) && (int) $input['size'] > 0 ) {
679 $current['size'] = (int) $input['size'];
680 } else {
681 $current['size'] = null;
682 }
683 return $current;
684 }
685
686 /**
687 * HTML for admin settings page.
688 *
689 * @uses self::get_options, checked, esc_html__
690 */
691 public function print_setting_html() {
692 $options = $this->get_options();
693
694 $ui_settings_template = <<<EOT
695 <p class="description">%s</p>
696 <ul id="settings-reading-relatedposts-customize">
697 <li>
698 <label><input name="jetpack_relatedposts[show_headline]" type="checkbox" value="1" %s /> %s</label>
699 </li>
700 <li>
701 <label><input name="jetpack_relatedposts[show_thumbnails]" type="checkbox" value="1" %s /> %s</label>
702 </li>
703 <li>
704 <label><input name="jetpack_relatedposts[show_date]" type="checkbox" value="1" %s /> %s</label>
705 </li>
706 <li>
707 <label><input name="jetpack_relatedposts[show_context]" type="checkbox" value="1" %s /> %s</label>
708 </li>
709 </ul>
710 <div id='settings-reading-relatedposts-preview'>
711 %s
712 <div id="jp-relatedposts" class="jp-relatedposts"></div>
713 </div>
714 EOT;
715 $ui_settings = sprintf(
716 $ui_settings_template,
717 esc_html__( 'The following settings will impact all related posts on your site, except for those you created via the block editor:', 'jetpack' ),
718 checked( $options['show_headline'], true, false ),
719 esc_html__( 'Highlight related content with a heading', 'jetpack' ),
720 checked( $options['show_thumbnails'], true, false ),
721 esc_html__( 'Show a thumbnail image where available', 'jetpack' ),
722 checked( $options['show_date'], true, false ),
723 esc_html__( 'Show entry date', 'jetpack' ),
724 checked( $options['show_context'], true, false ),
725 esc_html__( 'Show context (category or tag)', 'jetpack' ),
726 esc_html__( 'Preview:', 'jetpack' )
727 );
728
729 if ( ! $this->allow_feature_toggle() ) {
730 $template = <<<EOT
731 <input type="hidden" name="jetpack_relatedposts[enabled]" value="1" />
732 %s
733 EOT;
734 printf(
735 $template, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
736 $ui_settings // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- data is escaped when variable is set.
737 );
738 } else {
739 $template = <<<EOT
740 <ul id="settings-reading-relatedposts">
741 <li>
742 <label><input type="radio" name="jetpack_relatedposts[enabled]" value="0" class="tog" %s /> %s</label>
743 </li>
744 <li>
745 <label><input type="radio" name="jetpack_relatedposts[enabled]" value="1" class="tog" %s /> %s</label>
746 %s
747 </li>
748 </ul>
749 EOT;
750 printf(
751 $template, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
752 checked( $options['enabled'], false, false ),
753 esc_html__( 'Hide related content after posts', 'jetpack' ),
754 checked( $options['enabled'], true, false ),
755 esc_html__( 'Show related content after posts', 'jetpack' ),
756 $ui_settings // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- data is escaped when variable is set.
757 );
758 }
759 }
760
761 /**
762 * Head JS/CSS for admin settings page.
763 *
764 * @uses esc_html__
765 * @return null
766 */
767 public function print_setting_head() {
768
769 // only dislay the Related Posts JavaScript on the Reading Settings Admin Page.
770 $current_screen = get_current_screen();
771
772 if ( $current_screen === null ) {
773 return;
774 }
775
776 if ( 'options-reading' !== $current_screen->id ) {
777 return;
778 }
779
780 $related_headline = sprintf(
781 '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>',
782 esc_html__( 'Related', 'jetpack' )
783 );
784
785 $href_params = 'class="jp-relatedposts-post-a" href="#jetpack_relatedposts" rel="nofollow" data-origin="0" data-position="0"';
786 $related_with_images = <<<EOT
787 <div class="jp-relatedposts-items jp-relatedposts-items-visual">
788 <div class="jp-relatedposts-post jp-relatedposts-post0 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
789 <a $href_params>
790 <img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2019/03/cat-blog.png" width="350" alt="Big iPhone/iPad Update Now Available" scale="0">
791 </a>
792 <h4 class="jp-relatedposts-post-title">
793 <a $href_params>Big iPhone/iPad Update Now Available</a>
794 </h4>
795 <p class="jp-relatedposts-post-excerpt">Big iPhone/iPad Update Now Available</p>
796 <p class="jp-relatedposts-post-context">In "Mobile"</p>
797 </div>
798 <div class="jp-relatedposts-post jp-relatedposts-post1 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
799 <a $href_params>
800 <img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2019/03/devices.jpg" width="350" alt="The WordPress for Android App Gets a Big Facelift" scale="0">
801 </a>
802 <h4 class="jp-relatedposts-post-title">
803 <a $href_params>The WordPress for Android App Gets a Big Facelift</a>
804 </h4>
805 <p class="jp-relatedposts-post-excerpt">The WordPress for Android App Gets a Big Facelift</p>
806 <p class="jp-relatedposts-post-context">In "Mobile"</p>
807 </div>
808 <div class="jp-relatedposts-post jp-relatedposts-post2 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
809 <a $href_params>
810 <img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg" width="350" alt="Upgrade Focus: VideoPress For Weddings" scale="0">
811 </a>
812 <h4 class="jp-relatedposts-post-title">
813 <a $href_params>Upgrade Focus: VideoPress For Weddings</a>
814 </h4>
815 <p class="jp-relatedposts-post-excerpt">Upgrade Focus: VideoPress For Weddings</p>
816 <p class="jp-relatedposts-post-context">In "Upgrade"</p>
817 </div>
818 </div>
819 EOT;
820 $related_with_images = str_replace( "\n", '', $related_with_images );
821 $related_without_images = <<<EOT
822 <div class="jp-relatedposts-items jp-relatedposts-items-minimal">
823 <p class="jp-relatedposts-post jp-relatedposts-post0" data-post-id="0" data-post-format="image">
824 <span class="jp-relatedposts-post-title"><a $href_params>Big iPhone/iPad Update Now Available</a></span>
825 <span class="jp-relatedposts-post-context">In "Mobile"</span>
826 </p>
827 <p class="jp-relatedposts-post jp-relatedposts-post1" data-post-id="0" data-post-format="image">
828 <span class="jp-relatedposts-post-title"><a $href_params>The WordPress for Android App Gets a Big Facelift</a></span>
829 <span class="jp-relatedposts-post-context">In "Mobile"</span>
830 </p>
831 <p class="jp-relatedposts-post jp-relatedposts-post2" data-post-id="0" data-post-format="image">
832 <span class="jp-relatedposts-post-title"><a $href_params>Upgrade Focus: VideoPress For Weddings</a></span>
833 <span class="jp-relatedposts-post-context">In "Upgrade"</span>
834 </p>
835 </div>
836 EOT;
837 $related_without_images = str_replace( "\n", '', $related_without_images );
838
839 if ( $this->allow_feature_toggle() ) {
840 $extra_css = '#settings-reading-relatedposts-customize { padding-left:2em; margin-top:.5em; }';
841 } else {
842 $extra_css = '';
843 }
844 // phpcs:disable WordPress.Security.EscapeOutput.HeredocOutputNotEscaped -- Escaped above where needed.
845 echo <<<EOT
846 <style type="text/css">
847 #settings-reading-relatedposts .disabled { opacity:.5; filter:Alpha(opacity=50); }
848 #settings-reading-relatedposts-preview .jp-relatedposts { background:#fff; padding:.5em; width:75%; }
849 $extra_css
850 </style>
851 <script type="text/javascript">
852 jQuery( document ).ready( function($) {
853 var update_ui = function() {
854 var is_enabled = true;
855 if ( 'radio' == $( 'input[name="jetpack_relatedposts[enabled]"]' ).attr('type') ) {
856 if ( '0' == $( 'input[name="jetpack_relatedposts[enabled]"]:checked' ).val() ) {
857 is_enabled = false;
858 }
859 }
860 if ( is_enabled ) {
861 $( '#settings-reading-relatedposts-customize' )
862 .removeClass( 'disabled' )
863 .find( 'input' )
864 .attr( 'disabled', false );
865 $( '#settings-reading-relatedposts-preview' )
866 .removeClass( 'disabled' );
867 } else {
868 $( '#settings-reading-relatedposts-customize' )
869 .addClass( 'disabled' )
870 .find( 'input' )
871 .attr( 'disabled', true );
872 $( '#settings-reading-relatedposts-preview' )
873 .addClass( 'disabled' );
874 }
875 };
876
877 var update_preview = function() {
878 var html = '';
879 if ( $( 'input[name="jetpack_relatedposts[show_headline]"]:checked' ).length ) {
880 html += '$related_headline';
881 }
882 if ( $( 'input[name="jetpack_relatedposts[show_thumbnails]"]:checked' ).length ) {
883 html += '$related_with_images';
884 } else {
885 html += '$related_without_images';
886 }
887 $( '#settings-reading-relatedposts-preview .jp-relatedposts' ).html( html );
888 if ( $( 'input[name="jetpack_relatedposts[show_date]"]:checked' ).length ) {
889 $( '.jp-relatedposts-post-title' ).each( function() {
890 $( this ).after( $( '<span>August 8, 2005</span>' ) );
891 } );
892 }
893 if ( $( 'input[name="jetpack_relatedposts[show_context]"]:checked' ).length ) {
894 $( '.jp-relatedposts-post-context' ).show();
895 } else {
896 $( '.jp-relatedposts-post-context' ).hide();
897 }
898 $( '#settings-reading-relatedposts-preview .jp-relatedposts' ).show();
899 };
900
901 // Update on load
902 update_preview();
903 update_ui();
904
905 // Update on change
906 $( '#settings-reading-relatedposts-customize input' )
907 .change( update_preview );
908 $( '#settings-reading-relatedposts' )
909 .find( 'input.tog' )
910 .change( update_ui );
911 });
912 </script>
913 EOT;
914 // phpcs:enable WordPress.Security.EscapeOutput.HeredocOutputNotEscaped
915 }
916
917 /**
918 * Gets an array of related posts that match the given post_id.
919 *
920 * @param int $post_id Post which we want to find related posts for.
921 * @param array $args - params to use when building Elasticsearch filters to narrow down the search domain.
922 * @uses self::get_options, get_post_type, wp_parse_args, apply_filters
923 * @return array
924 */
925 public function get_for_post_id( $post_id, array $args ) {
926 $options = $this->get_options();
927
928 if ( ! empty( $args['size'] ) ) {
929 $options['size'] = $args['size'];
930 }
931
932 if (
933 ! $options['enabled']
934 || 0 === (int) $post_id
935 || empty( $options['size'] )
936 ) {
937 return array();
938 }
939
940 $defaults = array(
941 'size' => (int) $options['size'],
942 'post_type' => get_post_type( $post_id ),
943 'post_formats' => array(),
944 'has_terms' => array(),
945 'date_range' => array(),
946 'exclude_post_ids' => array(),
947 );
948 $args = wp_parse_args( $args, $defaults );
949 /**
950 * Filter the arguments used to retrieve a list of Related Posts.
951 *
952 * @module related-posts
953 *
954 * @since 2.8.0
955 *
956 * @param array $args Array of options to retrieve Related Posts.
957 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
958 */
959 $args = apply_filters( 'jetpack_relatedposts_filter_args', $args, $post_id );
960
961 $filters = $this->get_es_filters_from_args( $post_id, $args );
962 /**
963 * Filter Elasticsearch options used to calculate Related Posts.
964 *
965 * @module related-posts
966 *
967 * @since 2.8.0
968 *
969 * @param array $filters Array of Elasticsearch filters based on the post_id and args.
970 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
971 */
972 $filters = apply_filters( 'jetpack_relatedposts_filter_filters', $filters, $post_id );
973
974 $results = $this->get_related_posts( $post_id, $args['size'], $filters );
975 /**
976 * Filter the array of related posts matched by Elasticsearch.
977 *
978 * @module related-posts
979 *
980 * @since 2.8.0
981 *
982 * @param array $results Array of related posts matched by Elasticsearch.
983 * @param int $post_id Post ID of the post for which we are retrieving Related Posts.
984 */
985 return apply_filters( 'jetpack_relatedposts_returned_results', $results, $post_id );
986 }
987
988 /**
989 * =========================
990 * PRIVATE UTILITY FUNCTIONS
991 * =========================
992 */
993
994 /**
995 * Creates an array of Elasticsearch filters based on the post_id and args.
996 *
997 * @param int $post_id - the post ID.
998 * @param array $args - the arguments.
999 * @uses apply_filters, get_post_types, get_post_format_strings
1000 * @return array
1001 */
1002 protected function get_es_filters_from_args( $post_id, array $args ) {
1003 $filters = array();
1004
1005 /**
1006 * Filter the terms used to search for Related Posts.
1007 *
1008 * @module related-posts
1009 *
1010 * @since 2.8.0
1011 *
1012 * @param array $args['has_terms'] Array of terms associated to the Related Posts.
1013 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1014 */
1015 $args['has_terms'] = apply_filters( 'jetpack_relatedposts_filter_has_terms', $args['has_terms'], $post_id );
1016 if ( ! empty( $args['has_terms'] ) ) {
1017 foreach ( (array) $args['has_terms'] as $term ) {
1018 if ( mb_strlen( $term->taxonomy ) ) {
1019 switch ( $term->taxonomy ) {
1020 case 'post_tag':
1021 $tax_fld = 'tag.slug';
1022 break;
1023 case 'category':
1024 $tax_fld = 'category.slug';
1025 break;
1026 default:
1027 $tax_fld = 'taxonomy.' . $term->taxonomy . '.slug';
1028 break;
1029 }
1030 $filters[] = array( 'term' => array( $tax_fld => $term->slug ) );
1031 }
1032 }
1033 }
1034
1035 /**
1036 * Filter the Post Types where we search Related Posts.
1037 *
1038 * @module related-posts
1039 *
1040 * @since 2.8.0
1041 *
1042 * @param array $args['post_type'] Array of Post Types.
1043 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1044 */
1045 $args['post_type'] = apply_filters( 'jetpack_relatedposts_filter_post_type', $args['post_type'], $post_id );
1046 $valid_post_types = get_post_types();
1047 if ( is_array( $args['post_type'] ) ) {
1048 $sanitized_post_types = array();
1049 foreach ( $args['post_type'] as $pt ) {
1050 if ( in_array( $pt, $valid_post_types, true ) ) {
1051 $sanitized_post_types[] = $pt;
1052 }
1053 }
1054 if ( ! empty( $sanitized_post_types ) ) {
1055 $filters[] = array( 'terms' => array( 'post_type' => $sanitized_post_types ) );
1056 }
1057 } elseif ( in_array( $args['post_type'], $valid_post_types, true ) && 'all' !== $args['post_type'] ) {
1058 $filters[] = array( 'term' => array( 'post_type' => $args['post_type'] ) );
1059 }
1060
1061 /**
1062 * Filter the Post Formats where we search Related Posts.
1063 *
1064 * @module related-posts
1065 *
1066 * @since 3.3.0
1067 *
1068 * @param array $args['post_formats'] Array of Post Formats.
1069 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1070 */
1071 $args['post_formats'] = apply_filters( 'jetpack_relatedposts_filter_post_formats', $args['post_formats'], $post_id );
1072 $valid_post_formats = get_post_format_strings();
1073 $sanitized_post_formats = array();
1074 foreach ( $args['post_formats'] as $pf ) {
1075 if ( array_key_exists( $pf, $valid_post_formats ) ) {
1076 $sanitized_post_formats[] = $pf;
1077 }
1078 }
1079 if ( ! empty( $sanitized_post_formats ) ) {
1080 $filters[] = array( 'terms' => array( 'post_format' => $sanitized_post_formats ) );
1081 }
1082
1083 /**
1084 * Filter the date range used to search Related Posts.
1085 *
1086 * @module related-posts
1087 *
1088 * @since 2.8.0
1089 *
1090 * @param array $args['date_range'] Array of a month interval where we search Related Posts.
1091 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1092 */
1093 $args['date_range'] = apply_filters( 'jetpack_relatedposts_filter_date_range', $args['date_range'], $post_id );
1094 if ( is_array( $args['date_range'] ) && ! empty( $args['date_range'] ) ) {
1095 $args['date_range'] = array_map( 'intval', $args['date_range'] );
1096 if ( ! empty( $args['date_range']['from'] ) && ! empty( $args['date_range']['to'] ) ) {
1097 $filters[] = array(
1098 'range' => array(
1099 'date_gmt' => $this->get_coalesced_range( $args['date_range'] ),
1100 ),
1101 );
1102 }
1103 }
1104
1105 /**
1106 * Filter the Post IDs excluded from appearing in Related Posts.
1107 *
1108 * @module related-posts
1109 *
1110 * @since 2.9.0
1111 *
1112 * @param array $args['exclude_post_ids'] Array of Post IDs.
1113 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1114 */
1115 $args['exclude_post_ids'] = apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', $args['exclude_post_ids'], $post_id );
1116 if ( ! empty( $args['exclude_post_ids'] ) && is_array( $args['exclude_post_ids'] ) ) {
1117 $excluded_post_ids = array();
1118 foreach ( $args['exclude_post_ids'] as $exclude_post_id ) {
1119 $exclude_post_id = (int) $exclude_post_id;
1120 if ( $exclude_post_id > 0 ) {
1121 $excluded_post_ids[] = $exclude_post_id;
1122 }
1123 }
1124 $filters[] = array( 'not' => array( 'terms' => array( 'post_id' => $excluded_post_ids ) ) );
1125 }
1126
1127 return $filters;
1128 }
1129
1130 /**
1131 * Takes a range and coalesces it into a month interval bracketed by a time as determined by the blog_id to enhance caching.
1132 *
1133 * @todo Rewrite this function with proper date handling rather than `strtotime()` and `date()`.
1134 *
1135 * @param array $date_range - the date range.
1136 * @return array
1137 */
1138 protected function get_coalesced_range( array $date_range ) {
1139 $now = time();
1140 $coalesce_time = $this->get_blog_id() % 86400;
1141 $current_time = $now - strtotime( 'today', $now );
1142
1143 if ( $current_time < $coalesce_time && '01' === date( 'd', $now ) ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1144 // Move back 1 period.
1145 return array(
1146 'from' => date( 'Y-m-01', strtotime( '-1 month', $date_range['from'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1147 'to' => date( 'Y-m-01', $date_range['to'] ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1148 );
1149 } else {
1150 // Use current period.
1151 return array(
1152 'from' => date( 'Y-m-01', $date_range['from'] ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1153 'to' => date( 'Y-m-01', strtotime( '+1 month', $date_range['to'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1154 );
1155 }
1156 }
1157
1158 /**
1159 * Generate and output ajax response for related posts API call.
1160 * NOTE: Calls exit() to end all further processing after payload has been outputed.
1161 *
1162 * @param array $excludes array of post_ids to exclude.
1163 * @uses send_nosniff_header, self::get_for_post_id, get_the_ID
1164 * @return never
1165 */
1166 protected function action_frontend_init_ajax( array $excludes ) {
1167 define( 'DOING_AJAX', true );
1168
1169 header( 'Content-type: application/json; charset=utf-8' ); // JSON can only be UTF-8.
1170 send_nosniff_header();
1171
1172 $options = $this->get_options();
1173
1174 if ( isset( $_GET['jetpackrpcustomize'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- adds dummy content if we're in the customizer.
1175
1176 // If we're in the customizer, add dummy content.
1177 $date_now = current_time( get_option( 'date_format' ) );
1178 $related_posts = array(
1179 array(
1180 'id' => - 1,
1181 'url' => 'https://jetpackme.files.wordpress.com/2019/03/cat-blog.png',
1182 'url_meta' => array(
1183 'origin' => 0,
1184 'position' => 0,
1185 ),
1186 'title' => esc_html__( 'Big iPhone/iPad Update Now Available', 'jetpack' ),
1187 'date' => $date_now,
1188 'format' => false,
1189 'excerpt' => esc_html__( 'It is that time of the year when devices are shiny again.', 'jetpack' ),
1190 'rel' => 'nofollow',
1191 'context' => esc_html__( 'In "Mobile"', 'jetpack' ),
1192 'img' => array(
1193 'src' => 'https://jetpackme.files.wordpress.com/2019/03/cat-blog.png',
1194 'width' => 350,
1195 'height' => 200,
1196 ),
1197 'classes' => array(),
1198 ),
1199 array(
1200 'id' => - 1,
1201 'url' => 'https://jetpackme.files.wordpress.com/2019/03/devices.jpg',
1202 'url_meta' => array(
1203 'origin' => 0,
1204 'position' => 0,
1205 ),
1206 'title' => esc_html__( 'The WordPress for Android App Gets a Big Facelift', 'jetpack' ),
1207 'date' => $date_now,
1208 'format' => false,
1209 'excerpt' => esc_html__( 'Writing is new again in Android with the new WordPress app.', 'jetpack' ),
1210 'rel' => 'nofollow',
1211 'context' => esc_html__( 'In "Mobile"', 'jetpack' ),
1212 'img' => array(
1213 'src' => 'https://jetpackme.files.wordpress.com/2019/03/devices.jpg',
1214 'width' => 350,
1215 'height' => 200,
1216 ),
1217 'classes' => array(),
1218 ),
1219 array(
1220 'id' => - 1,
1221 'url' => 'https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg',
1222 'url_meta' => array(
1223 'origin' => 0,
1224 'position' => 0,
1225 ),
1226 'title' => esc_html__( 'Upgrade Focus, VideoPress for weddings', 'jetpack' ),
1227 'date' => $date_now,
1228 'format' => false,
1229 'excerpt' => esc_html__( 'Weddings are in the spotlight now with VideoPress for weddings.', 'jetpack' ),
1230 'rel' => 'nofollow',
1231 'context' => esc_html__( 'In "Mobile"', 'jetpack' ),
1232 'img' => array(
1233 'src' => 'https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg',
1234 'width' => 350,
1235 'height' => 200,
1236 ),
1237 'classes' => array(),
1238 ),
1239 );
1240
1241 for ( $total = 0; $total < $options['size'] - 3; $total++ ) {
1242 $related_posts[] = $related_posts[ $total ];
1243 }
1244
1245 $current_post = get_post();
1246
1247 // Exclude current post after filtering to make sure it's excluded and not lost during filtering.
1248 $excluded_posts = array_merge(
1249 /** This filter is already documented in modules/related-posts/jetpack-related-posts.php */
1250 apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', array() ),
1251 array( $current_post->ID )
1252 );
1253
1254 // Fetch posts with featured image.
1255 $with_post_thumbnails = get_posts(
1256 array(
1257 'posts_per_page' => $options['size'],
1258 'post__not_in' => $excluded_posts,
1259 'post_type' => $current_post->post_type,
1260 'meta_key' => '_thumbnail_id',
1261 'suppress_filters' => false,
1262 )
1263 );
1264
1265 // If we don't have enough, fetch posts without featured image.
1266 $count_post_with_thumbnails = is_countable( $with_post_thumbnails ) ? count( $with_post_thumbnails ) : 0;
1267 $more = $options['size'] - $count_post_with_thumbnails;
1268 if ( 0 < $more ) {
1269 $no_post_thumbnails = get_posts(
1270 array(
1271 'posts_per_page' => $more,
1272 'post__not_in' => $excluded_posts,
1273 'post_type' => $current_post->post_type,
1274 'meta_query' => array(
1275 array(
1276 'key' => '_thumbnail_id',
1277 'compare' => 'NOT EXISTS',
1278 ),
1279 ),
1280 'suppress_filters' => false,
1281 )
1282 );
1283 } else {
1284 $no_post_thumbnails = array();
1285 }
1286
1287 foreach ( array_merge( $with_post_thumbnails, $no_post_thumbnails ) as $index => $real_post ) {
1288 $related_posts[ $index ]['id'] = $real_post->ID;
1289 $related_posts[ $index ]['url'] = esc_url( get_permalink( $real_post ) );
1290 $related_posts[ $index ]['title'] = $this->to_utf8( $this->get_title( $real_post->post_title, $real_post->post_content, $real_post->ID ) );
1291 $related_posts[ $index ]['date'] = get_the_date( '', $real_post );
1292 $related_posts[ $index ]['excerpt'] = html_entity_decode( $this->to_utf8( $this->get_excerpt( $real_post->post_excerpt, $real_post->post_content ) ), ENT_QUOTES, 'UTF-8' );
1293 $related_posts[ $index ]['img'] = $this->generate_related_post_image_params( $real_post->ID );
1294 $related_posts[ $index ]['context'] = $this->generate_related_post_context( $real_post->ID );
1295 }
1296 } else {
1297 $related_posts = $this->get_for_post_id(
1298 get_the_ID(),
1299 array(
1300 'exclude_post_ids' => $excludes,
1301 )
1302 );
1303 }
1304
1305 $response = array(
1306 'version' => self::VERSION,
1307 'show_thumbnails' => (bool) $options['show_thumbnails'],
1308 'show_date' => (bool) $options['show_date'],
1309 'show_context' => (bool) $options['show_context'],
1310 'layout' => (string) $options['layout'],
1311 'headline' => (string) $options['headline'],
1312 'items' => array(),
1313 );
1314
1315 if ( count( $related_posts ) === $options['size'] ) {
1316 $response['items'] = $related_posts;
1317 }
1318
1319 echo wp_json_encode( $response );
1320
1321 exit();
1322 }
1323
1324 /**
1325 * Returns a UTF-8 encoded array of post information for the given post_id
1326 *
1327 * @param int $post_id - the post ID.
1328 * @param int $position - position of the post.
1329 * @param int $origin - The post id that this is related to.
1330 * @uses get_post, get_permalink, remove_query_arg, get_post_format, apply_filters
1331 * @return array
1332 */
1333 public function get_related_post_data_for_post( $post_id, $position, $origin ) {
1334 $post = get_post( $post_id );
1335 return array(
1336 'id' => $post->ID,
1337 'url' => get_permalink( $post->ID ),
1338 'url_meta' => array(
1339 'origin' => $origin,
1340 'position' => $position,
1341 ),
1342 'title' => $this->to_utf8( $this->get_title( $post->post_title, $post->post_content, $post->ID ) ),
1343 'author' => $this->generate_related_post_display_author( $post->ID ),
1344 'date' => get_the_date( '', $post->ID ),
1345 'format' => get_post_format( $post->ID ),
1346 'excerpt' => html_entity_decode( $this->to_utf8( $this->get_excerpt( $post->post_excerpt, $post->post_content ) ), ENT_QUOTES, 'UTF-8' ),
1347 /**
1348 * Filters the rel attribute for the Related Posts' links.
1349 *
1350 * @module related-posts
1351 *
1352 * @since 3.7.0
1353 * @since 7.9.0 - Change Default value to empty.
1354 *
1355 * @param string $link_rel Link rel attribute for Related Posts' link. Default is empty.
1356 * @param int $post->ID Post ID.
1357 */
1358 'rel' => apply_filters( 'jetpack_relatedposts_filter_post_link_rel', '', $post->ID ),
1359 /**
1360 * Filter the context displayed below each Related Post.
1361 *
1362 * This context is used when rendering the legacy 'widget' version of Related Posts.
1363 * It is not used when rendering the block-based version. See 'block_context' below for that.
1364 *
1365 * @module related-posts
1366 *
1367 * @since 3.0.0
1368 *
1369 * @param string $this->to_utf8( $this->generate_related_post_context( $post->ID ) ) Context displayed below each related post.
1370 * @param int $post_id Post ID of the post for which we are retrieving Related Posts.
1371 */
1372 'context' => apply_filters(
1373 'jetpack_relatedposts_filter_post_context',
1374 $this->to_utf8( $this->generate_related_post_context( $post->ID ) ),
1375 $post->ID
1376 ),
1377 // The context used when rendering as a Block. No filtering applied.
1378 'block_context' => $this->generate_related_post_context_block( $post->ID ),
1379 'img' => $this->generate_related_post_image_params( $post->ID ),
1380 /**
1381 * Filter the post css classes added on HTML markup.
1382 *
1383 * @module related-posts
1384 *
1385 * @since 3.8.0
1386 *
1387 * @param array array() CSS classes added on post HTML markup.
1388 * @param string $post_id Post ID.
1389 */
1390 'classes' => apply_filters(
1391 'jetpack_relatedposts_filter_post_css_classes',
1392 array(),
1393 $post->ID
1394 ),
1395 );
1396 }
1397
1398 /**
1399 * Returns either the title or a small excerpt to use as title for post.
1400 *
1401 * @uses strip_shortcodes, wp_trim_words, __, apply_filters
1402 *
1403 * @param string $post_title Post title.
1404 * @param string $post_content Post content.
1405 * @param int $post_id Post ID.
1406 *
1407 * @return string
1408 */
1409 protected function get_title( $post_title, $post_content, $post_id ) {
1410 if ( ! empty( $post_title ) ) {
1411 return wp_strip_all_tags(
1412 /** This filter is documented in core/src/wp-includes/post-template.php */
1413 apply_filters( 'the_title', $post_title, $post_id )
1414 );
1415 }
1416
1417 $post_title = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post_content ) ), 5, '…' );
1418 if ( ! empty( $post_title ) ) {
1419 return $post_title;
1420 }
1421
1422 return __( 'Untitled Post', 'jetpack' );
1423 }
1424
1425 /**
1426 * Returns a plain text post excerpt for title attribute of links.
1427 *
1428 * @param string $post_excerpt - the post excerpt.
1429 * @param string $post_content - the post content.
1430 * @uses strip_shortcodes, wp_strip_all_tags, wp_trim_words
1431 * @return string
1432 */
1433 protected function get_excerpt( $post_excerpt, $post_content ) {
1434 if ( empty( $post_excerpt ) ) {
1435 $excerpt = $post_content;
1436 } else {
1437 $excerpt = $post_excerpt;
1438 }
1439
1440 return wp_trim_words( wp_strip_all_tags( strip_shortcodes( $excerpt ) ), 50, '…' );
1441 }
1442
1443 /**
1444 * Generates the thumbnail image to be used for the post. Uses the
1445 * image as returned by Jetpack_PostImages::get_image()
1446 *
1447 * @param int $post_id - the post ID.
1448 * @uses self::get_options, apply_filters, Jetpack_PostImages::get_image, Jetpack_PostImages::fit_image_url
1449 * @return string
1450 */
1451 protected function generate_related_post_image_params( $post_id ) {
1452 $image_params = array(
1453 'alt_text' => '',
1454 'src' => '',
1455 'width' => 0,
1456 'height' => 0,
1457 );
1458
1459 /**
1460 * Filter the size of the Related Posts images.
1461 *
1462 * @module related-posts
1463 *
1464 * @since 2.8.0
1465 *
1466 * @param array array( 'width' => 350, 'height' => 200 ) Size of the images displayed below each Related Post.
1467 */
1468 $thumbnail_size = apply_filters(
1469 'jetpack_relatedposts_filter_thumbnail_size',
1470 array(
1471 'width' => 350,
1472 'height' => 200,
1473 )
1474 );
1475 if ( ! is_array( $thumbnail_size ) ) {
1476 $thumbnail_size = array(
1477 'width' => (int) $thumbnail_size,
1478 'height' => (int) $thumbnail_size,
1479 );
1480 }
1481
1482 // Try to get post image.
1483 if ( class_exists( 'Jetpack_PostImages' ) ) {
1484 $img_url = '';
1485 $post_image = Jetpack_PostImages::get_image(
1486 $post_id,
1487 $thumbnail_size
1488 );
1489
1490 if ( is_array( $post_image ) ) {
1491 $img_url = $post_image['src'];
1492 } elseif ( class_exists( 'Jetpack_Media_Summary' ) ) {
1493 $media = Jetpack_Media_Summary::get( $post_id );
1494
1495 if ( is_array( $media ) && ! empty( $media['image'] ) ) {
1496 $img_url = $media['image'];
1497 }
1498 }
1499
1500 if ( ! empty( $img_url ) ) {
1501 if ( ! empty( $post_image['alt_text'] ) ) {
1502 $image_params['alt_text'] = $post_image['alt_text'];
1503 } else {
1504 $image_params['alt_text'] = '';
1505 }
1506
1507 $thumbnail_width = 0;
1508 $thumbnail_height = 0;
1509
1510 if ( ! empty( $thumbnail_size['width'] ) ) {
1511 $thumbnail_width = $thumbnail_size['width'];
1512 $image_params['width'] = $thumbnail_width;
1513 }
1514
1515 if ( ! empty( $thumbnail_size['height'] ) ) {
1516 $thumbnail_height = $thumbnail_size['height'];
1517 $image_params['height'] = $thumbnail_height;
1518 }
1519
1520 $image_params['src'] = Jetpack_PostImages::fit_image_url(
1521 $img_url,
1522 $thumbnail_width,
1523 $thumbnail_height
1524 );
1525
1526 // Add a srcset to handle zoomed views and high-density screens.
1527 $srcset = Jetpack_PostImages::generate_cropped_srcset(
1528 $post_image,
1529 $thumbnail_width,
1530 $thumbnail_height
1531 );
1532 if ( ! empty( $srcset ) ) {
1533 $image_params['srcset'] = $srcset;
1534 }
1535 }
1536 }
1537
1538 return $image_params;
1539 }
1540
1541 /**
1542 * Returns the string UTF-8 encoded
1543 *
1544 * @param string $text - the text we want to convert.
1545 * @return string
1546 */
1547 protected function to_utf8( $text ) {
1548 if ( $this->convert_charset ) {
1549 return iconv( $this->blog_charset, 'UTF-8', $text );
1550 } else {
1551 return $text;
1552 }
1553 }
1554
1555 /**
1556 * =============================================
1557 * PROTECTED UTILITY FUNCTIONS EXTENDED BY WPCOM
1558 * =============================================
1559 */
1560
1561 /**
1562 * Workhorse method to return array of related posts matched by Elasticsearch.
1563 *
1564 * @param int $post_id - the ID of the post.
1565 * @param int $size - the size of the post.
1566 * @param array $filters - filters.
1567 * @uses wp_remote_post, is_wp_error, get_option, wp_remote_retrieve_body, get_post, add_query_arg, remove_query_arg, get_permalink, get_post_format, apply_filters
1568 * @return array
1569 */
1570 protected function get_related_posts( $post_id, $size, array $filters ) {
1571 $hits = $this->filter_non_public_posts(
1572 $this->get_related_post_ids(
1573 $post_id,
1574 $size,
1575 $filters
1576 )
1577 );
1578
1579 /**
1580 * Filter the Related Posts matched by Elasticsearch.
1581 *
1582 * @module related-posts
1583 *
1584 * @since 2.9.0
1585 *
1586 * @param array $hits Array of Post IDs matched by Elasticsearch.
1587 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1588 */
1589 $hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
1590
1591 $related_posts = array();
1592 foreach ( $hits as $i => $hit ) {
1593 $related_posts[] = $this->get_related_post_data_for_post( $hit['id'], $i, $post_id );
1594 }
1595 return $related_posts;
1596 }
1597
1598 /**
1599 * Get array of related posts matched by Elasticsearch.
1600 *
1601 * @param int $post_id - the post ID.
1602 * @param int $size - the size.
1603 * @param array $filters - some filters.
1604 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body, get_post_meta, update_post_meta
1605 * @return array
1606 */
1607 protected function get_related_post_ids( $post_id, $size, array $filters ) {
1608 $transient_name = null;
1609 $now_ts = time();
1610 $cache_meta_key = '_jetpack_related_posts_cache';
1611
1612 $body = array(
1613 'size' => (int) $size,
1614 );
1615
1616 if ( ! empty( $filters ) ) {
1617 $body['filter'] = array( 'and' => $filters );
1618 }
1619
1620 // Build cache key.
1621 $cache_key = md5( serialize( $body ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- this is used for caching.
1622
1623 // Load all cached values.
1624 if ( wp_using_ext_object_cache() ) {
1625 $transient_name = "{$cache_meta_key}_{$cache_key}_{$post_id}";
1626 $cache = get_transient( $transient_name );
1627 if ( false !== $cache ) {
1628 return $cache;
1629 }
1630 } else {
1631 $cache = get_post_meta( $post_id, $cache_meta_key, true );
1632
1633 if ( empty( $cache ) ) {
1634 $cache = array();
1635 }
1636
1637 // Cache is valid! Return cached value.
1638 if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) && $cache[ $cache_key ]['expires'] > $now_ts ) {
1639 return $cache[ $cache_key ]['payload'];
1640 }
1641 }
1642
1643 $user_agent = '';
1644 if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
1645 $user_agent = strtolower( filter_var( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) );
1646 }
1647
1648 $response = wp_remote_post(
1649 "https://public-api.wordpress.com/rest/v1/sites/{$this->get_blog_id()}/posts/$post_id/related/",
1650 array(
1651 'timeout' => 10,
1652 'user-agent' => "jetpack_related_posts, $user_agent",
1653 'sslverify' => true,
1654 'body' => $body,
1655 )
1656 );
1657
1658 // Oh no... return nothing don't cache errors. Also, don't cache HTTP 409 conflict responses.
1659 if ( is_wp_error( $response ) || WP_Http::CONFLICT === wp_remote_retrieve_response_code( $response ) ) {
1660 if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) ) {
1661 return $cache[ $cache_key ]['payload']; // return stale.
1662 } else {
1663 return array();
1664 }
1665 }
1666
1667 $results = json_decode( wp_remote_retrieve_body( $response ), true );
1668 $related_posts = array();
1669 if ( is_array( $results ) && ! empty( $results['hits'] ) ) {
1670 foreach ( $results['hits'] as $hit ) {
1671 $related_posts[] = array(
1672 'id' => $hit['fields']['post_id'],
1673 );
1674 }
1675 }
1676
1677 // An empty array might indicate no related posts or that posts
1678 // are not yet synced to WordPress.com, so we cache for only 1
1679 // minute in this case.
1680 if ( empty( $related_posts ) ) {
1681 $cache_ttl = 60;
1682 } else {
1683 $cache_ttl = 12 * HOUR_IN_SECONDS;
1684 }
1685
1686 // Update cache.
1687 if ( wp_using_ext_object_cache() ) {
1688 set_transient( $transient_name, $related_posts, $cache_ttl );
1689 } else {
1690 // Copy all valid cache values.
1691 $new_cache = array();
1692 foreach ( $cache as $k => $v ) {
1693 if ( is_array( $v ) && $v['expires'] > $now_ts ) {
1694 $new_cache[ $k ] = $v;
1695 }
1696 }
1697
1698 // Set new cache value.
1699 $cache_expires = $cache_ttl + $now_ts;
1700 $new_cache[ $cache_key ] = array(
1701 'expires' => $cache_expires,
1702 'payload' => $related_posts,
1703 );
1704 update_post_meta( $post_id, $cache_meta_key, $new_cache );
1705 }
1706
1707 return $related_posts;
1708 }
1709
1710 /**
1711 * Filter out any hits that are not public anymore.
1712 *
1713 * @param array $related_posts - the related posts.
1714 * @uses get_post_stati, get_post_status
1715 * @return array
1716 */
1717 protected function filter_non_public_posts( array $related_posts ) {
1718 $public_stati = get_post_stati( array( 'public' => true ) );
1719
1720 $filtered = array();
1721 foreach ( $related_posts as $hit ) {
1722 if ( in_array( get_post_status( $hit['id'] ), $public_stati, true ) ) {
1723 $filtered[] = $hit;
1724 }
1725 }
1726 return $filtered;
1727 }
1728
1729 /**
1730 * Generates the author byline for the related post.
1731 *
1732 * @param int $post_id - the post ID.
1733 * @uses get_post_field, get_the_author_meta
1734 * @return string
1735 */
1736 protected function generate_related_post_display_author( $post_id ) {
1737 $post_author = get_post_field( 'post_author', $post_id );
1738 $author_display_name = get_the_author_meta( 'display_name', $post_author );
1739 if ( ! empty( $author_display_name ) ) {
1740 return $author_display_name;
1741 }
1742 return '';
1743 }
1744
1745 /**
1746 * Generates a context for the related content (second line in related post output).
1747 * Order of importance:
1748 * - First category (Not 'Uncategorized')
1749 * - First post tag
1750 * - Number of comments
1751 *
1752 * @param int $post_id - the post ID.
1753 * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n
1754 * @return string
1755 */
1756 protected function generate_related_post_context_block( $post_id ) {
1757 $categories = get_the_category( $post_id );
1758 if ( is_array( $categories ) ) {
1759 foreach ( $categories as $category ) {
1760 $cat_link = get_category_link( $category );
1761 if ( 'uncategorized' !== $category->slug && '' !== trim( $category->name ) ) {
1762 return array(
1763 'text' => trim( $category->name ),
1764 'link' => $cat_link,
1765 );
1766 }
1767 }
1768 }
1769 $tags = get_the_terms( $post_id, 'post_tag' );
1770 if ( is_array( $tags ) ) {
1771 foreach ( $tags as $tag ) {
1772 $tag_link = get_tag_link( $tag );
1773 if ( '' !== trim( $tag->name ) ) {
1774 return array(
1775 'text' => trim( $tag->name ),
1776 'link' => $tag_link,
1777 );
1778 }
1779 }
1780 }
1781 $comment_count = get_comments_number( $post_id );
1782 if ( $comment_count > 0 ) {
1783 $comments_string = sprintf(
1784 // Translators: amount of comments.
1785 _n( 'With %s comment', 'With %s comments', $comment_count, 'jetpack' ),
1786 number_format_i18n( $comment_count )
1787 );
1788 $comments_link = get_comments_link( $post_id );
1789 return array(
1790 'text' => $comments_string,
1791 'link' => $comments_link,
1792 );
1793 }
1794 $fallback_string = __( 'Similar post', 'jetpack' );
1795 return array(
1796 'text' => $fallback_string,
1797 'link' => '',
1798 );
1799 }
1800
1801 /**
1802 * Generates a context for the related content (second line in related post output).
1803 * Order of importance:
1804 * - First category (Not 'Uncategorized')
1805 * - First post tag
1806 * - Number of comments
1807 *
1808 * @param int $post_id - the post ID.
1809 * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n
1810 * @return string
1811 */
1812 protected function generate_related_post_context( $post_id ) {
1813 $categories = get_the_category( $post_id );
1814 if ( is_array( $categories ) ) {
1815 foreach ( $categories as $category ) {
1816 if ( 'uncategorized' !== $category->slug && '' !== trim( $category->name ) ) {
1817 $post_cat_context = sprintf(
1818 // Translators: The category or tag name.
1819 esc_html_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1820 $category->name
1821 );
1822 /**
1823 * Filter the "In Category" line displayed in the post context below each Related Post.
1824 *
1825 * @module related-posts
1826 *
1827 * @since 3.2.0
1828 *
1829 * @param string $post_cat_context "In Category" line displayed in the post context below each Related Post.
1830 * @param array $category Array containing information about the category.
1831 */
1832 return apply_filters( 'jetpack_relatedposts_post_category_context', $post_cat_context, $category );
1833 }
1834 }
1835 }
1836
1837 $tags = get_the_terms( $post_id, 'post_tag' );
1838 if ( is_array( $tags ) ) {
1839 foreach ( $tags as $tag ) {
1840 if ( '' !== trim( $tag->name ) ) {
1841 $post_tag_context = sprintf(
1842 // Translators: the category or tag name.
1843 _x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1844 $tag->name
1845 );
1846 /**
1847 * Filter the "In Tag" line displayed in the post context below each Related Post.
1848 *
1849 * @module related-posts
1850 *
1851 * @since 3.2.0
1852 *
1853 * @param string $post_tag_context "In Tag" line displayed in the post context below each Related Post.
1854 * @param array $tag Array containing information about the tag.
1855 */
1856 return apply_filters( 'jetpack_relatedposts_post_tag_context', $post_tag_context, $tag );
1857 }
1858 }
1859 }
1860
1861 $comment_count = get_comments_number( $post_id );
1862 if ( $comment_count > 0 ) {
1863 return sprintf(
1864 // Translators: amount of comments.
1865 _n( 'With %s comment', 'With %s comments', $comment_count, 'jetpack' ),
1866 number_format_i18n( $comment_count )
1867 );
1868 }
1869
1870 return __( 'Similar post', 'jetpack' );
1871 }
1872
1873 /**
1874 * Logs clicks for clickthrough analysis and related result tuning.
1875 *
1876 * @param int $post_id - the post ID.
1877 * @param int $to_post_id - the to post ID.
1878 * @param int $link_position - the link position.
1879 */
1880 protected function log_click( $post_id, $to_post_id, $link_position ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1881 }
1882
1883 /**
1884 * Determines if the current post is able to use related posts.
1885 *
1886 * @since 14.0 Checks for singular instead of single to allow usage on non-posts CPTs in block themes.
1887 * @uses self::get_options, is_admin, is_singular, apply_filters
1888 * @return bool
1889 */
1890 protected function enabled_for_request() {
1891 /*
1892 * On block themes, allow usage on any singular view (post, page, CPT).
1893 * On classic themes, only allow usage on single posts by default.
1894 */
1895 $enabled_on_singular_views = wp_is_block_theme()
1896 ? is_singular()
1897 : is_single();
1898
1899 $enabled = $enabled_on_singular_views
1900 && ! is_attachment()
1901 && ! is_admin()
1902 && ! is_embed()
1903 && ( ! $this->allow_feature_toggle() || $this->get_option( 'enabled' ) );
1904
1905 /**
1906 * Filter the Enabled value to allow related posts to be selectively enabled/disabled.
1907 *
1908 * @module related-posts
1909 *
1910 * @since 3.3.0
1911 *
1912 * @param bool $enabled Should Related Posts be enabled on the current page.
1913 */
1914 return apply_filters( 'jetpack_relatedposts_filter_enabled_for_request', $enabled );
1915 }
1916
1917 /**
1918 * Adds filters.
1919 *
1920 * @uses self::enqueue_assets, self::setup_shortcode, add_filter
1921 */
1922 protected function action_frontend_init_page() {
1923 $this->enqueue_assets( true, true );
1924 $this->setup_shortcode();
1925
1926 add_filter( 'the_content', array( $this, 'filter_add_target_to_dom' ), 40 );
1927 }
1928
1929 /**
1930 * Determines if the scripts need be enqueued.
1931 *
1932 * @return bool
1933 */
1934 protected function requires_scripts() {
1935 return (
1936 ! ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) &&
1937 ! has_block( 'jetpack/related-posts' ) &&
1938 ! Blocks::is_fse_theme()
1939 );
1940 }
1941
1942 /**
1943 * Enqueues assets needed to do async loading of related posts.
1944 *
1945 * @param string $script - the script we're enqueing.
1946 * @param string $style - the style we're enqueing.
1947 *
1948 * @uses wp_enqueue_script, wp_enqueue_style, plugins_url
1949 */
1950 protected function enqueue_assets( $script, $style ) {
1951 $dependencies = is_customize_preview() ? array( 'customize-base' ) : array();
1952 // Do not enqueue scripts unless they are required.
1953 if ( $script && $this->requires_scripts() ) {
1954 wp_enqueue_script(
1955 'jetpack_related-posts',
1956 Assets::get_file_url_for_environment(
1957 '_inc/build/related-posts/related-posts.min.js',
1958 'modules/related-posts/related-posts.js'
1959 ),
1960 $dependencies,
1961 self::VERSION,
1962 false
1963 );
1964 $related_posts_js_options = array(
1965 /**
1966 * Filter each Related Post Heading structure.
1967 *
1968 * @since 4.0.0
1969 *
1970 * @param string $str Related Post Heading structure. Default to h4.
1971 */
1972 'post_heading' => apply_filters( 'jetpack_relatedposts_filter_post_heading', esc_attr( 'h4' ) ),
1973 );
1974 wp_localize_script( 'jetpack_related-posts', 'related_posts_js_options', $related_posts_js_options );
1975 }
1976 if ( $style ) {
1977 wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'related-posts.css', __FILE__ ), array(), self::VERSION );
1978 wp_style_add_data( 'jetpack_related-posts', 'rtl', 'replace' );
1979 add_action( 'amp_post_template_css', array( $this, 'render_amp_reader_mode_css' ) );
1980 }
1981 }
1982
1983 /**
1984 * Render AMP's reader mode CSS.
1985 */
1986 public function render_amp_reader_mode_css() {
1987 echo file_get_contents( __DIR__ . '/related-posts.css' ); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped, WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- this is loading a CSS file.
1988 }
1989
1990 /**
1991 * Sets up the shortcode processing.
1992 *
1993 * @uses add_filter, add_shortcode
1994 */
1995 protected function setup_shortcode() {
1996 add_filter( 'the_content', array( $this, 'test_for_shortcode' ), 0 );
1997
1998 add_shortcode( self::SHORTCODE, array( $this, 'get_client_rendered_html' ) );
1999 }
2000
2001 /**
2002 * Return status of related posts toggle.
2003 */
2004 protected function allow_feature_toggle() {
2005 if ( null === $this->allow_feature_toggle ) {
2006 /**
2007 * Filter the display of the Related Posts toggle in Settings > Reading.
2008 *
2009 * @module related-posts
2010 *
2011 * @since 2.8.0
2012 *
2013 * @param bool $allow_feature_toggle Display a feature toggle. Default to false.
2014 */
2015 $this->allow_feature_toggle = apply_filters( 'jetpack_relatedposts_filter_allow_feature_toggle', false );
2016 }
2017 return $this->allow_feature_toggle;
2018 }
2019
2020 /**
2021 * ===================================================
2022 * FUNCTIONS EXPOSING RELATED POSTS IN THE WP REST API
2023 * ===================================================
2024 */
2025
2026 /**
2027 * Add Related Posts to the REST API Post response.
2028 *
2029 * @since 4.4.0
2030 *
2031 * @action rest_api_init
2032 * @uses register_rest_field, self::rest_get_related_posts
2033 */
2034 public function rest_register_related_posts() {
2035 /** This filter is already documented in class.json-api-endpoints.php */
2036 $post_types = apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'revision' ) );
2037 foreach ( $post_types as $post_type ) {
2038 register_rest_field(
2039 $post_type,
2040 'jetpack-related-posts',
2041 array(
2042 'get_callback' => array( $this, 'rest_get_related_posts' ),
2043 'update_callback' => null,
2044 'schema' => null,
2045 )
2046 );
2047 }
2048 }
2049
2050 /**
2051 * Build an array of Related Posts.
2052 * By default returns cached results that are stored for up to 12 hours.
2053 *
2054 * @since 4.4.0
2055 *
2056 * @param array $object Details of current post.
2057 *
2058 * @uses self::get_for_post_id
2059 *
2060 * @return array
2061 */
2062 public function rest_get_related_posts( $object ) {
2063 if ( ! isset( $object['id'] ) ) {
2064 return array();
2065 }
2066
2067 // If the Related Posts option is turned off, don't get the related posts.
2068 $options = \Jetpack_Options::get_option( 'relatedposts', array() );
2069 if ( empty( $options['enabled'] ) || ! $options['enabled'] ) {
2070 return array();
2071 }
2072
2073 // If the current post don't contain a Related Posts block, don't get the related posts.
2074 if ( ! has_block( 'jetpack/related-posts' ) ) {
2075 return array();
2076 }
2077
2078 return $this->get_for_post_id( $object['id'], array( 'size' => 6 ) );
2079 }
2080 }
2081
2082 /**
2083 * The raw related posts class can be used by other plugins or themes
2084 * to get related content. This class wraps the existing RelatedPosts
2085 * logic thus we never want to add anything to the DOM or do anything
2086 * for event hooks. We will also not present any settings for this
2087 * class and keep it enabled as calls to this class are done
2088 * programmatically.
2089 */
2090 class Jetpack_RelatedPosts_Raw extends Jetpack_RelatedPosts { //phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace, Generic.Files.OneObjectStructurePerFile.MultipleFound
2091
2092 /**
2093 * The query name we want to look up.
2094 *
2095 * @var string
2096 */
2097 protected $query_name;
2098
2099 /**
2100 * Allows callers of this class to tag each query with a unique name for tracking purposes.
2101 *
2102 * @param string $name - the name of the query.
2103 * @return Jetpack_RelatedPosts_Raw
2104 */
2105 public function set_query_name( $name ) {
2106 $this->query_name = (string) $name;
2107 return $this;
2108 }
2109
2110 /**
2111 * Initialize admin.
2112 */
2113 public function action_admin_init() {}
2114
2115 /**
2116 * Initialize front end.
2117 */
2118 public function action_frontend_init() {}
2119
2120 /**
2121 * Get options.
2122 */
2123 public function get_options() {
2124 return array(
2125 'enabled' => true,
2126 );
2127 }
2128
2129 /**
2130 * Workhorse method to return array of related posts ids matched by Elasticsearch.
2131 *
2132 * @param int $post_id - the post ID.
2133 * @param int $size - size of the post.
2134 * @param array $filters - filters we're using.
2135 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body
2136 * @return array
2137 */
2138 protected function get_related_posts( $post_id, $size, array $filters ) {
2139 $hits = $this->filter_non_public_posts(
2140 $this->get_related_post_ids(
2141 $post_id,
2142 $size,
2143 $filters
2144 )
2145 );
2146
2147 /** This filter is already documented in modules/related-posts/related-posts.php */
2148 $hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
2149
2150 return $hits;
2151 }
2152 }
2153