Initial plugin setup (v0.0.1)
Some checks failed
Create Release Package / build-release (push) Failing after 48s

- Create initial WordPress plugin structure
- Add Prometheus metrics collector with default metrics
- Implement authenticated /metrics endpoint with Bearer token
- Add license management integration
- Create admin settings page under Settings > Metrics
- Set up Gitea CI/CD pipeline for automated releases
- Add extensibility via wp_prometheus_collect_metrics hook

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 15:31:21 +01:00
commit 7ff87f7c8d
21 changed files with 2890 additions and 0 deletions

406
src/Admin/Settings.php Normal file
View File

@@ -0,0 +1,406 @@
<?php
/**
* Admin settings class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus\Admin;
use Magdev\WpPrometheus\License\Manager as LicenseManager;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Settings class.
*
* Handles plugin settings page in the WordPress admin.
*/
class Settings {
/**
* Constructor.
*/
public function __construct() {
add_action( 'admin_menu', array( $this, 'add_settings_page' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
add_action( 'admin_enqueue_scripts', array( $this, 'enqueue_scripts' ) );
}
/**
* Add settings page to admin menu.
*
* @return void
*/
public function add_settings_page(): void {
add_options_page(
__( 'Metrics Settings', 'wp-prometheus' ),
__( 'Metrics', 'wp-prometheus' ),
'manage_options',
'wp-prometheus',
array( $this, 'render_settings_page' )
);
}
/**
* Register plugin settings.
*
* @return void
*/
public function register_settings(): void {
// License settings section.
add_settings_section(
'wp_prometheus_license_section',
__( 'License Settings', 'wp-prometheus' ),
array( $this, 'render_license_section' ),
'wp-prometheus'
);
// Auth token section.
add_settings_section(
'wp_prometheus_auth_section',
__( 'Authentication', 'wp-prometheus' ),
array( $this, 'render_auth_section' ),
'wp-prometheus'
);
// Metrics section.
add_settings_section(
'wp_prometheus_metrics_section',
__( 'Default Metrics', 'wp-prometheus' ),
array( $this, 'render_metrics_section' ),
'wp-prometheus'
);
// Register settings.
register_setting( 'wp_prometheus_settings', 'wp_prometheus_auth_token', array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
) );
register_setting( 'wp_prometheus_settings', 'wp_prometheus_enabled_metrics', array(
'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize_metrics' ),
) );
// Auth token field.
add_settings_field(
'wp_prometheus_auth_token',
__( 'Auth Token', 'wp-prometheus' ),
array( $this, 'render_auth_token_field' ),
'wp-prometheus',
'wp_prometheus_auth_section'
);
// Enabled metrics field.
add_settings_field(
'wp_prometheus_enabled_metrics',
__( 'Enabled Metrics', 'wp-prometheus' ),
array( $this, 'render_enabled_metrics_field' ),
'wp-prometheus',
'wp_prometheus_metrics_section'
);
}
/**
* Enqueue admin scripts.
*
* @param string $hook_suffix Current admin page.
* @return void
*/
public function enqueue_scripts( string $hook_suffix ): void {
if ( 'settings_page_wp-prometheus' !== $hook_suffix ) {
return;
}
wp_enqueue_script(
'wp-prometheus-admin',
WP_PROMETHEUS_URL . 'assets/js/admin.js',
array( 'jquery' ),
WP_PROMETHEUS_VERSION,
true
);
wp_localize_script( 'wp-prometheus-admin', 'wpPrometheus', array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'wp_prometheus_license_action' ),
) );
}
/**
* Render the settings page.
*
* @return void
*/
public function render_settings_page(): void {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
// Handle license settings save.
if ( isset( $_POST['wp_prometheus_license_nonce'] ) && wp_verify_nonce( sanitize_key( $_POST['wp_prometheus_license_nonce'] ), 'wp_prometheus_save_license' ) ) {
LicenseManager::save_settings( array(
'license_key' => isset( $_POST['license_key'] ) ? sanitize_text_field( wp_unslash( $_POST['license_key'] ) ) : '',
'server_url' => isset( $_POST['license_server_url'] ) ? esc_url_raw( wp_unslash( $_POST['license_server_url'] ) ) : '',
'server_secret' => isset( $_POST['license_server_secret'] ) ? sanitize_text_field( wp_unslash( $_POST['license_server_secret'] ) ) : '',
) );
echo '<div class="notice notice-success is-dismissible"><p>' . esc_html__( 'License settings saved.', 'wp-prometheus' ) . '</p></div>';
}
?>
<div class="wrap">
<h1><?php echo esc_html( get_admin_page_title() ); ?></h1>
<?php $this->render_license_form(); ?>
<form method="post" action="options.php">
<?php
settings_fields( 'wp_prometheus_settings' );
do_settings_sections( 'wp-prometheus' );
submit_button();
?>
</form>
<?php $this->render_endpoint_info(); ?>
</div>
<?php
}
/**
* Render license settings form.
*
* @return void
*/
private function render_license_form(): void {
$license_key = LicenseManager::get_license_key();
$server_url = LicenseManager::get_server_url();
$license_status = LicenseManager::get_cached_status();
$license_data = LicenseManager::get_cached_data();
$last_check = LicenseManager::get_last_check();
$status_classes = array(
'valid' => 'notice-success',
'invalid' => 'notice-error',
'expired' => 'notice-warning',
'revoked' => 'notice-error',
'inactive' => 'notice-warning',
'unchecked' => 'notice-info',
'unconfigured' => 'notice-info',
);
$status_messages = array(
'valid' => __( 'License is active and valid.', 'wp-prometheus' ),
'invalid' => __( 'License is invalid.', 'wp-prometheus' ),
'expired' => __( 'License has expired.', 'wp-prometheus' ),
'revoked' => __( 'License has been revoked.', 'wp-prometheus' ),
'inactive' => __( 'License is inactive.', 'wp-prometheus' ),
'unchecked' => __( 'License has not been validated yet.', 'wp-prometheus' ),
'unconfigured' => __( 'License server is not configured.', 'wp-prometheus' ),
);
$status_class = $status_classes[ $license_status ] ?? 'notice-info';
$status_message = $status_messages[ $license_status ] ?? __( 'Unknown status.', 'wp-prometheus' );
?>
<div class="wp-prometheus-license-status notice <?php echo esc_attr( $status_class ); ?>" style="padding: 12px;">
<strong><?php echo esc_html( $status_message ); ?></strong>
<?php if ( 'valid' === $license_status && ! empty( $license_data['expires_at'] ) ) : ?>
<br><span class="description">
<?php
printf(
/* translators: %s: Expiration date */
esc_html__( 'Expires: %s', 'wp-prometheus' ),
esc_html( $license_data['expires_at'] )
);
?>
</span>
<?php endif; ?>
<?php if ( $last_check > 0 ) : ?>
<br><span class="description">
<?php
printf(
/* translators: %s: Time ago */
esc_html__( 'Last checked: %s ago', 'wp-prometheus' ),
esc_html( human_time_diff( $last_check, time() ) )
);
?>
</span>
<?php endif; ?>
</div>
<form method="post" action="" id="wp-prometheus-license-form">
<?php wp_nonce_field( 'wp_prometheus_save_license', 'wp_prometheus_license_nonce' ); ?>
<table class="form-table">
<tr>
<th scope="row">
<label for="license_server_url"><?php esc_html_e( 'License Server URL', 'wp-prometheus' ); ?></label>
</th>
<td>
<input type="url" name="license_server_url" id="license_server_url"
value="<?php echo esc_attr( $server_url ); ?>"
class="regular-text" placeholder="https://example.com">
</td>
</tr>
<tr>
<th scope="row">
<label for="license_key"><?php esc_html_e( 'License Key', 'wp-prometheus' ); ?></label>
</th>
<td>
<input type="text" name="license_key" id="license_key"
value="<?php echo esc_attr( $license_key ); ?>"
class="regular-text" placeholder="XXXX-XXXX-XXXX-XXXX">
</td>
</tr>
<tr>
<th scope="row">
<label for="license_server_secret"><?php esc_html_e( 'Server Secret', 'wp-prometheus' ); ?></label>
</th>
<td>
<input type="password" name="license_server_secret" id="license_server_secret"
value="" class="regular-text" placeholder="<?php echo esc_attr( ! empty( LicenseManager::get_server_secret() ) ? '••••••••••••••••' : '' ); ?>">
<p class="description"><?php esc_html_e( 'Leave empty to keep existing.', 'wp-prometheus' ); ?></p>
</td>
</tr>
</table>
<p class="submit">
<?php submit_button( __( 'Save License Settings', 'wp-prometheus' ), 'primary', 'submit', false ); ?>
<button type="button" id="wp-prometheus-validate-license" class="button button-secondary" style="margin-left: 10px;">
<?php esc_html_e( 'Validate License', 'wp-prometheus' ); ?>
</button>
<button type="button" id="wp-prometheus-activate-license" class="button button-secondary" style="margin-left: 10px;">
<?php esc_html_e( 'Activate License', 'wp-prometheus' ); ?>
</button>
<span id="wp-prometheus-license-spinner" class="spinner" style="float: none;"></span>
</p>
</form>
<div id="wp-prometheus-license-message" style="display: none; margin-top: 10px;"></div>
<hr>
<?php
}
/**
* Render license section description.
*
* @return void
*/
public function render_license_section(): void {
// License section rendered separately in render_license_form().
}
/**
* Render auth section description.
*
* @return void
*/
public function render_auth_section(): void {
echo '<p>' . esc_html__( 'Configure authentication for the /metrics endpoint.', 'wp-prometheus' ) . '</p>';
}
/**
* Render metrics section description.
*
* @return void
*/
public function render_metrics_section(): void {
echo '<p>' . esc_html__( 'Select which default metrics to expose.', 'wp-prometheus' ) . '</p>';
}
/**
* Render auth token field.
*
* @return void
*/
public function render_auth_token_field(): void {
$token = get_option( 'wp_prometheus_auth_token', '' );
?>
<input type="text" name="wp_prometheus_auth_token" id="wp_prometheus_auth_token"
value="<?php echo esc_attr( $token ); ?>" class="regular-text" readonly>
<button type="button" id="wp-prometheus-regenerate-token" class="button button-secondary">
<?php esc_html_e( 'Regenerate', 'wp-prometheus' ); ?>
</button>
<p class="description">
<?php esc_html_e( 'Use this token to authenticate Prometheus scrape requests.', 'wp-prometheus' ); ?>
</p>
<?php
}
/**
* Render enabled metrics field.
*
* @return void
*/
public function render_enabled_metrics_field(): void {
$enabled = get_option( 'wp_prometheus_enabled_metrics', array() );
$metrics = array(
'wordpress_info' => __( 'WordPress Info (version, PHP version, multisite)', 'wp-prometheus' ),
'wordpress_users_total' => __( 'Total Users by Role', 'wp-prometheus' ),
'wordpress_posts_total' => __( 'Total Posts by Type and Status', 'wp-prometheus' ),
'wordpress_comments_total' => __( 'Total Comments by Status', 'wp-prometheus' ),
'wordpress_plugins_total' => __( 'Total Plugins (active/inactive)', 'wp-prometheus' ),
);
foreach ( $metrics as $key => $label ) {
?>
<label style="display: block; margin-bottom: 5px;">
<input type="checkbox" name="wp_prometheus_enabled_metrics[]"
value="<?php echo esc_attr( $key ); ?>"
<?php checked( in_array( $key, $enabled, true ) ); ?>>
<?php echo esc_html( $label ); ?>
</label>
<?php
}
}
/**
* Render endpoint info.
*
* @return void
*/
private function render_endpoint_info(): void {
$token = get_option( 'wp_prometheus_auth_token', '' );
$endpoint_url = home_url( '/metrics/' );
?>
<hr>
<h2><?php esc_html_e( 'Prometheus Configuration', 'wp-prometheus' ); ?></h2>
<p><?php esc_html_e( 'Add the following to your prometheus.yml:', 'wp-prometheus' ); ?></p>
<pre style="background: #f1f1f1; padding: 15px; overflow-x: auto;">
scrape_configs:
- job_name: 'wordpress'
static_configs:
- targets: ['<?php echo esc_html( wp_parse_url( home_url(), PHP_URL_HOST ) ); ?>']
metrics_path: '/metrics/'
scheme: '<?php echo esc_html( wp_parse_url( home_url(), PHP_URL_SCHEME ) ); ?>'
authorization:
type: Bearer
credentials: '<?php echo esc_html( $token ); ?>'</pre>
<p>
<?php
printf(
/* translators: %s: Endpoint URL */
esc_html__( 'Metrics endpoint: %s', 'wp-prometheus' ),
'<code>' . esc_url( $endpoint_url ) . '</code>'
);
?>
</p>
<?php
}
/**
* Sanitize enabled metrics.
*
* @param mixed $input Input value.
* @return array
*/
public function sanitize_metrics( $input ): array {
if ( ! is_array( $input ) ) {
return array();
}
return array_map( 'sanitize_text_field', $input );
}
}

