Completed
Push — update/podcast-player-allow-un... ( 73fb2c...683ff9 )
by
unknown
86:54 queued 79:16
created

The_Neverending_Home_Page::add_mejs_config()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
nc 3
nop 1
dl 0
loc 18
rs 9.6666
c 0
b 0
f 0
1
<?php
2
3
use Automattic\Jetpack\Assets;
4
5
/*
6
Plugin Name: The Neverending Home Page.
7
Plugin URI: https://automattic.com/
8
Description: Adds infinite scrolling support to the front-end blog post view for themes, pulling the next set of posts automatically into view when the reader approaches the bottom of the page.
9
Version: 1.1
10
Author: Automattic
11
Author URI: https://automattic.com/
12
License: GNU General Public License v2 or later
13
License URI: https://www.gnu.org/licenses/gpl-2.0.html
14
*/
15
16
/**
17
 * Class: The_Neverending_Home_Page relies on add_theme_support, expects specific
18
 * styling from each theme; including fixed footer.
19
 */
20
class The_Neverending_Home_Page {
21
22
	/**
23
	 * Register actions and filters, plus parse IS settings
24
	 *
25
	 * @uses add_action, add_filter, self::get_settings
26
	 * @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...
27
	 */
28
	function __construct() {
29
		add_action( 'pre_get_posts', array( $this, 'posts_per_page_query' ) );
30
		add_action( 'admin_init', array( $this, 'settings_api_init' ) );
31
		add_action( 'template_redirect', array( $this, 'action_template_redirect' ) );
32
		add_action( 'customize_preview_init', array( $this, 'init_customizer_assets' ) );
33
		add_action( 'template_redirect', array( $this, 'ajax_response' ) );
34
		add_action( 'custom_ajax_infinite_scroll', array( $this, 'query' ) );
35
		add_filter( 'infinite_scroll_query_args', array( $this, 'inject_query_args' ) );
36
		add_filter( 'infinite_scroll_allowed_vars', array( $this, 'allowed_query_vars' ) );
37
		add_action( 'the_post', array( $this, 'preserve_more_tag' ) );
38
		add_action( 'wp_footer', array( $this, 'footer' ) );
39
		add_filter( 'infinite_scroll_additional_scripts', array( $this, 'add_mejs_config' ) );
40
41
		// Plugin compatibility
42
		add_filter( 'grunion_contact_form_redirect_url', array( $this, 'filter_grunion_redirect_url' ) );
43
44
		// Parse IS settings from theme
45
		self::get_settings();
46
	}
47
48
	/**
49
	 * Initialize our static variables
50
	 */
51
	static $the_time = null;
52
	static $settings = null; // Don't access directly, instead use self::get_settings().
53
54
	static $option_name_enabled = 'infinite_scroll';
55
56
	/**
57
	 * Parse IS settings provided by theme
58
	 *
59
	 * @uses get_theme_support, infinite_scroll_has_footer_widgets, sanitize_title, add_action, get_option, wp_parse_args, is_active_sidebar
60
	 * @return object
61
	 */
62
	static function get_settings() {
63
		if ( is_null( self::$settings ) ) {
64
			$css_pattern = '#[^A-Z\d\-_]#i';
65
66
			$settings = $defaults = array(
67
				'type'            => 'scroll', // scroll | click
68
				'requested_type'  => 'scroll', // store the original type for use when logic overrides it
69
				'footer_widgets'  => false, // true | false | sidebar_id | array of sidebar_ids -- last two are checked with is_active_sidebar
70
				'container'       => 'content', // container html id
71
				'wrapper'         => true, // true | false | html class
72
				'render'          => false, // optional function, otherwise the `content` template part will be used
73
				'footer'          => true, // boolean to enable or disable the infinite footer | string to provide an html id to derive footer width from
74
				'footer_callback' => false, // function to be called to render the IS footer, in place of the default
75
				'posts_per_page'  => false, // int | false to set based on IS type
76
				'click_handle'    => true, // boolean to enable or disable rendering the click handler div. If type is click and this is false, page must include its own trigger with the HTML ID `infinite-handle`.
77
			);
78
79
			// Validate settings passed through add_theme_support()
80
			$_settings = get_theme_support( 'infinite-scroll' );
81
82
			if ( is_array( $_settings ) ) {
83
				// Preferred implementation, where theme provides an array of options
84
				if ( isset( $_settings[0] ) && is_array( $_settings[0] ) ) {
85
					foreach ( $_settings[0] as $key => $value ) {
86
						switch ( $key ) {
87
							case 'type' :
88
								if ( in_array( $value, array( 'scroll', 'click' ) ) )
89
									$settings[ $key ] = $settings['requested_type'] = $value;
90
91
								break;
92
93
							case 'footer_widgets' :
94
								if ( is_string( $value ) )
95
									$settings[ $key ] = sanitize_title( $value );
96
								elseif ( is_array( $value ) )
97
									$settings[ $key ] = array_map( 'sanitize_title', $value );
98
								elseif ( is_bool( $value ) )
99
									$settings[ $key ] = $value;
100
101
								break;
102
103
							case 'container' :
104 View Code Duplication
							case 'wrapper' :
105
								if ( 'wrapper' == $key && is_bool( $value ) ) {
106
									$settings[ $key ] = $value;
107
								} else {
108
									$value = preg_replace( $css_pattern, '', $value );
109
110
									if ( ! empty( $value ) )
111
										$settings[ $key ] = $value;
112
								}
113
114
								break;
115
116
							case 'render' :
117
								if ( false !== $value && is_callable( $value ) ) {
118
									$settings[ $key ] = $value;
119
								}
120
121
								break;
122
123 View Code Duplication
							case 'footer' :
124
								if ( is_bool( $value ) ) {
125
									$settings[ $key ] = $value;
126
								} elseif ( is_string( $value ) ) {
127
									$value = preg_replace( $css_pattern, '', $value );
128
129
									if ( ! empty( $value ) )
130
										$settings[ $key ] = $value;
131
								}
132
133
								break;
134
135
							case 'footer_callback' :
136
								if ( is_callable( $value ) )
137
									$settings[ $key ] = $value;
138
								else
139
									$settings[ $key ] = false;
140
141
								break;
142
143
							case 'posts_per_page' :
144
								if ( is_numeric( $value ) )
145
									$settings[ $key ] = (int) $value;
146
147
								break;
148
149
							case 'click_handle' :
150
								if ( is_bool( $value ) ) {
151
									$settings[ $key ] = $value;
152
								}
153
154
								break;
155
156
							default:
157
								break;
158
						}
159
					}
160
				} elseif ( is_string( $_settings[0] ) ) {
161
					// Checks below are for backwards compatibility
162
163
					// Container to append new posts to
164
					$settings['container'] = preg_replace( $css_pattern, '', $_settings[0] );
165
166
					// Wrap IS elements?
167
					if ( isset( $_settings[1] ) )
168
						$settings['wrapper'] = (bool) $_settings[1];
169
				}
170
			}
171
172
			// Always ensure all values are present in the final array
173
			$settings = wp_parse_args( $settings, $defaults );
174
175
			// Check if a legacy `infinite_scroll_has_footer_widgets()` function is defined and override the footer_widgets parameter's value.
176
			// Otherwise, if a widget area ID or array of IDs was provided in the footer_widgets parameter, check if any contains any widgets.
177
			// It is safe to use `is_active_sidebar()` before the sidebar is registered as this function doesn't check for a sidebar's existence when determining if it contains any widgets.
178
			if ( function_exists( 'infinite_scroll_has_footer_widgets' ) ) {
179
				$settings['footer_widgets'] = (bool) infinite_scroll_has_footer_widgets();
180
			} elseif ( is_array( $settings['footer_widgets'] ) ) {
181
				$sidebar_ids = $settings['footer_widgets'];
182
				$settings['footer_widgets'] = false;
183
184
				foreach ( $sidebar_ids as $sidebar_id ) {
185
					if ( is_active_sidebar( $sidebar_id ) ) {
186
						$settings['footer_widgets'] = true;
187
						break;
188
					}
189
				}
190
191
				unset( $sidebar_ids );
192
				unset( $sidebar_id );
193
			} elseif ( is_string( $settings['footer_widgets'] ) ) {
194
				$settings['footer_widgets'] = (bool) is_active_sidebar( $settings['footer_widgets'] );
195
			}
196
197
			/**
198
			 * Filter Infinite Scroll's `footer_widgets` parameter.
199
			 *
200
			 * @module infinite-scroll
201
			 *
202
			 * @since 2.0.0
203
			 *
204
			 * @param bool $settings['footer_widgets'] Does the current theme have Footer Widgets.
205
			 */
206
			$settings['footer_widgets'] = apply_filters( 'infinite_scroll_has_footer_widgets', $settings['footer_widgets'] );
207
208
			// Finally, after all of the sidebar checks and filtering, ensure that a boolean value is present, otherwise set to default of `false`.
209
			if ( ! is_bool( $settings['footer_widgets'] ) )
210
				$settings['footer_widgets'] = false;
211
212
			// Ensure that IS is enabled and no footer widgets exist if the IS type isn't already "click".
213
			if ( 'click' != $settings['type'] ) {
214
				// Check the setting status
215
				$disabled = '' === get_option( self::$option_name_enabled ) ? true : false;
216
217
				// Footer content or Reading option check
218
				if ( $settings['footer_widgets'] || $disabled )
219
					$settings['type'] = 'click';
220
			}
221
222
			// Force display of the click handler and attendant bits when the type isn't `click`
223
			if ( 'click' !== $settings['type'] ) {
224
				$settings['click_handle'] = true;
225
			}
226
227
			// Store final settings in a class static to avoid reparsing
228
			/**
229
			 * Filter the array of Infinite Scroll settings.
230
			 *
231
			 * @module infinite-scroll
232
			 *
233
			 * @since 2.0.0
234
			 *
235
			 * @param array $settings Array of Infinite Scroll settings.
236
			 */
237
			self::$settings = apply_filters( 'infinite_scroll_settings', $settings );
238
		}
239
240
		/** This filter is already documented in modules/infinite-scroll/infinity.php */
241
		return (object) apply_filters( 'infinite_scroll_settings', self::$settings );
242
	}
243
244
	/**
245
	 * Number of posts per page.
246
	 *
247
	 * @uses self::wp_query, self::get_settings, apply_filters
248
	 * @return int
249
	 */
250
	static function posts_per_page() {
251
		$posts_per_page = self::get_settings()->posts_per_page ? self::get_settings()->posts_per_page : self::wp_query()->get( 'posts_per_page' );
252
253
		// Take JS query into consideration here
254
		if ( true === isset( $_REQUEST['query_args']['posts_per_page'] ) ) {
255
			$posts_per_page = $_REQUEST['query_args']['posts_per_page'];
256
		}
257
258
		/**
259
		 * Filter the number of posts per page.
260
		 *
261
		 * @module infinite-scroll
262
		 *
263
		 * @since 6.0.0
264
		 *
265
		 * @param int $posts_per_page The number of posts to display per page.
266
		 */
267
		return (int) apply_filters( 'infinite_scroll_posts_per_page', $posts_per_page );
268
	}
269
270
	/**
271
	 * Retrieve the query used with Infinite Scroll
272
	 *
273
	 * @global $wp_the_query
274
	 * @uses apply_filters
275
	 * @return object
276
	 */
277
	static function wp_query() {
278
		global $wp_the_query;
279
		/**
280
		 * Filter the Infinite Scroll query object.
281
		 *
282
		 * @module infinite-scroll
283
		 *
284
		 * @since 2.2.1
285
		 *
286
		 * @param WP_Query $wp_the_query WP Query.
287
		 */
288
		return apply_filters( 'infinite_scroll_query_object', $wp_the_query );
289
	}
290
291
	/**
292
	 * Has infinite scroll been triggered?
293
	 */
294
	static function got_infinity() {
295
		/**
296
		 * Filter the parameter used to check if Infinite Scroll has been triggered.
297
		 *
298
		 * @module infinite-scroll
299
		 *
300
		 * @since 3.9.0
301
		 *
302
		 * @param bool isset( $_GET[ 'infinity' ] ) Return true if the "infinity" parameter is set.
303
		 */
304
		return apply_filters( 'infinite_scroll_got_infinity', isset( $_GET[ 'infinity' ] ) );
305
	}
306
307
	/**
308
	 * Is this guaranteed to be the last batch of posts?
309
	 */
310
	static function is_last_batch() {
311
		/**
312
		 * Override whether or not this is the last batch for a request
313
		 *
314
		 * @module infinite-scroll
315
		 *
316
		 * @since 4.8.0
317
		 *
318
		 * @param bool|null null                 Bool if value should be overridden, null to determine from query
319
		 * @param object    self::wp_query()     WP_Query object for current request
320
		 * @param object    self::get_settings() Infinite Scroll settings
321
		 */
322
		$override = apply_filters( 'infinite_scroll_is_last_batch', null, self::wp_query(), self::get_settings() );
323
		if ( is_bool( $override ) ) {
324
			return $override;
325
		}
326
327
		$entries = (int) self::wp_query()->found_posts;
328
		$posts_per_page = self::posts_per_page();
329
330
		// This is to cope with an issue in certain themes or setups where posts are returned but found_posts is 0.
331
		if ( 0 == $entries ) {
332
			return (bool) ( count( self::wp_query()->posts ) < $posts_per_page );
333
		}
334
		$paged = max( 1, self::wp_query()->get( 'paged' ) );
335
336
		// Are there enough posts for more than the first page?
337
		if ( $entries <= $posts_per_page ) {
338
			return true;
339
		}
340
341
		// Calculate entries left after a certain number of pages
342
		if ( $paged && $paged > 1 ) {
343
			$entries -= $posts_per_page * $paged;
344
		}
345
346
		// Are there some entries left to display?
347
		return $entries <= 0;
348
	}
349
350
	/**
351
	 * The more tag will be ignored by default if the blog page isn't our homepage.
352
	 * Let's force the $more global to false.
353
	 */
354
	function preserve_more_tag( $array ) {
355
		global $more;
356
357
		if ( self::got_infinity() )
358
			$more = 0; //0 = show content up to the more tag. Add more link.
359
360
		return $array;
361
	}
362
363
	/**
364
	 * Add a checkbox field to Settings > Reading
365
	 * for enabling infinite scroll.
366
	 *
367
	 * Only show if the current theme supports infinity.
368
	 *
369
	 * @uses current_theme_supports, add_settings_field, __, register_setting
370
	 * @action admin_init
371
	 * @return null
372
	 */
373
	function settings_api_init() {
374
		if ( ! current_theme_supports( 'infinite-scroll' ) )
375
			return;
376
377
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
378
			// This setting is no longer configurable in wp-admin on WordPress.com -- leave a pointer
379
			add_settings_field( self::$option_name_enabled,
380
				'<span id="infinite-scroll-options">' . esc_html__( 'Infinite Scroll Behavior', 'jetpack' ) . '</span>',
381
				array( $this, 'infinite_setting_html_calypso_placeholder' ),
382
				'reading'
383
			);
384
			return;
385
		}
386
387
		// Add the setting field [infinite_scroll] and place it in Settings > Reading
388
		add_settings_field( self::$option_name_enabled, '<span id="infinite-scroll-options">' . esc_html__( 'Infinite Scroll Behavior', 'jetpack' ) . '</span>', array( $this, 'infinite_setting_html' ), 'reading' );
389
		register_setting( 'reading', self::$option_name_enabled, 'esc_attr' );
390
	}
