This commit is contained in:
Javier Casares 2026-08-17 16:03:10 +00:00
commit e80820f416
10 changed files with 389 additions and 9 deletions

View file

@ -54,19 +54,297 @@ class Robotstxt_Manager_Installer {
$name = $this->entry_name( $entry, $slug ); $name = $this->entry_name( $entry, $slug );
$this->ensure_plugin_functions(); $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 ); $result = $this->download_and_install( $slug );
if ( is_wp_error( $result ) ) { if ( is_wp_error( $result ) ) {
$this->redirect_error( $result->get_error_message() ); $this->redirect_error( $result->get_error_message() );
} }
$this->redirect_success( $message = sprintf(
sprintf( /* translators: %s: plugin name. */
/* translators: %s: plugin name. */ __( '%s installed. Activate it from the list below.', 'robotstxt-manager' ),
__( '%s installed. Activate it from the list below.', 'robotstxt-manager' ), $name
$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;
} }
/** /**

View file

@ -135,6 +135,18 @@ $compat_warnings = 0;
$website_url = '' !== $page_url ? $page_url : $homepage; $website_url = '' !== $page_url ? $page_url : $homepage;
$has_detail = ( '' !== $website_url || '' !== $desc ); $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. // Compatibility checks.
$wp_ok = '' === $req_wp || version_compare( $local_wp_version, $req_wp, '>=' ); $wp_ok = '' === $req_wp || version_compare( $local_wp_version, $req_wp, '>=' );
$php_ok = '' === $req_php || version_compare( $local_php_version, $req_php, '>=' ); $php_ok = '' === $req_php || version_compare( $local_php_version, $req_php, '>=' );
@ -251,6 +263,19 @@ $compat_warnings = 0;
<?php if ( '' !== $desc ) : ?> <?php if ( '' !== $desc ) : ?>
<span class="robotstxt-manager-detail-description"><?php echo esc_html( $desc ); ?></span> <span class="robotstxt-manager-detail-description"><?php echo esc_html( $desc ); ?></span>
<?php endif; ?> <?php endif; ?>
<?php if ( array() !== $req_slugs ) : ?>
<span class="robotstxt-manager-detail-requires">
<?php
echo esc_html(
sprintf(
/* translators: %s: list of plugin slugs. */
__( 'Requires: %s', 'robotstxt-manager' ),
implode( ', ', $req_slugs )
)
);
?>
</span>
<?php endif; ?>
</td> </td>
</tr> </tr>
<?php endif; ?> <?php endif; ?>

View file

@ -1,5 +1,22 @@
== Changelog == == 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 = = 1.0.0 =
_Release date: 2026-08-15_ _Release date: 2026-08-15_

Binary file not shown.

View file

@ -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." 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." 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"

Binary file not shown.

View file

@ -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." 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." 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"

View file

@ -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." 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 "" 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 ""

View file

@ -3,9 +3,9 @@ Contributors: javiercasares, robotstxt
Tags: dashboard, catalog, updates, subscriptions, management Tags: dashboard, catalog, updates, subscriptions, management
Requires at least: 4.4 Requires at least: 4.4
Tested up to: 7.1 Tested up to: 7.1
Stable tag: 1.0.0 Stable tag: 1.1.0
Requires PHP: 8.0 Requires PHP: 8.0
Version: 1.0.0 Version: 1.1.0
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
@ -92,6 +92,12 @@ Encrypted at rest using AES-256-CBC with a key derived from your site's WordPres
== Changelog == == 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 = = 1.0.0 =
_Release date: 2026-08-15_ _Release date: 2026-08-15_

View file

@ -3,7 +3,7 @@
* 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 installs, activates, and updates plugins directly from the store. * 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 at least: 4.4
* Requires PHP: 8.0 * Requires PHP: 8.0
* Author: ROBOTSTXT * Author: ROBOTSTXT
@ -21,7 +21,7 @@ if ( ! defined( 'ABSPATH' ) ) {
} }
/** Plugin version. */ /** 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. */ /** 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__ ) );