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