Completed
Push — update/editor-blocks-icon-colo... ( 093ab2...3cfb5e )
by
unknown
08:47
created

Jetpack_RelatedPosts::__construct()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 4
nop 0
dl 0
loc 21
rs 9.584
c 0
b 0
f 0
1
<?php
2
3
use Automattic\Jetpack\Assets;
4
use Automattic\Jetpack\Sync\Settings;
5
6
class Jetpack_RelatedPosts {
7
	const VERSION   = '20191011';
8
	const SHORTCODE = 'jetpack-related-posts';
9
10
	private static $instance     = null;
11
	private static $instance_raw = null;
12
13
	/**
14
	 * Creates and returns a static instance of Jetpack_RelatedPosts.
15
	 *
16
	 * @return Jetpack_RelatedPosts
17
	 */
18
	public static function init() {
19
		if ( ! self::$instance ) {
20
			if ( class_exists('WPCOM_RelatedPosts') && method_exists( 'WPCOM_RelatedPosts', 'init' ) ) {
21
				self::$instance = WPCOM_RelatedPosts::init();
22
			} else {
23
				self::$instance = new Jetpack_RelatedPosts();
24
			}
25
		}
26
27
		return self::$instance;
28
	}
29
30
	/**
31
	 * Creates and returns a static instance of Jetpack_RelatedPosts_Raw.
32
	 *
33
	 * @return Jetpack_RelatedPosts
34
	 */
35
	public static function init_raw() {
36
		if ( ! self::$instance_raw ) {
37
			if ( class_exists('WPCOM_RelatedPosts') && method_exists( 'WPCOM_RelatedPosts', 'init_raw' ) ) {
38
				self::$instance_raw = WPCOM_RelatedPosts::init_raw();
39
			} else {
40
				self::$instance_raw = new Jetpack_RelatedPosts_Raw();
41
			}
42
		}
43
44
		return self::$instance_raw;
45
	}
46
47
	protected $_options;
48
	protected $_allow_feature_toggle;
49
	protected $_blog_charset;
50
	protected $_convert_charset;
51
	protected $_previous_post_id;
52
	protected $_found_shortcode = false;
53
54
	/**
55
	 * Constructor for Jetpack_RelatedPosts.
56
	 *
57
	 * @uses get_option, add_action, apply_filters
58
	 *
59
	 * @return null
0 ignored issues
show
Comprehensibility Best Practice introduced by
Adding a @return annotation to constructors is generally not recommended as a constructor does not have a meaningful return value.

Adding a @return annotation to a constructor is not recommended, since a constructor does not have a meaningful return value.

Please refer to the PHP core documentation on constructors.

Loading history...
60
	 */
61
	public function __construct() {
62
		$this->_blog_charset = get_option( 'blog_charset' );
63
		$this->_convert_charset = ( function_exists( 'iconv' ) && ! preg_match( '/^utf\-?8$/i', $this->_blog_charset ) );
64
65
		add_action( 'admin_init', array( $this, 'action_admin_init' ) );
66
		add_action( 'wp', array( $this, 'action_frontend_init' ) );
67
68
		if ( ! class_exists( 'Jetpack_Media_Summary' ) ) {
69
			jetpack_require_lib( 'class.media-summary' );
70
		}
71
72
		// Add Related Posts to the REST API Post response.
73
		add_action( 'rest_api_init', array( $this, 'rest_register_related_posts' ) );
74
75
		jetpack_register_block(
76
			'jetpack/related-posts',
77
			array(
78
				'render_callback' => array( $this, 'render_block' ),
79
			)
80
		);
81
	}
82
83
	protected function get_blog_id() {
84
		return Jetpack_Options::get_option( 'id' );
85
	}
86
87
	/**
88
	 * =================
89
	 * ACTIONS & FILTERS
90
	 * =================
91
	 */
92
93
	/**
94
	 * Add a checkbox field to Settings > Reading for enabling related posts.
95
	 *
96
	 * @action admin_init
97
	 * @uses add_settings_field, __, register_setting, add_action
98
	 * @return null
99
	 */
100
	public function action_admin_init() {
101
102
		// Add the setting field [jetpack_relatedposts] and place it in Settings > Reading
103
		add_settings_field( 'jetpack_relatedposts', '<span id="jetpack_relatedposts">' . __( 'Related posts', 'jetpack' ) . '</span>', array( $this, 'print_setting_html' ), 'reading' );
104
		register_setting( 'reading', 'jetpack_relatedposts', array( $this, 'parse_options' ) );
105
		add_action('admin_head', array( $this, 'print_setting_head' ) );
106
107
		if( 'options-reading.php' == $GLOBALS['pagenow'] ) {
108
			// Enqueue style for live preview on the reading settings page
109
			$this->_enqueue_assets( false, true );
110
		}
111
	}
112
113
	/**
114
	 * Load related posts assets if it's a elegiable front end page or execute search and return JSON if it's an endpoint request.
115
	 *
116
	 * @global $_GET
117
	 * @action wp
118
	 * @uses add_shortcode, get_the_ID
119
	 * @returns null
120
	 */
121
	public function action_frontend_init() {
122
		// Add a shortcode handler that outputs nothing, this gets overridden later if we can display related content
123
		add_shortcode( self::SHORTCODE, array( $this, 'get_client_rendered_html_unsupported' ) );
124
125
		if ( ! $this->_enabled_for_request() )
126
			return;
127
128
		if ( isset( $_GET['relatedposts'] ) ) {
129
			$excludes = $this->parse_numeric_get_arg( 'relatedposts_exclude' );
130
			$this->_action_frontend_init_ajax( $excludes );
131
		} else {
132
			if ( isset( $_GET['relatedposts_hit'], $_GET['relatedposts_origin'], $_GET['relatedposts_position'] ) ) {
133
				$this->_log_click( $_GET['relatedposts_origin'], get_the_ID(), $_GET['relatedposts_position'] );
134
				$this->_previous_post_id = (int) $_GET['relatedposts_origin'];
135
			}
136
137
			$this->_action_frontend_init_page();
138
		}
139
140
	}
141
142
	/**
143
	 * Render insertion point.
144
	 *
145
	 * @since 4.2.0
146
	 *
147
	 * @return string
148
	 */
149
	public function get_headline() {
150
		$options = $this->get_options();
151
152
		if ( $options['show_headline'] ) {
153
			$headline = sprintf(
154
				/** This filter is already documented in modules/sharedaddy/sharing-service.php */
155
				apply_filters( 'jetpack_sharing_headline_html', '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>', esc_html( $options['headline'] ), 'related-posts' ),
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with esc_html($options['headline']).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
156
				esc_html( $options['headline'] )
157
			);
158
		} else {
159
			$headline = '';
160
		}
161
		return $headline;
162
	}
163
164
	/**
165
	 * Adds a target to the post content to load related posts into if a shortcode for it did not already exist.
166
	 * Will skip adding the target if the post content contains a Related Posts block or if the 'get_the_excerpt'
167
	 * hook is in the current filter list.
168
	 *
169
	 * @filter the_content
170
	 *
171
	 * @param string $content Post content.
172
	 *
173
	 * @returns string
174
	 */
175
	public function filter_add_target_to_dom( $content ) {
176
		if ( has_block( 'jetpack/related-posts' ) ) {
177
			return $content;
178
		}
179
180
		if ( ! $this->_found_shortcode && ! doing_filter( 'get_the_excerpt' ) ) {
181
			if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
182
				$content .= "\n" . $this->get_server_rendered_html();
183
			} else {
184
				$content .= "\n" . $this->get_client_rendered_html();
185
			}
186
		}
187
188
		return $content;
189
	}
190
191
	/**
192
	 * Render static markup based on the Gutenberg block code
193
	 *
194
	 * @return string Rendered related posts HTML.
195
	 */
196
	public function get_server_rendered_html() {
197
		$rp_settings       = $this->get_options();
198
		$block_rp_settings = array(
199
			'displayThumbnails' => $rp_settings['show_thumbnails'],
200
			'showHeadline'      => $rp_settings['show_headline'],
201
			'displayDate'       => isset( $rp_settings['show_date'] ) ? (bool) $rp_settings['show_date'] : true,
202
			'displayContext'    => isset( $rp_settings['show_context'] ) && $rp_settings['show_context'],
203
			'postLayout'        => isset( $rp_settings['layout'] ) ? $rp_settings['layout'] : 'grid',
204
			'postsToShow'       => isset( $rp_settings['size'] ) ? $rp_settings['size'] : 3,
205
			/** This filter is already documented in modules/related-posts/jetpack-related-posts.php */
206
			'headline'          => apply_filters( 'jetpack_relatedposts_filter_headline', $this->get_headline() ),
207
		);
208
209
		return $this->render_block( $block_rp_settings );
210
	}
211
212
	/**
213
	 * Looks for our shortcode on the unfiltered content, this has to execute early.
214
	 *
215
	 * @filter the_content
216
	 * @param string $content
217
	 * @uses has_shortcode
218
	 * @returns string
219
	 */
