WordPress tip: A simple way to search all post types

I love WordPress, but its huge designer/developer community and extensible structure have made it possible to over-engineer a solution to just about every problem. And then under-document that solution.

Case in point: today I needed to add the ability to search across custom post types, along with pages. But by default search only searches posts. (That is, the “post” post type. Are you with me?)

This isn’t a new problem, even to me, although very few of the sites I build have (or need) internal search. It’s just not that useful on a site that doesn’t have hundreds of pages or posts, and most of the sites I build don’t.

In the few times in the past when I needed to be able to search across other post types, or other content like taxonomy data, I’ve relied on the Search Everything plugin. And judging by the fact that (as of today) it’s been downloaded 555,309 times, clearly I am not alone.

It’s a pretty good plugin, as plugins go. But it can be overkill, especially if all you need is the ability to search other post types.

And that’s where we run into the real, multifaceted problem with WordPress for developers: 1) there’s a plugin (no, make that dozens of plugins) for just about every obscure task, and 2) there are also several ways to go about building your own custom solution, especially if you’re building your own theme, but 3) the documentation is all over the place, and none of it is comprehensive.

Granted, by offering a targeted solution to a very specific problem in this blog post, I’m contributing to that documentation fragmentation, but whaddayagonnado.

There’s a fourth (and probably even more important) facet as well: plugins are developed independently by countless individuals (of varying degrees of skill), and it’s impossible for anyone to test them all for interoperability. The more plugins you install — especially if they’re excessively complex for the problem you’re intending to solve — the greater the chance you’ll introduce an incompatibility that will break your site. So it’s in your best interest to try to keep things as simple as possible. (And to err on the side of installing fewer plugins.)

tl;dr

If I understand correctly, every parameter in WP_Query can be passed in the query string, which means you can add corresponding input fields into searchform.php in your theme to modify the search functionality.

OK, now that was too simple (and abstract). Give me an example I can work with.

Here’s a one-line solution to get WordPress to search all of your post types (even custom post types), not just “post”-type posts. Add this into the form in searchform.php:

<input type="hidden" name="post_type" value="any" />

Guess what… you can also specify multiple specific types of posts using PHP’s method of using square brackets in your input name to pass in data as an array, like this:

<input type="hidden" name="post_type[]" value="foo" />
<input type="hidden" name="post_type[]" value="bar" />
<input type="hidden" name="post_type[]" value="baz" />

A caveat: I tried the above with page as one of the values and it didn’t work; it showed my custom post types, but not “page”-type posts. I suspect it’s because that’s one of the predefined type parameters that make the query behave slightly differently. So this solution isn’t perfect, but using any as the value will work: it gets “post”, “page” and your custom post types.

My goal was to have a simple search form that would just search all post types, so I made this a hidden field, but you could make it radio buttons, checkboxes or a select menu if you wanted to let the user pick, and this just scratches the surface of what you can do to customize your search form to leverage the capabilities of WP_Query.

Nintendo: 2 Darn Stubborn?

Since the newly announced Nintendo 2DS (yes, you read that correctly) is obviously targeted at a young audience, I censored the title of this post. Kotaku already won the battle for best title anyway.

Source: NintendoMy first reaction, upon hearing the name “2DS” was “What the hell?” My second reaction, upon seeing a picture of it, was “No, seriously… what the hell?”

I have been a Nintendo defender for a long time. I love Nintendo. My kids and I waited in line outside the Richfield Best Buy last fall to get a Wii U at midnight when it was being released.

Now, a few fun games aside, all of us think the Wii U itself is kind of a P.O.S., but that’s not even the point. Except, it kind of is.

Nintendo used to be the king of the video game world. They dominated the late ’80s and early ’90s. After faltering a bit, they roared back in the mid-2000s with the original Wii. But then the world changed on them. The iPhone happened. And suddenly Nintendo was Wile E. Coyote running off a cliff. Except when they looked down, they didn’t suddenly realize the ground beneath them was gone. They just kept right on running… into a strange world where all known laws of physics no longer apply.

