Completed
Push — master-stable ( f5da56...c14258 )
by
unknown
09:30
created

Jetpack_RelatedPosts_Raw::action_admin_init()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 1
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 1
rs 10
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
				$excluded_post_ids = array();
774
				if ( $exclude_post_id > 0 )
775
					$excluded_post_ids[] = $exclude_post_id;
776
			}
777
			$filters[] = array( 'not' => array( 'terms' => array( 'post_id' => $excluded_post_ids ) ) );
0 ignored issues
show
Bug introduced by
The variable $excluded_post_ids 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...
778
		}
779
780
		return $filters;
781
	}
782
783
	/**
784
	 * Takes a range and coalesces it into a month interval bracketed by a time as determined by the blog_id to enhance caching.
785
	 *
786
	 * @param array $date_range
787
	 * @return array
788
	 */
789
	protected function _get_coalesced_range( array $date_range ) {
790
		$now = time();
791
		$coalesce_time = $this->_blog_id_wpcom % 86400;
792
		$current_time = $now - strtotime( 'today', $now );
793
794
		if ( $current_time < $coalesce_time && '01' == date( 'd', $now ) ) {
795
			// Move back 1 period
796
			return array(
797
				'from' => date( 'Y-m-01', strtotime( '-1 month', $date_range['from'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),
798
				'to'   => date( 'Y-m-01', $date_range['to'] ) . ' ' . date( 'H:i:s', $coalesce_time ),
799
			);
800
		} else {
801
			// Use current period
802
			return array(
803
				'from' => date( 'Y-m-01', $date_range['from'] ) . ' ' . date( 'H:i:s', $coalesce_time ),
804
				'to'   => date( 'Y-m-01', strtotime( '+1 month', $date_range['to'] ) ) . ' ' . date( 'H:i:s', $coalesce_time ),
805
			);
806
		}
807
	}
808
809
	/**
810
	 * Generate and output ajax response for related posts API call.
811
	 * NOTE: Calls exit() to end all further processing after payload has been outputed.
812
	 *
813
	 * @param array $excludes array of post_ids to exclude
814
	 * @uses send_nosniff_header, self::get_for_post_id, get_the_ID
815
	 * @return null
816
	 */
817
	protected function _action_frontend_init_ajax( array $excludes ) {
818
		define( 'DOING_AJAX', true );
819
820
		header( 'Content-type: application/json; charset=utf-8' ); // JSON can only be UTF-8
821
		send_nosniff_header();
822
823
		$options = $this->get_options();
824
825
		if ( isset( $_GET['jetpackrpcustomize'] ) ) {
826
827
			// If we're in the customizer, add dummy content.
828
			$date_now = current_time( get_option( 'date_format' ) );
829
			$related_posts = array(
830
				array(
831
					'id'       => - 1,
832
					'url'      => 'https://jetpackme.files.wordpress.com/2014/08/1-wpios-ipad-3-1-viewsite.png?w=350&h=200&crop=1',
833
					'url_meta' => array(
834
						'origin'   => 0,
835
						'position' => 0
836
					),
837
					'title'    => esc_html__( 'Big iPhone/iPad Update Now Available', 'jetpack' ),
838
					'date'     => $date_now,
839
					'format'   => false,
840
					'excerpt'  => esc_html__( 'It is that time of the year when devices are shiny again.', 'jetpack' ),
841
					'rel'      => 'nofollow',
842
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
843
					'img'      => array(
844
						'src'    => 'https://jetpackme.files.wordpress.com/2014/08/1-wpios-ipad-3-1-viewsite.png?w=350&h=200&crop=1',
845
						'width'  => 350,
846
						'height' => 200
847
					),
848
					'classes'  => array()
849
				),
850
				array(
851
					'id'       => - 1,
852
					'url'      => 'https://jetpackme.files.wordpress.com/2014/08/wordpress-com-news-wordpress-for-android-ui-update2.jpg?w=350&h=200&crop=1',
853
					'url_meta' => array(
854
						'origin'   => 0,
855
						'position' => 0
856
					),
857
					'title'    => esc_html__( 'The WordPress for Android App Gets a Big Facelift', 'jetpack' ),
858
					'date'     => $date_now,
859
					'format'   => false,
860
					'excerpt'  => esc_html__( 'Writing is new again in Android with the new WordPress app.', 'jetpack' ),
861
					'rel'      => 'nofollow',
862
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
863
					'img'      => array(
864
						'src'    => 'https://jetpackme.files.wordpress.com/2014/08/wordpress-com-news-wordpress-for-android-ui-update2.jpg?w=350&h=200&crop=1',
865
						'width'  => 350,
866
						'height' => 200
867
					),
868
					'classes'  => array()
869
				),
870
				array(
871
					'id'       => - 1,
872
					'url'      => 'https://jetpackme.files.wordpress.com/2014/08/videopresswedding.jpg?w=350&h=200&crop=1',
873
					'url_meta' => array(
874
						'origin'   => 0,
875
						'position' => 0
876
					),
877
					'title'    => esc_html__( 'Upgrade Focus, VideoPress for weddings', 'jetpack' ),
878
					'date'     => $date_now,
879
					'format'   => false,
880
					'excerpt'  => esc_html__( 'Weddings are in the spotlight now with VideoPress for weddings.', 'jetpack' ),
881
					'rel'      => 'nofollow',
882
					'context'  => esc_html__( 'In "Mobile"', 'jetpack' ),
883
					'img'      => array(
884
						'src'    => 'https://jetpackme.files.wordpress.com/2014/08/videopresswedding.jpg?w=350&h=200&crop=1',
885
						'width'  => 350,
886
						'height' => 200
887
					),
888
					'classes'  => array()
889
				),
890
			);
891
892
			for ( $total = 0; $total < $options['size'] - 3; $total++ ) {
893
				$related_posts[] = $related_posts[ $total ];
894
			}
895
896
			$current_post = get_post();
897
898
			// Exclude current post after filtering to make sure it's excluded and not lost during filtering.
899
			$excluded_posts = array_merge(
900
				/** This filter is already documented in modules/related-posts/jetpack-related-posts.php */
901
				apply_filters( 'jetpack_relatedposts_filter_exclude_post_ids', array() ),
902
				array( $current_post->ID )
903
			);
904
905
			// Fetch posts with featured image.
906
			$with_post_thumbnails = get_posts( array(
907
				'posts_per_page'   => $options['size'],
908
				'post__not_in'     => $excluded_posts,
909
				'post_type'        => $current_post->post_type,
910
				'meta_key'         => '_thumbnail_id',
911
				'suppress_filters' => false,
912
			) );
913
914
			// If we don't have enough, fetch posts without featured image.
915
			if ( 0 < ( $more = $options['size'] - count( $with_post_thumbnails ) ) ) {
916
				$no_post_thumbnails = get_posts( array(
917
					'posts_per_page'  => $more,
918
					'post__not_in'    => $excluded_posts,
919
					'post_type'       => $current_post->post_type,
920
					'meta_query' => array(
921
						array(
922
							'key'     => '_thumbnail_id',
923
							'compare' => 'NOT EXISTS',
924
						),
925
					),
926
					'suppress_filters' => false,
927
				) );
928
			} else {
929
				$no_post_thumbnails = array();
930
			}
931
932
			foreach ( array_merge( $with_post_thumbnails, $no_post_thumbnails ) as $index => $real_post ) {
933
				$related_posts[ $index ]['id']      = $real_post->ID;
934
				$related_posts[ $index ]['url']     = esc_url( get_permalink( $real_post ) );
935
				$related_posts[ $index ]['title']   = $this->_to_utf8( $this->_get_title( $real_post->post_title, $real_post->post_content ) );
936
				$related_posts[ $index ]['date']    = get_the_date( '', $real_post );
937
				$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' );
938
				$related_posts[ $index ]['img']     = $this->_generate_related_post_image_params( $real_post->ID );
939
				$related_posts[ $index ]['context'] = $this->_generate_related_post_context( $real_post->ID );
940
			}
941
		} else {
942
			$related_posts = $this->get_for_post_id(
943
				get_the_ID(),
944
				array(
945
					'exclude_post_ids' => $excludes,
946
				)
947
			);
948
		}
949
950
		$response = array(
951
			'version' => self::VERSION,
952
			'show_thumbnails' => (bool) $options['show_thumbnails'],
953
			'show_date' => (bool) $options['show_date'],
954
			'show_context' => (bool) $options['show_context'],
955
			'layout' => (string) $options['layout'],
956
			'headline' => (string) $options['headline'],
957
			'items' => array(),
958
		);
959
960
		if ( count( $related_posts ) == $options['size'] )
961
			$response['items'] = $related_posts;
962
963
		echo json_encode( $response );
964
965
		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...
966
	}
967
968
	/**
969
	 * Returns a UTF-8 encoded array of post information for the given post_id
970
	 *
971
	 * @param int $post_id
972
	 * @param int $position
973
	 * @param int $origin The post id that this is related to
974
	 * @uses get_post, get_permalink, remove_query_arg, get_post_format, apply_filters
975
	 * @return array
976
	 */
977
	protected function _get_related_post_data_for_post( $post_id, $position, $origin ) {
978
		$post = get_post( $post_id );
979
980
		return array(
981
			'id' => $post->ID,
982
			'url' => get_permalink( $post->ID ),
983
			'url_meta' => array( 'origin' => $origin, 'position' => $position ),
984
			'title' => $this->_to_utf8( $this->_get_title( $post->post_title, $post->post_content ) ),
985
			'date' => get_the_date( '', $post->ID ),
986
			'format' => get_post_format( $post->ID ),
987
			'excerpt' => html_entity_decode( $this->_to_utf8( $this->_get_excerpt( $post->post_excerpt, $post->post_content ) ), ENT_QUOTES, 'UTF-8' ),
988
			/**
989
			 * Filters the rel attribute for the Related Posts' links.
990
			 *
991
			 * @module related-posts
992
			 *
993
			 * @since 3.7.0
994
			 *
995
			 * @param string nofollow Link rel attribute for Related Posts' link. Default is nofollow.
996
			 * @param int $post->ID Post ID.
997
			 */
998
			'rel' => apply_filters( 'jetpack_relatedposts_filter_post_link_rel', 'nofollow', $post->ID ),
999
			/**
1000
			 * Filter the context displayed below each Related Post.
1001
			 *
1002
			 * @module related-posts
1003
			 *
1004
			 * @since 3.0.0
1005
			 *
1006
			 * @param string $this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ) Context displayed below each related post.
1007
			 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1008
			 */
1009
			'context' => apply_filters(
1010
				'jetpack_relatedposts_filter_post_context',
1011
				$this->_to_utf8( $this->_generate_related_post_context( $post->ID ) ),
1012
				$post->ID
1013
			),
1014
			'img' => $this->_generate_related_post_image_params( $post->ID ),
1015
			/**
1016
			 * Filter the post css classes added on HTML markup.
1017
			 *
1018
			 * @module related-posts
1019
			 *
1020
			 * @since 3.8.0
1021
			 *
1022
			 * @param array array() CSS classes added on post HTML markup.
1023
			 * @param string $post_id Post ID.
1024
			 */
1025
			'classes' => apply_filters(
1026
				'jetpack_relatedposts_filter_post_css_classes',
1027
				array(),
1028
				$post->ID
1029
			),
1030
		);
1031
	}
1032
1033
	/**
1034
	 * Returns either the title or a small excerpt to use as title for post.
1035
	 *
1036
	 * @param string $post_title
1037
	 * @param string $post_content
1038
	 * @uses strip_shortcodes, wp_trim_words, __
1039
	 * @return string
1040
	 */
1041
	protected function _get_title( $post_title, $post_content ) {
1042
		if ( ! empty( $post_title ) ) {
1043
			return wp_strip_all_tags( $post_title );
1044
		}
1045
1046
		$post_title = wp_trim_words( wp_strip_all_tags( strip_shortcodes( $post_content ) ), 5, '…' );
1047
		if ( ! empty( $post_title ) ) {
1048
			return $post_title;
1049
		}
1050
1051
		return __( 'Untitled Post', 'jetpack' );
1052
	}
1053
1054
	/**
1055
	 * Returns a plain text post excerpt for title attribute of links.
1056
	 *
1057
	 * @param string $post_excerpt
1058
	 * @param string $post_content
1059
	 * @uses strip_shortcodes, wp_strip_all_tags, wp_trim_words
1060
	 * @return string
1061
	 */
1062
	protected function _get_excerpt( $post_excerpt, $post_content ) {
1063
		if ( empty( $post_excerpt ) )
1064
			$excerpt = $post_content;
1065
		else
1066
			$excerpt = $post_excerpt;
1067
1068
		return wp_trim_words( wp_strip_all_tags( strip_shortcodes( $excerpt ) ), 50, '…' );
1069
	}
1070
1071
	/**
1072
	 * Generates the thumbnail image to be used for the post. Uses the
1073
	 * image as returned by Jetpack_PostImages::get_image()
1074
	 *
1075
	 * @param int $post_id
1076
	 * @uses self::get_options, apply_filters, Jetpack_PostImages::get_image, Jetpack_PostImages::fit_image_url
1077
	 * @return string
1078
	 */
1079
	protected function _generate_related_post_image_params( $post_id ) {
1080
		$options = $this->get_options();
1081
		$image_params = array(
1082
			'src' => '',
1083
			'width' => 0,
1084
			'height' => 0,
1085
		);
1086
1087
		if ( ! $options['show_thumbnails'] ) {
1088
			return $image_params;
1089
		}
1090
1091
		/**
1092
		 * Filter the size of the Related Posts images.
1093
		 *
1094
		 * @module related-posts
1095
		 *
1096
		 * @since 2.8.0
1097
		 *
1098
		 * @param array array( 'width' => 350, 'height' => 200 ) Size of the images displayed below each Related Post.
1099
		 */
1100
		$thumbnail_size = apply_filters(
1101
			'jetpack_relatedposts_filter_thumbnail_size',
1102
			array( 'width' => 350, 'height' => 200 )
1103
		);
1104
		if ( !is_array( $thumbnail_size ) ) {
1105
			$thumbnail_size = array(
1106
				'width' => (int)$thumbnail_size,
1107
				'height' => (int)$thumbnail_size
1108
			);
1109
		}
1110
1111
		// Try to get post image
1112
		if ( class_exists( 'Jetpack_PostImages' ) ) {
1113
			$img_url = '';
1114
			$post_image = Jetpack_PostImages::get_image(
1115
				$post_id,
1116
				$thumbnail_size
1117
			);
1118
1119
			if ( is_array($post_image) ) {
1120
				$img_url = $post_image['src'];
1121
			} elseif ( class_exists( 'Jetpack_Media_Summary' ) ) {
1122
				$media = Jetpack_Media_Summary::get( $post_id );
1123
1124
				if ( is_array($media) && !empty( $media['image'] ) ) {
1125
					$img_url = $media['image'];
1126
				}
1127
			}
1128
1129
			if ( !empty( $img_url ) ) {
1130
				$image_params['width'] = $thumbnail_size['width'];
1131
				$image_params['height'] = $thumbnail_size['height'];
1132
				$image_params['src'] = Jetpack_PostImages::fit_image_url(
1133
					$img_url,
1134
					$thumbnail_size['width'],
1135
					$thumbnail_size['height']
1136
				);
1137
			}
1138
		}
1139
1140
		return $image_params;
1141
	}
1142
1143
	/**
1144
	 * Returns the string UTF-8 encoded
1145
	 *
1146
	 * @param string $text
1147
	 * @return string
1148
	 */
1149
	protected function _to_utf8( $text ) {
1150
		if ( $this->_convert_charset ) {
1151
			return iconv( $this->_blog_charset, 'UTF-8', $text );
1152
		} else {
1153
			return $text;
1154
		}
1155
	}
1156
1157
	/**
1158
	 * =============================================
1159
	 * PROTECTED UTILITY FUNCTIONS EXTENDED BY WPCOM
1160
	 * =============================================
1161
	 */
1162
1163
	/**
1164
	 * Workhorse method to return array of related posts matched by ElasticSearch.
1165
	 *
1166
	 * @param int $post_id
1167
	 * @param int $size
1168
	 * @param array $filters
1169
	 * @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
1170
	 * @return array
1171
	 */
1172
	protected function _get_related_posts( $post_id, $size, array $filters ) {
1173
		$hits = $this->_filter_non_public_posts(
1174
			$this->_get_related_post_ids(
1175
				$post_id,
1176
				$size,
1177
				$filters
1178
			)
1179
		);
1180
1181
		/**
1182
		 * Filter the Related Posts matched by ElasticSearch.
1183
		 *
1184
		 * @module related-posts
1185
		 *
1186
		 * @since 2.9.0
1187
		 *
1188
		 * @param array $hits Array of Post IDs matched by ElasticSearch.
1189
		 * @param string $post_id Post ID of the post for which we are retrieving Related Posts.
1190
		 */
1191
		$hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
1192
1193
		$related_posts = array();
1194
		foreach ( $hits as $i => $hit ) {
1195
			$related_posts[] = $this->_get_related_post_data_for_post( $hit['id'], $i, $post_id );
1196
		}
1197
		return $related_posts;
1198
	}
1199
1200
	/**
1201
	 * Get array of related posts matched by ElasticSearch.
1202
	 *
1203
	 * @param int $post_id
1204
	 * @param int $size
1205
	 * @param array $filters
1206
	 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body, get_post_meta, update_post_meta
1207
	 * @return array
1208
	 */
1209
	protected function _get_related_post_ids( $post_id, $size, array $filters ) {
1210
		$now_ts = time();
1211
		$cache_meta_key = '_jetpack_related_posts_cache';
1212
1213
		$body = array(
1214
			'size' => (int) $size,
1215
		);
1216
1217
		if ( !empty( $filters ) )
1218
			$body['filter'] = array( 'and' => $filters );
1219
1220
		// Build cache key
1221
		$cache_key = md5( serialize( $body ) );
1222
1223
		// Load all cached values
1224
		if ( wp_using_ext_object_cache() ) {
1225
			$transient_name = "{$cache_meta_key}_{$cache_key}_{$post_id}";
1226
			$cache = get_transient( $transient_name );
1227
			if ( false !== $cache ) {
1228
				return $cache;
1229
			}
1230
		} else {
1231
			$cache = get_post_meta( $post_id, $cache_meta_key, true );
1232
1233
			if ( empty( $cache ) )
1234
				$cache = array();
1235
1236
1237
			// Cache is valid! Return cached value.
1238
			if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) && $cache[ $cache_key ][ 'expires' ] > $now_ts ) {
1239
				return $cache[ $cache_key ][ 'payload' ];
1240
			}
1241
		}
1242
1243
		$response = wp_remote_post(
1244
			"https://public-api.wordpress.com/rest/v1/sites/{$this->_blog_id_wpcom}/posts/$post_id/related/",
1245
			array(
1246
				'timeout' => 10,
1247
				'user-agent' => 'jetpack_related_posts',
1248
				'sslverify' => true,
1249
				'body' => $body,
1250
			)
1251
		);
1252
1253
		// Oh no... return nothing don't cache errors.
1254
		if ( is_wp_error( $response ) ) {
1255
			if ( isset( $cache[ $cache_key ] ) && is_array( $cache[ $cache_key ] ) )
1256
				return $cache[ $cache_key ][ 'payload' ]; // return stale
1257
			else
1258
				return array();
1259
		}
1260
1261
		$results = json_decode( wp_remote_retrieve_body( $response ), true );
1262
		$related_posts = array();
1263
		if ( is_array( $results ) && !empty( $results['hits'] ) ) {
1264
			foreach( $results['hits'] as $hit ) {
1265
				$related_posts[] = array(
1266
					'id' => $hit['fields']['post_id'],
1267
				);
1268
			}
1269
		}
1270
1271
		// An empty array might indicate no related posts or that posts
1272
		// are not yet synced to WordPress.com, so we cache for only 1
1273
		// minute in this case
1274
		if ( empty( $related_posts ) ) {
1275
			$cache_ttl = 60;
1276
		} else {
1277
			$cache_ttl = 12 * HOUR_IN_SECONDS;
1278
		}
1279
1280
		// Update cache
1281
		if ( wp_using_ext_object_cache() ) {
1282
			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...
1283
		} else {
1284
			// Copy all valid cache values
1285
			$new_cache = array();
1286
			foreach ( $cache as $k => $v ) {
1287
				if ( is_array( $v ) && $v[ 'expires' ] > $now_ts ) {
1288
					$new_cache[ $k ] = $v;
1289
				}
1290
			}
1291
1292
			// Set new cache value
1293
			$cache_expires = $cache_ttl + $now_ts;
1294
			$new_cache[ $cache_key ] = array(
1295
				'expires' => $cache_expires,
1296
				'payload' => $related_posts,
1297
			);
1298
			update_post_meta( $post_id, $cache_meta_key, $new_cache );
1299
		}
1300
1301
		return $related_posts;
1302
	}
