├── CHANGES.md ├── LICENSE ├── README.md ├── composer.json ├── plugin.php ├── readme.txt ├── src ├── Init_Plugin.php ├── wp-admin │ ├── css │ │ └── wp-plugin-dependencies.css │ ├── includes │ │ ├── class-pd-install-list-table.php │ │ ├── class-pd-list-table.php │ │ └── plugin-install.php │ └── js │ │ └── updates.js └── wp-includes │ └── class-wp-plugin-dependencies.php └── test-plugins ├── circlea └── circlea.php ├── circleb └── circleb.php ├── test-dependencies1.php └── test-dependencies2.php /CHANGES.md: -------------------------------------------------------------------------------- 1 | [unreleased] 2 | #### 3.0.4 / 2024-02-09 3 | * update kill switch 4 | 5 | #### 3.0.1 / 2023-11-21 6 | * fix for multisite, too many `%s` 7 | 8 | #### 3.0.0 / 2023-10-06 9 | * override `WP_Plugins_List_Table` to add filter and restructuring of PR 10 | * convert to static class 11 | * update plugin card description for clarity 12 | * update Requires WP to 6.4 due to `wp_admin_notice()` use 13 | * many more updates to coincide with refactoring of PR 14 | 15 | #### 2.0.2 / 2023-08-18 16 | * add single file plugin to `$plugin_dirnames` 17 | 18 | #### 2.0.1 / 2023-08-16 19 | * cleanup 20 | 21 | #### 2.0.0 / 2023-08-08 22 | * remove Dependencies tab, Manage Dependencies link, etc, per @azaozz 23 | * skip associated PHPUnit tests 24 | * increase scope to protected for many things 25 | * remove `class Init`, not needed 26 | * deactivate buttons, don't change text 27 | 28 | #### 1.14.3 / 2023-70-30 29 | * add null coalesce 30 | * require PHP 7.0 31 | * make commit guard more permissive 32 | 33 | #### 1.14.2 / 2023-07-20 34 | * update guard in `get_dependency_filepaths()` 35 | 36 | #### 1.14.1 / 2023-07-20 37 | * update modal button on plugin-install.php 38 | 39 | #### 1.14.0 / 2023-07-19 40 | * update _More details_ link 41 | * fixed strange error between slug from different sources in PD part 2 42 | * update JS to correctly display Plugin Card button, thanks @costdev 43 | 44 | #### 1.13.0 / 2023-07-10 45 | * update version check 46 | * simplify plugin card notice 47 | 48 | #### 1.12.1 / 2023-07-01 49 | * extra life to 6.4-beta1 50 | 51 | #### 1.12.0 / 2023-05-21 52 | * change plugin card button to 'Cannot Install' if dependencies not met 53 | * override `WP_Plugin_Install_List_Table::display_rows()` to use our refactored `wp_get_plugin_action_button()` 54 | 55 | #### 1.11.0 / 2023-05-21 56 | * add **Requires:** data to plugin cards of uninstalled plugins where repo plugins have `Requires Plugins` header set 57 | * add temporary style kludge to above 58 | * add caching to uninstalled plugin data 59 | * abstract code to create plugin install action buttons 60 | 61 | #### 1.10.0 / 2023-04-29 62 | * show `Cannot Install` button in Dependencies tab for dependencies with no package 63 | * return of generic plugins_api() response to it's own hook, avoids having to hide items in plugin card 64 | * add more data to generic plugin card 65 | * update for WP-CLI 66 | * no need to start on hook 67 | 68 | #### 1.9.0 / 2023-04-10 69 | * ensure WP 6.0 compatibility with `move_dir()` 70 | * use JSON in plugin root for non-dot org dependencies _acceptable_ for dot org 🤞 71 | * update test plugins 72 | * run hooks during AJAX in case you really want an Install to happen 73 | * update regex to strictly follow plugin repository slug format with tests 74 | 75 | #### 1.8.0 / 2023-04-07 76 | * update to work natively with `|` format in `Requires Plugins` header 77 | * split PD and PDv2 into different classes 78 | * add more tests 79 | 80 | #### 1.7.9 / 2023-04-05 81 | * update action link to keep `Cannot Activate | Manage Dependencies` together 82 | * fix for multisite plugin card 83 | 84 | #### 1.7.8 / 2023-03-03 85 | * composer update 86 | 87 | #### 1.7.7 / 2023-02-11 88 | * add a11y that I (@afragen) clearly forgot, it's a start 89 | * fix circular dependency test plugins to have containing folder, dependencies must have a containing folder 90 | 91 | #### 1.7.6 / 2023-02-11 92 | * update `Name` header of test plugins so they can't be mistaken for core plugin after AJAX Install 93 | 94 | #### 1.7.5 / 2023-02-09 95 | * cleanup docblocks 96 | * initialize during class loading 97 | 98 | #### 1.7.4 / 2023-02-08 99 | * composer update 100 | 101 | #### 1.7.3 / 2023-01-30 102 | * composer update using Composer 2.5.0 to avoid bug 103 | 104 | #### 1.7.2 / 2023-01-02 105 | * add unresolvable circular dependency example 106 | * update for PHP standards 107 | 108 | #### 1.7.1 / 2022-10-27 109 | * remove "improved visibility" of `Dependencies` link 110 | 111 | #### 1.7.0 / 2022-10-25 112 | * notification of circular dependencies 113 | * add info text under Dependencies tab, I found a hook 🙌 114 | * display admin notices on specific pages 115 | * added some code improvements, thanks Colin 116 | * add `Requires:` data to plugin card 117 | * modify plugin card action links if dependency not met 118 | * improve visibility of `Dependencies` link 119 | 120 | #### 1.6.2 / 2022-10-18 121 | * composer update better checking in `afragen/add-plugin-dependency-api` 122 | 123 | #### 1.6.1 / 2022-10-18 124 | * more precise check of dependency slug for file path 125 | * don't show admin notice to users who are unable to act upon them 126 | * update composer dependencies 127 | * add skeleton JSON response for Gravity Forms 128 | 129 | #### 1.6.0 / 2022-10-15 130 | * move `plugin_dependency_endpoints` hook outside of class 131 | * composer update 132 | * add filter `wp_plugin_dependencies_slugs` to modify slugs in cases of non-premium plugin replaced with premium plugin 133 | * keep checking plugins API for plugin with generic response 134 | * update conditional for generic response 135 | * update testing plugins 136 | 137 | #### 1.5.1 / 2022-09-02 138 | * fix for actual `gravityforms` slug 139 | 140 | #### 1.5.0 / 2022-09-02 141 | * add `afragen/add-plugin-dependency-api` as composer requirement 142 | * update test plugins removing `hello-dolly` and adding `git-updater` as non-dot org example 143 | * check empty plugin response for error 144 | 145 | #### 1.4.1 / 2022-08-18 146 | * oops, fixed typo in one of the testing plugins 147 | 148 | #### 1.4.0 / 2022-07-28 149 | * bring more inline with PR 150 | * remove action on class requires, use hook 151 | * fix multisite compatibility 152 | 153 | #### 1.3.0 / 2022-07-04 🎆 154 | * fix `get_requires_plugin_names()` to account for empty header 155 | * update regex to allow for some non-ascii languages and symbols as slugs 156 | 157 | #### 1.2.1 / 2022-06-23 158 | * added several single file testing plugins to `test-plugins/` 159 | 160 | #### 1.2.0 / 2022-06-10 161 | * don't display admin notice link to Dependencies tab when on Dependencies tab 162 | * be more specific about only removing dependency plugin row checkbox when a requiring plugin is active 163 | 164 | #### 1.1.1 / 2022-06-06 165 | * limit scope of class methods where we can 166 | * update screenshots 167 | 168 | #### 1.1.0 / 2022-06-02 169 | * change 'Activate' plugin action link to 'Cannot Activate' text when plugin has unmet dependencies 170 | * remove checkbox from plugin row when plugin has unmet dependencies 171 | * use _View details_ link for plugins listed in **Requires:** in plugin row 172 | 173 | #### 1.0.0 / 2022-05-31 🎂 174 | * fix typo 175 | * initial dot org release 176 | 177 | #### 0.16.2 / 2022-05-27 178 | * update requirements to WP 6.0 179 | 180 | #### 0.16.1 / 2022-05-24 181 | * add auto-deactivate for when committed to trunk, will need updating later 182 | 183 | #### 0.16.0 / 2022-05-08 184 | * rename `parse_headers()` to `parse_plugin_headers()`, future proofing 185 | * update unit tests 186 | 187 | #### 0.15.1 / 2022-04-29 188 | * minor cleanup 189 | 190 | #### 0.15.0 / 2022-04-28 191 | * refactor with `get_requires_plugins_names()` 192 | * update admin notice for multisite 193 | 194 | #### 0.14.0 195 | * updated required plugin data expiration 196 | 197 | #### 0.13.1 / 2022-04-25 198 | * fix `parse_headers()` 199 | 200 | #### 0.13.0 / 2022-04-23 201 | * prep for initial release 202 | 203 | #### 0.12.9 / 2022-04-19 204 | * add plugin cards for slugs with no API data 205 | * hide action links and bottom of card in plugin cards for slugs with no API data 206 | 207 | #### 0.12.3 208 | *rename and reschuffle some functions 209 | 210 | #### 0.12.2 / 2022-04-06 211 | * harden a bit 212 | * clean up some testing stuff 213 | * `plugin_install_description` filter committed to core 214 | 215 | #### 0.12.0 / 2022-04-03 216 | * readme.txt 217 | * fix PHP error if no plugins with `Requires Plugins` header found 218 | * only show single, relevant admin notice 219 | 220 | #### 0.11.6.4 221 | * plugin to date with new changelog 222 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Andy Fragen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WordPress Feature Project: Plugin Dependencies 2 | 3 | * Contributors: afragen, costdev, pbiron 4 | * Description: Parses 'Requires Plugins' header, add plugin install dependencies tab, and information about dependencies. 5 | * License: MIT 6 | * Network: true 7 | * Requires at least: 6.0 8 | * Requires PHP: 5.6 9 | 10 | ## Description 11 | 12 | Parses a 'Requires Plugins' header. If a requiring plugin does not have all its dependencies installed and active, it will not activate. 13 | 14 | [Make post for Plugin Dependencies Feature Project](https://make.wordpress.org/core/2022/02/24/feature-project-plugin-dependencies/) 15 | 16 | My solution to [#22316](https://core.trac.wordpress.org/ticket/22316). Feature plugin version of [PR #3032](https://github.com/WordPress/wordpress-develop/pull/3032) 17 | 18 | * Parses the **Requires Plugins** header that defines plugin dependencies using a comma separated list of wp.org slugs. 19 | * In the plugins page, a dependent plugin is unable to be deleted or deactivated if the requiring plugin is active. 20 | * Plugin dependencies can be deactivated or deleted if the requiring plugin is not active. 21 | * Messaging in the plugin row description is inserted; as is data noting which plugins require the dependency. 22 | * Circular dependencies cannot be activated and an admin notice noting the circular dependencies is displayed. 23 | * Ensures that plugins with unmet dependencies cannot be activated. 24 | 25 | There are several single file plugins that may be used for testing in `test-plugins/`. 26 | 27 | ## Pull Requests 28 | 29 | PRs should be made against the `develop` branch. 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wordpress/wp-plugin-dependencies", 3 | "description": "Parses 'Requires Plugins' header and information about dependencies.", 4 | "type": "wordpress-plugin", 5 | "license": "MIT", 6 | "authors": [ 7 | { 8 | "name": "Andy Fragen", 9 | "email": "andy@thefragens.com", 10 | "homepage": "https://thefragens.com", 11 | "role": "Developer" 12 | } 13 | ], 14 | "support": { 15 | "issues": "https://github.com/WordPress/wp-plugin-dependencies/issues", 16 | "source": "https://github.com/WordPress/wp-plugin-dependencies" 17 | }, 18 | "prefer-stable": true, 19 | "require": { 20 | "php": ">=5.6" 21 | }, 22 | "require-dev": { 23 | "wp-coding-standards/wpcs": "^3.0.0" 24 | }, 25 | "config": { 26 | "allow-plugins": { 27 | "dealerdirect/phpcodesniffer-composer-installer": true 28 | } 29 | }, 30 | "scripts": { 31 | "wpcs": [ 32 | "vendor/bin/phpcbf .; vendor/bin/phpcs ." 33 | ] 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /plugin.php: -------------------------------------------------------------------------------- 1 | =' ) 37 | || \class_exists( 'WP_Plugin_Dependencies' ) 38 | ) { 39 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; 40 | deactivate_plugins( __FILE__ ); 41 | return; 42 | } 43 | 44 | require_once __DIR__ . '/src/wp-includes/class-wp-plugin-dependencies.php'; 45 | \WP_Plugin_Dependencies::initialize(); 46 | 47 | // Let's get started. 48 | add_action( 49 | 'plugins_loaded', 50 | function () { 51 | require_once __DIR__ . '/src/Init_Plugin.php'; 52 | Init_Plugin::init(); 53 | } 54 | ); 55 | -------------------------------------------------------------------------------- /readme.txt: -------------------------------------------------------------------------------- 1 | # Plugin Dependencies 2 | 3 | Contributors: afragen, costdev, pbiron 4 | Description: Parses 'Requires Plugins' header, add plugin install dependencies tab, and information about dependencies. 5 | License: MIT 6 | Network: true 7 | Requires at least: 6.4 8 | Requires PHP: 7.0 9 | Tested up to: 6.4 10 | Stable tag: 3.0.4 11 | 12 | ## Description 13 | 14 | Parses a 'Requires Plugins' header. If a requiring plugin does not have all its dependencies installed and active, it will not activate. 15 | 16 | [Make post for Plugin Dependencies Feature Project](https://make.wordpress.org/core/2022/02/24/feature-project-plugin-dependencies/) 17 | 18 | Please open issues at [WordPress/wp-plugin-dependencies issues](https://github.com/WordPress/wp-plugin-dependencies/issues) 19 | 20 | My solution to [#22316](https://core.trac.wordpress.org/ticket/22316). Feature plugin version of [PR #3032](https://github.com/WordPress/wordpress-develop/pull/3032) 21 | 22 | * Parses the **Requires Plugins** header that defines plugin dependencies using a comma separated list of wp.org slugs. To test, you will need to add the header and content to a plugin. 23 | * In the plugins page, a dependent plugin is unable to be deleted or deactivated if the requiring plugin is active. 24 | * Plugin dependencies can be deactivated or deleted if the requiring plugin is not active. 25 | * Messaging in the plugin row description is inserted; as is data noting which plugins require the dependency. 26 | * Ensures that plugins with unmet dependencies cannot be activated. 27 | * Circular dependencies cannot be activated and an admin notice noting the circular dependencies is displayed. 28 | * Ensures that plugins with unmet dependencies cannot be activated. 29 | 30 | There are several single file plugins that may be used for testing in `test-plugins/`. 31 | 32 | ## Pull Requests 33 | 34 | PRs should be made against the `develop` branch. 35 | 36 | ## Screenshots 37 | 38 | 1. Plugin is a Dependency and Plugin needing Dependencies 39 | 2. Plugin with Dependencies 40 | 3. Plugin Dependencies tab 41 | 4. Search page with dependencies 42 | 43 | ## Changelog 44 | 45 | #### 3.0.1 / 2023-11-21 46 | * fix for multisite, too many `%s` 47 | 48 | #### 3.0.0 / 2023-10-06 49 | * override `WP_Plugins_List_Table` to add filter and restructuring of PR 50 | * convert to static class 51 | * update plugin card description for clarity 52 | * update Requires WP to 6.4 due to `wp_admin_notice()` use 53 | * many more updates to coincide with refactoring of PR 54 | 55 | #### 2.0.2 / 2023-08-18 56 | * add single file plugin to `$plugin_dirnames` 57 | 58 | #### 2.0.1 / 2023-08-16 59 | * cleanup 60 | 61 | #### 2.0.0 / 2023-08-08 62 | * remove Dependencies tab, Manage Dependencies link, etc, per @azaozz 63 | * skip associated PHPUnit tests 64 | * increase scope to protected for many things 65 | * remove `class Init`, not needed 66 | * deactivate buttons, don't change text 67 | 68 | #### 1.14.3 / 2023-70-30 69 | * add null coalesce 70 | * require PHP 7.0 71 | * make commit guard more permissive 72 | 73 | #### 1.14.2 / 2023-07-20 74 | * update guard in `get_dependency_filepaths()` 75 | 76 | #### 1.14.1 / 2023-07-20 77 | * update modal button on plugin-install.php 78 | 79 | #### 1.14.0 / 2023-07-19 80 | * update _More details_ link 81 | * fixed strange error between slug from different sources in PD part 2 82 | * update JS to correctly display Plugin Card button, thanks @costdev 83 | 84 | #### 1.13.0 / 2023-07-10 85 | * update version check 86 | * simplify plugin card notice 87 | 88 | #### 1.12.1 / 2023-07-01 89 | * extra life to 6.4-beta1 90 | 91 | #### 1.12.0 / 2023-05-21 92 | * change plugin card button to 'Cannot Install' if dependencies not met 93 | * override `WP_Plugin_Install_List_Table::display_rows()` to use our refactored `wp_get_plugin_action_button()` 94 | 95 | #### 1.11.0 / 2023-05-21 96 | * add **Requires:** data to plugin cards of uninstalled plugins where repo plugins have `Requires Plugins` header set 97 | * add temporary style kludge to above 98 | * add caching to uninstalled plugin data 99 | * abstract code to create plugin install action buttons 100 | 101 | #### 1.10.0 / 2023-04-29 102 | * show `Cannot Install` button in Dependencies tab for dependencies with no package 103 | * return of generic plugins_api() response to it's own hook, avoids having to hide items in plugin card 104 | * add more data to generic plugin card 105 | * update for WP-CLI 106 | * no need to start on hook 107 | 108 | #### 1.9.0 / 2023-04-10 109 | * ensure WP 6.0 compatibility with `move_dir()` 110 | * use JSON in plugin root for non-dot org dependencies _acceptable_ for dot org 🤞 111 | * update test plugins 112 | * run hooks during AJAX in case you really want an Install to happen 113 | * update regex to strictly follow plugin repository slug format with tests 114 | 115 | #### 1.8.0 / 2023-04-07 116 | * update to work natively with `|` format in `Requires Plugins` header 117 | * split PD and PDv2 into different classes 118 | * add more tests 119 | 120 | #### 1.7.9 / 2023-04-05 121 | * update action link to keep `Cannot Activate | Manage Dependencies` together 122 | * fix for multisite plugin card 123 | 124 | #### 1.7.8 / 2023-03-03 125 | * composer update 126 | 127 | #### 1.7.7 / 2023-02-11 128 | * add a11y that I (@afragen) clearly forgot, it's a start 129 | * fix circular dependency test plugins to have containing folder, dependencies must have a containing folder 130 | 131 | #### 1.7.6 / 2023-02-11 132 | * update `Name` header of test plugins so they can't be mistaken for core plugin after AJAX Install 133 | 134 | #### 1.7.5 / 2023-02-09 135 | * cleanup docblocks 136 | * initialize during class loading 137 | 138 | #### 1.7.4 / 2023-02-08 139 | * composer update 140 | 141 | #### 1.7.3 / 2023-01-30 142 | * composer update using Composer 2.5.0 to avoid bug 143 | 144 | #### 1.7.2 / 2023-01-02 145 | * add unresolvable circular dependency example 146 | * update for PHP standards 147 | 148 | #### 1.7.1 / 2022-10-27 149 | * remove "improved visibility" of `Dependencies` link 150 | 151 | #### 1.7.0 / 2022-10-25 152 | * notification of circular dependencies 153 | * add info text under Dependencies tab, I found a hook 🙌 154 | * display admin notices on specific pages 155 | * added some code improvements, thanks Colin 156 | * add `Requires:` data to plugin card 157 | * modify plugin card action links if dependency not met 158 | * improve visibility of `Dependencies` link 159 | 160 | #### 1.6.2 / 2022-10-18 161 | * composer update better checking in `afragen/add-plugin-dependency-api` 162 | 163 | #### 1.6.1 / 2022-10-18 164 | * more precise check of dependency slug for file path 165 | * don't show admin notice to users who are unable to act upon them 166 | * update composer dependencies 167 | * add skeleton JSON response for Gravity Forms 168 | 169 | #### 1.6.0 / 2022-10-15 170 | * move `plugin_dependency_endpoints` hook outside of class 171 | * composer update 172 | * add filter `wp_plugin_dependencies_slugs` to modify slugs in cases of non-premium plugin replaced with premium plugin 173 | * keep checking plugins API for plugin with generic response 174 | * update conditional for generic response 175 | * update testing plugins 176 | 177 | #### 1.5.1 / 2022-09-02 178 | * fix for actual `gravityforms` slug 179 | 180 | #### 1.5.0 / 2022-09-02 181 | * add `afragen/add-plugin-dependency-api` as composer requirement 182 | * update test plugins removing `hello-dolly` and adding `git-updater` as non dot org example 183 | * check empty plugin response for error 184 | 185 | #### 1.4.1 / 2022-08-18 186 | * oops, fixed typo in one of the testing plugins 187 | 188 | #### 1.4.0 / 2022-07-28 189 | * bring more inline with PR 190 | * remove action on class requires, use hook 191 | * fix multisite compatibility 192 | 193 | #### 1.3.0 / 2022-07-04 🎆 194 | * fix `get_requires_plugin_names()` to account for empty header 195 | * update regex to allow for some non-ascii languages and symbols as slugs 196 | 197 | #### 1.2.1 / 2022-06-23 198 | * added several single file testing plugins to `test-plugins/` 199 | 200 | #### 1.2.0 / 2022-06-10 201 | * don't display admin notice link to Dependencies tab when on Dependencies tab 202 | * be more specific about only removing dependency plugin row checkbox when a requiring plugin is active 203 | 204 | #### 1.1.1 / 2022-06-06 205 | * limit scope of class methods where we can 206 | * update screenshots 207 | 208 | #### 1.1.0 / 2022-06-02 209 | * change 'Activate' plugin action link to 'Cannot Activate' text when plugin has unmet dependencies 210 | * remove checkbox from plugin row when plugin has unmet dependencies 211 | * use _View details_ link for plugins listed in **Requires:** in plugin row 212 | 213 | #### 1.0.0 / 2022-05-31 🎂 214 | * fix typo 215 | * initial dot org release 216 | 217 | #### 0.16.2 / 2022-05-27 218 | * update requirements to WP 6.0 219 | 220 | #### 0.16.1 / 2022-05-24 221 | * add auto-deactivate for when committed to trunk, will need updating later 222 | 223 | #### 0.16.0 / 2022-05-08 224 | * rename `parse_headers()` to `parse_plugin_headers()`, future proofing 225 | * update unit tests 226 | 227 | #### 0.15.1 / 2022-04-29 228 | * minor cleanup 229 | 230 | #### 0.15.0 / 2022-04-28 231 | * refactor with `get_requires_plugins_names()` 232 | * update admin notice for multisite 233 | 234 | #### 0.14.0 235 | * updated required plugin data expiration 236 | 237 | #### 0.13.1 / 2022-04-25 238 | * fix `parse_headers()` 239 | 240 | #### 0.13.0 / 2022-04-23 241 | * prep for initial release 242 | 243 | #### 0.12.9 / 2022-04-19 244 | * add plugin cards for slugs with no API data 245 | * hide action links and bottom of card in plugin cards for slugs with no API data 246 | 247 | #### 0.12.3 248 | *rename and reschuffle some functions 249 | 250 | #### 0.12.2 / 2022-04-06 251 | * harden a bit 252 | * clean up some testing stuff 253 | * `plugin_install_description` filter committed to core 254 | 255 | #### 0.12.0 / 2022-04-03 256 | * readme.txt 257 | * fix PHP error if no plugins with `Requires Plugins` header found 258 | * only show single, relevant admin notice 259 | 260 | #### 0.11.6.4 261 | * plugin to date with new changelog 262 | -------------------------------------------------------------------------------- /src/Init_Plugin.php: -------------------------------------------------------------------------------- 1 | p { 12 | margin-left: 148px; 13 | } 14 | 15 | @media (min-width: 1101px) { 16 | .plugin-card .name, .plugin-card .desc > p { 17 | margin-right: 128px; 18 | } 19 | } 20 | 21 | @media (min-width: 481px) and (max-width: 781px) { 22 | .plugin-card .name, .plugin-card .desc > p { 23 | margin-right: 128px; 24 | } 25 | } 26 | 27 | .plugin-card .column-description { 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: flex-start; 31 | } 32 | 33 | .plugin-card .column-description > p { 34 | margin-top: 0; 35 | } 36 | 37 | .plugin-card .column-description .authors { 38 | order: 1; 39 | } 40 | 41 | .plugin-card .column-description .plugin-dependencies { 42 | order: 2; 43 | } 44 | 45 | .plugin-card .column-description p:empty { 46 | display: none; 47 | } 48 | 49 | .plugin-card .plugin-dependencies { 50 | background-color: #e5f5fa; 51 | border-left: 3px solid #72aee6; 52 | margin-bottom: .5em; 53 | padding: 15px; 54 | } 55 | 56 | .plugin-card .plugin-dependencies-explainer-text { 57 | margin-block: 0; 58 | } 59 | 60 | .plugin-card .plugin-dependency { 61 | align-items: center; 62 | display: flex; 63 | flex-wrap: wrap; 64 | margin-top: .5em; 65 | column-gap: 1%; 66 | row-gap: .5em; 67 | } 68 | 69 | .plugin-card .plugin-dependency:nth-child(2), 70 | .plugin-card .plugin-dependency:last-child { 71 | margin-top: 1em; 72 | } 73 | 74 | .plugin-card .plugin-dependency-name { 75 | flex-basis: 74%; 76 | } 77 | 78 | .plugin-card .plugin-dependency .more-details-link { 79 | margin-left: auto; 80 | } 81 | 82 | .rtl .plugin-card .plugin-dependency .more-details-link { 83 | margin-right: auto; 84 | } 85 | 86 | @media (max-width: 939px) { 87 | .plugin-card .plugin-dependency-name { 88 | flex-basis: 69%; 89 | } 90 | .plugin-card .plugin-dependency .more-details-link { 91 | } 92 | } 93 | 94 | .plugins #the-list .required-by, 95 | .plugins #the-list .requires { 96 | margin-top: 1em; 97 | } 98 | -------------------------------------------------------------------------------- /src/wp-admin/includes/class-pd-install-list-table.php: -------------------------------------------------------------------------------- 1 | array( 28 | 'href' => array(), 29 | 'title' => array(), 30 | 'target' => array(), 31 | ), 32 | 'abbr' => array( 'title' => array() ), 33 | 'acronym' => array( 'title' => array() ), 34 | 'code' => array(), 35 | 'pre' => array(), 36 | 'em' => array(), 37 | 'strong' => array(), 38 | 'ul' => array(), 39 | 'ol' => array(), 40 | 'li' => array(), 41 | 'p' => array(), 42 | 'br' => array(), 43 | ); 44 | 45 | $plugins_group_titles = array( 46 | 'Performance' => _x( 'Performance', 'Plugin installer group title' ), 47 | 'Social' => _x( 'Social', 'Plugin installer group title' ), 48 | 'Tools' => _x( 'Tools', 'Plugin installer group title' ), 49 | ); 50 | 51 | $group = null; 52 | 53 | foreach ( (array) $this->items as $plugin ) { 54 | if ( is_object( $plugin ) ) { 55 | $plugin = (array) $plugin; 56 | } 57 | 58 | // Display the group heading if there is one. 59 | if ( isset( $plugin['group'] ) && $plugin['group'] !== $group ) { 60 | if ( isset( $this->groups[ $plugin['group'] ] ) ) { 61 | $group_name = $this->groups[ $plugin['group'] ]; 62 | if ( isset( $plugins_group_titles[ $group_name ] ) ) { 63 | $group_name = $plugins_group_titles[ $group_name ]; 64 | } 65 | } else { 66 | $group_name = $plugin['group']; 67 | } 68 | 69 | // Starting a new group, close off the divs of the last one. 70 | if ( ! empty( $group ) ) { 71 | echo ''; 72 | } 73 | 74 | echo '

