Completed
Push — fix/publicize-disabled-by-cust... ( 8e93bf )
by
unknown
09:03
created

Jetpack_RelatedPosts::init_raw()   A

Complexity

Conditions 4
Paths 3

Size

Total Lines 16
Code Lines 10

Duplication

Lines 16
Ratio 100 %

Importance

Changes 0
Metric Value
cc 4
eloc 10
nc 3
nop 0
dl 16
loc 16
rs 9.2
c 0
b 0
f 0
1
<?php
2
class Jetpack_RelatedPosts {
3
	const VERSION = '20150408';
4
	const SHORTCODE = 'jetpack-related-posts';
5
6
	/**
7
	 * Creates and returns a static instance of Jetpack_RelatedPosts.
8
	 *
9
	 * @return Jetpack_RelatedPosts
10
	 */
11 View Code Duplication
	public static function init() {
12
		static $instance = NULL;
13
14
		if ( ! $instance ) {
15
			if ( class_exists('WPCOM_RelatedPosts') && method_exists( 'WPCOM_RelatedPosts', 'init' ) ) {
16
				$instance = WPCOM_RelatedPosts::init();
17
			} else {
18
				$instance = new Jetpack_RelatedPosts(
19
					get_current_blog_id(),
20
					Jetpack_Options::get_option( 'id' )
21
				);
22
			}
23
		}
24
25
		return $instance;
26
	}
27
28
	/**
29
	 * Creates and returns a static instance of Jetpack_RelatedPosts_Raw.
30
	 *
31
	 * @return Jetpack_RelatedPosts
32
	 */
33 View Code Duplication
	public static function init_raw() {
34
		static $instance = NULL;
35
36
		if ( ! $instance ) {
37
			if ( class_exists('WPCOM_RelatedPosts') && method_exists( 'WPCOM_RelatedPosts', 'init_raw' ) ) {
38
				$instance = WPCOM_RelatedPosts::init_raw();
39
			} else {
40
				$instance = new Jetpack_RelatedPosts_Raw(
41
					get_current_blog_id(),
42
					Jetpack_Options::get_option( 'id' )
43
				);
44
			}
45
		}
46
47
		return $instance;
48
	}
49
50
	protected $_blog_id_local;
51
	protected $_blog_id_wpcom;
52
	protected $_options;
53
	protected $_allow_feature_toggle;
54
	protected $_blog_charset;
55
	protected $_convert_charset;
56
	protected $_previous_post_id;
57
	protected $_found_shortcode = false;
58
59
	/**
60
	 * Constructor for Jetpack_RelatedPosts.
61
	 *
62
	 * @param int $blog_id_local
63
	 * @param int $blog_id_wpcom
64
	 * @uses get_option, add_action, apply_filters
65
	 * @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...
66
	 */
67
	public function __construct( $blog_id_local, $blog_id_wpcom ) {
68
		$this->_blog_id_local = $blog_id_local;
69
		$this->_blog_id_wpcom = $blog_id_wpcom;
70
		$this->_blog_charset = get_option( 'blog_charset' );
71
		$this->_convert_charset = ( function_exists( 'iconv' ) && ! preg_match( '/^utf\-?8$/i', $this->_blog_charset ) );
72
73
		add_action( 'admin_init', array( $this, 'action_admin_init' ) );
74
		add_action( 'wp', array( $this, 'action_frontend_init' ) );
75
76
		if ( ! class_exists( 'Jetpack_Media_Summary' ) ) {
77
			jetpack_require_lib( 'class.media-summary' );
78
		}
79
80
		// Add Related Posts to the REST API Post response.
81
		if ( function_exists( 'register_rest_field' ) ) {
82
			add_action( 'rest_api_init',  array( $this, 'rest_register_related_posts' ) );
83
		}
84
	}
85
86
	/**
87
	 * =================
88
	 * ACTIONS & FILTERS
89
	 * =================
90
	 */
91
92
	/**
93
	 * Add a checkbox field to Settings > Reading for enabling related posts.
94
	 *
95
	 * @action admin_init
96
	 * @uses add_settings_field, __, register_setting, add_action
97
	 * @return null
98
	 */
99
	public function action_admin_init() {
100
101
		// Add the setting field [jetpack_relatedposts] and place it in Settings > Reading
102
		add_settings_field( 'jetpack_relatedposts', '<span id="jetpack_relatedposts">' . __( 'Related posts', 'jetpack' ) . '</span>', array( $this, 'print_setting_html' ), 'reading' );
103
		register_setting( 'reading', 'jetpack_relatedposts', array( $this, 'parse_options' ) );
104
		add_action('admin_head', array( $this, 'print_setting_head' ) );
105
106
		if( 'options-reading.php' == $GLOBALS['pagenow'] ) {
107
			// Enqueue style for live preview on the reading settings page
108
			$this->_enqueue_assets( false, true );
109
		}
110
	}
111
112
	/**
113
	 * Load related posts assets if it's a elegiable front end page or execute search and return JSON if it's an endpoint request.
114
	 *
115
	 * @global $_GET
116
	 * @action wp
117
	 * @uses add_shortcode, get_the_ID
118
	 * @returns null
119
	 */
120
	public function action_frontend_init() {
121
		// Add a shortcode handler that outputs nothing, this gets overridden later if we can display related content
122
		add_shortcode( self::SHORTCODE, array( $this, 'get_target_html_unsupported' ) );
123
124
		if ( ! $this->_enabled_for_request() )
125
			return;
126
127
		if ( isset( $_GET['relatedposts'] ) ) {
128
			$excludes = array();
129
			if ( isset( $_GET['relatedposts_exclude'] ) ) {
130
				$excludes = explode( ',', $_GET['relatedposts_exclude'] );
131
			}
132
133
			$this->_action_frontend_init_ajax( $excludes );
134
		} else {
135
			if ( isset( $_GET['relatedposts_hit'], $_GET['relatedposts_origin'], $_GET['relatedposts_position'] ) ) {
136
				$this->_log_click( $_GET['relatedposts_origin'], get_the_ID(), $_GET['relatedposts_position'] );
137
				$this->_previous_post_id = (int) $_GET['relatedposts_origin'];
138
			}
139
140
			$this->_action_frontend_init_page();
141
		}
142
143
	}
144
145
	/**
146
	 * Render insertion point.
147
	 *
148
	 * @since 4.2.0
149
	 *
150
	 * @return string
151
	 */
152
	public function get_headline() {
153
		$options = $this->get_options();
154
155
		if ( $options['show_headline'] ) {
156
			$headline = sprintf(
157
				'<h3 class="jp-relatedposts-headline"><em>%s</em></h3>',
158
				esc_html( $options['headline'] )
159
			);
160
		} else {
161
			$headline = '';
162
		}
163
		return $headline;
164
	}
165
166
	/**
167
	 * Adds a target to the post content to load related posts into if a shortcode for it did not already exist.
168
	 *
169
	 * @filter the_content
170
	 * @param string $content
171
	 * @returns string
172
	 */
173
	public function filter_add_target_to_dom( $content ) {
174
		if ( !$this->_found_shortcode ) {
175
			$content .= "\n" . $this->get_target_html();
176
		}
177
178
		return $content;
179
	}
180
181
	/**
182
	 * Looks for our shortcode on the unfiltered content, this has to execute early.
183
	 *
184
	 * @filter the_content
185
	 * @param string $content
186
	 * @uses has_shortcode
187
	 * @returns string
188
	 */
189
	public function test_for_shortcode( $content ) {
190
		$this->_found_shortcode = has_shortcode( $content, self::SHORTCODE );
191
192
		return $content;
193
	}
194
195
	/**
196
	 * Returns the HTML for the related posts section.
197
	 *
198
	 * @uses esc_html__, apply_filters
199
	 * @returns string
200
	 */
201
	public function get_target_html() {
202
		require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
203
		if ( Jetpack_Sync_Settings::is_syncing() ) {
204
			return '';
205
		}
206
207
		/**
208
		 * Filter the Related Posts headline.
209
		 *
210
		 * @module related-posts
211
		 *
212
		 * @since 3.0.0
213
		 *
214
		 * @param string $headline Related Posts heading.
215
		 */
216
		$headline = apply_filters( 'jetpack_relatedposts_filter_headline', $this->get_headline() );
217
218
		if ( $this->_previous_post_id ) {
219
			$exclude = "data-exclude='{$this->_previous_post_id}'";
220
		} else {
221
			$exclude = "";
222
		}
223
224
		return <<<EOT
225
<div id='jp-relatedposts' class='jp-relatedposts' $exclude>
226
	$headline
227
</div>
228
EOT;
229
	}
230
231
	/**
232
	 * 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.
233
	 *
234
	 * @returns string
235
	 */
236
	public function get_target_html_unsupported() {
237
		require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
238
		if ( Jetpack_Sync_Settings::is_syncing() ) {
239
			return '';
240
		}
241
		return "\n\n<!-- Jetpack Related Posts is not supported in this context. -->\n\n";
242
	}
243
244
	/**
245
	 * ========================
246
	 * PUBLIC UTILITY FUNCTIONS
247
	 * ========================
248
	 */
249
250
	/**
251
	 * Gets options set for Jetpack_RelatedPosts and merge with defaults.
252
	 *
253
	 * @uses Jetpack_Options::get_option, apply_filters
254
	 * @return array
255
	 */
256
	public function get_options() {
257
		if ( null === $this->_options ) {
258
			$this->_options = Jetpack_Options::get_option( 'relatedposts', array() );
259
			if ( ! is_array( $this->_options ) )
260
				$this->_options = array();
261
			if ( ! isset( $this->_options['enabled'] ) )
262
				$this->_options['enabled'] = true;
263
			if ( ! isset( $this->_options['show_headline'] ) )
264
				$this->_options['show_headline'] = true;
265
			if ( ! isset( $this->_options['show_thumbnails'] ) )
266
				$this->_options['show_thumbnails'] = false;
267
			if ( ! isset( $this->_options['show_date'] ) ) {
268
				$this->_options['show_date'] = true;
269
			}
270
			if ( ! isset( $this->_options['show_context'] ) ) {
271
				$this->_options['show_context'] = true;
272
			}
273
			if ( ! isset( $this->_options['layout'] ) ) {
274
				$this->_options['layout'] = 'grid';
275
			}
276
			if ( ! isset( $this->_options['headline'] ) ) {
277
				$this->_options['headline'] = esc_html__( 'Related', 'jetpack' );
278
			}
279
			if ( empty( $this->_options['size'] ) || (int)$this->_options['size'] < 1 )
280
				$this->_options['size'] = 3;
281
282
			/**
283
			 * Filter Related Posts basic options.
284
			 *
285
			 * @module related-posts
286
			 *
287
			 * @since 2.8.0
288
			 *
289
			 * @param array $this->_options Array of basic Related Posts options.
290
			 */
291
			$this->_options = apply_filters( 'jetpack_relatedposts_filter_options', $this->_options );
292
		}
293
294
		return $this->_options;
295
	}
296
	
297
	public function get_option( $option_name ) {
298
		$options = $this->get_options();
299
		
300
		if ( isset( $options[ $option_name ] ) ) {
301
			return $options[ $option_name ];
302
		}
303
		
304
		return false;
305
	}
306
307
	/**
308
	 * Parses input and returns normalized options array.
309
	 *
310
	 * @param array $input
311
	 * @uses self::get_options
312
	 * @return array
313
	 */
314
	public function parse_options( $input ) {
315
		$current = $this->get_options();
316
317
		if ( !is_array( $input ) )
318
			$input = array();
319
320
		if ( isset( $input['enabled'] ) && '1' == $input['enabled'] ) {
321
			$current['enabled'] = true;
322
			$current['show_headline'] = ( isset( $input['show_headline'] ) && '1' == $input['show_headline'] );
323
			$current['show_thumbnails'] = ( isset( $input['show_thumbnails'] ) && '1' == $input['show_thumbnails'] );
324
			$current['show_date'] = ( isset( $input['show_date'] ) && '1' == $input['show_date'] );
325
			$current['show_context'] = ( isset( $input['show_context'] ) && '1' == $input['show_context'] );
326
			$current['layout'] = isset( $input['layout'] ) && in_array( $input['layout'], array( 'grid', 'list' ), true ) ? $input['layout'] : 'grid';
327
			$current['headline'] = isset( $input['headline'] ) ? $input['headline'] : esc_html__( 'Related', 'jetpack' );
328
		} else {
329
			$current['enabled'] = false;
330
		}
331
332
		if ( isset( $input['size'] ) && (int)$input['size'] > 0 )
333
			$current['size'] = (int)$input['size'];
334
		else
335
			$current['size'] = null;
336
337
		return $current;
338
	}
339
340
	/**
341
	 * HTML for admin settings page.
342
	 *
343
	 * @uses self::get_options, checked, esc_html__
344
	 * @returns null
345
	 */
346
	public function print_setting_html() {
347
		$options = $this->get_options();
348
349
		$ui_settings_template = <<<EOT
350
<ul id="settings-reading-relatedposts-customize">
351
	<li>
352
		<label><input name="jetpack_relatedposts[show_headline]" type="checkbox" value="1" %s /> %s</label>
353
	</li>
354
	<li>
355
		<label><input name="jetpack_relatedposts[show_thumbnails]" type="checkbox" value="1" %s /> %s</label>
356
	</li>
357
	<li>
358
		<label><input name="jetpack_relatedposts[show_date]" type="checkbox" value="1" %s /> %s</label>
359
	</li>
360
	<li>
361
		<label><input name="jetpack_relatedposts[show_context]" type="checkbox" value="1" %s /> %s</label>
362
	</li>
363
</ul>
364
<div id='settings-reading-relatedposts-preview'>
365
	%s
366
	<div id="jp-relatedposts" class="jp-relatedposts"></div>
367
</div>
368
EOT;
369
		$ui_settings = sprintf(
370
			$ui_settings_template,
371
			checked( $options['show_headline'], true, false ),
372
			esc_html__( 'Show a "Related" header to more clearly separate the related section from posts', 'jetpack' ),
373
			checked( $options['show_thumbnails'], true, false ),
374
			esc_html__( 'Use a large and visually striking layout', 'jetpack' ),
375
			checked( $options['show_date'], true, false ),
376
			esc_html__( 'Show entry date', 'jetpack' ),
377
			checked( $options['show_context'], true, false ),
378
			esc_html__( 'Show context (category or tag)', 'jetpack' ),
379
			esc_html__( 'Preview:', 'jetpack' )
380
		);
381
382
		if ( !$this->_allow_feature_toggle() ) {
383
			$template = <<<EOT
384
<input type="hidden" name="jetpack_relatedposts[enabled]" value="1" />
385
%s
386
EOT;
387
			printf(
388
				$template,
389
				$ui_settings
390
			);
391
		} else {
392
			$template = <<<EOT
393
<ul id="settings-reading-relatedposts">
394
	<li>
395
		<label><input type="radio" name="jetpack_relatedposts[enabled]" value="0" class="tog" %s /> %s</label>
396
	</li>
397
	<li>
398
		<label><input type="radio" name="jetpack_relatedposts[enabled]" value="1" class="tog" %s /> %s</label>
399
		%s
400
	</li>
401
</ul>
402
EOT;
403
			printf(
404
				$template,
405
				checked( $options['enabled'], false, false ),
406
				esc_html__( 'Hide related content after posts', 'jetpack' ),
407
				checked( $options['enabled'], true, false ),
408
				esc_html__( 'Show related content after posts', 'jetpack' ),
409
				$ui_settings
410
			);
411
		}
412
	}
413
414
	/**
415
	 * Head JS/CSS for admin settings page.
416
	 *
417
	 * @uses esc_html__
418
	 * @returns null
419
	 */
420
	public function print_setting_head() {
421
422
		// only dislay the Related Posts JavaScript on the Reading Settings Admin Page
423
		$current_screen =  get_current_screen();
424
425
		if ( is_null( $current_screen ) ) {
426
			return;
427
		}
428
429
		if( 'options-reading' != $current_screen->id )
430
			return;
431
432
		$related_headline = sprintf(
433
			'<h3 class="jp-relatedposts-headline"><em>%s</em></h3>',
434
			esc_html__( 'Related', 'jetpack' )
435
		);
436
437
		$href_params = 'class="jp-relatedposts-post-a" href="#jetpack_relatedposts" rel="nofollow" data-origin="0" data-position="0"';
438
		$related_with_images = <<<EOT
439
<div class="jp-relatedposts-items jp-relatedposts-items-visual">
440
	<div class="jp-relatedposts-post jp-relatedposts-post0 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
441
		<a $href_params>
442
			<img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2014/08/1-wpios-ipad-3-1-viewsite.png?w=350&amp;h=200&amp;crop=1" width="350" alt="Big iPhone/iPad Update Now Available" scale="0">
443
		</a>
444
		<h4 class="jp-relatedposts-post-title">
445
			<a $href_params>Big iPhone/iPad Update Now Available</a>
446
		</h4>
447
		<p class="jp-relatedposts-post-excerpt">Big iPhone/iPad Update Now Available</p>
448
		<p class="jp-relatedposts-post-context">In "Mobile"</p>
449
	</div>
450
	<div class="jp-relatedposts-post jp-relatedposts-post1 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
451
		<a $href_params>
452
			<img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2014/08/wordpress-com-news-wordpress-for-android-ui-update2.jpg?w=350&amp;h=200&amp;crop=1" width="350" alt="The WordPress for Android App Gets a Big Facelift" scale="0">
453
		</a>
454
		<h4 class="jp-relatedposts-post-title">
455
			<a $href_params>The WordPress for Android App Gets a Big Facelift</a>
456
		</h4>
457
		<p class="jp-relatedposts-post-excerpt">The WordPress for Android App Gets a Big Facelift</p>
458
		<p class="jp-relatedposts-post-context">In "Mobile"</p>
459
	</div>
460
	<div class="jp-relatedposts-post jp-relatedposts-post2 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
461
		<a $href_params>
462
			<img class="jp-relatedposts-post-img" src="https://jetpackme.files.wordpress.com/2014/08/videopresswedding.jpg?w=350&amp;h=200&amp;crop=1" width="350" alt="Upgrade Focus: VideoPress For Weddings" scale="0">
463
		</a>
464
		<h4 class="jp-relatedposts-post-title">
465
			<a $href_params>Upgrade Focus: VideoPress For Weddings</a>
466
		</h4>
467
		<p class="jp-relatedposts-post-excerpt">Upgrade Focus: VideoPress For Weddings</p>
468
		<p class="jp-relatedposts-post-context">In "Upgrade"</p>
469
	</div>
470
</div>
471
EOT;
472
		$related_with_images = str_replace( "\n", '', $related_with_images );
473
		$related_without_images = <<<EOT
474
<div class="jp-relatedposts-items jp-relatedposts-items-minimal">
475
	<p class="jp-relatedposts-post jp-relatedposts-post0" data-post-id="0" data-post-format="image">
476
		<span class="jp-relatedposts-post-title"><a $href_params>Big iPhone/iPad Update Now Available</a></span>
477
		<span class="jp-relatedposts-post-context">In "Mobile"</span>
478
	</p>
479
	<p class="jp-relatedposts-post jp-relatedposts-post1" data-post-id="0" data-post-format="image">
480
		<span class="jp-relatedposts-post-title"><a $href_params>The WordPress for Android App Gets a Big Facelift</a></span>
481
		<span class="jp-relatedposts-post-context">In "Mobile"</span>
482
	</p>
483
	<p class="jp-relatedposts-post jp-relatedposts-post2" data-post-id="0" data-post-format="image">
484
		<span class="jp-relatedposts-post-title"><a $href_params>Upgrade Focus: VideoPress For Weddings</a></span>
485
		<span class="jp-relatedposts-post-context">In "Upgrade"</span>
486
	</p>
487
</div>
488
EOT;
489
		$related_without_images = str_replace( "\n", '', $related_without_images );
490
491
		if ( $this->_allow_feature_toggle() ) {
492
			$extra_css = '#settings-reading-relatedposts-customize { padding-left:2em; margin-top:.5em; }';
493
		} else {
494
			$extra_css = '';
495
		}
496
497
		echo <<<EOT
498
<style type="text/css">
499
	#settings-reading-relatedposts .disabled { opacity:.5; filter:Alpha(opacity=50); }
500
	#settings-reading-relatedposts-preview .jp-relatedposts { background:#fff; padding:.5em; width:75%; }
501
	$extra_css
502
</style>
503
<script type="text/javascript">
504
	jQuery( document ).ready( function($) {
505
		var update_ui = function() {
506
			var is_enabled = true;
507
			if ( 'radio' == $( 'input[name="jetpack_relatedposts[enabled]"]' ).attr('type') ) {
508
				if ( '0' == $( 'input[name="jetpack_relatedposts[enabled]"]:checked' ).val() ) {
509
					is_enabled = false;
510
				}
511
			}
512
			if ( is_enabled ) {
513
				$( '#settings-reading-relatedposts-customize' )
514
					.removeClass( 'disabled' )
515
					.find( 'input' )
516
					.attr( 'disabled', false );
517
				$( '#settings-reading-relatedposts-preview' )
518
					.removeClass( 'disabled' );
519
			} else {
520
				$( '#settings-reading-relatedposts-customize' )
521
					.addClass( 'disabled' )
522
					.find( 'input' )
523
					.attr( 'disabled', true );
524
				$( '#settings-reading-relatedposts-preview' )
525
					.addClass( 'disabled' );
526
			}
527
		};
528
529
		var update_preview = function() {
530
			var html = '';
531
			if ( $( 'input[name="jetpack_relatedposts[show_headline]"]:checked' ).length ) {
532
				html += '$related_headline';
533
			}
534
			if ( $( 'input[name="jetpack_relatedposts[show_thumbnails]"]:checked' ).length ) {
535
				html += '$related_with_images';
536
			} else {
537
				html += '$related_without_images';
538
			}
539
			$( '#settings-reading-relatedposts-preview .jp-relatedposts' ).html( html );
540
			if ( $( 'input[name="jetpack_relatedposts[show_date]"]:checked' ).length ) {
541
				$( '.jp-relatedposts-post-title' ).each( function() {
542
					$( this ).after( $( '<span>August 8, 2005</span>' ) );
543
				} );
544
			}
545
			if ( $( 'input[name="jetpack_relatedposts[show_context]"]:checked' ).length ) {
546
				$( '.jp-relatedposts-post-context' ).show();
547
			} else {
548
				$( '.jp-relatedposts-post-context' ).hide();
549
			}
550
			$( '#settings-reading-relatedposts-preview .jp-relatedposts' ).show();
551
		};
552
553
		// Update on load
554
		update_preview();
555
		update_ui();
556
557
		// Update on change
558
		$( '#settings-reading-relatedposts-customize input' )
559
			.change( update_preview );
560
		$( '#settings-reading-relatedposts' )
561
			.find( 'input.tog' )
562
			.change( update_ui );
563
	});
564
</script>
565
EOT;
566
	}
567
568
	/**
569
	 * Gets an array of related posts that match the given post_id.
570
	 *
571
	 * @param int $post_id
572
	 * @param array $args - params to use when building ElasticSearch filters to narrow down the search domain.
573
	 * @uses self::get_options, get_post_type, wp_parse_args, apply_filters
574
	 * @return array
575
	 */
576
	public function get_for_post_id( $post_id, array $args ) {
577
		$options = $this->get_options();
578
579
		if ( ! empty( $args['size'] ) )
580
			$options['size'] = $args['size'];
581
582
		if ( ! $options['enabled'] || 0 == (int)$post_id || empty( $options['size'] ) )
583
			return array();
584
585
		$defaults = array(
586
			'size' => (int)$options['size'],
587
			'post_type' => get_post_type( $post_id ),
588
			'post_formats' => array(),
589
			'has_terms' => array(),
590
			'date_range' => array(),
591
			'exclude_post_ids' => array(),
592
		);
593
		$args = wp_parse_args( $args, $defaults );
594
		/**
595
		 * Filter the arguments used to retrieve a list of Related Posts.
596
		 *
597
		 * @module related-posts
598
		 *
599
		 * @since 2.8.0
600
		 *
601
		 * @param array $args Array of options to retrieve Related Posts.
602
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
603
		 */
604
		$args = apply_filters( 'jetpack_relatedposts_filter_args', $args, $post_id );
605
606
		$filters = $this->_get_es_filters_from_args( $post_id, $args );
607
		/**
608
		 * Filter ElasticSearch options used to calculate Related Posts.
609
		 *
610
		 * @module related-posts
611
		 *
612
		 * @since 2.8.0
613
		 *
614
		 * @param array $filters Array of ElasticSearch filters based on the post_id and args.
615
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
616
		 */
617
		$filters = apply_filters( 'jetpack_relatedposts_filter_filters', $filters, $post_id );
618
619
		$results = $this->_get_related_posts( $post_id, $args['size'], $filters );
620
		/**
621
		 * Filter the array of related posts matched by ElasticSearch.
622
		 *
623
		 * @module related-posts
624
		 *
625
		 * @since 2.8.0
626
		 *
627
		 * @param array $results Array of related posts matched by ElasticSearch.
628
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
629
		 */
630
		return apply_filters( 'jetpack_relatedposts_returned_results', $results, $post_id );
631
	}
632
633
	/**
634
	 * =========================
635
	 * PRIVATE UTILITY FUNCTIONS
636
	 * =========================
637
	 */
638
639
	/**
640
	 * Creates an array of ElasticSearch filters based on the post_id and args.
641
	 *
642
	 * @param int $post_id
643
	 * @param array $args
644
	 * @uses apply_filters, get_post_types, get_post_format_strings
645
	 * @return array
646
	 */
647
	protected function _get_es_filters_from_args( $post_id, array $args ) {
648
		$filters = array();
649
650
		/**
651
		 * Filter the terms used to search for Related Posts.
652
		 *
653
		 * @module related-posts
654
		 *
655
		 * @since 2.8.0
656
		 *
657
		 * @param array $args['has_terms'] Array of terms associated to the Related Posts.
658
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
659
		 */
660
		$args['has_terms'] = apply_filters( 'jetpack_relatedposts_filter_has_terms', $args['has_terms'], $post_id );
661
		if ( ! empty( $args['has_terms'] ) ) {
662
			foreach( (array)$args['has_terms'] as $term ) {
663
				if ( mb_strlen( $term->taxonomy ) ) {
664
					switch ( $term->taxonomy ) {
665
						case 'post_tag':
666
							$tax_fld = 'tag.slug';
667
							break;
668
						case 'category':
669
							$tax_fld = 'category.slug';
670
							break;
671
						default:
672
							$tax_fld = 'taxonomy.' . $term->taxonomy . '.slug';
673
							break;
674
					}
675
					$filters[] = array( 'term' => array( $tax_fld => $term->slug ) );
676
				}
677
			}
678
		}
679
680
		/**
681
		 * Filter the Post Types where we search Related Posts.
682
		 *
683
		 * @module related-posts
684
		 *
685
		 * @since 2.8.0
686
		 *
687
		 * @param array $args['post_type'] Array of Post Types.
688
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
689
		 */
690
		$args['post_type'] = apply_filters( 'jetpack_relatedposts_filter_post_type', $args['post_type'], $post_id );
691
		$valid_post_types = get_post_types();
692
		if ( is_array( $args['post_type'] ) ) {
693
			$sanitized_post_types = array();
694
			foreach ( $args['post_type'] as $pt ) {
695
				if ( in_array( $pt, $valid_post_types ) )
696
					$sanitized_post_types[] = $pt;
697
			}
698
			if ( ! empty( $sanitized_post_types ) )
699
				$filters[] = array( 'terms' => array( 'post_type' => $sanitized_post_types ) );
700
		} else if ( in_array( $args['post_type'], $valid_post_types ) && 'all' != $args['post_type'] ) {
701
			$filters[] = array( 'term' => array( 'post_type' => $args['post_type'] ) );
702
		}
703
704
		/**
705
		 * Filter the Post Formats where we search Related Posts.
706
		 *
707
		 * @module related-posts
708
		 *
709
		 * @since 3.3.0
710
		 *
711
		 * @param array $args['post_formats'] Array of Post Formats.
712
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
713
		 */
714
		$args['post_formats'] = apply_filters( 'jetpack_relatedposts_filter_post_formats', $args['post_formats'], $post_id );
715
		$valid_post_formats = get_post_format_strings();
716
		$sanitized_post_formats = array();
717
		foreach ( $args['post_formats'] as $pf ) {
718
			if ( array_key_exists( $pf, $valid_post_formats ) ) {
719
				$sanitized_post_formats[] = $pf;
720
			}
721
		}
722
		if ( ! empty( $sanitized_post_formats ) ) {
723
			$filters[] = array( 'terms' => array( 'post_format' => $sanitized_post_formats ) );
724
		}
725
726
		/**
727
		 * Filter the date range used to search Related Posts.
728
		 *
729
		 * @module related-posts
730
		 *
731
		 * @since 2.8.0
732
		 *
733
		 * @param array $args['date_range'] Array of a month interval where we search Related Posts.
734
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
735
		 */
736
		$args['date_range'] = apply_filters( 'jetpack_relatedposts_filter_date_range', $args['date_range'], $post_id );
737
		if ( is_array( $args['date_range'] ) && ! empty( $args['date_range'] ) ) {
738
			$args['date_range'] = array_map( 'intval', $args['date_range'] );
739
			if ( !empty( $args['date_range']['from'] ) && !empty( $args['date_range']['to'] ) ) {
740
				$filters[] = array(
741
					'range' => array(
742
						'date_gmt' => $this->_get_coalesced_range( $args['date_range'] ),
743
					)
744
				);
745
			}
746
		}
747
748
		/**
749
		 * Filter the Post IDs excluded from appearing in Related Posts.
750
		 *
751
		 * @module related-posts
752
		 *
753
		 * @since 2.9.0
754
		 *
755
		 * @param array $args['exclude_post_ids'] Array of Post IDs.
756
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
757
		 */
758
		$args['exclude_post_ids'] = apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', $args['exclude_post_ids'], $post_id );
759
		if ( !empty( $args['exclude_post_ids'] ) && is_array( $args['exclude_post_ids'] ) ) {
760
			foreach ( $args['exclude_post_ids'] as $exclude_post_id) {
761
				$exclude_post_id = (int)$exclude_post_id;
762
763
				if ( $exclude_post_id > 0 )
764
					$filters[] = array( 'not' => array( 'term' => array( 'post_id' => $exclude_post_id ) ) );
765
			}
766
		}
767
768
		return $filters;
769
	}
770
771
	/**
772
	 * Takes a range and coalesces it into a month interval bracketed by a time as determined by the blog_id to enhance caching.
773
	 *
774
	 * @param array $date_range
775
	 * @return array
776
	 */
777
	protected function _get_coalesced_range( array $date_range ) {
778
		$now = time();
779
		$coalesce_time = $this->_blog_id_wpcom % 86400;
780
		$current_time = $now - strtotime( 'today', $now );
781
782
		if ( $current_time < $coalesce_time && '01' == date( 'd', $now ) ) {
783
			// Move back 1 period
784
			return array(
785
				'from' => date( 'Y-m-01', strtotime( '-1 month', $date_range['from'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),
786
				'to'   => date( 'Y-m-01', $date_range['to'] ) . ' ' . date( 'H:i:s', $coalesce_time ),
787
			);
788
		} else {
789
			// Use current period
790
			return array(
791
				'from' => date( 'Y-m-01', $date_range['from'] ) . ' ' . date( 'H:i:s', $coalesce_time ),
792
				'to'   => date( 'Y-m-01', strtotime( '+1 month', $date_range['to'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),
793
			);
794
		}
795
	}
796
797
	/**
798
	 * Generate and output ajax response for related posts API call.
799
	 * NOTE: Calls exit() to end all further processing after payload has been outputed.
800
	 *
801
	 * @param array $excludes array of post_ids to exclude
802
	 * @uses send_nosniff_header, self::get_for_post_id, get_the_ID
803
	 * @return null
804
	 */
805
	protected function _action_frontend_init_ajax( array $excludes ) {
806
		define( 'DOING_AJAX', true );
807
808
		header( 'Content-type: application/json; charset=utf-8' ); // JSON can only be UTF-8
809
		send_nosniff_header();
810
811
		$options = $this->get_options();
812
813
		if ( isset( $_GET['jetpackrpcustomize'] ) ) {
814
815
			// If we're in the customizer, add dummy content.
816
			$date_now = current_time( get_option( 'date_format' ) );
817
			$related_posts = array(
818
				array(
819
					'id'       => - 1,
820
					'url'      => 'https://jetpackme.files.wordpress.com/2014/08/1-wpios-ipad-3-1-viewsite.png?w=350&h=200&crop=1',
821
					'url_meta' => array(
822
						'origin'   => 0,
823
						'position' => 0
824
					),
825
					'title'    => esc_html__( 'Big iPhone/iPad Update Now Available', 'jetpack' ),
826
					'date'     => $date_now,
827
					'format'   => false,
828
					'excerpt'  => esc_html__( 'It is that time of the year when devices are shiny again.', 'jetpack' ),
829
					'rel'      => 'nofollow',
830
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
831
					'img'      => array(
832
						'src'    => 'https://jetpackme.files.wordpress.com/2014/08/1-wpios-ipad-3-1-viewsite.png?w=350&h=200&crop=1',
833
						'width'  => 350,
834
						'height' => 200
835
					),
836
					'classes'  => array()
837
				),
838
				array(
839
					'id'       => - 1,
840
					'url'      => 'https://jetpackme.files.wordpress.com/2014/08/wordpress-com-news-wordpress-for-android-ui-update2.jpg?w=350&h=200&crop=1',
841
					'url_meta' => array(
842
						'origin'   => 0,
843
						'position' => 0
844
					),
845
					'title'    => esc_html__( 'The WordPress for Android App Gets a Big Facelift', 'jetpack' ),
846
					'date'     => $date_now,
847
					'format'   => false,
848
					'excerpt'  => esc_html__( 'Writing is new again in Android with the new WordPress app.', 'jetpack' ),
849
					'rel'      => 'nofollow',
850
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
851
					'img'      => array(
852
						'src'    => 'https://jetpackme.files.wordpress.com/2014/08/wordpress-com-news-wordpress-for-android-ui-update2.jpg?w=350&h=200&crop=1',
853
						'width'  => 350,
854
						'height' => 200
855
					),
856
					'classes'  => array()
857
				),
858
				array(
859
					'id'       => - 1,
860
					'url'      => 'https://jetpackme.files.wordpress.com/2014/08/videopresswedding.jpg?w=350&h=200&crop=1',
861
					'url_meta' => array(
862
						'origin'   => 0,
863
						'position' => 0
864
					),
865
					'title'    => esc_html__( 'Upgrade Focus, VideoPress for weddings', 'jetpack' ),
866
					'date'     => $date_now,
867
					'format'   => false,
868
					'excerpt'  => esc_html__( 'Weddings are in the spotlight now with VideoPress for weddings.', 'jetpack' ),
869
					'rel'      => 'nofollow',
870
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
871
					'img'      => array(
872
						'src'    => 'https://jetpackme.files.wordpress.com/2014/08/videopresswedding.jpg?w=350&h=200&crop=1',
873
						'width'  => 350,
874
						'height' => 200
875
					),
876
					'classes'  => array()
877
				),
878
			);
879
880
			for ( $total = 0; $total < $options['size'] - 3; $total++ ) {
881
				$related_posts[] = $related_posts[ $total ];
882
			}
883
884
			$current_post = get_post();
885
886
			// Exclude current post after filtering to make sure it's excluded and not lost during filtering.
887
			$excluded_posts = array_merge(
888
				/** This filter is already documented in modules/related-posts/jetpack-related-posts.php */
889
				apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', array() ),
890
				array( $current_post->ID )
891
			);
892
893
			// Fetch posts with featured image.
894
			$with_post_thumbnails = get_posts( array(
895
				'posts_per_page'   => $options['size'],
896
				'post__not_in'     => $excluded_posts,
897
				'post_type'        => $current_post->post_type,
898
				'meta_key'         => '_thumbnail_id',
899
				'suppress_filters' => false,
900
			) );
901
902
			// If we don't have enough, fetch posts without featured image.
903
			if ( 0 < ( $more = $options['size'] - count( $with_post_thumbnails ) ) ) {
904
				$no_post_thumbnails = get_posts( array(
905
					'posts_per_page'  => $more,
906
					'post__not_in'    => $excluded_posts,
907
					'post_type'       => $current_post->post_type,
908
					'meta_query' => array(
909
						array(
910
							'key'     => '_thumbnail_id',
911
							'compare' => 'NOT EXISTS',
912
						),
913
					),
914
					'suppress_filters' => false,
915
				) );
916
			} else {
917
				$no_post_thumbnails = array();
918
			}
919
920
			foreach ( array_merge( $with_post_thumbnails, $no_post_thumbnails ) as $index => $real_post ) {
921
				$related_posts[ $index ]['id']      = $real_post->ID;
922
				$related_posts[ $index ]['url']     = esc_url( get_permalink( $real_post ) );
923
				$related_posts[ $index ]['title']   = $this->_to_utf8( $this->_get_title( $real_post->post_title, $real_post->post_content ) );
924
				$related_posts[ $index ]['date']    = get_the_date( '', $real_post );
925
				$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' );
926
				$related_posts[ $index ]['img']     = $this->_generate_related_post_image_params( $real_post->ID );
927
				$related_posts[ $index ]['context'] = $this->_generate_related_post_context( $real_post->ID );
928
			}
929
		} else {
930
			$related_posts = $this->get_for_post_id(
931
				get_the_ID(),
932
				array(
933
					'exclude_post_ids' => $excludes,
934
				)
935
			);
936
		}
937
938
		$response = array(
939
			'version' => self::VERSION,
940
			'show_thumbnails' => (bool) $options['show_thumbnails'],
941
			'show_date' => (bool) $options['show_date'],
942
			'show_context' => (bool) $options['show_context'],
943
			'layout' => (string) $options['layout'],
944
			'headline' => (string) $options['headline'],
945
			'items' => array(),
946
		);
947
948
		if ( count( $related_posts ) == $options['size'] )
949
			$response['items'] = $related_posts;
950
951
		echo json_encode( $response );
952
953
		exit();
0 ignored issues
show
Coding Style Compatibility introduced by
The method _action_frontend_init_ajax() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
954
	}
955
956
	/**
957
	 * Returns a UTF-8 encoded array of post information for the given post_id
958
	 *
959
	 * @param int $post_id
960
	 * @param int $position
961
	 * @param int $origin The post id that this is related to
962
	 * @uses get_post, get_permalink, remove_query_arg, get_post_format, apply_filters
963
	 * @return array
964
	 */
965
	protected function _get_related_post_data_for_post( $post_id, $position, $origin ) {
966
		$post = get_post( $post_id );
967
968
		return array(
969
			'id' => $post->ID,
970
			'url' => get_permalink( $post->ID ),
971
			'url_meta' => array( 'origin' => $origin, 'position' => $position ),
972
			'title' => $this->_to_utf8( $this->_get_title( $post->post_title, $post->post_content ) ),
973
			'date' => get_the_date( '', $post->ID ),
974
			'format' => get_post_format( $post->ID ),
975
			'excerpt' => html_entity_decode( $this->_to_utf8( $this->_get_excerpt( $post->post_excerpt, $post->post_content ) ), ENT_QUOTES, 'UTF-8' ),
976
			/**
977
			 * Filters the rel attribute for the Related Posts' links.
978
			 *
979
			 * @module related-posts
980
			 *
981
			 * @since 3.7.0
982
			 *
983
			 * @param string nofollow Link rel attribute for Related Posts' link. Default is nofollow.
984
			 * @param int $post->ID Post ID.
985
			 */
986
			'rel' => apply_filters( 'jetpack_relatedposts_filter_post_link_rel', 'nofollow', $post->ID ),
987
			/**
988
			 * Filter the context displayed below each Related Post.
989
			 *
990
			 * @module related-posts
991
			 *
992
			 * @since 3.0.0
993
			 *
994
			 * @param string $this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ) Context displayed below each related post.
995
			 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
996
			 */
997
			'context' => apply_filters(
998
				'jetpack_relatedposts_filter_post_context',
999
				$this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ),
1000
				$post->ID
1001
			),
1002
			'img' => $this->_generate_related_post_image_params( $post->ID ),
1003
			/**
1004
			 * Filter the post css classes added on HTML markup.
1005
			 *
1006
			 * @module related-posts
1007
			 *
1008
			 * @since 3.8.0
1009
			 *
1010
			 * @param array array() CSS classes added on post HTML markup.
1011
			 * @param string $post_id Post ID.
1012
			 */
1013
			'classes' => apply_filters(
1014
				'jetpack_relatedposts_filter_post_css_classes',
1015
				array(),
1016
				$post->ID
1017
			),
1018
		);
1019
	}
1020
1021
	/**
1022
	 * Returns either the title or a small excerpt to use as title for post.
1023
	 *
1024
	 * @param string $post_title
1025
	 * @param string $post_content
1026
	 * @uses strip_shortcodes, wp_trim_words, __
1027
	 * @return string
1028
	 */
1029
	protected function _get_title( $post_title, $post_content ) {
1030
		if ( ! empty( $post_title ) ) {
1031
			return wp_strip_all_tags( $post_title );
1032
		}
1033
1034
		$post_title = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post_content ) ), 5, '…' );
1035
		if ( ! empty( $post_title ) ) {
1036
			return $post_title;
1037
		}
1038
1039
		return __( 'Untitled Post', 'jetpack' );
1040
	}
1041
1042
	/**
1043
	 * Returns a plain text post excerpt for title attribute of links.
1044
	 *
1045
	 * @param string $post_excerpt
1046
	 * @param string $post_content
1047
	 * @uses strip_shortcodes, wp_strip_all_tags, wp_trim_words
1048
	 * @return string
1049
	 */
1050
	protected function _get_excerpt( $post_excerpt, $post_content ) {
1051
		if ( empty( $post_excerpt ) )
1052
			$excerpt = $post_content;
1053
		else
1054
			$excerpt = $post_excerpt;
1055
1056
		return wp_trim_words( wp_strip_all_tags( strip_shortcodes( $excerpt ) ), 50, '…' );
1057
	}
1058
1059
	/**
1060
	 * Generates the thumbnail image to be used for the post. Uses the
1061
	 * image as returned by Jetpack_PostImages::get_image()
1062
	 *
1063
	 * @param int $post_id
1064
	 * @uses self::get_options, apply_filters, Jetpack_PostImages::get_image, Jetpack_PostImages::fit_image_url
1065
	 * @return string
1066
	 */
1067
	protected function _generate_related_post_image_params( $post_id ) {
1068
		$options = $this->get_options();
1069
		$image_params = array(
1070
			'src' => '',
1071
			'width' => 0,
1072
			'height' => 0,
1073
		);
1074
1075
		if ( ! $options['show_thumbnails'] ) {
1076
			return $image_params;
1077
		}
1078
1079
		/**
1080
		 * Filter the size of the Related Posts images.
1081
		 *
1082
		 * @module related-posts
1083
		 *
1084
		 * @since 2.8.0
1085
		 *
1086
		 * @param array array( 'width' => 350, 'height' => 200 ) Size of the images displayed below each Related Post.
1087
		 */
1088
		$thumbnail_size = apply_filters(
1089
			'jetpack_relatedposts_filter_thumbnail_size',
1090
			array( 'width' => 350, 'height' => 200 )
1091
		);
1092
		if ( !is_array( $thumbnail_size ) ) {
1093
			$thumbnail_size = array(
1094
				'width' => (int)$thumbnail_size,
1095
				'height' => (int)$thumbnail_size
1096
			);
1097
		}
1098
1099
		// Try to get post image
1100
		if ( class_exists( 'Jetpack_PostImages' ) ) {
1101
			$img_url = '';
1102
			$post_image = Jetpack_PostImages::get_image(
1103
				$post_id,
1104
				$thumbnail_size
1105
			);
1106
1107
			if ( is_array($post_image) ) {
1108
				$img_url = $post_image['src'];
1109
			} elseif ( class_exists( 'Jetpack_Media_Summary' ) ) {
1110
				$media = Jetpack_Media_Summary::get( $post_id );
1111
1112
				if ( is_array($media) && !empty( $media['image'] ) ) {
1113
					$img_url = $media['image'];
1114
				}
1115
			}
1116
1117
			if ( !empty( $img_url ) ) {
1118
				$image_params['width'] = $thumbnail_size['width'];
1119
				$image_params['height'] = $thumbnail_size['height'];
1120
				$image_params['src'] = Jetpack_PostImages::fit_image_url(
1121
					$img_url,
1122
					$thumbnail_size['width'],
1123
					$thumbnail_size['height']
1124
				);
1125
			}
1126
		}
1127
1128
		return $image_params;
1129
	}
1130
1131
	/**
1132
	 * Returns the string UTF-8 encoded
1133
	 *
1134
	 * @param string $text
1135
	 * @return string
1136
	 */
1137
	protected function _to_utf8( $text ) {
1138
		if ( $this->_convert_charset ) {
1139
			return iconv( $this->_blog_charset, 'UTF-8', $text );
1140
		} else {
1141
			return $text;
1142
		}
1143
	}
1144
1145
	/**
1146
	 * =============================================
1147
	 * PROTECTED UTILITY FUNCTIONS EXTENDED BY WPCOM
1148
	 * =============================================
1149
	 */
1150
1151
	/**
1152
	 * Workhorse method to return array of related posts matched by ElasticSearch.
1153
	 *
1154
	 * @param int $post_id
1155
	 * @param int $size
1156
	 * @param array $filters
1157
	 * @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
1158
	 * @return array
1159
	 */
1160
	protected function _get_related_posts( $post_id, $size, array $filters ) {
1161
		$hits = $this->_filter_non_public_posts(
1162
			$this->_get_related_post_ids(
1163
				$post_id,
1164
				$size,
1165
				$filters
1166
			)
1167
		);
1168
1169
		/**
1170
		 * Filter the Related Posts matched by ElasticSearch.
1171
		 *
1172
		 * @module related-posts
1173
		 *
1174
		 * @since 2.9.0
1175
		 *
1176
		 * @param array $hits Array of Post IDs matched by ElasticSearch.
1177
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1178
		 */
1179
		$hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
1180
1181
		$related_posts = array();
1182
		foreach ( $hits as $i => $hit ) {
1183
			$related_posts[] = $this->_get_related_post_data_for_post( $hit['id'], $i, $post_id );
1184
		}
1185
		return $related_posts;
1186
	}
1187
1188
	/**
1189
	 * Get array of related posts matched by ElasticSearch.
1190
	 *
1191
	 * @param int $post_id
1192
	 * @param int $size
1193
	 * @param array $filters
1194
	 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body, get_post_meta, update_post_meta
1195
	 * @return array
1196
	 */
1197
	protected function _get_related_post_ids( $post_id, $size, array $filters ) {
1198
		$now_ts = time();
1199
		$cache_meta_key = '_jetpack_related_posts_cache';
1200
1201
		$body = array(
1202
			'size' => (int) $size,
1203
		);
1204
1205
		if ( !empty( $filters ) )
1206
			$body['filter'] = array( 'and' => $filters );
1207
1208
		// Build cache key
1209
		$cache_key = md5( serialize( $body ) );
1210
1211
		// Load all cached values
1212
		if ( wp_using_ext_object_cache() ) {
1213
			$transient_name = "{$cache_meta_key}_{$cache_key}_{$post_id}";
1214
			$cache = get_transient( $transient_name );
1215
			if ( false !== $cache ) {
1216
				return $cache;
1217
			}
1218
		} else {
1219
			$cache = get_post_meta( $post_id, $cache_meta_key, true );
1220
1221
			if ( empty( $cache ) )
1222
				$cache = array();
1223
1224
1225
			// Cache is valid! Return cached value.
1226
			if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) && $cache[ $cache_key ][ 'expires' ] > $now_ts ) {
1227
				return $cache[ $cache_key ][ 'payload' ];
1228
			}
1229
		}