1303
1304
	/**
1305
	 * Filter out any hits that are not public anymore.
1306
	 *
1307
	 * @param array $related_posts
1308
	 * @uses get_post_stati, get_post_status
1309
	 * @return array
1310
	 */
1311
	protected function _filter_non_public_posts( array $related_posts ) {
1312
		$public_stati = get_post_stati( array( 'public' => true ) );
1313
1314
		$filtered = array();
1315
		foreach ( $related_posts as $hit ) {
1316
			if ( in_array( get_post_status( $hit['id'] ), $public_stati ) ) {
1317
				$filtered[] = $hit;
1318
			}
1319
		}
1320
		return $filtered;
1321
	}
1322
1323
	/**
1324
	 * Generates a context for the related content (second line in related post output).
1325
	 * Order of importance:
1326
	 *   - First category (Not 'Uncategorized')
1327
	 *   - First post tag
1328
	 *   - Number of comments
1329
	 *
1330
	 * @param int $post_id
1331
	 * @uses get_the_category, get_the_terms, get_comments_number, number_format_i18n, __, _n
1332
	 * @return string
1333
	 */
1334
	protected function _generate_related_post_context( $post_id ) {
1335
		$categories = get_the_category( $post_id );
1336 View Code Duplication
		if ( is_array( $categories ) ) {
1337
			foreach ( $categories as $category ) {
1338
				if ( 'uncategorized' != $category->slug && '' != trim( $category->name ) ) {
1339
					$post_cat_context = sprintf(
1340
						_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1341
						$category->name
1342
					);
1343
					/**
1344
					 * Filter the "In Category" line displayed in the post context below each Related Post.
1345
					 *
1346
					 * @module related-posts
1347
					 *
1348
					 * @since 3.2.0
1349
					 *
1350
					 * @param string $post_cat_context "In Category" line displayed in the post context below each Related Post.
1351
					 * @param array $category Array containing information about the category.
1352
					 */
1353
					return apply_filters( 'jetpack_relatedposts_post_category_context', $post_cat_context, $category );
1354
				}
1355
			}
1356
		}
1357
1358
		$tags = get_the_terms( $post_id, 'post_tag' );
1359 View Code Duplication
		if ( is_array( $tags ) ) {
1360
			foreach ( $tags as $tag ) {
1361
				if ( '' != trim( $tag->name ) ) {
1362
					$post_tag_context = sprintf(
1363
						_x( 'In "%s"', 'in {category/tag name}', 'jetpack' ),
1364
						$tag->name
1365
					);
1366
					/**
1367
					 * Filter the "In Tag" line displayed in the post context below each Related Post.
1368
					 *
1369
					 * @module related-posts
1370
					 *
1371
					 * @since 3.2.0
1372
					 *
1373
					 * @param string $post_tag_context "In Tag" line displayed in the post context below each Related Post.
1374
					 * @param array $tag Array containing information about the tag.
1375
					 */
1376
					return apply_filters( 'jetpack_relatedposts_post_tag_context', $post_tag_context, $tag );
1377
				}
1378
			}
1379
		}
1380
1381
		$comment_count = get_comments_number( $post_id );
1382
		if ( $comment_count > 0 ) {
1383
			return sprintf(
1384
				_n( 'With 1 comment', 'With %s comments', $comment_count, 'jetpack' ),
1385
				number_format_i18n( $comment_count )
1386
			);
1387
		}
1388
1389
		return __( 'Similar post', 'jetpack' );
1390
	}