220
	public function test_for_shortcode( $content ) {
221
		$this->_found_shortcode = has_shortcode( $content, self::SHORTCODE );
222
223
		return $content;
224
	}
225
226
	/**
227
	 * Returns the HTML for the related posts section.
228
	 *
229
	 * @uses esc_html__, apply_filters
230
	 * @returns string
231
	 */
232
	public function get_client_rendered_html() {
233
		if ( Settings::is_syncing() ) {
234
			return '';
235
		}
236
237
		/**
238
		 * Filter the Related Posts headline.
239
		 *
240
		 * @module related-posts
241
		 *
242
		 * @since 3.0.0
243
		 *
244
		 * @param string $headline Related Posts heading.
245
		 */
246
		$headline = apply_filters( 'jetpack_relatedposts_filter_headline', $this->get_headline() );
247
248
		if ( $this->_previous_post_id ) {
249
			$exclude = "data-exclude='{$this->_previous_post_id}'";
250
		} else {
251
			$exclude = "";
252
		}
253
254
		return <<<EOT
255
<div id='jp-relatedposts' class='jp-relatedposts' $exclude>
256
	$headline
257
</div>
258
EOT;
259
	}
260
261
	/**
262
	 * 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.
263
	 *
264
	 * @returns string
265
	 */
266
	public function get_client_rendered_html_unsupported() {
267
		if ( Settings::is_syncing() ) {
268
			return '';
269
		}
270
		return "\n\n<!-- Jetpack Related Posts is not supported in this context. -->\n\n";
271
	}
272
273
	/**
274
	 * ===============
275
	 * GUTENBERG BLOCK
276
	 * ===============
277
	 */
278
279
	/**
280
	 * Echoes out items for the Gutenberg block
281
	 *
282
	 * @param array $related_post The post oject.
283
	 * @param array $block_attributes The block attributes.
284
	 */
285
	public function render_block_item( $related_post, $block_attributes ) {
286
		$instance_id = 'related-posts-item-' . uniqid();
287
		$label_id    = $instance_id . '-label';
288
289
		$item_markup = sprintf(
290
			'<ul id="%1$s" aria-labelledby="%2$s" class="jp-related-posts-i2__post" role="menuitem">',
291
			esc_attr( $instance_id ),
292
			esc_attr( $label_id )
293
		);
294
295
		$item_markup .= sprintf(
296
			'<li class="jp-related-posts-i2__post-link"><a id="%1$s" href="%2$s" %4$s>%3$s</a></li>',
297
			esc_attr( $label_id ),
298
			esc_url( $related_post['url'] ),
299
			esc_attr( $related_post['title'] ),
300
			( ! empty( $related_post['rel'] ) ? 'rel="' . esc_attr( $related_post['rel'] ) . '"' : '' )
301
		);
302
303
		if ( ! empty( $block_attributes['show_thumbnails'] ) && ! empty( $related_post['img']['src'] ) ) {
304
			$img_link = sprintf(
305
				'<li class="jp-related-posts-i2__post-img-link"><a href="%1$s" %2$s><img src="%3$s" width="%4$s" alt="%5$s" /></a></li>',
306
				esc_url( $related_post['url'] ),
307
				( ! empty( $related_post['rel'] ) ? 'rel="' . esc_attr( $related_post['rel'] ) . '"' : '' ),
308
				esc_url( $related_post['img']['src'] ),
309
				esc_attr( $related_post['img']['width'] ),
310
				esc_attr( $related_post['img']['alt_text'] )
311
			);
312
313
			$item_markup .= $img_link;
314
		}
315
316
		if ( $block_attributes['show_date'] ) {
317
			$date_tag = sprintf(
318
				'<li class="jp-related-posts-i2__post-date">%1$s</li>',
319
				esc_html( $related_post['date'] )
320
			);
321
322
			$item_markup .= $date_tag;
323
		}
324
325
		if ( ( $block_attributes['show_context'] ) && ! empty( $related_post['context'] ) ) {
326
			$context_tag = sprintf(
327
				'<li class="jp-related-posts-i2__post-context">%1$s</li>',
328
				esc_html( $related_post['context'] )
329
			);
330
331
			$item_markup .= $context_tag;
332
		}
333
334
		$item_markup .= '</ul>';
335
336
		return $item_markup;
337
	}
338
339
	/**
340
	 * Render a related posts row.
341
	 *
342
	 * @param array $posts The posts to render into the row.
343
	 * @param array $block_attributes Block attributes.
344
	 */
345
	public function render_block_row( $posts, $block_attributes ) {
346
		$rows_markup = '';
347
		foreach ( $posts as $post ) {
348
			$rows_markup .= $this->render_block_item( $post, $block_attributes );
349
		}
350
		return sprintf(
351
			'<div class="jp-related-posts-i2__row" data-post-count="%1$s">%2$s</div>',
352
			count( $posts ),
353
			$rows_markup
354
		);
355
	}
356
357
	/**
358
	 * Render the related posts markup.
359
	 *
360
	 * @param array $attributes Block attributes.
361
	 * @return string
362
	 */
363
	public function render_block( $attributes ) {
364
		$block_attributes = array(
365
			'headline'        => isset( $attributes['headline'] ) ? $attributes['headline'] : null,
366
			'show_thumbnails' => isset( $attributes['displayThumbnails'] ) && $attributes['displayThumbnails'],
367
			'show_date'       => isset( $attributes['displayDate'] ) ? (bool) $attributes['displayDate'] : true,
368
			'show_context'    => isset( $attributes['displayContext'] ) && $attributes['displayContext'],
369
			'layout'          => isset( $attributes['postLayout'] ) && 'list' === $attributes['postLayout'] ? $attributes['postLayout'] : 'grid',
370
			'size'            => ! empty( $attributes['postsToShow'] ) ? absint( $attributes['postsToShow'] ) : 3,
371
		);
372
373
		$excludes = $this->parse_numeric_get_arg( 'relatedposts_origin' );
374
375
		$related_posts = $this->get_for_post_id(
376
			get_the_ID(),
377
			array(
378
				'size'             => $block_attributes['size'],
379
				'exclude_post_ids' => $excludes,
380
			)
381
		);
382
383
		$display_lower_row = $block_attributes['size'] > 3;
384
385
		if ( empty( $related_posts ) ) {
386
			return '';
387
		}
388
389
		switch ( count( $related_posts ) ) {
390
			case 2:
391
			case 4:
392
			case 5:
393
				$top_row_end = 2;
394
				break;
395
396
			default:
397
				$top_row_end = 3;
398
				break;
399
		}
400
401
		$upper_row_posts = array_slice( $related_posts, 0, $top_row_end );
402
		$lower_row_posts = array_slice( $related_posts, $top_row_end );
403
404
		$rows_markup = $this->render_block_row( $upper_row_posts, $block_attributes );
405
		if ( $display_lower_row ) {
406
			$rows_markup .= $this->render_block_row( $lower_row_posts, $block_attributes );
407
		}
408
409
		return sprintf(
410
			'<nav class="jp-relatedposts-i2" data-layout="%1$s">%2$s%3$s</nav>',
411
			esc_attr( $block_attributes['layout'] ),
412
			$block_attributes['headline'],
413
			$rows_markup
414
		);
415
	}
416
417
	/**
418
	 * ========================
419
	 * PUBLIC UTILITY FUNCTIONS
420
	 * ========================
421
	 */
422
423
	/**
424
	 * Parse a numeric GET variable to an array of values.
425
	 *
426
	 * @since 6.9.0
427
	 *
428
	 * @uses absint
429
	 *
430
	 * @param string $arg Name of the GET variable.
431
	 * @return array $result Parsed value(s)
432
	 */
433
	public function parse_numeric_get_arg( $arg ) {
434
		$result = array();
435
436
		if ( isset( $_GET[ $arg ] ) ) {
437
			if ( is_string( $_GET[ $arg ] ) ) {
438
				$result = explode( ',', $_GET[ $arg ] );
439
			} elseif ( is_array( $_GET[ $arg ] ) ) {
440
				$result = array_values( $_GET[ $arg ] );
441
			}
442
443
			$result = array_unique( array_filter( array_map( 'absint', $result ) ) );
444
		}
445
446
		return $result;
447
	}
448
449
	/**
450
	 * Gets options set for Jetpack_RelatedPosts and merge with defaults.
451
	 *
452
	 * @uses Jetpack_Options::get_option, apply_filters
453
	 * @return array
454
	 */
