PluginProbe ʕ •ᴥ•ʔ
Jetpack – WP Security, Backup, Speed, & Growth / 13.6.2
Jetpack – WP Security, Backup, Speed, & Growth v13.6.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
2164 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
1479 add_action( 'infinite_scroll_render', $callback );
1480
1481 /**
1482 * This action is already documented above.
1483 * See https://github.com/Automattic/jetpack/pull/16317/
1484 * for more details as to why it was introduced.
1485 */
1486 do_action( 'infinite_scroll_render' );
1487
1488 // Fire wp_head to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
1489 wp_head();
1490
1491 $results['html'] = ob_get_clean();
1492 remove_action( 'infinite_scroll_render', $callback );
1493 }
1494 if ( ! empty( $results['html'] ) ) {
1495 break;
1496 }
1497 }
1498 }
1499
1500 // If primary and fallback rendering methods fail, prevent further IS rendering attempts. Otherwise, wrap the output if requested.
1501 if ( empty( $results['html'] ) ) {
1502 unset( $results['html'] );
1503 /**
1504 * Fires when Infinite Scoll doesn't render any posts.
1505 *
1506 * @module infinite-scroll
1507 *
1508 * @since 2.0.0
1509 */
1510 do_action( 'infinite_scroll_empty' );
1511 $results['type'] = 'empty';
1512 } elseif ( $this->has_wrapper() ) {
1513 $wrapper_classes = is_string( self::get_settings()->wrapper ) ? self::get_settings()->wrapper : 'infinite-wrap';
1514 $wrapper_classes .= ' infinite-view-' . $page;
1515 $wrapper_classes = trim( $wrapper_classes );
1516 $aria_label = sprintf(
1517 /* translators: %1$s is the page count */
1518 __( 'Page: %1$d.', 'jetpack' ),
1519 $page
1520 );
1521
1522 $results['html'] = '<div class="' . esc_attr( $wrapper_classes ) . '" id="infinite-view-' . $page . '" data-page-num="' . $page . '" role="region" aria-label="' . esc_attr( $aria_label ) . '">' . $results['html'] . '</div>';
1523 }
1524
1525 // Fire wp_footer to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
1526 ob_start();
1527 wp_footer();
1528 while ( ob_get_length() ) {
1529 ob_end_clean();
1530 }
1531
1532 if ( 'success' === $results['type'] ) {
1533 global $currentday;
1534 $results['lastbatch'] = self::is_last_batch();
1535 $results['currentday'] = $currentday;
1536 }
1537
1538 // Loop through posts to capture sharing data for new posts loaded via Infinite Scroll
1539 if ( 'success' === $results['type'] && function_exists( 'sharing_register_post_for_share_counts' ) ) {
1540 global $jetpack_sharing_counts;
1541
1542 while ( have_posts() ) {
1543 the_post();
1544
1545 sharing_register_post_for_share_counts( get_the_ID() );
1546 }
1547
1548 // If sharing counts are not initialized for any reason, we initialize them here.
1549 if ( ! is_array( $jetpack_sharing_counts ) ) {
1550 $jetpack_sharing_counts = array();
1551 }
1552
1553 $results['postflair'] = array_flip( $jetpack_sharing_counts );
1554 }
1555 } else {
1556 /** This action is already documented in modules/infinite-scroll/infinity.php */
1557 do_action( 'infinite_scroll_empty' );
1558 $results['type'] = 'empty';
1559 }
1560
1561 wp_send_json(
1562 /**
1563 * Filter the Infinite Scroll results.
1564 *
1565 * @module infinite-scroll
1566 *
1567 * @since 2.0.0
1568 *
1569 * @param array $results Array of Infinite Scroll results.
1570 * @param array $query_args Array of main query arguments.
1571 * @param WP_Query $wp_query WP Query.
1572 */
1573 apply_filters( 'infinite_scroll_results', $results, $query_args, self::wp_query() )
1574 );
1575 }
1576
1577 /**
1578 * Update the $allowed_vars array with the standard WP public and private
1579 * query vars, as well as taxonomy vars
1580 *
1581 * @global $wp
1582 * @param array $allowed_vars - the allowed variables array.
1583 * @filter infinite_scroll_allowed_vars
1584 * @return array
1585 */
1586 public function allowed_query_vars( $allowed_vars ) {
1587 global $wp;
1588
1589 $allowed_vars += $wp->public_query_vars;
1590 $allowed_vars += $wp->private_query_vars;
1591 $allowed_vars += $this->get_taxonomy_vars();
1592
1593 foreach ( array_keys( $allowed_vars, 'paged', true ) as $key ) {
1594 unset( $allowed_vars[ $key ] );
1595 }
1596
1597 return array_unique( $allowed_vars );
1598 }
1599
1600 /**
1601 * Returns an array of stock and custom taxonomy query vars
1602 *
1603 * @global $wp_taxonomies
1604 * @return array
1605 */
1606 public function get_taxonomy_vars() {
1607 global $wp_taxonomies;
1608
1609 $taxonomy_vars = array();
1610 foreach ( $wp_taxonomies as $t ) {
1611 if ( $t->query_var ) {
1612 $taxonomy_vars[] = $t->query_var;
1613 }
1614 }
1615
1616 // still needed?
1617 $taxonomy_vars[] = 'tag_id';
1618
1619 return $taxonomy_vars;
1620 }
1621
1622 /**
1623 * Update the $query_args array with the parameters provided via AJAX/GET.
1624 *
1625 * @param array $query_args - the query args.
1626 * @filter infinite_scroll_query_args
1627 * @return array
1628 */
1629 public function inject_query_args( $query_args ) {
1630 /**
1631 * Filter the array of allowed Infinite Scroll query arguments.
1632 *
1633 * @module infinite-scroll
1634 *
1635 * @since 2.6.0
1636 *
1637 * @param array $args Array of allowed Infinite Scroll query arguments.
1638 * @param array $query_args Array of query arguments.
1639 */
1640 $allowed_vars = apply_filters( 'infinite_scroll_allowed_vars', array(), $query_args );
1641
1642 $query_args = array_merge(
1643 $query_args,
1644 array(
1645 'suppress_filters' => false,
1646 )
1647 );
1648
1649 if ( isset( $_REQUEST['query_args'] ) && is_array( $_REQUEST['query_args'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
1650 foreach ( wp_unslash( $_REQUEST['query_args'] ) as $var => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- no site changes, sanitized below.
1651 if ( in_array( $var, $allowed_vars, true ) && ! empty( $value ) ) {
1652 $query_args[ $var ] = filter_var( $value );
1653 }
1654 }
1655 }
1656
1657 return $query_args;
1658 }
1659
1660 /**
1661 * Rendering fallback used when themes don't specify their own handler.
1662 *
1663 * @uses have_posts, the_post, get_template_part, get_post_format
1664 * @action infinite_scroll_render
1665 */
1666 public function render() {
1667 while ( have_posts() ) {
1668 the_post();
1669
1670 get_template_part( 'content', get_post_format() );
1671 }
1672 }
1673
1674 /**
1675 * Allow plugins to filter what archives Infinite Scroll supports
1676 *
1677 * @uses current_theme_supports, is_home, is_archive, apply_filters, self::get_settings
1678 * @return bool
1679 */
1680 public static function archive_supports_infinity() {
1681 $supported = current_theme_supports( 'infinite-scroll' ) && ( is_home() || is_archive() || is_search() );
1682
1683 // Disable when previewing a non-active theme in the customizer
1684 if ( is_customize_preview() && ! $GLOBALS['wp_customize']->is_theme_active() ) {
1685 return false;
1686 }
1687
1688 /**
1689 * Allow plugins to filter what archives Infinite Scroll supports.
1690 *
1691 * @module infinite-scroll
1692 *
1693 * @since 2.0.0
1694 *
1695 * @param bool $supported Does the Archive page support Infinite Scroll.
1696 * @param object self::get_settings() IS settings provided by theme.
1697 */
1698 return (bool) apply_filters( 'infinite_scroll_archive_supported', $supported, self::get_settings() );
1699 }
1700
1701 /**
1702 * The Infinite Blog Footer
1703 *
1704 * @uses self::get_settings, self::archive_supports_infinity, self::default_footer
1705 * @return string or null
1706 */
1707 public function footer() {
1708 if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
1709 return;
1710 }
1711
1712 // Bail if theme requested footer not show
1713 if ( false === self::get_settings()->footer ) {
1714 return;
1715 }
1716
1717 // We only need the new footer for the 'scroll' type
1718 if ( 'scroll' !== self::get_settings()->type || ! self::archive_supports_infinity() ) {
1719 return;
1720 }
1721
1722 if ( self::is_last_batch() ) {
1723 return;
1724 }
1725
1726 // Display a footer, either user-specified or a default
1727 if ( false !== self::get_settings()->footer_callback && is_callable( self::get_settings()->footer_callback ) ) {
1728 call_user_func( self::get_settings()->footer_callback, self::get_settings() );
1729 } else {
1730 self::default_footer();
1731 }
1732 }
1733
1734 /**
1735 * Render default IS footer
1736 *
1737 * @uses __, wp_get_theme, apply_filters, home_url, esc_attr, get_bloginfo, bloginfo
1738 */
1739 private function default_footer() {
1740 if ( '' !== get_privacy_policy_url() ) {
1741 $credits = get_the_privacy_policy_link() . '<span role="separator" aria-hidden="true"> / </span>';
1742 } else {
1743 $credits = '';
1744 }
1745 $credits .= sprintf(
1746 '<a href="https://wordpress.org/" rel="noopener noreferrer" target="_blank" rel="generator">%1$s</a> ',
1747 __( 'Proudly powered by WordPress', 'jetpack' )
1748 );
1749 $credits .= sprintf(
1750 /* translators: %1$s is the name of a theme */
1751 __( 'Theme: %1$s.', 'jetpack' ),
1752 wp_get_theme()->Name
1753 );
1754 /**
1755 * Filter Infinite Scroll's credit text.
1756 *
1757 * @module infinite-scroll
1758 *
1759 * @since 2.0.0
1760 *
1761 * @param string $credits Infinite Scroll credits.
1762 */
1763 $credits = apply_filters( 'infinite_scroll_credit', $credits );
1764
1765 ?>
1766 <div id="infinite-footer">
1767 <div class="container">
1768 <div class="blog-info">
1769 <a id="infinity-blog-title" href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
1770 <?php bloginfo( 'name' ); ?>
1771 </a>
1772 </div>
1773 <div class="blog-credits">
1774 <?php echo $credits; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
1775 </div>
1776 </div>
1777 </div><!-- #infinite-footer -->
1778 <?php
1779 }
1780
1781 /**
1782 * Ensure that IS doesn't interfere with Grunion by stripping IS query arguments from the Grunion redirect URL.
1783 * When arguments are present, Grunion redirects to the IS AJAX endpoint.
1784 *
1785 * @param string $url - the Grunion redirect URL.
1786 * @uses remove_query_arg
1787 * @filter grunion_contact_form_redirect_url
1788 * @return string
1789 */
1790 public function filter_grunion_redirect_url( $url ) {
1791 // Remove IS query args, if present
1792 if ( str_contains( $url, 'infinity=scrolling' ) ) {
1793 $url = remove_query_arg(
1794 array(
1795 'infinity',
1796 'action',
1797 'page',
1798 'order',
1799 'scripts',
1800 'styles',
1801 ),
1802 $url
1803 );
1804 }
1805
1806 return $url;
1807 }
1808
1809 /**
1810 * When the MediaElement is loaded in dynamically, we need to enforce that
1811 * its settings are added to the page as well.
1812 *
1813 * @param array $scripts_data New scripts exposed to the infinite scroll.
1814 *
1815 * @since 8.4.0
1816 */
1817 public function add_mejs_config( $scripts_data ) {
1818 foreach ( $scripts_data as $key => $data ) {
1819 if ( 'mediaelement-core' === $data['handle'] ) {
1820 $mejs_settings = array(
1821 'pluginPath' => includes_url( 'js/mediaelement/', 'relative' ),
1822 'classPrefix' => 'mejs-',
1823 'stretching' => 'responsive',
1824 );
1825
1826 $scripts_data[ $key ]['extra_data'] = sprintf(
1827 'window.%s = %s',
1828 '_wpmejsSettings',
1829 wp_json_encode( apply_filters( 'mejs_settings', $mejs_settings ) )
1830 );
1831 }
1832 }
1833 return $scripts_data;
1834 }
1835
1836 /**
1837 * Determines whether the legacy AMP Reader post templates are being used.
1838 *
1839 * @return bool
1840 */
1841 private function is_exempted_amp_page() {
1842 if ( is_singular( 'web-story' ) ) {
1843 // Ensure that <amp-next-page> is not injected after <amp-story> as generated by the Web Stories plugin.
1844 return true;
1845 }
1846 if ( function_exists( 'amp_is_legacy' ) ) {
1847 // Available since AMP v2.0, this will return false if a theme like Twenty Twenty is selected as the Reader theme.
1848 return amp_is_legacy();
1849 }
1850 if ( method_exists( 'AMP_Options_Manager', 'get_option' ) ) {
1851 // In versions prior to v2.0, checking the template mode as being 'reader' is sufficient.
1852 return 'reader' === AMP_Options_Manager::get_option( 'theme_support' );
1853 }
1854 return false;
1855 }
1856
1857 /**
1858 * Load AMP specific hooks.
1859 *
1860 * @return void
1861 */
1862 public function amp_load_hooks() {
1863 if ( $this->is_exempted_amp_page() ) {
1864 return;
1865 }
1866
1867 if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
1868 $template = self::get_settings()->render;
1869
1870 add_filter( 'jetpack_infinite_scroll_load_scripts_and_styles', '__return_false' );
1871
1872 add_action( 'template_redirect', array( $this, 'amp_start_output_buffering' ), 0 );
1873 add_action( 'shutdown', array( $this, 'amp_output_buffer' ), 1 );
1874
1875 if ( is_string( $template ) && strpos( $template, '::' ) === false && is_callable( "amp_{$template}_hooks" ) ) {
1876 call_user_func( "amp_{$template}_hooks" );
1877 }
1878
1879 // Warms up the amp next page markup.
1880 // This should be done outside the output buffering callback started in the template_redirect.
1881 $this->amp_get_footer_template();
1882 }
1883 }
1884
1885 /**
1886 * Start the AMP output buffering.
1887 *
1888 * @return void
1889 */
1890 public function amp_start_output_buffering() {
1891 ob_start( array( $this, 'amp_finish_output_buffering' ) );
1892 }
1893
1894 /**
1895 * Flush the AMP output buffer.
1896 *
1897 * @return void
1898 */
1899 public function amp_output_buffer() {
1900 if ( ob_get_contents() ) {
1901 ob_end_flush();
1902 }
1903 }
1904
1905 /**
1906 * Filter the AMP output buffer contents.
1907 *
1908 * @param string $buffer Contents of the output buffer.
1909 *
1910 * @return string|false
1911 */
1912 public function amp_finish_output_buffering( $buffer ) {
1913 // Hide WordPress admin bar on next page load.
1914 $buffer = preg_replace(
1915 '/id="wpadminbar"/',
1916 '$0 next-page-hide',
1917 $buffer
1918 );
1919
1920 /**
1921 * Get the theme footers.
1922 *
1923 * @module infinite-scroll
1924 *
1925 * @since 9.0.0
1926 *
1927 * @param array array() An array to store multiple markup entries to be added to the footer.
1928 * @param string $buffer The contents of the output buffer.
1929 */
1930 $footers = apply_filters( 'jetpack_amp_infinite_footers', array(), $buffer );
1931
1932 /**
1933 * Filter the output buffer.
1934 * Themes can leverage this hook to add custom markup on next page load.
1935 *
1936 * @module infinite-scroll
1937 *
1938 * @since 9.0.0
1939 *
1940 * @param string $buffer The contents of the output buffer.
1941 */
1942 $buffer = apply_filters( 'jetpack_amp_infinite_output', $buffer );
1943
1944 // Add the amp next page markup.
1945 $buffer = preg_replace(
1946 '~</body>~',
1947 $this->amp_get_footer_template( $footers ) . '$0',
1948 $buffer
1949 );
1950
1951 return $buffer;
1952 }
1953
1954 /**
1955 * Get AMP next page markup with the custom footers.
1956 *
1957 * @param string[] $footers The theme footers.
1958 *
1959 * @return string
1960 */
1961 protected function amp_get_footer_template( $footers = array() ) {
1962 static $template = null;
1963
1964 if ( null === $template ) {
1965 $template = $this->amp_footer_template();
1966 }
1967
1968 if ( empty( $footers ) ) {
1969 return $template;
1970 }
1971
1972 return preg_replace(
1973 '/%%footer%%/',
1974 implode( '', $footers ),
1975 $template
1976 );
1977 }
1978
1979 /**
1980 * AMP Next Page markup.
1981 *
1982 * @return string
1983 */
1984 protected function amp_footer_template() {
1985 ob_start();
1986 ?>
1987 <amp-next-page max-pages="<?php echo esc_attr( static::amp_get_max_pages() ); ?>">
1988 <script type="application/json">
1989 [
1990 <?php echo wp_json_encode( $this->amp_next_page() ); ?>
1991 ]
1992 </script>
1993 <div separator>
1994 <?php
1995 echo wp_kses_post(
1996 /**
1997 * AMP infinite scroll separator.
1998 *
1999 * @module infinite-scroll
2000 *
2001 * @since 9.0.0
2002 *
2003 * @param string '' The markup for the next page separator.
2004 */
2005 apply_filters( 'jetpack_amp_infinite_separator', '' )
2006 );
2007 ?>
2008 </div>
2009 <div recommendation-box class="recommendation-box">
2010 <template type="amp-mustache">
2011 {{#pages}}
2012 <?php
2013 echo wp_kses_post(
2014 /**
2015 * AMP infinite scroll older posts markup.
2016 *
2017 * @module infinite-scroll
2018 *
2019 * @since 9.0.0
2020 *
2021 * @param string '' The markup for the older posts/next page.
2022 */
2023 apply_filters( 'jetpack_amp_infinite_older_posts', '' )
2024 );
2025 ?>
2026 {{/pages}}
2027 </template>
2028 </div>
2029 <div footer>
2030 %%footer%%
2031 </div>
2032 </amp-next-page>
2033 <?php
2034 return ob_get_clean();
2035 }
2036
2037 /**
2038 * Get the AMP next page information.
2039 *
2040 * @return array
2041 */
2042 protected function amp_next_page() {
2043 $title = '';
2044 $url = '';
2045 $image = '';
2046
2047 if ( ! static::amp_is_last_page() ) {
2048 $title = sprintf(
2049 '%s - %s %d - %s',
2050 wp_title( '', false ),
2051 __( 'Page', 'jetpack' ),
2052 max( get_query_var( 'paged', 1 ), 1 ) + 1,
2053 get_bloginfo( 'name' )
2054 );
2055 $url = get_next_posts_page_link();
2056 }
2057
2058 $next_page = array(
2059 'title' => $title,
2060 'url' => $url,
2061 'image' => $image,
2062 );
2063
2064 /**
2065 * The next page settings.
2066 * An array containing:
2067 * - title => The title to be featured on the browser tab.
2068 * - url => The URL of next page.
2069 * - image => The image URL. A required AMP setting, not in use currently. Themes are welcome to leverage.
2070 *
2071 * @module infinite-scroll
2072 *
2073 * @since 9.0.0
2074 *
2075 * @param array $next_page The contents of the output buffer.
2076 */
2077 return apply_filters( 'jetpack_amp_infinite_next_page_data', $next_page );
2078 }
2079
2080 /**
2081 * Get the number of pages left.
2082 *
2083 * @return int
2084 */
2085 protected static function amp_get_max_pages() {
2086 global $wp_query;
2087
2088 return (int) $wp_query->max_num_pages - $wp_query->query_vars['paged'];
2089 }
2090
2091 /**
2092 * Is the last page.
2093 *
2094 * @return bool
2095 */
2096 protected static function amp_is_last_page() {
2097 return 0 === static::amp_get_max_pages();
2098 }
2099 }
2100
2101 /**
2102 * Initialize The_Neverending_Home_Page
2103 */
2104 function the_neverending_home_page_init() {
2105 if ( ! current_theme_supports( 'infinite-scroll' ) ) {
2106 return;
2107 }
2108
2109 new The_Neverending_Home_Page();
2110 }
2111 add_action( 'init', 'the_neverending_home_page_init', 20 );
2112
2113 /**
2114 * Check whether the current theme is infinite-scroll aware.
2115 * If so, include the files which add theme support.
2116 */
2117 function the_neverending_home_page_theme_support() {
2118 if (
2119 defined( 'IS_WPCOM' ) && IS_WPCOM &&
2120 defined( 'REST_API_REQUEST' ) && REST_API_REQUEST &&
2121 ! doing_action( 'restapi_theme_after_setup_theme' )
2122 ) {
2123 // Don't source theme compat files until we're in the site's context
2124 return;
2125 }
2126 $theme_name = get_stylesheet();
2127
2128 /**
2129 * Filter the path to the Infinite Scroll compatibility file.
2130 *
2131 * @module infinite-scroll
2132 *
2133 * @since 2.0.0
2134 *
2135 * @param string $str IS compatibility file path.
2136 * @param string $theme_name Theme name.
2137 */
2138 $customization_file = apply_filters( 'infinite_scroll_customization_file', __DIR__ . "/themes/{$theme_name}.php", $theme_name );
2139
2140 if ( is_readable( $customization_file ) ) {
2141 require_once $customization_file;
2142 }
2143 }
2144 add_action( 'after_setup_theme', 'the_neverending_home_page_theme_support', 5 );
2145
2146 /**
2147 * Early accommodation of the Infinite Scroll AJAX request
2148 */
2149 if ( The_Neverending_Home_Page::got_infinity() ) {
2150 /**
2151 * If we're sure this is an AJAX request (i.e. the HTTP_X_REQUESTED_WITH header says so),
2152 * indicate it as early as possible for actions like init
2153 */
2154 if ( ! defined( 'DOING_AJAX' ) &&
2155 isset( $_SERVER['HTTP_X_REQUESTED_WITH'] ) &&
2156 strtoupper( sanitize_text_field( wp_unslash( $_SERVER['HTTP_X_REQUESTED_WITH'] ) ) ) === 'XMLHTTPREQUEST'
2157 ) {
2158 define( 'DOING_AJAX', true );
2159 }
2160
2161 // Don't load the admin bar when doing the AJAX response.
2162 show_admin_bar( false );
2163 }
2164