1230
1231
		$response = wp_remote_post(
1232
			"https://public-api.wordpress.com/rest/v1/sites/{$this->_blog_id_wpcom}/posts/$post_id/related/",
1233
			array(
1234
				'timeout' => 10,
1235
				'user-agent' => 'jetpack_related_posts',
1236
				'sslverify' => true,
1237
				'body' => $body,
1238
			)
1239
		);
1240
1241
		// Oh no... return nothing don't cache errors.
1242
		if ( is_wp_error( $response ) ) {
1243
			if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) )
1244
				return $cache[ $cache_key ][ 'payload' ]; // return stale
1245
			else
1246
				return array();
1247
		}
1248
1249
		$results = json_decode( wp_remote_retrieve_body( $response ), true );
1250
		$related_posts = array();
1251
		if ( is_array( $results ) && !empty( $results['hits'] ) ) {
1252
			foreach( $results['hits'] as $hit ) {
1253
				$related_posts[] = array(
1254
					'id' => $hit['fields']['post_id'],
1255
				);
1256
			}
1257
		}
1258
1259
		// An empty array might indicate no related posts or that posts
1260
		// are not yet synced to WordPress.com, so we cache for only 1
1261
		// minute in this case
1262
		if ( empty( $related_posts ) ) {
1263
			$cache_ttl = 60;
1264
		} else {
1265
			$cache_ttl = 12 * HOUR_IN_SECONDS;
1266
		}
