Completed
Push — feature/settings-overhaul ( 9ca94b...7e5f0d )
by
unknown
33:48 queued 25:34
created

modules/infinite-scroll/infinity.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
/*
4
Plugin Name: The Neverending Home Page.
5
Plugin URI: http://automattic.com/
6
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.
7
Version: 1.1
8
Author: Automattic
9
Author URI: http://automattic.com/
10
License: GNU General Public License v2 or later
11
License URI: http://www.gnu.org/licenses/gpl-2.0.html
12
*/
13
14
/**
15
 * Class: The_Neverending_Home_Page relies on add_theme_support, expects specific
16
 * styling from each theme; including fixed footer.
17
 */
18
class The_Neverending_Home_Page {
19
	/**
20
	 * Register actions and filters, plus parse IS settings
21
	 *
22
	 * @uses add_action, add_filter, self::get_settings
23
	 * @return null
24
	 */
25
	function __construct() {
26
		add_action( 'pre_get_posts',                  array( $this, 'posts_per_page_query' ) );
27
28
		add_action( 'admin_init',                     array( $this, 'settings_api_init' ) );
29
		add_action( 'template_redirect',              array( $this, 'action_template_redirect' ) );
30
		add_action( 'template_redirect',              array( $this, 'ajax_response' ) );
31
		add_action( 'custom_ajax_infinite_scroll',    array( $this, 'query' ) );
32
		add_filter( 'infinite_scroll_query_args',     array( $this, 'inject_query_args' ) );
33
		add_filter( 'infinite_scroll_allowed_vars',   array( $this, 'allowed_query_vars' ) );
34
		add_action( 'the_post',                       array( $this, 'preserve_more_tag' ) );
35
		add_action( 'wp_footer',                      array( $this, 'footer' ) );
36
37
		// Plugin compatibility
38
		add_filter( 'grunion_contact_form_redirect_url', array( $this, 'filter_grunion_redirect_url' ) );
39
40
		// Parse IS settings from theme
41
		self::get_settings();
42
	}
43
44
	/**
45
	 * Initialize our static variables
46
	 */
47
	static $the_time = null;
48
	static $settings = null; // Don't access directly, instead use self::get_settings().
49
50
	static $option_name_enabled = 'infinite_scroll';
51
52
	/**
53
	 * Parse IS settings provided by theme
54
	 *
55
	 * @uses get_theme_support, infinite_scroll_has_footer_widgets, sanitize_title, add_action, get_option, wp_parse_args, is_active_sidebar
56
	 * @return object
57
	 */
58
	static function get_settings() {
59
		if ( is_null( self::$settings ) ) {
60
			$css_pattern = '#[^A-Z\d\-_]#i';
61
62
			$settings = $defaults = array(
63
				'type'            => 'scroll', // scroll | click
64
				'requested_type'  => 'scroll', // store the original type for use when logic overrides it
65
				'footer_widgets'  => false, // true | false | sidebar_id | array of sidebar_ids -- last two are checked with is_active_sidebar
66
				'container'       => 'content', // container html id
67
				'wrapper'         => true, // true | false | html class
68
				'render'          => false, // optional function, otherwise the `content` template part will be used
69
				'footer'          => true, // boolean to enable or disable the infinite footer | string to provide an html id to derive footer width from
70
				'footer_callback' => false, // function to be called to render the IS footer, in place of the default
71
				'posts_per_page'  => false, // int | false to set based on IS type
72
				'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`.
73
			);
74
75
			// Validate settings passed through add_theme_support()
76
			$_settings = get_theme_support( 'infinite-scroll' );
77
78
			if ( is_array( $_settings ) ) {
79
				// Preferred implementation, where theme provides an array of options
80
				if ( isset( $_settings[0] ) && is_array( $_settings[0] ) ) {
81
					foreach ( $_settings[0] as $key => $value ) {
82
						switch ( $key ) {
83
							case 'type' :
84
								if ( in_array( $value, array( 'scroll', 'click' ) ) )
85
									$settings[ $key ] = $settings['requested_type'] = $value;
86
87
								break;
88
89
							case 'footer_widgets' :
90
								if ( is_string( $value ) )
91
									$settings[ $key ] = sanitize_title( $value );
92
								elseif ( is_array( $value ) )
93
									$settings[ $key ] = array_map( 'sanitize_title', $value );
94
								elseif ( is_bool( $value ) )
95
									$settings[ $key ] = $value;
96
97
								break;
98
99
							case 'container' :
100 View Code Duplication
							case 'wrapper' :
101
								if ( 'wrapper' == $key && is_bool( $value ) ) {
102
									$settings[ $key ] = $value;
103
								} else {
104
									$value = preg_replace( $css_pattern, '', $value );
105
106
									if ( ! empty( $value ) )
107
										$settings[ $key ] = $value;
108
								}
109
110
								break;
111
112
							case 'render' :
113
								if ( false !== $value && is_callable( $value ) ) {
114
									$settings[ $key ] = $value;
115
116
									add_action( 'infinite_scroll_render', $value );
117
								}
118
119
								break;
120
121 View Code Duplication
							case 'footer' :
122
								if ( is_bool( $value ) ) {
123
									$settings[ $key ] = $value;
124
								} elseif ( is_string( $value ) ) {
125
									$value = preg_replace( $css_pattern, '', $value );
126
127
									if ( ! empty( $value ) )
128
										$settings[ $key ] = $value;
129
								}
130
131
								break;
132
133
							case 'footer_callback' :
134
								if ( is_callable( $value ) )
135
									$settings[ $key ] = $value;
136
								else
137
									$settings[ $key ] = false;
138
139
								break;
140
141
							case 'posts_per_page' :
142
								if ( is_numeric( $value ) )
143
									$settings[ $key ] = (int) $value;
144
145
								break;
146
147
							case 'click_handle' :
148
								if ( is_bool( $value ) ) {
149
									$settings[ $key ] = $value;
150
								}
151
152
								break;
153
154
							default:
155
								continue;
156
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
			// posts_per_page defaults to 7 for scroll, posts_per_page option for click
223
			if ( false === $settings['posts_per_page'] ) {
224
				if ( 'scroll' === $settings['type'] ) {
225
					$settings['posts_per_page'] = 7;
226
				}
227
				else {
228
					$settings['posts_per_page'] = (int) get_option( 'posts_per_page' );
229
				}
230
			}
231
232
			// If IS is set to click, and if the site owner changed posts_per_page, let's use that
233
			if (
234
				'click' == $settings['type']
235
				&& ( '10' !== get_option( 'posts_per_page' ) )
236
			) {
237
				$settings['posts_per_page'] = (int) get_option( 'posts_per_page' );
238
			}
239
240
			// Force display of the click handler and attendant bits when the type isn't `click`
241
			if ( 'click' !== $settings['type'] ) {
242
				$settings['click_handle'] = true;
243
			}
244
245
			// Store final settings in a class static to avoid reparsing
246
			/**
247
			 * Filter the array of Infinite Scroll settings.
248
			 *
249
			 * @module infinite-scroll
250
			 *
251
			 * @since 2.0.0
252
			 *
253
			 * @param array $settings Array of Infinite Scroll settings.
254
			 */
255
			self::$settings = apply_filters( 'infinite_scroll_settings', $settings );
256
		}
257
258
		/** This filter is already documented in modules/infinite-scroll/infinity.php */
259
		return (object) apply_filters( 'infinite_scroll_settings', self::$settings );
260
	}
261
262
	/**
263
	 * Retrieve the query used with Infinite Scroll
264
	 *
265
	 * @global $wp_the_query
266
	 * @uses apply_filters
267
	 * @return object
268
	 */
269
	static function wp_query() {
270
		global $wp_the_query;
271
		/**
272
		 * Filter the Infinite Scroll query object.
273
		 *
274
		 * @module infinite-scroll
275
		 *
276
		 * @since 2.2.1
277
		 *
278
		 * @param WP_Query $wp_the_query WP Query.
279
		 */
280
		return apply_filters( 'infinite_scroll_query_object', $wp_the_query );
281
	}
282
283
	/**
284
	 * Has infinite scroll been triggered?
285
	 */
286
	static function got_infinity() {
287
		/**
288
		 * Filter the parameter used to check if Infinite Scroll has been triggered.
289
		 *
290
		 * @module infinite-scroll
291
		 *
292
		 * @since 3.9.0
293
		 *
294
		 * @param bool isset( $_GET[ 'infinity' ] ) Return true if the "infinity" parameter is set.
295
		 */
296
		return apply_filters( 'infinite_scroll_got_infinity', isset( $_GET[ 'infinity' ] ) );
297
	}
298
299
	/**
300
	 * Is this guaranteed to be the last batch of posts?
301
	 */
302
	static function is_last_batch() {
303
		$entries = (int) self::wp_query()->found_posts;
304
		$posts_per_page = self::get_settings()->posts_per_page;
305
		$paged = self::wp_query()->get( 'paged' );
306
307
		// Are there enough posts for more than the first page?
308
		if ( $entries <= $posts_per_page ) {
309
			return true;
310
		}
311
312
		// Calculate entries left after a certain number of pages
313
		if ( $paged && $paged > 1 ) {
314
			$entries -= $posts_per_page * $paged;
315
		}
316
317
		// Are there some entries left to display?
318
		return $entries <= 0;
319
	}
320
321
	/**
322
	 * The more tag will be ignored by default if the blog page isn't our homepage.
323
	 * Let's force the $more global to false.
324
	 */
325
	function preserve_more_tag( $array ) {
326
		global $more;
327
328
		if ( self::got_infinity() )
329
			$more = 0; //0 = show content up to the more tag. Add more link.
330
331
		return $array;
332
	}
333
334
	/**
335
	 * Add a checkbox field to Settings > Reading
336
	 * for enabling infinite scroll.
337
	 *
338
	 * Only show if the current theme supports infinity.
339
	 *
340
	 * @uses current_theme_supports, add_settings_field, __, register_setting
341
	 * @action admin_init
342
	 * @return null
343
	 */
344
	function settings_api_init() {
345
		if ( ! current_theme_supports( 'infinite-scroll' ) )
346
			return;
347
348
		// Add the setting field [infinite_scroll] and place it in Settings > Reading
349
		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' );
350
		register_setting( 'reading', self::$option_name_enabled, 'esc_attr' );
351
	}
352
353
	/**
354
	 * HTML code to display a checkbox true/false option
355
	 * for the infinite_scroll setting.
356
	 */
357
	function infinite_setting_html() {
358
		$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>';
359
360
		// If the blog has footer widgets, show a notice instead of the checkbox
361
		if ( self::get_settings()->footer_widgets || 'click' == self::get_settings()->requested_type ) {
362
			echo '<label>' . $notice . '</label>';
363
		} else {
364
			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>';
365
			echo '<p class="description">' . esc_html( sprintf( _n( 'Shows %s post on each load.', 'Shows %s posts on each load.', self::get_settings()->posts_per_page, 'jetpack' ), number_format_i18n( self::get_settings()->posts_per_page ) ) ) . '</p>';
366
		}
367
	}
368
369
	/**
370
	 * Does the legwork to determine whether the feature is enabled.
371
	 *
372
	 * @uses current_theme_supports, self::archive_supports_infinity, self::get_settings, add_filter, wp_enqueue_script, plugins_url, wp_enqueue_style, add_action
373
	 * @action template_redirect
374
	 * @return null
375
	 */
376
	function action_template_redirect() {
377
		// Check that we support infinite scroll, and are on the home page.
378
		if ( ! current_theme_supports( 'infinite-scroll' ) || ! self::archive_supports_infinity() )
379
			return;
380
381
		$id = self::get_settings()->container;
382
383
		// Check that we have an id.
384
		if ( empty( $id ) )
385
			return;
386
387
		// Add our scripts.
388
		wp_register_script( 'the-neverending-homepage', plugins_url( 'infinity.js', __FILE__ ), array( 'jquery' ), '4.0.0', true );
389
390
		// Add our default styles.
391
		wp_register_style( 'the-neverending-homepage', plugins_url( 'infinity.css', __FILE__ ), array(), '20140422' );
392
393
		// Make sure there are enough posts for IS
394
		if ( self::is_last_batch() ) {
395
			return;
396
		}
397
398
		// Add our scripts.
399
		wp_enqueue_script( 'the-neverending-homepage' );
400
401
		// Add our default styles.
402
		wp_enqueue_style( 'the-neverending-homepage' );
403
404
		add_action( 'wp_enqueue_scripts', array( $this, 'enqueue_spinner_scripts' ) );
405
406
		add_action( 'wp_footer', array( $this, 'action_wp_footer_settings' ), 2 );
407
408
		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
409
410
		add_filter( 'infinite_scroll_results', array( $this, 'filter_infinite_scroll_results' ), 10, 3 );
411
	}
412
413
	/**
414
	 * Enqueue spinner scripts.
415
	 */
416
	function enqueue_spinner_scripts() {
417
		wp_enqueue_script( 'jquery.spin' );
418
	}
419
420
	/**
421
	 * Returns classes to be added to <body>. If it's enabled, 'infinite-scroll'. If set to continuous scroll, adds 'neverending' too.
422
	 *
423
	 * @since 4.7.0 No longer added as a 'body_class' filter but passed to JS environment and added using JS.
424
	 *
425
	 * @return string
426
	 */
427
	function body_class() {
428
		$classes = '';
429
		// Do not add infinity-scroll class if disabled through the Reading page
430
		$disabled = '' === get_option( self::$option_name_enabled ) ? true : false;
431
		if ( ! $disabled || 'click' == self::get_settings()->type ) {
432
			$classes = 'infinite-scroll';
433
434
			if ( 'scroll' == self::get_settings()->type )
435
				$classes .= ' neverending';
436
		}
437
438
		return $classes;
439
	}
440
441
	/**
442
	 * 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
443
	 *
444
	 * @uses self::wp_query
445
	 * @uses self::get_last_post_date
446
	 * @uses self::has_only_title_matching_posts
447
	 * @return array
448
	 */
449
	function get_excluded_posts() {
450
451
		$excluded_posts = array();
452
		//loop through posts returned by wp_query call
453
		foreach( self::wp_query()->get_posts() as $post ) {
454
455
			$orderby = isset( self::wp_query()->query_vars['orderby'] ) ? self::wp_query()->query_vars['orderby'] : '';
456
			$post_date = ( ! empty( $post->post_date ) ? $post->post_date : false );
457
			if ( 'modified' === $orderby || false === $post_date ) {
458
				$post_date = $post->post_modified;
459
			}
460
461
			//in case all posts initially displayed match the keyword by title we add em all to excluded posts array
462
			//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
463
			if ( self::has_only_title_matching_posts() || $post_date <= self::get_last_post_date() ) {
464
				array_push( $excluded_posts, $post->ID );
465
			}
466
		}
467
		return $excluded_posts;
468
	}
469
470
	/**
471
	 * 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
472
	 *
473
	 * @uses self::wp_query
474
	 * @uses self::get_excluded_posts
475
	 * @return array
476
	 */
477
	function get_query_vars() {
478
479
		$query_vars = self::wp_query()->query_vars;
480
		//applies to search page only
481
		if ( true === self::wp_query()->is_search() ) {
482
			//set post__not_in array in query_vars in case it does not exists
483
			if ( false === isset( $query_vars['post__not_in'] ) ) {
484
				$query_vars['post__not_in'] = array();
485
			}
486
			//get excluded posts
487
			$excluded = self::get_excluded_posts();
488
			//merge them with other post__not_in posts (eg.: sticky posts)
489
			$query_vars['post__not_in'] = array_merge( $query_vars['post__not_in'], $excluded );
490
		}
491
		return $query_vars;
492
	}
493
494
	/**
495
	 * This function checks whether all posts returned by initial wp_query match the keyword by title
496
	 * The code used in this function is borrowed from WP_Query class where it is used to construct like conditions for keywords
497
	 *
498
	 * @uses self::wp_query
499
	 * @return bool
500
	 */
501
	function has_only_title_matching_posts() {
502
503
		//apply following logic for search page results only
504
		if ( false === self::wp_query()->is_search() ) {
505
			return false;
506
		}
507
508
		//grab the last posts in the stack as if the last one is title-matching the rest is title-matching as well
509
		$post = end( self::wp_query()->posts );
510
511
		//code inspired by WP_Query class
512
		if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', self::wp_query()->get( 's' ), $matches ) ) {
513
			$search_terms = self::wp_query()->query_vars['search_terms'];
514
			// if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence
515
			if ( empty( $search_terms ) || count( $search_terms ) > 9 ) {
516
				$search_terms = array( self::wp_query()->get( 's' ) );
517
			}
518
		} else {
519
			$search_terms = array( self::wp_query()->get( 's' ) );
520
		}
521
522
		//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
523
		$term = current( $search_terms );
524
		if ( ! empty( $term ) && false !== strpos( $post->post_title, $term ) ) {
525
			return true;
526
		}
527
528
		return false;
529
	}
530
531
	/**
532
	 * Grab the timestamp for the initial query's last post.
533
	 *
534
	 * This takes into account the query's 'orderby' parameter and returns
535
	 * false if the posts are not ordered by date.
536
	 *
537
	 * @uses self::got_infinity
538
	 * @uses self::has_only_title_matching_posts
539
	 * @uses self::wp_query
540
	 * @return string 'Y-m-d H:i:s' or false
541
	 */
542
	function get_last_post_date() {
543
		if ( self::got_infinity() )
544
			return;
545
546
		if ( ! self::wp_query()->have_posts() ) {
547
			return null;
548
		}
549
550
		//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
551
		if ( true === self::has_only_title_matching_posts() ) {
552
			return false;
553
		}
554
555
		$post = end( self::wp_query()->posts );
556
		$orderby = isset( self::wp_query()->query_vars['orderby'] ) ?
557
			self::wp_query()->query_vars['orderby'] : '';
558
		$post_date = ( ! empty( $post->post_date ) ? $post->post_date : false );
559 View Code Duplication
		switch ( $orderby ) {
560
			case 'modified':
561
				return $post->post_modified;
562
			case 'date':
563
			case '':
564
				return $post_date;
565
			default:
566
				return false;
567
		}
568
	}
569
570
	/**
571
	 * Returns the appropriate `wp_posts` table field for a given query's
572
	 * 'orderby' parameter, if applicable.
573
	 *
574
	 * @param optional object $query
575
	 * @uses self::wp_query
576
	 * @return string or false
577
	 */
578
	function get_query_sort_field( $query = null ) {
579
		if ( empty( $query ) )
580
			$query = self::wp_query();
581
582
		$orderby = isset( $query->query_vars['orderby'] ) ? $query->query_vars['orderby'] : '';
583
584 View Code Duplication
		switch ( $orderby ) {
585
			case 'modified':
586
				return 'post_modified';
587
			case 'date':
588
			case '':
589
				return 'post_date';
590
			default:
591
				return false;
592
		}
593
	}
594
595
	/**
596
	 * Create a where clause that will make sure post queries
597
	 * will always return results prior to (descending sort)
598
	 * or before (ascending sort) the last post date.
599
	 *
600
	 * @global $wpdb
601
	 * @param string $where
602
	 * @param object $query
603
	 * @uses apply_filters
604
	 * @filter posts_where
605
	 * @return string
606
	 */
607
	function query_time_filter( $where, $query ) {
608
		if ( self::got_infinity() ) {
609
			global $wpdb;
610
611
			$sort_field = self::get_query_sort_field( $query );
612
			if ( false == $sort_field )
613
				return $where;
614
615
			$last_post_date = $_REQUEST['last_post_date'];
616
			// Sanitize timestamp
617
			if ( empty( $last_post_date ) || !preg_match( '|\d{4}\-\d{2}\-\d{2}|', $last_post_date ) )
618
				return $where;
619
620
			$operator = 'ASC' == $_REQUEST['query_args']['order'] ? '>' : '<';
621
622
			// Construct the date query using our timestamp
623
			$clause = $wpdb->prepare( " AND {$wpdb->posts}.{$sort_field} {$operator} %s", $last_post_date );
624
625
			/**
626
			 * Filter Infinite Scroll's SQL date query making sure post queries
627
			 * will always return results prior to (descending sort)
628
			 * or before (ascending sort) the last post date.
629
			 *
630
			 * @module infinite-scroll
631
			 *
632
			 * @param string $clause SQL Date query.
633
			 * @param object $query Query.
634
			 * @param string $operator Query operator.
635
			 * @param string $last_post_date Last Post Date timestamp.
636
			 */
637
			$where .= apply_filters( 'infinite_scroll_posts_where', $clause, $query, $operator, $last_post_date );
638
		}
639
640
		return $where;
641
	}
642
643
	/**
644
	 * Let's overwrite the default post_per_page setting to always display a fixed amount.
645
	 *
646
	 * @param object $query
647
	 * @uses is_admin, self::archive_supports_infinity, self::get_settings
648
	 * @return null
649
	 */
650
	function posts_per_page_query( $query ) {
651
		if ( ! is_admin() && self::archive_supports_infinity() && $query->is_main_query() )
652
			$query->set( 'posts_per_page', self::get_settings()->posts_per_page );
653
	}
654
655
	/**
656
	 * Check if the IS output should be wrapped in a div.
657
	 * Setting value can be a boolean or a string specifying the class applied to the div.
658
	 *
659
	 * @uses self::get_settings
660
	 * @return bool
661
	 */
662
	function has_wrapper() {
663
		return (bool) self::get_settings()->wrapper;
664
	}
665
666
	/**
667
	 * Returns the Ajax url
668
	 *
669
	 * @global $wp
670
	 * @uses home_url, add_query_arg, apply_filters
671
	 * @return string
672
	 */
673
	function ajax_url() {
674
		$base_url = set_url_scheme( home_url( '/' ) );
675
676
		$ajaxurl = add_query_arg( array( 'infinity' => 'scrolling' ), $base_url );
677
678
		/**
679
		 * Filter the Infinite Scroll Ajax URL.
680
		 *
681
		 * @module infinite-scroll
682
		 *
683
		 * @since 2.0.0
684
		 *
685
		 * @param string $ajaxurl Infinite Scroll Ajax URL.
686
		 */
687
		return apply_filters( 'infinite_scroll_ajax_url', $ajaxurl );
688
	}
689
690
	/**
691
	 * Our own Ajax response, avoiding calling admin-ajax
692
	 */
693
	function ajax_response() {
694
		// Only proceed if the url query has a key of "Infinity"
695
		if ( ! self::got_infinity() )
696
			return false;
697
698
		// This should already be defined below, but make sure.
699
		if ( ! defined( 'DOING_AJAX' ) ) {
700
			define( 'DOING_AJAX', true );
701
		}
702
703
		@header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) );
704
		send_nosniff_header();
705
706
		/**
707
		 * Fires at the end of the Infinite Scroll Ajax response.
708
		 *
709
		 * @module infinite-scroll
710
		 *
711
		 * @since 2.0.0
712
		 */
713
		do_action( 'custom_ajax_infinite_scroll' );
714
		die( '0' );
715
	}
716
717
	/**
718
	 * Alias for renamed class method.
719
	 *
720
	 * Previously, JS settings object was unnecessarily output in the document head.
721
	 * When the hook was changed, the method name no longer made sense.
722
	 */
723
	function action_wp_head() {
724
		$this->action_wp_footer_settings();
725
	}
726
727
	/**
728
	 * Prints the relevant infinite scroll settings in JS.
729
	 *
730
	 * @global $wp_rewrite
731
	 * @uses self::get_settings, esc_js, esc_url_raw, self::has_wrapper, __, apply_filters, do_action, self::get_query_vars
732
	 * @action wp_footer
733
	 * @return string
734
	 */
735
	function action_wp_footer_settings() {
736
		global $wp_rewrite;
737
		global $currentday;
738
739
		// Default click handle text
740
		$click_handle_text = __( 'Older posts', 'jetpack' );
741
742
		// If a single CPT is displayed, use its plural name instead of "posts"
743
		// Could be empty (posts) or an array of multiple post types.
744
		// In the latter two cases cases, the default text is used, leaving the `infinite_scroll_js_settings` filter for further customization.
745
		$post_type = self::wp_query()->get( 'post_type' );
746
		if ( is_string( $post_type ) && ! empty( $post_type ) ) {
747
			$post_type = get_post_type_object( $post_type );
748
749
			if ( is_object( $post_type ) && ! is_wp_error( $post_type ) ) {
750
				if ( isset( $post_type->labels->name ) ) {
751
					$cpt_text = $post_type->labels->name;
752
				} elseif ( isset( $post_type->label ) ) {
753
					$cpt_text = $post_type->label;
754
				}
755
756
				if ( isset( $cpt_text ) ) {
757
					/* translators: %s is the name of a custom post type */
758
					$click_handle_text = sprintf( __( 'Older %s', 'jetpack' ), $cpt_text );
759
					unset( $cpt_text );
760
				}
761
			}
762
		}
763
764
		unset( $post_type );
765
766
		// Base JS settings
767
		$js_settings = array(
768
			'id'               => self::get_settings()->container,
769
			'ajaxurl'          => esc_url_raw( self::ajax_url() ),
770
			'type'             => esc_js( self::get_settings()->type ),
771
			'wrapper'          => self::has_wrapper(),
772
			'wrapper_class'    => is_string( self::get_settings()->wrapper ) ? esc_js( self::get_settings()->wrapper ) : 'infinite-wrap',
773
			'footer'           => is_string( self::get_settings()->footer ) ? esc_js( self::get_settings()->footer ) : self::get_settings()->footer,
774
			'click_handle'     => esc_js( self::get_settings()->click_handle ),
775
			'text'             => esc_js( $click_handle_text ),
776
			'totop'            => esc_js( __( 'Scroll back to top', 'jetpack' ) ),
777
			'currentday'       => $currentday,
778
			'order'            => 'DESC',
779
			'scripts'          => array(),
780
			'styles'           => array(),
781
			'google_analytics' => false,
782
			'offset'           => self::wp_query()->get( 'paged' ),
783
			'history'          => array(
784
				'host'                 => preg_replace( '#^http(s)?://#i', '', untrailingslashit( esc_url( get_home_url() ) ) ),
785
				'path'                 => self::get_request_path(),
786
				'use_trailing_slashes' => $wp_rewrite->use_trailing_slashes,
787
				'parameters'           => self::get_request_parameters(),
788
			),
789
			'query_args'      => self::get_query_vars(),
790
			'last_post_date'  => self::get_last_post_date(),
791
			'body_class'	  => self::body_class(),
792
		);
793
794
		// Optional order param
795
		if ( isset( $_REQUEST['order'] ) ) {
796
			$order = strtoupper( $_REQUEST['order'] );
797
798
			if ( in_array( $order, array( 'ASC', 'DESC' ) ) )
799
				$js_settings['order'] = $order;
800
		}
801
802
		/**
803
		 * Filter the Infinite Scroll JS settings outputted in the head.
804
		 *
805
		 * @module infinite-scroll
806
		 *
807
		 * @since 2.0.0
808
		 *
809
		 * @param array $js_settings Infinite Scroll JS settings.
810
		 */
811
		$js_settings = apply_filters( 'infinite_scroll_js_settings', $js_settings );
812
813
		/**
814
		 * Fires before Infinite Scroll outputs inline JavaScript in the head.
815
		 *
816
		 * @module infinite-scroll
817
		 *
818
		 * @since 2.0.0
819
		 */
820
		do_action( 'infinite_scroll_wp_head' );
821
822
		?>
823
		<script type="text/javascript">
824
		//<![CDATA[
825
		var infiniteScroll = <?php echo json_encode( array( 'settings' => $js_settings ) ); ?>;
826
		//]]>
827
		</script>
828
		<?php
829
	}
830
831
	/**
832
	 * Build path data for current request.
833
	 * Used for Google Analytics and pushState history tracking.
834
	 *
835
	 * @global $wp_rewrite
836
	 * @global $wp
837
	 * @uses user_trailingslashit, sanitize_text_field, add_query_arg
838
	 * @return string|bool
839
	 */
840
	private function get_request_path() {
841
		global $wp_rewrite;
842
843
		if ( $wp_rewrite->using_permalinks() ) {
844
			global $wp;
845
846
			// If called too early, bail
847
			if ( ! isset( $wp->request ) )
848
				return false;
849
850
			// Determine path for paginated version of current request
851
			if ( false != preg_match( '#' . $wp_rewrite->pagination_base . '/\d+/?$#i', $wp->request ) )
852
				$path = preg_replace( '#' . $wp_rewrite->pagination_base . '/\d+$#i', $wp_rewrite->pagination_base . '/%d', $wp->request );
853
			else
854
				$path = $wp->request . '/' . $wp_rewrite->pagination_base . '/%d';
855
856
			// Slashes everywhere we need them
857
			if ( 0 !== strpos( $path, '/' ) )
858
				$path = '/' . $path;
859
860
			$path = user_trailingslashit( $path );
861
		} else {
862
			// Clean up raw $_REQUEST input
863
			$path = array_map( 'sanitize_text_field', $_REQUEST );
864
			$path = array_filter( $path );
865
866
			$path['paged'] = '%d';
867
868
			$path = add_query_arg( $path, '/' );
869
		}
870
871
		return empty( $path ) ? false : $path;
872
	}
873
874
	/**
875
	 * Return query string for current request, prefixed with '?'.
876
	 *
877
	 * @return string
878
	 */
879
	private function get_request_parameters() {
880
		$uri = $_SERVER[ 'REQUEST_URI' ];
881
		$uri = preg_replace( '/^[^?]*(\?.*$)/', '$1', $uri, 1, $count );
882
		if ( $count != 1 )
883
			return '';
884
		return $uri;
885
	}
886
887
	/**
888
	 * Provide IS with a list of the scripts and stylesheets already present on the page.
889
	 * Since posts may contain require additional assets that haven't been loaded, this data will be used to track the additional assets.
890
	 *
891
	 * @global $wp_scripts, $wp_styles
892
	 * @action wp_footer
893
	 * @return string
894
	 */
895
	function action_wp_footer() {
896
		global $wp_scripts, $wp_styles;
897
898
		$scripts = is_a( $wp_scripts, 'WP_Scripts' ) ? $wp_scripts->done : array();
899
		/**
900
		 * Filter the list of scripts already present on the page.
901
		 *
902
		 * @module infinite-scroll
903
		 *
904
		 * @since 2.1.2
905
		 *
906
		 * @param array $scripts Array of scripts present on the page.
907
		 */
908
		$scripts = apply_filters( 'infinite_scroll_existing_scripts', $scripts );
909
910
		$styles = is_a( $wp_styles, 'WP_Styles' ) ? $wp_styles->done : array();
911
		/**
912
		 * Filter the list of styles already present on the page.
913
		 *
914
		 * @module infinite-scroll
915
		 *
916
		 * @since 2.1.2
917
		 *
918
		 * @param array $styles Array of styles present on the page.
919
		 */
920
		$styles = apply_filters( 'infinite_scroll_existing_stylesheets', $styles );
921
922
		?><script type="text/javascript">
923
			jQuery.extend( infiniteScroll.settings.scripts, <?php echo json_encode( $scripts ); ?> );
924
			jQuery.extend( infiniteScroll.settings.styles, <?php echo json_encode( $styles ); ?> );
925
		</script><?php
926
	}
927
928
	/**
929
	 * Identify additional scripts required by the latest set of IS posts and provide the necessary data to the IS response handler.
930
	 *
931
	 * @global $wp_scripts
932
	 * @uses sanitize_text_field, add_query_arg
933
	 * @filter infinite_scroll_results
934
	 * @return array
935
	 */
936
	function filter_infinite_scroll_results( $results, $query_args, $wp_query ) {
937
		// Don't bother unless there are posts to display
938
		if ( 'success' != $results['type'] )
939
			return $results;
940
941
		// Parse and sanitize the script handles already output
942
		$initial_scripts = isset( $_REQUEST['scripts'] ) && is_array( $_REQUEST['scripts'] ) ? array_map( 'sanitize_text_field', $_REQUEST['scripts'] ) : false;
943
944
		if ( is_array( $initial_scripts ) ) {
945
			global $wp_scripts;
946
947
			// Identify new scripts needed by the latest set of IS posts
948
			$new_scripts = array_diff( $wp_scripts->done, $initial_scripts );
949
950
			// If new scripts are needed, extract relevant data from $wp_scripts
951
			if ( ! empty( $new_scripts ) ) {
952
				$results['scripts'] = array();
953
954
				foreach ( $new_scripts as $handle ) {
955
					// Abort if somehow the handle doesn't correspond to a registered script
956
					if ( ! isset( $wp_scripts->registered[ $handle ] ) )
957
						continue;
958
959
					// Provide basic script data
960
					$script_data = array(
961
						'handle'     => $handle,
962
						'footer'     => ( is_array( $wp_scripts->in_footer ) && in_array( $handle, $wp_scripts->in_footer ) ),
963
						'extra_data' => $wp_scripts->print_extra_script( $handle, false )
964
					);
965
966
					// Base source
967
					$src = $wp_scripts->registered[ $handle ]->src;
968
969
					// Take base_url into account
970
					if ( strpos( $src, 'http' ) !== 0 )
971
						$src = $wp_scripts->base_url . $src;
972
973
					// Version and additional arguments
974 View Code Duplication
					if ( null === $wp_scripts->registered[ $handle ]->ver )
975
						$ver = '';
976
					else
977
						$ver = $wp_scripts->registered[ $handle ]->ver ? $wp_scripts->registered[ $handle ]->ver : $wp_scripts->default_version;
978
979 View Code Duplication
					if ( isset( $wp_scripts->args[ $handle ] ) )
980
						$ver = $ver ? $ver . '&amp;' . $wp_scripts->args[$handle] : $wp_scripts->args[$handle];
981
982
					// Full script source with version info
983
					$script_data['src'] = add_query_arg( 'ver', $ver, $src );
984
985
					// Add script to data that will be returned to IS JS
986
					array_push( $results['scripts'], $script_data );
987
				}
988
			}
989
		}
990
991
		// Expose additional script data to filters, but only include in final `$results` array if needed.
992
		if ( ! isset( $results['scripts'] ) )
993
			$results['scripts'] = array();
994
995
		/**
996
		 * Filter the additional scripts required by the latest set of IS posts.
997
		 *
998
		 * @module infinite-scroll
999
		 *
1000
		 * @since 2.1.2
1001
		 *
1002
		 * @param array $results['scripts'] Additional scripts required by the latest set of IS posts.
1003
		 * @param array|bool $initial_scripts Set of scripts loaded on each page.
1004
		 * @param array $results Array of Infinite Scroll results.
1005
		 * @param array $query_args Array of Query arguments.
1006
		 * @param WP_Query $wp_query WP Query.
1007
		 */
1008
		$results['scripts'] = apply_filters(
1009
			'infinite_scroll_additional_scripts',
1010
			$results['scripts'],
1011
			$initial_scripts,
1012
			$results,
1013
			$query_args,
1014
			$wp_query
1015
		);
1016
1017
		if ( empty( $results['scripts'] ) )
1018
			unset( $results['scripts' ] );
1019
1020
		// Parse and sanitize the style handles already output
1021
		$initial_styles = isset( $_REQUEST['styles'] ) && is_array( $_REQUEST['styles'] ) ? array_map( 'sanitize_text_field', $_REQUEST['styles'] ) : false;
1022
1023
		if ( is_array( $initial_styles ) ) {
1024
			global $wp_styles;
1025
1026
			// Identify new styles needed by the latest set of IS posts
1027
			$new_styles = array_diff( $wp_styles->done, $initial_styles );
1028
1029
			// If new styles are needed, extract relevant data from $wp_styles
1030
			if ( ! empty( $new_styles ) ) {
1031
				$results['styles'] = array();
1032
1033
				foreach ( $new_styles as $handle ) {
1034
					// Abort if somehow the handle doesn't correspond to a registered stylesheet
1035
					if ( ! isset( $wp_styles->registered[ $handle ] ) )
1036
						continue;
1037
1038
					// Provide basic style data
1039
					$style_data = array(
1040
						'handle' => $handle,
1041
						'media'  => 'all'
1042
					);
1043
1044
					// Base source
1045
					$src = $wp_styles->registered[ $handle ]->src;
1046
1047
					// Take base_url into account
1048
					if ( strpos( $src, 'http' ) !== 0 )
1049
						$src = $wp_styles->base_url . $src;
1050
1051
					// Version and additional arguments
1052 View Code Duplication
					if ( null === $wp_styles->registered[ $handle ]->ver )
1053
						$ver = '';
1054
					else
1055
						$ver = $wp_styles->registered[ $handle ]->ver ? $wp_styles->registered[ $handle ]->ver : $wp_styles->default_version;
1056
1057 View Code Duplication
					if ( isset($wp_styles->args[ $handle ] ) )
1058
						$ver = $ver ? $ver . '&amp;' . $wp_styles->args[$handle] : $wp_styles->args[$handle];
1059
1060
					// Full stylesheet source with version info
1061
					$style_data['src'] = add_query_arg( 'ver', $ver, $src );
1062
1063
					// Parse stylesheet's conditional comments if present, converting to logic executable in JS
1064
					if ( isset( $wp_styles->registered[ $handle ]->extra['conditional'] ) && $wp_styles->registered[ $handle ]->extra['conditional'] ) {
1065
						// First, convert conditional comment operators to standard logical operators. %ver is replaced in JS with the IE version
1066
						$style_data['conditional'] = str_replace( array(
1067
							'lte',
1068
							'lt',
1069
							'gte',
1070
							'gt'
1071
						), array(
1072
							'%ver <=',
1073
							'%ver <',
1074
							'%ver >=',
1075
							'%ver >',
1076
						), $wp_styles->registered[ $handle ]->extra['conditional'] );
1077
1078
						// 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().
1079
						$style_data['conditional'] = preg_replace( '#!\s*IE(\s*\d+){0}#i', '1==2', $style_data['conditional'] );
1080
1081
						// Lastly, remove the IE strings
1082
						$style_data['conditional'] = str_replace( 'IE', '', $style_data['conditional'] );
1083
					}
1084
1085
					// Parse requested media context for stylesheet
1086 View Code Duplication
					if ( isset( $wp_styles->registered[ $handle ]->args ) )
1087
						$style_data['media'] = esc_attr( $wp_styles->registered[ $handle ]->args );
1088
1089
					// Add stylesheet to data that will be returned to IS JS
1090
					array_push( $results['styles'], $style_data );
1091
				}
1092
			}
1093
		}
1094
1095
		// Expose additional stylesheet data to filters, but only include in final `$results` array if needed.
1096
		if ( ! isset( $results['styles'] ) )
1097
			$results['styles'] = array();
1098
1099
		/**
1100
		 * Filter the additional styles required by the latest set of IS posts.
1101
		 *
1102
		 * @module infinite-scroll
1103
		 *
1104
		 * @since 2.1.2
1105
		 *
1106
		 * @param array $results['styles'] Additional styles required by the latest set of IS posts.
1107
		 * @param array|bool $initial_styles Set of styles loaded on each page.
1108
		 * @param array $results Array of Infinite Scroll results.
1109
		 * @param array $query_args Array of Query arguments.
1110
		 * @param WP_Query $wp_query WP Query.
1111
		 */
1112
		$results['styles'] = apply_filters(
1113
			'infinite_scroll_additional_stylesheets',
1114
			$results['styles'],
1115
			$initial_styles,
1116
			$results,
1117
			$query_args,
1118
			$wp_query
1119
		);
1120
1121
		if ( empty( $results['styles'] ) )
1122
			unset( $results['styles' ] );
1123
1124
		// Lastly, return the IS results array
1125
		return $results;
1126
	}
1127
1128
	/**
1129
	 * Runs the query and returns the results via JSON.
1130
	 * Triggered by an AJAX request.
1131
	 *
1132
	 * @global $wp_query
1133
	 * @global $wp_the_query
1134
	 * @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
1135
	 * @return string or null
1136
	 */
1137
	function query() {
1138
		global $wp_customize;
1139
		global $wp_version;
1140
		if ( ! isset( $_REQUEST['page'] ) || ! current_theme_supports( 'infinite-scroll' ) )
1141
			die;
0 ignored issues
show
Coding Style Compatibility introduced by
The method query() 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...
1142
1143
		$page = (int) $_REQUEST['page'];
1144
1145
		// Sanitize and set $previousday. Expected format: dd.mm.yy
1146
		if ( preg_match( '/^\d{2}\.\d{2}\.\d{2}$/', $_REQUEST['currentday'] ) ) {
1147
			global $previousday;
1148
			$previousday = $_REQUEST['currentday'];
1149
		}
1150
1151
		$sticky = get_option( 'sticky_posts' );
1152
		$post__not_in = self::wp_query()->get( 'post__not_in' );
1153
1154
		//we have to take post__not_in args into consideration here not only sticky posts
1155
		if ( true === isset( $_REQUEST['query_args']['post__not_in'] ) ) {
1156
			$post__not_in = array_merge( $post__not_in, array_map( 'intval', (array) $_REQUEST['query_args']['post__not_in'] ) );
1157
		}
1158
1159
		if ( ! empty( $post__not_in ) )
1160
			$sticky = array_unique( array_merge( $sticky, $post__not_in ) );
1161
1162
		$post_status = array( 'publish' );
1163
		if ( current_user_can( 'read_private_posts' ) )
1164
			array_push( $post_status, 'private' );
1165
1166
		$order = in_array( $_REQUEST['order'], array( 'ASC', 'DESC' ) ) ? $_REQUEST['order'] : 'DESC';
1167
1168
		$query_args = array_merge( self::wp_query()->query_vars, array(
1169
			'paged'          => $page,
1170
			'post_status'    => $post_status,
1171
			'posts_per_page' => self::get_settings()->posts_per_page,
1172
			'post__not_in'   => ( array ) $sticky,
1173
			'order'          => $order
1174
		) );
1175
1176
		// 4.0 ?s= compatibility, see https://core.trac.wordpress.org/ticket/11330#comment:50
1177
		if ( empty( $query_args['s'] ) && ! isset( self::wp_query()->query['s'] ) ) {
1178
			unset( $query_args['s'] );
1179
		}
1180
1181
		// By default, don't query for a specific page of a paged post object.
1182
		// This argument can come from merging self::wp_query() into $query_args above.
1183
		// Since IS is only used on archives, we should always display the first page of any paged content.
1184
		unset( $query_args['page'] );
1185
1186
		/**
1187
		 * Filter the array of main query arguments.
1188
		 *
1189
		 * @module infinite-scroll
1190
		 *
1191
		 * @since 2.0.1
1192
		 *
1193
		 * @param array $query_args Array of Query arguments.
1194
		 */
1195
		$query_args = apply_filters( 'infinite_scroll_query_args', $query_args );
1196
1197
		// Add query filter that checks for posts below the date
1198
		add_filter( 'posts_where', array( $this, 'query_time_filter' ), 10, 2 );
1199
1200
		$GLOBALS['wp_the_query'] = $GLOBALS['wp_query'] = new WP_Query( $query_args );
1201
1202
		remove_filter( 'posts_where', array( $this, 'query_time_filter' ), 10, 2 );
1203
1204
		$results = array();
1205
1206
		if ( have_posts() ) {
1207
			// Fire wp_head to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
1208
			ob_start();
1209
			wp_head();
1210
			while ( ob_get_length() ) {
1211
				ob_end_clean();
1212
			}
1213
1214
			$results['type'] = 'success';
1215
1216
			// First, try theme's specified rendering handler, either specified via `add_theme_support` or by hooking to this action directly.
1217
			ob_start();
1218
			/**
1219
			 * Fires when rendering Infinite Scroll posts.
1220
			 *
1221
			 * @module infinite-scroll
1222
			 *
1223
			 * @since 2.0.0
1224
			 */
1225
			do_action( 'infinite_scroll_render' );
1226
			$results['html'] = ob_get_clean();
1227
1228
			// Fall back if a theme doesn't specify a rendering function. Because themes may hook additional functions to the `infinite_scroll_render` action, `has_action()` is ineffective here.
1229
			if ( empty( $results['html'] ) ) {
1230
				add_action( 'infinite_scroll_render', array( $this, 'render' ) );
1231
				rewind_posts();
1232
1233
				ob_start();
1234
				/** This action is already documented in modules/infinite-scroll/infinity.php */
1235
				do_action( 'infinite_scroll_render' );
1236
				$results['html'] = ob_get_clean();
1237
			}
1238
1239
			// If primary and fallback rendering methods fail, prevent further IS rendering attempts. Otherwise, wrap the output if requested.
1240
			if ( empty( $results['html'] ) ) {
1241
				unset( $results['html'] );
1242
				/**
1243
				 * Fires when Infinite Scoll doesn't render any posts.
1244
				 *
1245
				 * @module infinite-scroll
1246
				 *
1247
				 * @since 2.0.0
1248
				 */
1249
				do_action( 'infinite_scroll_empty' );
1250
				$results['type'] = 'empty';
1251
			} elseif ( $this->has_wrapper() ) {
1252
				$wrapper_classes = is_string( self::get_settings()->wrapper ) ? self::get_settings()->wrapper : 'infinite-wrap';
1253
				$wrapper_classes .= ' infinite-view-' . $page;
1254
				$wrapper_classes = trim( $wrapper_classes );
1255
1256
				$results['html'] = '<div class="' . esc_attr( $wrapper_classes ) . '" id="infinite-view-' . $page . '" data-page-num="' . $page . '">' . $results['html'] . '</div>';
1257
			}
1258
1259
			// Fire wp_footer to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
1260
			ob_start();
1261
			wp_footer();
1262
			while ( ob_get_length() ) {
1263
				ob_end_clean();
1264
			}
1265
1266
			if ( 'success' == $results['type'] ) {
1267
				global $currentday;
1268
				$results['lastbatch'] = self::is_last_batch();
1269
				$results['currentday'] = $currentday;
1270
			}
1271
1272
			// Loop through posts to capture sharing data for new posts loaded via Infinite Scroll
1273
			if ( 'success' == $results['type'] && function_exists( 'sharing_register_post_for_share_counts' ) ) {
1274
				global $jetpack_sharing_counts;
1275
1276
				while( have_posts() ) {
1277
					the_post();
1278
1279
					sharing_register_post_for_share_counts( get_the_ID() );
1280
				}
1281
1282
				$results['postflair'] = array_flip( $jetpack_sharing_counts );
1283
			}
1284
		} else {
1285
			/** This action is already documented in modules/infinite-scroll/infinity.php */
1286
			do_action( 'infinite_scroll_empty' );
1287
			$results['type'] = 'empty';
1288
		}
1289
1290
		// This should be removed when WordPress 4.8 is released.
1291
		if ( version_compare( $wp_version, '4.7', '<' ) && is_customize_preview() ) {
1292
			$wp_customize->remove_preview_signature();
1293
		}
1294
1295
		wp_send_json(
1296
			/**
1297
			 * Filter the Infinite Scroll results.
1298
			 *
1299
			 * @module infinite-scroll
1300
			 *
1301
			 * @since 2.0.0
1302
			 *
1303
			 * @param array $results Array of Infinite Scroll results.
1304
			 * @param array $query_args Array of main query arguments.
1305
			 * @param WP_Query $wp_query WP Query.
1306
			 */
1307
			apply_filters( 'infinite_scroll_results', $results, $query_args, self::wp_query() )
1308
		);
1309
	}
1310
1311
	/**
1312
	 * Update the $allowed_vars array with the standard WP public and private
1313
	 * query vars, as well as taxonomy vars
1314
	 *
1315
	 * @global $wp
1316
	 * @param array $allowed_vars
1317
	 * @filter infinite_scroll_allowed_vars
1318
	 * @return array
1319
	 */
1320
	function allowed_query_vars( $allowed_vars ) {
1321
		global $wp;
1322
1323
		$allowed_vars += $wp->public_query_vars;
1324
		$allowed_vars += $wp->private_query_vars;
1325
		$allowed_vars += $this->get_taxonomy_vars();
1326
1327
		foreach ( array_keys( $allowed_vars, 'paged' ) as $key ) {
1328
			unset( $allowed_vars[ $key ] );
1329
		}
1330
1331
		return array_unique( $allowed_vars );
1332
	}
1333
1334
	/**
1335
	 * Returns an array of stock and custom taxonomy query vars
1336
	 *
1337
	 * @global $wp_taxonomies
1338
	 * @return array
1339
	 */
1340
	function get_taxonomy_vars() {
1341
		global $wp_taxonomies;
1342
1343
		$taxonomy_vars = array();
1344
		foreach ( $wp_taxonomies as $taxonomy => $t ) {
1345
			if ( $t->query_var )
1346
				$taxonomy_vars[] = $t->query_var;
1347
		}
1348
1349
		// still needed?
1350
		$taxonomy_vars[] = 'tag_id';
1351
1352
		return $taxonomy_vars;
1353
	}
1354
1355
	/**
1356
	 * Update the $query_args array with the parameters provided via AJAX/GET.
1357
	 *
1358
	 * @param array $query_args
1359
	 * @filter infinite_scroll_query_args
1360
	 * @return array
1361
	 */
1362
	function inject_query_args( $query_args ) {
1363
		/**
1364
		 * Filter the array of allowed Infinite Scroll query arguments.
1365
		 *
1366
		 * @module infinite-scroll
1367
		 *
1368
		 * @since 2.6.0
1369
		 *
1370
		 * @param array $args Array of allowed Infinite Scroll query arguments.
1371
		 * @param array $query_args Array of query arguments.
1372
		 */
1373
		$allowed_vars = apply_filters( 'infinite_scroll_allowed_vars', array(), $query_args );
1374
1375
		$query_args = array_merge( $query_args, array(
1376
			'suppress_filters' => false,
1377
		) );
1378
1379
		if ( is_array( $_REQUEST[ 'query_args' ] ) ) {
1380
			foreach ( $_REQUEST[ 'query_args' ] as $var => $value ) {
1381
				if ( in_array( $var, $allowed_vars ) && ! empty( $value ) )
1382
					$query_args[ $var ] = $value;
1383
			}
1384
		}
1385
1386
		return $query_args;
1387
	}
1388
1389
	/**
1390
	 * Rendering fallback used when themes don't specify their own handler.
1391
	 *
1392
	 * @uses have_posts, the_post, get_template_part, get_post_format
1393
	 * @action infinite_scroll_render
1394
	 * @return string
1395
	 */
1396
	function render() {
1397
		while ( have_posts() ) {
1398
			the_post();
1399
1400
			get_template_part( 'content', get_post_format() );
1401
		}
1402
	}
1403
1404
	/**
1405
	 * Allow plugins to filter what archives Infinite Scroll supports
1406
	 *
1407
	 * @uses current_theme_supports, is_home, is_archive, apply_filters, self::get_settings
1408
	 * @return bool
1409
	 */
1410
	public static function archive_supports_infinity() {
1411
		$supported = current_theme_supports( 'infinite-scroll' ) && ( is_home() || is_archive() || is_search() );
1412
1413
		// Disable when previewing a non-active theme in the customizer
1414
		if ( is_customize_preview() && ! $GLOBALS['wp_customize']->is_theme_active() ) {
1415
			return false;
1416
		}
1417
1418
		/**
1419
		 * Allow plugins to filter what archives Infinite Scroll supports.
1420
		 *
1421
		 * @module infinite-scroll
1422
		 *
1423
		 * @since 2.0.0
1424
		 *
1425
		 * @param bool $supported Does the Archive page support Infinite Scroll.
1426
		 * @param object self::get_settings() IS settings provided by theme.
1427
		 */
1428
		return (bool) apply_filters( 'infinite_scroll_archive_supported', $supported, self::get_settings() );
1429
	}
1430
1431
	/**
1432
	 * The Infinite Blog Footer
1433
	 *
1434
	 * @uses self::get_settings, self::archive_supports_infinity, self::default_footer
1435
	 * @return string or null
1436
	 */
1437
	function footer() {
1438
		// Bail if theme requested footer not show
1439
		if ( false == self::get_settings()->footer )
1440
			return;
1441
1442
		// We only need the new footer for the 'scroll' type
1443
		if ( 'scroll' != self::get_settings()->type || ! self::archive_supports_infinity() )
1444
			return;
1445
1446
		if ( self::is_last_batch() ) {
1447
			return;
1448
		}
1449
1450
		// Display a footer, either user-specified or a default
1451
		if ( false !== self::get_settings()->footer_callback && is_callable( self::get_settings()->footer_callback ) )
1452
			call_user_func( self::get_settings()->footer_callback, self::get_settings() );
1453
		else
1454
			self::default_footer();
1455
	}
1456
1457
	/**
1458
	 * Render default IS footer
1459
	 *
1460
	 * @uses __, wp_get_theme, get_current_theme, apply_filters, home_url, esc_attr, get_bloginfo, bloginfo
1461
	 * @return string
1462
	 */
1463
	private function default_footer() {
1464
		$credits = sprintf(
1465
			'<a href="https://wordpress.org/" target="_blank" rel="generator">%1$s</a> ',
1466
			__( 'Proudly powered by WordPress', 'jetpack' )
1467
		);
1468
		$credits .= sprintf(
1469
			/* translators: %1$s is the name of a theme */
1470
			__( 'Theme: %1$s.', 'jetpack' ),
1471
			function_exists( 'wp_get_theme' ) ? wp_get_theme()->Name : get_current_theme()
1472
		);
1473
		/**
1474
		 * Filter Infinite Scroll's credit text.
1475
		 *
1476
		 * @module infinite-scroll
1477
		 *
1478
		 * @since 2.0.0
1479
		 *
1480
		 * @param string $credits Infinite Scroll credits.
1481
		 */
1482
		$credits = apply_filters( 'infinite_scroll_credit', $credits );
1483
1484
		?>
1485
		<div id="infinite-footer">
1486
			<div class="container">
1487
				<div class="blog-info">
1488
					<a id="infinity-blog-title" href="<?php echo home_url( '/' ); ?>" rel="home">
1489
						<?php bloginfo( 'name' ); ?>
1490
					</a>
1491
				</div>
1492
				<div class="blog-credits">
1493
					<?php echo $credits; ?>
1494
				</div>
1495
			</div>
1496
		</div><!-- #infinite-footer -->
1497
		<?php
1498
	}
1499
1500
	/**
1501
	 * Ensure that IS doesn't interfere with Grunion by stripping IS query arguments from the Grunion redirect URL.
1502
	 * When arguments are present, Grunion redirects to the IS AJAX endpoint.
1503
	 *
1504
	 * @param string $url
1505
	 * @uses remove_query_arg
1506
	 * @filter grunion_contact_form_redirect_url
1507
	 * @return string
1508
	 */
1509
	public function filter_grunion_redirect_url( $url ) {
1510
		// Remove IS query args, if present
1511
		if ( false !== strpos( $url, 'infinity=scrolling' ) ) {
1512
			$url = remove_query_arg( array(
1513
				'infinity',
1514
				'action',
1515
				'page',
1516
				'order',
1517
				'scripts',
1518
				'styles'
1519
			), $url );
1520
		}
1521
1522
		return $url;
1523
	}
1524
};
1525
1526
/**
1527
 * Initialize The_Neverending_Home_Page
1528
 */
1529
function the_neverending_home_page_init() {
1530
	if ( ! current_theme_supports( 'infinite-scroll' ) )
1531
		return;
1532
1533
	new The_Neverending_Home_Page;
1534
}
1535
add_action( 'init', 'the_neverending_home_page_init', 20 );
1536
1537
/**
1538
 * Check whether the current theme is infinite-scroll aware.
1539
 * If so, include the files which add theme support.
1540
 */
1541
function the_neverending_home_page_theme_support() {
1542
	$theme_name = get_stylesheet();
1543
1544
	/**
1545
	 * Filter the path to the Infinite Scroll compatibility file.
1546
	 *
1547
	 * @module infinite-scroll
1548
	 *
1549
	 * @since 2.0.0
1550
	 *
1551
	 * @param string $str IS compatibility file path.
1552
	 * @param string $theme_name Theme name.
1553
	 */
1554
	$customization_file = apply_filters( 'infinite_scroll_customization_file', dirname( __FILE__ ) . "/themes/{$theme_name}.php", $theme_name );
1555
1556
	if ( is_readable( $customization_file ) )
1557
		require_once( $customization_file );
1558
}
1559
add_action( 'after_setup_theme', 'the_neverending_home_page_theme_support', 5 );
1560
1561
/**
1562
 * Early accommodation of the Infinite Scroll AJAX request
1563
 */
1564
if ( The_Neverending_Home_Page::got_infinity() ) {
1565
	/**
1566
	 * If we're sure this is an AJAX request (i.e. the HTTP_X_REQUESTED_WITH header says so),
1567
	 * indicate it as early as possible for actions like init
1568
	 */
1569
	if ( ! defined( 'DOING_AJAX' ) &&
1570
		isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) &&
1571
		strtoupper( $_SERVER['HTTP_X_REQUESTED_WITH'] ) == 'XMLHTTPREQUEST'
1572
	) {
1573
		define( 'DOING_AJAX', true );
1574
	}
1575
1576
	// Don't load the admin bar when doing the AJAX response.
1577
	show_admin_bar( false );
1578
}
1579