View File

@@ -0,0 +1,148 @@
<?php
/**
* Metrics endpoint class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus\Endpoint;
use Magdev\WpPrometheus\Metrics\Collector;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* MetricsEndpoint class.
*
* Provides the /metrics endpoint for Prometheus scraping.
*/
class MetricsEndpoint {
/**
* Metrics collector instance.
*
* @var Collector
*/
private Collector $collector;
/**
* Constructor.
*
* @param Collector $collector Metrics collector instance.
*/
public function __construct( Collector $collector ) {
$this->collector = $collector;
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @return void
*/
private function init_hooks(): void {
add_action( 'init', array( $this, 'register_endpoint' ) );
add_action( 'template_redirect', array( $this, 'handle_request' ) );
}
/**
* Register the metrics endpoint rewrite rule.
*
* @return void
*/
public function register_endpoint(): void {
add_rewrite_rule(
'^metrics/?$',
'index.php?wp_prometheus_metrics=1',
'top'
);
add_rewrite_tag( '%wp_prometheus_metrics%', '([^&]+)' );
}
/**
* Handle the metrics endpoint request.
*
* @return void
*/
public function handle_request(): void {
if ( ! get_query_var( 'wp_prometheus_metrics' ) ) {
return;
}
// Authenticate the request.
if ( ! $this->authenticate() ) {
status_header( 401 );
header( 'WWW-Authenticate: Bearer realm="WP Prometheus Metrics"' );
header( 'Content-Type: text/plain; charset=utf-8' );
echo 'Unauthorized';
exit;
}
// Output metrics.
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 $this->collector->render();
exit;
}
/**
* Authenticate the metrics request.
*
* @return bool
*/
private function authenticate(): bool {
$auth_token = get_option( 'wp_prometheus_auth_token', '' );
// If no token is set, deny access.
if ( empty( $auth_token ) ) {
return false;
}
// Check for Bearer token in Authorization header.
$auth_header = $this->get_authorization_header();
if ( ! empty( $auth_header ) && preg_match( '/Bearer\s+(.*)$/i', $auth_header, $matches ) ) {
return hash_equals( $auth_token, $matches[1] );
}
// Check for token in query parameter (less secure but useful for testing).
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Auth token check.
if ( isset( $_GET['token'] ) && hash_equals( $auth_token, sanitize_text_field( wp_unslash( $_GET['token'] ) ) ) ) {
return true;
}
return false;
}
/**
* Get the Authorization header from the request.
*
* @return string
*/
private function get_authorization_header(): string {
if ( isset( $_SERVER['HTTP_AUTHORIZATION'] ) ) {
return sanitize_text_field( wp_unslash( $_SERVER['HTTP_AUTHORIZATION'] ) );
}
if ( isset( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) ) {
return sanitize_text_field( wp_unslash( $_SERVER['REDIRECT_HTTP_AUTHORIZATION'] ) );
}
if ( function_exists( 'apache_request_headers' ) ) {
$headers = apache_request_headers();
if ( isset( $headers['Authorization'] ) ) {
return sanitize_text_field( $headers['Authorization'] );
}
}
return '';
}
}

103
src/Installer.php Normal file
View File

@@ -0,0 +1,103 @@
<?php
/**
* Plugin installer class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Installer class.
*
* Handles plugin activation, deactivation, and uninstallation.
*/
final class Installer {
/**
* Plugin activation.
*
* @return void
*/
public static function activate(): void {
// Set default options.
self::set_default_options();
// Flush rewrite rules for the metrics endpoint.
flush_rewrite_rules();
// Store activation time for reference.
update_option( 'wp_prometheus_activated', time() );
}
/**
* Plugin deactivation.
*
* @return void
*/
public static function deactivate(): void {
// Flush rewrite rules.
flush_rewrite_rules();
}
/**
* Plugin uninstallation.
*
* @return void
*/
public static function uninstall(): void {
// Remove all plugin options.
$options = array(
'wp_prometheus_activated',
'wp_prometheus_license_key',
'wp_prometheus_license_server_url',
'wp_prometheus_license_server_secret',
'wp_prometheus_license_status',
'wp_prometheus_license_data',
'wp_prometheus_license_last_check',
'wp_prometheus_auth_token',
'wp_prometheus_enable_default_metrics',
'wp_prometheus_enabled_metrics',
);
foreach ( $options as $option ) {
delete_option( $option );
}
// Remove transients.
delete_transient( 'wp_prometheus_license_check' );
}
/**
* Set default plugin options.
*
* @return void
*/
private static function set_default_options(): void {
// Generate a random auth token if not set.
if ( ! get_option( 'wp_prometheus_auth_token' ) ) {
update_option( 'wp_prometheus_auth_token', wp_generate_password( 32, false ) );
}
// Enable default metrics by default.
if ( false === get_option( 'wp_prometheus_enable_default_metrics' ) ) {
update_option( 'wp_prometheus_enable_default_metrics', 1 );
}
// Default enabled metrics.
if ( false === get_option( 'wp_prometheus_enabled_metrics' ) ) {
update_option( 'wp_prometheus_enabled_metrics', array(
'wordpress_info',
'wordpress_users_total',
'wordpress_posts_total',
'wordpress_comments_total',
'wordpress_plugins_total',
) );
}
}
}

496
src/License/Manager.php Normal file
View File

@@ -0,0 +1,496 @@
<?php
/**
* License management class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus\License;
use Magdev\WcLicensedProductClient\SecureLicenseClient;
use Magdev\WcLicensedProductClient\Dto\LicenseState;
use Magdev\WcLicensedProductClient\Exception\LicenseException;
use Magdev\WcLicensedProductClient\Exception\LicenseNotFoundException;
use Magdev\WcLicensedProductClient\Exception\LicenseExpiredException;
use Magdev\WcLicensedProductClient\Exception\LicenseRevokedException;
use Magdev\WcLicensedProductClient\Exception\LicenseInactiveException;
use Magdev\WcLicensedProductClient\Exception\DomainMismatchException;
use Magdev\WcLicensedProductClient\Exception\MaxActivationsReachedException;
use Magdev\WcLicensedProductClient\Exception\RateLimitExceededException;
use Magdev\WcLicensedProductClient\Security\SignatureException;
use Symfony\Component\HttpClient\HttpClient;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* License Manager class.
*
* Handles license validation, activation, and status checking.
*/
final class Manager {
/**
* Option names for license settings.
*/
public const OPTION_LICENSE_KEY = 'wp_prometheus_license_key';
public const OPTION_SERVER_URL = 'wp_prometheus_license_server_url';
public const OPTION_SERVER_SECRET = 'wp_prometheus_license_server_secret';
public const OPTION_LICENSE_STATUS = 'wp_prometheus_license_status';
public const OPTION_LICENSE_DATA = 'wp_prometheus_license_data';
public const OPTION_LAST_CHECK = 'wp_prometheus_license_last_check';
/**
* Transient name for caching license validation.
*/
private const TRANSIENT_LICENSE_CHECK = 'wp_prometheus_license_check';
/**
* Cache TTL in seconds (24 hours).
*/
private const CACHE_TTL = 86400;
/**
* Singleton instance.
*
* @var Manager|null
*/
private static ?Manager $instance = null;
/**
* License client instance.
*
* @var SecureLicenseClient|null
*/
private ?SecureLicenseClient $client = null;
/**
* Get singleton instance.
*
* @return Manager
*/
public static function get_instance(): Manager {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Private constructor.
*/
private function __construct() {
$this->init_hooks();
}
/**
* Initialize WordPress hooks.
*
* @return void
*/
private function init_hooks(): void {
add_action( 'wp_ajax_wp_prometheus_validate_license', array( $this, 'ajax_validate_license' ) );
add_action( 'wp_ajax_wp_prometheus_activate_license', array( $this, 'ajax_activate_license' ) );
add_action( 'wp_ajax_wp_prometheus_deactivate_license', array( $this, 'ajax_deactivate_license' ) );
add_action( 'wp_ajax_wp_prometheus_check_license_status', array( $this, 'ajax_check_status' ) );
}
/**
* Initialize the license client.
*
* @return bool True if client was initialized successfully.
*/
private function init_client(): bool {
if ( null !== $this->client ) {
return true;
}
$server_url = self::get_server_url();
$server_secret = self::get_server_secret();
if ( empty( $server_url ) || empty( $server_secret ) ) {
return false;
}
try {
$this->client = new SecureLicenseClient(
httpClient: HttpClient::create(),
baseUrl: $server_url,
serverSecret: $server_secret,
);
return true;
} catch ( \Throwable $e ) {
return false;
}
}
/**
* Validate the current license.
*
* @return array{success: bool, message: string, data?: array}
*/
public function validate(): array {
if ( ! $this->init_client() ) {
return array(
'success' => false,
'message' => __( 'License server configuration is incomplete.', 'wp-prometheus' ),
);
}
$license_key = self::get_license_key();
if ( empty( $license_key ) ) {
return array(
'success' => false,
'message' => __( 'No license key provided.', 'wp-prometheus' ),
);
}
$domain = wp_parse_url( home_url(), PHP_URL_HOST );
try {
$result = $this->client->validate( $license_key, $domain );
$this->update_cached_status( 'valid', array(
'product_id' => $result->productId,
'expires_at' => $result->expiresAt?->format( 'c' ),
'version_id' => $result->versionId,
) );
return array(
'success' => true,
'message' => __( 'License validated successfully.', 'wp-prometheus' ),
'data' => array(
'status' => 'valid',
'product_id' => $result->productId,
'expires_at' => $result->expiresAt?->format( 'Y-m-d' ),
'lifetime' => $result->isLifetime(),
),
);
} catch ( LicenseNotFoundException $e ) {
$this->update_cached_status( 'invalid' );
return array(
'success' => false,
'message' => __( 'License key not found.', 'wp-prometheus' ),
);
} catch ( LicenseExpiredException $e ) {
$this->update_cached_status( 'expired' );
return array(
'success' => false,
'message' => __( 'Your license has expired.', 'wp-prometheus' ),
);
} catch ( LicenseRevokedException $e ) {
$this->update_cached_status( 'revoked' );
return array(
'success' => false,
'message' => __( 'Your license has been revoked.', 'wp-prometheus' ),
);
} catch ( LicenseInactiveException $e ) {
$this->update_cached_status( 'inactive' );
return array(
'success' => false,
'message' => __( 'License is inactive. Please activate it first.', 'wp-prometheus' ),
);
} catch ( DomainMismatchException $e ) {
$this->update_cached_status( 'invalid' );
return array(
'success' => false,
'message' => __( 'This license is not activated for this domain.', 'wp-prometheus' ),
);
} catch ( SignatureException $e ) {
return array(
'success' => false,
'message' => __( 'License verification failed. Please check your server secret.', 'wp-prometheus' ),
);
} catch ( RateLimitExceededException $e ) {
return array(
'success' => false,
'message' => __( 'Too many requests. Please try again later.', 'wp-prometheus' ),
);
} catch ( LicenseException $e ) {
return array(
'success' => false,
'message' => sprintf(
/* translators: %s: Error message */
__( 'License validation failed: %s', 'wp-prometheus' ),
$e->getMessage()
),
);
} catch ( \Throwable $e ) {
return array(
'success' => false,
'message' => __( 'Unable to verify license. Please try again later.', 'wp-prometheus' ),
);
}
}
/**
* Activate the license for this domain.
*
* @return array{success: bool, message: string, data?: array}
*/
public function activate(): array {
if ( ! $this->init_client() ) {
return array(
'success' => false,
'message' => __( 'License server configuration is incomplete.', 'wp-prometheus' ),
);
}
$license_key = self::get_license_key();
if ( empty( $license_key ) ) {
return array(
'success' => false,
'message' => __( 'No license key provided.', 'wp-prometheus' ),
);
}
$domain = wp_parse_url( home_url(), PHP_URL_HOST );
try {
$result = $this->client->activate( $license_key, $domain );
if ( $result->success ) {
return $this->validate();
}
return array(
'success' => false,
'message' => $result->message,
);
} catch ( MaxActivationsReachedException $e ) {
return array(
'success' => false,
'message' => __( 'Maximum activations reached. Please deactivate another site.', 'wp-prometheus' ),
);
} catch ( LicenseNotFoundException $e ) {
return array(
'success' => false,
'message' => __( 'License key not found.', 'wp-prometheus' ),
);
} catch ( LicenseExpiredException $e ) {
return array(
'success' => false,
'message' => __( 'Your license has expired.', 'wp-prometheus' ),
);
} catch ( SignatureException $e ) {
return array(
'success' => false,
'message' => __( 'License verification failed. Please check your server secret.', 'wp-prometheus' ),
);
} catch ( LicenseException $e ) {
return array(
'success' => false,
'message' => sprintf(
/* translators: %s: Error message */
__( 'License activation failed: %s', 'wp-prometheus' ),
$e->getMessage()
),
);
} catch ( \Throwable $e ) {
return array(
'success' => false,
'message' => __( 'Unable to activate license. Please try again later.', 'wp-prometheus' ),
);
}
}
/**
* Check if the license is currently valid.
*
* @return bool
*/
public static function is_license_valid(): bool {
$status = get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
return 'valid' === $status;
}
/**
* Get the license key.
*
* @return string
*/
public static function get_license_key(): string {
return get_option( self::OPTION_LICENSE_KEY, '' );
}
/**
* Get the license server URL.
*
* @return string
*/
public static function get_server_url(): string {
return get_option( self::OPTION_SERVER_URL, '' );
}
/**
* Get the server secret.
*
* @return string
*/
public static function get_server_secret(): string {
return get_option( self::OPTION_SERVER_SECRET, '' );
}
/**
* Get cached license status.
*
* @return string
*/
public static function get_cached_status(): string {
return get_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
}
/**
* Get cached license data.
*
* @return array
*/
public static function get_cached_data(): array {
return get_option( self::OPTION_LICENSE_DATA, array() );
}
/**
* Get last check timestamp.
*
* @return int
*/
public static function get_last_check(): int {
return (int) get_option( self::OPTION_LAST_CHECK, 0 );
}
/**
* Save license settings.
*
* @param array $data Settings data.
* @return bool
*/
public static function save_settings( array $data ): bool {
if ( isset( $data['license_key'] ) ) {
update_option( self::OPTION_LICENSE_KEY, sanitize_text_field( $data['license_key'] ) );
}
if ( isset( $data['server_url'] ) ) {
update_option( self::OPTION_SERVER_URL, esc_url_raw( $data['server_url'] ) );
}
if ( isset( $data['server_secret'] ) ) {
$secret = sanitize_text_field( $data['server_secret'] );
if ( ! empty( $secret ) ) {
update_option( self::OPTION_SERVER_SECRET, $secret );
}
}
update_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
delete_transient( self::TRANSIENT_LICENSE_CHECK );
return true;
}
/**
* Update cached license status.
*
* @param string $status Status value.
* @param array $data Additional data.
* @return void
*/
private function update_cached_status( string $status, array $data = array() ): void {
update_option( self::OPTION_LICENSE_STATUS, $status );
update_option( self::OPTION_LICENSE_DATA, $data );
update_option( self::OPTION_LAST_CHECK, time() );
}
/**
* AJAX handler: Validate license.
*
* @return void
*/
public function ajax_validate_license(): void {
check_ajax_referer( 'wp_prometheus_license_action', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-prometheus' ),
) );
}
$result = $this->validate();
if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}
/**
* AJAX handler: Activate license.
*
* @return void
*/
public function ajax_activate_license(): void {
check_ajax_referer( 'wp_prometheus_license_action', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-prometheus' ),
) );
}
$result = $this->activate();
if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}
/**
* AJAX handler: Deactivate license.
*
* @return void
*/
public function ajax_deactivate_license(): void {
check_ajax_referer( 'wp_prometheus_license_action', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-prometheus' ),
) );
}
update_option( self::OPTION_LICENSE_STATUS, 'unchecked' );
update_option( self::OPTION_LICENSE_DATA, array() );
delete_transient( self::TRANSIENT_LICENSE_CHECK );
wp_send_json_success( array(
'success' => true,
'message' => __( 'License deactivated.', 'wp-prometheus' ),
) );
}
/**
* AJAX handler: Check license status.
*
* @return void
*/
public function ajax_check_status(): void {
check_ajax_referer( 'wp_prometheus_license_action', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array(
'message' => __( 'You do not have permission to perform this action.', 'wp-prometheus' ),
) );
}
$result = $this->validate();
if ( $result['success'] ) {
wp_send_json_success( $result );
} else {
wp_send_json_error( $result );
}
}
}