1267
1268
		// Update cache
1269
		if ( wp_using_ext_object_cache() ) {
1270
			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...
1271
		} else {
1272
			// Copy all valid cache values
1273
			$new_cache = array();
1274
			foreach ( $cache as $k => $v ) {
1275
				if ( is_array( $v ) && $v[ 'expires' ] > $now_ts ) {
1276
					$new_cache[ $k ] = $v;
1277
				}
1278
			}
1279
1280
			// Set new cache value
1281
			$cache_expires = $cache_ttl + $now_ts;
1282
			$new_cache[ $cache_key ] = array(
1283
				'expires' => $cache_expires,
1284
				'payload' => $related_posts,
1285
			);
1286
			update_post_meta( $post_id, $cache_meta_key, $new_cache );
1287
		}
1288
1289
		return $related_posts;
1290
	}
1291
1292
	/**
1293
	 * Filter out any hits that are not public anymore.
1294
	 *
1295
	 * @param array $related_posts
1296
	 * @uses get_post_stati, get_post_status
1297
	 * @return array
1298
	 */
1299
	protected function _filter_non_public_posts( array $related_posts ) {
1300
		$public_stati = get_post_stati( array( 'public' => true ) );
1301
1302
		$filtered = array();
1303
		foreach ( $related_posts as $hit ) {
1304
			if ( in_array( get_post_status( $hit['id'] ), $public_stati ) ) {
1305
				$filtered[] = $hit;
1306
			}
1307
		}
1308
		return $filtered;
1309
	}
