├── test-plugins
├── circlea
│ └── circlea.php
├── circleb
│ └── circleb.php
├── test-dependencies2.php
├── test-dependencies1.php
└── test-non-dot-org-dependencies
│ ├── test-non-dot-org-dependencies.php
│ ├── gravityforms.json
│ └── git-updater.json
├── CHANGES.md
├── LICENSE
├── composer.json
├── README.md
├── readme.txt
├── languages
└── advanced-plugin-dependencies.pot
├── plugin.php
└── src
└── advanced-plugin-dependencies.php
/test-plugins/circlea/circlea.php:
--------------------------------------------------------------------------------
1 | =7.0"
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 | "make-pot": [
32 | "wp i18n make-pot --headers='{\"Report-Msgid-Bugs-To\":\"https://github.com/afragen/advanced-plugin-dependencies/issues\"}' . languages/advanced-plugin-dependencies.pot"
33 | ],
34 | "wpcs": [
35 | "vendor/bin/phpcbf .; vendor/bin/phpcs ."
36 | ]
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Advanced Plugin Dependencies
2 |
3 | * Contributors: afragen, costdev, pbiron
4 | * Description: Add plugin install dependencies tab, support for non dot org plugin cards, and information about dependencies.
5 | * License: MIT
6 | * Network: true
7 | * Requires at least: 6.5
8 | * Requires PHP: 8.0
9 | * Stable release: main
10 |
11 | ## Description
12 |
13 | An add-on the the Plugin Dependencies feature. Adds a Dependencies tab in the plugin install page. Adds support for non dot org plugin cards. If a requiring plugin does not have all its dependencies installed and active, it will not activate.
14 |
15 | * Plugins not in dot org may use the format ` Go to GravityForms.com to purchase and download the plugin.2.6.6 | 2022-08-23<\/h3>
gform_field_container filter.<\/li>
This plugin was originally designed to simply update any GitHub hosted WordPress plugin or theme. Currently, plugins or themes hosted on Bitbucket, GitLab, Gitea, or Gist are also supported via additional API plugins. Additionally, self-hosted git servers are supported.<\/p>\n
Your plugin or theme must<\/strong> contain a header in the style.css header or in the plugin's header denoting the location on GitHub. The format is as follows.<\/p>\n or<\/p>\n ...where the above URI leads to the owner\/repository<\/strong> of your theme or plugin. The URI may be in the format API plugins for Bitbucket, GitLab, Gitea, and Gist are available. API plugins are available for a one-click install from the Add-Ons<\/strong> tab.<\/p>\n Purchase a license at the Git Updater Store<\/a>. An unlimited yearly license is very reasonable and allows for authenticated API requests. There is an initial free trial period. After the trial period Git Updater will not be able to make authenticated API requests.<\/p>\n You can sponsor me on GitHub<\/a> to help with continued development and support.<\/p>\n The following headers are available for use depending upon your hosting source.<\/p>\n Go to git-updater.com to download the latest release.",
21 | "changelog": "Refer to changelog",
22 | "faq": " Comprehensive information regarding Git Updater is available in the Knowledge Base.<\/a><\/p>\n We now have a Slack team for Git Updater<\/a>. Please click here for an invite<\/a>. You will be automatically added to the #general<\/em> and #support<\/em> channels. Please take a look at other channels too.<\/p>\n If you are a polyglot I would greatly appreciate translation contributions to GlotPress for Git Updater<\/a>.<\/p>\n"
23 | },
24 | "short_description": "This plugin was originally designed to simply update any GitHub hosted WordPress plugin or theme. Currently, plugins or themes hosted on Bitbucket, GitLab, Gitea, or Gist are...",
25 | "primary_branch": "master",
26 | "branch": "develop",
27 | "download_link": "",
28 | "banners": {
29 | "low": "http:\/\/git-updater.com\/wp-content\/plugins\/git-updater\/assets\/banner-772x250.png",
30 | "high": "http:\/\/git-updater.com\/wp-content\/plugins\/git-updater\/assets\/banner-1544x500.png"
31 | },
32 | "icons": {
33 | "svg": "http:\/\/git-updater.com\/wp-content\/plugins\/git-updater\/assets\/icon.svg",
34 | "default": "https:\/\/s.w.org\/plugins\/geopattern-icon\/git-updater.svg"
35 | },
36 | "last_updated": "2023-04-07T23:37:21Z",
37 | "num_ratings": 0,
38 | "rating": 0,
39 | "active_installs": 0,
40 | "homepage": "https://git-updater.com",
41 | "external": "xxx"
42 | }
43 |
--------------------------------------------------------------------------------
/src/advanced-plugin-dependencies.php:
--------------------------------------------------------------------------------
1 | $data ) {
77 | // Skip plugins with no dependencies or no non-dotorg dependencies.
78 | if ( empty( $data['RequiresPlugins'] ) || ! str_contains( $data['RequiresPlugins'], '|' ) ) {
79 | continue;
80 | }
81 |
82 | $dependencies = array_map( 'trim', explode( ',', $data['RequiresPlugins'] ) );
83 |
84 | foreach ( $dependencies as $dependency ) {
85 | // Skip invalid formats.
86 | if ( ! str_contains( $dependency, '|' ) || str_starts_with( $dependency, '|' ) || str_ends_with( $dependency, '|' ) ) {
87 | continue;
88 | }
89 |
90 | list( $slug, $endpoint ) = array_map( 'trim', explode( '|', $dependency ) );
91 |
92 | if ( str_contains( $slug, '|' ) || str_contains( '|', $endpoint ) ) {
93 | continue;
94 | }
95 |
96 | if ( isset( self::$dependencies[ $plugin ] ) && in_array( $slug, self::$dependencies[ $plugin ], true ) ) {
97 | self::$dependencies[ $plugin ][] = $slug;
98 | self::$dependency_slugs[] = $slug;
99 | self::$dependent_slugs[ $plugin ] = str_contains( $plugin, '/' ) ? dirname( $plugin ) : $plugin;
100 | self::$non_dotorg_dependency_slugs[] = $slug;
101 | }
102 |
103 | // Handle local JSON files.
104 | if ( ! str_starts_with( $endpoint, 'http' ) && str_ends_with( $endpoint, '.json' ) ) {
105 | $endpoint = plugin_dir_url( $plugin ) . $endpoint;
106 | }
107 |
108 | self::$api_endpoints[ $slug ] = $endpoint;
109 | }
110 | }
111 | }
112 |
113 | /**
114 | * Adds non-WordPress.org dependency API data to `self::$dependency_api_data`.
115 | *
116 | * @return void
117 | */
118 | protected static function add_non_dotorg_dependency_api_data() {
119 | $short_description = esc_html__( "You will need to manually install this dependency. Please contact the plugin's developer and ask them to add plugin dependencies support and for information on how to install the this dependency.", 'advanced-plugin-dependencies' );
120 | foreach ( self::$dependency_slugs as $slug ) {
121 | if ( is_array( self::$dependency_api_data ) && ! array_key_exists( $slug, self::$dependency_api_data ) ) {
122 | self::$non_dotorg_dependency_slugs[] = $slug;
123 | self::$dependency_api_data[ $slug ] = self::get_empty_plugins_api_response( $slug );
124 | }
125 | }
126 | foreach ( self::$non_dotorg_dependency_slugs as $slug ) {
127 | $dependency_data = array();
128 | $dependency_data = (array) self::fetch_non_dotorg_dependency_data( $slug );
129 | if ( ! isset( $dependency_data['name'] ) ) {
130 | continue;
131 | }
132 | $dependency_data['Name'] = $dependency_data['name'];
133 |
134 | if ( ! isset( $dependency_data['short_description'] ) ) {
135 | if ( isset( $dependency_data['sections']['description'] ) ) {
136 | $dependency_data['short_description'] = substr( $dependency_data['sections']['description'], 0, 150 ) . '...';
137 | } else {
138 | $dependency_data['short_description'] = $short_description;
139 | }
140 | }
141 | $dependency_data['download_link'] = sanitize_url( $dependency_data['download_link'] );
142 | self::$dependency_api_data[ $slug ] = $dependency_data;
143 | }
144 | // Set transient for WP_Plugin_Dependencies.
145 | set_site_transient( 'wp_plugin_dependencies_plugin_data', self::$dependency_api_data, 0 );
146 | }
147 |
148 | /**
149 | * Fetches non-WordPress.org dependency data from their designated endpoints.
150 | *
151 | * @param string $dependency The dependency's slug.
152 | * @return array|\WP_Error
153 | */
154 | protected static function fetch_non_dotorg_dependency_data( $dependency ) {
155 | // Get cached data.
156 | $response = get_site_transient( "non_dot_org_dependency_data_{$dependency}" );
157 | if ( $response ) {
158 | return $response;
159 | }
160 |
161 | /**
162 | * Filter the REST enpoints used for lookup of plugins API data.
163 | *
164 | * @param array
165 | */
166 | $rest_endpoints = array_merge( self::$api_endpoints, apply_filters( 'plugin_dependency_endpoints', array() ) );
167 |
168 | // Ensure dependency has REST endpoint.
169 | if ( isset( $rest_endpoints[ $dependency ] ) ) {
170 |
171 | // Get local JSON endpoint.
172 | $response = wp_remote_get( $rest_endpoints[ $dependency ] );
173 |
174 | // Convert response to associative array.
175 | $response = json_decode( wp_remote_retrieve_body( $response ), true );
176 | if ( null === $response || isset( $response['error'] ) || isset( $response['code'] ) ) {
177 | $message = isset( $response['error'] ) ? $response['error'] : '';
178 | $response = new WP_Error( 'error', 'Error retrieving plugin data.', $message );
179 | }
180 | if ( is_wp_error( $response ) ) {
181 | return $response;
182 | }
183 | }
184 |
185 | // Cache data for 12 hours.
186 | set_site_transient( "non_dot_org_dependency_data_{$dependency}", $response, 12 * HOUR_IN_SECONDS );
187 |
188 | // Add slug to hook_extra.
189 | add_filter( 'upgrader_package_options', array( __CLASS__, 'upgrader_package_options' ), 10, 1 );
190 |
191 | return $response;
192 | }
193 |
194 | /**
195 | * Modify plugins_api() response.
196 | *
197 | * @param stdClass $res Object of results.
198 | * @param string $action Variable for plugins_api().
199 | * @param stdClass $args Object of plugins_api() args.
200 | * @return stdClass
201 | */
202 | public static function plugins_api_result( $res, $action, $args ) {
203 | if ( property_exists( $args, 'browse' ) && 'dependencies' === $args->browse ) {
204 | $res->info = array(
205 | 'page' => 1,
206 | 'pages' => 1,
207 | 'results' => count( (array) self::$dependency_api_data ),
208 | );
209 |
210 | $res->plugins = self::$dependency_api_data;
211 | }
212 |
213 | if ( is_wp_error( $res ) && isset( self::$dependency_api_data[ $args->slug ] ) ) {
214 | self::$args = $args;
215 | $res = (object) self::$dependency_api_data[ $args->slug ];
216 | }
217 | return $res;
218 | }
219 |
220 | /**
221 | * Switch admin notice markup with markup including link to Dependencies tab.
222 | *
223 | * @global $pagenow Current page.
224 | *
225 | * @param string $markup The HTML markup for the admin notice.
226 | * @return string
227 | */
228 | public static function dependency_notice_with_link( $markup ) {
229 | global $pagenow;
230 |
231 | if ( 'plugin-install.php' === $pagenow
232 | // phpcs:ignore WordPress.Security.NonceVerification.Recommended
233 | && ( isset( $_GET['tab'] ) && 'dependencies' === sanitize_title_with_dashes( wp_unslash( $_GET['tab'] ) ) )
234 | ) {
235 | return $markup;
236 | }
237 |
238 | $message = __( 'Some required plugins are missing or inactive.', 'advanced-plugin-dependencies' );
239 |
240 | /* translators: s: link to Dependencies install page */
241 | $link_message = sprintf( __( 'Go to the %s install page.', 'advanced-plugin-dependencies' ), self::get_dependency_link() );
242 |
243 | if ( str_contains( $markup, $message ) && ! str_contains( $markup, $link_message ) ) {
244 | $markup = str_replace( $message, "$message $link_message", $markup );
245 | }
246 |
247 | return wp_kses_post( $markup ); }
248 |
249 | /**
250 | * Get Dependencies link.
251 | *
252 | * @return string
253 | */
254 | private static function get_dependency_link() {
255 | $link = sprintf(
256 | '%s',
257 | __( 'Dependencies', 'advanced-plugin-dependencies' )
258 | );
259 |
260 | return wp_kses_post( $link );
261 | }
262 |
263 | /**
264 | * Return empty plugins_api() response.
265 | *
266 | * @param string $slug Plugin slug.
267 | * @param array $args Array of plugin.
268 | * @return array
269 | */
270 | private static function get_empty_plugins_api_response( $slug, $args = array() ) {
271 | $defaults = array(
272 | 'Name' => $slug,
273 | 'Version' => '',
274 | 'Author' => '',
275 | 'Description' => '',
276 | 'RequiresWP' => '',
277 | 'RequiresPHP' => '',
278 | 'PluginURI' => '',
279 | );
280 | $args = array_merge( $defaults, $args );
281 | $dependencies = self::get_dependency_filepaths();
282 | if ( ! isset( $dependencies[ $slug ] ) ) {
283 | $file = array_filter(
284 | array_keys( self::$plugins ),
285 | function ( $file ) use ( $slug ) {
286 | return str_contains( $file, $slug );
287 | }
288 | );
289 | $file = array_pop( $file );
290 | } else {
291 | $file = $dependencies[ $slug ];
292 | }
293 | $args = $file ? self::$plugins[ $file ] : $args;
294 | $short_description = esc_html__( "You will need to manually install this dependency. Please contact the plugin's developer and ask them to add plugin dependencies support and for information on how to install this dependency.", 'advanced-plugin-dependencies' );
295 | $dependencies = isset( self::$plugin_dirnames[ $slug ] ) && ! empty( self::$plugins[ self::$plugin_dirnames[ $slug ] ]['RequiresPlugins'] )
296 | ? self::$plugins[ self::$plugin_dirnames[ $slug ] ]['RequiresPlugins'] : array();
297 | $response = array(
298 | 'name' => $args['Name'],
299 | 'Name' => $args['Name'],
300 | 'slug' => $slug,
301 | 'version' => $args['Version'],
302 | 'author' => $args['Author'],
303 | 'contributors' => array(),
304 | 'requires' => $args['RequiresWP'],
305 | 'tested' => '',
306 | 'requires_php' => $args['RequiresPHP'],
307 | 'requires_plugins' => is_array( $dependencies ) ? $dependencies : explode( ',', $dependencies ),
308 | 'sections' => array(
309 | 'description' => $args['Description'],
310 | 'installation' => esc_html__( 'Ask the plugin developer where to download and install this plugin dependency.', 'advanced-plugin-dependencies' ),
311 | ),
312 | 'short_description' => $short_description,
313 | 'download_link' => '',
314 | 'banners' => array(),
315 | 'icons' => array( 'default' => "https://s.w.org/plugins/geopattern-icon/{$slug}.svg" ),
316 | 'last_updated' => '',
317 | 'num_ratings' => 0,
318 | 'rating' => 0,
319 | 'active_installs' => 0,
320 | 'homepage' => $args['PluginURI'],
321 | 'external' => 'xxx',
322 | );
323 |
324 | return $response;
325 | }
326 |
327 | /**
328 | * Split slug into slug and endpoint.
329 | *
330 | * @param string $slug Slug.
331 | * @return string
332 | */
333 | public static function split_slug( $slug ) {
334 | if ( ! str_contains( $slug, '|' ) || str_starts_with( $slug, '|' ) || str_ends_with( $slug, '|' ) ) {
335 | return $slug;
336 | }
337 |
338 | $original_slug = $slug;
339 | list( $slug, $endpoint ) = explode( '|', $slug );
340 | $slug = trim( $slug );
341 | $endpoint = trim( $endpoint );
342 |
343 | if ( '' === $slug || '' === $endpoint ) {
344 | return $original_slug;
345 | }
346 |
347 | if ( ! isset( self::$api_endpoints[ $slug ] ) ) {
348 | self::$api_endpoints[ $slug ] = sanitize_url( $endpoint );
349 | }
350 |
351 | return $slug;
352 | }
353 |
354 | /**
355 | * Add slug to hook_extra.
356 | *
357 | * @see WP_Upgrader::run() for $options details.
358 | *
359 | * @param array $options Array of options.
360 | * @return array
361 | */
362 | public static function upgrader_package_options( $options ) {
363 | if ( isset( $options['hook_extra']['temp_backup'] ) ) {
364 | $options['hook_extra']['slug'] = $options['hook_extra']['temp_backup']['slug'];
365 | } elseif ( isset( self::$args->slug ) ) {
366 | $options['hook_extra']['slug'] = self::$args->slug;
367 | }
368 | remove_filter( 'upgrader_package_options', array( __CLASS__, 'upgrader_package_options' ), 10 );
369 |
370 | return $options;
371 | }
372 |
373 | /**
374 | * Fix $source for non-dot org plugins.
375 | *
376 | * @param string $source File path of $ource.
377 | * @param string $remote_source File path of $remote_source.
378 | * @param Plugin|Theme $upgrader_object An Upgrader object.
379 | * @param array $hook_extra Array of $hook_extra data.
380 | * @return string
381 | */
382 | public static function fix_upgrader_source_selection( $source, $remote_source, $upgrader_object, $hook_extra ) {
383 | if ( isset( $hook_extra['slug'] ) ) {
384 | $new_source = trailingslashit( $remote_source ) . $hook_extra['slug'] . '/';
385 |
386 | $from = untrailingslashit( $source );
387 | $to = $new_source;
388 |
389 | if ( trailingslashit( strtolower( $from ) ) !== trailingslashit( strtolower( $to ) ) ) {
390 | move_dir( $from, $to, true );
391 | }
392 |
393 | return $new_source;
394 | }
395 |
396 | return $source;
397 | }
398 | }
399 |
400 | Advanced_Plugin_Dependencies::initialize();
401 |
--------------------------------------------------------------------------------
GitHub Plugin URI: afragen\/git-updater\nGitHub Plugin URI: https:\/\/github.com\/afragen\/git-updater<\/code><\/pre>\nGitHub Theme URI: afragen\/test-child\nGitHub Theme URI: https:\/\/github.com\/afragen\/test-child<\/code><\/pre>\nhttps:\/\/github.com\/<owner>\/<repo><\/code> or the short format <owner>\/<repo><\/code>. You do not need both. Only one Plugin or Theme URI is required. You must not<\/strong> include any extensions like .git<\/code>.<\/p>\nAPI Plugins<\/h3>\n
\n
Sponsor<\/h3>\n
Headers<\/h3>\n
GitHub<\/h4>\n
\n
Knowledge Base<\/h4>\n
Slack<\/h4>\n
Translations<\/h4>\n