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