1310
1311
	/**
1312
	 * Generates a context for the related content (second line in related post output).
1313
	 * Order of importance:
1314
	 *   - First category (Not 'Uncategorized')
1315
	 *   - First post tag
1316
	 *   - Number of comments
1317
	 *
1318
	 * @param int $post_id
1319
	 * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n
1320
	 * @return string
1321
	 */
1322
	protected function _generate_related_post_context( $post_id ) {
1323
		$categories = get_the_category( $post_id );
1324 View Code Duplication
		if ( is_array( $categories ) ) {
1325
			foreach ( $categories as $category ) {
1326
				if ( 'uncategorized' != $category->slug && '' != trim( $category->name ) ) {
1327
					$post_cat_context = sprintf(
1328
						_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1329
						$category->name
1330
					);
1331
					/**
1332
					 * Filter the "In Category" line displayed in the post context below each Related Post.
1333
					 *
1334
					 * @module related-posts
1335
					 *
1336
					 * @since 3.2.0
1337
					 *
1338
					 * @param string $post_cat_context "In Category" line displayed in the post context below each Related Post.
1339
					 * @param array $category Array containing information about the category.
1340
					 */
1341
					return apply_filters( 'jetpack_relatedposts_post_category_context', $post_cat_context, $category );
1342
				}
1343
			}
1344
		}
1345
1346
		$tags = get_the_terms( $post_id, 'post_tag' );
1347 View Code Duplication
		if ( is_array( $tags ) ) {
1348
			foreach ( $tags as $tag ) {
1349
				if ( '' != trim( $tag->name ) ) {
1350
					$post_tag_context = sprintf(
1351
						_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1352
						$tag->name
1353
					);
1354
					/**
1355
					 * Filter the "In Tag" line displayed in the post context below each Related Post.
1356
					 *
1357
					 * @module related-posts
1358
					 *
1359
					 * @since 3.2.0
1360
					 *
1361
					 * @param string $post_tag_context "In Tag" line displayed in the post context below each Related Post.
1362
					 * @param array $tag Array containing information about the tag.
1363
					 */
1364
					return apply_filters( 'jetpack_relatedposts_post_tag_context', $post_tag_context, $tag );
1365
				}
1366
			}
1367
		}
1368
1369
		$comment_count = get_comments_number( $post_id );
1370
		if ( $comment_count > 0 ) {
1371
			return sprintf(
1372
				_n( 'With 1 comment', 'With %s comments', $comment_count, 'jetpack' ),
1373
				number_format_i18n( $comment_count )
1374
			);
1375
		}
1376
1377
		return __( 'Similar post', 'jetpack' );
1378
	}
