283 lines
8.3 KiB
PHP
283 lines
8.3 KiB
PHP
<?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 {
|
|
$menu_hook = is_multisite() ? 'network_admin_menu' : 'admin_menu';
|
|
$loader->add_action( $menu_hook, $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 {
|
|
$cap = is_multisite() ? 'manage_network_options' : 'manage_options';
|
|
add_menu_page(
|
|
esc_html__( 'Manager (by ROBOTSTXT) — Plugins', 'robotstxt-manager' ),
|
|
esc_html__( 'ROBOTSTXT', 'robotstxt-manager' ),
|
|
$cap,
|
|
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( is_multisite() ? 'manage_network_options' : '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 );
|
|
$subscriptions = $client->get_subscriptions();
|
|
|
|
// Admin notices for subscriptions needing attention.
|
|
$manager_notices = $this->build_subscription_notices( $catalog, $subscriptions );
|
|
$manager_subscriptions = $subscriptions;
|
|
$manager_has_api_key = $client->has_api_key();
|
|
$manager_store_url = $client->get_store_url();
|
|
|
|
require ROBOTSTXT_MANAGER_DIR . 'admin/views/page-catalog.php';
|
|
}
|
|
|
|
|
|
/**
|
|
* Builds admin notices for subscriptions that need attention.
|
|
*
|
|
* - payment_failed: persistent warning per plugin.
|
|
* - expiring within 14 days: per-plugin warning.
|
|
* - expired while the plugin is still installed+active: per-plugin warning.
|
|
*
|
|
* @param list<array<string, mixed>> $catalog Catalog entries.
|
|
* @param array<string, array<string, mixed>> $subscriptions Rows keyed by slug.
|
|
*
|
|
* @return list<array{type:string, message:string}>
|
|
*/
|
|
private function build_subscription_notices( array $catalog, array $subscriptions ): array {
|
|
$notices = array();
|
|
$local = $this->resolve_local_state( $catalog );
|
|
|
|
foreach ( $subscriptions as $slug => $sub ) {
|
|
$raw_status = $sub['status'] ?? '';
|
|
$raw_expires_at = $sub['expires_at'] ?? '';
|
|
$status = is_string( $raw_status ) ? $raw_status : '';
|
|
$expires_at = is_string( $raw_expires_at ) ? $raw_expires_at : '';
|
|
|
|
if ( 'payment_failed' === $status ) {
|
|
$notices[] = array(
|
|
'type' => 'warning',
|
|
/* translators: %s: plugin slug. */
|
|
'message' => sprintf( __( 'The payment for %s failed. Update your payment method from your ROBOTSTXT account page to keep access.', 'robotstxt-manager' ), $slug ),
|
|
);
|
|
continue;
|
|
}
|
|
|
|
if ( 'expired' === $status ) {
|
|
$state = $local[ $slug ] ?? array();
|
|
if ( ! empty( $state['active'] ) ) {
|
|
$notices[] = array(
|
|
'type' => 'warning',
|
|
/* translators: %s: plugin slug. */
|
|
'message' => sprintf( __( 'The subscription for %s has expired, but the plugin is still active on this site. Renew from your ROBOTSTXT account page to keep receiving updates.', 'robotstxt-manager' ), $slug ),
|
|
);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if ( 'active' === $status && '' !== $expires_at ) {
|
|
$days = (int) floor( ( (int) strtotime( $expires_at ) - time() ) / DAY_IN_SECONDS );
|
|
|
|
if ( $days >= 0 && $days <= 14 ) {
|
|
$notices[] = array(
|
|
'type' => 'warning',
|
|
/* translators: 1: plugin slug, 2: days remaining. */
|
|
'message' => sprintf( _n( 'The subscription for %1$s expires in %2$d day.', 'The subscription for %1$s expires in %2$d days.', $days, 'robotstxt-manager' ), $slug, $days ),
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $notices;
|
|
}
|
|
|
|
/**
|
|
* Handles the "Refresh catalog" admin-post action.
|
|
*
|
|
* Clears the cached catalog response, purges WordPress's update_plugins
|
|
* transient so native update badges re-evaluate immediately, and
|
|
* redirects back to the page. Rate-limited to 6 refreshes per minute
|
|
* per user so a stuck browser cannot hammer the store.
|
|
*
|
|
* @return void
|
|
*/
|
|
public function handle_refresh(): void {
|
|
if ( ! current_user_can( is_multisite() ? 'manage_network_options' : 'manage_options' ) ) {
|
|
wp_die( esc_html__( 'Insufficient permissions.', 'robotstxt-manager' ) );
|
|
}
|
|
|
|
check_admin_referer( 'robotstxt_manager_refresh_catalog' );
|
|
|
|
$bucket = 'robotstxt_manager_refresh_' . get_current_user_id();
|
|
$hits_raw = get_site_transient( $bucket );
|
|
$hits = is_numeric( $hits_raw ) ? (int) $hits_raw : 0;
|
|
|
|
if ( $hits >= 6 ) {
|
|
$this->redirect_refresh_error();
|
|
}
|
|
|
|
set_site_transient( $bucket, $hits + 1, MINUTE_IN_SECONDS );
|
|
|
|
$client = Robotstxt_Manager_Core_Client::from_options();
|
|
$client->clear_catalog_cache();
|
|
$client->clear_subscriptions_cache();
|
|
delete_site_transient( 'update_plugins' );
|
|
|
|
$redirect = add_query_arg(
|
|
array(
|
|
'page' => self::PAGE_SLUG,
|
|
'refreshed' => '1',
|
|
),
|
|
( is_multisite() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' ) )
|
|
);
|
|
|
|
wp_safe_redirect( $redirect );
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Redirects back to the catalog page with a rate-limit error notice.
|
|
*
|
|
* @return void
|
|
*/
|
|
private function redirect_refresh_error(): void {
|
|
wp_safe_redirect(
|
|
add_query_arg(
|
|
array(
|
|
'page' => self::PAGE_SLUG,
|
|
'robotstxt_manager_result' => 'error',
|
|
'robotstxt_manager_message' => rawurlencode(
|
|
__( 'Too many refreshes. Please wait a minute before refreshing again.', 'robotstxt-manager' )
|
|
),
|
|
),
|
|
( is_multisite() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' ) )
|
|
)
|
|
);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Builds a nonce-protected admin-post action URL for a plugin row.
|
|
*
|
|
* @param string $action One of 'install', 'activate', 'update'.
|
|
* @param string $slug Plugin slug.
|
|
*
|
|
* @return string The action URL.
|
|
*/
|
|
public static function action_url( string $action, string $slug ): string {
|
|
return wp_nonce_url(
|
|
add_query_arg(
|
|
array(
|
|
'action' => 'robotstxt_manager_' . $action,
|
|
'slug' => $slug,
|
|
),
|
|
admin_url( 'admin-post.php' )
|
|
),
|
|
'robotstxt_manager_' . $action . '_' . $slug
|
|
);
|
|
}
|
|
|
|
/**
|
|
* 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;
|
|
}
|
|
}
|