robotstxt-manager/admin/views/page-catalog.php
2026-08-17 16:06:40 +00:00

437 lines
19 KiB
PHP

<?php
/**
* Admin page view: ROBOTSTXT Plugins catalog.
*
* Catalog table (Plugin · Version · Requires WP · Requires PHP · Price ·
* Action · Status) with an always-open detail row under each plugin
* (website link + description), an intro paragraph, and Support /
* Payments sections below the table.
*
* Also available:
* $manager_subscriptions array<string, array{status:string, expires_at:string}>
* Account subscriptions keyed by slug (may be empty).
*
* @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
* @var array<string, array<string,mixed>> $manager_subscriptions
* @var list<array{type:string, message:string}> $manager_notices
* @var bool $manager_has_api_key
* @var string $manager_store_url
*/
$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
// Action notice from an install/activate/update redirect.
$notice_result = '';
$notice_message = '';
if ( isset( $_GET['robotstxt_manager_result'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only, set by this plugin's own redirects.
$raw_result = wp_unslash( $_GET['robotstxt_manager_result'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized on the next line.
if ( is_string( $raw_result ) ) {
$notice_result = sanitize_key( $raw_result );
}
}
if ( isset( $_GET['robotstxt_manager_message'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- display-only, set by this plugin's own redirects.
$raw_message = wp_unslash( $_GET['robotstxt_manager_message'] ); // phpcs:ignore WordPress.Security.NonceVerification.Recommended,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized on the next line after decoding.
if ( is_string( $raw_message ) ) {
$notice_message = sanitize_text_field( rawurldecode( $raw_message ) );
}
}
$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>
<p class="description">
<?php esc_html_e( 'This is the ROBOTSTXT plugin store: the catalog of plugins published by ROBOTSTXT, installable and updatable straight from your own wp-admin. Free plugins install with one click; premium plugins require an annual subscription that you purchase on our website.', 'robotstxt-manager' ); ?>
</p>
<?php if ( $refreshed ) : ?>
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Catalog refreshed.', 'robotstxt-manager' ); ?></p></div>
<?php endif; ?>
<?php foreach ( ( $manager_notices ?? array() ) as $manager_notice ) : ?>
<div class="notice notice-<?php echo esc_attr( is_string( $manager_notice['type'] ?? '' ) ? $manager_notice['type'] : 'warning' ); ?>">
<p><?php echo esc_html( is_string( $manager_notice['message'] ?? '' ) ? $manager_notice['message'] : '' ); ?></p>
</div>
<?php endforeach; ?>
<?php if ( empty( $manager_has_api_key ) && '' !== ( $manager_store_url ?? '' ) ) : ?>
<div class="notice notice-info">
<p>
<?php
echo wp_kses_post(
sprintf(
/* translators: %s: store registration URL. */
__( 'Create your free account at the <a href="%s" target="_blank" rel="noopener noreferrer">ROBOTSTXT store</a> to get your personal API key. The key links your subscriptions to this site and unlocks premium plugins and updates.', 'robotstxt-manager' ),
esc_url( $manager_store_url . '/wp-login.php?action=register' )
)
);
?>
</p>
</div>
<?php endif; ?>
<?php if ( '' !== $notice_message && in_array( $notice_result, array( 'success', 'error' ), true ) ) : ?>
<div class="notice notice-<?php echo esc_attr( $notice_result ); ?> is-dismissible"><p><?php echo esc_html( $notice_message ); ?></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( 'Version', '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( 'Price', 'robotstxt-manager' ); ?></th>
<th scope="col"><?php esc_html_e( 'Action', 'robotstxt-manager' ); ?></th>
<th scope="col"><?php esc_html_e( 'Status', '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_home = $entry['homepage'] ?? '';
$raw_desc = $entry['description'] ?? '';
$raw_rwp = $entry['requires_wp'] ?? '';
$raw_rphp = $entry['requires_php'] ?? '';
$raw_icon = $entry['icon_url'] ?? '';
$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 : '';
$homepage = is_string( $raw_home ) ? $raw_home : '';
$desc = is_string( $raw_desc ) ? trim( $raw_desc ) : '';
$req_wp = is_string( $raw_rwp ) ? $raw_rwp : '';
$req_php = is_string( $raw_rphp ) ? $raw_rphp : '';
$icon_url = is_string( $raw_icon ) ? esc_url_raw( $raw_icon ) : '';
$website_url = '' !== $page_url ? $page_url : $homepage;
$has_detail = ( '' !== $website_url || '' !== $desc );
// Localised description: the admin user's locale first,
// falling back to the (English) default.
$raw_translations = $entry['description_translations'] ?? '';
$translations = is_array( $raw_translations ) ? $raw_translations : array();
$user_locale = function_exists( 'get_user_locale' ) ? get_user_locale() : get_locale();
$locale_text = $translations[ $user_locale ] ?? '';
$display_desc = is_string( $locale_text ) && '' !== trim( $locale_text )
? trim( $locale_text )
: $desc;
$has_detail = ( '' !== $website_url || '' !== $desc || '' !== $display_desc );
// Subscription state: premium plugins are only usable
// (install/update) with an active or in-grace subscription.
$is_premium = ( $cost > 0.0 || 'premium' === $kind );
$sub_row = $manager_subscriptions[ $slug ] ?? null;
$raw_status = is_array( $sub_row ) ? ( $sub_row['status'] ?? '' ) : '';
$sub_status = is_string( $raw_status ) ? $raw_status : '';
$subscribed = in_array( $sub_status, array( 'active', 'payment_failed' ), true );
$raw_reqs = $entry['requires_plugins'] ?? '';
$req_slugs = array();
if ( is_string( $raw_reqs ) && '' !== trim( $raw_reqs ) ) {
foreach ( explode( ',', $raw_reqs ) as $part ) {
$dep = sanitize_key( trim( $part ) );
if ( '' !== $dep ) {
$req_slugs[] = $dep;
}
}
}
$has_detail = $has_detail || array() !== $req_slugs;
// 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 class="robotstxt-manager-name-cell">
<?php if ( '' !== $icon_url ) : ?>
<img class="robotstxt-manager-plugin-icon" src="<?php echo esc_url( $icon_url ); ?>" alt="" width="64" height="64" loading="lazy" />
<?php else : ?>
<span class="robotstxt-manager-plugin-icon robotstxt-manager-plugin-icon--placeholder" aria-hidden="true"></span>
<?php endif; ?>
<strong><?php echo esc_html( $name ); ?></strong>
</td>
<td><?php echo '' !== $remote_v ? esc_html( $remote_v ) : '&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 ( $is_premium ) {
echo esc_html(
sprintf(
/* translators: %s: formatted price number. */
__( '€%s / year', 'robotstxt-manager' ),
number_format_i18n( $cost, 0 )
)
);
// Subscription pill for premium plugins.
$sub_text = '';
if ( is_array( $sub_row ) ) {
$raw_sub_expiry = $sub_row['expires_at'] ?? '';
$sub_expiry = is_string( $raw_sub_expiry ) ? $raw_sub_expiry : '';
if ( 'active' === $sub_status && '' !== $sub_expiry ) {
$days_left = (int) floor( ( (int) strtotime( $sub_expiry ) - time() ) / DAY_IN_SECONDS );
if ( $days_left >= 0 && $days_left <= 14 ) {
$sub_text = sprintf(
/* translators: %d: days remaining. */
__( 'Subscribed — %d days left', 'robotstxt-manager' ),
$days_left
);
} else {
$sub_text = __( 'Subscribed', 'robotstxt-manager' );
}
} elseif ( 'payment_failed' === $sub_status ) {
$sub_text = __( 'Payment failed', 'robotstxt-manager' );
} elseif ( 'cancelled' === $sub_status ) {
$sub_text = __( 'Cancelled', 'robotstxt-manager' );
} elseif ( 'expired' === $sub_status ) {
$sub_text = __( 'Expired', 'robotstxt-manager' );
}
}
if ( '' !== $sub_text ) {
echo '<br /><span class="robotstxt-manager-subscription robotstxt-manager-subscription--'
. esc_attr( $sub_status )
. '">' . esc_html( $sub_text ) . '</span>';
}
} else {
echo esc_html__( 'Free', 'robotstxt-manager' );
}
?>
</td>
<td>
<?php if ( ! $wp_ok || ! $php_ok ) : ?>
<span class="description"><?php esc_html_e( 'Incompatible', 'robotstxt-manager' ); ?></span>
<?php elseif ( $is_premium && ! $subscribed ) : ?>
<?php if ( '' !== $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 else : ?>
&mdash;
<?php endif; ?>
<?php elseif ( ! $l_installed ) : ?>
<a class="button button-primary" href="<?php echo esc_url( Robotstxt_Manager_Admin::action_url( 'install', $slug ) ); ?>"><?php esc_html_e( 'Install', 'robotstxt-manager' ); ?></a>
<?php elseif ( ! $l_active ) : ?>
<a class="button button-primary" href="<?php echo esc_url( Robotstxt_Manager_Admin::action_url( 'activate', $slug ) ); ?>"><?php esc_html_e( 'Activate', 'robotstxt-manager' ); ?></a>
<?php elseif ( $update_available ) : ?>
<a class="button button-secondary" href="<?php echo esc_url( Robotstxt_Manager_Admin::action_url( 'update', $slug ) ); ?>"><?php esc_html_e( 'Update', 'robotstxt-manager' ); ?></a>
<?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>
</tr>
<?php if ( $has_detail ) : ?>
<tr class="robotstxt-manager-detail-row">
<td colspan="7">
<?php if ( '' !== $website_url ) : ?>
<a href="<?php echo esc_url( $website_url ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Visit website', 'robotstxt-manager' ); ?> <span class="screen-reader-text"><?php echo esc_html( sprintf( /* translators: %s: plugin name. */ __( '(about %s)', 'robotstxt-manager' ), $name ) ); ?></span></a>
<?php endif; ?>
<?php if ( '' !== $display_desc ) : ?>
<span class="robotstxt-manager-detail-description"><?php echo esc_html( $display_desc ); ?></span>
<?php endif; ?>
<?php if ( array() !== $req_slugs ) : ?>
<span class="robotstxt-manager-detail-requires">
<?php
echo esc_html(
sprintf(
/* translators: %s: list of plugin slugs. */
__( 'Requires: %s', 'robotstxt-manager' ),
implode( ', ', $req_slugs )
)
);
?>
</span>
<?php endif; ?>
</td>
</tr>
<?php endif; ?>
<?php endforeach; ?>
</tbody>
</table>
<h2><?php esc_html_e( 'Support', 'robotstxt-manager' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Need help with a ROBOTSTXT plugin? Visit the plugin\'s website (the link in its row above) for documentation and guides, or contact us through our website — we are happy to help.', 'robotstxt-manager' ); ?>
</p>
<h2><?php esc_html_e( 'Payments, and how it works', 'robotstxt-manager' ); ?></h2>
<p class="description">
<?php esc_html_e( 'Free plugins install instantly at no cost. Premium plugins are annual subscriptions: you pay once on our website and the subscription renews automatically every year until you cancel. You can cancel at any time from your ROBOTSTXT account page — access keeps working until the end of the paid period. All payments are processed securely by Mollie; we never see or store your card details.', 'robotstxt-manager' ); ?>
</p>
<?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; }
/* Detail rows (always open): muted, attached to the row above. */
.robotstxt-manager-detail-row td { border-top: none !important; padding-top: 0; }
.robotstxt-manager-detail-description { color: #50575e; }
/* Plugin icons: 64x64, 1:1, beside the name. */
.robotstxt-manager-plugin-icon {
display: inline-block;
vertical-align: middle;
width: 64px;
height: 64px;
margin-right: 10px;
border-radius: 4px;
object-fit: contain;
background: #fff;
}
.robotstxt-manager-plugin-icon--placeholder {
border: 1px solid #dcdcde;
box-sizing: border-box;
}
</style>