1379
1380
	/**
1381
	 * Logs clicks for clickthrough analysis and related result tuning.
1382
	 *
1383
	 * @return null
1384
	 */
1385
	protected function _log_click( $post_id, $to_post_id, $link_position ) {
0 ignored issues
show
Unused Code introduced by
The parameter $post_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $to_post_id is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $link_position is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1386
1387
	}
1388
1389
	/**
1390
	 * Determines if the current post is able to use related posts.
1391
	 *
1392
	 * @uses self::get_options, is_admin, is_single, apply_filters
1393
	 * @return bool
1394
	 */
1395
	protected function _enabled_for_request() {
1396
		$enabled = is_single() 
1397
			&&
1398
				! is_admin()
1399
			&&
1400
				( $this->_allow_feature_toggle() && $this->get_option( 'enabled' ) );
1401
1402
		/**
1403
		 * Filter the Enabled value to allow related posts to be shown on pages as well.
1404
		 *
1405
		 * @module related-posts
1406
		 *
1407
		 * @since 3.3.0
1408
		 *
1409
		 * @param bool $enabled Should Related Posts be enabled on the current page.
1410
		 */
1411
		return apply_filters( 'jetpack_relatedposts_filter_enabled_for_request', $enabled );
1412
	}
1413
1414
	/**
1415
	 * Adds filters and enqueues assets.
1416
	 *
1417
	 * @uses self::_enqueue_assets, self::_setup_shortcode, add_filter
1418
	 * @return null
1419
	 */