1391
1392
	/**
1393
	 * Logs clicks for clickthrough analysis and related result tuning.
1394
	 *
1395
	 * @return null
1396
	 */
1397
	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...
1398
1399
	}
1400
1401
	/**
1402
	 * Determines if the current post is able to use related posts.
1403
	 *
1404
	 * @uses self::get_options, is_admin, is_single, apply_filters
1405
	 * @return bool
1406
	 */
1407
	protected function _enabled_for_request() {
1408
		$enabled = is_single()
1409
			&&
1410
				! is_admin()
1411
			&&
1412
				( !$this->_allow_feature_toggle() || $this->get_option( 'enabled' ) );
1413
1414
		/**
1415
		 * Filter the Enabled value to allow related posts to be shown on pages as well.
1416
		 *
1417
		 * @module related-posts
1418
		 *
1419
		 * @since 3.3.0
1420
		 *
1421
		 * @param bool $enabled Should Related Posts be enabled on the current page.
1422
		 */
1423
		return apply_filters( 'jetpack_relatedposts_filter_enabled_for_request', $enabled );
1424
	}
1425
1426
	/**
1427
	 * Adds filters and enqueues assets.
1428
	 *
1429
	 * @uses self::_enqueue_assets, self::_setup_shortcode, add_filter
1430
	 * @return null
1431
	 */
