Completed
Push — add/jetpack-plan-support ( c36d23...95100b )
by
unknown
14:33 queued 05:53
created

Jetpack_RelatedPosts::_enabled_for_request()   A

Complexity

Conditions 4
Paths 5

Size

Total Lines 18
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 7
nc 5
nop 0
dl 0
loc 18
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
				/** This filter is already documented in modules/sharedaddy/sharing-service.php */
158
				apply_filters( 'jetpack_sharing_headline_html', '<h3 class="jp-relatedposts-headline"><em>%s</em></h3>', esc_html( $options['headline'] ), 'related-posts' ),
159
				esc_html( $options['headline'] )
160
			);
161
		} else {
162
			$headline = '';
163
		}
164
		return $headline;
165
	}
166
167
	/**
168
	 * Adds a target to the post content to load related posts into if a shortcode for it did not already exist.
169
	 *
170
	 * @filter the_content
171
	 * @param string $content
172
	 * @returns string
173
	 */
174
	public function filter_add_target_to_dom( $content ) {
175
		if ( !$this->_found_shortcode ) {
176
			$content .= "\n" . $this->get_target_html();
177
		}
178
179
		return $content;
180
	}
181
182
	/**
183
	 * Looks for our shortcode on the unfiltered content, this has to execute early.
184
	 *
185
	 * @filter the_content
186
	 * @param string $content
187
	 * @uses has_shortcode
188
	 * @returns string
189
	 */
190
	public function test_for_shortcode( $content ) {
191
		$this->_found_shortcode = has_shortcode( $content, self::SHORTCODE );
192
193
		return $content;
194
	}
195
196
	/**
197
	 * Returns the HTML for the related posts section.
198
	 *
199
	 * @uses esc_html__, apply_filters
200
	 * @returns string
201
	 */
202
	public function get_target_html() {
203
		require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
204
		if ( Jetpack_Sync_Settings::is_syncing() ) {
205
			return '';
206
		}
207
208
		/**
209
		 * Filter the Related Posts headline.
210
		 *
211
		 * @module related-posts
212
		 *
213
		 * @since 3.0.0
214
		 *
215
		 * @param string $headline Related Posts heading.
216
		 */
217
		$headline = apply_filters( 'jetpack_relatedposts_filter_headline', $this->get_headline() );
218
219
		if ( $this->_previous_post_id ) {
220
			$exclude = "data-exclude='{$this->_previous_post_id}'";
221
		} else {
222
			$exclude = "";
223
		}
224
225
		return <<<EOT
226
<div id='jp-relatedposts' class='jp-relatedposts' $exclude>
227
	$headline
228
</div>
229
EOT;
230
	}
231
232
	/**
233
	 * 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.
234
	 *
235
	 * @returns string
236
	 */
237
	public function get_target_html_unsupported() {
238
		require_once JETPACK__PLUGIN_DIR . '/sync/class.jetpack-sync-settings.php';
239
		if ( Jetpack_Sync_Settings::is_syncing() ) {
240
			return '';
241
		}
242
		return "\n\n<!-- Jetpack Related Posts is not supported in this context. -->\n\n";
243
	}
244
245
	/**
246
	 * ========================
247
	 * PUBLIC UTILITY FUNCTIONS
248
	 * ========================
249
	 */
250
251
	/**
252
	 * Gets options set for Jetpack_RelatedPosts and merge with defaults.
253
	 *
254
	 * @uses Jetpack_Options::get_option, apply_filters
255
	 * @return array
256
	 */
257
	public function get_options() {
258
		if ( null === $this->_options ) {
259
			$this->_options = Jetpack_Options::get_option( 'relatedposts', array() );
260
			if ( ! is_array( $this->_options ) )
261
				$this->_options = array();
262
			if ( ! isset( $this->_options['enabled'] ) )
263
				$this->_options['enabled'] = true;
264
			if ( ! isset( $this->_options['show_headline'] ) )
265
				$this->_options['show_headline'] = true;
266
			if ( ! isset( $this->_options['show_thumbnails'] ) )
267
				$this->_options['show_thumbnails'] = false;
268
			if ( ! isset( $this->_options['show_date'] ) ) {
269
				$this->_options['show_date'] = true;
270
			}
271
			if ( ! isset( $this->_options['show_context'] ) ) {
272
				$this->_options['show_context'] = true;
273
			}
274
			if ( ! isset( $this->_options['layout'] ) ) {
275
				$this->_options['layout'] = 'grid';
276
			}
277
			if ( ! isset( $this->_options['headline'] ) ) {
278
				$this->_options['headline'] = esc_html__( 'Related', 'jetpack' );
279
			}
280
			if ( empty( $this->_options['size'] ) || (int)$this->_options['size'] < 1 )
281
				$this->_options['size'] = 3;
282
283
			/**
284
			 * Filter Related Posts basic options.
285
			 *
286
			 * @module related-posts
287
			 *
288
			 * @since 2.8.0
289
			 *
290
			 * @param array $this->_options Array of basic Related Posts options.
291
			 */
292
			$this->_options = apply_filters( 'jetpack_relatedposts_filter_options', $this->_options );
293
		}
294
295
		return $this->_options;
296
	}
297
	
298
	public function get_option( $option_name ) {
299
		$options = $this->get_options();
300
		
301
		if ( isset( $options[ $option_name ] ) ) {
302
			return $options[ $option_name ];
303
		}
304
		
305
		return false;
306
	}
307
308
	/**
309
	 * Parses input and returns normalized options array.
310
	 *
311
	 * @param array $input
312
	 * @uses self::get_options
313
	 * @return array
314
	 */
315
	public function parse_options( $input ) {
316
		$current = $this->get_options();
317
318
		if ( !is_array( $input ) )
319
			$input = array();
320
321
		if (
322
			! isset( $input['enabled'] )
323
			|| isset( $input['show_date'] )
324
			|| isset( $input['show_context'] )
325
			|| isset( $input['layout'] )
326
			|| isset( $input['headline'] )
327
			) {
328
			$input['enabled'] = '1';
329
		}
330
331
		if ( '1' == $input['enabled'] ) {
332
			$current['enabled'] = true;
333
			$current['show_headline'] = ( isset( $input['show_headline'] ) && '1' == $input['show_headline'] );
334
			$current['show_thumbnails'] = ( isset( $input['show_thumbnails'] ) && '1' == $input['show_thumbnails'] );
335
			$current['show_date'] = ( isset( $input['show_date'] ) && '1' == $input['show_date'] );
336
			$current['show_context'] = ( isset( $input['show_context'] ) && '1' == $input['show_context'] );
337
			$current['layout'] = isset( $input['layout'] ) && in_array( $input['layout'], array( 'grid', 'list' ), true ) ? $input['layout'] : 'grid';
338
			$current['headline'] = isset( $input['headline'] ) ? $input['headline'] : esc_html__( 'Related', 'jetpack' );
339
		} else {
340
			$current['enabled'] = false;
341
		}
342
343
		if ( isset( $input['size'] ) && (int)$input['size'] > 0 )
344
			$current['size'] = (int)$input['size'];
345
		else
346
			$current['size'] = null;
347
348
		return $current;
349
	}