288
src/Metrics/Collector.php Normal file
View File

@@ -0,0 +1,288 @@
<?php
/**
* Metrics collector class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus\Metrics;
use Prometheus\CollectorRegistry;
use Prometheus\Storage\InMemory;
use Prometheus\RenderTextFormat;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Collector class.
*
* Collects and manages Prometheus metrics.
*/
class Collector {
/**
* Prometheus collector registry.
*
* @var CollectorRegistry
*/
private CollectorRegistry $registry;
/**
* Metric namespace.
*
* @var string
*/
private string $namespace = 'wordpress';
/**
* Constructor.
*/
public function __construct() {
$this->registry = new CollectorRegistry( new InMemory() );
}
/**
* Get the collector registry.
*
* @return CollectorRegistry
*/
public function get_registry(): CollectorRegistry {
return $this->registry;
}
/**
* Get the metric namespace.
*
* @return string
*/
public function get_namespace(): string {
return $this->namespace;
}
/**
* Collect all enabled metrics.
*
* @return void
*/
public function collect(): void {
$enabled_metrics = get_option( 'wp_prometheus_enabled_metrics', array() );
// Always collect WordPress info.
if ( in_array( 'wordpress_info', $enabled_metrics, true ) ) {
$this->collect_wordpress_info();
}
// Collect user metrics.
if ( in_array( 'wordpress_users_total', $enabled_metrics, true ) ) {
$this->collect_users_total();
}
// Collect posts metrics.
if ( in_array( 'wordpress_posts_total', $enabled_metrics, true ) ) {
$this->collect_posts_total();
}
// Collect comments metrics.
if ( in_array( 'wordpress_comments_total', $enabled_metrics, true ) ) {
$this->collect_comments_total();
}
// Collect plugins metrics.
if ( in_array( 'wordpress_plugins_total', $enabled_metrics, true ) ) {
$this->collect_plugins_total();
}
/**
* Fires after default metrics are collected.
*
* @param Collector $collector The metrics collector instance.
*/
do_action( 'wp_prometheus_collect_metrics', $this );
}
/**
* Render metrics in Prometheus text format.
*
* @return string
*/
public function render(): string {
$this->collect();
$renderer = new RenderTextFormat();
return $renderer->render( $this->registry->getMetricFamilySamples() );
}
/**
* Collect WordPress info metric.
*
* @return void
*/
private function collect_wordpress_info(): void {
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'info',
'WordPress installation information',
array( 'version', 'php_version', 'multisite' )
);
$gauge->set(
1,
array(
get_bloginfo( 'version' ),
PHP_VERSION,
is_multisite() ? 'yes' : 'no',
)
);
}
/**
* Collect total users metric.
*
* @return void
*/
private function collect_users_total(): void {
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'users_total',
'Total number of WordPress users',
array( 'role' )
);
$user_count = count_users();
foreach ( $user_count['avail_roles'] as $role => $count ) {
$gauge->set( $count, array( $role ) );
}
}
/**
* Collect total posts metric.
*
* @return void
*/
private function collect_posts_total(): void {
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'posts_total',
'Total number of posts by type and status',
array( 'post_type', 'status' )
);
$post_types = get_post_types( array( 'public' => true ) );
foreach ( $post_types as $post_type ) {
$counts = wp_count_posts( $post_type );
foreach ( get_object_vars( $counts ) as $status => $count ) {
if ( $count > 0 ) {
$gauge->set( (int) $count, array( $post_type, $status ) );
}
}
}
}
/**
* Collect total comments metric.
*
* @return void
*/
private function collect_comments_total(): void {
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'comments_total',
'Total number of comments by status',
array( 'status' )
);
$comments = wp_count_comments();
$statuses = array(
'approved' => $comments->approved,
'moderated' => $comments->moderated,
'spam' => $comments->spam,
'trash' => $comments->trash,
'total_comments' => $comments->total_comments,
);
foreach ( $statuses as $status => $count ) {
$gauge->set( (int) $count, array( $status ) );
}
}
/**
* Collect total plugins metric.
*
* @return void
*/
private function collect_plugins_total(): void {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$gauge = $this->registry->getOrRegisterGauge(
$this->namespace,
'plugins_total',
'Total number of plugins by status',
array( 'status' )
);
$all_plugins = get_plugins();
$active_plugins = get_option( 'active_plugins', array() );
$gauge->set( count( $all_plugins ), array( 'installed' ) );
$gauge->set( count( $active_plugins ), array( 'active' ) );
$gauge->set( count( $all_plugins ) - count( $active_plugins ), array( 'inactive' ) );
}
/**
* Register a custom gauge metric.
*
* @param string $name Metric name.
* @param string $help Metric description.
* @param array $labels Label names.
* @return \Prometheus\Gauge
*/
public function register_gauge( string $name, string $help, array $labels = array() ): \Prometheus\Gauge {
return $this->registry->getOrRegisterGauge(
$this->namespace,
$name,
$help,
$labels
);
}
/**
* Register a custom counter metric.
*
* @param string $name Metric name.
* @param string $help Metric description.
* @param array $labels Label names.
* @return \Prometheus\Counter
*/
public function register_counter( string $name, string $help, array $labels = array() ): \Prometheus\Counter {
return $this->registry->getOrRegisterCounter(
$this->namespace,
$name,
$help,
$labels
);
}
/**
* Register a custom histogram metric.
*
* @param string $name Metric name.
* @param string $help Metric description.
* @param array $labels Label names.
* @param array|null $buckets Histogram buckets.
* @return \Prometheus\Histogram
*/
public function register_histogram( string $name, string $help, array $labels = array(), ?array $buckets = null ): \Prometheus\Histogram {
return $this->registry->getOrRegisterHistogram(
$this->namespace,
$name,
$help,
$labels,
$buckets
);
}
}

