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