350
351
	/**
352
	 * HTML for admin settings page.
353
	 *
354
	 * @uses self::get_options, checked, esc_html__
355
	 * @returns null
356
	 */
357
	public function print_setting_html() {
358
		$options = $this->get_options();
359
360
		$ui_settings_template = <<<EOT
361
<ul id="settings-reading-relatedposts-customize">
362
	<li>
363
		<label><input name="jetpack_relatedposts[show_headline]" type="checkbox" value="1" %s /> %s</label>
364
	</li>
365
	<li>
366
		<label><input name="jetpack_relatedposts[show_thumbnails]" type="checkbox" value="1" %s /> %s</label>
367
	</li>
368
	<li>
369
		<label><input name="jetpack_relatedposts[show_date]" type="checkbox" value="1" %s /> %s</label>
370
	</li>
371
	<li>
372
		<label><input name="jetpack_relatedposts[show_context]" type="checkbox" value="1" %s /> %s</label>
373
	</li>
374
</ul>
375
<div id='settings-reading-relatedposts-preview'>
376
	%s
377
	<div id="jp-relatedposts" class="jp-relatedposts"></div>
378
</div>
379
EOT;
380
		$ui_settings = sprintf(
381
			$ui_settings_template,
382
			checked( $options['show_headline'], true, false ),
383
			esc_html__( 'Show a "Related" header to more clearly separate the related section from posts', 'jetpack' ),
384
			checked( $options['show_thumbnails'], true, false ),
385
			esc_html__( 'Use a large and visually striking layout', 'jetpack' ),
386
			checked( $options['show_date'], true, false ),
387
			esc_html__( 'Show entry date', 'jetpack' ),
388
			checked( $options['show_context'], true, false ),
389
			esc_html__( 'Show context (category or tag)', 'jetpack' ),
390
			esc_html__( 'Preview:', 'jetpack' )
391
		);
392
393
		if ( !$this->_allow_feature_toggle() ) {
394
			$template = <<<EOT
395
<input type="hidden" name="jetpack_relatedposts[enabled]" value="1" />
396
%s
397
EOT;
398
			printf(
399
				$template,
400
				$ui_settings
401
			);
402
		} else {
403
			$template = <<<EOT
404
<ul id="settings-reading-relatedposts">
405
	<li>
406
		<label><input type="radio" name="jetpack_relatedposts[enabled]" value="0" class="tog" %s /> %s</label>
407
	</li>
408
	<li>
409
		<label><input type="radio" name="jetpack_relatedposts[enabled]" value="1" class="tog" %s /> %s</label>
410
		%s
411
	</li>
412
</ul>
413
EOT;
414
			printf(
415
				$template,
416
				checked( $options['enabled'], false, false ),
417
				esc_html__( 'Hide related content after posts', 'jetpack' ),
418
				checked( $options['enabled'], true, false ),
419
				esc_html__( 'Show related content after posts', 'jetpack' ),
420
				$ui_settings
421
			);
422
		}
423
	}
424
425
	/**
426
	 * Head JS/CSS for admin settings page.
427
	 *
428
	 * @uses esc_html__
429
	 * @returns null
430
	 */
431
	public function print_setting_head() {
432
433
		// only dislay the Related Posts JavaScript on the Reading Settings Admin Page
434
		$current_screen =  get_current_screen();
435
436
		if ( is_null( $current_screen ) ) {
437
			return;
438
		}
439
440
		if( 'options-reading' != $current_screen->id )
441
			return;
442
443
		$related_headline = sprintf(
444
			'<h3 class="jp-relatedposts-headline"><em>%s</em></h3>',
445
			esc_html__( 'Related', 'jetpack' )
446
		);
447
448
		$href_params = 'class="jp-relatedposts-post-a" href="#jetpack_relatedposts" rel="nofollow" data-origin="0" data-position="0"';
449
		$related_with_images = <<<EOT
450
<div class="jp-relatedposts-items jp-relatedposts-items-visual">
451
	<div class="jp-relatedposts-post jp-relatedposts-post0 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
452
		<a $href_params>
453
			<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">
454
		</a>
455
		<h4 class="jp-relatedposts-post-title">
456
			<a $href_params>Big iPhone/iPad Update Now Available</a>
457
		</h4>
458
		<p class="jp-relatedposts-post-excerpt">Big iPhone/iPad Update Now Available</p>
459
		<p class="jp-relatedposts-post-context">In "Mobile"</p>
460
	</div>
461
	<div class="jp-relatedposts-post jp-relatedposts-post1 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
462
		<a $href_params>
463
			<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">
464
		</a>
465
		<h4 class="jp-relatedposts-post-title">
466
			<a $href_params>The WordPress for Android App Gets a Big Facelift</a>
467
		</h4>
468
		<p class="jp-relatedposts-post-excerpt">The WordPress for Android App Gets a Big Facelift</p>
469
		<p class="jp-relatedposts-post-context">In "Mobile"</p>
470
	</div>
471
	<div class="jp-relatedposts-post jp-relatedposts-post2 jp-relatedposts-post-thumbs" data-post-id="0" data-post-format="image">
472
		<a $href_params>
473
			<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">
474
		</a>
475
		<h4 class="jp-relatedposts-post-title">
476
			<a $href_params>Upgrade Focus: VideoPress For Weddings</a>
477
		</h4>
478
		<p class="jp-relatedposts-post-excerpt">Upgrade Focus: VideoPress For Weddings</p>
479
		<p class="jp-relatedposts-post-context">In "Upgrade"</p>
480
	</div>
481
</div>
482
EOT;
483
		$related_with_images = str_replace( "\n", '', $related_with_images );
484
		$related_without_images = <<<EOT
485
<div class="jp-relatedposts-items jp-relatedposts-items-minimal">
486
	<p class="jp-relatedposts-post jp-relatedposts-post0" data-post-id="0" data-post-format="image">
487
		<span class="jp-relatedposts-post-title"><a $href_params>Big iPhone/iPad Update Now Available</a></span>
488
		<span class="jp-relatedposts-post-context">In "Mobile"</span>
489
	</p>
490
	<p class="jp-relatedposts-post jp-relatedposts-post1" data-post-id="0" data-post-format="image">
491
		<span class="jp-relatedposts-post-title"><a $href_params>The WordPress for Android App Gets a Big Facelift</a></span>
492
		<span class="jp-relatedposts-post-context">In "Mobile"</span>
493
	</p>
494
	<p class="jp-relatedposts-post jp-relatedposts-post2" data-post-id="0" data-post-format="image">
495
		<span class="jp-relatedposts-post-title"><a $href_params>Upgrade Focus: VideoPress For Weddings</a></span>
496
		<span class="jp-relatedposts-post-context">In "Upgrade"</span>
497
	</p>
498
</div>
499
EOT;
500
		$related_without_images = str_replace( "\n", '', $related_without_images );
501
502
		if ( $this->_allow_feature_toggle() ) {
503
			$extra_css = '#settings-reading-relatedposts-customize { padding-left:2em; margin-top:.5em; }';
504
		} else {
505
			$extra_css = '';
506
		}
507
508
		echo <<<EOT
509
<style type="text/css">
510
	#settings-reading-relatedposts .disabled { opacity:.5; filter:Alpha(opacity=50); }
511
	#settings-reading-relatedposts-preview .jp-relatedposts { background:#fff; padding:.5em; width:75%; }
512
	$extra_css
513
</style>
514
<script type="text/javascript">
515
	jQuery( document ).ready( function($) {
516
		var update_ui = function() {
517
			var is_enabled = true;
518
			if ( 'radio' == $( 'input[name="jetpack_relatedposts[enabled]"]' ).attr('type') ) {
519
				if ( '0' == $( 'input[name="jetpack_relatedposts[enabled]"]:checked' ).val() ) {
520
					is_enabled = false;
521
				}
522
			}
523
			if ( is_enabled ) {
524
				$( '#settings-reading-relatedposts-customize' )
525
					.removeClass( 'disabled' )
526
					.find( 'input' )
527
					.attr( 'disabled', false );
528
				$( '#settings-reading-relatedposts-preview' )
529
					.removeClass( 'disabled' );
530
			} else {
531
				$( '#settings-reading-relatedposts-customize' )
532
					.addClass( 'disabled' )
533
					.find( 'input' )
534
					.attr( 'disabled', true );
535
				$( '#settings-reading-relatedposts-preview' )
536
					.addClass( 'disabled' );
537
			}
538
		};
539
540
		var update_preview = function() {
541
			var html = '';
542
			if ( $( 'input[name="jetpack_relatedposts[show_headline]"]:checked' ).length ) {
543
				html += '$related_headline';
544
			}
545
			if ( $( 'input[name="jetpack_relatedposts[show_thumbnails]"]:checked' ).length ) {
546
				html += '$related_with_images';
547
			} else {
548
				html += '$related_without_images';
549
			}
550
			$( '#settings-reading-relatedposts-preview .jp-relatedposts' ).html( html );
551
			if ( $( 'input[name="jetpack_relatedposts[show_date]"]:checked' ).length ) {
552
				$( '.jp-relatedposts-post-title' ).each( function() {
553
					$( this ).after( $( '<span>August 8, 2005</span>' ) );
554
				} );
555
			}
556
			if ( $( 'input[name="jetpack_relatedposts[show_context]"]:checked' ).length ) {
557
				$( '.jp-relatedposts-post-context' ).show();
558
			} else {
559
				$( '.jp-relatedposts-post-context' ).hide();
560
			}
561
			$( '#settings-reading-relatedposts-preview .jp-relatedposts' ).show();
562
		};
563
564
		// Update on load
565
		update_preview();
566
		update_ui();
567
568
		// Update on change
569
		$( '#settings-reading-relatedposts-customize input' )
570
			.change( update_preview );
571
		$( '#settings-reading-relatedposts' )
572
			.find( 'input.tog' )
573
			.change( update_ui );
574
	});
575
</script>
576
EOT;
577
	}
