From e80820f41612d053ea82c3486a1649b3ba62c8ec Mon Sep 17 00:00:00 2001 From: Javier Casares Date: Mon, 17 Aug 2026 16:03:10 +0000 Subject: [PATCH] v1.1.0 --- admin/class-robotstxt-manager-installer.php | 288 +++++++++++++++++++- admin/views/page-catalog.php | 25 ++ changelog.txt | 17 ++ languages/robotstxt-manager-ca.mo | Bin 9816 -> 10430 bytes languages/robotstxt-manager-ca.po | 18 ++ languages/robotstxt-manager-es_ES.mo | Bin 9783 -> 10405 bytes languages/robotstxt-manager-es_ES.po | 18 ++ languages/robotstxt-manager.pot | 18 ++ readme.txt | 10 +- robotstxt-manager.php | 4 +- 10 files changed, 389 insertions(+), 9 deletions(-) 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 20a059df8c8f3021abd939b05a2d0d291045be66..dfd0845f796c12e6bb8cd4ce0d9065d0084a3d73 100644 GIT binary patch delta 2355 zcmZY9Z)_Ar7{~FsUMWyp{*)pu0v#wVRHT5Vs6~_(TBNpx))J#)blcmOlk07}x2M%; zE)gX$ijY)Mo?o2F&bmk_zzzwB=W}8H@r~4zrCvjoOJFpvpc)< z%rmq7w&PTH=BKj4gND*eT|hlqV9W^aoWc*qPBrFjT#Tjo1=iz_cpa8ZGo~0T@JX!3 z42~m1jGb;w9WKZD*n@{~2#Yjy3QsfEl;A>~i+$LDQG6K{%PTk!$FU4QK>p0xyx-s| z`sYy-&L%pG>v1;j#2J{xb@&uMqEO$X!Jnxr$wjyV=g@D%LcAaMMvQp~kJJB+G|)6h zc$-%ECYIy7r~p69_kYGJ`V&}*?3Ni@m5+e~(Jp1te7_ z$}%dj7`0W^sLX6cvT52dh1-!=nlq?CzeX+ae7=7PmGR4%313V;^@3v5i_1|HtwF7@ z9reN%=G-8$m5BO?sMf9QFLKsCh4ylYgaTf(x2x-rU^E zt5B(1iR-Zw8DgG81$Gp*vJ?0W6P>}6^tY0Kt+a?iH)9zp@K)5sJMblZ828~{735#3 z*uw_hiZ9?|`~=^}-%*hsVISmcsQYi>Af7`7-grYUpbe-6wIeZ^A=JcsP#Jy^HP2yO zjbCPH+(e^*yzB6+Lb7JI;BEK-?#F|u4Ak(6EW#$#`1?=`7{QBV;tBkPevI@fL#;L! zKo2sNc@Xvds$l^8K}_y=+09@-E~{%+F#^96;%wgX28Ff-`r>d-@_J*z8 zInej215}!Qz3A`gqIfbDIEjP}cDdGfN78ZMjoINudN7`}HL2{O;#1KD8oHZ+?XHUnVHXxgfvt zimGjqhAz*hmhEzE;Cp>eVEdh6+vmhbotR^tYmzWbz)qM3dp=UCG`PMS9}PomT{1n= z-1=WQ;VS|=`2(c`>|D+B{{r=BOOF5m delta 1782 zcmXxkduYvJ9LMqR*_mUti)_@hRv zA%+nLlHtd6Ea0gz(2!;~O=-LGwjU6}yU*K{4 zflL{(X1^JIYr_mYiEHpCdNGsPdto*X!Vo57De}+iVy?sqj2keBCvX7Xz$AQth4>OT zU^2T;)K+)W(a!c`3Z6qHe;xOF%)0Oz07Sho~%aH2WdaT7QSdQJO4E)sHOGU+W72sHGMBRmUBuVSUV(i8iw4u?=4`C+5^AS4w!X4DkU*Tse@B!a4e$125 zMmqRyE!>6t$nK#gc+ZD6=FQ^XU^;5Uc{md5@C$CoQ&=C2z6Kvzlaoc#=%^GEkl(RF z)H7UydIeiinQp^F*o0arl~r`6V^A9`MqR#2)Mc$j?x8Ki(b#~RzZJEfgIuGC-H3MV zAvW@Zx3~anxN%`@$2^bOU7W=DXO39~hI69}ZbnsR2adsgs3UZc1?(nH#YY%KKV@MF z3u7O>|7tqS?=~&0rVOVMI=ga08CDT$1%xJ6^Ad4xFPe3%>sF$g!F-~I&=LJ_Eax^I zw~zJRzgoK0I*%EI%C9q4n@N-qL1H4IyYjy&Yh6CIQbI38jY7Fi)d&&uh*1Qu*lmS; z>S*-0ATpVc3St~Fn^04k)pTw}gvv?v+*THoKVohh8&mLVZBA@FH|BWMxmOaLoO@|? z9I=+~K0lqaE|Z>29x;pHsk#?XXQeis;1!wQ$>{yp>r^CmdYwW2(>%`7z%ftjuL1X+ pq?D{Ur#kJ3&j}1W9q;r6t3A%*>^B~#JSV~9H0SP$Yb^*J_y;W0pP&E$ 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 9b6d0bd652a507ae2403051a2f7244edf96e5dad..763f8f0602fa7ca2c9067e3deb392ae026b649e5 100644 GIT binary patch delta 2365 zcmZwITWl0n9LMp~8(?XHLJqzl?uY5+CkjSmg7D=T4GEeeoXx*(m>I? zLvLE)`&f$~peFcLT>la4so%vqtetAiLTttc?83ZKm8Gx@mrgV02=<^-{XHsW<4CGZ z8OxZBm8h+1KxJkVl11RU*h|F=N4Afhx55VfDhsk)WC0}#<_qxe3x)J zj^`*Wp-@ZSwZe@^)=W39#Q{8n=TLjNo>xSfX+suaTvRHD@CKPUg14#n(ny)uWD9%U zi8_RPkgiM`HD2x*1+Dlz>hyjQ*Kfv}1;+f${RZ5ClWF`ewxb3ZM?Lo&Y9iGf0Onw7 zu@P6H?mvghKp(z|N#wbl`JI9~nzE>{!nvsWVpQtapgL$pUIeohmtqep6GNzW=TMn@ z50$Aas4e;uci>89(}AzxGW;4Bm9YQ6Qdq@}*^4>pxD(aEDb(JbNA1-`)NAt{Ds^M1 zt@{I8&{9!qdr=d68P(5QSb`G?U7S_JoT4!|Fn;vPfO(jpZ8S^O*AiOsB;pxDv*wjG zZNz%wS%Tjd^BAEC>e6ZNic0)s;}dat7i#}E5;~y8E4Q1HX1$3}npYFrKdqpQs35ix zN}=9)T}t!)mol(|Aeqt6u)g1}53ZF2KM~RORIJ#l{pSse_FFfUeyal4PGU2mLsL%Z zv~$Lye?IM~Gf=!rFNaasGf zj@Uv}=SL>oE1RG6!iL{KQ|=u5T?q z{O`bp<~HHGq4oTXJ>b0HMl*C@O1u3T8V>lWgd5l}6J+}{*&sh!(^Ojj-!W6Z|9m!W z2b?66*Ie`CGcHW1FD%#zteYyXQwwv0WWraY*XJBBUl6T7j8+_d5!(2%gdZ=vrhZ$= z`dz*aUF&-NzLyC`^VUmxgIUk92{-I_fq({A7-ocx-Gq0%KCj@>_cC1h=h|Cz}E Nn>w10hQ8Ez^lyMoQnUa7 delta 1781 zcmX}sT};nW9LMqR|Nrk%qz8$p{-u&A@=z(El*cl35u2wmlel0mJ^U`h*lI2+#>O-c z(;91j3uCdl=mK|5Ha24}nhS2og=F5J-~Y#%^Lw51*YBL~`JQurx3@lOa6Y6&bsJ?f zbvCs!ZmOmNCQ7emM)M=Q63M}96SL00j+b|c8;2gY& zF1mOY4^AbcO4U~y3Sb;biA_&3n}M0AEh$4~pb1He?ZD%>A9=;zq5>O5VznQrjB5EB zFCO(?5^B5*)cED-XeC={XrS##cB}=r;c=|RA=C=u$$N;56k`$nQqom}J5ldlM@`s^ zT6sTe{9)ADd5=~21(mt{Y2;s#RMOK5ny{M*_Tn@8Khq+=4@@^(ME?aU(C?@TL*)G` zCSnKnqE=eYpewKj7078Ez#ddU&FqidGn4#>X&mH-Ry^<@EV4r<~G z)I@8r3>&c!&!ARz4_TzWz;YbHtC%<|vPHd^O@F|lK~VMvHQ_fLAp>Lhnf`08gfi1l z6x!24Bqkd|?d3<*gcHc(Ern6F;v(Nh9HoC0dvJR=GTtxL-#f86k%_ZW*RKHcvCjX! z6}8f1*p4SqFUB#80t=%KT`?-9%TWudLC&GA$9cE|iNV@Xe|HnfkYjx`G{AGz7QDg+ zEaSu#VH@TJ%JG?}L z@x8LiS9`4kw47Q?)k+nV%2Fy9z$+o1LBAdCD->8GRe^4%a*qEOrt^w|jA4H7tEE$| zeO*b_Hn9C(S;bS)hpDAhrT$;hsw@1?YE*VqNR(HU4IRo2)VWk{u~&3pwKd9yvy4YQ zwU}B*Rap|j+qZJs+Ap%^l^S0@VqRI`E0peaet*4h2`b%dsqCD0n<~*Si!Fy%5mg@u zT^Zf;HB{XSeTF#>Ua98EEwXqwGxkr&txtFma?_`#1>EMOOM%W`$uHf+)a;