This commit is contained in:
Javier Casares 2026-08-18 16:32:08 +00:00
commit 515af45d09
10 changed files with 1189 additions and 147 deletions

View file

@ -1,5 +1,183 @@
== Changelog ==
= 1.0.0 =
_Release date: 2026-08-18_
**Highlights**
* First stable release
**Changed**
* Version number bumped to 1.0.0 marking the feature set as stable: outbound HTTP request logging with secret redaction, Safe modes for the WordPress.org Core/Themes/Plugins update checks, per-plugin hiding, User-Agent and WordPress version controls, installation language masking, dashboard services replaced (news feed, events, browser check), log retention, and Multisite Per-site/Global configuration modes
**Compatibility**
* WordPress: 4.0 - 7.1
* PHP: 5.6 - 8.5
* MariaDB: 10.6+
* Multisite: compatible (Per-site and Global modes)
**Tests**
* WordPress 7.2-alpha-63320, PHP 8.5.9, MariaDB 11.8.8
* PHP Coding Standards: 3.13.6
* WordPress Coding Standards: 3.4.1
* PHPCompatibilityWP (PHP 5.6 - 8.5) and wp-since (WordPress 4.0) static reviews
= 0.10.0 =
_Release date: 2026-08-18_
**Highlights**
* Per-plugin privacy: choose exactly which plugins are reported to WordPress.org, in a new Plugins settings tab
**Added**
* "Plugins" settings tab (second position) listing every installed plugin in a native table: hide checkbox, status, plugin name (with its folder/file path), and version
* Checked plugins are excluded from the WordPress.org plugin update check in both Safe and Original modes: their plugin data, their entry in the active list, and their translation projects are never sent, which also means they stop receiving WordPress.org update notifications
* All plugins are unchecked by default, except Telemetry disabler (by ROBOTSTXT) which is always hidden and cannot be unchecked
* The tab explains the trade-off and recommends keeping external or private plugins (not hosted on WordPress.org) checked
* Works in per-site and global configurations (the list reflects the plugins of the current site)
**Changed**
* The WordPress version masking (0.9.0) is now applied to the User-Agent only; the version query parameters of the update and information APIs always send the real version so updates keep working
**Fixed**
* The User-Agent masking now also matches the trailing-slash form of the site URL used by the WordPress.org update checks (previously the slash could leak in "Do not send anything" mode)
**Compatibility**
* WordPress: 4.0 - 7.1
* PHP: 5.6 - 8.5
* MariaDB: 10.6+
* Multisite: compatible (Per-site and Global modes)
**Tests**
* WordPress 7.2-alpha-63320, PHP 8.5.9, MariaDB 11.8.8
* PHP Coding Standards: 3.13.6
* WordPress Coding Standards: 3.4.1
= 0.9.1 =
_Release date: 2026-08-18_
**Highlights**
* The Settings screen is organized in native WordPress tabs: Settings, Logs, and General
**Changed**
* Settings split into three tabs: "Settings" (privacy controls: User-Agent, WordPress version, language, dashboard services, update checks), "Logs" (log retention), and "General" (uninstall behavior)
* Each tab saves independently; saving one tab never modifies the options of the others (both in the per-site screen and the Network Admin one)
**Compatibility**
* WordPress: 4.0 - 7.1
* PHP: 5.6 - 8.5
* MariaDB: 10.6+
* Multisite: compatible (Per-site and Global modes)
**Tests**
* WordPress 7.2-alpha-63320, PHP 8.5.9, MariaDB 11.8.8
* PHP Coding Standards: 3.13.6
* WordPress Coding Standards: 3.4.1
= 0.9.0 =
_Release date: 2026-08-18_
**Highlights**
* Full control over what the User-Agent reveals: the site URL can be sent, hashed (default), or removed, and the WordPress version can be actual, major-only ("7.2.n"), or nulled ("0.0.0")
**Added**
* "Outbound requests" setting with three options: "Send your URL" (original behavior), "Send a hash" (default, replaces the site URL with a deterministic SHA-256 hash), and "Do not send anything" (removes the site identifier from the User-Agent)
* "WordPress version" setting with three options: "Actual version" (default), "Major version" (the first two segments plus ".n", for example "7.2.n"), and "Nulled version" ("0.0.0")
* The version masking applies to the User-Agent and to the WordPress.org version fields (core version check, plugin and theme information APIs)
* The checksum and translation requests always use the real version so core updates and language packs keep working
* Existing installations keep their previous behavior through an automatic migration of the former "Hash the site URL" boolean setting
**Compatibility**
* WordPress: 4.0 - 7.1
* PHP: 5.6 - 8.5
* MariaDB: 10.6+
* Multisite: compatible (Per-site and Global modes)
**Tests**
* WordPress 7.2-alpha-63320, PHP 8.5.9, MariaDB 11.8.8
* PHP Coding Standards: 3.13.6
* WordPress Coding Standards: 3.4.1
= 0.8.0 =
_Release date: 2026-08-18_
**Highlights**
* The dashboard stops talking to WordPress.org: the news feed and the events service are replaced by Fair services (enabled by default), and the browser check is disabled
**Added**
* "Replace the WordPress News feed" setting (enabled by default): the Events and News widget reads https://planet.fair.pm/atom.xml (WordPress Planet by Fair) instead of wordpress.org/news (also covers localized Rosetta feeds)
* "Replace the WordPress Events service" setting (enabled by default): the events requests are served by https://api.fair.pm/fair/v1/events (WordPress Events by The WP World), sending the same request data
* "Disable the WordPress browser check" setting (enabled by default): the api.wordpress.org/core/browse-happy/ endpoint is never contacted and the browser is always reported as up to date and secure, so no browser nag is shown
* New endpoint interception layer (`pre_http_request`) shared by the three features
**Fixed**
* The locale masking of 0.7.0 now also rewrites the locale query parameters of real requests; the previous implementation used a request-URL filter that WordPress does not provide
**Compatibility**
* WordPress: 4.0 - 7.1
* PHP: 5.6 - 8.5
* MariaDB: 10.6+
* Multisite: compatible (Per-site and Global modes)
**Tests**
* WordPress 7.2-alpha-63320, PHP 8.5.9, MariaDB 11.8.8
* PHP Coding Standards: 3.13.6
* WordPress Coding Standards: 3.4.1
= 0.7.0 =
_Release date: 2026-08-18_
**Highlights**
* The installation language can be hidden: outbound requests to WordPress.org report en_US instead of the real locale
**Added**
* "Send en_US as the language of outbound requests" setting (disabled by default)
* When enabled, the locale query parameters (core version check, credits, community events) and `request[locale]` parameters (plugin and theme information APIs) are rewritten to en_US, and the locale lists of the plugins and themes update checks are masked
* The translation endpoints (`api.wordpress.org/translations/*`) and the translation payloads of the update checks keep using the real language, so installed language packs keep receiving updates
* Works in both Safe and Original modes, in per-site and global configurations
**Compatibility**
* WordPress: 4.0 - 7.1
* PHP: 5.6 - 8.5
* MariaDB: 10.6+
* Multisite: compatible (Per-site and Global modes)
**Tests**
* WordPress 7.2-alpha-63320, PHP 8.5.9, MariaDB 11.8.8
* PHP Coding Standards: 3.13.6
* WordPress Coding Standards: 3.4.1
= 0.6.3 =
_Release date: 2026-08-18_

View file