578
579
	/**
580
	 * Gets an array of related posts that match the given post_id.
581
	 *
582
	 * @param int $post_id
583
	 * @param array $args - params to use when building ElasticSearch filters to narrow down the search domain.
584
	 * @uses self::get_options, get_post_type, wp_parse_args, apply_filters
585
	 * @return array
586
	 */
587
	public function get_for_post_id( $post_id, array $args ) {
588
		$options = $this->get_options();
589
590
		if ( ! empty( $args['size'] ) )
591
			$options['size'] = $args['size'];
592
593
		if ( ! $options['enabled'] || 0 == (int)$post_id || empty( $options['size'] ) )
594
			return array();
595
596
		$defaults = array(
597
			'size' => (int)$options['size'],
598
			'post_type' => get_post_type( $post_id ),
599
			'post_formats' => array(),
600
			'has_terms' => array(),
601
			'date_range' => array(),
602
			'exclude_post_ids' => array(),
603
		);
604
		$args = wp_parse_args( $args, $defaults );
605
		/**
606
		 * Filter the arguments used to retrieve a list of Related Posts.
607
		 *
608
		 * @module related-posts
609
		 *
610
		 * @since 2.8.0
611
		 *
612
		 * @param array $args Array of options to retrieve Related Posts.
613
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
614
		 */
615
		$args = apply_filters( 'jetpack_relatedposts_filter_args', $args, $post_id );
616
617
		$filters = $this->_get_es_filters_from_args( $post_id, $args );
618
		/**
619
		 * Filter ElasticSearch options used to calculate Related Posts.
620
		 *
621
		 * @module related-posts
622
		 *
623
		 * @since 2.8.0
624
		 *
625
		 * @param array $filters Array of ElasticSearch filters based on the post_id and args.
626
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
627
		 */
628
		$filters = apply_filters( 'jetpack_relatedposts_filter_filters', $filters, $post_id );
629
630
		$results = $this->_get_related_posts( $post_id, $args['size'], $filters );
631
		/**
632
		 * Filter the array of related posts matched by ElasticSearch.
633
		 *
634
		 * @module related-posts
635
		 *
636
		 * @since 2.8.0
637
		 *
638
		 * @param array $results Array of related posts matched by ElasticSearch.
639
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
640
		 */
641
		return apply_filters( 'jetpack_relatedposts_returned_results', $results, $post_id );
642
	}
643
644
	/**
645
	 * =========================
646
	 * PRIVATE UTILITY FUNCTIONS
647
	 * =========================
648
	 */
649
650
	/**
651
	 * Creates an array of ElasticSearch filters based on the post_id and args.
652
	 *
653
	 * @param int $post_id
654
	 * @param array $args
655
	 * @uses apply_filters, get_post_types, get_post_format_strings
656
	 * @return array
657
	 */
