diff --git a/CHANGELOG.md b/CHANGELOG.md index a801d59..3be76a3 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/CLAUDE.md b/CLAUDE.md index 68d8252..7990fee 100644 --- a/CLAUDE.md +++ b/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 diff --git a/README.md b/README.md index b2a3b62..64f7768 100644 --- a/README.md +++ b/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 diff --git a/assets/css/admin.css b/assets/css/admin.css index cb552e5..96397a7 100644 --- a/assets/css/admin.css +++ b/assets/css/admin.css @@ -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; diff --git a/languages/wp-prometheus-de_CH.mo b/languages/wp-prometheus-de_CH.mo index 415103a..8b7480b 100644 Binary files a/languages/wp-prometheus-de_CH.mo and b/languages/wp-prometheus-de_CH.mo differ diff --git a/languages/wp-prometheus-de_CH.po b/languages/wp-prometheus-de_CH.po index c6cc7de..b34e09b 100644 --- a/languages/wp-prometheus-de_CH.po +++ b/languages/wp-prometheus-de_CH.po @@ -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." diff --git a/languages/wp-prometheus.pot b/languages/wp-prometheus.pot index fd5b117..50dece5 100644 --- a/languages/wp-prometheus.pot +++ b/languages/wp-prometheus.pot @@ -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 "" diff --git a/src/Admin/DashboardProvider.php b/src/Admin/DashboardProvider.php index 59f458f..81b91ff 100644 --- a/src/Admin/DashboardProvider.php +++ b/src/Admin/DashboardProvider.php @@ -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; } } diff --git a/src/Admin/Settings.php b/src/Admin/Settings.php index dd527ac..7ada569 100644 --- a/src/Admin/Settings.php +++ b/src/Admin/Settings.php @@ -1214,20 +1214,45 @@ class Settings {
-