The Wii U is a bit of a muddled mess, but its main failing is the poor user experience of its horribly designed system software. But it was indicative of the larger problem Nintendo currently has… it has become dangerously (to its own future) out-of-touch with how people are using not just video games, but technology devices in general. The 2DS seems like perhaps they have crossed a point of no return.

I “get” the 2DS. It’s designed to address a few very specific problems, all revolving around the fact that Nintendo’s core audience, especially for handhelds, tends to be young… single-digit young. The 3DS, Nintendo’s current flagship handheld system, has three problems with that audience:

1. It’s fragile.
2. It’s (kind of) expensive.
3. Its 3D effects can be harmful to young eyes.

Little kids break things. The delicate plastic hinges on the traditional clamshell DS designs are a perfect example. Parents don’t want to spend $150-$200 on a device their young child will break easily. And for ocular health, Nintendo themselves discourage use of the 3D effect on the 3DS for those under 7 years old. Parents can disable the 3D effect entirely, but it’s a cumbersome process.

Enter the 2DS: No hinge. Comparatively cheap at $129. And no 3D. Problem(s) solved, right? Except… targeting those specific issues has led to this monstrosity. Something that could only be created by a combination of focus group feedback and head-in-the-sand corporate executives, deliberately ignoring everything that’s happening in the world around them, denying the true source of the rot eating away at their company’s business model.

Set aside the Playstation Vita for a minute (since everyone pretty much has, amirite?)… there is one primary competitor to the Nintendo DS family for young portable video game enthusiasts: the iPod touch. There are plenty of reasons a parent might choose to get their kid an iPod touch, besides the obvious fact that the kid really wants one. But perhaps the most compelling factor is that the parents themselves already own iPhones. The iPod touch, after all, is pretty much just an iPhone without the phone. (And GPS, and a few other features, but you get my point.)

iOS is already familiar to these parents, so they can relate to their child’s experience. And more importantly, these parents understand the App Store, which is really the single reason why I believe Nintendo as it currently functions is doomed.

Let’s look at three more potential problem factors for the Nintendo DS family:

1. Its games are expensive ($30-$40 each).
2. Its game media can get lost.
3. Its games can only be used on a single device at a time.

True, the iPod touch starts at $229, a full $100 more than the 2DS. But buy just three games for the 2DS and you’re up to the price of an iPod touch. Granted, Nintendo has created an equivalent to the App Store for the DS line, but its selection of games is pitifully small compared to the iOS App Store, and many of those games are iOS ports! Even the best, deepest, biggest-budget iOS games rarely break the $20 barrier, and most are priced somewhere between free and $3.

Every subsequent generation of Nintendo handheld has seen its game media shrink in size, from the fairly large cartridges of the original Game Boy to the tiny SD-like cards of the DS line. They’re more portable, but in some ways smaller is worse… as any parent whose kids bring their DS on car trips will tell you, the games are incredibly easy to misplace, and at up to $40 each that’s an expensive scenario. And, of course, a physical game can only be played on one device at a time. Even Nintendo’s eShop is built around a ridiculous model where you can’t transfer purchases between devices.

Contrast that with the iOS App Store. There are no physical media to keep track of, anyone on the same App Store account can download (and re- download) apps to their device without re-purchasing, and you’re not limited to the single device you originally bought the app on.

So while Apple (and Android) reinvented the world of mobile gaming, what did Nintendo do? They continued to drift into this strange territory of weird proprietary hardware, trying to create a unique experience by building devices, and games around them, that would be impossible anywhere else. That’s great, I guess… if any of it really made any sense. And never has it been clearer just how little sense it all makes than with the 2DS.