658
	protected function _get_es_filters_from_args( $post_id, array $args ) {
659
		$filters = array();
660
661
		/**
662
		 * Filter the terms used to search for Related Posts.
663
		 *
664
		 * @module related-posts
665
		 *
666
		 * @since 2.8.0
667
		 *
668
		 * @param array $args['has_terms'] Array of terms associated to the Related Posts.
669
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
670
		 */
671
		$args['has_terms'] = apply_filters( 'jetpack_relatedposts_filter_has_terms', $args['has_terms'], $post_id );
672
		if ( ! empty( $args['has_terms'] ) ) {
673
			foreach( (array)$args['has_terms'] as $term ) {
674
				if ( mb_strlen( $term->taxonomy ) ) {
675
					switch ( $term->taxonomy ) {
676
						case 'post_tag':
677
							$tax_fld = 'tag.slug';
678
							break;
679
						case 'category':
680
							$tax_fld = 'category.slug';
681
							break;
682
						default:
683
							$tax_fld = 'taxonomy.' . $term->taxonomy . '.slug';
684
							break;
685
					}
686
					$filters[] = array( 'term' => array( $tax_fld => $term->slug ) );
687
				}
688
			}
689
		}
690
691
		/**
692
		 * Filter the Post Types where we search Related Posts.
693
		 *
694
		 * @module related-posts
695
		 *
696
		 * @since 2.8.0
697
		 *
698
		 * @param array $args['post_type'] Array of Post Types.
699
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
700
		 */
701
		$args['post_type'] = apply_filters( 'jetpack_relatedposts_filter_post_type', $args['post_type'], $post_id );
702
		$valid_post_types = get_post_types();
703
		if ( is_array( $args['post_type'] ) ) {
704
			$sanitized_post_types = array();
705
			foreach ( $args['post_type'] as $pt ) {
706
				if ( in_array( $pt, $valid_post_types ) )
707
					$sanitized_post_types[] = $pt;
708
			}
709
			if ( ! empty( $sanitized_post_types ) )
710
				$filters[] = array( 'terms' => array( 'post_type' => $sanitized_post_types ) );
711
		} else if ( in_array( $args['post_type'], $valid_post_types ) && 'all' != $args['post_type'] ) {
712
			$filters[] = array( 'term' => array( 'post_type' => $args['post_type'] ) );
713
		}
714
715
		/**
716
		 * Filter the Post Formats where we search Related Posts.
717
		 *
718
		 * @module related-posts
719
		 *
720
		 * @since 3.3.0
721
		 *
722
		 * @param array $args['post_formats'] Array of Post Formats.
723
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
724
		 */
725
		$args['post_formats'] = apply_filters( 'jetpack_relatedposts_filter_post_formats', $args['post_formats'], $post_id );
726
		$valid_post_formats = get_post_format_strings();
727
		$sanitized_post_formats = array();
728
		foreach ( $args['post_formats'] as $pf ) {
729
			if ( array_key_exists( $pf, $valid_post_formats ) ) {
730
				$sanitized_post_formats[] = $pf;
731
			}
732
		}
733
		if ( ! empty( $sanitized_post_formats ) ) {
734
			$filters[] = array( 'terms' => array( 'post_format' => $sanitized_post_formats ) );
735
		}
736
737
		/**
738
		 * Filter the date range used to search Related Posts.
739
		 *
740
		 * @module related-posts
741
		 *
742
		 * @since 2.8.0
743
		 *
744
		 * @param array $args['date_range'] Array of a month interval where we search Related Posts.
745
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
746
		 */
747
		$args['date_range'] = apply_filters( 'jetpack_relatedposts_filter_date_range', $args['date_range'], $post_id );
748
		if ( is_array( $args['date_range'] ) && ! empty( $args['date_range'] ) ) {
749
			$args['date_range'] = array_map( 'intval', $args['date_range'] );
750
			if ( !empty( $args['date_range']['from'] ) && !empty( $args['date_range']['to'] ) ) {
751
				$filters[] = array(
752
					'range' => array(
753
						'date_gmt' => $this->_get_coalesced_range( $args['date_range'] ),
754
					)
755
				);
756
			}
757
		}
758
759
		/**
760
		 * Filter the Post IDs excluded from appearing in Related Posts.
761
		 *
762
		 * @module related-posts
763
		 *
764
		 * @since 2.9.0
765
		 *
766
		 * @param array $args['exclude_post_ids'] Array of Post IDs.
767
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
768
		 */
769
		$args['exclude_post_ids'] = apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', $args['exclude_post_ids'], $post_id );
770
		if ( !empty( $args['exclude_post_ids'] ) && is_array( $args['exclude_post_ids'] ) ) {
771
			foreach ( $args['exclude_post_ids'] as $exclude_post_id) {
772
				$exclude_post_id = (int)$exclude_post_id;
773
774
				if ( $exclude_post_id > 0 )
775
					$filters[] = array( 'not' => array( 'term' => array( 'post_id' => $exclude_post_id ) ) );
776
			}
777
		}
778
779
		return $filters;
780
	}
781
782
	/**
783
	 * Takes a range and coalesces it into a month interval bracketed by a time as determined by the blog_id to enhance caching.
784
	 *
785
	 * @param array $date_range
786
	 * @return array
787
	 */