' . esc_html( $group_name ) . '

'; 75 | // Needs an extra wrapping div for nth-child selectors to work. 76 | echo '
'; 77 | 78 | $group = $plugin['group']; 79 | } 80 | 81 | $title = wp_kses( $plugin['name'], $plugins_allowedtags ); 82 | 83 | // Remove any HTML from the description. 84 | $description = strip_tags( $plugin['short_description'] ); 85 | 86 | $description .= $this->get_dependencies_notice( $plugin ); 87 | 88 | /** 89 | * Filters the plugin card description on the Add Plugins screen. 90 | * 91 | * @since 6.0.0 92 | * 93 | * @param string $description Plugin card description. 94 | * @param array $plugin An array of plugin data. See {@see plugins_api()} 95 | * for the list of possible values. 96 | */ 97 | $description = apply_filters( 'plugin_install_description', $description, $plugin ); 98 | 99 | $version = wp_kses( $plugin['version'], $plugins_allowedtags ); 100 | 101 | $name = strip_tags( $title . ' ' . $version ); 102 | 103 | $author = wp_kses( $plugin['author'], $plugins_allowedtags ); 104 | if ( ! empty( $author ) ) { 105 | /* translators: %s: Plugin author. */ 106 | $author = ' ' . sprintf( __( 'By %s' ), $author ) . ''; 107 | } 108 | 109 | $requires_php = isset( $plugin['requires_php'] ) ? $plugin['requires_php'] : null; 110 | $requires_wp = isset( $plugin['requires'] ) ? $plugin['requires'] : null; 111 | 112 | $compatible_php = is_php_version_compatible( $requires_php ); 113 | $compatible_wp = is_wp_version_compatible( $requires_wp ); 114 | $tested_wp = ( empty( $plugin['tested'] ) || version_compare( get_bloginfo( 'version' ), $plugin['tested'], '<=' ) ); 115 | 116 | $action_links = array(); 117 | 118 | $action_links[] = wp_get_plugin_action_button( $name, $plugin, $compatible_php, $compatible_wp ); 119 | 120 | $details_link = self_admin_url( 121 | 'plugin-install.php?tab=plugin-information&plugin=' . $plugin['slug'] . 122 | '&TB_iframe=true&width=600&height=550' 123 | ); 124 | 125 | $action_links[] = sprintf( 126 | '%s', 127 | esc_url( $details_link ), 128 | /* translators: %s: Plugin name and version. */ 129 | esc_attr( sprintf( __( 'More information about %s' ), $name ) ), 130 | esc_attr( $name ), 131 | __( 'More Details' ) 132 | ); 133 | 134 | if ( ! empty( $plugin['icons']['svg'] ) ) { 135 | $plugin_icon_url = $plugin['icons']['svg']; 136 | } elseif ( ! empty( $plugin['icons']['2x'] ) ) { 137 | $plugin_icon_url = $plugin['icons']['2x']; 138 | } elseif ( ! empty( $plugin['icons']['1x'] ) ) { 139 | $plugin_icon_url = $plugin['icons']['1x']; 140 | } else { 141 | $plugin_icon_url = $plugin['icons']['default']; 142 | } 143 | 144 | /** 145 | * Filters the install action links for a plugin. 146 | * 147 | * @since 2.7.0 148 | * 149 | * @param string[] $action_links An array of plugin action links. 150 | * Defaults are links to Details and Install Now. 151 | * @param array $plugin An array of plugin data. See {@see plugins_api()} 152 | * for the list of possible values. 153 | */ 154 | $action_links = apply_filters( 'plugin_install_action_links', $action_links, $plugin ); 155 | 156 | $last_updated_timestamp = strtotime( $plugin['last_updated'] ); 157 | ?> 158 |
159 |

