v0.3.1
This commit is contained in:
parent
65a00cf6ab
commit
681147e8e3
11 changed files with 567 additions and 25 deletions
|
|
@ -68,7 +68,7 @@ class Robotstxt_Manager_Admin {
|
||||||
$catalog = $client->get_catalog();
|
$catalog = $client->get_catalog();
|
||||||
$local = $this->resolve_local_state( $catalog );
|
$local = $this->resolve_local_state( $catalog );
|
||||||
|
|
||||||
require_once ROBOTSTXT_MANAGER_DIR . 'admin/views/page-catalog.php';
|
require ROBOTSTXT_MANAGER_DIR . 'admin/views/page-catalog.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -100,6 +100,27 @@ class Robotstxt_Manager_Admin {
|
||||||
exit;
|
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.
|
* Resolves local install state for every catalog entry.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
432
admin/class-robotstxt-manager-installer.php
Normal file
432
admin/class-robotstxt-manager-installer.php
Normal file
|
|
@ -0,0 +1,432 @@
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Classic admin-post installer: install, activate, and update plugins from the ROBOTSTXT store.
|
||||||
|
*
|
||||||
|
* @package Robotstxt_Manager
|
||||||
|
*/
|
||||||
|
|
||||||
|
if ( ! defined( 'ABSPATH' ) ) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class Robotstxt_Manager_Installer
|
||||||
|
*
|
||||||
|
* Handles three admin-post actions (`robotstxt_manager_install`,
|
||||||
|
* `robotstxt_manager_activate`, `robotstxt_manager_update`). Each runs on a
|
||||||
|
* full page load, then redirects back to the catalog page with a classic
|
||||||
|
* admin notice (success/error) rendered from redirect query arguments.
|
||||||
|
*
|
||||||
|
* Downloads resolve through the remote Core catalog: free plugins with a
|
||||||
|
* public download URL are fetched directly; everything else goes through
|
||||||
|
* Core's authenticated `/download` endpoint, which accepts the account-level
|
||||||
|
* API key as a Bearer token (Core 1.4.0+).
|
||||||
|
*/
|
||||||
|
class Robotstxt_Manager_Installer {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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_post_robotstxt_manager_install', $this, 'handle_install' );
|
||||||
|
$loader->add_action( 'admin_post_robotstxt_manager_activate', $this, 'handle_activate' );
|
||||||
|
$loader->add_action( 'admin_post_robotstxt_manager_update', $this, 'handle_update' );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Installs a plugin from the ROBOTSTXT store.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle_install(): void {
|
||||||
|
$slug = $this->authorize( 'install' );
|
||||||
|
|
||||||
|
$entry = $this->find_catalog_entry( $slug );
|
||||||
|
|
||||||
|
if ( null === $entry ) {
|
||||||
|
$this->redirect_error( __( 'Plugin not found in catalog.', 'robotstxt-manager' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = $this->entry_name( $entry, $slug );
|
||||||
|
$this->ensure_plugin_functions();
|
||||||
|
|
||||||
|
$result = $this->download_and_install( $slug );
|
||||||
|
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
$this->redirect_error( $result->get_error_message() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->redirect_success(
|
||||||
|
sprintf(
|
||||||
|
/* translators: %s: plugin name. */
|
||||||
|
__( '%s installed. Activate it from the list below.', 'robotstxt-manager' ),
|
||||||
|
$name
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Activates an installed plugin.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle_activate(): void {
|
||||||
|
$slug = $this->authorize( 'activate' );
|
||||||
|
|
||||||
|
$this->ensure_plugin_functions();
|
||||||
|
|
||||||
|
$file = $this->find_plugin_file( $slug );
|
||||||
|
|
||||||
|
if ( '' === $file ) {
|
||||||
|
$this->redirect_error( __( 'Plugin is not installed.', 'robotstxt-manager' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = activate_plugins( $file );
|
||||||
|
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
$this->redirect_error( $result->get_error_message() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->redirect_success(
|
||||||
|
sprintf(
|
||||||
|
/* translators: %s: plugin name (slug). */
|
||||||
|
__( '%s activated.', 'robotstxt-manager' ),
|
||||||
|
$slug
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates an installed plugin to the latest catalog version.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
public function handle_update(): void {
|
||||||
|
$slug = $this->authorize( 'update' );
|
||||||
|
|
||||||
|
$this->ensure_plugin_functions();
|
||||||
|
|
||||||
|
$file = $this->find_plugin_file( $slug );
|
||||||
|
|
||||||
|
if ( '' === $file ) {
|
||||||
|
$this->redirect_error( __( 'Plugin is not installed.', 'robotstxt-manager' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $this->download_and_install( $slug, true );
|
||||||
|
|
||||||
|
if ( is_wp_error( $result ) ) {
|
||||||
|
$this->redirect_error( $result->get_error_message() );
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->redirect_success(
|
||||||
|
sprintf(
|
||||||
|
/* translators: %s: plugin name (slug). */
|
||||||
|
__( '%s updated.', 'robotstxt-manager' ),
|
||||||
|
$slug
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates the capability and action-specific nonce, then returns the slug.
|
||||||
|
*
|
||||||
|
* @param string $action One of 'install', 'activate', 'update'.
|
||||||
|
*
|
||||||
|
* @return string The sanitized plugin slug.
|
||||||
|
*/
|
||||||
|
private function authorize( string $action ): string {
|
||||||
|
if ( ! current_user_can( 'manage_options' ) ) {
|
||||||
|
wp_die( esc_html__( 'Insufficient permissions.', 'robotstxt-manager' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw_slug = '';
|
||||||
|
|
||||||
|
if ( isset( $_GET['slug'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- nonce verified below; slug only read after.
|
||||||
|
$unslashed = wp_unslash( $_GET['slug'] ); // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- sanitized below.
|
||||||
|
if ( is_string( $unslashed ) ) {
|
||||||
|
$raw_slug = sanitize_key( $unslashed );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
check_admin_referer( 'robotstxt_manager_' . $action . '_' . $raw_slug );
|
||||||
|
|
||||||
|
return $raw_slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds a catalog entry by slug.
|
||||||
|
*
|
||||||
|
* @param string $slug Plugin slug.
|
||||||
|
*
|
||||||
|
* @return array<string,mixed>|null The entry, or null when not found.
|
||||||
|
*/
|
||||||
|
private function find_catalog_entry( string $slug ): ?array {
|
||||||
|
$client = Robotstxt_Manager_Core_Client::from_options();
|
||||||
|
|
||||||
|
if ( ! $client->is_configured() ) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ( $client->get_catalog() as $row ) {
|
||||||
|
$row_slug = $row['slug'] ?? '';
|
||||||
|
if ( is_string( $row_slug ) && $slug === $row_slug ) {
|
||||||
|
return $row;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts a display name from a catalog entry.
|
||||||
|
*
|
||||||
|
* @param array<string,mixed>|null $entry Catalog entry (null tolerated).
|
||||||
|
* @param string $slug Fallback slug.
|
||||||
|
*
|
||||||
|
* @return string The plugin name.
|
||||||
|
*/
|
||||||
|
private function entry_name( ?array $entry, string $slug ): string {
|
||||||
|
$raw_name = ( null !== $entry ) ? ( $entry['name'] ?? '' ) : '';
|
||||||
|
$name = is_string( $raw_name ) ? $raw_name : '';
|
||||||
|
|
||||||
|
return '' !== $name ? $name : $slug;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolves a plugin slug to its installed plugin file.
|
||||||
|
*
|
||||||
|
* @param string $slug Plugin slug (directory name).
|
||||||
|
*
|
||||||
|
* @return string Plugin file ("slug/file.php") or '' when not installed.
|
||||||
|
*/
|
||||||
|
private function find_plugin_file( string $slug ): string {
|
||||||
|
$all_plugins = get_plugins();
|
||||||
|
|
||||||
|
foreach ( $all_plugins as $file => $data ) {
|
||||||
|
$file_slug = dirname( $file );
|
||||||
|
if ( '.' === $file_slug ) {
|
||||||
|
$file_slug = basename( $file, '.php' );
|
||||||
|
}
|
||||||
|
if ( $slug === $file_slug ) {
|
||||||
|
return $file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Downloads the plugin ZIP from the store and installs it.
|
||||||
|
*
|
||||||
|
* @param string $slug Plugin slug.
|
||||||
|
* @param bool $overwrite Whether to overwrite an existing install (update).
|
||||||
|
*
|
||||||
|
* @return true|WP_Error True on success.
|
||||||
|
*/
|
||||||
|
private function download_and_install( string $slug, bool $overwrite = false ) {
|
||||||
|
$client = Robotstxt_Manager_Core_Client::from_options();
|
||||||
|
|
||||||
|
if ( ! $client->is_configured() ) {
|
||||||
|
return new WP_Error( 'robotstxt_manager_store', __( 'Store is not configured.', 'robotstxt-manager' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$entry = $this->find_catalog_entry( $slug );
|
||||||
|
|
||||||
|
if ( null === $entry ) {
|
||||||
|
return new WP_Error( 'robotstxt_manager_catalog', __( 'Plugin not found in catalog.', 'robotstxt-manager' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$raw_kind = $entry['type'] ?? 'free';
|
||||||
|
$kind = is_string( $raw_kind ) ? $raw_kind : 'free';
|
||||||
|
$raw_dl = $entry['download_url'] ?? '';
|
||||||
|
$dl_url = is_string( $raw_dl ) ? $raw_dl : '';
|
||||||
|
|
||||||
|
$is_free = 'premium' !== $kind;
|
||||||
|
|
||||||
|
// Free plugins that publish a public download URL in the catalog are
|
||||||
|
// fetched directly (no auth). Everything else goes through Core's
|
||||||
|
// authenticated download endpoint (account API key as Bearer).
|
||||||
|
$use_download_endpoint = ! ( $is_free && '' !== $dl_url );
|
||||||
|
|
||||||
|
$zip_url = $use_download_endpoint
|
||||||
|
? $client->get_store_url() . '/wp-json/robotstxt-core/v1/plugins/' . rawurlencode( $slug ) . '/download'
|
||||||
|
: $dl_url;
|
||||||
|
|
||||||
|
$tmp_file = wp_tempnam( $slug . '.zip' );
|
||||||
|
|
||||||
|
if ( ! $tmp_file ) {
|
||||||
|
return new WP_Error( 'robotstxt_manager_temp', __( 'Could not create a temporary file for download.', 'robotstxt-manager' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$headers = array();
|
||||||
|
|
||||||
|
if ( $use_download_endpoint ) {
|
||||||
|
$api_key_raw = get_option( 'robotstxt_manager_api_key', '' );
|
||||||
|
$api_key = is_string( $api_key_raw ) ? Robotstxt_Manager_Encryption::decrypt( $api_key_raw ) : '';
|
||||||
|
|
||||||
|
if ( '' !== $api_key ) {
|
||||||
|
$headers = array(
|
||||||
|
'Authorization' => 'Bearer ' . $api_key,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$response = wp_remote_get(
|
||||||
|
$zip_url,
|
||||||
|
array(
|
||||||
|
'timeout' => 300,
|
||||||
|
'stream' => true,
|
||||||
|
'filename' => $tmp_file,
|
||||||
|
'headers' => $headers,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
if ( is_wp_error( $response ) ) {
|
||||||
|
wp_delete_file( $tmp_file );
|
||||||
|
|
||||||
|
return new WP_Error(
|
||||||
|
'robotstxt_manager_download',
|
||||||
|
sprintf(
|
||||||
|
/* translators: %s: HTTP transport error message. */
|
||||||
|
__( 'Download failed: %s', 'robotstxt-manager' ),
|
||||||
|
$response->get_error_message()
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$code = (int) wp_remote_retrieve_response_code( $response );
|
||||||
|
|
||||||
|
if ( 200 !== $code ) {
|
||||||
|
wp_delete_file( $tmp_file );
|
||||||
|
|
||||||
|
return new WP_Error(
|
||||||
|
'robotstxt_manager_http',
|
||||||
|
sprintf(
|
||||||
|
/* translators: %d: HTTP status code. */
|
||||||
|
__( 'Download failed (HTTP %d).', 'robotstxt-manager' ),
|
||||||
|
$code
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! $this->is_valid_zip( $tmp_file ) ) {
|
||||||
|
wp_delete_file( $tmp_file );
|
||||||
|
|
||||||
|
return new WP_Error( 'robotstxt_manager_zip', __( 'The store returned an invalid file.', 'robotstxt-manager' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
$upgrader = new Plugin_Upgrader( new Automatic_Upgrader_Skin() );
|
||||||
|
$result = $upgrader->install(
|
||||||
|
$tmp_file,
|
||||||
|
array(
|
||||||
|
'overwrite' => $overwrite,
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
wp_delete_file( $tmp_file );
|
||||||
|
|
||||||
|
if ( true !== $result ) {
|
||||||
|
$detail = ( $result instanceof WP_Error ) ? $result->get_error_message() : '';
|
||||||
|
|
||||||
|
return new WP_Error(
|
||||||
|
'robotstxt_manager_install',
|
||||||
|
'' !== $detail
|
||||||
|
? sprintf(
|
||||||
|
/* translators: %s: upgrader error message. */
|
||||||
|
__( 'Installation failed: %s', 'robotstxt-manager' ),
|
||||||
|
$detail
|
||||||
|
)
|
||||||
|
: __( 'Installation failed.', 'robotstxt-manager' )
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Loads the wp-admin plugin/upgrader dependencies.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function ensure_plugin_functions(): void {
|
||||||
|
if ( ! function_exists( 'get_plugins' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/plugin.php';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ( ! class_exists( 'Plugin_Upgrader' ) ) {
|
||||||
|
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects back to the catalog page with a success notice.
|
||||||
|
*
|
||||||
|
* @param string $message Notice text.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function redirect_success( string $message ): void {
|
||||||
|
$this->redirect( 'success', $message );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects back to the catalog page with an error notice.
|
||||||
|
*
|
||||||
|
* @param string $message Notice text.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function redirect_error( string $message ): void {
|
||||||
|
$this->redirect( 'error', $message );
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Redirects back to the catalog page carrying a notice.
|
||||||
|
*
|
||||||
|
* @param string $result 'success' or 'error'.
|
||||||
|
* @param string $message Notice text.
|
||||||
|
*
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
private function redirect( string $result, string $message ): void {
|
||||||
|
wp_safe_redirect(
|
||||||
|
add_query_arg(
|
||||||
|
array(
|
||||||
|
'page' => Robotstxt_Manager_Admin::PAGE_SLUG,
|
||||||
|
'robotstxt_manager_result' => $result,
|
||||||
|
'robotstxt_manager_message' => rawurlencode( $message ),
|
||||||
|
),
|
||||||
|
admin_url( 'admin.php' )
|
||||||
|
)
|
||||||
|
);
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates a downloaded archive: must exist, be non-empty, and start
|
||||||
|
* with the ZIP magic bytes "PK".
|
||||||
|
*
|
||||||
|
* @param string $file Absolute path to the downloaded file.
|
||||||
|
*
|
||||||
|
* @return bool True when the file looks like a valid ZIP archive.
|
||||||
|
*/
|
||||||
|
private function is_valid_zip( string $file ): bool {
|
||||||
|
if ( ! file_exists( $file ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$size = filesize( $file );
|
||||||
|
|
||||||
|
if ( false === $size || 0 >= $size ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
$magic = file_get_contents( $file, false, null, 0, 2 ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents -- reading 2 bytes of a local temp file.
|
||||||
|
|
||||||
|
return is_string( $magic ) && 'PK' === substr( $magic, 0, 2 );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -83,7 +83,7 @@ class Robotstxt_Manager_Settings {
|
||||||
array(
|
array(
|
||||||
'type' => 'string',
|
'type' => 'string',
|
||||||
'sanitize_callback' => 'esc_url_raw',
|
'sanitize_callback' => 'esc_url_raw',
|
||||||
'default' => 'https://plugins.robotstxt.es',
|
'default' => 'https://www.robotstxt.software',
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -216,11 +216,11 @@ class Robotstxt_Manager_Settings {
|
||||||
* @return void
|
* @return void
|
||||||
*/
|
*/
|
||||||
public function render_field_store_url(): void {
|
public function render_field_store_url(): void {
|
||||||
$raw = get_option( 'robotstxt_manager_store_url', 'https://plugins.robotstxt.es' );
|
$raw = get_option( 'robotstxt_manager_store_url', 'https://www.robotstxt.software' );
|
||||||
$value = is_string( $raw ) ? $raw : 'https://plugins.robotstxt.es';
|
$value = is_string( $raw ) ? $raw : 'https://www.robotstxt.software';
|
||||||
|
|
||||||
printf(
|
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" />',
|
'<input type="url" id="robotstxt_manager_store_url" name="robotstxt_manager_store_url" value="%s" class="regular-text" maxlength="500" placeholder="https://www.robotstxt.software" />',
|
||||||
esc_attr( $value )
|
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>';
|
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>';
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,24 @@ $refresh_url = add_query_arg(
|
||||||
$refresh_url = wp_nonce_url( $refresh_url, 'robotstxt_manager_refresh_catalog' );
|
$refresh_url = wp_nonce_url( $refresh_url, 'robotstxt_manager_refresh_catalog' );
|
||||||
$refreshed = isset( $_GET['refreshed'] ) && '1' === $_GET['refreshed']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
|
$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_wp_version = (string) get_bloginfo( 'version' );
|
||||||
$local_php_version = (string) PHP_VERSION;
|
$local_php_version = (string) PHP_VERSION;
|
||||||
$compat_warnings = 0;
|
$compat_warnings = 0;
|
||||||
|
|
@ -43,6 +61,10 @@ $compat_warnings = 0;
|
||||||
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Catalog refreshed.', 'robotstxt-manager' ); ?></p></div>
|
<div class="notice notice-success is-dismissible"><p><?php esc_html_e( 'Catalog refreshed.', 'robotstxt-manager' ); ?></p></div>
|
||||||
<?php endif; ?>
|
<?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 ) : ?>
|
<?php if ( ! $client_configured ) : ?>
|
||||||
<div class="notice notice-error">
|
<div class="notice notice-error">
|
||||||
<p>
|
<p>
|
||||||
|
|
@ -190,13 +212,19 @@ $compat_warnings = 0;
|
||||||
<?php if ( ! $l_installed ) : ?>
|
<?php if ( ! $l_installed ) : ?>
|
||||||
<?php if ( ! $wp_ok || ! $php_ok ) : ?>
|
<?php if ( ! $wp_ok || ! $php_ok ) : ?>
|
||||||
<span class="description"><?php esc_html_e( 'Incompatible', 'robotstxt-manager' ); ?></span>
|
<span class="description"><?php esc_html_e( 'Incompatible', 'robotstxt-manager' ); ?></span>
|
||||||
<?php elseif ( $cost > 0.0 && '' !== $page_url ) : ?>
|
<?php else : ?>
|
||||||
<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 if ( $cost > 0.0 && '' !== $page_url ) : ?>
|
||||||
<?php elseif ( $cost <= 0.0 ) : ?>
|
<a class="button button-secondary" href="<?php echo esc_url( $page_url ); ?>" target="_blank" rel="noopener noreferrer"><?php esc_html_e( 'Buy', 'robotstxt-manager' ); ?></a>
|
||||||
<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 endif; ?>
|
||||||
|
<?php
|
||||||
|
$install_class = ( $cost > 0.0 && '' !== $page_url ) ? 'button-secondary' : 'button-primary';
|
||||||
|
?>
|
||||||
|
<a class="button <?php echo esc_attr( $install_class ); ?>" href="<?php echo esc_url( Robotstxt_Manager_Admin::action_url( 'install', $slug ) ); ?>"><?php esc_html_e( 'Install', 'robotstxt-manager' ); ?></a>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?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 ) : ?>
|
<?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>
|
<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; ?>
|
<?php endif; ?>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,61 @@
|
||||||
== Changelog ==
|
== Changelog ==
|
||||||
|
|
||||||
|
= 0.3.1 =
|
||||||
|
|
||||||
|
_Release date: 2026-08-14_
|
||||||
|
|
||||||
|
* Default store URL is now `https://www.robotstxt.software` (was `https://plugins.robotstxt.es`). Applies to new activations and fresh installs; existing saved URLs are preserved. The catalog API at the new domain is verified live.
|
||||||
|
* Plugin version 0.3.0 → 0.3.1.
|
||||||
|
|
||||||
|
= 0.3.0 =
|
||||||
|
|
||||||
|
_Release date: 2026-08-13_
|
||||||
|
|
||||||
|
**Highlights**
|
||||||
|
|
||||||
|
* Classic install/activate/update flow: the catalog actions now run on a full page load (`admin-post.php` with per-action-and-slug nonces) and report the outcome through standard admin notices (green success / red error) at the top of the page, replacing the previous AJAX flow.
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
* Activate action — installed-but-inactive plugins show an "Activate" button that calls `activate_plugins()`.
|
||||||
|
* Update action — plugins with a newer catalog version show an "Update" button that re-downloads the ZIP and runs the `Plugin_Upgrader` with `overwrite` enabled.
|
||||||
|
* Install/Activate/Update buttons are nonce links (`robotstxt_manager_{action}_{slug}` nonce actions) to `admin-post.php`, all `manage_options`-gated.
|
||||||
|
* Redirect-based notices: handlers redirect back to the catalog page carrying `robotstxt_manager_result` (success/error) and `robotstxt_manager_message`, rendered as dismissible standard notices.
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
* `Robotstxt_Manager_Installer` rewritten from an AJAX handler to three admin-post handlers (`robotstxt_manager_install`, `robotstxt_manager_activate`, `robotstxt_manager_update`); download logic (Bearer account-key auth, ZIP validation, temp-file cleanup) unchanged.
|
||||||
|
* Removed the AJAX endpoint, `manager-install.js`, and its script localization.
|
||||||
|
* Plugin version 0.2.0 → 0.3.0.
|
||||||
|
|
||||||
|
**Compatibility**
|
||||||
|
|
||||||
|
* WordPress: 7.0 - 7.1 (declared floor of the ecosystem; scan-confirmed lower)
|
||||||
|
* PHP: 8.4 - 8.5 (declared floor of the ecosystem; scan-confirmed lower)
|
||||||
|
|
||||||
|
= 0.2.0 =
|
||||||
|
|
||||||
|
_Release date: 2026-08-12_
|
||||||
|
|
||||||
|
**Highlights**
|
||||||
|
|
||||||
|
* Phase 2: the Install action. Free and premium plugins can be installed directly from the ROBOTSTXT catalog into the local site, authenticated with the account-level API key (requires Plugins Core 1.4.0+).
|
||||||
|
|
||||||
|
**Added**
|
||||||
|
|
||||||
|
* `Robotstxt_Manager_Installer` — AJAX handler (`wp_ajax_robotstxt_manager_install_plugin`, nonce-protected, `manage_options`-gated) that resolves the plugin in the remote catalog, downloads the ZIP (free plugins with a published public `download_url` directly; everything else via Core's authenticated `/download` endpoint with an `Authorization: Bearer <account API key>` header), validates the archive (ZIP magic bytes), installs it via `Plugin_Upgrader`, and cleans up the temp file on every path.
|
||||||
|
* `public/js/manager-install.js` — wires the Install buttons to the AJAX action with in-flight state and result rendering.
|
||||||
|
* Catalog view: enabled Install buttons for compatible plugins (free and premium).
|
||||||
|
|
||||||
|
**Changed**
|
||||||
|
|
||||||
|
* Plugin version 0.1.3 → 0.2.0. Description updated to reflect Phase 2.
|
||||||
|
|
||||||
|
**Compatibility**
|
||||||
|
|
||||||
|
* WordPress: 7.0 - 7.1 (declared floor of the ecosystem; scan-confirmed lower)
|
||||||
|
* PHP: 8.4 - 8.5 (declared floor of the ecosystem; scan-confirmed lower)
|
||||||
|
|
||||||
= 0.1.3 =
|
= 0.1.3 =
|
||||||
|
|
||||||
_Release date: 2026-08-12_
|
_Release date: 2026-08-12_
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ class Robotstxt_Manager_Activator {
|
||||||
*/
|
*/
|
||||||
private static function ensure_defaults(): void {
|
private static function ensure_defaults(): void {
|
||||||
if ( '' === get_option( 'robotstxt_manager_store_url', '' ) ) {
|
if ( '' === get_option( 'robotstxt_manager_store_url', '' ) ) {
|
||||||
update_option( 'robotstxt_manager_store_url', 'https://plugins.robotstxt.es' );
|
update_option( 'robotstxt_manager_store_url', 'https://www.robotstxt.software' );
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( '' === get_option( 'robotstxt_manager_cache_ttl_minutes', '' ) ) {
|
if ( '' === get_option( 'robotstxt_manager_cache_ttl_minutes', '' ) ) {
|
||||||
|
|
|
||||||
|
|
@ -61,9 +61,11 @@ final class Robotstxt_Manager_Plugin {
|
||||||
|
|
||||||
$admin = new Robotstxt_Manager_Admin();
|
$admin = new Robotstxt_Manager_Admin();
|
||||||
$settings = new Robotstxt_Manager_Settings();
|
$settings = new Robotstxt_Manager_Settings();
|
||||||
|
$installer = new Robotstxt_Manager_Installer();
|
||||||
|
|
||||||
$admin->register( $this->loader );
|
$admin->register( $this->loader );
|
||||||
$settings->register( $this->loader );
|
$settings->register( $this->loader );
|
||||||
|
$installer->register( $this->loader );
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
|
|
@ -3,9 +3,9 @@ Contributors: javiercasares, robotstxt
|
||||||
Tags: dashboard, catalog, updates, subscriptions, management
|
Tags: dashboard, catalog, updates, subscriptions, management
|
||||||
Requires at least: 4.7
|
Requires at least: 4.7
|
||||||
Tested up to: 7.1
|
Tested up to: 7.1
|
||||||
Stable tag: 0.1.3
|
Stable tag: 0.3.1
|
||||||
Requires PHP: 7.4
|
Requires PHP: 7.4
|
||||||
Version: 0.1.3
|
Version: 0.3.1
|
||||||
License: GPL-3.0-or-later
|
License: GPL-3.0-or-later
|
||||||
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
|
License URI: https://www.gnu.org/licenses/gpl-3.0.txt
|
||||||
|
|
||||||
|
|
@ -30,7 +30,7 @@ This plugin does **not** re-implement the per-plugin update mechanism. Every ROB
|
||||||
|
|
||||||
Configure at **ROBOTSTXT → Settings**:
|
Configure at **ROBOTSTXT → Settings**:
|
||||||
|
|
||||||
* **Store URL** — Base URL of the remote Plugins Core installation (defaults to `https://plugins.robotstxt.es`).
|
* **Store URL** — Base URL of the remote Plugins Core installation (defaults to `https://www.robotstxt.software`).
|
||||||
* **API Key** — Account-level API key issued by the ROBOTSTXT store. Encrypted before storage; masked in the UI (last 4 characters shown). Use the "Test connection" button to verify connectivity.
|
* **API Key** — Account-level API key issued by the ROBOTSTXT store. Encrypted before storage; masked in the UI (last 4 characters shown). Use the "Test connection" button to verify connectivity.
|
||||||
* **Catalog Cache (minutes)** — How long the catalog response is cached in a transient. Default: 60 minutes.
|
* **Catalog Cache (minutes)** — How long the catalog response is cached in a transient. Default: 60 minutes.
|
||||||
* **Data on Uninstall** — Opt-in checkbox. When enabled, all Manager options and transients are deleted on uninstall. Default: off.
|
* **Data on Uninstall** — Opt-in checkbox. When enabled, all Manager options and transients are deleted on uninstall. Default: off.
|
||||||
|
|
|
||||||
|
|
@ -2,8 +2,8 @@
|
||||||
/**
|
/**
|
||||||
* Plugin Name: Manager (by ROBOTSTXT)
|
* Plugin Name: Manager (by ROBOTSTXT)
|
||||||
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-manager
|
* Plugin URI: https://git.robotstxt.es/ROBOTSTXT/robotstxt-manager
|
||||||
* Description: Client-side dashboard for the ROBOTSTXT plugin ecosystem. Lists the catalog from a remote Plugins Core install, resolves local install/update state, and surfaces subscription health. Phase 1: read-only catalog view.
|
* Description: Client-side dashboard for the ROBOTSTXT plugin ecosystem. Lists the catalog from a remote Plugins Core install, resolves local install/update state, and installs, activates, and updates plugins directly from the store.
|
||||||
* Version: 0.1.3
|
* Version: 0.3.1
|
||||||
* Requires at least: 4.7
|
* Requires at least: 4.7
|
||||||
* Requires PHP: 7.4
|
* Requires PHP: 7.4
|
||||||
* Author: ROBOTSTXT
|
* Author: ROBOTSTXT
|
||||||
|
|
@ -21,7 +21,7 @@ if ( ! defined( 'ABSPATH' ) ) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Plugin version. */
|
/** Plugin version. */
|
||||||
define( 'ROBOTSTXT_MANAGER_VERSION', '0.1.3' );
|
define( 'ROBOTSTXT_MANAGER_VERSION', '0.3.1' );
|
||||||
|
|
||||||
/** Absolute path to the plugin directory, with trailing slash. */
|
/** Absolute path to the plugin directory, with trailing slash. */
|
||||||
define( 'ROBOTSTXT_MANAGER_DIR', plugin_dir_path( __FILE__ ) );
|
define( 'ROBOTSTXT_MANAGER_DIR', plugin_dir_path( __FILE__ ) );
|
||||||
|
|
@ -44,6 +44,7 @@ if ( file_exists( ROBOTSTXT_MANAGER_DIR . 'vendor/autoload.php' ) ) {
|
||||||
require_once ROBOTSTXT_MANAGER_DIR . 'includes/class-robotstxt-manager-plugin.php';
|
require_once ROBOTSTXT_MANAGER_DIR . 'includes/class-robotstxt-manager-plugin.php';
|
||||||
require_once ROBOTSTXT_MANAGER_DIR . 'admin/class-robotstxt-manager-admin.php';
|
require_once ROBOTSTXT_MANAGER_DIR . 'admin/class-robotstxt-manager-admin.php';
|
||||||
require_once ROBOTSTXT_MANAGER_DIR . 'admin/class-robotstxt-manager-settings.php';
|
require_once ROBOTSTXT_MANAGER_DIR . 'admin/class-robotstxt-manager-settings.php';
|
||||||
|
require_once ROBOTSTXT_MANAGER_DIR . 'admin/class-robotstxt-manager-installer.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
register_activation_hook( __FILE__, array( 'Robotstxt_Manager_Activator', 'activate' ) );
|
register_activation_hook( __FILE__, array( 'Robotstxt_Manager_Activator', 'activate' ) );
|
||||||
|
|
|
||||||
1
vendor/composer/autoload_classmap.php
vendored
1
vendor/composer/autoload_classmap.php
vendored
|
|
@ -11,6 +11,7 @@ return array(
|
||||||
'Robotstxt_Manager_Admin' => $baseDir . '/admin/class-robotstxt-manager-admin.php',
|
'Robotstxt_Manager_Admin' => $baseDir . '/admin/class-robotstxt-manager-admin.php',
|
||||||
'Robotstxt_Manager_Core_Client' => $baseDir . '/includes/class-robotstxt-manager-core-client.php',
|
'Robotstxt_Manager_Core_Client' => $baseDir . '/includes/class-robotstxt-manager-core-client.php',
|
||||||
'Robotstxt_Manager_Encryption' => $baseDir . '/includes/class-robotstxt-manager-encryption.php',
|
'Robotstxt_Manager_Encryption' => $baseDir . '/includes/class-robotstxt-manager-encryption.php',
|
||||||
|
'Robotstxt_Manager_Installer' => $baseDir . '/admin/class-robotstxt-manager-installer.php',
|
||||||
'Robotstxt_Manager_Loader' => $baseDir . '/includes/class-robotstxt-manager-loader.php',
|
'Robotstxt_Manager_Loader' => $baseDir . '/includes/class-robotstxt-manager-loader.php',
|
||||||
'Robotstxt_Manager_Plugin' => $baseDir . '/includes/class-robotstxt-manager-plugin.php',
|
'Robotstxt_Manager_Plugin' => $baseDir . '/includes/class-robotstxt-manager-plugin.php',
|
||||||
'Robotstxt_Manager_Settings' => $baseDir . '/admin/class-robotstxt-manager-settings.php',
|
'Robotstxt_Manager_Settings' => $baseDir . '/admin/class-robotstxt-manager-settings.php',
|
||||||
|
|
|
||||||
1
vendor/composer/autoload_static.php
vendored
1
vendor/composer/autoload_static.php
vendored
|
|
@ -12,6 +12,7 @@ class ComposerStaticInit21193cc5ee16adfbe1fcd1cc267e45ef
|
||||||
'Robotstxt_Manager_Admin' => __DIR__ . '/../..' . '/admin/class-robotstxt-manager-admin.php',
|
'Robotstxt_Manager_Admin' => __DIR__ . '/../..' . '/admin/class-robotstxt-manager-admin.php',
|
||||||
'Robotstxt_Manager_Core_Client' => __DIR__ . '/../..' . '/includes/class-robotstxt-manager-core-client.php',
|
'Robotstxt_Manager_Core_Client' => __DIR__ . '/../..' . '/includes/class-robotstxt-manager-core-client.php',
|
||||||
'Robotstxt_Manager_Encryption' => __DIR__ . '/../..' . '/includes/class-robotstxt-manager-encryption.php',
|
'Robotstxt_Manager_Encryption' => __DIR__ . '/../..' . '/includes/class-robotstxt-manager-encryption.php',
|
||||||
|
'Robotstxt_Manager_Installer' => __DIR__ . '/../..' . '/admin/class-robotstxt-manager-installer.php',
|
||||||
'Robotstxt_Manager_Loader' => __DIR__ . '/../..' . '/includes/class-robotstxt-manager-loader.php',
|
'Robotstxt_Manager_Loader' => __DIR__ . '/../..' . '/includes/class-robotstxt-manager-loader.php',
|
||||||
'Robotstxt_Manager_Plugin' => __DIR__ . '/../..' . '/includes/class-robotstxt-manager-plugin.php',
|
'Robotstxt_Manager_Plugin' => __DIR__ . '/../..' . '/includes/class-robotstxt-manager-plugin.php',
|
||||||
'Robotstxt_Manager_Settings' => __DIR__ . '/../..' . '/admin/class-robotstxt-manager-settings.php',
|
'Robotstxt_Manager_Settings' => __DIR__ . '/../..' . '/admin/class-robotstxt-manager-settings.php',
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue