diff --git a/admin/class-robotstxt-manager-installer.php b/admin/class-robotstxt-manager-installer.php index 3ad4bb3..2714e78 100644 --- a/admin/class-robotstxt-manager-installer.php +++ b/admin/class-robotstxt-manager-installer.php @@ -54,19 +54,297 @@ class Robotstxt_Manager_Installer { $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() ); } - $this->redirect_success( - sprintf( - /* translators: %s: plugin name. */ - __( '%s installed. Activate it from the list below.', 'robotstxt-manager' ), - $name + $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 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 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; } /** diff --git a/admin/views/page-catalog.php b/admin/views/page-catalog.php index d419227..7a79180 100644 --- a/admin/views/page-catalog.php +++ b/admin/views/page-catalog.php @@ -135,6 +135,18 @@ $compat_warnings = 0; $website_url = '' !== $page_url ? $page_url : $homepage; $has_detail = ( '' !== $website_url || '' !== $desc ); + $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, '>=' ); @@ -251,6 +263,19 @@ $compat_warnings = 0; + + + + + diff --git a/changelog.txt b/changelog.txt index 56d6dfd..58d7465 100644 --- a/changelog.txt +++ b/changelog.txt @@ -1,5 +1,22 @@ == Changelog == += 1.1.0 = + +_Release date: 2026-08-15_ + +**Added** + +* Cascade dependency install: when installing a plugin whose catalog entry declares `requires_plugins` (Core 1.7.0+), Manager first installs every missing dependency — ecosystem plugins through the store flow, WordPress.org plugins (e.g. Action Scheduler) via `plugins_api` and their repository ZIPs — dependencies of dependencies first, cycle-safe. The success notice lists what was installed; failures name the dependency and abort before the main install. Catalog rows show a "Requires: …" hint in the detail line. + +**Changed** + +* Plugin version 1.0.0 → 1.1.0. No database schema changes (no custom tables). + +**Compatibility** + +* WordPress: 4.4 - 7.1 (scan-verified: wp-compat clean from 4.4) +* PHP: 8.0 - 8.5 (scan-verified: PHPCompatibility + manual feature audit) + = 1.0.0 = _Release date: 2026-08-15_ diff --git a/languages/robotstxt-manager-ca.mo b/languages/robotstxt-manager-ca.mo index 20a059d..dfd0845 100644 Binary files a/languages/robotstxt-manager-ca.mo and b/languages/robotstxt-manager-ca.mo differ diff --git a/languages/robotstxt-manager-ca.po b/languages/robotstxt-manager-ca.po index cd3e9bf..da6c088 100644 --- a/languages/robotstxt-manager-ca.po +++ b/languages/robotstxt-manager-ca.po @@ -249,3 +249,21 @@ msgstr "Pagaments, i com funciona" msgid "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." msgstr "Els plugins gratuïts s'instal·len a l'instant i sense cost. Els plugins premium són subscripcions anuals: pagues una vegada al nostre lloc web i la subscripció es renova automàticament cada any fins que la cancellis. Pots cancel·lar-la en qualsevol moment des de la teva pàgina de compte de ROBOTSTXT; l'accés segueix funcionant fins al final del període pagat. Tots els pagaments els processa de manera segura Mollie; nosaltres mai no veiem ni desem les dades de la teva targeta." + +msgid "Installed required plugins: %s." +msgstr "Plugins requerits instal·lats: %s." + +msgid "Could not install the required plugin %s." +msgstr "No s'ha pogut instal·lar el plugin requerit %s." + +msgid "WordPress.org lookup failed: %s" +msgstr "La cerca a WordPress.org ha fallat: %s" + +msgid "No download found on WordPress.org for %s." +msgstr "No s'ha trobat cap baixada a WordPress.org per a %s." + +msgid "The downloaded plugin for %s does not have the expected folder structure." +msgstr "El plugin baixat per a %s no té lestructura de carpetes esperada." + +msgid "Requires: %s" +msgstr "Requereix: %s" diff --git a/languages/robotstxt-manager-es_ES.mo b/languages/robotstxt-manager-es_ES.mo index 9b6d0bd..763f8f0 100644 Binary files a/languages/robotstxt-manager-es_ES.mo and b/languages/robotstxt-manager-es_ES.mo differ diff --git a/languages/robotstxt-manager-es_ES.po b/languages/robotstxt-manager-es_ES.po index 3059230..35554f4 100644 --- a/languages/robotstxt-manager-es_ES.po +++ b/languages/robotstxt-manager-es_ES.po @@ -249,3 +249,21 @@ msgstr "Pagos, y cómo funciona" msgid "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." msgstr "Los plugins gratis se instalan al instante y sin coste. Los plugins premium son suscripciones anuales: pagas una vez en nuestro sitio web y la suscripción se renueva automáticamente cada año hasta que la canceles. Puedes cancelarla en cualquier momento desde tu página de cuenta de ROBOTSTXT; el acceso sigue funcionando hasta el final del periodo pagado. Todos los pagos los procesa de forma segura Mollie; nosotros nunca vemos ni guardamos los datos de tu tarjeta." + +msgid "Installed required plugins: %s." +msgstr "Plugins requeridos instalados: %s." + +msgid "Could not install the required plugin %s." +msgstr "No se pudo instalar el plugin requerido %s." + +msgid "WordPress.org lookup failed: %s" +msgstr "La búsqueda en WordPress.org falló: %s" + +msgid "No download found on WordPress.org for %s." +msgstr "No se encontró ninguna descarga en WordPress.org para %s." + +msgid "The downloaded plugin for %s does not have the expected folder structure." +msgstr "El plugin descargado para %s no tiene la estructura de carpetas esperada." + +msgid "Requires: %s" +msgstr "Requiere: %s" diff --git a/languages/robotstxt-manager.pot b/languages/robotstxt-manager.pot index 2118156..dffb2a3 100644 --- a/languages/robotstxt-manager.pot +++ b/languages/robotstxt-manager.pot @@ -248,3 +248,21 @@ msgstr "" msgid "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." msgstr "" + +msgid "Installed required plugins: %s." +msgstr "" + +msgid "Could not install the required plugin %s." +msgstr "" + +msgid "WordPress.org lookup failed: %s" +msgstr "" + +msgid "No download found on WordPress.org for %s." +msgstr "" + +msgid "The downloaded plugin for %s does not have the expected folder structure." +msgstr "" + +msgid "Requires: %s" +msgstr "" diff --git a/readme.txt b/readme.txt index 67dc346..7b135d2 100644 --- a/readme.txt +++ b/readme.txt @@ -3,9 +3,9 @@ Contributors: javiercasares, robotstxt Tags: dashboard, catalog, updates, subscriptions, management Requires at least: 4.4 Tested up to: 7.1 -Stable tag: 1.0.0 +Stable tag: 1.1.0 Requires PHP: 8.0 -Version: 1.0.0 +Version: 1.1.0 License: GPL-3.0-or-later License URI: https://www.gnu.org/licenses/gpl-3.0.txt @@ -92,6 +92,12 @@ Encrypted at rest using AES-256-CBC with a key derived from your site's WordPres == Changelog == += 1.1.0 = + +_Release date: 2026-08-15_ + +* Cascade dependency install: required plugins (ecosystem or WordPress.org, e.g. Action Scheduler) are installed automatically before the plugin; "Requires:" hints in the catalog. + = 1.0.0 = _Release date: 2026-08-15_ diff --git a/robotstxt-manager.php b/robotstxt-manager.php index f814d46..dbac845 100644 --- a/robotstxt-manager.php +++ b/robotstxt-manager.php @@ -3,7 +3,7 @@ * Plugin Name: Manager (by ROBOTSTXT) * 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 installs, activates, and updates plugins directly from the store. - * Version: 1.0.0 + * Version: 1.1.0 * Requires at least: 4.4 * Requires PHP: 8.0 * Author: ROBOTSTXT @@ -21,7 +21,7 @@ if ( ! defined( 'ABSPATH' ) ) { } /** Plugin version. */ -define( 'ROBOTSTXT_MANAGER_VERSION', '1.0.0' ); +define( 'ROBOTSTXT_MANAGER_VERSION', '1.1.0' ); /** Absolute path to the plugin directory, with trailing slash. */ define( 'ROBOTSTXT_MANAGER_DIR', plugin_dir_path( __FILE__ ) );