1420
	protected function _action_frontend_init_page() {
1421
		$this->_enqueue_assets( true, true );
1422
		$this->_setup_shortcode();
1423
1424
		add_filter( 'the_content', array( $this, 'filter_add_target_to_dom' ), 40 );
1425
	}
1426
1427
	/**
1428
	 * Enqueues assets needed to do async loading of related posts.
1429
	 *
1430
	 * @uses wp_enqueue_script, wp_enqueue_style, plugins_url
1431
	 * @return null
1432
	 */
1433
	protected function _enqueue_assets( $script, $style ) {
1434
		$dependencies = is_customize_preview() ? array( 'customize-base' ) : array( 'jquery' );
1435
		if ( $script ) {
1436
			wp_enqueue_script( 'jetpack_related-posts', plugins_url( 'related-posts.js', __FILE__ ), $dependencies, self::VERSION );
1437
			$related_posts_js_options = array(
1438
				/**
1439
				 * Filter each Related Post Heading structure.
1440
				 *
1441
				 * @since 4.0.0
1442
				 *
1443
				 * @param string $str Related Post Heading structure. Default to h4.
1444
				 */
1445
				'post_heading' => apply_filters( 'jetpack_relatedposts_filter_post_heading', esc_attr( 'h4' ) ),
1446
			);
1447
			wp_localize_script( 'jetpack_related-posts', 'related_posts_js_options', $related_posts_js_options );
1448
		}
1449
		if ( $style ){
1450
			if( is_rtl() ) {
1451
				wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'rtl/related-posts-rtl.css', __FILE__ ), array(), self::VERSION );
1452
			} else {
1453
				wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'related-posts.css', __FILE__ ), array(), self::VERSION );