1432
	protected function _action_frontend_init_page() {
1433
		$this->_enqueue_assets( true, true );
1434
		$this->_setup_shortcode();
1435
1436
		add_filter( 'the_content', array( $this, 'filter_add_target_to_dom' ), 40 );
1437
	}
1438
1439
	/**
1440
	 * Enqueues assets needed to do async loading of related posts.
1441
	 *
1442
	 * @uses wp_enqueue_script, wp_enqueue_style, plugins_url
1443
	 * @return null
1444
	 */
1445
	protected function _enqueue_assets( $script, $style ) {
1446
		$dependencies = is_customize_preview() ? array( 'customize-base' ) : array( 'jquery' );
1447
		if ( $script ) {
1448
			wp_enqueue_script( 'jetpack_related-posts', plugins_url( 'related-posts.js', __FILE__ ), $dependencies, self::VERSION );
1449
			$related_posts_js_options = array(
1450
				/**
1451
				 * Filter each Related Post Heading structure.
1452
				 *
1453
				 * @since 4.0.0
1454
				 *
1455
				 * @param string $str Related Post Heading structure. Default to h4.
1456
				 */
1457
				'post_heading' => apply_filters( 'jetpack_relatedposts_filter_post_heading', esc_attr( 'h4' ) ),
1458
			);
1459
			wp_localize_script( 'jetpack_related-posts', 'related_posts_js_options', $related_posts_js_options );
1460
		}
1461
		if ( $style ){
1462
			if( is_rtl() ) {
1463
				wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'rtl/related-posts-rtl.css', __FILE__ ), array(), self::VERSION );
1464
			} else {
1465
				wp_enqueue_style( 'jetpack_related-posts', plugins_url( 'related-posts.css', __FILE__ ), array(), self::VERSION );
1466
			}
