PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 13.1.5
Jetpack – WP Security, Backup, Speed, & Growth v13.1.5
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 2 years 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 3 years ago
jetpack-related-posts.php
2131 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 * @returns 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 * @returns 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 * @return string
454 */
455 public function render_block( $attributes, $content ) {
456 if ( ! jetpack_is_frontend() ) {
457 return $content;
458 }
459
460 $wrapper_attributes = array();
461 $post_id = get_the_ID();
462 $block_attributes = array(
463 'headline' => isset( $attributes['headline'] ) ? $attributes['headline'] : null,
464 'show_thumbnails' => isset( $attributes['displayThumbnails'] ) && $attributes['displayThumbnails'],
465 'show_author' => isset( $attributes['displayAuthor'] ) ? (bool) $attributes['displayAuthor'] : false,
466 'show_headline' => isset( $attributes['displayHeadline'] ) ? (bool) $attributes['displayHeadline'] : false,
467 'show_date' => isset( $attributes['displayDate'] ) ? (bool) $attributes['displayDate'] : true,
468 'show_context' => isset( $attributes['displayContext'] ) && $attributes['displayContext'],
469 'layout' => isset( $attributes['postLayout'] ) && 'list' === $attributes['postLayout'] ? $attributes['postLayout'] : 'grid',
470 'size' => ! empty( $attributes['postsToShow'] ) ? absint( $attributes['postsToShow'] ) : 3,
471 );
472
473 $excludes = $this->parse_numeric_get_arg( 'relatedposts_origin' );
474
475 $related_posts = $this->get_for_post_id(
476 $post_id,
477 array(
478 'size' => $block_attributes['size'],
479 'exclude_post_ids' => $excludes,
480 )
481 );
482
483 if ( empty( $related_posts ) ) {
484 return '';
485 }
486
487 $list_markup = $this->render_post_list( $related_posts, $block_attributes );
488
489 if ( empty( $attributes['isServerRendered'] ) ) {
490 // The get_server_rendered_html() path won't register a block,
491 // so only apply block supports when not server rendered.
492 $wrapper_attributes = \WP_Block_Supports::get_instance()->apply_block_supports();
493 }
494
495 $headline_id = 'relatedposts-headline-' . uniqid();
496 $headline_markup = '';
497
498 if ( $block_attributes['show_headline'] === true ) {
499 $headline = $block_attributes['headline'];
500 if ( strlen( trim( $headline ) ) !== 0 ) {
501 $headline_markup = sprintf(
502 '<h3 id="%2$s" class="jp-relatedposts-headline">%1$s</h3>',
503 esc_html( $headline ),
504 esc_attr( $headline_id )
505 );
506 }
507 }
508 $display_markup = sprintf(
509 '<nav class="jp-relatedposts-i2%1$s"%2$s data-layout="%3$s" %6$s>%4$s%5$s</nav>',
510 ! empty( $wrapper_attributes['class'] ) ? ' ' . esc_attr( $wrapper_attributes['class'] ) : '',
511 ! empty( $wrapper_attributes['style'] ) ? ' style="' . esc_attr( $wrapper_attributes['style'] ) . '"' : '',
512 esc_attr( $block_attributes['layout'] ),
513 $headline_markup,
514 $list_markup,
515 empty( $headline_markup ) ? 'aria-label="' . esc_attr( __( 'Related Posts', 'jetpack' ) ) . '"' : 'aria-labelledby="' . esc_attr( $headline_id ) . '"'
516 );
517
518 /**
519 * Filter the output HTML of Related Posts.
520 *
521 * @module related-posts
522 *
523 * @since 10.7
524 *
525 * @param string $display_markup HTML output of Related Posts.
526 * @param int|false get_the_ID() Post ID of the post for which we are retrieving Related Posts.
527 * @param array $related_posts Array of related posts.
528 * @param array $block_attributes Array of Block attributes.
529 */
530 return (string) apply_filters( 'jetpack_related_posts_display_markup', $display_markup, $post_id, $related_posts, $block_attributes );
531 }
532
533 /**
534 * ========================
535 * PUBLIC UTILITY FUNCTIONS
536 * ========================
537 */
538
539 /**
540 * Parse a numeric GET variable to an array of values.
541 *
542 * @since 6.9.0
543 *
544 * @uses absint
545 *
546 * @param string $arg Name of the GET variable.
547 * @return array $result Parsed value(s)
548 */
549 public function parse_numeric_get_arg( $arg ) {
550 $result = array();
551
552 if ( isset( $_GET[ $arg ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- requests are used to generate a list of related posts we want to exclude.
553 if ( is_string( $_GET[ $arg ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
554 $result = explode( ',', sanitize_text_field( wp_unslash( $_GET[ $arg ] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
555 } elseif ( is_array( $_GET[ $arg ] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended
556 $args = array_map( 'sanitize_text_field', wp_unslash( $_GET[ $arg ] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
557 $result = array_values( $args );
558 }
559
560 $result = array_unique( array_filter( array_map( 'absint', $result ) ) );
561 }
562
563 return $result;
564 }
565
566 /**
567 * Gets options set for Jetpack_RelatedPosts and merge with defaults.
568 *
569 * @uses Jetpack_Options::get_option, apply_filters
570 * @return array
571 */
572 public function get_options() {
573 if ( null === $this->options ) {
574 $this->options = Jetpack_Options::get_option( 'relatedposts', array() );
575 if ( ! is_array( $this->options ) ) {
576 $this->options = array();
577 }
578 if ( ! isset( $this->options['enabled'] ) ) {
579 $this->options['enabled'] = true;
580 }
581 if ( ! isset( $this->options['show_headline'] ) ) {
582 $this->options['show_headline'] = true;
583 }
584 if ( ! isset( $this->options['show_thumbnails'] ) ) {
585 $this->options['show_thumbnails'] = false;
586 }
587 if ( ! isset( $this->options['show_date'] ) ) {
588 $this->options['show_date'] = true;
589 }
590 if ( ! isset( $this->options['show_context'] ) ) {
591 $this->options['show_context'] = true;
592 }
593 if ( ! isset( $this->options['layout'] ) ) {
594 $this->options['layout'] = 'grid';
595 }
596 if ( ! isset( $this->options['headline'] ) ) {
597 $this->options['headline'] = esc_html__( 'Related', 'jetpack' );
598 }
599 if ( empty( $this->options['size'] ) || (int) $this->options['size'] < 1 ) {
600 $this->options['size'] = 3;
601 }
602
603 /**
604 * Filter Related Posts basic options.
605 *
606 * @module related-posts
607 *
608 * @since 2.8.0
609 *
610 * @param array $this->_options Array of basic Related Posts options.
611 */
612 $this->options = apply_filters( 'jetpack_relatedposts_filter_options', $this->options );
613 }
614
615 return $this->options;
616 }
617
618 /**
619 * Gets options.
620 *
621 * @param string $option_name - option we want to get.
622 */
623 public function get_option( $option_name ) {
624 $options = $this->get_options();
625
626 if ( isset( $options[ $option_name ] ) ) {
627 return $options[ $option_name ];
628 }
629
630 return false;
631 }
632
633 /**
634 * Parses input and returns normalized options array.
635 *
636 * @param array $input - input we're parsing.
637 * @uses self::get_options
638 * @return array
639 */
640 public function parse_options( $input ) {
641 $current = $this->get_options();
642
643 if ( ! is_array( $input ) ) {
644 $input = array();
645 }
646
647 if (
648 ! isset( $input['enabled'] )
649 || isset( $input['show_date'] )
650 || isset( $input['show_context'] )
651 || isset( $input['layout'] )
652 || isset( $input['headline'] )
653 ) {
654 $input['enabled'] = '1';
655 }
656
657 if ( '1' == $input['enabled'] ) { // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual -- expecting string, but may return bools.
658 $current['enabled'] = true;
659 $current['show_headline'] = ( isset( $input['show_headline'] ) && '1' == $input['show_headline'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
660 $current['show_thumbnails'] = ( isset( $input['show_thumbnails'] ) && '1' == $input['show_thumbnails'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
661 $current['show_date'] = ( isset( $input['show_date'] ) && '1' == $input['show_date'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
662 $current['show_context'] = ( isset( $input['show_context'] ) && '1' == $input['show_context'] ); // phpcs:ignore Universal.Operators.StrictComparisons.LooseEqual
663 $current['layout'] = isset( $input['layout'] ) && in_array( $input['layout'], array( 'grid', 'list' ), true ) ? $input['layout'] : 'grid';
664 $current['headline'] = isset( $input['headline'] ) ? $input['headline'] : esc_html__( 'Related', 'jetpack' );
665 } else {
666 $current['enabled'] = false;
667 }
668
669 if ( isset( $input['size'] ) && (int) $input['size'] > 0 ) {
670 $current['size'] = (int) $input['size'];
671 } else {
672 $current['size'] = null;
673 }
674 return $current;
675 }
676
677 /**
678 * HTML for admin settings page.
679 *
680 * @uses self::get_options, checked, esc_html__
681 * @returns null
682 */
683 public function print_setting_html() {
684 $options = $this->get_options();
685
686 $ui_settings_template = <<<EOT
687 <p class="description">%s</p>
688 <ul id="settings-reading-relatedposts-customize">
689 <li>
690 <label><input name="jetpack_relatedposts[show_headline]" type="checkbox" value="1" %s /> %s</label>
691 </li>
692 <li>
693 <label><input name="jetpack_relatedposts[show_thumbnails]" type="checkbox" value="1" %s /> %s</label>
694 </li>
695 <li>
696 <label><input name="jetpack_relatedposts[show_date]" type="checkbox" value="1" %s /> %s</label>
697 </li>
698 <li>
699 <label><input name="jetpack_relatedposts[show_context]" type="checkbox" value="1" %s /> %s</label>
700 </li>
701 </ul>
702 <div id='settings-reading-relatedposts-preview'>
703 %s
704 <div id="jp-relatedposts" class="jp-relatedposts"></div>
705 </div>
706 EOT;
707 $ui_settings = sprintf(
708 $ui_settings_template,
709 esc_html__( 'The following settings will impact all related posts on your site, except for those you created via the block editor:', 'jetpack' ),
710 checked( $options['show_headline'], true, false ),
711 esc_html__( 'Highlight related content with a heading', 'jetpack' ),
712 checked( $options['show_thumbnails'], true, false ),
713 esc_html__( 'Show a thumbnail image where available', 'jetpack' ),
714 checked( $options['show_date'], true, false ),
715 esc_html__( 'Show entry date', 'jetpack' ),
716 checked( $options['show_context'], true, false ),
717 esc_html__( 'Show context (category or tag)', 'jetpack' ),
718 esc_html__( 'Preview:', 'jetpack' )
719 );
720
721 if ( ! $this->allow_feature_toggle() ) {
722 $template = <<<EOT
723 <input type="hidden" name="jetpack_relatedposts[enabled]" value="1" />
724 %s
725 EOT;
726 printf(
727 $template, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
728 $ui_settings // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- data is escaped when variable is set.
729 );
730 } else {
731 $template = <<<EOT
732 <ul id="settings-reading-relatedposts">
733 <li>
734 <label><input type="radio" name="jetpack_relatedposts[enabled]" value="0" class="tog" %s /> %s</label>
735 </li>
736 <li>
737 <label><input type="radio" name="jetpack_relatedposts[enabled]" value="1" class="tog" %s /> %s</label>
738 %s
739 </li>
740 </ul>
741 EOT;
742 printf(
743 $template, // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
744 checked( $options['enabled'], false, false ),
745 esc_html__( 'Hide related content after posts', 'jetpack' ),
746 checked( $options['enabled'], true, false ),
747 esc_html__( 'Show related content after posts', 'jetpack' ),
748 $ui_settings // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- data is escaped when variable is set.
749 );
750 }
751 }
752
753 /**
754 * Head JS/CSS for admin settings page.
755 *
756 * @uses esc_html__
757 * @returns null
758 */
759 public function print_setting_head() {
760
761 // only dislay the Related Posts JavaScript on the Reading Settings Admin Page.
762 $current_screen = get_current_screen();
763
764 if ( $current_screen === null ) {
765 return;
766 }
767
768 if ( 'options-reading' !== $current_screen->id ) {
769 return;
770 }
771
772 $related_headline = sprintf(
773 '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>',
774 esc_html__( 'Related', 'jetpack' )
775 );
776
777 $href_params = 'class="jp-relatedposts-post-a" href="#jetpack_relatedposts" rel="nofollow" data-origin="0" data-position="0"';
778 $related_with_images = <<<EOT
779 <div class="jp-relatedposts-items jp-relatedposts-items-visual">
780 <div class="jp-relatedposts-post jp-relatedposts-post0 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
781 <a $href_params>
782 <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">
783 </a>
784 <h4 class="jp-relatedposts-post-title">
785 <a $href_params>Big iPhone/iPad Update Now Available</a>
786 </h4>
787 <p class="jp-relatedposts-post-excerpt">Big iPhone/iPad Update Now Available</p>
788 <p class="jp-relatedposts-post-context">In "Mobile"</p>
789 </div>
790 <div class="jp-relatedposts-post jp-relatedposts-post1 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
791 <a $href_params>
792 <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">
793 </a>
794 <h4 class="jp-relatedposts-post-title">
795 <a $href_params>The WordPress for Android App Gets a Big Facelift</a>
796 </h4>
797 <p class="jp-relatedposts-post-excerpt">The WordPress for Android App Gets a Big Facelift</p>
798 <p class="jp-relatedposts-post-context">In "Mobile"</p>
799 </div>
800 <div class="jp-relatedposts-post jp-relatedposts-post2 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
801 <a $href_params>
802 <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">
803 </a>
804 <h4 class="jp-relatedposts-post-title">
805 <a $href_params>Upgrade Focus: VideoPress For Weddings</a>
806 </h4>
807 <p class="jp-relatedposts-post-excerpt">Upgrade Focus: VideoPress For Weddings</p>
808 <p class="jp-relatedposts-post-context">In "Upgrade"</p>
809 </div>
810 </div>
811 EOT;
812 $related_with_images = str_replace( "\n", '', $related_with_images );
813 $related_without_images = <<<EOT
814 <div class="jp-relatedposts-items jp-relatedposts-items-minimal">
815 <p class="jp-relatedposts-post jp-relatedposts-post0" data-post-id="0" data-post-format="image">
816 <span class="jp-relatedposts-post-title"><a $href_params>Big iPhone/iPad Update Now Available</a></span>
817 <span class="jp-relatedposts-post-context">In "Mobile"</span>
818 </p>
819 <p class="jp-relatedposts-post jp-relatedposts-post1" data-post-id="0" data-post-format="image">
820 <span class="jp-relatedposts-post-title"><a $href_params>The WordPress for Android App Gets a Big Facelift</a></span>
821 <span class="jp-relatedposts-post-context">In "Mobile"</span>
822 </p>
823 <p class="jp-relatedposts-post jp-relatedposts-post2" data-post-id="0" data-post-format="image">
824 <span class="jp-relatedposts-post-title"><a $href_params>Upgrade Focus: VideoPress For Weddings</a></span>
825 <span class="jp-relatedposts-post-context">In "Upgrade"</span>
826 </p>
827 </div>
828 EOT;
829 $related_without_images = str_replace( "\n", '', $related_without_images );
830
831 if ( $this->allow_feature_toggle() ) {
832 $extra_css = '#settings-reading-relatedposts-customize { padding-left:2em; margin-top:.5em; }';
833 } else {
834 $extra_css = '';
835 }
836 // phpcs:disable WordPress.Security.EscapeOutput.HeredocOutputNotEscaped -- Escaped above where needed.
837 echo <<<EOT
838 <style type="text/css">
839 #settings-reading-relatedposts .disabled { opacity:.5; filter:Alpha(opacity=50); }
840 #settings-reading-relatedposts-preview .jp-relatedposts { background:#fff; padding:.5em; width:75%; }
841 $extra_css
842 </style>
843 <script type="text/javascript">
844 jQuery( document ).ready( function($) {
845 var update_ui = function() {
846 var is_enabled = true;
847 if ( 'radio' == $( 'input[name="jetpack_relatedposts[enabled]"]' ).attr('type') ) {
848 if ( '0' == $( 'input[name="jetpack_relatedposts[enabled]"]:checked' ).val() ) {
849 is_enabled = false;
850 }
851 }
852 if ( is_enabled ) {
853 $( '#settings-reading-relatedposts-customize' )
854 .removeClass( 'disabled' )
855 .find( 'input' )
856 .attr( 'disabled', false );
857 $( '#settings-reading-relatedposts-preview' )
858 .removeClass( 'disabled' );
859 } else {
860 $( '#settings-reading-relatedposts-customize' )
861 .addClass( 'disabled' )
862 .find( 'input' )
863 .attr( 'disabled', true );
864 $( '#settings-reading-relatedposts-preview' )
865 .addClass( 'disabled' );
866 }
867 };
868
869 var update_preview = function() {
870 var html = '';
871 if ( $( 'input[name="jetpack_relatedposts[show_headline]"]:checked' ).length ) {
872 html += '$related_headline';
873 }
874 if ( $( 'input[name="jetpack_relatedposts[show_thumbnails]"]:checked' ).length ) {
875 html += '$related_with_images';
876 } else {
877 html += '$related_without_images';
878 }
879 $( '#settings-reading-relatedposts-preview .jp-relatedposts' ).html( html );
880 if ( $( 'input[name="jetpack_relatedposts[show_date]"]:checked' ).length ) {
881 $( '.jp-relatedposts-post-title' ).each( function() {
882 $( this ).after( $( '<span>August 8, 2005</span>' ) );
883 } );
884 }
885 if ( $( 'input[name="jetpack_relatedposts[show_context]"]:checked' ).length ) {
886 $( '.jp-relatedposts-post-context' ).show();
887 } else {
888 $( '.jp-relatedposts-post-context' ).hide();
889 }
890 $( '#settings-reading-relatedposts-preview .jp-relatedposts' ).show();
891 };
892
893 // Update on load
894 update_preview();
895 update_ui();
896
897 // Update on change
898 $( '#settings-reading-relatedposts-customize input' )
899 .change( update_preview );
900 $( '#settings-reading-relatedposts' )
901 .find( 'input.tog' )
902 .change( update_ui );
903 });
904 </script>
905 EOT;
906 // phpcs:enable WordPress.Security.EscapeOutput.HeredocOutputNotEscaped
907 }
908
909 /**
910 * Gets an array of related posts that match the given post_id.
911 *
912 * @param int $post_id Post which we want to find related posts for.
913 * @param array $args - params to use when building Elasticsearch filters to narrow down the search domain.
914 * @uses self::get_options, get_post_type, wp_parse_args, apply_filters
915 * @return array
916 */
917 public function get_for_post_id( $post_id, array $args ) {
918 $options = $this->get_options();
919
920 if ( ! empty( $args['size'] ) ) {
921 $options['size'] = $args['size'];
922 }
923
924 if (
925 ! $options['enabled']
926 || 0 === (int) $post_id
927 || empty( $options['size'] )
928 ) {
929 return array();
930 }
931
932 $defaults = array(
933 'size' => (int) $options['size'],
934 'post_type' => get_post_type( $post_id ),
935 'post_formats' => array(),
936 'has_terms' => array(),
937 'date_range' => array(),
938 'exclude_post_ids' => array(),
939 );
940 $args = wp_parse_args( $args, $defaults );
941 /**
942 * Filter the arguments used to retrieve a list of Related Posts.
943 *
944 * @module related-posts
945 *
946 * @since 2.8.0
947 *
948 * @param array $args Array of options to retrieve Related Posts.
949 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
950 */
951 $args = apply_filters( 'jetpack_relatedposts_filter_args', $args, $post_id );
952
953 $filters = $this->get_es_filters_from_args( $post_id, $args );
954 /**
955 * Filter Elasticsearch options used to calculate Related Posts.
956 *
957 * @module related-posts
958 *
959 * @since 2.8.0
960 *
961 * @param array $filters Array of Elasticsearch filters based on the post_id and args.
962 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
963 */
964 $filters = apply_filters( 'jetpack_relatedposts_filter_filters', $filters, $post_id );
965
966 $results = $this->get_related_posts( $post_id, $args['size'], $filters );
967 /**
968 * Filter the array of related posts matched by Elasticsearch.
969 *
970 * @module related-posts
971 *
972 * @since 2.8.0
973 *
974 * @param array $results Array of related posts matched by Elasticsearch.
975 * @param int $post_id Post ID of the post for which we are retrieving Related Posts.
976 */
977 return apply_filters( 'jetpack_relatedposts_returned_results', $results, $post_id );
978 }
979
980 /**
981 * =========================
982 * PRIVATE UTILITY FUNCTIONS
983 * =========================
984 */
985
986 /**
987 * Creates an array of Elasticsearch filters based on the post_id and args.
988 *
989 * @param int $post_id - the post ID.
990 * @param array $args - the arguments.
991 * @uses apply_filters, get_post_types, get_post_format_strings
992 * @return array
993 */
994 protected function get_es_filters_from_args( $post_id, array $args ) {
995 $filters = array();
996
997 /**
998 * Filter the terms used to search for Related Posts.
999 *
1000 * @module related-posts
1001 *
1002 * @since 2.8.0
1003 *
1004 * @param array $args['has_terms'] Array of terms associated to the Related Posts.
1005 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1006 */
1007 $args['has_terms'] = apply_filters( 'jetpack_relatedposts_filter_has_terms', $args['has_terms'], $post_id );
1008 if ( ! empty( $args['has_terms'] ) ) {
1009 foreach ( (array) $args['has_terms'] as $term ) {
1010 if ( mb_strlen( $term->taxonomy ) ) {
1011 switch ( $term->taxonomy ) {
1012 case 'post_tag':
1013 $tax_fld = 'tag.slug';
1014 break;
1015 case 'category':
1016 $tax_fld = 'category.slug';
1017 break;
1018 default:
1019 $tax_fld = 'taxonomy.' . $term->taxonomy . '.slug';
1020 break;
1021 }
1022 $filters[] = array( 'term' => array( $tax_fld => $term->slug ) );
1023 }
1024 }
1025 }
1026
1027 /**
1028 * Filter the Post Types where we search Related Posts.
1029 *
1030 * @module related-posts
1031 *
1032 * @since 2.8.0
1033 *
1034 * @param array $args['post_type'] Array of Post Types.
1035 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1036 */
1037 $args['post_type'] = apply_filters( 'jetpack_relatedposts_filter_post_type', $args['post_type'], $post_id );
1038 $valid_post_types = get_post_types();
1039 if ( is_array( $args['post_type'] ) ) {
1040 $sanitized_post_types = array();
1041 foreach ( $args['post_type'] as $pt ) {
1042 if ( in_array( $pt, $valid_post_types, true ) ) {
1043 $sanitized_post_types[] = $pt;
1044 }
1045 }
1046 if ( ! empty( $sanitized_post_types ) ) {
1047 $filters[] = array( 'terms' => array( 'post_type' => $sanitized_post_types ) );
1048 }
1049 } elseif ( in_array( $args['post_type'], $valid_post_types, true ) && 'all' !== $args['post_type'] ) {
1050 $filters[] = array( 'term' => array( 'post_type' => $args['post_type'] ) );
1051 }
1052
1053 /**
1054 * Filter the Post Formats where we search Related Posts.
1055 *
1056 * @module related-posts
1057 *
1058 * @since 3.3.0
1059 *
1060 * @param array $args['post_formats'] Array of Post Formats.
1061 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1062 */
1063 $args['post_formats'] = apply_filters( 'jetpack_relatedposts_filter_post_formats', $args['post_formats'], $post_id );
1064 $valid_post_formats = get_post_format_strings();
1065 $sanitized_post_formats = array();
1066 foreach ( $args['post_formats'] as $pf ) {
1067 if ( array_key_exists( $pf, $valid_post_formats ) ) {
1068 $sanitized_post_formats[] = $pf;
1069 }
1070 }
1071 if ( ! empty( $sanitized_post_formats ) ) {
1072 $filters[] = array( 'terms' => array( 'post_format' => $sanitized_post_formats ) );
1073 }
1074
1075 /**
1076 * Filter the date range used to search Related Posts.
1077 *
1078 * @module related-posts
1079 *
1080 * @since 2.8.0
1081 *
1082 * @param array $args['date_range'] Array of a month interval where we search Related Posts.
1083 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1084 */
1085 $args['date_range'] = apply_filters( 'jetpack_relatedposts_filter_date_range', $args['date_range'], $post_id );
1086 if ( is_array( $args['date_range'] ) && ! empty( $args['date_range'] ) ) {
1087 $args['date_range'] = array_map( 'intval', $args['date_range'] );
1088 if ( ! empty( $args['date_range']['from'] ) && ! empty( $args['date_range']['to'] ) ) {
1089 $filters[] = array(
1090 'range' => array(
1091 'date_gmt' => $this->get_coalesced_range( $args['date_range'] ),
1092 ),
1093 );
1094 }
1095 }
1096
1097 /**
1098 * Filter the Post IDs excluded from appearing in Related Posts.
1099 *
1100 * @module related-posts
1101 *
1102 * @since 2.9.0
1103 *
1104 * @param array $args['exclude_post_ids'] Array of Post IDs.
1105 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1106 */
1107 $args['exclude_post_ids'] = apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', $args['exclude_post_ids'], $post_id );
1108 if ( ! empty( $args['exclude_post_ids'] ) && is_array( $args['exclude_post_ids'] ) ) {
1109 $excluded_post_ids = array();
1110 foreach ( $args['exclude_post_ids'] as $exclude_post_id ) {
1111 $exclude_post_id = (int) $exclude_post_id;
1112 if ( $exclude_post_id > 0 ) {
1113 $excluded_post_ids[] = $exclude_post_id;
1114 }
1115 }
1116 $filters[] = array( 'not' => array( 'terms' => array( 'post_id' => $excluded_post_ids ) ) );
1117 }
1118
1119 return $filters;
1120 }
1121
1122 /**
1123 * Takes a range and coalesces it into a month interval bracketed by a time as determined by the blog_id to enhance caching.
1124 *
1125 * @todo Rewrite this function with proper date handling rather than `strtotime()` and `date()`.
1126 *
1127 * @param array $date_range - the date range.
1128 * @return array
1129 */
1130 protected function get_coalesced_range( array $date_range ) {
1131 $now = time();
1132 $coalesce_time = $this->get_blog_id() % 86400;
1133 $current_time = $now - strtotime( 'today', $now );
1134
1135 if ( $current_time < $coalesce_time && '01' === date( 'd', $now ) ) { // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1136 // Move back 1 period.
1137 return array(
1138 'from' => date( 'Y-m-01', strtotime( '-1 month', $date_range['from'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1139 'to' => date( 'Y-m-01', $date_range['to'] ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1140 );
1141 } else {
1142 // Use current period.
1143 return array(
1144 'from' => date( 'Y-m-01', $date_range['from'] ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1145 'to' => date( 'Y-m-01', strtotime( '+1 month', $date_range['to'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ), //phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
1146 );
1147 }
1148 }
1149
1150 /**
1151 * Generate and output ajax response for related posts API call.
1152 * NOTE: Calls exit() to end all further processing after payload has been outputed.
1153 *
1154 * @param array $excludes array of post_ids to exclude.
1155 * @uses send_nosniff_header, self::get_for_post_id, get_the_ID
1156 */
1157 protected function action_frontend_init_ajax( array $excludes ) {
1158 define( 'DOING_AJAX', true );
1159
1160 header( 'Content-type: application/json; charset=utf-8' ); // JSON can only be UTF-8.
1161 send_nosniff_header();
1162
1163 $options = $this->get_options();
1164
1165 if ( isset( $_GET['jetpackrpcustomize'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- adds dummy content if we're in the customizer.
1166
1167 // If we're in the customizer, add dummy content.
1168 $date_now = current_time( get_option( 'date_format' ) );
1169 $related_posts = array(
1170 array(
1171 'id' => - 1,
1172 'url' => 'https://jetpackme.files.wordpress.com/2019/03/cat-blog.png',
1173 'url_meta' => array(
1174 'origin' => 0,
1175 'position' => 0,
1176 ),
1177 'title' => esc_html__( 'Big iPhone/iPad Update Now Available', 'jetpack' ),
1178 'date' => $date_now,
1179 'format' => false,
1180 'excerpt' => esc_html__( 'It is that time of the year when devices are shiny again.', 'jetpack' ),
1181 'rel' => 'nofollow',
1182 'context' => esc_html__( 'In "Mobile"', 'jetpack' ),
1183 'img' => array(
1184 'src' => 'https://jetpackme.files.wordpress.com/2019/03/cat-blog.png',
1185 'width' => 350,
1186 'height' => 200,
1187 ),
1188 'classes' => array(),
1189 ),
1190 array(
1191 'id' => - 1,
1192 'url' => 'https://jetpackme.files.wordpress.com/2019/03/devices.jpg',
1193 'url_meta' => array(
1194 'origin' => 0,
1195 'position' => 0,
1196 ),
1197 'title' => esc_html__( 'The WordPress for Android App Gets a Big Facelift', 'jetpack' ),
1198 'date' => $date_now,
1199 'format' => false,
1200 'excerpt' => esc_html__( 'Writing is new again in Android with the new WordPress app.', 'jetpack' ),
1201 'rel' => 'nofollow',
1202 'context' => esc_html__( 'In "Mobile"', 'jetpack' ),
1203 'img' => array(
1204 'src' => 'https://jetpackme.files.wordpress.com/2019/03/devices.jpg',
1205 'width' => 350,
1206 'height' => 200,
1207 ),
1208 'classes' => array(),
1209 ),
1210 array(
1211 'id' => - 1,
1212 'url' => 'https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg',
1213 'url_meta' => array(
1214 'origin' => 0,
1215 'position' => 0,
1216 ),
1217 'title' => esc_html__( 'Upgrade Focus, VideoPress for weddings', 'jetpack' ),
1218 'date' => $date_now,
1219 'format' => false,
1220 'excerpt' => esc_html__( 'Weddings are in the spotlight now with VideoPress for weddings.', 'jetpack' ),
1221 'rel' => 'nofollow',
1222 'context' => esc_html__( 'In "Mobile"', 'jetpack' ),
1223 'img' => array(
1224 'src' => 'https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg',
1225 'width' => 350,
1226 'height' => 200,
1227 ),
1228 'classes' => array(),
1229 ),
1230 );
1231
1232 for ( $total = 0; $total < $options['size'] - 3; $total++ ) {
1233 $related_posts[] = $related_posts[ $total ];
1234 }
1235
1236 $current_post = get_post();
1237
1238 // Exclude current post after filtering to make sure it's excluded and not lost during filtering.
1239 $excluded_posts = array_merge(
1240 /** This filter is already documented in modules/related-posts/jetpack-related-posts.php */
1241 apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', array() ),
1242 array( $current_post->ID )
1243 );
1244
1245 // Fetch posts with featured image.
1246 $with_post_thumbnails = get_posts(
1247 array(
1248 'posts_per_page' => $options['size'],
1249 'post__not_in' => $excluded_posts,
1250 'post_type' => $current_post->post_type,
1251 'meta_key' => '_thumbnail_id',
1252 'suppress_filters' => false,
1253 )
1254 );
1255
1256 // If we don't have enough, fetch posts without featured image.
1257 $count_post_with_thumbnails = is_countable( $with_post_thumbnails ) ? count( $with_post_thumbnails ) : 0;
1258 $more = $options['size'] - $count_post_with_thumbnails;
1259 if ( 0 < $more ) {
1260 $no_post_thumbnails = get_posts(
1261 array(
1262 'posts_per_page' => $more,
1263 'post__not_in' => $excluded_posts,
1264 'post_type' => $current_post->post_type,
1265 'meta_query' => array(
1266 array(
1267 'key' => '_thumbnail_id',
1268 'compare' => 'NOT EXISTS',
1269 ),
1270 ),
1271 'suppress_filters' => false,
1272 )
1273 );
1274 } else {
1275 $no_post_thumbnails = array();
1276 }
1277
1278 foreach ( array_merge( $with_post_thumbnails, $no_post_thumbnails ) as $index => $real_post ) {
1279 $related_posts[ $index ]['id'] = $real_post->ID;
1280 $related_posts[ $index ]['url'] = esc_url( get_permalink( $real_post ) );
1281 $related_posts[ $index ]['title'] = $this->to_utf8( $this->get_title( $real_post->post_title, $real_post->post_content, $real_post->ID ) );
1282 $related_posts[ $index ]['date'] = get_the_date( '', $real_post );
1283 $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' );
1284 $related_posts[ $index ]['img'] = $this->generate_related_post_image_params( $real_post->ID );
1285 $related_posts[ $index ]['context'] = $this->generate_related_post_context( $real_post->ID );
1286 }
1287 } else {
1288 $related_posts = $this->get_for_post_id(
1289 get_the_ID(),
1290 array(
1291 'exclude_post_ids' => $excludes,
1292 )
1293 );
1294 }
1295
1296 $response = array(
1297 'version' => self::VERSION,
1298 'show_thumbnails' => (bool) $options['show_thumbnails'],
1299 'show_date' => (bool) $options['show_date'],
1300 'show_context' => (bool) $options['show_context'],
1301 'layout' => (string) $options['layout'],
1302 'headline' => (string) $options['headline'],
1303 'items' => array(),
1304 );
1305
1306 if ( count( $related_posts ) === $options['size'] ) {
1307 $response['items'] = $related_posts;
1308 }
1309
1310 echo wp_json_encode( $response );
1311
1312 exit();
1313 }
1314
1315 /**
1316 * Returns a UTF-8 encoded array of post information for the given post_id
1317 *
1318 * @param int $post_id - the post ID.
1319 * @param int $position - position of the post.
1320 * @param int $origin - The post id that this is related to.
1321 * @uses get_post, get_permalink, remove_query_arg, get_post_format, apply_filters
1322 * @return array
1323 */
1324 public function get_related_post_data_for_post( $post_id, $position, $origin ) {
1325 $post = get_post( $post_id );
1326 return array(
1327 'id' => $post->ID,
1328 'url' => get_permalink( $post->ID ),
1329 'url_meta' => array(
1330 'origin' => $origin,
1331 'position' => $position,
1332 ),
1333 'title' => $this->to_utf8( $this->get_title( $post->post_title, $post->post_content, $post->ID ) ),
1334 'author' => $this->generate_related_post_display_author( $post->ID ),
1335 'date' => get_the_date( '', $post->ID ),
1336 'format' => get_post_format( $post->ID ),
1337 'excerpt' => html_entity_decode( $this->to_utf8( $this->get_excerpt( $post->post_excerpt, $post->post_content ) ), ENT_QUOTES, 'UTF-8' ),
1338 /**
1339 * Filters the rel attribute for the Related Posts' links.
1340 *
1341 * @module related-posts
1342 *
1343 * @since 3.7.0
1344 * @since 7.9.0 - Change Default value to empty.
1345 *
1346 * @param string $link_rel Link rel attribute for Related Posts' link. Default is empty.
1347 * @param int $post->ID Post ID.
1348 */
1349 'rel' => apply_filters( 'jetpack_relatedposts_filter_post_link_rel', '', $post->ID ),
1350 /**
1351 * Filter the context displayed below each Related Post.
1352 *
1353 * This context is used when rendering the legacy 'widget' version of Related Posts.
1354 * It is not used when rendering the block-based version. See 'block_context' below for that.
1355 *
1356 * @module related-posts
1357 *
1358 * @since 3.0.0
1359 *
1360 * @param string $this->to_utf8( $this->generate_related_post_context( $post->ID ) ) Context displayed below each related post.
1361 * @param int $post_id Post ID of the post for which we are retrieving Related Posts.
1362 */
1363 'context' => apply_filters(
1364 'jetpack_relatedposts_filter_post_context',
1365 $this->to_utf8( $this->generate_related_post_context( $post->ID ) ),
1366 $post->ID
1367 ),
1368 // The context used when rendering as a Block. No filtering applied.
1369 'block_context' => $this->generate_related_post_context_block( $post->ID ),
1370 'img' => $this->generate_related_post_image_params( $post->ID ),
1371 /**
1372 * Filter the post css classes added on HTML markup.
1373 *
1374 * @module related-posts
1375 *
1376 * @since 3.8.0
1377 *
1378 * @param array array() CSS classes added on post HTML markup.
1379 * @param string $post_id Post ID.
1380 */
1381 'classes' => apply_filters(
1382 'jetpack_relatedposts_filter_post_css_classes',
1383 array(),
1384 $post->ID
1385 ),
1386 );
1387 }
1388
1389 /**
1390 * Returns either the title or a small excerpt to use as title for post.
1391 *
1392 * @uses strip_shortcodes, wp_trim_words, __, apply_filters
1393 *
1394 * @param string $post_title Post title.
1395 * @param string $post_content Post content.
1396 * @param int $post_id Post ID.
1397 *
1398 * @return string
1399 */
1400 protected function get_title( $post_title, $post_content, $post_id ) {
1401 if ( ! empty( $post_title ) ) {
1402 return wp_strip_all_tags(
1403 /** This filter is documented in core/src/wp-includes/post-template.php */
1404 apply_filters( 'the_title', $post_title, $post_id )
1405 );
1406 }
1407
1408 $post_title = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post_content ) ), 5, '…' );
1409 if ( ! empty( $post_title ) ) {
1410 return $post_title;
1411 }
1412
1413 return __( 'Untitled Post', 'jetpack' );
1414 }
1415
1416 /**
1417 * Returns a plain text post excerpt for title attribute of links.
1418 *
1419 * @param string $post_excerpt - the post excerpt.
1420 * @param string $post_content - the post content.
1421 * @uses strip_shortcodes, wp_strip_all_tags, wp_trim_words
1422 * @return string
1423 */
1424 protected function get_excerpt( $post_excerpt, $post_content ) {
1425 if ( empty( $post_excerpt ) ) {
1426 $excerpt = $post_content;
1427 } else {
1428 $excerpt = $post_excerpt;
1429 }
1430
1431 return wp_trim_words( wp_strip_all_tags( strip_shortcodes( $excerpt ) ), 50, '…' );
1432 }
1433
1434 /**
1435 * Generates the thumbnail image to be used for the post. Uses the
1436 * image as returned by Jetpack_PostImages::get_image()
1437 *
1438 * @param int $post_id - the post ID.
1439 * @uses self::get_options, apply_filters, Jetpack_PostImages::get_image, Jetpack_PostImages::fit_image_url
1440 * @return string
1441 */
1442 protected function generate_related_post_image_params( $post_id ) {
1443 $image_params = array(
1444 'alt_text' => '',
1445 'src' => '',
1446 'width' => 0,
1447 'height' => 0,
1448 );
1449
1450 /**
1451 * Filter the size of the Related Posts images.
1452 *
1453 * @module related-posts
1454 *
1455 * @since 2.8.0
1456 *
1457 * @param array array( 'width' => 350, 'height' => 200 ) Size of the images displayed below each Related Post.
1458 */
1459 $thumbnail_size = apply_filters(
1460 'jetpack_relatedposts_filter_thumbnail_size',
1461 array(
1462 'width' => 350,
1463 'height' => 200,
1464 )
1465 );
1466 if ( ! is_array( $thumbnail_size ) ) {
1467 $thumbnail_size = array(
1468 'width' => (int) $thumbnail_size,
1469 'height' => (int) $thumbnail_size,
1470 );
1471 }
1472
1473 // Try to get post image.
1474 if ( class_exists( 'Jetpack_PostImages' ) ) {
1475 $img_url = '';
1476 $post_image = Jetpack_PostImages::get_image(
1477 $post_id,
1478 $thumbnail_size
1479 );
1480
1481 if ( is_array( $post_image ) ) {
1482 $img_url = $post_image['src'];
1483 } elseif ( class_exists( 'Jetpack_Media_Summary' ) ) {
1484 $media = Jetpack_Media_Summary::get( $post_id );
1485
1486 if ( is_array( $media ) && ! empty( $media['image'] ) ) {
1487 $img_url = $media['image'];
1488 }
1489 }
1490
1491 if ( ! empty( $img_url ) ) {
1492 if ( ! empty( $post_image['alt_text'] ) ) {
1493 $image_params['alt_text'] = $post_image['alt_text'];
1494 } else {
1495 $image_params['alt_text'] = '';
1496 }
1497
1498 $thumbnail_width = 0;
1499 $thumbnail_height = 0;
1500
1501 if ( ! empty( $thumbnail_size['width'] ) ) {
1502 $thumbnail_width = $thumbnail_size['width'];
1503 $image_params['width'] = $thumbnail_width;
1504 }
1505
1506 if ( ! empty( $thumbnail_size['height'] ) ) {
1507 $thumbnail_height = $thumbnail_size['height'];
1508 $image_params['height'] = $thumbnail_height;
1509 }
1510
1511 $image_params['src'] = Jetpack_PostImages::fit_image_url(
1512 $img_url,
1513 $thumbnail_width,
1514 $thumbnail_height
1515 );
1516
1517 // Add a srcset to handle zoomed views and high-density screens.
1518 $srcset = Jetpack_PostImages::generate_cropped_srcset(
1519 $post_image,
1520 $thumbnail_width,
1521 $thumbnail_height
1522 );
1523 if ( ! empty( $srcset ) ) {
1524 $image_params['srcset'] = $srcset;
1525 }
1526 }
1527 }
1528
1529 return $image_params;
1530 }
1531
1532 /**
1533 * Returns the string UTF-8 encoded
1534 *
1535 * @param string $text - the text we want to convert.
1536 * @return string
1537 */
1538 protected function to_utf8( $text ) {
1539 if ( $this->convert_charset ) {
1540 return iconv( $this->blog_charset, 'UTF-8', $text );
1541 } else {
1542 return $text;
1543 }
1544 }
1545
1546 /**
1547 * =============================================
1548 * PROTECTED UTILITY FUNCTIONS EXTENDED BY WPCOM
1549 * =============================================
1550 */
1551
1552 /**
1553 * Workhorse method to return array of related posts matched by Elasticsearch.
1554 *
1555 * @param int $post_id - the ID of the post.
1556 * @param int $size - the size of the post.
1557 * @param array $filters - filters.
1558 * @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
1559 * @return array
1560 */
1561 protected function get_related_posts( $post_id, $size, array $filters ) {
1562 $hits = $this->filter_non_public_posts(
1563 $this->get_related_post_ids(
1564 $post_id,
1565 $size,
1566 $filters
1567 )
1568 );
1569
1570 /**
1571 * Filter the Related Posts matched by Elasticsearch.
1572 *
1573 * @module related-posts
1574 *
1575 * @since 2.9.0
1576 *
1577 * @param array $hits Array of Post IDs matched by Elasticsearch.
1578 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1579 */
1580 $hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
1581
1582 $related_posts = array();
1583 foreach ( $hits as $i => $hit ) {
1584 $related_posts[] = $this->get_related_post_data_for_post( $hit['id'], $i, $post_id );
1585 }
1586 return $related_posts;
1587 }
1588
1589 /**
1590 * Get array of related posts matched by Elasticsearch.
1591 *
1592 * @param int $post_id - the post ID.
1593 * @param int $size - the size.
1594 * @param array $filters - some filters.
1595 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body, get_post_meta, update_post_meta
1596 * @return array
1597 */
1598 protected function get_related_post_ids( $post_id, $size, array $filters ) {
1599 $transient_name = null;
1600 $now_ts = time();
1601 $cache_meta_key = '_jetpack_related_posts_cache';
1602
1603 $body = array(
1604 'size' => (int) $size,
1605 );
1606
1607 if ( ! empty( $filters ) ) {
1608 $body['filter'] = array( 'and' => $filters );
1609 }
1610
1611 // Build cache key.
1612 $cache_key = md5( serialize( $body ) ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.serialize_serialize -- this is used for caching.
1613
1614 // Load all cached values.
1615 if ( wp_using_ext_object_cache() ) {
1616 $transient_name = "{$cache_meta_key}_{$cache_key}_{$post_id}";
1617 $cache = get_transient( $transient_name );
1618 if ( false !== $cache ) {
1619 return $cache;
1620 }
1621 } else {
1622 $cache = get_post_meta( $post_id, $cache_meta_key, true );
1623
1624 if ( empty( $cache ) ) {
1625 $cache = array();
1626 }
1627
1628 // Cache is valid! Return cached value.
1629 if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) && $cache[ $cache_key ]['expires'] > $now_ts ) {
1630 return $cache[ $cache_key ]['payload'];
1631 }
1632 }
1633
1634 $user_agent = '';
1635 if ( isset( $_SERVER['HTTP_USER_AGENT'] ) ) {
1636 $user_agent = strtolower( filter_var( wp_unslash( $_SERVER['HTTP_USER_AGENT'] ) ) );
1637 }
1638
1639 $response = wp_remote_post(
1640 "https://public-api.wordpress.com/rest/v1/sites/{$this->get_blog_id()}/posts/$post_id/related/",
1641 array(
1642 'timeout' => 10,
1643 'user-agent' => "jetpack_related_posts, $user_agent",
1644 'sslverify' => true,
1645 'body' => $body,
1646 )
1647 );
1648
1649 // Oh no... return nothing don't cache errors. Also, don't cache HTTP 409 conflict responses.
1650 if ( is_wp_error( $response ) || WP_Http::CONFLICT === wp_remote_retrieve_response_code( $response ) ) {
1651 if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) ) {
1652 return $cache[ $cache_key ]['payload']; // return stale.
1653 } else {
1654 return array();
1655 }
1656 }
1657
1658 $results = json_decode( wp_remote_retrieve_body( $response ), true );
1659 $related_posts = array();
1660 if ( is_array( $results ) && ! empty( $results['hits'] ) ) {
1661 foreach ( $results['hits'] as $hit ) {
1662 $related_posts[] = array(
1663 'id' => $hit['fields']['post_id'],
1664 );
1665 }
1666 }
1667
1668 // An empty array might indicate no related posts or that posts
1669 // are not yet synced to WordPress.com, so we cache for only 1
1670 // minute in this case.
1671 if ( empty( $related_posts ) ) {
1672 $cache_ttl = 60;
1673 } else {
1674 $cache_ttl = 12 * HOUR_IN_SECONDS;
1675 }
1676
1677 // Update cache.
1678 if ( wp_using_ext_object_cache() ) {
1679 set_transient( $transient_name, $related_posts, $cache_ttl );
1680 } else {
1681 // Copy all valid cache values.
1682 $new_cache = array();
1683 foreach ( $cache as $k => $v ) {
1684 if ( is_array( $v ) && $v['expires'] > $now_ts ) {
1685 $new_cache[ $k ] = $v;
1686 }
1687 }
1688
1689 // Set new cache value.
1690 $cache_expires = $cache_ttl + $now_ts;
1691 $new_cache[ $cache_key ] = array(
1692 'expires' => $cache_expires,
1693 'payload' => $related_posts,
1694 );
1695 update_post_meta( $post_id, $cache_meta_key, $new_cache );
1696 }
1697
1698 return $related_posts;
1699 }
1700
1701 /**
1702 * Filter out any hits that are not public anymore.
1703 *
1704 * @param array $related_posts - the related posts.
1705 * @uses get_post_stati, get_post_status
1706 * @return array
1707 */
1708 protected function filter_non_public_posts( array $related_posts ) {
1709 $public_stati = get_post_stati( array( 'public' => true ) );
1710
1711 $filtered = array();
1712 foreach ( $related_posts as $hit ) {
1713 if ( in_array( get_post_status( $hit['id'] ), $public_stati, true ) ) {
1714 $filtered[] = $hit;
1715 }
1716 }
1717 return $filtered;
1718 }
1719
1720 /**
1721 * Generates the author byline for the related post.
1722 *
1723 * @param int $post_id - the post ID.
1724 * @uses get_post_field, get_the_author_meta
1725 * @return string
1726 */
1727 protected function generate_related_post_display_author( $post_id ) {
1728 $post_author = get_post_field( 'post_author', $post_id );
1729 $author_display_name = get_the_author_meta( 'display_name', $post_author );
1730 if ( ! empty( $author_display_name ) ) {
1731 return $author_display_name;
1732 }
1733 return '';
1734 }
1735
1736 /**
1737 * Generates a context for the related content (second line in related post output).
1738 * Order of importance:
1739 * - First category (Not 'Uncategorized')
1740 * - First post tag
1741 * - Number of comments
1742 *
1743 * @param int $post_id - the post ID.
1744 * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n
1745 * @return string
1746 */
1747 protected function generate_related_post_context_block( $post_id ) {
1748 $categories = get_the_category( $post_id );
1749 if ( is_array( $categories ) ) {
1750 foreach ( $categories as $category ) {
1751 $cat_link = get_category_link( $category );
1752 if ( 'uncategorized' !== $category->slug && '' !== trim( $category->name ) ) {
1753 return array(
1754 'text' => trim( $category->name ),
1755 'link' => $cat_link,
1756 );
1757 }
1758 }
1759 }
1760 $tags = get_the_terms( $post_id, 'post_tag' );
1761 if ( is_array( $tags ) ) {
1762 foreach ( $tags as $tag ) {
1763 $tag_link = get_tag_link( $tag );
1764 if ( '' !== trim( $tag->name ) ) {
1765 return array(
1766 'text' => trim( $tag->name ),
1767 'link' => $tag_link,
1768 );
1769 }
1770 }
1771 }
1772 $comment_count = get_comments_number( $post_id );
1773 if ( $comment_count > 0 ) {
1774 $comments_string = sprintf(
1775 // Translators: amount of comments.
1776 _n( 'With %s comment', 'With %s comments', $comment_count, 'jetpack' ),
1777 number_format_i18n( $comment_count )
1778 );
1779 $comments_link = get_comments_link( $post_id );
1780 return array(
1781 'text' => $comments_string,
1782 'link' => $comments_link,
1783 );
1784 }
1785 $fallback_string = __( 'Similar post', 'jetpack' );
1786 return array(
1787 'text' => $fallback_string,
1788 'link' => '',
1789 );
1790 }
1791
1792 /**
1793 * Generates a context for the related content (second line in related post output).
1794 * Order of importance:
1795 * - First category (Not 'Uncategorized')
1796 * - First post tag
1797 * - Number of comments
1798 *
1799 * @param int $post_id - the post ID.
1800 * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n
1801 * @return string
1802 */
1803 protected function generate_related_post_context( $post_id ) {
1804 $categories = get_the_category( $post_id );
1805 if ( is_array( $categories ) ) {
1806 foreach ( $categories as $category ) {
1807 if ( 'uncategorized' !== $category->slug && '' !== trim( $category->name ) ) {
1808 $post_cat_context = sprintf(
1809 // Translators: The category or tag name.
1810 esc_html_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1811 $category->name
1812 );
1813 /**
1814 * Filter the "In Category" line displayed in the post context below each Related Post.
1815 *
1816 * @module related-posts
1817 *
1818 * @since 3.2.0
1819 *
1820 * @param string $post_cat_context "In Category" line displayed in the post context below each Related Post.
1821 * @param array $category Array containing information about the category.
1822 */
1823 return apply_filters( 'jetpack_relatedposts_post_category_context', $post_cat_context, $category );
1824 }
1825 }
1826 }
1827
1828 $tags = get_the_terms( $post_id, 'post_tag' );
1829 if ( is_array( $tags ) ) {
1830 foreach ( $tags as $tag ) {
1831 if ( '' !== trim( $tag->name ) ) {
1832 $post_tag_context = sprintf(
1833 // Translators: the category or tag name.
1834 _x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1835 $tag->name
1836 );
1837 /**
1838 * Filter the "In Tag" line displayed in the post context below each Related Post.
1839 *
1840 * @module related-posts
1841 *
1842 * @since 3.2.0
1843 *
1844 * @param string $post_tag_context "In Tag" line displayed in the post context below each Related Post.
1845 * @param array $tag Array containing information about the tag.
1846 */
1847 return apply_filters( 'jetpack_relatedposts_post_tag_context', $post_tag_context, $tag );
1848 }
1849 }
1850 }
1851
1852 $comment_count = get_comments_number( $post_id );
1853 if ( $comment_count > 0 ) {
1854 return sprintf(
1855 // Translators: amount of comments.
1856 _n( 'With %s comment', 'With %s comments', $comment_count, 'jetpack' ),
1857 number_format_i18n( $comment_count )
1858 );
1859 }
1860
1861 return __( 'Similar post', 'jetpack' );
1862 }
1863
1864 /**
1865 * Logs clicks for clickthrough analysis and related result tuning.
1866 *
1867 * @param int $post_id - the post ID.
1868 * @param int $to_post_id - the to post ID.
1869 * @param int $link_position - the link position.
1870 */
1871 protected function log_click( $post_id, $to_post_id, $link_position ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
1872 }
1873
1874 /**
1875 * Determines if the current post is able to use related posts.
1876 *
1877 * @uses self::get_options, is_admin, is_single, apply_filters
1878 * @return bool
1879 */
1880 protected function enabled_for_request() {
1881 $enabled = is_single()
1882 && ! is_attachment()
1883 && ! is_admin()
1884 && ! is_embed()
1885 && ( ! $this->allow_feature_toggle() || $this->get_option( 'enabled' ) );
1886
1887 /**
1888 * Filter the Enabled value to allow related posts to be shown on pages as well.
1889 *
1890 * @module related-posts
1891 *
1892 * @since 3.3.0
1893 *
1894 * @param bool $enabled Should Related Posts be enabled on the current page.
1895 */
1896 return apply_filters( 'jetpack_relatedposts_filter_enabled_for_request', $enabled );
1897 }
1898
1899 /**
1900 * Adds filters.
1901 *
1902 * @uses self::enqueue_assets, self::setup_shortcode, add_filter
1903 */
1904 protected function action_frontend_init_page() {
1905 $this->enqueue_assets( true, true );
1906 $this->setup_shortcode();
1907
1908 add_filter( 'the_content', array( $this, 'filter_add_target_to_dom' ), 40 );
1909 }
1910
1911 /**
1912 * Determines if the scripts need be enqueued.
1913 *
1914 * @return bool
1915 */
1916 protected function requires_scripts() {
1917 return (
1918 ! ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) &&
1919 ! has_block( 'jetpack/related-posts' ) &&
1920 ! Blocks::is_fse_theme()
1921 );
1922 }
1923
1924 /**
1925 * Enqueues assets needed to do async loading of related posts.
1926 *
1927 * @param string $script - the script we're enqueing.
1928 * @param string $style - the style we're enqueing.
1929 *
1930 * @uses wp_enqueue_script, wp_enqueue_style, plugins_url
1931 */
1932 protected function enqueue_assets( $script, $style ) {
1933 $dependencies = is_customize_preview() ? array( 'customize-base' ) : array();
1934 // Do not enqueue scripts unless they are required.
1935 if ( $script && $this->requires_scripts() ) {
1936 wp_enqueue_script(
1937 'jetpack_related-posts',
1938 Assets::get_file_url_for_environment(
1939 '_inc/build/related-posts/related-posts.min.js',
1940 'modules/related-posts/related-posts.js'
1941 ),
1942 $dependencies,
1943 self::VERSION,
1944 false
1945 );
1946 $related_posts_js_options = array(
1947 /**
1948 * Filter each Related Post Heading structure.
1949 *
1950 * @since 4.0.0
1951 *
1952 * @param string $str Related Post Heading structure. Default to h4.
1953 */
1954 'post_heading' => apply_filters( 'jetpack_relatedposts_filter_post_heading', esc_attr( 'h4' ) ),
1955 );
1956 wp_localize_script( 'jetpack_related-posts', 'related_posts_js_options', $related_posts_js_options );
1957 }
1958 if ( $style ) {
1959 wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'related-posts.css', __FILE__ ), array(), self::VERSION );
1960 wp_style_add_data( 'jetpack_related-posts', 'rtl', 'replace' );
1961 add_action( 'amp_post_template_css', array( $this, 'render_amp_reader_mode_css' ) );
1962 }
1963 }
1964
1965 /**
1966 * Render AMP's reader mode CSS.
1967 */
1968 public function render_amp_reader_mode_css() {
1969 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.
1970 }
1971
1972 /**
1973 * Sets up the shortcode processing.
1974 *
1975 * @uses add_filter, add_shortcode
1976 */
1977 protected function setup_shortcode() {
1978 add_filter( 'the_content', array( $this, 'test_for_shortcode' ), 0 );
1979
1980 add_shortcode( self::SHORTCODE, array( $this, 'get_client_rendered_html' ) );
1981 }
1982
1983 /**
1984 * Return status of related posts toggle.
1985 */
1986 protected function allow_feature_toggle() {
1987 if ( null === $this->allow_feature_toggle ) {
1988 /**
1989 * Filter the display of the Related Posts toggle in Settings > Reading.
1990 *
1991 * @module related-posts
1992 *
1993 * @since 2.8.0
1994 *
1995 * @param bool $allow_feature_toggle Display a feature toggle. Default to false.
1996 */
1997 $this->allow_feature_toggle = apply_filters( 'jetpack_relatedposts_filter_allow_feature_toggle', false );
1998 }
1999 return $this->allow_feature_toggle;
2000 }
2001
2002 /**
2003 * ===================================================
2004 * FUNCTIONS EXPOSING RELATED POSTS IN THE WP REST API
2005 * ===================================================
2006 */
2007
2008 /**
2009 * Add Related Posts to the REST API Post response.
2010 *
2011 * @since 4.4.0
2012 *
2013 * @action rest_api_init
2014 * @uses register_rest_field, self::rest_get_related_posts
2015 */
2016 public function rest_register_related_posts() {
2017 /** This filter is already documented in class.json-api-endpoints.php */
2018 $post_types = apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'revision' ) );
2019 foreach ( $post_types as $post_type ) {
2020 register_rest_field(
2021 $post_type,
2022 'jetpack-related-posts',
2023 array(
2024 'get_callback' => array( $this, 'rest_get_related_posts' ),
2025 'update_callback' => null,
2026 'schema' => null,
2027 )
2028 );
2029 }
2030 }
2031
2032 /**
2033 * Build an array of Related Posts.
2034 * By default returns cached results that are stored for up to 12 hours.
2035 *
2036 * @since 4.4.0
2037 *
2038 * @param array $object Details of current post.
2039 *
2040 * @uses self::get_for_post_id
2041 *
2042 * @return array
2043 */
2044 public function rest_get_related_posts( $object ) {
2045 // If the Related Posts option is turned off, don't get the related posts.
2046 $options = \Jetpack_Options::get_option( 'relatedposts', array() );
2047 if ( empty( $options['enabled'] ) || ! $options['enabled'] ) {
2048 return array();
2049 }
2050
2051 // If the current post don't contain a Related Posts block, don't get the related posts.
2052 if ( ! has_block( 'jetpack/related-posts' ) ) {
2053 return array();
2054 }
2055
2056 return $this->get_for_post_id( $object['id'], array( 'size' => 6 ) );
2057 }
2058 }
2059
2060 /**
2061 * The raw related posts class can be used by other plugins or themes
2062 * to get related content. This class wraps the existing RelatedPosts
2063 * logic thus we never want to add anything to the DOM or do anything
2064 * for event hooks. We will also not present any settings for this
2065 * class and keep it enabled as calls to this class are done
2066 * programmatically.
2067 */
2068 class Jetpack_RelatedPosts_Raw extends Jetpack_RelatedPosts { //phpcs:ignore Generic.Classes.OpeningBraceSameLine.ContentAfterBrace, Generic.Files.OneObjectStructurePerFile.MultipleFound
2069
2070 /**
2071 * The query name we want to look up.
2072 *
2073 * @var string
2074 */
2075 protected $query_name;
2076
2077 /**
2078 * Allows callers of this class to tag each query with a unique name for tracking purposes.
2079 *
2080 * @param string $name - the name of the query.
2081 * @return Jetpack_RelatedPosts_Raw
2082 */
2083 public function set_query_name( $name ) {
2084 $this->query_name = (string) $name;
2085 return $this;
2086 }
2087
2088 /**
2089 * Initialize admin.
2090 */
2091 public function action_admin_init() {}
2092
2093 /**
2094 * Initialize front end.
2095 */
2096 public function action_frontend_init() {}
2097
2098 /**
2099 * Get options.
2100 */
2101 public function get_options() {
2102 return array(
2103 'enabled' => true,
2104 );
2105 }
2106
2107 /**
2108 * Workhorse method to return array of related posts ids matched by Elasticsearch.
2109 *
2110 * @param int $post_id - the post ID.
2111 * @param int $size - size of the post.
2112 * @param array $filters - filters we're using.
2113 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body
2114 * @return array
2115 */
2116 protected function get_related_posts( $post_id, $size, array $filters ) {
2117 $hits = $this->filter_non_public_posts(
2118 $this->get_related_post_ids(
2119 $post_id,
2120 $size,
2121 $filters
2122 )
2123 );
2124
2125 /** This filter is already documented in modules/related-posts/related-posts.php */
2126 $hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
2127
2128 return $hits;
2129 }
2130 }
2131