391
392
	function infinite_setting_html_calypso_placeholder() {
393
		$details = get_blog_details();
394
		echo '<span>' . sprintf(
395
			/* translators: Variables are the enclosing link to the settings page */
396
			esc_html__( 'This option has moved. You can now manage it %1$shere%2$s.' ),
397
			'<a href="' . esc_url( 'https://wordpress.com/settings/writing/' . $details->domain ) . '">',
398
			'</a>'
399
		) . '</span>';
400
	}
401
402
	/**
403
	 * HTML code to display a checkbox true/false option
404
	 * for the infinite_scroll setting.
405
	 */
406
	function infinite_setting_html() {
407
		$notice = '<em>' . __( 'We&rsquo;ve changed this option to a click-to-scroll version for you since you have footer widgets in Appearance &rarr; Widgets, or your theme uses click-to-scroll as the default behavior.', 'jetpack' ) . '</em>';
408
409
		// If the blog has footer widgets, show a notice instead of the checkbox
410
		if ( self::get_settings()->footer_widgets || 'click' == self::get_settings()->requested_type ) {
411
			echo '<label>' . $notice . '</label>';
412
		} else {
413
			echo '<label><input name="infinite_scroll" type="checkbox" value="1" ' . checked( 1, '' !== get_option( self::$option_name_enabled ), false ) . ' /> ' . esc_html__( 'Check to load posts as you scroll. Uncheck to show clickable button to load posts', 'jetpack' ) . '</label>';
414
			echo '<p class="description">' . esc_html( sprintf( _n( 'Shows %s post on each load.', 'Shows %s posts on each load.', self::posts_per_page(), 'jetpack' ), number_format_i18n( self::posts_per_page() ) ) ) . '</p>';
415
		}
416
	}