455
	public function get_options() {
456
		if ( null === $this->_options ) {
457
			$this->_options = Jetpack_Options::get_option( 'relatedposts', array() );
458
			if ( ! is_array( $this->_options ) )
459
				$this->_options = array();
460
			if ( ! isset( $this->_options['enabled'] ) )
461
				$this->_options['enabled'] = true;
462
			if ( ! isset( $this->_options['show_headline'] ) )
463
				$this->_options['show_headline'] = true;
464
			if ( ! isset( $this->_options['show_thumbnails'] ) )
465
				$this->_options['show_thumbnails'] = false;
466
			if ( ! isset( $this->_options['show_date'] ) ) {
467
				$this->_options['show_date'] = true;
468
			}
469
			if ( ! isset( $this->_options['show_context'] ) ) {
470
				$this->_options['show_context'] = true;
471
			}
472
			if ( ! isset( $this->_options['layout'] ) ) {
473
				$this->_options['layout'] = 'grid';
474
			}
475
			if ( ! isset( $this->_options['headline'] ) ) {
476
				$this->_options['headline'] = esc_html__( 'Related', 'jetpack' );
477
			}
478
			if ( empty( $this->_options['size'] ) || (int)$this->_options['size'] < 1 )
479
				$this->_options['size'] = 3;
480
481
			/**
482
			 * Filter Related Posts basic options.
483
			 *
484
			 * @module related-posts
485
			 *
486
			 * @since 2.8.0
487
			 *
488
			 * @param array $this->_options Array of basic Related Posts options.
489
			 */
490
			$this->_options = apply_filters( 'jetpack_relatedposts_filter_options', $this->_options );
491
		}
492
493
		return $this->_options;
494
	}
495
496
	public function get_option( $option_name ) {
497
		$options = $this->get_options();
498
499
		if ( isset( $options[ $option_name ] ) ) {
500
			return $options[ $option_name ];
501
		}
502
503
		return false;
504
	}
505
506
	/**
507
	 * Parses input and returns normalized options array.
508
	 *
509
	 * @param array $input
510
	 * @uses self::get_options
511
	 * @return array
512
	 */
513
	public function parse_options( $input ) {
514
		$current = $this->get_options();
515
516
		if ( !is_array( $input ) )
517
			$input = array();
518
519
		if (
520
			! isset( $input['enabled'] )
521
			|| isset( $input['show_date'] )
522
			|| isset( $input['show_context'] )
523
			|| isset( $input['layout'] )
524
			|| isset( $input['headline'] )
525
			) {
526
			$input['enabled'] = '1';
527
		}
528
529
		if ( '1' == $input['enabled'] ) {
530
			$current['enabled'] = true;
531
			$current['show_headline'] = ( isset( $input['show_headline'] ) && '1' == $input['show_headline'] );
532
			$current['show_thumbnails'] = ( isset( $input['show_thumbnails'] ) && '1' == $input['show_thumbnails'] );
533
			$current['show_date'] = ( isset( $input['show_date'] ) && '1' == $input['show_date'] );
534
			$current['show_context'] = ( isset( $input['show_context'] ) && '1' == $input['show_context'] );
535
			$current['layout'] = isset( $input['layout'] ) && in_array( $input['layout'], array( 'grid', 'list' ), true ) ? $input['layout'] : 'grid';
536
			$current['headline'] = isset( $input['headline'] ) ? $input['headline'] : esc_html__( 'Related', 'jetpack' );
537
		} else {
538
			$current['enabled'] = false;
539
		}
540
541
		if ( isset( $input['size'] ) && (int)$input['size'] > 0 )
542
			$current['size'] = (int)$input['size'];
543
		else
544
			$current['size'] = null;
545
546
		return $current;
547
	}
548
549
	/**
550
	 * HTML for admin settings page.
551
	 *
552
	 * @uses self::get_options, checked, esc_html__
553
	 * @returns null
554
	 */
555
	public function print_setting_html() {
556
		$options = $this->get_options();
557
558
		$ui_settings_template = <<<EOT
559
<p class="description">%s</p>
560
<ul id="settings-reading-relatedposts-customize">
561
	<li>
562
		<label><input name="jetpack_relatedposts[show_headline]" type="checkbox" value="1" %s /> %s</label>
563
	</li>
564
	<li>
565
		<label><input name="jetpack_relatedposts[show_thumbnails]" type="checkbox" value="1" %s /> %s</label>
566
	</li>
567
	<li>
568
		<label><input name="jetpack_relatedposts[show_date]" type="checkbox" value="1" %s /> %s</label>
569
	</li>
570
	<li>
571
		<label><input name="jetpack_relatedposts[show_context]" type="checkbox" value="1" %s /> %s</label>
572
	</li>
573
</ul>
574
<div id='settings-reading-relatedposts-preview'>
575
	%s
576
	<div id="jp-relatedposts" class="jp-relatedposts"></div>
577
</div>
578
EOT;
579
		$ui_settings = sprintf(
580
			$ui_settings_template,
581
			esc_html__( 'The following settings will impact all related posts on your site, except for those you created via the block editor:', 'jetpack' ),
582
			checked( $options['show_headline'], true, false ),
583
			esc_html__( 'Highlight related content with a heading', 'jetpack' ),
584
			checked( $options['show_thumbnails'], true, false ),
585
			esc_html__( 'Show a thumbnail image where available', 'jetpack' ),
586
			checked( $options['show_date'], true, false ),
587
			esc_html__( 'Show entry date', 'jetpack' ),
588
			checked( $options['show_context'], true, false ),
589
			esc_html__( 'Show context (category or tag)', 'jetpack' ),
590
			esc_html__( 'Preview:', 'jetpack' )
591
		);
592
593
		if ( !$this->_allow_feature_toggle() ) {
594
			$template = <<<EOT
595
<input type="hidden" name="jetpack_relatedposts[enabled]" value="1" />
596
%s
597
EOT;
598
			printf(
599
				$template,
600
				$ui_settings
601
			);
602
		} else {
603
			$template = <<<EOT
604
<ul id="settings-reading-relatedposts">
605
	<li>
606
		<label><input type="radio" name="jetpack_relatedposts[enabled]" value="0" class="tog" %s /> %s</label>
607
	</li>
608
	<li>
609
		<label><input type="radio" name="jetpack_relatedposts[enabled]" value="1" class="tog" %s /> %s</label>
610
		%s
611
	</li>
612
</ul>
613
EOT;
614
			printf(
615
				$template,
616
				checked( $options['enabled'], false, false ),
617
				esc_html__( 'Hide related content after posts', 'jetpack' ),
618
				checked( $options['enabled'], true, false ),
619
				esc_html__( 'Show related content after posts', 'jetpack' ),
620
				$ui_settings
621
			);
622
		}
623
	}
624
625
	/**
626
	 * Head JS/CSS for admin settings page.
627
	 *
628
	 * @uses esc_html__
629
	 * @returns null
630
	 */
