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