150
src/Plugin.php Normal file
View File

@@ -0,0 +1,150 @@
<?php
/**
* Main plugin class.
*
* @package WP_Prometheus
*/
namespace Magdev\WpPrometheus;
use Magdev\WpPrometheus\Admin\Settings;
use Magdev\WpPrometheus\Endpoint\MetricsEndpoint;
use Magdev\WpPrometheus\License\Manager as LicenseManager;
use Magdev\WpPrometheus\Metrics\Collector;
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Plugin singleton class.
*
* Initializes and manages all plugin components.
*/
final class Plugin {
/**
* Singleton instance.
*
* @var Plugin|null
*/
private static ?Plugin $instance = null;
/**
* Metrics collector instance.
*
* @var Collector|null
*/
private ?Collector $collector = null;
/**
* Get singleton instance.
*
* @return Plugin
*/
public static function get_instance(): Plugin {
if ( null === self::$instance ) {
self::$instance = new self();
}
return self::$instance;
}
/**
* Private constructor to enforce singleton pattern.
*/
private function __construct() {
$this->init_components();
$this->init_hooks();
$this->load_textdomain();
}
/**
* Prevent cloning.
*
* @return void
*/
private function __clone() {}
/**
* Prevent unserialization.
*
* @throws \Exception Always throws to prevent unserialization.
* @return void
*/
public function __wakeup(): void {
throw new \Exception( 'Cannot unserialize singleton' );
}
/**
* Initialize plugin components.
*
* @return void
*/
private function init_components(): void {
// Initialize license manager.
LicenseManager::get_instance();
// Initialize admin settings (always needed).
if ( is_admin() ) {
new Settings();
}
// Initialize metrics endpoint (only if licensed).
if ( LicenseManager::is_license_valid() ) {
$this->collector = new Collector();
new MetricsEndpoint( $this->collector );
}
}
/**
* Initialize WordPress hooks.
*
* @return void
*/
private function init_hooks(): void {
// Add settings link to plugins page.
add_filter( 'plugin_action_links_' . WP_PROMETHEUS_BASENAME, array( $this, 'add_plugin_action_links' ) );
}
/**
* Add action links to the plugins page.
*
* @param array $links Existing action links.
* @return array Modified action links.
*/
public function add_plugin_action_links( array $links ): array {
$settings_link = sprintf(
'<a href="%s">%s</a>',
esc_url( admin_url( 'options-general.php?page=wp-prometheus' ) ),
esc_html__( 'Settings', 'wp-prometheus' )
);
// Add our link at the beginning.
array_unshift( $links, $settings_link );
return $links;
}
/**
* Load plugin textdomain.
*
* @return void
*/
private function load_textdomain(): void {
load_plugin_textdomain(
'wp-prometheus',
false,
dirname( WP_PROMETHEUS_BASENAME ) . '/languages'
);
}
/**
* Get the metrics collector.
*
* @return Collector|null
*/
public function get_collector(): ?Collector {
return $this->collector;
}
}

11
src/index.php Normal file
View File

@@ -0,0 +1,11 @@
<?php
/**
* Silence is golden.
*
* @package WP_Prometheus
*/
// Prevent direct file access.
if ( ! defined( 'ABSPATH' ) ) {
exit;
}