2026-02-01 15:31:21 +01:00
|
|
|
<?php
|
|
|
|
|
/**
|
|
|
|
|
* 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.
|
2026-02-03 11:16:18 +01:00
|
|
|
* Version: 0.4.6
|
2026-02-01 15:31:21 +01:00
|
|
|
* Requires at least: 6.4
|
|
|
|
|
* Requires PHP: 8.3
|
|
|
|
|
* Author: Marco Graetsch
|
|
|
|
|
* Author URI: https://src.bundespruefstelle.ch/magdev
|
|
|
|
|
* License: GPL v2 or later
|
|
|
|
|
* License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
|
|
|
|
* Text Domain: wp-prometheus
|
|
|
|
|
* Domain Path: /languages
|
|
|
|
|
*
|
|
|
|
|
* @package WP_Prometheus
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
// Prevent direct file access.
|
|
|
|
|
if ( ! defined( 'ABSPATH' ) ) {
|
|
|
|
|
exit;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 20:23:29 +01:00
|
|
|
/**
|
2026-02-02 23:12:41 +01:00
|
|
|
* Early metrics request detection.
|
2026-02-02 20:23:29 +01:00
|
|
|
*
|
2026-02-02 23:12:41 +01:00
|
|
|
* Detects /metrics requests early and removes problematic content filters
|
|
|
|
|
* to prevent recursion issues with Twig-based plugins. Unlike the previous
|
|
|
|
|
* "early mode", this allows WordPress to continue loading so that third-party
|
|
|
|
|
* plugins can register their wp_prometheus_collect_metrics hooks.
|
|
|
|
|
*
|
|
|
|
|
* Two modes are available:
|
|
|
|
|
* - Safe mode (default): Removes filters early, lets WP load, fires custom hooks
|
|
|
|
|
* - Isolated mode: Outputs metrics immediately without custom hooks (legacy early mode)
|
2026-02-02 20:23:29 +01:00
|
|
|
*/
|
|
|
|
|
function wp_prometheus_early_metrics_check(): void {
|
|
|
|
|
// Only handle /metrics requests.
|
|
|
|
|
$request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
|
|
|
|
|
$path = wp_parse_url( $request_uri, PHP_URL_PATH );
|
|
|
|
|
|
|
|
|
|
if ( ! preg_match( '#/metrics/?$#', $path ) ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 23:12:41 +01:00
|
|
|
// Set flag to indicate we're handling a metrics request.
|
|
|
|
|
define( 'WP_PROMETHEUS_METRICS_REQUEST', true );
|
|
|
|
|
|
|
|
|
|
// Check if isolated mode is enabled via environment variable.
|
|
|
|
|
$env_isolated = getenv( 'WP_PROMETHEUS_ISOLATED_MODE' );
|
|
|
|
|
$isolated_mode = false !== $env_isolated && in_array( strtolower( $env_isolated ), array( '1', 'true', 'yes', 'on' ), true );
|
|
|
|
|
|
|
|
|
|
// Check if isolated mode is enabled via option (legacy "early mode" setting).
|
|
|
|
|
if ( ! $isolated_mode && ! get_option( 'wp_prometheus_disable_early_mode', false ) ) {
|
|
|
|
|
// Check for legacy isolated mode option.
|
|
|
|
|
$isolated_mode = (bool) get_option( 'wp_prometheus_isolated_mode', false );
|
2026-02-02 21:09:14 +01:00
|
|
|
}
|
|
|
|
|
|
2026-02-02 23:12:41 +01:00
|
|
|
// Remove all content filters immediately to prevent recursion with Twig-based plugins.
|
|
|
|
|
// This is done for BOTH safe mode and isolated mode.
|
|
|
|
|
add_action( 'plugins_loaded', 'wp_prometheus_remove_content_filters', 0 );
|
|
|
|
|
|
|
|
|
|
// Also remove filters now in case they were added by mu-plugins.
|
|
|
|
|
wp_prometheus_remove_content_filters();
|
|
|
|
|
|
|
|
|
|
// If isolated mode is enabled, handle metrics immediately without waiting for plugins.
|
|
|
|
|
if ( $isolated_mode ) {
|
|
|
|
|
wp_prometheus_isolated_metrics_handler();
|
2026-02-02 21:09:14 +01:00
|
|
|
}
|
2026-02-02 23:12:41 +01:00
|
|
|
}
|
2026-02-02 21:09:14 +01:00
|
|
|
|
2026-02-02 23:12:41 +01:00
|
|
|
/**
|
|
|
|
|
* Remove content filters that can cause recursion.
|
|
|
|
|
*
|
|
|
|
|
* Called early during metrics requests to prevent infinite loops
|
|
|
|
|
* with Twig-based plugins that hook into content filters.
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
function wp_prometheus_remove_content_filters(): void {
|
|
|
|
|
remove_all_filters( 'the_content' );
|
|
|
|
|
remove_all_filters( 'the_excerpt' );
|
|
|
|
|
remove_all_filters( 'get_the_excerpt' );
|
|
|
|
|
remove_all_filters( 'the_title' );
|
|
|
|
|
remove_all_filters( 'the_content_feed' );
|
|
|
|
|
remove_all_filters( 'comment_text' );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Handle metrics in isolated mode (no custom hooks).
|
|
|
|
|
*
|
|
|
|
|
* This is the legacy "early mode" that outputs metrics immediately
|
|
|
|
|
* without allowing third-party plugins to add custom metrics.
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
function wp_prometheus_isolated_metrics_handler(): void {
|
2026-02-02 20:23:29 +01:00
|
|
|
// Check if autoloader exists.
|
|
|
|
|
$autoloader = __DIR__ . '/vendor/autoload.php';
|
|
|
|
|
if ( ! file_exists( $autoloader ) ) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
require_once $autoloader;
|
|
|
|
|
|
|
|
|
|
// Check license validity.
|
|
|
|
|
if ( ! \Magdev\WpPrometheus\License\Manager::is_license_valid() ) {
|
|
|
|
|
return; // Let normal flow handle unlicensed state.
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Authenticate.
|
|
|
|
|
$auth_token = get_option( 'wp_prometheus_auth_token', '' );
|
|
|
|
|
if ( empty( $auth_token ) ) {
|
|
|
|
|
status_header( 401 );
|
|
|
|
|
header( 'WWW-Authenticate: Bearer realm="WP Prometheus Metrics"' );
|
|
|
|
|
header( 'Content-Type: text/plain; charset=utf-8' );
|
|
|
|
|
echo 'Unauthorized';
|
|
|
|
|
exit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check Bearer token.
|
|
|
|
|
$auth_header = '';
|
|
|
|
|
if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
|
|
|
|
|
$auth_header = sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
|
|
|
|
|
} elseif ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
|
|
|
|
|
$auth_header = sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$authenticated = false;
|
|
|
|
|
if ( ! empty( $auth_header ) && preg_match( '/Bearer\s+(.*)$/i', $auth_header, $matches ) ) {
|
|
|
|
|
$authenticated = hash_equals( $auth_token, $matches[1] );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check query parameter fallback.
|
|
|
|
|
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Auth token check.
|
|
|
|
|
if ( ! $authenticated && isset( $_GET['token'] ) ) {
|
|
|
|
|
$authenticated = hash_equals( $auth_token, sanitize_text_field( wp_unslash( $_GET['token'] ) ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ( ! $authenticated ) {
|
|
|
|
|
status_header( 401 );
|
|
|
|
|
header( 'WWW-Authenticate: Bearer realm="WP Prometheus Metrics"' );
|
|
|
|
|
header( 'Content-Type: text/plain; charset=utf-8' );
|
|
|
|
|
echo 'Unauthorized';
|
|
|
|
|
exit;
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-02 23:12:41 +01:00
|
|
|
// Set flag to indicate isolated mode - Collector will skip extensibility hooks.
|
|
|
|
|
define( 'WP_PROMETHEUS_ISOLATED_MODE', true );
|
2026-02-02 20:23:29 +01:00
|
|
|
|
|
|
|
|
// Output metrics and exit immediately.
|
|
|
|
|
$collector = new \Magdev\WpPrometheus\Metrics\Collector();
|
|
|
|
|
|
|
|
|
|
status_header( 200 );
|
|
|
|
|
header( 'Content-Type: text/plain; version=0.0.4; charset=utf-8' );
|
|
|
|
|
header( 'Cache-Control: no-cache, no-store, must-revalidate' );
|
|
|
|
|
header( 'Pragma: no-cache' );
|
|
|
|
|
header( 'Expires: 0' );
|
|
|
|
|
|
|
|
|
|
// phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- Prometheus format.
|
|
|
|
|
echo $collector->render();
|
|
|
|
|
exit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Try early metrics handling before full plugin initialization.
|
|
|
|
|
wp_prometheus_early_metrics_check();
|
|
|
|
|
|
2026-02-01 15:31:21 +01:00
|
|
|
/**
|
|
|
|
|
* Plugin version.
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
2026-02-03 11:16:18 +01:00
|
|
|
define( 'WP_PROMETHEUS_VERSION', '0.4.6' );
|
2026-02-01 15:31:21 +01:00
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Plugin file path.
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
define( 'WP_PROMETHEUS_FILE', __FILE__ );
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Plugin directory path.
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
define( 'WP_PROMETHEUS_PATH', plugin_dir_path( __FILE__ ) );
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Plugin directory URL.
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
define( 'WP_PROMETHEUS_URL', plugin_dir_url( __FILE__ ) );
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Plugin basename.
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
define( 'WP_PROMETHEUS_BASENAME', plugin_basename( __FILE__ ) );
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Minimum WordPress version required.
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
define( 'WP_PROMETHEUS_MIN_WP_VERSION', '6.4' );
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Minimum PHP version required.
|
|
|
|
|
*
|
|
|
|
|
* @var string
|
|
|
|
|
*/
|
|
|
|
|
define( 'WP_PROMETHEUS_MIN_PHP_VERSION', '8.3' );
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Check requirements and bootstrap the plugin.
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
function wp_prometheus_init(): void {
|
|
|
|
|
// Check PHP version.
|
|
|
|
|
if ( version_compare( PHP_VERSION, WP_PROMETHEUS_MIN_PHP_VERSION, '<' ) ) {
|
|
|
|
|
add_action( 'admin_notices', 'wp_prometheus_php_version_notice' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check WordPress version.
|
|
|
|
|
if ( version_compare( get_bloginfo( 'version' ), WP_PROMETHEUS_MIN_WP_VERSION, '<' ) ) {
|
|
|
|
|
add_action( 'admin_notices', 'wp_prometheus_wp_version_notice' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if Composer autoloader exists.
|
|
|
|
|
$autoloader = WP_PROMETHEUS_PATH . 'vendor/autoload.php';
|
|
|
|
|
if ( ! file_exists( $autoloader ) ) {
|
|
|
|
|
add_action( 'admin_notices', 'wp_prometheus_autoloader_notice' );
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
require_once $autoloader;
|
|
|
|
|
|
|
|
|
|
// Initialize the plugin.
|
|
|
|
|
\Magdev\WpPrometheus\Plugin::get_instance();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Display PHP version notice.
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
function wp_prometheus_php_version_notice(): void {
|
|
|
|
|
$message = sprintf(
|
|
|
|
|
/* translators: 1: Required PHP version, 2: Current PHP version */
|
|
|
|
|
__( 'WP Prometheus requires PHP version %1$s or higher. You are running PHP %2$s.', 'wp-prometheus' ),
|
|
|
|
|
WP_PROMETHEUS_MIN_PHP_VERSION,
|
|
|
|
|
PHP_VERSION
|
|
|
|
|
);
|
|
|
|
|
printf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $message ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Display WordPress version notice.
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
function wp_prometheus_wp_version_notice(): void {
|
|
|
|
|
$message = sprintf(
|
|
|
|
|
/* translators: 1: Required WordPress version, 2: Current WordPress version */
|
|
|
|
|
__( 'WP Prometheus requires WordPress version %1$s or higher. You are running WordPress %2$s.', 'wp-prometheus' ),
|
|
|
|
|
WP_PROMETHEUS_MIN_WP_VERSION,
|
|
|
|
|
get_bloginfo( 'version' )
|
|
|
|
|
);
|
|
|
|
|
printf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $message ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Display autoloader notice.
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
function wp_prometheus_autoloader_notice(): void {
|
|
|
|
|
$message = __( 'WP Prometheus requires Composer dependencies to be installed. Please run "composer install" in the plugin directory.', 'wp-prometheus' );
|
|
|
|
|
printf( '<div class="notice notice-error"><p>%s</p></div>', esc_html( $message ) );
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Plugin activation hook.
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
function wp_prometheus_activate(): void {
|
|
|
|
|
// Check requirements before activation.
|
|
|
|
|
if ( version_compare( PHP_VERSION, WP_PROMETHEUS_MIN_PHP_VERSION, '<' ) ) {
|
|
|
|
|
deactivate_plugins( WP_PROMETHEUS_BASENAME );
|
|
|
|
|
wp_die(
|
|
|
|
|
sprintf(
|
|
|
|
|
/* translators: %s: Required PHP version */
|
|
|
|
|
esc_html__( 'WP Prometheus requires PHP version %s or higher.', 'wp-prometheus' ),
|
|
|
|
|
WP_PROMETHEUS_MIN_PHP_VERSION
|
|
|
|
|
)
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$autoloader = WP_PROMETHEUS_PATH . 'vendor/autoload.php';
|
|
|
|
|
if ( file_exists( $autoloader ) ) {
|
|
|
|
|
require_once $autoloader;
|
|
|
|
|
\Magdev\WpPrometheus\Installer::activate();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Plugin deactivation hook.
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
function wp_prometheus_deactivate(): void {
|
|
|
|
|
$autoloader = WP_PROMETHEUS_PATH . 'vendor/autoload.php';
|
|
|
|
|
if ( file_exists( $autoloader ) ) {
|
|
|
|
|
require_once $autoloader;
|
|
|
|
|
\Magdev\WpPrometheus\Installer::deactivate();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Plugin uninstall hook.
|
|
|
|
|
*
|
|
|
|
|
* @return void
|
|
|
|
|
*/
|
|
|
|
|
function wp_prometheus_uninstall(): void {
|
|
|
|
|
$autoloader = WP_PROMETHEUS_PATH . 'vendor/autoload.php';
|
|
|
|
|
if ( file_exists( $autoloader ) ) {
|
|
|
|
|
require_once $autoloader;
|
|
|
|
|
\Magdev\WpPrometheus\Installer::uninstall();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Register activation/deactivation hooks.
|
|
|
|
|
register_activation_hook( __FILE__, 'wp_prometheus_activate' );
|
|
|
|
|
register_deactivation_hook( __FILE__, 'wp_prometheus_deactivate' );
|
|
|
|
|
|
|
|
|
|
// Initialize plugin after all plugins are loaded.
|
|
|
|
|
add_action( 'plugins_loaded', 'wp_prometheus_init' );
|