PluginProbe
WP Debugging / 2.11.18
WP Debugging v2.11.18
2.12.6 2.12.5 trunk 2.10.0 2.10.1 2.10.2 2.11.0 2.11.1 2.11.10 2.11.11 2.11.12 2.11.13 2.11.14 2.11.15 2.11.16 2.11.17 2.11.18 2.11.19 2.11.2 2.11.20 2.11.21 2.11.22 2.11.23 2.11.24 2.11.3 All 59 releases
wp-debugging / vendor / afragen / wp-dependency-installer / wp-dependency-installer.php

wp-dependency-installer.php in WP Debugging 2.11.18, at vendor/afragen/wp-dependency-installer/wp-dependency-installer.php

975 lines 28.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * WP Dependency Installer
4 *
5 * A lightweight class to add to WordPress plugins or themes to automatically install
6 * required plugin dependencies. Uses a JSON config file to declare plugin dependencies.
7 * It can install a plugin from w.org, GitHub, Bitbucket, GitLab, Gitea or direct URL.
8 *
9 * @package WP_Dependency_Installer
10 * @author Andy Fragen, Matt Gibbs, Raruto
11 * @license MIT
12 * @link https://github.com/afragen/wp-dependency-installer
13 */
14
15 /**
16 * Exit if called directly.
17 */
18 if ( ! defined( 'WPINC' ) ) {
19 die;
20 }
21
22 if ( ! class_exists( 'WP_Dependency_Installer' ) ) {
23 /**
24 * Class WP_Dependency_Installer
25 */
26 class WP_Dependency_Installer {
27 /**
28 * Holds the JSON file contents.
29 *
30 * @var array $config
31 */
32 private $config;
33
34 /**
35 * Holds the current dependency's slug.
36 *
37 * @var string $current_slug
38 */
39 private $current_slug;
40
41 /**
42 * Holds the calling plugin/theme file path.
43 *
44 * @var string $source
45 */
46 private static $caller;
47
48 /**
49 * Holds the calling plugin/theme slug.
50 *
51 * @var string $source
52 */
53 private static $source;
54
55 /**
56 * Holds names of installed dependencies for admin notices.
57 *
58 * @var array $notices
59 */
60 private $notices;
61
62 /**
63 * Factory.
64 *
65 * @param string $caller File path to calling plugin/theme.
66 */
67 public static function instance( $caller = false ) {
68 static $instance = null;
69 if ( null === $instance ) {
70 $instance = new self();
71 }
72 self::$caller = $caller;
73 self::$source = ! $caller ? false : basename( $caller );
74
75 return $instance;
76 }
77
78 /**
79 * Private constructor.
80 */
81 private function __construct() {
82 $this->config = [];
83 $this->notices = [];
84 }
85
86 /**
87 * Load hooks.
88 *
89 * @return void
90 */
91 public function load_hooks() {
92 add_action( 'admin_init', [ $this, 'admin_init' ] );
93 add_action( 'admin_footer', [ $this, 'admin_footer' ] );
94 add_action( 'admin_notices', [ $this, 'admin_notices' ] );
95 add_action( 'network_admin_notices', [ $this, 'admin_notices' ] );
96 add_action( 'wp_ajax_dependency_installer', [ $this, 'ajax_router' ] );
97 add_filter( 'http_request_args', [ $this, 'add_basic_auth_headers' ], 15, 2 );
98
99 add_filter(
100 'wp_dependency_notices',
101 function( $notices, $slug ) {
102 foreach ( array_keys( $notices ) as $key ) {
103 if ( ! is_wp_error( $notices[ $key ] ) && $notices[ $key ]['slug'] === $slug ) {
104 $notices[ $key ]['nonce'] = $this->config[ $slug ]['nonce'];
105 }
106 }
107
108 return $notices;
109 },
110 10,
111 2
112 );
113
114 new \WP_Dismiss_Notice();
115 }
116
117 /**
118 * Let's get going.
119 * First load data from wp-dependencies.json if present.
120 * Then load hooks needed to run.
121 *
122 * @param string $caller Path to plugin or theme calling the framework.
123 *
124 * @return self
125 */
126 public function run( $caller = false ) {
127 $caller = ! $caller ? self::$caller : $caller;
128 $config = $this->json_file_decode( $caller . '/wp-dependencies.json' );
129 if ( ! empty( $config ) ) {
130 $this->register( $config, $caller );
131 }
132 if ( ! empty( $this->config ) ) {
133 $this->load_hooks();
134 }
135
136 return $this;
137 }
138
139 /**
140 * Decode JSON config data from a file.
141 *
142 * @param string $json_path File path to JSON config file.
143 *
144 * @return bool|array $config
145 */
146 public function json_file_decode( $json_path ) {
147 $config = [];
148 if ( file_exists( $json_path ) ) {
149 $config = file_get_contents( $json_path );
150 $config = json_decode( $config, true );
151 }
152
153 return $config;
154 }
155
156 /**
157 * Register dependencies (supports multiple instances).
158 *
159 * @param array $config JSON config as array.
160 * @param string $caller Path to plugin or theme calling the framework.
161 *
162 * @return self
163 */
164 public function register( $config, $caller = false ) {
165 $source = ! self::$source ? basename( $caller ) : self::$source;
166 foreach ( $config as $dependency ) {
167 // Save a reference of current dependent plugin.
168 $dependency['source'] = $source;
169 $dependency['sources'][] = $source;
170 $slug = $dependency['slug'];
171 $dependency['nonce'] = \wp_create_nonce( 'wp-dependency-installer_' . $slug );
172
173 // Keep a reference of all dependent plugins.
174 if ( isset( $this->config[ $slug ] ) ) {
175 $dependency['sources'] = array_merge( $this->config[ $slug ]['sources'], $dependency['sources'] );
176 }
177 // Update config.
178 if ( ! isset( $this->config[ $slug ] ) || $this->is_required( $dependency ) ) {
179 $this->config[ $slug ] = $dependency;
180 }
181 }
182
183 return $this;
184 }
185
186 /**
187 * Process the registered dependencies.
188 */
189 private function apply_config() {
190 foreach ( $this->config as $dependency ) {
191 $download_link = null;
192 $base = null;
193 $uri = $dependency['uri'];
194 $slug = $dependency['slug'];
195 $uri_args = parse_url( $uri ); // phpcs:ignore WordPress.WP.AlternativeFunctions.parse_url_parse_url
196 $port = isset( $uri_args['port'] ) ? $uri_args['port'] : null;
197 $api = isset( $uri_args['host'] ) ? $uri_args['host'] : null;
198 $api = ! $port ? $api : "{$api}:{$port}";
199 $scheme = isset( $uri_args['scheme'] ) ? $uri_args['scheme'] : null;
200 $scheme = null !== $scheme ? $scheme . '://' : 'https://';
201 $path = isset( $uri_args['path'] ) ? $uri_args['path'] : null;
202 $owner_repo = str_replace( '.git', '', trim( $path, '/' ) );
203
204 switch ( $dependency['host'] ) {
205 case 'github':
206 $base = null === $api || 'github.com' === $api ? 'api.github.com' : $api;
207 $download_link = "{$scheme}{$base}/repos/{$owner_repo}/zipball/{$dependency['branch']}";
208 break;
209 case 'bitbucket':
210 $base = null === $api || 'bitbucket.org' === $api ? 'bitbucket.org' : $api;
211 $download_link = "{$scheme}{$base}/{$owner_repo}/get/{$dependency['branch']}.zip";
212 break;
213 case 'gitlab':
214 $base = null === $api || 'gitlab.com' === $api ? 'gitlab.com' : $api;
215 $project_id = rawurlencode( $owner_repo );
216 $download_link = "{$scheme}{$base}/api/v4/projects/{$project_id}/repository/archive.zip";
217 $download_link = add_query_arg( 'sha', $dependency['branch'], $download_link );
218 break;
219 case 'gitea':
220 $download_link = "{$scheme}{$api}/api/v1/repos/{$owner_repo}/archive/{$dependency['branch']}.zip";
221 break;
222 case 'wordpress': // phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled
223 $download_link = $this->get_dot_org_latest_download( basename( $owner_repo ) );
224 break;
225 case 'direct':
226 $download_link = filter_var( $uri, FILTER_VALIDATE_URL );
227 break;
228 }
229
230 /**
231 * Allow filtering of download link for dependency configuration.
232 *
233 * @since 1.4.11
234 *
235 * @param string $download_link Download link.
236 * @param array $dependency Dependency configuration.
237 */
238 $dependency['download_link'] = apply_filters( 'wp_dependency_download_link', $download_link, $dependency );
239
240 /**
241 * Allow filtering of individual dependency config.
242 *
243 * @since 3.0.0
244 *
245 * @param array $dependency Dependency configuration.
246 */
247 $this->config[ $slug ] = apply_filters( 'wp_dependency_config', $dependency );
248 }
249 }
250
251 /**
252 * Get lastest download link from WordPress API.
253 *
254 * @param string $slug Plugin slug.
255 * @return string $download_link
256 */
257 private function get_dot_org_latest_download( $slug ) {
258 $download_link = get_site_transient( 'wpdi-' . md5( $slug ) );
259
260 if ( ! $download_link ) {
261 $url = 'https://api.wordpress.org/plugins/info/1.1/';
262 $url = add_query_arg(
263 [
264 'action' => 'plugin_information',
265 rawurlencode( 'request[slug]' ) => $slug,
266 ],
267 $url
268 );
269 $response = wp_remote_get( $url );
270 $response = json_decode( wp_remote_retrieve_body( $response ) );
271 $download_link = empty( $response )
272 ? "https://downloads.wordpress.org/plugin/{$slug}.zip"
273 : $response->download_link;
274
275 set_site_transient( 'wpdi-' . md5( $slug ), $download_link, DAY_IN_SECONDS );
276 }
277
278 return $download_link;
279 }
280
281 /**
282 * Determine if dependency is active or installed.
283 */
284 public function admin_init() {
285 // Get the gears turning.
286 $this->apply_config();
287
288 // Generate admin notices.
289 foreach ( $this->config as $slug => $dependency ) {
290 $is_required = $this->is_required( $dependency );
291
292 if ( $is_required ) {
293 $this->modify_plugin_row( $slug );
294 }
295
296 if ( ! wp_verify_nonce( $dependency['nonce'], 'wp-dependency-installer_' . $slug ) ) {
297 return false;
298 }
299
300 // phpcs:ignore Generic.CodeAnalysis.EmptyStatement.DetectedIf
301 if ( $this->is_active( $slug ) ) {
302 // Do nothing.
303 } elseif ( $this->is_installed( $slug ) ) {
304 if ( $is_required ) {
305 $this->notices[] = $this->activate( $slug );
306 } else {
307 $this->notices[] = $this->activate_notice( $slug );
308 }
309 } else {
310 if ( $is_required ) {
311 $this->notices[] = $this->install( $slug );
312 } else {
313 $this->notices[] = $this->install_notice( $slug );
314 }
315 }
316
317 /**
318 * Allow filtering of admin notices.
319 *
320 * @since 3.0.0
321 *
322 * @param array $notices admin notices.
323 * @param string $slug plugin slug.
324 */
325 $this->notices = apply_filters( 'wp_dependency_notices', $this->notices, $slug );
326 }
327 }
328
329 /**
330 * Register jQuery AJAX.
331 */
332 public function admin_footer() {
333 ?>
334 <script>
335 (function ($) {
336 $(function () {
337 $(document).on('click', '.wpdi-button', function () {
338 var $this = $(this);
339 var $parent = $(this).closest('p');
340 $parent.html('Running...');
341 $.post(ajaxurl, {
342 action: 'dependency_installer',
343 method: $this.attr('data-action'),
344 slug : $this.attr('data-slug'),
345 nonce : $this.attr('data-nonce')
346 }, function (response) {
347 $parent.html(response);
348 });
349 });
350 $(document).on('click', '.dependency-installer .notice-dismiss', function () {
351 var $this = $(this);
352 $.post(ajaxurl, {
353 action: 'dependency_installer',
354 method: 'dismiss',
355 slug : $this.attr('data-slug')
356 });
357 });
358 });
359 })(jQuery);
360 </script>
361 <?php
362 }
363
364 /**
365 * AJAX router.
366 */
367 public function ajax_router() {
368 if ( ! isset( $_POST['nonce'], $_POST['slug'] )
369 || ! wp_verify_nonce( sanitize_key( wp_unslash( $_POST['nonce'] ) ), 'wp-dependency-installer_' . sanitize_text_field( wp_unslash( $_POST['slug'] ) ) )
370 ) {
371 return;
372 }
373 $method = isset( $_POST['method'] ) ? sanitize_text_field( wp_unslash( $_POST['method'] ) ) : '';
374 $slug = isset( $_POST['slug'] ) ? sanitize_text_field( wp_unslash( $_POST['slug'] ) ) : '';
375 $whitelist = [ 'install', 'activate', 'dismiss' ];
376
377 if ( in_array( $method, $whitelist, true ) ) {
378 $response = $this->$method( $slug );
379 $message = is_wp_error( $response ) ? $response->get_error_message() : $response['message'];
380 esc_html_e( $message );
381 }
382 wp_die();
383 }
384
385 /**
386 * Check if a dependency is currently required.
387 *
388 * @param string|array $plugin Plugin dependency slug or config.
389 *
390 * @return boolean True if required. Default: False
391 */
392 public function is_required( &$plugin ) {
393 if ( empty( $this->config ) ) {
394 return false;
395 }
396 if ( is_string( $plugin ) && isset( $this->config[ $plugin ] ) ) {
397 $dependency = &$this->config[ $plugin ];
398 } else {
399 $dependency = &$plugin;
400 }
401 if ( isset( $dependency['required'] ) ) {
402 return true === $dependency['required'] || 'true' === $dependency['required'];
403 }
404 if ( isset( $dependency['optional'] ) ) {
405 return false === $dependency['optional'] || 'false' === $dependency['optional'];
406 }
407
408 return false;
409 }
410
411 /**
412 * Is dependency installed?
413 *
414 * @param string $slug Plugin slug.
415 *
416 * @return boolean
417 */
418 public function is_installed( $slug ) {
419 $plugins = get_plugins();
420
421 return isset( $plugins[ $slug ] );
422 }
423
424 /**
425 * Is dependency active?
426 *
427 * @param string $slug Plugin slug.
428 *
429 * @return boolean
430 */
431 public function is_active( $slug ) {
432 return is_plugin_active( $slug );
433 }
434
435 /**
436 * Install and activate dependency.
437 *
438 * @param string $slug Plugin slug.
439 *
440 * @return bool|array false or Message.
441 */
442 public function install( $slug ) {
443 if ( $this->is_installed( $slug ) || ! current_user_can( 'update_plugins' ) ) {
444 return false;
445 }
446
447 $this->current_slug = $slug;
448 add_filter( 'upgrader_source_selection', [ $this, 'upgrader_source_selection' ], 10, 2 );
449
450 $skin = new WP_Dependency_Installer_Skin(
451 [
452 'type' => 'plugin',
453 'nonce' => wp_nonce_url( $this->config[ $slug ]['download_link'] ),
454 ]
455 );
456 $upgrader = new Plugin_Upgrader( $skin );
457 $result = $upgrader->install( $this->config[ $slug ]['download_link'] );
458
459 if ( is_wp_error( $result ) ) {
460 return [
461 'status' => 'error',
462 'message' => $result->get_error_message(),
463 ];
464 }
465
466 if ( null === $result ) {
467 return [
468 'status' => 'error',
469 'message' => esc_html__( 'Plugin download failed' ),
470 ];
471 }
472
473 wp_cache_flush();
474 if ( $this->is_required( $slug ) ) {
475 $result = $this->activate( $slug );
476 if ( ! is_wp_error( $result ) ) {
477 return [
478 'status' => 'updated',
479 'slug' => $slug,
480 /* translators: %s: Plugin name */
481 'message' => sprintf( esc_html__( '%s has been installed and activated.' ), $this->config[ $slug ]['name'] ),
482 'source' => $this->config[ $slug ]['source'],
483 ];
484 }
485 }
486
487 if ( is_wp_error( $result ) || ( true !== $result && 'error' === $result['status'] ) ) {
488 return $result;
489 }
490
491 return [
492 'status' => 'updated',
493 /* translators: %s: Plugin name */
494 'message' => sprintf( esc_html__( '%s has been installed.' ), $this->config[ $slug ]['name'] ),
495 'source' => $this->config[ $slug ]['source'],
496 ];
497 }
498
499 /**
500 * Get install plugin notice.
501 *
502 * @param string $slug Plugin slug.
503 *
504 * @return array Admin notice.
505 */
506 public function install_notice( $slug ) {
507 $dependency = $this->config[ $slug ];
508
509 return [
510 'action' => 'install',
511 'slug' => $slug,
512 /* translators: %s: Plugin name */
513 'message' => sprintf( esc_html__( 'The %s plugin is recommended.' ), $dependency['name'] ),
514 'source' => $dependency['source'],
515 ];
516 }
517
518 /**
519 * Activate dependency.
520 *
521 * @param string $slug Plugin slug.
522 *
523 * @return array Message.
524 */
525 public function activate( $slug ) {
526 if ( ! current_user_can( 'activate_plugins' ) ) {
527 return new WP_Error( 'wpdi_activate_plugins', __( 'Current user cannot activate plugins.' ), $this->config[ $slug ]['name'] );
528 }
529
530 // network activate only if on network admin pages.
531 $result = is_network_admin() ? activate_plugin( $slug, null, true ) : activate_plugin( $slug );
532
533 if ( is_wp_error( $result ) ) {
534 return [
535 'status' => 'error',
536 'message' => $result->get_error_message(),
537 ];
538 }
539
540 return [
541 'status' => 'updated',
542 'slug' => $slug,
543 /* translators: %s: Plugin name */
544 'message' => sprintf( esc_html__( '%s has been activated.' ), $this->config[ $slug ]['name'] ),
545 'source' => $this->config[ $slug ]['source'],
546 ];
547 }
548
549 /**
550 * Get activate plugin notice.
551 *
552 * @param string $slug Plugin slug.
553 *
554 * @return array Admin notice.
555 */
556 public function activate_notice( $slug ) {
557 $dependency = $this->config[ $slug ];
558
559 return [
560 'action' => 'activate',
561 'slug' => $slug,
562 /* translators: %s: Plugin name */
563 'message' => sprintf( esc_html__( 'Please activate the %s plugin.' ), $dependency['name'] ),
564 'source' => $dependency['source'],
565 ];
566 }
567
568 /**
569 * Dismiss admin notice for a week.
570 *
571 * @return array Empty Message.
572 */
573 public function dismiss() {
574 return [
575 'status' => 'updated',
576 'message' => '',
577 ];
578 }
579
580 /**
581 * Correctly rename dependency for activation.
582 *
583 * @param string $source Path fo $source.
584 * @param string $remote_source Path of $remote_source.
585 *
586 * @return string $new_source
587 */
588 public function upgrader_source_selection( $source, $remote_source ) {
589 $new_source = trailingslashit( $remote_source ) . dirname( $this->current_slug );
590 $this->move_dir( $source, $new_source, true );
591
592 return trailingslashit( $new_source );
593 }
594
595 /**
596 * Moves a directory from one location to another via the rename() PHP function.
597 * If the renaming failed, falls back to copy_dir().
598 *
599 * Assumes that WP_Filesystem() has already been called and setup.
600 *
601 * @since 6.2.0
602 *
603 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
604 *
605 * @param string $from Source directory.
606 * @param string $to Destination directory.
607 * @param bool $overwrite Overwrite destination.
608 * Default is false.
609 * @return bool|WP_Error True on success, False or WP_Error on failure.
610 */
611 private function move_dir( $from, $to, $overwrite = false ) {
612 global $wp_filesystem;
613
614 if ( trailingslashit( strtolower( $from ) ) === trailingslashit( strtolower( $to ) ) ) {
615 return new \WP_Error( 'source_destination_same_move_dir', __( 'The source and destination are the same.' ) );
616 }
617
618 if ( $wp_filesystem->exists( $to ) ) {
619 if ( ! $overwrite ) {
620 return new \WP_Error( 'destination_already_exists_move_dir', __( 'The destination folder already exists.' ), $to );
621 } elseif ( ! $wp_filesystem->delete( $to, true ) ) {
622 // Can't overwrite if the destination couldn't be deleted.
623 return \WP_Error( 'destination_not_deleted_move_dir', __( 'The destination directory already exists and could not be removed.' ) );
624 }
625 }
626
627 $result = false;
628
629 if ( 'direct' === $wp_filesystem->method ) {
630 if ( $wp_filesystem->delete( $to, true ) ) {
631 $result = @rename( $from, $to );
632 }
633 } else {
634 // Non-direct filesystems use some version of rename without a fallback.
635 $result = $wp_filesystem->move( $from, $to, $overwrite );
636 }
637
638 if ( $result ) {
639 /*
640 * When using an environment with shared folders,
641 * there is a delay in updating the filesystem's cache.
642 *
643 * This is a known issue in environments with a VirtualBox provider.
644 *
645 * A 200ms delay gives time for the filesystem to update its cache,
646 * prevents "Operation not permitted", and "No such file or directory" warnings.
647 *
648 * This delay is used in other projects, including Composer.
649 * @link https://github.com/composer/composer/blob/main/src/Composer/Util/Platform.php#L228-L233
650 */
651 usleep( 200000 );
652 $this->wp_opcache_invalidate_directory( $to );
653 }
654
655 if ( ! $result ) {
656 if ( ! $wp_filesystem->is_dir( $to ) ) {
657 if ( ! $wp_filesystem->mkdir( $to, FS_CHMOD_DIR ) ) {
658 return new \WP_Error( 'mkdir_failed_move_dir', __( 'Could not create directory.' ), $to );
659 }
660 }
661
662 $result = copy_dir( $from, $to, [ basename( $to ) ] );
663
664 // Clear the source directory.
665 if ( ! is_wp_error( $result ) ) {
666 $wp_filesystem->delete( $from, true );
667 }
668 }
669
670 return $result;
671 }
672
673
674 /**
675 * Attempts to clear the opcode cache for a directory of files.
676 *
677 * @since 6.2.0
678 *
679 * @see wp_opcache_invalidate()
680 * @link https://www.php.net/manual/en/function.opcache-invalidate.php
681 * @global WP_Filesystem_Base $wp_filesystem WordPress filesystem subclass.
682 *
683 * @param string $dir The path to the directory for which the opcode cache is to be cleared.
684 */
685 private function wp_opcache_invalidate_directory( $dir ) {
686 global $wp_filesystem;
687
688 if ( ! is_string( $dir ) || '' === trim( $dir ) ) {
689 if ( WP_DEBUG ) {
690 $error_message = sprintf(
691 /* translators: %s: The function name. */
692 __( '%s expects a non-empty string.' ),
693 '<code>wp_opcache_invalidate_directory()</code>'
694 );
695 // phpcs:ignore WordPress.PHP.DevelopmentFunctions.error_log_trigger_error
696 trigger_error( $error_message );
697 }
698 return;
699 }
700
701 $dirlist = $wp_filesystem->dirlist( $dir, false, true );
702
703 if ( empty( $dirlist ) ) {
704 return;
705 }
706
707 /*
708 * Recursively invalidate opcache of files in a directory.
709 *
710 * WP_Filesystem_*::dirlist() returns an array of file and directory information.
711 *
712 * This does not include a path to the file or directory.
713 * To invalidate files within sub-directories, recursion is needed
714 * to prepend an absolute path containing the sub-directory's name.
715 *
716 * @param array $dirlist Array of file/directory information from WP_Filesystem_Base::dirlist(),
717 * with sub-directories represented as nested arrays.
718 * @param string $path Absolute path to the directory.
719 */
720 $invalidate_directory = function( $dirlist, $path ) use ( &$invalidate_directory ) {
721 $path = trailingslashit( $path );
722
723 foreach ( $dirlist as $name => $details ) {
724 if ( 'f' === $details['type'] ) {
725 wp_opcache_invalidate( $path . $name, true );
726 continue;
727 }
728
729 if ( is_array( $details['files'] ) && ! empty( $details['files'] ) ) {
730 $invalidate_directory( $details['files'], $path . $name );
731 }
732 }
733 };
734
735 $invalidate_directory( $dirlist, $dir );
736 }
737
738 /**
739 * Display admin notices / action links.
740 *
741 * @return bool/string false or Admin notice.
742 */
743 public function admin_notices() {
744 if ( ! current_user_can( 'update_plugins' ) ) {
745 return false;
746 }
747 foreach ( $this->notices as $notice ) {
748 $status = isset( $notice['status'] ) ? $notice['status'] : 'updated';
749 $source = isset( $notice['source'] ) ? $notice['source'] : __( 'Dependency' );
750 $class = esc_attr( $status ) . ' notice is-dismissible dependency-installer';
751 $label = esc_html( $this->get_dismiss_label( $source ) );
752 $message = '';
753 $action = '';
754 $dismissible = '';
755
756 if ( isset( $notice['message'] ) ) {
757 $message = esc_html( $notice['message'] );
758 }
759
760 if ( isset( $notice['action'] ) ) {
761 $action = sprintf(
762 ' <a href="javascript:;" class="wpdi-button" data-action="%1$s" data-slug="%2$s" data-nonce="%3$s">%4$s Now &raquo;</a> ',
763 esc_attr( $notice['action'] ),
764 esc_attr( $notice['slug'] ),
765 esc_attr( $notice['nonce'] ),
766 esc_html( ucfirst( $notice['action'] ) )
767 );
768 }
769 if ( isset( $notice['slug'] ) ) {
770 /**
771 * Filters the dismissal timeout.
772 *
773 * @since 1.4.1
774 *
775 * @param string|int '7' Default dismissal in days.
776 * @param string $notice['source'] Plugin slug of calling plugin.
777 * @return string|int Dismissal timeout in days.
778 */
779 $timeout = apply_filters( 'wp_dependency_timeout', '7', $source );
780 $dependency = dirname( $notice['slug'] );
781 $dismissible = empty( $timeout ) ? '' : sprintf( 'dependency-installer-%1$s-%2$s', esc_attr( $dependency ), esc_attr( $timeout ) );
782 }
783 if ( \WP_Dismiss_Notice::is_admin_notice_active( $dismissible ) ) {
784 printf(
785 '<div class="%1$s" data-dismissible="%2$s"><p><strong>[%3$s]</strong> %4$s%5$s</p></div>',
786 esc_attr( $class ),
787 esc_attr( $dismissible ),
788 esc_html( $label ),
789 esc_html( $message ),
790 // $action is escaped above.
791 // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
792 $action
793 );
794 }
795 }
796 }
797
798 /**
799 * Make modifications to plugin row.
800 *
801 * @param string $plugin_file Plugin file.
802 */
803 private function modify_plugin_row( $plugin_file ) {
804 add_filter( 'network_admin_plugin_action_links_' . $plugin_file, [ $this, 'unset_action_links' ], 10, 2 );
805 add_filter( 'plugin_action_links_' . $plugin_file, [ $this, 'unset_action_links' ], 10, 2 );
806 add_action( 'after_plugin_row_' . $plugin_file, [ $this, 'modify_plugin_row_elements' ] );
807 }
808
809 /**
810 * Unset plugin action links so required plugins can't be removed or deactivated.
811 *
812 * @param array $actions Action links.
813 * @param string $plugin_file Plugin file.
814 *
815 * @return mixed
816 */
817 public function unset_action_links( $actions, $plugin_file ) {
818 /**
819 * Allow to remove required plugin action links.
820 *
821 * @since 3.0.0
822 *
823 * @param bool $unset remove default action links.
824 */
825 if ( apply_filters( 'wp_dependency_unset_action_links', true ) ) {
826 if ( isset( $actions['delete'] ) ) {
827 unset( $actions['delete'] );
828 }
829
830 if ( isset( $actions['deactivate'] ) ) {
831 unset( $actions['deactivate'] );
832 }
833 }
834
835 /**
836 * Allow to display of requied plugin label.
837 *
838 * @since 3.0.0
839 *
840 * @param bool $display show required plugin label.
841 */
842 if ( apply_filters( 'wp_dependency_required_label', true ) ) {
843 /* translators: %s: opening and closing span tags */
844 $actions = array_merge( [ 'required-plugin' => sprintf( esc_html__( '%1$sRequired Plugin%2$s' ), '<span class="network_active" style="font-variant-caps: small-caps;">', '</span>' ) ], $actions );
845 }
846
847 return $actions;
848 }
849
850 /**
851 * Modify the plugin row elements.
852 *
853 * @param string $plugin_file Plugin file.
854 *
855 * @return void
856 */
857 public function modify_plugin_row_elements( $plugin_file ) {
858 print '<script>';
859 /**
860 * Allow to display additional row meta info of required plugin.
861 *
862 * @since 3.0.0
863 *
864 * @param bool $display show plugin row meta.
865 */
866 if ( apply_filters( 'wp_dependency_required_row_meta', true ) ) {
867 print 'jQuery("tr[data-plugin=\'' . esc_attr( $plugin_file ) . '\'] .plugin-version-author-uri").append("<br><br><strong>' . esc_html__( 'Required by:' ) . '</strong> ' . esc_html( $this->get_dependency_sources( $plugin_file ) ) . '");';
868 }
869 print 'jQuery(".inactive[data-plugin=\'' . esc_attr( $plugin_file ) . '\']").attr("class", "active");';
870 print 'jQuery(".active[data-plugin=\'' . esc_attr( $plugin_file ) . '\'] .check-column input").remove();';
871 print '</script>';
872 }
873
874 /**
875 * Get formatted string of dependent plugins.
876 *
877 * @param string $plugin_file Plugin file.
878 *
879 * @return string $dependents
880 */
881 private function get_dependency_sources( $plugin_file ) {
882 // Remove empty values from $sources.
883 $sources = array_filter( $this->config[ $plugin_file ]['sources'] );
884 $sources = array_map( [ $this, 'get_dismiss_label' ], $sources );
885 $sources = implode( ', ', $sources );
886
887 return $sources;
888 }
889
890 /**
891 * Get formatted source string for text usage.
892 *
893 * @param string $source plugin source.
894 *
895 * @return string friendly plugin name.
896 */
897 private function get_dismiss_label( $source ) {
898 $label = str_replace( '-', ' ', $source );
899 $label = ucwords( $label );
900 $label = str_ireplace( 'wp ', 'WP ', $label );
901
902 /**
903 * Filters the dismissal notice label
904 *
905 * @since 3.0.0
906 *
907 * @param string $label Default dismissal notice string.
908 * @param string $source Plugin slug of calling plugin.
909 * @return string Dismissal notice string.
910 */
911 return apply_filters( 'wp_dependency_dismiss_label', $label, $source );
912 }
913
914 /**
915 * Get the configuration.
916 *
917 * @since 1.4.11
918 *
919 * @param string $slug Plugin slug.
920 * @param string $key Dependency key.
921 *
922 * @return mixed|array The configuration.
923 */
924 public function get_config( $slug = '', $key = '' ) {
925 if ( empty( $slug ) && empty( $key ) ) {
926 return $this->config;
927 } elseif ( empty( $key ) ) {
928 return isset( $this->config[ $slug ] ) ? $this->config[ $slug ] : null;
929 } else {
930 return isset( $this->config[ $slug ][ $key ] ) ? $this->config[ $slug ][ $key ] : null;
931 }
932 }
933
934 /**
935 * Add Basic Auth headers for authentication.
936 *
937 * @param array $args HTTP header args.
938 * @param string $url URL.
939 *
940 * @return array $args
941 */
942 public function add_basic_auth_headers( $args, $url ) {
943 if ( null === $this->current_slug ) {
944 return $args;
945 }
946 $package = $this->config[ $this->current_slug ];
947 $host = $package['host'];
948 $token = empty( $package['token'] ) ? false : $package['token'];
949
950 if ( $token && $url === $package['download_link'] ) {
951 if ( 'bitbucket' === $host ) {
952 // Bitbucket token must be in the form of 'username:password'.
953 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
954 $args['headers']['Authorization'] = 'Basic ' . base64_encode( $token );
955 }
956 if ( 'github' === $host || 'gitea' === $host ) {
957 $args['headers']['Authorization'] = 'token ' . $token;
958 }
959 if ( 'gitlab' === $host ) {
960 $args['headers']['Authorization'] = 'Bearer ' . $token;
961 }
962 }
963
964 // dot org should not have auth header.
965 // phpcs:ignore WordPress.WP.CapitalPDangit.Misspelled
966 if ( 'wordpress' === $host ) {
967 unset( $args['headers']['Authorization'] );
968 }
969 remove_filter( 'http_request_args', [ $this, 'add_basic_auth_headers' ] );
970
971 return $args;
972 }
973 }
974 }
975