This commit is contained in:
Javier Casares 2026-08-12 13:18:40 +00:00
commit bc1cb5e00a
32 changed files with 4254 additions and 0 deletions

View file

@ -0,0 +1,157 @@
<?php
/**
* Admin menu and main catalog page.
*
* @package Robotstxt_Manager
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Robotstxt_Manager_Admin
*
* Adds the top-level "ROBOTSTXT Plugins" menu and renders the catalog table
* that lists every plugin from the remote Plugins Core install alongside
* its local install/active/update state.
*/
class Robotstxt_Manager_Admin {
/**
* Admin page and menu slug.
*
* @var string
*/
public const PAGE_SLUG = 'robotstxt-manager';
/**
* Registers all hooks via the loader.
*
* @param Robotstxt_Manager_Loader $loader The plugin hook loader.
*
* @return void
*/
public function register( Robotstxt_Manager_Loader $loader ): void {
$loader->add_action( 'admin_menu', $this, 'add_menu' );
$loader->add_action( 'admin_post_robotstxt_manager_refresh_catalog', $this, 'handle_refresh' );
}
/**
* Adds the top-level admin menu.
*
* @return void
*/
public function add_menu(): void {
add_menu_page(
esc_html__( 'Manager (by ROBOTSTXT) — Plugins', 'robotstxt-manager' ),
esc_html__( 'ROBOTSTXT', 'robotstxt-manager' ),
'manage_options',
self::PAGE_SLUG,
array( $this, 'render_page' ),
'dashicons-screenoptions',
71
);
}
/**
* Renders the main catalog page.
*
* @return void
*/
public function render_page(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-manager' ) );
}
$client = Robotstxt_Manager_Core_Client::from_options();
$catalog = $client->get_catalog();
$local = $this->resolve_local_state( $catalog );
require_once ROBOTSTXT_MANAGER_DIR . 'admin/views/page-catalog.php';
}
/**
* Handles the "Refresh catalog" admin-post action.
*
* Clears the cached catalog response and redirects back to the page.
*
* @return void
*/
public function handle_refresh(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'Insufficient permissions.', 'robotstxt-manager' ) );
}
check_admin_referer( 'robotstxt_manager_refresh_catalog' );
$client = Robotstxt_Manager_Core_Client::from_options();
$client->clear_catalog_cache();
$redirect = add_query_arg(
array(
'page' => self::PAGE_SLUG,
'refreshed' => '1',
),
admin_url( 'admin.php' )
);
wp_safe_redirect( $redirect );
exit;
}
/**
* Resolves local install state for every catalog entry.
*
* Reads the local plugin list once and matches by slug. Does not perform
* any network calls.
*
* @param list<array<string,mixed>> $catalog Catalog entries from Core.
*
* @return array<string, array{installed:bool, active:bool, version:string}> Keyed by slug.
*/
private function resolve_local_state( array $catalog ): array {
if ( ! function_exists( 'get_plugins' ) ) {
require_once ABSPATH . 'wp-admin/includes/plugin.php';
}
$all_plugins = get_plugins();
$local = array();
// Build a slug → file map from local installs.
$slug_to_file = array();
foreach ( $all_plugins as $file => $data ) {
$slug = dirname( $file );
if ( '.' === $slug ) {
$slug = basename( $file, '.php' );
}
$slug_to_file[ $slug ] = $file;
}
foreach ( $catalog as $entry ) {
$raw_slug = $entry['slug'] ?? '';
$slug = is_string( $raw_slug ) ? $raw_slug : '';
if ( '' === $slug ) {
continue;
}
$file = $slug_to_file[ $slug ] ?? null;
$version = '';
if ( null !== $file && isset( $all_plugins[ $file ]['Version'] ) ) {
$raw_ver = $all_plugins[ $file ]['Version'];
$version = is_string( $raw_ver ) ? $raw_ver : '';
}
$active = null !== $file && is_string( $file ) && is_plugin_active( $file );
$local[ $slug ] = array(
'installed' => null !== $file,
'active' => $active,
'version' => $version,
);
}
return $local;
}
}