What is Nintendo’s greatest asset? Not its “unique” game hardware. It’s the intellectual property of great franchises like Mario, Zelda, Metroid and Pokémon. For decades now, Nintendo has sustained a (more or less) thriving business by making these must-have games and then selling the only hardware anyone can play them on.

But times have changed. The video game landscape is so different now, that I don’t think these legendary franchises are enough to carry Nintendo’s increasingly absurd hardware business any longer. I’ve been saying for years that Nintendo needs to do what Sega did… get out of the hardware business and start putting their games on other companies’ devices. This will mean a leaner, smaller Nintendo, but I bet they could wring just as much or more profit from selling their games on other systems as from building and selling their own.

Put Mario, Link, Samus or (God help me) Pikachu on my iPhone, and I will buy it in a second. But this crazy new hardware Nintendo keeps dreaming up? I’m not buying it anymore. And, for the first time, neither are my kids.

What’s so Neue about Helvetica?

fonts
So, I was just reading Rani Molla’s post on GigaOM called What’s all the fuss about Apple and Helvetica Neue? and I felt compelled (as I so often do, about so many things) to comment on the issue here.

Contrary to how the GigaOM article seems to frame it, the controversy — the, if you will, fontroversy (I regret it already) — when Apple demoed iOS 7 at WWDC last month was not that they were switching to Helvetica Neue as the iOS 7 system font. It’s that they were switching to Helvetica Neue Ultra Light, a particularly delicate weight of the general Helvetica Neue font family. (I’ve read some things that suggest they’re reversing course on that decision based on developer feedback, but the GigaOM post doesn’t even touch that.)

The fact is, Helvetica Neue has been the iOS system font ever since the introduction of the iPhone 4. When the iPhone was first introduced, it used plain old Helvetica as the system font. But with the introduction of the Retina Display, Apple switched to the slightly more refined Helvetica Neue.

So the concern with iOS 7 is not Helvetica Neue itself — that’s been working out just fine. It’s this extra thin weight of the font, which becomes difficult to read at smaller sizes.

Personally I like Helvetica Neue Ultra Light. I think it continues the trend towards refinement Apple began with the switch to Helvetica Neue itself, and is demonstrated effectively in Cabel Sasser’s animated GIF featured in the GigaOM article. The version using Helvetica Neue Regular feels heavier and clunkier to me. That said, I do understand and appreciate the legibility concerns with Ultra Light at very small sizes.

I’m not sure how this will work itself out. I doubt Apple will switch to a different typeface, though they may increase the weight of the font in the final version of iOS 7. But part of the reason Apple went with Helvetica in the first place is that it’s neutral (at least in principle). It gets out of the way and isn’t distracting. It doesn’t convey any particular personality. It’s a “blank canvas” of a font, which makes it a perfect fit for iOS devices, where the device itself disappears and becomes the app you’re using. Developers don’t have to use the system font in their apps, but a lot of them do, and by keeping the system font as neutral as possible, Apple avoids predisposing apps to a certain personality or style.

This is exactly the opposite of the opinions expressed in the closing of the GigaOM article, and is I think the opposite of Apple’s intentions with the iOS experience. Using a custom font that “reinforces a more distinctive brand voice” would be the equivalent of sticking a big Apple logo on the front of the iPhone. Apple’s branding goes on the back (where it can be an effective marketing tool). It’s never a part of the user experience.

Maintaining session between SSL and non-SSL pages in CakePHP

It’s funny, in a way, that cms34 has been around for nearly five years now, and it’s only just become a real issue that we were not maintaining sessions between SSL and non-SSL pages. This is somewhat understandable: practically speaking, the only time it really matters to carry over the session between the two is when a user is logged in. (This might not be the case with all web applications, but for us, at least, there’s rarely enough happening in the session when a user is not logged in for it to matter.)

As it happens, not that many cms34 sites use SSL; not that many cms34 sites use the user login capability on the front end. And very few use both. But we’ve had a couple of new sites come online lately that do use both, and it’s become a bit of an issue.

