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