Category: web

  • wp_unslash()

    A Legacy That Won’t Die

    If you’ve ever saved form data in WordPress and watched in horror as “O’Reilly” becomes “O\’Reilly” in your database, you’ve encountered one of the framework’s most confusing behaviors. This isn’t a bug. It’s a deliberate choice that WordPress makes every single time it boots, and understanding why requires traveling back to PHP’s most controversial feature.

    PHP’s Magic Quotes

    In the early 2000s, PHP introduced a feature called magic quotes. The reasoning seemed sound at the time: automatically escape all incoming user data to protect novice developers from SQL injection attacks. When a user submitted a form with “O’Reilly” in a text field, PHP would silently transform it into “O\’Reilly” before your code ever saw it. The backslash escaped the quote, theoretically making it safe to insert into SQL queries.

    The problem was that magic quotes solved one problem while creating dozens of others. Developers who knew what they were doing had to constantly check whether magic quotes were enabled on a particular server, leading to this pattern scattered throughout PHP applications:

    if ( get_magic_quotes_gpc() ) {
        $value = stripslashes( $_POST['name'] );
    } else {
        $value = $_POST['name'];
    }

    Different servers had different configurations. The same code behaved differently depending on whether the hosting provider had enabled magic quotes in php.ini. Worse, the feature didn’t actually provide real security, it was a band-aid on a wound that needed proper treatment through prepared statements and parameterized queries.

    The PHP community eventually recognized this mistake. PHP deprecated magic quotes in version 5.3, removed them in 5.4, and eliminated even the detection functions by PHP 8. The feature was universally acknowledged as a failed experiment, and good riddance.

    WordPress’s Fateful Decision

    Here’s where WordPress diverged from the rest of the PHP world. Instead of celebrating the death of magic quotes, WordPress decided to preserve them. Not just preserve them, actively implement them regardless of PHP version or configuration.

    This happens through a function called wp_magic_quotes() that runs during WordPress’s boot process. Open wp-settings.php and you’ll find it on line 587 (at the time of writing), executing after plugins load but before themes initialize. At this moment, WordPress deliberately adds slashes to every value in $_GET$_POST$_COOKIE, and $_SERVER.

    Why would WordPress do this? The answer lies in backward compatibility and the sheer scale of the ecosystem. By 2012, when magic quotes were removed from PHP, thousands of WordPress plugins and themes had been written assuming input data would arrive pre-escaped. Core WordPress functions expected slashed data. Removing this behavior would create security vulnerabilities throughout the ecosystem as code that expected escaped data suddenly received raw input.

    The WordPress core team faced an impossible choice: break backward compatibility and potentially create security holes in thousands of sites, or maintain the legacy behavior and confuse every new developer who encounters it. They chose compatibility. In their view, one confused developer is better than one compromised website.

    The Consequences We Live With

    This decision means that in 2025, long after PHP abandoned magic quotes, WordPress developers must still deal with automatically slashed data. Every time you access $_POST$_GET, or any other superglobal, WordPress has already modified it.

    This is where wp_unslash() enters the story. It’s WordPress’s official solution to its own deliberate slashing. Introduced in WordPress 3.6.0, the function is remarkably simple, it just calls stripslashes_deep(), which recursively removes backslashes from strings, arrays, and objects. But using it correctly requires understanding a pattern that’s easy to get wrong.

    Here’s what happens when you forget it:

    // Without wp_unslash - data gets corrupted
    $title = sanitize_text_field( $_POST['title'] );
    update_option( 'page_title', $title );
    // Database now contains: "O\\'Reilly"

    The correct pattern requires unslashing before sanitization:

    // With wp_unslash - data stays clean
    $title = sanitize_text_field( wp_unslash( $_POST['title'] ) );
    update_option( 'page_title', $title );
    // Database contains: "O'Reilly"

    The trap is subtle. Your code runs without errors. No exceptions get thrown. The data just silently arrives with extra backslashes, and you don’t notice until a client asks why their carefully typed content looks wrong.

    The situation gets more confusing because some WordPress functions expect slashed data while others don’t. Functions like wp_insert_post() and update_post_meta() expect their arguments to be pre-slashed, leading to this seemingly paradoxical pattern:

    $title = sanitize_text_field( wp_unslash( $_POST['title'] ) );
    wp_insert_post( wp_slash( array( 'post_title' => $title ) ) );

    You unslash the input, sanitize it, then slash it again before passing it to WordPress’s internal functions. This dance—unslash, clean, re-slash—appears throughout WordPress development, a constant reminder of the framework’s legacy.

    Why the REST API Chose Differently

    When WordPress introduced its REST API in version 4.4, the core team had an opportunity to break from this legacy. The REST API represents a modern interface, and forcing JSON data to follow PHP’s abandoned escaping conventions would be absurd.

    So the REST API makes a different choice. In WP_REST_Server::dispatch(), you’ll find this line:

    $request->set_query_params( wp_unslash( $_GET ) );

    The REST API unslashes all parameters immediately after receiving them, before any endpoint callbacks run. This means when you write a REST endpoint, you work with clean data from the start. No backslashes, no escaping artifacts, just the data as the client sent it.

    This architectural decision makes the REST API easier to work with than traditional WordPress form handling. It’s WordPress acknowledging that while backward compatibility demands maintaining the slash system for existing code, new APIs can and should work differently.

    Living With Legacy

    WordPress’s magic quotes system isn’t going anywhere. The backward compatibility concerns that justified it in 2012 still exist today. Removing it would break plugins, corrupt data, and create security vulnerabilities across millions of websites. The cost is too high, the benefit too uncertain.

    So we adapt. We learn the unslash-sanitize-slash pattern. We remember that wp_unslash() must come before sanitization. We document which functions expect slashed data and plan accordingly. We make this odd behavior second nature.

    The irony is that PHP’s failed experiment lives on in WordPress long after PHP itself moved on. Magic quotes died in the broader PHP world, but WordPress preserved them, frozen in code like an extinct species kept alive in captivity. Every call to wp_unslash() is a reminder that sometimes the hardest part of building software isn’t writing new features, it’s maintaining compatibility with decisions made fifteen years ago.

    Understanding wp_unslash() means understanding WordPress’s philosophy: backward compatibility trumps developer convenience. The framework will carry its history forward, even when that history includes other people’s mistakes. For developers, this means learning to work with WordPress as it is, not as we wish it would be.

    References

    WordPress Trac Tickets

    WordPress Functions

    • wp_unslash() – Wrapper that calls stripslashes_deep() (wp-includes/formatting.php)
    • stripslashes_deep() – Uses map_deep() to recursively remove slashes (wp-includes/formatting.php)
    • wp_magic_quotes() – Calls add_magic_quotes() on superglobals during boot (wp-includes/load.php)
    • add_magic_quotes() – Recursively applies addslashes() to arrays (wp-includes/functions.php)
  • Why Your Boss Is Right About PHP’s empty()

    Why Your Boss Is Right About PHP’s empty()

    The function shows up everywhere: in Stack Overflow snippets, in AI-generated code, in that “quick fix” someone pushed at 5pm Friday. It’s convenient, it’s short, and it silently breaks your code in ways that are hard to debug. If your boss, your tech lead, or that senior developer keeps telling you not to use it, they’re right. Here’s why, written down so you never have to be reminded again.

    What empty() Actually Does

    Most developers know empty() checks for falsy values. What they might not realize is what it considers falsy:

    empty(null);        // true
    empty(false);       // true
    empty(0);           // true
    empty("0");         // true
    empty("");          // true
    empty([]);          // true
    empty($undefined);  // true

    That last line is the biggest problem. When empty() encounters an undefined variable, it returns true instead of throwing a warning. This is almost always unexpected behavior.

    The Real Danger

    Consider this WordPress code where a developer switches two letters:

    $payment_gateway = get_option('payment_gateway');
    
    if (empty($paymentGateway)) {  // Wrong variable name
        $payment_gateway = 'test_mode';
    }

    The code runs without error. The typo ships to production. Production charges start going to test mode. Any other check would have thrown an “undefined variable” warning and caught the bug immediately.

    Beyond typos, empty() masks function failures by treating them like empty values. WordPress’s get_post_meta() returns false when a post doesn’t exist, but returns an empty string when the field is genuinely empty. With empty(), these two very different states look identical:

    // Bad: Can't distinguish "not found" from "empty value"
    $price = get_post_meta($post_id, 'price', true);
    if (empty($price)) {
        return 'Price not set';  // But what if $post_id was invalid?
    }
    
    // Good: Check for false (error) separately from empty string
    $price = get_post_meta($post_id, 'price', true);
    if ($price === false) {
        return 'Product not found';  // Invalid post ID
    }
    if ($price === '' || $price === '0') {
        return 'Price not set';  // Legitimate empty value
    }

    What To Do Instead

    Use explicit checks.

    Writing if ($price === null || $price === '') is barely longer than if (empty($price)), but it generates warnings when you make typos instead of silently hiding them.

    The truthiness check !$var is better than empty() because it generates warnings for undefined variables, but whether it works correctly depends on your API’s design. Laravel’s Model::find() returns null for missing records and throws exceptions for actual errors, so if (!$user) works as expected. WordPress functions often return different falsy values for different purposes. For example, get_post_meta() returns false for an invalid post but an empty string for a missing field. In that case, !$price can’t distinguish between “the post doesn’t exist” and “the price is empty,” so you need explicit checks: if ($price === false) for errors versus if ($price === '') for empty values.

    The one exception is checking array keys that might not exist, like form submissions or configuration arrays. Though even here, null coalescing is usually clearer:

    // Works, but obscures what you're checking
    if (!empty($_POST['subscribe'])) {
        subscribe_user_to_newsletter();
    }
    
    // Clearer: explicitly provides a default for missing key
    if (($_POST['subscribe'] ?? false)) {
        subscribe_user_to_newsletter();
    }
    
    // WordPress: Feature flags in configuration
    $features = get_option('plugin_features', []);
    if (($features['beta_mode'] ?? false)) {
        enable_beta_features();
    }

    The ?? operator makes it obvious you’re providing a default for a potentially missing key.

    The Bottom Line

    empty() treats undefined variables as normal values, returning true when it should throw a warning. This silently swallows the errors that matter most: typos, refactoring mistakes, and function failures. These are the bugs you’ll debug at 2am wondering why there’s no error message, no stack trace, no indication anything went wrong. Use explicit checks instead and let your code fail loudly when something breaks.