The issue was exacerbated by the fact that I recently modified the Users controller to require SSL on the login page, if the site has an SSL certificate. Consequently there were issues with trying to access login-restricted, but non-SSL pages… redirect loops and other such fun.

What’s the problem?

The problem is simple: for obvious security reasons, sessions and cookies cannot be shared directly between two different domains. It’s possible (although less secure) to share them between both SSL and non-SSL on the same domain, and it’s also relatively easy to set them up to work between different subdomains. But if your SSL pages use a different domain name than the non-SSL pages, even if they’re on the same server, there’s no way to get them to automatically use the same session.

The solution (though still not ideal, as it can introduce the risk of session hijacking), as you’ll find in lots of places, is to pass the session ID as a query string variable. Then you can use that to restore the same session ID, even if it’s on another domain — as long as it’s on the same physical server.

Some improvements

There are two key improvements I made to the basic “pass the session ID in the query string” scenario.

First, when the session is created I am writing the user’s IP address (using $_SERVER['REMOTE_ADDR']) as a session variable. Then, when I am attempting to restore the session with the ID passed as a query string variable, I read the session file on the server first, and make sure the IP address in the file matches still matches the user’s current IP address. Only then do I restore the session.

Second, and this is an aesthetic issue almost as much as a security one, once the session has been re-established, and before any response has been sent, I strip the session ID out of the requested URL and redirect to that new URL. It’s all invisible to the user, and the session ID never actually appears in the browser’s address bar.

A look at the code

There’s a lot going on in the cms34 code, much of which is highly specific to this application. But in short the keys to making this work happen in two places:

UsersController::login()

I have a login() action in UsersController that handles all of the special functionality that needs to happen when a user logs in. The actual login itself happens “automagically” via AuthComponent, but Auth doesn’t know everything I need to have happen when a user logs in, so after Auth does its work, my login() action takes it from there.

Honestly not a lot needs to happen here to make this work. Just two things: you have to write the user’s IP address to the session as I noted above, and you have to pass the session ID in a query string variable on the redirect that happens when login is successful. My code looks a little something like this (note that I have an array in the session called Misc that I use for… miscellaneous stuff like this):

class UsersController extends AppController {

  var $name = 'Users'
  // Other controller variables go here, of course.

  function login() {

    // All of this should only run if AuthComponent has already logged the user in.
    // Your session variable names may vary.
    if ($this->Session->read('Auth.User')) {

      // Various session prep stuff happens here.

      // Write IP address to session (used to verify user when restoring session).
      $this->Session->write('Misc.remote_addr(',$_SERVER['REMOTE_ADDR']);

      // Some conditionals for special redirects come here but we'll skip that.

      // Redirect user to home page, with session ID in query string.
      // Make up a query string variable that makes sense for you.
      $this->redirect('/?cms34sid=' . session_id());

    }
  }
}

So far, so good. The rest of the excitement happens in…

AppController::beforeFilter()

Ah yes, the magical beforeFilter() method. There’s a whole lot of stuff going on in AppController::beforeFilter() in cms34, most of which is highly specific to our application. But this is where you will need to put your code to retrieve the session ID from the query string and restore the session… this function runs at the beginning of every page load on your site.

I’ve put this logic almost at the beginning of beforeFilter(), because we really do want that session restored as soon as possible.

Here’s a look…

class AppController extends Controller {