417
418
	/**
419
	 * Does the legwork to determine whether the feature is enabled.
420
	 *
421
	 * @uses current_theme_supports, self::archive_supports_infinity, self::get_settings, add_filter, wp_enqueue_script, plugins_url, wp_enqueue_style, add_action
422
	 * @action template_redirect
423
	 * @return null
424
	 */
425
	function action_template_redirect() {
426
		// Check that we support infinite scroll, and are on the home page.
427
		if ( ! current_theme_supports( 'infinite-scroll' ) || ! self::archive_supports_infinity() )
428
			return;
429
430
		$id = self::get_settings()->container;
431
432
		// Check that we have an id.
433
		if ( empty( $id ) )
434
			return;
435
436
		// Add our scripts.
437
		wp_register_script(
438
			'the-neverending-homepage',
439
			Assets::get_file_url_for_environment(
440
				'_inc/build/infinite-scroll/infinity.min.js',
441
				'modules/infinite-scroll/infinity.js'
442
			),
443
			array(),
444
			JETPACK__VERSION . '-is5.0.0', // Added for ability to cachebust on WP.com.
445
			true
446
		);
447
448
		// Add our default styles.
449
		wp_register_style( 'the-neverending-homepage', plugins_url( 'infinity.css', __FILE__ ), array(), '20140422' );
450
451
		// Make sure there are enough posts for IS
452
		if ( self::is_last_batch() ) {
453
			return;
454
		}
455
456
		// Add our scripts.
457
		wp_enqueue_script( 'the-neverending-homepage' );
458
459
		// Add our default styles.
460
		wp_enqueue_style( 'the-neverending-homepage' );
461
462
		add_action( 'wp_footer', array( $this, 'action_wp_footer_settings' ), 2 );
463
464
		add_action( 'wp_footer', array( $this, 'action_wp_footer' ), 21 ); // Core prints footer scripts at priority 20, so we just need to be one later than that
465
466
		add_filter( 'infinite_scroll_results', array( $this, 'filter_infinite_scroll_results' ), 10, 3 );
467
	}
468
469
	/**
470
	 * Initialize the Customizer logic separately from the main JS.
471
	 *
472
	 * @since 8.4.0
473
	 */
474
	public function init_customizer_assets() {
475
		// Add our scripts.
476
		wp_register_script(
477
			'the-neverending-homepage-customizer',
478
			Assets::get_file_url_for_environment(
479
				'_inc/build/infinite-scroll/infinity-customizer.min.js',
480
				'modules/infinite-scroll/infinity-customizer.js'
481
			),
482
			array( 'customize-base' ),
483
			JETPACK__VERSION . '-is5.0.0', // Added for ability to cachebust on WP.com.
484
			true
485
		);
486
487
		wp_enqueue_script( 'the-neverending-homepage-customizer' );
488
	}
489
490
	/**
491
	 * Returns classes to be added to <body>. If it's enabled, 'infinite-scroll'. If set to continuous scroll, adds 'neverending' too.
492
	 *
493
	 * @since 4.7.0 No longer added as a 'body_class' filter but passed to JS environment and added using JS.
494
	 *
495
	 * @return string
496
	 */
497
	function body_class() {
498
		$classes = '';
499
		// Do not add infinity-scroll class if disabled through the Reading page
500
		$disabled = '' === get_option( self::$option_name_enabled ) ? true : false;
501
		if ( ! $disabled || 'click' == self::get_settings()->type ) {
502
			$classes = 'infinite-scroll';
503
504
			if ( 'scroll' == self::get_settings()->type )
505
				$classes .= ' neverending';
506
		}
507
508
		return $classes;
509
	}
510
511
	/**
512
	 * In case IS is activated on search page, we have to exclude initially loaded posts which match the keyword by title, not the content as they are displayed before content-matching ones
513
	 *
514
	 * @uses self::wp_query
515
	 * @uses self::get_last_post_date
516
	 * @uses self::has_only_title_matching_posts
517
	 * @return array
518
	 */
519
	function get_excluded_posts() {
520
521
		$excluded_posts = array();
522
		//loop through posts returned by wp_query call
523
		foreach( self::wp_query()->get_posts() as $post ) {
524
525
			$orderby = isset( self::wp_query()->query_vars['orderby'] ) ? self::wp_query()->query_vars['orderby'] : '';
526
			$post_date = ( ! empty( $post->post_date ) ? $post->post_date : false );
527
			if ( 'modified' === $orderby || false === $post_date ) {
528
				$post_date = $post->post_modified;
529
			}
530
531
			//in case all posts initially displayed match the keyword by title we add em all to excluded posts array
532
			//else, we add only posts which are older than last_post_date param as newer are natually excluded by last_post_date condition in the SQL query
533
			if ( self::has_only_title_matching_posts() || $post_date <= self::get_last_post_date() ) {
534
				array_push( $excluded_posts, $post->ID );
535
			}
536
		}
537
		return $excluded_posts;
538
	}
539
540
	/**
541
	 * In case IS is active on search, we have to exclude posts matched by title rather than by post_content in order to prevent dupes on next pages
542
	 *
543
	 * @uses self::wp_query
544
	 * @uses self::get_excluded_posts
545
	 * @return array
546
	 */
547
	function get_query_vars() {
548
549
		$query_vars = self::wp_query()->query_vars;
550
		//applies to search page only
551
		if ( true === self::wp_query()->is_search() ) {
552
			//set post__not_in array in query_vars in case it does not exists
553
			if ( false === isset( $query_vars['post__not_in'] ) ) {
554
				$query_vars['post__not_in'] = array();
555
			}
556
			//get excluded posts
557
			$excluded = self::get_excluded_posts();
558
			//merge them with other post__not_in posts (eg.: sticky posts)
559
			$query_vars['post__not_in'] = array_merge( $query_vars['post__not_in'], $excluded );
560
		}
561
		return $query_vars;
562
	}
563
564
	/**
565
	 * This function checks whether all posts returned by initial wp_query match the keyword by title
566
	 * The code used in this function is borrowed from WP_Query class where it is used to construct like conditions for keywords
567
	 *
568
	 * @uses self::wp_query
569
	 * @return bool
570
	 */
571
	function has_only_title_matching_posts() {
572
573
		//apply following logic for search page results only
574
		if ( false === self::wp_query()->is_search() ) {
575
			return false;
576
		}
577
578
		//grab the last posts in the stack as if the last one is title-matching the rest is title-matching as well
579
		$post = end( self::wp_query()->posts );
580
581
		//code inspired by WP_Query class
582
		if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', self::wp_query()->get( 's' ), $matches ) ) {
583
			$search_terms = self::wp_query()->query_vars['search_terms'];
584
			// if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence
585
			if ( empty( $search_terms ) || count( $search_terms ) > 9 ) {
586
				$search_terms = array( self::wp_query()->get( 's' ) );
587
			}
588
		} else {
589
			$search_terms = array( self::wp_query()->get( 's' ) );
590
		}
591
592
		//actual testing. As search query combines multiple keywords with AND, it's enough to check if any of the keywords is present in the title
593
		$term = current( $search_terms );
594
		if ( ! empty( $term ) && false !== strpos( $post->post_title, $term ) ) {
595
			return true;
596
		}
597
598
		return false;
599
	}
600
601
	/**
602
	 * Grab the timestamp for the initial query's last post.
603
	 *
604
	 * This takes into account the query's 'orderby' parameter and returns
605
	 * false if the posts are not ordered by date.
606
	 *
607
	 * @uses self::got_infinity
608
	 * @uses self::has_only_title_matching_posts
609
	 * @uses self::wp_query
610
	 * @return string 'Y-m-d H:i:s' or false
611
	 */
612
	function get_last_post_date() {
613
		if ( self::got_infinity() )
614
			return;
615
616
		if ( ! self::wp_query()->have_posts() ) {
617
			return null;
618
		}
619
620
		//In case there are only title-matching posts in the initial WP_Query result, we don't want to use the last_post_date param yet
621
		if ( true === self::has_only_title_matching_posts() ) {
622
			return false;
623
		}
624
625
		$post = end( self::wp_query()->posts );
626
		$orderby = isset( self::wp_query()->query_vars['orderby'] ) ?
627
			self::wp_query()->query_vars['orderby'] : '';
628
		$post_date = ( ! empty( $post->post_date ) ? $post->post_date : false );
629 View Code Duplication
		switch ( $orderby ) {
630
			case 'modified':
631
				return $post->post_modified;
632
			case 'date':
633
			case '':
634
				return $post_date;
635
			default:
636
				return false;
637
		}
638
	}