631
	public function print_setting_head() {
632
633
		// only dislay the Related Posts JavaScript on the Reading Settings Admin Page
634
		$current_screen =  get_current_screen();
635
636
		if ( is_null( $current_screen ) ) {
637
			return;
638
		}
639
640
		if( 'options-reading' != $current_screen->id )
641
			return;
642
643
		$related_headline = sprintf(
644
			'<h3 class="jp-relatedposts-headline"><em>%s</em></h3>',
645
			esc_html__( 'Related', 'jetpack' )
646
		);
647
648
		$href_params = 'class="jp-relatedposts-post-a" href="#jetpack_relatedposts" rel="nofollow" data-origin="0" data-position="0"';
649
		$related_with_images = <<<EOT
650
<div class="jp-relatedposts-items jp-relatedposts-items-visual">
651
	<div class="jp-relatedposts-post jp-relatedposts-post0 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
652
		<a $href_params>
653
			<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">
654
		</a>
655
		<h4 class="jp-relatedposts-post-title">
656
			<a $href_params>Big iPhone/iPad Update Now Available</a>
657
		</h4>
658
		<p class="jp-relatedposts-post-excerpt">Big iPhone/iPad Update Now Available</p>
659
		<p class="jp-relatedposts-post-context">In "Mobile"</p>
660
	</div>
661
	<div class="jp-relatedposts-post jp-relatedposts-post1 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
662
		<a $href_params>
663
			<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">
664
		</a>
665
		<h4 class="jp-relatedposts-post-title">
666
			<a $href_params>The WordPress for Android App Gets a Big Facelift</a>
667
		</h4>
668
		<p class="jp-relatedposts-post-excerpt">The WordPress for Android App Gets a Big Facelift</p>
669
		<p class="jp-relatedposts-post-context">In "Mobile"</p>
670
	</div>
671
	<div class="jp-relatedposts-post jp-relatedposts-post2 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
672
		<a $href_params>
673
			<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">
674
		</a>
675
		<h4 class="jp-relatedposts-post-title">
676
			<a $href_params>Upgrade Focus: VideoPress For Weddings</a>
677
		</h4>
678
		<p class="jp-relatedposts-post-excerpt">Upgrade Focus: VideoPress For Weddings</p>
679
		<p class="jp-relatedposts-post-context">In "Upgrade"</p>
680
	</div>
681
</div>
682
EOT;
683
		$related_with_images = str_replace( "\n", '', $related_with_images );
684
		$related_without_images = <<<EOT
685
<div class="jp-relatedposts-items jp-relatedposts-items-minimal">
686
	<p class="jp-relatedposts-post jp-relatedposts-post0" data-post-id="0" data-post-format="image">
687
		<span class="jp-relatedposts-post-title"><a $href_params>Big iPhone/iPad Update Now Available</a></span>
688
		<span class="jp-relatedposts-post-context">In "Mobile"</span>
689
	</p>
690
	<p class="jp-relatedposts-post jp-relatedposts-post1" data-post-id="0" data-post-format="image">
691
		<span class="jp-relatedposts-post-title"><a $href_params>The WordPress for Android App Gets a Big Facelift</a></span>
692
		<span class="jp-relatedposts-post-context">In "Mobile"</span>
693
	</p>
694
	<p class="jp-relatedposts-post jp-relatedposts-post2" data-post-id="0" data-post-format="image">
695
		<span class="jp-relatedposts-post-title"><a $href_params>Upgrade Focus: VideoPress For Weddings</a></span>
696
		<span class="jp-relatedposts-post-context">In "Upgrade"</span>
697
	</p>
698
</div>
699
EOT;
700
		$related_without_images = str_replace( "\n", '', $related_without_images );
701
702
		if ( $this->_allow_feature_toggle() ) {
703
			$extra_css = '#settings-reading-relatedposts-customize { padding-left:2em; margin-top:.5em; }';
704
		} else {
705
			$extra_css = '';
706
		}
707
708
		echo <<<EOT
709
<style type="text/css">
710
	#settings-reading-relatedposts .disabled { opacity:.5; filter:Alpha(opacity=50); }
711
	#settings-reading-relatedposts-preview .jp-relatedposts { background:#fff; padding:.5em; width:75%; }
712
	$extra_css
713
</style>
714
<script type="text/javascript">
715
	jQuery( document ).ready( function($) {
716
		var update_ui = function() {
717
			var is_enabled = true;
718
			if ( 'radio' == $( 'input[name="jetpack_relatedposts[enabled]"]' ).attr('type') ) {
719
				if ( '0' == $( 'input[name="jetpack_relatedposts[enabled]"]:checked' ).val() ) {
720
					is_enabled = false;
721
				}
722
			}
723
			if ( is_enabled ) {
724
				$( '#settings-reading-relatedposts-customize' )
725
					.removeClass( 'disabled' )
726
					.find( 'input' )
727
					.attr( 'disabled', false );
728
				$( '#settings-reading-relatedposts-preview' )
729
					.removeClass( 'disabled' );
730
			} else {
731
				$( '#settings-reading-relatedposts-customize' )
732
					.addClass( 'disabled' )
733
					.find( 'input' )
734
					.attr( 'disabled', true );
735
				$( '#settings-reading-relatedposts-preview' )
736
					.addClass( 'disabled' );
737
			}
738
		};
739
740
		var update_preview = function() {
741
			var html = '';
742
			if ( $( 'input[name="jetpack_relatedposts[show_headline]"]:checked' ).length ) {
743
				html += '$related_headline';
744
			}
745
			if ( $( 'input[name="jetpack_relatedposts[show_thumbnails]"]:checked' ).length ) {
746
				html += '$related_with_images';
747
			} else {
748
				html += '$related_without_images';
749
			}
750
			$( '#settings-reading-relatedposts-preview .jp-relatedposts' ).html( html );
751
			if ( $( 'input[name="jetpack_relatedposts[show_date]"]:checked' ).length ) {
752
				$( '.jp-relatedposts-post-title' ).each( function() {
753
					$( this ).after( $( '<span>August 8, 2005</span>' ) );
754
				} );
755
			}
756
			if ( $( 'input[name="jetpack_relatedposts[show_context]"]:checked' ).length ) {
757
				$( '.jp-relatedposts-post-context' ).show();
758
			} else {
759
				$( '.jp-relatedposts-post-context' ).hide();
760
			}
761
			$( '#settings-reading-relatedposts-preview .jp-relatedposts' ).show();
762
		};
763
764
		// Update on load
765
		update_preview();
766
		update_ui();
767
768
		// Update on change
769
		$( '#settings-reading-relatedposts-customize input' )
770
			.change( update_preview );
771
		$( '#settings-reading-relatedposts' )
772
			.find( 'input.tog' )
773
			.change( update_ui );
774
	});
775
</script>
776
EOT;
777
	}
778
779
	/**
780
	 * Gets an array of related posts that match the given post_id.
781
	 *
782
	 * @param int   $post_id Post which we want to find related posts for.
783
	 * @param array $args - params to use when building Elasticsearch filters to narrow down the search domain.
784
	 * @uses self::get_options, get_post_type, wp_parse_args, apply_filters
785
	 * @return array
786
	 */
