772 lines
22 KiB
PHP
772 lines
22 KiB
PHP
<?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();
|
|
|
|
// Install missing dependencies first (ecosystem catalog plugins via
|
|
// the store, WordPress.org plugins via their repository ZIPs).
|
|
$deps_installed = $this->install_dependencies( $slug );
|
|
|
|
if ( is_wp_error( $deps_installed ) ) {
|
|
$this->redirect_error( $deps_installed->get_error_message() );
|
|
}
|
|
|
|
$result = $this->download_and_install( $slug );
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
$this->redirect_error( $result->get_error_message() );
|
|
}
|
|
|
|
$message = sprintf(
|
|
/* translators: %s: plugin name. */
|
|
__( '%s installed. Activate it from the list below.', 'robotstxt-manager' ),
|
|
$name
|
|
);
|
|
|
|
if ( is_string( $deps_installed ) && '' !== $deps_installed ) {
|
|
$message .= ' ' . $deps_installed;
|
|
}
|
|
|
|
$this->redirect_success( $message );
|
|
}
|
|
|
|
/**
|
|
* Installs the plugin's missing dependencies, dependencies first.
|
|
*
|
|
* Each dependency slug is resolved either against the store catalog
|
|
* (installed through the store's download endpoint, like any catalog
|
|
* plugin) or, when not in the catalog, against WordPress.org (installed
|
|
* from the repository's plugin ZIP).
|
|
*
|
|
* @param string $slug Plugin slug being installed.
|
|
*
|
|
* @return string|WP_Error Installed-dependency names for the notice, '' when none were needed.
|
|
*/
|
|
private function install_dependencies( string $slug ) {
|
|
$deps = $this->dependencies_for( $slug );
|
|
|
|
if ( array() === $deps ) {
|
|
return '';
|
|
}
|
|
|
|
$installed_names = array();
|
|
|
|
foreach ( $deps as $dep_slug ) {
|
|
if ( '' !== $this->find_plugin_file( $dep_slug ) ) {
|
|
continue; // Already installed.
|
|
}
|
|
|
|
$result = $this->install_one_dependency( $dep_slug );
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
/* translators: %s: dependency slug. */
|
|
$detail = sprintf( __( 'Could not install the required plugin %s.', 'robotstxt-manager' ), $dep_slug );
|
|
|
|
return new WP_Error(
|
|
'robotstxt_manager_dependency',
|
|
$detail . ' ' . $result->get_error_message()
|
|
);
|
|
}
|
|
|
|
$installed_names[] = $result;
|
|
}
|
|
|
|
if ( array() === $installed_names ) {
|
|
return '';
|
|
}
|
|
|
|
/* translators: %s: list of installed dependency names. */
|
|
return sprintf( __( 'Installed required plugins: %s.', 'robotstxt-manager' ), implode( ', ', $installed_names ) );
|
|
}
|
|
|
|
/**
|
|
* Installs a single dependency: catalog plugin via the store, else wp.org.
|
|
*
|
|
* @param string $dep_slug Dependency slug.
|
|
*
|
|
* @return string|WP_Error The dependency's display name on success.
|
|
*/
|
|
private function install_one_dependency( string $dep_slug ) {
|
|
if ( null !== $this->find_catalog_entry( $dep_slug ) ) {
|
|
return $this->install_via_store( $dep_slug );
|
|
}
|
|
|
|
// Not in the catalog — treat as a WordPress.org plugin.
|
|
return $this->install_via_wordpress_org( $dep_slug );
|
|
}
|
|
|
|
/**
|
|
* Installs a dependency that exists in the store catalog.
|
|
*
|
|
* @param string $dep_slug Dependency slug.
|
|
*
|
|
* @return string|WP_Error Dependency name on success.
|
|
*/
|
|
protected function install_via_store( string $dep_slug ) {
|
|
$result = $this->download_and_install( $dep_slug );
|
|
|
|
if ( is_wp_error( $result ) ) {
|
|
return $result;
|
|
}
|
|
|
|
$entry = $this->find_catalog_entry( $dep_slug );
|
|
|
|
return $this->entry_name( is_array( $entry ) ? $entry : null, $dep_slug );
|
|
}
|
|
|
|
/**
|
|
* Installs a dependency that is not in the catalog from WordPress.org.
|
|
*
|
|
* @param string $dep_slug wp.org plugin slug.
|
|
*
|
|
* @return string|WP_Error Plugin name on success.
|
|
*/
|
|
protected function install_via_wordpress_org( string $dep_slug ) {
|
|
return $this->install_wporg_zip( $dep_slug );
|
|
}
|
|
|
|
/**
|
|
* Resolves a plugin's dependency slugs (parsed, deduplicated, deps-first
|
|
* order, self-references dropped).
|
|
*
|
|
* @param string $slug Plugin slug.
|
|
*
|
|
* @return list<string> Dependency slugs.
|
|
*/
|
|
private function dependencies_for( string $slug ): array {
|
|
$all = array( $slug => true );
|
|
$queue = array( $slug );
|
|
$ordered = array();
|
|
|
|
while ( ! empty( $queue ) ) {
|
|
$current = array_shift( $queue );
|
|
|
|
foreach ( $this->raw_dependencies_of( $current ) as $dep ) {
|
|
if ( isset( $all[ $dep ] ) ) {
|
|
continue; // Already seen: self, duplicate, or cycle.
|
|
}
|
|
|
|
$all[ $dep ] = true;
|
|
$ordered[] = $dep;
|
|
$queue[] = $dep;
|
|
}
|
|
}
|
|
|
|
// Dependencies discovered later are deeper — reverse so dependencies
|
|
// of dependencies install first.
|
|
return array_reverse( $ordered );
|
|
}
|
|
|
|
/**
|
|
* Reads the raw requires-plugins list of a catalog entry.
|
|
*
|
|
* @param string $slug Plugin slug.
|
|
*
|
|
* @return list<string> Dependency slugs (unresolved).
|
|
*/
|
|
private function raw_dependencies_of( string $slug ): array {
|
|
$entry = $this->find_catalog_entry( $slug );
|
|
|
|
if ( null === $entry ) {
|
|
return array(); // wp.org plugin: dependencies come from its own headers on install.
|
|
}
|
|
|
|
$raw = $entry['requires_plugins'] ?? '';
|
|
$raw = is_string( $raw ) ? $raw : '';
|
|
|
|
if ( '' === $raw ) {
|
|
return array();
|
|
}
|
|
|
|
$slugs = array();
|
|
foreach ( explode( ',', $raw ) as $part ) {
|
|
$dep = sanitize_key( trim( $part ) );
|
|
if ( '' !== $dep ) {
|
|
$slugs[ $dep ] = true;
|
|
}
|
|
}
|
|
|
|
return array_keys( $slugs );
|
|
}
|
|
|
|
/**
|
|
* Installs a WordPress.org plugin by slug via plugins_api + Plugin_Upgrader.
|
|
*
|
|
* @param string $slug wp.org plugin slug.
|
|
*
|
|
* @return string|WP_Error Plugin name on success.
|
|
*/
|
|
private function install_wporg_zip( string $slug ) {
|
|
$this->ensure_plugin_functions();
|
|
|
|
if ( ! function_exists( 'plugins_api' ) ) {
|
|
require_once ABSPATH . 'wp-admin/includes/plugin-install.php';
|
|
}
|
|
|
|
$api = plugins_api(
|
|
'plugin_information',
|
|
array(
|
|
'slug' => $slug,
|
|
'fields' => array(
|
|
'sections' => false,
|
|
'versions' => false,
|
|
'downloaded' => false,
|
|
'rating' => false,
|
|
),
|
|
)
|
|
);
|
|
|
|
if ( is_wp_error( $api ) ) {
|
|
/* translators: %s: error message from WordPress.org. */
|
|
return new WP_Error( 'robotstxt_manager_wporg', sprintf( __( 'WordPress.org lookup failed: %s', 'robotstxt-manager' ), $api->get_error_message() ) );
|
|
}
|
|
|
|
$download_link = is_object( $api ) && isset( $api->download_link ) && is_string( $api->download_link )
|
|
? $api->download_link
|
|
: '';
|
|
|
|
if ( '' === $download_link ) {
|
|
/* translators: %s: plugin slug. */
|
|
return new WP_Error( 'robotstxt_manager_wporg', sprintf( __( 'No download found on WordPress.org for %s.', 'robotstxt-manager' ), $slug ) );
|
|
}
|
|
|
|
$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' ) );
|
|
}
|
|
|
|
$response = wp_remote_get(
|
|
$download_link,
|
|
array(
|
|
'timeout' => 300,
|
|
'stream' => true,
|
|
'filename' => $tmp_file,
|
|
)
|
|
);
|
|
|
|
if ( is_wp_error( $response ) ) {
|
|
wp_delete_file( $tmp_file );
|
|
|
|
/* translators: %s: HTTP transport error message. */
|
|
return new WP_Error( 'robotstxt_manager_download', sprintf( __( 'Download failed: %s', 'robotstxt-manager' ), $response->get_error_message() ) );
|
|
}
|
|
|
|
if ( 200 !== (int) wp_remote_retrieve_response_code( $response ) ) {
|
|
wp_delete_file( $tmp_file );
|
|
|
|
/* translators: %d: HTTP status code. */
|
|
return new WP_Error( 'robotstxt_manager_http', sprintf( __( 'Download failed (HTTP %d).', 'robotstxt-manager' ), (int) wp_remote_retrieve_response_code( $response ) ) );
|
|
}
|
|
|
|
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' => false,
|
|
'overwrite_package' => false,
|
|
)
|
|
);
|
|
|
|
wp_delete_file( $tmp_file );
|
|
|
|
if ( true !== $result ) {
|
|
$detail = ( $result instanceof WP_Error ) ? $result->get_error_message() : '';
|
|
|
|
/* translators: %s: upgrader error message. */
|
|
return new WP_Error( 'robotstxt_manager_install', '' !== $detail ? sprintf( __( 'Installation failed: %s', 'robotstxt-manager' ), $detail ) : __( 'Installation failed.', 'robotstxt-manager' ) );
|
|
}
|
|
|
|
$file = $this->find_plugin_file( $slug );
|
|
|
|
if ( '' === $file ) {
|
|
/* translators: %s: plugin slug. */
|
|
return new WP_Error( 'robotstxt_manager_wporg', sprintf( __( 'The downloaded plugin for %s does not have the expected folder structure.', 'robotstxt-manager' ), $slug ) );
|
|
}
|
|
|
|
$data = get_plugins();
|
|
$raw_name = isset( $data[ $file ]['Name'] ) && is_string( $data[ $file ]['Name'] ) ? $data[ $file ]['Name'] : '';
|
|
|
|
return '' !== $raw_name ? $raw_name : $slug;
|
|
}
|
|
|
|
/**
|
|
* 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 — or, when a
|
|
* security patch is declared for the exact installed version (Core
|
|
* 1.16.0+), to that patch instead of the feature mainline.
|
|
*
|
|
* @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' ) );
|
|
}
|
|
|
|
$patch = '';
|
|
|
|
$entry = $this->find_catalog_entry( $slug );
|
|
|
|
if ( is_array( $entry ) ) {
|
|
$all = get_plugins();
|
|
$version = isset( $all[ $file ]['Version'] ) && is_string( $all[ $file ]['Version'] ) ? $all[ $file ]['Version'] : '';
|
|
$patch = Robotstxt_Manager_Updater::security_patch_for( $entry, $version );
|
|
}
|
|
|
|
$result = $this->download_and_install( $slug, true, $patch );
|
|
|
|
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( is_multisite() ? 'manage_network_options' : '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).
|
|
* @param string $security_version Patch version to download instead of the
|
|
* stable mainline (Core 1.16.0+), '' for stable.
|
|
*
|
|
* @return true|WP_Error True on success.
|
|
*/
|
|
private function download_and_install( string $slug, bool $overwrite = false, string $security_version = '' ) {
|
|
$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), with
|
|
// this site's domain for per-domain license binding (Core 1.11.0+).
|
|
// Security patches always stream through the endpoint with a version
|
|
// parameter — the public URL only carries the mainline stable ZIP.
|
|
$use_download_endpoint = '' !== $security_version || ! ( $is_free && '' !== $dl_url );
|
|
|
|
if ( $use_download_endpoint ) {
|
|
$dl_args = array(
|
|
'domain' => rawurlencode( $this->site_domain() ),
|
|
);
|
|
|
|
if ( '' !== $security_version ) {
|
|
$dl_args['version'] = rawurlencode( $security_version );
|
|
}
|
|
|
|
$zip_url = add_query_arg(
|
|
$dl_args,
|
|
$client->get_store_url() . '/wp-json/robotstxt-core/v1/plugins/' . rawurlencode( $slug ) . '/download'
|
|
);
|
|
} else {
|
|
$zip_url = $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_site_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 );
|
|
|
|
// Surface the store's own error message (e.g. the per-domain
|
|
// license "change the domain in your account" explanation).
|
|
$body = json_decode( wp_remote_retrieve_body( $response ), true );
|
|
$detail = is_array( $body ) && isset( $body['message'] ) && is_string( $body['message'] ) ? $body['message'] : '';
|
|
|
|
return new WP_Error(
|
|
'robotstxt_manager_http',
|
|
'' !== $detail
|
|
? sprintf(
|
|
/* translators: 1: HTTP status code, 2: store error message. */
|
|
__( 'Download failed (HTTP %1$d): %2$s', 'robotstxt-manager' ),
|
|
$code,
|
|
$detail
|
|
)
|
|
: 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(
|
|
// Option key differs across WP versions: 'overwrite' (5.5-era)
|
|
// vs 'overwrite_package' (current). Pass both; the unused key
|
|
// is ignored by wp_parse_args().
|
|
'overwrite' => $overwrite,
|
|
'overwrite_package' => $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' ) ) {
|
|
// class-wp-upgrader.php bundles WP_Upgrader + the skins (incl.
|
|
// Automatic_Upgrader_Skin), but Plugin_Upgrader itself lives in
|
|
// its own file since the WP 5.3 split — both are required.
|
|
require_once ABSPATH . 'wp-admin/includes/class-wp-upgrader.php';
|
|
require_once ABSPATH . 'wp-admin/includes/class-plugin-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 ),
|
|
),
|
|
( is_multisite() ? network_admin_url( 'admin.php' ) : admin_url( 'admin.php' ) )
|
|
)
|
|
);
|
|
exit;
|
|
}
|
|
|
|
/**
|
|
* Returns the normalised domain of the current site.
|
|
*
|
|
* @return string Domain (e.g. 'example.com').
|
|
*/
|
|
private function site_domain(): string {
|
|
$host = strtolower( (string) wp_parse_url( home_url(), PHP_URL_HOST ) );
|
|
|
|
// Strip the literal "www." prefix (ltrim would eat any leading w/).
|
|
return (string) preg_replace( '/^www\./', '', $host );
|
|
}
|
|
|
|
/**
|
|
* 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 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 );
|
|
}
|
|
}
|