788
	protected function _get_coalesced_range( array $date_range ) {
789
		$now = time();
790
		$coalesce_time = $this->_blog_id_wpcom % 86400;
791
		$current_time = $now - strtotime( 'today', $now );
792
793
		if ( $current_time < $coalesce_time && '01' == date( 'd', $now ) ) {
794
			// Move back 1 period
795
			return array(
796
				'from' => date( 'Y-m-01', strtotime( '-1 month', $date_range['from'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),
797
				'to'   => date( 'Y-m-01', $date_range['to'] ) . ' ' . date( 'H:i:s', $coalesce_time ),
798
			);
799
		} else {
800
			// Use current period
801
			return array(
802
				'from' => date( 'Y-m-01', $date_range['from'] ) . ' ' . date( 'H:i:s', $coalesce_time ),
803
				'to'   => date( 'Y-m-01', strtotime( '+1 month', $date_range['to'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),
804
			);
805
		}
806
	}
807
808
	/**
809
	 * Generate and output ajax response for related posts API call.
810
	 * NOTE: Calls exit() to end all further processing after payload has been outputed.
811
	 *
812
	 * @param array $excludes array of post_ids to exclude
813
	 * @uses send_nosniff_header, self::get_for_post_id, get_the_ID
814
	 * @return null
815
	 */
816
	protected function _action_frontend_init_ajax( array $excludes ) {
817
		define( 'DOING_AJAX', true );
818
819
		header( 'Content-type: application/json; charset=utf-8' ); // JSON can only be UTF-8
820
		send_nosniff_header();
821
822
		$options = $this->get_options();
823
824
		if ( isset( $_GET['jetpackrpcustomize'] ) ) {
825
826
			// If we're in the customizer, add dummy content.
827
			$date_now = current_time( get_option( 'date_format' ) );
828
			$related_posts = array(
829
				array(
830
					'id'       => - 1,
831
					'url'      => 'https://jetpackme.files.wordpress.com/2014/08/1-wpios-ipad-3-1-viewsite.png?w=350&h=200&crop=1',
832
					'url_meta' => array(
833
						'origin'   => 0,
834
						'position' => 0
835
					),
836
					'title'    => esc_html__( 'Big iPhone/iPad Update Now Available', 'jetpack' ),
837
					'date'     => $date_now,
838
					'format'   => false,
839
					'excerpt'  => esc_html__( 'It is that time of the year when devices are shiny again.', 'jetpack' ),
840
					'rel'      => 'nofollow',
841
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
842
					'img'      => array(
843
						'src'    => 'https://jetpackme.files.wordpress.com/2014/08/1-wpios-ipad-3-1-viewsite.png?w=350&h=200&crop=1',
844
						'width'  => 350,
845
						'height' => 200
846
					),
847
					'classes'  => array()
848
				),
849
				array(
850
					'id'       => - 1,
851
					'url'      => 'https://jetpackme.files.wordpress.com/2014/08/wordpress-com-news-wordpress-for-android-ui-update2.jpg?w=350&h=200&crop=1',
852
					'url_meta' => array(
853
						'origin'   => 0,
854
						'position' => 0
855
					),
856
					'title'    => esc_html__( 'The WordPress for Android App Gets a Big Facelift', 'jetpack' ),
857
					'date'     => $date_now,
858
					'format'   => false,
859
					'excerpt'  => esc_html__( 'Writing is new again in Android with the new WordPress app.', 'jetpack' ),
860
					'rel'      => 'nofollow',
861
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
862
					'img'      => array(
863
						'src'    => 'https://jetpackme.files.wordpress.com/2014/08/wordpress-com-news-wordpress-for-android-ui-update2.jpg?w=350&h=200&crop=1',
864
						'width'  => 350,
865
						'height' => 200
866
					),
867
					'classes'  => array()
868
				),
869
				array(
870
					'id'       => - 1,
871
					'url'      => 'https://jetpackme.files.wordpress.com/2014/08/videopresswedding.jpg?w=350&h=200&crop=1',
872
					'url_meta' => array(
873
						'origin'   => 0,
874
						'position' => 0
875
					),
876
					'title'    => esc_html__( 'Upgrade Focus, VideoPress for weddings', 'jetpack' ),
877
					'date'     => $date_now,
878
					'format'   => false,
879
					'excerpt'  => esc_html__( 'Weddings are in the spotlight now with VideoPress for weddings.', 'jetpack' ),
880
					'rel'      => 'nofollow',
881
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
882
					'img'      => array(
883
						'src'    => 'https://jetpackme.files.wordpress.com/2014/08/videopresswedding.jpg?w=350&h=200&crop=1',
884
						'width'  => 350,
885
						'height' => 200
886
					),
887
					'classes'  => array()
888
				),
889
			);
890
891
			for ( $total = 0; $total < $options['size'] - 3; $total++ ) {
892
				$related_posts[] = $related_posts[ $total ];
893
			}
894
895
			$current_post = get_post();
896
897
			// Exclude current post after filtering to make sure it's excluded and not lost during filtering.
898
			$excluded_posts = array_merge(
899
				/** This filter is already documented in modules/related-posts/jetpack-related-posts.php */
900
				apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', array() ),
901
				array( $current_post->ID )
902
			);
903
904
			// Fetch posts with featured image.
905
			$with_post_thumbnails = get_posts( array(
906
				'posts_per_page'   => $options['size'],
907
				'post__not_in'     => $excluded_posts,
908
				'post_type'        => $current_post->post_type,
909
				'meta_key'         => '_thumbnail_id',
910
				'suppress_filters' => false,
911
			) );
912
913
			// If we don't have enough, fetch posts without featured image.
914
			if ( 0 < ( $more = $options['size'] - count( $with_post_thumbnails ) ) ) {
915
				$no_post_thumbnails = get_posts( array(
916
					'posts_per_page'  => $more,
917
					'post__not_in'    => $excluded_posts,
918
					'post_type'       => $current_post->post_type,
919
					'meta_query' => array(
920
						array(
921
							'key'     => '_thumbnail_id',
922
							'compare' => 'NOT EXISTS',
923
						),
924
					),
925
					'suppress_filters' => false,
926
				) );
927
			} else {
928
				$no_post_thumbnails = array();
929
			}
930
931
			foreach ( array_merge( $with_post_thumbnails, $no_post_thumbnails ) as $index => $real_post ) {
932
				$related_posts[ $index ]['id']      = $real_post->ID;
933
				$related_posts[ $index ]['url']     = esc_url( get_permalink( $real_post ) );
934
				$related_posts[ $index ]['title']   = $this->_to_utf8( $this->_get_title( $real_post->post_title, $real_post->post_content ) );
935
				$related_posts[ $index ]['date']    = get_the_date( '', $real_post );
936
				$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' );
937
				$related_posts[ $index ]['img']     = $this->_generate_related_post_image_params( $real_post->ID );
938
				$related_posts[ $index ]['context'] = $this->_generate_related_post_context( $real_post->ID );
939
			}
940
		} else {
941
			$related_posts = $this->get_for_post_id(
942
				get_the_ID(),
943
				array(
944
					'exclude_post_ids' => $excludes,
945
				)
946
			);
947
		}
948
949
		$response = array(
950
			'version' => self::VERSION,
951
			'show_thumbnails' => (bool) $options['show_thumbnails'],
952
			'show_date' => (bool) $options['show_date'],
953
			'show_context' => (bool) $options['show_context'],
954
			'layout' => (string) $options['layout'],
955
			'headline' => (string) $options['headline'],
956
			'items' => array(),
957
		);
958
959
		if ( count( $related_posts ) == $options['size'] )
960
			$response['items'] = $related_posts;
961
962
		echo json_encode( $response );
963
964
		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...
965
	}
966
967
	/**
968
	 * Returns a UTF-8 encoded array of post information for the given post_id
969
	 *
970
	 * @param int $post_id
971
	 * @param int $position
972
	 * @param int $origin The post id that this is related to
973
	 * @uses get_post, get_permalink, remove_query_arg, get_post_format, apply_filters
974
	 * @return array
975
	 */
976
	protected function _get_related_post_data_for_post( $post_id, $position, $origin ) {
977
		$post = get_post( $post_id );
978
979
		return array(
980
			'id' => $post->ID,
981
			'url' => get_permalink( $post->ID ),
982
			'url_meta' => array( 'origin' => $origin, 'position' => $position ),
983
			'title' => $this->_to_utf8( $this->_get_title( $post->post_title, $post->post_content ) ),
984
			'date' => get_the_date( '', $post->ID ),
985
			'format' => get_post_format( $post->ID ),
986
			'excerpt' => html_entity_decode( $this->_to_utf8( $this->_get_excerpt( $post->post_excerpt, $post->post_content ) ), ENT_QUOTES, 'UTF-8' ),
987
			/**
988
			 * Filters the rel attribute for the Related Posts' links.
989
			 *
990
			 * @module related-posts
991
			 *
992
			 * @since 3.7.0
993
			 *
994
			 * @param string nofollow Link rel attribute for Related Posts' link. Default is nofollow.
995
			 * @param int $post->ID Post ID.
996
			 */
997
			'rel' => apply_filters( 'jetpack_relatedposts_filter_post_link_rel', 'nofollow', $post->ID ),
998
			/**
999
			 * Filter the context displayed below each Related Post.
1000
			 *
1001
			 * @module related-posts
1002
			 *
1003
			 * @since 3.0.0
1004
			 *
1005
			 * @param string $this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ) Context displayed below each related post.
1006
			 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1007
			 */
1008
			'context' => apply_filters(
1009
				'jetpack_relatedposts_filter_post_context',
1010
				$this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ),
1011
				$post->ID
1012
			),
1013
			'img' => $this->_generate_related_post_image_params( $post->ID ),
1014
			/**
1015
			 * Filter the post css classes added on HTML markup.
1016
			 *
1017
			 * @module related-posts
1018
			 *
1019
			 * @since 3.8.0
1020
			 *
1021
			 * @param array array() CSS classes added on post HTML markup.
1022
			 * @param string $post_id Post ID.
1023
			 */
1024
			'classes' => apply_filters(
1025
				'jetpack_relatedposts_filter_post_css_classes',
1026
				array(),
1027
				$post->ID
1028
			),
1029
		);
1030
	}
1031
1032
	/**
1033
	 * Returns either the title or a small excerpt to use as title for post.
1034
	 *
1035
	 * @param string $post_title
1036
	 * @param string $post_content
1037
	 * @uses strip_shortcodes, wp_trim_words, __
1038
	 * @return string
1039
	 */
1040
	protected function _get_title( $post_title, $post_content ) {
1041
		if ( ! empty( $post_title ) ) {
1042
			return wp_strip_all_tags( $post_title );
1043
		}
1044
1045
		$post_title = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post_content ) ), 5, '…' );
1046
		if ( ! empty( $post_title ) ) {
1047
			return $post_title;
1048
		}
1049
1050
		return __( 'Untitled Post', 'jetpack' );
1051
	}