787
	public function get_for_post_id( $post_id, array $args ) {
788
		$options = $this->get_options();
789
790
		if ( ! empty( $args['size'] ) ) {
791
			$options['size'] = $args['size'];
792
		}
793
794
		if (
795
			! $options['enabled']
796
			|| 0 === (int) $post_id
797
			|| empty( $options['size'] )
798
		) {
799
			return array();
800
		}
801
802
		$defaults = array(
803
			'size'             => (int) $options['size'],
804
			'post_type'        => get_post_type( $post_id ),
805
			'post_formats'     => array(),
806
			'has_terms'        => array(),
807
			'date_range'       => array(),
808
			'exclude_post_ids' => array(),
809
		);
810
		$args     = wp_parse_args( $args, $defaults );
0 ignored issues
show
Documentation introduced by
$defaults is of type array<string,?,{"size":"...ude_post_ids":"array"}>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
811
		/**
812
		 * Filter the arguments used to retrieve a list of Related Posts.
813
		 *
814
		 * @module related-posts
815
		 *
816
		 * @since 2.8.0
817
		 *
818
		 * @param array $args Array of options to retrieve Related Posts.
819
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
820
		 */
821
		$args = apply_filters( 'jetpack_relatedposts_filter_args', $args, $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
822
823
		$filters = $this->_get_es_filters_from_args( $post_id, $args );
824
		/**
825
		 * Filter Elasticsearch options used to calculate Related Posts.
826
		 *
827
		 * @module related-posts
828
		 *
829
		 * @since 2.8.0
830
		 *
831
		 * @param array $filters Array of Elasticsearch filters based on the post_id and args.
832
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
833
		 */
834
		$filters = apply_filters( 'jetpack_relatedposts_filter_filters', $filters, $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
835
836
		$results = $this->_get_related_posts( $post_id, $args['size'], $filters );
837
		/**
838
		 * Filter the array of related posts matched by Elasticsearch.
839
		 *
840
		 * @module related-posts
841
		 *
842
		 * @since 2.8.0
843
		 *
844
		 * @param array $results Array of related posts matched by Elasticsearch.
845
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
846
		 */
847
		return apply_filters( 'jetpack_relatedposts_returned_results', $results, $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
848
	}
849
850
	/**
851
	 * =========================
852
	 * PRIVATE UTILITY FUNCTIONS
853
	 * =========================
854
	 */
855
856
	/**
857
	 * Creates an array of Elasticsearch filters based on the post_id and args.
858
	 *
859
	 * @param int $post_id
860
	 * @param array $args
861
	 * @uses apply_filters, get_post_types, get_post_format_strings
862
	 * @return array
863
	 */
864
	protected function _get_es_filters_from_args( $post_id, array $args ) {
865
		$filters = array();
866
867
		/**
868
		 * Filter the terms used to search for Related Posts.
869
		 *
870
		 * @module related-posts
871
		 *
872
		 * @since 2.8.0
873
		 *
874
		 * @param array $args['has_terms'] Array of terms associated to the Related Posts.
875
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
876
		 */
877
		$args['has_terms'] = apply_filters( 'jetpack_relatedposts_filter_has_terms', $args['has_terms'], $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
878
		if ( ! empty( $args['has_terms'] ) ) {
879
			foreach( (array)$args['has_terms'] as $term ) {
880
				if ( mb_strlen( $term->taxonomy ) ) {
881 View Code Duplication
					switch ( $term->taxonomy ) {
882
						case 'post_tag':
883
							$tax_fld = 'tag.slug';
884
							break;
885
						case 'category':
886
							$tax_fld = 'category.slug';
887
							break;
888
						default:
889
							$tax_fld = 'taxonomy.' . $term->taxonomy . '.slug';
890
							break;
891
					}
892
					$filters[] = array( 'term' => array( $tax_fld => $term->slug ) );
893
				}
894
			}
895
		}
896
897
		/**
898
		 * Filter the Post Types where we search Related Posts.
899
		 *
900
		 * @module related-posts
901
		 *
902
		 * @since 2.8.0
903
		 *
904
		 * @param array $args['post_type'] Array of Post Types.
905
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
906
		 */
907
		$args['post_type'] = apply_filters( 'jetpack_relatedposts_filter_post_type', $args['post_type'], $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
908
		$valid_post_types = get_post_types();
909
		if ( is_array( $args['post_type'] ) ) {
910
			$sanitized_post_types = array();
911
			foreach ( $args['post_type'] as $pt ) {
912
				if ( in_array( $pt, $valid_post_types ) )
913
					$sanitized_post_types[] = $pt;
914
			}
915
			if ( ! empty( $sanitized_post_types ) )
916
				$filters[] = array( 'terms' => array( 'post_type' => $sanitized_post_types ) );
917
		} else if ( in_array( $args['post_type'], $valid_post_types ) && 'all' != $args['post_type'] ) {
918
			$filters[] = array( 'term' => array( 'post_type' => $args['post_type'] ) );
919
		}
920
921
		/**
922
		 * Filter the Post Formats where we search Related Posts.
923
		 *
924
		 * @module related-posts
925
		 *
926
		 * @since 3.3.0
927
		 *
928
		 * @param array $args['post_formats'] Array of Post Formats.
929
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
930
		 */
931
		$args['post_formats'] = apply_filters( 'jetpack_relatedposts_filter_post_formats', $args['post_formats'], $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
932
		$valid_post_formats = get_post_format_strings();
933
		$sanitized_post_formats = array();
934
		foreach ( $args['post_formats'] as $pf ) {
935
			if ( array_key_exists( $pf, $valid_post_formats ) ) {
936
				$sanitized_post_formats[] = $pf;
937
			}
938
		}
939
		if ( ! empty( $sanitized_post_formats ) ) {
940
			$filters[] = array( 'terms' => array( 'post_format' => $sanitized_post_formats ) );
941
		}
942
943
		/**
944
		 * Filter the date range used to search Related Posts.
945
		 *
946
		 * @module related-posts
947
		 *
948
		 * @since 2.8.0
949
		 *
950
		 * @param array $args['date_range'] Array of a month interval where we search Related Posts.
951
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
952
		 */
953
		$args['date_range'] = apply_filters( 'jetpack_relatedposts_filter_date_range', $args['date_range'], $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
954
		if ( is_array( $args['date_range'] ) && ! empty( $args['date_range'] ) ) {
955
			$args['date_range'] = array_map( 'intval', $args['date_range'] );
956
			if ( !empty( $args['date_range']['from'] ) && !empty( $args['date_range']['to'] ) ) {
957
				$filters[] = array(
958
					'range' => array(
959
						'date_gmt' => $this->_get_coalesced_range( $args['date_range'] ),
960
					)
961
				);
962
			}
963
		}
964
965
		/**
966
		 * Filter the Post IDs excluded from appearing in Related Posts.
967
		 *
968
		 * @module related-posts
969
		 *
970
		 * @since 2.9.0
971
		 *
972
		 * @param array $args['exclude_post_ids'] Array of Post IDs.
973
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
974
		 */
975
		$args['exclude_post_ids'] = apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', $args['exclude_post_ids'], $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
976
		if ( !empty( $args['exclude_post_ids'] ) && is_array( $args['exclude_post_ids'] ) ) {
977
			$excluded_post_ids = array();
978
			foreach ( $args['exclude_post_ids'] as $exclude_post_id) {
979
				$exclude_post_id = (int)$exclude_post_id;
980
				if ( $exclude_post_id > 0 )
981
					$excluded_post_ids[] = $exclude_post_id;
982
			}
983
			$filters[] = array( 'not' => array( 'terms' => array( 'post_id' => $excluded_post_ids ) ) );
984
		}
985
986
		return $filters;
987
	}
988
989
	/**
990
	 * Takes a range and coalesces it into a month interval bracketed by a time as determined by the blog_id to enhance caching.
991
	 *
992
	 * @param array $date_range
993
	 * @return array
994
	 */
995
	protected function _get_coalesced_range( array $date_range ) {
996
		$now = time();
997
		$coalesce_time = $this->get_blog_id() % 86400;
998
		$current_time = $now - strtotime( 'today', $now );
999
1000
		if ( $current_time < $coalesce_time && '01' == date( 'd', $now ) ) {
1001
			// Move back 1 period
1002
			return array(
1003
				'from' => date( 'Y-m-01', strtotime( '-1 month', $date_range['from'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),
1004
				'to'   => date( 'Y-m-01', $date_range['to'] ) . ' ' . date( 'H:i:s', $coalesce_time ),
1005
			);
1006
		} else {
1007
			// Use current period
1008
			return array(
1009
				'from' => date( 'Y-m-01', $date_range['from'] ) . ' ' . date( 'H:i:s', $coalesce_time ),
1010
				'to'   => date( 'Y-m-01', strtotime( '+1 month', $date_range['to'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),
1011
			);
1012
		}
1013
	}
1014
1015
	/**
1016
	 * Generate and output ajax response for related posts API call.
1017
	 * NOTE: Calls exit() to end all further processing after payload has been outputed.
1018
	 *
1019
	 * @param array $excludes array of post_ids to exclude
1020
	 * @uses send_nosniff_header, self::get_for_post_id, get_the_ID
1021
	 * @return null
1022
	 */
1023
	protected function _action_frontend_init_ajax( array $excludes ) {
1024
		define( 'DOING_AJAX', true );
1025
1026
		header( 'Content-type: application/json; charset=utf-8' ); // JSON can only be UTF-8
1027
		send_nosniff_header();
1028
1029
		$options = $this->get_options();
1030
1031
		if ( isset( $_GET['jetpackrpcustomize'] ) ) {
1032
1033
			// If we're in the customizer, add dummy content.
1034
			$date_now = current_time( get_option( 'date_format' ) );
1035
			$related_posts = array(
1036
				array(
1037
					'id'       => - 1,
1038
					'url'      => 'https://jetpackme.files.wordpress.com/2019/03/cat-blog.png',
1039
					'url_meta' => array(
1040
						'origin'   => 0,
1041
						'position' => 0
1042
					),
1043
					'title'    => esc_html__( 'Big iPhone/iPad Update Now Available', 'jetpack' ),
1044
					'date'     => $date_now,
1045
					'format'   => false,
1046
					'excerpt'  => esc_html__( 'It is that time of the year when devices are shiny again.', 'jetpack' ),
1047
					'rel'      => 'nofollow',
1048
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
1049
					'img'      => array(
1050
						'src'    => 'https://jetpackme.files.wordpress.com/2019/03/cat-blog.png',
1051
						'width'  => 350,
1052
						'height' => 200
1053
					),
1054
					'classes'  => array()
1055
				),
1056
				array(
1057
					'id'       => - 1,
1058
					'url'      => 'https://jetpackme.files.wordpress.com/2019/03/devices.jpg',
1059
					'url_meta' => array(
1060
						'origin'   => 0,
1061
						'position' => 0
1062
					),
1063
					'title'    => esc_html__( 'The WordPress for Android App Gets a Big Facelift', 'jetpack' ),
1064
					'date'     => $date_now,
1065
					'format'   => false,
1066
					'excerpt'  => esc_html__( 'Writing is new again in Android with the new WordPress app.', 'jetpack' ),
1067
					'rel'      => 'nofollow',
1068
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
1069
					'img'      => array(
1070
						'src'    => 'https://jetpackme.files.wordpress.com/2019/03/devices.jpg',
1071
						'width'  => 350,
1072
						'height' => 200
1073
					),
1074
					'classes'  => array()
1075
				),
1076
				array(
1077
					'id'       => - 1,
1078
					'url'      => 'https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg',
1079
					'url_meta' => array(
1080
						'origin'   => 0,
1081
						'position' => 0
1082
					),
1083
					'title'    => esc_html__( 'Upgrade Focus, VideoPress for weddings', 'jetpack' ),
1084
					'date'     => $date_now,
1085
					'format'   => false,
1086
					'excerpt'  => esc_html__( 'Weddings are in the spotlight now with VideoPress for weddings.', 'jetpack' ),
1087
					'rel'      => 'nofollow',
1088
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
1089
					'img'      => array(
1090
						'src'    => 'https://jetpackme.files.wordpress.com/2019/03/mobile-wedding.jpg',
1091
						'width'  => 350,
1092
						'height' => 200
1093
					),
1094
					'classes'  => array()
1095
				),
1096
			);
1097
1098
			for ( $total = 0; $total < $options['size'] - 3; $total++ ) {
1099
				$related_posts[] = $related_posts[ $total ];
1100
			}
1101
1102
			$current_post = get_post();
1103
1104
			// Exclude current post after filtering to make sure it's excluded and not lost during filtering.
1105
			$excluded_posts = array_merge(
1106
				/** This filter is already documented in modules/related-posts/jetpack-related-posts.php */
1107
				apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', array() ),
1108
				array( $current_post->ID )
1109
			);
1110
1111
			// Fetch posts with featured image.
1112
			$with_post_thumbnails = get_posts( array(
1113
				'posts_per_page'   => $options['size'],
1114
				'post__not_in'     => $excluded_posts,
1115
				'post_type'        => $current_post->post_type,
1116
				'meta_key'         => '_thumbnail_id',
1117
				'suppress_filters' => false,
1118
			) );
1119
1120
			// If we don't have enough, fetch posts without featured image.
1121
			if ( 0 < ( $more = $options['size'] - count( $with_post_thumbnails ) ) ) {
1122
				$no_post_thumbnails = get_posts( array(
1123
					'posts_per_page'  => $more,
1124
					'post__not_in'    => $excluded_posts,
1125
					'post_type'       => $current_post->post_type,
1126
					'meta_query' => array(
1127
						array(
1128
							'key'     => '_thumbnail_id',
1129
							'compare' => 'NOT EXISTS',
1130
						),
1131
					),
1132
					'suppress_filters' => false,
1133
				) );
1134
			} else {
1135
				$no_post_thumbnails = array();
1136
			}
1137
1138
			foreach ( array_merge( $with_post_thumbnails, $no_post_thumbnails ) as $index => $real_post ) {
1139
				$related_posts[ $index ]['id']      = $real_post->ID;
1140
				$related_posts[ $index ]['url']     = esc_url( get_permalink( $real_post ) );
1141
				$related_posts[ $index ]['title']   = $this->_to_utf8( $this->_get_title( $real_post->post_title, $real_post->post_content ) );
1142
				$related_posts[ $index ]['date']    = get_the_date( '', $real_post );
1143
				$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' );
1144
				$related_posts[ $index ]['img']     = $this->_generate_related_post_image_params( $real_post->ID );
1145
				$related_posts[ $index ]['context'] = $this->_generate_related_post_context( $real_post->ID );
1146
			}
1147
		} else {
1148
			$related_posts = $this->get_for_post_id(
1149
				get_the_ID(),
1150
				array(
1151
					'exclude_post_ids' => $excludes,
1152
				)
1153
			);
1154
		}
1155
1156
		$response = array(
1157
			'version' => self::VERSION,
1158
			'show_thumbnails' => (bool) $options['show_thumbnails'],
1159
			'show_date' => (bool) $options['show_date'],
1160
			'show_context' => (bool) $options['show_context'],
1161
			'layout' => (string) $options['layout'],
1162
			'headline' => (string) $options['headline'],
1163
			'items' => array(),
1164
		);
1165
1166
		if ( count( $related_posts ) == $options['size'] )
1167
			$response['items'] = $related_posts;
1168
1169
		echo json_encode( $response );
1170
1171
		exit();
1172
	}
1173
1174
	/**
1175
	 * Returns a UTF-8 encoded array of post information for the given post_id
1176
	 *
1177
	 * @param int $post_id
1178
	 * @param int $position
1179
	 * @param int $origin The post id that this is related to
1180
	 * @uses get_post, get_permalink, remove_query_arg, get_post_format, apply_filters
1181
	 * @return array
1182
	 */
1183
	public function get_related_post_data_for_post( $post_id, $position, $origin ) {
1184
		$post = get_post( $post_id );
1185
1186
		return array(
1187
			'id' => $post->ID,
1188
			'url' => get_permalink( $post->ID ),
1189
			'url_meta' => array( 'origin' => $origin, 'position' => $position ),
1190
			'title' => $this->_to_utf8( $this->_get_title( $post->post_title, $post->post_content ) ),
1191
			'date' => get_the_date( '', $post->ID ),
1192
			'format' => get_post_format( $post->ID ),
1193
			'excerpt' => html_entity_decode( $this->_to_utf8( $this->_get_excerpt( $post->post_excerpt, $post->post_content ) ), ENT_QUOTES, 'UTF-8' ),
1194
			/**
1195
			 * Filters the rel attribute for the Related Posts' links.
1196
			 *
1197
			 * @module related-posts
1198
			 *
1199
			 * @since 3.7.0
1200
			 * @since 7.9.0 - Change Default value to empty.
1201
			 *
1202
			 * @param string $link_rel Link rel attribute for Related Posts' link. Default is empty.
1203
			 * @param int    $post->ID Post ID.
1204
			 */
1205
			'rel' => apply_filters( 'jetpack_relatedposts_filter_post_link_rel', '', $post->ID ),
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post->ID.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1206
			/**
1207
			 * Filter the context displayed below each Related Post.
1208
			 *
1209
			 * @module related-posts
1210
			 *
1211
			 * @since 3.0.0
1212
			 *
1213
			 * @param string $this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ) Context displayed below each related post.
1214
			 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1215
			 */
1216
			'context' => apply_filters(
1217
				'jetpack_relatedposts_filter_post_context',
1218
				$this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ),
1219
				$post->ID
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post->ID.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1220
			),
1221
			'img' => $this->_generate_related_post_image_params( $post->ID ),
1222
			/**
1223
			 * Filter the post css classes added on HTML markup.
1224
			 *
1225
			 * @module related-posts
1226
			 *
1227
			 * @since 3.8.0
1228
			 *
1229
			 * @param array array() CSS classes added on post HTML markup.
1230
			 * @param string $post_id Post ID.
1231
			 */
1232
			'classes' => apply_filters(
1233
				'jetpack_relatedposts_filter_post_css_classes',
1234
				array(),
1235
				$post->ID
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post->ID.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1236
			),
1237
		);
1238
	}
1239
1240
	/**
1241
	 * Returns either the title or a small excerpt to use as title for post.
1242
	 *
1243
	 * @param string $post_title
1244
	 * @param string $post_content
1245
	 * @uses strip_shortcodes, wp_trim_words, __
1246
	 * @return string
1247
	 */
1248
	protected function _get_title( $post_title, $post_content ) {
1249
		if ( ! empty( $post_title ) ) {
1250
			return wp_strip_all_tags( $post_title );
1251
		}
1252
1253
		$post_title = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post_content ) ), 5, '…' );
1254
		if ( ! empty( $post_title ) ) {
1255
			return $post_title;
1256
		}
1257
1258
		return __( 'Untitled Post', 'jetpack' );
1259
	}
1260
1261
	/**
1262
	 * Returns a plain text post excerpt for title attribute of links.
1263
	 *
1264
	 * @param string $post_excerpt
1265
	 * @param string $post_content
1266
	 * @uses strip_shortcodes, wp_strip_all_tags, wp_trim_words
1267
	 * @return string
1268
	 */
1269
	protected function _get_excerpt( $post_excerpt, $post_content ) {
1270
		if ( empty( $post_excerpt ) )
1271
			$excerpt = $post_content;
1272
		else
1273
			$excerpt = $post_excerpt;
1274
1275
		return wp_trim_words( wp_strip_all_tags( strip_shortcodes( $excerpt ) ), 50, '…' );
1276
	}
1277
1278
	/**
1279
	 * Generates the thumbnail image to be used for the post. Uses the
1280
	 * image as returned by Jetpack_PostImages::get_image()
1281
	 *
1282
	 * @param int $post_id
1283
	 * @uses self::get_options, apply_filters, Jetpack_PostImages::get_image, Jetpack_PostImages::fit_image_url
1284
	 * @return string
1285
	 */
1286
	protected function _generate_related_post_image_params( $post_id ) {
1287
		$options = $this->get_options();
0 ignored issues
show
Unused Code introduced by
$options is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1288
		$image_params = array(
1289
			'alt_text' => '',
1290
			'src'      => '',
1291
			'width'    => 0,
1292
			'height'   => 0,
1293
		);
1294
1295
		/**
1296
		 * Filter the size of the Related Posts images.
1297
		 *
1298
		 * @module related-posts
1299
		 *
1300
		 * @since 2.8.0
1301
		 *
1302
		 * @param array array( 'width' => 350, 'height' => 200 ) Size of the images displayed below each Related Post.
1303
		 */
1304
		$thumbnail_size = apply_filters(
1305
			'jetpack_relatedposts_filter_thumbnail_size',
1306
			array( 'width' => 350, 'height' => 200 )
1307
		);
1308
		if ( !is_array( $thumbnail_size ) ) {
1309
			$thumbnail_size = array(
1310
				'width' => (int)$thumbnail_size,
1311
				'height' => (int)$thumbnail_size
1312
			);
1313
		}
1314
1315
		// Try to get post image
1316
		if ( class_exists( 'Jetpack_PostImages' ) ) {
1317
			$img_url    = '';
1318
			$post_image = Jetpack_PostImages::get_image(
1319
				$post_id,
1320
				$thumbnail_size
1321
			);
1322
1323
			if ( is_array($post_image) ) {
1324
				$img_url = $post_image['src'];
1325
			} elseif ( class_exists( 'Jetpack_Media_Summary' ) ) {
1326
				$media = Jetpack_Media_Summary::get( $post_id );
1327
1328
				if ( is_array($media) && !empty( $media['image'] ) ) {
1329
					$img_url = $media['image'];
1330
				}
1331
			}
1332
1333
			if ( ! empty( $img_url ) ) {
1334
				if ( ! empty( $post_image['alt_text'] ) ) {
1335
					$image_params['alt_text'] = $post_image['alt_text'];
1336
				} else {
1337
					$image_params['alt_text'] = '';
1338
				}
1339
				$image_params['width']  = $thumbnail_size['width'];
1340
				$image_params['height'] = $thumbnail_size['height'];
1341
				$image_params['src']    = Jetpack_PostImages::fit_image_url(
1342
					$img_url,
1343
					$thumbnail_size['width'],
1344
					$thumbnail_size['height']
1345
				);
1346
			}
1347
		}
1348
1349
		return $image_params;
1350
	}
1351
1352
	/**
1353
	 * Returns the string UTF-8 encoded
1354
	 *
1355
	 * @param string $text
1356
	 * @return string
1357
	 */
1358
	protected function _to_utf8( $text ) {
1359
		if ( $this->_convert_charset ) {
1360
			return iconv( $this->_blog_charset, 'UTF-8', $text );
1361
		} else {
1362
			return $text;
1363
		}
1364
	}
1365
1366
	/**
1367
	 * =============================================
1368
	 * PROTECTED UTILITY FUNCTIONS EXTENDED BY WPCOM
1369
	 * =============================================
1370
	 */
1371
1372
	/**
1373
	 * Workhorse method to return array of related posts matched by Elasticsearch.
1374
	 *
1375
	 * @param int $post_id
1376
	 * @param int $size
1377
	 * @param array $filters
1378
	 * @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
1379
	 * @return array
1380
	 */
1381
	protected function _get_related_posts( $post_id, $size, array $filters ) {
1382
		$hits = $this->_filter_non_public_posts(
1383
			$this->_get_related_post_ids(
1384
				$post_id,
1385
				$size,
1386
				$filters
1387
			)
1388
		);
1389
1390
		/**
1391
		 * Filter the Related Posts matched by Elasticsearch.
1392
		 *
1393
		 * @module related-posts
1394
		 *
1395
		 * @since 2.9.0
1396
		 *
1397
		 * @param array $hits Array of Post IDs matched by Elasticsearch.
1398
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1399
		 */
1400
		$hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1401
1402
		$related_posts = array();
1403
		foreach ( $hits as $i => $hit ) {
1404
			$related_posts[] = $this->get_related_post_data_for_post( $hit['id'], $i, $post_id );
1405
		}
1406
		return $related_posts;
1407
	}
1408
1409
	/**
1410
	 * Get array of related posts matched by Elasticsearch.
1411
	 *
1412
	 * @param int $post_id
1413
	 * @param int $size
1414
	 * @param array $filters
1415
	 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body, get_post_meta, update_post_meta
1416
	 * @return array
1417
	 */
1418
	protected function _get_related_post_ids( $post_id, $size, array $filters ) {
1419
		$now_ts = time();
1420
		$cache_meta_key = '_jetpack_related_posts_cache';
1421
1422
		$body = array(
1423
			'size' => (int) $size,
1424
		);
1425
1426
		if ( !empty( $filters ) )
1427
			$body['filter'] = array( 'and' => $filters );
1428
1429
		// Build cache key
1430
		$cache_key = md5( serialize( $body ) );
1431
1432
		// Load all cached values
1433
		if ( wp_using_ext_object_cache() ) {
1434
			$transient_name = "{$cache_meta_key}_{$cache_key}_{$post_id}";
1435
			$cache = get_transient( $transient_name );
1436
			if ( false !== $cache ) {
1437
				return $cache;
1438
			}
1439
		} else {
1440
			$cache = get_post_meta( $post_id, $cache_meta_key, true );
1441
1442
			if ( empty( $cache ) )
1443
				$cache = array();
1444
1445
1446
			// Cache is valid! Return cached value.
1447
			if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) && $cache[ $cache_key ][ 'expires' ] > $now_ts ) {
1448
				return $cache[ $cache_key ][ 'payload' ];
1449
			}
1450
		}
1451
1452
		$response = wp_remote_post(
1453
			"https://public-api.wordpress.com/rest/v1/sites/{$this->get_blog_id()}/posts/$post_id/related/",
1454
			array(
1455
				'timeout' => 10,
1456
				'user-agent' => 'jetpack_related_posts',
1457
				'sslverify' => true,
1458
				'body' => $body,
1459
			)
1460
		);
1461
1462
		// Oh no... return nothing don't cache errors.
1463
		if ( is_wp_error( $response ) ) {
1464
			if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) )
1465
				return $cache[ $cache_key ][ 'payload' ]; // return stale
1466
			else
1467
				return array();
1468
		}
1469
1470
		$results = json_decode( wp_remote_retrieve_body( $response ), true );
1471
		$related_posts = array();
1472
		if ( is_array( $results ) && !empty( $results['hits'] ) ) {
1473
			foreach( $results['hits'] as $hit ) {
1474
				$related_posts[] = array(
1475
					'id' => $hit['fields']['post_id'],
1476
				);
1477
			}
1478
		}
1479
1480
		// An empty array might indicate no related posts or that posts
1481
		// are not yet synced to WordPress.com, so we cache for only 1
1482
		// minute in this case
1483
		if ( empty( $related_posts ) ) {
1484
			$cache_ttl = 60;
1485
		} else {
1486
			$cache_ttl = 12 * HOUR_IN_SECONDS;
1487
		}
1488
1489
		// Update cache
1490
		if ( wp_using_ext_object_cache() ) {
1491
			set_transient( $transient_name, $related_posts, $cache_ttl );
0 ignored issues
show
Bug introduced by
The variable $transient_name does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1492
		} else {
1493
			// Copy all valid cache values
1494
			$new_cache = array();
1495
			foreach ( $cache as $k => $v ) {
1496
				if ( is_array( $v ) && $v[ 'expires' ] > $now_ts ) {
1497
					$new_cache[ $k ] = $v;
1498
				}
1499
			}
1500
1501
			// Set new cache value
1502
			$cache_expires = $cache_ttl + $now_ts;
1503
			$new_cache[ $cache_key ] = array(
1504
				'expires' => $cache_expires,
1505
				'payload' => $related_posts,
1506
			);
1507
			update_post_meta( $post_id, $cache_meta_key, $new_cache );
1508
		}
1509
1510
		return $related_posts;
1511
	}
1512
1513
	/**
1514
	 * Filter out any hits that are not public anymore.
1515
	 *
1516
	 * @param array $related_posts
1517
	 * @uses get_post_stati, get_post_status
1518
	 * @return array
1519
	 */
1520
	protected function _filter_non_public_posts( array $related_posts ) {
1521
		$public_stati = get_post_stati( array( 'public' => true ) );
1522
1523
		$filtered = array();
1524
		foreach ( $related_posts as $hit ) {
1525
			if ( in_array( get_post_status( $hit['id'] ), $public_stati ) ) {
1526
				$filtered[] = $hit;
1527
			}
1528
		}
1529
		return $filtered;
1530
	}
1531
1532
	/**
1533
	 * Generates a context for the related content (second line in related post output).
1534
	 * Order of importance:
1535
	 *   - First category (Not 'Uncategorized')
1536
	 *   - First post tag
1537
	 *   - Number of comments
1538
	 *
1539
	 * @param int $post_id
1540
	 * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n
1541
	 * @return string
1542
	 */
1543
	protected function _generate_related_post_context( $post_id ) {
1544
		$categories = get_the_category( $post_id );
1545 View Code Duplication
		if ( is_array( $categories ) ) {
1546
			foreach ( $categories as $category ) {
1547
				if ( 'uncategorized' != $category->slug && '' != trim( $category->name ) ) {
1548
					$post_cat_context = sprintf(
1549
						esc_html_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1550
						$category->name
1551
					);
1552
					/**
1553
					 * Filter the "In Category" line displayed in the post context below each Related Post.
1554
					 *
1555
					 * @module related-posts
1556
					 *
1557
					 * @since 3.2.0
1558
					 *
1559
					 * @param string $post_cat_context "In Category" line displayed in the post context below each Related Post.
1560
					 * @param array $category Array containing information about the category.
1561
					 */
1562
					return apply_filters( 'jetpack_relatedposts_post_category_context', $post_cat_context, $category );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $category.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1563
				}
1564
			}
1565
		}
1566
1567
		$tags = get_the_terms( $post_id, 'post_tag' );
1568 View Code Duplication
		if ( is_array( $tags ) ) {
1569
			foreach ( $tags as $tag ) {
1570
				if ( '' != trim( $tag->name ) ) {
1571
					$post_tag_context = sprintf(
1572
						_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1573
						$tag->name
1574
					);
1575
					/**
1576
					 * Filter the "In Tag" line displayed in the post context below each Related Post.
1577
					 *
1578
					 * @module related-posts
1579
					 *
1580
					 * @since 3.2.0
1581
					 *
1582
					 * @param string $post_tag_context "In Tag" line displayed in the post context below each Related Post.
1583
					 * @param array $tag Array containing information about the tag.
1584
					 */
1585
					return apply_filters( 'jetpack_relatedposts_post_tag_context', $post_tag_context, $tag );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $tag.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1586
				}
1587
			}
1588
		}
1589
1590
		$comment_count = get_comments_number( $post_id );
1591
		if ( $comment_count > 0 ) {
1592
			return sprintf(
1593
				_n( 'With 1 comment', 'With %s comments', $comment_count, 'jetpack' ),
1594
				number_format_i18n( $comment_count )
1595
			);
1596
		}
1597
1598
		return __( 'Similar post', 'jetpack' );
1599
	}
1600
1601
	/**
1602
	 * Logs clicks for clickthrough analysis and related result tuning.
1603
	 *
1604
	 * @return null
1605
	 */
1606
	protected function _log_click( $post_id, $to_post_id, $link_position ) {
1607
1608
	}
1609
1610
	/**
1611
	 * Determines if the current post is able to use related posts.
1612
	 *
1613
	 * @uses self::get_options, is_admin, is_single, apply_filters
1614
	 * @return bool
1615
	 */
1616
	protected function _enabled_for_request() {
1617
		$enabled = is_single()
1618
			&& ! is_attachment()
1619
			&& ! is_admin()
1620
			&& ! is_embed()
1621
			&& ( ! $this->_allow_feature_toggle() || $this->get_option( 'enabled' ) );
1622
1623
		/**
1624
		 * Filter the Enabled value to allow related posts to be shown on pages as well.
1625
		 *
1626
		 * @module related-posts
1627
		 *
1628
		 * @since 3.3.0
1629
		 *
1630
		 * @param bool $enabled Should Related Posts be enabled on the current page.
1631
		 */
1632
		return apply_filters( 'jetpack_relatedposts_filter_enabled_for_request', $enabled );
1633
	}
1634
1635
	/**
1636
	 * Adds filters and enqueues assets.
1637
	 *
1638
	 * @uses self::_enqueue_assets, self::_setup_shortcode, add_filter
1639
	 * @return null
1640
	 */
1641
	protected function _action_frontend_init_page() {
1642
1643
		$enqueue_script = ! ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() );
1644
		$this->_enqueue_assets( $enqueue_script, true );
1645
		$this->_setup_shortcode();
1646
1647
		add_filter( 'the_content', array( $this, 'filter_add_target_to_dom' ), 40 );
1648
	}
1649
1650
	/**
1651
	 * Enqueues assets needed to do async loading of related posts.
1652
	 *
1653
	 * @uses wp_enqueue_script, wp_enqueue_style, plugins_url
1654
	 * @return null
1655
	 */
1656
	protected function _enqueue_assets( $script, $style ) {
1657
		$dependencies = is_customize_preview() ? array( 'customize-base' ) : array( 'jquery' );
1658
		if ( $script ) {
1659
			wp_enqueue_script(
1660
				'jetpack_related-posts',
1661
				Assets::get_file_url_for_environment(
1662
					'_inc/build/related-posts/related-posts.min.js',
1663
					'modules/related-posts/related-posts.js'
1664
				),
1665
				$dependencies,
1666
				self::VERSION
1667
			);
1668
			$related_posts_js_options = array(
1669
				/**
1670
				 * Filter each Related Post Heading structure.
1671
				 *
1672
				 * @since 4.0.0
1673
				 *
1674
				 * @param string $str Related Post Heading structure. Default to h4.
1675
				 */
1676
				'post_heading' => apply_filters( 'jetpack_relatedposts_filter_post_heading', esc_attr( 'h4' ) ),
1677
			);
1678
			wp_localize_script( 'jetpack_related-posts', 'related_posts_js_options', $related_posts_js_options );
1679
		}
1680
		if ( $style ){
1681
			wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'related-posts.css', __FILE__ ), array(), self::VERSION );
1682
			wp_style_add_data( 'jetpack_related-posts', 'rtl', 'replace' );
1683
			add_action( 'amp_post_template_css', array( $this, 'render_amp_reader_mode_css' ) );
1684
		}
1685
	}