1454
			}
1455
		}
1456
	}
1457
1458
	/**
1459
	 * Sets up the shortcode processing.
1460
	 *
1461
	 * @uses add_filter, add_shortcode
1462
	 * @return null
1463
	 */
1464
	protected function _setup_shortcode() {
1465
		add_filter( 'the_content', array( $this, 'test_for_shortcode' ), 0 );
1466
1467
		add_shortcode( self::SHORTCODE, array( $this, 'get_target_html' ) );
1468
	}
1469
1470
	protected function _allow_feature_toggle() {
1471
		if ( null === $this->_allow_feature_toggle ) {
1472
			/**
1473
			 * Filter the display of the Related Posts toggle in Settings > Reading.
1474
			 *
1475
			 * @module related-posts
1476
			 *
1477
			 * @since 2.8.0
1478
			 *
1479
			 * @param bool false Display a feature toggle. Default to false.
1480
			 */
1481
			$this->_allow_feature_toggle = apply_filters( 'jetpack_relatedposts_filter_allow_feature_toggle', false );
1482
		}
1483
		return $this->_allow_feature_toggle;
1484
	}
1485
1486
	/**
1487
	 * ===================================================
1488
	 * FUNCTIONS EXPOSING RELATED POSTS IN THE WP REST API
1489
	 * ===================================================
1490
	 */