1052
1053
	/**
1054
	 * Returns a plain text post excerpt for title attribute of links.
1055
	 *
1056
	 * @param string $post_excerpt
1057
	 * @param string $post_content
1058
	 * @uses strip_shortcodes, wp_strip_all_tags, wp_trim_words
1059
	 * @return string
1060
	 */
1061
	protected function _get_excerpt( $post_excerpt, $post_content ) {
1062
		if ( empty( $post_excerpt ) )
1063
			$excerpt = $post_content;
1064
		else
1065
			$excerpt = $post_excerpt;
1066
1067
		return wp_trim_words( wp_strip_all_tags( strip_shortcodes( $excerpt ) ), 50, '…' );
1068
	}
1069
1070
	/**
1071
	 * Generates the thumbnail image to be used for the post. Uses the
1072
	 * image as returned by Jetpack_PostImages::get_image()
1073
	 *
1074
	 * @param int $post_id
1075
	 * @uses self::get_options, apply_filters, Jetpack_PostImages::get_image, Jetpack_PostImages::fit_image_url
1076
	 * @return string
1077
	 */
1078
	protected function _generate_related_post_image_params( $post_id ) {
1079
		$options = $this->get_options();
1080
		$image_params = array(
1081
			'src' => '',
1082
			'width' => 0,
1083
			'height' => 0,
1084
		);
1085
1086
		if ( ! $options['show_thumbnails'] ) {
1087
			return $image_params;
1088
		}
1089
1090
		/**
1091
		 * Filter the size of the Related Posts images.
1092
		 *
1093
		 * @module related-posts
1094
		 *
1095
		 * @since 2.8.0
1096
		 *
1097
		 * @param array array( 'width' => 350, 'height' => 200 ) Size of the images displayed below each Related Post.
1098
		 */
1099
		$thumbnail_size = apply_filters(
1100
			'jetpack_relatedposts_filter_thumbnail_size',
1101
			array( 'width' => 350, 'height' => 200 )
1102
		);
1103
		if ( !is_array( $thumbnail_size ) ) {
1104
			$thumbnail_size = array(
1105
				'width' => (int)$thumbnail_size,
1106
				'height' => (int)$thumbnail_size
1107
			);
1108
		}
1109
1110
		// Try to get post image
1111
		if ( class_exists( 'Jetpack_PostImages' ) ) {
1112
			$img_url = '';
1113
			$post_image = Jetpack_PostImages::get_image(
1114
				$post_id,
1115
				$thumbnail_size
1116
			);
1117
1118
			if ( is_array($post_image) ) {
1119
				$img_url = $post_image['src'];
1120
			} elseif ( class_exists( 'Jetpack_Media_Summary' ) ) {
1121
				$media = Jetpack_Media_Summary::get( $post_id );
1122
1123
				if ( is_array($media) && !empty( $media['image'] ) ) {
1124
					$img_url = $media['image'];
1125
				}
1126
			}
1127
1128
			if ( !empty( $img_url ) ) {
1129
				$image_params['width'] = $thumbnail_size['width'];
1130
				$image_params['height'] = $thumbnail_size['height'];
1131
				$image_params['src'] = Jetpack_PostImages::fit_image_url(
1132
					$img_url,
1133
					$thumbnail_size['width'],
1134
					$thumbnail_size['height']
1135
				);
1136
			}
1137
		}
1138
1139
		return $image_params;
1140
	}
1141
1142
	/**
1143
	 * Returns the string UTF-8 encoded
1144
	 *
1145
	 * @param string $text
1146
	 * @return string
1147
	 */
1148
	protected function _to_utf8( $text ) {
1149
		if ( $this->_convert_charset ) {
1150
			return iconv( $this->_blog_charset, 'UTF-8', $text );
1151
		} else {
1152
			return $text;
1153
		}
1154
	}
1155
1156
	/**
1157
	 * =============================================
1158
	 * PROTECTED UTILITY FUNCTIONS EXTENDED BY WPCOM
1159
	 * =============================================
1160
	 */
1161
1162
	/**
1163
	 * Workhorse method to return array of related posts matched by ElasticSearch.
1164
	 *
1165
	 * @param int $post_id
1166
	 * @param int $size
1167
	 * @param array $filters
1168
	 * @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
1169
	 * @return array
1170
	 */
1171
	protected function _get_related_posts( $post_id, $size, array $filters ) {
1172
		$hits = $this->_filter_non_public_posts(
1173
			$this->_get_related_post_ids(
1174
				$post_id,
1175
				$size,
1176
				$filters
1177
			)
1178
		);
1179
1180
		/**
1181
		 * Filter the Related Posts matched by ElasticSearch.
1182
		 *
1183
		 * @module related-posts
1184
		 *
1185
		 * @since 2.9.0
1186
		 *
1187
		 * @param array $hits Array of Post IDs matched by ElasticSearch.
1188
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1189
		 */
1190
		$hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
1191
1192
		$related_posts = array();
1193
		foreach ( $hits as $i => $hit ) {
1194
			$related_posts[] = $this->_get_related_post_data_for_post( $hit['id'], $i, $post_id );
1195
		}
1196
		return $related_posts;
1197
	}
1198
1199
	/**
1200
	 * Get array of related posts matched by ElasticSearch.
1201
	 *
1202
	 * @param int $post_id
1203
	 * @param int $size
1204
	 * @param array $filters
1205
	 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body, get_post_meta, update_post_meta
1206
	 * @return array
1207
	 */
