PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 12.3.2
Jetpack – WP Security, Backup, Speed, & Growth v12.3.2
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / infinite-scroll / infinity.php
jetpack / modules / infinite-scroll Last commit date
themes 4 years ago infinity-customizer.js 6 years ago infinity.css 5 years ago infinity.js 3 years ago infinity.php 3 years ago
infinity.php
2151 lines
1 <?php // phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2
3 // phpcs:disable Universal.Files.SeparateFunctionsFromOO.Mixed -- TODO: Move classes to appropriately-named class files.
4
5 use Automattic\Jetpack\Assets;
6 use Automattic\Jetpack\Redirect;
7
8 /*
9 Plugin Name: The Neverending Home Page.
10 Plugin URI: https://automattic.com/
11 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.
12 Version: 1.1
13 Author: Automattic
14 Author URI: https://automattic.com/
15 License: GNU General Public License v2 or later
16 License URI: https://www.gnu.org/licenses/gpl-2.0.html
17 Text Domain: jetpack
18 */
19
20 /**
21 * Class: The_Neverending_Home_Page relies on add_theme_support, expects specific
22 * styling from each theme; including fixed footer.
23 */
24 class The_Neverending_Home_Page {
25 /**
26 * Maximum allowed number of posts per page in $_REQUEST.
27 */
28 const MAX_ALLOWED_POSTS_PER_PAGE_ΙΝ_REQUEST = 5000;
29
30 /**
31 * Register actions and filters, plus parse IS settings
32 *
33 * @uses add_action, add_filter, self::get_settings
34 */
35 public function __construct() {
36 add_action( 'pre_get_posts', array( $this, 'posts_per_page_query' ) );
37 add_action( 'admin_init', array( $this, 'settings_api_init' ) );
38 add_action( 'template_redirect', array( $this, 'action_template_redirect' ) );
39 add_action( 'customize_preview_init', array( $this, 'init_customizer_assets' ) );
40 add_action( 'template_redirect', array( $this, 'ajax_response' ) );
41 add_action( 'custom_ajax_infinite_scroll', array( $this, 'query' ) );
42 add_filter( 'infinite_scroll_query_args', array( $this, 'inject_query_args' ) );
43 add_filter( 'infinite_scroll_allowed_vars', array( $this, 'allowed_query_vars' ) );
44 add_action( 'the_post', array( $this, 'preserve_more_tag' ) );
45 add_action( 'wp_footer', array( $this, 'footer' ) );
46 add_filter( 'infinite_scroll_additional_scripts', array( $this, 'add_mejs_config' ) );
47
48 // Plugin compatibility
49 add_filter( 'grunion_contact_form_redirect_url', array( $this, 'filter_grunion_redirect_url' ) );
50
51 // AMP compatibility
52 // needs to happen after parse_query so that Jetpack_AMP_Support::is_amp_request() is ready.
53 add_action( 'wp', array( $this, 'amp_load_hooks' ) );
54
55 // Parse IS settings from theme
56 self::get_settings();
57 }
58
59 /**
60 * Initialize our static variables
61 */
62
63 /**
64 * The time.
65 *
66 * @var null - I don't think this is used?
67 */
68 public static $the_time = null;
69
70 /**
71 * Settings.
72 *
73 * Don't access directly, instead use self::get_settings().
74 *
75 * @var array
76 */
77 public static $settings = null;
78
79 /**
80 * The enabled option name.
81 *
82 * @var string
83 */
84 public static $option_name_enabled = 'infinite_scroll';
85
86 /**
87 * Parse IS settings provided by theme
88 *
89 * @uses get_theme_support, infinite_scroll_has_footer_widgets, sanitize_title, add_action, get_option, wp_parse_args, is_active_sidebar
90 * @return object
91 */
92 public static function get_settings() {
93 if ( self::$settings === null ) {
94 $css_pattern = '#[^A-Z\d\-_]#i';
95
96 $defaults = array(
97 'type' => 'scroll', // scroll | click
98 'requested_type' => 'scroll', // store the original type for use when logic overrides it
99 'footer_widgets' => false, // true | false | sidebar_id | array of sidebar_ids -- last two are checked with is_active_sidebar
100 'container' => 'content', // container html id
101 'wrapper' => true, // true | false | html class -- the html class.
102 'render' => false, // optional function, otherwise the `content` template part will be used
103 'footer' => true, // boolean to enable or disable the infinite footer | string to provide an html id to derive footer width from
104 'footer_callback' => false, // function to be called to render the IS footer, in place of the default
105 'posts_per_page' => false, // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page -- int | false to set based on IS type
106 '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`.
107 );
108 $settings = $defaults;
109 // Validate settings passed through add_theme_support()
110 $_settings = get_theme_support( 'infinite-scroll' );
111
112 if ( is_array( $_settings ) ) {
113 // Preferred implementation, where theme provides an array of options
114 if ( isset( $_settings[0] ) && is_array( $_settings[0] ) ) {
115 foreach ( $_settings[0] as $key => $value ) {
116 switch ( $key ) {
117 case 'type':
118 if ( in_array( $value, array( 'scroll', 'click' ), true ) ) {
119 $settings['requested_type'] = $value;
120 $settings[ $key ] = $settings['requested_type'];
121 }
122
123 break;
124
125 case 'footer_widgets':
126 if ( is_string( $value ) ) {
127 $settings[ $key ] = sanitize_title( $value );
128 } elseif ( is_array( $value ) ) {
129 $settings[ $key ] = array_map( 'sanitize_title', $value );
130 } elseif ( is_bool( $value ) ) {
131 $settings[ $key ] = $value;
132 }
133
134 break;
135
136 case 'container':
137 case 'wrapper':
138 if ( 'wrapper' === $key && is_bool( $value ) ) {
139 $settings[ $key ] = $value;
140 } else {
141 $value = preg_replace( $css_pattern, '', $value );
142
143 if ( ! empty( $value ) ) {
144 $settings[ $key ] = $value;
145 }
146 }
147
148 break;
149
150 case 'render':
151 if ( false !== $value && is_callable( $value ) ) {
152 $settings[ $key ] = $value;
153 }
154
155 break;
156
157 case 'footer':
158 if ( is_bool( $value ) ) {
159 $settings[ $key ] = $value;
160 } elseif ( is_string( $value ) ) {
161 $value = preg_replace( $css_pattern, '', $value );
162
163 if ( ! empty( $value ) ) {
164 $settings[ $key ] = $value;
165 }
166 }
167
168 break;
169
170 case 'footer_callback':
171 if ( is_callable( $value ) ) {
172 $settings[ $key ] = $value;
173 } else {
174 $settings[ $key ] = false;
175 }
176
177 break;
178
179 case 'posts_per_page':
180 if ( is_numeric( $value ) ) {
181 $settings[ $key ] = (int) $value;
182 }
183
184 break;
185
186 case 'click_handle':
187 if ( is_bool( $value ) ) {
188 $settings[ $key ] = $value;
189 }
190
191 break;
192
193 default:
194 break;
195 }
196 }
197 } elseif ( is_string( $_settings[0] ) ) {
198 // Checks below are for backwards compatibility
199
200 // Container to append new posts to
201 $settings['container'] = preg_replace( $css_pattern, '', $_settings[0] );
202
203 // Wrap IS elements?
204 if ( isset( $_settings[1] ) ) {
205 $settings['wrapper'] = (bool) $_settings[1];
206 }
207 }
208 }
209
210 // Always ensure all values are present in the final array
211 $settings = wp_parse_args( $settings, $defaults );
212
213 // Check if a legacy `infinite_scroll_has_footer_widgets()` function is defined and override the footer_widgets parameter's value.
214 // Otherwise, if a widget area ID or array of IDs was provided in the footer_widgets parameter, check if any contains any widgets.
215 // 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.
216 if ( function_exists( 'infinite_scroll_has_footer_widgets' ) ) {
217 $settings['footer_widgets'] = (bool) infinite_scroll_has_footer_widgets();
218 } elseif ( is_array( $settings['footer_widgets'] ) ) {
219 $sidebar_ids = $settings['footer_widgets'];
220 $settings['footer_widgets'] = false;
221
222 foreach ( $sidebar_ids as $sidebar_id ) {
223 if ( is_active_sidebar( $sidebar_id ) ) {
224 $settings['footer_widgets'] = true;
225 break;
226 }
227 }
228
229 unset( $sidebar_ids );
230 unset( $sidebar_id );
231 } elseif ( is_string( $settings['footer_widgets'] ) ) {
232 $settings['footer_widgets'] = (bool) is_active_sidebar( $settings['footer_widgets'] );
233 }
234
235 /**
236 * Filter Infinite Scroll's `footer_widgets` parameter.
237 *
238 * @module infinite-scroll
239 *
240 * @since 2.0.0
241 *
242 * @param bool $settings['footer_widgets'] Does the current theme have Footer Widgets.
243 */
244 $settings['footer_widgets'] = apply_filters( 'infinite_scroll_has_footer_widgets', $settings['footer_widgets'] );
245
246 // Finally, after all of the sidebar checks and filtering, ensure that a boolean value is present, otherwise set to default of `false`.
247 if ( ! is_bool( $settings['footer_widgets'] ) ) {
248 $settings['footer_widgets'] = false;
249 }
250
251 // Ensure that IS is enabled and no footer widgets exist if the IS type isn't already "click".
252 if ( 'click' !== $settings['type'] ) {
253 // Check the setting status
254 $disabled = '' === get_option( self::$option_name_enabled ) ? true : false;
255
256 // Footer content or Reading option check
257 if ( $settings['footer_widgets'] || $disabled ) {
258 $settings['type'] = 'click';
259 }
260 }
261
262 // Force display of the click handler and attendant bits when the type isn't `click`
263 if ( 'click' !== $settings['type'] ) {
264 $settings['click_handle'] = true;
265 }
266
267 // Store final settings in a class static to avoid reparsing
268 /**
269 * Filter the array of Infinite Scroll settings.
270 *
271 * @module infinite-scroll
272 *
273 * @since 2.0.0
274 *
275 * @param array $settings Array of Infinite Scroll settings.
276 */
277 self::$settings = apply_filters( 'infinite_scroll_settings', $settings );
278 }
279
280 /** This filter is already documented in modules/infinite-scroll/infinity.php */
281 return (object) apply_filters( 'infinite_scroll_settings', self::$settings );
282 }
283
284 /**
285 * Number of posts per page.
286 *
287 * @uses self::wp_query, self::get_settings, apply_filters
288 * @return int
289 */
290 public static function posts_per_page() {
291 $posts_per_page = self::get_settings()->posts_per_page ? self::get_settings()->posts_per_page : self::wp_query()->get( 'posts_per_page' );
292 $posts_per_page_core_option = get_option( 'posts_per_page' );
293
294 // If Infinite Scroll is set to click, and if the site owner changed posts_per_page, let's use that.
295 if (
296 'click' === self::get_settings()->type
297 && ( '10' !== $posts_per_page_core_option )
298 ) {
299 $posts_per_page = $posts_per_page_core_option;
300 }
301
302 // Take JS query into consideration here.
303 $posts_per_page_in_request = isset( $_REQUEST['query_args']['posts_per_page'] ) ? (int) $_REQUEST['query_args']['posts_per_page'] : 0; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
304 if ( $posts_per_page_in_request > 0 &&
305 self::MAX_ALLOWED_POSTS_PER_PAGE_ΙΝ_REQUEST >= $posts_per_page_in_request
306 ) {
307 $posts_per_page = $posts_per_page_in_request;
308 }
309
310 /**
311 * Filter the number of posts per page.
312 *
313 * @module infinite-scroll
314 *
315 * @since 6.0.0
316 *
317 * @param int $posts_per_page The number of posts to display per page.
318 */
319 return (int) apply_filters( 'infinite_scroll_posts_per_page', $posts_per_page );
320 }
321
322 /**
323 * Retrieve the query used with Infinite Scroll
324 *
325 * @global $wp_the_query
326 * @uses apply_filters
327 * @return object
328 */
329 public static function wp_query() {
330 global $wp_the_query;
331 /**
332 * Filter the Infinite Scroll query object.
333 *
334 * @module infinite-scroll
335 *
336 * @since 2.2.1
337 *
338 * @param WP_Query $wp_the_query WP Query.
339 */
340 return apply_filters( 'infinite_scroll_query_object', $wp_the_query );
341 }
342
343 /**
344 * Has infinite scroll been triggered?
345 */
346 public static function got_infinity() {
347 /**
348 * Filter the parameter used to check if Infinite Scroll has been triggered.
349 *
350 * @module infinite-scroll
351 *
352 * @since 3.9.0
353 *
354 * @param bool isset( $_GET[ 'infinity' ] ) Return true if the "infinity" parameter is set.
355 */
356 return apply_filters( 'infinite_scroll_got_infinity', isset( $_GET['infinity'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes made to the site.
357 }
358
359 /**
360 * Is this guaranteed to be the last batch of posts?
361 */
362 public static function is_last_batch() {
363 /**
364 * Override whether or not this is the last batch for a request
365 *
366 * @module infinite-scroll
367 *
368 * @since 4.8.0
369 *
370 * @param bool|null null Bool if value should be overridden, null to determine from query
371 * @param object self::wp_query() WP_Query object for current request
372 * @param object self::get_settings() Infinite Scroll settings
373 */
374 $override = apply_filters( 'infinite_scroll_is_last_batch', null, self::wp_query(), self::get_settings() ); // phpcs:ignore WordPress.WP.ClassNameCase.Incorrect -- False positive.
375 if ( is_bool( $override ) ) {
376 return $override;
377 }
378
379 $entries = (int) self::wp_query()->found_posts;
380 $posts_per_page = self::posts_per_page();
381
382 // This is to cope with an issue in certain themes or setups where posts are returned but found_posts is 0.
383 if ( 0 === $entries ) {
384 return (bool) ( ! is_countable( self::wp_query()->posts ) || ( count( self::wp_query()->posts ) < $posts_per_page ) );
385 }
386 $paged = max( 1, self::wp_query()->get( 'paged' ) );
387
388 // Are there enough posts for more than the first page?
389 if ( $entries <= $posts_per_page ) {
390 return true;
391 }
392
393 // Calculate entries left after a certain number of pages
394 if ( $paged && $paged > 1 ) {
395 $entries -= $posts_per_page * $paged;
396 }
397
398 // Are there some entries left to display?
399 return $entries <= 0;
400 }
401
402 /**
403 * The more tag will be ignored by default if the blog page isn't our homepage.
404 * Let's force the $more global to false.
405 *
406 * @param array $array - the_post array.
407 * @return array
408 */
409 public function preserve_more_tag( $array ) {
410 global $more;
411
412 if ( self::got_infinity() ) {
413 $more = 0; // phpcs:ignore WordPress.WP.GlobalVariablesOverride.Prohibited -- 0 = show content up to the more tag. Add more link.
414 }
415
416 return $array;
417 }
418
419 /**
420 * Add a checkbox field to Settings > Reading
421 * for enabling infinite scroll.
422 *
423 * Only show if the current theme supports infinity.
424 *
425 * @uses current_theme_supports, add_settings_field, __, register_setting
426 * @action admin_init
427 * @return null
428 */
429 public function settings_api_init() {
430 if ( ! current_theme_supports( 'infinite-scroll' ) ) {
431 return;
432 }
433
434 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
435 // This setting is no longer configurable in wp-admin on WordPress.com -- leave a pointer
436 add_settings_field(
437 self::$option_name_enabled,
438 '<span id="infinite-scroll-options">' . esc_html__( 'Infinite Scroll Behavior', 'jetpack' ) . '</span>',
439 array( $this, 'infinite_setting_html_calypso_placeholder' ),
440 'reading'
441 );
442 return;
443 }
444
445 // Add the setting field [infinite_scroll] and place it in Settings > Reading
446 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' );
447 register_setting( 'reading', self::$option_name_enabled, 'esc_attr' );
448 }
449
450 /**
451 * Render the redirect link to the infinite scroll settings in Calypso.
452 */
453 public function infinite_setting_html_calypso_placeholder() {
454 $details = get_blog_details();
455 $writing_url = Redirect::get_url( 'calypso-settings-writing', array( 'site' => $details->domain ) );
456 echo '<span>' . sprintf(
457 /* translators: Variables are the enclosing link to the settings page */
458 esc_html__( 'This option has moved. You can now manage it %1$shere%2$s.', 'jetpack' ),
459 '<a href="' . esc_url( $writing_url ) . '">',
460 '</a>'
461 ) . '</span>';
462 }
463
464 /**
465 * HTML code to display a checkbox true/false option
466 * for the infinite_scroll setting.
467 */
468 public function infinite_setting_html() {
469
470 // If the blog has footer widgets, show a notice instead of the checkbox
471 if ( self::get_settings()->footer_widgets || 'click' === self::get_settings()->requested_type ) {
472 echo '<label><em>' . esc_html__( '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></label>';
473 } else {
474 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>';
475 // translators: the number of posts to show on each page load.
476 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>';
477 }
478 }
479
480 /**
481 * Does the legwork to determine whether the feature is enabled.
482 *
483 * @uses current_theme_supports, self::archive_supports_infinity, self::get_settings, add_filter, wp_enqueue_script, plugins_url, wp_enqueue_style, add_action
484 * @action template_redirect
485 * @return null
486 */
487 public function action_template_redirect() {
488 // Check that we support infinite scroll, and are on the home page.
489 if ( ! current_theme_supports( 'infinite-scroll' ) || ! self::archive_supports_infinity() ) {
490 return;
491 }
492
493 $id = self::get_settings()->container;
494
495 // Check that we have an id.
496 if ( empty( $id ) ) {
497 return;
498 }
499
500 // AMP infinite scroll functionality will start on amp_load_hooks().
501 if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
502 return;
503 }
504
505 // Add our scripts.
506 wp_register_script(
507 'the-neverending-homepage',
508 Assets::get_file_url_for_environment(
509 '_inc/build/infinite-scroll/infinity.min.js',
510 'modules/infinite-scroll/infinity.js'
511 ),
512 array(),
513 JETPACK__VERSION . '-is5.0.1', // Added for ability to cachebust on WP.com.
514 true
515 );
516
517 // Add our default styles.
518 wp_register_style( 'the-neverending-homepage', plugins_url( 'infinity.css', __FILE__ ), array(), '20140422' );
519
520 // Make sure there are enough posts for IS
521 if ( self::is_last_batch() ) {
522 return;
523 }
524
525 // Add our scripts.
526 wp_enqueue_script( 'the-neverending-homepage' );
527
528 // Add our default styles.
529 wp_enqueue_style( 'the-neverending-homepage' );
530
531 add_action( 'wp_footer', array( $this, 'action_wp_footer_settings' ), 2 );
532
533 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
534
535 add_filter( 'infinite_scroll_results', array( $this, 'filter_infinite_scroll_results' ), 10, 3 );
536 }
537
538 /**
539 * Initialize the Customizer logic separately from the main JS.
540 *
541 * @since 8.4.0
542 */
543 public function init_customizer_assets() {
544 // Add our scripts.
545 wp_register_script(
546 'the-neverending-homepage-customizer',
547 Assets::get_file_url_for_environment(
548 '_inc/build/infinite-scroll/infinity-customizer.min.js',
549 'modules/infinite-scroll/infinity-customizer.js'
550 ),
551 array( 'jquery', 'customize-base' ),
552 JETPACK__VERSION . '-is5.0.0', // Added for ability to cachebust on WP.com.
553 true
554 );
555
556 wp_enqueue_script( 'the-neverending-homepage-customizer' );
557 }
558
559 /**
560 * Returns classes to be added to <body>. If it's enabled, 'infinite-scroll'. If set to continuous scroll, adds 'neverending' too.
561 *
562 * @since 4.7.0 No longer added as a 'body_class' filter but passed to JS environment and added using JS.
563 *
564 * @return string
565 */
566 public function body_class() {
567 $classes = '';
568 // Do not add infinity-scroll class if disabled through the Reading page
569 $disabled = '' === get_option( self::$option_name_enabled ) ? true : false;
570 if ( ! $disabled || 'click' === self::get_settings()->type ) {
571 $classes = 'infinite-scroll';
572
573 if ( 'scroll' === self::get_settings()->type ) {
574 $classes .= ' neverending';
575 }
576 }
577
578 return $classes;
579 }
580
581 /**
582 * 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
583 *
584 * @uses self::wp_query
585 * @uses self::get_last_post_date
586 * @uses self::has_only_title_matching_posts
587 * @return array
588 */
589 public function get_excluded_posts() {
590
591 $excluded_posts = array();
592 // loop through posts returned by wp_query call
593 foreach ( self::wp_query()->get_posts() as $post ) {
594
595 $orderby = isset( self::wp_query()->query_vars['orderby'] ) ? self::wp_query()->query_vars['orderby'] : '';
596 $post_date = ( ! empty( $post->post_date ) ? $post->post_date : false );
597 if ( 'modified' === $orderby || false === $post_date ) {
598 $post_date = $post->post_modified;
599 }
600
601 // in case all posts initially displayed match the keyword by title we add em all to excluded posts array
602 // 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
603 if ( self::has_only_title_matching_posts() || $post_date <= self::get_last_post_date() ) {
604 array_push( $excluded_posts, $post->ID );
605 }
606 }
607 return $excluded_posts;
608 }
609
610 /**
611 * 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
612 *
613 * @uses self::wp_query
614 * @uses self::get_excluded_posts
615 * @return array
616 */
617 public function get_query_vars() {
618
619 $query_vars = self::wp_query()->query_vars;
620 // applies to search page only
621 if ( true === self::wp_query()->is_search() ) {
622 // set post__not_in array in query_vars in case it does not exists
623 if ( false === isset( $query_vars['post__not_in'] ) ) {
624 $query_vars['post__not_in'] = array();
625 }
626 // get excluded posts
627 $excluded = self::get_excluded_posts();
628 // merge them with other post__not_in posts (eg.: sticky posts)
629 $query_vars['post__not_in'] = array_merge( $query_vars['post__not_in'], $excluded );
630 }
631 return $query_vars;
632 }
633
634 /**
635 * This function checks whether all posts returned by initial wp_query match the keyword by title
636 * The code used in this function is borrowed from WP_Query class where it is used to construct like conditions for keywords
637 *
638 * @uses self::wp_query
639 * @return bool
640 */
641 public function has_only_title_matching_posts() {
642
643 // apply following logic for search page results only
644 if ( false === self::wp_query()->is_search() ) {
645 return false;
646 }
647
648 // grab the last posts in the stack as if the last one is title-matching the rest is title-matching as well
649 $post = end( self::wp_query()->posts );
650
651 // code inspired by WP_Query class
652 if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', self::wp_query()->get( 's' ), $matches ) ) {
653 $search_terms = self::wp_query()->query_vars['search_terms'];
654 // if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence
655 if ( empty( $search_terms ) || ! is_countable( $search_terms ) || count( $search_terms ) > 9 ) {
656 $search_terms = array( self::wp_query()->get( 's' ) );
657 }
658 } else {
659 $search_terms = array( self::wp_query()->get( 's' ) );
660 }
661
662 // 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
663 $term = current( $search_terms );
664 if ( ! empty( $term ) && false !== strpos( $post->post_title, $term ) ) {
665 return true;
666 }
667
668 return false;
669 }
670
671 /**
672 * Grab the timestamp for the initial query's last post.
673 *
674 * This takes into account the query's 'orderby' parameter and returns
675 * false if the posts are not ordered by date.
676 *
677 * @uses self::got_infinity
678 * @uses self::has_only_title_matching_posts
679 * @uses self::wp_query
680 * @return string 'Y-m-d H:i:s' or false
681 */
682 public function get_last_post_date() {
683 if ( self::got_infinity() ) {
684 return;
685 }
686
687 if ( ! self::wp_query()->have_posts() ) {
688 return null;
689 }
690
691 // 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
692 if ( true === self::has_only_title_matching_posts() ) {
693 return false;
694 }
695
696 $post = end( self::wp_query()->posts );
697 $orderby = isset( self::wp_query()->query_vars['orderby'] ) ?
698 self::wp_query()->query_vars['orderby'] : '';
699 $post_date = ( ! empty( $post->post_date ) ? $post->post_date : false );
700 switch ( $orderby ) {
701 case 'modified':
702 return $post->post_modified;
703 case 'date':
704 case '':
705 return $post_date;
706 default:
707 return false;
708 }
709 }
710
711 /**
712 * Returns the appropriate `wp_posts` table field for a given query's
713 * 'orderby' parameter, if applicable.
714 *
715 * @param object $query - an optional query object.
716 * @uses self::wp_query
717 * @return string or false
718 */
719 public function get_query_sort_field( $query = null ) {
720 if ( empty( $query ) ) {
721 $query = self::wp_query();
722 }
723
724 $orderby = isset( $query->query_vars['orderby'] ) ? $query->query_vars['orderby'] : '';
725
726 switch ( $orderby ) {
727 case 'modified':
728 return 'post_modified';
729 case 'date':
730 case '':
731 return 'post_date';
732 default:
733 return false;
734 }
735 }
736
737 /**
738 * Create a where clause that will make sure post queries return posts
739 * in the correct order, without duplicates, if a new post is added
740 * and we're sorting by post date.
741 *
742 * @global $wpdb
743 * @param string $where - the where clause.
744 * @param object $query - the query.
745 * @uses apply_filters
746 * @filter posts_where
747 * @return string
748 */
749 public function query_time_filter( $where, $query ) {
750 if ( self::got_infinity() ) {
751 global $wpdb;
752
753 $sort_field = self::get_query_sort_field( $query );
754
755 if ( 'post_date' !== $sort_field || 'DESC' !== $_REQUEST['query_args']['order'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- no changes made to the site.
756 return $where;
757 }
758
759 $query_before = sanitize_text_field( wp_unslash( $_REQUEST['query_before'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- no changes made to the site.
760
761 if ( empty( $query_before ) ) {
762 return $where;
763 }
764
765 // Construct the date query using our timestamp
766 $clause = $wpdb->prepare( " AND {$wpdb->posts}.post_date <= %s", $query_before );
767
768 /**
769 * Filter Infinite Scroll's SQL date query making sure post queries
770 * will always return results prior to (descending sort)
771 * or before (ascending sort) the last post date.
772 *
773 * @module infinite-scroll
774 *
775 * @param string $clause SQL Date query.
776 * @param object $query Query.
777 * @param string $operator @deprecated Query operator.
778 * @param string $last_post_date @deprecated Last Post Date timestamp.
779 */
780 $operator = 'ASC' === $_REQUEST['query_args']['order'] ? '>' : '<'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- no changes to the site.
781 $last_post_date = sanitize_text_field( wp_unslash( $_REQUEST['last_post_date'] ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotValidated -- no changes to the site.
782 $where .= apply_filters( 'infinite_scroll_posts_where', $clause, $query, $operator, $last_post_date );
783 }
784
785 return $where;
786 }
787
788 /**
789 * Let's overwrite the default post_per_page setting to always display a fixed amount.
790 *
791 * @param object $query - the query.
792 * @uses is_admin, self::archive_supports_infinity, self::get_settings
793 */
794 public function posts_per_page_query( $query ) {
795 if ( ! is_admin() && self::archive_supports_infinity() && $query->is_main_query() ) {
796 $query->set( 'posts_per_page', self::posts_per_page() );
797 }
798 }
799
800 /**
801 * Check if the IS output should be wrapped in a div.
802 * Setting value can be a boolean or a string specifying the class applied to the div.
803 *
804 * @uses self::get_settings
805 * @return bool
806 */
807 public function has_wrapper() {
808 return (bool) self::get_settings()->wrapper;
809 }
810
811 /**
812 * Returns the Ajax url
813 *
814 * @global $wp
815 * @uses home_url, add_query_arg, apply_filters
816 * @return string
817 */
818 public function ajax_url() {
819 $base_url = set_url_scheme( home_url( '/' ) );
820
821 $ajaxurl = add_query_arg( array( 'infinity' => 'scrolling' ), $base_url );
822
823 /**
824 * Filter the Infinite Scroll Ajax URL.
825 *
826 * @module infinite-scroll
827 *
828 * @since 2.0.0
829 *
830 * @param string $ajaxurl Infinite Scroll Ajax URL.
831 */
832 return apply_filters( 'infinite_scroll_ajax_url', $ajaxurl );
833 }
834
835 /**
836 * Our own Ajax response, avoiding calling admin-ajax
837 */
838 public function ajax_response() {
839 // Only proceed if the url query has a key of "Infinity"
840 if ( ! self::got_infinity() ) {
841 return false;
842 }
843
844 // This should already be defined below, but make sure.
845 if ( ! defined( 'DOING_AJAX' ) ) {
846 define( 'DOING_AJAX', true );
847 }
848
849 @header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
850 send_nosniff_header();
851
852 /**
853 * Fires at the end of the Infinite Scroll Ajax response.
854 *
855 * @module infinite-scroll
856 *
857 * @since 2.0.0
858 */
859 do_action( 'custom_ajax_infinite_scroll' );
860 die( '0' );
861 }
862
863 /**
864 * Alias for renamed class method.
865 *
866 * Previously, JS settings object was unnecessarily output in the document head.
867 * When the hook was changed, the method name no longer made sense.
868 */
869 public function action_wp_head() {
870 $this->action_wp_footer_settings();
871 }
872
873 /**
874 * Prints the relevant infinite scroll settings in JS.
875 *
876 * @global $wp_rewrite
877 * @uses self::get_settings, esc_js, esc_url_raw, self::has_wrapper, __, apply_filters, do_action, self::get_query_vars
878 * @action wp_footer
879 */
880 public function action_wp_footer_settings() {
881 global $wp_rewrite;
882 global $currentday;
883
884 // Default click handle text
885 $click_handle_text = __( 'Older posts', 'jetpack' );
886
887 // If a single CPT is displayed, use its plural name instead of "posts"
888 // Could be empty (posts) or an array of multiple post types.
889 // In the latter two cases cases, the default text is used, leaving the `infinite_scroll_js_settings` filter for further customization.
890 $post_type = self::wp_query()->get( 'post_type' );
891
892 // If it's a taxonomy, try to change the button text.
893 if ( is_tax() ) {
894 // Get current taxonomy slug.
895 $taxonomy_slug = self::wp_query()->get( 'taxonomy' );
896
897 // Get taxonomy settings.
898 $taxonomy = get_taxonomy( $taxonomy_slug );
899
900 // Check if the taxonomy is attached to one post type only and use its plural name.
901 // If not, use "Posts" without confusing the users.
902 if (
903 is_a( $taxonomy, 'WP_Taxonomy' )
904 && is_countable( $taxonomy->object_type )
905 && count( $taxonomy->object_type ) < 2
906 ) {
907 $post_type = $taxonomy->object_type[0];
908 }
909 }
910
911 if ( is_string( $post_type ) && ! empty( $post_type ) ) {
912 $post_type = get_post_type_object( $post_type );
913
914 if ( is_object( $post_type ) && ! is_wp_error( $post_type ) ) {
915 if ( isset( $post_type->labels->name ) ) {
916 $cpt_text = $post_type->labels->name;
917 } elseif ( isset( $post_type->label ) ) {
918 $cpt_text = $post_type->label;
919 }
920
921 if ( isset( $cpt_text ) ) {
922 /* translators: %s is the name of a custom post type */
923 $click_handle_text = sprintf( __( 'More %s', 'jetpack' ), $cpt_text );
924 unset( $cpt_text );
925 }
926 }
927 }
928
929 unset( $post_type );
930
931 // Base JS settings
932 $js_settings = array(
933 'id' => self::get_settings()->container,
934 'ajaxurl' => esc_url_raw( self::ajax_url() ),
935 'type' => esc_js( self::get_settings()->type ),
936 'wrapper' => self::has_wrapper(),
937 'wrapper_class' => is_string( self::get_settings()->wrapper ) ? esc_js( self::get_settings()->wrapper ) : 'infinite-wrap',
938 'footer' => is_string( self::get_settings()->footer ) ? esc_js( self::get_settings()->footer ) : self::get_settings()->footer,
939 'click_handle' => esc_js( self::get_settings()->click_handle ),
940 'text' => esc_js( $click_handle_text ),
941 'totop' => esc_js( __( 'Scroll back to top', 'jetpack' ) ),
942 'currentday' => $currentday,
943 'order' => 'DESC',
944 'scripts' => array(),
945 'styles' => array(),
946 'google_analytics' => false,
947 'offset' => max( 1, self::wp_query()->get( 'paged' ) ), // Pass through the current page so we can use that to offset the first load.
948 'history' => array(
949 'host' => preg_replace( '#^http(s)?://#i', '', untrailingslashit( esc_url( get_home_url() ) ) ),
950 'path' => self::get_request_path(),
951 'use_trailing_slashes' => $wp_rewrite->use_trailing_slashes,
952 'parameters' => self::get_request_parameters(),
953 ),
954 'query_args' => self::get_query_vars(),
955 'query_before' => current_time( 'mysql' ),
956 'last_post_date' => self::get_last_post_date(),
957 'body_class' => self::body_class(),
958 'loading_text' => esc_js( __( 'Loading new page', 'jetpack' ) ),
959 );
960
961 // Optional order param
962 if ( isset( $_REQUEST['order'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes made to the site.
963 $order = strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes made to the site.
964
965 if ( in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
966 $js_settings['order'] = $order;
967 }
968 }
969
970 /**
971 * Filter the Infinite Scroll JS settings outputted in the head.
972 *
973 * @module infinite-scroll
974 *
975 * @since 2.0.0
976 *
977 * @param array $js_settings Infinite Scroll JS settings.
978 */
979 $js_settings = apply_filters( 'infinite_scroll_js_settings', $js_settings );
980
981 /**
982 * Fires before Infinite Scroll outputs inline JavaScript in the head.
983 *
984 * @module infinite-scroll
985 *
986 * @since 2.0.0
987 */
988 do_action( 'infinite_scroll_wp_head' );
989
990 ?>
991 <script type="text/javascript">
992 var infiniteScroll = <?php echo wp_json_encode( array( 'settings' => $js_settings ), JSON_HEX_TAG ); ?>;
993 </script>
994 <?php
995 }
996
997 // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
998
999 /**
1000 * Build path data for current request.
1001 * Used for Google Analytics and pushState history tracking.
1002 *
1003 * @global $wp_rewrite
1004 * @global $wp
1005 * @uses user_trailingslashit, sanitize_text_field, add_query_arg
1006 * @return string|bool
1007 */
1008 private function get_request_path() {
1009 global $wp_rewrite;
1010
1011 if ( $wp_rewrite->using_permalinks() ) {
1012 global $wp;
1013
1014 // If called too early, bail
1015 if ( ! isset( $wp->request ) ) {
1016 return false;
1017 }
1018
1019 // Determine path for paginated version of current request
1020 if ( preg_match( '#' . preg_quote( $wp_rewrite->pagination_base, '#' ) . '/\d+/?$#i', $wp->request ) ) {
1021 $path = preg_replace( '#' . preg_quote( $wp_rewrite->pagination_base, '#' ) . '/\d+$#i', $wp_rewrite->pagination_base . '/%d', $wp->request );
1022 } else {
1023 $path = $wp->request . '/' . $wp_rewrite->pagination_base . '/%d';
1024 }
1025
1026 // Slashes everywhere we need them
1027 if ( 0 !== strpos( $path, '/' ) ) {
1028 $path = '/' . $path;
1029 }
1030
1031 $path = user_trailingslashit( $path );
1032 } else {
1033 // Clean up raw $_REQUEST input
1034 $path = array_map( 'sanitize_text_field', wp_unslash( $_REQUEST ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- seems this is used for Google Analytics and browser history tracking.
1035 $path = array_filter( $path );
1036
1037 $path['paged'] = '%d';
1038
1039 $path = add_query_arg( $path, '/' );
1040 }
1041
1042 return empty( $path ) ? false : $path;
1043 }
1044
1045 /**
1046 * Return query string for current request, prefixed with '?'.
1047 *
1048 * @return string
1049 */
1050 private function get_request_parameters() {
1051 $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
1052 $uri = preg_replace( '/^[^?]*(\?.*$)/', '$1', $uri, 1, $count );
1053 if ( $count !== 1 ) {
1054 return '';
1055 }
1056 return $uri;
1057 }
1058
1059 /**
1060 * Provide IS with a list of the scripts and stylesheets already present on the page.
1061 * Since posts may contain require additional assets that haven't been loaded, this data will be used to track the additional assets.
1062 *
1063 * @global $wp_scripts, $wp_styles
1064 * @action wp_footer
1065 */
1066 public function action_wp_footer() {
1067 global $wp_scripts, $wp_styles;
1068
1069 $scripts = is_a( $wp_scripts, 'WP_Scripts' ) ? $wp_scripts->done : array();
1070 /**
1071 * Filter the list of scripts already present on the page.
1072 *
1073 * @module infinite-scroll
1074 *
1075 * @since 2.1.2
1076 *
1077 * @param array $scripts Array of scripts present on the page.
1078 */
1079 $scripts = apply_filters( 'infinite_scroll_existing_scripts', $scripts );
1080
1081 $styles = is_a( $wp_styles, 'WP_Styles' ) ? $wp_styles->done : array();
1082 /**
1083 * Filter the list of styles already present on the page.
1084 *
1085 * @module infinite-scroll
1086 *
1087 * @since 2.1.2
1088 *
1089 * @param array $styles Array of styles present on the page.
1090 */
1091 $styles = apply_filters( 'infinite_scroll_existing_stylesheets', $styles );
1092
1093 ?>
1094 <script type="text/javascript">
1095 (function() {
1096 var extend = function(out) {
1097 out = out || {};
1098
1099 for (var i = 1; i < arguments.length; i++) {
1100 if (!arguments[i])
1101 continue;
1102
1103 for (var key in arguments[i]) {
1104 if (arguments[i].hasOwnProperty(key))
1105 out[key] = arguments[i][key];
1106 }
1107 }
1108
1109 return out;
1110 };
1111 extend( window.infiniteScroll.settings.scripts, <?php echo wp_json_encode( $scripts ); ?> );
1112 extend( window.infiniteScroll.settings.styles, <?php echo wp_json_encode( $styles ); ?> );
1113 })();
1114 </script>
1115 <?php
1116 $aria_live = 'assertive';
1117 if ( 'scroll' === self::get_settings()->type ) {
1118 $aria_live = 'polite';
1119 }
1120 ?>
1121 <span id="infinite-aria" aria-live="<?php echo esc_attr( $aria_live ); ?>"></span>
1122 <?php
1123 }
1124
1125 /**
1126 * Identify additional scripts required by the latest set of IS posts and provide the necessary data to the IS response handler.
1127 *
1128 * @param array $results - the results.
1129 * @param array $query_args - Array of Query arguments.
1130 * @param array $wp_query - the WP query.
1131 * @global $wp_scripts
1132 * @uses sanitize_text_field, add_query_arg
1133 * @filter infinite_scroll_results
1134 * @return array
1135 */
1136 public function filter_infinite_scroll_results( $results, $query_args, $wp_query ) {
1137 // Don't bother unless there are posts to display
1138 if ( 'success' !== $results['type'] ) {
1139 return $results;
1140 }
1141
1142 // Parse and sanitize the script handles already output
1143 $initial_scripts = isset( $_REQUEST['scripts'] ) && is_array( $_REQUEST['scripts'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_REQUEST['scripts'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes made.
1144
1145 if ( is_array( $initial_scripts ) ) {
1146 global $wp_scripts;
1147
1148 // Identify new scripts needed by the latest set of IS posts
1149 $new_scripts = array_filter(
1150 $wp_scripts->done,
1151 function ( $script_name ) use ( $initial_scripts ) {
1152 // Jetpack block scripts should always be sent, even if they've been
1153 // sent before. These scripts only run once on when loaded, they don't
1154 // watch for new blocks being added.
1155 if ( 0 === strpos( $script_name, 'jetpack-block-' ) ) {
1156 return true;
1157 }
1158
1159 return ! in_array( $script_name, $initial_scripts, true );
1160 }
1161 );
1162
1163 // If new scripts are needed, extract relevant data from $wp_scripts
1164 if ( ! empty( $new_scripts ) ) {
1165 $results['scripts'] = array();
1166
1167 foreach ( $new_scripts as $handle ) {
1168 // Abort if somehow the handle doesn't correspond to a registered script
1169 // or if the script doesn't have `src` set.
1170 $script_not_registered = ! isset( $wp_scripts->registered[ $handle ] );
1171 $empty_src = empty( $wp_scripts->registered[ $handle ]->src );
1172 if ( $script_not_registered || $empty_src ) {
1173 continue;
1174 }
1175
1176 // Provide basic script data
1177 $script_data = array(
1178 'handle' => $handle,
1179 'footer' => ( is_array( $wp_scripts->in_footer ) && in_array( $handle, $wp_scripts->in_footer, true ) ),
1180 'extra_data' => $wp_scripts->print_extra_script( $handle, false ),
1181 'before_handle' => $wp_scripts->print_inline_script( $handle, 'before', false ),
1182 'after_handle' => $wp_scripts->print_inline_script( $handle, 'after', false ),
1183 );
1184
1185 // Base source
1186 $src = $wp_scripts->registered[ $handle ]->src;
1187
1188 // Take base_url into account
1189 if ( strpos( $src, 'http' ) !== 0 ) {
1190 $src = $wp_scripts->base_url . $src;
1191 }
1192
1193 // Version and additional arguments
1194 if ( null === $wp_scripts->registered[ $handle ]->ver ) {
1195 $ver = '';
1196 } else {
1197 $ver = $wp_scripts->registered[ $handle ]->ver ? $wp_scripts->registered[ $handle ]->ver : $wp_scripts->default_version;
1198 }
1199
1200 if ( isset( $wp_scripts->args[ $handle ] ) ) {
1201 $ver = $ver ? $ver . '&amp;' . $wp_scripts->args[ $handle ] : $wp_scripts->args[ $handle ];
1202 }
1203
1204 // Full script source with version info
1205 $script_data['src'] = add_query_arg( 'ver', $ver, $src );
1206
1207 // Add script to data that will be returned to IS JS
1208 array_push( $results['scripts'], $script_data );
1209 }
1210 }
1211 }
1212
1213 // Expose additional script data to filters, but only include in final `$results` array if needed.
1214 if ( ! isset( $results['scripts'] ) ) {
1215 $results['scripts'] = array();
1216 }
1217
1218 /**
1219 * Filter the additional scripts required by the latest set of IS posts.
1220 *
1221 * @module infinite-scroll
1222 *
1223 * @since 2.1.2
1224 *
1225 * @param array $results['scripts'] Additional scripts required by the latest set of IS posts.
1226 * @param array|bool $initial_scripts Set of scripts loaded on each page.
1227 * @param array $results Array of Infinite Scroll results.
1228 * @param array $query_args Array of Query arguments.
1229 * @param WP_Query $wp_query WP Query.
1230 */
1231 $results['scripts'] = apply_filters(
1232 'infinite_scroll_additional_scripts',
1233 $results['scripts'],
1234 $initial_scripts,
1235 $results,
1236 $query_args,
1237 $wp_query
1238 );
1239
1240 if ( empty( $results['scripts'] ) ) {
1241 unset( $results['scripts'] );
1242 }
1243
1244 // Parse and sanitize the style handles already output
1245 $initial_styles = isset( $_REQUEST['styles'] ) && is_array( $_REQUEST['styles'] ) ? array_map( 'sanitize_text_field', wp_unslash( $_REQUEST['styles'] ) ) : false; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
1246
1247 if ( is_array( $initial_styles ) ) {
1248 global $wp_styles;
1249
1250 // Identify new styles needed by the latest set of IS posts
1251 $new_styles = array_diff( $wp_styles->done, $initial_styles );
1252
1253 // If new styles are needed, extract relevant data from $wp_styles
1254 if ( ! empty( $new_styles ) ) {
1255 $results['styles'] = array();
1256
1257 foreach ( $new_styles as $handle ) {
1258 // Abort if somehow the handle doesn't correspond to a registered stylesheet
1259 if ( ! isset( $wp_styles->registered[ $handle ] ) ) {
1260 continue;
1261 }
1262
1263 // Provide basic style data
1264 $style_data = array(
1265 'handle' => $handle,
1266 'media' => 'all',
1267 );
1268
1269 // Base source
1270 $src = $wp_styles->registered[ $handle ]->src;
1271
1272 // Take base_url into account
1273 if ( strpos( $src, 'http' ) !== 0 ) {
1274 $src = $wp_styles->base_url . $src;
1275 }
1276
1277 // Version and additional arguments
1278 if ( null === $wp_styles->registered[ $handle ]->ver ) {
1279 $ver = '';
1280 } else {
1281 $ver = $wp_styles->registered[ $handle ]->ver ? $wp_styles->registered[ $handle ]->ver : $wp_styles->default_version;
1282 }
1283
1284 if ( isset( $wp_styles->args[ $handle ] ) ) {
1285 $ver = $ver ? $ver . '&amp;' . $wp_styles->args[ $handle ] : $wp_styles->args[ $handle ];
1286 }
1287
1288 // Full stylesheet source with version info
1289 $style_data['src'] = add_query_arg( 'ver', $ver, $src );
1290
1291 // Parse stylesheet's conditional comments if present, converting to logic executable in JS
1292 if ( isset( $wp_styles->registered[ $handle ]->extra['conditional'] ) && $wp_styles->registered[ $handle ]->extra['conditional'] ) {
1293 // First, convert conditional comment operators to standard logical operators. %ver is replaced in JS with the IE version
1294 $style_data['conditional'] = str_replace(
1295 array(
1296 'lte',
1297 'lt',
1298 'gte',
1299 'gt',
1300 ),
1301 array(
1302 '%ver <=',
1303 '%ver <',
1304 '%ver >=',
1305 '%ver >',
1306 ),
1307 $wp_styles->registered[ $handle ]->extra['conditional']
1308 );
1309
1310 // 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().
1311 $style_data['conditional'] = preg_replace( '#!\s*IE(\s*\d+){0}#i', '1==2', $style_data['conditional'] );
1312
1313 // Lastly, remove the IE strings
1314 $style_data['conditional'] = str_replace( 'IE', '', $style_data['conditional'] );
1315 }
1316
1317 // Parse requested media context for stylesheet
1318 if ( isset( $wp_styles->registered[ $handle ]->args ) ) {
1319 $style_data['media'] = esc_attr( $wp_styles->registered[ $handle ]->args );
1320 }
1321
1322 // Add stylesheet to data that will be returned to IS JS
1323 array_push( $results['styles'], $style_data );
1324 }
1325 }
1326 }
1327
1328 // Expose additional stylesheet data to filters, but only include in final `$results` array if needed.
1329 if ( ! isset( $results['styles'] ) ) {
1330 $results['styles'] = array();
1331 }
1332
1333 /**
1334 * Filter the additional styles required by the latest set of IS posts.
1335 *
1336 * @module infinite-scroll
1337 *
1338 * @since 2.1.2
1339 *
1340 * @param array $results['styles'] Additional styles required by the latest set of IS posts.
1341 * @param array|bool $initial_styles Set of styles loaded on each page.
1342 * @param array $results Array of Infinite Scroll results.
1343 * @param array $query_args Array of Query arguments.
1344 * @param WP_Query $wp_query WP Query.
1345 */
1346 $results['styles'] = apply_filters(
1347 'infinite_scroll_additional_stylesheets',
1348 $results['styles'],
1349 $initial_styles,
1350 $results,
1351 $query_args,
1352 $wp_query
1353 );
1354
1355 if ( empty( $results['styles'] ) ) {
1356 unset( $results['styles'] );
1357 }
1358
1359 // Lastly, return the IS results array
1360 return $results;
1361 }
1362
1363 /**
1364 * Runs the query and returns the results via JSON.
1365 * Triggered by an AJAX request.
1366 *
1367 * @global $wp_query
1368 * @global $wp_the_query
1369 * @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
1370 */
1371 public function query() {
1372 if ( ! isset( $_REQUEST['page'] ) || ! current_theme_supports( 'infinite-scroll' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes to the site.
1373 die;
1374 }
1375
1376 // @todo see if we should validate this nonce since we use it to form a query.
1377 $page = (int) $_REQUEST['page']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- we're casting this to an int and not making changes to the site.
1378
1379 // Sanitize and set $previousday. Expected format: dd.mm.yy
1380 if ( isset( $_REQUEST['currentday'] ) && preg_match( '/^\d{2}\.\d{2}\.\d{2}$/', $_REQUEST['currentday'] ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput, WordPress.Security.NonceVerification.Recommended -- manually validating, no changes to site
1381 global $previousday;
1382 $previousday = $_REQUEST['currentday']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
1383 }
1384
1385 $post_status = array( 'publish' );
1386 if ( current_user_can( 'read_private_posts' ) ) {
1387 array_push( $post_status, 'private' );
1388 }
1389
1390 $order = isset( $_REQUEST['order'] ) && in_array( $_REQUEST['order'], array( 'ASC', 'DESC' ), true ) ? sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) : 'DESC'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes made to the site.
1391
1392 $query_args = array_merge(
1393 self::wp_query()->query_vars,
1394 array(
1395 'paged' => $page,
1396 'post_status' => $post_status,
1397 'posts_per_page' => self::posts_per_page(), // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
1398 'order' => $order,
1399 )
1400 );
1401
1402 // 4.0 ?s= compatibility, see https://core.trac.wordpress.org/ticket/11330#comment:50
1403 if ( empty( $query_args['s'] ) && ! isset( self::wp_query()->query['s'] ) ) {
1404 unset( $query_args['s'] );
1405 }
1406
1407 // By default, don't query for a specific page of a paged post object.
1408 // This argument can come from merging self::wp_query() into $query_args above.
1409 // Since IS is only used on archives, we should always display the first page of any paged content.
1410 unset( $query_args['page'] );
1411
1412 /**
1413 * Filter the array of main query arguments.
1414 *
1415 * @module infinite-scroll
1416 *
1417 * @since 2.0.1
1418 *
1419 * @param array $query_args Array of Query arguments.
1420 */
1421 $query_args = apply_filters( 'infinite_scroll_query_args', $query_args );
1422
1423 add_filter( 'posts_where', array( $this, 'query_time_filter' ), 10, 2 );
1424
1425 $infinite_scroll_query = new WP_Query();
1426 $GLOBALS['wp_the_query'] = $infinite_scroll_query;
1427 $GLOBALS['wp_query'] = $infinite_scroll_query;
1428
1429 $infinite_scroll_query->query( $query_args );
1430
1431 remove_filter( 'posts_where', array( $this, 'query_time_filter' ), 10, 2 );
1432
1433 $results = array();
1434
1435 if ( have_posts() ) {
1436 // Fire wp_head to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
1437 ob_start();
1438 wp_head();
1439 while ( ob_get_length() ) {
1440 ob_end_clean();
1441 }
1442
1443 $results['type'] = 'success';
1444
1445 /**
1446 * Fires when rendering Infinite Scroll posts.
1447 *
1448 * @module infinite-scroll
1449 *
1450 * @since 2.0.0
1451 */
1452 do_action( 'infinite_scroll_render' );
1453 $results['html'] = ob_get_clean();
1454 if ( empty( $results['html'] ) ) {
1455 /**
1456 * 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.
1457 *
1458 * @module infinite-scroll
1459 *
1460 * @since 6.0.0
1461 */
1462 $callbacks = apply_filters(
1463 'infinite_scroll_render_callbacks',
1464 array( self::get_settings()->render ) // This is the setting callback e.g. from add theme support.
1465 );
1466
1467 // Append fallback callback. That rhymes.
1468 $callbacks[] = array( $this, 'render' );
1469
1470 foreach ( $callbacks as $callback ) {
1471 if ( false !== $callback && is_callable( $callback ) ) {
1472 rewind_posts();
1473 ob_start();
1474 add_action( 'infinite_scroll_render', $callback );
1475
1476 /**
1477 * This action is already documented above.
1478 * See https://github.com/Automattic/jetpack/pull/16317/
1479 * for more details as to why it was introduced.
1480 */
1481 do_action( 'infinite_scroll_render' );
1482
1483 $results['html'] = ob_get_clean();
1484 remove_action( 'infinite_scroll_render', $callback );
1485 }
1486 if ( ! empty( $results['html'] ) ) {
1487 break;
1488 }
1489 }
1490 }
1491
1492 // If primary and fallback rendering methods fail, prevent further IS rendering attempts. Otherwise, wrap the output if requested.
1493 if ( empty( $results['html'] ) ) {
1494 unset( $results['html'] );
1495 /**
1496 * Fires when Infinite Scoll doesn't render any posts.
1497 *
1498 * @module infinite-scroll
1499 *
1500 * @since 2.0.0
1501 */
1502 do_action( 'infinite_scroll_empty' );
1503 $results['type'] = 'empty';
1504 } elseif ( $this->has_wrapper() ) {
1505 $wrapper_classes = is_string( self::get_settings()->wrapper ) ? self::get_settings()->wrapper : 'infinite-wrap';
1506 $wrapper_classes .= ' infinite-view-' . $page;
1507 $wrapper_classes = trim( $wrapper_classes );
1508 $aria_label = sprintf(
1509 /* translators: %1$s is the page count */
1510 __( 'Page: %1$d.', 'jetpack' ),
1511 $page
1512 );
1513
1514 $results['html'] = '<div class="' . esc_attr( $wrapper_classes ) . '" id="infinite-view-' . $page . '" data-page-num="' . $page . '" role="region" aria-label="' . esc_attr( $aria_label ) . '">' . $results['html'] . '</div>';
1515 }
1516
1517 // Fire wp_footer to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
1518 ob_start();
1519 wp_footer();
1520 while ( ob_get_length() ) {
1521 ob_end_clean();
1522 }
1523
1524 if ( 'success' === $results['type'] ) {
1525 global $currentday;
1526 $results['lastbatch'] = self::is_last_batch();
1527 $results['currentday'] = $currentday;
1528 }
1529
1530 // Loop through posts to capture sharing data for new posts loaded via Infinite Scroll
1531 if ( 'success' === $results['type'] && function_exists( 'sharing_register_post_for_share_counts' ) ) {
1532 global $jetpack_sharing_counts;
1533
1534 while ( have_posts() ) {
1535 the_post();
1536
1537 sharing_register_post_for_share_counts( get_the_ID() );
1538 }
1539
1540 $results['postflair'] = array_flip( $jetpack_sharing_counts );
1541 }
1542 } else {
1543 /** This action is already documented in modules/infinite-scroll/infinity.php */
1544 do_action( 'infinite_scroll_empty' );
1545 $results['type'] = 'empty';
1546 }
1547
1548 wp_send_json(
1549 /**
1550 * Filter the Infinite Scroll results.
1551 *
1552 * @module infinite-scroll
1553 *
1554 * @since 2.0.0
1555 *
1556 * @param array $results Array of Infinite Scroll results.
1557 * @param array $query_args Array of main query arguments.
1558 * @param WP_Query $wp_query WP Query.
1559 */
1560 apply_filters( 'infinite_scroll_results', $results, $query_args, self::wp_query() )
1561 );
1562 }
1563
1564 /**
1565 * Update the $allowed_vars array with the standard WP public and private
1566 * query vars, as well as taxonomy vars
1567 *
1568 * @global $wp
1569 * @param array $allowed_vars - the allowed variables array.
1570 * @filter infinite_scroll_allowed_vars
1571 * @return array
1572 */
1573 public function allowed_query_vars( $allowed_vars ) {
1574 global $wp;
1575
1576 $allowed_vars += $wp->public_query_vars;
1577 $allowed_vars += $wp->private_query_vars;
1578 $allowed_vars += $this->get_taxonomy_vars();
1579
1580 foreach ( array_keys( $allowed_vars, 'paged', true ) as $key ) {
1581 unset( $allowed_vars[ $key ] );
1582 }
1583
1584 return array_unique( $allowed_vars );
1585 }
1586
1587 /**
1588 * Returns an array of stock and custom taxonomy query vars
1589 *
1590 * @global $wp_taxonomies
1591 * @return array
1592 */
1593 public function get_taxonomy_vars() {
1594 global $wp_taxonomies;
1595
1596 $taxonomy_vars = array();
1597 foreach ( $wp_taxonomies as $t ) {
1598 if ( $t->query_var ) {
1599 $taxonomy_vars[] = $t->query_var;
1600 }
1601 }
1602
1603 // still needed?
1604 $taxonomy_vars[] = 'tag_id';
1605
1606 return $taxonomy_vars;
1607 }
1608
1609 /**
1610 * Update the $query_args array with the parameters provided via AJAX/GET.
1611 *
1612 * @param array $query_args - the query args.
1613 * @filter infinite_scroll_query_args
1614 * @return array
1615 */
1616 public function inject_query_args( $query_args ) {
1617 /**
1618 * Filter the array of allowed Infinite Scroll query arguments.
1619 *
1620 * @module infinite-scroll
1621 *
1622 * @since 2.6.0
1623 *
1624 * @param array $args Array of allowed Infinite Scroll query arguments.
1625 * @param array $query_args Array of query arguments.
1626 */
1627 $allowed_vars = apply_filters( 'infinite_scroll_allowed_vars', array(), $query_args );
1628
1629 $query_args = array_merge(
1630 $query_args,
1631 array(
1632 'suppress_filters' => false,
1633 )
1634 );
1635
1636 if ( isset( $_REQUEST['query_args'] ) && is_array( $_REQUEST['query_args'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
1637 foreach ( wp_unslash( $_REQUEST['query_args'] ) as $var => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- no site changes, sanitized below.
1638 if ( in_array( $var, $allowed_vars, true ) && ! empty( $value ) ) {
1639 $query_args[ $var ] = filter_var( $value );
1640 }
1641 }
1642 }
1643
1644 return $query_args;
1645 }
1646
1647 /**
1648 * Rendering fallback used when themes don't specify their own handler.
1649 *
1650 * @uses have_posts, the_post, get_template_part, get_post_format
1651 * @action infinite_scroll_render
1652 */
1653 public function render() {
1654 while ( have_posts() ) {
1655 the_post();
1656
1657 get_template_part( 'content', get_post_format() );
1658 }
1659 }
1660
1661 /**
1662 * Allow plugins to filter what archives Infinite Scroll supports
1663 *
1664 * @uses current_theme_supports, is_home, is_archive, apply_filters, self::get_settings
1665 * @return bool
1666 */
1667 public static function archive_supports_infinity() {
1668 $supported = current_theme_supports( 'infinite-scroll' ) && ( is_home() || is_archive() || is_search() );
1669
1670 // Disable when previewing a non-active theme in the customizer
1671 if ( is_customize_preview() && ! $GLOBALS['wp_customize']->is_theme_active() ) {
1672 return false;
1673 }
1674
1675 /**
1676 * Allow plugins to filter what archives Infinite Scroll supports.
1677 *
1678 * @module infinite-scroll
1679 *
1680 * @since 2.0.0
1681 *
1682 * @param bool $supported Does the Archive page support Infinite Scroll.
1683 * @param object self::get_settings() IS settings provided by theme.
1684 */
1685 return (bool) apply_filters( 'infinite_scroll_archive_supported', $supported, self::get_settings() );
1686 }
1687
1688 /**
1689 * The Infinite Blog Footer
1690 *
1691 * @uses self::get_settings, self::archive_supports_infinity, self::default_footer
1692 * @return string or null
1693 */
1694 public function footer() {
1695 if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
1696 return;
1697 }
1698
1699 // Bail if theme requested footer not show
1700 if ( false === self::get_settings()->footer ) {
1701 return;
1702 }
1703
1704 // We only need the new footer for the 'scroll' type
1705 if ( 'scroll' !== self::get_settings()->type || ! self::archive_supports_infinity() ) {
1706 return;
1707 }
1708
1709 if ( self::is_last_batch() ) {
1710 return;
1711 }
1712
1713 // Display a footer, either user-specified or a default
1714 if ( false !== self::get_settings()->footer_callback && is_callable( self::get_settings()->footer_callback ) ) {
1715 call_user_func( self::get_settings()->footer_callback, self::get_settings() );
1716 } else {
1717 self::default_footer();
1718 }
1719 }
1720
1721 /**
1722 * Render default IS footer
1723 *
1724 * @uses __, wp_get_theme, apply_filters, home_url, esc_attr, get_bloginfo, bloginfo
1725 */
1726 private function default_footer() {
1727 if ( '' !== get_privacy_policy_url() ) {
1728 $credits = get_the_privacy_policy_link() . '<span role="separator" aria-hidden="true"> / </span>';
1729 } else {
1730 $credits = '';
1731 }
1732 $credits .= sprintf(
1733 '<a href="https://wordpress.org/" rel="noopener noreferrer" target="_blank" rel="generator">%1$s</a> ',
1734 __( 'Proudly powered by WordPress', 'jetpack' )
1735 );
1736 $credits .= sprintf(
1737 /* translators: %1$s is the name of a theme */
1738 __( 'Theme: %1$s.', 'jetpack' ),
1739 wp_get_theme()->Name
1740 );
1741 /**
1742 * Filter Infinite Scroll's credit text.
1743 *
1744 * @module infinite-scroll
1745 *
1746 * @since 2.0.0
1747 *
1748 * @param string $credits Infinite Scroll credits.
1749 */
1750 $credits = apply_filters( 'infinite_scroll_credit', $credits );
1751
1752 ?>
1753 <div id="infinite-footer">
1754 <div class="container">
1755 <div class="blog-info">
1756 <a id="infinity-blog-title" href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
1757 <?php bloginfo( 'name' ); ?>
1758 </a>
1759 </div>
1760 <div class="blog-credits">
1761 <?php echo $credits; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1762 </div>
1763 </div>
1764 </div><!-- #infinite-footer -->
1765 <?php
1766 }
1767
1768 /**
1769 * Ensure that IS doesn't interfere with Grunion by stripping IS query arguments from the Grunion redirect URL.
1770 * When arguments are present, Grunion redirects to the IS AJAX endpoint.
1771 *
1772 * @param string $url - the Grunion redirect URL.
1773 * @uses remove_query_arg
1774 * @filter grunion_contact_form_redirect_url
1775 * @return string
1776 */
1777 public function filter_grunion_redirect_url( $url ) {
1778 // Remove IS query args, if present
1779 if ( false !== strpos( $url, 'infinity=scrolling' ) ) {
1780 $url = remove_query_arg(
1781 array(
1782 'infinity',
1783 'action',
1784 'page',
1785 'order',
1786 'scripts',
1787 'styles',
1788 ),
1789 $url
1790 );
1791 }
1792
1793 return $url;
1794 }
1795
1796 /**
1797 * When the MediaElement is loaded in dynamically, we need to enforce that
1798 * its settings are added to the page as well.
1799 *
1800 * @param array $scripts_data New scripts exposed to the infinite scroll.
1801 *
1802 * @since 8.4.0
1803 */
1804 public function add_mejs_config( $scripts_data ) {
1805 foreach ( $scripts_data as $key => $data ) {
1806 if ( 'mediaelement-core' === $data['handle'] ) {
1807 $mejs_settings = array(
1808 'pluginPath' => includes_url( 'js/mediaelement/', 'relative' ),
1809 'classPrefix' => 'mejs-',
1810 'stretching' => 'responsive',
1811 );
1812
1813 $scripts_data[ $key ]['extra_data'] = sprintf(
1814 'window.%s = %s',
1815 '_wpmejsSettings',
1816 wp_json_encode( apply_filters( 'mejs_settings', $mejs_settings ) )
1817 );
1818 }
1819 }
1820 return $scripts_data;
1821 }
1822
1823 /**
1824 * Determines whether the legacy AMP Reader post templates are being used.
1825 *
1826 * @return bool
1827 */
1828 private function is_exempted_amp_page() {
1829 if ( is_singular( 'web-story' ) ) {
1830 // Ensure that <amp-next-page> is not injected after <amp-story> as generated by the Web Stories plugin.
1831 return true;
1832 }
1833 if ( function_exists( 'amp_is_legacy' ) ) {
1834 // Available since AMP v2.0, this will return false if a theme like Twenty Twenty is selected as the Reader theme.
1835 return amp_is_legacy();
1836 }
1837 if ( method_exists( 'AMP_Options_Manager', 'get_option' ) ) {
1838 // In versions prior to v2.0, checking the template mode as being 'reader' is sufficient.
1839 return 'reader' === AMP_Options_Manager::get_option( 'theme_support' );
1840 }
1841 return false;
1842 }
1843
1844 /**
1845 * Load AMP specific hooks.
1846 *
1847 * @return void
1848 */
1849 public function amp_load_hooks() {
1850 if ( $this->is_exempted_amp_page() ) {
1851 return;
1852 }
1853
1854 if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
1855 $template = self::get_settings()->render;
1856
1857 add_filter( 'jetpack_infinite_scroll_load_scripts_and_styles', '__return_false' );
1858
1859 add_action( 'template_redirect', array( $this, 'amp_start_output_buffering' ), 0 );
1860 add_action( 'shutdown', array( $this, 'amp_output_buffer' ), 1 );
1861
1862 if ( is_string( $template ) && strpos( $template, '::' ) === false && is_callable( "amp_{$template}_hooks" ) ) {
1863 call_user_func( "amp_{$template}_hooks" );
1864 }
1865
1866 // Warms up the amp next page markup.
1867 // This should be done outside the output buffering callback started in the template_redirect.
1868 $this->amp_get_footer_template();
1869 }
1870 }
1871
1872 /**
1873 * Start the AMP output buffering.
1874 *
1875 * @return void
1876 */
1877 public function amp_start_output_buffering() {
1878 ob_start( array( $this, 'amp_finish_output_buffering' ) );
1879 }
1880
1881 /**
1882 * Flush the AMP output buffer.
1883 *
1884 * @return void
1885 */
1886 public function amp_output_buffer() {
1887 if ( ob_get_contents() ) {
1888 ob_end_flush();
1889 }
1890 }
1891
1892 /**
1893 * Filter the AMP output buffer contents.
1894 *
1895 * @param string $buffer Contents of the output buffer.
1896 *
1897 * @return string|false
1898 */
1899 public function amp_finish_output_buffering( $buffer ) {
1900 // Hide WordPress admin bar on next page load.
1901 $buffer = preg_replace(
1902 '/id="wpadminbar"/',
1903 '$0 next-page-hide',
1904 $buffer
1905 );
1906
1907 /**
1908 * Get the theme footers.
1909 *
1910 * @module infinite-scroll
1911 *
1912 * @since 9.0.0
1913 *
1914 * @param array array() An array to store multiple markup entries to be added to the footer.
1915 * @param string $buffer The contents of the output buffer.
1916 */
1917 $footers = apply_filters( 'jetpack_amp_infinite_footers', array(), $buffer );
1918
1919 /**
1920 * Filter the output buffer.
1921 * Themes can leverage this hook to add custom markup on next page load.
1922 *
1923 * @module infinite-scroll
1924 *
1925 * @since 9.0.0
1926 *
1927 * @param string $buffer The contents of the output buffer.
1928 */
1929 $buffer = apply_filters( 'jetpack_amp_infinite_output', $buffer );
1930
1931 // Add the amp next page markup.
1932 $buffer = preg_replace(
1933 '~</body>~',
1934 $this->amp_get_footer_template( $footers ) . '$0',
1935 $buffer
1936 );
1937
1938 return $buffer;
1939 }
1940
1941 /**
1942 * Get AMP next page markup with the custom footers.
1943 *
1944 * @param string[] $footers The theme footers.
1945 *
1946 * @return string
1947 */
1948 protected function amp_get_footer_template( $footers = array() ) {
1949 static $template = null;
1950
1951 if ( null === $template ) {
1952 $template = $this->amp_footer_template();
1953 }
1954
1955 if ( empty( $footers ) ) {
1956 return $template;
1957 }
1958
1959 return preg_replace(
1960 '/%%footer%%/',
1961 implode( '', $footers ),
1962 $template
1963 );
1964 }
1965
1966 /**
1967 * AMP Next Page markup.
1968 *
1969 * @return string
1970 */
1971 protected function amp_footer_template() {
1972 ob_start();
1973 ?>
1974 <amp-next-page max-pages="<?php echo esc_attr( static::amp_get_max_pages() ); ?>">
1975 <script type="application/json">
1976 [
1977 <?php echo wp_json_encode( $this->amp_next_page() ); ?>
1978 ]
1979 </script>
1980 <div separator>
1981 <?php
1982 echo wp_kses_post(
1983 /**
1984 * AMP infinite scroll separator.
1985 *
1986 * @module infinite-scroll
1987 *
1988 * @since 9.0.0
1989 *
1990 * @param string '' The markup for the next page separator.
1991 */
1992 apply_filters( 'jetpack_amp_infinite_separator', '' )
1993 );
1994 ?>
1995 </div>
1996 <div recommendation-box class="recommendation-box">
1997 <template type="amp-mustache">
1998 {{#pages}}
1999 <?php
2000 echo wp_kses_post(
2001 /**
2002 * AMP infinite scroll older posts markup.
2003 *
2004 * @module infinite-scroll
2005 *
2006 * @since 9.0.0
2007 *
2008 * @param string '' The markup for the older posts/next page.
2009 */
2010 apply_filters( 'jetpack_amp_infinite_older_posts', '' )
2011 );
2012 ?>
2013 {{/pages}}
2014 </template>
2015 </div>
2016 <div footer>
2017 %%footer%%
2018 </div>
2019 </amp-next-page>
2020 <?php
2021 return ob_get_clean();
2022 }
2023
2024 /**
2025 * Get the AMP next page information.
2026 *
2027 * @return array
2028 */
2029 protected function amp_next_page() {
2030 $title = '';
2031 $url = '';
2032 $image = '';
2033
2034 if ( ! static::amp_is_last_page() ) {
2035 $title = sprintf(
2036 '%s - %s %d - %s',
2037 wp_title( '', false ),
2038 __( 'Page', 'jetpack' ),
2039 max( get_query_var( 'paged', 1 ), 1 ) + 1,
2040 get_bloginfo( 'name' )
2041 );
2042 $url = get_next_posts_page_link();
2043 }
2044
2045 $next_page = array(
2046 'title' => $title,
2047 'url' => $url,
2048 'image' => $image,
2049 );
2050
2051 /**
2052 * The next page settings.
2053 * An array containing:
2054 * - title => The title to be featured on the browser tab.
2055 * - url => The URL of next page.
2056 * - image => The image URL. A required AMP setting, not in use currently. Themes are welcome to leverage.
2057 *
2058 * @module infinite-scroll
2059 *
2060 * @since 9.0.0
2061 *
2062 * @param array $next_page The contents of the output buffer.
2063 */
2064 return apply_filters( 'jetpack_amp_infinite_next_page_data', $next_page );
2065 }
2066
2067 /**
2068 * Get the number of pages left.
2069 *
2070 * @return int
2071 */
2072 protected static function amp_get_max_pages() {
2073 global $wp_query;
2074
2075 return (int) $wp_query->max_num_pages - $wp_query->query_vars['paged'];
2076 }
2077
2078 /**
2079 * Is the last page.
2080 *
2081 * @return bool
2082 */
2083 protected static function amp_is_last_page() {
2084 return 0 === static::amp_get_max_pages();
2085 }
2086 }
2087
2088 /**
2089 * Initialize The_Neverending_Home_Page
2090 */
2091 function the_neverending_home_page_init() {
2092 if ( ! current_theme_supports( 'infinite-scroll' ) ) {
2093 return;
2094 }
2095
2096 new The_Neverending_Home_Page();
2097 }
2098 add_action( 'init', 'the_neverending_home_page_init', 20 );
2099
2100 /**
2101 * Check whether the current theme is infinite-scroll aware.
2102 * If so, include the files which add theme support.
2103 */
2104 function the_neverending_home_page_theme_support() {
2105 if (
2106 defined( 'IS_WPCOM' ) && IS_WPCOM &&
2107 defined( 'REST_API_REQUEST' ) && REST_API_REQUEST &&
2108 ! doing_action( 'restapi_theme_after_setup_theme' )
2109 ) {
2110 // Don't source theme compat files until we're in the site's context
2111 return;
2112 }
2113 $theme_name = get_stylesheet();
2114
2115 /**
2116 * Filter the path to the Infinite Scroll compatibility file.
2117 *
2118 * @module infinite-scroll
2119 *
2120 * @since 2.0.0
2121 *
2122 * @param string $str IS compatibility file path.
2123 * @param string $theme_name Theme name.
2124 */
2125 $customization_file = apply_filters( 'infinite_scroll_customization_file', __DIR__ . "/themes/{$theme_name}.php", $theme_name );
2126
2127 if ( is_readable( $customization_file ) ) {
2128 require_once $customization_file;
2129 }
2130 }
2131 add_action( 'after_setup_theme', 'the_neverending_home_page_theme_support', 5 );
2132
2133 /**
2134 * Early accommodation of the Infinite Scroll AJAX request
2135 */
2136 if ( The_Neverending_Home_Page::got_infinity() ) {
2137 /**
2138 * If we're sure this is an AJAX request (i.e. the HTTP_X_REQUESTED_WITH header says so),
2139 * indicate it as early as possible for actions like init
2140 */
2141 if ( ! defined( 'DOING_AJAX' ) &&
2142 isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) &&
2143 strtoupper( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REQUESTED_WITH'] ) ) ) === 'XMLHTTPREQUEST'
2144 ) {
2145 define( 'DOING_AJAX', true );
2146 }
2147
2148 // Don't load the admin bar when doing the AJAX response.
2149 show_admin_bar( false );
2150 }
2151