639
640
	/**
641
	 * Returns the appropriate `wp_posts` table field for a given query's
642
	 * 'orderby' parameter, if applicable.
643
	 *
644
	 * @param optional object $query
645
	 * @uses self::wp_query
646
	 * @return string or false
647
	 */
648
	function get_query_sort_field( $query = null ) {
649
		if ( empty( $query ) )
650
			$query = self::wp_query();
651
652
		$orderby = isset( $query->query_vars['orderby'] ) ? $query->query_vars['orderby'] : '';
653
654 View Code Duplication
		switch ( $orderby ) {
655
			case 'modified':
656
				return 'post_modified';
657
			case 'date':
658
			case '':
659
				return 'post_date';
660
			default:
661
				return false;
662
		}
663
	}
664
665
	/**
666
	 * Create a where clause that will make sure post queries return posts
667
	 * in the correct order, without duplicates, if a new post is added
668
	 * and we're sorting by post date.
669
	 *
670
	 * @global $wpdb
671
	 * @param string $where
672
	 * @param object $query
673
	 * @uses apply_filters
674
	 * @filter posts_where
675
	 * @return string
676
	 */
677
	function query_time_filter( $where, $query ) {
678
		if ( self::got_infinity() ) {
679
			global $wpdb;
680
681
			$sort_field = self::get_query_sort_field( $query );
682
683
			if ( 'post_date' !== $sort_field || 'DESC' !== $_REQUEST['query_args']['order'] ) {
684
				return $where;
685
			}
686
687
			$query_before = sanitize_text_field( wp_unslash( $_REQUEST['query_before'] ) );
688
689
			if ( empty( $query_before ) ) {
690
				return $where;
691
			}
692
693
			// Construct the date query using our timestamp
694
			$clause = $wpdb->prepare( " AND {$wpdb->posts}.post_date <= %s", $query_before );
695
696
			/**
697
			 * Filter Infinite Scroll's SQL date query making sure post queries
698
			 * will always return results prior to (descending sort)
699
			 * or before (ascending sort) the last post date.
700
			 *
701
			 * @module infinite-scroll
702
			 *
703
			 * @param string $clause SQL Date query.
704
			 * @param object $query Query.
705
			 * @param string $operator @deprecated Query operator.
706
			 * @param string $last_post_date @deprecated Last Post Date timestamp.
707
			 */
708
			$operator       = 'ASC' === $_REQUEST['query_args']['order'] ? '>' : '<';
709
			$last_post_date = sanitize_text_field( wp_unslash( $_REQUEST['last_post_date'] ) );
710
			$where         .= apply_filters( 'infinite_scroll_posts_where', $clause, $query, $operator, $last_post_date );
711
		}
712
713
		return $where;
714
	}
715
716
	/**
717
	 * Let's overwrite the default post_per_page setting to always display a fixed amount.
718
	 *
719
	 * @param object $query
720
	 * @uses is_admin, self::archive_supports_infinity, self::get_settings
721
	 * @return null
722
	 */
723
	function posts_per_page_query( $query ) {
724
		if ( ! is_admin() && self::archive_supports_infinity() && $query->is_main_query() )
725
			$query->set( 'posts_per_page', self::posts_per_page() );
726
	}
727
728
	/**
729
	 * Check if the IS output should be wrapped in a div.
730
	 * Setting value can be a boolean or a string specifying the class applied to the div.
731
	 *
732
	 * @uses self::get_settings
733
	 * @return bool
734
	 */
735
	function has_wrapper() {
736
		return (bool) self::get_settings()->wrapper;
737
	}
738
739
	/**
740
	 * Returns the Ajax url
741
	 *
742
	 * @global $wp
743
	 * @uses home_url, add_query_arg, apply_filters
744
	 * @return string
745
	 */
746
	function ajax_url() {
747
		$base_url = set_url_scheme( home_url( '/' ) );
748
749
		$ajaxurl = add_query_arg( array( 'infinity' => 'scrolling' ), $base_url );
750
751
		/**
752
		 * Filter the Infinite Scroll Ajax URL.
753
		 *
754
		 * @module infinite-scroll
755
		 *
756
		 * @since 2.0.0
757
		 *
758
		 * @param string $ajaxurl Infinite Scroll Ajax URL.
759
		 */
760
		return apply_filters( 'infinite_scroll_ajax_url', $ajaxurl );
761
	}
762
763
	/**
764
	 * Our own Ajax response, avoiding calling admin-ajax
765
	 */
766
	function ajax_response() {
767
		// Only proceed if the url query has a key of "Infinity"
768
		if ( ! self::got_infinity() )
769
			return false;
770
771
		// This should already be defined below, but make sure.
772
		if ( ! defined( 'DOING_AJAX' ) ) {
773
			define( 'DOING_AJAX', true );
774
		}
775
776
		@header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) );
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition here. This can introduce security issues, and is generally not recommended.

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
777
		send_nosniff_header();
778
779
		/**
780
		 * Fires at the end of the Infinite Scroll Ajax response.
781
		 *
782
		 * @module infinite-scroll
783
		 *
784
		 * @since 2.0.0
785
		 */
786
		do_action( 'custom_ajax_infinite_scroll' );
787
		die( '0' );
788
	}
789
790
	/**
791
	 * Alias for renamed class method.
792
	 *
793
	 * Previously, JS settings object was unnecessarily output in the document head.
794
	 * When the hook was changed, the method name no longer made sense.
795
	 */
796
	function action_wp_head() {
797
		$this->action_wp_footer_settings();
798
	}
799
800
	/**
801
	 * Prints the relevant infinite scroll settings in JS.
802
	 *
803
	 * @global $wp_rewrite
804
	 * @uses self::get_settings, esc_js, esc_url_raw, self::has_wrapper, __, apply_filters, do_action, self::get_query_vars
805
	 * @action wp_footer
806
	 * @return string
807
	 */