'; 162 | if ( ! $compatible_php && ! $compatible_wp ) { 163 | _e( 'This plugin does not work with your versions of WordPress and PHP.' ); 164 | if ( current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) { 165 | printf( 166 | /* translators: 1: URL to WordPress Updates screen, 2: URL to Update PHP page. */ 167 | ' ' . __( 'Please update WordPress, and then learn more about updating PHP.' ), 168 | self_admin_url( 'update-core.php' ), 169 | esc_url( wp_get_update_php_url() ) 170 | ); 171 | wp_update_php_annotation( '

', '' ); 172 | } elseif ( current_user_can( 'update_core' ) ) { 173 | printf( 174 | /* translators: %s: URL to WordPress Updates screen. */ 175 | ' ' . __( 'Please update WordPress.' ), 176 | self_admin_url( 'update-core.php' ) 177 | ); 178 | } elseif ( current_user_can( 'update_php' ) ) { 179 | printf( 180 | /* translators: %s: URL to Update PHP page. */ 181 | ' ' . __( 'Learn more about updating PHP.' ), 182 | esc_url( wp_get_update_php_url() ) 183 | ); 184 | wp_update_php_annotation( '

', '' ); 185 | } 186 | } elseif ( ! $compatible_wp ) { 187 | _e( 'This plugin does not work with your version of WordPress.' ); 188 | if ( current_user_can( 'update_core' ) ) { 189 | printf( 190 | /* translators: %s: URL to WordPress Updates screen. */ 191 | ' ' . __( 'Please update WordPress.' ), 192 | self_admin_url( 'update-core.php' ) 193 | ); 194 | } 195 | } elseif ( ! $compatible_php ) { 196 | _e( 'This plugin does not work with your version of PHP.' ); 197 | if ( current_user_can( 'update_php' ) ) { 198 | printf( 199 | /* translators: %s: URL to Update PHP page. */ 200 | ' ' . __( 'Learn more about updating PHP.' ), 201 | esc_url( wp_get_update_php_url() ) 202 | ); 203 | wp_update_php_annotation( '

', '' ); 204 | } 205 | } 206 | echo '

'; 207 | } 208 | ?> 209 |
210 |
211 |

212 | 213 | 214 | 215 | 216 |

217 |
218 | 225 |
226 |

227 |

228 |
229 |
230 |
231 |
232 | $plugin['rating'], 236 | 'type' => 'percent', 237 | 'number' => $plugin['num_ratings'], 238 | ) 239 | ); 240 | ?> 241 | 242 |
243 |
244 | 245 | 249 |
250 |
251 | = 1000000 ) { 253 | $active_installs_millions = floor( $plugin['active_installs'] / 1000000 ); 254 | $active_installs_text = sprintf( 255 | /* translators: %s: Number of millions. */ 256 | _nx( '%s+ Million', '%s+ Million', $active_installs_millions, 'Active plugin installations' ), 257 | number_format_i18n( $active_installs_millions ) 258 | ); 259 | } elseif ( 0 === $plugin['active_installs'] ) { 260 | $active_installs_text = _x( 'Less Than 10', 'Active plugin installations' ); 261 | } else { 262 | $active_installs_text = number_format_i18n( $plugin['active_installs'] ) . '+'; 263 | } 264 | /* translators: %s: Number of installations. */ 265 | printf( __( '%s Active Installations' ), $active_installs_text ); 266 | ?> 267 |
268 |
269 | ' . __( 'Untested with your version of WordPress' ) . ''; 272 | } elseif ( ! $compatible_wp ) { 273 | echo '' . __( 'Incompatible with your version of WordPress' ) . ''; 274 | } else { 275 | echo '' . __( 'Compatible with your version of WordPress' ) . ''; 276 | } 277 | ?> 278 |
279 |
280 |
281 |
'; 287 | } 288 | } 289 | 290 | /** 291 | * Returns a notice containing a list of dependencies required by the plugin. 292 | * 293 | * @since 6.5.0 294 | * 295 | * @param array $plugin_data An array of plugin data. See {@see plugins_api()} 296 | * for the list of possible values. 297 | * @return string A notice containing a list of dependencies required by the plugin, 298 | * or an empty string if none is required. 299 | */ 300 | protected function get_dependencies_notice( $plugin_data ) { 301 | if ( empty( $plugin_data['requires_plugins'] ) ) { 302 | return ''; 303 | } 304 | 305 | $no_name_markup = '
%s
'; 306 | $has_name_markup = '
%s %s
'; 307 | 308 | $dependencies_list = ''; 309 | foreach ( $plugin_data['requires_plugins'] as $dependency ) { 310 | $dependency_data = \WP_Plugin_Dependencies::get_dependency_data( $dependency ); 311 | 312 | if ( 313 | false !== $dependency_data && 314 | ! empty( $dependency_data['name'] ) && 315 | ! empty( $dependency_data['slug'] ) && 316 | ! empty( $dependency_data['version'] ) 317 | ) { 318 | $more_details_link = $this->get_more_details_link( $dependency_data['name'], $dependency_data['slug'] ); 319 | $dependencies_list .= sprintf( $has_name_markup, esc_html( $dependency_data['name'] ), $more_details_link ); 320 | continue; 321 | } 322 | 323 | $result = plugins_api( 'plugin_information', array( 'slug' => $dependency ) ); 324 | 325 | if ( ! empty( $result->name ) ) { 326 | $more_details_link = $this->get_more_details_link( $result->name, $result->slug ); 327 | $dependencies_list .= sprintf( $has_name_markup, esc_html( $result->name ), $more_details_link ); 328 | continue; 329 | } 330 | 331 | $dependencies_list .= sprintf( $no_name_markup, esc_html( $dependency ) ); 332 | } 333 | 334 | $dependencies_notice = sprintf( 335 | '

%s

%s
', 336 | '' . __( 'Additional plugins are required' ) . '', 337 | $dependencies_list 338 | ); 339 | 340 | return $dependencies_notice; 341 | } 342 | 343 | /** 344 | * Creates a 'More details' link for the plugin. 345 | * 346 | * @since 6.5.0 347 | * 348 | * @param string $name The plugin's name. 349 | * @param string $slug The plugin's slug. 350 | * @return string The 'More details' link for the plugin. 351 | */ 352 | protected function get_more_details_link( $name, $slug ) { 353 | $url = add_query_arg( 354 | array( 355 | 'tab' => 'plugin-information', 356 | 'plugin' => $slug, 357 | 'TB_iframe' => 'true', 358 | 'width' => '600', 359 | 'height' => '550', 360 | ), 361 | network_admin_url( 'plugin-install.php' ) 362 | ); 363 | 364 | $more_details_link = sprintf( 365 | '%4$s', 366 | esc_url( $url ), 367 | /* translators: %s: Plugin name. */ 368 | sprintf( __( 'More information about %s' ), esc_html( $name ) ), 369 | esc_attr( $name ), 370 | __( 'More Details' ) 371 | ); 372 | 373 | return $more_details_link; 374 | } 375 | } 376 | -------------------------------------------------------------------------------- /src/wp-admin/includes/class-pd-list-table.php: -------------------------------------------------------------------------------- 1 | screen; 48 | 49 | // Pre-order. 50 | $actions = array( 51 | 'deactivate' => '', 52 | 'activate' => '', 53 | 'details' => '', 54 | 'delete' => '', 55 | ); 56 | 57 | // Do not restrict by default. 58 | $restrict_network_active = false; 59 | $restrict_network_only = false; 60 | 61 | $requires_php = isset( $plugin_data['RequiresPHP'] ) ? $plugin_data['RequiresPHP'] : null; 62 | $requires_wp = isset( $plugin_data['RequiresWP'] ) ? $plugin_data['RequiresWP'] : null; 63 | 64 | $compatible_php = is_php_version_compatible( $requires_php ); 65 | $compatible_wp = is_wp_version_compatible( $requires_wp ); 66 | 67 | $has_active_dependents = \WP_Plugin_Dependencies::has_active_dependents( $plugin_file ); 68 | $has_unmet_dependencies = \WP_Plugin_Dependencies::has_unmet_dependencies( $plugin_file ); 69 | 70 | if ( 'mustuse' === $context ) { 71 | $is_active = true; 72 | } elseif ( 'dropins' === $context ) { 73 | $dropins = _get_dropins(); 74 | $plugin_name = $plugin_file; 75 | 76 | if ( $plugin_file !== $plugin_data['Name'] ) { 77 | $plugin_name .= '
' . $plugin_data['Name']; 78 | } 79 | 80 | if ( true === ( $dropins[ $plugin_file ][1] ) ) { // Doesn't require a constant. 81 | $is_active = true; 82 | $description = '

' . $dropins[ $plugin_file ][0] . '

'; 83 | } elseif ( defined( $dropins[ $plugin_file ][1] ) && constant( $dropins[ $plugin_file ][1] ) ) { // Constant is true. 84 | $is_active = true; 85 | $description = '

' . $dropins[ $plugin_file ][0] . '

'; 86 | } else { 87 | $is_active = false; 88 | $description = '

' . $dropins[ $plugin_file ][0] . ' ' . __( 'Inactive:' ) . ' ' . 89 | sprintf( 90 | /* translators: 1: Drop-in constant name, 2: wp-config.php */ 91 | __( 'Requires %1$s in %2$s file.' ), 92 | "define('" . $dropins[ $plugin_file ][1] . "', true);", 93 | 'wp-config.php' 94 | ) . '

'; 95 | } 96 | 97 | if ( $plugin_data['Description'] ) { 98 | $description .= '

' . $plugin_data['Description'] . '