1467
		}
1468
	}
1469
1470
	/**
1471
	 * Sets up the shortcode processing.
1472
	 *
1473
	 * @uses add_filter, add_shortcode
1474
	 * @return null
1475
	 */
1476
	protected function _setup_shortcode() {
1477
		add_filter( 'the_content', array( $this, 'test_for_shortcode' ), 0 );
1478
1479
		add_shortcode( self::SHORTCODE, array( $this, 'get_target_html' ) );
1480
	}
1481
1482
	protected function _allow_feature_toggle() {
1483
		if ( null === $this->_allow_feature_toggle ) {
1484
			/**
1485
			 * Filter the display of the Related Posts toggle in Settings > Reading.
1486
			 *
1487
			 * @module related-posts
1488
			 *
1489
			 * @since 2.8.0
1490
			 *
1491
			 * @param bool false Display a feature toggle. Default to false.
1492
			 */
1493
			$this->_allow_feature_toggle = apply_filters( 'jetpack_relatedposts_filter_allow_feature_toggle', false );
1494
		}
1495
		return $this->_allow_feature_toggle;
1496
	}
1497
1498
	/**
1499
	 * ===================================================
1500
	 * FUNCTIONS EXPOSING RELATED POSTS IN THE WP REST API
1501
	 * ===================================================
1502
	 */
