PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 14.9.2
Jetpack – WP Security, Backup, Speed, & Growth v14.9.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 1 year ago infinity-customizer.js 1 year ago infinity.css 1 year ago infinity.js 1 year ago infinity.php 1 year ago
infinity.php
2153 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, 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
578 $orderby = isset( self::wp_query()->query_vars['orderby'] ) ? self::wp_query()->query_vars['orderby'] : '';
579 $post_date = ( ! empty( $post->post_date ) ? $post->post_date : false );
580 if ( 'modified' === $orderby || false === $post_date ) {
581 $post_date = $post->post_modified;
582 }
583
584 // in case all posts initially displayed match the keyword by title we add em all to excluded posts array
585 // 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
586 if ( self::has_only_title_matching_posts() || $post_date <= self::get_last_post_date() ) {
587 array_push( $excluded_posts, $post->ID );
588 }
589 }
590 return $excluded_posts;
591 }
592
593 /**
594 * 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
595 *
596 * @uses self::wp_query
597 * @uses self::get_excluded_posts
598 * @return array
599 */
600 public function get_query_vars() {
601
602 $query_vars = self::wp_query()->query_vars;
603 // applies to search page only
604 if ( true === self::wp_query()->is_search() ) {
605 // set post__not_in array in query_vars in case it does not exists
606 if ( false === isset( $query_vars['post__not_in'] ) ) {
607 $query_vars['post__not_in'] = array();
608 }
609 // get excluded posts
610 $excluded = self::get_excluded_posts();
611 // merge them with other post__not_in posts (eg.: sticky posts)
612 $query_vars['post__not_in'] = array_merge( $query_vars['post__not_in'], $excluded );
613 }
614 return $query_vars;
615 }
616
617 /**
618 * This function checks whether all posts returned by initial wp_query match the keyword by title
619 * The code used in this function is borrowed from WP_Query class where it is used to construct like conditions for keywords
620 *
621 * @uses self::wp_query
622 * @return bool
623 */
624 public function has_only_title_matching_posts() {
625
626 // apply following logic for search page results only
627 if ( false === self::wp_query()->is_search() ) {
628 return false;
629 }
630
631 // grab the last posts in the stack as if the last one is title-matching the rest is title-matching as well
632 $post = end( self::wp_query()->posts );
633
634 // code inspired by WP_Query class
635 if ( preg_match_all( '/".*?("|$)|((?<=[\t ",+])|^)[^\t ",+]+/', self::wp_query()->get( 's' ), $matches ) ) {
636 $search_terms = self::wp_query()->query_vars['search_terms'];
637 // if the search string has only short terms or stopwords, or is 10+ terms long, match it as sentence
638 if ( empty( $search_terms ) || ! is_countable( $search_terms ) || count( $search_terms ) > 9 ) {
639 $search_terms = array( self::wp_query()->get( 's' ) );
640 }
641 } else {
642 $search_terms = array( self::wp_query()->get( 's' ) );
643 }
644
645 // 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
646 $term = current( $search_terms );
647 if ( ! empty( $term ) && str_contains( $post->post_title, $term ) ) {
648 return true;
649 }
650
651 return false;
652 }
653
654 /**
655 * Grab the timestamp for the initial query's last post.
656 *
657 * This takes into account the query's 'orderby' parameter and returns
658 * false if the posts are not ordered by date.
659 *
660 * @uses self::got_infinity
661 * @uses self::has_only_title_matching_posts
662 * @uses self::wp_query
663 * @return string 'Y-m-d H:i:s' or false
664 */
665 public function get_last_post_date() {
666 if ( self::got_infinity() ) {
667 return;
668 }
669
670 if ( ! self::wp_query()->have_posts() ) {
671 return null;
672 }
673
674 // 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
675 if ( true === self::has_only_title_matching_posts() ) {
676 return false;
677 }
678
679 $post = end( self::wp_query()->posts );
680 $orderby = isset( self::wp_query()->query_vars['orderby'] ) ?
681 self::wp_query()->query_vars['orderby'] : '';
682 $post_date = ( ! empty( $post->post_date ) ? $post->post_date : false );
683 switch ( $orderby ) {
684 case 'modified':
685 return $post->post_modified;
686 case 'date':
687 case '':
688 return $post_date;
689 default:
690 return false;
691 }
692 }
693
694 /**
695 * Returns the appropriate `wp_posts` table field for a given query's
696 * 'orderby' parameter, if applicable.
697 *
698 * @param object $query - an optional query object.
699 * @uses self::wp_query
700 * @return string or false
701 */
702 public function get_query_sort_field( $query = null ) {
703 if ( empty( $query ) ) {
704 $query = self::wp_query();
705 }
706
707 $orderby = isset( $query->query_vars['orderby'] ) ? $query->query_vars['orderby'] : '';
708
709 switch ( $orderby ) {
710 case 'modified':
711 return 'post_modified';
712 case 'date':
713 case '':
714 return 'post_date';
715 default:
716 return false;
717 }
718 }
719
720 /**
721 * Create a where clause that will make sure post queries return posts
722 * in the correct order, without duplicates, if a new post is added
723 * and we're sorting by post date.
724 *
725 * @global $wpdb
726 * @param string $where - the where clause.
727 * @param object $query - the query.
728 * @uses apply_filters
729 * @filter posts_where
730 * @return string
731 */
732 public function query_time_filter( $where, $query ) {
733 if ( self::got_infinity() ) {
734 global $wpdb;
735
736 $sort_field = self::get_query_sort_field( $query );
737
738 if ( 'post_date' !== $sort_field ||
739 ! isset( $_REQUEST['query_args']['order'] ) || 'DESC' !== $_REQUEST['query_args']['order'] ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes made to the site.
740 return $where;
741 }
742
743 $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.
744
745 if ( empty( $query_before ) ) {
746 return $where;
747 }
748
749 // Construct the date query using our timestamp
750 $clause = $wpdb->prepare( " AND {$wpdb->posts}.post_date <= %s", $query_before );
751
752 /**
753 * Filter Infinite Scroll's SQL date query making sure post queries
754 * will always return results prior to (descending sort)
755 * or before (ascending sort) the last post date.
756 *
757 * @deprecated 14.0
758 *
759 * @module infinite-scroll
760 *
761 * @param string $clause SQL Date query.
762 * @param object $query Query.
763 * @param string $operator @deprecated Query operator.
764 * @param string $last_post_date @deprecated Last Post Date timestamp.
765 */
766 $operator = '<';
767 $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
768 $where .= apply_filters_deprecated( 'infinite_scroll_posts_where', array( $clause, $query, $operator, $last_post_date ), '14.0', '' );
769 }
770
771 return $where;
772 }
773
774 /**
775 * Let's overwrite the default post_per_page setting to always display a fixed amount.
776 *
777 * @param object $query - the query.
778 * @uses is_admin, self::archive_supports_infinity, self::get_settings
779 */
780 public function posts_per_page_query( $query ) {
781 if ( ! is_admin() && self::archive_supports_infinity() && $query->is_main_query() ) {
782 $query->set( 'posts_per_page', self::posts_per_page() );
783 }
784 }
785
786 /**
787 * Check if the IS output should be wrapped in a div.
788 * Setting value can be a boolean or a string specifying the class applied to the div.
789 *
790 * @uses self::get_settings
791 * @return bool
792 */
793 public function has_wrapper() {
794 return (bool) self::get_settings()->wrapper;
795 }
796
797 /**
798 * Returns the Ajax url
799 *
800 * @global $wp
801 * @uses home_url, add_query_arg, apply_filters
802 * @return string
803 */
804 public function ajax_url() {
805 $base_url = set_url_scheme( home_url( '/' ) );
806
807 $ajaxurl = add_query_arg( array( 'infinity' => 'scrolling' ), $base_url );
808
809 /**
810 * Filter the Infinite Scroll Ajax URL.
811 *
812 * @module infinite-scroll
813 *
814 * @since 2.0.0
815 *
816 * @param string $ajaxurl Infinite Scroll Ajax URL.
817 */
818 return apply_filters( 'infinite_scroll_ajax_url', $ajaxurl );
819 }
820
821 /**
822 * Our own Ajax response, avoiding calling admin-ajax
823 */
824 public function ajax_response() {
825 // Only proceed if the url query has a key of "Infinity"
826 if ( ! self::got_infinity() ) {
827 return false;
828 }
829
830 // This should already be defined below, but make sure.
831 if ( ! defined( 'DOING_AJAX' ) ) {
832 define( 'DOING_AJAX', true );
833 }
834
835 @header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
836 send_nosniff_header();
837
838 /**
839 * Fires at the end of the Infinite Scroll Ajax response.
840 *
841 * @module infinite-scroll
842 *
843 * @since 2.0.0
844 */
845 do_action( 'custom_ajax_infinite_scroll' );
846 die( '0' );
847 }
848
849 /**
850 * Alias for renamed class method.
851 *
852 * Previously, JS settings object was unnecessarily output in the document head.
853 * When the hook was changed, the method name no longer made sense.
854 */
855 public function action_wp_head() {
856 $this->action_wp_footer_settings();
857 }
858
859 /**
860 * Prints the relevant infinite scroll settings in JS.
861 *
862 * @global $wp_rewrite
863 * @uses self::get_settings, esc_js, esc_url_raw, self::has_wrapper, __, apply_filters, do_action, self::get_query_vars
864 * @action wp_footer
865 */
866 public function action_wp_footer_settings() {
867 global $wp_rewrite;
868 global $currentday;
869
870 $settings = self::get_settings();
871
872 // Default click handle text
873 $click_handle_text = __( 'Older posts', 'jetpack' );
874
875 // If a single CPT is displayed, use its plural name instead of "posts"
876 // Could be empty (posts) or an array of multiple post types.
877 // In the latter two cases cases, the default text is used, leaving the `infinite_scroll_js_settings` filter for further customization.
878 $post_type = self::wp_query()->get( 'post_type' );
879
880 // If it's a taxonomy, try to change the button text.
881 if ( is_tax() ) {
882 // Get current taxonomy slug.
883 $taxonomy_slug = self::wp_query()->get( 'taxonomy' );
884
885 // Get taxonomy settings.
886 $taxonomy = get_taxonomy( $taxonomy_slug );
887
888 // Check if the taxonomy is attached to one post type only and use its plural name.
889 // If not, use "Posts" without confusing the users.
890 if (
891 is_a( $taxonomy, 'WP_Taxonomy' )
892 && is_countable( $taxonomy->object_type )
893 && count( $taxonomy->object_type ) < 2
894 ) {
895 $post_type = $taxonomy->object_type[0];
896 }
897 }
898
899 if ( is_string( $post_type ) && ! empty( $post_type ) ) {
900 $post_type = get_post_type_object( $post_type );
901
902 if ( is_object( $post_type ) && ! is_wp_error( $post_type ) ) {
903 if ( isset( $post_type->labels->name ) ) {
904 $cpt_text = $post_type->labels->name;
905 } elseif ( isset( $post_type->label ) ) {
906 $cpt_text = $post_type->label;
907 }
908
909 if ( isset( $cpt_text ) ) {
910 /* translators: %s is the name of a custom post type */
911 $click_handle_text = sprintf( __( 'More %s', 'jetpack' ), $cpt_text );
912 unset( $cpt_text );
913 }
914 }
915 }
916
917 unset( $post_type );
918
919 // Base JS settings
920 $js_settings = array(
921 'id' => $settings->container,
922 'ajaxurl' => esc_url_raw( self::ajax_url() ),
923 'type' => esc_js( $settings->type ),
924 'wrapper' => self::has_wrapper(),
925 'wrapper_class' => is_string( $settings->wrapper ) ? esc_js( $settings->wrapper ) : 'infinite-wrap',
926 'footer' => is_string( $settings->footer ) ? esc_js( $settings->footer ) : $settings->footer,
927 'click_handle' => esc_js( $settings->click_handle ),
928 'text' => esc_js( $click_handle_text ),
929 'totop' => esc_js( __( 'Scroll back to top', 'jetpack' ) ),
930 'currentday' => $currentday,
931 'order' => 'DESC',
932 'scripts' => array(),
933 'styles' => array(),
934 'google_analytics' => false,
935 'offset' => max( 1, self::wp_query()->get( 'paged' ) ), // Pass through the current page so we can use that to offset the first load.
936 'history' => array(
937 'host' => preg_replace( '#^http(s)?://#i', '', untrailingslashit( esc_url( get_home_url() ) ) ),
938 'path' => self::get_request_path(),
939 'use_trailing_slashes' => $wp_rewrite->use_trailing_slashes,
940 'parameters' => self::get_request_parameters(),
941 ),
942 'query_args' => self::get_query_vars(),
943 'query_before' => current_time( 'mysql' ),
944 'last_post_date' => self::get_last_post_date(),
945 'body_class' => self::body_class(),
946 'loading_text' => esc_js( __( 'Loading new page', 'jetpack' ) ),
947 );
948
949 // Optional order param
950 if ( isset( $_REQUEST['order'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes made to the site.
951 $order = strtoupper( sanitize_text_field( wp_unslash( $_REQUEST['order'] ) ) ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes made to the site.
952
953 if ( in_array( $order, array( 'ASC', 'DESC' ), true ) ) {
954 $js_settings['order'] = $order;
955 }
956 }
957
958 /**
959 * Filter the Infinite Scroll JS settings outputted in the head.
960 *
961 * @module infinite-scroll
962 *
963 * @since 2.0.0
964 *
965 * @param array $js_settings Infinite Scroll JS settings.
966 */
967 $js_settings = apply_filters( 'infinite_scroll_js_settings', $js_settings );
968
969 /**
970 * Fires before Infinite Scroll outputs inline JavaScript in the head.
971 *
972 * @module infinite-scroll
973 *
974 * @since 2.0.0
975 */
976 do_action( 'infinite_scroll_wp_head' );
977
978 ?>
979 <script type="text/javascript">
980 var infiniteScroll = <?php echo wp_json_encode( array( 'settings' => $js_settings ), JSON_HEX_TAG ); ?>;
981 </script>
982 <?php
983 }
984
985 // phpcs:disable WordPress.WP.GlobalVariablesOverride.Prohibited
986
987 /**
988 * Build path data for current request.
989 * Used for Google Analytics and pushState history tracking.
990 *
991 * @global $wp_rewrite
992 * @global $wp
993 * @uses user_trailingslashit, sanitize_text_field, add_query_arg
994 * @return string|bool
995 */
996 private function get_request_path() {
997 global $wp_rewrite;
998
999 if ( $wp_rewrite->using_permalinks() ) {
1000 global $wp;
1001
1002 // If called too early, bail
1003 if ( ! isset( $wp->request ) ) {
1004 return false;
1005 }
1006
1007 // Determine path for paginated version of current request
1008 if ( preg_match( '#' . preg_quote( $wp_rewrite->pagination_base, '#' ) . '/\d+/?$#i', $wp->request ) ) {
1009 $path = preg_replace( '#' . preg_quote( $wp_rewrite->pagination_base, '#' ) . '/\d+$#i', $wp_rewrite->pagination_base . '/%d', $wp->request );
1010 } else {
1011 $path = $wp->request . '/' . $wp_rewrite->pagination_base . '/%d';
1012 }
1013
1014 // Slashes everywhere we need them
1015 if ( ! str_starts_with( $path, '/' ) ) {
1016 $path = '/' . $path;
1017 }
1018
1019 $path = user_trailingslashit( $path );
1020 } else {
1021 // Clean up raw $_REQUEST input
1022 $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.
1023 $path = array_filter( $path );
1024
1025 $path['paged'] = '%d';
1026
1027 $path = add_query_arg( $path, '/' );
1028 }
1029
1030 return empty( $path ) ? false : $path;
1031 }
1032
1033 /**
1034 * Return query string for current request, prefixed with '?'.
1035 *
1036 * @return string
1037 */
1038 private function get_request_parameters() {
1039 $uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
1040 $uri = preg_replace( '/^[^?]*(\?.*$)/', '$1', $uri, 1, $count );
1041 if ( $count !== 1 ) {
1042 return '';
1043 }
1044 return $uri;
1045 }
1046
1047 /**
1048 * Provide IS with a list of the scripts and stylesheets already present on the page.
1049 * Since posts may contain require additional assets that haven't been loaded, this data will be used to track the additional assets.
1050 *
1051 * @global $wp_scripts, $wp_styles
1052 * @action wp_footer
1053 */
1054 public function action_wp_footer() {
1055 global $wp_scripts, $wp_styles;
1056
1057 $scripts = is_a( $wp_scripts, 'WP_Scripts' ) ? $wp_scripts->done : array();
1058 /**
1059 * Filter the list of scripts already present on the page.
1060 *
1061 * @module infinite-scroll
1062 *
1063 * @since 2.1.2
1064 *
1065 * @param array $scripts Array of scripts present on the page.
1066 */
1067 $scripts = apply_filters( 'infinite_scroll_existing_scripts', $scripts );
1068
1069 $styles = is_a( $wp_styles, 'WP_Styles' ) ? $wp_styles->done : array();
1070 /**
1071 * Filter the list of styles already present on the page.
1072 *
1073 * @module infinite-scroll
1074 *
1075 * @since 2.1.2
1076 *
1077 * @param array $styles Array of styles present on the page.
1078 */
1079 $styles = apply_filters( 'infinite_scroll_existing_stylesheets', $styles );
1080
1081 ?>
1082 <script type="text/javascript">
1083 (function() {
1084 var extend = function(out) {
1085 out = out || {};
1086
1087 for (var i = 1; i < arguments.length; i++) {
1088 if (!arguments[i])
1089 continue;
1090
1091 for (var key in arguments[i]) {
1092 if (arguments[i].hasOwnProperty(key))
1093 out[key] = arguments[i][key];
1094 }
1095 }
1096
1097 return out;
1098 };
1099 extend( window.infiniteScroll.settings.scripts, <?php echo wp_json_encode( $scripts ); ?> );
1100 extend( window.infiniteScroll.settings.styles, <?php echo wp_json_encode( $styles ); ?> );
1101 })();
1102 </script>
1103 <?php
1104 $aria_live = 'assertive';
1105 if ( 'scroll' === self::get_settings()->type ) {
1106 $aria_live = 'polite';
1107 }
1108 ?>
1109 <span id="infinite-aria" aria-live="<?php echo esc_attr( $aria_live ); ?>"></span>
1110 <?php
1111 }
1112
1113 /**
1114 * Identify additional scripts required by the latest set of IS posts and provide the necessary data to the IS response handler.
1115 *
1116 * @param array $results - the results.
1117 * @param array $query_args - Array of Query arguments.
1118 * @param array $wp_query - the WP query.
1119 * @global $wp_scripts
1120 * @uses sanitize_text_field, add_query_arg
1121 * @filter infinite_scroll_results
1122 * @return array
1123 */
1124 public function filter_infinite_scroll_results( $results, $query_args, $wp_query ) {
1125 // Don't bother unless there are posts to display
1126 if ( 'success' !== $results['type'] ) {
1127 return $results;
1128 }
1129
1130 // Parse and sanitize the script handles already output
1131 $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.
1132
1133 if ( is_array( $initial_scripts ) ) {
1134 global $wp_scripts;
1135
1136 // Identify new scripts needed by the latest set of IS posts
1137 $new_scripts = array_filter(
1138 $wp_scripts->done,
1139 function ( $script_name ) use ( $initial_scripts ) {
1140 // Jetpack block scripts should always be sent, even if they've been
1141 // sent before. These scripts only run once on when loaded, they don't
1142 // watch for new blocks being added.
1143 if ( str_starts_with( $script_name, 'jetpack-block-' ) ) {
1144 return true;
1145 }
1146
1147 return ! in_array( $script_name, $initial_scripts, true );
1148 }
1149 );
1150
1151 // If new scripts are needed, extract relevant data from $wp_scripts
1152 if ( ! empty( $new_scripts ) ) {
1153 $results['scripts'] = array();
1154
1155 foreach ( $new_scripts as $handle ) {
1156 // Abort if somehow the handle doesn't correspond to a registered script
1157 // or if the script doesn't have `src` set.
1158 $script_not_registered = ! isset( $wp_scripts->registered[ $handle ] );
1159 $empty_src = empty( $wp_scripts->registered[ $handle ]->src );
1160 if ( $script_not_registered || $empty_src ) {
1161 continue;
1162 }
1163
1164 $before_handle = $wp_scripts->get_inline_script_data( $handle, 'before' );
1165 $after_handle = $wp_scripts->get_inline_script_data( $handle, 'after' );
1166
1167 // Provide basic script data
1168 $script_data = array(
1169 'handle' => $handle,
1170 'footer' => ( is_array( $wp_scripts->in_footer ) && in_array( $handle, $wp_scripts->in_footer, true ) ),
1171 'extra_data' => $wp_scripts->print_extra_script( $handle, false ),
1172 'before_handle' => $before_handle,
1173 'after_handle' => $after_handle,
1174 );
1175
1176 // Base source
1177 $src = $wp_scripts->registered[ $handle ]->src;
1178
1179 // Take base_url into account
1180 if ( strpos( $src, 'http' ) !== 0 ) {
1181 $src = $wp_scripts->base_url . $src;
1182 }
1183
1184 // Version and additional arguments
1185 if ( null === $wp_scripts->registered[ $handle ]->ver ) {
1186 $ver = '';
1187 } else {
1188 $ver = $wp_scripts->registered[ $handle ]->ver ? $wp_scripts->registered[ $handle ]->ver : $wp_scripts->default_version;
1189 }
1190
1191 if ( isset( $wp_scripts->args[ $handle ] ) ) {
1192 $ver = $ver ? $ver . '&amp;' . $wp_scripts->args[ $handle ] : $wp_scripts->args[ $handle ];
1193 }
1194
1195 // Full script source with version info
1196 $script_data['src'] = add_query_arg( 'ver', $ver, $src );
1197
1198 // Add script to data that will be returned to IS JS
1199 array_push( $results['scripts'], $script_data );
1200 }
1201 }
1202 }
1203
1204 // Expose additional script data to filters, but only include in final `$results` array if needed.
1205 if ( ! isset( $results['scripts'] ) ) {
1206 $results['scripts'] = array();
1207 }
1208
1209 /**
1210 * Filter the additional scripts required by the latest set of IS posts.
1211 *
1212 * @module infinite-scroll
1213 *
1214 * @since 2.1.2
1215 *
1216 * @param array $results['scripts'] Additional scripts required by the latest set of IS posts.
1217 * @param array|bool $initial_scripts Set of scripts loaded on each page.
1218 * @param array $results Array of Infinite Scroll results.
1219 * @param array $query_args Array of Query arguments.
1220 * @param WP_Query $wp_query WP Query.
1221 */
1222 $results['scripts'] = apply_filters(
1223 'infinite_scroll_additional_scripts',
1224 $results['scripts'],
1225 $initial_scripts,
1226 $results,
1227 $query_args,
1228 $wp_query
1229 );
1230
1231 if ( empty( $results['scripts'] ) ) {
1232 unset( $results['scripts'] );
1233 }
1234
1235 // Parse and sanitize the style handles already output
1236 $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
1237
1238 if ( is_array( $initial_styles ) ) {
1239 global $wp_styles;
1240
1241 // Identify new styles needed by the latest set of IS posts
1242 $new_styles = array_diff( $wp_styles->done, $initial_styles );
1243
1244 // If new styles are needed, extract relevant data from $wp_styles
1245 if ( ! empty( $new_styles ) ) {
1246 $results['styles'] = array();
1247
1248 foreach ( $new_styles as $handle ) {
1249 // Abort if somehow the handle doesn't correspond to a registered stylesheet
1250 if ( ! isset( $wp_styles->registered[ $handle ] ) ) {
1251 continue;
1252 }
1253
1254 // Provide basic style data
1255 $style_data = array(
1256 'handle' => $handle,
1257 'media' => 'all',
1258 );
1259
1260 // Base source
1261 $src = $wp_styles->registered[ $handle ]->src;
1262
1263 // Take base_url into account
1264 if ( strpos( $src, 'http' ) !== 0 ) {
1265 $src = $wp_styles->base_url . $src;
1266 }
1267
1268 // Version and additional arguments
1269 if ( null === $wp_styles->registered[ $handle ]->ver ) {
1270 $ver = '';
1271 } else {
1272 $ver = $wp_styles->registered[ $handle ]->ver ? $wp_styles->registered[ $handle ]->ver : $wp_styles->default_version;
1273 }
1274
1275 if ( isset( $wp_styles->args[ $handle ] ) ) {
1276 $ver = $ver ? $ver . '&amp;' . $wp_styles->args[ $handle ] : $wp_styles->args[ $handle ];
1277 }
1278
1279 // Full stylesheet source with version info
1280 $style_data['src'] = add_query_arg( 'ver', $ver, $src );
1281
1282 // Parse stylesheet's conditional comments if present, converting to logic executable in JS
1283 if ( isset( $wp_styles->registered[ $handle ]->extra['conditional'] ) && $wp_styles->registered[ $handle ]->extra['conditional'] ) {
1284 // First, convert conditional comment operators to standard logical operators. %ver is replaced in JS with the IE version
1285 $style_data['conditional'] = str_replace(
1286 array(
1287 'lte',
1288 'lt',
1289 'gte',
1290 'gt',
1291 ),
1292 array(
1293 '%ver <=',
1294 '%ver <',
1295 '%ver >=',
1296 '%ver >',
1297 ),
1298 $wp_styles->registered[ $handle ]->extra['conditional']
1299 );
1300
1301 // 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().
1302 $style_data['conditional'] = preg_replace( '#!\s*IE(\s*\d+){0}#i', '1==2', $style_data['conditional'] );
1303
1304 // Lastly, remove the IE strings
1305 $style_data['conditional'] = str_replace( 'IE', '', $style_data['conditional'] );
1306 }
1307
1308 // Parse requested media context for stylesheet
1309 if ( isset( $wp_styles->registered[ $handle ]->args ) ) {
1310 $style_data['media'] = esc_attr( $wp_styles->registered[ $handle ]->args );
1311 }
1312
1313 // Add stylesheet to data that will be returned to IS JS
1314 array_push( $results['styles'], $style_data );
1315 }
1316 }
1317 }
1318
1319 // Expose additional stylesheet data to filters, but only include in final `$results` array if needed.
1320 if ( ! isset( $results['styles'] ) ) {
1321 $results['styles'] = array();
1322 }
1323
1324 /**
1325 * Filter the additional styles required by the latest set of IS posts.
1326 *
1327 * @module infinite-scroll
1328 *
1329 * @since 2.1.2
1330 *
1331 * @param array $results['styles'] Additional styles required by the latest set of IS posts.
1332 * @param array|bool $initial_styles Set of styles loaded on each page.
1333 * @param array $results Array of Infinite Scroll results.
1334 * @param array $query_args Array of Query arguments.
1335 * @param WP_Query $wp_query WP Query.
1336 */
1337 $results['styles'] = apply_filters(
1338 'infinite_scroll_additional_stylesheets',
1339 $results['styles'],
1340 $initial_styles,
1341 $results,
1342 $query_args,
1343 $wp_query
1344 );
1345
1346 if ( empty( $results['styles'] ) ) {
1347 unset( $results['styles'] );
1348 }
1349
1350 // Lastly, return the IS results array
1351 return $results;
1352 }
1353
1354 /**
1355 * Runs the query and returns the results via JSON.
1356 * Triggered by an AJAX request.
1357 *
1358 * @global $wp_query
1359 * @global $wp_the_query
1360 * @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
1361 */
1362 public function query() {
1363 if ( ! isset( $_REQUEST['page'] ) || ! current_theme_supports( 'infinite-scroll' ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no changes to the site.
1364 die( 0 );
1365 }
1366
1367 // @todo see if we should validate this nonce since we use it to form a query.
1368 $page = (int) $_REQUEST['page']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- we're casting this to an int and not making changes to the site.
1369
1370 // Sanitize and set $previousday. Expected format: dd.mm.yy
1371 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
1372 global $previousday;
1373 $previousday = $_REQUEST['currentday']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput
1374 }
1375
1376 $post_status = array( 'publish' );
1377 if ( current_user_can( 'read_private_posts' ) ) {
1378 array_push( $post_status, 'private' );
1379 }
1380
1381 $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.
1382
1383 $query_args = array_merge(
1384 self::wp_query()->query_vars,
1385 array(
1386 'paged' => $page,
1387 'post_status' => $post_status,
1388 'posts_per_page' => self::posts_per_page(), // phpcs:ignore WordPress.WP.PostsPerPage.posts_per_page_posts_per_page
1389 'order' => $order,
1390 )
1391 );
1392
1393 // 4.0 ?s= compatibility, see https://core.trac.wordpress.org/ticket/11330#comment:50
1394 if ( empty( $query_args['s'] ) && ! isset( self::wp_query()->query['s'] ) ) {
1395 unset( $query_args['s'] );
1396 }
1397
1398 // By default, don't query for a specific page of a paged post object.
1399 // This argument can come from merging self::wp_query() into $query_args above.
1400 // Since IS is only used on archives, we should always display the first page of any paged content.
1401 unset( $query_args['page'] );
1402
1403 /**
1404 * Filter the array of main query arguments.
1405 *
1406 * @module infinite-scroll
1407 *
1408 * @since 2.0.1
1409 *
1410 * @param array $query_args Array of Query arguments.
1411 */
1412 $query_args = apply_filters( 'infinite_scroll_query_args', $query_args );
1413
1414 add_filter( 'posts_where', array( $this, 'query_time_filter' ), 10, 2 );
1415
1416 $infinite_scroll_query = new WP_Query();
1417 $GLOBALS['wp_the_query'] = $infinite_scroll_query;
1418 $GLOBALS['wp_query'] = $infinite_scroll_query;
1419
1420 $infinite_scroll_query->query( $query_args );
1421
1422 remove_filter( 'posts_where', array( $this, 'query_time_filter' ), 10 );
1423
1424 $results = array();
1425
1426 if ( have_posts() ) {
1427 // Fire wp_head to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
1428 ob_start();
1429 wp_head();
1430 while ( ob_get_length() ) {
1431 ob_end_clean();
1432 }
1433
1434 $results['type'] = 'success';
1435
1436 /**
1437 * Fires when rendering Infinite Scroll posts.
1438 *
1439 * @module infinite-scroll
1440 *
1441 * @since 2.0.0
1442 */
1443 do_action( 'infinite_scroll_render' );
1444 $results['html'] = ob_get_clean();
1445 if ( empty( $results['html'] ) ) {
1446 /**
1447 * 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.
1448 *
1449 * @module infinite-scroll
1450 *
1451 * @since 6.0.0
1452 */
1453 $callbacks = apply_filters(
1454 'infinite_scroll_render_callbacks',
1455 array( self::get_settings()->render ) // This is the setting callback e.g. from add theme support.
1456 );
1457
1458 // Append fallback callback. That rhymes.
1459 $callbacks[] = array( $this, 'render' );
1460
1461 foreach ( $callbacks as $callback ) {
1462 if ( false !== $callback && is_callable( $callback ) ) {
1463 rewind_posts();
1464 ob_start();
1465
1466 add_action( 'infinite_scroll_render', $callback );
1467
1468 /**
1469 * This action is already documented above.
1470 * See https://github.com/Automattic/jetpack/pull/16317/
1471 * for more details as to why it was introduced.
1472 */
1473 do_action( 'infinite_scroll_render' );
1474
1475 // Fire wp_head to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
1476 wp_head();
1477
1478 $results['html'] = ob_get_clean();
1479 remove_action( 'infinite_scroll_render', $callback );
1480 }
1481 if ( ! empty( $results['html'] ) ) {
1482 break;
1483 }
1484 }
1485 }
1486
1487 // If primary and fallback rendering methods fail, prevent further IS rendering attempts. Otherwise, wrap the output if requested.
1488 if ( empty( $results['html'] ) ) {
1489 unset( $results['html'] );
1490 /**
1491 * Fires when Infinite Scoll doesn't render any posts.
1492 *
1493 * @module infinite-scroll
1494 *
1495 * @since 2.0.0
1496 */
1497 do_action( 'infinite_scroll_empty' );
1498 $results['type'] = 'empty';
1499 } elseif ( $this->has_wrapper() ) {
1500 $wrapper_classes = is_string( self::get_settings()->wrapper ) ? self::get_settings()->wrapper : 'infinite-wrap';
1501 $wrapper_classes .= ' infinite-view-' . $page;
1502 $wrapper_classes = trim( $wrapper_classes );
1503 $aria_label = sprintf(
1504 /* translators: %1$s is the page count */
1505 __( 'Page: %1$d.', 'jetpack' ),
1506 $page
1507 );
1508
1509 $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>';
1510 }
1511
1512 // Fire wp_footer to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
1513 ob_start();
1514 wp_footer();
1515 while ( ob_get_length() ) {
1516 ob_end_clean();
1517 }
1518
1519 if ( 'success' === $results['type'] ) {
1520 global $currentday;
1521 $results['lastbatch'] = self::is_last_batch();
1522 $results['currentday'] = $currentday;
1523 }
1524
1525 // Loop through posts to capture sharing data for new posts loaded via Infinite Scroll
1526 if ( 'success' === $results['type'] && function_exists( 'sharing_register_post_for_share_counts' ) ) {
1527 global $jetpack_sharing_counts;
1528
1529 while ( have_posts() ) {
1530 the_post();
1531
1532 sharing_register_post_for_share_counts( get_the_ID() );
1533 }
1534
1535 // If sharing counts are not initialized for any reason, we initialize them here.
1536 if ( ! is_array( $jetpack_sharing_counts ) ) {
1537 $jetpack_sharing_counts = array();
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 $settings = self::get_settings();
1700
1701 // Bail if theme requested footer not show
1702 if ( false === $settings->footer ) {
1703 return;
1704 }
1705
1706 // We only need the new footer for the 'scroll' type
1707 if ( 'scroll' !== $settings->type || ! self::archive_supports_infinity() ) {
1708 return;
1709 }
1710
1711 if ( self::is_last_batch() ) {
1712 return;
1713 }
1714
1715 // Display a footer, either user-specified or a default
1716 if ( false !== $settings->footer_callback && is_callable( $settings->footer_callback ) ) {
1717 call_user_func( $settings->footer_callback, $settings );
1718 } else {
1719 self::default_footer();
1720 }
1721 }
1722
1723 /**
1724 * Render default IS footer
1725 *
1726 * @uses __, wp_get_theme, apply_filters, home_url, esc_attr, get_bloginfo, bloginfo
1727 */
1728 private function default_footer() {
1729 if ( '' !== get_privacy_policy_url() ) {
1730 $credits = get_the_privacy_policy_link() . '<span role="separator" aria-hidden="true"> / </span>';
1731 } else {
1732 $credits = '';
1733 }
1734 $credits .= sprintf(
1735 '<a href="https://wordpress.org/" rel="noopener noreferrer" target="_blank" rel="generator">%1$s</a> ',
1736 __( 'Proudly powered by WordPress', 'jetpack' )
1737 );
1738 $credits .= sprintf(
1739 /* translators: %1$s is the name of a theme */
1740 __( 'Theme: %1$s.', 'jetpack' ),
1741 wp_get_theme()->Name
1742 );
1743 /**
1744 * Filter Infinite Scroll's credit text.
1745 *
1746 * @module infinite-scroll
1747 *
1748 * @since 2.0.0
1749 *
1750 * @param string $credits Infinite Scroll credits.
1751 */
1752 $credits = apply_filters( 'infinite_scroll_credit', $credits );
1753
1754 ?>
1755 <div id="infinite-footer">
1756 <div class="container">
1757 <div class="blog-info">
1758 <a id="infinity-blog-title" href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
1759 <?php bloginfo( 'name' ); ?>
1760 </a>
1761 </div>
1762 <div class="blog-credits">
1763 <?php echo $credits; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1764 </div>
1765 </div>
1766 </div><!-- #infinite-footer -->
1767 <?php
1768 }
1769
1770 /**
1771 * Ensure that IS doesn't interfere with Grunion by stripping IS query arguments from the Grunion redirect URL.
1772 * When arguments are present, Grunion redirects to the IS AJAX endpoint.
1773 *
1774 * @param string $url - the Grunion redirect URL.
1775 * @uses remove_query_arg
1776 * @filter grunion_contact_form_redirect_url
1777 * @return string
1778 */
1779 public function filter_grunion_redirect_url( $url ) {
1780 // Remove IS query args, if present
1781 if ( str_contains( $url, 'infinity=scrolling' ) ) {
1782 $url = remove_query_arg(
1783 array(
1784 'infinity',
1785 'action',
1786 'page',
1787 'order',
1788 'scripts',
1789 'styles',
1790 ),
1791 $url
1792 );
1793 }
1794
1795 return $url;
1796 }
1797
1798 /**
1799 * When the MediaElement is loaded in dynamically, we need to enforce that
1800 * its settings are added to the page as well.
1801 *
1802 * @param array $scripts_data New scripts exposed to the infinite scroll.
1803 *
1804 * @since 8.4.0
1805 */
1806 public function add_mejs_config( $scripts_data ) {
1807 foreach ( $scripts_data as $key => $data ) {
1808 if ( 'mediaelement-core' === $data['handle'] ) {
1809 $mejs_settings = array(
1810 'pluginPath' => includes_url( 'js/mediaelement/', 'relative' ),
1811 'classPrefix' => 'mejs-',
1812 'stretching' => 'responsive',
1813 );
1814
1815 $scripts_data[ $key ]['extra_data'] = sprintf(
1816 'window.%s = %s',
1817 '_wpmejsSettings',
1818 wp_json_encode( apply_filters( 'mejs_settings', $mejs_settings ) )
1819 );
1820 }
1821 }
1822 return $scripts_data;
1823 }
1824
1825 /**
1826 * Determines whether the legacy AMP Reader post templates are being used.
1827 *
1828 * @return bool
1829 */
1830 private function is_exempted_amp_page() {
1831 if ( is_singular( 'web-story' ) ) {
1832 // Ensure that <amp-next-page> is not injected after <amp-story> as generated by the Web Stories plugin.
1833 return true;
1834 }
1835 if ( function_exists( 'amp_is_legacy' ) ) {
1836 // Available since AMP v2.0, this will return false if a theme like Twenty Twenty is selected as the Reader theme.
1837 return amp_is_legacy();
1838 }
1839 if ( method_exists( 'AMP_Options_Manager', 'get_option' ) ) {
1840 // In versions prior to v2.0, checking the template mode as being 'reader' is sufficient.
1841 return 'reader' === AMP_Options_Manager::get_option( 'theme_support' );
1842 }
1843 return false;
1844 }
1845
1846 /**
1847 * Load AMP specific hooks.
1848 *
1849 * @return void
1850 */
1851 public function amp_load_hooks() {
1852 if ( $this->is_exempted_amp_page() ) {
1853 return;
1854 }
1855
1856 if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
1857 $template = self::get_settings()->render;
1858
1859 add_filter( 'jetpack_infinite_scroll_load_scripts_and_styles', '__return_false' );
1860
1861 add_action( 'template_redirect', array( $this, 'amp_start_output_buffering' ), 0 );
1862 add_action( 'shutdown', array( $this, 'amp_output_buffer' ), 1 );
1863
1864 if ( is_string( $template ) && strpos( $template, '::' ) === false && is_callable( "amp_{$template}_hooks" ) ) {
1865 call_user_func( "amp_{$template}_hooks" );
1866 }
1867
1868 // Warms up the amp next page markup.
1869 // This should be done outside the output buffering callback started in the template_redirect.
1870 $this->amp_get_footer_template();
1871 }
1872 }
1873
1874 /**
1875 * Start the AMP output buffering.
1876 *
1877 * @return void
1878 */
1879 public function amp_start_output_buffering() {
1880 ob_start( array( $this, 'amp_finish_output_buffering' ) );
1881 }
1882
1883 /**
1884 * Flush the AMP output buffer.
1885 *
1886 * @return void
1887 */
1888 public function amp_output_buffer() {
1889 if ( ob_get_contents() ) {
1890 ob_end_flush();
1891 }
1892 }
1893
1894 /**
1895 * Filter the AMP output buffer contents.
1896 *
1897 * @param string $buffer Contents of the output buffer.
1898 *
1899 * @return string|false
1900 */
1901 public function amp_finish_output_buffering( $buffer ) {
1902 // Hide WordPress admin bar on next page load.
1903 $buffer = preg_replace(
1904 '/id="wpadminbar"/',
1905 '$0 next-page-hide',
1906 $buffer
1907 );
1908
1909 /**
1910 * Get the theme footers.
1911 *
1912 * @module infinite-scroll
1913 *
1914 * @since 9.0.0
1915 *
1916 * @param array array() An array to store multiple markup entries to be added to the footer.
1917 * @param string $buffer The contents of the output buffer.
1918 */
1919 $footers = apply_filters( 'jetpack_amp_infinite_footers', array(), $buffer );
1920
1921 /**
1922 * Filter the output buffer.
1923 * Themes can leverage this hook to add custom markup on next page load.
1924 *
1925 * @module infinite-scroll
1926 *
1927 * @since 9.0.0
1928 *
1929 * @param string $buffer The contents of the output buffer.
1930 */
1931 $buffer = apply_filters( 'jetpack_amp_infinite_output', $buffer );
1932
1933 // Add the amp next page markup.
1934 $buffer = preg_replace(
1935 '~</body>~',
1936 $this->amp_get_footer_template( $footers ) . '$0',
1937 $buffer
1938 );
1939
1940 return $buffer;
1941 }
1942
1943 /**
1944 * Get AMP next page markup with the custom footers.
1945 *
1946 * @param string[] $footers The theme footers.
1947 *
1948 * @return string
1949 */
1950 protected function amp_get_footer_template( $footers = array() ) {
1951 static $template = null;
1952
1953 if ( null === $template ) {
1954 $template = $this->amp_footer_template();
1955 }
1956
1957 if ( empty( $footers ) ) {
1958 return $template;
1959 }
1960
1961 return preg_replace(
1962 '/%%footer%%/',
1963 implode( '', $footers ),
1964 $template
1965 );
1966 }
1967
1968 /**
1969 * AMP Next Page markup.
1970 *
1971 * @return string
1972 */
1973 protected function amp_footer_template() {
1974 ob_start();
1975 ?>
1976 <amp-next-page max-pages="<?php echo esc_attr( static::amp_get_max_pages() ); ?>">
1977 <script type="application/json">
1978 [
1979 <?php echo wp_json_encode( $this->amp_next_page() ); ?>
1980 ]
1981 </script>
1982 <div separator>
1983 <?php
1984 echo wp_kses_post(
1985 /**
1986 * AMP infinite scroll separator.
1987 *
1988 * @module infinite-scroll
1989 *
1990 * @since 9.0.0
1991 *
1992 * @param string '' The markup for the next page separator.
1993 */
1994 apply_filters( 'jetpack_amp_infinite_separator', '' )
1995 );
1996 ?>
1997 </div>
1998 <div recommendation-box class="recommendation-box">
1999 <template type="amp-mustache">
2000 {{#pages}}
2001 <?php
2002 echo wp_kses_post(
2003 /**
2004 * AMP infinite scroll older posts markup.
2005 *
2006 * @module infinite-scroll
2007 *
2008 * @since 9.0.0
2009 *
2010 * @param string '' The markup for the older posts/next page.
2011 */
2012 apply_filters( 'jetpack_amp_infinite_older_posts', '' )
2013 );
2014 ?>
2015 {{/pages}}
2016 </template>
2017 </div>
2018 <div footer>
2019 %%footer%%
2020 </div>
2021 </amp-next-page>
2022 <?php
2023 return ob_get_clean();
2024 }
2025
2026 /**
2027 * Get the AMP next page information.
2028 *
2029 * @return array
2030 */
2031 protected function amp_next_page() {
2032 $title = '';
2033 $url = '';
2034 $image = '';
2035
2036 if ( ! static::amp_is_last_page() ) {
2037 $title = sprintf(
2038 '%s - %s %d - %s',
2039 wp_title( '', false ),
2040 __( 'Page', 'jetpack' ),
2041 max( get_query_var( 'paged', 1 ), 1 ) + 1,
2042 get_bloginfo( 'name' )
2043 );
2044 $url = get_next_posts_page_link();
2045 }
2046
2047 $next_page = array(
2048 'title' => $title,
2049 'url' => $url,
2050 'image' => $image,
2051 );
2052
2053 /**
2054 * The next page settings.
2055 * An array containing:
2056 * - title => The title to be featured on the browser tab.
2057 * - url => The URL of next page.
2058 * - image => The image URL. A required AMP setting, not in use currently. Themes are welcome to leverage.
2059 *
2060 * @module infinite-scroll
2061 *
2062 * @since 9.0.0
2063 *
2064 * @param array $next_page The contents of the output buffer.
2065 */
2066 return apply_filters( 'jetpack_amp_infinite_next_page_data', $next_page );
2067 }
2068
2069 /**
2070 * Get the number of pages left.
2071 *
2072 * @return int
2073 */
2074 protected static function amp_get_max_pages() {
2075 global $wp_query;
2076
2077 return (int) $wp_query->max_num_pages - (int) $wp_query->query_vars['paged'];
2078 }
2079
2080 /**
2081 * Is the last page.
2082 *
2083 * @return bool
2084 */
2085 protected static function amp_is_last_page() {
2086 return 0 === static::amp_get_max_pages();
2087 }
2088 }
2089
2090 /**
2091 * Initialize The_Neverending_Home_Page
2092 */
2093 function the_neverending_home_page_init() {
2094 if ( ! current_theme_supports( 'infinite-scroll' ) ) {
2095 return;
2096 }
2097
2098 new The_Neverending_Home_Page();
2099 }
2100 add_action( 'init', 'the_neverending_home_page_init', 20 );
2101
2102 /**
2103 * Check whether the current theme is infinite-scroll aware.
2104 * If so, include the files which add theme support.
2105 */
2106 function the_neverending_home_page_theme_support() {
2107 if (
2108 defined( 'IS_WPCOM' ) && IS_WPCOM &&
2109 defined( 'REST_API_REQUEST' ) && REST_API_REQUEST &&
2110 ! doing_action( 'restapi_theme_after_setup_theme' )
2111 ) {
2112 // Don't source theme compat files until we're in the site's context
2113 return;
2114 }
2115 $theme_name = get_stylesheet();
2116
2117 /**
2118 * Filter the path to the Infinite Scroll compatibility file.
2119 *
2120 * @module infinite-scroll
2121 *
2122 * @since 2.0.0
2123 *
2124 * @param string $str IS compatibility file path.
2125 * @param string $theme_name Theme name.
2126 */
2127 $customization_file = apply_filters( 'infinite_scroll_customization_file', __DIR__ . "/themes/{$theme_name}.php", $theme_name );
2128
2129 if ( is_readable( $customization_file ) ) {
2130 require_once $customization_file;
2131 }
2132 }
2133 add_action( 'after_setup_theme', 'the_neverending_home_page_theme_support', 5 );
2134
2135 /**
2136 * Early accommodation of the Infinite Scroll AJAX request
2137 */
2138 if ( The_Neverending_Home_Page::got_infinity() ) {
2139 /**
2140 * If we're sure this is an AJAX request (i.e. the HTTP_X_REQUESTED_WITH header says so),
2141 * indicate it as early as possible for actions like init
2142 */
2143 if ( ! defined( 'DOING_AJAX' ) &&
2144 isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) &&
2145 strtoupper( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REQUESTED_WITH'] ) ) ) === 'XMLHTTPREQUEST'
2146 ) {
2147 define( 'DOING_AJAX', true );
2148 }
2149
2150 // Don't load the admin bar when doing the AJAX response.
2151 show_admin_bar( false );
2152 }
2153