Skip to content

Instantly share code, notes, and snippets.

@cmcnulty
Last active September 22, 2025 13:23
Show Gist options
  • Save cmcnulty/148da98998b7bf70e799b0922003e0d2 to your computer and use it in GitHub Desktop.
Save cmcnulty/148da98998b7bf70e799b0922003e0d2 to your computer and use it in GitHub Desktop.
<?php
/**
* Plugin Name: Global REST Prefix (mu) + /wp-json logger
* Description: Must-use plugin that sets a site-wide REST API prefix, preserves /wp-json for legacy callers, and logs when /wp-json is used.
* Version: 1.1.0
* Author: Charles McNulty
* License: CC0 1.0 (Public Domain - do whatever)
*
* Notes:
* - As an MU plugin, this file must live directly in wp-content/mu-plugins/ (not inside a subfolder) to auto-load.
* - After adding this file, visit Settings → Permalinks → Save to flush rewrite rules once.
*/
if (!defined('ABSPATH')) exit;
/**
* Generate or return the site-specific REST prefix.
* Stored per-site (single site) using an option for stability.
* For multisite network-wide shared prefix, replace get_option/update_option with get_site_option/update_site_option.
*/
function rp_get_rest_prefix(): string {
$opt_name = 'rp_rest_prefix';
$prefix = get_option($opt_name);
if (!$prefix) {
// Build "<site-slug>-<short-salt>" to avoid obvious analytics words.
$site_slug = sanitize_title(get_bloginfo('name'));
if (!$site_slug) $site_slug = 'site';
$salt = wp_generate_password(6, false, false); // 6 URL-safe chars
$prefix = strtolower($site_slug . '-' . $salt);
// Sanitize strictly to be safe in URLs.
$prefix = preg_replace('~[^a-z0-9\-]~', '', $prefix);
update_option($opt_name, $prefix, false);
}
return $prefix;
}
/**
* 1) Set the global REST URL prefix (canonical entrypoint becomes /<prefix>/...).
*/
add_filter('rest_url_prefix', function ($default) {
return rp_get_rest_prefix();
}, 10, 1);
/**
* 2) Add a rewrite so /wp-json/... still works (no redirect).
* This helps legacy/hard-coded callers keep working.
*/
add_action('init', function () {
// /wp-json (index)
add_rewrite_rule(
'^wp-json/?$',
'index.php?rest_route=/',
'top'
);
// /wp-json/<route> (everything under it)
add_rewrite_rule(
'^wp-json/(.*)?',
'index.php?rest_route=/$matches[1]',
'top'
);
}, 9);
/**
* 3) Log any request that still comes in via /wp-json/...
* This lets us spot misbehaving code that hardcodes the legacy base.
* Logs go to wp-content/debug.log if WP_DEBUG_LOG is enabled.
*/
add_action('parse_request', function($wp) {
// REQUEST_URI contains path + query; robustly check the path only.
$uri = $_SERVER['REQUEST_URI'] ?? '';
// Extract path portion only for matching
$path = $uri;
if (false !== ($qpos = strpos($uri, '?'))) {
$path = substr($uri, 0, $qpos);
}
if (strpos($path, '/wp-json/') === 0 || rtrim($path, '/') === '/wp-json') {
// Enrich log with method, remote IP, UA, and resolved route if present.
$method = $_SERVER['REQUEST_METHOD'] ?? 'GET';
$ip = $_SERVER['REMOTE_ADDR'] ?? '';
$ua = $_SERVER['HTTP_USER_AGENT'] ?? '';
$route = isset($_GET['rest_route']) ? $_GET['rest_route'] : '';
$prefix = rp_get_rest_prefix();
$msg = sprintf(
'[REST-PREFIX] Legacy /wp-json usage detected | method=%s uri=%s route=%s ip=%s prefix=%s ua=%s',
$method,
$uri,
$route,
$ip,
$prefix,
$ua
);
error_log($msg);
}
}, 5);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment