PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 15.1.2
Jetpack – WP Security, Backup, Speed, & Growth v15.1.2
12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 14.3.1 14.4.2 14.5.1 14.6.1 14.7.1 14.8.1 14.9.2 15.0.3 15.1.2 15.2.1 15.3.2 15.4.1 15.5.1 15.6.1 15.7.2 15.8.1 15.9.2 16.0.2 16.1.3 16.2-a.5 16.2-a.3 16.1.2 16.2-a.1 16.1.1 16.1 16.1-beta 16.1-beta.2 16.1-beta.3 16.1-a.5 16.1-a.3 16.0.1 16.1-a.1 16.0 16.0-beta 16.0-a.7 16.0-a.5 15.9.1 16.0-a.3 16.0-a.1 15.9 15.9-beta 15.9-a.7 15.9-a.5 15.9-a.3 15.9-a.1 15.8 15.8-beta 15.8-a.7 15.8-a.5 5.2.5 5.3.4 5.4.4 5.5.5 5.6.5 5.7.5 5.8.4 5.9.4 6.0.4 6.1 6.1.1 6.1.2 6.1.3 6.1.4 6.1.5 6.2 6.2.1 6.2.2 6.2.3 6.2.4 6.2.5 6.3 6.3.1 6.3.2 6.3.3 6.3.4 6.3.5 6.3.6 6.3.7 6.4 6.4.1 6.4.2 6.4.3 6.4.4 6.4.5 6.4.6 6.5 6.5.1 6.5.2 6.5.3 6.5.4 6.6 6.6.1 6.6.2 6.6.3 6.6.4 6.6.5 6.7 6.7.1 6.7.2 6.7.3 6.7.4 6.8 6.8.1 6.8.2 6.8.3 6.8.4 6.8.5 6.9 6.9.1 6.9.2 6.9.3 6.9.4 7.0 7.0.1 7.0.2 7.0.3 7.0.4 7.0.5 7.1 7.1.1 7.1.2 7.1.3 7.1.4 7.1.5 7.2 7.2.1 7.2.1.1 7.2.2 7.2.3 7.2.4 7.2.5 7.3 7.3.0.1 7.3.1 7.3.1.1 7.3.2 7.3.3 7.3.4 7.3.5 7.4 7.4.1 7.4.2 7.4.3 7.4.4 7.4.5 7.5 7.5.0.1 7.5.1 7.5.2 7.5.3 7.5.4 7.5.5 7.5.6 7.5.7 7.6 7.6.1 7.6.2 7.6.3 7.6.4 7.7 7.7.1 7.7.2 7.7.3 7.7.4 7.7.5 7.7.6 7.8 7.8.1 7.8.2 7.8.3 7.8.4 7.9 7.9.1 7.9.2 7.9.3 7.9.4 8.0 8.0.1 8.0.2 8.0.3 8.1 8.1.1 8.1.2 8.1.3 8.1.4 8.2 8.2.0.1 8.2.1 8.2.2 8.2.3 8.2.4 8.2.5 8.2.6 8.3 8.3.1 8.3.2 8.3.3 8.4 8.4.1 8.4.2 8.4.3 8.4.4 8.4.5 8.5 8.5.1 8.5.2 8.5.3 8.6 8.6.1 8.6.2 8.6.3 8.6.4 8.7 8.7.0.1 8.7.1 8.7.2 8.7.3 8.7.4 8.8 8.8.1 8.8.2 8.8.3 8.8.4 8.8.5 8.9 8.9.1 8.9.2 8.9.3 8.9.4 9.0 9.0.1 9.0.2 9.0.3 9.0.4 9.0.5 9.1 9.1.1 9.1.2 9.1.3 9.2 9.2.1 9.2.2 9.2.3 9.2.4 9.3 9.3.1 9.3.2 9.3.3 9.3.4 9.3.5 9.4 9.4.1 9.4.2 9.4.3 9.4.4 9.5 9.5.1 9.5.2 9.5.3 9.5.4 9.5.5 9.6 9.6.1 9.6.2 9.6.3 9.6.4 9.7 9.7.1 9.7.2 15.7-beta.2 9.7.3 15.7.1 9.8 15.8-a.1 9.8.1 15.8-a.3 9.8.2 2.0.9 9.8.3 2.1.7 9.9 2.2.10 9.9.1 2.3.10 9.9.2 2.4.7 9.9.3 2.5.5 2.6.6 2.7.5 2.8.5 2.9.6 3.0.6 3.1.5 3.2.5 3.3.6 3.4.6 3.5.6 3.6.4 3.7.5 3.8.5 3.9.10 4.0.7 4.1.4 4.2.5 4.3.5 4.4.5 4.5.3 4.6.3 4.7.4 4.8.5 4.9.3 5.0.3 5.1.4 trunk 10.0 10.0.1 10.0.2 10.1 10.1.1 10.1.2 10.2 10.2.1 10.2.2 10.2.3 10.3 10.3.1 10.3.2 10.4 10.4.1 10.4.2 10.5 10.5.1 10.5.2 10.5.3 10.6 10.6.1 10.6.2 10.7 10.7.1 10.7.2 10.8 10.8.1 10.8.2 10.9 10.9.1 10.9.2 10.9.3 11.0 11.0.1 11.0.2 11.1 11.1.1 11.1.2 11.1.3 11.1.4 11.2 11.2.1 11.2.2 11.3 11.3.1 11.3.2 11.3.3 11.3.4 11.4 11.4.1 11.4.2 11.5 11.5.1 11.5.2 11.5.3 11.6 11.6.1 11.6.2 11.7 11.7.1 11.7.2 11.7.3 11.8 11.8.3 11.8.4 11.8.5 11.8.6 11.9 11.9.1 11.9.2 11.9.3 12.0 12.0.1 12.0.2 12.1 12.1.1 12.1.2 12.2 12.2.1 12.2.2 12.3 12.3.1 12.4 12.4.1 12.5 12.5.1 12.6 12.6.1 12.6.2 12.6.3 12.7 12.7.1 12.7.2 12.8 12.8.1 12.8.2 12.9 12.9.1 12.9.2 12.9.3 12.9.4 13.0 13.0.1 13.1 13.1.1 13.1.2 13.1.3 13.1.4 13.2 13.2.1 13.2.2 13.2.3 13.3 13.3.1 13.3.2 13.4 13.4.1 13.4.2 13.4.3 13.4.4 13.5 13.5.1 13.6 13.6.1 13.7 13.7.1 13.8 13.8.1 13.8.2 13.9 13.9.1 14.0 14.1 14.2 14.2.1 14.3 14.4 14.4.1 14.5 14.6 14.7 14.8 14.9 14.9.1 15.0 15.0.1 15.0.2 15.1 15.1.1 15.2 15.3 15.3.1 15.4 15.5 15.6 15.7 15.7-a.1 15.7-a.3 15.7-a.5 15.7-a.7 15.7-beta
jetpack / modules / infinite-scroll / infinity.php
jetpack / modules / infinite-scroll Last commit date
themes 1 year ago infinity-customizer.js 1 year ago infinity.css 1 year ago infinity.js 1 year ago infinity.php 11 months ago
infinity.php
2169 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 (bool) ( ! 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' => esc_js( $click_handle_text ),
937 'totop' => esc_js( __( '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' => esc_js( __( '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_HEX_TAG ); ?>;
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 ); ?> );
1108 extend( window.infiniteScroll.settings.styles, <?php echo wp_json_encode( $styles ); ?> );
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 );
1578 }
1579
1580 /**
1581 * Update the $allowed_vars array with the standard WP public and private
1582 * query vars, as well as taxonomy vars
1583 *
1584 * @global $wp
1585 * @param array $allowed_vars - the allowed variables array.
1586 * @filter infinite_scroll_allowed_vars
1587 * @return array
1588 */
1589 public function allowed_query_vars( $allowed_vars ) {
1590 global $wp;
1591
1592 $allowed_vars += $wp->public_query_vars;
1593 $allowed_vars += $wp->private_query_vars;
1594 $allowed_vars += $this->get_taxonomy_vars();
1595
1596 foreach ( array_keys( $allowed_vars, 'paged', true ) as $key ) {
1597 unset( $allowed_vars[ $key ] );
1598 }
1599
1600 return array_unique( $allowed_vars );
1601 }
1602
1603 /**
1604 * Returns an array of stock and custom taxonomy query vars
1605 *
1606 * @global $wp_taxonomies
1607 * @return array
1608 */
1609 public function get_taxonomy_vars() {
1610 global $wp_taxonomies;
1611
1612 $taxonomy_vars = array();
1613 foreach ( $wp_taxonomies as $t ) {
1614 if ( $t->query_var ) {
1615 $taxonomy_vars[] = $t->query_var;
1616 }
1617 }
1618
1619 // still needed?
1620 $taxonomy_vars[] = 'tag_id';
1621
1622 return $taxonomy_vars;
1623 }
1624
1625 /**
1626 * Update the $query_args array with the parameters provided via AJAX/GET.
1627 *
1628 * @param array $query_args - the query args.
1629 * @filter infinite_scroll_query_args
1630 * @return array
1631 */
1632 public function inject_query_args( $query_args ) {
1633 /**
1634 * Filter the array of allowed Infinite Scroll query arguments.
1635 *
1636 * @module infinite-scroll
1637 *
1638 * @since 2.6.0
1639 *
1640 * @param array $args Array of allowed Infinite Scroll query arguments.
1641 * @param array $query_args Array of query arguments.
1642 */
1643 $allowed_vars = apply_filters( 'infinite_scroll_allowed_vars', array(), $query_args );
1644
1645 $query_args = array_merge(
1646 $query_args,
1647 array(
1648 'suppress_filters' => false,
1649 )
1650 );
1651
1652 if ( isset( $_REQUEST['query_args'] ) && is_array( $_REQUEST['query_args'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
1653 foreach ( wp_unslash( $_REQUEST['query_args'] ) as $var => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- no site changes, sanitized below.
1654 if ( in_array( $var, $allowed_vars, true ) && ! empty( $value ) ) {
1655 $query_args[ $var ] = filter_var( $value );
1656 }
1657 }
1658 }
1659
1660 return $query_args;
1661 }
1662
1663 /**
1664 * Rendering fallback used when themes don't specify their own handler.
1665 *
1666 * @uses have_posts, the_post, get_template_part, get_post_format
1667 * @action infinite_scroll_render
1668 */
1669 public function render() {
1670 while ( have_posts() ) {
1671 the_post();
1672
1673 get_template_part( 'content', get_post_format() );
1674 }
1675 }
1676
1677 /**
1678 * Allow plugins to filter what archives Infinite Scroll supports
1679 *
1680 * @uses current_theme_supports, is_home, is_archive, apply_filters, self::get_settings
1681 * @return bool
1682 */
1683 public static function archive_supports_infinity() {
1684 $supported = current_theme_supports( 'infinite-scroll' ) && ( is_home() || is_archive() || is_search() );
1685
1686 // Disable when previewing a non-active theme in the customizer
1687 if ( is_customize_preview() && ! $GLOBALS['wp_customize']->is_theme_active() ) {
1688 return false;
1689 }
1690
1691 /**
1692 * Allow plugins to filter what archives Infinite Scroll supports.
1693 *
1694 * @module infinite-scroll
1695 *
1696 * @since 2.0.0
1697 *
1698 * @param bool $supported Does the Archive page support Infinite Scroll.
1699 * @param object self::get_settings() IS settings provided by theme.
1700 */
1701 return (bool) apply_filters( 'infinite_scroll_archive_supported', $supported, self::get_settings() );
1702 }
1703
1704 /**
1705 * The Infinite Blog Footer
1706 *
1707 * @uses self::get_settings, self::archive_supports_infinity, self::default_footer
1708 * @return string or null
1709 */
1710 public function footer() {
1711 if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
1712 return;
1713 }
1714
1715 $settings = self::get_settings();
1716
1717 // Bail if theme requested footer not show
1718 if ( false === $settings->footer ) {
1719 return;
1720 }
1721
1722 // We only need the new footer for the 'scroll' type
1723 if ( 'scroll' !== $settings->type || ! self::archive_supports_infinity() ) {
1724 return;
1725 }
1726
1727 if ( self::is_last_batch() ) {
1728 return;
1729 }
1730
1731 // Display a footer, either user-specified or a default
1732 if ( false !== $settings->footer_callback && is_callable( $settings->footer_callback ) ) {
1733 call_user_func( $settings->footer_callback, $settings );
1734 } else {
1735 self::default_footer();
1736 }
1737 }
1738
1739 /**
1740 * Render default IS footer
1741 *
1742 * @uses __, wp_get_theme, apply_filters, home_url, esc_attr, get_bloginfo, bloginfo
1743 */
1744 private function default_footer() {
1745 if ( '' !== get_privacy_policy_url() ) {
1746 $credits = get_the_privacy_policy_link() . '<span role="separator" aria-hidden="true"> / </span>';
1747 } else {
1748 $credits = '';
1749 }
1750 $credits .= sprintf(
1751 '<a href="https://wordpress.org/" rel="noopener noreferrer" target="_blank" rel="generator">%1$s</a> ',
1752 __( 'Proudly powered by WordPress', 'jetpack' )
1753 );
1754 $credits .= sprintf(
1755 /* translators: %1$s is the name of a theme */
1756 __( 'Theme: %1$s.', 'jetpack' ),
1757 wp_get_theme()->Name
1758 );
1759 /**
1760 * Filter Infinite Scroll's credit text.
1761 *
1762 * @module infinite-scroll
1763 *
1764 * @since 2.0.0
1765 *
1766 * @param string $credits Infinite Scroll credits.
1767 */
1768 $credits = apply_filters( 'infinite_scroll_credit', $credits );
1769
1770 ?>
1771 <div id="infinite-footer">
1772 <div class="container">
1773 <div class="blog-info">
1774 <a id="infinity-blog-title" href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
1775 <?php bloginfo( 'name' ); ?>
1776 </a>
1777 </div>
1778 <div class="blog-credits">
1779 <?php echo $credits; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1780 </div>
1781 </div>
1782 </div><!-- #infinite-footer -->
1783 <?php
1784 }
1785
1786 /**
1787 * Ensure that IS doesn't interfere with Grunion by stripping IS query arguments from the Grunion redirect URL.
1788 * When arguments are present, Grunion redirects to the IS AJAX endpoint.
1789 *
1790 * @param string $url - the Grunion redirect URL.
1791 * @uses remove_query_arg
1792 * @filter grunion_contact_form_redirect_url
1793 * @return string
1794 */
1795 public function filter_grunion_redirect_url( $url ) {
1796 // Remove IS query args, if present
1797 if ( str_contains( $url, 'infinity=scrolling' ) ) {
1798 $url = remove_query_arg(
1799 array(
1800 'infinity',
1801 'action',
1802 'page',
1803 'order',
1804 'scripts',
1805 'styles',
1806 ),
1807 $url
1808 );
1809 }
1810
1811 return $url;
1812 }
1813
1814 /**
1815 * When the MediaElement is loaded in dynamically, we need to enforce that
1816 * its settings are added to the page as well.
1817 *
1818 * @param array $scripts_data New scripts exposed to the infinite scroll.
1819 *
1820 * @since 8.4.0
1821 */
1822 public function add_mejs_config( $scripts_data ) {
1823 foreach ( $scripts_data as $key => $data ) {
1824 if ( 'mediaelement-core' === $data['handle'] ) {
1825 $mejs_settings = array(
1826 'pluginPath' => includes_url( 'js/mediaelement/', 'relative' ),
1827 'classPrefix' => 'mejs-',
1828 'stretching' => 'responsive',
1829 );
1830
1831 $scripts_data[ $key ]['extra_data'] = sprintf(
1832 'window.%s = %s',
1833 '_wpmejsSettings',
1834 wp_json_encode( apply_filters( 'mejs_settings', $mejs_settings ) )
1835 );
1836 }
1837 }
1838 return $scripts_data;
1839 }
1840
1841 /**
1842 * Determines whether the legacy AMP Reader post templates are being used.
1843 *
1844 * @return bool
1845 */
1846 private function is_exempted_amp_page() {
1847 if ( is_singular( 'web-story' ) ) {
1848 // Ensure that <amp-next-page> is not injected after <amp-story> as generated by the Web Stories plugin.
1849 return true;
1850 }
1851 if ( function_exists( 'amp_is_legacy' ) ) {
1852 // Available since AMP v2.0, this will return false if a theme like Twenty Twenty is selected as the Reader theme.
1853 return amp_is_legacy();
1854 }
1855 if ( method_exists( 'AMP_Options_Manager', 'get_option' ) ) {
1856 // In versions prior to v2.0, checking the template mode as being 'reader' is sufficient.
1857 return 'reader' === AMP_Options_Manager::get_option( 'theme_support' );
1858 }
1859 return false;
1860 }
1861
1862 /**
1863 * Load AMP specific hooks.
1864 *
1865 * @return void
1866 */
1867 public function amp_load_hooks() {
1868 if ( $this->is_exempted_amp_page() ) {
1869 return;
1870 }
1871
1872 if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
1873 $template = self::get_settings()->render;
1874
1875 add_filter( 'jetpack_infinite_scroll_load_scripts_and_styles', '__return_false' );
1876
1877 add_action( 'template_redirect', array( $this, 'amp_start_output_buffering' ), 0 );
1878 add_action( 'shutdown', array( $this, 'amp_output_buffer' ), 1 );
1879
1880 if ( is_string( $template ) && strpos( $template, '::' ) === false && is_callable( "amp_{$template}_hooks" ) ) {
1881 call_user_func( "amp_{$template}_hooks" );
1882 }
1883
1884 // Warms up the amp next page markup.
1885 // This should be done outside the output buffering callback started in the template_redirect.
1886 $this->amp_get_footer_template();
1887 }
1888 }
1889
1890 /**
1891 * Start the AMP output buffering.
1892 *
1893 * @return void
1894 */
1895 public function amp_start_output_buffering() {
1896 ob_start( array( $this, 'amp_finish_output_buffering' ) );
1897 }
1898
1899 /**
1900 * Flush the AMP output buffer.
1901 *
1902 * @return void
1903 */
1904 public function amp_output_buffer() {
1905 if ( ob_get_contents() ) {
1906 ob_end_flush();
1907 }
1908 }
1909
1910 /**
1911 * Filter the AMP output buffer contents.
1912 *
1913 * @param string $buffer Contents of the output buffer.
1914 *
1915 * @return string|false
1916 */
1917 public function amp_finish_output_buffering( $buffer ) {
1918 // Hide WordPress admin bar on next page load.
1919 $buffer = preg_replace(
1920 '/id="wpadminbar"/',
1921 '$0 next-page-hide',
1922 $buffer
1923 );
1924
1925 /**
1926 * Get the theme footers.
1927 *
1928 * @module infinite-scroll
1929 *
1930 * @since 9.0.0
1931 *
1932 * @param array array() An array to store multiple markup entries to be added to the footer.
1933 * @param string $buffer The contents of the output buffer.
1934 */
1935 $footers = apply_filters( 'jetpack_amp_infinite_footers', array(), $buffer );
1936
1937 /**
1938 * Filter the output buffer.
1939 * Themes can leverage this hook to add custom markup on next page load.
1940 *
1941 * @module infinite-scroll
1942 *
1943 * @since 9.0.0
1944 *
1945 * @param string $buffer The contents of the output buffer.
1946 */
1947 $buffer = apply_filters( 'jetpack_amp_infinite_output', $buffer );
1948
1949 // Add the amp next page markup.
1950 $buffer = preg_replace(
1951 '~</body>~',
1952 $this->amp_get_footer_template( $footers ) . '$0',
1953 $buffer
1954 );
1955
1956 return $buffer;
1957 }
1958
1959 /**
1960 * Get AMP next page markup with the custom footers.
1961 *
1962 * @param string[] $footers The theme footers.
1963 *
1964 * @return string
1965 */
1966 protected function amp_get_footer_template( $footers = array() ) {
1967 static $template = null;
1968
1969 if ( null === $template ) {
1970 $template = $this->amp_footer_template();
1971 }
1972
1973 if ( empty( $footers ) ) {
1974 return $template;
1975 }
1976
1977 return preg_replace(
1978 '/%%footer%%/',
1979 implode( '', $footers ),
1980 $template
1981 );
1982 }
1983
1984 /**
1985 * AMP Next Page markup.
1986 *
1987 * @return string
1988 */
1989 protected function amp_footer_template() {
1990 ob_start();
1991 ?>
1992 <amp-next-page max-pages="<?php echo esc_attr( static::amp_get_max_pages() ); ?>">
1993 <script type="application/json">
1994 [
1995 <?php echo wp_json_encode( $this->amp_next_page() ); ?>
1996 ]
1997 </script>
1998 <div separator>
1999 <?php
2000 echo wp_kses_post(
2001 /**
2002 * AMP infinite scroll separator.
2003 *
2004 * @module infinite-scroll
2005 *
2006 * @since 9.0.0
2007 *
2008 * @param string '' The markup for the next page separator.
2009 */
2010 apply_filters( 'jetpack_amp_infinite_separator', '' )
2011 );
2012 ?>
2013 </div>
2014 <div recommendation-box class="recommendation-box">
2015 <template type="amp-mustache">
2016 {{#pages}}
2017 <?php
2018 echo wp_kses_post(
2019 /**
2020 * AMP infinite scroll older posts markup.
2021 *
2022 * @module infinite-scroll
2023 *
2024 * @since 9.0.0
2025 *
2026 * @param string '' The markup for the older posts/next page.
2027 */
2028 apply_filters( 'jetpack_amp_infinite_older_posts', '' )
2029 );
2030 ?>
2031 {{/pages}}
2032 </template>
2033 </div>
2034 <div footer>
2035 %%footer%%
2036 </div>
2037 </amp-next-page>
2038 <?php
2039 return ob_get_clean();
2040 }
2041
2042 /**
2043 * Get the AMP next page information.
2044 *
2045 * @return array
2046 */
2047 protected function amp_next_page() {
2048 $title = '';
2049 $url = '';
2050 $image = '';
2051
2052 if ( ! static::amp_is_last_page() ) {
2053 $title = sprintf(
2054 '%s - %s %d - %s',
2055 wp_title( '', false ),
2056 __( 'Page', 'jetpack' ),
2057 max( get_query_var( 'paged', 1 ), 1 ) + 1,
2058 get_bloginfo( 'name' )
2059 );
2060 $url = get_next_posts_page_link();
2061 }
2062
2063 $next_page = array(
2064 'title' => $title,
2065 'url' => $url,
2066 'image' => $image,
2067 );
2068
2069 /**
2070 * The next page settings.
2071 * An array containing:
2072 * - title => The title to be featured on the browser tab.
2073 * - url => The URL of next page.
2074 * - image => The image URL. A required AMP setting, not in use currently. Themes are welcome to leverage.
2075 *
2076 * @module infinite-scroll
2077 *
2078 * @since 9.0.0
2079 *
2080 * @param array $next_page The contents of the output buffer.
2081 */
2082 return apply_filters( 'jetpack_amp_infinite_next_page_data', $next_page );
2083 }
2084
2085 /**
2086 * Get the number of pages left.
2087 *
2088 * @return int
2089 */
2090 protected static function amp_get_max_pages() {
2091 global $wp_query;
2092
2093 return (int) $wp_query->max_num_pages - (int) $wp_query->query_vars['paged'];
2094 }
2095
2096 /**
2097 * Is the last page.
2098 *
2099 * @return bool
2100 */
2101 protected static function amp_is_last_page() {
2102 return 0 === static::amp_get_max_pages();
2103 }
2104 }
2105
2106 /**
2107 * Initialize The_Neverending_Home_Page
2108 */
2109 function the_neverending_home_page_init() {
2110 if ( ! current_theme_supports( 'infinite-scroll' ) ) {
2111 return;
2112 }
2113
2114 new The_Neverending_Home_Page();
2115 }
2116 add_action( 'init', 'the_neverending_home_page_init', 20 );
2117
2118 /**
2119 * Check whether the current theme is infinite-scroll aware.
2120 * If so, include the files which add theme support.
2121 */
2122 function the_neverending_home_page_theme_support() {
2123 if (
2124 defined( 'IS_WPCOM' ) && IS_WPCOM &&
2125 defined( 'REST_API_REQUEST' ) && REST_API_REQUEST &&
2126 ! doing_action( 'restapi_theme_after_setup_theme' )
2127 ) {
2128 // Don't source theme compat files until we're in the site's context
2129 return;
2130 }
2131 $theme_name = get_stylesheet();
2132
2133 /**
2134 * Filter the path to the Infinite Scroll compatibility file.
2135 *
2136 * @module infinite-scroll
2137 *
2138 * @since 2.0.0
2139 *
2140 * @param string $str IS compatibility file path.
2141 * @param string $theme_name Theme name.
2142 */
2143 $customization_file = apply_filters( 'infinite_scroll_customization_file', __DIR__ . "/themes/{$theme_name}.php", $theme_name );
2144
2145 if ( is_readable( $customization_file ) ) {
2146 require_once $customization_file;
2147 }
2148 }
2149 add_action( 'after_setup_theme', 'the_neverending_home_page_theme_support', 5 );
2150
2151 /**
2152 * Early accommodation of the Infinite Scroll AJAX request
2153 */
2154 if ( The_Neverending_Home_Page::got_infinity() ) {
2155 /**
2156 * If we're sure this is an AJAX request (i.e. the HTTP_X_REQUESTED_WITH header says so),
2157 * indicate it as early as possible for actions like init
2158 */
2159 if ( ! defined( 'DOING_AJAX' ) &&
2160 isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) &&
2161 strtoupper( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REQUESTED_WITH'] ) ) ) === 'XMLHTTPREQUEST'
2162 ) {
2163 define( 'DOING_AJAX', true );
2164 }
2165
2166 // Don't load the admin bar when doing the AJAX response.
2167 show_admin_bar( false );
2168 }
2169