View file

@ -0,0 +1,402 @@
<?php
/**
* Admin settings page: API key, store URL, cache TTL, connection test.
*
* @package Robotstxt_Manager
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Class Robotstxt_Manager_Settings
*
* Registers the plugin settings page and all setting fields using the
* WordPress Settings API. The API key is stored encrypted via
* Robotstxt_Manager_Encryption and displayed masked in the UI. Provides an
* AJAX handler for the "Test connection" button.
*/
class Robotstxt_Manager_Settings {
/**
* Admin page and menu slug.
*
* @var string
*/
public const PAGE_SLUG = 'robotstxt-manager-settings';
/**
* Settings API option group.
*
* @var string
*/
public const OPTION_GROUP = 'robotstxt_manager_settings_group';
/**
* Registers all hooks via the loader.
*
* @param Robotstxt_Manager_Loader $loader The plugin hook loader.
*
* @return void
*/
public function register( Robotstxt_Manager_Loader $loader ): void {
$loader->add_action( 'admin_menu', $this, 'add_settings_page' );
$loader->add_action( 'admin_init', $this, 'register_settings' );
$loader->add_action( 'admin_enqueue_scripts', $this, 'enqueue_scripts' );
$loader->add_action( 'wp_ajax_robotstxt_manager_test_connection', $this, 'handle_test_connection' );
$loader->add_action( 'wp_ajax_robotstxt_manager_delete_key', $this, 'handle_delete_key' );
}
/**
* Adds the Settings submenu under the Manager top-level menu.
*
* @return void
*/
public function add_settings_page(): void {
add_submenu_page(
Robotstxt_Manager_Admin::PAGE_SLUG,
esc_html__( 'Manager (by ROBOTSTXT) — Settings', 'robotstxt-manager' ),
esc_html__( 'Settings', 'robotstxt-manager' ),
'manage_options',
self::PAGE_SLUG,
array( $this, 'render_page' )
);
}
/**
* Registers settings, sections, and fields with the Settings API.
*
* @return void
*/
public function register_settings(): void {
add_settings_section(
'robotstxt_manager_connection_section',
esc_html__( 'Connection', 'robotstxt-manager' ),
'__return_false',
self::PAGE_SLUG
);
register_setting(
self::OPTION_GROUP,
'robotstxt_manager_store_url',
array(
'type' => 'string',
'sanitize_callback' => 'esc_url_raw',
'default' => 'https://plugins.robotstxt.es',
)
);
add_settings_field(
'robotstxt_manager_store_url',
esc_html__( 'Store URL', 'robotstxt-manager' ),
array( $this, 'render_field_store_url' ),
self::PAGE_SLUG,
'robotstxt_manager_connection_section'
);
register_setting(
self::OPTION_GROUP,
'robotstxt_manager_api_key',
array(
'type' => 'string',
'sanitize_callback' => array( $this, 'sanitize_api_key' ),
'default' => '',
)
);
add_settings_field(
'robotstxt_manager_api_key',
esc_html__( 'API Key', 'robotstxt-manager' ),
array( $this, 'render_field_api_key' ),
self::PAGE_SLUG,
'robotstxt_manager_connection_section'
);
add_settings_section(
'robotstxt_manager_cache_section',
esc_html__( 'Cache', 'robotstxt-manager' ),
'__return_false',
self::PAGE_SLUG
);
register_setting(
self::OPTION_GROUP,
'robotstxt_manager_cache_ttl_minutes',
array(
'type' => 'integer',
'sanitize_callback' => array( $this, 'sanitize_cache_ttl' ),
'default' => 60,
)
);
add_settings_field(
'robotstxt_manager_cache_ttl_minutes',
esc_html__( 'Catalog Cache (minutes)', 'robotstxt-manager' ),
array( $this, 'render_field_cache_ttl' ),
self::PAGE_SLUG,
'robotstxt_manager_cache_section'
);
register_setting(
self::OPTION_GROUP,
'robotstxt_manager_delete_data_on_uninstall',
array(
'type' => 'boolean',
'sanitize_callback' => 'rest_sanitize_boolean',
'default' => false,
)
);
add_settings_field(
'robotstxt_manager_delete_data_on_uninstall',
esc_html__( 'Data on Uninstall', 'robotstxt-manager' ),
array( $this, 'render_field_delete_on_uninstall' ),
self::PAGE_SLUG,
'robotstxt_manager_cache_section'
);
}
/**
* Enqueues the settings page JS on the correct screen.
*
* @param string $hook_suffix The current admin page hook suffix.
*
* @return void
*/
public function enqueue_scripts( string $hook_suffix ): void {
if ( ! str_contains( $hook_suffix, self::PAGE_SLUG ) ) {
return;
}
wp_enqueue_script(
'robotstxt-manager-settings',
ROBOTSTXT_MANAGER_URL . 'admin/js/robotstxt-manager-settings.js',
array( 'jquery' ),
ROBOTSTXT_MANAGER_VERSION,
true
);
wp_localize_script(
'robotstxt-manager-settings',
'RobotstxtManagerSettings',
array(
'ajaxUrl' => admin_url( 'admin-ajax.php' ),
'nonce' => wp_create_nonce( 'robotstxt_manager_test_connection' ),
'deleteNonce' => wp_create_nonce( 'robotstxt_manager_delete_key' ),
'i18n' => array(
'testing' => __( 'Testing…', 'robotstxt-manager' ),
'testLabel' => __( 'Test connection', 'robotstxt-manager' ),
'deleting' => __( 'Deleting…', 'robotstxt-manager' ),
'deleteLabel' => __( 'Delete API key', 'robotstxt-manager' ),
'deleteConfirm' => __( 'Delete the stored API key? The catalog and subscription data will stop working until a new key is entered.', 'robotstxt-manager' ),
'deleted' => __( 'API key deleted. Save changes to persist.', 'robotstxt-manager' ),
'ajaxError' => __( 'An unexpected error occurred.', 'robotstxt-manager' ),
),
)
);
}
/**
* Renders the settings page.
*
* @return void
*/
public function render_page(): void {
if ( ! current_user_can( 'manage_options' ) ) {
wp_die( esc_html__( 'You do not have sufficient permissions to access this page.', 'robotstxt-manager' ) );
}
require_once ROBOTSTXT_MANAGER_DIR . 'admin/views/page-settings.php';
}
/**
* Renders the Store URL field.
*
* @return void
*/
public function render_field_store_url(): void {
$raw = get_option( 'robotstxt_manager_store_url', 'https://plugins.robotstxt.es' );
$value = is_string( $raw ) ? $raw : 'https://plugins.robotstxt.es';
printf(
'<input type="url" id="robotstxt_manager_store_url" name="robotstxt_manager_store_url" value="%s" class="regular-text" maxlength="500" placeholder="https://plugins.robotstxt.es" />',
esc_attr( $value )
);
echo '<p class="description">' . esc_html__( 'Base URL of the remote Plugins Core installation that this site will pull the plugin catalog from.', 'robotstxt-manager' ) . '</p>';
}
/**
* Renders the API key field. Always empty on render the stored value
* is never sent back to the browser. The masked last 4 chars of the
* stored key are shown as a hint.
*
* @return void
*/
public function render_field_api_key(): void {
$stored = get_option( 'robotstxt_manager_api_key', '' );
$has_key = is_string( $stored ) && '' !== $stored;
$last4 = '';
if ( $has_key ) {
$plain = Robotstxt_Manager_Encryption::decrypt( is_string( $stored ) ? $stored : '' );
// Only show last-4 if decryption produced a plausible key (36-char UUID).
if ( '' !== $plain && 36 === strlen( $plain ) ) {
$last4 = substr( $plain, -4 );
}
}
echo '<input type="password" id="robotstxt_manager_api_key" name="robotstxt_manager_api_key" value="" class="regular-text" autocomplete="new-password" />';
if ( $has_key ) {
echo '<p class="description">';
if ( '' !== $last4 ) {
echo wp_kses_post(
sprintf(
/* translators: %s: last 4 characters of the stored API key. */
__( 'A key is stored (last 4 characters: <code>%s</code>). Leave blank to keep the existing key; enter a new value to replace it.', 'robotstxt-manager' ),
esc_html( $last4 )
)
);
} else {
esc_html_e( 'A key is stored but could not be decoded. You can replace it by entering a new value above, or delete it with the button below.', 'robotstxt-manager' );
}
echo '</p>';
} else {
echo '<p class="description">' . esc_html__( 'Account-level API key issued by the ROBOTSTXT store. Required to authenticate catalog and subscription requests. The value is encrypted before storage.', 'robotstxt-manager' ) . '</p>';
}
// Action buttons.
echo '<p>';
// Test connection: disabled when no key is stored.
$test_disabled = $has_key ? '' : ' disabled';
printf( '<button type="button" id="robotstxt-manager-test-connection" class="button"%s>%s</button>', esc_attr( $test_disabled ), esc_html__( 'Test connection', 'robotstxt-manager' ) );
// Delete stored key: only shown when a key exists.
if ( $has_key ) {
echo ' <button type="button" id="robotstxt-manager-delete-key" class="button button-link-delete">' . esc_html__( 'Delete API key', 'robotstxt-manager' ) . '</button>';
}
echo '<span id="robotstxt-manager-test-result" style="margin-left:8px"></span>';
echo '</p>';
}
/**
* Renders the cache TTL field.
*
* @return void
*/
public function render_field_cache_ttl(): void {
$raw = get_option( 'robotstxt_manager_cache_ttl_minutes', 60 );
$value = is_numeric( $raw ) ? (int) $raw : 60;
printf(
'<input type="number" id="robotstxt_manager_cache_ttl_minutes" name="robotstxt_manager_cache_ttl_minutes" value="%d" class="small-text" min="1" max="1440" />',
absint( $value )
);
echo '<p class="description">' . esc_html__( 'How long the catalog response from Core is cached in a transient. Default: 60 minutes. Lower values refresh more often at the cost of more requests to Core. Maximum: 1440 (24 hours).', 'robotstxt-manager' ) . '</p>';
}
/**
* Renders the delete-on-uninstall checkbox.
*
* @return void
*/
public function render_field_delete_on_uninstall(): void {
$value = (bool) get_option( 'robotstxt_manager_delete_data_on_uninstall', false );
echo '<label>';
printf(
'<input type="checkbox" id="robotstxt_manager_delete_data_on_uninstall" name="robotstxt_manager_delete_data_on_uninstall" value="1"%s />',
checked( $value, true, false )
);
echo ' ' . esc_html__( 'Delete all plugin data when the plugin is uninstalled.', 'robotstxt-manager' );
echo '</label>';
}
/**
* Sanitizes the API key field.
*
* Empty input preserves the existing encrypted value; non-empty input is
* encrypted before storage. The catalog transient is cleared whenever
* the key changes so the next page load fetches fresh data.
*
* @param mixed $value Raw submitted value.
*
* @return string Encrypted key, or existing value if input is empty.
*/
public function sanitize_api_key( mixed $value ): string {
$plain = sanitize_text_field( is_string( $value ) ? $value : '' );
if ( '' === $plain ) {
$raw = get_option( 'robotstxt_manager_api_key', '' );
return is_string( $raw ) ? $raw : '';
}
delete_transient( 'robotstxt_manager_catalog' );
return Robotstxt_Manager_Encryption::encrypt( $plain );
}
/**
* Sanitizes the cache TTL value.
*
* @param mixed $value Raw submitted value.
*
* @return int Clamped to 1-1440 minutes.
*/
public function sanitize_cache_ttl( mixed $value ): int {
return max( 1, min( 1440, absint( is_scalar( $value ) ? $value : 0 ) ) );
}
/**
* Handles the "Test connection" AJAX request.
*
* Reads the current Store URL + API key from options, attempts a GET
* /plugins against Core, and returns success/failure with a message.
*
* @return void
*/
public function handle_test_connection(): void {
check_ajax_referer( 'robotstxt_manager_test_connection', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'robotstxt-manager' ) ) );
}
$client = Robotstxt_Manager_Core_Client::from_options();
$result = $client->test_connection();
if ( $result['ok'] ) {
wp_send_json_success( array( 'message' => $result['message'] ) );
}
wp_send_json_error( array( 'message' => $result['message'] ) );
}
/**
* Handles the "Delete API key" AJAX request.
*
* Removes the encrypted API key from wp_options. The change takes effect
* immediately subsequent catalog fetches and connection tests will fail
* until a new key is entered and saved.
*
* @return void
*/
public function handle_delete_key(): void {
check_ajax_referer( 'robotstxt_manager_delete_key', 'nonce' );
if ( ! current_user_can( 'manage_options' ) ) {
wp_send_json_error( array( 'message' => __( 'Insufficient permissions.', 'robotstxt-manager' ) ) );
}
delete_option( 'robotstxt_manager_api_key' );
delete_transient( 'robotstxt_manager_catalog' );
wp_send_json_success(
array(
'message' => __( 'API key deleted. Save changes to persist.', 'robotstxt-manager' ),
)
);
}
}

