PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 14.8
Jetpack – WP Security, Backup, Speed, & Growth v14.8
16.2-beta 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 All 501 releases
jetpack / modules / infinite-scroll / infinity.php

infinity.php in Jetpack – WP Security, Backup, Speed, & Growth 14.8, at modules/infinite-scroll/infinity.php

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