1208
	protected function _get_related_post_ids( $post_id, $size, array $filters ) {
1209
		$now_ts = time();
1210
		$cache_meta_key = '_jetpack_related_posts_cache';
1211
1212
		$body = array(
1213
			'size' => (int) $size,
1214
		);
1215
1216
		if ( !empty( $filters ) )
1217
			$body['filter'] = array( 'and' => $filters );
1218
1219
		// Build cache key
1220
		$cache_key = md5( serialize( $body ) );
1221
1222
		// Load all cached values
1223
		if ( wp_using_ext_object_cache() ) {
1224
			$transient_name = "{$cache_meta_key}_{$cache_key}_{$post_id}";
1225
			$cache = get_transient( $transient_name );
1226
			if ( false !== $cache ) {
1227
				return $cache;
1228
			}
1229
		} else {
1230
			$cache = get_post_meta( $post_id, $cache_meta_key, true );
1231
1232
			if ( empty( $cache ) )
1233
				$cache = array();
1234
1235
1236
			// Cache is valid! Return cached value.
1237
			if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) && $cache[ $cache_key ][ 'expires' ] > $now_ts ) {
1238
				return $cache[ $cache_key ][ 'payload' ];
1239
			}
1240
		}
1241
1242
		$response = wp_remote_post(
1243
			"https://public-api.wordpress.com/rest/v1/sites/{$this->_blog_id_wpcom}/posts/$post_id/related/",
1244
			array(
1245
				'timeout' => 10,
1246
				'user-agent' => 'jetpack_related_posts',
1247
				'sslverify' => true,
1248
				'body' => $body,
1249
			)
1250
		);
1251
1252
		// Oh no... return nothing don't cache errors.
1253
		if ( is_wp_error( $response ) ) {
1254
			if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) )
1255
				return $cache[ $cache_key ][ 'payload' ]; // return stale
1256
			else
1257
				return array();
1258
		}
1259
1260
		$results = json_decode( wp_remote_retrieve_body( $response ), true );
1261
		$related_posts = array();
1262
		if ( is_array( $results ) && !empty( $results['hits'] ) ) {
1263
			foreach( $results['hits'] as $hit ) {
1264
				$related_posts[] = array(
1265
					'id' => $hit['fields']['post_id'],
1266
				);
1267
			}
1268
		}
1269
1270
		// An empty array might indicate no related posts or that posts
1271
		// are not yet synced to WordPress.com, so we cache for only 1
1272
		// minute in this case
1273
		if ( empty( $related_posts ) ) {
1274
			$cache_ttl = 60;
1275
		} else {
1276
			$cache_ttl = 12 * HOUR_IN_SECONDS;
1277
		}
1278
1279
		// Update cache
1280
		if ( wp_using_ext_object_cache() ) {
1281
			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...
1282
		} else {
1283
			// Copy all valid cache values
1284
			$new_cache = array();
1285
			foreach ( $cache as $k => $v ) {
1286
				if ( is_array( $v ) && $v[ 'expires' ] > $now_ts ) {
1287
					$new_cache[ $k ] = $v;
1288
				}
1289
			}
1290
1291
			// Set new cache value
1292
			$cache_expires = $cache_ttl + $now_ts;
1293
			$new_cache[ $cache_key ] = array(
1294
				'expires' => $cache_expires,
1295
				'payload' => $related_posts,
1296
			);
1297
			update_post_meta( $post_id, $cache_meta_key, $new_cache );
1298
		}
1299
1300
		return $related_posts;
1301
	}
1302
1303
	/**
1304
	 * Filter out any hits that are not public anymore.
1305
	 *
1306
	 * @param array $related_posts
1307
	 * @uses get_post_stati, get_post_status
1308
	 * @return array
1309
	 */
1310
	protected function _filter_non_public_posts( array $related_posts ) {
1311
		$public_stati = get_post_stati( array( 'public' => true ) );
1312
1313
		$filtered = array();
1314
		foreach ( $related_posts as $hit ) {
1315
			if ( in_array( get_post_status( $hit['id'] ), $public_stati ) ) {
1316
				$filtered[] = $hit;
1317
			}
1318
		}
1319
		return $filtered;
1320
	}
1321
1322
	/**
1323
	 * Generates a context for the related content (second line in related post output).
1324
	 * Order of importance:
1325
	 *   - First category (Not 'Uncategorized')
1326
	 *   - First post tag
1327
	 *   - Number of comments
1328
	 *
1329
	 * @param int $post_id
1330
	 * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n
1331
	 * @return string
1332
	 */
1333
	protected function _generate_related_post_context( $post_id ) {
1334
		$categories = get_the_category( $post_id );
1335 View Code Duplication
		if ( is_array( $categories ) ) {
1336
			foreach ( $categories as $category ) {
1337
				if ( 'uncategorized' != $category->slug && '' != trim( $category->name ) ) {
1338
					$post_cat_context = sprintf(
1339
						_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1340
						$category->name
1341
					);
1342
					/**
1343
					 * Filter the "In Category" line displayed in the post context below each Related Post.
1344
					 *
1345
					 * @module related-posts
1346
					 *
1347
					 * @since 3.2.0
1348
					 *
1349
					 * @param string $post_cat_context "In Category" line displayed in the post context below each Related Post.
1350
					 * @param array $category Array containing information about the category.
1351
					 */
1352
					return apply_filters( 'jetpack_relatedposts_post_category_context', $post_cat_context, $category );
1353
				}
1354
			}
1355
		}
1356
1357
		$tags = get_the_terms( $post_id, 'post_tag' );
1358 View Code Duplication
		if ( is_array( $tags ) ) {
1359
			foreach ( $tags as $tag ) {
1360
				if ( '' != trim( $tag->name ) ) {
1361
					$post_tag_context = sprintf(
1362
						_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1363
						$tag->name
1364
					);
1365
					/**
1366
					 * Filter the "In Tag" line displayed in the post context below each Related Post.
1367
					 *
1368
					 * @module related-posts
1369
					 *
1370
					 * @since 3.2.0
1371
					 *
1372
					 * @param string $post_tag_context "In Tag" line displayed in the post context below each Related Post.
1373
					 * @param array $tag Array containing information about the tag.
1374
					 */
1375
					return apply_filters( 'jetpack_relatedposts_post_tag_context', $post_tag_context, $tag );
1376
				}
1377
			}
1378
		}
1379
1380
		$comment_count = get_comments_number( $post_id );
1381
		if ( $comment_count > 0 ) {
1382
			return sprintf(
1383
				_n( 'With 1 comment', 'With %s comments', $comment_count, 'jetpack' ),
1384
				number_format_i18n( $comment_count )
1385
			);
1386
		}
1387
1388
		return __( 'Similar post', 'jetpack' );
1389
	}
1390
1391
	/**
1392
	 * Logs clicks for clickthrough analysis and related result tuning.
1393
	 *
1394
	 * @return null
1395
	 */
1396
	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...
1397
1398
	}
