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