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