Version Info.
*/
class SettingsPage {
// Note: `private const` requires PHP 7.1+; plain `const` keeps these readable on PHP 5.6.
const PRO_TABS = array( 'system_resources', 'environment', 'server_location', 'version_history', 'health_advisor', 'system_export', 'email_alerts' );
const AGENCY_TABS = array( 'white_label', 'access_control', 'error_log' );
/**
* Hooks the settings page and option registration.
*/
public function register() {
add_action( 'admin_menu', array( $this, 'add_page' ) );
add_action( 'admin_init', array( $this, 'register_settings' ) );
}
/**
* Adds the options page to the Settings menu.
*/
public function add_page() {
add_options_page(
__( 'Version Info Settings', 'version-info' ),
__( 'Version Info', 'version-info' ),
'manage_options',
'version-info',
array( $this, 'render' )
);
}
/**
* Registers every plugin option in its per-tab settings group.
*/
public function register_settings() {
// General tab — own group so saving doesn't affect other tabs.
register_setting(
'version_info_general_group',
'version_info_show_footer',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_bool' ),
'default' => true,
)
);
register_setting(
'version_info_general_group',
'version_info_show_admin_bar',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_bool' ),
'default' => false,
)
);
register_setting(
'version_info_general_group',
'version_info_show_dashboard_widget',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_bool' ),
'default' => false,
)
);
register_setting(
'version_info_general_group',
'version_info_widget_show_resources',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_bool' ),
'default' => false,
)
);
// In version_info_general_group, so its
MUST live in the General
// form below — an option registered to a group whose form doesn't
// render it gets reset to false on every save (the 2.0.3 bug class).
register_setting(
'version_info_general_group',
'version_info_show_memory',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_bool' ),
'default' => true,
)
);
// Server Location tab — own group so saving doesn't clobber the General-tab
// checkboxes (which would otherwise get reset to false on every Server Location
// save because unchecked checkboxes aren't in $_POST). Reported by Steve
// Guccione 2026-05-27 — saving Server Location was disabling the dashboard widget.
register_setting(
'version_info_location_group',
'version_info_location_enabled',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_bool' ),
'default' => true,
)
);
register_setting(
'version_info_location_group',
'version_info_location_provider',
array(
'type' => 'string',
'sanitize_callback' => array( $this, 'sanitize_location_provider' ),
'default' => 'vi_anonymous',
)
);
register_setting(
'version_info_location_group',
'version_info_location_maxmind_key',
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
)
);
// Environment tab — isolated group.
register_setting(
'version_info_environment_group',
'version_info_show_env_badge',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_bool' ),
'default' => false,
)
);
register_setting(
'version_info_environment_group',
'version_info_env_admin_bar_highlight',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_bool' ),
'default' => false,
)
);
// White Label tab — isolated group.
register_setting(
'version_info_white_label_group',
'version_info_wl_plugin_name',
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
)
);
register_setting(
'version_info_white_label_group',
'version_info_wl_author_name',
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
)
);
register_setting(
'version_info_white_label_group',
'version_info_wl_hide_branding',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_bool' ),
'default' => false,
)
);
register_setting(
'version_info_white_label_group',
'version_info_wl_lock_owner_id',
array(
'type' => 'integer',
'sanitize_callback' => array( $this, 'sanitize_lock_owner' ),
'default' => 0,
)
);
register_setting(
'version_info_white_label_group',
'version_info_wl_hide_doc_links',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_bool' ),
'default' => false,
)
);
// Access Control tab — isolated group.
register_setting(
'version_info_access_control_group',
'version_info_allowed_roles',
array(
'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize_roles' ),
'default' => array( 'administrator' ),
)
);
// Email Alerts tab — isolated group.
register_setting(
'version_info_email_alerts_group',
'version_info_alert_enabled',
array(
'type' => 'boolean',
'sanitize_callback' => array( $this, 'sanitize_bool' ),
'default' => false,
)
);
register_setting(
'version_info_email_alerts_group',
'version_info_alert_recipients',
array(
'type' => 'string',
'sanitize_callback' => 'sanitize_text_field',
'default' => '',
)
);
register_setting(
'version_info_email_alerts_group',
'version_info_alert_types',
array(
'type' => 'array',
'sanitize_callback' => array( $this, 'sanitize_alert_types' ),
'default' => array( 'wordpress', 'php', 'mysql' ),
)
);
}
/**
* Normalizes a checkbox value to a strict boolean.
*
* @param mixed $input Raw option value.
*/
public function sanitize_bool( $input ) {
return filter_var( $input, FILTER_VALIDATE_BOOLEAN );
}
/**
* Convert the "lock to me" checkbox state into a user-ID owner.
*
* Form posts "1" when checked or "0" via the paired hidden field when
* unchecked. We map "1" to the current user's ID so the option stores
* "who owns the lock". Only the existing owner is allowed to flip the
* value — anyone else attempting to edit gets the old value back.
*
* @param mixed $input Raw option value.
* @return int
*/
public function sanitize_lock_owner( $input ) {
$current_owner = (int) get_option( 'version_info_wl_lock_owner_id', 0 );
$current_user = (int) get_current_user_id();
if ( $current_owner > 0 && $current_owner !== $current_user ) {
return $current_owner;
}
$checked = ( '1' === (string) $input || 1 === $input || true === $input );
if ( $checked ) {
return $current_user > 0 ? $current_user : 0;
}
return 0;
}
/**
* Restrict the location-provider option to the known set.
*
* @param mixed $input Raw option value.
* @return string
*/
public function sanitize_location_provider( $input ) {
$valid = array(
'vi_anonymous',
'cloudflare_trace',
'ip_api',
'maxmind',
);
$input = is_string( $input ) ? $input : '';
return in_array( $input, $valid, true ) ? $input : 'vi_anonymous';
}
/**
* Sanitizes the allowed-roles option into a list of role slugs.
*
* @param mixed $input Raw option value.
* @return string[]
*/
public function sanitize_roles( $input ) {
if ( ! is_array( $input ) ) {
return array( 'administrator' );
}
return array_map( 'sanitize_key', $input );
}
/**
* Restricts the alert-types option to the known component set.
*
* @param mixed $input Raw option value.
* @return string[]
*/
public function sanitize_alert_types( $input ) {
if ( ! is_array( $input ) ) {
return array( 'wordpress', 'php', 'mysql' );
}
$valid = array( 'wordpress', 'php', 'mysql' );
return array_values( array_intersect( array_map( 'sanitize_key', $input ), $valid ) );
}
/**
* Renders the settings page shell and the active tab.
*/
public function render() {
if ( ! current_user_can( 'manage_options' ) ) {
return;
}
$tabs = array(
'general' => __( 'General', 'version-info' ),
'system_resources' => __( 'System Resources', 'version-info' ),
'environment' => __( 'Environment', 'version-info' ),
'server_location' => __( 'Server Location', 'version-info' ),
'version_history' => __( 'Version History', 'version-info' ),
'health_advisor' => __( 'Health Advisor', 'version-info' ),
'system_export' => __( 'System Export', 'version-info' ),
'white_label' => __( 'White Label', 'version-info' ),
'access_control' => __( 'Access Control', 'version-info' ),
'email_alerts' => __( 'Email Alerts', 'version-info' ),
'error_log' => __( 'Error Log', 'version-info' ),
);
/**
* Filtered settings-tab map.
*
* @var array $tabs
*/
$tabs = apply_filters( 'version_info_settings_tabs', $tabs );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended
$current_tab = isset( $_GET['tab'] ) ? sanitize_key( $_GET['tab'] ) : 'general';
if ( ! array_key_exists( $current_tab, $tabs ) ) {
$current_tab = 'general';
}
$can_use_premium = vi_fs()->can_use_premium_code();
// is_plan_or_trial matches the registration gate in Plugin::maybe_init_pro()
// — Agency-trial users previously saw locked tabs while their hooks ran.
$is_agency = $can_use_premium && vi_fs()->is_plan_or_trial( 'agency' );
echo '';
echo '
' . esc_html__( 'Version Info Settings', 'version-info' ) . '
';
echo '
';
$this->render_tab( $current_tab, $can_use_premium, $is_agency );
echo '
';
}
/**
* Renders the requested tab, or a locked placeholder for unavailable tiers.
*
* @param string $tab_id Active tab identifier.
* @param bool $can_use_premium Whether premium code may run.
* @param bool $is_agency Whether the Agency plan (or trial) is active.
*/
private function render_tab( $tab_id, $can_use_premium, $is_agency ) {
if ( in_array( $tab_id, self::AGENCY_TABS, true ) && ! $is_agency ) {
$this->render_agency_tab_placeholder( $tab_id );
return;
}
if ( in_array( $tab_id, self::PRO_TABS, true ) && ! $can_use_premium ) {
$this->render_pro_tab_placeholder( $tab_id );
return;
}
switch ( $tab_id ) {
case 'general':
$this->render_general_tab();
break;
default:
do_action( "version_info_render_tab_{$tab_id}" );
break;
}
}
/**
* Renders the General settings tab.
*/
private function render_general_tab() {
$can_use_premium = vi_fs()->can_use_premium_code();
$widget_enabled = (bool) get_option( 'version_info_show_dashboard_widget', false );
$doc_link = Plugin::doc_link( 'getting-started-installation-and-setup', __( 'Setup & display-locations docs', 'version-info' ) );
if ( '' !== $doc_link ) {
echo '' . $doc_link . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper returns safe markup
}
?>
array(
'title' => __( 'System Resources', 'version-info' ),
'description' => __( 'Monitor real-time CPU load and memory usage directly from your WordPress dashboard.', 'version-info' ),
'features' => array(
__( 'CPU load percentage with visual bar', 'version-info' ),
__( 'RAM usage percentage with visual bar', 'version-info' ),
__( 'Database size tracking (data + index)', 'version-info' ),
__( 'Live updates via WordPress Heartbeat API', 'version-info' ),
),
),
'environment' => array(
'title' => __( 'Environment Indicators', 'version-info' ),
'description' => __( 'Instantly identify your environment type with a color-coded badge in the Admin Bar.', 'version-info' ),
'features' => array(
__( 'Auto-detects Production, Staging, Development, and Local', 'version-info' ),
__( 'Supports WP_ENVIRONMENT_TYPE, Bedrock, Kinsta, WP Engine, and more', 'version-info' ),
__( 'Color-coded Admin Bar badge', 'version-info' ),
),
),
'server_location' => array(
'title' => __( 'Server Location', 'version-info' ),
'description' => __( 'Auto-detect where your server lives, with a privacy-first chooser. Anonymous default that logs nothing — or pick your own provider.', 'version-info' ),
'features' => array(
__( 'Four selectable providers (Version Info anonymous, Cloudflare trace, ip-api, MaxMind)', 'version-info' ),
__( 'Enable/disable auto-detect entirely with one checkbox', 'version-info' ),
__( '30-day transient cache + one-click Detect Now', 'version-info' ),
__( 'Reverse-DNS fallback when providers are unreachable', 'version-info' ),
),
),
'version_history' => array(
'title' => __( 'Version History', 'version-info' ),
'description' => __( 'Track changes in your WordPress, PHP, and MySQL versions over time.', 'version-info' ),
'features' => array(
__( 'Automatic detection of version changes', 'version-info' ),
__( 'Hooks into WordPress core, plugin, and theme update process', 'version-info' ),
__( 'Sortable history table with timestamps', 'version-info' ),
),
),
'health_advisor' => array(
'title' => __( 'Health Advisor', 'version-info' ),
'description' => __( 'Predictive alerts for end-of-life software and critical server issues.', 'version-info' ),
'features' => array(
__( 'PHP end-of-life date tracking', 'version-info' ),
__( 'MySQL end-of-life date tracking', 'version-info' ),
__( 'SSL certificate expiry monitoring', 'version-info' ),
__( 'Integrates with WordPress Site Health screen', 'version-info' ),
),
),
'email_alerts' => array(
'title' => __( 'Email Alerts', 'version-info' ),
'description' => __( 'Receive priority email notifications whenever a version change is detected on your site.', 'version-info' ),
'features' => array(
__( 'Alerts for WordPress, PHP, and MySQL version changes', 'version-info' ),
__( 'Configurable recipient list', 'version-info' ),
__( 'Per-component alert toggles', 'version-info' ),
),
),
'system_export' => array(
'title' => __( 'System Info Export', 'version-info' ),
'description' => __( 'Download a complete snapshot of your technical stack as a JSON file.', 'version-info' ),
'features' => array(
__( 'One-click JSON download of all system info', 'version-info' ),
__( 'Includes PHP extensions, active plugins, and theme data', 'version-info' ),
__( 'Ideal for support ticket attachments', 'version-info' ),
),
),
);
$preview = isset( $previews[ $tab_id ] ) ? $previews[ $tab_id ] : null;
if ( ! $preview ) {
return;
}
$this->render_locked_placeholder( $preview, 'pro' );
}
/**
* Renders the locked preview shown to non-Agency users on Agency tabs.
*
* @param string $tab_id Active tab identifier.
*/
private function render_agency_tab_placeholder( $tab_id ) {
$previews = array(
'white_label' => array(
'title' => __( 'White Label', 'version-info' ),
'description' => __( 'Fully rebrand the plugin with your own name and remove all Gaucho Plugins branding.', 'version-info' ),
'features' => array(
__( 'Custom plugin name throughout the dashboard', 'version-info' ),
__( 'Custom author name attribution', 'version-info' ),
__( 'Hide Freemius account and support menu items', 'version-info' ),
__( 'Hide in-plugin links to docs.versioninfoplugin.com', 'version-info' ),
__( 'Lock the White Label tab to a single administrator', 'version-info' ),
),
),
'access_control' => array(
'title' => __( 'Access Control', 'version-info' ),
'description' => __( 'Control which WordPress user roles can see version information across your site.', 'version-info' ),
'features' => array(
__( 'Per-role visibility toggles', 'version-info' ),
__( 'Applies to admin bar, footer, and dashboard widget', 'version-info' ),
__( 'Default restricted to administrators', 'version-info' ),
),
),
'error_log' => array(
'title' => __( 'PHP Error Log', 'version-info' ),
'description' => __( 'View and download your PHP error log directly from the WordPress dashboard.', 'version-info' ),
'features' => array(
__( 'Tail last 100 lines without loading full log into memory', 'version-info' ),
__( 'Sensitive file paths automatically masked', 'version-info' ),
__( 'Download full log as ZIP for offline analysis', 'version-info' ),
),
),
);
$preview = isset( $previews[ $tab_id ] ) ? $previews[ $tab_id ] : null;
if ( ! $preview ) {
return;
}
$this->render_locked_placeholder( $preview, 'agency' );
}
/**
* Renders the greyed-out feature preview plus the upgrade notice.
*
* @param array{title: string, description: string, features: string[]} $preview Preview copy for the locked tab.
* @param string $tier Either 'pro' or 'agency'.
*/
private function render_locked_placeholder( array $preview, $tier ) {
if ( 'agency' === $tier ) {
$label = __( 'This is an Agency feature.', 'version-info' );
$message = __( 'Upgrade to the Agency plan to unlock this feature.', 'version-info' );
$button = __( 'Upgrade to Agency', 'version-info' );
} else {
$label = __( 'This is a PRO feature.', 'version-info' );
$message = __( 'Upgrade to Version Info PRO to unlock this feature.', 'version-info' );
$button = __( 'Upgrade to PRO', 'version-info' );
}
echo '';
echo '
';
echo '
' . esc_html( $preview['title'] ) . '
';
echo '
' . esc_html( $preview['description'] ) . '
';
echo '
';
echo '
';
echo '
';
echo '
';
}
}