1503
1504
	/**
1505
	 * Add Related Posts to the REST API Post response.
1506
	 *
1507
	 * @since 4.4.0
1508
	 *
1509
	 * @action rest_api_init
1510
	 * @uses register_rest_field, self::rest_get_related_posts
1511
	 * @return null
1512
	 */
1513
	public function rest_register_related_posts() {
1514
		register_rest_field( 'post',
1515
			'jetpack-related-posts',
1516
			array(
1517
				'get_callback' => array( $this, 'rest_get_related_posts' ),
1518
				'update_callback' => null,
1519
				'schema'          => null,
1520
			)
1521
		);
1522
	}
1523
1524
	/**
1525
	 * Build an array of Related Posts.
1526
	 *
1527
	 * @since 4.4.0
1528
	 *
1529
	 * @param array $object Details of current post.
1530
	 * @param string $field_name Name of field.
1531
	 * @param WP_REST_Request $request Current request
1532
	 *
1533
	 * @uses self::get_for_post_id
1534
	 *
1535
	 * @return array
1536
	 */
1537
	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...
1538
		return $this->get_for_post_id( $object['id'], array() );
1539
	}
1540
}
1541
1542
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...
1543
	protected $_query_name;
1544
1545
	/**
1546
	 * Allows callers of this class to tag each query with a unique name for tracking purposes.
1547
	 *
1548
	 * @param string $name
1549
	 * @return Jetpack_RelatedPosts_Raw
1550
	 */