808
	function action_wp_footer_settings() {
809
		global $wp_rewrite;
810
		global $currentday;
811
812
		// Default click handle text
813
		$click_handle_text = __( 'Older posts', 'jetpack' );
814
815
		// If a single CPT is displayed, use its plural name instead of "posts"
816
		// Could be empty (posts) or an array of multiple post types.
817
		// In the latter two cases cases, the default text is used, leaving the `infinite_scroll_js_settings` filter for further customization.
818
		$post_type = self::wp_query()->get( 'post_type' );
819
820
		// If it's a taxonomy, try to change the button text.
821
		if ( is_tax() ) {
822
			// Get current taxonomy slug.
823
			$taxonomy_slug = self::wp_query()->get( 'taxonomy' );
824
825
			// Get taxonomy settings.
826
			$taxonomy = get_taxonomy( $taxonomy_slug );
827
828
			// Check if the taxonomy is attached to one post type only and use its plural name.
829
			// If not, use "Posts" without confusing the users.
830
			if ( count( $taxonomy->object_type ) < 2 ) {
831
				$post_type = $taxonomy->object_type[0];
832
			}
833
		}
834
835
		if ( is_string( $post_type ) && ! empty( $post_type ) ) {
836
			$post_type = get_post_type_object( $post_type );
837
838
			if ( is_object( $post_type ) && ! is_wp_error( $post_type ) ) {
839
				if ( isset( $post_type->labels->name ) ) {
840
					$cpt_text = $post_type->labels->name;
841
				} elseif ( isset( $post_type->label ) ) {
842
					$cpt_text = $post_type->label;
843
				}
844
845
				if ( isset( $cpt_text ) ) {
846
					/* translators: %s is the name of a custom post type */
847
					$click_handle_text = sprintf( __( 'More %s', 'jetpack' ), $cpt_text );
848
					unset( $cpt_text );
849
				}
850
			}
851
		}
852
853
		unset( $post_type );
854
855
		// Base JS settings
856
		$js_settings = array(
857
			'id'               => self::get_settings()->container,
858
			'ajaxurl'          => esc_url_raw( self::ajax_url() ),
859
			'type'             => esc_js( self::get_settings()->type ),
860
			'wrapper'          => self::has_wrapper(),
861
			'wrapper_class'    => is_string( self::get_settings()->wrapper ) ? esc_js( self::get_settings()->wrapper ) : 'infinite-wrap',
862
			'footer'           => is_string( self::get_settings()->footer ) ? esc_js( self::get_settings()->footer ) : self::get_settings()->footer,
863
			'click_handle'     => esc_js( self::get_settings()->click_handle ),
864
			'text'             => esc_js( $click_handle_text ),
865
			'totop'            => esc_js( __( 'Scroll back to top', 'jetpack' ) ),
866
			'currentday'       => $currentday,
867
			'order'            => 'DESC',
868
			'scripts'          => array(),
869
			'styles'           => array(),
870
			'google_analytics' => false,
871
			'offset'           => max( 1, self::wp_query()->get( 'paged' ) ), // Pass through the current page so we can use that to offset the first load.
872
			'history'          => array(
873
				'host'                 => preg_replace( '#^http(s)?://#i', '', untrailingslashit( esc_url( get_home_url() ) ) ),
874
				'path'                 => self::get_request_path(),
875
				'use_trailing_slashes' => $wp_rewrite->use_trailing_slashes,
876
				'parameters'           => self::get_request_parameters(),
877
			),
878
			'query_args'      => self::get_query_vars(),
879
			'query_before'    => current_time( 'mysql' ),
880
			'last_post_date'  => self::get_last_post_date(),
881
			'body_class'	  => self::body_class(),
882
		);
883
884
		// Optional order param
885
		if ( isset( $_REQUEST['order'] ) ) {
886
			$order = strtoupper( $_REQUEST['order'] );
887
888
			if ( in_array( $order, array( 'ASC', 'DESC' ) ) )
889
				$js_settings['order'] = $order;
890
		}
891
892
		/**
893
		 * Filter the Infinite Scroll JS settings outputted in the head.
894
		 *
895
		 * @module infinite-scroll
896
		 *
897
		 * @since 2.0.0
898
		 *
899
		 * @param array $js_settings Infinite Scroll JS settings.
900
		 */
901
		$js_settings = apply_filters( 'infinite_scroll_js_settings', $js_settings );
902
903
		/**
904
		 * Fires before Infinite Scroll outputs inline JavaScript in the head.
905
		 *
906
		 * @module infinite-scroll
907
		 *
908
		 * @since 2.0.0
909
		 */
910
		do_action( 'infinite_scroll_wp_head' );
911
912
		?>
913
		<script type="text/javascript">
914
		//<![CDATA[
915
		var infiniteScroll = JSON.parse( decodeURIComponent( '<?php echo
916
			rawurlencode( json_encode( array( 'settings' => $js_settings ) ) );
917
		?>' ) );
918
		//]]>
919
		</script>
920
		<?php
921
	}
922
923
	/**
924
	 * Build path data for current request.
925
	 * Used for Google Analytics and pushState history tracking.
926
	 *
927
	 * @global $wp_rewrite
928
	 * @global $wp
929
	 * @uses user_trailingslashit, sanitize_text_field, add_query_arg
930
	 * @return string|bool
931
	 */
932
	private function get_request_path() {
933
		global $wp_rewrite;
934
935
		if ( $wp_rewrite->using_permalinks() ) {
936
			global $wp;
937
938
			// If called too early, bail
939
			if ( ! isset( $wp->request ) )
940
				return false;
941
942
			// Determine path for paginated version of current request
943
			if ( false != preg_match( '#' . $wp_rewrite->pagination_base . '/\d+/?$#i', $wp->request ) )
0 ignored issues
show
Bug Best Practice introduced by
It seems like you are loosely comparing preg_match('#' . $wp_rew...d+/?$#i', $wp->request) of type integer to the boolean false. If you are specifically checking for non-zero, consider using something more explicit like > 0 or !== 0 instead.
Loading history...
944
				$path = preg_replace( '#' . $wp_rewrite->pagination_base . '/\d+$#i', $wp_rewrite->pagination_base . '/%d', $wp->request );
945
			else
946
				$path = $wp->request . '/' . $wp_rewrite->pagination_base . '/%d';
947
948
			// Slashes everywhere we need them
949
			if ( 0 !== strpos( $path, '/' ) )
950
				$path = '/' . $path;
951
952
			$path = user_trailingslashit( $path );
953
		} else {
954
			// Clean up raw $_REQUEST input
955
			$path = array_map( 'sanitize_text_field', $_REQUEST );
956
			$path = array_filter( $path );
957
958
			$path['paged'] = '%d';
959
960
			$path = add_query_arg( $path, '/' );
961
		}
962
963
		return empty( $path ) ? false : $path;
964
	}
965
966
	/**
967
	 * Return query string for current request, prefixed with '?'.
968
	 *
969
	 * @return string
970
	 */
971
	private function get_request_parameters() {
972
		$uri = $_SERVER[ 'REQUEST_URI' ];
973
		$uri = preg_replace( '/^[^?]*(\?.*$)/', '$1', $uri, 1, $count );
974
		if ( $count != 1 )
975
			return '';
976
		return $uri;
977
	}
978
979
	/**
980
	 * Provide IS with a list of the scripts and stylesheets already present on the page.
981
	 * Since posts may contain require additional assets that haven't been loaded, this data will be used to track the additional assets.
982
	 *
983
	 * @global $wp_scripts, $wp_styles
984
	 * @action wp_footer
985
	 * @return string
986
	 */
987
	function action_wp_footer() {
988
		global $wp_scripts, $wp_styles;
989
990
		$scripts = is_a( $wp_scripts, 'WP_Scripts' ) ? $wp_scripts->done : array();
991
		/**
992
		 * Filter the list of scripts already present on the page.
993
		 *
994
		 * @module infinite-scroll
995
		 *
996
		 * @since 2.1.2
997
		 *
998
		 * @param array $scripts Array of scripts present on the page.
999
		 */
1000
		$scripts = apply_filters( 'infinite_scroll_existing_scripts', $scripts );
1001
1002
		$styles = is_a( $wp_styles, 'WP_Styles' ) ? $wp_styles->done : array();
1003
		/**
1004
		 * Filter the list of styles already present on the page.
1005
		 *
1006
		 * @module infinite-scroll
1007
		 *
1008
		 * @since 2.1.2
1009
		 *
1010
		 * @param array $styles Array of styles present on the page.
1011
		 */
1012
		$styles = apply_filters( 'infinite_scroll_existing_stylesheets', $styles );
1013
1014
		?><script type="text/javascript">
1015
			(function() {
1016
				var extend = function(out) {
1017
					out = out || {};
1018
1019
					for (var i = 1; i < arguments.length; i++) {
1020
						if (!arguments[i])
1021
						continue;
1022
1023
						for (var key in arguments[i]) {
1024
						if (arguments[i].hasOwnProperty(key))
1025
							out[key] = arguments[i][key];
1026
						}
1027
					}
1028
1029
					return out;
1030
				};
1031
				extend( window.infiniteScroll.settings.scripts, <?php echo wp_json_encode( $scripts ); ?> );
1032
				extend( window.infiniteScroll.settings.styles, <?php echo wp_json_encode( $styles ); ?> );
1033
			})();
1034
		</script><?php
1035
	}
1036
1037
	/**
1038
	 * Identify additional scripts required by the latest set of IS posts and provide the necessary data to the IS response handler.
1039
	 *
1040
	 * @global $wp_scripts
1041
	 * @uses sanitize_text_field, add_query_arg
1042
	 * @filter infinite_scroll_results
1043
	 * @return array
1044
	 */
1045
	function filter_infinite_scroll_results( $results, $query_args, $wp_query ) {
1046
		// Don't bother unless there are posts to display
1047
		if ( 'success' != $results['type'] )
1048
			return $results;
1049
1050
		// Parse and sanitize the script handles already output
1051
		$initial_scripts = isset( $_REQUEST['scripts'] ) && is_array( $_REQUEST['scripts'] ) ? array_map( 'sanitize_text_field', $_REQUEST['scripts'] ) : false;
1052
1053
		if ( is_array( $initial_scripts ) ) {
1054
			global $wp_scripts;
1055
1056
			// Identify new scripts needed by the latest set of IS posts
1057
			$new_scripts = array_diff( $wp_scripts->done, $initial_scripts );
1058
1059
			// If new scripts are needed, extract relevant data from $wp_scripts
1060
			if ( ! empty( $new_scripts ) ) {
1061
				$results['scripts'] = array();
1062
1063
				foreach ( $new_scripts as $handle ) {
1064
					// Abort if somehow the handle doesn't correspond to a registered script
1065
					// or if the script doesn't have `src` set.
1066
					$script_not_registered = ! isset( $wp_scripts->registered[ $handle ] );
1067
					$empty_src             = empty( $wp_scripts->registered[ $handle ]->src );
1068
					if ( $script_not_registered || $empty_src ) {
1069
						continue;
1070
					}
1071
1072
					// Provide basic script data
1073
					$script_data = array(
1074
						'handle'     => $handle,
1075
						'footer'     => ( is_array( $wp_scripts->in_footer ) && in_array( $handle, $wp_scripts->in_footer ) ),
1076
						'extra_data' => $wp_scripts->print_extra_script( $handle, false )
1077
					);
1078
1079
					// Base source
1080
					$src = $wp_scripts->registered[ $handle ]->src;
1081
1082
					// Take base_url into account
1083
					if ( strpos( $src, 'http' ) !== 0 )
1084
						$src = $wp_scripts->base_url . $src;
1085
1086
					// Version and additional arguments
1087 View Code Duplication
					if ( null === $wp_scripts->registered[ $handle ]->ver )
1088
						$ver = '';
1089
					else
1090
						$ver = $wp_scripts->registered[ $handle ]->ver ? $wp_scripts->registered[ $handle ]->ver : $wp_scripts->default_version;
1091
1092 View Code Duplication
					if ( isset( $wp_scripts->args[ $handle ] ) )
1093
						$ver = $ver ? $ver . '&amp;' . $wp_scripts->args[$handle] : $wp_scripts->args[$handle];
1094
1095
					// Full script source with version info
1096
					$script_data['src'] = add_query_arg( 'ver', $ver, $src );
1097
1098
					// Add script to data that will be returned to IS JS
1099
					array_push( $results['scripts'], $script_data );
1100
				}
1101
			}
1102
		}
1103
1104
		// Expose additional script data to filters, but only include in final `$results` array if needed.
1105
		if ( ! isset( $results['scripts'] ) )
1106
			$results['scripts'] = array();
1107
1108
		/**
1109
		 * Filter the additional scripts required by the latest set of IS posts.
1110
		 *
1111
		 * @module infinite-scroll
1112
		 *
1113
		 * @since 2.1.2
1114
		 *
1115
		 * @param array $results['scripts'] Additional scripts required by the latest set of IS posts.
1116
		 * @param array|bool $initial_scripts Set of scripts loaded on each page.
1117
		 * @param array $results Array of Infinite Scroll results.
1118
		 * @param array $query_args Array of Query arguments.
1119
		 * @param WP_Query $wp_query WP Query.
1120
		 */
1121
		$results['scripts'] = apply_filters(
1122
			'infinite_scroll_additional_scripts',
1123
			$results['scripts'],
1124
			$initial_scripts,
1125
			$results,
1126
			$query_args,
1127
			$wp_query
1128
		);
1129
1130
		if ( empty( $results['scripts'] ) )
1131
			unset( $results['scripts' ] );
1132
1133
		// Parse and sanitize the style handles already output
1134
		$initial_styles = isset( $_REQUEST['styles'] ) && is_array( $_REQUEST['styles'] ) ? array_map( 'sanitize_text_field', $_REQUEST['styles'] ) : false;
1135
1136
		if ( is_array( $initial_styles ) ) {
1137
			global $wp_styles;
1138
1139
			// Identify new styles needed by the latest set of IS posts
1140
			$new_styles = array_diff( $wp_styles->done, $initial_styles );
1141
1142
			// If new styles are needed, extract relevant data from $wp_styles
1143
			if ( ! empty( $new_styles ) ) {
1144
				$results['styles'] = array();
1145
1146
				foreach ( $new_styles as $handle ) {
1147
					// Abort if somehow the handle doesn't correspond to a registered stylesheet
1148
					if ( ! isset( $wp_styles->registered[ $handle ] ) )
1149
						continue;
1150
1151
					// Provide basic style data
1152
					$style_data = array(
1153
						'handle' => $handle,
1154
						'media'  => 'all'
1155
					);
1156
1157
					// Base source
1158
					$src = $wp_styles->registered[ $handle ]->src;
1159
1160
					// Take base_url into account
1161
					if ( strpos( $src, 'http' ) !== 0 )
1162
						$src = $wp_styles->base_url . $src;
1163
1164
					// Version and additional arguments
1165 View Code Duplication
					if ( null === $wp_styles->registered[ $handle ]->ver )
1166
						$ver = '';
1167
					else
1168
						$ver = $wp_styles->registered[ $handle ]->ver ? $wp_styles->registered[ $handle ]->ver : $wp_styles->default_version;
1169
1170 View Code Duplication
					if ( isset($wp_styles->args[ $handle ] ) )
1171
						$ver = $ver ? $ver . '&amp;' . $wp_styles->args[$handle] : $wp_styles->args[$handle];
1172
1173
					// Full stylesheet source with version info
1174
					$style_data['src'] = add_query_arg( 'ver', $ver, $src );
1175
1176
					// Parse stylesheet's conditional comments if present, converting to logic executable in JS
1177
					if ( isset( $wp_styles->registered[ $handle ]->extra['conditional'] ) && $wp_styles->registered[ $handle ]->extra['conditional'] ) {
1178
						// First, convert conditional comment operators to standard logical operators. %ver is replaced in JS with the IE version
1179
						$style_data['conditional'] = str_replace( array(
1180
							'lte',
1181
							'lt',
1182
							'gte',
1183
							'gt'
1184
						), array(
1185
							'%ver <=',
1186
							'%ver <',
1187
							'%ver >=',
1188
							'%ver >',
1189
						), $wp_styles->registered[ $handle ]->extra['conditional'] );
1190
1191
						// Next, replace any !IE checks. These shouldn't be present since WP's conditional stylesheet implementation doesn't support them, but someone could be _doing_it_wrong().
1192
						$style_data['conditional'] = preg_replace( '#!\s*IE(\s*\d+){0}#i', '1==2', $style_data['conditional'] );
1193
1194
						// Lastly, remove the IE strings
1195
						$style_data['conditional'] = str_replace( 'IE', '', $style_data['conditional'] );
1196
					}
1197
1198
					// Parse requested media context for stylesheet
1199 View Code Duplication
					if ( isset( $wp_styles->registered[ $handle ]->args ) )
1200
						$style_data['media'] = esc_attr( $wp_styles->registered[ $handle ]->args );
1201
1202
					// Add stylesheet to data that will be returned to IS JS
1203
					array_push( $results['styles'], $style_data );
1204
				}
1205
			}
1206
		}
1207
1208
		// Expose additional stylesheet data to filters, but only include in final `$results` array if needed.
1209
		if ( ! isset( $results['styles'] ) )
1210
			$results['styles'] = array();
1211
1212
		/**
1213
		 * Filter the additional styles required by the latest set of IS posts.
1214
		 *
1215
		 * @module infinite-scroll
1216
		 *
1217
		 * @since 2.1.2
1218
		 *
1219
		 * @param array $results['styles'] Additional styles required by the latest set of IS posts.
1220
		 * @param array|bool $initial_styles Set of styles loaded on each page.
1221
		 * @param array $results Array of Infinite Scroll results.
1222
		 * @param array $query_args Array of Query arguments.
1223
		 * @param WP_Query $wp_query WP Query.
1224
		 */
1225
		$results['styles'] = apply_filters(
1226
			'infinite_scroll_additional_stylesheets',
1227
			$results['styles'],
1228
			$initial_styles,
1229
			$results,
1230
			$query_args,
1231
			$wp_query
1232
		);
1233
1234
		if ( empty( $results['styles'] ) )
1235
			unset( $results['styles' ] );
1236
1237
		// Lastly, return the IS results array
1238
		return $results;
1239
	}
1240
1241
	/**
1242
	 * Runs the query and returns the results via JSON.
1243
	 * Triggered by an AJAX request.
1244
	 *
1245
	 * @global $wp_query
1246
	 * @global $wp_the_query
1247
	 * @uses current_theme_supports, get_option, self::wp_query, current_user_can, apply_filters, self::get_settings, add_filter, WP_Query, remove_filter, have_posts, wp_head, do_action, add_action, this::render, this::has_wrapper, esc_attr, wp_footer, sharing_register_post_for_share_counts, get_the_id
1248
	 * @return string or null
1249
	 */
1250
	function query() {
1251
		if ( ! isset( $_REQUEST['page'] ) || ! current_theme_supports( 'infinite-scroll' ) )
1252
			die;
1253
1254
		$page = (int) $_REQUEST['page'];
1255
1256
		// Sanitize and set $previousday. Expected format: dd.mm.yy
1257
		if ( preg_match( '/^\d{2}\.\d{2}\.\d{2}$/', $_REQUEST['currentday'] ) ) {
1258
			global $previousday;
1259
			$previousday = $_REQUEST['currentday'];
1260
		}
1261
1262
		$post_status = array( 'publish' );
1263
		if ( current_user_can( 'read_private_posts' ) )
1264
			array_push( $post_status, 'private' );
1265
1266
		$order = in_array( $_REQUEST['order'], array( 'ASC', 'DESC' ) ) ? $_REQUEST['order'] : 'DESC';
1267
1268
		$query_args = array_merge( self::wp_query()->query_vars, array(
1269
			'paged'          => $page,
1270
			'post_status'    => $post_status,
1271
			'posts_per_page' => self::posts_per_page(),
1272
			'order'          => $order
1273
		) );
1274
1275
		// 4.0 ?s= compatibility, see https://core.trac.wordpress.org/ticket/11330#comment:50
1276
		if ( empty( $query_args['s'] ) && ! isset( self::wp_query()->query['s'] ) ) {
1277
			unset( $query_args['s'] );
1278
		}
1279
1280
		// By default, don't query for a specific page of a paged post object.
1281
		// This argument can come from merging self::wp_query() into $query_args above.
1282
		// Since IS is only used on archives, we should always display the first page of any paged content.
1283
		unset( $query_args['page'] );
1284
1285
		/**
1286
		 * Filter the array of main query arguments.
1287
		 *
1288
		 * @module infinite-scroll
1289
		 *
1290
		 * @since 2.0.1
1291
		 *
1292
		 * @param array $query_args Array of Query arguments.
1293
		 */
1294
		$query_args = apply_filters( 'infinite_scroll_query_args', $query_args );
1295
1296
		add_filter( 'posts_where', array( $this, 'query_time_filter' ), 10, 2 );
1297
1298
		$GLOBALS['wp_the_query'] = $GLOBALS['wp_query'] = $infinite_scroll_query = new WP_Query();
1299
1300
		$infinite_scroll_query->query( $query_args );
1301
1302
		remove_filter( 'posts_where', array( $this, 'query_time_filter' ), 10, 2 );
1303
1304
		$results = array();
1305
1306
		if ( have_posts() ) {
1307
			// Fire wp_head to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
1308
			ob_start();
1309
			wp_head();
1310
			while ( ob_get_length() ) {
1311
				ob_end_clean();
1312
			}
1313
1314
			$results['type'] = 'success';
1315
1316
			/**
1317
			 * Gather renderer callbacks. These will be called in order and allow multiple callbacks to be queued. Once content is found, no futher callbacks will run.
1318
			 *
1319
			 * @module infinite-scroll
1320
			 *
1321
			 * @since 6.0.0
1322
			 */
1323
			$callbacks = apply_filters( 'infinite_scroll_render_callbacks', array(
1324
				self::get_settings()->render, // This is the setting callback e.g. from add theme support.
1325
			) );
1326
1327
			// Append fallback callback. That rhymes.
1328
			$callbacks[] = array( $this, 'render' );
1329
1330
			foreach ( $callbacks as $callback ) {
1331
				if ( false !== $callback && is_callable( $callback ) ) {
1332
					rewind_posts();
1333
					ob_start();
1334
					add_action( 'infinite_scroll_render', $callback );
1335
1336
					/**
1337
					 * Fires when rendering Infinite Scroll posts.
1338
					 *
1339
					 * @module infinite-scroll
1340
					 *
1341
					 * @since 2.0.0
1342
					 */
1343
					do_action( 'infinite_scroll_render' );
1344
1345
					$results['html'] = ob_get_clean();
1346
					remove_action( 'infinite_scroll_render', $callback );
1347
				}
1348
				if ( ! empty( $results['html'] ) ) {
1349
					break;
1350
				}
1351
			}
1352
1353
			// If primary and fallback rendering methods fail, prevent further IS rendering attempts. Otherwise, wrap the output if requested.
1354
			if ( empty( $results['html'] ) ) {
1355
				unset( $results['html'] );
1356
				/**
1357
				 * Fires when Infinite Scoll doesn't render any posts.
1358
				 *
1359
				 * @module infinite-scroll
1360
				 *
1361
				 * @since 2.0.0
1362
				 */
1363
				do_action( 'infinite_scroll_empty' );
1364
				$results['type'] = 'empty';
1365
			} elseif ( $this->has_wrapper() ) {
1366
				$wrapper_classes = is_string( self::get_settings()->wrapper ) ? self::get_settings()->wrapper : 'infinite-wrap';
1367
				$wrapper_classes .= ' infinite-view-' . $page;
1368
				$wrapper_classes = trim( $wrapper_classes );
1369
1370
				$results['html'] = '<div class="' . esc_attr( $wrapper_classes ) . '" id="infinite-view-' . $page . '" data-page-num="' . $page . '">' . $results['html'] . '</div>';
1371
			}
1372
1373
			// Fire wp_footer to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
1374
			ob_start();
1375
			wp_footer();
1376
			while ( ob_get_length() ) {
1377
				ob_end_clean();
1378
			}
1379
1380
			if ( 'success' == $results['type'] ) {
1381
				global $currentday;
1382
				$results['lastbatch'] = self::is_last_batch();
1383
				$results['currentday'] = $currentday;
1384
			}
1385
1386
			// Loop through posts to capture sharing data for new posts loaded via Infinite Scroll
1387
			if ( 'success' == $results['type'] && function_exists( 'sharing_register_post_for_share_counts' ) ) {
1388
				global $jetpack_sharing_counts;
1389
1390
				while( have_posts() ) {
1391
					the_post();
1392
1393
					sharing_register_post_for_share_counts( get_the_ID() );
1394
				}
1395
1396
				$results['postflair'] = array_flip( $jetpack_sharing_counts );
1397
			}
1398
		} else {
1399
			/** This action is already documented in modules/infinite-scroll/infinity.php */
1400
			do_action( 'infinite_scroll_empty' );
1401
			$results['type'] = 'empty';
1402
		}
1403
1404
		wp_send_json(
1405
			/**
1406
			 * Filter the Infinite Scroll results.
1407
			 *
1408
			 * @module infinite-scroll
1409
			 *
1410
			 * @since 2.0.0
1411
			 *
1412
			 * @param array $results Array of Infinite Scroll results.
1413
			 * @param array $query_args Array of main query arguments.
1414
			 * @param WP_Query $wp_query WP Query.
1415
			 */
1416
			apply_filters( 'infinite_scroll_results', $results, $query_args, self::wp_query() )
1417
		);
1418
	}
1419
1420
	/**
1421
	 * Update the $allowed_vars array with the standard WP public and private
1422
	 * query vars, as well as taxonomy vars
1423
	 *
1424
	 * @global $wp
1425
	 * @param array $allowed_vars
1426
	 * @filter infinite_scroll_allowed_vars
1427
	 * @return array
1428
	 */
1429
	function allowed_query_vars( $allowed_vars ) {
1430
		global $wp;
1431
1432
		$allowed_vars += $wp->public_query_vars;
1433
		$allowed_vars += $wp->private_query_vars;
1434
		$allowed_vars += $this->get_taxonomy_vars();
1435
1436
		foreach ( array_keys( $allowed_vars, 'paged' ) as $key ) {
1437
			unset( $allowed_vars[ $key ] );
1438
		}
1439
1440
		return array_unique( $allowed_vars );
1441
	}
1442
1443
	/**
1444
	 * Returns an array of stock and custom taxonomy query vars
1445
	 *
1446
	 * @global $wp_taxonomies
1447
	 * @return array
1448
	 */
1449
	function get_taxonomy_vars() {
1450
		global $wp_taxonomies;
1451
1452
		$taxonomy_vars = array();
1453
		foreach ( $wp_taxonomies as $taxonomy => $t ) {
1454
			if ( $t->query_var )
1455
				$taxonomy_vars[] = $t->query_var;
1456
		}
1457
1458
		// still needed?
1459
		$taxonomy_vars[] = 'tag_id';
1460
1461
		return $taxonomy_vars;
1462
	}
1463
1464
	/**
1465
	 * Update the $query_args array with the parameters provided via AJAX/GET.
1466
	 *
1467
	 * @param array $query_args
1468
	 * @filter infinite_scroll_query_args
1469
	 * @return array
1470
	 */
1471
	function inject_query_args( $query_args ) {
1472
		/**
1473
		 * Filter the array of allowed Infinite Scroll query arguments.
1474
		 *
1475
		 * @module infinite-scroll
1476
		 *
1477
		 * @since 2.6.0
1478
		 *
1479
		 * @param array $args Array of allowed Infinite Scroll query arguments.
1480
		 * @param array $query_args Array of query arguments.
1481
		 */
1482
		$allowed_vars = apply_filters( 'infinite_scroll_allowed_vars', array(), $query_args );
1483
1484
		$query_args = array_merge( $query_args, array(
1485
			'suppress_filters' => false,
1486
		) );
1487
1488
		if ( is_array( $_REQUEST[ 'query_args' ] ) ) {
1489
			foreach ( $_REQUEST[ 'query_args' ] as $var => $value ) {
1490
				if ( in_array( $var, $allowed_vars ) && ! empty( $value ) )
1491
					$query_args[ $var ] = $value;
1492
			}
1493
		}
1494
1495
		return $query_args;
1496
	}
1497
1498
	/**
1499
	 * Rendering fallback used when themes don't specify their own handler.
1500
	 *
1501
	 * @uses have_posts, the_post, get_template_part, get_post_format
1502
	 * @action infinite_scroll_render
1503
	 * @return string
1504
	 */
1505
	function render() {
1506
		while ( have_posts() ) {
1507
			the_post();
1508
1509
			get_template_part( 'content', get_post_format() );
1510
		}
1511
	}
1512
1513
	/**
1514
	 * Allow plugins to filter what archives Infinite Scroll supports
1515
	 *
1516
	 * @uses current_theme_supports, is_home, is_archive, apply_filters, self::get_settings
1517
	 * @return bool
1518
	 */
1519
	public static function archive_supports_infinity() {
1520
		$supported = current_theme_supports( 'infinite-scroll' ) && ( is_home() || is_archive() || is_search() );
1521
1522
		// Disable when previewing a non-active theme in the customizer
1523
		if ( is_customize_preview() && ! $GLOBALS['wp_customize']->is_theme_active() ) {
1524
			return false;
1525
		}
1526
1527
		/**
1528
		 * Allow plugins to filter what archives Infinite Scroll supports.
1529
		 *
1530
		 * @module infinite-scroll
1531
		 *
1532
		 * @since 2.0.0
1533
		 *
1534
		 * @param bool $supported Does the Archive page support Infinite Scroll.
1535
		 * @param object self::get_settings() IS settings provided by theme.
1536
		 */
1537
		return (bool) apply_filters( 'infinite_scroll_archive_supported', $supported, self::get_settings() );
1538
	}
1539
1540
	/**
1541
	 * The Infinite Blog Footer
1542
	 *
1543
	 * @uses self::get_settings, self::archive_supports_infinity, self::default_footer
1544
	 * @return string or null
1545
	 */
1546
	function footer() {
1547
		// Bail if theme requested footer not show
1548
		if ( false == self::get_settings()->footer )
1549
			return;
1550
1551
		// We only need the new footer for the 'scroll' type
1552
		if ( 'scroll' != self::get_settings()->type || ! self::archive_supports_infinity() )
1553
			return;
1554
1555
		if ( self::is_last_batch() ) {
1556
			return;
1557
		}
1558
1559
		// Display a footer, either user-specified or a default
1560
		if ( false !== self::get_settings()->footer_callback && is_callable( self::get_settings()->footer_callback ) )
1561
			call_user_func( self::get_settings()->footer_callback, self::get_settings() );
1562
		else
1563
			self::default_footer();
1564
	}
1565
1566
	/**
1567
	 * Render default IS footer
1568
	 *
1569
	 * @uses __, wp_get_theme, apply_filters, home_url, esc_attr, get_bloginfo, bloginfo
1570
	 * @return string
1571
	 *
1572
	 */
1573
	private function default_footer() {
1574
		if ( '' !== get_privacy_policy_url() ) {
1575
			$credits = get_the_privacy_policy_link() . '<span role="separator" aria-hidden="true"> / </span>';
1576
		} else {
1577
			$credits = '';
1578
		}
1579
		$credits .= sprintf(
1580
			'<a href="https://wordpress.org/" rel="noopener noreferrer" target="_blank" rel="generator">%1$s</a> ',
1581
			__( 'Proudly powered by WordPress', 'jetpack' )
1582
		);
1583
		$credits .= sprintf(
1584
			/* translators: %1$s is the name of a theme */
1585
			__( 'Theme: %1$s.', 'jetpack' ),
1586
			wp_get_theme()->Name
1587
		);
1588
		/**
1589
		 * Filter Infinite Scroll's credit text.
1590
		 *
1591
		 * @module infinite-scroll
1592
		 *
1593
		 * @since 2.0.0
1594
		 *
1595
		 * @param string $credits Infinite Scroll credits.
1596
		 */
1597
		$credits = apply_filters( 'infinite_scroll_credit', $credits );
1598
1599
		?>
1600
		<div id="infinite-footer">
1601
			<div class="container">
1602
				<div class="blog-info">
1603
					<a id="infinity-blog-title" href="<?php echo home_url( '/' ); ?>" rel="home">
1604
						<?php bloginfo( 'name' ); ?>
1605
					</a>
1606
				</div>
1607
				<div class="blog-credits">
1608
					<?php echo $credits; ?>
1609
				</div>
1610
			</div>
1611
		</div><!-- #infinite-footer -->
1612
		<?php
1613
	}
1614
1615
	/**
1616
	 * Ensure that IS doesn't interfere with Grunion by stripping IS query arguments from the Grunion redirect URL.
1617
	 * When arguments are present, Grunion redirects to the IS AJAX endpoint.
1618
	 *
1619
	 * @param string $url
1620
	 * @uses remove_query_arg
1621
	 * @filter grunion_contact_form_redirect_url
1622
	 * @return string
1623
	 */
1624
	public function filter_grunion_redirect_url( $url ) {
1625
		// Remove IS query args, if present
1626
		if ( false !== strpos( $url, 'infinity=scrolling' ) ) {
1627
			$url = remove_query_arg( array(
1628
				'infinity',
1629
				'action',
1630
				'page',
1631
				'order',
1632
				'scripts',
1633
				'styles'
1634
			), $url );
1635
		}
1636
1637
		return $url;
1638
	}
1639
1640
	/**
1641
	 * When the MediaElement is loaded in dynamically, we need to enforce that
1642
	 * its settings are added to the page as well.
1643
	 *
1644
	 * @param array $scripts_data New scripts exposed to the infinite scroll.
1645
	 *
1646
	 * @since 8.4.0
1647
	 */
1648
	public function add_mejs_config( $scripts_data ) {
1649
		foreach ( $scripts_data as $key => $data ) {
1650
			if ( 'mediaelement-core' === $data['handle'] ) {
1651
				$mejs_settings = array(
1652
					'pluginPath'  => includes_url( 'js/mediaelement/', 'relative' ),
1653
					'classPrefix' => 'mejs-',
1654
					'stretching'  => 'responsive',
1655
				);
1656
1657
				$scripts_data[ $key ]['extra_data'] = sprintf(
1658
					'window.%s = %s',
1659
					'_wpmejsSettings',
1660
					wp_json_encode( apply_filters( 'mejs_settings', $mejs_settings ) )
1661
				);
1662
			}
1663
		}
1664
		return $scripts_data;
1665
	}
1666
};
1667
1668
/**
1669
 * Initialize The_Neverending_Home_Page
1670
 */
1671
function the_neverending_home_page_init() {
1672
	if ( ! current_theme_supports( 'infinite-scroll' ) )
1673
		return;
1674
1675
	new The_Neverending_Home_Page();
1676
}
1677
add_action( 'init', 'the_neverending_home_page_init', 20 );
1678
1679
/**
1680
 * Check whether the current theme is infinite-scroll aware.
1681
 * If so, include the files which add theme support.
1682
 */
1683
function the_neverending_home_page_theme_support() {
1684
	if (
1685
			defined( 'IS_WPCOM' ) && IS_WPCOM &&
1686
			defined( 'REST_API_REQUEST' ) && REST_API_REQUEST &&
1687
			! doing_action( 'restapi_theme_after_setup_theme' )
1688
	) {
1689
		// Don't source theme compat files until we're in the site's context
1690
		return;
1691
	}
1692
	$theme_name = get_stylesheet();
1693
1694
	/**
1695
	 * Filter the path to the Infinite Scroll compatibility file.
1696
	 *
1697
	 * @module infinite-scroll
1698
	 *
1699
	 * @since 2.0.0
1700
	 *
1701
	 * @param string $str IS compatibility file path.
1702
	 * @param string $theme_name Theme name.
1703
	 */
1704
	$customization_file = apply_filters( 'infinite_scroll_customization_file', dirname( __FILE__ ) . "/themes/{$theme_name}.php", $theme_name );
1705
1706
	if ( is_readable( $customization_file ) )
1707
		require_once( $customization_file );
1708
}
1709
add_action( 'after_setup_theme', 'the_neverending_home_page_theme_support', 5 );
1710
1711
/**
1712
 * Early accommodation of the Infinite Scroll AJAX request
1713
 */
1714
if ( The_Neverending_Home_Page::got_infinity() ) {
1715
	/**
1716
	 * If we're sure this is an AJAX request (i.e. the HTTP_X_REQUESTED_WITH header says so),
1717
	 * indicate it as early as possible for actions like init
1718
	 */
1719
	if ( ! defined( 'DOING_AJAX' ) &&
1720
		isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) &&
1721
		strtoupper( $_SERVER['HTTP_X_REQUESTED_WITH'] ) == 'XMLHTTPREQUEST'
1722
	) {
1723
		define( 'DOING_AJAX', true );
1724
	}
1725
1726
	// Don't load the admin bar when doing the AJAX response.
1727
	show_admin_bar( false );
1728
}
1729