1686
1687
	public function render_amp_reader_mode_css() {
1688
		echo file_get_contents( plugin_dir_path( __FILE__ ) . 'related-posts.css' );
1689
	}
1690
1691
	/**
1692
	 * Sets up the shortcode processing.
1693
	 *
1694
	 * @uses add_filter, add_shortcode
1695
	 * @return null
1696
	 */
1697
	protected function _setup_shortcode() {
1698
		add_filter( 'the_content', array( $this, 'test_for_shortcode' ), 0 );
1699
1700
		add_shortcode( self::SHORTCODE, array( $this, 'get_client_rendered_html' ) );
1701
	}
1702
1703
	protected function _allow_feature_toggle() {
1704
		if ( null === $this->_allow_feature_toggle ) {
1705
			/**
1706
			 * Filter the display of the Related Posts toggle in Settings > Reading.
1707
			 *
1708
			 * @module related-posts
1709
			 *
1710
			 * @since 2.8.0
1711
			 *
1712
			 * @param bool false Display a feature toggle. Default to false.
1713
			 */
1714
			$this->_allow_feature_toggle = apply_filters( 'jetpack_relatedposts_filter_allow_feature_toggle', false );
1715
		}
1716
		return $this->_allow_feature_toggle;
1717
	}
1718
1719
	/**
1720
	 * ===================================================
1721
	 * FUNCTIONS EXPOSING RELATED POSTS IN THE WP REST API
1722
	 * ===================================================
1723
	 */