1551
	public function set_query_name( $name ) {
1552
		$this->_query_name = (string) $name;
1553
		return $this;
1554
	}
1555
1556
	/**
1557
	 * The raw related posts class can be used by other plugins or themes
1558
	 * to get related content. This class wraps the existing RelatedPosts
1559
	 * logic thus we never want to add anything to the DOM or do anything
1560
	 * for event hooks. We will also not present any settings for this
1561
	 * class and keep it enabled as calls to this class is done
1562
	 * programmatically.
1563
	 */
1564
	public function action_admin_init() {}
1565
	public function action_frontend_init() {}
1566
	public function get_options() {
1567
		return array(
1568
			'enabled' => true,
1569
		);
1570
	}
1571
1572
	/**
1573
	 * Workhorse method to return array of related posts ids matched by ElasticSearch.
1574
	 *
1575
	 * @param int $post_id
1576
	 * @param int $size
1577
	 * @param array $filters
1578
	 * @uses wp_remote_post, is_wp_error, wp_remote_retrieve_body
1579
	 * @return array
1580
	 */
1581
	protected function _get_related_posts( $post_id, $size, array $filters ) {
1582
		$hits = $this->_filter_non_public_posts(
1583
			$this->_get_related_post_ids(
1584
				$post_id,
1585
				$size,
1586
				$filters
1587
			)
1588
		);
1589
1590
		/** This filter is already documented in modules/related-posts/related-posts.php */
1591
		$hits = apply_filters( 'jetpack_relatedposts_filter_hits', $hits, $post_id );
1592
1593
		return $hits;
1594
	}
1595
}
1596