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