1724
1725
	/**
1726
	 * Add Related Posts to the REST API Post response.
1727
	 *
1728
	 * @since 4.4.0
1729
	 *
1730
	 * @action rest_api_init
1731
	 * @uses register_rest_field, self::rest_get_related_posts
1732
	 * @return null
1733
	 */
1734
	public function rest_register_related_posts() {
1735
		/** This filter is already documented in class.json-api-endpoints.php */
1736
		$post_types = apply_filters( 'rest_api_allowed_post_types', array( 'post', 'page', 'revision' ) );
1737
		foreach ( $post_types as $post_type ) {
1738
			register_rest_field(
1739
				$post_type,
1740
				'jetpack-related-posts',
1741
				array(
1742
					'get_callback'    => array( $this, 'rest_get_related_posts' ),
1743
					'update_callback' => null,
1744
					'schema'          => null,
1745
				)
1746
			);
1747
		}
1748
	}
1749
1750
	/**
1751
	 * Build an array of Related Posts.
1752
	 * By default returns cached results that are stored for up to 12 hours.
1753
	 *
1754
	 * @since 4.4.0
1755
	 *
1756
	 * @param array $object Details of current post.
1757
	 * @param string $field_name Name of field.
1758
	 * @param WP_REST_Request $request Current request
1759
	 *
1760
	 * @uses self::get_for_post_id
1761
	 *
1762
	 * @return array
1763
	 */
