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 ROBOTSTXT_MANAGER_DIR . 'admin/views/page-catalog.php'; } /** * 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. * * @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(); delete_site_transient( 'update_plugins' ); $redirect = add_query_arg( array( 'page' => self::PAGE_SLUG, 'refreshed' => '1', ), admin_url( 'admin.php' ) ); wp_safe_redirect( $redirect ); 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> $catalog Catalog entries from Core. * * @return array 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; } }