'; 99 | } 100 | } else { 101 | if ( $screen->in_admin( 'network' ) ) { 102 | $is_active = is_plugin_active_for_network( $plugin_file ); 103 | } else { 104 | $is_active = is_plugin_active( $plugin_file ); 105 | $restrict_network_active = ( is_multisite() && is_plugin_active_for_network( $plugin_file ) ); 106 | $restrict_network_only = ( is_multisite() && is_network_only_plugin( $plugin_file ) && ! $is_active ); 107 | } 108 | 109 | if ( $screen->in_admin( 'network' ) ) { 110 | if ( $is_active && false === $has_active_dependents ) { 111 | if ( current_user_can( 'manage_network_plugins' ) ) { 112 | $actions['deactivate'] = sprintf( 113 | '%s', 114 | wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'deactivate-plugin_' . $plugin_file ), 115 | esc_attr( $plugin_id_attr ), 116 | /* translators: %s: Plugin name. */ 117 | esc_attr( sprintf( _x( 'Network Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ), 118 | __( 'Network Deactivate' ) 119 | ); 120 | } 121 | } else { 122 | if ( current_user_can( 'manage_network_plugins' ) ) { 123 | if ( $compatible_php && $compatible_wp ) { 124 | if ( $has_unmet_dependencies ) { 125 | $actions['activate'] = __( 'Network Activate' ); 126 | $actions['activate'] .= '' . __( 'Cannot activate due to unmet dependencies' ) . ''; 127 | } else { 128 | $actions['activate'] = sprintf( 129 | '%s', 130 | wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'activate-plugin_' . $plugin_file ), 131 | esc_attr( $plugin_id_attr ), 132 | /* translators: %s: Plugin name. */ 133 | esc_attr( sprintf( _x( 'Network Activate %s', 'plugin' ), $plugin_data['Name'] ) ), 134 | __( 'Network Activate' ) 135 | ); 136 | } 137 | } else { 138 | $actions['activate'] = sprintf( 139 | '%s', 140 | _x( 'Cannot Activate', 'plugin' ) 141 | ); 142 | } 143 | } 144 | 145 | if ( current_user_can( 'delete_plugins' ) && false === $has_active_dependents && ! is_plugin_active( $plugin_file ) ) { 146 | $actions['delete'] = sprintf( 147 | '%s', 148 | wp_nonce_url( 'plugins.php?action=delete-selected&checked[]=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'bulk-plugins' ), 149 | esc_attr( $plugin_id_attr ), 150 | /* translators: %s: Plugin name. */ 151 | esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ), 152 | __( 'Delete' ) 153 | ); 154 | } 155 | } 156 | } else { 157 | if ( $restrict_network_active ) { 158 | $actions = array( 159 | 'network_active' => __( 'Network Active' ), 160 | ); 161 | } elseif ( $restrict_network_only ) { 162 | $actions = array( 163 | 'network_only' => __( 'Network Only' ), 164 | ); 165 | } elseif ( $is_active ) { 166 | if ( current_user_can( 'deactivate_plugin', $plugin_file ) && false === $has_active_dependents ) { 167 | $actions['deactivate'] = sprintf( 168 | '%s', 169 | wp_nonce_url( 'plugins.php?action=deactivate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'deactivate-plugin_' . $plugin_file ), 170 | esc_attr( $plugin_id_attr ), 171 | /* translators: %s: Plugin name. */ 172 | esc_attr( sprintf( _x( 'Deactivate %s', 'plugin' ), $plugin_data['Name'] ) ), 173 | __( 'Deactivate' ) 174 | ); 175 | } 176 | 177 | if ( current_user_can( 'resume_plugin', $plugin_file ) && is_plugin_paused( $plugin_file ) ) { 178 | $actions['resume'] = sprintf( 179 | '%s', 180 | wp_nonce_url( 'plugins.php?action=resume&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'resume-plugin_' . $plugin_file ), 181 | esc_attr( $plugin_id_attr ), 182 | /* translators: %s: Plugin name. */ 183 | esc_attr( sprintf( _x( 'Resume %s', 'plugin' ), $plugin_data['Name'] ) ), 184 | __( 'Resume' ) 185 | ); 186 | } 187 | } else { 188 | if ( current_user_can( 'activate_plugin', $plugin_file ) ) { 189 | if ( $compatible_php && $compatible_wp ) { 190 | if ( $has_unmet_dependencies ) { 191 | $actions['activate'] = __( 'Activate' ); 192 | $actions['activate'] .= '' . __( 'Cannot activate due to unmet dependencies' ) . ''; 193 | } else { 194 | $actions['activate'] = sprintf( 195 | '%s', 196 | wp_nonce_url( 'plugins.php?action=activate&plugin=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'activate-plugin_' . $plugin_file ), 197 | esc_attr( $plugin_id_attr ), 198 | /* translators: %s: Plugin name. */ 199 | esc_attr( sprintf( _x( 'Activate %s', 'plugin' ), $plugin_data['Name'] ) ), 200 | __( 'Activate' ) 201 | ); 202 | } 203 | } else { 204 | $actions['activate'] = sprintf( 205 | '%s', 206 | _x( 'Cannot Activate', 'plugin' ) 207 | ); 208 | } 209 | } 210 | 211 | if ( ! is_multisite() && current_user_can( 'delete_plugins' ) && false === $has_active_dependents ) { 212 | $actions['delete'] = sprintf( 213 | '%s', 214 | wp_nonce_url( 'plugins.php?action=delete-selected&checked[]=' . urlencode( $plugin_file ) . '&plugin_status=' . $context . '&paged=' . $page . '&s=' . $s, 'bulk-plugins' ), 215 | esc_attr( $plugin_id_attr ), 216 | /* translators: %s: Plugin name. */ 217 | esc_attr( sprintf( _x( 'Delete %s', 'plugin' ), $plugin_data['Name'] ) ), 218 | __( 'Delete' ) 219 | ); 220 | } 221 | } // End if $is_active. 222 | } // End if $screen->in_admin( 'network' ). 223 | } // End if $context. 224 | 225 | $actions = array_filter( $actions ); 226 | 227 | if ( $screen->in_admin( 'network' ) ) { 228 | 229 | /** 230 | * Filters the action links displayed for each plugin in the Network Admin Plugins list table. 231 | * 232 | * @since 3.1.0 233 | * 234 | * @param string[] $actions An array of plugin action links. By default this can include 235 | * 'activate', 'deactivate', and 'delete'. 236 | * @param string $plugin_file Path to the plugin file relative to the plugins directory. 237 | * @param array $plugin_data An array of plugin data. See get_plugin_data() 238 | * and the {@see 'plugin_row_meta'} filter for the list 239 | * of possible values. 240 | * @param string $context The plugin context. By default this can include 'all', 241 | * 'active', 'inactive', 'recently_activated', 'upgrade', 242 | * 'mustuse', 'dropins', and 'search'. 243 | */ 244 | $actions = apply_filters( 'network_admin_plugin_action_links', $actions, $plugin_file, $plugin_data, $context ); 245 | 246 | /** 247 | * Filters the list of action links displayed for a specific plugin in the Network Admin Plugins list table. 248 | * 249 | * The dynamic portion of the hook name, `$plugin_file`, refers to the path 250 | * to the plugin file, relative to the plugins directory. 251 | * 252 | * @since 3.1.0 253 | * 254 | * @param string[] $actions An array of plugin action links. By default this can include 255 | * 'activate', 'deactivate', and 'delete'. 256 | * @param string $plugin_file Path to the plugin file relative to the plugins directory. 257 | * @param array $plugin_data An array of plugin data. See get_plugin_data() 258 | * and the {@see 'plugin_row_meta'} filter for the list 259 | * of possible values. 260 | * @param string $context The plugin context. By default this can include 'all', 261 | * 'active', 'inactive', 'recently_activated', 'upgrade', 262 | * 'mustuse', 'dropins', and 'search'. 263 | */ 264 | $actions = apply_filters( "network_admin_plugin_action_links_{$plugin_file}", $actions, $plugin_file, $plugin_data, $context ); 265 | 266 | } else { 267 | 268 | /** 269 | * Filters the action links displayed for each plugin in the Plugins list table. 270 | * 271 | * @since 2.5.0 272 | * @since 2.6.0 The `$context` parameter was added. 273 | * @since 4.9.0 The 'Edit' link was removed from the list of action links. 274 | * 275 | * @param string[] $actions An array of plugin action links. By default this can include 276 | * 'activate', 'deactivate', and 'delete'. With Multisite active 277 | * this can also include 'network_active' and 'network_only' items. 278 | * @param string $plugin_file Path to the plugin file relative to the plugins directory. 279 | * @param array $plugin_data An array of plugin data. See get_plugin_data() 280 | * and the {@see 'plugin_row_meta'} filter for the list 281 | * of possible values. 282 | * @param string $context The plugin context. By default this can include 'all', 283 | * 'active', 'inactive', 'recently_activated', 'upgrade', 284 | * 'mustuse', 'dropins', and 'search'. 285 | */ 286 | $actions = apply_filters( 'plugin_action_links', $actions, $plugin_file, $plugin_data, $context ); 287 | 288 | /** 289 | * Filters the list of action links displayed for a specific plugin in the Plugins list table. 290 | * 291 | * The dynamic portion of the hook name, `$plugin_file`, refers to the path 292 | * to the plugin file, relative to the plugins directory. 293 | * 294 | * @since 2.7.0 295 | * @since 4.9.0 The 'Edit' link was removed from the list of action links. 296 | * 297 | * @param string[] $actions An array of plugin action links. By default this can include 298 | * 'activate', 'deactivate', and 'delete'. With Multisite active 299 | * this can also include 'network_active' and 'network_only' items. 300 | * @param string $plugin_file Path to the plugin file relative to the plugins directory. 301 | * @param array $plugin_data An array of plugin data. See get_plugin_data() 302 | * and the {@see 'plugin_row_meta'} filter for the list 303 | * of possible values. 304 | * @param string $context The plugin context. By default this can include 'all', 305 | * 'active', 'inactive', 'recently_activated', 'upgrade', 306 | * 'mustuse', 'dropins', and 'search'. 307 | */ 308 | $actions = apply_filters( "plugin_action_links_{$plugin_file}", $actions, $plugin_file, $plugin_data, $context ); 309 | 310 | } 311 | 312 | $class = $is_active ? 'active' : 'inactive'; 313 | $checkbox_id = 'checkbox_' . md5( $plugin_file ); 314 | $disabled = ''; 315 | 316 | if ( \WP_Plugin_Dependencies::has_active_dependents( $plugin_file ) || \WP_Plugin_Dependencies::has_unmet_dependencies( $plugin_file ) ) { 317 | $disabled = 'disabled'; 318 | } 319 | 320 | if ( $restrict_network_active || $restrict_network_only || in_array( $status, array( 'mustuse', 'dropins' ), true ) || ! $compatible_php ) { 321 | $checkbox = ''; 322 | } else { 323 | $checkbox = sprintf( 324 | '' . 325 | '', 326 | $checkbox_id, 327 | /* translators: Hidden accessibility text. %s: Plugin name. */ 328 | sprintf( __( 'Select %s' ), $plugin_data['Name'] ), 329 | esc_attr( $plugin_file ) 330 | ); 331 | } 332 | 333 | if ( 'dropins' !== $context ) { 334 | $description = '

' . ( $plugin_data['Description'] ? $plugin_data['Description'] : ' ' ) . '

'; 335 | $plugin_name = $plugin_data['Name']; 336 | } 337 | 338 | if ( ! empty( $totals['upgrade'] ) && ! empty( $plugin_data['update'] ) 339 | || ! $compatible_php || ! $compatible_wp 340 | ) { 341 | $class .= ' update'; 342 | } 343 | 344 | $paused = ! $screen->in_admin( 'network' ) && is_plugin_paused( $plugin_file ); 345 | 346 | if ( $paused ) { 347 | $class .= ' paused'; 348 | } 349 | 350 | if ( is_uninstallable_plugin( $plugin_file ) ) { 351 | $class .= ' is-uninstallable'; 352 | } 353 | 354 | printf( 355 | '', 356 | esc_attr( $class ), 357 | esc_attr( $plugin_slug ), 358 | esc_attr( $plugin_file ) 359 | ); 360 | 361 | list( $columns, $hidden, $sortable, $primary ) = $this->get_column_info(); 362 | 363 | $auto_updates = (array) get_site_option( 'auto_update_plugins', array() ); 364 | 365 | foreach ( $columns as $column_name => $column_display_name ) { 366 | $extra_classes = ''; 367 | if ( in_array( $column_name, $hidden, true ) ) { 368 | $extra_classes = ' hidden'; 369 | } 370 | 371 | switch ( $column_name ) { 372 | case 'cb': 373 | echo "$checkbox"; 374 | break; 375 | case 'name': 376 | echo "$plugin_name"; 377 | echo $this->row_actions( $actions, true ); 378 | echo ''; 379 | break; 380 | case 'description': 381 | $classes = 'column-description desc'; 382 | 383 | echo " 384 |
$description
385 |
"; 386 | 387 | $plugin_meta = array(); 388 | if ( ! empty( $plugin_data['Version'] ) ) { 389 | /* translators: %s: Plugin version number. */ 390 | $plugin_meta[] = sprintf( __( 'Version %s' ), $plugin_data['Version'] ); 391 | } 392 | if ( ! empty( $plugin_data['Author'] ) ) { 393 | $author = $plugin_data['Author']; 394 | if ( ! empty( $plugin_data['AuthorURI'] ) ) { 395 | $author = '' . $plugin_data['Author'] . ''; 396 | } 397 | /* translators: %s: Plugin author name. */ 398 | $plugin_meta[] = sprintf( __( 'By %s' ), $author ); 399 | } 400 | 401 | // Details link using API info, if available. 402 | if ( isset( $plugin_data['slug'] ) && current_user_can( 'install_plugins' ) ) { 403 | $plugin_meta[] = sprintf( 404 | '%s', 405 | esc_url( 406 | network_admin_url( 407 | 'plugin-install.php?tab=plugin-information&plugin=' . $plugin_data['slug'] . 408 | '&TB_iframe=true&width=600&height=550' 409 | ) 410 | ), 411 | /* translators: %s: Plugin name. */ 412 | esc_attr( sprintf( __( 'More information about %s' ), $plugin_name ) ), 413 | esc_attr( $plugin_name ), 414 | __( 'View details' ) 415 | ); 416 | } elseif ( ! empty( $plugin_data['PluginURI'] ) ) { 417 | /* translators: %s: Plugin name. */ 418 | $aria_label = sprintf( __( 'Visit plugin site for %s' ), $plugin_name ); 419 | 420 | $plugin_meta[] = sprintf( 421 | '%s', 422 | esc_url( $plugin_data['PluginURI'] ), 423 | esc_attr( $aria_label ), 424 | __( 'Visit plugin site' ) 425 | ); 426 | } 427 | 428 | /** 429 | * Filters the array of row meta for each plugin in the Plugins list table. 430 | * 431 | * @since 2.8.0 432 | * 433 | * @param string[] $plugin_meta An array of the plugin's metadata, including 434 | * the version, author, author URI, and plugin URI. 435 | * @param string $plugin_file Path to the plugin file relative to the plugins directory. 436 | * @param array $plugin_data { 437 | * An array of plugin data. 438 | * 439 | * @type string $id Plugin ID, e.g. `w.org/plugins/[plugin-name]`. 440 | * @type string $slug Plugin slug. 441 | * @type string $plugin Plugin basename. 442 | * @type string $new_version New plugin version. 443 | * @type string $url Plugin URL. 444 | * @type string $package Plugin update package URL. 445 | * @type string[] $icons An array of plugin icon URLs. 446 | * @type string[] $banners An array of plugin banner URLs. 447 | * @type string[] $banners_rtl An array of plugin RTL banner URLs. 448 | * @type string $requires The version of WordPress which the plugin requires. 449 | * @type string $tested The version of WordPress the plugin is tested against. 450 | * @type string $requires_php The version of PHP which the plugin requires. 451 | * @type string $upgrade_notice The upgrade notice for the new plugin version. 452 | * @type bool $update-supported Whether the plugin supports updates. 453 | * @type string $Name The human-readable name of the plugin. 454 | * @type string $PluginURI Plugin URI. 455 | * @type string $Version Plugin version. 456 | * @type string $Description Plugin description. 457 | * @type string $Author Plugin author. 458 | * @type string $AuthorURI Plugin author URI. 459 | * @type string $TextDomain Plugin textdomain. 460 | * @type string $DomainPath Relative path to the plugin's .mo file(s). 461 | * @type bool $Network Whether the plugin can only be activated network-wide. 462 | * @type string $RequiresWP The version of WordPress which the plugin requires. 463 | * @type string $RequiresPHP The version of PHP which the plugin requires. 464 | * @type string $UpdateURI ID of the plugin for update purposes, should be a URI. 465 | * @type string $Title The human-readable title of the plugin. 466 | * @type string $AuthorName Plugin author's name. 467 | * @type bool $update Whether there's an available update. Default null. 468 | * } 469 | * @param string $status Status filter currently applied to the plugin list. Possible 470 | * values are: 'all', 'active', 'inactive', 'recently_activated', 471 | * 'upgrade', 'mustuse', 'dropins', 'search', 'paused', 472 | * 'auto-update-enabled', 'auto-update-disabled'. 473 | */ 474 | $plugin_meta = apply_filters( 'plugin_row_meta', $plugin_meta, $plugin_file, $plugin_data, $status ); 475 | 476 | echo implode( ' | ', $plugin_meta ); 477 | 478 | echo '
'; 479 | 480 | if ( \WP_Plugin_Dependencies::has_dependents( $plugin_file ) ) { 481 | $this->add_dependents_to_dependency_plugin_row( $plugin_file ); 482 | } 483 | 484 | if ( \WP_Plugin_Dependencies::has_dependencies( $plugin_file ) ) { 485 | $this->add_dependencies_to_dependent_plugin_row( $plugin_file ); 486 | } 487 | 488 | /** 489 | * Fires after plugin row meta. 490 | * 491 | * @since 6.5.0 492 | * 493 | * @param string $plugin_file Refer to {@see 'plugin_row_meta'} filter. 494 | * @param array $plugin_data Refer to {@see 'plugin_row_meta'} filter. 495 | */ 496 | do_action( 'after_plugin_row_meta', $plugin_file, $plugin_data ); 497 | 498 | if ( $paused ) { 499 | $notice_text = __( 'This plugin failed to load properly and is paused during recovery mode.' ); 500 | 501 | printf( '

