robotstxt-manager/admin/views/page-catalog.php
2026-09-23 06:11:14 +00:00

494 lines
22 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?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 list<array{slug:string, name:string, installed:string, patch:string}> $manager_security_updates
* @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 foreach ( ( $manager_security_updates ?? array() ) as $manager_security ) : ?>
<div class="notice notice-error">
<p>
<?php
echo wp_kses_post(
sprintf(
/* translators: 1: plugin name, 2: installed version, 3: patch version, 4: update URL. */
__( '<strong>Security update available:</strong> %1$s (v%2$s → v%3$s). <a href="%4$s">Update now</a> — this is a security patch for the version this site runs, not a feature update.', 'robotstxt-manager' ),
esc_html( $manager_security['name'] ),
esc_html( $manager_security['installed'] ),
esc_html( $manager_security['patch'] ),
esc_url( Robotstxt_Manager_Admin::action_url( 'update', $manager_security['slug'] ) )
)
);
?>
</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( ( is_multisite() ? network_admin_url( 'admin.php?page=' . Robotstxt_Manager_Settings::PAGE_SLUG ) : 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, '<' );
// Security patch declared for exactly the installed version:
// the Update action installs the patch, not the mainline.
$security_patch = Robotstxt_Manager_Updater::security_patch_for( $entry, $l_version );
if ( '' !== $security_patch ) {
$update_available = true;
}
$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' );
}
// Bound domain (per-domain licenses, Core 1.11.0+):
// shown once the license is tied to a site.
$raw_bound = $sub_row['bound_domain'] ?? '';
$bound_text = is_string( $raw_bound ) ? trim( $raw_bound ) : '';
// Multi-license: how many licenses the account
// holds for this plugin (Core 1.14.0+).
$raw_count = $sub_row['license_count'] ?? 1;
$count_int = is_numeric( $raw_count ) ? (int) ( $raw_count + 0 ) : 1;
$count_txt = $count_int > 1 ? ' ×' . $count_int : '';
if ( '' !== $bound_text && in_array( $sub_status, array( 'active', 'payment_failed' ), true ) ) {
$sub_text = '' !== $sub_text
? $sub_text . ' @ ' . $bound_text . $count_txt
: $bound_text . $count_txt;
} elseif ( '' !== $count_txt ) {
$sub_text = '' !== $sub_text ? $sub_text . $count_txt : __( 'Subscribed', 'robotstxt-manager' ) . $count_txt;
}
}
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 ( '' !== $security_patch ) {
echo '<span class="robotstxt-manager-state robotstxt-manager-state--security">' . esc_html(
sprintf(
/* translators: 1: installed version, 2: security patch version. */
__( 'Security update available (v%1$s → v%2$s)', 'robotstxt-manager' ),
$l_version,
$security_patch
)
) . '</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; }
.robotstxt-manager-state--security { color: #d63638; font-weight: 600; }
/* 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>