Drobber: * * 1. The widget. Paste the project key and Drobber's script goes on every * page. Ordinary visitors see nothing; only people who open the site with a * Drobber review link do. * * 2. Claude. With WordPress's MCP Adapter plugin, Claude Code connects to this * site and uses the abilities below to find and change text, in the page * content and in Elementor, Breakdance, Oxygen, Bricks and Beaver Builder's own data. One button * makes an application password and the command to paste into Terminal. * Every change is kept, so the last one can be undone. */ require_once __DIR__ . '/includes/abilities.php'; function drobber_option($name, $default = '') { $value = get_option('drobber_' . $name, $default); return is_string($value) ? $value : $default; } function drobber_api() { $api = untrailingslashit(drobber_option('api', DROBBER_DEFAULT_API)); return $api && drobber_secure_address($api) ? $api : DROBBER_DEFAULT_API; } /** HTTPS, or a copy of Drobber running on this computer for testing (localhost, *.local, *.test). */ function drobber_secure_address($url) { $parts = wp_parse_url($url); if (!$parts || empty($parts['host'])) { return false; } if ('https' === ($parts['scheme'] ?? '')) { return true; } return 'http' === ($parts['scheme'] ?? '') && (bool) preg_match('/^(localhost|127\.0\.0\.1|.+\.(local|test|localhost))$/i', $parts['host']); } /* ---------- The widget ---------- */ add_action('wp_enqueue_scripts', 'drobber_enqueue_widget'); function drobber_enqueue_widget() { if (!drobber_option('key')) { return; } // Drobber's widget, served by the Drobber service (see readme.txt, External services). wp_enqueue_script('drobber-widget', drobber_api() . '/embed.js', array(), DROBBER_VERSION, array('strategy' => 'defer', 'in_footer' => false)); } /* * The widget's tag carries the project key and where Drobber is, and asks * common speed plugins (Cloudflare Rocket Loader, WP Rocket, LiteSpeed, * Autoptimize) to leave it alone: delayed or combined, it would never load. */ add_filter('script_loader_tag', 'drobber_widget_tag', 10, 2); function drobber_widget_tag($tag, $handle) { if ('drobber-widget' !== $handle) { return $tag; } $attributes = sprintf( ' data-drobber-key="%s" data-drobber-api="%s" data-cfasync="false" data-no-optimize="1" data-no-defer="1" data-no-minify="1" nowprocket', esc_attr(drobber_option('key')), esc_attr(drobber_api()) ); return str_replace(' src=', $attributes . ' src=', $tag); } /** Tells Drobber the plugin is here (and whether Claude is connected), so its setup wizard can tick. */ function drobber_ping($claude = false) { $key = drobber_option('key'); if (!$key) { return new WP_Error('drobber_no_key', __('Add your project key first.', 'drobber')); } $response = wp_remote_post(drobber_api() . '/api/v1/wordpress', array( 'timeout' => 10, 'headers' => array('Content-Type' => 'application/json'), 'body' => wp_json_encode(array( 'key' => $key, 'site' => home_url('/'), 'version' => DROBBER_VERSION, 'claude' => (bool) $claude, )), )); if (is_wp_error($response)) { return $response; } $body = json_decode(wp_remote_retrieve_body($response), true); if (wp_remote_retrieve_response_code($response) !== 200 || empty($body['ok'])) { return new WP_Error('drobber_bad_key', __('Drobber didn’t recognise that key. Copy it again from the setup wizard or the project’s Settings.', 'drobber')); } update_option('drobber_project', isset($body['project']) ? sanitize_text_field($body['project']) : ''); return $body; } /* * Claude signing in with the password made for it: only then is it really * connected, so only then does Drobber's setup wizard tick. */ add_action('application_password_did_authenticate', function ($user, $item) { if (!is_array($item) || ($item['name'] ?? '') !== 'Claude (Drobber)' || get_option('drobber_claude_seen')) { return; } update_option('drobber_claude_seen', time()); drobber_ping(true); }, 10, 2); /* ---------- Settings > Drobber ---------- */ add_action('admin_menu', function () { add_options_page(__('Drobber', 'drobber'), __('Drobber', 'drobber'), 'manage_options', 'drobber', 'drobber_settings_page'); }); /* The settings page's few styles, and its Copy button. */ add_action('admin_enqueue_scripts', function ($hook) { if ('settings_page_drobber' !== $hook) { return; } wp_register_style('drobber-admin', false, array(), DROBBER_VERSION); wp_enqueue_style('drobber-admin'); wp_add_inline_style( 'drobber-admin', '.drobber .card{max-width:720px;padding:18px 22px;margin-top:16px}' . '.drobber h2{margin-top:0;display:flex;align-items:center;gap:10px}' . '.drobber h2 em{font-style:normal;width:24px;height:24px;border-radius:50%;background:#0f253b;color:#fff;display:inline-grid;place-items:center;font-size:12px}' . '.drobber .ok{color:#167a53;font-weight:600}' . '.drobber .cmd{display:flex;gap:8px;align-items:center;background:#0f253b;border-radius:8px;padding:10px 12px;margin:10px 0}' . '.drobber .cmd code{flex:1;background:none;color:#d6dfea;white-space:nowrap;overflow-x:auto;font-size:12px}' ); wp_register_script('drobber-admin', false, array(), DROBBER_VERSION, array('in_footer' => true)); wp_enqueue_script('drobber-admin'); wp_add_inline_script( 'drobber-admin', sprintf( 'document.addEventListener("click",function(e){var b=e.target.closest("[data-drobber-copy]");if(!b)return;var c=document.getElementById(b.getAttribute("data-drobber-copy"));if(c&&navigator.clipboard){navigator.clipboard.writeText(c.textContent);b.textContent=%s;}});', wp_json_encode(__('Copied', 'drobber')) ) ); }); add_filter('plugin_action_links_' . plugin_basename(__FILE__), function ($links) { array_unshift($links, '' . esc_html__('Settings', 'drobber') . ''); return $links; }); function drobber_mcp_adapter_active() { if (!function_exists('is_plugin_active')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return class_exists('WP\\MCP\\Plugin') || class_exists('WP\\MCP\\Core\\McpAdapter') || is_plugin_active('mcp-adapter/mcp-adapter.php'); } function drobber_mcp_adapter_installed() { if (!function_exists('get_plugins')) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } return isset(get_plugins()['mcp-adapter/mcp-adapter.php']); } /** Installs (if needed) and activates WordPress's MCP Adapter, from wordpress.org. */ function drobber_install_mcp_adapter() { if (!current_user_can('install_plugins') || !current_user_can('activate_plugins')) { return new WP_Error('drobber_cap', __('You need to be allowed to install plugins.', 'drobber')); } require_once ABSPATH . 'wp-admin/includes/plugin.php'; if (!drobber_mcp_adapter_installed()) { require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php'; $info = plugins_api('plugin_information', array('slug' => 'mcp-adapter', 'fields' => array('sections' => false))); if (is_wp_error($info)) { return $info; } $upgrader = new Plugin_Upgrader(new WP_Ajax_Upgrader_Skin()); $result = $upgrader->install($info->download_link); if (is_wp_error($result)) { return $result; } if (!$result) { return new WP_Error('drobber_install', __('WordPress couldn’t install the MCP Adapter. Install it from Plugins > Add New instead.', 'drobber')); } } $activated = activate_plugin('mcp-adapter/mcp-adapter.php'); return is_wp_error($activated) ? $activated : true; } /** The command that adds this site to Claude Code, with a fresh application password. Shown once. */ function drobber_connect_claude() { if (!class_exists('WP_Application_Passwords') || !wp_is_application_passwords_available()) { return new WP_Error('drobber_app_passwords', __('This site has application passwords turned off (WordPress needs HTTPS for them, or a security plugin has switched them off).', 'drobber')); } $user = wp_get_current_user(); $made = WP_Application_Passwords::create_new_application_password($user->ID, array('name' => 'Claude (Drobber)')); if (is_wp_error($made)) { return $made; } $host = preg_replace('/[^a-z0-9]+/', '-', strtolower((string) wp_parse_url(home_url(), PHP_URL_HOST))); $name = 'wordpress-' . trim($host, '-'); // HTTP Basic authentication (RFC 7617) is the user name and password joined by a colon, base64-encoded. // It is shown once, for the site's own admin to paste into Claude Code; nothing is sent anywhere. $auth = base64_encode($user->user_login . ':' . $made[0]); // Connected only once Claude actually signs in with it (see below). update_option('drobber_claude_seen', 0); return sprintf( 'claude mcp add --scope user --transport http %s %s --header "Authorization: Basic %s"', $name, rest_url('mcp/mcp-adapter-default-server'), $auth ); } function drobber_settings_page() { if (!current_user_can('manage_options')) { return; } $notice = ''; $error = ''; $command = ''; $action = ''; if (isset($_POST['drobber_action']) && check_admin_referer('drobber_settings')) { $action = sanitize_key(wp_unslash($_POST['drobber_action'])); if ($action === 'save') { update_option('drobber_key', sanitize_text_field(wp_unslash($_POST['drobber_key'] ?? ''))); $api = isset($_POST['drobber_api']) ? esc_url_raw(trim(sanitize_text_field(wp_unslash($_POST['drobber_api'])))) : ''; if ($api && !drobber_secure_address($api)) { // The widget's script loads from here, so only over HTTPS (a local test copy of Drobber aside). $error = __('The Drobber address must start with https://, so it was left as it was.', 'drobber'); } else { update_option('drobber_api', $api ? untrailingslashit($api) : DROBBER_DEFAULT_API); } if (drobber_option('key')) { $pinged = drobber_ping(false); if (is_wp_error($pinged)) { $error = $pinged->get_error_message(); } elseif (!$error) { $notice = __('Connected. The widget is on your pages now.', 'drobber'); } } else { delete_option('drobber_project'); } } elseif ($action === 'adapter') { $done = drobber_install_mcp_adapter(); if (is_wp_error($done)) { $error = $done->get_error_message(); } else { $notice = __('The MCP Adapter is installed and on.', 'drobber'); } } elseif ($action === 'claude') { $made = drobber_connect_claude(); if (is_wp_error($made)) { $error = $made->get_error_message(); } else { $command = $made; } } } $key = drobber_option('key'); $project = drobber_option('project'); $adapter = drobber_mcp_adapter_active() || ('adapter' === $action && !$error); $abilities = function_exists('wp_register_ability'); ?>

1

Install).', 'drobber'); ?>


>


2

Profile > Application Passwords.', 'drobber'); ?>