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