View file

@ -0,0 +1,89 @@
/**
* Settings page JS "Test connection" and "Delete API key" handlers.
*
* @global RobotstxtManagerSettings
* @global jQuery
* @package Robotstxt_Manager
*/
( function ( $ ) {
'use strict';
// Test connection.
$( document ).on(
'click',
'#robotstxt-manager-test-connection',
function () {
var $btn = $( this );
var $result = $( '#robotstxt-manager-test-result' );
$btn.prop( 'disabled', true ).text( RobotstxtManagerSettings.i18n.testing );
$result.text( '' ).css( 'color', '' );
$.post(
RobotstxtManagerSettings.ajaxUrl,
{
action: 'robotstxt_manager_test_connection',
nonce: RobotstxtManagerSettings.nonce,
},
function ( response ) {
if ( response.success ) {
$result.text( response.data.message ).css( 'color', '#00a32a' );
} else {
$result.text( response.data.message ).css( 'color', '#d63638' );
}
}
).fail(
function () {
$result.text( RobotstxtManagerSettings.i18n.ajaxError ).css( 'color', '#d63638' );
}
).always(
function () {
$btn.prop( 'disabled', false ).text( RobotstxtManagerSettings.i18n.testLabel );
}
);
}
);
// Delete API key.
$( document ).on(
'click',
'#robotstxt-manager-delete-key',
function () {
var $btn = $( this );
var $result = $( '#robotstxt-manager-test-result' );
if ( ! window.confirm( RobotstxtManagerSettings.i18n.deleteConfirm ) ) { /* eslint-disable-line no-alert */
return;
}
$btn.prop( 'disabled', true ).text( RobotstxtManagerSettings.i18n.deleting );
$result.text( '' ).css( 'color', '' );
$.post(
RobotstxtManagerSettings.ajaxUrl,
{
action: 'robotstxt_manager_delete_key',
nonce: RobotstxtManagerSettings.deleteNonce,
},
function ( response ) {
if ( response.success ) {
$result.text( RobotstxtManagerSettings.i18n.deleted ).css( 'color', '#00a32a' );
// Hide the delete button + test button; update description.
$btn.hide();
$( '#robotstxt-manager-test-connection' ).prop( 'disabled', true );
$( '#robotstxt_manager_api_key' ).next( '.description' ).html( RobotstxtManagerSettings.i18n.deleted );
} else {
$result.text( response.data.message ).css( 'color', '#d63638' );
$btn.prop( 'disabled', false ).text( RobotstxtManagerSettings.i18n.deleteLabel );
}
}
).fail(
function () {
$result.text( RobotstxtManagerSettings.i18n.ajaxError ).css( 'color', '#d63638' );
$btn.prop( 'disabled', false ).text( RobotstxtManagerSettings.i18n.deleteLabel );
}
);
}
);
}( jQuery ) );

