Last active
September 22, 2025 13:23
-
-
Save cmcnulty/148da98998b7bf70e799b0922003e0d2 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <?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