robotstxt-telemetry/includes/class-robotstxt-telemetry.php
2026-08-18 19:06:22 +00:00

182 lines
5.6 KiB
PHP

<?php
/**
* Main plugin bootstrap.
*
* @package RobotstxtTelemetry
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
if ( ! class_exists( 'Robotstxt_Telemetry' ) ) {
/**
* Class Robotstxt_Telemetry
*/
class Robotstxt_Telemetry {
/**
* Initialize the plugin.
*/
public static function init() {
$instance = new self();
$instance->register_hooks();
}
/**
* Register hooks.
*/
private function register_hooks() {
add_action( 'plugins_loaded', array( $this, 'load_textdomain' ) );
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, 'filter_useragent' ), 9, 2 );
$wordpress_api = new Robotstxt_Telemetry_WordPress_Api();
$wordpress_api->register();
$plugin_guards = new Robotstxt_Telemetry_Plugin_Guards();
$plugin_guards->register();
$endpoints = new Robotstxt_Telemetry_Endpoints();
$endpoints->register();
$logger = new Robotstxt_Telemetry_Logger();
$logger->register();
if ( is_admin() ) {
$notice = new Robotstxt_Telemetry_Manager_Notice();
$notice->register();
$admin = new Robotstxt_Telemetry_Admin();
$admin->register();
}
}
/**
* 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. 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.
*
* @param array $args HTTP request arguments.
* @param string $url Request URL.
* @return array
*/
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 ( 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 );
}
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.
*
* In global mode the shared table is cleaned from the main site
* only, so subsites do not schedule the event.
*/
public function schedule_cleanup() {
if ( Robotstxt_Telemetry_Network::is_global() && ! is_main_site() ) {
return;
}
if ( ! wp_next_scheduled( 'robotstxt_telemetry_cleanup' ) ) {
wp_schedule_event( time() + HOUR_IN_SECONDS, 'twicedaily', 'robotstxt_telemetry_cleanup' );
}
}
/**
* Deactivation handler: clear the scheduled cleanup event.
*/
public static function deactivate() {
wp_clear_scheduled_hook( 'robotstxt_telemetry_cleanup' );
if ( is_multisite() ) {
foreach ( Robotstxt_Telemetry_Network::get_site_ids() as $site_id ) {
switch_to_blog( $site_id );
wp_clear_scheduled_hook( 'robotstxt_telemetry_cleanup' );
restore_current_blog();
}
}
}
/**
* Load plugin translations.
*/
public function load_textdomain() {
load_plugin_textdomain(
'robotstxt-telemetry',
false,
dirname( plugin_basename( ROBOTSTXT_TELEMETRY_PLUGIN_FILE ) ) . '/languages'
);
}
}
}