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