182 lines
5.6 KiB
PHP
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'
|
|
);
|
|
}
|
|
}
|
|
}
|