1491
1492
	/**
1493
	 * Add Related Posts to the REST API Post response.
1494
	 *
1495
	 * @since 4.4.0
1496
	 *
1497
	 * @action rest_api_init
1498
	 * @uses register_rest_field, self::rest_get_related_posts
1499
	 * @return null
1500
	 */
1501
	public function rest_register_related_posts() {
1502
		register_rest_field( 'post',
1503
			'jetpack-related-posts',
1504
			array(
1505
				'get_callback' => array( $this, 'rest_get_related_posts' ),
1506
				'update_callback' => null,
1507
				'schema'          => null,
1508
			)
1509
		);
1510
	}
1511
1512
	/**
1513
	 * Build an array of Related Posts.
1514
	 *
1515
	 * @since 4.4.0
1516
	 *
1517
	 * @param array $object Details of current post.
1518
	 * @param string $field_name Name of field.
1519
	 * @param WP_REST_Request $request Current request
1520
	 *
1521
	 * @uses self::get_for_post_id
1522
	 *
1523
	 * @return array
1524
	 */
1525
	public function rest_get_related_posts( $object, $field_name, $request ) {
0 ignored issues
show
Unused Code introduced by
The parameter $field_name is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $request is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1526
		return $this->get_for_post_id( $object['id'], array() );
1527
	}
1528
}
1529
1530
class Jetpack_RelatedPosts_Raw extends Jetpack_RelatedPosts {
0 ignored issues
show
Coding Style Compatibility introduced by
PSR1 recommends that each class should be in its own file to aid autoloaders.

Having each class in a dedicated file usually plays nice with PSR autoloaders and is therefore a well established practice. If you use other autoloaders, you might not want to follow this rule.

Loading history...
1531
	protected $_query_name;
1532
1533
	/**
1534
	 * Allows callers of this class to tag each query with a unique name for tracking purposes.
1535
	 *
1536
	 * @param string $name
1537
	 * @return Jetpack_RelatedPosts_Raw
1538
	 */
1539
	public function set_query_name( $name ) {
1540
		$this->_query_name = (string) $name;
1541
		return $this;
1542
	}
1543
1544
	/**
1545
	 * The raw related posts class can be used by other plugins or themes
1546
	 * to get related content. This class wraps the existing RelatedPosts
1547
	 * logic thus we never want to add anything to the DOM or do anything
1548
	 * for event hooks. We will also not present any settings for this
1549
	 * class and keep it enabled as calls to this class is done
1550
	 * programmatically.
1551
	 */
1552
	public function action_admin_init() {}
1553
	public function action_frontend_init() {}
1554
	public function get_options() {
1555
		return array(
1556
			'enabled' => true,
1557
		);
1558
	}
1559
1560
	/**
1561
	 * Workhorse method to return array of related posts ids matched by ElasticSearch.
1562
	 *
1563
	 * @param int $post_id
1564
	 * @param int $size
1565
	 * @param array $filters
1566
	 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body
1567
	 * @return array
1568
	 */
1569
	protected function _get_related_posts( $post_id, $size, array $filters ) {
1570
		$hits = $this->_filter_non_public_posts(
1571
			$this->_get_related_post_ids(
1572
				$post_id,
1573
				$size,
1574
				$filters
1575
			)
1576
		);
1577
1578
		/** This filter is already documented in modules/related-posts/related-posts.php */
1579
		$hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
1580
1581
		return $hits;
1582
	}
1583
}
1584