1764
	public function rest_get_related_posts( $object, $field_name, $request ) {
1765
		return $this->get_for_post_id( $object['id'], array( 'size' => 6 ) );
1766
	}
1767
}
1768
1769
class Jetpack_RelatedPosts_Raw extends Jetpack_RelatedPosts {
1770
	protected $_query_name;
1771
1772
	/**
1773
	 * Allows callers of this class to tag each query with a unique name for tracking purposes.
1774
	 *
1775
	 * @param string $name
1776
	 * @return Jetpack_RelatedPosts_Raw
1777
	 */
1778
	public function set_query_name( $name ) {
1779
		$this->_query_name = (string) $name;
1780
		return $this;
1781
	}
1782
1783
	/**
1784
	 * The raw related posts class can be used by other plugins or themes
1785
	 * to get related content. This class wraps the existing RelatedPosts
1786
	 * logic thus we never want to add anything to the DOM or do anything
1787
	 * for event hooks. We will also not present any settings for this
1788
	 * class and keep it enabled as calls to this class is done
1789
	 * programmatically.
1790
	 */
1791
	public function action_admin_init() {}
1792
	public function action_frontend_init() {}
1793
	public function get_options() {
1794
		return array(
1795
			'enabled' => true,
1796
		);
1797
	}
1798
1799
	/**
1800
	 * Workhorse method to return array of related posts ids matched by Elasticsearch.
1801
	 *
1802
	 * @param int $post_id
1803
	 * @param int $size
1804
	 * @param array $filters
1805
	 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body
1806
	 * @return array
1807
	 */
1808
	protected function _get_related_posts( $post_id, $size, array $filters ) {
1809
		$hits = $this->_filter_non_public_posts(
1810
			$this->_get_related_post_ids(
1811
				$post_id,
1812
				$size,
1813
				$filters
1814
			)
1815
		);
1816
1817
		/** This filter is already documented in modules/related-posts/related-posts.php */
1818
		$hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
0 ignored issues
show
Unused Code introduced by
The call to apply_filters() has too many arguments starting with $post_id.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
1819
1820
		return $hits;
1821
	}
1822
}
1823