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