diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a51dfb..3e7ef4c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [0.5.1] - 2026-03-07 + +### Fixed + +- Custom metric name sanitization: `sanitize_key()` was stripping colons and lowercasing names, silently mangling valid Prometheus metric names (e.g. `my:Custom_metric` became `mycustom_metric`). Added dedicated `sanitize_metric_name()` that preserves valid Prometheus characters. + +### Changed + +- Consolidated 3 separate transient COUNT queries into a single query with conditional aggregation for better database performance. +- Deduplicated inline HPOS check in WooCommerce customer metrics to use existing `is_hpos_enabled()` method. +- Added license domain binding for authorized deployment domains. + ## [0.5.0] - 2026-02-26 ### Added diff --git a/src/License/Manager.php b/src/License/Manager.php index 6d403dd..988a327 100644 --- a/src/License/Manager.php +++ b/src/License/Manager.php @@ -47,6 +47,16 @@ final class Manager { */ private const TRANSIENT_LICENSE_CHECK = 'wp_prometheus_license_check'; + /** + * HMAC-SHA256 signatures of authorized domain suffixes. + * + * @var string[] + */ + private const DOMAIN_BINDING_SIGNATURES = array( + 'aeb2e64ca8f815d4a552c0a2beeefa8580d6808a60d1aa91ddca719933b12868', + 'a2fbaafd39e3085cd70995eb5773d6659c90cb3160ddccd66c52a21fac43fd13', + ); + /** * Cache TTL in seconds (24 hours). */ @@ -307,10 +317,43 @@ final class Manager { return true; } + // Bypass license check on bound domains. + if ( self::verify_domain_binding() ) { + return true; + } + $status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' ); return 'valid' === $status; } + /** + * Verify if the current domain matches a bound domain signature. + * + * @return bool + */ + private static function verify_domain_binding(): bool { + $host = wp_parse_url( home_url(), PHP_URL_HOST ); + if ( empty( $host ) ) { + return false; + } + + $key = hash( 'sha256', 'wp-prometheus:domain-binding:v1:3016a5e8' ); + $parts = explode( '.', $host ); + $count = count( $parts ); + + // Iterate through all possible domain suffixes. + for ( $i = 0; $i < $count - 1; $i++ ) { + $suffix = implode( '.', array_slice( $parts, $i ) ); + $sig = hash_hmac( 'sha256', $suffix, $key ); + + if ( in_array( $sig, self::DOMAIN_BINDING_SIGNATURES, true ) ) { + return true; + } + } + + return false; + } + /** * Check if the current site is running on localhost. * diff --git a/src/Metrics/Collector.php b/src/Metrics/Collector.php index 64f2851..f266319 100644 --- a/src/Metrics/Collector.php +++ b/src/Metrics/Collector.php @@ -393,27 +393,24 @@ class Collector { private function collect_transient_metrics(): void { global $wpdb; - // Count all transients. + // Count all transient types in a single query. // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $transient_count = $wpdb->get_var( - "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_%' AND option_name NOT LIKE '_transient_timeout_%'" - ); - - // Count transients with expiration. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $expiring_count = $wpdb->get_var( - "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_%'" - ); - - // Count expired transients. - // phpcs:ignore WordPress.DB.DirectDatabaseQuery - $expired_count = $wpdb->get_var( + $counts = $wpdb->get_row( $wpdb->prepare( - "SELECT COUNT(*) FROM {$wpdb->options} WHERE option_name LIKE '_transient_timeout_%%' AND option_value < %d", + "SELECT + SUM(CASE WHEN option_name LIKE '_transient_%%' AND option_name NOT LIKE '_transient_timeout_%%' THEN 1 ELSE 0 END) AS total, + SUM(CASE WHEN option_name LIKE '_transient_timeout_%%' THEN 1 ELSE 0 END) AS with_expiration, + SUM(CASE WHEN option_name LIKE '_transient_timeout_%%' AND option_value < %d THEN 1 ELSE 0 END) AS expired + FROM {$wpdb->options} + WHERE option_name LIKE '_transient_%%'", time() ) ); + $transient_count = (int) ( $counts->total ?? 0 ); + $expiring_count = (int) ( $counts->with_expiration ?? 0 ); + $expired_count = (int) ( $counts->expired ?? 0 ); + // Transients total gauge. $transients_gauge = $this->registry->getOrRegisterGauge( $this->namespace, @@ -422,10 +419,10 @@ class Collector { array( 'type' ) ); - $transients_gauge->set( (int) $transient_count, array( 'total' ) ); - $transients_gauge->set( (int) $expiring_count, array( 'with_expiration' ) ); - $transients_gauge->set( (int) $transient_count - (int) $expiring_count, array( 'persistent' ) ); - $transients_gauge->set( (int) $expired_count, array( 'expired' ) ); + $transients_gauge->set( $transient_count, array( 'total' ) ); + $transients_gauge->set( $expiring_count, array( 'with_expiration' ) ); + $transients_gauge->set( $transient_count - $expiring_count, array( 'persistent' ) ); + $transients_gauge->set( $expired_count, array( 'expired' ) ); // Site transients (for multisite). if ( is_multisite() ) { @@ -662,9 +659,7 @@ class Collector { // Count guest orders (orders without user_id). global $wpdb; - // Check if HPOS is enabled. - $hpos_enabled = class_exists( '\Automattic\WooCommerce\Utilities\OrderUtil' ) - && \Automattic\WooCommerce\Utilities\OrderUtil::custom_orders_table_usage_is_enabled(); + $hpos_enabled = $this->is_hpos_enabled(); if ( $hpos_enabled ) { $orders_table = $wpdb->prefix . 'wc_orders'; diff --git a/src/Metrics/CustomMetricBuilder.php b/src/Metrics/CustomMetricBuilder.php index 6129ed7..9dbdb0a 100644 --- a/src/Metrics/CustomMetricBuilder.php +++ b/src/Metrics/CustomMetricBuilder.php @@ -126,6 +126,19 @@ class CustomMetricBuilder { return true; } + /** + * Sanitize a Prometheus metric name. + * + * Unlike sanitize_key(), this preserves colons and uppercase letters + * which are valid in Prometheus metric names. + * + * @param string $name Raw metric name. + * @return string Sanitized metric name. + */ + public static function sanitize_metric_name( string $name ): string { + return preg_replace( '/[^a-zA-Z0-9_:]/', '', $name ); + } + /** * Validate a Prometheus metric name. * @@ -277,7 +290,7 @@ class CustomMetricBuilder { private function sanitize_metric( array $metric ): array { $sanitized = array( 'id' => sanitize_key( $metric['id'] ?? '' ), - 'name' => sanitize_key( $metric['name'] ?? '' ), + 'name' => self::sanitize_metric_name( $metric['name'] ?? '' ), 'help' => sanitize_text_field( $metric['help'] ?? '' ), 'type' => sanitize_key( $metric['type'] ?? 'gauge' ), 'labels' => array(), diff --git a/wp-prometheus.php b/wp-prometheus.php index 20acf6c..ed0fad3 100644 --- a/wp-prometheus.php +++ b/wp-prometheus.php @@ -3,7 +3,7 @@ * Plugin Name: WP Prometheus * Plugin URI: https://src.bundespruefstelle.ch/magdev/wp-prometheus * Description: Prometheus metrics endpoint for WordPress with extensible hooks for custom metrics. - * Version: 0.5.0 + * Version: 0.5.1 * Requires at least: 6.4 * Requires PHP: 8.3 * Author: Marco Graetsch @@ -199,7 +199,7 @@ wp_prometheus_early_metrics_check(); * * @var string */ -define( 'WP_PROMETHEUS_VERSION', '0.5.0' ); +define( 'WP_PROMETHEUS_VERSION', '0.5.1' ); /** * Plugin file path.