  function beforeFilter() {

    // Additional code specific to your app will likely come before and after this.

    // Only run if session ID query string variable is passed and different from the existing ID.
    // Be sure to replace cms34sid with your actual query string variable name.
    if (!empty($this->params['url']['cms34sid']) && $this->params['url']['cms34sid'] != session_id()) {

      // Verify session file exists.
      // I am using CakeSession; your session files may be elsewhere.
      $session_file = TMP.DS.'sessions'.DS.'sess_'.$this->params['url']['cms34sid'];

      if (file_exists($session_file)) {
        $session_contents = file_get_contents($session_file);

        // Find user's IP address in session file data (to verify identity).
        // The CakePHP session file stores a set of serialized arrays; we're reading raw serialized data.
        // If you used a different session variable name than remote_addr, change it AND the 11 to its string length.
        $session_match = 's:11:"remote_addr";s:'.strlen($_SERVER['REMOTE_ADDR']).':"'.$_SERVER['REMOTE_ADDR'] .'";';

        // User's IP address is in session file; so we can continue.
        if (strpos($session_contents,$session_match) !== false) {

          // Set session ID to restore session
          $this->Session->id($this->params['url']['cms34sid']);

          // Redirect to this same URL without session ID in query string
          $current_url = rtrim(preg_replace('/cms34sid=[^&]+[&]?/','',current_url()),'?&');
          $this->redirect($current_url);
        }
      }
    }
  }
}

A few final thoughts

I didn’t really discuss cookies at all here, but suffice to say there’s a cookie containing the session ID that gets written. If you’re only using cookies for the session ID (which is probably a good idea), then you don’t really need to do anything else with them. But if you’re writing certain cookies when a user logs in (like I do), you’ll need to write additional logic to restore them in AppController::beforeFilter(). In my case, the relevant cookies are all duplicates of session data, but are intended for a few edge cases where I need to access that information through JavaScript or in .htaccess files that are protecting login-restricted downloadable files — in other words, places where I can’t use PHP to look at session data.

You may also notice near the end of the code in the AppController::beforeFilter() example above that I am calling a function called current_url(). This is not a built-in part of PHP or CakePHP; it’s a simple little function I have in my config/functions.php file. Here it is:

function current_url() {
  return (!empty($_SERVER['HTTPS']) ? 'https://' : 'http://') . $_SERVER['SERVER_NAME'] . $_SERVER['REQUEST_URI'];
}

From the Stupid PHP Tricks files: rounding numbers and creeping inaccuracy

This morning as I walked to the studio I was doing what geeks do best: pondering a slightly esoteric mathematical quandary.

Glass Half Full by S_novaIngraining the American spirit of optimism at a young age, and under dubious circumstances, our schools always taught rounding numbers in a peculiar way. You always round your decimal values to the nearest integer. That part makes sense. But what if the decimal is .5 — exactly half? In my education, at least until late in high school (or was it college?), we were always taught to round up! The glass is half full. Optimism.

Eventually — far later than it should have been, I think — the concept was introduced that always rounding .5 up is not really that accurate, statistically speaking. It might be nice in the case of a single number to be an optimist and think a solid half is good as a whole, but in aggregate this thinking introduces a problem.

If you have a whole lot of numbers, and you’re always rounding your halves up, eventually your totals are going to be grossly inaccurate.

Of course, the same would happen if you were ever the pessimist and always rounded down.

The solution, I later learned, was to round halves up or down, depending upon the integer value that precedes them. Which way you go doesn’t really matter, as long as you’re consistent, but as it happens, I learned it as such: if the integer is odd, round up; if it is even, round down.

In my work, I write a lot of PHP code. Most of it is of the extremely practical variety; I’m building websites for clients, after all. But every once in a while I like to indulge my coding abilities in a bit of frivolous experimentation, and so today I produced a little PHP script that generates 10,000 random numbers between 1 and 100, with one decimal place, and then it shows the actual sum and average of those numbers, along with what you get as the sum and average if you go through all 10,000 numbers and round them to whole integers by the various methods described above. Try it for yourself!

Any time the rounded average is different from the “precise” (and I use that term somewhat loosely) average, it is displayed in red. Interestingly, and not at all surprisingly, when you always round halves in one direction or the other, at least one of those directions will (almost) always yield an incorrect average. Yet if you use the “even or odd” methods, both of those methods will almost always yield a correct average.

It’s all about the aggregate.