* @module infinite-scroll
do_action( 'infinite_scroll_empty' );
$results['type'] = 'empty';
} elseif ( $this->has_wrapper() ) {
$wrapper_classes = is_string( self::get_settings()->wrapper ) ? self::get_settings()->wrapper : 'infinite-wrap';
$wrapper_classes .= ' infinite-view-' . $page;
$wrapper_classes = trim( $wrapper_classes );
/* translators: %1$s is the page count */
__( 'Page: %1$d.', 'jetpack' ),
$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>';
// Fire wp_footer to ensure that all necessary scripts are enqueued. Output isn't used, but scripts are extracted in self::action_wp_footer.
while ( ob_get_length() ) {
if ( 'success' === $results['type'] ) {
$results['lastbatch'] = self::is_last_batch();
$results['currentday'] = $currentday;
// Loop through posts to capture sharing data for new posts loaded via Infinite Scroll
if ( 'success' === $results['type'] && function_exists( 'sharing_register_post_for_share_counts' ) ) {
global $jetpack_sharing_counts;
sharing_register_post_for_share_counts( get_the_ID() );
// If sharing counts are not initialized for any reason, we initialize them here.
if ( ! is_array( $jetpack_sharing_counts ) ) {
$jetpack_sharing_counts = array();
// Filter out non-string and non-integer values to avoid warnings with array_flip.
$flippable_jetpack_sharing_counts = array_filter(
return is_string( $value ) || is_int( $value );
$results['postflair'] = array_flip( $flippable_jetpack_sharing_counts ?? array() );
/** This action is already documented in modules/infinite-scroll/infinity.php */
do_action( 'infinite_scroll_empty' );
$results['type'] = 'empty';
* Filter the Infinite Scroll results.
* @module infinite-scroll
* @param array $results Array of Infinite Scroll results.
* @param array $query_args Array of main query arguments.
* @param WP_Query $wp_query WP Query.
apply_filters( 'infinite_scroll_results', $results, $query_args, self::wp_query() ),
null, // @phan-suppress-current-line PhanTypeMismatchArgumentProbablyReal -- It takes null, but its phpdoc only says int.
* Update the $allowed_vars array with the standard WP public and private
* query vars, as well as taxonomy vars
* @param array $allowed_vars - the allowed variables array.
* @filter infinite_scroll_allowed_vars
public function allowed_query_vars( $allowed_vars ) {
$allowed_vars += $wp->public_query_vars;
$allowed_vars += $wp->private_query_vars;
$allowed_vars += $this->get_taxonomy_vars();
foreach ( array_keys( $allowed_vars, 'paged', true ) as $key ) {
unset( $allowed_vars[ $key ] );
return array_unique( $allowed_vars );
* Returns an array of stock and custom taxonomy query vars
public function get_taxonomy_vars() {
$taxonomy_vars = array();
foreach ( $wp_taxonomies as $t ) {
$taxonomy_vars[] = $t->query_var;
$taxonomy_vars[] = 'tag_id';
* Update the $query_args array with the parameters provided via AJAX/GET.
* @param array $query_args - the query args.
* @filter infinite_scroll_query_args
public function inject_query_args( $query_args ) {
* Filter the array of allowed Infinite Scroll query arguments.
* @module infinite-scroll
* @param array $args Array of allowed Infinite Scroll query arguments.
* @param array $query_args Array of query arguments.
$allowed_vars = apply_filters( 'infinite_scroll_allowed_vars', array(), $query_args );
$query_args = array_merge(
'suppress_filters' => false,
if ( isset( $_REQUEST['query_args'] ) && is_array( $_REQUEST['query_args'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- no site changes.
foreach ( wp_unslash( $_REQUEST['query_args'] ) as $var => $value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- no site changes, sanitized below.
if ( in_array( $var, $allowed_vars, true ) && ! empty( $value ) ) {
$query_args[ $var ] = filter_var( $value );
* Rendering fallback used when themes don't specify their own handler.
* @uses have_posts, the_post, get_template_part, get_post_format
* @action infinite_scroll_render
public function render() {
get_template_part( 'content', get_post_format() );
* Allow plugins to filter what archives Infinite Scroll supports
* @uses current_theme_supports, is_home, is_archive, apply_filters, self::get_settings
public static function archive_supports_infinity() {
$supported = current_theme_supports( 'infinite-scroll' ) && ( is_home() || is_archive() || is_search() );
// Disable when previewing a non-active theme in the customizer
if ( is_customize_preview() && ! $GLOBALS['wp_customize']->is_theme_active() ) {
* Allow plugins to filter what archives Infinite Scroll supports.
* @module infinite-scroll
* @param bool $supported Does the Archive page support Infinite Scroll.
* @param object self::get_settings() IS settings provided by theme.
return (bool) apply_filters( 'infinite_scroll_archive_supported', $supported, self::get_settings() );
* The Infinite Blog Footer
* @uses self::get_settings, self::archive_supports_infinity, self::default_footer
public function footer() {
if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
$settings = self::get_settings();
// Bail if theme requested footer not show
if ( false === $settings->footer ) {
// We only need the new footer for the 'scroll' type
if ( 'scroll' !== $settings->type || ! self::archive_supports_infinity() ) {
if ( self::is_last_batch() ) {
// Display a footer, either user-specified or a default
if ( false !== $settings->footer_callback && is_callable( $settings->footer_callback ) ) {
call_user_func( $settings->footer_callback, $settings );
* Render default IS footer
* @uses __, wp_get_theme, apply_filters, home_url, esc_attr, get_bloginfo, bloginfo
private function default_footer() {
if ( '' !== get_privacy_policy_url() ) {
$credits = get_the_privacy_policy_link() . '<span role="separator" aria-hidden="true"> / </span>';
'<a href="https://wordpress.org/" rel="noopener noreferrer" target="_blank" rel="generator">%1$s</a> ',
__( 'Proudly powered by WordPress', 'jetpack' )
/* translators: %1$s is the name of a theme */
__( 'Theme: %1$s.', 'jetpack' ),
* Filter Infinite Scroll's credit text.
* @module infinite-scroll
* @param string $credits Infinite Scroll credits.
$credits = apply_filters( 'infinite_scroll_credit', $credits );
<div id="infinite-footer">
<a id="infinity-blog-title" href="<?php echo esc_url( home_url( '/' ) ); ?>" rel="home">
<?php bloginfo( 'name' ); ?>
<div class="blog-credits">
<?php echo $credits; //phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
</div><!-- #infinite-footer -->
* Ensure that IS doesn't interfere with Grunion by stripping IS query arguments from the Grunion redirect URL.
* When arguments are present, Grunion redirects to the IS AJAX endpoint.
* @param string $url - the Grunion redirect URL.
* @filter grunion_contact_form_redirect_url
public function filter_grunion_redirect_url( $url ) {
// Remove IS query args, if present
if ( str_contains( $url, 'infinity=scrolling' ) ) {
* When the MediaElement is loaded in dynamically, we need to enforce that
* its settings are added to the page as well.
* @param array $scripts_data New scripts exposed to the infinite scroll.
public function add_mejs_config( $scripts_data ) {
foreach ( $scripts_data as $key => $data ) {
if ( 'mediaelement-core' === $data['handle'] ) {
'pluginPath' => includes_url( 'js/mediaelement/', 'relative' ),
'classPrefix' => 'mejs-',
'stretching' => 'responsive',
$scripts_data[ $key ]['extra_data'] = sprintf(
wp_json_encode( apply_filters( 'mejs_settings', $mejs_settings ), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP )
* Determines whether the legacy AMP Reader post templates are being used.
private function is_exempted_amp_page() {
if ( is_singular( 'web-story' ) ) {
// Ensure that <amp-next-page> is not injected after <amp-story> as generated by the Web Stories plugin.
if ( function_exists( 'amp_is_legacy' ) ) {
// Available since AMP v2.0, this will return false if a theme like Twenty Twenty is selected as the Reader theme.
if ( method_exists( 'AMP_Options_Manager', 'get_option' ) ) {
// In versions prior to v2.0, checking the template mode as being 'reader' is sufficient.
return 'reader' === AMP_Options_Manager::get_option( 'theme_support' );
* Load AMP specific hooks.
public function amp_load_hooks() {
if ( $this->is_exempted_amp_page() ) {
if ( class_exists( 'Jetpack_AMP_Support' ) && Jetpack_AMP_Support::is_amp_request() ) {
$template = self::get_settings()->render;
add_filter( 'jetpack_infinite_scroll_load_scripts_and_styles', '__return_false' );
add_action( 'template_redirect', array( $this, 'amp_start_output_buffering' ), 0 );
add_action( 'shutdown', array( $this, 'amp_output_buffer' ), 1 );
if ( is_string( $template ) && strpos( $template, '::' ) === false && is_callable( "amp_{$template}_hooks" ) ) {
call_user_func( "amp_{$template}_hooks" );
// Warms up the amp next page markup.
// This should be done outside the output buffering callback started in the template_redirect.
$this->amp_get_footer_template();
* Start the AMP output buffering.
public function amp_start_output_buffering() {
ob_start( array( $this, 'amp_finish_output_buffering' ) );
* Flush the AMP output buffer.
public function amp_output_buffer() {
if ( ob_get_contents() ) {
* Filter the AMP output buffer contents.
* @param string $buffer Contents of the output buffer.
public function amp_finish_output_buffering( $buffer ) {
// Hide WordPress admin bar on next page load.
* @module infinite-scroll
* @param array array() An array to store multiple markup entries to be added to the footer.
* @param string $buffer The contents of the output buffer.
$footers = apply_filters( 'jetpack_amp_infinite_footers', array(), $buffer );
* Filter the output buffer.
* Themes can leverage this hook to add custom markup on next page load.
* @module infinite-scroll
* @param string $buffer The contents of the output buffer.
$buffer = apply_filters( 'jetpack_amp_infinite_output', $buffer );
// Add the amp next page markup.
$this->amp_get_footer_template( $footers ) . '$0',
* Get AMP next page markup with the custom footers.
* @param string[] $footers The theme footers.
protected function amp_get_footer_template( $footers = array() ) {
if ( null === $template ) {
$template = $this->amp_footer_template();
if ( empty( $footers ) ) {
protected function amp_footer_template() {
<amp-next-page max-pages="<?php echo esc_attr( static::amp_get_max_pages() ); ?>">
<script type="application/json">
<?php echo wp_json_encode( $this->amp_next_page(), JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ); ?>