%s

', $notice_text ); 502 | 503 | $error = wp_get_plugin_error( $plugin_file ); 504 | 505 | if ( false !== $error ) { 506 | printf( '

%s

', wp_get_extension_error_description( $error ) ); 507 | } 508 | } 509 | 510 | echo ''; 511 | break; 512 | case 'auto-updates': 513 | if ( ! $this->show_autoupdates || in_array( $status, array( 'mustuse', 'dropins' ), true ) ) { 514 | break; 515 | } 516 | 517 | echo ""; 518 | 519 | $html = array(); 520 | 521 | if ( isset( $plugin_data['auto-update-forced'] ) ) { 522 | if ( $plugin_data['auto-update-forced'] ) { 523 | // Forced on. 524 | $text = __( 'Auto-updates enabled' ); 525 | } else { 526 | $text = __( 'Auto-updates disabled' ); 527 | } 528 | $action = 'unavailable'; 529 | $time_class = ' hidden'; 530 | } elseif ( empty( $plugin_data['update-supported'] ) ) { 531 | $text = ''; 532 | $action = 'unavailable'; 533 | $time_class = ' hidden'; 534 | } elseif ( in_array( $plugin_file, $auto_updates, true ) ) { 535 | $text = __( 'Disable auto-updates' ); 536 | $action = 'disable'; 537 | $time_class = ''; 538 | } else { 539 | $text = __( 'Enable auto-updates' ); 540 | $action = 'enable'; 541 | $time_class = ' hidden'; 542 | } 543 | 544 | $query_args = array( 545 | 'action' => "{$action}-auto-update", 546 | 'plugin' => $plugin_file, 547 | 'paged' => $page, 548 | 'plugin_status' => $status, 549 | ); 550 | 551 | $url = add_query_arg( $query_args, 'plugins.php' ); 552 | 553 | if ( 'unavailable' === $action ) { 554 | $html[] = '' . $text . ''; 555 | } else { 556 | $html[] = sprintf( 557 | '', 558 | wp_nonce_url( $url, 'updates' ), 559 | $action 560 | ); 561 | 562 | $html[] = ''; 563 | $html[] = '' . $text . ''; 564 | $html[] = ''; 565 | } 566 | 567 | if ( ! empty( $plugin_data['update'] ) ) { 568 | $html[] = sprintf( 569 | '
%s
', 570 | $time_class, 571 | wp_get_auto_update_message() 572 | ); 573 | } 574 | 575 | $html = implode( '', $html ); 576 | 577 | /** 578 | * Filters the HTML of the auto-updates setting for each plugin in the Plugins list table. 579 | * 580 | * @since 5.5.0 581 | * 582 | * @param string $html The HTML of the plugin's auto-update column content, 583 | * including toggle auto-update action links and 584 | * time to next update. 585 | * @param string $plugin_file Path to the plugin file relative to the plugins directory. 586 | * @param array $plugin_data An array of plugin data. See get_plugin_data() 587 | * and the {@see 'plugin_row_meta'} filter for the list 588 | * of possible values. 589 | */ 590 | echo apply_filters( 'plugin_auto_update_setting_html', $html, $plugin_file, $plugin_data ); 591 | 592 | echo ''; 593 | echo ''; 594 | 595 | break; 596 | default: 597 | $classes = "$column_name column-$column_name $class"; 598 | 599 | echo ""; 600 | 601 | /** 602 | * Fires inside each custom column of the Plugins list table. 603 | * 604 | * @since 3.1.0 605 | * 606 | * @param string $column_name Name of the column. 607 | * @param string $plugin_file Path to the plugin file relative to the plugins directory. 608 | * @param array $plugin_data An array of plugin data. See get_plugin_data() 609 | * and the {@see 'plugin_row_meta'} filter for the list 610 | * of possible values. 611 | */ 612 | do_action( 'manage_plugins_custom_column', $column_name, $plugin_file, $plugin_data ); 613 | 614 | echo ''; 615 | } 616 | } 617 | 618 | echo ''; 619 | 620 | if ( ! $compatible_php || ! $compatible_wp ) { 621 | printf( 622 | '' . 623 | '' . 624 | '

', 625 | esc_attr( $this->get_column_count() ) 626 | ); 627 | 628 | if ( ! $compatible_php && ! $compatible_wp ) { 629 | _e( 'This plugin does not work with your versions of WordPress and PHP.' ); 630 | if ( current_user_can( 'update_core' ) && current_user_can( 'update_php' ) ) { 631 | printf( 632 | /* translators: 1: URL to WordPress Updates screen, 2: URL to Update PHP page. */ 633 | ' ' . __( 'Please update WordPress, and then learn more about updating PHP.' ), 634 | self_admin_url( 'update-core.php' ), 635 | esc_url( wp_get_update_php_url() ) 636 | ); 637 | wp_update_php_annotation( '

', '' ); 638 | } elseif ( current_user_can( 'update_core' ) ) { 639 | printf( 640 | /* translators: %s: URL to WordPress Updates screen. */ 641 | ' ' . __( 'Please update WordPress.' ), 642 | self_admin_url( 'update-core.php' ) 643 | ); 644 | } elseif ( current_user_can( 'update_php' ) ) { 645 | printf( 646 | /* translators: %s: URL to Update PHP page. */ 647 | ' ' . __( 'Learn more about updating PHP.' ), 648 | esc_url( wp_get_update_php_url() ) 649 | ); 650 | wp_update_php_annotation( '

', '' ); 651 | } 652 | } elseif ( ! $compatible_wp ) { 653 | _e( 'This plugin does not work with your version of WordPress.' ); 654 | if ( current_user_can( 'update_core' ) ) { 655 | printf( 656 | /* translators: %s: URL to WordPress Updates screen. */ 657 | ' ' . __( 'Please update WordPress.' ), 658 | self_admin_url( 'update-core.php' ) 659 | ); 660 | } 661 | } elseif ( ! $compatible_php ) { 662 | _e( 'This plugin does not work with your version of PHP.' ); 663 | if ( current_user_can( 'update_php' ) ) { 664 | printf( 665 | /* translators: %s: URL to Update PHP page. */ 666 | ' ' . __( 'Learn more about updating PHP.' ), 667 | esc_url( wp_get_update_php_url() ) 668 | ); 669 | wp_update_php_annotation( '

', '' ); 670 | } 671 | } 672 | 673 | echo '

'; 674 | } 675 | 676 | /** 677 | * Fires after each row in the Plugins list table. 678 | * 679 | * @since 2.3.0 680 | * @since 5.5.0 Added 'auto-update-enabled' and 'auto-update-disabled' 681 | * to possible values for `$status`. 682 | * 683 | * @param string $plugin_file Path to the plugin file relative to the plugins directory. 684 | * @param array $plugin_data An array of plugin data. See get_plugin_data() 685 | * and the {@see 'plugin_row_meta'} filter for the list 686 | * of possible values. 687 | * @param string $status Status filter currently applied to the plugin list. 688 | * Possible values are: 'all', 'active', 'inactive', 689 | * 'recently_activated', 'upgrade', 'mustuse', 'dropins', 690 | * 'search', 'paused', 'auto-update-enabled', 'auto-update-disabled'. 691 | */ 692 | do_action( 'after_plugin_row', $plugin_file, $plugin_data, $status ); 693 | 694 | /** 695 | * Fires after each specific row in the Plugins list table. 696 | * 697 | * The dynamic portion of the hook name, `$plugin_file`, refers to the path 698 | * to the plugin file, relative to the plugins directory. 699 | * 700 | * @since 2.7.0 701 | * @since 5.5.0 Added 'auto-update-enabled' and 'auto-update-disabled' 702 | * to possible values for `$status`. 703 | * 704 | * @param string $plugin_file Path to the plugin file relative to the plugins directory. 705 | * @param array $plugin_data An array of plugin data. See get_plugin_data() 706 | * and the {@see 'plugin_row_meta'} filter for the list 707 | * of possible values. 708 | * @param string $status Status filter currently applied to the plugin list. 709 | * Possible values are: 'all', 'active', 'inactive', 710 | * 'recently_activated', 'upgrade', 'mustuse', 'dropins', 711 | * 'search', 'paused', 'auto-update-enabled', 'auto-update-disabled'. 712 | */ 713 | do_action( "after_plugin_row_{$plugin_file}", $plugin_file, $plugin_data, $status ); 714 | } 715 | 716 | /** 717 | * Prints a list of other plugins that depend on the plugin. 718 | * 719 | * @since 6.5.0 720 | * 721 | * @param string $dependency The dependency's filepath, relative to the plugins directory. 722 | */ 723 | protected function add_dependents_to_dependency_plugin_row( $dependency ) { 724 | $dependent_names = \WP_Plugin_Dependencies::get_dependent_names( $dependency ); 725 | 726 | if ( empty( $dependent_names ) ) { 727 | return; 728 | } 729 | 730 | printf( 731 | '
%1$s %2$s
', 732 | __( 'Required by:' ), 733 | esc_html( implode( ' | ', $dependent_names ) ) 734 | ); 735 | } 736 | 737 | /** 738 | * Prints a list of other plugins that the plugin depends on. 739 | * 740 | * @since 6.5.0 741 | * 742 | * @param string $dependent The dependent plugin's filepath, relative to the plugins directory. 743 | */ 744 | protected function add_dependencies_to_dependent_plugin_row( $dependent ) { 745 | $dependency_names = \WP_Plugin_Dependencies::get_dependency_names( $dependent ); 746 | 747 | if ( array() === $dependency_names ) { 748 | return; 749 | } 750 | 751 | $links = array(); 752 | foreach ( $dependency_names as $slug => $name ) { 753 | $links[] = $this->get_dependency_view_details_link( $name, $slug ); 754 | } 755 | 756 | printf( 757 | '
%1$s %2$s
', 758 | __( 'Requires:' ), 759 | implode( ' | ', $links ) 760 | ); 761 | } 762 | 763 | /** 764 | * Returns a 'View details' like link for a dependency. 765 | * 766 | * @since 6.5.0 767 | * 768 | * @param string $name The dependency's name. 769 | * @param string $slug The dependency's slug. 770 | * @return string A 'View details' link for the dependency. 771 | */ 772 | protected function get_dependency_view_details_link( $name, $slug ) { 773 | $dependency_data = \WP_Plugin_Dependencies::get_dependency_data( $slug ); 774 | 775 | if ( false === $dependency_data 776 | || $name === $slug 777 | || $name !== $dependency_data['name'] 778 | || empty( $dependency_data['version'] ) 779 | ) { 780 | return $name; 781 | } 782 | 783 | return $this->get_view_details_link( $name, $slug ); 784 | } 785 | 786 | /** 787 | * Returns a 'View details' link for the plugin. 788 | * 789 | * @since 6.5.0 790 | * 791 | * @param string $name The plugin's name. 792 | * @param string $slug The plugin's slug. 793 | * @return string A 'View details' link for the plugin. 794 | */ 795 | protected function get_view_details_link( $name, $slug ) { 796 | $url = add_query_arg( 797 | array( 798 | 'tab' => 'plugin-information', 799 | 'plugin' => $slug, 800 | 'TB_iframe' => 'true', 801 | 'width' => '600', 802 | 'height' => '550', 803 | ), 804 | network_admin_url( 'plugin-install.php' ) 805 | ); 806 | 807 | $name_attr = esc_attr( $name ); 808 | return sprintf( 809 | "%s", 810 | esc_url( $url ), 811 | /* translators: %s: Plugin name. */ 812 | sprintf( __( 'More information about %s' ), $name_attr ), 813 | $name_attr, 814 | esc_html( $name ) 815 | ); 816 | } 817 | } 818 | -------------------------------------------------------------------------------- /src/wp-admin/includes/plugin-install.php: -------------------------------------------------------------------------------- 1 | requires_plugins ?? array(); 36 | 37 | // Determine the status of plugin dependencies. 38 | $installed_plugins = get_plugins(); 39 | $active_plugins = get_option( 'active_plugins' ); 40 | $plugin_dependencies_count = count( $requires_plugins ); 41 | $installed_plugin_dependencies_count = 0; 42 | $active_plugin_dependencies_count = 0; 43 | foreach ( $requires_plugins as $dependency ) { 44 | foreach ( array_keys( $installed_plugins ) as $installed_plugin_file ) { 45 | if ( str_contains( $installed_plugin_file, '/' ) && explode( '/', $installed_plugin_file )[0] === $dependency ) { 46 | ++$installed_plugin_dependencies_count; 47 | } 48 | } 49 | 50 | foreach ( $active_plugins as $active_plugin_file ) { 51 | if ( str_contains( $active_plugin_file, '/' ) && explode( '/', $active_plugin_file )[0] === $dependency ) { 52 | ++$active_plugin_dependencies_count; 53 | } 54 | } 55 | } 56 | $all_plugin_dependencies_installed = $installed_plugin_dependencies_count === $plugin_dependencies_count; 57 | $all_plugin_dependencies_active = $active_plugin_dependencies_count === $plugin_dependencies_count; 58 | 59 | sprintf( 60 | '%s', 61 | esc_attr( $data->slug ), 62 | esc_url( $status['url'] ), 63 | /* translators: %s: Plugin name and version. */ 64 | esc_attr( sprintf( _x( 'Install %s now', 'plugin' ), $name ) ), 65 | esc_attr( $name ), 66 | __( 'Install Now' ) 67 | ); 68 | 69 | if ( current_user_can( 'install_plugins' ) || current_user_can( 'update_plugins' ) ) { 70 | switch ( $status['status'] ) { 71 | case 'install': 72 | if ( $status['url'] ) { 73 | if ( $compatible_php && $compatible_wp && $all_plugin_dependencies_installed && ! empty( $data->download_link ) ) { 74 | $button = sprintf( 75 | '%s', 76 | esc_attr( $data->slug ), 77 | esc_url( $status['url'] ), 78 | /* translators: %s: Plugin name and version. */ 79 | esc_attr( sprintf( _x( 'Install %s now', 'plugin' ), $name ) ), 80 | esc_attr( $name ), 81 | __( 'Install Now' ) 82 | ); 83 | } else { 84 | $button = sprintf( 85 | '', 86 | _x( 'Install Now', 'plugin' ) 87 | ); 88 | } 89 | } 90 | break; 91 | 92 | case 'update_available': 93 | if ( $status['url'] ) { 94 | if ( $compatible_php && $compatible_wp ) { 95 | $button = sprintf( 96 | '%s', 97 | esc_attr( $status['file'] ), 98 | esc_attr( $data->slug ), 99 | esc_url( $status['url'] ), 100 | /* translators: %s: Plugin name and version. */ 101 | esc_attr( sprintf( _x( 'Update %s now', 'plugin' ), $name ) ), 102 | esc_attr( $name ), 103 | __( 'Update Now' ) 104 | ); 105 | } else { 106 | $button = sprintf( 107 | '', 108 | _x( 'Update Now', 'plugin' ) 109 | ); 110 | } 111 | } 112 | break; 113 | 114 | case 'latest_installed': 115 | case 'newer_installed': 116 | if ( is_plugin_active( $status['file'] ) ) { 117 | $button = sprintf( 118 | '', 119 | _x( 'Active', 'plugin' ) 120 | ); 121 | } elseif ( current_user_can( 'activate_plugin', $status['file'] ) ) { 122 | if ( $compatible_php && $compatible_wp && $all_plugin_dependencies_active ) { 123 | $button_text = __( 'Activate' ); 124 | /* translators: %s: Plugin name. */ 125 | $button_label = _x( 'Activate %s', 'plugin' ); 126 | $activate_url = add_query_arg( 127 | array( 128 | '_wpnonce' => wp_create_nonce( 'activate-plugin_' . $status['file'] ), 129 | 'action' => 'activate', 130 | 'plugin' => $status['file'], 131 | ), 132 | network_admin_url( 'plugins.php' ) 133 | ); 134 | 135 | if ( is_network_admin() ) { 136 | $button_text = __( 'Network Activate' ); 137 | /* translators: %s: Plugin name. */ 138 | $button_label = _x( 'Network Activate %s', 'plugin' ); 139 | $activate_url = add_query_arg( array( 'networkwide' => 1 ), $activate_url ); 140 | } 141 | 142 | $button = sprintf( 143 | '%3$s', 144 | esc_url( $activate_url ), 145 | esc_attr( sprintf( $button_label, $name ) ), 146 | $button_text 147 | ); 148 | } else { 149 | $button = sprintf( 150 | '', 151 | is_network_admin() ? _x( 'Network Activate %s', 'plugin' ) : _x( 'Activate', 'plugin' ) 152 | ); 153 | } 154 | } else { 155 | $button = sprintf( 156 | '', 157 | _x( 'Installed', 'plugin' ) 158 | ); 159 | } 160 | break; 161 | } 162 | 163 | return $button; 164 | } 165 | } 166 | 167 | /** 168 | * Displays plugin information in dialog box form. 169 | * 170 | * ONLY PART IS FOR CORE MERGE. See "// START CORE MERGE". 171 | * 172 | * @global string $tab 173 | */ 174 | function pd_install_plugin_information() { 175 | global $tab; 176 | 177 | if ( empty( $_REQUEST['plugin'] ) ) { 178 | return; 179 | } 180 | 181 | $api = plugins_api( 182 | 'plugin_information', 183 | array( 184 | 'slug' => wp_unslash( $_REQUEST['plugin'] ), 185 | ) 186 | ); 187 | 188 | if ( is_wp_error( $api ) ) { 189 | wp_die( $api ); 190 | } 191 | 192 | $plugins_allowedtags = array( 193 | 'a' => array( 194 | 'href' => array(), 195 | 'title' => array(), 196 | 'target' => array(), 197 | ), 198 | 'abbr' => array( 'title' => array() ), 199 | 'acronym' => array( 'title' => array() ), 200 | 'code' => array(), 201 | 'pre' => array(), 202 | 'em' => array(), 203 | 'strong' => array(), 204 | 'div' => array( 'class' => array() ), 205 | 'span' => array( 'class' => array() ), 206 | 'p' => array(), 207 | 'br' => array(), 208 | 'ul' => array(), 209 | 'ol' => array(), 210 | 'li' => array(), 211 | 'h1' => array(), 212 | 'h2' => array(), 213 | 'h3' => array(), 214 | 'h4' => array(), 215 | 'h5' => array(), 216 | 'h6' => array(), 217 | 'img' => array( 218 | 'src' => array(), 219 | 'class' => array(), 220 | 'alt' => array(), 221 | ), 222 | 'blockquote' => array( 'cite' => true ), 223 | ); 224 | 225 | $plugins_section_titles = array( 226 | 'description' => _x( 'Description', 'Plugin installer section title' ), 227 | 'installation' => _x( 'Installation', 'Plugin installer section title' ), 228 | 'faq' => _x( 'FAQ', 'Plugin installer section title' ), 229 | 'screenshots' => _x( 'Screenshots', 'Plugin installer section title' ), 230 | 'changelog' => _x( 'Changelog', 'Plugin installer section title' ), 231 | 'reviews' => _x( 'Reviews', 'Plugin installer section title' ), 232 | 'other_notes' => _x( 'Other Notes', 'Plugin installer section title' ), 233 | ); 234 | 235 | // Sanitize HTML. 236 | foreach ( (array) $api->sections as $section_name => $content ) { 237 | $api->sections[ $section_name ] = wp_kses( $content, $plugins_allowedtags ); 238 | } 239 | 240 | foreach ( array( 'version', 'author', 'requires', 'tested', 'homepage', 'downloaded', 'slug' ) as $key ) { 241 | if ( isset( $api->$key ) ) { 242 | $api->$key = wp_kses( $api->$key, $plugins_allowedtags ); 243 | } 244 | } 245 | 246 | $_tab = esc_attr( $tab ); 247 | 248 | // Default to the Description tab, Do not translate, API returns English. 249 | $section = isset( $_REQUEST['section'] ) ? wp_unslash( $_REQUEST['section'] ) : 'description'; 250 | if ( empty( $section ) || ! isset( $api->sections[ $section ] ) ) { 251 | $section_titles = array_keys( (array) $api->sections ); 252 | $section = reset( $section_titles ); 253 | } 254 | 255 | iframe_header( __( 'Plugin Installation' ) ); 256 | 257 | $_with_banner = ''; 258 | 259 | if ( ! empty( $api->banners ) && ( ! empty( $api->banners['low'] ) || ! empty( $api->banners['high'] ) ) ) { 260 | $_with_banner = 'with-banner'; 261 | $low = empty( $api->banners['low'] ) ? $api->banners['high'] : $api->banners['low']; 262 | $high = empty( $api->banners['high'] ) ? $api->banners['low'] : $api->banners['high']; 263 | ?> 264 | 274 | '; 278 | echo "

{$api->name}

"; 279 | echo "
\n"; 280 | 281 | foreach ( (array) $api->sections as $section_name => $content ) { 282 | if ( 'reviews' === $section_name && ( empty( $api->ratings ) || 0 === array_sum( (array) $api->ratings ) ) ) { 283 | continue; 284 | } 285 | 286 | if ( isset( $plugins_section_titles[ $section_name ] ) ) { 287 | $title = $plugins_section_titles[ $section_name ]; 288 | } else { 289 | $title = ucwords( str_replace( '_', ' ', $section_name ) ); 290 | } 291 | 292 | $class = ( $section_name === $section ) ? ' class="current"' : ''; 293 | $href = add_query_arg( 294 | array( 295 | 'tab' => $tab, 296 | 'section' => $section_name, 297 | ) 298 | ); 299 | $href = esc_url( $href ); 300 | $san_section = esc_attr( $section_name ); 301 | echo "\t$title\n"; 302 | } 303 | 304 | echo "
\n"; 305 | 306 | ?> 307 |
308 |
309 |
    310 | version ) ) { ?> 311 |
  • version; ?>
  • 312 | author ) ) { ?> 313 |
  • author, '_blank' ); ?>
  • 314 | last_updated ) ) { ?> 315 |
  • 316 | last_updated ) ) ); 319 | ?> 320 |
  • 321 | requires ) ) { ?> 322 |
  • 323 | 324 | requires ); 327 | ?> 328 |
  • 329 | tested ) ) { ?> 330 |
  • tested; ?>
  • 331 | requires_php ) ) { ?> 332 |
  • 333 | 334 | requires_php ); 337 | ?> 338 |
  • 339 | active_installs ) ) { ?> 340 |
  • 341 | active_installs >= 1000000 ) { 343 | $active_installs_millions = floor( $api->active_installs / 1000000 ); 344 | printf( 345 | /* translators: %s: Number of millions. */ 346 | _nx( '%s+ Million', '%s+ Million', $active_installs_millions, 'Active plugin installations' ), 347 | number_format_i18n( $active_installs_millions ) 348 | ); 349 | } elseif ( $api->active_installs < 10 ) { 350 | _ex( 'Less Than 10', 'Active plugin installations' ); 351 | } else { 352 | echo number_format_i18n( $api->active_installs ) . '+'; 353 | } 354 | ?> 355 |
  • 356 | slug ) && empty( $api->external ) ) { ?> 357 |
  • 358 | homepage ) ) { ?> 359 |
  • 360 | donate_link ) && empty( $api->contributors ) ) { ?> 361 |
  • 362 | 363 |
364 | rating ) ) { ?> 365 |

366 | $api->rating, 370 | 'type' => 'percent', 371 | 'number' => $api->num_ratings, 372 | ) 373 | ); 374 | ?> 375 | 384 | ratings ) && array_sum( (array) $api->ratings ) > 0 ) { 388 | ?> 389 |

390 |

391 | ratings as $key => $ratecount ) { 393 | // Avoid div-by-zero. 394 | $_rating = $api->num_ratings ? ( $ratecount / $api->num_ratings ) : 0; 395 | $aria_label = esc_attr( 396 | sprintf( 397 | /* translators: 1: Number of stars (used to determine singular/plural), 2: Number of reviews. */ 398 | _n( 399 | 'Reviews with %1$d star: %2$s. Opens in a new tab.', 400 | 'Reviews with %1$d stars: %2$s. Opens in a new tab.', 401 | $key 402 | ), 403 | $key, 404 | number_format_i18n( $ratecount ) 405 | ) 406 | ); 407 | ?> 408 |
409 | 410 | %s', 413 | "https://wordpress.org/support/plugin/{$api->slug}/reviews/?filter={$key}", 414 | $aria_label, 415 | /* translators: %s: Number of stars. */ 416 | sprintf( _n( '%d star', '%d stars', $key ), $key ) 417 | ); 418 | ?> 419 | 420 | 421 | 422 | 423 | 424 |
425 | contributors ) ) { 429 | ?> 430 |

431 |
    432 | contributors as $contrib_username => $contrib_details ) { 434 | $contrib_name = $contrib_details['display_name']; 435 | if ( ! $contrib_name ) { 436 | $contrib_name = $contrib_username; 437 | } 438 | $contrib_name = esc_html( $contrib_name ); 439 | 440 | $contrib_profile = esc_url( $contrib_details['profile'] ); 441 | $contrib_avatar = esc_url( add_query_arg( 's', '36', $contrib_details['avatar'] ) ); 442 | 443 | echo "
  • {$contrib_name}
  • "; 444 | } 445 | ?> 446 |
447 | donate_link ) ) { ?> 448 | 449 | 450 | 451 |
452 |
453 | requires_php ) ? $api->requires_php : null; 455 | $requires_wp = isset( $api->requires ) ? $api->requires : null; 456 | 457 | $compatible_php = is_php_version_compatible( $requires_php ); 458 | $compatible_wp = is_wp_version_compatible( $requires_wp ); 459 | $tested_wp = ( empty( $api->tested ) || version_compare( get_bloginfo( 'version' ), $api->tested, '<=' ) ); 460 | 461 | if ( ! $compatible_php ) { 462 | echo '

'; 463 | _e( 'Error: This plugin requires a newer version of PHP.' ); 464 | if ( current_user_can( 'update_php' ) ) { 465 | printf( 466 | /* translators: %s: URL to Update PHP page. */ 467 | ' ' . __( 'Click here to learn more about updating PHP.' ), 468 | esc_url( wp_get_update_php_url() ) 469 | ); 470 | 471 | wp_update_php_annotation( '

', '' ); 472 | } else { 473 | echo '

'; 474 | } 475 | echo '
'; 476 | } 477 | 478 | if ( ! $tested_wp ) { 479 | echo '

'; 480 | _e( 'Warning: This plugin has not been tested with your current version of WordPress.' ); 481 | echo '

'; 482 | } elseif ( ! $compatible_wp ) { 483 | echo '

'; 484 | _e( 'Error: This plugin requires a newer version of WordPress.' ); 485 | if ( current_user_can( 'update_core' ) ) { 486 | printf( 487 | /* translators: %s: URL to WordPress Updates screen. */ 488 | ' ' . __( 'Click here to update WordPress.' ), 489 | esc_url( self_admin_url( 'update-core.php' ) ) 490 | ); 491 | } 492 | echo '

'; 493 | } 494 | 495 | foreach ( (array) $api->sections as $section_name => $content ) { 496 | $content = links_add_base_url( $content, 'https://wordpress.org/plugins/' . $api->slug . '/' ); 497 | $content = links_add_target( $content, '_blank' ); 498 | 499 | $san_section = esc_attr( $section_name ); 500 | 501 | $display = ( $section_name === $section ) ? 'block' : 'none'; 502 | 503 | echo "\t
\n"; 504 | echo $content; 505 | echo "\t
\n"; 506 | } 507 | echo "
\n"; 508 | echo "
\n"; 509 | echo "\n"; // #plugin-information-scrollable 510 | echo "\n"; 524 | 525 | iframe_footer(); 526 | exit; 527 | } 528 | 529 | remove_action( 'install_plugins_pre_plugin-information', 'install_plugin_information' ); 530 | add_action( 'install_plugins_pre_plugin-information', 'pd_install_plugin_information' ); 531 | -------------------------------------------------------------------------------- /src/wp-admin/js/updates.js: -------------------------------------------------------------------------------- 1 | (function( $, wp, pagenow ) { 2 | var $document = $( document ), 3 | __ = wp.i18n.__, 4 | _x = wp.i18n._x, 5 | sprintf = wp.i18n.sprintf; 6 | 7 | wp.updates.installPluginSuccess = function( response ) { 8 | var $message = $( '.plugin-card-' + response.slug ).find( '.install-now' ); 9 | 10 | $message 11 | .removeClass( 'updating-message' ) 12 | .addClass( 'updated-message installed button-disabled' ) 13 | .attr( 14 | 'aria-label', 15 | sprintf( 16 | /* translators: %s: Plugin name and version. */ 17 | _x( '%s installed!', 'plugin' ), 18 | response.pluginName 19 | ) 20 | ) 21 | .text( _x( 'Installed!', 'plugin' ) ); 22 | 23 | wp.a11y.speak( __( 'Installation completed successfully.' ) ); 24 | 25 | $document.trigger( 'wp-plugin-install-success', response ); 26 | 27 | if ( response.activateUrl ) { 28 | setTimeout( function() { 29 | wp.updates.ajax( 30 | 'check_plugin_dependencies', 31 | { 32 | slug: response.slug, 33 | success: function() { 34 | // Transform the 'Install' button into an 'Activate' button. 35 | $message.removeClass( 'install-now installed button-disabled updated-message' ) 36 | .addClass( 'activate-now button-primary' ) 37 | .attr( 'href', response.activateUrl ); 38 | 39 | if ( 'plugins-network' === pagenow ) { 40 | $message 41 | .attr( 42 | 'aria-label', 43 | sprintf( 44 | /* translators: %s: Plugin name. */ 45 | _x( 'Network Activate %s', 'plugin' ), 46 | response.pluginName 47 | ) 48 | ) 49 | .text( __( 'Network Activate' ) ); 50 | } else { 51 | $message 52 | .attr( 53 | 'aria-label', 54 | sprintf( 55 | /* translators: %s: Plugin name. */ 56 | _x( 'Activate %s', 'plugin' ), 57 | response.pluginName 58 | ) 59 | ) 60 | .text( __( 'Activate' ) ); 61 | } 62 | }, 63 | error: function( error ) { 64 | $message 65 | .removeClass( 'install-now installed updated-message' ) 66 | .addClass( 'activate-now button-primary' ) 67 | .attr( 68 | 'aria-label', 69 | sprintf( 70 | /* translators: 1: Plugin name, 2. The reason the plugin cannot be activated. */ 71 | _x( 'Cannot activate %1$s. %2$s', 'plugin' ), 72 | response.pluginName, 73 | error.errorMessage 74 | ) 75 | ) 76 | .text( __( 'Activate' ) ); 77 | } 78 | } 79 | ); 80 | }, 1000 ); 81 | } 82 | }; 83 | })( jQuery, window.wp, window._wpUpdatesSettings ); 84 | -------------------------------------------------------------------------------- /src/wp-includes/class-wp-plugin-dependencies.php: -------------------------------------------------------------------------------- 1 | $dependencies ) { 166 | if ( in_array( $slug, $dependencies, true ) ) { 167 | $dependents[] = $dependent; 168 | } 169 | } 170 | 171 | return $dependents; 172 | } 173 | 174 | /** 175 | * Gets the slugs of plugins that the dependent requires. 176 | * 177 | * @since 6.5.0 178 | * 179 | * @param string $plugin_file The dependent plugin's filepath, relative to the plugins directory. 180 | * @return array An array of dependency plugin slugs. 181 | */ 182 | public static function get_dependencies( $plugin_file ) { 183 | if ( isset( self::$dependencies[ $plugin_file ] ) ) { 184 | return self::$dependencies[ $plugin_file ]; 185 | } 186 | 187 | return array(); 188 | } 189 | 190 | /** 191 | * Gets a dependent plugin's filepath. 192 | * 193 | * @since 6.5.0 194 | * 195 | * @param string $slug The dependent plugin's slug. 196 | * @return string|false The dependent plugin's filepath, relative to the plugins directory, 197 | * or false if the plugin has no dependencies. 198 | */ 199 | public static function get_dependent_filepath( $slug ) { 200 | if ( ! isset( self::$dependent_slugs[ $slug ] ) ) { 201 | return false; 202 | } 203 | 204 | return self::$dependent_slugs[ $slug ]; 205 | } 206 | 207 | /** 208 | * Determines whether the plugin has unmet dependencies. 209 | * 210 | * @since 6.5.0 211 | * 212 | * @param string $plugin_file The plugin's filepath, relative to the plugins directory. 213 | * @return bool Whether the plugin has unmet dependencies. 214 | */ 215 | public static function has_unmet_dependencies( $plugin_file ) { 216 | if ( ! isset( self::$dependencies[ $plugin_file ] ) ) { 217 | return false; 218 | } 219 | 220 | foreach ( self::$dependencies[ $plugin_file ] as $dependency ) { 221 | $dependency_filepath = self::get_dependency_filepath( $dependency ); 222 | 223 | if ( false === $dependency_filepath || is_plugin_inactive( $dependency_filepath ) ) { 224 | return true; 225 | } 226 | } 227 | 228 | return false; 229 | } 230 | 231 | /** 232 | * Gets the names of plugins that require the plugin. 233 | * 234 | * @since 6.5.0 235 | * 236 | * @param string $plugin_file The plugin's filepath, relative to the plugins directory. 237 | * @return array An array of dependent names. 238 | */ 239 | public static function get_dependent_names( $plugin_file ) { 240 | $dependent_names = array(); 241 | 242 | if ( empty( self::$plugins ) ) { 243 | self::$plugins = get_plugins(); 244 | } 245 | 246 | $slug = self::convert_to_slug( $plugin_file ); 247 | 248 | foreach ( self::get_dependents( $slug ) as $dependent ) { 249 | if ( ! isset( $dependent_names[ $dependent ] ) ) { 250 | $dependent_names[ $dependent ] = self::$plugins[ $dependent ]['Name']; 251 | } 252 | } 253 | sort( $dependent_names ); 254 | 255 | return $dependent_names; 256 | } 257 | 258 | /** 259 | * Gets the names of plugins required by the plugin. 260 | * 261 | * @since 6.5.0 262 | * 263 | * @param string $plugin_file The dependent plugin's filepath, relative to the plugins directory. 264 | * @return array An array of dependency names. 265 | */ 266 | public static function get_dependency_names( $plugin_file ) { 267 | if ( empty( self::$dependency_api_data ) ) { 268 | self::get_dependency_api_data(); 269 | } 270 | 271 | $dependencies = self::get_dependencies( $plugin_file ); 272 | $dependency_filepaths = self::get_dependency_filepaths(); 273 | 274 | $dependency_names = array(); 275 | foreach ( $dependencies as $dependency ) { 276 | if ( ! isset( $dependency_filepaths[ $dependency ] ) ) { 277 | continue; 278 | } 279 | 280 | // Use the name if it's available, otherwise fall back to the slug. 281 | if ( isset( self::$dependency_api_data[ $dependency ]['name'] ) ) { 282 | $name = self::$dependency_api_data[ $dependency ]['name']; 283 | } elseif ( isset( self::$plugin_dirnames[ $dependency ] ) ) { 284 | $dependency_filepath = self::get_dependency_filepath( $dependency ); 285 | if ( false !== $dependency_filepath ) { 286 | $name = self::$plugins[ $dependency_filepath ]['Name']; 287 | } 288 | } else { 289 | $name = $dependency; 290 | } 291 | 292 | $dependency_names[ $dependency ] = $name; 293 | } 294 | 295 | return $dependency_names; 296 | } 297 | 298 | /** 299 | * Gets the filepath for a dependency, relative to the plugin's directory. 300 | * 301 | * @since 6.5.0 302 | * 303 | * @param string $slug The dependency's slug. 304 | * @return string|false If installed, the dependency's filepath relative to the plugins directory, otherwise false. 305 | */ 306 | public static function get_dependency_filepath( $slug ) { 307 | if ( empty( self::$dependency_filepaths ) ) { 308 | self::get_dependency_filepaths(); 309 | } 310 | 311 | if ( ! isset( self::$dependency_filepaths[ $slug ] ) ) { 312 | return false; 313 | } 314 | 315 | return self::$dependency_filepaths[ $slug ]; 316 | } 317 | 318 | /** 319 | * Returns API data for the dependency. 320 | * 321 | * @since 6.5.0 322 | * 323 | * @param string $slug The dependency's slug. 324 | * @return array|false The dependency's API data on success, otherwise false. 325 | */ 326 | public static function get_dependency_data( $slug ) { 327 | if ( empty( self::$dependency_api_data ) ) { 328 | self::get_dependency_api_data(); 329 | } 330 | 331 | if ( isset( self::$dependency_api_data[ $slug ] ) ) { 332 | return self::$dependency_api_data[ $slug ]; 333 | } 334 | 335 | return false; 336 | } 337 | 338 | /** 339 | * Displays an admin notice if dependencies are not installed. 340 | * 341 | * @since 6.5.0 342 | */ 343 | public static function display_admin_notice_for_unmet_dependencies() { 344 | if ( in_array( false, self::get_dependency_filepaths(), true ) ) { 345 | wp_admin_notice( 346 | __( 'There are additional plugin dependencies that must be installed.' ), 347 | array( 348 | 'type' => 'info', 349 | ) 350 | ); 351 | } 352 | } 353 | 354 | /** 355 | * Displays an admin notice if dependencies have been deactivated. 356 | * 357 | * @since 6.5.0 358 | */ 359 | public static function display_admin_notice_for_deactivated_dependents() { 360 | /* 361 | * Plugin deactivated if dependencies not met. 362 | * Transient on a 10 second timeout. 363 | */ 364 | $deactivate_requires = get_site_transient( 'wp_plugin_dependencies_deactivated_plugins' ); 365 | if ( ! empty( $deactivate_requires ) ) { 366 | $deactivated_plugins = ''; 367 | foreach ( $deactivate_requires as $deactivated ) { 368 | $deactivated_plugins .= '
  • ' . esc_html( self::$plugins[ $deactivated ]['Name'] ) . '
  • '; 369 | } 370 | wp_admin_notice( 371 | sprintf( 372 | /* translators: 1: plugin names */ 373 | __( 'The following plugin(s) have been deactivated due to uninstalled or inactive dependencies: %s' ), 374 | "
      $deactivated_plugins
    " 375 | ), 376 | array( 377 | 'type' => 'error', 378 | 'dismissible' => true, 379 | ) 380 | ); 381 | } 382 | } 383 | 384 | /** 385 | * Displays an admin notice if circular dependencies are installed. 386 | * 387 | * @since 6.5.0 388 | */ 389 | public static function display_admin_notice_for_circular_dependencies() { 390 | $circular_dependencies = self::get_circular_dependencies(); 391 | if ( ! empty( $circular_dependencies ) && count( $circular_dependencies ) > 1 ) { 392 | $circular_dependencies = array_unique( $circular_dependencies, SORT_REGULAR ); 393 | 394 | if ( ! empty( $circular_dependencies ) && empty( self::$plugin_dirnames ) ) { 395 | self::get_dependency_filepaths(); 396 | } 397 | 398 | // Build output lines. 399 | $circular_dependency_lines = ''; 400 | foreach ( $circular_dependencies as $circular_dependency ) { 401 | $first_filepath = self::$plugin_dirnames[ $circular_dependency[0] ]; 402 | $second_filepath = self::$plugin_dirnames[ $circular_dependency[1] ]; 403 | $circular_dependency_lines .= sprintf( 404 | /* translators: 1: First plugin name, 2: Second plugin name. */ 405 | '
  • ' . _x( '%1$s -> %2$s', 'The first plugin requires the second plugin.' ) . '
  • ', 406 | '' . esc_html( self::$plugins[ $first_filepath ]['Name'] ) . '', 407 | '' . esc_html( self::$plugins[ $second_filepath ]['Name'] ) . '' 408 | ); 409 | } 410 | 411 | wp_admin_notice( 412 | sprintf( 413 | '

    %1$s

      %2$s

    %3$s

    ', 414 | __( 'These plugins cannot be activated because their requirements form a loop: ' ), 415 | $circular_dependency_lines, 416 | __( 'Please contact the plugin developers and make them aware.' ) 417 | ), 418 | array( 419 | 'type' => 'warning', 420 | 'paragraph_wrap' => false, 421 | ) 422 | ); 423 | } 424 | } 425 | 426 | /** 427 | * Checks plugin dependencies after a plugin is installed via AJAX. 428 | * 429 | * @since 6.5.0 430 | */ 431 | public static function check_plugin_dependencies_during_ajax() { 432 | check_ajax_referer( 'updates' ); 433 | 434 | if ( empty( $_POST['slug'] ) ) { 435 | wp_send_json_error( 436 | array( 437 | 'slug' => '', 438 | 'errorCode' => 'no_plugin_specified', 439 | 'errorMessage' => __( 'No plugin specified.' ), 440 | ) 441 | ); 442 | } 443 | 444 | $slug = sanitize_key( wp_unslash( $_POST['slug'] ) ); 445 | $status = array( 'slug' => $slug ); 446 | 447 | self::get_plugins(); 448 | self::get_plugin_dirnames(); 449 | 450 | if ( ! isset( self::$plugin_dirnames[ $slug ] ) ) { 451 | $status['errorCode'] = 'plugin_not_installed'; 452 | $status['errorMessage'] = __( 'The plugin is not installed.' ); 453 | wp_send_json_error( $status ); 454 | } 455 | 456 | $plugin_file = self::$plugin_dirnames[ $slug ]; 457 | $dependencies = self::get_dependencies( $plugin_file ); 458 | 459 | if ( empty( $dependencies ) ) { 460 | $status['message'] = __( 'The plugin has no required plugins.' ); 461 | wp_send_json_success( $status ); 462 | } 463 | 464 | $inactive_dependencies = array(); 465 | foreach ( $dependencies as $dependency ) { 466 | if ( false === self::$plugin_dirnames[ $dependency ] || is_plugin_inactive( self::$plugin_dirnames[ $dependency ] ) ) { 467 | $inactive_dependencies[] = $dependency; 468 | } 469 | } 470 | 471 | if ( ! empty( $inactive_dependencies ) ) { 472 | $inactive_dependency_names = array_map( 473 | function ( $dependency ) { 474 | if ( isset( self::$dependency_api_data[ $dependency ]['Name'] ) ) { 475 | $inactive_dependency_name = self::$dependency_api_data[ $dependency ]['Name']; 476 | } else { 477 | $inactive_dependency_name = $dependency; 478 | } 479 | return $inactive_dependency_name; 480 | }, 481 | $inactive_dependencies 482 | ); 483 | 484 | $status['errorCode'] = 'inactive_dependencies'; 485 | $status['errorMessage'] = sprintf( 486 | /* translators: %s: A list of inactive dependency plugin names. */ 487 | __( 'The following plugins must be activated first: %s.' ), 488 | implode( ', ', $inactive_dependency_names ) 489 | ); 490 | $status['errorData'] = array_combine( $inactive_dependencies, $inactive_dependency_names ); 491 | 492 | wp_send_json_error( $status ); 493 | } 494 | 495 | $status['message'] = __( 'All required plugins are installed and activated.' ); 496 | wp_send_json_success( $status ); 497 | } 498 | 499 | /** 500 | * Stores the result of 'get_plugins()'. 501 | * 502 | * @since 6.5.0 503 | */ 504 | protected static function get_plugins() { 505 | if ( ! function_exists( 'get_plugins' ) ) { 506 | require_once ABSPATH . 'wp-admin/includes/plugin.php'; 507 | } 508 | self::$plugins = get_plugins(); 509 | } 510 | 511 | /** 512 | * Reads and stores dependency slugs from a plugin's 'Requires Plugins' header. 513 | * 514 | * @since 6.5.0 515 | * 516 | * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass. 517 | */ 518 | protected static function read_dependencies_from_plugin_headers() { 519 | global $wp_filesystem; 520 | 521 | if ( ! $wp_filesystem ) { 522 | require_once ABSPATH . '/wp-admin/includes/file.php'; 523 | WP_Filesystem(); 524 | } 525 | 526 | self::get_plugins(); 527 | $plugins_dir = trailingslashit( $wp_filesystem->wp_plugins_dir() ); 528 | $default_headers = array( 'RequiresPlugins' => 'Requires Plugins' ); 529 | 530 | foreach ( array_keys( self::$plugins ) as $plugin ) { 531 | $header = get_file_data( $plugins_dir . $plugin, $default_headers, 'plugin' ); 532 | 533 | if ( '' === $header['RequiresPlugins'] ) { 534 | continue; 535 | } 536 | 537 | self::$plugins[ $plugin ]['RequiresPlugins'] = $header['RequiresPlugins']; // TODO: remove for PR. 538 | 539 | $dependency_slugs = self::sanitize_dependency_slugs( $header['RequiresPlugins'] ); 540 | self::$dependencies[ $plugin ] = $dependency_slugs; 541 | self::$dependency_slugs = array_merge( self::$dependency_slugs, $dependency_slugs ); 542 | 543 | $dependent_slug = self::convert_to_slug( $plugin ); 544 | self::$dependent_slugs[ $plugin ] = $dependent_slug; 545 | } 546 | self::$dependency_slugs = array_unique( self::$dependency_slugs ); 547 | } 548 | 549 | /** 550 | * Sanitizes slugs. 551 | * 552 | * @since 6.5.0 553 | * 554 | * @param string $slugs A comma-separated string of plugin dependency slugs. 555 | * @return array An array of sanitized plugin dependency slugs. 556 | */ 557 | protected static function sanitize_dependency_slugs( $slugs ) { 558 | $sanitized_slugs = array(); 559 | $slugs = explode( ',', $slugs ); 560 | 561 | foreach ( $slugs as $slug ) { 562 | $slug = trim( $slug ); 563 | 564 | /** 565 | * Filters a plugin dependency's slug before matching to 566 | * the WordPress.org slug format. 567 | * 568 | * Can be used to switch between free and premium plugin slugs, for example. 569 | * 570 | * @since 6.5.0 571 | * 572 | * @param string $slug The slug. 573 | */ 574 | $slug = apply_filters( 'wp_plugin_dependencies_slug', $slug ); 575 | 576 | // Match to WordPress.org slug format. 577 | if ( preg_match( '/^[a-z0-9]+(-[a-z0-9]+)*$/mu', $slug ) ) { 578 | $sanitized_slugs[] = $slug; 579 | } 580 | } 581 | $sanitized_slugs = array_unique( $sanitized_slugs ); 582 | sort( $sanitized_slugs ); 583 | 584 | return $sanitized_slugs; 585 | } 586 | 587 | /** 588 | * Gets plugin filepaths for active plugins that depend on the dependency. 589 | * 590 | * Recurses for each dependent that is also a dependency. 591 | * 592 | * @param string $plugin_file The dependency's filepath, relative to the plugin directory. 593 | * @return string[] An array of active dependent plugin filepaths, relative to the plugin directory. 594 | */ 595 | protected static function get_active_dependents_in_dependency_tree( $plugin_file ) { 596 | $all_dependents = array(); 597 | $dependents = self::get_dependents( self::convert_to_slug( $plugin_file ) ); 598 | 599 | if ( empty( $dependents ) ) { 600 | return $all_dependents; 601 | } 602 | 603 | foreach ( $dependents as $dependent ) { 604 | if ( is_plugin_active( $dependent ) ) { 605 | $all_dependents[] = $dependent; 606 | $all_dependents = array_merge( 607 | $all_dependents, 608 | self::get_active_dependents_in_dependency_tree( $dependent ) 609 | ); 610 | } 611 | } 612 | 613 | return $all_dependents; 614 | } 615 | 616 | /** 617 | * Deactivates dependent plugins with unmet dependencies. 618 | * 619 | * @since 6.5.0 620 | */ 621 | protected static function deactivate_dependents_with_unmet_dependencies() { 622 | $dependents_to_deactivate = array(); 623 | $circular_dependencies = array_reduce( 624 | self::get_circular_dependencies(), 625 | function ( $all_circular, $circular_pair ) { 626 | return array_merge( $all_circular, $circular_pair ); 627 | }, 628 | array() 629 | ); 630 | 631 | foreach ( self::$dependencies as $dependent => $dependencies ) { 632 | // Skip dependents that are no longer installed or aren't active. 633 | if ( ! array_key_exists( $dependent, self::$plugins ) || is_plugin_inactive( $dependent ) ) { 634 | continue; 635 | } 636 | 637 | // Skip plugins within a circular dependency tree or plugins that have no unmet dependencies. 638 | if ( in_array( $dependent, $circular_dependencies, true ) || ! self::has_unmet_dependencies( $dependent ) ) { 639 | continue; 640 | } 641 | 642 | $dependents_to_deactivate[] = $dependent; 643 | 644 | // Also add any plugins that rely on any of this plugin's dependents. 645 | $dependents_to_deactivate = array_merge( 646 | $dependents_to_deactivate, 647 | self::get_active_dependents_in_dependency_tree( $dependent ) 648 | ); 649 | } 650 | 651 | $dependents_to_deactivate = array_unique( $dependents_to_deactivate ); 652 | 653 | deactivate_plugins( $dependents_to_deactivate ); 654 | set_site_transient( 'wp_plugin_dependencies_deactivated_plugins', $dependents_to_deactivate, 10 ); 655 | } 656 | 657 | /** 658 | * Gets the filepath of installed dependencies. 659 | * If a dependency is not installed, the filepath defaults to false. 660 | * 661 | * @since 6.5.0 662 | * 663 | * @return array An array of install dependencies filepaths, relative to the plugins directory. 664 | */ 665 | protected static function get_dependency_filepaths() { 666 | if ( ! empty( self::$dependency_filepaths ) ) { 667 | return self::$dependency_filepaths; 668 | } 669 | 670 | $dependency_filepaths = array(); 671 | 672 | $plugin_dirnames = self::get_plugin_dirnames(); 673 | if ( empty( $plugin_dirnames ) ) { 674 | return $dependency_filepaths; 675 | } 676 | 677 | foreach ( self::$dependency_slugs as $slug ) { 678 | if ( isset( $plugin_dirnames[ $slug ] ) ) { 679 | $dependency_filepaths[ $slug ] = self::$plugin_dirnames[ $slug ]; 680 | continue; 681 | } 682 | 683 | $dependency_filepaths[ $slug ] = false; 684 | } 685 | 686 | self::$dependency_filepaths = $dependency_filepaths; 687 | 688 | return self::$dependency_filepaths; 689 | } 690 | 691 | /** 692 | * Retrieves and stores dependency plugin data from the WordPress.org Plugin API. 693 | * 694 | * @since 6.5.0 695 | */ 696 | protected static function get_dependency_api_data() { 697 | self::$dependency_api_data = (array) get_site_transient( 'wp_plugin_dependencies_plugin_data' ); 698 | foreach ( self::$dependency_slugs as $slug ) { 699 | // Set transient for individual data, remove from self::$dependency_api_data if transient expired. 700 | if ( ! get_site_transient( "wp_plugin_dependencies_plugin_timeout_{$slug}" ) ) { 701 | unset( self::$dependency_api_data[ $slug ] ); 702 | set_site_transient( "wp_plugin_dependencies_plugin_timeout_{$slug}", true, 12 * HOUR_IN_SECONDS ); 703 | } 704 | 705 | if ( isset( self::$dependency_api_data[ $slug ] ) ) { 706 | if ( false === self::$dependency_api_data[ $slug ] ) { 707 | if ( empty( self::$plugin_dirnames ) ) { 708 | self::get_dependency_filepaths(); 709 | } 710 | 711 | $dependency_file = ! empty( self::$plugin_dirnames[ $slug ] ) 712 | ? self::$plugin_dirnames[ $slug ] 713 | : $slug; 714 | if ( isset( self::$plugins[ $dependency_file ] ) ) { 715 | self::$dependency_api_data[ $slug ] = array( 'Name' => self::$plugins[ $dependency_file ]['Name'] ); 716 | } else { 717 | self::$dependency_api_data[ $slug ] = array( 'Name' => $slug ); 718 | } 719 | continue; 720 | } 721 | 722 | // Don't hit the Plugin API if data exists. 723 | if ( ! empty( self::$dependency_api_data[ $slug ]['last_updated'] ) ) { 724 | continue; 725 | } 726 | } 727 | 728 | if ( ! function_exists( 'plugins_api' ) ) { 729 | require_once ABSPATH . 'wp-admin/includes/plugin-install.php'; 730 | } 731 | 732 | $information = plugins_api( 733 | 'plugin_information', 734 | array( 735 | 'slug' => $slug, 736 | 'fields' => array( 737 | 'short_description' => true, 738 | 'icons' => true, 739 | ), 740 | ) 741 | ); 742 | 743 | if ( is_wp_error( $information ) ) { 744 | continue; 745 | } 746 | 747 | self::$dependency_api_data[ $slug ] = (array) $information; 748 | // plugins_api() returns 'name' not 'Name'. 749 | self::$dependency_api_data[ $information->slug ]['Name'] = self::$dependency_api_data[ $information->slug ]['name']; 750 | set_site_transient( 'wp_plugin_dependencies_plugin_data', self::$dependency_api_data, 0 ); 751 | } 752 | 753 | // Remove from self::$dependency_api_data if slug no longer a dependency. 754 | $differences = array_diff( array_keys( self::$dependency_api_data ), self::$dependency_slugs ); 755 | foreach ( $differences as $difference ) { 756 | unset( self::$dependency_api_data[ $difference ] ); 757 | } 758 | 759 | ksort( self::$dependency_api_data ); 760 | // Remove empty elements. 761 | self::$dependency_api_data = array_filter( self::$dependency_api_data ); 762 | set_site_transient( 'wp_plugin_dependencies_plugin_data', self::$dependency_api_data, 0 ); 763 | } 764 | 765 | /** 766 | * Gets plugin directory names. 767 | * 768 | * @since 6.5.0 769 | * 770 | * @return array An array of plugin directory names. 771 | */ 772 | protected static function get_plugin_dirnames() { 773 | // Cache the plugin directory names. 774 | if ( empty( self::$plugin_dirnames ) || self::$plugin_dirnames_cache !== self::$plugins ) { 775 | self::$plugin_dirnames = array(); 776 | self::$plugin_dirnames_cache = self::$plugins; 777 | 778 | foreach ( array_keys( self::$plugins ) as $plugin ) { 779 | $slug = self::convert_to_slug( $plugin ); 780 | self::$plugin_dirnames[ $slug ] = $plugin; 781 | } 782 | } 783 | 784 | return self::$plugin_dirnames; 785 | } 786 | 787 | /** 788 | * Gets circular dependency data. 789 | * 790 | * @since 6.5.0 791 | * 792 | * @return array[] An array of circular dependency pairings. 793 | */ 794 | protected static function get_circular_dependencies() { 795 | $circular_dependencies = array(); 796 | foreach ( self::$dependencies as $dependent => $dependencies ) { 797 | /* 798 | * $dependent is in 'a/a.php' format. Dependencies are stored as slugs, i.e. 'a'. 799 | * 800 | * Convert $dependent to slug format for checking. 801 | */ 802 | $dependent_slug = self::convert_to_slug( $dependent ); 803 | 804 | $circular_dependencies = array_merge( 805 | $circular_dependencies, 806 | self::check_for_circular_dependencies( array( $dependent_slug ), $dependencies ) 807 | ); 808 | } 809 | 810 | if ( empty( $circular_dependencies ) ) { 811 | return $circular_dependencies; 812 | } 813 | 814 | return $circular_dependencies; 815 | } 816 | 817 | /** 818 | * Checks for circular dependencies. 819 | * 820 | * @since 6.5.0 821 | * 822 | * @param array $dependents Array of dependent plugins. 823 | * @param array $dependencies Array of plugins dependencies. 824 | * @return array A circular dependency pairing, or an empty array if none exists. 825 | */ 826 | protected static function check_for_circular_dependencies( $dependents, $dependencies ) { 827 | $circular_dependencies = array(); 828 | 829 | // Check for a self-dependency. 830 | $dependents_location_in_its_own_dependencies = array_intersect( $dependents, $dependencies ); 831 | if ( ! empty( $dependents_location_in_its_own_dependencies ) ) { 832 | foreach ( $dependents_location_in_its_own_dependencies as $self_dependency ) { 833 | $circular_dependencies[] = array( $self_dependency, $self_dependency ); 834 | 835 | // No need to check for itself again. 836 | unset( $dependencies[ array_search( $self_dependency, $dependencies, true ) ] ); 837 | } 838 | } 839 | 840 | /* 841 | * Check each dependency to see: 842 | * 1. If it has dependencies. 843 | * 2. If its list of dependencies includes one of its own dependents. 844 | */ 845 | foreach ( $dependencies as $dependency ) { 846 | // Check if the dependency is also a dependent. 847 | $dependency_location_in_dependents = array_search( $dependency, self::$dependent_slugs, true ); 848 | 849 | if ( false !== $dependency_location_in_dependents ) { 850 | $dependencies_of_the_dependency = self::$dependencies[ $dependency_location_in_dependents ]; 851 | 852 | foreach ( $dependents as $dependent ) { 853 | // Check if its dependencies includes one of its own dependents. 854 | $dependent_location_in_dependency_dependencies = array_search( 855 | $dependent, 856 | $dependencies_of_the_dependency, 857 | true 858 | ); 859 | 860 | if ( false !== $dependent_location_in_dependency_dependencies ) { 861 | $circular_dependencies[] = array( $dependent, $dependency ); 862 | 863 | // Remove the dependent from its dependency's dependencies. 864 | unset( $dependencies_of_the_dependency[ $dependent_location_in_dependency_dependencies ] ); 865 | } 866 | } 867 | 868 | $dependents[] = $dependency; 869 | 870 | /* 871 | * Now check the dependencies of the dependency's dependencies for the dependent. 872 | * 873 | * Yes, that does make sense. 874 | */ 875 | $circular_dependencies = array_merge( 876 | $circular_dependencies, 877 | self::check_for_circular_dependencies( $dependents, array_unique( $dependencies_of_the_dependency ) ) 878 | ); 879 | } 880 | } 881 | 882 | return $circular_dependencies; 883 | } 884 | 885 | /** 886 | * Converts a plugin filepath to a slug. 887 | * 888 | * @since 6.5.0 889 | * 890 | * @param string $plugin_file The plugin's filepath, relative to the plugins directory. 891 | * @return string The plugin's slug. 892 | */ 893 | private static function convert_to_slug( $plugin_file ) { 894 | return str_contains( $plugin_file, '/' ) ? dirname( $plugin_file ) : str_replace( '.php', '', $plugin_file ); 895 | } 896 | } 897 | -------------------------------------------------------------------------------- /test-plugins/circlea/circlea.php: -------------------------------------------------------------------------------- 1 |