@ -113,12 +113,18 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Admin' ) ) {
*/
public function register_settings() {
$settings = array(
'robotstxt_telemetry_hash_useragent_url' => array( 'boolean', true, 'sanitize_checkbox' ),
'robotstxt_telemetry_wp_core_check' => array( 'string', 'safe', 'sanitize_mode' ),
'robotstxt_telemetry_wp_themes_check' => array( 'string', 'safe', 'sanitize_mode' ),
'robotstxt_telemetry_wp_plugins_check' => array( 'string', 'safe', 'sanitize_mode' ),
'robotstxt_telemetry_retention_period' => array( 'string', '12hours', 'sanitize_retention' ),
'robotstxt_telemetry_delete_on_uninstall' => array( 'boolean', false, 'sanitize_checkbox' ),
'robotstxt_telemetry_useragent_url' => array( 'string', 'hash', 'sanitize_useragent_mode' ),
'robotstxt_telemetry_wp_version' => array( 'string', 'actual', 'sanitize_version_mode' ),
'robotstxt_telemetry_mask_locale' => array( 'boolean', false, 'sanitize_checkbox' ),
'robotstxt_telemetry_replace_news_feed' => array( 'boolean', true, 'sanitize_checkbox' ),
'robotstxt_telemetry_replace_events_api' => array( 'boolean', true, 'sanitize_checkbox' ),
'robotstxt_telemetry_disable_browse_happy' => array( 'boolean', true, 'sanitize_checkbox' ),
'robotstxt_telemetry_wp_core_check' => array( 'string', 'safe', 'sanitize_mode' ),
'robotstxt_telemetry_wp_themes_check' => array( 'string', 'safe', 'sanitize_mode' ),
'robotstxt_telemetry_wp_plugins_check' => array( 'string', 'safe', 'sanitize_mode' ),
'robotstxt_telemetry_hidden_plugins' => array( 'array', array(), 'sanitize_hidden_plugins' ),
'robotstxt_telemetry_retention_period' => array( 'string', '12hours', 'sanitize_retention' ),
'robotstxt_telemetry_delete_on_uninstall' => array( 'boolean', false, 'sanitize_checkbox' ),
);
foreach ( $settings as $name => $config ) {
@ -140,6 +146,60 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Admin' ) ) {
return 'original' === $value ? 'original' : 'safe';
}
/**
* Sanitize a User-Agent URL mode setting.
*
* @param mixed $value Raw input value.
* @return string
*/
public function sanitize_useragent_mode( $value ) {
return in_array( $value, array( 'url', 'none' ), true ) ? $value : 'hash';
}
/**
* Sanitize a WordPress version mode setting.
*
* @param mixed $value Raw input value.
* @return string
*/
public function sanitize_version_mode( $value ) {
return in_array( $value, array( 'major', 'nulled' ), true ) ? $value : 'actual';
}
/**
* Sanitize the hidden plugins list.
*
* Only basenames of currently installed plugins are kept; the
* telemetry plugin itself is always hidden.
*
* @param mixed $value Raw input value.
* @return string[]
*/
public function sanitize_hidden_plugins( $value ) {
if ( ! is_array( $value ) ) {
$value = array();
}
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$installed = array_keys( get_plugins() );
$hidden = array();
foreach ( $value as $basename ) {
$basename = sanitize_text_field( (string) $basename );
if ( in_array( $basename, $installed, true ) ) {
$hidden[] = $basename;
}
}
$hidden[] = 'robotstxt-telemetry/robotstxt-telemetry.php';
return array_values( array_unique( $hidden ) );
}
/**
* Sanitize a retention period setting.
*
@ -259,22 +319,42 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Admin' ) ) {
check_admin_referer( 'robotstxt_telemetry_network_settings' );
// phpcs:disable WordPress.Security.NonceVerification.Missing -- Verified by check_admin_referer() above.
$hash = ! empty( $_POST['robotstxt_telemetry_hash_useragent_url'] );
$core = isset( $_POST['robotstxt_telemetry_wp_core_check'] ) ? sanitize_key( wp_unslash( $_POST['robotstxt_telemetry_wp_core_check'] ) ) : 'safe';
$themes = isset( $_POST['robotstxt_telemetry_wp_themes_check'] ) ? sanitize_key( wp_unslash( $_POST['robotstxt_telemetry_wp_themes_check'] ) ) : 'safe';
$plugins = isset( $_POST['robotstxt_telemetry_wp_plugins_check'] ) ? sanitize_key( wp_unslash( $_POST['robotstxt_telemetry_wp_plugins_check'] ) ) : 'safe';
$retention = isset( $_POST['robotstxt_telemetry_retention_period'] ) ? sanitize_key( wp_unslash( $_POST['robotstxt_telemetry_retention_period'] ) ) : '12hours';
$delete = ! empty( $_POST['robotstxt_telemetry_delete_on_uninstall'] );
$tab = isset( $_POST['tab'] ) ? sanitize_key( wp_unslash( $_POST['tab'] ) ) : 'main';
$tab = in_array( $tab, array( 'main', 'logs', 'general' ), true ) ? $tab : 'main';
if ( 'logs' === $tab ) {
$retention = isset( $_POST['robotstxt_telemetry_retention_period'] ) ? sanitize_key( wp_unslash( $_POST['robotstxt_telemetry_retention_period'] ) ) : '12hours';
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_retention_period', $this->sanitize_retention( $retention ) );
} elseif ( 'plugins' === $tab ) {
$hidden = isset( $_POST['robotstxt_telemetry_hidden_plugins'] ) ? array_map( 'sanitize_text_field', (array) wp_unslash( $_POST['robotstxt_telemetry_hidden_plugins'] ) ) : array();
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_hidden_plugins', $this->sanitize_hidden_plugins( $hidden ) );
} elseif ( 'general' === $tab ) {
$delete = ! empty( $_POST['robotstxt_telemetry_delete_on_uninstall'] );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_delete_on_uninstall', $delete );
} else {
$useragent_mode = isset( $_POST['robotstxt_telemetry_useragent_url'] ) ? sanitize_key( wp_unslash( $_POST['robotstxt_telemetry_useragent_url'] ) ) : 'hash';
$version_mode = isset( $_POST['robotstxt_telemetry_wp_version'] ) ? sanitize_key( wp_unslash( $_POST['robotstxt_telemetry_wp_version'] ) ) : 'actual';
$mask_locale = ! empty( $_POST['robotstxt_telemetry_mask_locale'] );
$replace_news = ! empty( $_POST['robotstxt_telemetry_replace_news_feed'] );
$replace_events = ! empty( $_POST['robotstxt_telemetry_replace_events_api'] );
$disable_browse_happy = ! empty( $_POST['robotstxt_telemetry_disable_browse_happy'] );
$core = isset( $_POST['robotstxt_telemetry_wp_core_check'] ) ? sanitize_key( wp_unslash( $_POST['robotstxt_telemetry_wp_core_check'] ) ) : 'safe';
$themes = isset( $_POST['robotstxt_telemetry_wp_themes_check'] ) ? sanitize_key( wp_unslash( $_POST['robotstxt_telemetry_wp_themes_check'] ) ) : 'safe';
$plugins = isset( $_POST['robotstxt_telemetry_wp_plugins_check'] ) ? sanitize_key( wp_unslash( $_POST['robotstxt_telemetry_wp_plugins_check'] ) ) : 'safe';
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_useragent_url', $this->sanitize_useragent_mode( $useragent_mode ) );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_wp_version', $this->sanitize_version_mode( $version_mode ) );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_mask_locale', $mask_locale );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_replace_news_feed', $replace_news );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_replace_events_api', $replace_events );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_disable_browse_happy', $disable_browse_happy );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_wp_core_check', $this->sanitize_mode( $core ) );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_wp_themes_check', $this->sanitize_mode( $themes ) );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_wp_plugins_check', $this->sanitize_mode( $plugins ) );
}
// phpcs:enable
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_hash_useragent_url', $hash );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_wp_core_check', $this->sanitize_mode( $core ) );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_wp_themes_check', $this->sanitize_mode( $themes ) );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_wp_plugins_check', $this->sanitize_mode( $plugins ) );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_retention_period', $this->sanitize_retention( $retention ) );
Robotstxt_Telemetry_Network::update_setting( 'robotstxt_telemetry_delete_on_uninstall', $delete );
$this->redirect_network_settings( 'settings-updated' );
$this->redirect_network_settings( 'settings-updated', $tab );
}
}
@ -282,12 +362,14 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Admin' ) ) {
* Redirect back to the Network Admin settings screen.
*
* @param string $flag Query flag to append.
* @param string $tab Settings tab to return to.
*/
private function redirect_network_settings( $flag ) {
private function redirect_network_settings( $flag, $tab = 'main' ) {
wp_safe_redirect(
add_query_arg(
array(
'page' => 'robotstxt-telemetry',
'tab' => $tab,
$flag => 'true',
),
network_admin_url( 'admin.php' )
@ -333,35 +415,38 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Admin' ) ) {
$mode = get_site_option( Robotstxt_Telemetry_Network::MODE_OPTION, 'per-site' );
$is_global = Robotstxt_Telemetry_Network::is_global();
$tab = $this->get_current_tab();
echo '<div class="wrap">';
echo '<h1>' . esc_html__( 'Telemetry Network Settings', 'robotstxt-telemetry' ) . '</h1>';
echo '<form method="post" action="' . esc_url( add_query_arg( 'page', 'robotstxt-telemetry', network_admin_url( 'admin.php' ) ) ) . '">';
wp_nonce_field( 'robotstxt_telemetry_network_mode' );
echo '<input type="hidden" name="action" value="save_network_mode" />';
echo '<input type="hidden" name="page" value="robotstxt-telemetry" />';
if ( 'main' === $tab ) {
echo '<form method="post" action="' . esc_url( add_query_arg( 'page', 'robotstxt-telemetry', network_admin_url( 'admin.php' ) ) ) . '">';
wp_nonce_field( 'robotstxt_telemetry_network_mode' );
echo '<input type="hidden" name="action" value="save_network_mode" />';
echo '<input type="hidden" name="page" value="robotstxt-telemetry" />';
echo '<table class="form-table" role="presentation">';
echo '<tbody>';
echo '<tr>';
echo '<th scope="row">' . esc_html__( 'Configuration mode', 'robotstxt-telemetry' ) . '</th>';
echo '<td>';
echo '<fieldset>';
echo '<legend class="screen-reader-text">' . esc_html__( 'Configuration mode', 'robotstxt-telemetry' ) . '</legend>';
echo '<p style="margin: 0;">';
echo '<label><input type="radio" name="robotstxt_telemetry_config_mode" value="per-site" ' . checked( $mode, 'per-site', false ) . '/> ' . esc_html__( 'Per-site', 'robotstxt-telemetry' ) . '</label>';
echo '&nbsp;&nbsp;';
echo '<label><input type="radio" name="robotstxt_telemetry_config_mode" value="global" ' . checked( $mode, 'global', false ) . '/> ' . esc_html__( 'Global', 'robotstxt-telemetry' ) . '</label>';
echo '</p>';
echo '<p class="description">' . esc_html__( 'Per-site: every site keeps its own settings and its own log table, managed by each site administrator. Global: one shared configuration and one central log on the main site collect the outbound requests of the whole network; the Telemetry screens are then managed from here only.', 'robotstxt-telemetry' ) . '</p>';
echo '</fieldset>';
echo '</td>';
echo '</tr></tbody>';
echo '</table>';
echo '<table class="form-table" role="presentation">';
echo '<tbody>';
echo '<tr>';
echo '<th scope="row">' . esc_html__( 'Configuration mode', 'robotstxt-telemetry' ) . '</th>';
echo '<td>';
echo '<fieldset>';
echo '<legend class="screen-reader-text">' . esc_html__( 'Configuration mode', 'robotstxt-telemetry' ) . '</legend>';
echo '<p style="margin: 0;">';
echo '<label><input type="radio" name="robotstxt_telemetry_config_mode" value="per-site" ' . checked( $mode, 'per-site', false ) . '/> ' . esc_html__( 'Per-site', 'robotstxt-telemetry' ) . '</label>';
echo '&nbsp;&nbsp;';
echo '<label><input type="radio" name="robotstxt_telemetry_config_mode" value="global" ' . checked( $mode, 'global', false ) . '/> ' . esc_html__( 'Global', 'robotstxt-telemetry' ) . '</label>';
echo '</p>';
echo '<p class="description">' . esc_html__( 'Per-site: every site keeps its own settings and its own log table, managed by each site administrator. Global: one shared configuration and one central log on the main site collect the outbound requests of the whole network; the Telemetry screens are then managed from here only.', 'robotstxt-telemetry' ) . '</p>';
echo '</fieldset>';
echo '</td>';
echo '</tr></tbody>';
echo '</table>';
submit_button( __( 'Save configuration mode', 'robotstxt-telemetry' ) );
echo '</form>';
submit_button( __( 'Save configuration mode', 'robotstxt-telemetry' ) );
echo '</form>';
}
if ( ! $is_global ) {
echo '<p>' . esc_html__( 'Switch to Global mode to manage the settings and the logs for the whole network from this screen.', 'robotstxt-telemetry' ) . '</p>';
@ -369,20 +454,69 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Admin' ) ) {
return;
}
echo '<h2>' . esc_html__( 'Network settings', 'robotstxt-telemetry' ) . '</h2>';
$this->render_tabs( $tab );
echo '<form method="post" action="' . esc_url( add_query_arg( 'page', 'robotstxt-telemetry', network_admin_url( 'admin.php' ) ) ) . '">';
wp_nonce_field( 'robotstxt_telemetry_network_settings' );
echo '<input type="hidden" name="action" value="save_network_settings" />';
echo '<input type="hidden" name="page" value="robotstxt-telemetry" />';
echo '<input type="hidden" name="tab" value="' . esc_attr( $tab ) . '" />';
$this->render_settings_fields();
$this->render_settings_fields( $tab );
submit_button();
echo '</form>';
echo '</div>';
}
/**
* Get the current settings tab.
*
* @return string
*/
private function get_current_tab() {
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Read-only navigation state.
$tab = isset( $_GET['tab'] ) ? sanitize_key( wp_unslash( $_GET['tab'] ) ) : 'main';
return in_array( $tab, array( 'main', 'plugins', 'logs', 'general' ), true ) ? $tab : 'main';
}
/**
* Render the settings screen tabs.
*
* @param string $current_tab Current tab slug.
*/
private function render_tabs( $current_tab ) {
$tabs = array(
'main' => __( 'Settings', 'robotstxt-telemetry' ),
'plugins' => __( 'Plugins', 'robotstxt-telemetry' ),
'logs' => __( 'Logs', 'robotstxt-telemetry' ),
'general' => __( 'General', 'robotstxt-telemetry' ),
);
echo '<nav class="nav-tab-wrapper" aria-label="' . esc_attr__( 'Settings sections', 'robotstxt-telemetry' ) . '">';
foreach ( $tabs as $slug => $label ) {
$url = add_query_arg(
array(
'page' => 'robotstxt-telemetry',
'tab' => $slug,
),
Robotstxt_Telemetry_Network::base_url()
);
printf(
'<a href="%s" class="nav-tab%s">%s</a>',
esc_url( $url ),
$current_tab === $slug ? ' nav-tab-active' : '',
esc_html( $label )
);
}
echo '</nav>';
echo '<br />';
}
/**
* Render settings page.
*/
@ -391,16 +525,20 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Admin' ) ) {
return;
}
$tab = $this->get_current_tab();
echo '<div class="wrap">';
echo '<h1>' . esc_html__( 'Telemetry Settings', 'robotstxt-telemetry' ) . '</h1>';
$manager_notice = new Robotstxt_Telemetry_Manager_Notice();
$manager_notice->render_settings_notice();
$this->render_tabs( $tab );
echo '<form method="post" action="options.php">';
settings_fields( 'robotstxt_telemetry_settings' );
$this->render_settings_fields();
$this->render_settings_fields( $tab );
submit_button();
echo '</form>';
@ -411,23 +549,105 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Admin' ) ) {
* Render the settings fields table.
*
* Values honor the configuration mode: per-site options or the
* shared network options in global mode.
* shared network options in global mode. Only the rows of the
* given tab are rendered, so each tab saves independently.
*
* @param string $tab Tab slug: 'main', 'logs', or 'general'.
*/
private function render_settings_fields() {
private function render_settings_fields( $tab = 'main' ) {
echo '<table class="form-table" role="presentation">';
echo '<tbody>';
if ( 'main' === $tab ) {
$url_mode = Robotstxt_Telemetry_Network::get_useragent_url_mode();
echo '<tr>';
echo '<th scope="row">' . esc_html__( 'Outbound requests', 'robotstxt-telemetry' ) . '</th>';
echo '<td>';
echo '<label for="robotstxt-telemetry-hash-useragent">';
echo '<input type="checkbox" id="robotstxt-telemetry-hash-useragent" name="robotstxt_telemetry_hash_useragent_url" value="1" ' . checked( (bool) Robotstxt_Telemetry_Network::get_setting( 'robotstxt_telemetry_hash_useragent_url', true ), true, false ) . '/>';
echo '<span>' . esc_html__( 'Hash the site URL in the User-Agent of outbound requests', 'robotstxt-telemetry' ) . '</span>';
echo '</label>';
echo '<p class="description">' . esc_html__( 'Outbound requests normally identify this site with its URL (for example "WordPress/6.9; https://example.com/"). When enabled, the URL is replaced with a fixed hash so the site is no longer identifiable.', 'robotstxt-telemetry' ) . '</p>';
echo '<fieldset>';
echo '<legend class="screen-reader-text">' . esc_html__( 'Outbound requests', 'robotstxt-telemetry' ) . '</legend>';
echo '<p style="margin: 0;">';
$useragent_options = array(
'url' => __( 'Send your URL', 'robotstxt-telemetry' ),
'hash' => __( 'Send a hash', 'robotstxt-telemetry' ),
'none' => __( 'Do not send anything', 'robotstxt-telemetry' ),
);
foreach ( $useragent_options as $value => $label ) {
echo '<label style="display: block; margin: 0;"><input type="radio" name="robotstxt_telemetry_useragent_url" value="' . esc_attr( $value ) . '" ' . checked( $url_mode, $value, false ) . '/> ' . esc_html( $label ) . '</label>';
}
echo '</p>';
echo '<p class="description">' . esc_html__( 'Outbound requests normally identify this site in the User-Agent with its URL (for example "WordPress/6.9; https://example.com/"). "Send a hash" (default) replaces it with a fixed hash so the site is no longer identifiable; "Do not send anything" removes it completely.', 'robotstxt-telemetry' ) . '</p>';
echo '</fieldset>';
echo '</td>';
echo '</tr>';
$version_mode = Robotstxt_Telemetry_Network::get_wp_version_mode();
echo '<tr>';
echo '<th scope="row">' . esc_html__( 'WordPress version', 'robotstxt-telemetry' ) . '</th>';
echo '<td>';
echo '<fieldset>';
echo '<legend class="screen-reader-text">' . esc_html__( 'WordPress version', 'robotstxt-telemetry' ) . '</legend>';
echo '<p style="margin: 0;">';
$version_options = array(
'actual' => __( 'Actual version', 'robotstxt-telemetry' ),
'major' => __( 'Major version', 'robotstxt-telemetry' ),
'nulled' => __( 'Nulled version', 'robotstxt-telemetry' ),
);
foreach ( $version_options as $value => $label ) {
echo '<label style="display: block; margin: 0;"><input type="radio" name="robotstxt_telemetry_wp_version" value="' . esc_attr( $value ) . '" ' . checked( $version_mode, $value, false ) . '/> ' . esc_html( $label ) . '</label>';
}
echo '</p>';
echo '<p class="description">' . esc_html__( 'The version reported in the User-Agent and in the WordPress.org version fields: the actual version (default), the major version with the rest masked (for example "7.2.n"), or a nulled version ("0.0.0"). The checksum and translation requests always use the real version so updates keep working.', 'robotstxt-telemetry' ) . '</p>';
echo '</fieldset>';
echo '</td>';
echo '</tr>';
echo '<tr>';
echo '<th scope="row">' . esc_html__( 'Installation language', 'robotstxt-telemetry' ) . '</th>';
echo '<td>';
echo '<label for="robotstxt-telemetry-mask-locale">';
echo '<input type="checkbox" id="robotstxt-telemetry-mask-locale" name="robotstxt_telemetry_mask_locale" value="1" ' . checked( (bool) Robotstxt_Telemetry_Network::get_setting( 'robotstxt_telemetry_mask_locale', false ), true, false ) . '/>';
echo '<span>' . esc_html__( 'Send en_US as the language of outbound requests', 'robotstxt-telemetry' ) . '</span>';
echo '</label>';
echo '<p class="description">' . esc_html__( 'When enabled, requests to WordPress.org report English (United States) instead of the installation language. The translation endpoints keep using the real language so installed language packs keep receiving updates.', 'robotstxt-telemetry' ) . '</p>';
echo '</td>';
echo '</tr>';
$dashboard_services = array(
'robotstxt_telemetry_replace_news_feed' => array(
'label' => __( 'Replace the WordPress News feed', 'robotstxt-telemetry' ),
'title' => __( 'Use WordPress Planet by Fair (planet.fair.pm) instead of wordpress.org/news for the Events and News dashboard widget', 'robotstxt-telemetry' ),
),
'robotstxt_telemetry_replace_events_api' => array(
'label' => __( 'Replace the WordPress Events service', 'robotstxt-telemetry' ),
'title' => __( 'Use WordPress Events by The WP World (api.fair.pm) instead of api.wordpress.org/events, sending the same request data', 'robotstxt-telemetry' ),
),
'robotstxt_telemetry_disable_browse_happy' => array(
'label' => __( 'Disable the WordPress browser check', 'robotstxt-telemetry' ),
'title' => __( 'Do not send the browser version to api.wordpress.org/core/browse-happy; the browser is always reported as compatible', 'robotstxt-telemetry' ),
),
);
foreach ( $dashboard_services as $option_name => $service ) {
echo '<tr>';
echo '<th scope="row">' . esc_html( $service['label'] ) . '</th>';
echo '<td>';
echo '<label for="robotstxt-telemetry-' . esc_attr( str_replace( 'robotstxt_telemetry_', '', $option_name ) ) . '">';
echo '<input type="checkbox" id="robotstxt-telemetry-' . esc_attr( str_replace( 'robotstxt_telemetry_', '', $option_name ) ) . '" name="' . esc_attr( $option_name ) . '" value="1" ' . checked( (bool) Robotstxt_Telemetry_Network::get_setting( $option_name, true ), true, false ) . '/>';
echo '<span>' . esc_html( $service['title'] ) . '</span>';
echo '</label>';
echo '</td>';
echo '</tr>';
}
$wp_checks = array(
'robotstxt_telemetry_wp_core_check' => array(
'label' => __( 'WordPress Core version check', 'robotstxt-telemetry' ),
@ -460,9 +680,15 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Admin' ) ) {
echo '</td>';
echo '</tr>';
}
} // End of the 'main' tab.
if ( 'plugins' === $tab ) {
$this->render_plugins_tab();
} // End of the 'plugins' tab.
$retention = Robotstxt_Telemetry_Network::get_setting( 'robotstxt_telemetry_retention_period', '12hours' );
if ( 'logs' === $tab ) {
echo '<tr>';
echo '<th scope="row">' . esc_html__( 'Log retention', 'robotstxt-telemetry' ) . '</th>';
echo '<td>';
@ -488,7 +714,9 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Admin' ) ) {
echo '<p class="description">' . esc_html__( 'Logs older than the selected period are deleted (12 hours by default). A maximum of 1000 entries is always kept; the cleanup runs when browsing the logs and twice a day automatically.', 'robotstxt-telemetry' ) . '</p>';
echo '</td>';
echo '</tr>';
} // End of the 'logs' tab.
if ( 'general' === $tab ) {
echo '<tr>';
echo '<th scope="row">' . esc_html__( 'Uninstall behavior', 'robotstxt-telemetry' ) . '</th>';
echo '<td>';
@ -498,10 +726,79 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Admin' ) ) {
echo '</label>';
echo '<p class="description">' . esc_html__( 'By default, all telemetry logs are preserved when the plugin is uninstalled.', 'robotstxt-telemetry' ) . '</p>';
echo '</td>';
echo '</tr></tbody>';
echo '</tr>';
} // End of the 'general' tab.
echo '</tbody>';
echo '</table>';
}
/**
* Render the Plugins tab contents.
*
* A widefat table lists every installed plugin with a hide checkbox
* (disabled and checked for this plugin), its status, its name,
* and its version.
*/
private function render_plugins_tab() {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$installed = get_plugins();
$hidden = Robotstxt_Telemetry_Network::get_hidden_plugins();
$active_plugins = (array) get_option( 'active_plugins', array() );
echo '<tr>';
echo '<td colspan="2" style="padding-left: 0;">';
echo '<p class="description">' . esc_html__( 'The plugins checked below are never sent to WordPress.org: they are excluded from the plugin update check (including their translations and the active list). That also means they will not receive update notifications from WordPress.org.', 'robotstxt-telemetry' ) . '</p>';
echo '<p class="description"><strong>' . esc_html__( 'Recommendation: keep external or private plugins (those not hosted on WordPress.org) checked, since they do not receive WordPress.org updates anyway and nothing is gained by reporting them.', 'robotstxt-telemetry' ) . '</strong></p>';
echo '</td>';
echo '</tr>';
echo '<tr>';
echo '<td colspan="2" style="padding: 0;">';
echo '<table class="widefat striped robotstxt-telemetry-plugins" style="border: 1px solid #c3c4c7;">';
echo '<thead>';
echo '<tr>';
echo '<td class="check-column" scope="col"><span class="screen-reader-text">' . esc_html__( 'Hide from WordPress.org', 'robotstxt-telemetry' ) . '</span></td>';
echo '<th scope="col">' . esc_html__( 'Status', 'robotstxt-telemetry' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Plugin', 'robotstxt-telemetry' ) . '</th>';
echo '<th scope="col">' . esc_html__( 'Version', 'robotstxt-telemetry' ) . '</th>';
echo '</tr>';
echo '</thead>';
echo '<tbody>';
foreach ( $installed as $basename => $data ) {
$is_forced = 'robotstxt-telemetry/robotstxt-telemetry.php' === $basename;
$is_hidden = in_array( $basename, $hidden, true );
$is_active = in_array( $basename, $active_plugins, true );
$name = isset( $data['Name'] ) && '' !== $data['Name'] ? $data['Name'] : $basename;
$version = isset( $data['Version'] ) ? $data['Version'] : '';
echo '<tr>';
echo '<th scope="row" class="check-column">';
printf(
'<input type="checkbox" name="robotstxt_telemetry_hidden_plugins[]" value="%s"%s%s />',
esc_attr( $basename ),
checked( $is_hidden, true, false ),
$is_forced ? ' disabled' : ''
);
echo '</th>';
echo '<td>' . esc_html( $is_forced ? __( 'Always hidden', 'robotstxt-telemetry' ) : ( $is_active ? __( 'Active', 'robotstxt-telemetry' ) : __( 'Inactive', 'robotstxt-telemetry' ) ) ) . '</td>';
echo '<td><strong>' . esc_html( $name ) . '</strong>';
echo '<code style="margin-left: 8px;">' . esc_html( $basename ) . '</code>';
echo '</td>';
echo '<td>' . esc_html( $version ) . '</td>';
echo '</tr>';
}
echo '</tbody>';
echo '</table>';
echo '</td>';
echo '</tr>';
}
/**
* Render logs page.
*/

View file

@ -0,0 +1,188 @@
<?php
/**
* Endpoint rewrites and blocks.
*
* Intercepts outbound requests to WordPress.org services at the HTTP
* layer and, depending on the settings (all enabled by default):
*
* - Replaces the WordPress News feed with WordPress Planet by Fair.
* - Replaces the WordPress Events service with WordPress Events by
* The WP World, sending the same request data.
* - Blocks the Browse Happy browser check, answering that the browser
* is compatible.
*
* Also rewrites the locale query parameters when the installation
* language masking is enabled (the rewrite must happen at this layer
* because WordPress does not provide a request-URL filter).
*
* @package RobotstxtTelemetry
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'Robotstxt_Telemetry_Endpoints' ) ) {
/**
* Class Robotstxt_Telemetry_Endpoints
*/
class Robotstxt_Telemetry_Endpoints {
/**
* Replacement URL for the WordPress News feed.
*
* @var string
*/
const NEWS_FEED_URL = 'https://planet.fair.pm/atom.xml';
/**
* Replacement URL for the WordPress Events service.
*
* @var string
*/
const EVENTS_API_URL = 'https://api.fair.pm/fair/v1/events';
/**
* Register hooks.
*/
public function register() {
add_filter( 'pre_http_request', array( $this, 'intercept' ), 10, 3 );
}
/**
* Intercept outbound WordPress.org requests.
*
* @param false|array|WP_Error $preempt Short-circuit value from other filters.
* @param array $args HTTP request arguments.
* @param string $url Request URL.
* @return false|array|WP_Error
*/
public function intercept( $preempt, $args, $url ) {
if ( false !== $preempt ) {
return $preempt;
}
$parsed = wp_parse_url( $url );
if ( empty( $parsed['host'] ) ) {
return $preempt;
}
$host = strtolower( $parsed['host'] );
$path = isset( $parsed['path'] ) ? $parsed['path'] : '';
if ( 'api.wordpress.org' === $host ) {
// Browse Happy: answer that the browser is compatible.
if ( 0 === stripos( $path, '/core/browse-happy' ) && $this->setting( 'robotstxt_telemetry_disable_browse_happy' ) ) {
return $this->mock_browse_happy_response();
}
// Events: proxy to the replacement service, same request data.
if ( 0 === stripos( $path, '/events' ) && $this->setting( 'robotstxt_telemetry_replace_events_api' ) ) {
$query = isset( $parsed['query'] ) ? '?' . $parsed['query'] : '';
return wp_remote_request( self::EVENTS_API_URL . $query, $args );
}
// Locale masking: proxy to the rewritten URL. The WordPress
// version is never masked in query parameters: the update
// and information APIs need the real version to work.
if ( 0 !== stripos( $path, '/translations' ) && $this->mask_locale_enabled() ) {
$masked = $this->mask_locale_url( $url );
if ( $masked !== $url ) {
return wp_remote_request( $masked, $args );
}
}
return $preempt;
}
// News feed: proxy to the replacement feed (also covers Rosetta hosts).
$is_wordpress_host = 'wordpress.org' === $host || '.wordpress.org' === substr( $host, -strlen( '.wordpress.org' ) );
if ( $is_wordpress_host && '/news/feed/' === $path && $this->setting( 'robotstxt_telemetry_replace_news_feed' ) ) {
return wp_remote_request( self::NEWS_FEED_URL, $args );
}
return $preempt;
}
/**
* Build a successful Browse Happy response marking the browser
* as up to date and secure, so no browser nag is shown.
*
* @return array
*/
private function mock_browse_happy_response() {
$body = wp_json_encode(
array(
'platform' => 'other',
'name' => 'Browser',
'version' => '1.0',
'current_version' => '1.0',
'upgrade' => false,
'insecure' => false,
'update_url' => '',
'img_src' => '',
'img_src_ssl' => '',
)
);
return array(
'headers' => array(),
'body' => $body,
'response' => array(
'code' => 200,
'message' => 'OK',
),
'cookies' => array(),
'filename' => null,
);
}
/**
* Whether the installation language should be masked as en_US.
*
* @return bool
*/
private function mask_locale_enabled() {
return (bool) Robotstxt_Telemetry_Network::get_setting( 'robotstxt_telemetry_mask_locale', false );
}
/**
* Whether a feature setting is enabled.
*
* @param string $name Option name.
* @return bool
*/
private function setting( $name ) {
return (bool) Robotstxt_Telemetry_Network::get_setting( $name, true );
}
/**
* Replace the locale query parameters of a WordPress.org URL.
*
* Covers standalone `locale` parameters and `request[locale]`
* parameters (also URL-encoded).
*
* @param string $url Request URL.
* @return string
*/
private function mask_locale_url( $url ) {
$masked = preg_replace(
'/([?&](?:request(?:%5[Bb]|\[)locale(?:%5[Dd]|\])|locale)=)[^&]*/',
'${1}en_US',
$url,
-1,
$count
);
if ( $count > 0 && null !== $masked ) {
return $masked;
}
return $url;
}
}
}

View file

@ -36,12 +36,18 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Network' ) ) {
* @var array
*/
const SETTINGS = array(
'robotstxt_telemetry_hash_useragent_url' => true,
'robotstxt_telemetry_wp_core_check' => 'safe',
'robotstxt_telemetry_wp_themes_check' => 'safe',
'robotstxt_telemetry_wp_plugins_check' => 'safe',
'robotstxt_telemetry_retention_period' => '12hours',
'robotstxt_telemetry_delete_on_uninstall' => false,
'robotstxt_telemetry_useragent_url' => 'hash',
'robotstxt_telemetry_wp_version' => 'actual',
'robotstxt_telemetry_mask_locale' => false,
'robotstxt_telemetry_replace_news_feed' => true,
'robotstxt_telemetry_replace_events_api' => true,
'robotstxt_telemetry_disable_browse_happy' => true,
'robotstxt_telemetry_wp_core_check' => 'safe',
'robotstxt_telemetry_wp_themes_check' => 'safe',
'robotstxt_telemetry_wp_plugins_check' => 'safe',
'robotstxt_telemetry_hidden_plugins' => array(),
'robotstxt_telemetry_retention_period' => '12hours',
'robotstxt_telemetry_delete_on_uninstall' => false,
);
/**
@ -179,6 +185,79 @@ if ( ! class_exists( 'Robotstxt_Telemetry_Network' ) ) {
return 'global' === $value ? 'global' : 'per-site';
}
/**
* The plugin basenames never sent to the WordPress.org update check.
*
* The telemetry plugin itself is always included.
*
* @return string[]
*/
public static function get_hidden_plugins() {
$hidden = self::get_setting( 'robotstxt_telemetry_hidden_plugins', array() );
if ( ! is_array( $hidden ) ) {
$hidden = array();
}
$hidden[] = 'robotstxt-telemetry/robotstxt-telemetry.php';
return array_values( array_unique( $hidden ) );
}
/**
* The User-Agent URL mode: 'url', 'hash' (default), or 'none'.
*
* Falls back to the legacy boolean option when the new option is
* not set yet, so existing installations keep their behavior.
*
* @return string
*/
public static function get_useragent_url_mode() {
$mode = self::get_setting( 'robotstxt_telemetry_useragent_url', '' );
if ( in_array( $mode, array( 'url', 'hash', 'none' ), true ) ) {
return $mode;
}
$legacy = self::get_setting( 'robotstxt_telemetry_hash_useragent_url', true );
return (bool) $legacy ? 'hash' : 'url';
}
/**
* The WordPress version mode: 'actual' (default), 'major', or 'nulled'.
*
* @return string
*/
public static function get_wp_version_mode() {
$mode = self::get_setting( 'robotstxt_telemetry_wp_version', 'actual' );
return in_array( $mode, array( 'actual', 'major', 'nulled' ), true ) ? $mode : 'actual';
}
/**
* Mask a WordPress version value according to the mode.
*
* 'major' keeps the first two segments and replaces the rest with
* an "n" (for example "6.9.1" becomes "6.9.n", "7.0" becomes
* "7.0.n"); 'nulled' always returns "0.0.0".
*
* @param string $version Version value.
* @param string $mode Version mode.
* @return string
*/
public static function mask_wp_version( $version, $mode ) {
if ( 'nulled' === $mode ) {
return '0.0.0';
}
if ( 'major' === $mode && preg_match( '/^(\d+\.\d+)/', (string) $version, $matches ) ) {
return $matches[1] . '.n';
}
return (string) $version;
}
/**
* Seed the network settings from the main site when switching to
* global mode, so the network keeps behaving like the main site.

View file

@ -87,6 +87,11 @@ if ( ! class_exists( 'Robotstxt_Telemetry_WordPress_Api' ) ) {
$path = isset( $parsed['path'] ) ? $parsed['path'] : '';
// The translations endpoints need the real locale to work.
if ( $this->mask_locale_enabled() && 0 !== stripos( $path, '/translations' ) ) {
$args = $this->mask_locale_in_body( $args );
}
if ( 0 === stripos( $path, '/core/version-check' ) ) {
return $this->filter_core_request( $args );
}
@ -96,6 +101,8 @@ if ( ! class_exists( 'Robotstxt_Telemetry_WordPress_Api' ) ) {
}
if ( 0 === stripos( $path, '/plugins/update-check' ) ) {
$args = $this->exclude_hidden_plugins( $args );
return $this->filter_plugins_request( $args );
}
@ -216,6 +223,104 @@ if ( ! class_exists( 'Robotstxt_Telemetry_WordPress_Api' ) ) {
return $args;
}
/**
* Remove the hidden plugins from the update check payload.
*
* Applies in both Safe and Original modes: the checked plugins are
* never reported to WordPress.org (neither their plugin data, nor
* the active list, nor their translation projects), which also
* means they stop receiving WordPress.org update notifications.
*
* @param array $args HTTP request arguments.
* @return array
*/
private function exclude_hidden_plugins( $args ) {
$hidden = Robotstxt_Telemetry_Network::get_hidden_plugins();
if ( empty( $hidden ) || empty( $args['body']['plugins'] ) || ! is_string( $args['body']['plugins'] ) ) {
return $args;
}
$data = json_decode( $args['body']['plugins'], true );
if ( ! is_array( $data ) ) {
return $args;
}
$hidden_slugs = array();
foreach ( $hidden as $basename ) {
$slug = dirname( $basename );
$hidden_slugs[ $slug ] = true;
unset( $data['plugins'][ $basename ], $data['active'][ $basename ] );
}
if ( isset( $args['body']['translations'] ) && is_string( $args['body']['translations'] ) ) {
$translations = json_decode( $args['body']['translations'], true );
if ( is_array( $translations ) ) {
foreach ( array_keys( $translations ) as $slug ) {
if ( isset( $hidden_slugs[ $slug ] ) ) {
unset( $translations[ $slug ] );
}
}
$args['body']['translations'] = wp_json_encode( $translations );
}
}
$args['body']['plugins'] = wp_json_encode( $data );
return $args;
}
/**
* Whether the installation language should be masked as en_US.
*
* @return bool
*/
private function mask_locale_enabled() {
return (bool) Robotstxt_Telemetry_Network::get_setting( 'robotstxt_telemetry_mask_locale', false );
}
/**
* Replace the locale field of a WordPress.org request body.
*
* Handles the JSON-encoded locale arrays of the update checks
* (for example `["es_ES","en_US"]`) and plain locale strings.
*
* @param array $args HTTP request arguments.
* @return array
*/
private function mask_locale_in_body( $args ) {
if ( ! isset( $args['body']['locale'] ) || ! is_array( $args['body'] ) ) {
return $args;
}
$locale = $args['body']['locale'];
if ( is_string( $locale ) ) {
$decoded = json_decode( $locale, true );
if ( is_array( $decoded ) ) {
$masked = array();
foreach ( $decoded as $single_locale ) {
$masked[] = 'en_US';
}
$args['body']['locale'] = wp_json_encode( array_values( array_unique( $masked ) ) );
return $args;
}
$args['body']['locale'] = 'en_US';
}
return $args;
}
/**
* Read a string field from the request body arguments.
*

View file

@ -31,11 +31,14 @@ if ( ! class_exists( 'Robotstxt_Telemetry' ) ) {
add_action( 'init', array( 'Robotstxt_Telemetry_DB', 'maybe_upgrade' ) );
add_action( 'init', array( $this, 'schedule_cleanup' ) );
add_action( 'robotstxt_telemetry_cleanup', array( 'Robotstxt_Telemetry_DB', 'cleanup' ) );
add_filter( 'http_request_args', array( $this, 'hash_useragent_url' ), 9, 2 );
add_filter( 'http_request_args', array( $this, 'filter_useragent' ), 9, 2 );
$wordpress_api = new Robotstxt_Telemetry_WordPress_Api();
$wordpress_api->register();
$endpoints = new Robotstxt_Telemetry_Endpoints();
$endpoints->register();
$logger = new Robotstxt_Telemetry_Logger();
$logger->register();
@ -49,13 +52,14 @@ if ( ! class_exists( 'Robotstxt_Telemetry' ) ) {
}
/**
* Replace the site URL in the outbound User-Agent with a fixed hash.
* Filter the outbound User-Agent.
*
* The default outbound User-Agent is "WordPress/<version>; <site URL>",
* and the WordPress.org update checks send an explicit one with the
* same shape. When the setting is enabled, the URL part is replaced
* with a deterministic SHA-256 hash so the site is no longer
* identifiable while staying consistent across requests.
* same shape. Depending on the settings, the site URL is sent as-is,
* replaced with a deterministic SHA-256 hash, or removed, and the
* WordPress version can be reduced to its major version ("7.2.n")
* or nulled ("0.0.0").
*
* Runs at priority 9 so the telemetry logger records the value that
* is actually sent on the wire.
@ -64,30 +68,72 @@ if ( ! class_exists( 'Robotstxt_Telemetry' ) ) {
* @param string $url Request URL.
* @return array
*/
public function hash_useragent_url( $args, $url ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter -- Second argument required by the http_request_args filter signature.
if ( ! (bool) Robotstxt_Telemetry_Network::get_setting( 'robotstxt_telemetry_hash_useragent_url', true ) ) {
public function filter_useragent( $args, $url ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter -- Second argument required by the http_request_args filter signature.
$url_mode = Robotstxt_Telemetry_Network::get_useragent_url_mode();
$version_mode = Robotstxt_Telemetry_Network::get_wp_version_mode();
if ( 'url' === $url_mode && 'actual' === $version_mode ) {
return $args;
}
$site_url = get_bloginfo( 'url' );
if ( '' === $site_url ) {
return $args;
if ( isset( $args['user-agent'] ) && is_string( $args['user-agent'] ) ) {
$args['user-agent'] = $this->mask_user_agent( $args['user-agent'], $url_mode, $version_mode, $site_url );
}
$hash = substr( hash( 'sha256', $site_url ), 0, 16 );
if ( isset( $args['user-agent'] ) && is_string( $args['user-agent'] ) && false !== strpos( $args['user-agent'], $site_url ) ) {
$args['user-agent'] = str_replace( $site_url, $hash, $args['user-agent'] );
}
if ( isset( $args['headers']['User-Agent'] ) && is_string( $args['headers']['User-Agent'] ) && false !== strpos( $args['headers']['User-Agent'], $site_url ) ) {
$args['headers']['User-Agent'] = str_replace( $site_url, $hash, $args['headers']['User-Agent'] );
foreach ( array( 'User-Agent', 'user-agent' ) as $header ) {
if ( isset( $args['headers'][ $header ] ) && is_string( $args['headers'][ $header ] ) ) {
$args['headers'][ $header ] = $this->mask_user_agent( $args['headers'][ $header ], $url_mode, $version_mode, $site_url );
}
}
return $args;
}
/**
* Apply the URL and version masking to a User-Agent string.
*
* @param string $user_agent User-Agent value.
* @param string $url_mode 'url', 'hash', or 'none'.
* @param string $version_mode 'actual', 'major', or 'nulled'.
* @param string $site_url Site URL.
* @return string
*/
private function mask_user_agent( $user_agent, $url_mode, $version_mode, $site_url ) {
// Core sends the site URL with (update checks) and without (HTTP
// API) a trailing slash; the slashed form must be replaced first
// so its slash does not leak, then the bare form.
$bare = untrailingslashit( $site_url );
$hash = substr( hash( 'sha256', $bare ), 0, 16 );
$url_forms = array( $bare . '/', $bare );
$url_forms = array_unique( $url_forms );
foreach ( $url_forms as $url_form ) {
if ( false === strpos( $user_agent, $url_form ) ) {
continue;
}
if ( 'hash' === $url_mode ) {
$user_agent = str_replace( $url_form, $hash, $user_agent );
} elseif ( 'none' === $url_mode ) {
$user_agent = str_replace( '; ' . $url_form, '', $user_agent );
$user_agent = str_replace( $url_form, '', $user_agent );
}
}
$user_agent = trim( $user_agent );
if ( 'actual' !== $version_mode && preg_match( '#^WordPress/(\d+[^\s;]*)#', $user_agent, $matches ) ) {
$user_agent = preg_replace(
'#^WordPress/[^\s;]+#',
'WordPress/' . Robotstxt_Telemetry_Network::mask_wp_version( $matches[1], $version_mode ),
$user_agent
);
}
return $user_agent;
}
/**
* Schedule the log cleanup event if it is not scheduled yet.
*

View file

@ -2,14 +2,14 @@
# This file is distributed under the GPL v3 or later.
msgid ""
msgstr ""
"Project-Id-Version: Telemetry disabler (by ROBOTSTXT) 0.6.3\n"
"Project-Id-Version: Telemetry disabler (by ROBOTSTXT) 1.0.0\n"
"Report-Msgid-Bugs-To: https://www.robotstxt.software/plugins/robotstxt-telemetry/"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
"POT-Creation-Date: 2026-08-18T15:18:00+00:00\n"
"POT-Creation-Date: 2026-08-18T16:17:10+00:00\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"X-Generator: WP-CLI 2.12.0\n"
"X-Domain: robotstxt-telemetry\n"
@ -50,6 +50,7 @@ msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:53
#: includes/class-robotstxt-telemetry-admin.php:92
#: includes/class-robotstxt-telemetry-admin.php:93
#: includes/class-robotstxt-telemetry-admin.php:491
msgid "Settings"
msgstr ""
@ -57,213 +58,324 @@ msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:62
#: includes/class-robotstxt-telemetry-admin.php:102
#: includes/class-robotstxt-telemetry-admin.php:103
#: includes/class-robotstxt-telemetry-admin.php:493
msgid "Logs"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:318
#: includes/class-robotstxt-telemetry-admin.php:400
msgid "Settings saved."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:322
#: includes/class-robotstxt-telemetry-admin.php:404
msgid "Configuration mode saved."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:338
#: includes/class-robotstxt-telemetry-admin.php:421
msgid "Telemetry Network Settings"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:348
#: includes/class-robotstxt-telemetry-admin.php:351
#: includes/class-robotstxt-telemetry-admin.php:432
#: includes/class-robotstxt-telemetry-admin.php:435
msgid "Configuration mode"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:353
#: includes/class-robotstxt-telemetry-admin.php:437
msgid "Per-site"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:355
#: includes/class-robotstxt-telemetry-admin.php:439
msgid "Global"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:357
#: includes/class-robotstxt-telemetry-admin.php:441
msgid "Per-site: every site keeps its own settings and its own log table, managed by each site administrator. Global: one shared configuration and one central log on the main site collect the outbound requests of the whole network; the Telemetry screens are then managed from here only."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:363
#: includes/class-robotstxt-telemetry-admin.php:447
msgid "Save configuration mode"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:367
#: includes/class-robotstxt-telemetry-admin.php:452
msgid "Switch to Global mode to manage the settings and the logs for the whole network from this screen."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:372
msgid "Network settings"
#: includes/class-robotstxt-telemetry-admin.php:492
msgid "Plugins"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:395
msgid "Telemetry Settings"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:421
msgid "Outbound requests"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:425
msgid "Hash the site URL in the User-Agent of outbound requests"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:427
msgid "Outbound requests normally identify this site with its URL (for example \"WordPress/6.9; https://example.com/\"). When enabled, the URL is replaced with a fixed hash so the site is no longer identifiable."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:433
msgid "WordPress Core version check"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:434
msgid "Safe sends only the WordPress version, PHP version, locale, MySQL version, and update channel. Original also sends site counts, database history, PHP extensions, and platform details."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:437
msgid "WordPress Themes version check"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:438
msgid "Safe sends only the theme name, version, update URI, template, and stylesheet, plus the translation revision date and site locale. Original also sends author details and other theme metadata."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:441
msgid "WordPress Plugins version check"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:442
msgid "Safe sends only the plugin version, update URI, and requirements, plus the translation revision date, locale, and the \"all\" flag. Original also sends names, descriptions, authors, and other plugin metadata."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:454
msgid "Original"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:456
msgid "Safe"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:467
#: includes/class-robotstxt-telemetry-admin.php:469
msgid "Log retention"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:473
msgid "12 hours"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:474
msgid "1 day"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:475
msgid "3 days"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:488
msgid "Logs older than the selected period are deleted (12 hours by default). A maximum of 1000 entries is always kept; the cleanup runs when browsing the logs and twice a day automatically."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:493
msgid "Uninstall behavior"
#: includes/class-robotstxt-telemetry-admin.php:494
msgid "General"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:497
msgid "Delete all telemetry logs and plugin options on uninstall"
msgid "Settings sections"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:499
msgid "By default, all telemetry logs are preserved when the plugin is uninstalled."
#: includes/class-robotstxt-telemetry-admin.php:531
msgid "Telemetry Settings"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:522
msgid "Telemetry Logs"
#: includes/class-robotstxt-telemetry-admin.php:566
#: includes/class-robotstxt-telemetry-admin.php:569
msgid "Outbound requests"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:542
msgid "All log entries deleted."
#: includes/class-robotstxt-telemetry-admin.php:573
msgid "Send your URL"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:544
msgid "Log entry deleted."
#: includes/class-robotstxt-telemetry-admin.php:574
msgid "Send a hash"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:558
msgid "Delete all logs"
#: includes/class-robotstxt-telemetry-admin.php:575
msgid "Do not send anything"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:576
msgid "You are about to permanently delete all telemetry log entries. This action cannot be undone."
#: includes/class-robotstxt-telemetry-admin.php:583
msgid "Outbound requests normally identify this site in the User-Agent with its URL (for example \"WordPress/6.9; https://example.com/\"). \"Send a hash\" (default) replaces it with a fixed hash so the site is no longer identifiable; \"Do not send anything\" removes it completely."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:584
msgid "Yes, delete all logs"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:585
msgid "Cancel"
#: includes/class-robotstxt-telemetry-admin.php:591
#: includes/class-robotstxt-telemetry-admin.php:594
msgid "WordPress version"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:598
msgid "Actual version"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:599
msgid "Major version"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:600
msgid "Nulled version"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:608
msgid "The version reported in the User-Agent and in the WordPress.org version fields: the actual version (default), the major version with the rest masked (for example \"7.2.n\"), or a nulled version (\"0.0.0\"). The checksum and translation requests always use the real version so updates keep working."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:614
msgid "Installation language"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:618
msgid "Send en_US as the language of outbound requests"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:620
msgid "When enabled, requests to WordPress.org report English (United States) instead of the installation language. The translation endpoints keep using the real language so installed language packs keep receiving updates."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:626
msgid "Replace the WordPress News feed"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:627
msgid "Use WordPress Planet by Fair (planet.fair.pm) instead of wordpress.org/news for the Events and News dashboard widget"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:630
msgid "Replace the WordPress Events service"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:631
msgid "Use WordPress Events by The WP World (api.fair.pm) instead of api.wordpress.org/events, sending the same request data"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:634
msgid "Disable the WordPress browser check"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:635
msgid "Do not send the browser version to api.wordpress.org/core/browse-happy; the browser is always reported as compatible"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:653
msgid "WordPress Core version check"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:654
msgid "Safe sends only the WordPress version, PHP version, locale, MySQL version, and update channel. Original also sends site counts, database history, PHP extensions, and platform details."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:657
msgid "WordPress Themes version check"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:658
msgid "Safe sends only the theme name, version, update URI, template, and stylesheet, plus the translation revision date and site locale. Original also sends author details and other theme metadata."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:661
msgid "WordPress Plugins version check"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:662
msgid "Safe sends only the plugin version, update URI, and requirements, plus the translation revision date, locale, and the \"all\" flag. Original also sends names, descriptions, authors, and other plugin metadata."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:674
msgid "Original"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:676
msgid "Safe"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:693
#: includes/class-robotstxt-telemetry-admin.php:695
msgid "Log retention"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:699
msgid "12 hours"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:700
msgid "1 day"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:701
msgid "3 days"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:714
msgid "Logs older than the selected period are deleted (12 hours by default). A maximum of 1000 entries is always kept; the cleanup runs when browsing the logs and twice a day automatically."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:721
msgid "Uninstall behavior"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:725
msgid "Delete all telemetry logs and plugin options on uninstall"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:727
msgid "By default, all telemetry logs are preserved when the plugin is uninstalled."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:754
msgid "The plugins checked below are never sent to WordPress.org: they are excluded from the plugin update check (including their translations and the active list). That also means they will not receive update notifications from WordPress.org."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:755
msgid "Recommendation: keep external or private plugins (those not hosted on WordPress.org) checked, since they do not receive WordPress.org updates anyway and nothing is gained by reporting them."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:764
msgid "Hide from WordPress.org"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:765
msgid "Status"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:766
msgid "Plugin"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:767
msgid "Version"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:788
msgid "Always hidden"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:788
msgid "Active"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:788
msgid "Inactive"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:819
msgid "Telemetry Logs"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:839
msgid "All log entries deleted."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:841
msgid "Log entry deleted."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:855
msgid "Delete all logs"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:873
msgid "You are about to permanently delete all telemetry log entries. This action cannot be undone."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:881
msgid "Yes, delete all logs"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:882
msgid "Cancel"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:895
msgid "Log entry not found."
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:602
#: includes/class-robotstxt-telemetry-admin.php:899
msgid "Back to logs"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:606
#: includes/class-robotstxt-telemetry-admin.php:903
#: includes/class-robotstxt-telemetry-logs-table.php:49
msgid "Date"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:607
#: includes/class-robotstxt-telemetry-admin.php:904
#: includes/class-robotstxt-telemetry-logs-table.php:50
msgid "Method"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:608
#: includes/class-robotstxt-telemetry-admin.php:905
msgid "URL"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:609
#: includes/class-robotstxt-telemetry-admin.php:906
#: includes/class-robotstxt-telemetry-logs-table.php:51
#: includes/class-robotstxt-telemetry-logs-table.php:318
msgid "Host"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:610
#: includes/class-robotstxt-telemetry-admin.php:907
#: includes/class-robotstxt-telemetry-logs-table.php:52
msgid "Path"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:611
#: includes/class-robotstxt-telemetry-admin.php:908
msgid "Body Params"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:612
#: includes/class-robotstxt-telemetry-admin.php:909
msgid "Headers"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:613
#: includes/class-robotstxt-telemetry-admin.php:910
msgid "User Agent"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:614
#: includes/class-robotstxt-telemetry-admin.php:911
msgid "Raw Body"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:615
#: includes/class-robotstxt-telemetry-admin.php:912
msgid "Caller"
msgstr ""
#: includes/class-robotstxt-telemetry-admin.php:635
#: includes/class-robotstxt-telemetry-admin.php:932
msgid "Telemetry Analysis"
msgstr ""

View file

@ -3,9 +3,9 @@ Contributors: robotstxt, javiercasares
Tags: telemetry, privacy, http, requests, logging
Requires at least: 4.0
Tested up to: 7.1
Stable tag: 0.6.3
Stable tag: 1.0.0
Requires PHP: 5.6
Version: 0.6.3
Version: 1.0.0
License: GPL-3.0-or-later
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
@ -28,7 +28,13 @@ The three WordPress.org update checks run in "Safe" mode by default, meaning the
Each check can be switched back to "Original" in the Settings screen to send exactly what WordPress would send without this plugin.
Additionally, outbound requests normally identify the site in the User-Agent header (for example "WordPress/6.9; https://example.com/"). With the "Hash the site URL in the User-Agent" setting (enabled by default), the URL is replaced with a fixed hash so the site is no longer identifiable while remaining consistent across requests.
The dashboard services can also be redirected (all enabled by default): the Events and News widget reads [WordPress Planet by Fair](https://planet.fair.pm/) instead of WordPress News, the events list comes from [WordPress Events by The WP World](https://api.fair.pm/fair/v1/events) instead of the WordPress.org Events API (sending the same request data), and the Browse Happy browser check is disabled and always reports the browser as compatible, so no browser nag is shown.
Outbound requests normally identify the site in the User-Agent header (for example "WordPress/6.9; https://example.com/"). The "Outbound requests" setting controls what is sent: the site URL ("Send your URL"), a fixed hash so the site is no longer identifiable while remaining consistent across requests ("Send a hash", default), or no identifier at all ("Do not send anything").
The reported WordPress version is also configurable: the "WordPress version" setting sends the actual version (default), the major version with the rest masked (for example "7.2.n"), or a nulled version ("0.0.0"). The version is masked in the User-Agent and in the WordPress.org version fields; the checksum and translation requests always use the real version so updates keep working.
The installation language can also be hidden: with the "Send en_US as the language of outbound requests" setting (disabled by default), every request to WordPress.org reports English (United States) instead of the real installation language, including the language lists of the update checks and the locale parameters of the core version check, credits, community events, and plugin/theme information APIs. The translation endpoints keep using the real language so installed language packs keep receiving updates.
= See everything =
@ -57,7 +63,23 @@ Navigate to **Telemetry → Logs**. Use the method, host and date filters to nar
= Settings =
Navigate to **Telemetry → Settings**. The three WordPress.org update checks (Core, Themes, Plugins) can run in "Safe" mode (default, limited data) or "Original" mode (unchanged WordPress behavior). "Hash the site URL in the User-Agent of outbound requests" (enabled by default) replaces the site URL in outbound User-Agent headers with a fixed hash. "Log retention" controls how long entries are kept (12 hours by default, 1 day, or 3 days). The "Delete all telemetry logs and plugin options on uninstall" checkbox controls the uninstall behavior: unchecked by default, so all telemetry data is preserved when the plugin is removed. When the Manager (by ROBOTSTXT) plugin is not active, a permanent notice about updates is shown on this screen.
Navigate to **Telemetry → Settings**. The screen is organized in native WordPress tabs: **Settings** (privacy controls), **Logs** (log retention), and **General** (uninstall behavior); each tab saves independently.
= Settings tab =
The three WordPress.org update checks (Core, Themes, Plugins) can run in "Safe" mode (default, limited data) or "Original" mode (unchanged WordPress behavior). "Outbound requests" controls the site identifier in the User-Agent: Send your URL, Send a hash (default), or Do not send anything. "WordPress version" controls the reported version: Actual version (default), Major version (for example "7.2.n"), or Nulled version ("0.0.0"). "Send en_US as the language of outbound requests" (disabled by default) hides the installation language from WordPress.org requests, except the translation endpoints. The dashboard services section controls the WordPress News feed replacement (enabled by default), the WordPress Events replacement (enabled by default), and the browser check disable (enabled by default).
= Plugins tab =
Every installed plugin is listed in a table with its status, name, and version. Checked plugins are never sent to WordPress.org: they are excluded from the plugin update check (including their translations and the active list), which also means they will not receive update notifications from WordPress.org. All plugins are unchecked by default, except this one, which is always hidden. Keeping external or private plugins (those not hosted on WordPress.org) checked is recommended, since they do not receive WordPress.org updates anyway.
= Logs tab =
"Log retention" controls how long entries are kept (12 hours by default, 1 day, or 3 days).
= General tab =
The "Delete all telemetry logs and plugin options on uninstall" checkbox controls the uninstall behavior: unchecked by default, so all telemetry data is preserved when the plugin is removed. When the Manager (by ROBOTSTXT) plugin is not active, a permanent notice about updates is shown on this screen.
= Multisite =
@ -120,19 +142,19 @@ On WordPress versions older than 5.1, the Core update check Safe mode cannot red
Only the 3 last versions. The full changelog will be at changelog.txt
= 0.6.3 =
= 1.0.0 =
* Changed: translations payloads in the WordPress.org update checks now keep the PO-Revision-Date (needed to know whether a translation update is available); other translation metadata is still removed
* First stable release: outbound HTTP request logging with secret redaction, Safe modes for the WordPress.org Core/Themes/Plugins update checks, per-plugin hiding, User-Agent and version controls, installation language masking, dashboard services replaced (news, events, browser check), log retention, and Multisite Per-site/Global modes
= 0.6.2 =
= 0.10.0 =
* Changed: translations payloads in the WordPress.org update checks now send empty entries (only the project and locale identifiers), removing the POT/PO dates
* Added: "Plugins" settings tab listing every installed plugin in a table (hide checkbox, status, name, version); checked plugins are never sent to WordPress.org (no update notifications either), all unchecked by default except this plugin which is always hidden
* Changed: the WordPress version masking is now User-Agent only; query parameters always send the real version so updates keep working
* Fixed: the User-Agent masking now covers the trailing-slash form of the site URL used by the update checks
= 0.6.1 =
= 0.9.1 =
* Changed: minimum requirements lowered to WordPress 4.0 and PHP 5.6, with compatibility fallbacks for older WordPress APIs
* Fixed: duplicated "Save Changes" button on the Settings screen
* Changed: plugin renamed to "Telemetry disabler (by ROBOTSTXT)" with a new description
* Changed: the Settings screen is organized in native WordPress tabs (Settings, Plugins, Logs, General), each saving independently
= Previous versions =

View file

@ -4,7 +4,7 @@
* Plugin URI: https://www.robotstxt.software/plugins/robotstxt-telemetry/
* Update URI: https://www.robotstxt.software/plugins/robotstxt-telemetry/
* Description: Reduces the telemetry WordPress sends out and logs every outbound HTTP request, so your site shares less and you can see everything.
* Version: 0.6.3
* Version: 1.0.0
* Author: ROBOTSTXT
* Author URI: https://www.robotstxt.software/
* Text Domain: robotstxt-telemetry
@ -23,12 +23,13 @@ if ( ! defined( 'ABSPATH' ) ) {
exit;
}
define( 'ROBOTSTXT_TELEMETRY_VERSION', '0.6.3' );
define( 'ROBOTSTXT_TELEMETRY_VERSION', '1.0.0' );
define( 'ROBOTSTXT_TELEMETRY_DB_VERSION', '1.1.0' );
define( 'ROBOTSTXT_TELEMETRY_PLUGIN_FILE', __FILE__ );
define( 'ROBOTSTXT_TELEMETRY_PLUGIN_DIR', __DIR__ );
require_once ROBOTSTXT_TELEMETRY_PLUGIN_DIR . '/includes/class-robotstxt-telemetry-network.php';
require_once ROBOTSTXT_TELEMETRY_PLUGIN_DIR . '/includes/class-robotstxt-telemetry-endpoints.php';
require_once ROBOTSTXT_TELEMETRY_PLUGIN_DIR . '/includes/class-robotstxt-telemetry-db.php';
require_once ROBOTSTXT_TELEMETRY_PLUGIN_DIR . '/includes/class-robotstxt-telemetry-logger.php';
require_once ROBOTSTXT_TELEMETRY_PLUGIN_DIR . '/includes/class-robotstxt-telemetry-admin.php';

View file

@ -75,9 +75,16 @@ function robotstxt_telemetry_uninstall_site( $force_drop = null ) {
delete_option( 'robotstxt_telemetry_delete_on_uninstall' );
delete_option( 'robotstxt_telemetry_hash_useragent_url' );
delete_option( 'robotstxt_telemetry_useragent_url' );
delete_option( 'robotstxt_telemetry_wp_version' );
delete_option( 'robotstxt_telemetry_mask_locale' );
delete_option( 'robotstxt_telemetry_replace_news_feed' );
delete_option( 'robotstxt_telemetry_replace_events_api' );
delete_option( 'robotstxt_telemetry_disable_browse_happy' );
delete_option( 'robotstxt_telemetry_wp_core_check' );
delete_option( 'robotstxt_telemetry_wp_themes_check' );
delete_option( 'robotstxt_telemetry_wp_plugins_check' );
delete_option( 'robotstxt_telemetry_hidden_plugins' );
delete_option( 'robotstxt_telemetry_retention_period' );
delete_option( 'robotstxt_telemetry_db_version' );
}
@ -91,9 +98,16 @@ function robotstxt_telemetry_uninstall_network() {
delete_site_option( 'robotstxt_telemetry_config_mode' );
delete_site_option( 'robotstxt_telemetry_delete_on_uninstall' );
delete_site_option( 'robotstxt_telemetry_hash_useragent_url' );
delete_site_option( 'robotstxt_telemetry_useragent_url' );
delete_site_option( 'robotstxt_telemetry_wp_version' );
delete_site_option( 'robotstxt_telemetry_mask_locale' );
delete_site_option( 'robotstxt_telemetry_replace_news_feed' );
delete_site_option( 'robotstxt_telemetry_replace_events_api' );
delete_site_option( 'robotstxt_telemetry_disable_browse_happy' );
delete_site_option( 'robotstxt_telemetry_wp_core_check' );
delete_site_option( 'robotstxt_telemetry_wp_themes_check' );
delete_site_option( 'robotstxt_telemetry_wp_plugins_check' );
delete_site_option( 'robotstxt_telemetry_hidden_plugins' );
delete_site_option( 'robotstxt_telemetry_retention_period' );
}