1399
1400
	/**
1401
	 * Determines if the current post is able to use related posts.
1402
	 *
1403
	 * @uses self::get_options, is_admin, is_single, apply_filters
1404
	 * @return bool
1405
	 */
1406
	protected function _enabled_for_request() {
1407
		$enabled = is_single() 
1408
			&&
1409
				! is_admin()
1410
			&&
1411
				( !$this->_allow_feature_toggle() || $this->get_option( 'enabled' ) );
1412
1413
		/**
1414
		 * Filter the Enabled value to allow related posts to be shown on pages as well.
1415
		 *
1416
		 * @module related-posts
1417
		 *
1418
		 * @since 3.3.0
1419
		 *
1420
		 * @param bool $enabled Should Related Posts be enabled on the current page.
1421
		 */
1422
		return apply_filters( 'jetpack_relatedposts_filter_enabled_for_request', $enabled );
1423
	}
1424
1425
	/**
1426
	 * Adds filters and enqueues assets.
1427
	 *
1428
	 * @uses self::_enqueue_assets, self::_setup_shortcode, add_filter
1429
	 * @return null
1430
	 */
1431
	protected function _action_frontend_init_page() {
1432
		$this->_enqueue_assets( true, true );
1433
		$this->_setup_shortcode();
1434
1435
		add_filter( 'the_content', array( $this, 'filter_add_target_to_dom' ), 40 );
1436
	}
1437
1438
	/**
1439
	 * Enqueues assets needed to do async loading of related posts.
1440
	 *
1441
	 * @uses wp_enqueue_script, wp_enqueue_style, plugins_url
1442
	 * @return null
1443
	 */
1444
	protected function _enqueue_assets( $script, $style ) {
1445
		$dependencies = is_customize_preview() ? array( 'customize-base' ) : array( 'jquery' );
1446
		if ( $script ) {
1447
			wp_enqueue_script( 'jetpack_related-posts', plugins_url( 'related-posts.js', __FILE__ ), $dependencies, self::VERSION );
1448
			$related_posts_js_options = array(
1449
				/**
1450
				 * Filter each Related Post Heading structure.
1451
				 *
1452
				 * @since 4.0.0
1453
				 *
1454
				 * @param string $str Related Post Heading structure. Default to h4.
1455
				 */
1456
				'post_heading' => apply_filters( 'jetpack_relatedposts_filter_post_heading', esc_attr( 'h4' ) ),
1457
			);
1458
			wp_localize_script( 'jetpack_related-posts', 'related_posts_js_options', $related_posts_js_options );
1459
		}
1460
		if ( $style ){
1461
			if( is_rtl() ) {
1462
				wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'rtl/related-posts-rtl.css', __FILE__ ), array(), self::VERSION );
1463
			} else {
1464
				wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'related-posts.css', __FILE__ ), array(), self::VERSION );
1465
			}
1466
		}
1467
	}
1468
1469
	/**
1470
	 * Sets up the shortcode processing.
1471
	 *
1472
	 * @uses add_filter, add_shortcode
1473
	 * @return null
1474
	 */
1475
	protected function _setup_shortcode() {
1476
		add_filter( 'the_content', array( $this, 'test_for_shortcode' ), 0 );
1477
1478
		add_shortcode( self::SHORTCODE, array( $this, 'get_target_html' ) );
1479
	}
1480
1481
	protected function _allow_feature_toggle() {
1482
		if ( null === $this->_allow_feature_toggle ) {
1483
			/**
1484
			 * Filter the display of the Related Posts toggle in Settings > Reading.
1485
			 *
1486
			 * @module related-posts
1487
			 *
1488
			 * @since 2.8.0
1489
			 *
1490
			 * @param bool false Display a feature toggle. Default to false.
1491
			 */
1492
			$this->_allow_feature_toggle = apply_filters( 'jetpack_relatedposts_filter_allow_feature_toggle', false );
1493
		}
1494
		return $this->_allow_feature_toggle;
1495
	}
1496
1497
	/**
1498
	 * ===================================================
1499
	 * FUNCTIONS EXPOSING RELATED POSTS IN THE WP REST API
1500
	 * ===================================================
1501
	 */
1502
1503
	/**
1504
	 * Add Related Posts to the REST API Post response.
1505
	 *
1506
	 * @since 4.4.0
1507
	 *
1508
	 * @action rest_api_init
1509
	 * @uses register_rest_field, self::rest_get_related_posts
1510
	 * @return null
1511
	 */
1512
	public function rest_register_related_posts() {
1513
		register_rest_field( 'post',
1514
			'jetpack-related-posts',
1515
			array(
1516
				'get_callback' => array( $this, 'rest_get_related_posts' ),
1517
				'update_callback' => null,
1518
				'schema'          => null,
1519
			)
1520
		);
1521
	}
1522
1523
	/**
1524
	 * Build an array of Related Posts.
1525
	 *
1526
	 * @since 4.4.0
1527
	 *
1528
	 * @param array $object Details of current post.
1529
	 * @param string $field_name Name of field.
1530
	 * @param WP_REST_Request $request Current request
1531
	 *
1532
	 * @uses self::get_for_post_id
1533
	 *
1534
	 * @return array
1535
	 */
1536
	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...
1537
		return $this->get_for_post_id( $object['id'], array() );
1538
	}
1539
}
1540
1541
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...
1542
	protected $_query_name;
1543
1544
	/**
1545
	 * Allows callers of this class to tag each query with a unique name for tracking purposes.
1546
	 *
1547
	 * @param string $name
1548
	 * @return Jetpack_RelatedPosts_Raw
1549
	 */
1550
	public function set_query_name( $name ) {
1551
		$this->_query_name = (string) $name;
1552
		return $this;
1553
	}
1554
1555
	/**
1556
	 * The raw related posts class can be used by other plugins or themes
1557
	 * to get related content. This class wraps the existing RelatedPosts
1558
	 * logic thus we never want to add anything to the DOM or do anything
1559
	 * for event hooks. We will also not present any settings for this
1560
	 * class and keep it enabled as calls to this class is done
1561
	 * programmatically.
1562
	 */
1563
	public function action_admin_init() {}
1564
	public function action_frontend_init() {}
1565
	public function get_options() {
1566
		return array(
1567
			'enabled' => true,
1568
		);
1569
	}
1570
1571
	/**
1572
	 * Workhorse method to return array of related posts ids matched by ElasticSearch.
1573
	 *
1574
	 * @param int $post_id
1575
	 * @param int $size
1576
	 * @param array $filters
1577
	 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body
1578
	 * @return array
1579
	 */
1580
	protected function _get_related_posts( $post_id, $size, array $filters ) {
1581
		$hits = $this->_filter_non_public_posts(
1582
			$this->_get_related_post_ids(
1583
				$post_id,
1584
				$size,
1585
				$filters
1586
			)
1587
		);
1588
1589
		/** This filter is already documented in modules/related-posts/related-posts.php */
1590
		$hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
1591
1592
		return $hits;
1593
	}
1594
}
1595