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