View file

@ -0,0 +1,240 @@
<?php
/**
* Admin page view: ROBOTSTXT Plugins catalog.
*
* Available variables:
* $catalog list<array<string,mixed>> Catalog entries from Core.
* $local array<string, array{installed:bool, active:bool, version:string}>
* Local install state, keyed by slug.
*
* @package Robotstxt_Manager
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
/**
* Variables provided by Robotstxt_Manager_Admin::render_page().
*
* @var list<array<string,mixed>> $catalog
* @var array<string, array{installed:bool, active:bool, version:string}> $local
*/
$client_configured = Robotstxt_Manager_Core_Client::from_options()->is_configured();
$refresh_url = add_query_arg(
array(
'action' => 'robotstxt_manager_refresh_catalog',
),
admin_url( 'admin-post.php' )
);
$refresh_url = wp_nonce_url( $refresh_url, 'robotstxt_manager_refresh_catalog' );
$refreshed = isset( $_GET['refreshed'] ) && '1' === $_GET['refreshed']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$local_wp_version = (string) get_bloginfo( 'version' );
$local_php_version = (string) PHP_VERSION;
$compat_warnings = 0;
?>
<div class="wrap">
<h1><?php esc_html_e( 'Manager (by ROBOTSTXT) — Plugins', 'robotstxt-manager' ); ?></h1>
<?php if ( $refreshed ) : ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Catalog refreshed.', 'robotstxt-manager' ); ?></p></div>
<?php endif; ?>
<?php if ( ! $client_configured ) : ?>
<div class="notice notice-error">
<p>
<?php
echo wp_kses_post(
sprintf(
/* translators: %s: settings URL. */
__( 'ROBOTSTXT Manager is not configured yet. <a href="%s">Set the Store URL and API key</a> to see your plugin catalog.', 'robotstxt-manager' ),
esc_url( admin_url( 'admin.php?page=' . Robotstxt_Manager_Settings::PAGE_SLUG ) )
)
);
?>
</p>
</div>
<?php elseif ( empty( $catalog ) ) : ?>
<div class="notice notice-warning">
<p>
<?php esc_html_e( 'No plugins were returned by the ROBOTSTXT store. Check the Store URL and API key on the Settings page, or click Refresh to try again.', 'robotstxt-manager' ); ?>
</p>
</div>
<p><a class="button" href="<?php echo esc_url( $refresh_url ); ?>"><?php esc_html_e( 'Refresh catalog', 'robotstxt-manager' ); ?></a></p>
<?php else : ?>
<p>
<a class="button" href="<?php echo esc_url( $refresh_url ); ?>"><?php esc_html_e( 'Refresh catalog', 'robotstxt-manager' ); ?></a>
</p>
<table class="wp-list-table widefat striped robotstxt-manager-catalog">
<thead>
<tr>
<th scope="col"><?php esc_html_e( 'Plugin', 'robotstxt-manager' ); ?></th>
<th scope="col"><?php esc_html_e( 'Type', 'robotstxt-manager' ); ?></th>
<th scope="col"><?php esc_html_e( 'Price', 'robotstxt-manager' ); ?></th>
<th scope="col"><?php esc_html_e( 'Requires WP', 'robotstxt-manager' ); ?></th>
<th scope="col"><?php esc_html_e( 'Requires PHP', 'robotstxt-manager' ); ?></th>
<th scope="col"><?php esc_html_e( 'Local state', 'robotstxt-manager' ); ?></th>
<th scope="col"><?php esc_html_e( 'Action', 'robotstxt-manager' ); ?></th>
</tr>
</thead>
<tbody>
<?php foreach ( $catalog as $entry ) : ?>
<?php
$raw_slug = $entry['slug'] ?? '';
$raw_name = $entry['name'] ?? '';
$raw_kind = $entry['type'] ?? 'free';
$raw_cost = $entry['price_annual'] ?? 0;
$raw_ver = $entry['current_version'] ?? '';
$raw_url = $entry['page_url'] ?? '';
$raw_rwp = $entry['requires_wp'] ?? '';
$raw_rphp = $entry['requires_php'] ?? '';
$slug = is_string( $raw_slug ) ? $raw_slug : '';
$name = is_string( $raw_name ) ? $raw_name : $slug;
$kind = is_string( $raw_kind ) ? $raw_kind : 'free';
$cost = is_numeric( $raw_cost ) ? (float) ( $raw_cost + 0.0 ) : 0.0;
$remote_v = is_string( $raw_ver ) ? $raw_ver : '';
$page_url = is_string( $raw_url ) ? $raw_url : '';
$req_wp = is_string( $raw_rwp ) ? $raw_rwp : '';
$req_php = is_string( $raw_rphp ) ? $raw_rphp : '';
// Compatibility checks.
$wp_ok = '' === $req_wp || version_compare( $local_wp_version, $req_wp, '>=' );
$php_ok = '' === $req_php || version_compare( $local_php_version, $req_php, '>=' );
if ( ! $wp_ok || ! $php_ok ) {
++$compat_warnings;
}
$raw_state = $local[ $slug ] ?? array(
'installed' => false,
'active' => false,
'version' => '',
);
$l_installed = (bool) ( $raw_state['installed'] ?? false );
$l_active = (bool) ( $raw_state['active'] ?? false );
$raw_lv = $raw_state['version'] ?? '';
$l_version = is_string( $raw_lv ) ? $raw_lv : '';
$update_available = $l_installed && '' !== $remote_v && '' !== $l_version
&& version_compare( $l_version, $remote_v, '<' );
$row_class = ( ! $wp_ok || ! $php_ok ) ? ' robotstxt-manager-row--incompatible' : '';
?>
<tr class="<?php echo esc_attr( ltrim( $row_class ) ); ?>">
<td>
<strong><?php echo esc_html( $name ); ?></strong>
<?php if ( '' !== $remote_v ) : ?>
<br /><span class="description"><?php echo esc_html( sprintf( 'v%s', $remote_v ) ); ?></span>
<?php endif; ?>
</td>
<td><?php echo esc_html( 'premium' === $kind ? __( 'Premium', 'robotstxt-manager' ) : __( 'Free', 'robotstxt-manager' ) ); ?></td>
<td>
<?php
if ( $cost > 0.0 ) {
echo esc_html( sprintf( '€%s / year', number_format_i18n( $cost, 0 ) ) );
} else {
echo '&mdash;';
}
?>
</td>
<td>
<?php if ( '' !== $req_wp ) : ?>
<span class="<?php echo $wp_ok ? 'robotstxt-manager-compat--ok' : 'robotstxt-manager-compat--fail'; ?>">
<?php echo esc_html( $req_wp ); ?>
<?php if ( ! $wp_ok ) : ?>
<span class="robotstxt-manager-compat-warning" title="<?php esc_attr_e( 'Your site runs WordPress', 'robotstxt-manager' ); ?> <?php echo esc_attr( $local_wp_version ); ?>"> &#9888;</span>
<?php endif; ?>
</span>
<?php else : ?>
&mdash;
<?php endif; ?>
</td>
<td>
<?php if ( '' !== $req_php ) : ?>
<span class="<?php echo $php_ok ? 'robotstxt-manager-compat--ok' : 'robotstxt-manager-compat--fail'; ?>">
<?php echo esc_html( $req_php ); ?>
<?php if ( ! $php_ok ) : ?>
<span class="robotstxt-manager-compat-warning" title="<?php esc_attr_e( 'Your server runs PHP', 'robotstxt-manager' ); ?> <?php echo esc_attr( $local_php_version ); ?>"> &#9888;</span>
<?php endif; ?>
</span>
<?php else : ?>
&mdash;
<?php endif; ?>
</td>
<td>
<?php
if ( ! $l_installed ) {
echo '<span class="robotstxt-manager-state robotstxt-manager-state--not-installed">' . esc_html__( 'Not installed', 'robotstxt-manager' ) . '</span>';
} elseif ( ! $l_active ) {
echo '<span class="robotstxt-manager-state robotstxt-manager-state--inactive">' . esc_html__( 'Installed (inactive)', 'robotstxt-manager' ) . '</span>';
} elseif ( $update_available ) {
echo '<span class="robotstxt-manager-state robotstxt-manager-state--update">' . esc_html(
sprintf(
/* translators: 1: installed version, 2: available version. */
__( 'Update available (v%1$s → v%2$s)', 'robotstxt-manager' ),
$l_version,
$remote_v
)
) . '</span>';
} else {
echo '<span class="robotstxt-manager-state robotstxt-manager-state--ok">' . esc_html__( 'Up to date', 'robotstxt-manager' ) . '</span>';
}
?>
</td>
<td>
<?php if ( ! $l_installed ) : ?>
<?php if ( ! $wp_ok || ! $php_ok ) : ?>
<span class="description"><?php esc_html_e( 'Incompatible', 'robotstxt-manager' ); ?></span>
<?php elseif ( $cost > 0.0 && '' !== $page_url ) : ?>
<a class="button button-primary" href="<?php echo esc_url( $page_url ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Buy', 'robotstxt-manager' ); ?></a>
<?php elseif ( $cost <= 0.0 ) : ?>
<button type="button" class="button button-secondary robotstxt-manager-action-install" data-slug="<?php echo esc_attr( $slug ); ?>" disabled><?php esc_html_e( 'Install (soon)', 'robotstxt-manager' ); ?></button>
<?php endif; ?>
<?php elseif ( $update_available ) : ?>
<button type="button" class="button button-secondary robotstxt-manager-action-update" data-slug="<?php echo esc_attr( $slug ); ?>" disabled><?php esc_html_e( 'Update (soon)', 'robotstxt-manager' ); ?></button>
<?php endif; ?>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php if ( $compat_warnings > 0 ) : ?>
<div class="notice notice-warning inline" style="margin-top:1em">
<p>
<?php
echo esc_html(
sprintf(
/* translators: %d: number of incompatible plugins. */
_n( '%d plugin in the catalog is not compatible with this site\'s WordPress or PHP version.', '%d plugins in the catalog are not compatible with this site\'s WordPress or PHP version.', $compat_warnings, 'robotstxt-manager' ),
$compat_warnings
)
);
?>
<?php
echo esc_html(
sprintf(
/* translators: 1: local WP version, 2: local PHP version. */
__( 'This site runs WordPress %1$s on PHP %2$s.', 'robotstxt-manager' ),
$local_wp_version,
$local_php_version
)
);
?>
</p>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
<style>
.robotstxt-manager-compat--ok { color: #00a32a; }
.robotstxt-manager-compat--fail { color: #d63638; font-weight: 600; }
.robotstxt-manager-compat-warning { cursor: help; }
.robotstxt-manager-row--incompatible { background-color: #fef7f0 !important; }
</style>

View file

@ -0,0 +1,21 @@
<?php
/**
* Admin page view: ROBOTSTXT Manager settings.
*
* @package Robotstxt_Manager
*/
if ( ! defined( 'ABSPATH' ) ) {
exit;
}
?>
<div class="wrap">
<h1><?php esc_html_e( 'Manager (by ROBOTSTXT) — Settings', 'robotstxt-manager' ); ?></h1>
<form method="post" action="<?php echo esc_url( admin_url( 'options.php' ) ); ?>">
<?php
settings_fields( Robotstxt_Manager_Settings::OPTION_GROUP );
do_settings_sections( Robotstxt_Manager_Settings::PAGE_SLUG );
submit_button();
?>
</form>
</div>