admin_url( 'admin-ajax.php' ), 'nonce' => wp_create_nonce( 'miintg_admin' ), 'cronDisabled' => miintg_cron_disabled(), ) ) . ';', 'before' ); } /** * @return string Current tab slug. */ function miintg_current_tab() { $tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'instructions'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return in_array( $tab, array( 'instructions', 'settings', 'run-scan', 'results', 'logs', 'support' ), true ) ? $tab : 'instructions'; } /** * @return string Current Settings sub-tab slug. */ function miintg_current_subtab() { $subtab = isset( $_GET['subtab'] ) ? sanitize_key( wp_unslash( $_GET['subtab'] ) ) : 'connectors'; // phpcs:ignore WordPress.Security.NonceVerification.Recommended return in_array( $subtab, array( 'connectors', 'general' ), true ) ? $subtab : 'connectors'; } function miintg_render_admin_page() { if ( ! current_user_can( 'manage_options' ) ) { return; } $tab = miintg_current_tab(); $base_url = admin_url( 'admin.php?page=mi-integrity' ); $tabs = array( 'instructions' => array( 'label' => 'Instructions', 'icon' => 'dashicons-info' ), 'settings' => array( 'label' => 'Settings', 'icon' => 'dashicons-admin-generic' ), 'run-scan' => array( 'label' => 'Run Scan', 'icon' => 'dashicons-shield' ), 'results' => array( 'label' => 'Results', 'icon' => 'dashicons-list-view' ), 'logs' => array( 'label' => 'Logs', 'icon' => 'dashicons-media-text' ), 'support' => array( 'label' => 'Support', 'icon' => 'dashicons-sos' ), ); ?>
'settings', 'subtab' => 'connectors' ), $base ); ?>This plugin checks your site's files for unexpected changes. It works on its own, with no account and no external service to connect. Nothing is changed on your site; every check is read-only.
Open Run Scan and click Run Full Scan. It checks six areas and usually finishes within a minute or two.
Open Results and click View Details. Each area is a section you can expand or collapse. Critical items are worth checking right away. Review items are lower risk.
If a flagged file is expected (for example a file a plugin creates), click Mark reviewed. It is left out of future scans until the file changes again.
| Area | What it looks for |
|---|---|
| WordPress Core Files | Files in wp-admin, wp-includes and the site root that differ from the official WordPress.org version, are missing, or should not be there. |
| Plugins | Each plugin's files compared with the WordPress.org copy, plus whether an update is available. Premium plugins are listed but cannot be compared. |
| Uploads folder | PHP files, double extensions and PHP code hiding in the uploads folder, where only media should be. |
| Autoloaded code | Must-use plugins and drop-ins, which run without appearing in the Plugins list. |
| Config files | A fingerprint of wp-config.php and .htaccess so you can spot changes, and the status of a few security settings. |
| Administrator accounts | Administrators hidden from the normal user list, and permissions granted outside a normal role. |
You do not need this to run scans. If you want an AI assistant such as Claude to run the checks for you, you can connect one under Settings → Connectors. It is off until you read the data-sharing notice there and confirm it.
'Connectors', 'general' => 'Other Settings', ); ?>Optional. Scans work without this. You only need this page if you want an AI assistant (for example Claude) to run the checks for you.
Nothing is sent unless you connect an assistant and it asks for a check. Each check returns a short text summary of the results. Your files are never uploaded and file contents are never read.
| Sent | WordPress version and locale. Relative paths of core and plugin files that are modified, missing or unexpected. Plugin names, versions and whether an update exists. File names, sizes, dates and MD5 hashes of mu-plugins, drop-ins, wp-config.php and .htaccess. Paths of unusual files in the uploads folder. The status of DISALLOW_FILE_EDIT, DISALLOW_FILE_MODS and WP_DEBUG. User IDs and logins of administrators that look hidden or have unusual permissions. |
| Never sent | File contents, passwords, email addresses, post or page content, or database contents. |
| Goes to | The AI assistant you connect and its provider (for Claude, Anthropic). That is a third party outside this website. Their own terms and privacy policy apply to what they receive. |
This plugin is free. Connecting an assistant uses that assistant's own usage allowance (for Claude, your Claude plan). Because files are never uploaded, usage does not grow with the size of your site. A check returns a text summary with counts and lists of file paths, capped at 200 entries per list. A clean site returns a short summary; a site with many findings returns more. How that counts against your plan is set by your AI provider, so check their usage page.
One URL with your token built in. Paste it as the connector's Server URL and nothing else.
| Enable MCP | |
| Enable MCP logging | |
| Access Token | ' . esc_html( $last4 ) . '' : 'No token generated yet'; ?> |
| Url for connectors |
?token=
|
| Enable OAuth 2.1 |
Only one token is active at a time — generating a new one immediately retires the
old one and disconnects anything still using it. The plaintext token is shown once, right after you generate
it; the site only ever stores a one-way hash, so it can't be shown again after that (regenerate if lost).
The old header method (X-MIINTG-Key) still works too, for adapters that prefer it over a
URL parameter.
| AI Copilot plugin (optional) | |
| Data sharing | Confirmed' : 'Not confirmed. Remote access is off.'; ?> |
| REST namespace | /wp-json/mi-integrity/v1/ — active |
| MCP endpoint | /wp-json/mi-integrity/v1/mcp — disabled'; ?> |
Connector request history and scan activity moved to the Logs tab.
| Results retention | Last runs (oldest pruned automatically after each scan) |
| Background execution | disabled on this site. Use Force Run on the Run Scan tab.' : 'WP-Cron — one check per cron event'; ?> |
| Plugin version |
Files you marked as reviewed on the Results tab (core and plugin findings only). Each is left out of scan counts while the file stays in the same state, and counts again if its contents change. Removing one makes it count again from the next scan.
Nothing marked as reviewed.
| Where | File | Finding | Reviewed by | When (UTC) | |
|---|---|---|---|---|---|
|
WP-Cron is turned off on this site. Scans cannot start on their own. After you click Run Full Scan, use the Run Pending Checks Now button that appears. If your host runs cron from the server instead, scans may work without it.
Runs all six checks in the background (WP-Cron) — one check per cron event, so a slow site or a cold checksum-cache never risks a page-request timeout. Results appear on the Results tab as each check finishes.
No scans yet — run one from the Run Scan tab.
| # | Started | Result | Duration | |
|---|---|---|---|---|
The last 50 actions taken from this plugin's admin screens — who did what, when. Never records a token or key value.
No activity logged yet.
| Time (UTC) | User | Action | Detail |
|---|---|---|---|
The last 20 requests to the connector endpoint — use this to check what a failed Claude tool call actually did server-side (whether a credential arrived, and whether it was accepted) without needing browser dev tools.
No requests logged yet.
| Time (UTC) | Method | Credential Sent | Auth Result | Client |
|---|---|---|---|---|
|
No'; } ?> | Passed'; } else { echo 'Failed'; } ?> |
Permanently deletes every stored scan run and result (not just the log views above) — the Results tab will be empty afterward. Does not affect the Activity Log or MCP Request Log above.
This plugin is built and maintained by Medium Interactive, a digital marketing and SEO agency. If you have a question, found a bug, or want a new feature, get in touch through our contact page and a real person will help.
We use this plugin daily across our own client sites, so feedback directly shapes its roadmap.
'Not allowed.' ), 403 ); } $run_id = miintg_schedule_scan( null, 'manual', get_current_user_id() ); miintg_log_activity( 'Started scan', 'Run #' . $run_id ); wp_send_json_success( array( 'run_id' => $run_id ) ); } add_action( 'wp_ajax_miintg_poll_run', 'miintg_ajax_poll_run' ); function miintg_ajax_poll_run() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $run_id = isset( $_POST['run_id'] ) ? (int) $_POST['run_id'] : 0; $run = miintg_db_get_run_with_checks( $run_id ); if ( ! $run ) { wp_send_json_error( array( 'message' => 'No such run.' ), 404 ); } // Trim result payloads out of the poll response — the UI only needs // per-check status here, not the full JSON (that's the View button). foreach ( $run['checks'] as &$check ) { unset( $check['result'] ); } wp_send_json_success( $run ); } add_action( 'wp_ajax_miintg_view_run', 'miintg_ajax_view_run' ); function miintg_ajax_view_run() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $run_id = isset( $_POST['run_id'] ) ? (int) $_POST['run_id'] : 0; $run = miintg_db_get_run_with_checks( $run_id ); if ( ! $run ) { wp_send_json_error( array( 'message' => 'No such run.' ), 404 ); } wp_send_json_success( array( 'html' => miintg_render_run_report_html( $run ) ) ); } add_action( 'wp_ajax_miintg_generate_key', 'miintg_ajax_generate_key' ); function miintg_ajax_generate_key() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } if ( ! miintg_ai_consent() ) { wp_send_json_error( array( 'message' => 'Confirm the data-sharing notice first.' ), 400 ); } $key = miintg_generate_api_key( get_current_user_id() ); miintg_log_activity( 'Generated access token', 'Ends in ' . substr( $key, -4 ) ); wp_send_json_success( array( 'key' => $key, 'last4' => substr( $key, -4 ) ) ); } add_action( 'wp_ajax_miintg_revoke_key', 'miintg_ajax_revoke_key' ); function miintg_ajax_revoke_key() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } miintg_revoke_api_key(); miintg_log_activity( 'Revoked access token' ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_force_run', 'miintg_ajax_force_run' ); function miintg_ajax_force_run() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $run_id = isset( $_POST['run_id'] ) ? (int) $_POST['run_id'] : 0; $run = miintg_db_get_run_with_checks( $run_id ); if ( ! $run ) { wp_send_json_error( array( 'message' => 'No such run.' ), 404 ); } foreach ( $run['checks'] as $check ) { if ( in_array( $check['status'], array( 'complete', 'error' ), true ) ) { continue; } // Drop the pending cron event for this check so it doesn't also // fire later and re-record a result that's already in. wp_clear_scheduled_hook( MIINTG_CRON_HOOK, array( $run_id, $check['check_name'] ) ); // Runs inline, in this request, instead of waiting on WP-Cron. miintg_cron_run_check( $run_id, $check['check_name'] ); } miintg_log_activity( 'Force-ran pending checks', 'Run #' . $run_id ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_toggle_setting', 'miintg_ajax_toggle_setting' ); function miintg_ajax_toggle_setting() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $setting = isset( $_POST['setting'] ) ? sanitize_key( wp_unslash( $_POST['setting'] ) ) : ''; $allowed = array( 'miintg_mcp_enabled', 'miintg_mcp_logging_enabled', 'miintg_ai_consent' ); if ( ! in_array( $setting, $allowed, true ) ) { wp_send_json_error( array( 'message' => 'Unknown setting.' ), 400 ); } $value = isset( $_POST['value'] ) && 'true' === sanitize_text_field( wp_unslash( $_POST['value'] ) ); update_option( $setting, $value, false ); $labels = array( 'miintg_mcp_enabled' => 'MCP endpoint', 'miintg_mcp_logging_enabled' => 'MCP request logging', 'miintg_ai_consent' => 'data sharing with an AI assistant', ); miintg_log_activity( ( $value ? 'Enabled ' : 'Disabled ' ) . $labels[ $setting ] ); wp_send_json_success( array( 'value' => $value ) ); } add_action( 'wp_ajax_miintg_mark_reviewed', 'miintg_ajax_mark_reviewed' ); function miintg_ajax_mark_reviewed() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $scope = isset( $_POST['scope'] ) ? sanitize_text_field( wp_unslash( $_POST['scope'] ) ) : ''; $path = isset( $_POST['path'] ) ? sanitize_text_field( wp_unslash( $_POST['path'] ) ) : ''; $kind = isset( $_POST['kind'] ) ? sanitize_key( wp_unslash( $_POST['kind'] ) ) : ''; if ( ! in_array( $kind, array( 'modified', 'missing', 'unexpected' ), true ) ) { wp_send_json_error( array( 'message' => 'Unknown finding type.' ), 400 ); } // The state is read from the file now, server-side — never trusted from the browser. $state = miintg_reviewed_state( $scope, $path, $kind ); if ( null === $state ) { wp_send_json_error( array( 'message' => 'Could not read that file, or its state no longer matches this finding. Run a new scan and try again.' ), 400 ); } miintg_reviewed_add( $scope, $path, $kind, $state ); miintg_log_activity( 'Marked file as reviewed', ( 'core' === $scope ? 'core' : $scope ) . ': ' . $path ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_unreview', 'miintg_ajax_unreview' ); function miintg_ajax_unreview() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } $key = isset( $_POST['key'] ) ? sanitize_key( wp_unslash( $_POST['key'] ) ) : ''; if ( ! miintg_reviewed_remove( $key ) ) { wp_send_json_error( array( 'message' => 'That entry no longer exists.' ), 404 ); } miintg_log_activity( 'Removed a reviewed file' ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_clear_reviewed', 'miintg_ajax_clear_reviewed' ); function miintg_ajax_clear_reviewed() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } miintg_reviewed_clear(); miintg_log_activity( 'Cleared all reviewed files' ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_clear_activity_log', 'miintg_ajax_clear_activity_log' ); function miintg_ajax_clear_activity_log() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } delete_option( 'miintg_activity_log' ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_clear_mcp_log', 'miintg_ajax_clear_mcp_log' ); function miintg_ajax_clear_mcp_log() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } delete_option( 'miintg_mcp_log' ); miintg_log_activity( 'Cleared MCP request log' ); wp_send_json_success(); } add_action( 'wp_ajax_miintg_clear_scan_history', 'miintg_ajax_clear_scan_history' ); function miintg_ajax_clear_scan_history() { check_ajax_referer( 'miintg_admin' ); if ( ! current_user_can( 'manage_options' ) ) { wp_send_json_error( array( 'message' => 'Not allowed.' ), 403 ); } miintg_db_clear_all_runs(); miintg_log_activity( 'Cleared scan history' ); wp_send_json_success(); }