You've already forked wp-prometheus
feat: Add dashboard extension hook for third-party plugins (v0.4.6)
All checks were successful
Create Release Package / build-release (push) Successful in 1m5s
All checks were successful
Create Release Package / build-release (push) Successful in 1m5s
Add wp_prometheus_register_dashboards action hook allowing third-party plugins to register their own Grafana dashboard templates. - DashboardProvider: registration system with file/JSON support - Security: path traversal protection, JSON validation - Admin UI: "Extension" badge and plugin attribution - Isolated mode support for dashboard registration hook Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
17
CHANGELOG.md
17
CHANGELOG.md
@@ -5,6 +5,23 @@ 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.4.6] - 2026-02-03
|
||||
|
||||
### Added
|
||||
|
||||
- Dashboard extension hook `wp_prometheus_register_dashboards` for third-party plugins
|
||||
- Third-party plugins can now register their own Grafana dashboard templates
|
||||
- Support for file-based and inline JSON dashboard registration
|
||||
- "Extension" badge for third-party dashboards in admin UI
|
||||
- Plugin attribution display for third-party dashboards
|
||||
- Security: Path traversal protection for registered dashboard files
|
||||
- Isolated mode support for dashboard registration hook
|
||||
|
||||
### Changed
|
||||
|
||||
- DashboardProvider now supports both built-in and third-party registered dashboards
|
||||
- Dashboard cards show source (built-in vs extension) with visual distinction
|
||||
|
||||
## [0.4.5] - 2026-02-02
|
||||
|
||||
### Fixed
|
||||
|
||||
29
CLAUDE.md
29
CLAUDE.md
@@ -23,6 +23,7 @@ This plugin provides a Prometheus `/metrics` endpoint and an extensible way to a
|
||||
- Grafana dashboard templates for easy visualization
|
||||
- Dedicated plugin settings under 'Settings/Metrics' menu
|
||||
- Extensible by other plugins using `wp_prometheus_collect_metrics` action hook
|
||||
- Dashboard extension hook `wp_prometheus_register_dashboards` for third-party Grafana dashboards
|
||||
- License management integration
|
||||
|
||||
### Key Fact: 100% AI-Generated
|
||||
@@ -33,7 +34,7 @@ This project is proudly **"vibe-coded"** using Claude.AI - the entire codebase w
|
||||
|
||||
**Note for AI Assistants:** Clean this section after the specific features are done or new releases are made. Effective changes are tracked in `CHANGELOG.md`. Do not add completed versions here - document them in the Session History section at the end of this file.
|
||||
|
||||
*No planned features at this time.*
|
||||
*No pending roadmap items.*
|
||||
|
||||
## Technical Stack
|
||||
|
||||
@@ -291,6 +292,32 @@ add_action( 'wp_prometheus_collect_metrics', function( $collector ) {
|
||||
|
||||
## Session History
|
||||
|
||||
### 2026-02-03 - Dashboard Extension Hook (v0.4.6)
|
||||
|
||||
- Added `wp_prometheus_register_dashboards` action hook for third-party plugins
|
||||
- Third-party plugins can now register their own Grafana dashboard templates
|
||||
- Implementation in `DashboardProvider.php`:
|
||||
- `register_dashboard(slug, args)` method for registrations
|
||||
- Supports file-based dashboards (absolute path to JSON) or inline JSON content
|
||||
- Security: Path traversal protection (files must be under `WP_CONTENT_DIR`)
|
||||
- `fire_registration_hook()` with output buffering and exception handling
|
||||
- Respects isolated mode setting (skips third-party hooks when enabled)
|
||||
- `is_third_party()` and `get_plugin_name()` helper methods
|
||||
- Updated admin UI in Settings.php:
|
||||
- "Extension" badge displayed on third-party dashboard cards
|
||||
- Plugin attribution shown below third-party dashboards
|
||||
- Visual distinction with blue border for third-party cards
|
||||
- **Key Learning**: Extension hook design pattern
|
||||
- Fire hook lazily on first `get_available()` call, not in constructor
|
||||
- Use `$hook_fired` flag to prevent double-firing
|
||||
- Wrap hook execution in try-catch to isolate failures
|
||||
- Validate registrations thoroughly before accepting them
|
||||
- **Key Learning**: Security for file-based registrations
|
||||
- Require absolute paths (`path_is_absolute()`)
|
||||
- Validate files exist and are readable
|
||||
- Use `realpath()` to resolve symlinks and prevent traversal
|
||||
- Restrict to `WP_CONTENT_DIR` (not just plugin directories)
|
||||
|
||||
### 2026-02-02 - Settings Persistence Fix (v0.4.5)
|
||||
|
||||
- Fixed critical bug where settings would get cleared when saving from different Metrics sub-tabs
|
||||
|
||||
51
README.md
51
README.md
@@ -6,7 +6,13 @@ A WordPress plugin that provides a Prometheus-compatible `/metrics` endpoint wit
|
||||
|
||||
- Prometheus-compatible authenticated `/metrics` endpoint
|
||||
- Default WordPress metrics (users, posts, comments, plugins)
|
||||
- Runtime metrics (HTTP requests, database queries)
|
||||
- Cron job and transient cache metrics
|
||||
- WooCommerce integration (products, orders, revenue)
|
||||
- Custom metric builder with admin UI
|
||||
- Grafana dashboard templates with download
|
||||
- Extensible by other plugins using hooks
|
||||
- Dashboard extension hook for third-party Grafana dashboards
|
||||
- Settings page under Settings > Metrics
|
||||
- Bearer token authentication
|
||||
- License management integration
|
||||
@@ -154,6 +160,51 @@ $histogram = $collector->register_histogram( $name, $help, $labels, $buckets );
|
||||
$histogram->observe( $value, $labelValues );
|
||||
```
|
||||
|
||||
## Extending with Custom Dashboards (v0.4.6+)
|
||||
|
||||
Add your own Grafana dashboard templates using the `wp_prometheus_register_dashboards` action:
|
||||
|
||||
```php
|
||||
add_action( 'wp_prometheus_register_dashboards', function( $provider ) {
|
||||
// File-based dashboard
|
||||
$provider->register_dashboard( 'my-plugin-dashboard', array(
|
||||
'title' => __( 'My Plugin Metrics', 'my-plugin' ),
|
||||
'description' => __( 'Dashboard for my custom metrics', 'my-plugin' ),
|
||||
'icon' => 'dashicons-chart-bar',
|
||||
'file' => MY_PLUGIN_PATH . 'assets/dashboards/my-dashboard.json',
|
||||
'plugin' => 'My Plugin Name',
|
||||
) );
|
||||
|
||||
// OR inline JSON dashboard
|
||||
$provider->register_dashboard( 'dynamic-dashboard', array(
|
||||
'title' => __( 'Dynamic Dashboard', 'my-plugin' ),
|
||||
'description' => __( 'Dynamically generated dashboard', 'my-plugin' ),
|
||||
'icon' => 'dashicons-admin-generic',
|
||||
'json' => json_encode( $dashboard_array ),
|
||||
'plugin' => 'My Plugin Name',
|
||||
) );
|
||||
} );
|
||||
```
|
||||
|
||||
### Registration Parameters
|
||||
|
||||
| Parameter | Required | Description |
|
||||
| --------- | -------- | ----------- |
|
||||
| `title` | Yes | Dashboard title displayed in admin |
|
||||
| `description` | No | Description shown below the title |
|
||||
| `icon` | No | Dashicon class (default: `dashicons-chart-line`) |
|
||||
| `file` | Yes* | Absolute path to JSON file |
|
||||
| `json` | Yes* | Inline JSON content |
|
||||
| `plugin` | No | Plugin name for attribution |
|
||||
|
||||
*Either `file` or `json` is required, but not both.
|
||||
|
||||
### Security Notes
|
||||
|
||||
- File paths must be absolute and within `wp-content/`
|
||||
- Inline JSON is validated during registration
|
||||
- Third-party dashboards are marked with an "Extension" badge in the admin UI
|
||||
|
||||
## Development
|
||||
|
||||
### Build for Release
|
||||
|
||||
@@ -216,6 +216,32 @@
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
/* Third-party dashboard card styling */
|
||||
.wp-prometheus-dashboard-card.third-party {
|
||||
position: relative;
|
||||
border-color: #2271b1;
|
||||
}
|
||||
|
||||
.wp-prometheus-dashboard-card .dashboard-badge {
|
||||
position: absolute;
|
||||
top: -8px;
|
||||
right: -8px;
|
||||
background: #2271b1;
|
||||
color: #fff;
|
||||
font-size: 10px;
|
||||
font-weight: 600;
|
||||
padding: 3px 8px;
|
||||
border-radius: 10px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.wp-prometheus-dashboard-card .dashboard-plugin {
|
||||
color: #646970;
|
||||
margin: -5px 0 15px 0;
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
/* Import options panel */
|
||||
#import-options {
|
||||
border-radius: 4px;
|
||||
|
||||
Binary file not shown.
@@ -947,3 +947,49 @@ msgstr "Laufzeit-Metriken erfassen HTTP-Anfragen und Datenbank-Abfragen ueber me
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Reset Data"
|
||||
msgstr "Daten zuruecksetzen"
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Extension"
|
||||
msgstr "Erweiterung"
|
||||
|
||||
#. translators: %s: Plugin name
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Provided by: %s"
|
||||
msgstr "Bereitgestellt von: %s"
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "No dashboards available."
|
||||
msgstr "Keine Dashboards verfuegbar."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Pre-built dashboards for visualizing your WordPress metrics in Grafana."
|
||||
msgstr "Vorgefertigte Dashboards zur Visualisierung Ihrer WordPress-Metriken in Grafana."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Installation Instructions"
|
||||
msgstr "Installationsanleitung"
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Download the JSON file for your desired dashboard."
|
||||
msgstr "Laden Sie die JSON-Datei fuer das gewuenschte Dashboard herunter."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "In Grafana, go to Dashboards → Import."
|
||||
msgstr "Gehen Sie in Grafana zu Dashboards → Import."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Upload the JSON file or paste its contents."
|
||||
msgstr "Laden Sie die JSON-Datei hoch oder fuegen Sie den Inhalt ein."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Select your Prometheus data source when prompted."
|
||||
msgstr "Waehlen Sie Ihre Prometheus-Datenquelle, wenn Sie dazu aufgefordert werden."
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Click Import to create the dashboard."
|
||||
msgstr "Klicken Sie auf Import, um das Dashboard zu erstellen."
|
||||
|
||||
#. translators: %s: Metrics URL
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Make sure your Prometheus instance is configured to scrape %s with the correct authentication token."
|
||||
msgstr "Stellen Sie sicher, dass Ihre Prometheus-Instanz so konfiguriert ist, dass sie %s mit dem richtigen Authentifizierungs-Token abruft."
|
||||
|
||||
@@ -944,3 +944,49 @@ msgstr ""
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Reset Data"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Extension"
|
||||
msgstr ""
|
||||
|
||||
#. translators: %s: Plugin name
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Provided by: %s"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "No dashboards available."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Pre-built dashboards for visualizing your WordPress metrics in Grafana."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Installation Instructions"
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Download the JSON file for your desired dashboard."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "In Grafana, go to Dashboards → Import."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Upload the JSON file or paste its contents."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Select your Prometheus data source when prompted."
|
||||
msgstr ""
|
||||
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Click Import to create the dashboard."
|
||||
msgstr ""
|
||||
|
||||
#. translators: %s: Metrics URL
|
||||
#: src/Admin/Settings.php
|
||||
msgid "Make sure your Prometheus instance is configured to scrape %s with the correct authentication token."
|
||||
msgstr ""
|
||||
|
||||
@@ -16,22 +16,37 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||
* DashboardProvider class.
|
||||
*
|
||||
* Provides Grafana dashboard templates for download.
|
||||
* Supports both built-in dashboards and third-party registrations.
|
||||
*/
|
||||
class DashboardProvider {
|
||||
|
||||
/**
|
||||
* Dashboard directory path.
|
||||
* Dashboard directory path for built-in dashboards.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private string $dashboard_dir;
|
||||
|
||||
/**
|
||||
* Available dashboard definitions.
|
||||
* Built-in dashboard definitions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $dashboards = array();
|
||||
private array $builtin_dashboards = array();
|
||||
|
||||
/**
|
||||
* Third-party registered dashboard definitions.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
private array $registered_dashboards = array();
|
||||
|
||||
/**
|
||||
* Whether the registration hook has been fired.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
private bool $hook_fired = false;
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
@@ -39,43 +54,241 @@ class DashboardProvider {
|
||||
public function __construct() {
|
||||
$this->dashboard_dir = WP_PROMETHEUS_PATH . 'assets/dashboards/';
|
||||
|
||||
$this->dashboards = array(
|
||||
$this->builtin_dashboards = array(
|
||||
'wordpress-overview' => array(
|
||||
'title' => __( 'WordPress Overview', 'wp-prometheus' ),
|
||||
'description' => __( 'General WordPress metrics including users, posts, comments, and plugins.', 'wp-prometheus' ),
|
||||
'file' => 'wordpress-overview.json',
|
||||
'icon' => 'dashicons-wordpress',
|
||||
'source' => 'builtin',
|
||||
),
|
||||
'wordpress-runtime' => array(
|
||||
'title' => __( 'Runtime Performance', 'wp-prometheus' ),
|
||||
'description' => __( 'HTTP request metrics, database query performance, and response times.', 'wp-prometheus' ),
|
||||
'file' => 'wordpress-runtime.json',
|
||||
'icon' => 'dashicons-performance',
|
||||
'source' => 'builtin',
|
||||
),
|
||||
'wordpress-woocommerce' => array(
|
||||
'title' => __( 'WooCommerce Store', 'wp-prometheus' ),
|
||||
'description' => __( 'WooCommerce metrics including products, orders, revenue, and customers.', 'wp-prometheus' ),
|
||||
'file' => 'wordpress-woocommerce.json',
|
||||
'icon' => 'dashicons-cart',
|
||||
'source' => 'builtin',
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Register a third-party dashboard.
|
||||
*
|
||||
* @param string $slug Dashboard slug (unique identifier).
|
||||
* @param array $args Dashboard configuration {
|
||||
* @type string $title Dashboard title (required).
|
||||
* @type string $description Dashboard description.
|
||||
* @type string $icon Dashicon class (e.g., 'dashicons-chart-bar').
|
||||
* @type string $file Absolute path to JSON file (mutually exclusive with 'json').
|
||||
* @type string $json Inline JSON content (mutually exclusive with 'file').
|
||||
* @type string $plugin Plugin name for attribution.
|
||||
* }
|
||||
* @return bool True if registered successfully, false otherwise.
|
||||
*/
|
||||
public function register_dashboard( string $slug, array $args ): bool {
|
||||
// Sanitize slug - must be valid identifier.
|
||||
$slug = sanitize_key( $slug );
|
||||
|
||||
if ( empty( $slug ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( 'WP Prometheus: Dashboard registration failed - invalid slug' );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate slugs (built-in takes precedence).
|
||||
if ( isset( $this->builtin_dashboards[ $slug ] ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard slug '$slug' conflicts with built-in dashboard" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check for duplicate slugs in already registered dashboards.
|
||||
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard slug '$slug' already registered" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate required fields.
|
||||
if ( empty( $args['title'] ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' missing required 'title'" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Must have either 'file' or 'json', not both.
|
||||
$has_file = ! empty( $args['file'] );
|
||||
$has_json = ! empty( $args['json'] );
|
||||
|
||||
if ( ! $has_file && ! $has_json ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' must have 'file' or 'json'" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
if ( $has_file && $has_json ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' cannot have both 'file' and 'json'" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Validate file path if provided.
|
||||
if ( $has_file ) {
|
||||
$file_path = $args['file'];
|
||||
|
||||
// Must be absolute path.
|
||||
if ( ! path_is_absolute( $file_path ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' file path must be absolute" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// File must exist and be readable.
|
||||
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' file not found: $file_path" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Security: Prevent path traversal - file must be under wp-content.
|
||||
$real_path = realpath( $file_path );
|
||||
$wp_content_dir = realpath( WP_CONTENT_DIR );
|
||||
|
||||
if ( false === $real_path || false === $wp_content_dir ||
|
||||
strpos( $real_path, $wp_content_dir ) !== 0 ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' file outside wp-content" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Validate JSON if provided inline.
|
||||
if ( $has_json ) {
|
||||
$decoded = json_decode( $args['json'], true );
|
||||
if ( null === $decoded && json_last_error() !== JSON_ERROR_NONE ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( "WP Prometheus: Dashboard '$slug' has invalid JSON" );
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Build dashboard entry.
|
||||
$this->registered_dashboards[ $slug ] = array(
|
||||
'title' => sanitize_text_field( $args['title'] ),
|
||||
'description' => sanitize_text_field( $args['description'] ?? '' ),
|
||||
'icon' => sanitize_html_class( $args['icon'] ?? 'dashicons-chart-line' ),
|
||||
'file' => $has_file ? $file_path : null,
|
||||
'json' => $has_json ? $args['json'] : null,
|
||||
'plugin' => sanitize_text_field( $args['plugin'] ?? '' ),
|
||||
'source' => 'third-party',
|
||||
);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fire the dashboard registration hook with protection.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
private function fire_registration_hook(): void {
|
||||
if ( $this->hook_fired ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->hook_fired = true;
|
||||
|
||||
// Check for isolated mode - skip third-party hooks.
|
||||
$isolated_mode = defined( 'WP_PROMETHEUS_ISOLATED_MODE' ) && WP_PROMETHEUS_ISOLATED_MODE;
|
||||
|
||||
// Also check option for admin-side isolated mode.
|
||||
if ( ! $isolated_mode ) {
|
||||
$isolated_mode = (bool) get_option( 'wp_prometheus_isolated_mode', false );
|
||||
}
|
||||
|
||||
if ( $isolated_mode ) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Use output buffering to capture any accidental output.
|
||||
ob_start();
|
||||
|
||||
try {
|
||||
/**
|
||||
* Fires to allow third-party plugins to register dashboards.
|
||||
*
|
||||
* @since 0.4.6
|
||||
*
|
||||
* @param DashboardProvider $provider The dashboard provider instance.
|
||||
*/
|
||||
do_action( 'wp_prometheus_register_dashboards', $this );
|
||||
} catch ( \Throwable $e ) {
|
||||
if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
|
||||
// phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_error_log
|
||||
error_log( 'WP Prometheus: Error in dashboard registration hook: ' . $e->getMessage() );
|
||||
}
|
||||
}
|
||||
|
||||
// Discard any output from plugins.
|
||||
ob_end_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get list of available dashboards.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function get_available(): array {
|
||||
// Fire registration hook first (only once).
|
||||
$this->fire_registration_hook();
|
||||
|
||||
$available = array();
|
||||
|
||||
foreach ( $this->dashboards as $slug => $dashboard ) {
|
||||
// Add built-in dashboards (check file exists).
|
||||
foreach ( $this->builtin_dashboards as $slug => $dashboard ) {
|
||||
$file_path = $this->dashboard_dir . $dashboard['file'];
|
||||
if ( file_exists( $file_path ) ) {
|
||||
$available[ $slug ] = $dashboard;
|
||||
}
|
||||
}
|
||||
|
||||
// Add registered third-party dashboards.
|
||||
foreach ( $this->registered_dashboards as $slug => $dashboard ) {
|
||||
// Already validated during registration, but double-check.
|
||||
if ( ! empty( $dashboard['json'] ) ||
|
||||
( ! empty( $dashboard['file'] ) && file_exists( $dashboard['file'] ) ) ) {
|
||||
$available[ $slug ] = $dashboard;
|
||||
}
|
||||
}
|
||||
|
||||
return $available;
|
||||
}
|
||||
|
||||
@@ -86,35 +299,70 @@ class DashboardProvider {
|
||||
* @return string|null JSON content or null if not found.
|
||||
*/
|
||||
public function get_dashboard( string $slug ): ?string {
|
||||
// Validate slug to prevent directory traversal.
|
||||
// Fire registration hook first.
|
||||
$this->fire_registration_hook();
|
||||
|
||||
// Validate slug.
|
||||
$slug = sanitize_file_name( $slug );
|
||||
|
||||
if ( ! isset( $this->dashboards[ $slug ] ) ) {
|
||||
return null;
|
||||
// Check built-in dashboards first.
|
||||
if ( isset( $this->builtin_dashboards[ $slug ] ) ) {
|
||||
$dashboard = $this->builtin_dashboards[ $slug ];
|
||||
$file_path = $this->dashboard_dir . $dashboard['file'];
|
||||
|
||||
// Security: Ensure file is within dashboard directory.
|
||||
$real_path = realpath( $file_path );
|
||||
$real_dir = realpath( $this->dashboard_dir );
|
||||
|
||||
if ( false === $real_path || false === $real_dir ||
|
||||
strpos( $real_path, $real_dir ) !== 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$content = file_get_contents( $file_path );
|
||||
|
||||
return false === $content ? null : $content;
|
||||
}
|
||||
|
||||
$file_path = $this->dashboard_dir . $this->dashboards[ $slug ]['file'];
|
||||
// Check registered dashboards.
|
||||
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||
$dashboard = $this->registered_dashboards[ $slug ];
|
||||
|
||||
// Security: Ensure file is within dashboard directory.
|
||||
$real_path = realpath( $file_path );
|
||||
$real_dir = realpath( $this->dashboard_dir );
|
||||
// Inline JSON.
|
||||
if ( ! empty( $dashboard['json'] ) ) {
|
||||
return $dashboard['json'];
|
||||
}
|
||||
|
||||
if ( false === $real_path || false === $real_dir || strpos( $real_path, $real_dir ) !== 0 ) {
|
||||
return null;
|
||||
// File-based.
|
||||
if ( ! empty( $dashboard['file'] ) ) {
|
||||
$file_path = $dashboard['file'];
|
||||
|
||||
// Security: Re-verify file is under wp-content.
|
||||
$real_path = realpath( $file_path );
|
||||
$wp_content_dir = realpath( WP_CONTENT_DIR );
|
||||
|
||||
if ( false === $real_path || false === $wp_content_dir ||
|
||||
strpos( $real_path, $wp_content_dir ) !== 0 ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$content = file_get_contents( $file_path );
|
||||
|
||||
return false === $content ? null : $content;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! file_exists( $file_path ) || ! is_readable( $file_path ) ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
|
||||
$content = file_get_contents( $file_path );
|
||||
|
||||
if ( false === $content ) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return $content;
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,13 +372,20 @@ class DashboardProvider {
|
||||
* @return array|null Dashboard metadata or null if not found.
|
||||
*/
|
||||
public function get_metadata( string $slug ): ?array {
|
||||
// Fire registration hook first.
|
||||
$this->fire_registration_hook();
|
||||
|
||||
$slug = sanitize_file_name( $slug );
|
||||
|
||||
if ( ! isset( $this->dashboards[ $slug ] ) ) {
|
||||
return null;
|
||||
if ( isset( $this->builtin_dashboards[ $slug ] ) ) {
|
||||
return $this->builtin_dashboards[ $slug ];
|
||||
}
|
||||
|
||||
return $this->dashboards[ $slug ];
|
||||
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||
return $this->registered_dashboards[ $slug ];
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,12 +395,61 @@ class DashboardProvider {
|
||||
* @return string|null Filename or null if not found.
|
||||
*/
|
||||
public function get_filename( string $slug ): ?string {
|
||||
// Fire registration hook first.
|
||||
$this->fire_registration_hook();
|
||||
|
||||
$slug = sanitize_file_name( $slug );
|
||||
|
||||
if ( ! isset( $this->dashboards[ $slug ] ) ) {
|
||||
return null;
|
||||
// Built-in dashboards have predefined filenames.
|
||||
if ( isset( $this->builtin_dashboards[ $slug ] ) ) {
|
||||
return $this->builtin_dashboards[ $slug ]['file'];
|
||||
}
|
||||
|
||||
return $this->dashboards[ $slug ]['file'];
|
||||
// Registered dashboards - use file basename or generate from slug.
|
||||
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||
$dashboard = $this->registered_dashboards[ $slug ];
|
||||
|
||||
if ( ! empty( $dashboard['file'] ) ) {
|
||||
return basename( $dashboard['file'] );
|
||||
}
|
||||
|
||||
// Generate filename from slug for inline JSON.
|
||||
return $slug . '.json';
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a dashboard is from a third-party plugin.
|
||||
*
|
||||
* @param string $slug Dashboard slug.
|
||||
* @return bool True if third-party, false if built-in or not found.
|
||||
*/
|
||||
public function is_third_party( string $slug ): bool {
|
||||
$this->fire_registration_hook();
|
||||
|
||||
$slug = sanitize_file_name( $slug );
|
||||
|
||||
return isset( $this->registered_dashboards[ $slug ] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin name for a third-party dashboard.
|
||||
*
|
||||
* @param string $slug Dashboard slug.
|
||||
* @return string|null Plugin name or null if not found/built-in.
|
||||
*/
|
||||
public function get_plugin_name( string $slug ): ?string {
|
||||
$this->fire_registration_hook();
|
||||
|
||||
$slug = sanitize_file_name( $slug );
|
||||
|
||||
if ( isset( $this->registered_dashboards[ $slug ] ) ) {
|
||||
$plugin = $this->registered_dashboards[ $slug ]['plugin'];
|
||||
return ! empty( $plugin ) ? $plugin : null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1214,20 +1214,45 @@ class Settings {
|
||||
<h2><?php esc_html_e( 'Grafana Dashboard Templates', 'wp-prometheus' ); ?></h2>
|
||||
<p class="description"><?php esc_html_e( 'Pre-built dashboards for visualizing your WordPress metrics in Grafana.', 'wp-prometheus' ); ?></p>
|
||||
|
||||
<div class="wp-prometheus-dashboard-grid">
|
||||
<?php foreach ( $dashboards as $slug => $dashboard ) : ?>
|
||||
<div class="wp-prometheus-dashboard-card">
|
||||
<div class="dashboard-icon">
|
||||
<span class="dashicons <?php echo esc_attr( $dashboard['icon'] ); ?>"></span>
|
||||
<?php if ( empty( $dashboards ) ) : ?>
|
||||
<p class="description"><?php esc_html_e( 'No dashboards available.', 'wp-prometheus' ); ?></p>
|
||||
<?php else : ?>
|
||||
<div class="wp-prometheus-dashboard-grid">
|
||||
<?php
|
||||
foreach ( $dashboards as $slug => $dashboard ) :
|
||||
$is_third_party = $this->dashboard_provider->is_third_party( $slug );
|
||||
$plugin_name = $this->dashboard_provider->get_plugin_name( $slug );
|
||||
$card_class = 'wp-prometheus-dashboard-card' . ( $is_third_party ? ' third-party' : '' );
|
||||
?>
|
||||
<div class="<?php echo esc_attr( $card_class ); ?>">
|
||||
<?php if ( $is_third_party ) : ?>
|
||||
<span class="dashboard-badge"><?php esc_html_e( 'Extension', 'wp-prometheus' ); ?></span>
|
||||
<?php endif; ?>
|
||||
<div class="dashboard-icon">
|
||||
<span class="dashicons <?php echo esc_attr( $dashboard['icon'] ); ?>"></span>
|
||||
</div>
|
||||
<h3><?php echo esc_html( $dashboard['title'] ); ?></h3>
|
||||
<p><?php echo esc_html( $dashboard['description'] ); ?></p>
|
||||
<?php if ( $is_third_party && $plugin_name ) : ?>
|
||||
<p class="dashboard-plugin">
|
||||
<small>
|
||||
<?php
|
||||
printf(
|
||||
/* translators: %s: Plugin name */
|
||||
esc_html__( 'Provided by: %s', 'wp-prometheus' ),
|
||||
esc_html( $plugin_name )
|
||||
);
|
||||
?>
|
||||
</small>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
<button type="button" class="button button-primary download-dashboard" data-slug="<?php echo esc_attr( $slug ); ?>">
|
||||
<?php esc_html_e( 'Download', 'wp-prometheus' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<h3><?php echo esc_html( $dashboard['title'] ); ?></h3>
|
||||
<p><?php echo esc_html( $dashboard['description'] ); ?></p>
|
||||
<button type="button" class="button button-primary download-dashboard" data-slug="<?php echo esc_attr( $slug ); ?>">
|
||||
<?php esc_html_e( 'Download', 'wp-prometheus' ); ?>
|
||||
</button>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endforeach; ?>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
<hr style="margin: 30px 0;">
|
||||
|
||||
|
||||
@@ -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.4.5
|
||||
* Version: 0.4.6
|
||||
* Requires at least: 6.4
|
||||
* Requires PHP: 8.3
|
||||
* Author: Marco Graetsch
|
||||
@@ -169,7 +169,7 @@ wp_prometheus_early_metrics_check();
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
define( 'WP_PROMETHEUS_VERSION', '0.4.5' );
|
||||
define( 'WP_PROMETHEUS_VERSION', '0.4.6' );
|
||||
|
||||
/**
|
||||
* Plugin file path.
|
||||
|
||||
Reference in New Issue
Block a user